use crate::e2e::config::E2eConfig;
use crate::e2e::escape::sanitize_filename;
use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::hash::{self, CommentStyle};
use crate::core::template_versions as tv;
use crate::e2e::fixture::{Fixture, FixtureGroup};
use anyhow::Result;
use std::fmt::Write as FmtWrite;
use std::path::PathBuf;
use super::E2eCodegen;
use super::typescript::config::render_global_setup;
pub struct WasmCodegen;
impl E2eCodegen for WasmCodegen {
fn generate(
&self,
groups: &[FixtureGroup],
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
enums: &[crate::core::ir::EnumDef],
) -> Result<Vec<GeneratedFile>> {
let lang = self.language_name();
let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
let tests_base = output_base.join("tests");
let mut files = Vec::new();
let call = &e2e_config.call;
let overrides = call.overrides.get(lang);
let module_path = overrides
.and_then(|o| o.module.as_ref())
.cloned()
.unwrap_or_else(|| call.module.clone());
let function_name = overrides
.and_then(|o| o.function.as_ref())
.cloned()
.unwrap_or_else(|| snake_to_camel(&call.function));
let client_factory = overrides.and_then(|o| o.client_factory.as_deref());
let wasm_pkg = e2e_config.resolve_package("wasm");
let (pkg_path, pkg_path_is_explicit) = match wasm_pkg.as_ref().and_then(|p| p.path.as_ref()) {
Some(p) => (p.clone(), true),
None => (config.wasm_crate_path(), false),
};
let pkg_name = wasm_pkg
.as_ref()
.and_then(|p| p.name.as_ref())
.cloned()
.unwrap_or_else(|| config.wasm_package_name());
let pkg_version = wasm_pkg
.as_ref()
.and_then(|p| p.version.as_ref())
.cloned()
.or_else(|| config.resolved_version())
.unwrap_or_else(|| "0.1.0".to_string());
let wasm_languages = config.wasm.as_ref().and_then(|w| {
if w.languages.is_empty() {
None
} else {
Some(w.languages.clone())
}
});
let active_per_group: Vec<Vec<Fixture>> = groups
.iter()
.map(|group| {
let mut result = Vec::new();
for fixture in &group.fixtures {
let mut base_include = super::should_include_fixture(fixture, lang, e2e_config);
if base_include {
if let Some(ref wasm_langs) = wasm_languages {
let fix_lang = fixture.input.get("language").and_then(|v| v.as_str()).or_else(|| {
fixture
.input
.get("config")
.and_then(|c| c.get("language"))
.and_then(|v| v.as_str())
});
if let Some(fix_lang) = fix_lang {
if !wasm_langs.iter().any(|l| l == fix_lang) {
base_include = false;
}
}
}
}
let cc = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
if cc.skip_languages.iter().any(|l| l == lang) {
continue;
}
if base_include {
if let Some(http) = &fixture.http {
if http
.request
.headers
.iter()
.any(|(k, _)| k.eq_ignore_ascii_case("content-length"))
{
continue;
}
let m = http.request.method.to_ascii_uppercase();
if m == "TRACE" || m == "CONNECT" {
continue;
}
}
result.push(fixture.clone());
} else {
continue;
}
}
result
})
.collect();
let any_fixtures = active_per_group.iter().flat_map(|g| g.iter());
let has_http_fixtures = any_fixtures.clone().any(|f| f.needs_mock_server());
let has_file_fixtures = active_per_group.iter().flatten().any(|f| {
let cc = e2e_config.resolve_call_for_fixture(
f.call.as_deref(),
&f.id,
&f.resolved_category(),
&f.tags,
&f.input,
);
cc.args
.iter()
.any(|a| a.arg_type == "file_path" || a.arg_type == "bytes")
});
files.push(GeneratedFile {
path: output_base.join("package.json"),
content: render_package_json(
&pkg_name,
&pkg_path,
pkg_path_is_explicit,
&pkg_version,
e2e_config.dep_mode,
e2e_config.harness_extras.get("wasm"),
),
generated_header: false,
});
let needs_global_setup = has_http_fixtures && !e2e_config.harness.imports.is_empty();
let with_file_setup_cfg = has_file_fixtures || has_http_fixtures;
files.push(GeneratedFile {
path: output_base.join("vitest.config.ts"),
content: render_vitest_config(needs_global_setup, with_file_setup_cfg),
generated_header: true,
});
if has_http_fixtures && !e2e_config.harness.imports.is_empty() {
let wasm_harness_content = render_wasm_app_harness(e2e_config, groups, &pkg_name);
files.push(GeneratedFile {
path: output_base.join("app_harness.mjs"),
content: wasm_harness_content,
generated_header: true,
});
}
if needs_global_setup {
files.push(GeneratedFile {
path: output_base.join("globalSetup.ts"),
content: render_global_setup(true),
generated_header: true,
});
}
let needs_setup_ts = has_file_fixtures || has_http_fixtures;
if needs_setup_ts {
files.push(GeneratedFile {
path: output_base.join("setup.ts"),
content: render_setup(&e2e_config.test_documents_dir, has_file_fixtures, &pkg_name),
generated_header: true,
});
}
files.push(GeneratedFile {
path: output_base.join("tsconfig.json"),
content: render_tsconfig(),
generated_header: false,
});
files.push(GeneratedFile {
path: output_base.join("pnpm-workspace.yaml"),
content: "packages:\n - \".\"\nallowBuilds:\n esbuild: true\n tree-sitter: true\n".to_string(),
generated_header: true,
});
let options_type = overrides.and_then(|o| o.options_type.clone());
let wasm_type_prefix = config.wasm_type_prefix();
for (group, active) in groups.iter().zip(active_per_group.iter()) {
if active.is_empty() {
continue;
}
let filename = format!("{}.test.ts", sanitize_filename(&group.category));
let active_refs: Vec<&Fixture> = active.iter().collect();
let content = super::typescript::render_test_file(
lang,
&group.category,
&active_refs,
&module_path,
&pkg_name,
&function_name,
&e2e_config.call.args,
options_type.as_deref(),
client_factory,
e2e_config,
type_defs,
enums,
&wasm_type_prefix,
config,
);
if !content.contains("it(") && !content.contains("it.skip(") && !content.contains("test(") {
continue;
}
let _ = (&pkg_path, &config.name);
files.push(GeneratedFile {
path: tests_base.join(filename),
content,
generated_header: true,
});
}
Ok(files)
}
fn language_name(&self) -> &'static str {
"wasm"
}
}
fn snake_to_camel(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut upper_next = false;
for ch in s.chars() {
if ch == '_' {
upper_next = true;
} else if upper_next {
out.push(ch.to_ascii_uppercase());
upper_next = false;
} else {
out.push(ch);
}
}
out
}
fn render_package_json(
pkg_name: &str,
pkg_path: &str,
pkg_path_is_explicit: bool,
pkg_version: &str,
dep_mode: crate::e2e::config::DependencyMode,
extras: Option<&crate::core::config::manifest_extras::ManifestExtras>,
) -> String {
let dep_value = match dep_mode {
crate::e2e::config::DependencyMode::Registry => {
let trimmed = pkg_version.trim_start();
if trimmed.starts_with(['^', '~', '>', '<', '=']) {
pkg_version.to_string()
} else {
format!("^{pkg_version}")
}
}
crate::e2e::config::DependencyMode::Local => {
if pkg_path_is_explicit {
format!("file:{pkg_path}")
} else {
format!("file:{pkg_path}/nodejs")
}
}
};
let rendered = crate::e2e::template_env::render(
"wasm/package.json.jinja",
minijinja::context! {
pkg_name => pkg_name,
dep_value => dep_value,
rollup => tv::npm::ROLLUP,
vitest => tv::npm::VITEST,
},
);
match extras {
Some(e) if !e.is_empty() => crate::e2e::codegen::typescript::config::inject_package_json_extras(&rendered, e),
_ => rendered,
}
}
fn render_vitest_config(with_global_setup: bool, with_file_setup: bool) -> String {
let header = hash::header(CommentStyle::DoubleSlash);
crate::e2e::template_env::render(
"wasm/vitest.config.ts.jinja",
minijinja::context! {
header => header,
with_global_setup => with_global_setup,
with_file_setup => with_file_setup,
},
)
}
fn render_setup(test_documents_dir: &str, include_file_setup: bool, pkg_name: &str) -> String {
let header = hash::header(CommentStyle::DoubleSlash);
let mut out = header;
out.push_str("import { createRequire } from 'module';\n");
out.push_str("import { readFileSync } from 'fs';\n");
out.push_str("import { fileURLToPath } from 'url';\n");
if include_file_setup {
out.push_str("import { dirname, join } from 'path';\n");
}
out.push('\n');
out.push_str("// Pre-initialize the wasm-bindgen module so that exports are callable\n");
out.push_str("// in every vitest worker. The async default export uses fetch() which\n");
out.push_str("// does not support file:// URLs in Node.js; use initSync with a\n");
out.push_str("// readFileSync buffer instead.\n");
let _ = writeln!(out, "try {{");
let _ = writeln!(out, " const _require = createRequire(import.meta.url);");
let _ = writeln!(out, " const wasmPkgDir = _require.resolve('{pkg_name}');");
let _ = writeln!(out, " const wasmModule = await import(/* @vite-ignore */ wasmPkgDir);");
out.push_str(" const initSync = (wasmModule as unknown as Record<string, unknown>).initSync as ((mod: WebAssembly.Module | BufferSource) => unknown) | undefined;\n");
out.push_str(" if (typeof initSync === 'function') {\n");
out.push_str(" // Locate the .wasm binary next to the JS entry.\n");
out.push_str(" const wasmJsPath = fileURLToPath(new URL(wasmPkgDir, 'file://'));\n");
out.push_str(" const wasmBinPath = wasmJsPath.replace(/\\.js$/, '_bg.wasm');\n");
out.push_str(" const wasmBytes = readFileSync(wasmBinPath);\n");
out.push_str(" // Pass as object form to avoid wasm-bindgen deprecation warning.\n");
out.push_str(" initSync({ module: wasmBytes });\n");
out.push_str(" } else {\n");
out.push_str(" // Fallback: try the async default init (wasm-pack --target nodejs bundles).\n");
out.push_str(" const initDefault = (wasmModule as unknown as Record<string, unknown>).default as (() => Promise<unknown>) | undefined;\n");
out.push_str(" if (typeof initDefault === 'function') await initDefault();\n");
out.push_str(" }\n");
out.push_str("} catch (err) {\n");
out.push_str(" // Module may not require explicit init — continue anyway.\n");
out.push_str(" console.warn('[alef wasm setup] init skipped:', (err as Error).message);\n");
out.push_str("}\n\n");
if include_file_setup {
let file_only = render_file_setup(test_documents_dir);
let body_start = file_only
.find("// Patch CommonJS")
.or_else(|| file_only.find("// Change to"))
.unwrap_or(0);
out.push_str(&file_only[body_start..]);
}
out
}
fn render_file_setup(test_documents_dir: &str) -> String {
let mut out = String::new();
out.push_str("// Patch CommonJS `require('env')` and `require('wasi_snapshot_preview1')` to\n");
out.push_str("// return shim objects. wasm-pack `--target nodejs` emits bare `require()`\n");
out.push_str("// calls for these from getrandom/wasi transitives, but they are not real\n");
out.push_str("// Node modules — the WASM module imports them by name and the host is\n");
out.push_str("// expected to satisfy them. Patch Module._load BEFORE the wasm bundle is\n");
out.push_str("// imported by any test file.\n");
out.push_str("// Note: setupFiles run per-test-worker; vitest imports the test files\n");
out.push_str("// AFTER setupFiles complete, so this hook installs in time.\n");
out.push_str("{\n");
out.push_str(" const _require = createRequire(import.meta.url);\n");
out.push_str(" const Module = _require('module');\n");
out.push_str(" // env.system / env.mkstemp come from C-runtime calls embedded in some\n");
out.push_str(" // WASM-compiled deps (e.g. tesseract-wasm). Tests that don't exercise\n");
out.push_str(" // those paths only need the imports to be callable for module instantiation.\n");
out.push_str(" const env = {\n");
out.push_str(" system: (_cmd: number) => -1,\n");
out.push_str(" mkstemp: (_template: number) => -1,\n");
out.push_str(" };\n");
out.push_str(" // WASI shims. Critical: clock_time_get and random_get must produce realistic\n");
out.push_str(" // values — returning 0 for all clock calls causes WASM-side timing loops to\n");
out.push_str(" // spin forever (e.g. getrandom's spin-until-elapsed retry), and zero-filled\n");
out.push_str(" // random buffers can cause init loops in deps expecting non-zero entropy.\n");
out.push_str(" const _wasiMemoryView = (): DataView | null => {\n");
out.push_str(" // Imports are wired before the WASM is instantiated; the bundle stashes\n");
out.push_str(" // its instance on a runtime-known global once available. We try to grab\n");
out.push_str(" // it lazily so writes to wasm memory go to the right place.\n");
out.push_str(" const g = globalThis as unknown as { __alef_wasm_memory__?: WebAssembly.Memory };\n");
out.push_str(" return g.__alef_wasm_memory__ ? new DataView(g.__alef_wasm_memory__.buffer) : null;\n");
out.push_str(" };\n");
out.push_str(" const _cryptoFill = (buf: Uint8Array) => {\n");
out.push_str(" const c = globalThis.crypto;\n");
out.push_str(" if (c && typeof c.getRandomValues === 'function') c.getRandomValues(buf);\n");
out.push_str(" else for (let i = 0; i < buf.length; i++) buf[i] = Math.floor(Math.random() * 256);\n");
out.push_str(" };\n");
out.push_str(" const wasi_snapshot_preview1 = {\n");
out.push_str(" proc_exit: () => {},\n");
out.push_str(" environ_get: () => 0,\n");
out.push_str(" environ_sizes_get: (countOut: number, _sizeOut: number) => {\n");
out.push_str(" const v = _wasiMemoryView();\n");
out.push_str(" if (v) v.setUint32(countOut, 0, true);\n");
out.push_str(" return 0;\n");
out.push_str(" },\n");
out.push_str(" // WASI fd_write must update `nwritten_ptr` with the total bytes consumed,\n");
out.push_str(" // otherwise libc-style callers (e.g. tesseract-compiled-to-wasm fputs)\n");
out.push_str(" // see 0 of N bytes written and retry forever, hanging the host.\n");
out.push_str(" fd_write: (_fd: number, iovsPtr: number, iovsLen: number, nwrittenPtr: number) => {\n");
out.push_str(" const v = _wasiMemoryView();\n");
out.push_str(" if (!v) return 0;\n");
out.push_str(" let total = 0;\n");
out.push_str(" for (let i = 0; i < iovsLen; i++) {\n");
out.push_str(" const off = iovsPtr + i * 8;\n");
out.push_str(" total += v.getUint32(off + 4, true);\n");
out.push_str(" }\n");
out.push_str(" v.setUint32(nwrittenPtr, total, true);\n");
out.push_str(" return 0;\n");
out.push_str(" },\n");
out.push_str(" // Mirror fd_write: callers retry on partial reads. Reporting 0 bytes\n");
out.push_str(" // read (EOF) is fine; just make sure `nread_ptr` is written.\n");
out.push_str(" fd_read: (_fd: number, _iovsPtr: number, _iovsLen: number, nreadPtr: number) => {\n");
out.push_str(" const v = _wasiMemoryView();\n");
out.push_str(" if (v) v.setUint32(nreadPtr, 0, true);\n");
out.push_str(" return 0;\n");
out.push_str(" },\n");
out.push_str(" fd_seek: () => 0,\n");
out.push_str(" fd_close: () => 0,\n");
out.push_str(" fd_prestat_get: () => 8, // EBADF — no preopens.\n");
out.push_str(" fd_prestat_dir_name: () => 0,\n");
out.push_str(" fd_fdstat_get: () => 0,\n");
out.push_str(" fd_fdstat_set_flags: () => 0,\n");
out.push_str(" path_open: () => 44, // ENOENT.\n");
out.push_str(" path_create_directory: () => 0,\n");
out.push_str(" path_remove_directory: () => 0,\n");
out.push_str(" path_unlink_file: () => 0,\n");
out.push_str(" path_filestat_get: () => 44, // ENOENT.\n");
out.push_str(" path_rename: () => 0,\n");
out.push_str(" clock_time_get: (_clockId: number, _precision: bigint, timeOut: number) => {\n");
out.push_str(" const ns = BigInt(Date.now()) * 1_000_000n + BigInt(performance.now() | 0) % 1_000_000n;\n");
out.push_str(" const v = _wasiMemoryView();\n");
out.push_str(" if (v) v.setBigUint64(timeOut, ns, true);\n");
out.push_str(" return 0;\n");
out.push_str(" },\n");
out.push_str(" clock_res_get: (_clockId: number, resOut: number) => {\n");
out.push_str(" const v = _wasiMemoryView();\n");
out.push_str(" if (v) v.setBigUint64(resOut, 1_000n, true);\n");
out.push_str(" return 0;\n");
out.push_str(" },\n");
out.push_str(" random_get: (bufPtr: number, bufLen: number) => {\n");
out.push_str(" const g = globalThis as unknown as { __alef_wasm_memory__?: WebAssembly.Memory };\n");
out.push_str(" if (!g.__alef_wasm_memory__) return 0;\n");
out.push_str(" _cryptoFill(new Uint8Array(g.__alef_wasm_memory__.buffer, bufPtr, bufLen));\n");
out.push_str(" return 0;\n");
out.push_str(" },\n");
out.push_str(" args_get: () => 0,\n");
out.push_str(" args_sizes_get: (countOut: number, _sizeOut: number) => {\n");
out.push_str(" const v = _wasiMemoryView();\n");
out.push_str(" if (v) v.setUint32(countOut, 0, true);\n");
out.push_str(" return 0;\n");
out.push_str(" },\n");
out.push_str(" poll_oneoff: () => 0,\n");
out.push_str(" sched_yield: () => 0,\n");
out.push_str(" };\n");
out.push_str(" const _origResolve = Module._resolveFilename;\n");
out.push_str(" Module._resolveFilename = function(request: string, parent: unknown, ...rest: unknown[]) {\n");
out.push_str(" if (request === 'env' || request === 'wasi_snapshot_preview1') return request;\n");
out.push_str(" return _origResolve.call(this, request, parent, ...rest);\n");
out.push_str(" };\n");
out.push_str(" const _origLoad = Module._load;\n");
out.push_str(" Module._load = function(request: string, parent: unknown, ...rest: unknown[]) {\n");
out.push_str(" if (request === 'env') return env;\n");
out.push_str(" if (request === 'wasi_snapshot_preview1') return wasi_snapshot_preview1;\n");
out.push_str(" return _origLoad.call(this, request, parent, ...rest);\n");
out.push_str(" };\n");
out.push_str(" // Capture the WASM linear memory at instantiation time so the WASI shims\n");
out.push_str(" // can read/write into it. Without this, every shim that needs memory\n");
out.push_str(" // (fd_write nwritten, clock_time_get, random_get, etc.) silently no-ops\n");
out.push_str(" // and the host-side C runtime hangs in a retry loop.\n");
out.push_str(" const _OrigInstance = WebAssembly.Instance;\n");
out.push_str(" const PatchedInstance = function(this: WebAssembly.Instance, mod: WebAssembly.Module, imports?: WebAssembly.Imports) {\n");
out.push_str(" const inst = new _OrigInstance(mod, imports);\n");
out.push_str(" const exportsMem = (inst.exports as Record<string, unknown>).memory;\n");
out.push_str(" if (exportsMem instanceof WebAssembly.Memory) {\n");
out.push_str(" (globalThis as unknown as { __alef_wasm_memory__?: WebAssembly.Memory }).__alef_wasm_memory__ = exportsMem;\n");
out.push_str(" }\n");
out.push_str(" return inst;\n");
out.push_str(" } as unknown as typeof WebAssembly.Instance;\n");
out.push_str(" PatchedInstance.prototype = _OrigInstance.prototype;\n");
out.push_str(
" (WebAssembly as unknown as { Instance: typeof WebAssembly.Instance }).Instance = PatchedInstance;\n",
);
out.push_str("}\n\n");
out.push_str("// Change to the configured test-documents directory so that fixture file paths like\n");
out.push_str("// \"pdf/fake_memo.pdf\" resolve correctly when vitest runs from e2e/wasm/.\n");
out.push_str("// setup.ts lives in e2e/wasm/; the fixtures dir lives at the repository root,\n");
out.push_str("// two directories up: e2e/wasm/ -> e2e/ -> repo root.\n");
out.push_str("const __filename = fileURLToPath(import.meta.url);\n");
out.push_str("const __dirname = dirname(__filename);\n");
let _ = writeln!(
out,
"const testDocumentsDir = join(__dirname, '..', '..', '{test_documents_dir}');"
);
out.push_str("process.chdir(testDocumentsDir);\n");
out
}
fn render_tsconfig() -> String {
crate::e2e::template_env::render("wasm/tsconfig.jinja", minijinja::context! {})
}
fn render_wasm_app_harness(e2e_config: &E2eConfig, groups: &[FixtureGroup], wasm_pkg_name: &str) -> String {
let mut fixtures_map = serde_json::Map::new();
for group in groups {
for fixture in &group.fixtures {
if fixture.http.is_none() {
continue;
}
let http_data = &fixture.http.as_ref().unwrap();
let fixture_json = serde_json::json!({
"http": {
"handler": {
"route": &http_data.handler.route,
"method": &http_data.handler.method,
"body_schema": http_data.handler.body_schema.clone(),
},
"request": {
"path": &http_data.request.path,
},
"expected_response": {
"status_code": http_data.expected_response.status_code,
"body": &http_data.expected_response.body,
"headers": &http_data.expected_response.headers,
}
}
});
fixtures_map.insert(fixture.id.clone(), fixture_json);
}
}
let fixtures_json = serde_json::to_string(&fixtures_map).unwrap_or_default();
let host = &e2e_config.harness.host;
let port = e2e_config.harness.port;
let header = hash::header(CommentStyle::DoubleSlash);
let app_class = &e2e_config.harness.app_class;
let method_enum = &e2e_config.harness.method_enum;
let run_method = &e2e_config.harness.run_method;
crate::e2e::template_env::render(
"typescript/app_harness.mjs.jinja",
minijinja::context! {
header => header,
host => host,
port => port,
response_body_field => e2e_config.harness.response_body_field.as_str(),
fixtures_json => fixtures_json,
imports => vec![wasm_pkg_name.to_string()],
app_class => app_class.as_deref().unwrap_or("App"),
method_enum => method_enum.as_deref().unwrap_or("Method"),
run_method => run_method.as_deref().unwrap_or("run"),
},
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_setup_ts_no_duplicate_imports() {
let setup = render_setup("fixtures", true, "@sample_core/wasm");
let create_require_count = setup.matches("import { createRequire }").count();
let file_url_to_path_count = setup.matches("import { fileURLToPath }").count();
let read_file_sync_count = setup.matches("readFileSync").count();
let path_imports_count = setup.matches("import { dirname, join }").count();
assert_eq!(create_require_count, 1, "createRequire should be imported exactly once");
assert_eq!(
file_url_to_path_count, 1,
"fileURLToPath should be imported exactly once"
);
assert!(read_file_sync_count >= 2, "readFileSync should be imported and used"); assert_eq!(path_imports_count, 1, "dirname, join should be imported exactly once");
let first_import = setup.find("import {").expect("should have imports");
let first_patch_comment = setup.find("// Patch CommonJS").expect("should have patch comment");
assert!(
first_import < first_patch_comment,
"all imports should come before the patch comment"
);
}
#[test]
fn test_package_json_includes_node_options_memory_cap() {
let pkg_json = render_package_json(
"@test/wasm",
"pkg",
false,
"0.1.0",
crate::e2e::config::DependencyMode::Local,
None,
);
assert!(
pkg_json.contains("NODE_OPTIONS=--max-old-space-size=4096"),
"package.json test script must include NODE_OPTIONS memory cap; got:\n{pkg_json}"
);
assert!(
pkg_json.contains("\"test\": \"NODE_OPTIONS=--max-old-space-size=4096 vitest run\""),
"NODE_OPTIONS must be part of the test script; got:\n{pkg_json}"
);
}
#[test]
fn test_package_json_registry_release_uses_caret() {
let pkg_json = render_package_json(
"@test/wasm",
"pkg",
false,
"1.2.3",
crate::e2e::config::DependencyMode::Registry,
None,
);
assert!(
pkg_json.contains("\"^1.2.3\""),
"registry release pin must use caret; got:\n{pkg_json}"
);
}
#[test]
fn test_package_json_registry_prerelease_uses_caret_semver() {
let pkg_json = render_package_json(
"@test/wasm",
"pkg",
false,
"3.6.0-rc.1",
crate::e2e::config::DependencyMode::Registry,
None,
);
assert!(
pkg_json.contains("\"^3.6.0-rc.1\""),
"registry pre-release pin must use caret with raw semver; got:\n{pkg_json}"
);
}
#[test]
fn test_package_json_registry_already_prefixed_passes_through() {
let pkg_json = render_package_json(
"@test/wasm",
"pkg",
false,
"^3.6.0-rc.1",
crate::e2e::config::DependencyMode::Registry,
None,
);
assert!(
pkg_json.contains("\"^3.6.0-rc.1\""),
"already-prefixed input must pass through verbatim; got:\n{pkg_json}"
);
assert!(
!pkg_json.contains("^^"),
"must not double the `^` prefix; got:\n{pkg_json}"
);
}
}