1use crate::request::{FileInfo, ServiceInfo, WebCodegenRequest, WebCodegenResponse};
12use heck::{ToKebabCase, ToLowerCamelCase, ToSnakeCase};
13use std::collections::HashMap;
14use std::path::{Path, PathBuf};
15use tracing::info;
16
17pub fn generate(request: &WebCodegenRequest) -> WebCodegenResponse {
19 match generate_inner(request) {
20 Ok(files) => WebCodegenResponse {
21 success: true,
22 generated_files: files,
23 errors: Vec::new(),
24 },
25 Err(e) => WebCodegenResponse {
26 success: false,
27 generated_files: Vec::new(),
28 errors: vec![e],
29 },
30 }
31}
32
33fn generate_inner(req: &WebCodegenRequest) -> Result<Vec<PathBuf>, String> {
34 let mut generated = Vec::new();
35
36 let config_path = req.output_dir.join("actr-config.ts");
38 let config_content = gen_actr_config(req)?;
39 write_file(&config_path, &config_content)?;
40 generated.push(config_path);
41
42 for svc in &req.local_services {
44 let file_name = format!("{}.actorref.ts", to_kebab_case(&svc.name));
45 let ref_path = req.output_dir.join(&file_name);
46 let ref_content = gen_actor_ref(svc, req)?;
47 write_file(&ref_path, &ref_content)?;
48 generated.push(ref_path);
49 }
50
51 let index_path = req.output_dir.join("index.ts");
53 let index_content = gen_index(req)?;
54 write_file(&index_path, &index_content)?;
55 generated.push(index_path);
56
57 let wasm_files = gen_wasm_scaffold(req)?;
59 generated.extend(wasm_files);
60
61 let sw_path = req.project_root.join("public/actor.sw.js");
63 let sw_content = gen_service_worker(req)?;
64 write_file(&sw_path, &sw_content)?;
65 generated.push(sw_path);
66
67 let build_sh = req.project_root.join("build.sh");
69 if !build_sh.exists() {
70 let build_content = gen_root_build_sh();
71 write_file(&build_sh, &build_content)?;
72 make_executable(&build_sh)?;
73 generated.push(build_sh);
74 }
75
76 Ok(generated)
77}
78
79fn gen_actr_config(req: &WebCodegenRequest) -> Result<String, String> {
84 let manufacturer = &req.manufacturer;
85 let actr_name = &req.actr_name;
86 let signaling_url = &req.signaling_url;
87 let realm_id = req.realm_id;
88
89 let edition = req.edition();
90 let exports = req.exports_list();
91 let platform_web = req.platform_web();
92 let raw_acl = req.raw_acl();
93
94 let mut dep_entries = Vec::new();
96 for dep in &req.dependencies {
97 if let Some(ref at) = dep.actr_type {
98 dep_entries.push(format!(
99 " '{}': {{ actr_type: '{}:{}:{}' }},",
100 dep.alias, at.manufacturer, at.name, at.version
101 ));
102 } else {
103 dep_entries.push(format!(" '{}': {{}},", dep.alias));
104 }
105 }
106
107 let type_export_name = "actrType";
110
111 let mut out = String::new();
112 out.push_str(
113 "/**\n * Auto-generated Actr configuration\n * Source: actr.toml\n *\n * DO NOT EDIT this file manually\n */\n\n",
114 );
115 out.push_str("import type { ActorClientConfig, SwRuntimeConfig } from '@actr/web';\n\n");
116
117 out.push_str("// ── Full actr.toml info ──\n\n");
119 out.push_str(&format!(
120 "/** actr.toml edition */\nexport const edition = {edition};\n\n"
121 ));
122
123 out.push_str("/** Exported proto files */\n");
125 if exports.is_empty() {
126 out.push_str("export const exports: string[] = [];\n\n");
127 } else {
128 out.push_str(&format!(
129 "export const exports = [{}];\n\n",
130 exports
131 .iter()
132 .map(|e| format!("'{e}'"))
133 .collect::<Vec<_>>()
134 .join(", ")
135 ));
136 }
137
138 let authors_js: Vec<String> = req.authors.iter().map(|a| format!("'{a}'")).collect();
140 let tags_js: Vec<String> = req.tags.iter().map(|t| format!("'{t}'")).collect();
141 out.push_str("/** Package info */\n");
142 out.push_str("export const packageInfo = {\n");
143 out.push_str(&format!(" name: '{}',\n", req.package_name));
144 out.push_str(&format!(" description: '{}',\n", req.description));
145 out.push_str(&format!(" authors: [{}],\n", authors_js.join(", ")));
146 out.push_str(&format!(" license: '{}',\n", req.license));
147 out.push_str(&format!(" tags: [{}],\n", tags_js.join(", ")));
148 out.push_str("} as const;\n\n");
149
150 let version = &req.version;
152 out.push_str("/** ActrType */\n");
153 out.push_str(&format!("export const {type_export_name} = {{\n"));
154 out.push_str(&format!(" manufacturer: '{manufacturer}',\n"));
155 out.push_str(&format!(" name: '{actr_name}',\n"));
156 out.push_str(&format!(" version: '{version}',\n"));
157 out.push_str(&format!(
158 " fullType: '{manufacturer}:{actr_name}:{version}',\n"
159 ));
160 out.push_str("} as const;\n\n");
161
162 out.push_str("/** Dependencies */\n");
164 if dep_entries.is_empty() {
165 out.push_str("export const dependencies = {} as const;\n\n");
166 } else {
167 out.push_str("export const dependencies = {\n");
168 for entry in &dep_entries {
169 out.push_str(entry);
170 out.push('\n');
171 }
172 out.push_str("} as const;\n\n");
173 }
174
175 if let Some(pw) = &platform_web {
177 out.push_str("/** Web platform config */\n");
178 out.push_str("export const platform = {\n web: ");
179 out.push_str(&format_toml_as_ts(pw, 2));
180 out.push_str(",\n} as const;\n\n");
181 }
182
183 out.push_str("/** System config */\n");
185 out.push_str("export const system = {\n");
186 out.push_str(" signaling: {\n");
187 out.push_str(&format!(
188 " url: '{}',\n",
189 signaling_url.trim_end_matches('/')
190 ));
191 out.push_str(" },\n");
192 out.push_str(" deployment: {\n");
193 out.push_str(&format!(" realm_id: {realm_id},\n"));
194 if !req.ais_endpoint.is_empty() {
195 out.push_str(&format!(" ais_endpoint: '{}',\n", req.ais_endpoint));
196 }
197 out.push_str(" },\n");
198 out.push_str(" discovery: {\n");
199 out.push_str(&format!(" visible: {},\n", req.visible_in_discovery));
200 out.push_str(" },\n");
201 out.push_str(" observability: {\n");
202 out.push_str(&format!(
203 " filter_level: '{}',\n",
204 req.observability.filter_level
205 ));
206 out.push_str(&format!(
207 " tracing_enabled: {},\n",
208 req.observability.tracing_enabled
209 ));
210 if req.observability.tracing_enabled {
211 out.push_str(&format!(
212 " tracing_endpoint: '{}',\n",
213 req.observability.tracing_endpoint
214 ));
215 out.push_str(&format!(
216 " tracing_service_name: '{}',\n",
217 req.observability.tracing_service_name
218 ));
219 }
220 out.push_str(" },\n");
221 out.push_str(&format!(
222 " webrtc: {{\n force_relay: {},\n",
223 req.force_relay
224 ));
225 if !req.stun_urls.is_empty() {
226 out.push_str(&format!(
227 " stun_urls: [{}],\n",
228 req.stun_urls
229 .iter()
230 .map(|u| format!("'{u}'"))
231 .collect::<Vec<_>>()
232 .join(", ")
233 ));
234 }
235 if !req.turn_urls.is_empty() {
236 out.push_str(&format!(
237 " turn_urls: [{}],\n",
238 req.turn_urls
239 .iter()
240 .map(|u| format!("'{u}'"))
241 .collect::<Vec<_>>()
242 .join(", ")
243 ));
244 }
245 out.push_str(" },\n");
246 out.push_str("} as const;\n\n");
247
248 if let Some(acl_val) = &raw_acl {
250 out.push_str("/** ACL config */\n");
251 out.push_str("export const acl = ");
252 out.push_str(&format_toml_as_ts(acl_val, 0));
253 out.push_str(" as const;\n\n");
254 } else {
255 out.push_str("/** ACL config */\n");
256 out.push_str("export const acl = {} as const;\n\n");
257 }
258
259 let client_actr_type = req.client_actr_type();
261 let target_actr_type = req.target_actr_type();
262 let acl_allow = req.get_acl_allow_types();
263 let acl_allow_js: Vec<String> = acl_allow.iter().map(|t| format!("'{t}'")).collect();
264
265 let package_url = format!("/packages/{}.actr", req.package_name);
270
271 let wasm_name = req.wasm_module_name();
274 let runtime_wasm_url = format!("/packages/{wasm_name}_bg.wasm");
275
276 out.push_str("// ── runtimeConfig (passed to Service Worker WASM registration) ──\n\n");
277 out.push_str("/**\n * Service Worker runtime config\n * Passed from main thread to Service Worker via DOM_PORT_INIT\n */\n");
278 out.push_str("export const runtimeConfig: SwRuntimeConfig = {\n");
279 if !req.ais_endpoint.is_empty() {
280 out.push_str(&format!(" ais_endpoint: '{}',\n", req.ais_endpoint));
281 }
282 out.push_str(" signaling_url: system.signaling.url,\n");
283 out.push_str(" realm_id: system.deployment.realm_id,\n");
284 out.push_str(&format!(" client_actr_type: '{client_actr_type}',\n"));
285 out.push_str(&format!(" target_actr_type: '{target_actr_type}',\n"));
286 out.push_str(" service_fingerprint: '',\n");
287 out.push_str(&format!(
288 " acl_allow_types: [{}],\n",
289 acl_allow_js.join(", ")
290 ));
291 out.push_str(&format!(" package_url: '{package_url}',\n"));
292 out.push_str(&format!(" runtime_wasm_url: '{runtime_wasm_url}',\n"));
293 out.push_str(" // Trust anchors — pubkey_b64 is substituted by deploy tooling.\n");
294 out.push_str(" trust: [{ kind: 'static', pubkey_b64: '__MFR_PUBKEY_PLACEHOLDER__' }],\n");
295 out.push_str("};\n\n");
296
297 out.push_str("// ── ActorClientConfig (passed to createActor) ──\n\n");
301 out.push_str(
302 "/**\n * Actor client config\n * Extracted from system config in actr.toml\n */\n",
303 );
304 out.push_str("export const actrConfig: ActorClientConfig = {\n");
305 out.push_str(" signalingUrl: system.signaling.url,\n");
306 out.push_str(" realm: String(system.deployment.realm_id),\n");
307 out.push_str(" iceServers: [\n");
308 if !req.stun_urls.is_empty() {
309 out.push_str(" ...system.webrtc.stun_urls.map((url) => ({ urls: url })),\n");
310 }
311 if !req.turn_urls.is_empty() {
312 out.push_str(" ...system.webrtc.turn_urls.map((url) => ({ urls: url })),\n");
313 }
314 out.push_str(" ],\n");
315 let ice_transport = if req.force_relay { "relay" } else { "all" };
316 out.push_str(&format!(" iceTransportPolicy: '{ice_transport}',\n"));
317 out.push_str(" serviceWorkerPath: '/actor.sw.js',\n");
318 out.push_str(" autoReconnect: true,\n");
319 out.push_str(" debug: false,\n");
320 out.push_str(" runtimeConfig,\n");
321 out.push_str("};\n");
322
323 Ok(out)
324}
325
326fn gen_actor_ref(service: &ServiceInfo, req: &WebCodegenRequest) -> Result<String, String> {
331 let service_name = &service.name;
332
333 let mut out = String::new();
334 out.push_str("/**\n");
335 out.push_str(" * Auto-generated ActorRef\n");
336 out.push_str(&format!(" * Local service: {service_name}\n"));
337 out.push_str(" *\n * DO NOT EDIT this file manually\n */\n\n");
338
339 let mut remote_imports: HashMap<String, Vec<String>> = HashMap::new();
341 for method in &service.methods {
342 for remote in &req.remote_services {
343 for rm in &remote.methods {
344 if rm.input_type == method.input_type || rm.output_type == method.output_type {
345 let import_path = format!(
346 "./{}",
347 to_kebab_case(
348 &remote
349 .relative_path
350 .parent()
351 .unwrap_or(Path::new(""))
352 .to_string_lossy()
353 .replace(std::path::MAIN_SEPARATOR, "/")
354 )
355 );
356 let types = remote_imports.entry(import_path).or_default();
357 if !types.contains(&method.input_type) {
358 types.push(method.input_type.clone());
359 }
360 if !types.contains(&method.output_type) {
361 types.push(method.output_type.clone());
362 }
363 }
364 }
365 }
366 }
367
368 for (path, types) in &remote_imports {
369 let unique_types: Vec<&str> = types.iter().map(|t| t.as_str()).collect();
370 out.push_str(&format!(
371 "import {{ {} }} from '{}';\n",
372 unique_types.join(", "),
373 path
374 ));
375 }
376 if !remote_imports.is_empty() {
377 out.push('\n');
378 }
379
380 out.push_str("/**\n * callRaw compatible interface\n */\n");
381 out.push_str("interface CallRawCapable {\n");
382 out.push_str(" callRaw(routeKey: string, payload: Uint8Array, timeout?: number): Promise<Uint8Array>;\n");
383 out.push_str("}\n\n");
384
385 let actr_type = service.actr_type.as_deref().unwrap_or("");
387 if !actr_type.is_empty() {
388 let parts: Vec<&str> = actr_type.splitn(2, ':').collect();
389 let (mfr, name) = if parts.len() == 2 {
390 (parts[0], parts[1])
391 } else {
392 ("", actr_type)
393 };
394 out.push_str("/**\n * ActrType definition\n */\n");
395 out.push_str(&format!("export const {service_name}ActrType = {{\n"));
396 out.push_str(&format!(" manufacturer: '{mfr}',\n"));
397 out.push_str(&format!(" name: '{name}',\n"));
398 out.push_str("};\n\n");
399 }
400
401 out.push_str(&format!(
403 "/**\n * {service_name} ActorRef wrapper\n * Provides type-safe RPC call methods\n */\n"
404 ));
405 out.push_str(&format!("export class {service_name}ActorRef {{\n"));
406 out.push_str(" private actor: CallRawCapable;\n\n");
407 out.push_str(" constructor(actor: CallRawCapable) {\n");
408 out.push_str(" this.actor = actor;\n");
409 out.push_str(" }\n");
410
411 for method in &service.methods {
412 let camel = to_camel_case(&method.name);
413 let input = &method.input_type;
414 let output = &method.output_type;
415 let route = &method.route_key;
416
417 out.push_str(&format!(
418 "\n /**\n * Call {} RPC method\n */\n",
419 method.name
420 ));
421 out.push_str(&format!(
422 " async {camel}(request: {input}): Promise<{output}> {{\n"
423 ));
424 out.push_str(&format!(
425 " const encoded = {input}.encode(request).finish();\n"
426 ));
427 out.push_str(&format!(
428 " const responseData = await this.actor.callRaw('{route}', encoded);\n"
429 ));
430 out.push_str(&format!(" return {output}.decode(responseData);\n"));
431 out.push_str(" }\n");
432 }
433
434 out.push_str("}\n");
435 Ok(out)
436}
437
438fn gen_index(req: &WebCodegenRequest) -> Result<String, String> {
443 let mut out = String::new();
444 out.push_str("/**\n * Auto-generated Actr code entry point\n *\n * DO NOT EDIT this file manually\n */\n\n");
445 out.push_str("export {\n");
446 out.push_str(" actrConfig,\n");
447 out.push_str(" runtimeConfig,\n");
448 out.push_str(" actrType,\n");
449 out.push_str(" edition,\n");
450 out.push_str(" exports,\n");
451 out.push_str(" packageInfo,\n");
452 out.push_str(" dependencies,\n");
453 out.push_str(" system,\n");
454 out.push_str(" acl,\n");
455 out.push_str("} from './actr-config';\n");
456
457 for svc in &req.local_services {
458 let file_name = to_kebab_case(&svc.name);
459 out.push_str(&format!("export * from './{file_name}.actorref';\n"));
460 }
461
462 Ok(out)
463}
464
465fn gen_service_worker(req: &WebCodegenRequest) -> Result<String, String> {
470 let wasm_name = req.wasm_module_name();
471
472 let mut out = String::new();
473
474 out.push_str("/* Actor-RTC Service Worker entry.\n");
478 out.push_str(" *\n * WASM includes: guest-bridge SW runtime\n");
479 out.push_str(" *\n * This file is auto-generated by actr gen — DO NOT EDIT\n */\n\n");
480 out.push_str("/* global wasm_bindgen */\n\n");
481
482 out.push_str(CONSOLE_INTERCEPTION);
484 out.push('\n');
485
486 out.push_str("/** @type {import('@actr/web').SwRuntimeConfig | null} */\n");
488 out.push_str("let RUNTIME_CONFIG = null;\n\n");
489
490 out.push_str("let wasmReady = false;\n");
492 out.push_str("let wsProbeDone = false;\n");
493 out.push_str("const clientPorts = new Map();\n");
494 out.push_str("const browserToSwClient = new Map();\n\n");
495
496 out.push_str(CLEANUP_STALE_CLIENTS);
497 out.push('\n');
498 out.push_str(EMIT_SW_LOG);
499 out.push('\n');
500
501 out.push_str("async function ensureWasmReady() {\n");
503 out.push_str(" if (wasmReady) return;\n\n");
504 out.push_str(" let runtimeUrl;\n let wasmUrl;\n try {\n");
505 out.push_str(&format!(
506 " runtimeUrl = new URL('{wasm_name}.js', self.location).toString();\n"
507 ));
508 out.push_str(&format!(
509 " wasmUrl = new URL('{wasm_name}_bg.wasm', self.location).toString();\n"
510 ));
511 out.push_str(" emitSwLog('info', 'runtime_url', runtimeUrl);\n\n");
512
513 out.push_str(" if (!wsProbeDone && RUNTIME_CONFIG) {\n");
515 out.push_str(" wsProbeDone = true;\n");
516 out.push_str(" try {\n");
517 out.push_str(" const probe = new WebSocket(RUNTIME_CONFIG.signaling_url);\n");
518 out.push_str(" probe.binaryType = 'arraybuffer';\n");
519 out.push_str(" probe.onopen = () => { emitSwLog('info', 'ws_probe_open', null); probe.close(); };\n");
520 out.push_str(
521 " probe.onerror = () => { emitSwLog('error', 'ws_probe_error', null); };\n",
522 );
523 out.push_str(
524 " } catch (error) { emitSwLog('error', 'ws_probe_throw', String(error)); }\n",
525 );
526 out.push_str(" }\n\n");
527
528 out.push_str(" const runtimeRes = await fetch(runtimeUrl, { cache: 'no-store' });\n");
530 out.push_str(" const wasmRes = await fetch(wasmUrl, { cache: 'no-store' });\n\n");
531 out.push_str(" try {\n");
532 out.push_str(" const runtimeText = await runtimeRes.text();\n");
533 out.push_str(" const patchedText = runtimeText.replace('let wasm_bindgen =', 'self.wasm_bindgen =');\n");
534 out.push_str(" (0, eval)(patchedText);\n");
535 out.push_str(" } catch (error) {\n");
536 out.push_str(" emitSwLog('error', 'eval_failed', String(error));\n");
537 out.push_str(" throw error;\n");
538 out.push_str(" }\n\n");
539 out.push_str(" await wasm_bindgen({ module_or_path: wasmUrl });\n");
540 out.push_str(" wasm_bindgen.init_global();\n\n");
541
542 out.push_str(" wasmReady = true;\n");
543 out.push_str(" emitSwLog('info', 'wasm_ready', null);\n");
544 out.push_str(" } catch (error) {\n");
545 out.push_str(" console.error('[SW] WASM init failed:', error);\n");
546 out.push_str(" emitSwLog('error', 'wasm_init_failed', { error: String(error) });\n");
547 out.push_str(" throw error;\n");
548 out.push_str(" }\n}\n\n");
549
550 out.push_str(SW_EVENT_LISTENERS);
552
553 Ok(out)
554}
555
556fn gen_wasm_scaffold(req: &WebCodegenRequest) -> Result<Vec<PathBuf>, String> {
561 let wasm_dir = req.project_root.join("wasm");
562 let wasm_src = wasm_dir.join("src");
563 let wasm_generated = wasm_src.join("generated");
564
565 let mut files = Vec::new();
566 let provider_only = req.is_service_provider_only();
570 let crate_name = req.wasm_module_name();
571 let pkg_name = format!("{}-wasm", req.package_name);
572
573 let cargo_path = wasm_dir.join("Cargo.toml");
575 if !cargo_path.exists() {
576 write_file(&cargo_path, &gen_wasm_cargo_toml(&pkg_name))?;
577 files.push(cargo_path);
578 }
579
580 let build_sh = wasm_dir.join("build.sh");
582 if !build_sh.exists() {
583 write_file(&build_sh, &gen_wasm_build_sh(&crate_name))?;
584 make_executable(&build_sh)?;
585 files.push(build_sh);
586 }
587
588 std::fs::create_dir_all(&wasm_generated)
590 .map_err(|e| format!("Failed to create wasm generated dir: {e}"))?;
591
592 let mut generated_modules = Vec::new();
593 for file_info in &req.files {
594 if !file_info.package.is_empty() {
595 let mod_name = to_snake_case(&file_info.package);
596 if !generated_modules.contains(&mod_name) {
597 generated_modules.push(mod_name.clone());
598 let rs_path = wasm_generated.join(format!("{mod_name}.rs"));
599 let rs_content = gen_proto_rs_types(file_info);
600 write_file(&rs_path, &rs_content)?;
601 files.push(rs_path);
602 }
603 }
604 }
605
606 let mod_rs = wasm_generated.join("mod.rs");
608 let mut mod_content = String::from("//! Auto-generated proto type modules\n\n");
609 for m in &generated_modules {
610 mod_content.push_str(&format!("pub mod {m};\n"));
611 }
612 write_file(&mod_rs, &mod_content)?;
613 files.push(mod_rs);
614
615 if provider_only {
617 for svc in &req.local_services {
618 let handler_name = to_snake_case(&svc.name);
619 let handler_path = wasm_src.join(format!("{handler_name}.rs"));
620 if !handler_path.exists() {
621 write_file(&handler_path, &gen_server_handler(svc))?;
622 files.push(handler_path);
623 }
624 }
625 let lib_path = wasm_src.join("lib.rs");
626 if !lib_path.exists() {
627 write_file(&lib_path, &gen_server_lib_rs(req))?;
628 files.push(lib_path);
629 }
630 } else {
631 for svc in &req.local_services {
632 let handler_name = to_snake_case(&svc.name);
633 let handler_path = wasm_src.join(format!("{handler_name}_handler.rs"));
634 if !handler_path.exists() {
635 write_file(&handler_path, &gen_client_handler(svc, req))?;
636 files.push(handler_path);
637 }
638 }
639 let lib_path = wasm_src.join("lib.rs");
640 if !lib_path.exists() {
641 write_file(&lib_path, &gen_client_lib_rs(req))?;
642 files.push(lib_path);
643 }
644 }
645
646 Ok(files)
647}
648
649fn gen_wasm_cargo_toml(pkg_name: &str) -> String {
654 let actr_source_tag = format!("v{}", env!("CARGO_PKG_VERSION"));
655
656 format!(
657 r#"[package]
658name = "{pkg_name}"
659version = "0.1.0"
660edition = "2024"
661rust-version = "1.88"
662
663[workspace]
664
665[lib]
666crate-type = ["cdylib", "rlib"]
667
668[dependencies]
669actr-sw-host = {{ git = "https://github.com/Actrium/actr", tag = "{actr_source_tag}" }}
670actr-web-common = {{ git = "https://github.com/Actrium/actr", tag = "{actr_source_tag}" }}
671
672wasm-bindgen = "0.2"
673wasm-bindgen-futures = "0.4"
674js-sys = "0.3"
675serde-wasm-bindgen = "0.6"
676web-sys = {{ version = "0.3", features = ["console"] }}
677futures = "0.3"
678async-trait = "0.1"
679serde = {{ version = "1.0", features = ["derive"] }}
680serde_json = "1.0"
681prost = "0.14"
682bytes = "1.0"
683log = "0.4"
684wasm-logger = "0.2"
685console_error_panic_hook = "0.1"
686"#
687 )
688}
689
690fn gen_wasm_build_sh(crate_name: &str) -> String {
691 format!(
692 r#"#!/bin/bash
693set -e
694
695echo "🔨 Building WASM..."
696
697TMPDIR="${{TMPDIR:-$(pwd)/.tmp}}"
698export TMPDIR
699mkdir -p "$TMPDIR"
700
701OUT_DIR="../public"
702
703rm -f "$OUT_DIR"/{crate_name}*.wasm "$OUT_DIR"/{crate_name}*.js "$OUT_DIR"/{crate_name}*.d.ts
704
705wasm-pack build \
706 --target no-modules \
707 --out-dir "$OUT_DIR" \
708 --out-name {crate_name} \
709 --release
710
711rm -f "$OUT_DIR/package.json" "$OUT_DIR/.gitignore"
712
713echo ""
714echo "✅ WASM built successfully!"
715echo "📁 Output: $OUT_DIR"
716echo ""
717ls -la "$OUT_DIR"/{crate_name}*
718"#
719 )
720}
721
722fn gen_proto_rs_types(file_info: &FileInfo) -> String {
723 let mut out = String::from(
724 "//! Auto-generated protobuf types\n\nuse prost::Message;\nuse serde::{Deserialize, Serialize};\n\n",
725 );
726
727 let set = match crate::descriptor::compile_to_descriptor_set(
733 std::slice::from_ref(&file_info.proto_file),
734 &[],
735 ) {
736 Ok(set) => set,
737 Err(err) => {
738 tracing::warn!(
739 "{}: protoc descriptor compile failed ({}); emitting empty message bodies",
740 file_info.proto_file.display(),
741 err
742 );
743 prost_types::FileDescriptorSet::default()
744 }
745 };
746 let file_desc = crate::descriptor::find_file(&set, &file_info.proto_file);
747
748 let service_names: std::collections::HashSet<&str> = file_desc
750 .map(|f| f.service.iter().map(|s| s.name()).collect())
751 .unwrap_or_default();
752
753 for type_name in &file_info.declared_type_names {
754 if service_names.contains(type_name.as_str()) {
755 continue;
756 }
757
758 let fields = file_desc
759 .and_then(|f| crate::descriptor::message_fields_for_scaffold(f, type_name))
760 .unwrap_or_default();
761
762 out.push_str("#[derive(Clone, PartialEq, Message, Serialize, Deserialize)]\n");
763 out.push_str(&format!("pub struct {type_name} {{\n"));
764 for (i, (field_name, field_type)) in fields.iter().enumerate() {
765 let tag = i + 1;
766 let rust_type = proto_type_to_rust(field_type);
767 let prost_attr = proto_type_to_prost_attr(field_type, tag);
768 out.push_str(&format!(" {prost_attr}\n"));
769 out.push_str(&format!(" pub {field_name}: {rust_type},\n"));
770 }
771 out.push_str("}\n\n");
772 }
773
774 out
775}
776
777fn gen_server_handler(service: &ServiceInfo) -> String {
778 let sn = &service.name;
779 let mut out = String::new();
780 out.push_str(&format!("//! {sn} implementation\n\n"));
781 out.push_str("use std::rc::Rc;\nuse actr_sw_host::RuntimeContext;\nuse prost::Message;\n\n");
782
783 let pkg = to_snake_case(&service.package);
784 for m in &service.methods {
785 out.push_str(&format!(
786 "use crate::generated::{pkg}::{{{}, {}}};\n",
787 m.input_type, m.output_type
788 ));
789 }
790 out.push('\n');
791
792 out.push_str(&format!("pub struct {sn};\n\n"));
793 out.push_str(&format!("impl {sn} {{\n"));
794 out.push_str(" pub fn new() -> Self { Self }\n\n");
795
796 for m in &service.methods {
797 out.push_str(&format!(
798 " pub async fn {}(&self, request: {}, _ctx: Rc<RuntimeContext>) -> Result<{}, String> {{\n",
799 m.snake_name, m.input_type, m.output_type
800 ));
801 out.push_str(&format!(
802 " log::info!(\"Received {sn} {{}} request\", \"{}\");\n",
803 m.name
804 ));
805 out.push_str(" // TODO: Implement business logic\n");
806 out.push_str(&format!(" Ok({}::default())\n", m.output_type));
807 out.push_str(" }\n\n");
808 }
809 out.push_str("}\n\n");
810
811 out.push_str(&format!(
812 "static SERVICE: std::sync::OnceLock<{sn}> = std::sync::OnceLock::new();\n\n"
813 ));
814 out.push_str(&format!("fn get_service() -> &'static {sn} {{\n"));
815 out.push_str(&format!(" SERVICE.get_or_init({sn}::new)\n}}\n\n"));
816
817 out.push_str("pub async fn handle_request(method: &str, request_bytes: &[u8], ctx: Rc<RuntimeContext>) -> Result<Vec<u8>, String> {\n");
818 out.push_str(" match method {\n");
819 for m in &service.methods {
820 out.push_str(&format!(
821 " \"{}\" | \"{}\" => {{\n",
822 m.name, m.snake_name
823 ));
824 out.push_str(&format!(
825 " let request = {}::decode(request_bytes).map_err(|e| format!(\"Decode failed: {{}}\", e))?;\n",
826 m.input_type
827 ));
828 out.push_str(&format!(
829 " let response = get_service().{}(request, ctx).await?;\n",
830 m.snake_name
831 ));
832 out.push_str(" let mut buf = Vec::with_capacity(response.encoded_len());\n");
833 out.push_str(" response.encode(&mut buf).map_err(|e| format!(\"Encode failed: {}\", e))?;\n");
834 out.push_str(" Ok(buf)\n");
835 out.push_str(" }\n");
836 }
837 out.push_str(" _ => Err(format!(\"Unknown method: {}\", method)),\n");
838 out.push_str(" }\n}\n");
839
840 out
841}
842
843fn gen_client_handler(service: &ServiceInfo, req: &WebCodegenRequest) -> String {
844 let sn = &service.name;
845 let mut out = String::new();
846 out.push_str(&format!(
847 "//! {sn} local handler — forwards requests to remote service\n\n"
848 ));
849 out.push_str("use std::rc::Rc;\nuse std::sync::OnceLock;\n");
850 out.push_str("use actr_sw_host::RuntimeContext;\nuse actr_sw_host::WebContext;\n\n");
851
852 let remote_target = req.dependencies.first().and_then(|d| d.actr_type.as_ref());
853
854 if let Some(target) = remote_target {
855 out.push_str("static TARGET_TYPE: OnceLock<actr_sw_host::actr_protocol::ActrType> = OnceLock::new();\n\n");
856 out.push_str("fn target_type() -> &'static actr_sw_host::actr_protocol::ActrType {\n");
857 out.push_str(" TARGET_TYPE.get_or_init(|| actr_sw_host::actr_protocol::ActrType {\n");
858 out.push_str(&format!(
859 " manufacturer: \"{}\".to_string(),\n",
860 target.manufacturer
861 ));
862 out.push_str(&format!(" name: \"{}\".to_string(),\n", target.name));
863 out.push_str(" version: \"1.0.0\".to_string(),\n })\n}\n\n");
864 }
865
866 let remote_route_key = req
867 .remote_services
868 .first()
869 .and_then(|s| s.methods.first())
870 .map(|m| m.route_key.as_str())
871 .unwrap_or("unknown.Unknown.Unknown");
872
873 out.push_str("pub async fn handle_request(method: &str, request_bytes: &[u8], ctx: Rc<RuntimeContext>) -> Result<Vec<u8>, String> {\n");
874 out.push_str(" match method {\n");
875 for m in &service.methods {
876 out.push_str(&format!(" \"{}\" => {{\n", m.name));
877 out.push_str(&format!(
878 " log::info!(\"[{}] Forwarding to remote...\");\n",
879 sn
880 ));
881 if remote_target.is_some() {
882 out.push_str(" let target = ctx.discover(target_type()).await.map_err(|e| format!(\"Discover failed: {}\", e))?;\n");
883 out.push_str(&format!(
884 " let response = ctx.call_raw(&target, \"{remote_route_key}\", request_bytes, 30000).await.map_err(|e| format!(\"call_raw failed: {{}}\", e))?;\n"
885 ));
886 out.push_str(" Ok(response)\n");
887 } else {
888 out.push_str(" Err(\"No remote target configured\".to_string())\n");
889 }
890 out.push_str(" }\n");
891 }
892 out.push_str(" _ => Err(format!(\"Unknown method: {}\", method)),\n");
893 out.push_str(" }\n}\n");
894
895 out
896}
897
898fn gen_server_lib_rs(req: &WebCodegenRequest) -> String {
899 let mut handler_mods = Vec::new();
900 let mut register_calls = Vec::new();
901
902 for svc in &req.local_services {
903 let hn = to_snake_case(&svc.name);
904 handler_mods.push(hn.clone());
905 let register_fn = format!("register_{hn}");
906 let service_match = if svc.package.is_empty() {
907 svc.name.clone()
908 } else {
909 format!("{}.{}", svc.package, svc.name)
910 };
911 register_calls.push((register_fn, hn, service_match));
912 }
913
914 let mut out = String::from(
915 "//! WASM entry — Service Worker Runtime + Service Handler\n\nmod generated;\n",
916 );
917 for m in &handler_mods {
918 out.push_str(&format!("mod {m};\n"));
919 }
920 out.push_str("\nuse std::rc::Rc;\nuse wasm_bindgen::prelude::*;\npub use actr_sw_host::*;\n\n");
921 out.push_str("#[wasm_bindgen(start)]\npub fn init() {\n console_error_panic_hook::set_once();\n wasm_logger::init(wasm_logger::Config::default());\n log::info!(\"WASM initialized\");\n}\n\n");
922
923 for (rf, hn, sm) in ®ister_calls {
924 out.push_str("#[wasm_bindgen]\n");
925 out.push_str(&format!("pub fn {rf}() {{\n"));
926 out.push_str(&format!(" log::info!(\"Registering {hn}...\");\n\n"));
927 out.push_str(
928 " actr_sw_host::register_service_handler(Rc::new(|route_key, bytes, ctx| {\n",
929 );
930 out.push_str(
931 " let route_key = route_key.to_string();\n let bytes = bytes.to_vec();\n",
932 );
933 out.push_str(" Box::pin(async move {\n");
934 out.push_str(
935 " let (service, method) = if let Some(last_dot) = route_key.rfind('.') {\n",
936 );
937 out.push_str(" (&route_key[..last_dot], &route_key[last_dot + 1..])\n");
938 out.push_str(
939 " } else {\n (route_key.as_str(), \"\")\n };\n",
940 );
941 out.push_str(" match service {\n");
942 out.push_str(&format!(
943 " \"{sm}\" => {hn}::handle_request(method, &bytes, ctx).await,\n"
944 ));
945 out.push_str(" _ => Err(format!(\"Unknown service: {}\", service)),\n");
946 out.push_str(" }\n })\n }));\n\n");
947 out.push_str(&format!(" log::info!(\"{hn} registered\");\n}}\n\n"));
948 }
949
950 out
951}
952
953fn gen_client_lib_rs(req: &WebCodegenRequest) -> String {
954 let mut handler_mods = Vec::new();
955 let mut register_calls = Vec::new();
956
957 for svc in &req.local_services {
958 let hn = to_snake_case(&svc.name);
959 let mn = format!("{hn}_handler");
960 handler_mods.push(mn.clone());
961 let rf = format!("register_{hn}_handler");
962 let sm = if svc.package.is_empty() {
963 svc.name.clone()
964 } else {
965 format!("{}.{}", svc.package, svc.name)
966 };
967 register_calls.push((rf, mn, sm));
968 }
969
970 let mut out =
971 String::from("//! WASM entry — Service Worker Runtime + Local Handler\n\nmod generated;\n");
972 for m in &handler_mods {
973 out.push_str(&format!("mod {m};\n"));
974 }
975 out.push_str("\nuse std::rc::Rc;\nuse wasm_bindgen::prelude::*;\npub use actr_sw_host::*;\n\n");
976 out.push_str("#[wasm_bindgen(start)]\npub fn init() {\n console_error_panic_hook::set_once();\n wasm_logger::init(wasm_logger::Config::default());\n log::info!(\"WASM initialized\");\n}\n\n");
977
978 for (rf, mn, sm) in ®ister_calls {
979 out.push_str("#[wasm_bindgen]\n");
980 out.push_str(&format!("pub fn {rf}() {{\n"));
981 out.push_str(" log::info!(\"Registering handler...\");\n\n");
982 out.push_str(
983 " actr_sw_host::register_service_handler(Rc::new(|route_key, bytes, ctx| {\n",
984 );
985 out.push_str(
986 " let route_key = route_key.to_string();\n let bytes = bytes.to_vec();\n",
987 );
988 out.push_str(" Box::pin(async move {\n");
989 out.push_str(
990 " let (service, method) = if let Some(last_dot) = route_key.rfind('.') {\n",
991 );
992 out.push_str(" (&route_key[..last_dot], &route_key[last_dot + 1..])\n");
993 out.push_str(
994 " } else {\n (route_key.as_str(), \"\")\n };\n",
995 );
996 out.push_str(" match service {\n");
997 out.push_str(&format!(
998 " \"{sm}\" => {mn}::handle_request(method, &bytes, ctx).await,\n"
999 ));
1000 out.push_str(" _ => Err(format!(\"Unknown service: {}\", service)),\n");
1001 out.push_str(" }\n })\n }));\n\n");
1002 out.push_str(" log::info!(\"Handler registered\");\n}\n\n");
1003 }
1004
1005 out
1006}
1007
1008fn gen_root_build_sh() -> String {
1009 r#"#!/bin/bash
1010set -e
1011
1012SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
1013WASM_DIR="$SCRIPT_DIR/wasm"
1014
1015echo "Building WASM..."
1016(
1017 cd "$WASM_DIR"
1018 ./build.sh
1019)
1020
1021echo "WASM artifacts ready in public/"
1022"#
1023 .to_string()
1024}
1025
1026fn write_file(path: &Path, content: &str) -> Result<(), String> {
1031 if let Some(parent) = path.parent() {
1032 std::fs::create_dir_all(parent)
1033 .map_err(|e| format!("Failed to create directory {}: {e}", parent.display()))?;
1034 }
1035 std::fs::write(path, content)
1036 .map_err(|e| format!("Failed to write {}: {e}", path.display()))?;
1037 info!(" 📄 {}", path.display());
1038 Ok(())
1039}
1040
1041fn make_executable(path: &Path) -> Result<(), String> {
1042 #[cfg(unix)]
1043 {
1044 use std::os::unix::fs::PermissionsExt;
1045 let mut perms = std::fs::metadata(path)
1046 .map_err(|e| format!("Failed to read metadata: {e}"))?
1047 .permissions();
1048 perms.set_mode(perms.mode() | 0o755);
1049 std::fs::set_permissions(path, perms)
1050 .map_err(|e| format!("Failed to set permissions: {e}"))?;
1051 }
1052 #[cfg(not(unix))]
1053 let _ = path;
1054 Ok(())
1055}
1056
1057fn to_kebab_case(name: &str) -> String {
1058 name.to_kebab_case()
1059}
1060
1061fn to_snake_case(name: &str) -> String {
1062 name.to_snake_case()
1063}
1064
1065fn to_camel_case(name: &str) -> String {
1066 name.to_lower_camel_case()
1067}
1068
1069fn proto_type_to_rust(proto_type: &str) -> String {
1070 match proto_type {
1071 "string" => "String".to_string(),
1072 "bytes" => "Vec<u8>".to_string(),
1073 "bool" => "bool".to_string(),
1074 "int32" | "sint32" | "sfixed32" => "i32".to_string(),
1075 "int64" | "sint64" | "sfixed64" => "i64".to_string(),
1076 "uint32" | "fixed32" => "u32".to_string(),
1077 "uint64" | "fixed64" => "u64".to_string(),
1078 "float" => "f32".to_string(),
1079 "double" => "f64".to_string(),
1080 other => other.to_string(),
1081 }
1082}
1083
1084fn proto_type_to_prost_attr(proto_type: &str, tag: usize) -> String {
1085 match proto_type {
1086 "string" => format!("#[prost(string, tag = \"{tag}\")]"),
1087 "bytes" => format!("#[prost(bytes, tag = \"{tag}\")]"),
1088 "bool" => format!("#[prost(bool, tag = \"{tag}\")]"),
1089 "int32" | "sint32" | "sfixed32" => format!("#[prost(int32, tag = \"{tag}\")]"),
1090 "int64" | "sint64" | "sfixed64" => format!("#[prost(int64, tag = \"{tag}\")]"),
1091 "uint32" | "fixed32" => format!("#[prost(uint32, tag = \"{tag}\")]"),
1092 "uint64" | "fixed64" => format!("#[prost(uint64, tag = \"{tag}\")]"),
1093 "float" => format!("#[prost(float, tag = \"{tag}\")]"),
1094 "double" => format!("#[prost(double, tag = \"{tag}\")]"),
1095 _ => format!("#[prost(message, tag = \"{tag}\")]"),
1096 }
1097}
1098
1099fn format_toml_as_ts(value: &toml::Value, indent: usize) -> String {
1101 let pad = " ".repeat(indent);
1102 let inner_pad = " ".repeat(indent + 1);
1103 match value {
1104 toml::Value::Table(table) => {
1105 if table.is_empty() {
1106 return "{}".to_string();
1107 }
1108 let mut out = "{\n".to_string();
1109 for (key, val) in table {
1110 let ts_key = if key.contains('-') || key.contains('.') {
1111 format!("'{key}'")
1112 } else {
1113 key.clone()
1114 };
1115 out.push_str(&format!(
1116 "{inner_pad}{ts_key}: {},\n",
1117 format_toml_as_ts(val, indent + 1)
1118 ));
1119 }
1120 out.push_str(&format!("{pad}}}"));
1121 out
1122 }
1123 toml::Value::Array(arr) => {
1124 if arr.is_empty() {
1125 return "[]".to_string();
1126 }
1127 let items: Vec<String> = arr
1128 .iter()
1129 .map(|v| format_toml_as_ts(v, indent + 1))
1130 .collect();
1131 if arr.iter().all(|v| !v.is_table()) {
1132 format!("[{}]", items.join(", "))
1133 } else {
1134 let mut out = "[\n".to_string();
1135 for item in &items {
1136 out.push_str(&format!("{inner_pad}{item},\n"));
1137 }
1138 out.push_str(&format!("{pad}]"));
1139 out
1140 }
1141 }
1142 toml::Value::String(s) => format!("'{}'", s.replace('\'', "\\'")),
1143 toml::Value::Integer(n) => n.to_string(),
1144 toml::Value::Float(f) => f.to_string(),
1145 toml::Value::Boolean(b) => b.to_string(),
1146 toml::Value::Datetime(dt) => format!("'{dt}'"),
1147 }
1148}
1149
1150const CONSOLE_INTERCEPTION: &str = r#"(function () {
1155 const _origInfo = console.info;
1156 const _origWarn = console.warn;
1157 const _origError = console.error;
1158 const _origLog = console.log;
1159
1160 function extractMessage(args) {
1161 return Array.from(args)
1162 .filter(a => typeof a === 'string' && !/^\s*(color|background|font-weight|padding)\s*:/.test(a))
1163 .join(' ')
1164 .replace(/%c/g, '')
1165 .trim();
1166 }
1167
1168 function broadcast(data) {
1169 self.clients.matchAll({ type: 'window' }).then(clients => {
1170 for (const client of clients) {
1171 client.postMessage(data);
1172 }
1173 }).catch(() => {});
1174 }
1175
1176 console.info = function (...args) {
1177 _origInfo.apply(console, args);
1178 const msg = extractMessage(args);
1179 if (msg.includes('[SW]') || msg.includes('Echo') || msg.includes('Registering')
1180 || msg.includes('Scheduler') || msg.includes('Dispatcher')) {
1181 broadcast({ type: 'sw_log', level: 'info', message: msg, ts: Date.now() });
1182 }
1183 };
1184
1185 console.warn = function (...args) {
1186 _origWarn.apply(console, args);
1187 const msg = extractMessage(args);
1188 broadcast({ type: 'sw_log', level: 'warn', message: msg, ts: Date.now() });
1189 };
1190
1191 console.error = function (...args) {
1192 _origError.apply(console, args);
1193 const msg = extractMessage(args);
1194 broadcast({ type: 'sw_log', level: 'error', message: msg, ts: Date.now() });
1195 };
1196
1197 console.log = function (...args) {
1198 _origLog.apply(console, args);
1199 const msg = extractMessage(args);
1200 if (msg.includes('[SW]') || msg.includes('[WebRTC]')) {
1201 broadcast({ type: 'sw_log', level: 'info', message: msg, ts: Date.now() });
1202 }
1203 };
1204})();
1205"#;
1206
1207const CLEANUP_STALE_CLIENTS: &str = r#"async function cleanupStaleClients() {
1208 if (!wasmReady) return;
1209 try {
1210 const activeWindows = await self.clients.matchAll({ type: 'window' });
1211 const activeIds = new Set(activeWindows.map(c => c.id));
1212 for (const [browserId, swClientId] of browserToSwClient.entries()) {
1213 if (!activeIds.has(browserId)) {
1214 browserToSwClient.delete(browserId);
1215 clientPorts.delete(swClientId);
1216 try { await wasm_bindgen.unregister_client(swClientId); } catch (_) {}
1217 }
1218 }
1219 } catch (_) {}
1220}
1221"#;
1222
1223const EMIT_SW_LOG: &str = r#"function emitSwLog(level, message, detail) {
1224 for (const port of clientPorts.values()) {
1225 try {
1226 port.postMessage({
1227 type: 'webrtc_event',
1228 payload: { eventType: 'sw_log', data: { level, message, detail } },
1229 });
1230 } catch (_) {}
1231 }
1232}
1233"#;
1234
1235const SW_EVENT_LISTENERS: &str = r#"self.addEventListener('install', (event) => {
1236 event.waitUntil(self.skipWaiting());
1237});
1238
1239self.addEventListener('activate', (event) => {
1240 event.waitUntil(self.clients.claim());
1241});
1242
1243self.addEventListener('message', (event) => {
1244 if (event.data && event.data.type === 'PING') {
1245 if (event.source && event.source.postMessage) {
1246 event.source.postMessage({ type: 'PONG' });
1247 }
1248 return;
1249 }
1250 if (!event.data || event.data.type !== 'DOM_PORT_INIT') return;
1251
1252 const port = event.data.port;
1253 const clientId = event.data.clientId;
1254 if (!port || !clientId) return;
1255
1256 // Receive runtime config from main thread (sourced from actr-config.ts)
1257 if (event.data.runtimeConfig && !RUNTIME_CONFIG) {
1258 RUNTIME_CONFIG = event.data.runtimeConfig;
1259 }
1260
1261 clientPorts.set(clientId, port);
1262 const browserId = event.source && event.source.id;
1263 if (browserId) browserToSwClient.set(browserId, clientId);
1264
1265 cleanupStaleClients();
1266
1267 if (event.source && event.source.postMessage) {
1268 event.source.postMessage({ type: 'sw_ack', message: 'port_ready' });
1269 }
1270
1271 emitSwLog('info', 'sw_env', {
1272 clientId,
1273 location: self.location ? self.location.href : null,
1274 totalClients: clientPorts.size,
1275 });
1276
1277 port.onmessage = async (portEvent) => {
1278 try { await ensureWasmReady(); } catch (_) { return; }
1279 const message = portEvent.data;
1280 if (!message || !message.type) return;
1281
1282 switch (message.type) {
1283 case 'control':
1284 try { await wasm_bindgen.handle_dom_control(clientId, message.payload); } catch (e) {
1285 console.error('[SW] handle_dom_control failed:', e);
1286 }
1287 break;
1288 case 'webrtc_event':
1289 try { await wasm_bindgen.handle_dom_webrtc_event(clientId, message.payload); } catch (e) {
1290 console.error('[SW] handle_dom_webrtc_event failed:', e);
1291 }
1292 break;
1293 case 'fast_path_data':
1294 try { await wasm_bindgen.handle_dom_fast_path(clientId, message.payload); } catch (e) {
1295 console.error('[SW] handle_dom_fast_path failed:', e);
1296 }
1297 break;
1298 case 'register_datachannel_port':
1299 try {
1300 const dcPort = message.payload.port;
1301 const dcPeerId = message.payload.peerId;
1302 if (dcPort && dcPeerId) {
1303 await wasm_bindgen.register_datachannel_port(clientId, dcPeerId, dcPort);
1304 }
1305 } catch (e) {
1306 console.error('[SW] register_datachannel_port failed:', e);
1307 }
1308 break;
1309 }
1310 };
1311
1312 port.start();
1313
1314 ensureWasmReady().then(async () => {
1315 try {
1316 if (!RUNTIME_CONFIG) {
1317 console.error('[SW] RUNTIME_CONFIG not received from main thread');
1318 return;
1319 }
1320 await wasm_bindgen.register_client(clientId, RUNTIME_CONFIG, port);
1321 emitSwLog('info', 'client_registered', { clientId });
1322 } catch (error) {
1323 console.error('[SW] register_client failed:', error);
1324 }
1325 });
1326});
1327"#;
1328
1329#[cfg(test)]
1330mod tests {
1331 use super::*;
1332
1333 #[test]
1334 fn wasm_cargo_toml_pins_actr_web_dependencies_to_current_release_tag() {
1335 let cargo_toml = gen_wasm_cargo_toml("demo-wasm");
1336 let expected_tag = format!("tag = \"v{}\"", env!("CARGO_PKG_VERSION"));
1337
1338 assert!(cargo_toml.contains("git = \"https://github.com/Actrium/actr\""));
1339 assert!(cargo_toml.contains(&expected_tag));
1340 assert!(!cargo_toml.contains("github.com/actor-rtc/actr"));
1341 assert!(!cargo_toml.contains("branch = \"web\""));
1342 }
1343}