diff --git a/.github/workflows/check-pr.yml b/.github/workflows/check-pr.yml index 1555be83cb..83bf5ffd07 100644 --- a/.github/workflows/check-pr.yml +++ b/.github/workflows/check-pr.yml @@ -64,9 +64,20 @@ jobs: - name: Start the lively server run: | chmod a+x ./start-server.sh - ./start-server.sh > /dev/null 2>&1 & - # wait until server is guaranteed to be running - sleep 30 + for attempt in 1 2 3; do + ./start-server.sh > /tmp/lively-next-boot-server.log 2>&1 & + server_pid=$! + if node ./scripts/wait-for-server.js http://127.0.0.1:9011/ 120000 1000 "$server_pid"; then + exit 0 + fi + echo "Lively server boot-check start attempt $attempt/3 failed. Recent server output:" + tail -80 /tmp/lively-next-boot-server.log 2>/dev/null || true + kill "$server_pid" 2>/dev/null || true + wait "$server_pid" 2>/dev/null || true + pkill -f "bin/start-server.js.*9011" 2>/dev/null || true + pkill -f "start-server.sh" 2>/dev/null || true + done + exit 1 - name: Run boot check script run: | chmod a+x ./scripts/check-boot.sh diff --git a/flatn/bun-install.js b/flatn/bun-install.js index 1a7d1295e8..0bcf8131b6 100644 --- a/flatn/bun-install.js +++ b/flatn/bun-install.js @@ -1,4 +1,4 @@ -import { execSync, spawnSync } from 'child_process'; +import { execSync, spawn, spawnSync } from 'child_process'; import fs from 'fs'; import path from 'path'; import { gitSpecFromVersion } from './flatn-cjs.js'; @@ -77,16 +77,7 @@ export async function bunInstall (bunPath, livelyDirs, destDir, projectRoot, ver ); // 4. Run bun install - const result = spawnSync(bunPath, ['install', '--no-progress'], { - cwd: bunWorkDir, - stdio: verbose ? 'inherit' : 'pipe', - env: { ...process.env } - }); - - if (result.status !== 0) { - const stderr = result.stderr ? result.stderr.toString() : ''; - throw new Error(`bun install failed (exit ${result.status}): ${stderr}`); - } + await runBunInstall(bunPath, bunWorkDir, verbose); // bun install completed // 5. Build git spec map from original dependency version strings @@ -137,6 +128,52 @@ export async function bunInstall (bunPath, livelyDirs, destDir, projectRoot, ver return { newPackages }; } +async function runBunInstall (bunPath, bunWorkDir, verbose) { + console.log(' Running bun install...'); + + const child = spawn(bunPath, ['install', '--no-progress'], { + cwd: bunWorkDir, + stdio: verbose ? 'inherit' : ['ignore', 'pipe', 'pipe'], + env: { ...process.env } + }); + + let stdout = ''; + let stderr = ''; + let lastOutputAt = Date.now(); + const startedAt = Date.now(); + + if (!verbose) { + child.stdout?.on('data', chunk => { + stdout += chunk.toString(); + lastOutputAt = Date.now(); + }); + child.stderr?.on('data', chunk => { + stderr += chunk.toString(); + lastOutputAt = Date.now(); + }); + } + + const heartbeat = !verbose && setInterval(() => { + const elapsedSec = Math.round((Date.now() - startedAt) / 1000); + const quietSec = Math.round((Date.now() - lastOutputAt) / 1000); + console.log(` bun install still running... ${elapsedSec}s elapsed, ${quietSec}s since last output`); + }, 10000); + + const result = await new Promise((resolve, reject) => { + child.on('error', reject); + child.on('close', (code, signal) => resolve({ code, signal })); + }); + + if (heartbeat) clearInterval(heartbeat); + + if (result.code !== 0) { + const output = [stderr.trim(), stdout.trim()].filter(Boolean).join('\n'); + throw new Error( + `bun install failed (${result.signal ? `signal ${result.signal}` : `exit ${result.code}`}): ${output}` + ); + } +} + function buildGitDepMap (aggregatedDeps, bunWorkDir, bunPath) { // Build a map of package name -> gitSpec using the ORIGINAL dependency version // strings. This is critical because PackageSpec.matches() computes its gitSpec diff --git a/lively.changesets/src/changeset.js b/lively.changesets/src/changeset.js index 88146e72bb..dcc53592b9 100644 --- a/lively.changesets/src/changeset.js +++ b/lively.changesets/src/changeset.js @@ -98,7 +98,7 @@ async function adjustChangeSets (doFunc, doneFunc, skipPrev) { // (() -> Promise const nextB = await targetBranchRead(pkg.address); if (prevB && !nextB) { console.log(`deactivating ${prevB}`); - prevB.deactivate(); + await prevB.deactivate(); } else if (!prevB && nextB || prevB && nextB && prevB.name !== nextB.name) { console.log(`activating ${nextB}`); await nextB.activate(); diff --git a/lively.changesets/tests/changeset-test.js b/lively.changesets/tests/changeset-test.js index adb41ef115..a461833b69 100644 --- a/lively.changesets/tests/changeset-test.js +++ b/lively.changesets/tests/changeset-test.js @@ -8,7 +8,9 @@ import changeSet, { localChangeSets } from '../src/changeset.js'; import { pkgDir, fileA, createPackage, deletePackage, initTestChangeSet } from './helpers.js'; import { install, uninstall } from 'lively.changesets'; -describe('changesets', () => { +describe('changesets', function () { + this.timeout(5000); + beforeEach(async () => { install(); await createPackage(); diff --git a/lively.changesets/tests/serialization-test.js b/lively.changesets/tests/serialization-test.js index 9187706225..64943bec62 100644 --- a/lively.changesets/tests/serialization-test.js +++ b/lively.changesets/tests/serialization-test.js @@ -6,7 +6,9 @@ import { module } from 'lively.modules'; import { importChangeSet, localChangeSets } from '../src/changeset.js'; import { fileA, createPackage, deletePackage, initTestChangeSet } from './helpers.js'; -describe('serialize', () => { +describe('serialize', function () { + this.timeout(5000); + let cs; beforeEach(async () => { await createPackage(); diff --git a/lively.classes/tests/object-class-test.js b/lively.classes/tests/object-class-test.js index 32df94722b..53eedaf7c6 100644 --- a/lively.classes/tests/object-class-test.js +++ b/lively.classes/tests/object-class-test.js @@ -27,8 +27,10 @@ let S, opts, packagesToRemove; describe('object package', function () { beforeEach(async () => { S = getSystem('test', { baseURL: testBaseURL }); - S.set('lively.transpiler.babel', System.get('lively.transpiler.babel')); - S.config({ transpiler: 'lively.transpiler.babel' }); + const transpiler = System.transpiler; + S.set(transpiler, System.get(transpiler)); + S.config({ transpiler }); + S._loader.transpilerPromise = System._loader.transpilerPromise; S.translate = async (load, opts) => await System.translate.bind(S)(load, opts); S._scripting = scripting; opts = { baseURL: testBaseURL, System: S }; diff --git a/lively.classes/tests/source-descriptor-test.js b/lively.classes/tests/source-descriptor-test.js index 640a2fccea..d06e092773 100644 --- a/lively.classes/tests/source-descriptor-test.js +++ b/lively.classes/tests/source-descriptor-test.js @@ -23,8 +23,10 @@ let S; describe('source descriptors', function () { beforeEach(async () => { S = getSystem('test', { baseURL: testDir }); - S.set('lively.transpiler.babel', System.get('lively.transpiler.babel')); - S.config({ transpiler: 'lively.transpiler.babel' }); + const transpiler = System.transpiler; + S.set(transpiler, System.get(transpiler)); + S.config({ transpiler }); + S._loader.transpilerPromise = System._loader.transpilerPromise; S.translate = async (load, opts) => await System.translate.bind(S)(load, opts); S._scripting = System._scripting; await createFiles(testDir, testResources); diff --git a/lively.freezer/package.json b/lively.freezer/package.json index 3bf93a584b..5c875657df 100644 --- a/lively.freezer/package.json +++ b/lively.freezer/package.json @@ -34,10 +34,11 @@ "build-landing-page": "./tools/build-landing-page.sh", "build-unified": "./tools/build-unified.sh", "build": "./tools/build-unified.sh", - "build-swc-plugin": "cd swc-plugin && cargo build --release --target wasm32-wasip1 && cp target/wasm32-wasip1/release/lively_swc_plugin.wasm lively_swc_plugin.wasm", - "build-swc-plugin-dev": "cd swc-plugin && cargo build --target wasm32-wasip1 && cp target/wasm32-wasip1/debug/lively_swc_plugin.wasm lively_swc_plugin.wasm", + "build-swc-plugin": "cd swc-plugin && cargo build --release --target wasm32-wasip1 -p lively-swc-plugin && cp target/wasm32-wasip1/release/lively_swc_plugin.wasm lively_swc_plugin.wasm", + "build-swc-plugin-dev": "cd swc-plugin && cargo build --target wasm32-wasip1 -p lively-swc-plugin && cp target/wasm32-wasip1/debug/lively_swc_plugin.wasm lively_swc_plugin.wasm", + "build-swc-browser": "cd swc-plugin && cargo build --release --target wasm32-unknown-unknown -p lively-swc-browser && wasm-bindgen --target web --out-dir ../swc-browser-wasm target/wasm32-unknown-unknown/release/lively_swc_browser.wasm", "example-swc-bundler": "./tools/run-swc-example.sh", - "test-swc-plugin": "cd swc-plugin && cargo test" + "test-swc-plugin": "cd swc-plugin && cargo test -p lively-swc-transforms" }, "systemjs": { "map": { diff --git a/lively.freezer/src/bundler-swc.js b/lively.freezer/src/bundler-swc.js index b0b2376ce1..99e7160b7e 100644 --- a/lively.freezer/src/bundler-swc.js +++ b/lively.freezer/src/bundler-swc.js @@ -11,6 +11,10 @@ import path from 'path'; import { fileURLToPath } from 'url'; import { createHash } from 'crypto'; +function hasInitializeClassRuntimeImport (code) { + return /^\s*import\s*\{\s*initializeClass\s+as\s+initializeES6ClassForLively\s*\}\s*from\s*['"](?:livelyClassesRuntime\.js|lively\.classes\/runtime\.js)['"]\s*;?/m.test(code); +} + /** * SWC-based transform pipeline for lively.next */ @@ -19,7 +23,8 @@ export class LivelySwcTransform { this.options = { captureObj: '__varRecorder__', exclude: [ - 'console', 'window', 'document', 'global', 'process', 'Buffer', + 'console', 'window', 'document', 'global', 'globalThis', 'self', 'lively', + 'process', 'Buffer', 'System', '__contextModule__', 'Object', 'Array', 'Function', 'String', 'Number', 'Boolean', 'Symbol', 'Date', 'Math', 'JSON', 'Promise', 'RegExp', 'Error', @@ -54,7 +59,8 @@ export class LivelySwcTransform { captureImports = true, sourceMap = true, filename = 'unknown.js', - moduleHash = null + moduleHash = null, + exclude = [] } = options; const swcConfig = { @@ -81,16 +87,24 @@ export class LivelySwcTransform { }; const classToFunctionConfig = classToFunction || null; + const transformExclude = [...new Set([ + ...(this.options.exclude || []), + ...(exclude || []) + ])]; const livelyConfig = { captureObj: this.options.captureObj, declarationWrapper, classToFunction: classToFunctionConfig, - exclude: this.options.exclude, + exclude: transformExclude, captureImports, + rewriteMixedDefaultImports: false, resurrection, moduleId, - currentModuleAccessor: currentModuleAccessor || classToFunctionConfig?.currentModuleAccessor || null, + // Scope capture in bundle mode always uses FreezerRuntime.recorderFor(). + // The class transform has its own currentModuleAccessor via classToFunction.currentModuleAccessor. + // Setting this to null ensures the FreezerRuntime path is used (not System.get("@lively-env").moduleEnv()). + currentModuleAccessor: null, packageName, packageVersion, enableComponentTransform: true, @@ -105,7 +119,7 @@ export class LivelySwcTransform { const classRuntimeModule = resurrection ? 'livelyClassesRuntime.js' : 'lively.classes/runtime.js'; const classRuntimeImport = `import { initializeClass as initializeES6ClassForLively } from "${classRuntimeModule}";\n`; const sourceForTransform = classToFunctionConfig && - !code.includes('initializeClass as initializeES6ClassForLively') + !hasInitializeClassRuntimeImport(code) ? classRuntimeImport + code : code; diff --git a/lively.freezer/src/util/bootstrap.js b/lively.freezer/src/util/bootstrap.js index c0ce9978be..7c13d0d983 100644 --- a/lively.freezer/src/util/bootstrap.js +++ b/lively.freezer/src/util/bootstrap.js @@ -10,7 +10,7 @@ import { updateBundledModules } from 'lively.modules/src/module.js'; import { Project } from 'lively.project/project.js'; import { pathForBrowserHistory } from 'lively.morphic/helpers.js'; import { installLinter } from 'lively.ide/js/linter.js'; -import { setupBabelTranspiler } from 'lively.source-transform/babel/plugin.js'; +import { setupSwcTranspiler } from 'lively.source-transform/swc/transpiler-setup.js'; import untar from 'js-untar'; import bowser from 'bowser'; @@ -185,8 +185,10 @@ async function shallowReloadModulesIfNeeded (modulesToCheck, moduleHashes, R) { let key = modId; let currMod; if (key === '@empty') continue; - if (key.startsWith('esm://')) continue; // do not revive esm modules - if (modHash !== moduleHashes['/' + key]) { + if (key.startsWith('esm://') || key.startsWith('http://') || key.startsWith('https://')) continue; // do not revive CDN modules + const serverHash = moduleHashes['/' + key]; + if (serverHash == null) continue; // module is not part of the server hash map + if (modHash !== serverHash) { console.log('reviving', modId); currMod = lively.modules.module(modId); try { @@ -233,7 +235,7 @@ function bootstrapLivelySystem (progress, fastLoad = query.fastLoad !== false || $world.env.installSystemChangeHandlers(); installLinter(System); - setupBabelTranspiler(System); + await setupSwcTranspiler(System); logInfo('Setup SystemJS:', Date.now() - ts + 'ms'); // load packages @@ -364,7 +366,8 @@ function bootstrapLivelySystem (progress, fastLoad = query.fastLoad !== false || const keysAfter = obj.keys(R.registry); if (keysBefore.length < keysAfter.length) { // detect modules to be reloaded - const modulesToUpdate = arr.withoutAll(keysAfter, keysBefore).filter(id => !id.startsWith('esm://')); + const modulesToUpdate = arr.withoutAll(keysAfter, keysBefore) + .filter(id => !id.startsWith('esm://') && !id.startsWith('http://') && !id.startsWith('https://')); for (const mod of modulesToUpdate) { System.set(System.decanonicalize(mod), System.newModule(R.exportsOf(mod))); const m = lively.modules.module(mod); diff --git a/lively.freezer/src/util/runtime.js b/lively.freezer/src/util/runtime.js index 4ed09d8c35..a4cb9985cf 100644 --- a/lively.freezer/src/util/runtime.js +++ b/lively.freezer/src/util/runtime.js @@ -138,6 +138,19 @@ export function runtimeDefinition () { return globalValue; } + function findRecorderExport (rec, ...names) { + for (let name of names) { + if (!name) continue; + if (name in rec) return { found: true, value: rec[name] }; + // The SWC freezer transform materializes imported exports under an + // internal alias so SystemJS emits them; __module_exports__ keeps the + // public name. + const exportName = `__export_${name}__`; + if (exportName in rec) return { found: true, value: rec[exportName] }; + } + return { found: false }; + } + if (G.lively.FreezerRuntime) { let [myMajor, myMinor, myPatch] = version.split('.').map(Number); let [otherMajor, otherMinor, otherPatch] = G.lively.FreezerRuntime.version.split('.').map(Number); @@ -587,13 +600,17 @@ export function runtimeDefinition () { for (let exp of rec.__module_exports__) { if (exp.startsWith('__rename__')) { const [local, exported] = exp.replace('__rename__', '').split('->'); - exports[exported] = rec[local]; + const value = findRecorderExport(rec, local, exported); + if (value.found) exports[exported] = value.value; } else if (exp.startsWith('__reexport__')) Object.assign(exports, this.exportsOf(exp.replace('__reexport__', ''))); else if (exp.startsWith('__default__')) { const localName = exp.replace('__default__', ''); - if (localName in rec) exports.default = rec[localName]; + const value = findRecorderExport(rec, localName); + if (value.found) exports.default = value.value; + } else { + const value = findRecorderExport(rec, exp); + if (value.found) exports[exp] = value.value; } - else if (exp in rec) exports[exp] = rec[exp]; } return exports; }, diff --git a/lively.freezer/swc-browser-wasm/lively_swc_browser.d.ts b/lively.freezer/swc-browser-wasm/lively_swc_browser.d.ts new file mode 100644 index 0000000000..edbe6d7093 --- /dev/null +++ b/lively.freezer/swc-browser-wasm/lively_swc_browser.d.ts @@ -0,0 +1,56 @@ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Transform JavaScript source code using lively.next's SWC transforms, + * then wrap in System.register() format for SystemJS module loading. + * + * # Arguments + * * `source` - The JavaScript source code to transform + * * `config_json` - JSON string matching `LivelyTransformConfig` + * + * # Returns + * JSON string: `{ "code": "...", "map": "..." }` + */ +export function transform(source: string, config_json: string): string; + +/** + * Returns the version of the transforms library. + */ +export function version(): string; + +export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; + +export interface InitOutput { + readonly memory: WebAssembly.Memory; + readonly transform: (a: number, b: number, c: number, d: number) => [number, number, number, number]; + readonly version: () => [number, number]; + readonly __wbindgen_externrefs: WebAssembly.Table; + readonly __wbindgen_malloc: (a: number, b: number) => number; + readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; + readonly __externref_table_dealloc: (a: number) => void; + readonly __wbindgen_free: (a: number, b: number, c: number) => void; + readonly __wbindgen_start: () => void; +} + +export type SyncInitInput = BufferSource | WebAssembly.Module; + +/** +* Instantiates the given `module`, which can either be bytes or +* a precompiled `WebAssembly.Module`. +* +* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated. +* +* @returns {InitOutput} +*/ +export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput; + +/** +* If `module_or_path` is {RequestInfo} or {URL}, makes a request and +* for everything else, calls `WebAssembly.instantiate` directly. +* +* @param {{ module_or_path: InitInput | Promise }} module_or_path - Passing `InitInput` directly is deprecated. +* +* @returns {Promise} +*/ +export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise } | InitInput | Promise): Promise; diff --git a/lively.freezer/swc-browser-wasm/lively_swc_browser.js b/lively.freezer/swc-browser-wasm/lively_swc_browser.js new file mode 100644 index 0000000000..19b26174cd --- /dev/null +++ b/lively.freezer/swc-browser-wasm/lively_swc_browser.js @@ -0,0 +1,251 @@ +let wasm; + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return decodeText(ptr, len); +} + +let cachedUint8ArrayMemory0 = null; +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function passStringToWasm0(arg, malloc, realloc) { + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8ArrayMemory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); + const ret = cachedTextEncoder.encodeInto(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +function takeFromExternrefTable0(idx) { + const value = wasm.__wbindgen_externrefs.get(idx); + wasm.__externref_table_dealloc(idx); + return value; +} + +let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); +cachedTextDecoder.decode(); +const MAX_SAFARI_DECODE_BYTES = 2146435072; +let numBytesDecoded = 0; +function decodeText(ptr, len) { + numBytesDecoded += len; + if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) { + cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + cachedTextDecoder.decode(); + numBytesDecoded = len; + } + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +const cachedTextEncoder = new TextEncoder(); + +if (!('encodeInto' in cachedTextEncoder)) { + cachedTextEncoder.encodeInto = function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; + } +} + +let WASM_VECTOR_LEN = 0; + +/** + * Transform JavaScript source code using lively.next's SWC transforms, + * then wrap in System.register() format for SystemJS module loading. + * + * # Arguments + * * `source` - The JavaScript source code to transform + * * `config_json` - JSON string matching `LivelyTransformConfig` + * + * # Returns + * JSON string: `{ "code": "...", "map": "..." }` + * @param {string} source + * @param {string} config_json + * @returns {string} + */ +export function transform(source, config_json) { + let deferred4_0; + let deferred4_1; + try { + const ptr0 = passStringToWasm0(source, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passStringToWasm0(config_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + const ret = wasm.transform(ptr0, len0, ptr1, len1); + var ptr3 = ret[0]; + var len3 = ret[1]; + if (ret[3]) { + ptr3 = 0; len3 = 0; + throw takeFromExternrefTable0(ret[2]); + } + deferred4_0 = ptr3; + deferred4_1 = len3; + return getStringFromWasm0(ptr3, len3); + } finally { + wasm.__wbindgen_free(deferred4_0, deferred4_1, 1); + } +} + +/** + * Returns the version of the transforms library. + * @returns {string} + */ +export function version() { + let deferred1_0; + let deferred1_1; + try { + const ret = wasm.version(); + deferred1_0 = ret[0]; + deferred1_1 = ret[1]; + return getStringFromWasm0(ret[0], ret[1]); + } finally { + wasm.__wbindgen_free(deferred1_0, deferred1_1, 1); + } +} + +const EXPECTED_RESPONSE_TYPES = new Set(['basic', 'cors', 'default']); + +async function __wbg_load(module, imports) { + if (typeof Response === 'function' && module instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + return await WebAssembly.instantiateStreaming(module, imports); + } catch (e) { + const validResponse = module.ok && EXPECTED_RESPONSE_TYPES.has(module.type); + + if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + + } else { + throw e; + } + } + } + + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + } else { + return instance; + } + } +} + +function __wbg_get_imports() { + const imports = {}; + imports.wbg = {}; + imports.wbg.__wbg_Error_52673b7de5a0ca89 = function(arg0, arg1) { + const ret = Error(getStringFromWasm0(arg0, arg1)); + return ret; + }; + imports.wbg.__wbindgen_init_externref_table = function() { + const table = wasm.__wbindgen_externrefs; + const offset = table.grow(4); + table.set(0, undefined); + table.set(offset + 0, undefined); + table.set(offset + 1, null); + table.set(offset + 2, true); + table.set(offset + 3, false); + }; + + return imports; +} + +function __wbg_finalize_init(instance, module) { + wasm = instance.exports; + __wbg_init.__wbindgen_wasm_module = module; + cachedUint8ArrayMemory0 = null; + + + wasm.__wbindgen_start(); + return wasm; +} + +function initSync(module) { + if (wasm !== undefined) return wasm; + + + if (typeof module !== 'undefined') { + if (Object.getPrototypeOf(module) === Object.prototype) { + ({module} = module) + } else { + console.warn('using deprecated parameters for `initSync()`; pass a single object instead') + } + } + + const imports = __wbg_get_imports(); + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } + const instance = new WebAssembly.Instance(module, imports); + return __wbg_finalize_init(instance, module); +} + +async function __wbg_init(module_or_path) { + if (wasm !== undefined) return wasm; + + + if (typeof module_or_path !== 'undefined') { + if (Object.getPrototypeOf(module_or_path) === Object.prototype) { + ({module_or_path} = module_or_path) + } else { + console.warn('using deprecated parameters for the initialization function; pass a single object instead') + } + } + + if (typeof module_or_path === 'undefined') { + module_or_path = new URL('lively_swc_browser_bg.wasm', import.meta.url); + } + const imports = __wbg_get_imports(); + + if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { + module_or_path = fetch(module_or_path); + } + + const { instance, module } = await __wbg_load(await module_or_path, imports); + + return __wbg_finalize_init(instance, module); +} + +export { initSync }; +export default __wbg_init; diff --git a/lively.freezer/swc-browser-wasm/lively_swc_browser_bg.wasm b/lively.freezer/swc-browser-wasm/lively_swc_browser_bg.wasm new file mode 100644 index 0000000000..32a44bc00d Binary files /dev/null and b/lively.freezer/swc-browser-wasm/lively_swc_browser_bg.wasm differ diff --git a/lively.freezer/swc-browser-wasm/lively_swc_browser_bg.wasm.d.ts b/lively.freezer/swc-browser-wasm/lively_swc_browser_bg.wasm.d.ts new file mode 100644 index 0000000000..4fd91338dd --- /dev/null +++ b/lively.freezer/swc-browser-wasm/lively_swc_browser_bg.wasm.d.ts @@ -0,0 +1,11 @@ +/* tslint:disable */ +/* eslint-disable */ +export const memory: WebAssembly.Memory; +export const transform: (a: number, b: number, c: number, d: number) => [number, number, number, number]; +export const version: () => [number, number]; +export const __wbindgen_externrefs: WebAssembly.Table; +export const __wbindgen_malloc: (a: number, b: number) => number; +export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; +export const __externref_table_dealloc: (a: number) => void; +export const __wbindgen_free: (a: number, b: number, c: number) => void; +export const __wbindgen_start: () => void; diff --git a/lively.freezer/swc-plugin/Cargo.lock b/lively.freezer/swc-plugin/Cargo.lock index 46cae7babd..1a035b91d8 100644 --- a/lively.freezer/swc-plugin/Cargo.lock +++ b/lively.freezer/swc-plugin/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + [[package]] name = "ahash" version = "0.8.12" @@ -9,6 +19,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", + "getrandom", "once_cell", "version_check", "zerocopy", @@ -276,6 +287,19 @@ dependencies = [ "syn", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "data-encoding" version = "2.9.0" @@ -438,9 +462,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", ] [[package]] @@ -673,17 +699,48 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +[[package]] +name = "lively-swc-browser" +version = "0.1.0" +dependencies = [ + "getrandom", + "js-sys", + "lively-swc-transforms", + "serde_json", + "swc_common", + "swc_ecma_ast", + "swc_ecma_codegen", + "swc_ecma_parser", + "swc_ecma_transforms_base", + "swc_ecma_transforms_module", + "swc_ecma_utils", + "swc_ecma_visit", + "wasm-bindgen", +] + [[package]] name = "lively-swc-plugin" version = "0.1.0" dependencies = [ - "serde", + "lively-swc-transforms", "serde_json", "swc_core", - "swc_ecma_ast", "swc_plugin_macro", ] +[[package]] +name = "lively-swc-transforms" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "swc_common", + "swc_ecma_ast", + "swc_ecma_codegen", + "swc_ecma_parser", + "swc_ecma_visit", +] + [[package]] name = "lock_api" version = "0.4.14" @@ -764,6 +821,15 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "normpath" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a9da8c9922c35a1033d76f7272dfc2e7ee20392083d75aeea6ced23c6266578" +dependencies = [ + "winapi", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -862,6 +928,24 @@ dependencies = [ "windows-link", ] +[[package]] +name = "path-clean" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecba01bf2678719532c5e3059e0b5f0811273d94b397088b82e3bd0a78c78fdd" + +[[package]] +name = "path-clean" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1347,6 +1431,20 @@ dependencies = [ "serde", ] +[[package]] +name = "swc_cached" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b6a5ef4cfec51d3fa30b73600f206453a37fc30cf1141e4644a57b1ed88616" +dependencies = [ + "ahash", + "anyhow", + "dashmap", + "once_cell", + "regex", + "serde", +] + [[package]] name = "swc_common" version = "5.0.1" @@ -1391,8 +1489,6 @@ dependencies = [ "swc_atoms", "swc_common", "swc_ecma_ast", - "swc_ecma_codegen", - "swc_ecma_parser", "swc_ecma_transforms_base", "swc_ecma_transforms_testing", "swc_ecma_utils", @@ -1456,6 +1552,25 @@ dependencies = [ "syn", ] +[[package]] +name = "swc_ecma_loader" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a19b132079bfcd19d6fdabce7e55ece93a30787f3b8684c8646ddaf2237812d" +dependencies = [ + "anyhow", + "dashmap", + "normpath", + "once_cell", + "path-clean 0.1.0", + "pathdiff", + "serde", + "serde_json", + "swc_atoms", + "swc_common", + "tracing", +] + [[package]] name = "swc_ecma_parser" version = "6.0.2" @@ -1514,6 +1629,33 @@ dependencies = [ "tracing", ] +[[package]] +name = "swc_ecma_transforms_module" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ccf19161b0ca21fc60f296fa936bfe9ce5d1c5cfe3bd70514af3ae616fc0a58" +dependencies = [ + "Inflector", + "anyhow", + "bitflags", + "indexmap", + "is-macro", + "path-clean 1.0.1", + "pathdiff", + "regex", + "serde", + "swc_atoms", + "swc_cached", + "swc_common", + "swc_ecma_ast", + "swc_ecma_loader", + "swc_ecma_parser", + "swc_ecma_transforms_base", + "swc_ecma_utils", + "swc_ecma_visit", + "tracing", +] + [[package]] name = "swc_ecma_transforms_testing" version = "6.0.0" diff --git a/lively.freezer/swc-plugin/Cargo.toml b/lively.freezer/swc-plugin/Cargo.toml index c475e46785..4e023cad96 100644 --- a/lively.freezer/swc-plugin/Cargo.toml +++ b/lively.freezer/swc-plugin/Cargo.toml @@ -1,25 +1,14 @@ -[package] -name = "lively-swc-plugin" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -swc_core = { version = "9.0.0", features = [ - "ecma_plugin_transform", - "ecma_visit", - "ecma_utils", - "ecma_ast", - "ecma_codegen", - "__parser", -] } -swc_ecma_ast = "=5.0.1" -swc_plugin_macro = "=1.0.0" -serde = { version = "=1.0.219", features = ["derive"] } -serde_json = "1" +[workspace] +members = ["lively-swc-transforms", "lively-swc-plugin", "lively-swc-browser"] +resolver = "2" [profile.release] lto = true opt-level = 3 + +# Size-optimized profile for browser WASM +[profile.browser-release] +inherits = "release" +opt-level = "z" +strip = "symbols" +codegen-units = 1 diff --git a/lively.freezer/swc-plugin/lively-swc-browser/Cargo.toml b/lively.freezer/swc-plugin/lively-swc-browser/Cargo.toml new file mode 100644 index 0000000000..7abdf693ef --- /dev/null +++ b/lively.freezer/swc-plugin/lively-swc-browser/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "lively-swc-browser" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +lively-swc-transforms = { path = "../lively-swc-transforms" } +swc_ecma_ast = "=5.0.1" +swc_ecma_visit = "=5.0.0" +swc_ecma_parser = "=6.0.2" +swc_ecma_codegen = "=5.0.1" +swc_common = { version = "=5.0.1", features = ["sourcemap"] } +wasm-bindgen = "0.2.106" +serde_json = "1" +swc_ecma_transforms_module = "=6.0.0" +swc_ecma_transforms_base = "=6.0.2" +swc_ecma_utils = "=6.0.1" +getrandom = { version = "0.3", features = ["wasm_js"] } +js-sys = "0.3" diff --git a/lively.freezer/swc-plugin/lively-swc-browser/src/lib.rs b/lively.freezer/swc-plugin/lively-swc-browser/src/lib.rs new file mode 100644 index 0000000000..7642447ae8 --- /dev/null +++ b/lively.freezer/swc-plugin/lively-swc-browser/src/lib.rs @@ -0,0 +1,1703 @@ +use std::collections::HashSet; +use wasm_bindgen::prelude::*; +use swc_common::{sync::Lrc, FileName, SourceMap, Mark, GLOBALS, Globals, DUMMY_SP}; +use swc_ecma_parser::{parse_file_as_module, Syntax, EsSyntax}; +use swc_ecma_codegen::{text_writer::JsWriter, Emitter, Config as CodegenConfig}; +use swc_ecma_ast::*; +use swc_ecma_visit::{VisitMut, VisitMutWith, Visit, VisitWith}; +use swc_ecma_transforms_module::path::Resolver; + +use lively_swc_transforms::config::LivelyTransformConfig; +use lively_swc_transforms::LivelyTransformVisitor; + +/// Transform JavaScript source code using lively.next's SWC transforms, +/// then wrap in System.register() format for SystemJS module loading. +/// +/// # Arguments +/// * `source` - The JavaScript source code to transform +/// * `config_json` - JSON string matching `LivelyTransformConfig` +/// +/// # Returns +/// JSON string: `{ "code": "...", "map": "..." }` +#[wasm_bindgen] +pub fn transform(source: &str, config_json: &str) -> Result { + let config: LivelyTransformConfig = serde_json::from_str(config_json) + .map_err(|e| JsError::new(&format!("Invalid config: {}", e)))?; + + // SWC's SystemJS transform requires the GLOBALS thread-local + GLOBALS.set(&Globals::default(), || transform_inner(source, config)) +} + +fn transform_inner(source: &str, config: LivelyTransformConfig) -> Result { + let cm = Lrc::new(SourceMap::default()); + let fm = cm.new_source_file( + FileName::Custom(config.module_id.clone()).into(), + source.to_string(), + ); + + let unresolved_mark = Mark::new(); + + let module = parse_file_as_module( + &fm, + Syntax::Es(EsSyntax { + jsx: false, + decorators: true, + ..Default::default() + }), + Default::default(), + None, + &mut vec![], + ) + .map_err(|e| JsError::new(&format!("Parse error: {:?}", e)))?; + + let mut program = swc_ecma_ast::Program::Module(module); + + // Phase 1: Run lively transforms (scope capture, class-to-function, etc.) + let capture_obj = config.capture_obj.clone(); + let module_id = config.module_id.clone(); + let declaration_wrapper = config.declaration_wrapper.clone(); + let excluded: Vec = config.exclude.iter().cloned().collect(); + let has_scope_capture = config.enable_scope_capture; + let mut visitor = LivelyTransformVisitor::new(config); + program.visit_mut_with(&mut visitor); + + // Phase 1 desugars `export { Y as a }` → `var __export_a__ = Y; export { __export_a__ as a }`. + // system_js hoists `__export_a__` (no collision) and keeps `a` as export name. + // No stripping/restoration needed — the alias stays for correct import resolution. + let export_aliases = std::collections::HashMap::::new(); + + // Phase 2: Wrap in System.register() for SystemJS module loading + let mut systemjs_pass = swc_ecma_transforms_module::system_js( + Resolver::Default, + unresolved_mark, + swc_ecma_transforms_module::system_js::Config { + allow_top_level_this: true, + ..Default::default() + }, + ); + systemjs_pass.process(&mut program); + + // Phase 3: Post-process the System.register output to match Babel's + // livelyPostTranspile (babel/plugin.js lines 1216-1327): + // + // (a) Remove "use strict" from factory (Babel removes it, line 1263) + // (b) Move __lvVarRecorder init from execute() to the factory body + // (c) Rewrite setters to capture imports to __lvVarRecorder (with defVar + // wrapper and normalizeImportedNamespace) + // (d) Insert early _export({name: void 0, ...}) in the factory + // (e) Add evaluationStart/evaluationEnd hooks to execute() (line 1127/1130) + remove_directives(&mut program); + fix_async_execute(&mut program); + fix_destructured_assignments(&mut program); + fix_default_keyword_exports(&mut program); + fix_shadowed_export_calls(&mut program); + fix_nested_fn_export_calls(&mut program); + if !export_aliases.is_empty() { + restore_export_aliases(&mut program, &export_aliases); + } + if has_scope_capture { + hoist_recorder_init(&mut program, &capture_obj); + rewrite_setters(&mut program, &capture_obj, declaration_wrapper.as_deref(), &excluded); + insert_evaluation_hooks(&mut program, &module_id); + } + // Note: we intentionally do NOT insert early _export({name: void 0}) calls + // in the factory body. Babel's SystemJS transform doesn't do this either. + // Early exports cause problems with circular deps (e.g. cycle-breaker.js + // classHolder becomes void 0). SystemJS handles circular deps by returning + // whatever exports have been set so far when a circular import is detected. + + let module = match &program { + swc_ecma_ast::Program::Module(m) => m, + swc_ecma_ast::Program::Script(s) => { + // SystemJS transform may convert Module to Script + // Emit as script in that case + let mut src_buf = vec![]; + let mut src_map_buf = vec![]; + { + let mut emitter = Emitter { + cfg: CodegenConfig::default().with_ascii_only(false), + cm: cm.clone(), + comments: None, + wr: JsWriter::new(cm.clone(), "\n", &mut src_buf, Some(&mut src_map_buf)), + }; + emitter.emit_script(s) + .map_err(|e| JsError::new(&format!("Codegen error: {:?}", e)))?; + } + return finish_output(cm, src_buf, src_map_buf); + } + }; + + // Generate code + source map + let mut src_buf = vec![]; + let mut src_map_buf = vec![]; + { + let mut emitter = Emitter { + cfg: CodegenConfig::default().with_ascii_only(false), + cm: cm.clone(), + comments: None, + wr: JsWriter::new(cm.clone(), "\n", &mut src_buf, Some(&mut src_map_buf)), + }; + emitter.emit_module(module) + .map_err(|e| JsError::new(&format!("Codegen error: {:?}", e)))?; + } + + finish_output(cm, src_buf, src_map_buf) +} + +/// Extract statements from either Script or Module program. +fn get_stmts_mut(program: &mut Program) -> Vec<&mut Stmt> { + match program { + Program::Script(s) => s.body.iter_mut().collect(), + Program::Module(m) => m.body.iter_mut().filter_map(|item| { + if let ModuleItem::Stmt(s) = item { Some(s) } else { None } + }).collect(), + } +} + +/// Rename local variables in nested functions that shadow exported module-level +/// bindings. SWC's system_js incorrectly wraps shadowed locals with _export(), +/// corrupting expressions like `o--` → `_export("bisectLeft", +o + 1)`. +/// +/// Strategy: collect all exported binding names, then visit nested scopes. +/// When a local declaration shadows an export name, rename the local and all +/// its references within that scope. +/// Strip export aliases: `export { Y as a }` → `export { Y }`. +/// Returns mapping: original_name → alias (e.g. "Y" → "a"). +/// system_js will hoist the original name (Y) instead of the alias (a), +/// avoiding collisions with single-letter locals in minified code. +fn strip_export_aliases(program: &mut Program) -> std::collections::HashMap { + use std::collections::HashMap; + let mut aliases: HashMap = HashMap::new(); + + let module = match program { + Program::Module(m) => m, + _ => return aliases, + }; + + for item in &mut module.body { + if let ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(named)) = item { + if named.src.is_some() { continue; } // re-exports keep aliases + for spec in &mut named.specifiers { + if let ExportSpecifier::Named(n) = spec { + if let Some(exported) = &n.exported { + let orig = match &n.orig { + ModuleExportName::Ident(id) => id.sym.to_string(), + ModuleExportName::Str(s) => s.value.to_string(), + }; + let alias = match exported { + ModuleExportName::Ident(id) => id.sym.to_string(), + ModuleExportName::Str(s) => s.value.to_string(), + }; + if orig != alias { + aliases.insert(orig.clone(), alias); + // Remove the alias: `export { Y as a }` → `export { Y }` + n.exported = None; + } + } + } + } + } + } + aliases +} + +/// Restore export aliases in _export() calls after system_js. +/// system_js generates `_export("Y", ...)` (using the original name). +/// We rewrite to `_export("a", ...)` (using the alias). +fn restore_export_aliases(program: &mut Program, aliases: &std::collections::HashMap) { + struct AliasRestorer<'a> { + aliases: &'a std::collections::HashMap, + } + impl VisitMut for AliasRestorer<'_> { + fn visit_mut_call_expr(&mut self, call: &mut CallExpr) { + if let Callee::Expr(callee) = &call.callee { + if let Expr::Ident(id) = &**callee { + if id.sym.as_ref() == "_export" && !call.args.is_empty() { + // Check first arg: string literal with original name + if let Expr::Lit(Lit::Str(s)) = &*call.args[0].expr { + if let Some(alias) = self.aliases.get(s.value.as_ref()) { + call.args[0].expr = Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: alias.as_str().into(), + raw: None, + }))); + } + } + // Also handle bulk _export({Y: ..., he: ...}) → _export({a: ..., b: ...}) + if call.args.len() == 1 { + if let Expr::Object(obj) = &mut *call.args[0].expr { + for prop in &mut obj.props { + if let PropOrSpread::Prop(p) = prop { + if let Prop::KeyValue(kv) = &mut **p { + let key_name = match &kv.key { + PropName::Ident(id) => Some(id.sym.to_string()), + PropName::Str(s) => Some(s.value.to_string()), + _ => None, + }; + if let Some(name) = key_name { + if let Some(alias) = self.aliases.get(&name) { + kv.key = PropName::Str(Str { + span: DUMMY_SP, + value: alias.as_str().into(), + raw: None, + }); + } + } + } + } + } + } + } + } + } + } + call.visit_mut_children_with(self); + } + } + program.visit_mut_with(&mut AliasRestorer { aliases }); +} + +fn rename_shadowed_exports(program: &mut Program) { + use std::collections::HashSet; + + // Step 1: Collect module-level exported binding names + let module = match program { + Program::Module(m) => m, + _ => return, + }; + + let mut exported_names: HashSet = HashSet::new(); + + // Collect names from `export { name }`, `export const name`, `export function name`, etc. + for item in &module.body { + match item { + ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(named)) if named.src.is_none() => { + for spec in &named.specifiers { + if let ExportSpecifier::Named(n) = spec { + let name = match &n.orig { + ModuleExportName::Ident(id) => id.sym.to_string(), + ModuleExportName::Str(s) => s.value.to_string(), + }; + exported_names.insert(name); + } + } + } + ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(decl)) => { + match &decl.decl { + Decl::Var(var) => { + for d in &var.decls { + if let Pat::Ident(id) = &d.name { + exported_names.insert(id.sym.to_string()); + } + } + } + Decl::Fn(f) => { exported_names.insert(f.ident.sym.to_string()); } + Decl::Class(c) => { exported_names.insert(c.ident.sym.to_string()); } + _ => {} + } + } + ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(_)) => { + exported_names.insert("default".to_string()); + } + _ => {} + } + } + + if exported_names.is_empty() { return; } + + // Step 2: Collect (sym, ctxt) pairs for all shadowed local declarations + // in nested scopes, then rename all matching identifiers. + use std::collections::HashMap; + use swc_common::SyntaxContext; + + struct ShadowCollector { + exported: HashSet, + depth: u32, + renames: HashMap<(String, SyntaxContext), String>, + counter: u32, + } + + impl ShadowCollector { + fn check_binding(&mut self, id: &Ident) { + if self.depth == 0 { return; } + let name = id.sym.to_string(); + if self.exported.contains(&name) { + let key = (name.clone(), id.ctxt); + if !self.renames.contains_key(&key) { + self.renames.insert(key, format!("__shadow_{}_{}", name, self.counter)); + self.counter += 1; + } + } + } + fn check_pat(&mut self, pat: &Pat) { + match pat { + Pat::Ident(id) => self.check_binding(&id.id), + Pat::Object(obj) => { + for prop in &obj.props { + match prop { + ObjectPatProp::Assign(a) => { + // `{ x }` shorthand — x is both key and binding + let id = Ident::new(a.key.sym.clone(), a.key.span, a.key.ctxt); + self.check_binding(&id); + } + ObjectPatProp::KeyValue(kv) => self.check_pat(&kv.value), + ObjectPatProp::Rest(r) => self.check_pat(&r.arg), + } + } + } + Pat::Array(arr) => { + for elem in arr.elems.iter().flatten() { self.check_pat(elem); } + } + Pat::Rest(r) => self.check_pat(&r.arg), + Pat::Assign(a) => self.check_pat(&a.left), + _ => {} + } + } + } + + impl Visit for ShadowCollector { + fn visit_function(&mut self, f: &Function) { + self.depth += 1; + for p in &f.params { self.check_pat(&p.pat); } + f.visit_children_with(self); + self.depth -= 1; + } + fn visit_arrow_expr(&mut self, f: &ArrowExpr) { + self.depth += 1; + for p in &f.params { self.check_pat(p); } + f.visit_children_with(self); + self.depth -= 1; + } + fn visit_var_decl(&mut self, decl: &VarDecl) { + if self.depth > 0 { + for d in &decl.decls { self.check_pat(&d.name); } + } + decl.visit_children_with(self); + } + fn visit_catch_clause(&mut self, c: &CatchClause) { + self.depth += 1; + if let Some(p) = &c.param { self.check_pat(p); } + c.visit_children_with(self); + self.depth -= 1; + } + fn visit_for_in_stmt(&mut self, f: &ForInStmt) { + if self.depth > 0 { + if let ForHead::VarDecl(d) = &f.left { + for dd in &d.decls { self.check_pat(&dd.name); } + } + if let ForHead::Pat(p) = &f.left { self.check_pat(p); } + } + f.visit_children_with(self); + } + fn visit_for_of_stmt(&mut self, f: &ForOfStmt) { + if self.depth > 0 { + if let ForHead::VarDecl(d) = &f.left { + for dd in &d.decls { self.check_pat(&dd.name); } + } + if let ForHead::Pat(p) = &f.left { self.check_pat(p); } + } + f.visit_children_with(self); + } + } + + let mut collector = ShadowCollector { + exported: exported_names, + depth: 0, + renames: HashMap::new(), + counter: 0, + }; + module.visit_with(&mut collector); + + if collector.renames.is_empty() { return; } + + // Step 3: For each function that declares a local shadow, rename all + // identifiers matching the shadowed name WITHIN that function body only. + // This is precise: a function that uses `c` (module-level export) but + // doesn't declare its own `c` won't have `c` renamed. + let names_to_rename: HashSet = collector.renames.keys() + .map(|(name, _)| name.clone()) + .collect(); + + struct FunctionShadowRenamer { + exported_names: HashSet, + } + + impl FunctionShadowRenamer { + /// Check if this function body declares any of the exported names. + /// Returns the set of names that are locally declared. + fn find_local_shadows_in_params_and_body( + &self, params: &[Pat], body: &Option + ) -> HashSet { + let mut shadows = HashSet::new(); + for p in params { + self.collect_pat_names(p, &mut shadows); + } + if let Some(body) = body { + for stmt in &body.stmts { + self.collect_decl_names(stmt, &mut shadows); + } + } + shadows.retain(|n| self.exported_names.contains(n)); + shadows + } + + fn collect_pat_names(&self, pat: &Pat, names: &mut HashSet) { + match pat { + Pat::Ident(id) => { names.insert(id.sym.to_string()); } + Pat::Object(obj) => { + for prop in &obj.props { + match prop { + ObjectPatProp::Assign(a) => { names.insert(a.key.sym.to_string()); } + ObjectPatProp::KeyValue(kv) => self.collect_pat_names(&kv.value, names), + ObjectPatProp::Rest(r) => self.collect_pat_names(&r.arg, names), + } + } + } + Pat::Array(arr) => { for e in arr.elems.iter().flatten() { self.collect_pat_names(e, names); } } + Pat::Rest(r) => self.collect_pat_names(&r.arg, names), + Pat::Assign(a) => self.collect_pat_names(&a.left, names), + _ => {} + } + } + + fn collect_decl_names(&self, stmt: &Stmt, names: &mut HashSet) { + if let Stmt::Decl(Decl::Var(var)) = stmt { + for d in &var.decls { self.collect_pat_names(&d.name, names); } + } + // Also check for-in/for-of + if let Stmt::ForIn(f) = stmt { + if let ForHead::VarDecl(d) = &f.left { + for dd in &d.decls { self.collect_pat_names(&dd.name, names); } + } + } + if let Stmt::ForOf(f) = stmt { + if let ForHead::VarDecl(d) = &f.left { + for dd in &d.decls { self.collect_pat_names(&dd.name, names); } + } + } + } + + fn rename_idents_in_block(names: &HashSet, block: &mut BlockStmt) { + struct InnerRenamer<'a> { names: &'a HashSet } + impl InnerRenamer<'_> { + /// Check if a function declares any of the names we're renaming. + /// If so, that function has its own shadow and will be handled + /// by FunctionShadowRenamer separately — we should NOT rename + /// those names inside it. If the function doesn't redeclare any + /// of our names, we DO descend (it's a closure using the outer var). + fn declares_any_shadow(&self, params: &[Pat], body: &Option) -> HashSet { + let mut declared = HashSet::new(); + for p in params { + Self::collect_pat_names(p, &mut declared); + } + if let Some(body) = body { + for stmt in &body.stmts { + if let Stmt::Decl(Decl::Var(var)) = stmt { + for d in &var.decls { Self::collect_pat_names(&d.name, &mut declared); } + } + } + } + declared.retain(|n| self.names.contains(n)); + declared + } + fn collect_pat_names(pat: &Pat, names: &mut HashSet) { + match pat { + Pat::Ident(id) => { names.insert(id.sym.to_string()); } + Pat::Object(obj) => { for p in &obj.props { match p { + ObjectPatProp::Assign(a) => { names.insert(a.key.sym.to_string()); } + ObjectPatProp::KeyValue(kv) => Self::collect_pat_names(&kv.value, names), + ObjectPatProp::Rest(r) => Self::collect_pat_names(&r.arg, names), + }}} + Pat::Array(arr) => { for e in arr.elems.iter().flatten() { Self::collect_pat_names(e, names); } } + Pat::Rest(r) => Self::collect_pat_names(&r.arg, names), + Pat::Assign(a) => Self::collect_pat_names(&a.left, names), + _ => {} + } + } + } + impl VisitMut for InnerRenamer<'_> { + fn visit_mut_ident(&mut self, id: &mut Ident) { + if self.names.contains(id.sym.as_ref()) { + id.sym = format!("__shadow_{}", id.sym).into(); + } + } + fn visit_mut_binding_ident(&mut self, id: &mut BindingIdent) { + if self.names.contains(id.id.sym.as_ref()) { + id.id.sym = format!("__shadow_{}", id.id.sym).into(); + } + } + fn visit_mut_function(&mut self, f: &mut Function) { + let param_pats: Vec = f.params.iter().map(|p| p.pat.clone()).collect(); + let redeclared = self.declares_any_shadow(¶m_pats, &f.body); + if redeclared.is_empty() { + // No redeclaration — descend normally (closure uses outer var) + f.visit_mut_children_with(self); + } else { + // This function redeclares some names — create a reduced + // renamer that excludes the redeclared names + let remaining: HashSet = self.names.iter() + .filter(|n| !redeclared.contains(*n)) + .cloned().collect(); + if !remaining.is_empty() { + f.visit_mut_children_with(&mut InnerRenamer { names: &remaining }); + } + // Don't rename the redeclared names here — they'll be + // handled by FunctionShadowRenamer on a separate pass + } + } + fn visit_mut_arrow_expr(&mut self, f: &mut ArrowExpr) { + // Arrows always descend (they can't redeclare with var hoisting) + f.visit_mut_children_with(self); + } + } + block.visit_mut_with(&mut InnerRenamer { names }); + } + } + + impl VisitMut for FunctionShadowRenamer { + fn visit_mut_function(&mut self, f: &mut Function) { + // First recurse into nested functions + f.visit_mut_children_with(self); + + // Then check if THIS function shadows any exports + let param_pats: Vec = f.params.iter().map(|p| p.pat.clone()).collect(); + let shadows = self.find_local_shadows_in_params_and_body( + ¶m_pats, &f.body + ); + if !shadows.is_empty() { + // Rename params + for p in &mut f.params { + struct PR<'a> { names: &'a HashSet } + impl VisitMut for PR<'_> { + fn visit_mut_ident(&mut self, id: &mut Ident) { + if self.names.contains(id.sym.as_ref()) { + id.sym = format!("__shadow_{}", id.sym).into(); + } + } + fn visit_mut_binding_ident(&mut self, id: &mut BindingIdent) { + if self.names.contains(id.id.sym.as_ref()) { + id.id.sym = format!("__shadow_{}", id.id.sym).into(); + } + } + } + p.pat.visit_mut_with(&mut PR { names: &shadows }); + } + // Rename in body (but NOT in nested functions) + if let Some(body) = &mut f.body { + Self::rename_idents_in_block(&shadows, body); + } + } + } + + fn visit_mut_arrow_expr(&mut self, f: &mut ArrowExpr) { + f.visit_mut_children_with(self); + let shadows = { + let mut s = HashSet::new(); + for p in &f.params { + let fsr = FunctionShadowRenamer { exported_names: self.exported_names.clone() }; + fsr.collect_pat_names(p, &mut s); + } + // Check body for var decls + if let BlockStmtOrExpr::BlockStmt(body) = &*f.body { + let fsr = FunctionShadowRenamer { exported_names: self.exported_names.clone() }; + for stmt in &body.stmts { fsr.collect_decl_names(stmt, &mut s); } + } + s.retain(|n| self.exported_names.contains(n)); + s + }; + if !shadows.is_empty() { + for p in &mut f.params { + struct PR<'a> { names: &'a HashSet } + impl VisitMut for PR<'_> { + fn visit_mut_ident(&mut self, id: &mut Ident) { + if self.names.contains(id.sym.as_ref()) { id.sym = format!("__shadow_{}", id.sym).into(); } + } + fn visit_mut_binding_ident(&mut self, id: &mut BindingIdent) { + if self.names.contains(id.id.sym.as_ref()) { id.id.sym = format!("__shadow_{}", id.id.sym).into(); } + } + } + p.visit_mut_with(&mut PR { names: &shadows }); + } + if let BlockStmtOrExpr::BlockStmt(body) = &mut *f.body { + Self::rename_idents_in_block(&shadows, body); + } + } + } + } + + program.visit_mut_with(&mut FunctionShadowRenamer { exported_names: names_to_rename }); +} + +/// Remove "use strict" and "format esm" directives. +/// Babel's livelyPostTranspile removes "use strict" (line 1263) and +/// "format esm" (line 1264 of babel/plugin.js). +fn remove_directives(program: &mut Program) { + fn is_removable_directive(s: &Stmt) -> bool { + if let Stmt::Expr(ExprStmt { expr, .. }) = s { + if let Expr::Lit(Lit::Str(str_lit)) = &**expr { + let v = str_lit.value.as_ref(); + return v == "use strict" || v == "format esm"; + } + } + false + } + + // Remove from entire program (catches directives in factory body AND execute body) + struct DirectiveRemover; + impl VisitMut for DirectiveRemover { + fn visit_mut_block_stmt(&mut self, block: &mut BlockStmt) { + block.stmts.retain(|s| !is_removable_directive(s)); + block.visit_mut_children_with(self); + } + } + program.visit_mut_with(&mut DirectiveRemover); + + // Also remove from factory body top level (outside blocks) + for stmt in get_stmts_mut(program) { + let call = match stmt { + Stmt::Expr(ExprStmt { expr, .. }) => match &mut **expr { + Expr::Call(c) => c, + _ => continue, + }, + _ => continue, + }; + if call.args.len() < 2 { continue; } + let factory = match &mut *call.args[1].expr { + Expr::Fn(f) => f, + _ => continue, + }; + let body = match &mut factory.function.body { + Some(b) => b, + None => continue, + }; + body.stmts.retain(|s| { + if let Stmt::Expr(ExprStmt { expr, .. }) = s { + if let Expr::Lit(Lit::Str(str_lit)) = &**expr { + let v = str_lit.value.as_ref(); + if v == "use strict" || v == "format esm" { + return false; + } + } + } + true + }); + break; + } +} + +/// Fix destructured assignments in execute() that SWC's system_js emits +/// without parentheses: `{ a, b } = obj` → `({ a, b } = obj)`. +/// Without parens, `{` is parsed as a block statement → SyntaxError. +/// +/// Walks into the execute function body and wraps any AssignExpr whose LHS +/// is an ObjectPat in a ParenExpr. +/// Remove `async` from the execute function. +/// SWC's system_js incorrectly sets execute to `async function()` when the +/// module body contains `await` inside async methods (not top-level await). +/// Babel's SystemJS transform correctly keeps execute synchronous. +/// An async execute delays _export() calls, breaking circular dep resolution. +fn fix_async_execute(program: &mut Program) { + struct AsyncFixer; + impl VisitMut for AsyncFixer { + fn visit_mut_prop(&mut self, prop: &mut Prop) { + if let Prop::KeyValue(kv) = prop { + if let PropName::Ident(id) = &kv.key { + if id.sym.as_ref() == "execute" { + if let Expr::Fn(f) = &mut *kv.value { + f.function.is_async = false; + } + } + } + } + prop.visit_mut_children_with(self); + } + } + program.visit_mut_with(&mut AsyncFixer); +} + +fn fix_destructured_assignments(program: &mut Program) { + fn is_obj_destructure(expr: &Expr) -> bool { + matches!(expr, + Expr::Assign(AssignExpr { left: AssignTarget::Pat(AssignTargetPat::Object(_)), .. }) + ) + } + + fn wrap_in_paren(target: &mut Box) { + let inner = std::mem::replace(&mut **target, Expr::Invalid(Invalid { span: DUMMY_SP })); + **target = Expr::Paren(ParenExpr { span: DUMMY_SP, expr: Box::new(inner) }); + } + + struct Fixer; + impl VisitMut for Fixer { + fn visit_mut_seq_expr(&mut self, seq: &mut SeqExpr) { + seq.visit_mut_children_with(self); + for expr in &mut seq.exprs { + if is_obj_destructure(expr) { + wrap_in_paren(expr); + } + } + } + fn visit_mut_expr_stmt(&mut self, stmt: &mut ExprStmt) { + stmt.visit_mut_children_with(self); + if is_obj_destructure(&stmt.expr) { + wrap_in_paren(&mut stmt.expr); + } + } + } + program.visit_mut_with(&mut Fixer); +} + +/// Fix `_export("localName", default)` → `_export("default", localName)`. +/// +/// SWC's system_js emits `default` as a bare identifier for +/// `export { x as default }`, but `default` is a reserved keyword. +/// The correct SystemJS output should be `_export("default", x)`. +fn fix_default_keyword_exports(program: &mut Program) { + struct DefaultExportFixer; + impl VisitMut for DefaultExportFixer { + fn visit_mut_call_expr(&mut self, call: &mut CallExpr) { + // Match: _export("someName", default) + if let Callee::Expr(callee) = &call.callee { + if let Expr::Ident(id) = &**callee { + if id.sym.as_ref() == "_export" && call.args.len() == 2 { + // Check if second arg is an Ident named "default" + let is_default = matches!(&*call.args[1].expr, Expr::Ident(id) if id.sym.as_ref() == "default"); + if is_default { + // Get the first arg string value (the local name) + if let Expr::Lit(Lit::Str(s)) = &*call.args[0].expr { + let local_name = s.value.to_string(); + // Swap: _export("default", localName) + call.args[0].expr = Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: "default".into(), + raw: None, + }))); + call.args[1].expr = Box::new(Expr::Ident(Ident::new( + local_name.into(), DUMMY_SP, Default::default(), + ))); + } + } + } + } + } + call.visit_mut_children_with(self); + } + } + program.visit_mut_with(&mut DefaultExportFixer); +} + +/// Fix SWC system_js variable shadowing bug: when a local variable in a +/// nested function shadows an exported module-level variable, system_js +/// incorrectly wraps the local usage with `_export("name", value)`. +/// +/// Valid `_export` calls only appear as: +/// 1. Standalone: `_export("name", value);` +/// 2. In a comma sequence: `x = _dep.x, _export("name", x);` +/// 3. As RHS of assignment: `_export("name", x = value)` +/// +/// Invalid (shadowing bug): `_export("name", value)` nested inside binary +/// ops, function args, object literals, etc. We replace these with just +/// the `value` argument, removing the spurious _export wrapper. +fn fix_shadowed_export_calls(program: &mut Program) { + struct ShadowFixer { + /// Track depth: 0 = inside SeqExpr/ExprStmt (valid), >0 = nested (invalid) + nested_expr_depth: u32, + } + + fn is_export_call(expr: &Expr) -> bool { + if let Expr::Call(call) = expr { + if let Callee::Expr(callee) = &call.callee { + if let Expr::Ident(id) = &**callee { + return id.sym.as_ref() == "_export" && call.args.len() >= 2; + } + } + } + false + } + + fn extract_export_value(expr: &mut Expr) -> Option> { + if let Expr::Call(call) = expr { + if call.args.len() >= 2 { + return Some(call.args[1].expr.clone()); + } + } + None + } + + impl VisitMut for ShadowFixer { + // At the ExprStmt level, _export calls in SeqExpr are valid. + // We DON'T increment depth for these. + fn visit_mut_expr_stmt(&mut self, stmt: &mut ExprStmt) { + // Don't increment depth for the statement's own expression + // (it might be a SeqExpr containing valid _export calls) + match &mut *stmt.expr { + Expr::Seq(seq) => { + // Each element of a top-level SeqExpr is a valid position + for expr in &mut seq.exprs { + if !is_export_call(expr) { + self.nested_expr_depth += 1; + expr.visit_mut_with(self); + self.nested_expr_depth -= 1; + } + } + } + _ => { + if !is_export_call(&stmt.expr) { + stmt.expr.visit_mut_with(self); + } + } + } + } + + fn visit_mut_expr(&mut self, expr: &mut Expr) { + if self.nested_expr_depth > 0 && is_export_call(expr) { + // Replace _export("name", value) with (value) — parens preserve + // the grouping that the _export() call provided. + if let Some(value) = extract_export_value(expr) { + *expr = Expr::Paren(ParenExpr { + span: DUMMY_SP, + expr: value, + }); + return; + } + } + + // For non-statement expressions, increment depth before recursing + self.nested_expr_depth += 1; + expr.visit_mut_children_with(self); + self.nested_expr_depth -= 1; + } + + // Reset depth for function bodies — they're new scopes + fn visit_mut_function(&mut self, f: &mut Function) { + let saved = self.nested_expr_depth; + self.nested_expr_depth = 0; + f.visit_mut_children_with(self); + self.nested_expr_depth = saved; + } + + fn visit_mut_arrow_expr(&mut self, f: &mut ArrowExpr) { + let saved = self.nested_expr_depth; + self.nested_expr_depth = 0; + f.visit_mut_children_with(self); + self.nested_expr_depth = saved; + } + } + + program.visit_mut_with(&mut ShadowFixer { nested_expr_depth: 0 }); +} + +/// Remove spurious `_export()` calls inside nested functions where the exported +/// name is shadowed by a local `let`/`const` declaration. +/// +/// SWC's system_js transform wraps ALL assignments to export-named variables +/// with `_export()`, even inside nested functions where a local `let`/`const` +/// shadows the exported binding. For example: +/// +/// export function sum(array) { +/// let sum = 0; // local binding, shadows export +/// for (...) { sum += array[i]; } // SWC wraps: _export("sum", sum += ...) +/// } +/// +/// This fix detects `_export("X", expr)` inside functions where `X` is declared +/// as `let`/`const` in that function's scope, and unwraps to just `expr`. +fn fix_nested_fn_export_calls(program: &mut Program) { + use swc_ecma_visit::Visit; + + struct LetConstCollector { + names: HashSet, + } + impl Visit for LetConstCollector { + fn visit_var_decl(&mut self, decl: &VarDecl) { + if matches!(decl.kind, VarDeclKind::Let | VarDeclKind::Const) { + for d in &decl.decls { + collect_pat_names(&d.name, &mut self.names); + } + } + } + // Don't descend into nested functions + fn visit_function(&mut self, _: &Function) {} + fn visit_arrow_expr(&mut self, _: &ArrowExpr) {} + } + + fn collect_pat_names(pat: &Pat, names: &mut HashSet) { + match pat { + Pat::Ident(id) => { names.insert(id.id.sym.to_string()); } + Pat::Array(arr) => { + for elem in arr.elems.iter().flatten() { + collect_pat_names(elem, names); + } + } + Pat::Object(obj) => { + for prop in &obj.props { + match prop { + ObjectPatProp::Assign(a) => { names.insert(a.key.sym.to_string()); } + ObjectPatProp::KeyValue(kv) => collect_pat_names(&kv.value, names), + ObjectPatProp::Rest(r) => collect_pat_names(&r.arg, names), + } + } + } + Pat::Rest(r) => collect_pat_names(&r.arg, names), + Pat::Assign(a) => collect_pat_names(&a.left, names), + _ => {} + } + } + + fn is_export_call_with_name<'a>(expr: &'a Expr) -> Option<&'a str> { + if let Expr::Call(call) = expr { + if let Callee::Expr(callee) = &call.callee { + if let Expr::Ident(id) = &**callee { + if id.sym.as_ref() == "_export" && call.args.len() >= 2 { + if let Expr::Lit(Lit::Str(s)) = &*call.args[0].expr { + return Some(s.value.as_ref()); + } + } + } + } + } + None + } + + struct NestedExportFixer { + /// Stack of let/const names per function scope + scope_stack: Vec>, + } + + impl VisitMut for NestedExportFixer { + fn visit_mut_function(&mut self, f: &mut Function) { + // Collect let/const names in this function body (not nested) + let mut collector = LetConstCollector { names: HashSet::new() }; + if let Some(body) = &f.body { + for stmt in &body.stmts { + stmt.visit_with(&mut collector); + } + } + // Also add params as local names + for param in &f.params { + collect_pat_names(¶m.pat, &mut collector.names); + } + self.scope_stack.push(collector.names); + f.visit_mut_children_with(self); + self.scope_stack.pop(); + } + + fn visit_mut_arrow_expr(&mut self, f: &mut ArrowExpr) { + let mut names = HashSet::new(); + for param in &f.params { + collect_pat_names(param, &mut names); + } + // Collect let/const from arrow body if block + if let BlockStmtOrExpr::BlockStmt(block) = &*f.body { + let mut collector = LetConstCollector { names: HashSet::new() }; + for stmt in &block.stmts { + stmt.visit_with(&mut collector); + } + names.extend(collector.names); + } + self.scope_stack.push(names); + f.visit_mut_children_with(self); + self.scope_stack.pop(); + } + + fn visit_mut_expr(&mut self, expr: &mut Expr) { + // Only fix inside nested functions (scope_stack has at least 2 entries: + // the System.register factory and the nested function) + if self.scope_stack.len() >= 2 { + if let Some(name) = is_export_call_with_name(expr) { + // Check if name is declared as let/const in any enclosing function + // (except the outermost factory scope) + let is_shadowed = self.scope_stack.iter() + .skip(1) // skip factory scope + .any(|scope| scope.contains(name)); + if is_shadowed { + // Unwrap _export("name", value) → value + if let Expr::Call(call) = expr { + if call.args.len() >= 2 { + let value = call.args.remove(1).expr; + *expr = *value; + // Continue visiting the unwrapped expression + expr.visit_mut_with(self); + return; + } + } + } + } + } + expr.visit_mut_children_with(self); + } + } + + program.visit_mut_with(&mut NestedExportFixer { + scope_stack: vec![], + }); +} + +/// Insert evaluationStart() at the beginning and evaluationEnd() at the end +/// of execute(). Matches Babel's livelyPreTranspile (lines 1127/1130). +/// +/// Generates: +/// System.get("@lively-env").evaluationStart("moduleId"); +/// ... original execute body ... +/// System.get("@lively-env").evaluationEnd("moduleId"); +fn insert_evaluation_hooks(program: &mut Program, module_id: &str) { + for stmt in get_stmts_mut(program) { + let call = match stmt { + Stmt::Expr(ExprStmt { expr, .. }) => match &mut **expr { + Expr::Call(c) => c, + _ => continue, + }, + _ => continue, + }; + if call.args.len() < 2 { continue; } + let factory = match &mut *call.args[1].expr { + Expr::Fn(f) => f, + _ => continue, + }; + let body = match &mut factory.function.body { + Some(b) => b, + None => continue, + }; + + // Find the return statement → execute property + let return_stmt = body.stmts.iter_mut().rev().find_map(|s| { + if let Stmt::Return(ret) = s { Some(ret) } else { None } + }); + let return_stmt = match return_stmt { + Some(r) => r, + None => continue, + }; + let return_obj = match &mut return_stmt.arg { + Some(a) => match &mut **a { + Expr::Object(o) => o, + _ => continue, + }, + None => continue, + }; + + // Find execute function + for prop in &mut return_obj.props { + if let PropOrSpread::Prop(p) = prop { + if let Prop::KeyValue(kv) = &mut **p { + if let PropName::Ident(id) = &kv.key { + if id.sym.as_ref() == "execute" { + if let Expr::Fn(f) = &mut *kv.value { + if let Some(exec_body) = &mut f.function.body { + let make_hook = |method: &str| -> Stmt { + Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + ctxt: Default::default(), + callee: Callee::Expr(Box::new(Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + ctxt: Default::default(), + callee: Callee::Expr(Box::new(Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: Box::new(Expr::Ident(Ident::new( + "System".into(), DUMMY_SP, Default::default(), + ))), + prop: MemberProp::Ident(IdentName { + span: DUMMY_SP, sym: "get".into(), + }), + }))), + args: vec![ExprOrSpread { + spread: None, + expr: Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, value: "@lively-env".into(), raw: None, + }))), + }], + type_args: None, + })), + prop: MemberProp::Ident(IdentName { + span: DUMMY_SP, sym: method.into(), + }), + }))), + args: vec![ExprOrSpread { + spread: None, + expr: Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, value: module_id.into(), raw: None, + }))), + }], + type_args: None, + })), + }) + }; + exec_body.stmts.insert(0, make_hook("evaluationStart")); + exec_body.stmts.push(make_hook("evaluationEnd")); + } + } + } + } + } + } + } + break; + } +} + +/// Move `__lvVarRecorder = System.get("@lively-env").moduleEnv(...).recorder` +/// from execute() to the factory body. This matches what Babel's +/// livelyPostTranspile does (lines 1307-1310 of babel/plugin.js). +fn hoist_recorder_init(program: &mut Program, capture_obj: &str) { + for stmt in get_stmts_mut(program) { + let call = match stmt { + Stmt::Expr(ExprStmt { expr, .. }) => match &mut **expr { + Expr::Call(c) => c, + _ => continue, + }, + _ => continue, + }; + + if call.args.len() < 2 { continue; } + let factory = match &mut *call.args[1].expr { + Expr::Fn(f) => f, + _ => continue, + }; + let body = match &mut factory.function.body { + Some(b) => b, + None => continue, + }; + + // Find the return statement to get execute function + let return_idx = body.stmts.iter().position(|s| matches!(s, Stmt::Return(_))); + let return_idx = match return_idx { + Some(i) => i, + None => continue, + }; + + let execute_body = { + let return_stmt = &body.stmts[return_idx]; + let ret_arg = match return_stmt { + Stmt::Return(ReturnStmt { arg: Some(a), .. }) => a, + _ => continue, + }; + let obj = match &**ret_arg { + Expr::Object(o) => o, + _ => continue, + }; + obj.props.iter().find_map(|prop| { + if let PropOrSpread::Prop(p) = prop { + if let Prop::KeyValue(kv) = &**p { + if let PropName::Ident(id) = &kv.key { + if id.sym.as_ref() == "execute" { + // Get the function body + if let Expr::Fn(f) = &*kv.value { + return f.function.body.as_ref().map(|b| b.stmts.clone()); + } + } + } + } + } + None + }) + }; + + let execute_stmts = match execute_body { + Some(s) => s, + None => continue, + }; + + // Find the __lvVarRecorder = ... assignment in execute() + // Pattern: __lvVarRecorder = System.get("@lively-env").moduleEnv(...).recorder + // Note: SWC's system_js may combine assignments into comma expressions (Seq), + // so we also check the first expression in a SeqExpr. + let is_recorder_assign = |expr: &Expr| -> bool { + if let Expr::Assign(AssignExpr { left, .. }) = expr { + if let Some(SimpleAssignTarget::Ident(id)) = left.as_simple() { + return id.sym.as_ref() == capture_obj; + } + } + false + }; + let recorder_idx = execute_stmts.iter().position(|s| { + if let Stmt::Expr(ExprStmt { expr, .. }) = s { + if is_recorder_assign(expr) { return true; } + // Also check first expr in a comma expression + if let Expr::Seq(seq) = &**expr { + if let Some(first) = seq.exprs.first() { + return is_recorder_assign(first); + } + } + } + false + }); + + let recorder_idx = match recorder_idx { + Some(i) => i, + None => continue, + }; + + // Extract the recorder init. It may be a standalone Assign or the first + // expr in a SeqExpr (comma expression). + let orig_stmt = &execute_stmts[recorder_idx]; + let recorder_stmt = if let Stmt::Expr(ExprStmt { expr, .. }) = orig_stmt { + if let Expr::Seq(seq) = &**expr { + // Extract first expression as standalone statement + if let Some(first) = seq.exprs.first() { + Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new((**first).clone()), + }) + } else { + orig_stmt.clone() + } + } else { + orig_stmt.clone() + } + } else { + orig_stmt.clone() + }; + + // Now mutably access execute to remove/modify the statement + let return_stmt = &mut body.stmts[return_idx]; + let ret_arg = match return_stmt { + Stmt::Return(ReturnStmt { arg: Some(a), .. }) => a, + _ => continue, + }; + let obj = match &mut **ret_arg { + Expr::Object(o) => o, + _ => continue, + }; + for prop in &mut obj.props { + if let PropOrSpread::Prop(p) = prop { + if let Prop::KeyValue(kv) = &mut **p { + if let PropName::Ident(id) = &kv.key { + if id.sym.as_ref() == "execute" { + if let Expr::Fn(f) = &mut *kv.value { + if let Some(b) = &mut f.function.body { + // If it was a SeqExpr, remove the first sub-expression + // (keep the rest as a SeqExpr or single expr) + let stmt = &mut b.stmts[recorder_idx]; + if let Stmt::Expr(ExprStmt { expr, .. }) = stmt { + if let Expr::Seq(seq) = &mut **expr { + if seq.exprs.len() > 2 { + seq.exprs.remove(0); + } else if seq.exprs.len() == 2 { + // Convert from Seq([a, b]) to just b + let remaining = seq.exprs.remove(1); + *expr = remaining; + } else { + b.stmts.remove(recorder_idx); + } + } else { + b.stmts.remove(recorder_idx); + } + } + } + } + } + } + } + } + } + + // Insert recorder init in factory body, before the return statement + body.stmts.insert(return_idx, recorder_stmt); + + break; + } +} + +/// Rewrite setters to capture imports to __lvVarRecorder. +/// Matches Babel's livelyPostTranspile (lines 1267-1296 of babel/plugin.js). +/// +/// Input setter: `function(_dep) { X = _dep.X; }` +/// Output setter: `function(_dep = {}) { __lvVarRecorder.X = wrapper("X", "var", X = _dep.X, __lvVarRecorder); }` +/// +/// Without wrapper: `function(_dep = {}) { __lvVarRecorder.X = X = _dep.X; }` +fn rewrite_setters(program: &mut Program, capture_obj: &str, declaration_wrapper: Option<&str>, excluded: &[String]) { + for stmt in get_stmts_mut(program) { + let call = match stmt { + Stmt::Expr(ExprStmt { expr, .. }) => match &mut **expr { + Expr::Call(c) => c, + _ => continue, + }, + _ => continue, + }; + + if call.args.len() < 2 { continue; } + let factory = match &mut *call.args[1].expr { + Expr::Fn(f) => f, + _ => continue, + }; + let body = match &mut factory.function.body { + Some(b) => b, + None => continue, + }; + + // Find the return statement + let return_stmt = body.stmts.iter_mut().rev().find_map(|s| { + if let Stmt::Return(ret) = s { Some(ret) } else { None } + }); + let return_stmt = match return_stmt { + Some(r) => r, + None => continue, + }; + let return_obj = match &mut return_stmt.arg { + Some(a) => match &mut **a { + Expr::Object(o) => o, + _ => continue, + }, + None => continue, + }; + + // Find the setters property + let setters_prop = return_obj.props.iter_mut().find_map(|prop| { + if let PropOrSpread::Prop(p) = prop { + if let Prop::KeyValue(kv) = &mut **p { + if let PropName::Ident(id) = &kv.key { + if id.sym.as_ref() == "setters" { + return Some(&mut kv.value); + } + } + } + } + None + }); + + let setters_arr = match setters_prop { + Some(v) => match &mut **v { + Expr::Array(a) => a, + _ => continue, + }, + None => continue, + }; + + // Rewrite each setter function + for elem in &mut setters_arr.elems { + let setter_fn = match elem { + Some(ExprOrSpread { expr, .. }) => match &mut **expr { + Expr::Fn(f) => f, + _ => continue, + }, + _ => continue, + }; + + // Add default parameter: _dep → _dep = {} + if let Some(param) = setter_fn.function.params.first_mut() { + if let Pat::Ident(id) = ¶m.pat { + let id_clone = id.clone(); + param.pat = Pat::Assign(AssignPat { + span: DUMMY_SP, + left: Box::new(Pat::Ident(id_clone)), + right: Box::new(Expr::Object(ObjectLit { + span: DUMMY_SP, + props: vec![], + })), + }); + } + } + + // Rewrite each statement in the setter body + let setter_body = match &mut setter_fn.function.body { + Some(b) => b, + None => continue, + }; + + let new_stmts: Vec = setter_body.stmts.drain(..).map(|s| { + // Match: X = _dep.X (ExpressionStatement with AssignmentExpression) + let expr_stmt = match &s { + Stmt::Expr(es) => es, + _ => return s, + }; + let assign = match &*expr_stmt.expr { + Expr::Assign(a) => a, + _ => return s, + }; + // LHS must be a simple identifier + let lhs_name = match &assign.left { + AssignTarget::Simple(SimpleAssignTarget::Ident(id)) => { + id.sym.to_string() + } + _ => return s, + }; + + // Skip excluded names + if excluded.contains(&lhs_name) { + return s; + } + + // Build: capture_obj.X = [wrapper(...) | X = _dep.X] + let capture_member = Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: Box::new(Expr::Ident(Ident::new( + capture_obj.into(), DUMMY_SP, Default::default(), + ))), + prop: MemberProp::Ident(IdentName { + span: DUMMY_SP, + sym: lhs_name.as_str().into(), + }), + }); + + // Keep the original assignment (always runs): X = _dep.X + let orig_stmt = s.clone(); + + // Build the recorder capture (guarded): + // if (typeof __rec !== "undefined") __rec.X = [defVar(..., X, __rec) | X] + let value_expr = Expr::Ident(Ident::new( + lhs_name.as_str().into(), DUMMY_SP, Default::default(), + )); + let rhs = if let Some(wrapper) = declaration_wrapper { + Expr::Call(CallExpr { + span: DUMMY_SP, + ctxt: Default::default(), + callee: Callee::Expr(Box::new(Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: Box::new(Expr::Ident(Ident::new( + capture_obj.into(), DUMMY_SP, Default::default(), + ))), + prop: MemberProp::Computed(ComputedPropName { + span: DUMMY_SP, + expr: Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: wrapper.into(), + raw: None, + }))), + }), + }))), + args: vec![ + ExprOrSpread { spread: None, expr: Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, value: lhs_name.as_str().into(), raw: None, + })))}, + ExprOrSpread { spread: None, expr: Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, value: "var".into(), raw: None, + })))}, + ExprOrSpread { spread: None, expr: Box::new(value_expr) }, + ExprOrSpread { spread: None, expr: Box::new(Expr::Ident(Ident::new( + capture_obj.into(), DUMMY_SP, Default::default(), + )))}, + ], + type_args: None, + }) + } else { + value_expr + }; + + let capture_assign = Expr::Assign(AssignExpr { + span: DUMMY_SP, + op: AssignOp::Assign, + left: AssignTarget::Simple(SimpleAssignTarget::Member(MemberExpr { + span: DUMMY_SP, + obj: Box::new(Expr::Ident(Ident::new( + capture_obj.into(), DUMMY_SP, Default::default(), + ))), + prop: MemberProp::Ident(IdentName { + span: DUMMY_SP, + sym: lhs_name.as_str().into(), + }), + })), + right: Box::new(rhs), + }); + + // Two statements: 1) original assignment, 2) guarded recorder capture + // Return a block to hold both. + // We'll flatten this below since we need to return Vec. + // Actually, just return both statements — we'll collect into a Vec. + // For now, combine as: `X = _dep.X; if (typeof __rec !== "undefined") __rec.X = defVar("X", "var", X, __rec);` + // We can't return 2 stmts from a map that expects 1. Use a block: + Stmt::Block(BlockStmt { + span: DUMMY_SP, + ctxt: Default::default(), + stmts: vec![ + orig_stmt, + Stmt::If(IfStmt { + span: DUMMY_SP, + test: Box::new(Expr::Bin(BinExpr { + span: DUMMY_SP, + op: BinaryOp::NotEqEq, + left: Box::new(Expr::Unary(UnaryExpr { + span: DUMMY_SP, + op: UnaryOp::TypeOf, + arg: Box::new(Expr::Ident(Ident::new( + capture_obj.into(), DUMMY_SP, Default::default(), + ))), + })), + right: Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: "undefined".into(), + raw: None, + }))), + })), + cons: Box::new(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(capture_assign), + })), + alt: None, + }), + ], + }) + }).collect(); + + setter_body.stmts = new_stmts; + } + + break; // Only one System.register per module + } +} + +/// Collect all _export("name", ...) call names from an AST subtree. +struct ExportNameCollector { + names: Vec, +} + +impl Visit for ExportNameCollector { + fn visit_call_expr(&mut self, call: &CallExpr) { + // Match _export("name", ...) + if let Callee::Expr(callee) = &call.callee { + if let Expr::Ident(id) = &**callee { + if id.sym.as_ref() == "_export" { + if let Some(first_arg) = call.args.first() { + if let Expr::Lit(Lit::Str(s)) = &*first_arg.expr { + let name = s.value.to_string(); + if !self.names.contains(&name) { + self.names.push(name); + } + } + } + } + } + } + // Continue visiting children + call.visit_children_with(self); + } +} + +/// Walk the System.register output and insert `_export({name: void 0, ...})` +/// in the factory body before `return { setters, execute }`. +fn insert_early_exports(program: &mut Program) { + // Find the System.register(..., function(_export, _context) { ... }) call + for stmt in get_stmts_mut(program) { + let call = match stmt { + Stmt::Expr(ExprStmt { expr, .. }) => match &mut **expr { + Expr::Call(c) => c, + _ => continue, + }, + _ => continue, + }; + + // The second argument is the factory function + if call.args.len() < 2 { continue; } + let factory = match &mut *call.args[1].expr { + Expr::Fn(f) => f, + _ => continue, + }; + let body = match &mut factory.function.body { + Some(b) => b, + None => continue, + }; + + // Find the execute function inside `return { setters: [...], execute: fn }` + // The return statement is typically the last statement in the factory body. + let return_stmt = body.stmts.iter().rev().find_map(|s| { + if let Stmt::Return(ret) = s { + ret.arg.as_ref() + } else { + None + } + }); + + let return_obj = match return_stmt { + Some(expr) => match &**expr { + Expr::Object(obj) => obj, + _ => continue, + }, + None => continue, + }; + + // Find the execute property + let execute_fn = return_obj.props.iter().find_map(|prop| { + if let PropOrSpread::Prop(p) = prop { + if let Prop::KeyValue(kv) = &**p { + if let PropName::Ident(id) = &kv.key { + if id.sym.as_ref() == "execute" { + return Some(&kv.value); + } + } + } + } + None + }); + + let execute_fn = match execute_fn { + Some(f) => f, + None => continue, + }; + + // Collect _export("name", ...) calls from inside execute + let mut collector = ExportNameCollector { names: vec![] }; + execute_fn.visit_with(&mut collector); + + if collector.names.is_empty() { continue; } + + // Build: _export({"name1": void 0, "name2": void 0, ...}) + let props: Vec = collector.names.iter().map(|name| { + PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::Str(Str { + span: DUMMY_SP, + value: name.as_str().into(), + raw: None, + }), + value: Box::new(Expr::Unary(UnaryExpr { + span: DUMMY_SP, + op: UnaryOp::Void, + arg: Box::new(Expr::Lit(Lit::Num(Number { + span: DUMMY_SP, + value: 0.0, + raw: None, + }))), + })), + }))) + }).collect(); + + let bulk_export = Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + ctxt: Default::default(), + callee: Callee::Expr(Box::new(Expr::Ident(Ident::new( + "_export".into(), DUMMY_SP, Default::default(), + )))), + args: vec![ExprOrSpread { + spread: None, + expr: Box::new(Expr::Object(ObjectLit { + span: DUMMY_SP, + props, + })), + }], + type_args: None, + })), + }); + + // Insert before the last statement (the return) in the factory body + let insert_pos = body.stmts.len().saturating_sub(1); + body.stmts.insert(insert_pos, bulk_export); + + break; // Only one System.register per module + } +} + +fn finish_output(cm: Lrc, src_buf: Vec, src_map_buf: Vec<(swc_common::BytePos, swc_common::LineCol)>) -> Result { + let code = String::from_utf8(src_buf) + .map_err(|e| JsError::new(&format!("UTF-8 error: {}", e)))?; + + let mut src_map = vec![]; + cm.build_source_map_from(&src_map_buf, None) + .to_writer(&mut src_map) + .map_err(|e| JsError::new(&format!("Source map error: {}", e)))?; + + let map = String::from_utf8(src_map) + .map_err(|e| JsError::new(&format!("Source map UTF-8 error: {}", e)))?; + + Ok(serde_json::json!({ + "code": code, + "map": map, + }).to_string()) +} + +/// Returns the version of the transforms library. +#[wasm_bindgen] +pub fn version() -> String { + env!("CARGO_PKG_VERSION").to_string() +} diff --git a/lively.freezer/swc-plugin/lively-swc-plugin/Cargo.toml b/lively.freezer/swc-plugin/lively-swc-plugin/Cargo.toml new file mode 100644 index 0000000000..f10a85c4c6 --- /dev/null +++ b/lively.freezer/swc-plugin/lively-swc-plugin/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "lively-swc-plugin" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +lively-swc-transforms = { path = "../lively-swc-transforms" } +swc_core = { version = "9.0.0", features = [ + "ecma_plugin_transform", + "ecma_visit", + "ecma_utils", + "ecma_ast", +] } +swc_plugin_macro = "=1.0.0" +serde_json = "1" diff --git a/lively.freezer/swc-plugin/lively-swc-plugin/src/lib.rs b/lively.freezer/swc-plugin/lively-swc-plugin/src/lib.rs new file mode 100644 index 0000000000..3fb84b1d9b --- /dev/null +++ b/lively.freezer/swc-plugin/lively-swc-plugin/src/lib.rs @@ -0,0 +1,23 @@ +use swc_core::ecma::{ + ast::Program, + visit::VisitMutWith, +}; +use swc_core::plugin::{plugin_transform, proxies::TransformPluginProgramMetadata}; + +use lively_swc_transforms::LivelyTransformVisitor; +use lively_swc_transforms::config::LivelyTransformConfig; + +/// SWC plugin entry point — thin wrapper around shared transform library. +#[plugin_transform] +pub fn process_transform(mut program: Program, metadata: TransformPluginProgramMetadata) -> Program { + let config = serde_json::from_str::( + &metadata + .get_transform_plugin_config() + .expect("Failed to get plugin config"), + ) + .unwrap_or_default(); + + let mut visitor = LivelyTransformVisitor::new(config); + program.visit_mut_with(&mut visitor); + program +} diff --git a/lively.freezer/swc-plugin/lively-swc-transforms/Cargo.toml b/lively.freezer/swc-plugin/lively-swc-transforms/Cargo.toml new file mode 100644 index 0000000000..b418b7b3b0 --- /dev/null +++ b/lively.freezer/swc-plugin/lively-swc-transforms/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "lively-swc-transforms" +version = "0.1.0" +edition = "2021" + +[dependencies] +swc_ecma_ast = "=5.0.1" +swc_ecma_visit = "=5.0.0" +swc_common = { version = "=5.0.1", features = ["sourcemap"] } +swc_ecma_parser = "=6.0.2" +serde = { version = "=1.0.219", features = ["derive"] } +serde_json = "1" + +[dev-dependencies] +swc_ecma_codegen = "=5.0.1" diff --git a/lively.freezer/swc-plugin/src/config.rs b/lively.freezer/swc-plugin/lively-swc-transforms/src/config.rs similarity index 84% rename from lively.freezer/swc-plugin/src/config.rs rename to lively.freezer/swc-plugin/lively-swc-transforms/src/config.rs index 82679177ac..ecb7d5c6bb 100644 --- a/lively.freezer/swc-plugin/src/config.rs +++ b/lively.freezer/swc-plugin/lively-swc-transforms/src/config.rs @@ -24,6 +24,13 @@ pub struct LivelyTransformConfig { #[serde(default = "default_true")] pub capture_imports: bool, + /// Whether scope capture should rewrite mixed default + named imports + /// through a temporary default binding. This is needed for SWC's SystemJS + /// module transform, but Rollup freezer builds must keep the original + /// import binding graph intact. + #[serde(default = "default_true")] + pub rewrite_mixed_default_imports: bool, + /// Whether this is a resurrection build (enables special transforms) #[serde(default)] pub resurrection: bool, @@ -36,6 +43,14 @@ pub struct LivelyTransformConfig { #[serde(default)] pub current_module_accessor: Option, + /// Identifier used for the embedded original module source. + #[serde(default)] + pub source_accessor_name: Option, + + /// Original module source, embedded into source metadata when present. + #[serde(default)] + pub original_source: Option, + /// Package name for class metadata #[serde(default)] pub package_name: Option, @@ -91,6 +106,10 @@ pub struct ClassToFunctionConfig { /// Expression to access current module metadata pub current_module_accessor: String, + + /// Identifier used for the embedded original module source. + #[serde(default)] + pub source_accessor_name: Option, } impl Default for LivelyTransformConfig { @@ -105,6 +124,9 @@ impl Default for LivelyTransformConfig { "window".to_string(), "document".to_string(), "global".to_string(), + "globalThis".to_string(), + "self".to_string(), + "lively".to_string(), "process".to_string(), "Buffer".to_string(), "System".to_string(), @@ -134,9 +156,12 @@ impl Default for LivelyTransformConfig { "Infinity".to_string(), ], capture_imports: true, + rewrite_mixed_default_imports: true, resurrection: false, module_id: String::new(), current_module_accessor: None, + source_accessor_name: None, + original_source: None, package_name: None, package_version: None, enable_component_transform: true, diff --git a/lively.freezer/swc-plugin/src/lib.rs b/lively.freezer/swc-plugin/lively-swc-transforms/src/lib.rs similarity index 56% rename from lively.freezer/swc-plugin/src/lib.rs rename to lively.freezer/swc-plugin/lively-swc-transforms/src/lib.rs index e36688e6ba..c8e022f633 100644 --- a/lively.freezer/swc-plugin/src/lib.rs +++ b/lively.freezer/swc-plugin/lively-swc-transforms/src/lib.rs @@ -1,33 +1,13 @@ -use swc_core::ecma::{ - ast::Program, - visit::VisitMutWith, -}; -use swc_core::plugin::{plugin_transform, proxies::TransformPluginProgramMetadata}; - -mod config; -mod transforms; -mod utils; +pub mod config; +pub mod transforms; +pub mod utils; use config::LivelyTransformConfig; +use swc_ecma_visit::VisitMutWith; use transforms::*; -/// Main plugin entry point -#[plugin_transform] -pub fn process_transform(mut program: Program, metadata: TransformPluginProgramMetadata) -> Program { - // Avoid binding conflicts with swc_core 9.x by not importing PluginDiagnosticsEmitter. - let config = serde_json::from_str::( - &metadata - .get_transform_plugin_config() - .expect("Failed to get plugin config"), - ) - .unwrap_or_default(); - - let mut visitor = LivelyTransformVisitor::new(config); - program.visit_mut_with(&mut visitor); - program -} - -/// Main transform visitor that orchestrates all lively transforms +/// Main transform visitor that orchestrates all lively transforms. +/// Shared between the SWC plugin (wasm32-wasip1) and the browser WASM module. pub struct LivelyTransformVisitor { config: LivelyTransformConfig, } @@ -38,8 +18,8 @@ impl LivelyTransformVisitor { } } -impl swc_core::ecma::visit::VisitMut for LivelyTransformVisitor { - fn visit_mut_program(&mut self, program: &mut Program) { +impl swc_ecma_visit::VisitMut for LivelyTransformVisitor { + fn visit_mut_program(&mut self, program: &mut swc_ecma_ast::Program) { // Apply transforms in the correct order: // 1. Split export variable declarations first (preprocessing) @@ -74,7 +54,10 @@ impl swc_core::ecma::visit::VisitMut for LivelyTransformVisitor { // Collects synthetic identifiers to exclude from scope capture. let mut namespace_excludes = Vec::new(); if self.config.enable_namespace_transform && self.config.resurrection { - let mut namespace_transform = NamespaceTransform::new(self.config.resolved_imports.clone(), self.config.capture_obj.clone()); + let mut namespace_transform = NamespaceTransform::new( + self.config.resolved_imports.clone(), + self.config.capture_obj.clone(), + ); program.visit_mut_with(&mut namespace_transform); namespace_excludes.extend(namespace_transform.added_excludes().iter().cloned()); } @@ -104,9 +87,12 @@ impl swc_core::ecma::visit::VisitMut for LivelyTransformVisitor { self.config.declaration_wrapper.clone(), exclude, self.config.capture_imports, + self.config.rewrite_mixed_default_imports, self.config.resurrection, self.config.module_id.clone(), self.config.current_module_accessor.clone(), + self.config.source_accessor_name.clone(), + self.config.original_source.clone(), self.config.module_hash, self.config.resolved_imports.clone(), ); @@ -116,9 +102,8 @@ impl swc_core::ecma::visit::VisitMut for LivelyTransformVisitor { // 8. Capture exported imports (after scope capture). // Babel runs insertCapturesForExportedImports for all captureModuleScope builds. if self.config.enable_scope_capture { - let mut exported_import_capture = ExportedImportCapturePass::new( - self.config.capture_obj.clone(), - ); + let mut exported_import_capture = + ExportedImportCapturePass::new(self.config.capture_obj.clone()); program.visit_mut_with(&mut exported_import_capture); } } @@ -127,15 +112,14 @@ impl swc_core::ecma::visit::VisitMut for LivelyTransformVisitor { #[cfg(test)] mod tests { use super::*; - use swc_core::common::{sync::Lrc, SourceMap, FileName}; + use swc_common::{sync::Lrc, FileName, SourceMap}; fn transform_code(code: &str, config: LivelyTransformConfig) -> String { let cm = Lrc::new(SourceMap::default()); let fm = cm.new_source_file(FileName::Anon.into(), code.to_string()); - // Parse the code - use swc_core::ecma::parser::{parse_file_as_module, Syntax}; - let mut program = parse_file_as_module( + use swc_ecma_parser::{parse_file_as_module, Syntax}; + let module = parse_file_as_module( &fm, Syntax::Es(Default::default()), Default::default(), @@ -144,17 +128,16 @@ mod tests { ) .unwrap(); - // Transform (wrap Module in Program for visit_mut_program) - let mut prog = Program::Module(program); + let mut program = swc_ecma_ast::Program::Module(module); let mut visitor = LivelyTransformVisitor::new(config); - prog.visit_mut_with(&mut visitor); - let program = match prog { - Program::Module(m) => m, - _ => unreachable!(), + program.visit_mut_with(&mut visitor); + + let module = match &program { + swc_ecma_ast::Program::Module(m) => m, + _ => panic!("Expected module"), }; - // Generate code - use swc_core::ecma::codegen::{text_writer::JsWriter, Emitter, Config}; + use swc_ecma_codegen::{text_writer::JsWriter, Config, Emitter}; let mut buf = vec![]; { let mut emitter = Emitter { @@ -163,8 +146,7 @@ mod tests { comments: None, wr: JsWriter::new(cm, "\n", &mut buf, None), }; - - emitter.emit_module(&program).unwrap(); + emitter.emit_module(module).unwrap(); } String::from_utf8(buf).unwrap() @@ -174,16 +156,21 @@ mod tests { fn test_basic_var_capture() { let input = "var x = 1; x + 2;"; let output = transform_code(input, LivelyTransformConfig::default()); - assert!(output.contains("__varRecorder__"), "actual output:\n{}", output); + assert!( + output.contains("__varRecorder__"), + "actual output:\n{}", + output + ); } #[test] fn test_export_class_is_captured_after_class_transform() { let mut config = LivelyTransformConfig::default(); - config.class_to_function = Some(crate::config::ClassToFunctionConfig { + config.class_to_function = Some(config::ClassToFunctionConfig { class_holder: "__varRecorder__".to_string(), function_node: "initializeES6ClassForLively".to_string(), current_module_accessor: "module.id".to_string(), + source_accessor_name: None, }); let input = "export class Color {}"; let output = transform_code(input, config); @@ -204,6 +191,7 @@ mod tests { class_holder: "__varRecorder__".to_string(), function_node: "initializeES6ClassForLively".to_string(), current_module_accessor: "module.id".to_string(), + source_accessor_name: None, }); config } @@ -217,6 +205,33 @@ mod tests { config } + #[test] + fn test_full_pipeline_can_keep_mixed_default_import_binding() { + let mut config = LivelyTransformConfig::default(); + config.capture_imports = true; + config.rewrite_mixed_default_imports = false; + let input = r#"import KeyHandler, { findKeysForPlatform } from "./events/KeyHandler.js"; +export function useKeyHandler() { + return KeyHandler.withBindings(findKeysForPlatform([])); +}"#; + let output = transform_code(input, config); + assert!( + !output.contains("__default_KeyHandler__"), + "does not rewrite the default import through a temp binding: {}", + output + ); + assert!( + output.contains("__varRecorder__.KeyHandler = KeyHandler"), + "still captures the imported default binding: {}", + output + ); + assert!( + output.contains("KeyHandler.withBindings"), + "ordinary uses keep the local import binding for Rollup: {}", + output + ); + } + // --- Tests for class-to-function + scope capture + export interaction --- // These reproduce the eval-strategies.js failure where rollup says // "Exported variable X is not defined" because the scope capture removes @@ -229,10 +244,17 @@ mod tests { let input = "class Foo { eval() { return 1; } }\nexport { Foo };"; let output = transform_code(input, config_with_class_to_function()); // Foo must still be declared (as var) so rollup can resolve export { Foo } - assert!(output.contains("export {"), "keeps export statement: {}", output); assert!( - output.contains("var Foo") || output.contains("let Foo") || output.contains("const Foo"), - "Foo must have a local declaration for rollup: {}", output + output.contains("export {"), + "keeps export statement: {}", + output + ); + assert!( + output.contains("var Foo") + || output.contains("let Foo") + || output.contains("const Foo"), + "Foo must have a local declaration for rollup: {}", + output ); } @@ -249,11 +271,13 @@ export { EvalStrategy, SimpleEvalStrategy }; // Both names must be locally declared assert!( output.contains("var EvalStrategy") || output.contains("EvalStrategy ="), - "EvalStrategy must be declared: {}", output + "EvalStrategy must be declared: {}", + output ); assert!( output.contains("var SimpleEvalStrategy") || output.contains("SimpleEvalStrategy ="), - "SimpleEvalStrategy must be declared: {}", output + "SimpleEvalStrategy must be declared: {}", + output ); } @@ -274,7 +298,11 @@ export { EvalStrategy, SimpleEvalStrategy }; let output = transform_code(input, LivelyTransformConfig::default()); assert!(output.contains("export {"), "keeps export: {}", output); // foo must be declared (function declaration survives) - assert!(output.contains("function foo"), "foo must be declared: {}", output); + assert!( + output.contains("function foo"), + "foo must be declared: {}", + output + ); } #[test] @@ -286,7 +314,8 @@ export { EvalStrategy, SimpleEvalStrategy }; // Must have either export { Foo } or export var Foo assert!( output.contains("export") && output.contains("Foo"), - "Foo must be exported: {}", output + "Foo must be exported: {}", + output ); } @@ -299,8 +328,12 @@ export { EvalStrategy, SimpleEvalStrategy }; let output = transform_code(input, config_resurrection_with_class_to_function()); assert!(output.contains("export {"), "keeps export: {}", output); assert!( - output.contains("var Foo") || output.contains("let Foo") || output.contains("const Foo") || output.contains("class Foo"), - "Foo must have a local declaration for rollup: {}", output + output.contains("var Foo") + || output.contains("let Foo") + || output.contains("const Foo") + || output.contains("class Foo"), + "Foo must have a local declaration for rollup: {}", + output ); } @@ -315,11 +348,13 @@ export { EvalStrategy, SimpleEvalStrategy }; assert!(output.contains("export {"), "keeps export: {}", output); assert!( output.contains("var EvalStrategy") || output.contains("EvalStrategy ="), - "EvalStrategy declared: {}", output + "EvalStrategy declared: {}", + output ); assert!( output.contains("var SimpleEvalStrategy") || output.contains("SimpleEvalStrategy ="), - "SimpleEvalStrategy declared: {}", output + "SimpleEvalStrategy declared: {}", + output ); } @@ -344,8 +379,10 @@ export { EvalStrategy, SimpleEvalStrategy }; fn config_resurrection_with_wrapper() -> LivelyTransformConfig { let mut config = config_resurrection_with_class_to_function(); - config.declaration_wrapper = Some(r#"__varRecorder__["test-module.js__define__"]"#.to_string()); - config.current_module_accessor = Some(r#"({ pathInPackage: () => "test-module.js" })"#.to_string()); + config.declaration_wrapper = + Some(r#"__varRecorder__["test-module.js__define__"]"#.to_string()); + config.current_module_accessor = + Some(r#"({ pathInPackage: () => "test-module.js" })"#.to_string()); config } @@ -354,14 +391,44 @@ export { EvalStrategy, SimpleEvalStrategy }; let input = "function greet() { return 'hello'; }"; let output = transform_code(input, config_resurrection_with_wrapper()); // Function declaration should be replaced with var + wrapper - assert!(output.contains("var greet"), "func replaced with var: {}", output); - assert!(!output.contains("function greet()"), "original func decl removed: {}", output); + assert!( + output.contains("var greet"), + "func replaced with var: {}", + output + ); + assert!( + !output.contains("function greet()"), + "original func decl removed: {}", + output + ); // Wrapper should use __moduleMeta__ - assert!(output.contains("__moduleMeta__"), "wrapper uses __moduleMeta__: {}", output); + assert!( + output.contains("__moduleMeta__"), + "wrapper uses __moduleMeta__: {}", + output + ); // __moduleMeta__ should be declared - assert!(output.contains("var __moduleMeta__"), "moduleMeta declared: {}", output); + assert!( + output.contains("var __moduleMeta__"), + "moduleMeta declared: {}", + output + ); // Wrapper should be a computed member expression - assert!(output.contains(r#"__define__"#), "wrapper is recorder define method: {}", output); + assert!( + output.contains(r#"__define__"#), + "wrapper is recorder define method: {}", + output + ); + assert!( + output.contains(r#"__varRecorder__["test-module.js__define__"]("greet""#), + "wrapper expression should be used directly as callee: {}", + output + ); + assert!( + !output.contains(r#"__varRecorder__["__varRecorder__["#), + "wrapper expression must not be treated as a property key: {}", + output + ); } // --- Full pipeline: named namespace re-export (Divergence 2) --- @@ -371,9 +438,92 @@ export { EvalStrategy, SimpleEvalStrategy }; let input = r#"export * as utils from './utils.js';"#; let output = transform_code(input, config_resurrection_with_class_to_function()); // Should transform into import + const + export - assert!(output.contains("utils_namespace"), "creates namespace import: {}", output); - assert!(output.contains("exportsOf"), "creates exportsOf fallback: {}", output); - assert!(output.contains("export {"), "creates named export: {}", output); + assert!( + output.contains("utils_namespace"), + "creates namespace import: {}", + output + ); + assert!( + output.contains("exportsOf"), + "creates exportsOf fallback: {}", + output + ); + assert!( + output.contains("export {"), + "creates named export: {}", + output + ); + } + + #[test] + fn test_full_pipeline_namespace_runtime_keeps_lively_global() { + let input = r#"export * as utils from './utils.js';"#; + let output = transform_code(input, config_resurrection_with_class_to_function()); + assert!( + output.contains("lively.FreezerRuntime") || output.contains("lively.frozenModules"), + "runtime lookup should use global lively: {}", + output + ); + assert!( + !output.contains("__varRecorder__.lively"), + "runtime lookup must not capture lively into the recorder: {}", + output + ); + } + + #[test] + fn test_full_pipeline_class_holder_runtime_keeps_lively_global() { + let mut config = LivelyTransformConfig::default(); + config.resurrection = true; + config.module_id = "test-module.js".to_string(); + config.capture_imports = false; + config.class_to_function = Some(crate::config::ClassToFunctionConfig { + class_holder: + r#"((lively.FreezerRuntime || lively.frozenModules).recorderFor("test-module.js", __contextModule__))"# + .to_string(), + function_node: "initializeES6ClassForLively".to_string(), + current_module_accessor: r#"({ pathInPackage: () => "test-module.js" })"#.to_string(), + source_accessor_name: None, + }); + let output = transform_code("class Foo {}", config); + assert!( + output.contains("lively.FreezerRuntime") || output.contains("lively.frozenModules"), + "class holder runtime lookup should use global lively: {}", + output + ); + assert!( + !output.contains("__varRecorder__.lively"), + "class holder runtime lookup must not capture lively into the recorder: {}", + output + ); + } + + #[test] + fn test_full_pipeline_class_runtime_import_stays_local_without_import_capture() { + let mut config = LivelyTransformConfig::default(); + config.module_id = "test-module.js".to_string(); + config.capture_imports = false; + config.class_to_function = Some(crate::config::ClassToFunctionConfig { + class_holder: + r#"((lively.FreezerRuntime || lively.frozenModules).recorderFor("test-module.js", __contextModule__))"# + .to_string(), + function_node: "initializeES6ClassForLively".to_string(), + current_module_accessor: r#"({ pathInPackage: () => "test-module.js" })"#.to_string(), + source_accessor_name: None, + }); + let input = r#"import { initializeClass as initializeES6ClassForLively } from "lively.classes/runtime.js"; +export class Foo {}"#; + let output = transform_code(input, config); + assert!( + output.contains("initializeES6ClassForLively("), + "class runtime should be called through the local import: {}", + output + ); + assert!( + !output.contains("__varRecorder__.initializeES6ClassForLively("), + "class runtime import must not be rewritten through the recorder: {}", + output + ); } #[test] @@ -381,8 +531,16 @@ export { EvalStrategy, SimpleEvalStrategy }; let input = r#"export * from './utils.js';"#; let output = transform_code(input, config_resurrection_with_class_to_function()); // Unnamed export * should still use recorderFor + Object.assign pattern - assert!(output.contains("recorderFor"), "uses recorderFor: {}", output); - assert!(output.contains("Object.assign"), "uses Object.assign: {}", output); + assert!( + output.contains("recorderFor"), + "uses recorderFor: {}", + output + ); + assert!( + output.contains("Object.assign"), + "uses Object.assign: {}", + output + ); } // --- Divergence S: ExportedImportCapturePass runs for all scope capture --- @@ -395,7 +553,35 @@ export { EvalStrategy, SimpleEvalStrategy }; let input = r#"export { name1, name2 } from "foo";"#; let output = transform_code(input, LivelyTransformConfig::default()); // Should have import + captures from ExportedImportCapturePass - assert!(output.contains("__varRecorder__.name1") || output.contains("__varRecorder__.name2"), - "non-resurrection export-from should get captures: {}", output); + assert!( + output.contains("__varRecorder__.name1") || output.contains("__varRecorder__.name2"), + "non-resurrection export-from should get captures: {}", + output + ); + } + + #[test] + fn test_declaration_wrapper_uses_computed_member() { + // With declaration_wrapper set, function declarations get wrapped with the wrapper + // as a direct function call, passing (name, kind, value, captureObj) as args. + // Variable declarations do NOT get __define__ wrapping. + let mut config = LivelyTransformConfig::default(); + config.capture_obj = "__lvVarRecorder".to_string(); + config.declaration_wrapper = Some("defVar_http://localhost:9011/test.js".to_string()); + config.module_id = "http://localhost:9011/test.js".to_string(); + let input = "function createLivelyLangObject() { return 1; }"; + let output = transform_code(input, config); + // Function declarations are wrapped: captureObj[wrapper]("name", "function", ident, captureObj) + assert!( + output.contains(r#"__lvVarRecorder["defVar_http://localhost:9011/test.js"]("createLivelyLangObject", "function", createLivelyLangObject, __lvVarRecorder)"#), + "Expected declaration wrapper call with __lvVarRecorder as 4th arg but got:\n{}", + output + ); + // The capture assignment should use the wrapper result + assert!( + output.contains("__lvVarRecorder.createLivelyLangObject ="), + "Expected recorder capture assignment but got:\n{}", + output + ); } } diff --git a/lively.freezer/swc-plugin/src/transforms/class_transform.rs b/lively.freezer/swc-plugin/lively-swc-transforms/src/transforms/class_transform.rs similarity index 75% rename from lively.freezer/swc-plugin/src/transforms/class_transform.rs rename to lively.freezer/swc-plugin/lively-swc-transforms/src/transforms/class_transform.rs index 3d835c3f60..a36df519c7 100644 --- a/lively.freezer/swc-plugin/src/transforms/class_transform.rs +++ b/lively.freezer/swc-plugin/lively-swc-transforms/src/transforms/class_transform.rs @@ -1,12 +1,15 @@ -use swc_core::common::{SyntaxContext, DUMMY_SP}; -use swc_core::ecma::{ - ast::*, - visit::{VisitMut, VisitMutWith}, -}; +use swc_common::{sync::Lrc, FileName, SourceMap as SwcSourceMap, SyntaxContext, DUMMY_SP}; +use swc_ecma_ast::*; +use swc_ecma_parser::{parse_file_as_expr, Syntax}; +use swc_ecma_visit::{VisitMut, VisitMutWith}; use crate::config::ClassToFunctionConfig; use crate::utils::ast_helpers::*; +fn create_this_expr() -> Expr { + Expr::This(ThisExpr { span: DUMMY_SP }) +} + /// Transform ES6 classes to lively's class system /// /// Transforms: @@ -32,6 +35,10 @@ pub struct ClassTransform { _package_name: Option, _package_version: Option, default_class_counter: usize, + /// Depth > 0 means we're inside a function body. Babel still transforms + /// nested classes, but uses a local class holder and does not subscribe to + /// top-level superclass binding changes there. + fn_depth: u32, } struct SuperRewriter { @@ -41,6 +48,20 @@ struct SuperRewriter { saw_direct_super_call: bool, } +fn parse_config_expr(source: &str) -> Expr { + let cm = Lrc::new(SwcSourceMap::default()); + let fm = cm.new_source_file(FileName::Anon.into(), source.to_string()); + parse_file_as_expr( + &fm, + Syntax::Es(Default::default()), + Default::default(), + None, + &mut vec![], + ) + .map(|expr| *expr) + .unwrap_or_else(|_| parse_expr_or_ident(source)) +} + impl SuperRewriter { fn new(class_name: &str, function_node: &str, is_static: bool) -> Self { Self { @@ -76,10 +97,7 @@ impl SuperRewriter { } fn private_name_to_ident_name(private_name: &PrivateName) -> IdentName { - IdentName::new( - format!("_{}", private_name.name.as_ref()).into(), - DUMMY_SP, - ) + IdentName::new(format!("_{}", private_name.name.as_ref()).into(), DUMMY_SP) } fn replace_super_get(&self, prop: Expr) -> Expr { @@ -88,7 +106,7 @@ impl SuperRewriter { vec![ to_expr_or_spread(self.super_holder_expr()), to_expr_or_spread(prop), - to_expr_or_spread(create_ident_expr("this")), + to_expr_or_spread(create_this_expr()), ], ) } @@ -100,22 +118,19 @@ impl SuperRewriter { to_expr_or_spread(self.super_holder_expr()), to_expr_or_spread(prop), to_expr_or_spread(value), - to_expr_or_spread(create_ident_expr("this")), + to_expr_or_spread(create_this_expr()), ], ) } fn replace_super_method_call(&self, prop: Expr, args: Vec) -> Expr { let get_call = self.replace_super_get(prop); - create_call_expr( - create_member_expr(get_call, "call"), - { - let mut call_args = Vec::with_capacity(args.len() + 1); - call_args.push(to_expr_or_spread(create_ident_expr("this"))); - call_args.extend(args); - call_args - }, - ) + create_call_expr(create_member_expr(get_call, "call"), { + let mut call_args = Vec::with_capacity(args.len() + 1); + call_args.push(to_expr_or_spread(create_this_expr())); + call_args.extend(args); + call_args + }) } fn replace_direct_super_call(&mut self, args: Vec) -> Expr { @@ -124,19 +139,21 @@ impl SuperRewriter { // initializer symbol on the superclass chain. let init_symbol = create_call_expr( create_member_expr(create_ident_expr("Symbol"), "for"), - vec![to_expr_or_spread(create_string_expr("lively-instance-initialize"))], + vec![to_expr_or_spread(create_string_expr( + "lively-instance-initialize", + ))], ); let get_call = create_call_expr( create_member_expr(self.function_node_expr(), "_get"), vec![ to_expr_or_spread(self.super_holder_expr()), to_expr_or_spread(init_symbol), - to_expr_or_spread(create_ident_expr("this")), + to_expr_or_spread(create_this_expr()), ], ); let call_expr = create_call_expr(create_member_expr(get_call, "call"), { let mut call_args = Vec::with_capacity(args.len() + 1); - call_args.push(to_expr_or_spread(create_ident_expr("this"))); + call_args.push(to_expr_or_spread(create_this_expr())); call_args.extend(args); call_args }); @@ -147,25 +164,24 @@ impl SuperRewriter { impl VisitMut for SuperRewriter { fn visit_mut_expr(&mut self, expr: &mut Expr) { match expr { - Expr::Call(call) => { - match &mut call.callee { - Callee::Super(_) => { - let new_expr = self.replace_direct_super_call(call.args.clone()); - *expr = new_expr; - return; - } - Callee::Expr(callee_expr) => { - if let Expr::SuperProp(super_prop) = &mut **callee_expr { - if let Some(prop_expr) = self.super_prop_expr(&super_prop.prop) { - let new_expr = self.replace_super_method_call(prop_expr, call.args.clone()); - *expr = new_expr; - return; - } + Expr::Call(call) => match &mut call.callee { + Callee::Super(_) => { + let new_expr = self.replace_direct_super_call(call.args.clone()); + *expr = new_expr; + return; + } + Callee::Expr(callee_expr) => { + if let Expr::SuperProp(super_prop) = &mut **callee_expr { + if let Some(prop_expr) = self.super_prop_expr(&super_prop.prop) { + let new_expr = + self.replace_super_method_call(prop_expr, call.args.clone()); + *expr = new_expr; + return; } } - Callee::Import(_) => {} } - } + Callee::Import(_) => {} + }, Expr::SuperProp(super_prop) => { if let Some(prop_expr) = self.super_prop_expr(&super_prop.prop) { *expr = self.replace_super_get(prop_expr); @@ -173,7 +189,9 @@ impl VisitMut for SuperRewriter { } } Expr::Assign(assign) => { - if let AssignTarget::Simple(SimpleAssignTarget::SuperProp(super_prop)) = &assign.left { + if let AssignTarget::Simple(SimpleAssignTarget::SuperProp(super_prop)) = + &assign.left + { if let Some(prop_expr) = self.super_prop_expr(&super_prop.prop) { let new_expr = self.replace_super_set(prop_expr, *assign.right.clone()); *expr = new_expr; @@ -216,6 +234,7 @@ impl ClassTransform { _package_name: package_name, _package_version: package_version, default_class_counter: 0, + fn_depth: 0, } } @@ -224,7 +243,12 @@ impl ClassTransform { } /// Transform a class into an initializeES6ClassForLively call - fn transform_class(&self, class_ident: Option<&Ident>, class: &Class, use_class_holder: bool) -> Expr { + fn transform_class( + &self, + class_ident: Option<&Ident>, + class: &Class, + use_class_holder: bool, + ) -> Expr { let class_name_for_methods = class_ident .map(|id| id.sym.as_ref()) .unwrap_or("anonymous_class"); @@ -235,11 +259,18 @@ impl ClassTransform { // 2. Add `{ referencedAs, value }` spec for simple identifier superclasses let superclass_spec = match &class.super_class { Some(super_expr) => { - if let Expr::Ident(super_ident) = &**super_expr { - create_object_lit(vec![ - create_prop("referencedAs", create_string_expr(super_ident.sym.as_ref())), - create_prop("value", *super_expr.clone()), - ]) + if self.fn_depth == 0 { + if let Expr::Ident(super_ident) = &**super_expr { + create_object_lit(vec![ + create_prop( + "referencedAs", + create_string_expr(super_ident.sym.as_ref()), + ), + create_prop("value", *super_expr.clone()), + ]) + } else { + *super_expr.clone() + } } else { *super_expr.clone() } @@ -253,14 +284,23 @@ impl ClassTransform { let init_fn = parse_expr_or_ident(&self.config.function_node); let class_holder_expr = if use_class_holder { - parse_expr_or_ident(&self.config.class_holder) + if self.fn_depth == 0 { + parse_config_expr(&self.config.class_holder) + } else { + create_object_lit(vec![]) + } } else { create_object_lit(vec![]) }; - let current_module_accessor = parse_expr_or_ident(&self.config.current_module_accessor); + let current_module_accessor = parse_config_expr(&self.config.current_module_accessor); - let class_holder_ident = Ident::new("__lively_classholder__".into(), DUMMY_SP, SyntaxContext::empty()); - let lively_class_ident = Ident::new("__lively_class__".into(), DUMMY_SP, SyntaxContext::empty()); + let class_holder_ident = Ident::new( + "__lively_classholder__".into(), + DUMMY_SP, + SyntaxContext::empty(), + ); + let lively_class_ident = + Ident::new("__lively_class__".into(), DUMMY_SP, SyntaxContext::empty()); let class_holder_ref = Expr::Ident(class_holder_ident.clone()); let class_ref = Expr::Ident(lively_class_ident.clone()); @@ -331,16 +371,19 @@ impl ClassTransform { )), right: Box::new(create_call_expr( create_member_expr(create_ident_expr("Object"), "isFrozen"), - vec![to_expr_or_spread(create_member_expr(class_ref.clone(), "prototype"))], + vec![to_expr_or_spread(create_member_expr( + class_ref.clone(), + "prototype", + ))], )), }); - let source_loc = create_object_lit(vec![ + let mut source_loc_props = vec![ create_prop( "start", Expr::Lit(Lit::Num(Number { span: DUMMY_SP, - value: class.span.lo.0 as f64, + value: class.span.lo.0.saturating_sub(1) as f64, raw: None, })), ), @@ -348,11 +391,18 @@ impl ClassTransform { "end", Expr::Lit(Lit::Num(Number { span: DUMMY_SP, - value: class.span.hi.0 as f64, + value: class.span.hi.0.saturating_sub(1) as f64, raw: None, })), ), - ]); + ]; + if let Some(source_accessor_name) = &self.config.source_accessor_name { + source_loc_props.push(create_prop( + "moduleSource", + parse_expr_or_ident(source_accessor_name), + )); + } + let source_loc = create_object_lit(source_loc_props); let initialize_call = create_call_expr( init_fn, @@ -431,11 +481,15 @@ impl ClassTransform { // that dispatches to Symbol.for("lively-instance-initialize"). let restorer_symbol = create_call_expr( create_member_expr(create_ident_expr("Symbol"), "for"), - vec![to_expr_or_spread(create_string_expr("lively-instance-restorer"))], + vec![to_expr_or_spread(create_string_expr( + "lively-instance-restorer", + ))], ); let initialize_symbol = create_call_expr( create_member_expr(create_ident_expr("Symbol"), "for"), - vec![to_expr_or_spread(create_string_expr("lively-instance-initialize"))], + vec![to_expr_or_spread(create_string_expr( + "lively-instance-initialize", + ))], ); let first_arg_ident = Ident::new("__first_arg__".into(), DUMMY_SP, SyntaxContext::empty()); @@ -450,11 +504,11 @@ impl ClassTransform { let init_call = create_call_expr( create_member_expr( - create_computed_member_expr(create_ident_expr("this"), initialize_symbol), + create_computed_member_expr(create_this_expr(), initialize_symbol), "apply", ), vec![ - to_expr_or_spread(create_ident_expr("this")), + to_expr_or_spread(create_this_expr()), to_expr_or_spread(create_ident_expr("arguments")), ], ); @@ -514,12 +568,8 @@ impl ClassTransform { ParamOrTsParamProp::Param(param) => param.clone(), ParamOrTsParamProp::TsParamProp(ts_param_prop) => { let pat = match &ts_param_prop.param { - TsParamPropParam::Ident(binding_ident) => { - Pat::Ident(binding_ident.clone()) - } - TsParamPropParam::Assign(assign_pat) => { - Pat::Assign(assign_pat.clone()) - } + TsParamPropParam::Ident(binding_ident) => Pat::Ident(binding_ident.clone()), + TsParamPropParam::Assign(assign_pat) => Pat::Assign(assign_pat.clone()), }; Param { @@ -532,7 +582,11 @@ impl ClassTransform { .collect() } - fn constructor_initializer_descriptor(&self, class_name: &str, class: &Class) -> Option> { + fn constructor_initializer_descriptor( + &self, + class_name: &str, + class: &Class, + ) -> Option> { for member in &class.body { if let ClassMember::Constructor(ctor) = member { let mut body = ctor.body.clone().unwrap_or(BlockStmt { @@ -542,10 +596,14 @@ impl ClassTransform { }); // Legacy transform rewrites super accesses against the synthetic // `__lively_class__` binding created inside the class IIFE. - let mut rewriter = SuperRewriter::new("__lively_class__", &self.config.function_node, false); + let mut rewriter = + SuperRewriter::new("__lively_class__", &self.config.function_node, false); body.visit_mut_with(&mut rewriter); if rewriter.saw_direct_super_call { - body.stmts.insert(0, Stmt::Decl(create_var_decl(VarDeclKind::Var, "_this", None))); + body.stmts.insert( + 0, + Stmt::Decl(create_var_decl(VarDeclKind::Var, "_this", None)), + ); body.stmts.push(Stmt::Return(ReturnStmt { span: DUMMY_SP, arg: Some(Box::new(create_ident_expr("_this"))), @@ -554,7 +612,11 @@ impl ClassTransform { let init_name = format!("{}_initialize_", class_name); let init_fn = Expr::Fn(FnExpr { - ident: Some(Ident::new(init_name.into(), DUMMY_SP, SyntaxContext::empty())), + ident: Some(Ident::new( + init_name.into(), + DUMMY_SP, + SyntaxContext::empty(), + )), function: Box::new(Function { params: self.map_constructor_params(ctor), decorators: vec![], @@ -573,7 +635,9 @@ impl ClassTransform { "key", create_call_expr( create_member_expr(create_ident_expr("Symbol"), "for"), - vec![to_expr_or_spread(create_string_expr("lively-instance-initialize"))], + vec![to_expr_or_spread(create_string_expr( + "lively-instance-initialize", + ))], ), ), create_prop("value", init_fn), @@ -600,7 +664,7 @@ impl ClassTransform { .as_ref() .map(|v| *v.clone()) .unwrap_or_else(|| create_ident_expr("null")); - let member = create_member_expr(create_ident_expr("this"), &field_name); + let member = create_member_expr(create_this_expr(), &field_name); let assign = create_assign_expr(expr_to_assign_target(member), value); stmts.push(Stmt::Expr(ExprStmt { @@ -616,7 +680,7 @@ impl ClassTransform { .as_ref() .map(|v| *v.clone()) .unwrap_or_else(|| create_ident_expr("null")); - let member = create_member_expr(create_ident_expr("this"), &field_name); + let member = create_member_expr(create_this_expr(), &field_name); let assign = create_assign_expr(expr_to_assign_target(member), value); stmts.push(Stmt::Expr(ExprStmt { @@ -681,7 +745,9 @@ impl ClassTransform { if let Some(initializer) = self.constructor_initializer_descriptor(class_name, class) { instance_methods.push(initializer); } - let mut class_methods = vec![Some(to_expr_or_spread(self.create_lively_class_name_descriptor(class_name)))]; + let mut class_methods = vec![Some(to_expr_or_spread( + self.create_lively_class_name_descriptor(class_name), + ))]; for member in &class.body { match member { @@ -694,8 +760,11 @@ impl ClassTransform { if let Some(key_expr) = self.method_key_expr(&method.key) { let mut function = method.function.clone(); - let mut rewriter = - SuperRewriter::new("__lively_class__", &self.config.function_node, method.is_static); + let mut rewriter = SuperRewriter::new( + "__lively_class__", + &self.config.function_node, + method.is_static, + ); function.visit_mut_with(&mut rewriter); let mut props = vec![create_prop("key", key_expr)]; @@ -720,8 +789,11 @@ impl ClassTransform { ClassMember::PrivateMethod(method) => { let key_expr = create_string_expr(&self.private_name_to_string(&method.key)); let mut function = method.function.clone(); - let mut rewriter = - SuperRewriter::new("__lively_class__", &self.config.function_node, method.is_static); + let mut rewriter = SuperRewriter::new( + "__lively_class__", + &self.config.function_node, + method.is_static, + ); function.visit_mut_with(&mut rewriter); let fn_expr = Expr::Fn(FnExpr { @@ -746,7 +818,10 @@ impl ClassTransform { } } - (create_array_lit(instance_methods), create_array_lit(class_methods)) + ( + create_array_lit(instance_methods), + create_array_lit(class_methods), + ) } } @@ -761,16 +836,17 @@ impl VisitMut for ClassTransform { self.default_class_counter += 1; Ident::new(name.as_str().into(), DUMMY_SP, SyntaxContext::empty()) }); - let transformed = self.transform_class(Some(&class_ident), &class_expr.class, true); + let transformed = + self.transform_class(Some(&class_ident), &class_expr.class, true); let decl = ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(VarDecl { - span: DUMMY_SP, + span: class_expr.class.span, ctxt: SyntaxContext::empty(), // Legacy class transform always lowers class declarations to `var`. // This avoids TDZ crashes in cyclic module evaluation. kind: VarDeclKind::Var, declare: false, decls: vec![VarDeclarator { - span: DUMMY_SP, + span: class_expr.class.span, name: Pat::Ident(BindingIdent { id: class_ident.clone(), type_ann: None, @@ -779,15 +855,18 @@ impl VisitMut for ClassTransform { definite: false, }], })))); - let export = ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(ExportDefaultExpr { - span: DUMMY_SP, - expr: Box::new(Expr::Ident(class_ident)), - })); + let export = + ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(ExportDefaultExpr { + span: DUMMY_SP, + expr: Box::new(Expr::Ident(class_ident)), + })); new_body.push(decl); new_body.push(export); continue; } - new_body.push(ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(export_decl))); + new_body.push(ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl( + export_decl, + ))); continue; } new_body.push(item); @@ -796,18 +875,37 @@ impl VisitMut for ClassTransform { module.visit_mut_children_with(self); } + fn visit_mut_function(&mut self, f: &mut Function) { + self.fn_depth += 1; + f.visit_mut_children_with(self); + self.fn_depth -= 1; + } + + fn visit_mut_arrow_expr(&mut self, f: &mut ArrowExpr) { + self.fn_depth += 1; + f.visit_mut_children_with(self); + self.fn_depth -= 1; + } + fn visit_mut_decl(&mut self, decl: &mut Decl) { if let Decl::Class(class_decl) = decl { - let transformed = self.transform_class(Some(&class_decl.ident), &class_decl.class, true); - - // Replace with variable declaration - *decl = create_var_decl_with_ident( - // Keep parity with lively.classes/class-to-function-transform.js - // (`result = n.varDecl(..., 'var')`). - VarDeclKind::Var, - class_decl.ident.clone(), - Some(transformed), - ); + let transformed = + self.transform_class(Some(&class_decl.ident), &class_decl.class, true); + *decl = Decl::Var(Box::new(VarDecl { + span: class_decl.class.span, + ctxt: SyntaxContext::empty(), + kind: VarDeclKind::Var, + declare: false, + decls: vec![VarDeclarator { + span: class_decl.class.span, + name: Pat::Ident(BindingIdent { + id: class_decl.ident.clone(), + type_ann: None, + }), + init: Some(Box::new(transformed)), + definite: false, + }], + })); return; } @@ -816,7 +914,8 @@ impl VisitMut for ClassTransform { fn visit_mut_expr(&mut self, expr: &mut Expr) { if let Expr::Class(class_expr) = expr { - let transformed = self.transform_class(class_expr.ident.as_ref(), &class_expr.class, false); + let transformed = + self.transform_class(class_expr.ident.as_ref(), &class_expr.class, false); *expr = transformed; return; } @@ -827,10 +926,10 @@ impl VisitMut for ClassTransform { #[cfg(test)] mod tests { use super::*; - use swc_core::common::{sync::Lrc, FileName, SourceMap}; - use swc_core::ecma::codegen::{text_writer::JsWriter, Emitter, Config}; - use swc_core::ecma::parser::{parse_file_as_module, Syntax}; - use swc_core::ecma::visit::VisitMutWith; + use swc_common::{sync::Lrc, FileName, SourceMap}; + use swc_ecma_codegen::{text_writer::JsWriter, Config, Emitter}; + use swc_ecma_parser::{parse_file_as_module, Syntax}; + use swc_ecma_visit::VisitMutWith; fn transform_code(code: &str) -> String { let cm = Lrc::new(SourceMap::default()); @@ -849,6 +948,7 @@ mod tests { class_holder: "__varRecorder__".to_string(), function_node: "initializeES6ClassForLively".to_string(), current_module_accessor: "module.id".to_string(), + source_accessor_name: None, }; let mut transform = ClassTransform::new( @@ -885,7 +985,11 @@ mod tests { #[test] fn test_preserves_constructor_params() { let output = transform_code("class Color { constructor(r, g, b, a) { this.r = r; this.g = g; this.b = b; this.a = a; } }"); - assert!(output.contains("(r, g, b, a)"), "should preserve constructor params: {}", output); + assert!( + output.contains("(r, g, b, a)"), + "should preserve constructor params: {}", + output + ); assert!(output.contains("this.r = r"), "body preserved: {}", output); assert!(output.contains("this.g = g"), "body preserved: {}", output); assert!(output.contains("this.b = b"), "body preserved: {}", output); @@ -894,14 +998,41 @@ mod tests { #[test] fn test_preserves_constructor_params_with_super() { - let output = transform_code("class Child extends Parent { constructor(arg) { super(arg); this.arg = arg; } }"); - assert!(output.contains("(arg)"), "should preserve constructor param: {}", output); - assert!(output.contains("this.arg = arg"), "body preserved: {}", output); + let output = transform_code( + "class Child extends Parent { constructor(arg) { super(arg); this.arg = arg; } }", + ); + assert!( + output.contains("(arg)"), + "should preserve constructor param: {}", + output + ); + assert!( + output.contains("this.arg = arg"), + "body preserved: {}", + output + ); + } + + #[test] + fn test_default_subclass_constructor_inherits_super_initializer() { + let output = transform_code("class Child extends Parent { method() {} }"); + assert!( + !output.contains("Child_initialize_"), + "subclasses without an explicit constructor should inherit the superclass initializer: {}", + output + ); + assert!( + !output.contains("__super_initialize__.apply(this, arguments)"), + "should not install a synthetic forwarding initializer: {}", + output + ); } #[test] fn test_class_expression_self_reference_preserved() { - let output = transform_code("const X = class NodePath { static create(h, p) { return new NodePath(h, p); } };"); + let output = transform_code( + "const X = class NodePath { static create(h, p) { return new NodePath(h, p); } };", + ); assert!(output.contains("var NodePath = function NodePath")); assert!(output.contains("new NodePath(h, p)")); assert!(!output.contains("new NodePath1(h, p)")); diff --git a/lively.freezer/swc-plugin/src/transforms/component.rs b/lively.freezer/swc-plugin/lively-swc-transforms/src/transforms/component.rs similarity index 85% rename from lively.freezer/swc-plugin/src/transforms/component.rs rename to lively.freezer/swc-plugin/lively-swc-transforms/src/transforms/component.rs index e48f24a905..8db29d5b66 100644 --- a/lively.freezer/swc-plugin/src/transforms/component.rs +++ b/lively.freezer/swc-plugin/lively-swc-transforms/src/transforms/component.rs @@ -1,9 +1,7 @@ -use swc_core::common::{Spanned, DUMMY_SP}; -use swc_core::ecma::{ - ast::*, - visit::{VisitMut, VisitMutWith}, -}; use crate::utils::ast_helpers::*; +use swc_common::{Spanned, DUMMY_SP}; +use swc_ecma_ast::*; +use swc_ecma_visit::{VisitMut, VisitMutWith}; /// Transform that wraps component definitions with metadata /// @@ -40,12 +38,14 @@ impl ComponentTransform { } /// Wrap a component call with component.for(...) - fn wrap_component_call(&self, component_call: Expr, export_name: &str, span: swc_core::common::Span) -> Expr { + fn wrap_component_call( + &self, + component_call: Expr, + export_name: &str, + span: swc_common::Span, + ) -> Expr { // Create: () => component(...) - let arrow_fn = create_arrow_fn( - vec![], - BlockStmtOrExpr::Expr(Box::new(component_call)), - ); + let arrow_fn = create_arrow_fn(vec![], BlockStmtOrExpr::Expr(Box::new(component_call))); // Create metadata object: { module: "...", export: "MyComp", range: { start: X, end: Y } } let metadata = create_object_lit(vec![ @@ -127,21 +127,16 @@ impl VisitMut for ComponentTransform { return; } if self.is_component_call(&stmt.expr) { - let metadata = create_object_lit(vec![ - create_prop("module", create_string_expr(&self.module_id)), - ]); - let arrow_fn = create_arrow_fn( - vec![], - BlockStmtOrExpr::Expr(stmt.expr.clone()), - ); + let metadata = create_object_lit(vec![create_prop( + "module", + create_string_expr(&self.module_id), + )]); + let arrow_fn = create_arrow_fn(vec![], BlockStmtOrExpr::Expr(stmt.expr.clone())); *stmt = ExprStmt { span: stmt.span, expr: Box::new(create_call_expr( create_member_expr(create_ident_expr("component"), "for"), - vec![ - to_expr_or_spread(arrow_fn), - to_expr_or_spread(metadata), - ], + vec![to_expr_or_spread(arrow_fn), to_expr_or_spread(metadata)], )), }; return; @@ -174,10 +169,10 @@ impl VisitMut for ComponentTransform { #[cfg(test)] mod tests { use super::*; - use swc_core::common::{sync::Lrc, FileName, SourceMap}; - use swc_core::ecma::codegen::{text_writer::JsWriter, Emitter, Config}; - use swc_core::ecma::parser::{parse_file_as_module, Syntax}; - use swc_core::ecma::visit::VisitMutWith; + use swc_common::{sync::Lrc, FileName, SourceMap}; + use swc_ecma_codegen::{text_writer::JsWriter, Config, Emitter}; + use swc_ecma_parser::{parse_file_as_module, Syntax}; + use swc_ecma_visit::VisitMutWith; fn transform_code(code: &str) -> String { let cm = Lrc::new(SourceMap::default()); diff --git a/lively.freezer/swc-plugin/src/transforms/dynamic_import.rs b/lively.freezer/swc-plugin/lively-swc-transforms/src/transforms/dynamic_import.rs similarity index 93% rename from lively.freezer/swc-plugin/src/transforms/dynamic_import.rs rename to lively.freezer/swc-plugin/lively-swc-transforms/src/transforms/dynamic_import.rs index 0d98b30b23..78364f55d3 100644 --- a/lively.freezer/swc-plugin/src/transforms/dynamic_import.rs +++ b/lively.freezer/swc-plugin/lively-swc-transforms/src/transforms/dynamic_import.rs @@ -1,8 +1,6 @@ -use swc_core::common::{SyntaxContext, DUMMY_SP}; -use swc_core::ecma::{ - ast::*, - visit::{VisitMut, VisitMutWith}, -}; +use swc_common::{SyntaxContext, DUMMY_SP}; +use swc_ecma_ast::*; +use swc_ecma_visit::{VisitMut, VisitMutWith}; use crate::utils::ast_helpers::is_member_expr_with_names; @@ -92,10 +90,10 @@ impl VisitMut for DynamicImportTransform { #[cfg(test)] mod tests { use super::*; - use swc_core::common::{sync::Lrc, FileName, SourceMap}; - use swc_core::ecma::codegen::{text_writer::JsWriter, Emitter, Config}; - use swc_core::ecma::parser::{parse_file_as_module, Syntax}; - use swc_core::ecma::visit::VisitMutWith; + use swc_common::{sync::Lrc, FileName, SourceMap}; + use swc_ecma_codegen::{text_writer::JsWriter, Config, Emitter}; + use swc_ecma_parser::{parse_file_as_module, Syntax}; + use swc_ecma_visit::VisitMutWith; fn transform_code(code: &str) -> String { let cm = Lrc::new(SourceMap::default()); diff --git a/lively.freezer/swc-plugin/src/transforms/export_split.rs b/lively.freezer/swc-plugin/lively-swc-transforms/src/transforms/export_split.rs similarity index 93% rename from lively.freezer/swc-plugin/src/transforms/export_split.rs rename to lively.freezer/swc-plugin/lively-swc-transforms/src/transforms/export_split.rs index b41f19f00f..ee09bcbc16 100644 --- a/lively.freezer/swc-plugin/src/transforms/export_split.rs +++ b/lively.freezer/swc-plugin/lively-swc-transforms/src/transforms/export_split.rs @@ -1,9 +1,7 @@ -use swc_core::common::DUMMY_SP; -use swc_core::ecma::{ - ast::*, - visit::{VisitMut, VisitMutWith}, -}; use crate::utils::ast_helpers::extract_idents_from_pat; +use swc_common::DUMMY_SP; +use swc_ecma_ast::*; +use swc_ecma_visit::{VisitMut, VisitMutWith}; /// Transform that splits export variable declarations /// @@ -38,7 +36,9 @@ impl VisitMut for ExportSplitTransform { decls: vec![decl.clone()], }; - split_items.push(ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(single_var))))); + split_items.push(ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new( + single_var, + ))))); let ids = extract_idents_from_pat(&decl.name); for (sym, ctxt) in ids { diff --git a/lively.freezer/swc-plugin/src/transforms/exported_import_capture.rs b/lively.freezer/swc-plugin/lively-swc-transforms/src/transforms/exported_import_capture.rs similarity index 87% rename from lively.freezer/swc-plugin/src/transforms/exported_import_capture.rs rename to lively.freezer/swc-plugin/lively-swc-transforms/src/transforms/exported_import_capture.rs index 4bec28bd98..f255d40ad0 100644 --- a/lively.freezer/swc-plugin/src/transforms/exported_import_capture.rs +++ b/lively.freezer/swc-plugin/lively-swc-transforms/src/transforms/exported_import_capture.rs @@ -1,8 +1,6 @@ -use swc_core::common::{SyntaxContext, DUMMY_SP}; -use swc_core::ecma::{ - ast::*, - visit::{VisitMut, VisitMutWith}, -}; +use swc_common::{SyntaxContext, DUMMY_SP}; +use swc_ecma_ast::*; +use swc_ecma_visit::{VisitMut, VisitMutWith}; use crate::utils::ast_helpers::*; @@ -69,7 +67,10 @@ impl ExportedImportCapturePass { } ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) if !export.specifiers.is_empty() - && export.specifiers.iter().any(|s| matches!(s, ExportSpecifier::Named(_))) => + && export + .specifiers + .iter() + .any(|s| matches!(s, ExportSpecifier::Named(_))) => { // Case 3: `export { x, y as z }` (no source, no declaration) let original = ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export.clone())); @@ -128,11 +129,7 @@ impl ExportedImportCapturePass { // import { x as local } from '...' import_specs.push(ImportSpecifier::Named(ImportNamedSpecifier { span: DUMMY_SP, - local: Ident::new( - local_name.as_str().into(), - DUMMY_SP, - SyntaxContext::empty(), - ), + local: Ident::new(local_name.as_str().into(), DUMMY_SP, SyntaxContext::empty()), imported: if orig_name == local_name { None } else { @@ -342,10 +339,10 @@ impl VisitMut for ExportedImportCapturePass { #[cfg(test)] mod tests { use super::*; - use swc_core::common::{sync::Lrc, FileName, SourceMap}; - use swc_core::ecma::codegen::{text_writer::JsWriter, Config, Emitter}; - use swc_core::ecma::parser::{parse_file_as_module, Syntax}; - use swc_core::ecma::visit::VisitMutWith; + use swc_common::{sync::Lrc, FileName, SourceMap}; + use swc_ecma_codegen::{text_writer::JsWriter, Config, Emitter}; + use swc_ecma_parser::{parse_file_as_module, Syntax}; + use swc_ecma_visit::VisitMutWith; fn transform_code(code: &str) -> String { let cm = Lrc::new(SourceMap::default()); @@ -381,40 +378,88 @@ mod tests { fn test_export_named_with_source() { let output = transform_code(r#"export { x as a } from 'mod';"#); // Should produce: import { x as a } from 'mod'; export { a }; __varRecorder__.a = a; - assert!(output.contains("import { x as a } from"), "output was: {}", output); + assert!( + output.contains("import { x as a } from"), + "output was: {}", + output + ); assert!(output.contains("export { a }"), "output was: {}", output); - assert!(output.contains("__varRecorder__.a = a"), "output was: {}", output); + assert!( + output.contains("__varRecorder__.a = a"), + "output was: {}", + output + ); } #[test] fn test_export_named_with_source_same_name() { let output = transform_code(r#"export { x } from 'mod';"#); - assert!(output.contains("import { x } from"), "output was: {}", output); + assert!( + output.contains("import { x } from"), + "output was: {}", + output + ); assert!(output.contains("export { x }"), "output was: {}", output); - assert!(output.contains("__varRecorder__.x = x"), "output was: {}", output); + assert!( + output.contains("__varRecorder__.x = x"), + "output was: {}", + output + ); } #[test] fn test_export_named_no_source() { let output = transform_code(r#"const x = 1; const y = 2; export { x, y as z };"#); - assert!(output.contains("export { x, y as z }"), "keeps original: {}", output); - assert!(output.contains("__varRecorder__.x = x"), "captures x: {}", output); - assert!(output.contains("__varRecorder__.y = y"), "captures y: {}", output); + assert!( + output.contains("export { x, y as z }"), + "keeps original: {}", + output + ); + assert!( + output.contains("__varRecorder__.x = x"), + "captures x: {}", + output + ); + assert!( + output.contains("__varRecorder__.y = y"), + "captures y: {}", + output + ); } #[test] fn test_export_const() { let output = transform_code(r#"export const x = 1;"#); - assert!(output.contains("export const x = 1"), "keeps export: {}", output); - assert!(output.contains("__varRecorder__.x = x"), "captures: {}", output); + assert!( + output.contains("export const x = 1"), + "keeps export: {}", + output + ); + assert!( + output.contains("__varRecorder__.x = x"), + "captures: {}", + output + ); } #[test] fn test_export_all() { let output = transform_code(r#"export * from 'mod';"#); - assert!(output.contains("export * from 'mod'"), "keeps original: {}", output); - assert!(output.contains("import * as __captured"), "adds import: {}", output); - assert!(output.contains("Object.assign(__varRecorder__"), "captures: {}", output); + assert!( + output.contains("export * from 'mod'"), + "keeps original: {}", + output + ); + assert!( + output.contains("import * as __captured"), + "adds import: {}", + output + ); + assert!( + output.contains("Object.assign(__varRecorder__"), + "captures: {}", + output + ); } #[test] diff --git a/lively.freezer/swc-plugin/src/transforms/mod.rs b/lively.freezer/swc-plugin/lively-swc-transforms/src/transforms/mod.rs similarity index 100% rename from lively.freezer/swc-plugin/src/transforms/mod.rs rename to lively.freezer/swc-plugin/lively-swc-transforms/src/transforms/mod.rs index 5a07d13d12..f699d018fe 100644 --- a/lively.freezer/swc-plugin/src/transforms/mod.rs +++ b/lively.freezer/swc-plugin/lively-swc-transforms/src/transforms/mod.rs @@ -1,17 +1,17 @@ -pub mod scope_capturing; pub mod class_transform; pub mod component; -pub mod namespace; pub mod dynamic_import; -pub mod systemjs; pub mod export_split; pub mod exported_import_capture; +pub mod namespace; +pub mod scope_capturing; +pub mod systemjs; -pub use scope_capturing::ScopeCapturingTransform; pub use class_transform::ClassTransform; pub use component::ComponentTransform; -pub use namespace::NamespaceTransform; pub use dynamic_import::DynamicImportTransform; -pub use systemjs::SystemJsTransform; pub use export_split::ExportSplitTransform; pub use exported_import_capture::ExportedImportCapturePass; +pub use namespace::NamespaceTransform; +pub use scope_capturing::ScopeCapturingTransform; +pub use systemjs::SystemJsTransform; diff --git a/lively.freezer/swc-plugin/src/transforms/namespace.rs b/lively.freezer/swc-plugin/lively-swc-transforms/src/transforms/namespace.rs similarity index 74% rename from lively.freezer/swc-plugin/src/transforms/namespace.rs rename to lively.freezer/swc-plugin/lively-swc-transforms/src/transforms/namespace.rs index 011386b012..41ef355568 100644 --- a/lively.freezer/swc-plugin/src/transforms/namespace.rs +++ b/lively.freezer/swc-plugin/lively-swc-transforms/src/transforms/namespace.rs @@ -1,8 +1,6 @@ -use swc_core::common::{SyntaxContext, DUMMY_SP}; -use swc_core::ecma::{ - ast::*, - visit::{VisitMut, VisitMutWith}, -}; +use swc_common::{SyntaxContext, DUMMY_SP}; +use swc_ecma_ast::*; +use swc_ecma_visit::{VisitMut, VisitMutWith}; use crate::utils::ast_helpers::*; @@ -23,7 +21,10 @@ pub struct NamespaceTransform { } impl NamespaceTransform { - pub fn new(resolved_imports: std::collections::HashMap, _capture_obj: String) -> Self { + pub fn new( + resolved_imports: std::collections::HashMap, + _capture_obj: String, + ) -> Self { Self { additional_decls: Vec::new(), resolved_imports, @@ -106,50 +107,55 @@ impl VisitMut for NamespaceTransform { ); // import * as name_namespace from 'mod' - let import_decl = ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { - span: DUMMY_SP, - specifiers: vec![ImportSpecifier::Namespace(ImportStarAsSpecifier { + let import_decl = + ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { span: DUMMY_SP, - local: ns_ident.clone(), - })], - src: Box::new(Str { - span: DUMMY_SP, - value: module_src.clone().into(), - raw: None, - }), - type_only: false, - with: None, - phase: Default::default(), - })); + specifiers: vec![ImportSpecifier::Namespace( + ImportStarAsSpecifier { + span: DUMMY_SP, + local: ns_ident.clone(), + }, + )], + src: Box::new(Str { + span: DUMMY_SP, + value: module_src.clone().into(), + raw: None, + }), + type_only: false, + with: None, + phase: Default::default(), + })); new_body.push(import_decl); // const name = exportsOf("resolved") || name_namespace - let fallback_expr = self.create_namespace_fallback(&module_src, &ns_ident); - let name_ident = Ident::new( - name.as_str().into(), - DUMMY_SP, - SyntaxContext::empty(), - ); - let const_decl = ModuleItem::Stmt(Stmt::Decl(create_var_decl_with_ident( - VarDeclKind::Const, - name_ident.clone(), - Some(fallback_expr), - ))); + let fallback_expr = + self.create_namespace_fallback(&module_src, &ns_ident); + let name_ident = + Ident::new(name.as_str().into(), DUMMY_SP, SyntaxContext::empty()); + let const_decl = + ModuleItem::Stmt(Stmt::Decl(create_var_decl_with_ident( + VarDeclKind::Const, + name_ident.clone(), + Some(fallback_expr), + ))); self.additional_decls.push(const_decl); // export { name } - let export_named = ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(NamedExport { - span: DUMMY_SP, - specifiers: vec![ExportSpecifier::Named(ExportNamedSpecifier { + let export_named = + ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(NamedExport { span: DUMMY_SP, - orig: ModuleExportName::Ident(name_ident), - exported: None, - is_type_only: false, - })], - src: None, - type_only: false, - with: None, - })); + specifiers: vec![ExportSpecifier::Named( + ExportNamedSpecifier { + span: DUMMY_SP, + orig: ModuleExportName::Ident(name_ident), + exported: None, + is_type_only: false, + }, + )], + src: None, + type_only: false, + with: None, + })); self.additional_decls.push(export_named); continue; // skip the original export * as name from 'mod' @@ -311,10 +317,10 @@ impl VisitMut for NamespaceTransform { #[cfg(test)] mod tests { use super::*; - use swc_core::common::{sync::Lrc, FileName, SourceMap}; - use swc_core::ecma::codegen::{text_writer::JsWriter, Emitter, Config}; - use swc_core::ecma::parser::{parse_file_as_module, Syntax}; - use swc_core::ecma::visit::VisitMutWith; + use swc_common::{sync::Lrc, FileName, SourceMap}; + use swc_ecma_codegen::{text_writer::JsWriter, Config, Emitter}; + use swc_ecma_parser::{parse_file_as_module, Syntax}; + use swc_ecma_visit::VisitMutWith; fn transform_code(code: &str) -> String { let cm = Lrc::new(SourceMap::default()); @@ -329,7 +335,10 @@ mod tests { ) .unwrap(); - let mut transform = NamespaceTransform::new(std::collections::HashMap::new(), "__varRecorder__".to_string()); + let mut transform = NamespaceTransform::new( + std::collections::HashMap::new(), + "__varRecorder__".to_string(), + ); module.visit_mut_with(&mut transform); let mut buf = vec![]; @@ -354,7 +363,10 @@ mod tests { assert!(output.contains("exportsOf")); } - fn transform_code_with_resolved(code: &str, resolved: std::collections::HashMap) -> (String, Vec) { + fn transform_code_with_resolved( + code: &str, + resolved: std::collections::HashMap, + ) -> (String, Vec) { let cm = Lrc::new(SourceMap::default()); let fm = cm.new_source_file(FileName::Anon.into(), code.to_string()); @@ -397,27 +409,52 @@ mod tests { std::collections::HashMap::new(), ); // Should have import * as utils_namespace - assert!(output.contains("utils_namespace"), "should create namespace import: {}", output); + assert!( + output.contains("utils_namespace"), + "should create namespace import: {}", + output + ); // Should have const utils = exportsOf(...) || utils_namespace - assert!(output.contains("exportsOf"), "should have exportsOf call: {}", output); - assert!(output.contains("const utils"), "should have const utils declaration: {}", output); + assert!( + output.contains("exportsOf"), + "should have exportsOf call: {}", + output + ); + assert!( + output.contains("const utils"), + "should have const utils declaration: {}", + output + ); // Should have export { utils } - assert!(output.contains("export {"), "should have named export: {}", output); + assert!( + output.contains("export {"), + "should have named export: {}", + output + ); assert!(output.contains("utils"), "should export utils: {}", output); // utils_namespace should be in excludes - assert!(excludes.contains(&"utils_namespace".to_string()), "utils_namespace should be excluded: {:?}", excludes); + assert!( + excludes.contains(&"utils_namespace".to_string()), + "utils_namespace should be excluded: {:?}", + excludes + ); } #[test] fn test_named_namespace_reexport_with_resolved() { // When resolved_imports has a mapping, use the resolved ID in exportsOf let mut resolved = std::collections::HashMap::new(); - resolved.insert("./utils.js".to_string(), "local://package/utils.js".to_string()); - let (output, _) = transform_code_with_resolved( - r#"export * as utils from './utils.js';"#, - resolved, + resolved.insert( + "./utils.js".to_string(), + "local://package/utils.js".to_string(), + ); + let (output, _) = + transform_code_with_resolved(r#"export * as utils from './utils.js';"#, resolved); + assert!( + output.contains("local://package/utils.js"), + "should use resolved ID in exportsOf: {}", + output ); - assert!(output.contains("local://package/utils.js"), "should use resolved ID in exportsOf: {}", output); } #[test] @@ -427,7 +464,15 @@ mod tests { r#"export * from './utils.js';"#, std::collections::HashMap::new(), ); - assert!(output.contains("recorderFor"), "unnamed export * should use recorderFor: {}", output); - assert!(output.contains("Object.assign"), "unnamed export * should use Object.assign: {}", output); + assert!( + output.contains("recorderFor"), + "unnamed export * should use recorderFor: {}", + output + ); + assert!( + output.contains("Object.assign"), + "unnamed export * should use Object.assign: {}", + output + ); } } diff --git a/lively.freezer/swc-plugin/lively-swc-transforms/src/transforms/scope_capturing.rs b/lively.freezer/swc-plugin/lively-swc-transforms/src/transforms/scope_capturing.rs new file mode 100644 index 0000000000..46555ed27f --- /dev/null +++ b/lively.freezer/swc-plugin/lively-swc-transforms/src/transforms/scope_capturing.rs @@ -0,0 +1,5936 @@ +use std::collections::{HashMap, HashSet}; +use swc_common::{Span, SyntaxContext, DUMMY_SP}; +use swc_ecma_ast::*; +use swc_ecma_visit::{Visit, VisitMut, VisitMutWith, VisitWith}; + +use crate::utils::ast_helpers::*; +use crate::utils::scope_analyzer::ScopeAnalyzer; + +/// Transform that captures top-level variables into a recorder object +/// +/// Transforms: +/// - `var x = 1` → `__varRecorder__.x = 1` +/// - `x + 2` → `__varRecorder__.x + 2` +/// - Handles destructuring, function hoisting, and exports +pub struct ScopeCapturingTransform { + /// Name of the capture object (e.g., "__varRecorder__") + capture_obj: String, + + /// Optional wrapper function for declarations (for resurrection builds) + declaration_wrapper: Option, + + /// Variables to exclude from capturing + excluded: HashSet, + + /// Whether to capture imports + capture_imports: bool, + + /// Whether to rewrite mixed default + named imports through a temp binding. + rewrite_mixed_default_imports: bool, + + /// Whether this is a resurrection build + resurrection: bool, + + /// Optional module hash for resurrection builds + module_hash: Option, + + /// Current module id + module_id: String, + + /// Optional current module accessor expression (legacy parity for import.meta). + current_module_accessor: Option, + + /// Identifier used for the embedded original module source. + source_accessor_name: Option, + + /// Original module source, embedded into source metadata when present. + original_source: Option, + + /// Top-level variables that should be captured + capturable_vars: HashSet, + + /// Identifier names referenced in the original module before this pass + /// generates recorder/System wrapper code. This mirrors Babel's + /// refsToReplace shape: generated identifiers are not captured just + /// because the mutation visitor later sees them. + original_ref_names: HashSet, + + /// Bindings introduced by `for` headers. Babel leaves those local, even + /// when the declaration is a top-level `var`. + loop_header_vars: HashSet, + + /// Top-level imported bindings + imported_vars: HashSet, + + /// Map from imported local name → (source module, original/imported name) + /// Used to convert `export { importedVar }` to `export { orig } from 'src'` + /// to work around SWC system_js dropping re-exported import bindings. + import_sources: HashMap, + + /// Top-level function capture assignments that must run before body code + hoisted_function_captures: Vec, + + /// Current depth (0 = module level, >0 = nested) + depth: usize, + /// Function nesting depth (0 = module level, >0 = inside function body) + fn_depth: usize, + + /// Lexically declared variable names in nested scopes (for shadowing detection) + scope_stack: Vec, + + /// Collected export metadata for __module_exports__ (resurrection builds only). + /// Entries follow the same format as the Babel path: + /// - `"exportedName"` for regular exports + /// - `"__rename__local->exported"` for renamed exports + /// - `"__reexport__moduleId"` for star re-exports + /// - `"__default__localName"` for default exports + collected_exports: Vec, + + /// Resolved import source specifier → normalized module ID. + /// Used for resurrection namespace transforms (exportsOf calls). + resolved_imports: HashMap, + + /// Names declared by the currently visited variable declaration chain. + /// Needed for parity with legacy transform behavior: references inside a + /// var-declarator initializer must resolve to local bindings declared in + /// the same declaration list, not the recorder member. + current_var_decl_stack: Vec>, +} + +struct ScopeFrame { + names: HashSet, + is_function_scope: bool, +} + +#[derive(Default)] +struct OriginalRefNameCollector { + names: HashSet, + scopes: Vec>, +} + +impl Visit for OriginalRefNameCollector { + fn visit_module(&mut self, module: &Module) { + self.scopes.clear(); + self.scopes.push(HashSet::new()); + self.predeclare_module_items(&module.body); + module.visit_children_with(self); + } + + fn visit_expr(&mut self, expr: &Expr) { + if let Expr::Ident(ident) = expr { + self.add_ref(ident.sym.as_ref()); + return; + } + + expr.visit_children_with(self); + } + + fn visit_assign_expr(&mut self, assign: &AssignExpr) { + if let AssignTarget::Simple(SimpleAssignTarget::Ident(binding_ident)) = &assign.left { + self.add_ref(binding_ident.id.sym.as_ref()); + } else { + assign.left.visit_with(self); + } + + assign.right.visit_with(self); + } + + fn visit_update_expr(&mut self, update: &UpdateExpr) { + update.arg.visit_with(self); + } + + fn visit_prop(&mut self, prop: &Prop) { + match prop { + Prop::Shorthand(ident) => { + self.add_ref(ident.sym.as_ref()); + } + Prop::Getter(getter) => { + getter.key.visit_with(self); + if let Some(body) = &getter.body { + self.enter_scope(); + self.predeclare_stmts(&body.stmts); + body.visit_with(self); + self.exit_scope(); + } + } + Prop::Setter(setter) => { + setter.key.visit_with(self); + self.enter_scope(); + if let Some(param) = &setter.this_param { + self.declare_pat(param); + } + self.declare_pat(&setter.param); + if let Some(body) = &setter.body { + self.predeclare_stmts(&body.stmts); + body.visit_with(self); + } + self.exit_scope(); + } + _ => prop.visit_children_with(self), + } + } + + fn visit_function(&mut self, func: &Function) { + self.enter_scope(); + for param in &func.params { + self.declare_pat(¶m.pat); + } + if let Some(body) = &func.body { + self.predeclare_stmts(&body.stmts); + body.visit_with(self); + } + self.exit_scope(); + } + + fn visit_arrow_expr(&mut self, arrow: &ArrowExpr) { + self.enter_scope(); + for param in &arrow.params { + self.declare_pat(param); + } + arrow.body.visit_with(self); + self.exit_scope(); + } + + fn visit_block_stmt(&mut self, block: &BlockStmt) { + self.enter_scope(); + self.predeclare_stmts(&block.stmts); + block.visit_children_with(self); + self.exit_scope(); + } + + fn visit_catch_clause(&mut self, clause: &CatchClause) { + self.enter_scope(); + if let Some(param) = &clause.param { + self.declare_pat(param); + } + clause.body.visit_with(self); + self.exit_scope(); + } +} + +impl OriginalRefNameCollector { + fn enter_scope(&mut self) { + self.scopes.push(HashSet::new()); + } + + fn exit_scope(&mut self) { + self.scopes.pop(); + } + + fn declare(&mut self, name: &str) { + if let Some(scope) = self.scopes.last_mut() { + scope.insert(name.to_string()); + } + } + + fn is_declared(&self, name: &str) -> bool { + self.scopes.iter().rev().any(|scope| scope.contains(name)) + } + + fn add_ref(&mut self, name: &str) { + if !self.is_declared(name) { + self.names.insert(name.to_string()); + } + } + + fn declare_pat(&mut self, pat: &Pat) { + for (sym, _) in extract_idents_from_pat(pat) { + self.declare(sym.as_ref()); + } + } + + fn predeclare_module_items(&mut self, items: &[ModuleItem]) { + for item in items { + match item { + ModuleItem::Stmt(stmt) => self.predeclare_stmt(stmt), + ModuleItem::ModuleDecl(ModuleDecl::Import(import)) => { + for spec in &import.specifiers { + match spec { + ImportSpecifier::Named(named) => self.declare(named.local.sym.as_ref()), + ImportSpecifier::Default(default) => { + self.declare(default.local.sym.as_ref()) + } + ImportSpecifier::Namespace(ns) => self.declare(ns.local.sym.as_ref()), + } + } + } + ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => { + self.predeclare_decl(&export.decl); + } + ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(export)) => match &export.decl + { + DefaultDecl::Class(class) => { + if let Some(ident) = &class.ident { + self.declare(ident.sym.as_ref()); + } + } + DefaultDecl::Fn(func) => { + if let Some(ident) = &func.ident { + self.declare(ident.sym.as_ref()); + } + } + _ => {} + }, + _ => {} + } + } + } + + fn predeclare_stmts(&mut self, stmts: &[Stmt]) { + for stmt in stmts { + self.predeclare_stmt(stmt); + } + } + + fn predeclare_stmt(&mut self, stmt: &Stmt) { + match stmt { + Stmt::Decl(decl) => self.predeclare_decl(decl), + _ => {} + } + } + + fn predeclare_decl(&mut self, decl: &Decl) { + match decl { + Decl::Class(class) => self.declare(class.ident.sym.as_ref()), + Decl::Fn(func) => self.declare(func.ident.sym.as_ref()), + Decl::Var(var) => { + for declarator in &var.decls { + self.declare_pat(&declarator.name); + } + } + _ => {} + } + } +} + +impl ScopeCapturingTransform { + pub fn new( + capture_obj: String, + declaration_wrapper: Option, + excluded: Vec, + capture_imports: bool, + rewrite_mixed_default_imports: bool, + resurrection: bool, + module_id: String, + current_module_accessor: Option, + source_accessor_name: Option, + original_source: Option, + module_hash: Option, + resolved_imports: HashMap, + ) -> Self { + Self { + capture_obj, + declaration_wrapper, + excluded: excluded.into_iter().collect(), + capture_imports, + rewrite_mixed_default_imports, + resurrection, + module_hash, + module_id, + current_module_accessor, + source_accessor_name, + original_source, + capturable_vars: HashSet::new(), + original_ref_names: HashSet::new(), + loop_header_vars: HashSet::new(), + imported_vars: HashSet::new(), + import_sources: HashMap::new(), + hoisted_function_captures: Vec::new(), + collected_exports: Vec::new(), + resolved_imports, + depth: 0, + fn_depth: 0, + scope_stack: vec![ScopeFrame { + names: HashSet::new(), + is_function_scope: true, + }], + current_var_decl_stack: Vec::new(), + } + } + + /// Check if an identifier should be captured + fn should_capture(&self, id: &Id) -> bool { + // Note: Babel captures inside ALL functions including nested ones. + // No fn_depth skip here. + + // Never rewrite identifiers that are shadowed in a nested lexical scope. + // This keeps locals (params/vars/lets) untouched while still rewriting + // references to captured top-level bindings in nested functions/blocks. + if self + .scope_stack + .iter() + .skip(1) + .rev() + .any(|scope| scope.names.contains(id.0.as_ref())) + { + return false; + } + + // While walking a variable declaration initializer, do not rewrite + // references to bindings declared by the same declaration statement. + // Example: + // var xe = ..., Ue = get(xe) + // must keep `xe` local here, matching legacy JS transform output. + if self + .current_var_decl_stack + .iter() + .rev() + .any(|names| names.contains(id.0.as_ref())) + { + return false; + } + + // Don't capture excluded variables + if self.excluded.contains(id.0.as_ref()) { + return false; + } + + // Never capture the recorder object itself. Generated recorder member + // accesses must stay anchored on the real recorder identifier. + if id.0.as_ref() == self.capture_obj.as_str() { + return false; + } + + // `arguments` is a function-local special binding. In particular, the + // class-to-function transform generates it in constructor dispatch. + if id.0.as_ref() == "arguments" { + return false; + } + + if self.loop_header_vars.contains(id.0.as_ref()) { + return false; + } + + // Keep imported bindings as plain identifiers. The import transform + // still emits recorder capture assignments (`_rec.x = x`) when + // capture_imports is enabled, but ordinary uses must read the live + // local import binding to avoid circular recorder initialization. + if self.imported_vars.contains(id) { + return false; + } + if self.import_sources.contains_key(id.0.as_ref()) { + return false; + } + + // Capture top-level declarations and free/global references that were + // present in the original module. Do not capture identifiers that only + // appear after this pass generates recorder/System wrapper code. + self.capturable_vars.contains(id) || self.original_ref_names.contains(id.0.as_ref()) + } + + /// Create a member expression to the capture object: __varRecorder__.name + fn create_captured_member(&self, name: &str) -> Expr { + create_member_expr(create_ident_expr(&self.capture_obj), name) + } + + /// Create `Object.prototype.hasOwnProperty.call(__varRecorder__, "name")`. + fn create_recorder_has_own(&self, name: &str) -> Expr { + create_call_expr( + create_member_expr( + create_member_expr( + create_member_expr(create_ident_expr("Object"), "prototype"), + "hasOwnProperty", + ), + "call", + ), + vec![ + to_expr_or_spread(create_ident_expr(&self.capture_obj)), + to_expr_or_spread(create_string_expr(name)), + ], + ) + } + + /// Create a computed member expression to the configured declaration + /// wrapper: __varRecorder__[wrapperName]. + fn create_declaration_wrapper_callee(&self) -> Expr { + create_declaration_wrapper_callee( + &self.capture_obj, + self.declaration_wrapper.as_ref().unwrap(), + ) + } + + fn source_loc_from_span(&self, span: Span) -> Expr { + let mut props = vec![ + create_prop( + "start", + Expr::Lit(Lit::Num(Number { + span: DUMMY_SP, + value: span.lo.0.saturating_sub(1) as f64, + raw: None, + })), + ), + create_prop( + "end", + Expr::Lit(Lit::Num(Number { + span: DUMMY_SP, + value: span.hi.0.saturating_sub(1) as f64, + raw: None, + })), + ), + ]; + if let Some(source_accessor_name) = &self.source_accessor_name { + props.push(create_prop( + "moduleSource", + parse_expr_or_ident(source_accessor_name), + )); + } + create_object_lit(props) + } + + /// Wrap a declaration with the declaration wrapper if configured. + /// Scope capture wrapper signature: wrapper(name, kind, value, captureObj, meta?) + /// Babel tests confirm the 4th arg is _rec (capture obj), not __moduleMeta__. + /// __moduleMeta__ is only used in insertCapturesForFunctionDeclarations (the LAST step). + fn wrap_declaration(&self, name: &str, kind: &str, value: Expr, meta: Option) -> Expr { + if self.declaration_wrapper.is_some() { + // capture_obj[wrapper]("name", "kind", value, capture_obj) + // Use computed member access because wrapper names like + // "defVar_http://localhost:9011/module.js" contain characters + // that are invalid in bare JS identifiers. + let mut args = vec![ + to_expr_or_spread(create_string_expr(name)), + to_expr_or_spread(create_string_expr(kind)), + to_expr_or_spread(value), + to_expr_or_spread(create_ident_expr(&self.capture_obj)), + ]; + if let Some(meta) = meta { + args.push(to_expr_or_spread(meta)); + } + create_call_expr(self.create_declaration_wrapper_callee(), args) + } else { + value + } + } + + fn should_wrap_live_declarations(&self) -> bool { + if self.declaration_wrapper.is_none() || self.resurrection { + return false; + } + + let module_id = self.module_id.as_str(); + if module_id.starts_with("esm://") || module_id.starts_with("https://") { + return false; + } + if module_id.starts_with("http://") + && !module_id.starts_with("http://localhost") + && !module_id.starts_with("http://127.0.0.1") + { + return false; + } + + true + } + + fn wrap_live_declaration( + &self, + name: &str, + kind: &str, + value: Expr, + meta: Option, + ) -> Expr { + if self.should_wrap_live_declarations() { + self.wrap_declaration(name, kind, value, meta) + } else { + value + } + } + + /// Check if a pattern includes any capturable identifiers + fn should_capture_pattern(&self, pat: &Pat) -> bool { + extract_idents_from_pat(pat) + .into_iter() + .any(|id| self.should_capture(&id)) + } + + /// Create a default initializer for an uninitialized binding + fn create_default_init(&self, name: &str) -> Expr { + Expr::Cond(CondExpr { + span: DUMMY_SP, + test: Box::new(self.create_recorder_has_own(name)), + cons: Box::new(self.create_captured_member(name)), + alt: Box::new(create_ident_expr("undefined")), + }) + } + + /// Transform a pattern into statements assigning to the capture object + fn transform_pattern_to_stmts( + &self, + pat: &Pat, + init: Option>, + declaration_kind: &str, + meta: Option, + ) -> Vec { + let mut stmts = Vec::new(); + + match pat { + Pat::Ident(BindingIdent { id, .. }) => { + if self.should_capture(&id.to_id()) { + let member = self.create_captured_member(id.sym.as_ref()); + let init_expr = + init.unwrap_or_else(|| Box::new(self.create_default_init(id.sym.as_ref()))); + // Resurrection/freezer builds only wrap function declarations, but + // live lively.modules loads need the wrapper for notifications and + // scheduled export updates. + let value = self.wrap_live_declaration( + id.sym.as_ref(), + declaration_kind, + *init_expr, + meta.clone(), + ); + stmts.push(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(create_assign_expr(expr_to_assign_target(member), value)), + })); + } else if let Some(init_expr) = init { + let kind = match declaration_kind { + "let" => VarDeclKind::Let, + "const" => VarDeclKind::Const, + _ => VarDeclKind::Var, + }; + stmts.push(Stmt::Decl(create_var_decl_with_ident( + kind, + id.clone(), + Some(*init_expr), + ))); + } + } + Pat::Array(ArrayPat { elems, .. }) => { + // For array destructuring: var [a, b] = arr + // Create temp variable: var _tmp = arr + // Then: a = _tmp[0], b = _tmp[1] + if let Some(init_expr) = init { + let temp_name = format!("_tmp_{}", rand_id()); + let temp_ident = create_ident_expr(&temp_name); + + // First assignment: _tmp = init + stmts.push(Stmt::Decl(create_var_decl( + VarDeclKind::Var, + &temp_name, + Some(*init_expr), + ))); + + // Then assign each element + for (i, elem) in elems.iter().enumerate() { + if let Some(elem_pat) = elem { + if let Pat::Rest(rest_pat) = elem_pat { + let slice_call = create_call_expr( + create_member_expr(temp_ident.clone(), "slice"), + vec![to_expr_or_spread(Expr::Lit(Lit::Num(Number { + span: DUMMY_SP, + value: i as f64, + raw: None, + })))], + ); + let rest_stmts = self.transform_pattern_to_stmts( + &rest_pat.arg, + Some(Box::new(slice_call)), + declaration_kind, + meta.clone(), + ); + stmts.extend(rest_stmts); + } else { + let indexed = create_computed_member_expr( + temp_ident.clone(), + Expr::Lit(Lit::Num(Number { + span: DUMMY_SP, + value: i as f64, + raw: None, + })), + ); + let elem_stmts = self.transform_pattern_to_stmts( + elem_pat, + Some(Box::new(indexed)), + declaration_kind, + meta.clone(), + ); + stmts.extend(elem_stmts); + } + } + } + } + } + Pat::Object(ObjectPat { props, .. }) => { + // For object destructuring: var {a, b: c} = obj + // Create temp variable: var _tmp = obj + // Then: a = _tmp.a, c = _tmp.b + if let Some(init_expr) = init { + let temp_name = format!("_tmp_{}", rand_id()); + let temp_ident = create_ident_expr(&temp_name); + + // First assignment: _tmp = init + stmts.push(Stmt::Decl(create_var_decl( + VarDeclKind::Var, + &temp_name, + Some(*init_expr), + ))); + + let known_keys: Vec = props + .iter() + .filter_map(|prop| match prop { + ObjectPatProp::KeyValue(kv) => match &kv.key { + PropName::Ident(id) => Some(id.sym.to_string()), + PropName::Str(s) => Some(s.value.to_string()), + _ => None, + }, + ObjectPatProp::Assign(assign) => Some(assign.key.sym.to_string()), + ObjectPatProp::Rest(_) => None, + }) + .collect(); + + // Then assign each property + for prop in props { + match prop { + ObjectPatProp::KeyValue(kv) => { + let key_name: String = match &kv.key { + PropName::Ident(id) => (&*id.sym).to_owned(), + PropName::Str(s) => s.value.to_string(), + _ => continue, + }; + + let member = create_member_expr(temp_ident.clone(), &key_name); + let prop_stmts = self.transform_pattern_to_stmts( + &kv.value, + Some(Box::new(member)), + declaration_kind, + meta.clone(), + ); + stmts.extend(prop_stmts); + } + ObjectPatProp::Assign(assign) => { + let key_name = assign.key.sym.to_string(); + let member = create_member_expr(temp_ident.clone(), &key_name); + + let value = if let Some(ref default) = assign.value { + // Handle default value: {a = 5} = obj + // Keep parity with legacy transform: + // value === undefined ? default : value + Expr::Cond(CondExpr { + span: DUMMY_SP, + test: Box::new(Expr::Bin(BinExpr { + span: DUMMY_SP, + op: BinaryOp::EqEqEq, + left: Box::new(member.clone()), + right: Box::new(create_ident_expr("undefined")), + })), + cons: default.clone(), + alt: Box::new(member), + }) + } else { + member + }; + + if self.should_capture(&assign.key.to_id()) { + let captured = self.create_captured_member(&key_name); + + stmts.push(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(create_assign_expr( + expr_to_assign_target(captured), + value, + )), + })); + } else { + let kind = match declaration_kind { + "let" => VarDeclKind::Let, + "const" => VarDeclKind::Const, + _ => VarDeclKind::Var, + }; + stmts.push(Stmt::Decl(create_var_decl_with_ident( + kind, + assign.key.id.clone(), + Some(value), + ))); + } + } + ObjectPatProp::Rest(rest) => { + // Handle rest properties: {...rest} + // Legacy transform materializes a new object and copies unknown keys. + if let Pat::Ident(BindingIdent { id: rest_ident, .. }) = &*rest.arg + { + stmts.push(Stmt::Decl(create_var_decl( + VarDeclKind::Var, + rest_ident.sym.as_ref(), + Some(create_object_lit(vec![])), + ))); + + if self.should_capture(&rest_ident.to_id()) { + let captured = + self.create_captured_member(rest_ident.sym.as_ref()); + stmts.push(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(create_assign_expr( + expr_to_assign_target(captured), + Expr::Ident(rest_ident.clone()), + )), + })); + } + + let key_ident = Ident::new( + "__key".into(), + DUMMY_SP, + SyntaxContext::empty(), + ); + let mut for_body_stmts: Vec = known_keys + .iter() + .map(|key| { + Stmt::If(IfStmt { + span: DUMMY_SP, + test: Box::new(Expr::Bin(BinExpr { + span: DUMMY_SP, + op: BinaryOp::EqEqEq, + left: Box::new(Expr::Ident(key_ident.clone())), + right: Box::new(create_string_expr(key)), + })), + cons: Box::new(Stmt::Continue(ContinueStmt { + span: DUMMY_SP, + label: None, + })), + alt: None, + }) + }) + .collect(); + + let rest_member = create_computed_member_expr( + Expr::Ident(rest_ident.clone()), + Expr::Ident(key_ident.clone()), + ); + let source_member = create_computed_member_expr( + temp_ident.clone(), + Expr::Ident(key_ident.clone()), + ); + for_body_stmts.push(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(create_assign_expr( + expr_to_assign_target(rest_member), + source_member, + )), + })); + + let for_in = Stmt::ForIn(ForInStmt { + span: DUMMY_SP, + left: ForHead::VarDecl(Box::new(VarDecl { + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + kind: VarDeclKind::Var, + declare: false, + decls: vec![VarDeclarator { + span: DUMMY_SP, + name: Pat::Ident(BindingIdent { + id: key_ident, + type_ann: None, + }), + init: None, + definite: false, + }], + })), + right: Box::new(temp_ident.clone()), + body: Box::new(Stmt::Block(BlockStmt { + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + stmts: for_body_stmts, + })), + }); + + let iife = Expr::Call(CallExpr { + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + callee: Callee::Expr(Box::new(Expr::Fn(FnExpr { + ident: None, + function: Box::new(Function { + params: vec![], + decorators: vec![], + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + body: Some(BlockStmt { + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + stmts: vec![for_in], + }), + is_generator: false, + is_async: false, + type_params: None, + return_type: None, + }), + }))), + args: vec![], + type_args: None, + }); + + stmts.push(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(iife), + })); + } else { + let rest_stmts = self.transform_pattern_to_stmts( + &rest.arg, + Some(Box::new(temp_ident.clone())), + declaration_kind, + meta.clone(), + ); + stmts.extend(rest_stmts); + } + } + } + } + } + } + Pat::Rest(RestPat { arg, .. }) => { + // Rest pattern in function params or arrays + let rest_stmts = self.transform_pattern_to_stmts(arg, init, declaration_kind, meta); + stmts.extend(rest_stmts); + } + Pat::Assign(AssignPat { left, right, .. }) => { + // Assignment pattern with default: x = 5 + if let Some(init_expr) = init { + let value = Expr::Cond(CondExpr { + span: DUMMY_SP, + test: Box::new(Expr::Bin(BinExpr { + span: DUMMY_SP, + op: BinaryOp::EqEqEq, + left: init_expr.clone(), + right: Box::new(create_ident_expr("undefined")), + })), + cons: right.clone(), + alt: init_expr, + }); + let assign_stmts = self.transform_pattern_to_stmts( + left, + Some(Box::new(value)), + declaration_kind, + meta.clone(), + ); + stmts.extend(assign_stmts); + } else { + let assign_stmts = self.transform_pattern_to_stmts( + left, + Some(right.clone()), + declaration_kind, + meta.clone(), + ); + stmts.extend(assign_stmts); + } + } + _ => {} + } + + stmts + } + + fn transform_assignment_pattern_to_stmts( + &self, + pat: &Pat, + init: Option>, + ) -> Vec { + let mut stmts = Vec::new(); + + match pat { + Pat::Ident(BindingIdent { id, .. }) => { + let init_expr = init.unwrap_or_else(|| Box::new(create_ident_expr("undefined"))); + let (target, value) = if self.should_capture(&id.to_id()) { + ( + self.create_captured_member(id.sym.as_ref()), + self.wrap_live_declaration(id.sym.as_ref(), "assignment", *init_expr, None), + ) + } else { + (Expr::Ident(id.clone()), *init_expr) + }; + stmts.push(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(create_assign_expr(expr_to_assign_target(target), value)), + })); + } + Pat::Array(ArrayPat { elems, .. }) => { + if let Some(init_expr) = init { + let temp_name = format!("_tmp_{}", rand_id()); + let temp_ident = create_ident_expr(&temp_name); + + stmts.push(Stmt::Decl(create_var_decl( + VarDeclKind::Var, + &temp_name, + Some(*init_expr), + ))); + + for (i, elem) in elems.iter().enumerate() { + if let Some(elem_pat) = elem { + let value = if let Pat::Rest(rest_pat) = elem_pat { + let slice_call = create_call_expr( + create_member_expr(temp_ident.clone(), "slice"), + vec![to_expr_or_spread(Expr::Lit(Lit::Num(Number { + span: DUMMY_SP, + value: i as f64, + raw: None, + })))], + ); + self.transform_assignment_pattern_to_stmts( + &rest_pat.arg, + Some(Box::new(slice_call)), + ) + } else { + let indexed = create_computed_member_expr( + temp_ident.clone(), + Expr::Lit(Lit::Num(Number { + span: DUMMY_SP, + value: i as f64, + raw: None, + })), + ); + self.transform_assignment_pattern_to_stmts( + elem_pat, + Some(Box::new(indexed)), + ) + }; + stmts.extend(value); + } + } + } + } + Pat::Object(ObjectPat { props, .. }) => { + if let Some(init_expr) = init { + let temp_name = format!("_tmp_{}", rand_id()); + let temp_ident = create_ident_expr(&temp_name); + + stmts.push(Stmt::Decl(create_var_decl( + VarDeclKind::Var, + &temp_name, + Some(*init_expr), + ))); + + for prop in props { + match prop { + ObjectPatProp::KeyValue(kv) => { + let key_name: String = match &kv.key { + PropName::Ident(id) => (&*id.sym).to_owned(), + PropName::Str(s) => s.value.to_string(), + _ => continue, + }; + let member = create_member_expr(temp_ident.clone(), &key_name); + stmts.extend(self.transform_assignment_pattern_to_stmts( + &kv.value, + Some(Box::new(member)), + )); + } + ObjectPatProp::Assign(assign) => { + let key_name = assign.key.sym.to_string(); + let member = create_member_expr(temp_ident.clone(), &key_name); + let value = if let Some(ref default) = assign.value { + Expr::Cond(CondExpr { + span: DUMMY_SP, + test: Box::new(Expr::Bin(BinExpr { + span: DUMMY_SP, + op: BinaryOp::EqEqEq, + left: Box::new(member.clone()), + right: Box::new(create_ident_expr("undefined")), + })), + cons: default.clone(), + alt: Box::new(member), + }) + } else { + member + }; + stmts.extend(self.transform_assignment_pattern_to_stmts( + &Pat::Ident(assign.key.clone()), + Some(Box::new(value)), + )); + } + ObjectPatProp::Rest(rest) => { + stmts.extend(self.transform_assignment_pattern_to_stmts( + &rest.arg, + Some(Box::new(temp_ident.clone())), + )); + } + } + } + } + } + Pat::Rest(RestPat { arg, .. }) => { + stmts.extend(self.transform_assignment_pattern_to_stmts(arg, init)); + } + Pat::Assign(AssignPat { left, right, .. }) => { + if let Some(init_expr) = init { + let value = Expr::Cond(CondExpr { + span: DUMMY_SP, + test: Box::new(Expr::Bin(BinExpr { + span: DUMMY_SP, + op: BinaryOp::EqEqEq, + left: init_expr.clone(), + right: Box::new(create_ident_expr("undefined")), + })), + cons: right.clone(), + alt: init_expr, + }); + stmts.extend( + self.transform_assignment_pattern_to_stmts(left, Some(Box::new(value))), + ); + } + } + _ => {} + } + + stmts + } + + /// Enter a new scope + fn enter_scope(&mut self, is_function_scope: bool) { + self.depth += 1; + self.scope_stack.push(ScopeFrame { + names: HashSet::new(), + is_function_scope, + }); + } + + /// Pre-scan a block of statements for `var` declarations and add them + /// to the current (function) scope. This simulates JS var hoisting — + /// `var` names are visible from the start of the function, not just + /// from the declaration position. Without this, `should_capture` might + /// incorrectly capture a reference that appears before the `var` decl. + fn prescan_var_decls_in_block(&mut self, stmts: &[Stmt]) { + use swc_ecma_visit::Visit; + struct VarCollector { + /// Names from `var x = fn()/obj/array/...` (substantial init) + var_names: Vec, + /// Names from `function x() {}` declarations + fn_decl_names: Vec, + } + impl Visit for VarCollector { + fn visit_var_decl(&mut self, decl: &VarDecl) { + if matches!(decl.kind, VarDeclKind::Var) { + for d in &decl.decls { + // Only collect vars that have a NON-TRIVIAL initializer. + // `var a;` (no init) or `var a = simple` are hoisting + // artifacts that Babel ignores. But `var Le = function(t){...}` + // is a genuinely new binding that should shadow. + let has_substantial_init = d.init.as_ref().map_or(false, |init| { + matches!( + &**init, + Expr::Fn(_) + | Expr::Arrow(_) + | Expr::Class(_) + | Expr::Call(_) + | Expr::New(_) + | Expr::Object(_) + | Expr::Array(_) + ) + }); + if has_substantial_init { + for (sym, _) in extract_idents_from_pat(&d.name) { + self.var_names.push(sym.to_string()); + } + } + } + } + } + fn visit_fn_decl(&mut self, decl: &FnDecl) { + self.fn_decl_names.push(decl.ident.sym.to_string()); + } + // Don't descend into nested functions — their vars are separate + fn visit_function(&mut self, _: &Function) {} + fn visit_arrow_expr(&mut self, _: &ArrowExpr) {} + } + let mut collector = VarCollector { + var_names: vec![], + fn_decl_names: vec![], + }; + for stmt in stmts { + stmt.visit_children_with(&mut collector); + } + // Function declarations inside nested functions ALWAYS shadow, + // even if the name exists at module level (capturable_vars). + // e.g. crypto.js: `function x() {}` inside dew() is a genuinely + // different binding from module-level `var x`. + for name in &collector.fn_decl_names { + self.declare_in_nearest_function_scope(name); + } + // Var declarations with substantial initializers ALWAYS shadow. + // The has_substantial_init check already filters out bare `var a;` + // (hoisting artifacts that Babel ignores), so no capturable_vars + // check is needed here. + // - fs.js: `var Le = function(t){...}(parent)` inside dew() — + // Call init (substantial) → shadows even though Le is module-level. + // - @babel/generator: `var a;` inside s() — no init → not collected + // → module-level `a` stays captured. + for name in &collector.var_names { + self.declare_in_nearest_function_scope(name); + } + } + + fn declare_in_current_scope(&mut self, name: &str) { + if let Some(scope) = self.scope_stack.last_mut() { + scope.names.insert(name.to_string()); + } + } + + fn declare_in_nearest_function_scope(&mut self, name: &str) { + if let Some(scope) = self + .scope_stack + .iter_mut() + .rev() + .find(|scope| scope.is_function_scope) + { + scope.names.insert(name.to_string()); + } + } + + fn declare_pattern_in_current_scope(&mut self, pat: &Pat) { + for (sym, _) in extract_idents_from_pat(pat) { + self.declare_in_current_scope(sym.as_ref()); + } + } + + fn collect_loop_header_vars(&mut self, module: &Module) { + struct Collector { + names: HashSet, + fn_depth: usize, + } + + impl Visit for Collector { + fn visit_function(&mut self, func: &Function) { + self.fn_depth += 1; + func.visit_children_with(self); + self.fn_depth -= 1; + } + + fn visit_arrow_expr(&mut self, arrow: &ArrowExpr) { + self.fn_depth += 1; + arrow.visit_children_with(self); + self.fn_depth -= 1; + } + + fn visit_for_stmt(&mut self, stmt: &ForStmt) { + if self.fn_depth == 0 { + if let Some(VarDeclOrExpr::VarDecl(var_decl)) = &stmt.init { + for decl in &var_decl.decls { + for (sym, _) in extract_idents_from_pat(&decl.name) { + self.names.insert(sym.to_string()); + } + } + } + } + stmt.visit_children_with(self); + } + + fn visit_for_in_stmt(&mut self, stmt: &ForInStmt) { + if self.fn_depth == 0 { + match &stmt.left { + ForHead::VarDecl(var_decl) => { + for decl in &var_decl.decls { + for (sym, _) in extract_idents_from_pat(&decl.name) { + self.names.insert(sym.to_string()); + } + } + } + ForHead::Pat(pat) => { + for (sym, _) in extract_idents_from_pat(pat) { + self.names.insert(sym.to_string()); + } + } + _ => {} + } + } + stmt.visit_children_with(self); + } + + fn visit_for_of_stmt(&mut self, stmt: &ForOfStmt) { + if self.fn_depth == 0 { + match &stmt.left { + ForHead::VarDecl(var_decl) => { + for decl in &var_decl.decls { + for (sym, _) in extract_idents_from_pat(&decl.name) { + self.names.insert(sym.to_string()); + } + } + } + ForHead::Pat(pat) => { + for (sym, _) in extract_idents_from_pat(pat) { + self.names.insert(sym.to_string()); + } + } + _ => {} + } + } + stmt.visit_children_with(self); + } + } + + let mut collector = Collector { + names: HashSet::new(), + fn_depth: 0, + }; + module.visit_with(&mut collector); + self.loop_header_vars = collector.names; + } + + /// Replace top-level function declarations with wrapper calls (Babel parity). + /// + /// Transforms: + /// function foo() { ... } + /// into: + /// var foo = wrapper("foo", "function", function() { ... }, __moduleMeta__) + /// + /// Also prepends `let __moduleMeta__ = ` to the module body. + fn replace_function_declarations_with_wrapper(&self, module: &mut Module) { + // Build: let __moduleMeta__ = + let meta_init = if let Some(ref accessor) = self.current_module_accessor { + // The currentModuleAccessor is a complex JS expression (object literal). + // Use SWC's parser to parse it properly. + use swc_common::{sync::Lrc, FileName, SourceMap as SwcSourceMap}; + use swc_ecma_parser::{parse_file_as_expr, Syntax}; + let cm = Lrc::new(SwcSourceMap::default()); + let fm = cm.new_source_file(FileName::Anon.into(), accessor.clone()); + match parse_file_as_expr( + &fm, + Syntax::Es(Default::default()), + Default::default(), + None, + &mut vec![], + ) { + Ok(expr) => *expr, + Err(_) => parse_expr_or_ident(accessor), + } + } else { + create_ident_expr("undefined") + }; + let meta_decl = ModuleItem::Stmt(Stmt::Decl(create_var_decl( + VarDeclKind::Var, + "__moduleMeta__", + Some(meta_init), + ))); + + // Two-pass approach matching Babel's putFunctionDeclsInFront + + // insertCapturesForFunctionDeclarations: + // 1. Extract all top-level FunctionDeclarations, replace them with references (foo;) + // 2. Create let declarations with wrapper calls, hoisted to top + + let mut hoisted_lets: Vec = Vec::new(); + let mut new_body = Vec::with_capacity(module.body.len() + 1); + + for item in module.body.drain(..) { + match item { + ModuleItem::Stmt(Stmt::Decl(Decl::Fn(fn_decl))) => { + let fn_name = fn_decl.ident.sym.to_string(); + // Create anonymous function expression (id removed) + let anon_fn = Expr::Fn(FnExpr { + ident: None, + function: fn_decl.function, + }); + // Hoisted: var foo = wrapper("foo", "function", function(){...}, __moduleMeta__) + // Babel uses let, but var avoids TDZ issues with rollup circular dep analysis. + let wrapped = create_call_expr( + self.create_declaration_wrapper_callee(), + vec![ + to_expr_or_spread(create_string_expr(&fn_name)), + to_expr_or_spread(create_string_expr("function")), + to_expr_or_spread(anon_fn), + to_expr_or_spread(create_ident_expr("__moduleMeta__")), + ], + ); + // Use the ORIGINAL ident (preserves SyntaxContext) to avoid + // SWC hygiene renaming the var to Path1/foo1/etc. + let original_ident = fn_decl.ident.clone(); + let var_decl = ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(VarDecl { + span: DUMMY_SP, + kind: VarDeclKind::Var, + decls: vec![VarDeclarator { + span: DUMMY_SP, + name: Pat::Ident(BindingIdent { + id: original_ident.clone(), + type_ann: None, + }), + init: Some(Box::new(wrapped)), + definite: false, + }], + ..Default::default() + })))); + hoisted_lets.push(var_decl); + // Also add recorder capture: __rec.foo = foo + // (hoisted captures are skipped for these when declaration_wrapper is set) + let capture = ModuleItem::Stmt(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(create_assign_expr( + expr_to_assign_target(self.create_captured_member(&fn_name)), + Expr::Ident(original_ident.clone()), + )), + })); + hoisted_lets.push(capture); + // Replace original position with reference: foo; + new_body.push(ModuleItem::Stmt(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(Expr::Ident(original_ident)), + }))); + } + _ => { + new_body.push(item); + } + } + } + + // Insert hoisted declarations after recorder init: + // imports → recorder init → __moduleMeta__ → hoisted lets → rest of body + let mut final_body = Vec::with_capacity(new_body.len() + hoisted_lets.len() + 1); + let mut inserted = false; + for item in new_body.drain(..) { + final_body.push(item); + if !inserted { + if let ModuleItem::Stmt(Stmt::Decl(Decl::Var(ref var_decl))) = + final_body.last().unwrap() + { + if var_decl.decls.iter().any(|d| { + matches!(&d.name, Pat::Ident(BindingIdent { id, .. }) if id.sym.as_ref() == self.capture_obj) + }) { + final_body.push(meta_decl.clone()); + for let_decl in hoisted_lets.drain(..) { + final_body.push(let_decl); + } + inserted = true; + } + } + } + } + if !inserted { + // No recorder init found — insert at the beginning + let mut prefix = vec![meta_decl]; + prefix.extend(hoisted_lets); + prefix.extend(final_body); + module.body = prefix; + } else { + module.body = final_body; + } + } + + /// Build the recorder runtime expression: + /// (lively.FreezerRuntime || lively.frozenModules) + /// or, if `lively` is locally bound: + /// (globalThis.lively.FreezerRuntime || globalThis.lively.frozenModules) + fn recorder_runtime_expr(&self, has_local_lively_binding: bool) -> Expr { + let lively_expr = if has_local_lively_binding { + create_member_expr(create_ident_expr("globalThis"), "lively") + } else { + create_ident_expr("lively") + }; + + Expr::Bin(BinExpr { + span: DUMMY_SP, + op: BinaryOp::LogicalOr, + left: Box::new(create_member_expr(lively_expr.clone(), "FreezerRuntime")), + right: Box::new(create_member_expr(lively_expr, "frozenModules")), + }) + } + + /// Build the recorder init declaration. + /// In lively.modules/browser context (current_module_accessor set): + /// const __varRecorder__ = System.get("@lively-env").moduleEnv("").recorder; + /// In FreezerRuntime/bundle context: + /// const __varRecorder__ = (..runtime..).recorderFor("", __contextModule__); + fn create_recorder_init_decl(&self, has_local_lively_binding: bool) -> ModuleItem { + let recorder_init = if self.current_module_accessor.is_some() { + // Browser/lively.modules context: recorder lives on the lively-env module environment + create_member_expr( + create_call_expr( + create_member_expr( + create_call_expr( + create_member_expr(create_ident_expr("System"), "get"), + vec![to_expr_or_spread(create_string_expr("@lively-env"))], + ), + "moduleEnv", + ), + vec![to_expr_or_spread(create_string_expr(&self.module_id))], + ), + "recorder", + ) + } else { + // FreezerRuntime/bundle context + create_call_expr( + create_member_expr( + self.recorder_runtime_expr(has_local_lively_binding), + "recorderFor", + ), + vec![ + to_expr_or_spread(create_string_expr(&self.module_id)), + to_expr_or_spread(create_ident_expr("__contextModule__")), + ], + ) + }; + + ModuleItem::Stmt(Stmt::Decl(create_var_decl( + VarDeclKind::Const, + &self.capture_obj, + Some(recorder_init), + ))) + } + + /// Build import.meta replacement expression. + /// Uses the module_id directly as the URL since it's known at bundle time. + fn create_import_meta_expr(&self) -> Expr { + let url_expr = if !self.module_id.is_empty() { + create_string_expr(&self.module_id) + } else if let Some(accessor) = &self.current_module_accessor { + create_member_expr(parse_expr_or_ident(accessor), "id") + } else { + create_ident_expr(r#"eval("typeof _context !== 'undefined' ? _context : {}").id"#) + }; + create_object_lit(vec![create_prop("url", url_expr)]) + } + + /// Build `__varRecorder__.name = ...` for a function declaration capture. + fn create_function_capture_assignment(&self, fn_decl: &FnDecl) -> ModuleItem { + let fn_name = fn_decl.ident.sym.to_string(); + let member = self.create_captured_member(&fn_name); + // Babel's putFunctionDeclsInFront wraps just the identifier, not the function body. + // The function body replacement happens later in replace_function_declarations_with_wrapper. + let value = self.wrap_declaration( + &fn_name, + "function", + Expr::Ident(fn_decl.ident.clone()), + Some(self.source_loc_from_span(fn_decl.function.span)), + ); + + ModuleItem::Stmt(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(create_assign_expr(expr_to_assign_target(member), value)), + })) + } + + /// Build `__varRecorder__.name = ...` for a class declaration capture. + /// Babel's bundler does NOT wrap class declarations with declarationWrapper — + /// only insertCapturesForFunctionDeclarations wraps function declarations. + fn create_class_capture_assignment(&self, class_decl: &ClassDecl) -> ModuleItem { + let class_name = class_decl.ident.sym.to_string(); + let member = self.create_captured_member(&class_name); + let value = Expr::Ident(class_decl.ident.clone()); + + ModuleItem::Stmt(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(create_assign_expr(expr_to_assign_target(member), value)), + })) + } + + /// Collect function captures that need to run before any top-level statements. + /// When resurrection + declaration_wrapper is set, function declarations will + /// be fully replaced by `replace_function_declarations_with_wrapper`, so we + /// skip hoisted captures for plain function declarations (they would be + /// redundant and reference __moduleMeta__ before it's declared). + fn collect_hoisted_function_capture(&mut self, item: &ModuleItem) { + let will_replace_func_decls = self.resurrection && self.declaration_wrapper.is_some(); + match item { + ModuleItem::Stmt(Stmt::Decl(Decl::Fn(fn_decl))) => { + if will_replace_func_decls { + return; // Will be handled by replace_function_declarations_with_wrapper + } + if self.should_capture(&fn_decl.ident.to_id()) { + self.hoisted_function_captures + .push(self.create_function_capture_assignment(fn_decl)); + } + } + ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export_decl)) => { + if let Decl::Fn(fn_decl) = &export_decl.decl { + if self.should_capture(&fn_decl.ident.to_id()) { + self.hoisted_function_captures + .push(self.create_function_capture_assignment(fn_decl)); + } + } + } + _ => {} + } + } + + /// Insert hoisted function capture assignments after imports and recorder init. + fn insert_hoisted_function_captures(&self, module: &mut Module, inserted_recorder_init: bool) { + if self.hoisted_function_captures.is_empty() { + return; + } + + let mut insert_idx = module + .body + .iter() + .take_while(|item| matches!(item, ModuleItem::ModuleDecl(ModuleDecl::Import(_)))) + .count(); + + if inserted_recorder_init { + insert_idx += 1; + if self.source_accessor_name.is_some() && self.original_source.is_some() { + insert_idx += 1; + } + if self.resurrection && self.module_hash.is_some() { + insert_idx += 1; + } + } + + for (offset, item) in self.hoisted_function_captures.iter().cloned().enumerate() { + module.body.insert(insert_idx + offset, item); + } + } + + /// Collect export metadata from the module for __module_exports__. + /// Follows the same format as the Babel bundler path. + fn collect_module_exports(&mut self, module: &Module) { + for item in &module.body { + match item { + ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) => { + if export.src.is_some() { + let raw_src = export.src.as_ref().unwrap().value.to_string(); + let resolved_src = self + .resolved_imports + .get(&raw_src) + .cloned() + .unwrap_or(raw_src); + if export.specifiers.is_empty() { + // export * from '...' (rare, usually ExportAll) + self.collected_exports + .push(format!("__reexport__{}", resolved_src)); + } else { + // export { x, y as z } from '...' + // Per-specifier entries (Babel does individual entries, not blanket __reexport__) + for spec in &export.specifiers { + if let ExportSpecifier::Named(named) = spec { + let local_name = match &named.orig { + ModuleExportName::Ident(id) => id.sym.to_string(), + ModuleExportName::Str(s) => s.value.to_string(), + }; + let exported_name = match &named.exported { + Some(ModuleExportName::Ident(id)) => id.sym.to_string(), + Some(ModuleExportName::Str(s)) => s.value.to_string(), + None => local_name.clone(), + }; + if exported_name != local_name && exported_name != "default" { + self.collected_exports.push(format!( + "__rename__{}->{}", + local_name, exported_name + )); + continue; // Babel: continue after __rename__ + } + if exported_name == "default" && !local_name.is_empty() { + self.collected_exports + .push(format!("__default__{}", local_name)); + } + self.collected_exports.push(exported_name); + } + } + } + } else { + // `export { x, y as z }` + for spec in &export.specifiers { + if let ExportSpecifier::Named(named) = spec { + let local_name = match &named.orig { + ModuleExportName::Ident(id) => id.sym.to_string(), + ModuleExportName::Str(s) => s.value.to_string(), + }; + let exported_name = match &named.exported { + Some(ModuleExportName::Ident(id)) => id.sym.to_string(), + Some(ModuleExportName::Str(s)) => s.value.to_string(), + None => local_name.clone(), + }; + if exported_name != local_name && exported_name != "default" { + self.collected_exports.push(format!( + "__rename__{}->{}", + local_name, exported_name + )); + continue; // Babel: continue after __rename__ + } + if exported_name == "default" && !local_name.is_empty() { + self.collected_exports + .push(format!("__default__{}", local_name)); + } + self.collected_exports.push(exported_name); + } + } + } + } + ModuleItem::ModuleDecl(ModuleDecl::ExportAll(export_all)) => { + let raw_src = export_all.src.value.to_string(); + let resolved_src = self + .resolved_imports + .get(&raw_src) + .cloned() + .unwrap_or(raw_src); + self.collected_exports + .push(format!("__reexport__{}", resolved_src)); + } + ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export_decl)) => { + match &export_decl.decl { + Decl::Fn(fn_decl) => { + self.collected_exports.push(fn_decl.ident.sym.to_string()); + } + Decl::Class(class_decl) => { + self.collected_exports + .push(class_decl.ident.sym.to_string()); + } + Decl::Var(var_decl) => { + for decl in &var_decl.decls { + for (sym, _) in extract_idents_from_pat(&decl.name) { + self.collected_exports.push(sym.to_string()); + } + } + } + _ => {} + } + } + ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(default_decl)) => { + let local = match &default_decl.decl { + DefaultDecl::Fn(f) => f.ident.as_ref().map(|id| id.sym.to_string()), + DefaultDecl::Class(c) => c.ident.as_ref().map(|id| id.sym.to_string()), + _ => None, + }; + if let Some(name) = local { + self.collected_exports.push(format!("__default__{}", name)); + } + self.collected_exports.push("default".to_string()); + } + ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(default_expr)) => { + if let Expr::Ident(ident) = &*default_expr.expr { + self.collected_exports + .push(format!("__default__{}", ident.sym)); + } + self.collected_exports.push("default".to_string()); + } + _ => {} + } + } + } + + /// Insert recorder initialization directly after static imports. + fn insert_recorder_init(&self, module: &mut Module, has_local_lively_binding: bool) { + let insert_idx = module + .body + .iter() + .take_while(|item| matches!(item, ModuleItem::ModuleDecl(ModuleDecl::Import(_)))) + .count(); + module.body.insert( + insert_idx, + self.create_recorder_init_decl(has_local_lively_binding), + ); + let mut next_insert_idx = insert_idx + 1; + if let (Some(source_accessor_name), Some(original_source)) = + (&self.source_accessor_name, &self.original_source) + { + module.body.insert( + next_insert_idx, + ModuleItem::Stmt(Stmt::Decl(create_var_decl( + VarDeclKind::Var, + source_accessor_name, + Some(create_string_expr(original_source)), + ))), + ); + next_insert_idx += 1; + } + // For resurrection builds, insert __module_hash__ right after recorder init + if self.resurrection { + if let Some(hash) = self.module_hash { + let hash_stmt = ModuleItem::Stmt(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(Expr::Assign(AssignExpr { + span: DUMMY_SP, + op: AssignOp::Assign, + left: AssignTarget::Simple(SimpleAssignTarget::Member(MemberExpr { + span: DUMMY_SP, + obj: Box::new(create_ident_expr(&self.capture_obj)), + prop: MemberProp::Ident(IdentName::new( + "__module_hash__".into(), + DUMMY_SP, + )), + })), + right: Box::new(Expr::Lit(Lit::Num(Number { + span: DUMMY_SP, + value: hash as f64, + raw: None, + }))), + })), + })); + module.body.insert(next_insert_idx, hash_stmt); + } + } + } + + /// Exit a scope + fn exit_scope(&mut self) { + self.depth -= 1; + self.scope_stack.pop(); + } + + /// Transform a module item at module level, allowing expansion into multiple items + fn transform_module_item(&self, item: ModuleItem) -> Vec { + match item { + ModuleItem::Stmt(Stmt::Decl(Decl::Var(var_decl))) => { + let mut items = Vec::new(); + + for decl in &var_decl.decls { + let mut init = decl.init.clone(); + if self.module_id.ends_with("lively.morphic/config.js") { + if let Some(init_expr) = &init { + if matches!(**init_expr, Expr::Object(_)) { + if let Pat::Ident(BindingIdent { id, .. }) = &decl.name { + let recorder_member = + self.create_captured_member(id.sym.as_ref()); + init = Some(Box::new(Expr::Bin(BinExpr { + span: DUMMY_SP, + op: BinaryOp::LogicalOr, + left: Box::new(recorder_member), + right: init_expr.clone(), + }))); + } + } + } + } + if self.should_capture_pattern(&decl.name) { + // After classToFunction, `class X` becomes `var X = IIFE(...)`. + // Detect this pattern and use "class" as the kind so the + // declarationWrapper callback sets lively-module-meta correctly. + let is_class_to_function = init.as_ref().map_or(false, |expr| { + if let Expr::Call(call) = &**expr { + // classToFunction wraps as: (function(superclass) { ... return initializeES6ClassForLively(...) })({...}) + if let swc_ecma_ast::Callee::Expr(callee) = &call.callee { + if let Expr::Paren(paren) = &**callee { + if let Expr::Fn(_) = &*paren.expr { + return true; + } + } + if let Expr::Fn(_) = &**callee { + return true; + } + } + } + false + }); + let declaration_kind = if is_class_to_function { + "class" + } else { + match var_decl.kind { + VarDeclKind::Var => "var", + VarDeclKind::Let => "let", + VarDeclKind::Const => "const", + } + }; + let meta = if is_class_to_function { + Some(self.source_loc_from_span(decl.span)) + } else { + None + }; + let stmts = self.transform_pattern_to_stmts( + &decl.name, + init.clone(), + declaration_kind, + meta, + ); + for stmt in stmts { + items.push(ModuleItem::Stmt(stmt)); + } + } else { + let single_var = VarDecl { + span: var_decl.span, + ctxt: var_decl.ctxt, + kind: var_decl.kind, + declare: var_decl.declare, + decls: vec![VarDeclarator { + init, + ..decl.clone() + }], + }; + items.push(ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new( + single_var, + ))))); + } + } + + items + } + ModuleItem::ModuleDecl(ModuleDecl::Import(import_decl)) => { + // Namespace import handling (import * as ns from 'dep') is done by + // NamespaceTransform (step 4) which runs before scope capture. + // The scope capture just needs to handle capture_imports if enabled. + if !self.capture_imports || import_decl.specifiers.is_empty() { + return vec![ModuleItem::ModuleDecl(ModuleDecl::Import(import_decl))]; + } + // Workaround for SWC system_js bug: when an import combines + // default + named specifiers (`import X, { y } from 'mod'`), + // system_js uses the default import's name as the setter + // parameter, shadowing the local variable. Rename the + // default specifier to a unique name and add an alias var. + let has_default = import_decl + .specifiers + .iter() + .any(|s| matches!(s, ImportSpecifier::Default(_))); + let has_named = import_decl + .specifiers + .iter() + .any(|s| matches!(s, ImportSpecifier::Named(_))); + let mut items = if self.rewrite_mixed_default_imports && has_default && has_named { + // Rename default import: `import X, {y} from 'mod'` + // → `import __default_X__, {y} from 'mod'; var X = __default_X__;` + let mut new_specs = Vec::new(); + let mut alias_stmts = Vec::new(); + for spec in &import_decl.specifiers { + match spec { + ImportSpecifier::Default(def) => { + let orig_name = def.local.sym.to_string(); + let temp_name = format!("__default_{}__", orig_name); + new_specs.push(ImportSpecifier::Default(ImportDefaultSpecifier { + span: DUMMY_SP, + local: Ident::new( + temp_name.as_str().into(), + DUMMY_SP, + SyntaxContext::empty(), + ), + })); + alias_stmts.push(ModuleItem::Stmt(Stmt::Decl(create_var_decl( + VarDeclKind::Var, + &orig_name, + Some(Expr::Ident(Ident::new( + temp_name.as_str().into(), + DUMMY_SP, + SyntaxContext::empty(), + ))), + )))); + } + other => new_specs.push(other.clone()), + } + } + let mut v = vec![ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { + specifiers: new_specs, + ..import_decl.clone() + }))]; + v.extend(alias_stmts); + v + } else { + vec![ModuleItem::ModuleDecl(ModuleDecl::Import( + import_decl.clone(), + ))] + }; + for spec in &import_decl.specifiers { + let local = match spec { + ImportSpecifier::Named(named) => named.local.clone(), + ImportSpecifier::Default(def) => def.local.clone(), + ImportSpecifier::Namespace(ns) => ns.local.clone(), + }; + let member = self.create_captured_member(local.sym.as_ref()); + let assign = + create_assign_expr(expr_to_assign_target(member), Expr::Ident(local)); + items.push(ModuleItem::Stmt(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(assign), + }))); + } + items + } + ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export_decl)) => { + // Split `export { x } from '...'` into import + export + recorder capture. + // Only for capture_imports mode (source-map builds). + // Resurrection builds use a different approach below. + if self.capture_imports && export_decl.src.is_some() { + // Keep the original re-export as-is (system_js handles + // `export { X } from 'mod'` correctly via the setter). + // Add a SEPARATE import so scope capture can access the + // bindings via the recorder. Previously this replaced the + // re-export with a local `export { X }` which system_js + // silently drops for imported bindings. + let src = export_decl.src.as_ref().unwrap().value.to_string(); + let mut import_specs = Vec::new(); + let mut assigns = Vec::new(); + let mut alias_specs = Vec::new(); + + for spec in &export_decl.specifiers { + if let ExportSpecifier::Named(named) = spec { + let exported_name = match &named.exported { + Some(ModuleExportName::Ident(id)) => id.sym.to_string(), + Some(ModuleExportName::Str(s)) => s.value.to_string(), + None => match &named.orig { + ModuleExportName::Ident(id) => id.sym.to_string(), + ModuleExportName::Str(s) => s.value.to_string(), + }, + }; + + let import_name = match &named.orig { + ModuleExportName::Ident(id) => id.sym.to_string(), + ModuleExportName::Str(s) => s.value.to_string(), + }; + + let local_name = format!("__reexport_{}__", exported_name); + + import_specs.push(ImportSpecifier::Named(ImportNamedSpecifier { + span: DUMMY_SP, + local: Ident::new( + local_name.as_str().into(), + DUMMY_SP, + SyntaxContext::empty(), + ), + imported: Some(ModuleExportName::Ident(Ident::new( + import_name.as_str().into(), + DUMMY_SP, + SyntaxContext::empty(), + ))), + is_type_only: false, + })); + + let member = self.create_captured_member(&exported_name); + let assign = create_assign_expr( + expr_to_assign_target(member), + Expr::Ident(Ident::new( + local_name.as_str().into(), + DUMMY_SP, + SyntaxContext::empty(), + )), + ); + assigns.push(ModuleItem::Stmt(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(assign), + }))); + + let alias_name = format!("__export_{}__", exported_name); + assigns.push(ModuleItem::Stmt(Stmt::Decl(create_var_decl( + VarDeclKind::Var, + &alias_name, + Some(create_ident_expr(&local_name)), + )))); + alias_specs.push(ExportSpecifier::Named(ExportNamedSpecifier { + span: DUMMY_SP, + orig: ModuleExportName::Ident(Ident::new( + alias_name.as_str().into(), + DUMMY_SP, + SyntaxContext::empty(), + )), + exported: Some(ModuleExportName::Ident(Ident::new( + exported_name.as_str().into(), + DUMMY_SP, + SyntaxContext::empty(), + ))), + is_type_only: false, + })); + } + } + + let import_decl = ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { + span: DUMMY_SP, + specifiers: import_specs, + src: Box::new(Str { + span: DUMMY_SP, + value: src.clone().into(), + raw: None, + }), + type_only: false, + with: None, + phase: Default::default(), + })); + + let mut items = vec![import_decl]; + items.extend(assigns); + if !alias_specs.is_empty() { + items.push(ModuleItem::ModuleDecl(ModuleDecl::ExportNamed( + NamedExport { + span: DUMMY_SP, + specifiers: alias_specs, + src: None, + type_only: false, + with: None, + }, + ))); + } + return items; + } + // Workaround for SWC system_js bug #2: system_js drops + // `export { importedBinding }` (no _export call generated). + // Materialize imported exports as local aliases instead: + // `var __export_x__ = x; export { __export_x__ as x }`. + // This is not a live binding, but it matches the existing + // browser/runtime need and forces a concrete `_export`. + if export_decl.src.is_none() { + let mut items: Vec = Vec::new(); + let mut local_specs: Vec = Vec::new(); + let mut alias_specs: Vec = Vec::new(); + + for spec in &export_decl.specifiers { + let local_name = match spec { + ExportSpecifier::Named(n) => match &n.orig { + ModuleExportName::Ident(id) => Some(id.sym.to_string()), + _ => None, + }, + _ => None, + }; + let src_info = local_name.as_ref().and_then(|n| self.import_sources.get(n)); + // Skip namespace imports ("*") — they can't be + // re-exported as named specifiers in this path. + if let (Some(ExportSpecifier::Named(named)), Some((_src, orig_name))) = + (Some(spec), src_info) + { + if orig_name == "*" { + local_specs.push(spec.clone()); + continue; + } + let local_name_str = local_name.unwrap(); + let exported_name = match &named.exported { + Some(ModuleExportName::Ident(id)) => id.sym.to_string(), + Some(ModuleExportName::Str(s)) => s.value.to_string(), + None => local_name_str.clone(), + }; + let alias_name = format!("__export_{}__", exported_name); + items.push(ModuleItem::Stmt(Stmt::Decl(create_var_decl( + VarDeclKind::Var, + &alias_name, + Some(create_ident_expr(&local_name_str)), + )))); + alias_specs.push(ExportSpecifier::Named(ExportNamedSpecifier { + span: DUMMY_SP, + orig: ModuleExportName::Ident(Ident::new( + alias_name.as_str().into(), + DUMMY_SP, + SyntaxContext::empty(), + )), + exported: Some(ModuleExportName::Ident(Ident::new( + exported_name.as_str().into(), + DUMMY_SP, + SyntaxContext::empty(), + ))), + is_type_only: false, + })); + } else { + local_specs.push(spec.clone()); + } + } + + if !alias_specs.is_empty() { + if !local_specs.is_empty() { + items.push(ModuleItem::ModuleDecl(ModuleDecl::ExportNamed( + NamedExport { + span: DUMMY_SP, + specifiers: local_specs, + src: None, + type_only: false, + with: None, + }, + ))); + } + items.push(ModuleItem::ModuleDecl(ModuleDecl::ExportNamed( + NamedExport { + span: DUMMY_SP, + specifiers: alias_specs, + src: None, + type_only: false, + with: None, + }, + ))); + return items; + } + } + + // Desugar `export { X as Y }` → `var __export_Y__ = X; export { __export_Y__ }` + // to prevent system_js from hoisting the short alias `Y` that + // collides with locals in minified code. The long prefix + // `__export_` avoids collisions. Phase 3 (restore_export_aliases) + // rewrites _export("__export_Y__", ...) → _export("Y", ...). + let has_alias = export_decl.src.is_none() + && export_decl + .specifiers + .iter() + .any(|s| matches!(s, ExportSpecifier::Named(n) if n.exported.is_some())); + if has_alias { + let mut items = Vec::new(); + let mut new_specs = Vec::new(); + for spec in &export_decl.specifiers { + if let ExportSpecifier::Named(named) = spec { + if let Some(ref exported) = named.exported { + let exported_name = match exported { + ModuleExportName::Ident(id) => id.sym.to_string(), + ModuleExportName::Str(s) => s.value.to_string(), + }; + let local_ident = match &named.orig { + ModuleExportName::Ident(id) => id.clone(), + ModuleExportName::Str(_) => { + new_specs.push(spec.clone()); + continue; + } + }; + // Only desugar if the orig name is a module-level + // declaration (in capturable_vars). CJS-to-ESM + // converters can create exports referencing + // function-scoped vars (e.g. `export { Le as X }` + // where Le is inside dew()). Skip these. + if !self + .capturable_vars + .iter() + .any(|id| id.0.as_ref() == local_ident.sym.as_ref()) + { + new_specs.push(spec.clone()); + continue; + } + // Use a long internal name to avoid collision + let internal_name = format!("__export_{}__", exported_name); + items.push(ModuleItem::Stmt(Stmt::Decl(create_var_decl( + VarDeclKind::Var, + &internal_name, + Some(Expr::Ident(local_ident)), + )))); + new_specs.push(ExportSpecifier::Named(ExportNamedSpecifier { + span: DUMMY_SP, + orig: ModuleExportName::Ident(Ident::new( + internal_name.as_str().into(), + DUMMY_SP, + SyntaxContext::empty(), + )), + exported: Some(ModuleExportName::Ident(Ident::new( + exported_name.as_str().into(), + DUMMY_SP, + SyntaxContext::empty(), + ))), + is_type_only: false, + })); + } else { + new_specs.push(spec.clone()); + } + } else { + new_specs.push(spec.clone()); + } + } + items.push(ModuleItem::ModuleDecl(ModuleDecl::ExportNamed( + NamedExport { + span: DUMMY_SP, + specifiers: new_specs, + src: None, + type_only: false, + with: None, + }, + ))); + items + } else { + vec![ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export_decl))] + } + } + ModuleItem::ModuleDecl(ModuleDecl::ExportAll(export_all)) => { + // For capture_imports builds, add namespace import + Object.assign. + // Resurrection builds skip this — NamespaceTransform (step 4) handles it. + if self.capture_imports && !self.resurrection { + let src = export_all.src.value.to_string(); + let resolved_id = self + .resolved_imports + .get(&src) + .cloned() + .unwrap_or_else(|| src.clone()); + let tmp_name = format!("__captured_export_all_{}__", rand_id()); + let import_decl = ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { + span: DUMMY_SP, + specifiers: vec![ImportSpecifier::Namespace(ImportStarAsSpecifier { + span: DUMMY_SP, + local: Ident::new( + tmp_name.as_str().into(), + DUMMY_SP, + SyntaxContext::empty(), + ), + })], + src: Box::new(Str { + span: DUMMY_SP, + value: src.into(), + raw: None, + }), + type_only: false, + with: None, + phase: Default::default(), + })); + let mut items = vec![ + ModuleItem::ModuleDecl(ModuleDecl::ExportAll(export_all)), + import_decl, + ]; + + // The normal namespace import capture already copies + // export-star bindings into the current module recorder. + // The extra dependency-recorder write belongs to the + // freezer/runtime path; in browser/lively.modules mode + // `__contextModule__` is not available here. + if self.current_module_accessor.is_none() { + // Object.assign(recorderFor(resolvedDep), tmp) + let recorder_for = create_call_expr( + create_member_expr( + Expr::Paren(ParenExpr { + span: DUMMY_SP, + expr: Box::new(Expr::Bin(BinExpr { + span: DUMMY_SP, + op: BinaryOp::LogicalOr, + left: Box::new(create_member_expr( + create_ident_expr("lively"), + "FreezerRuntime", + )), + right: Box::new(create_member_expr( + create_ident_expr("lively"), + "frozenModules", + )), + })), + }), + "recorderFor", + ), + vec![to_expr_or_spread(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: resolved_id.as_str().into(), + raw: None, + })))], + ); + items.push(ModuleItem::Stmt(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(create_call_expr( + create_member_expr(create_ident_expr("Object"), "assign"), + vec![ + to_expr_or_spread(recorder_for), + to_expr_or_spread(create_ident_expr(&tmp_name)), + ], + )), + }))); + } + return items; + } + vec![ModuleItem::ModuleDecl(ModuleDecl::ExportAll(export_all))] + } + ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export_decl)) => { + let mut items = vec![ModuleItem::ModuleDecl(ModuleDecl::ExportDecl( + export_decl.clone(), + ))]; + + match &export_decl.decl { + Decl::Class(class_decl) => { + if self.should_capture(&class_decl.ident.to_id()) { + items.push(self.create_class_capture_assignment(class_decl)); + } + } + Decl::Var(var_decl) => { + for decl in &var_decl.decls { + let ids = extract_idents_from_pat(&decl.name); + for (sym, ctxt) in ids { + let id = (sym.clone(), ctxt); + if !self.should_capture(&id) { + continue; + } + let name = sym.to_string(); + let ident = Ident::new(sym, DUMMY_SP, ctxt); + let member = self.create_captured_member(&name); + // Babel's bundler does NOT wrap exported var captures with + // declarationWrapper — only function declarations get wrapped. + let value = Expr::Ident(ident); + let assign = + create_assign_expr(expr_to_assign_target(member), value); + items.push(ModuleItem::Stmt(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(assign), + }))); + } + } + } + _ => {} + } + + items + } + ModuleItem::Stmt(Stmt::Decl(Decl::Fn(fn_decl))) => { + vec![ModuleItem::Stmt(Stmt::Decl(Decl::Fn(fn_decl)))] + } + ModuleItem::Stmt(Stmt::Decl(Decl::Class(class_decl))) => { + if self.should_capture(&class_decl.ident.to_id()) { + vec![ + ModuleItem::Stmt(Stmt::Decl(Decl::Class(class_decl.clone()))), + self.create_class_capture_assignment(&class_decl), + ] + } else { + vec![ModuleItem::Stmt(Stmt::Decl(Decl::Class(class_decl)))] + } + } + ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(export_default)) => { + // export default function x() {} → function x() {} __rec.x = x; export default x; + // export default class Foo {} → class Foo {} __rec.Foo = Foo; export default Foo; + let ident = match &export_default.decl { + DefaultDecl::Fn(f) => f.ident.as_ref().cloned(), + DefaultDecl::Class(c) => c.ident.as_ref().cloned(), + _ => None, + }; + if let Some(id) = ident { + let name = id.sym.to_string(); + let decl_item = match export_default.decl.clone() { + DefaultDecl::Fn(f) => ModuleItem::Stmt(Stmt::Decl(Decl::Fn(FnDecl { + ident: f.ident.clone().unwrap(), + declare: false, + function: f.function, + }))), + DefaultDecl::Class(c) => { + ModuleItem::Stmt(Stmt::Decl(Decl::Class(ClassDecl { + ident: c.ident.clone().unwrap(), + declare: false, + class: c.class, + }))) + } + _ => unreachable!(), + }; + // __rec.name = name + let capture_name = ModuleItem::Stmt(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(create_assign_expr( + expr_to_assign_target(self.create_captured_member(&name)), + Expr::Ident(Ident::new( + name.as_str().into(), + DUMMY_SP, + SyntaxContext::empty(), + )), + )), + })); + // __rec.default = name (Babel always captures default) + let capture_default = ModuleItem::Stmt(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(create_assign_expr( + expr_to_assign_target(self.create_captured_member("default")), + Expr::Ident(Ident::new( + name.as_str().into(), + DUMMY_SP, + SyntaxContext::empty(), + )), + )), + })); + let export_stmt = + ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(ExportDefaultExpr { + span: DUMMY_SP, + expr: Box::new(Expr::Ident(Ident::new( + name.as_str().into(), + DUMMY_SP, + SyntaxContext::empty(), + ))), + })); + vec![decl_item, capture_name, capture_default, export_stmt] + } else { + // Anonymous default export: export default function() {} or export default class {} + // Babel: __rec.default = ; export default __rec.default; + let anon_expr = match export_default.decl { + DefaultDecl::Fn(f) => Expr::Fn(FnExpr { + ident: None, + function: f.function, + }), + DefaultDecl::Class(c) => Expr::Class(ClassExpr { + ident: None, + class: c.class, + }), + _ => { + return vec![ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl( + export_default, + ))]; + } + }; + let capture_default = ModuleItem::Stmt(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(create_assign_expr( + expr_to_assign_target(self.create_captured_member("default")), + anon_expr, + )), + })); + let export_stmt = + ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(ExportDefaultExpr { + span: DUMMY_SP, + expr: Box::new(self.create_captured_member("default")), + })); + vec![capture_default, export_stmt] + } + } + // ExportDefaultExpr is kept as-is in transform_module_item. + // The __rec.default capture is added by insert_declarations_for_exports + // which runs after visit_mut transforms all expressions. + other => vec![other], + } + } + + fn collect_declared_top_level_names(&self, module: &Module) -> HashSet { + let mut names = HashSet::new(); + for item in &module.body { + match item { + ModuleItem::Stmt(Stmt::Decl(Decl::Var(var_decl))) => { + for decl in &var_decl.decls { + for (sym, _) in extract_idents_from_pat(&decl.name) { + names.insert(sym.to_string()); + } + } + } + ModuleItem::Stmt(Stmt::Decl(Decl::Fn(fn_decl))) => { + names.insert(fn_decl.ident.sym.to_string()); + } + ModuleItem::Stmt(Stmt::Decl(Decl::Class(class_decl))) => { + names.insert(class_decl.ident.sym.to_string()); + } + ModuleItem::ModuleDecl(ModuleDecl::Import(import_decl)) => { + for spec in &import_decl.specifiers { + match spec { + ImportSpecifier::Named(named) => { + names.insert(named.local.sym.to_string()); + } + ImportSpecifier::Default(def) => { + names.insert(def.local.sym.to_string()); + } + ImportSpecifier::Namespace(ns) => { + names.insert(ns.local.sym.to_string()); + } + } + } + } + ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export_decl)) => { + match &export_decl.decl { + Decl::Var(var_decl) => { + for decl in &var_decl.decls { + for (sym, _) in extract_idents_from_pat(&decl.name) { + names.insert(sym.to_string()); + } + } + } + Decl::Fn(fn_decl) => { + names.insert(fn_decl.ident.sym.to_string()); + } + Decl::Class(class_decl) => { + names.insert(class_decl.ident.sym.to_string()); + } + _ => {} + } + } + _ => {} + } + } + names + } + + /// JS parity: insert binding declarations for export specifiers whose locals + /// were rewritten to recorder assignments (e.g. `export { oe as default }`). + fn insert_declarations_for_exports(&self, module: &mut Module) { + let mut declared = self.collect_declared_top_level_names(module); + let mut new_body = Vec::with_capacity(module.body.len()); + + for item in module.body.drain(..) { + match &item { + ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export_decl)) + if export_decl.src.is_none() && !export_decl.specifiers.is_empty() => + { + for spec in &export_decl.specifiers { + let ExportSpecifier::Named(named) = spec else { + continue; + }; + let ModuleExportName::Ident(local_ident) = &named.orig else { + continue; + }; + let local_name = local_ident.sym.to_string(); + if declared.contains(&local_name) { + continue; + } + // Use the SAME SyntaxContext as the export specifier so + // SWC's codegen doesn't append hygiene suffixes. + let decl = ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(VarDecl { + span: DUMMY_SP, + kind: VarDeclKind::Var, + decls: vec![VarDeclarator { + span: DUMMY_SP, + name: Pat::Ident(BindingIdent { + id: local_ident.clone(), + type_ann: None, + }), + init: Some(Box::new(self.create_captured_member(&local_name))), + definite: false, + }], + ..Default::default() + })))); + new_body.push(decl); + declared.insert(local_name); + } + } + ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(ref export_default)) => { + if let Expr::Ident(local_ident) = &*export_default.expr { + let local_name = local_ident.sym.to_string(); + if !declared.contains(&local_name) { + let decl = ModuleItem::Stmt(Stmt::Decl(create_var_decl( + VarDeclKind::Var, + &local_name, + Some(self.create_captured_member(&local_name)), + ))); + new_body.push(decl); + declared.insert(local_name); + } + } + // Capture __rec.default = (Babel always does this) + let default_capture = ModuleItem::Stmt(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(create_assign_expr( + expr_to_assign_target(self.create_captured_member("default")), + *export_default.expr.clone(), + )), + })); + new_body.push(default_capture); + } + _ => {} + } + + new_body.push(item); + } + + module.body = new_body; + } +} + +impl VisitMut for ScopeCapturingTransform { + fn visit_mut_module(&mut self, module: &mut Module) { + // First pass: analyze scope to determine capturable variables + let mut analyzer = ScopeAnalyzer::new(); + use swc_ecma_visit::VisitWith; + module.visit_with(&mut analyzer); + + let mut original_refs = OriginalRefNameCollector::default(); + module.visit_with(&mut original_refs); + self.original_ref_names = original_refs.names; + + self.imported_vars.clear(); + self.import_sources.clear(); + self.loop_header_vars.clear(); + for item in &module.body { + if let ModuleItem::ModuleDecl(ModuleDecl::Import(import_decl)) = item { + let src = import_decl.src.value.to_string(); + for spec in &import_decl.specifiers { + let (id, orig_name) = match spec { + ImportSpecifier::Named(named) => { + let orig = match &named.imported { + Some(ModuleExportName::Ident(id)) => id.sym.to_string(), + Some(ModuleExportName::Str(s)) => s.value.to_string(), + None => named.local.sym.to_string(), + }; + (named.local.to_id(), orig) + } + ImportSpecifier::Default(def) => (def.local.to_id(), "default".to_string()), + ImportSpecifier::Namespace(ns) => (ns.local.to_id(), "*".to_string()), + }; + self.import_sources + .insert(id.0.to_string(), (src.clone(), orig_name)); + self.imported_vars.insert(id); + } + } + } + self.collect_loop_header_vars(module); + + let has_local_lively_binding = analyzer + .top_level_vars + .iter() + .any(|id| id.0.as_ref() == "lively"); + let has_capture_obj_binding = analyzer + .top_level_vars + .iter() + .any(|id| id.0.as_ref() == self.capture_obj.as_str()); + + self.capturable_vars = analyzer + .top_level_vars + .into_iter() + .filter(|id| !self.excluded.contains(id.0.as_ref())) + .collect(); + + // Collect export metadata for resurrection builds BEFORE the transform + // (since the transform removes export statements from the AST). + // imported_vars is now populated so we can skip re-exported imports. + if self.resurrection { + self.collected_exports.clear(); + self.collect_module_exports(module); + } + + // Second pass: transform the module + self.depth = 0; + self.hoisted_function_captures.clear(); + for item in &module.body { + self.collect_hoisted_function_capture(item); + } + + let mut new_body = Vec::with_capacity(module.body.len()); + for item in module.body.drain(..) { + let mut transformed_items = self.transform_module_item(item); + for transformed in &mut transformed_items { + transformed.visit_mut_children_with(self); + } + new_body.extend(transformed_items); + } + module.body = new_body; + self.insert_declarations_for_exports(module); + + // Ensure the capture object exists for generated __varRecorder__ references. + let mut inserted_recorder_init = false; + if !has_capture_obj_binding { + self.insert_recorder_init(module, has_local_lively_binding); + inserted_recorder_init = true; + } + + self.insert_hoisted_function_captures(module, inserted_recorder_init); + + // For resurrection builds with a declaration wrapper, replace top-level + // function declarations with let bindings wrapped through the wrapper. + // Babel: function foo() {...} => var foo = wrapper("foo", "function", function(){...}, __moduleMeta__) + // This runs LAST (after all scope capture processing) matching the Babel pipeline. + if self.resurrection && self.declaration_wrapper.is_some() { + self.replace_function_declarations_with_wrapper(module); + } + + // For resurrection builds, insert __module_exports__ right after the recorder init + // (matching Babel's placement at the top of the module). + if self.resurrection { + let arr_elems: Vec> = self + .collected_exports + .iter() + .map(|e| { + Some(ExprOrSpread { + spread: None, + expr: Box::new(create_string_expr(e)), + }) + }) + .collect(); + let member_expr = Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: Box::new(create_ident_expr(&self.capture_obj)), + prop: MemberProp::Ident(IdentName::new("__module_exports__".into(), DUMMY_SP)), + }); + let module_exports_stmt = ModuleItem::Stmt(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(Expr::Assign(AssignExpr { + span: DUMMY_SP, + op: AssignOp::Assign, + left: AssignTarget::Simple(SimpleAssignTarget::Member(MemberExpr { + span: DUMMY_SP, + obj: Box::new(create_ident_expr(&self.capture_obj)), + prop: MemberProp::Ident(IdentName::new( + "__module_exports__".into(), + DUMMY_SP, + )), + })), + right: Box::new(Expr::Bin(BinExpr { + span: DUMMY_SP, + op: BinaryOp::LogicalOr, + left: Box::new(member_expr), + right: Box::new(Expr::Array(ArrayLit { + span: DUMMY_SP, + elems: arr_elems, + })), + })), + })), + })); + // Insert after the recorder init (first non-import statement) + let insert_pos = module + .body + .iter() + .position(|item| !matches!(item, ModuleItem::ModuleDecl(ModuleDecl::Import(_)))) + .unwrap_or(0) + + 1; // +1 to go after the recorder init + let insert_pos = insert_pos.min(module.body.len()); + module.body.insert(insert_pos, module_exports_stmt); + } + } + + fn visit_mut_expr(&mut self, expr: &mut Expr) { + // Transform identifier references to captured members + match expr { + Expr::Assign(assign) => { + // Keep generated import-capture assignments intact: + // __varRecorder__.x = x + if let ( + AssignTarget::Simple(SimpleAssignTarget::Member(MemberExpr { + obj, + prop: MemberProp::Ident(prop_ident), + .. + })), + Expr::Ident(right_ident), + ) = (&assign.left, &*assign.right) + { + if let Expr::Ident(obj_ident) = &**obj { + if obj_ident.sym.as_ref() == self.capture_obj + && prop_ident.sym == right_ident.sym + { + return; + } + } + } + } + Expr::Ident(ident) => { + if self.should_capture(&ident.to_id()) { + *expr = self.create_captured_member(ident.sym.as_ref()); + return; + } + } + Expr::MetaProp(MetaPropExpr { + kind: MetaPropKind::ImportMeta, + .. + }) => { + *expr = self.create_import_meta_expr(); + return; + } + _ => {} + } + + expr.visit_mut_children_with(self); + } + + fn visit_mut_stmt(&mut self, stmt: &mut Stmt) { + if let Stmt::Expr(expr_stmt) = stmt { + let assign_expr = match &mut *expr_stmt.expr { + Expr::Assign(assign) => Some(assign), + Expr::Paren(paren) => match &mut *paren.expr { + Expr::Assign(assign) => Some(assign), + _ => None, + }, + _ => None, + }; + if let Some(assign) = assign_expr { + if assign.op == AssignOp::Assign { + if let AssignTarget::Pat(assign_pat) = &assign.left { + let pat: Pat = assign_pat.clone().into(); + if self.should_capture_pattern(&pat) { + assign.right.visit_mut_with(self); + let right = std::mem::replace( + &mut assign.right, + Box::new(Expr::Invalid(Invalid { span: DUMMY_SP })), + ); + let stmts = + self.transform_assignment_pattern_to_stmts(&pat, Some(right)); + *stmt = Stmt::Block(BlockStmt { + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + stmts, + }); + return; + } + } + } + } + } + + stmt.visit_mut_children_with(self); + } + + fn visit_mut_assign_expr(&mut self, assign: &mut AssignExpr) { + if let AssignTarget::Simple(SimpleAssignTarget::Ident(binding_ident)) = &assign.left { + let id = binding_ident.id.to_id(); + if self.should_capture(&id) { + let name = binding_ident.id.sym.to_string(); + let member = self.create_captured_member(&name); + assign.left = expr_to_assign_target(member); + if self.should_wrap_live_declarations() { + let right = std::mem::replace( + &mut assign.right, + Box::new(Expr::Invalid(Invalid { span: DUMMY_SP })), + ); + assign.right = + Box::new(self.wrap_declaration(&name, "assignment", *right, None)); + } + } + } + + assign.visit_mut_children_with(self); + } + + fn visit_mut_update_expr(&mut self, update: &mut UpdateExpr) { + if let Expr::Ident(ident) = &*update.arg { + if self.should_capture(&ident.to_id()) { + update.arg = Box::new(self.create_captured_member(ident.sym.as_ref())); + } + } + + update.visit_mut_children_with(self); + } + + fn visit_mut_prop(&mut self, prop: &mut Prop) { + match prop { + Prop::Shorthand(ident) => { + if self.should_capture(&ident.to_id()) { + let key = PropName::Ident(ident.clone().into()); + let value = Box::new(self.create_captured_member(ident.sym.as_ref())); + *prop = Prop::KeyValue(KeyValueProp { key, value }); + return; + } + } + Prop::Getter(getter) => { + getter.key.visit_mut_with(self); + self.fn_depth += 1; + self.enter_scope(true); + if let Some(body) = &getter.body { + self.prescan_var_decls_in_block(&body.stmts); + } + if let Some(body) = &mut getter.body { + body.visit_mut_with(self); + } + self.exit_scope(); + self.fn_depth -= 1; + return; + } + Prop::Setter(setter) => { + setter.key.visit_mut_with(self); + self.fn_depth += 1; + self.enter_scope(true); + if let Some(this_param) = &setter.this_param { + self.declare_pattern_in_current_scope(this_param); + } + self.declare_pattern_in_current_scope(&setter.param); + if let Some(body) = &setter.body { + self.prescan_var_decls_in_block(&body.stmts); + } + if let Some(body) = &mut setter.body { + body.visit_mut_with(self); + } + self.exit_scope(); + self.fn_depth -= 1; + return; + } + _ => {} + } + + prop.visit_mut_children_with(self); + } + + // Scope tracking for constructors (separate AST node from Function) + fn visit_mut_constructor(&mut self, cons: &mut Constructor) { + self.enter_scope(true); + for param in &cons.params { + match param { + ParamOrTsParamProp::Param(p) => self.declare_pattern_in_current_scope(&p.pat), + ParamOrTsParamProp::TsParamProp(tp) => match &tp.param { + TsParamPropParam::Ident(id) => { + self.declare_in_current_scope(id.id.sym.as_ref()) + } + TsParamPropParam::Assign(assign) => { + self.declare_pattern_in_current_scope(&assign.left) + } + }, + } + } + cons.visit_mut_children_with(self); + self.exit_scope(); + } + + // Scope tracking for nested functions/blocks + fn visit_mut_function(&mut self, func: &mut Function) { + self.fn_depth += 1; + self.enter_scope(true); + for param in &func.params { + self.declare_pattern_in_current_scope(¶m.pat); + } + // Pre-scan: hoist all `var` declarations in the function body + // to the function scope BEFORE transforming. This ensures + // `should_capture` sees var-hoisted names even when the var + // declaration appears later in the source (JS var hoisting). + if let Some(body) = &func.body { + self.prescan_var_decls_in_block(&body.stmts); + } + func.visit_mut_children_with(self); + self.exit_scope(); + self.fn_depth -= 1; + } + + fn visit_mut_arrow_expr(&mut self, arrow: &mut ArrowExpr) { + self.fn_depth += 1; + self.enter_scope(true); + for param in &arrow.params { + self.declare_pattern_in_current_scope(param); + } + arrow.visit_mut_children_with(self); + self.exit_scope(); + self.fn_depth -= 1; + } + + fn visit_mut_for_stmt(&mut self, stmt: &mut ForStmt) { + self.enter_scope(false); + if let Some(VarDeclOrExpr::VarDecl(var_decl)) = &stmt.init { + for decl in &var_decl.decls { + self.declare_pattern_in_current_scope(&decl.name); + } + } + stmt.visit_mut_children_with(self); + self.exit_scope(); + } + + fn visit_mut_for_in_stmt(&mut self, stmt: &mut ForInStmt) { + self.enter_scope(false); + match &stmt.left { + ForHead::VarDecl(var_decl) => { + for decl in &var_decl.decls { + self.declare_pattern_in_current_scope(&decl.name); + } + } + ForHead::Pat(pat) => self.declare_pattern_in_current_scope(pat), + _ => {} + } + stmt.visit_mut_children_with(self); + self.exit_scope(); + } + + fn visit_mut_for_of_stmt(&mut self, stmt: &mut ForOfStmt) { + self.enter_scope(false); + match &stmt.left { + ForHead::VarDecl(var_decl) => { + for decl in &var_decl.decls { + self.declare_pattern_in_current_scope(&decl.name); + } + } + ForHead::Pat(pat) => self.declare_pattern_in_current_scope(pat), + _ => {} + } + stmt.visit_mut_children_with(self); + self.exit_scope(); + } + + fn visit_mut_block_stmt(&mut self, block: &mut BlockStmt) { + self.enter_scope(false); + block.visit_mut_children_with(self); + self.exit_scope(); + } + + fn visit_mut_var_decl(&mut self, var_decl: &mut VarDecl) { + let mut current_names = HashSet::new(); + for decl in &var_decl.decls { + for (sym, _) in extract_idents_from_pat(&decl.name) { + current_names.insert(sym.to_string()); + } + } + self.current_var_decl_stack.push(current_names); + + if self.depth > 0 { + for decl in &var_decl.decls { + if matches!(var_decl.kind, VarDeclKind::Var) { + for (sym, _) in extract_idents_from_pat(&decl.name) { + self.declare_in_nearest_function_scope(sym.as_ref()); + } + } else { + self.declare_pattern_in_current_scope(&decl.name); + } + } + } + var_decl.visit_mut_children_with(self); + self.current_var_decl_stack.pop(); + } + + fn visit_mut_fn_expr(&mut self, fn_expr: &mut FnExpr) { + if let Some(ident) = &fn_expr.ident { + // Named fn expressions: the name is visible inside the function + // but declared in a scope between the outer and inner scopes. + self.enter_scope(false); + self.declare_in_current_scope(ident.sym.as_ref()); + } + // visit_mut_function handles entering the function scope + params + fn_expr.visit_mut_children_with(self); + if fn_expr.ident.is_some() { + self.exit_scope(); + } + } + + fn visit_mut_fn_decl(&mut self, fn_decl: &mut FnDecl) { + if self.depth > 0 { + self.declare_in_current_scope(fn_decl.ident.sym.as_ref()); + } + // visit_mut_function handles entering the function scope + params + fn_decl.visit_mut_children_with(self); + } + + fn visit_mut_class_decl(&mut self, class_decl: &mut ClassDecl) { + if self.depth > 0 { + self.declare_in_current_scope(class_decl.ident.sym.as_ref()); + } + class_decl.visit_mut_children_with(self); + } + + fn visit_mut_catch_clause(&mut self, catch: &mut CatchClause) { + self.enter_scope(false); + if let Some(param) = &catch.param { + self.declare_pattern_in_current_scope(param); + } + catch.visit_mut_children_with(self); + self.exit_scope(); + } +} + +// Helper to generate random IDs for temporary variables +fn rand_id() -> String { + use std::sync::atomic::{AtomicUsize, Ordering}; + static COUNTER: AtomicUsize = AtomicUsize::new(0); + format!("{}", COUNTER.fetch_add(1, Ordering::SeqCst)) +} + +#[cfg(test)] +mod tests { + use super::*; + use swc_common::{sync::Lrc, FileName, SourceMap}; + use swc_ecma_codegen::{text_writer::JsWriter, Config, Emitter}; + use swc_ecma_parser::{parse_file_as_module, Syntax}; + + fn transform_code(code: &str) -> String { + transform_code_with_exclusions(code, vec!["console".to_string()]) + } + + fn transform_code_with_exclusions(code: &str, excluded: Vec) -> String { + let cm = Lrc::new(SourceMap::default()); + let fm = cm.new_source_file(FileName::Anon.into(), code.to_string()); + + let mut module = parse_file_as_module( + &fm, + Syntax::Es(Default::default()), + Default::default(), + None, + &mut vec![], + ) + .unwrap(); + + let mut transform = ScopeCapturingTransform::new( + "__varRecorder__".to_string(), + None, + excluded, + true, + true, + false, + "test.js".to_string(), + None, + None, + None, + None, + HashMap::new(), + ); + + module.visit_mut_with(&mut transform); + + let mut buf = vec![]; + { + let mut emitter = Emitter { + cfg: Config::default(), + cm: cm.clone(), + comments: None, + wr: JsWriter::new(cm, "\n", &mut buf, None), + }; + + emitter.emit_module(&module).unwrap(); + } + + String::from_utf8(buf).unwrap() + } + + fn transform_code_live_module(code: &str) -> String { + transform_code_live_module_with_id(code, "test.js") + } + + fn transform_code_live_module_with_id(code: &str, module_id: &str) -> String { + let cm = Lrc::new(SourceMap::default()); + let fm = cm.new_source_file(FileName::Anon.into(), code.to_string()); + + let mut module = parse_file_as_module( + &fm, + Syntax::Es(Default::default()), + Default::default(), + None, + &mut vec![], + ) + .unwrap(); + + let mut transform = ScopeCapturingTransform::new( + "__varRecorder__".to_string(), + Some("defVar_test".to_string()), + vec!["console".to_string()], + true, + true, + false, + module_id.to_string(), + Some(format!( + "__varRecorder__.System.get(\"@lively-env\").moduleEnv(\"{}\")", + module_id + )), + None, + None, + None, + HashMap::new(), + ); + + module.visit_mut_with(&mut transform); + + let mut buf = vec![]; + { + let mut emitter = Emitter { + cfg: Config::default(), + cm: cm.clone(), + comments: None, + wr: JsWriter::new(cm, "\n", &mut buf, None), + }; + + emitter.emit_module(&module).unwrap(); + } + + String::from_utf8(buf).unwrap() + } + + // --- Basic variable capturing (matching Babel capturing-test.js) --- + + #[test] + fn test_simple_var() { + let output = transform_code("var x = 1;"); + assert!( + output.contains("__varRecorder__.x = 1;"), + "should capture var with value: {}", + output + ); + } + + #[test] + fn test_uninitialized_var_preserves_only_own_recorder_slot() { + let output = transform_code("var x;"); + assert!( + output.contains("Object.prototype.hasOwnProperty.call(__varRecorder__, \"x\") ? __varRecorder__.x : undefined"), + "uninitialized var should not read inherited globals: {}", + output + ); + } + + #[test] + fn test_var_reference() { + let output = transform_code("var x = 1; x + 2;"); + assert!( + output.contains("__varRecorder__.x + 2"), + "should capture var reference: {}", + output + ); + } + + #[test] + fn test_live_module_var_declarations_use_definition_wrapper() { + let output = transform_code_live_module("var x = 23;"); + assert!( + output.contains("__varRecorder__.x = __varRecorder__[\"defVar_test\"](\"x\", \"var\", 23, __varRecorder__)"), + "live var capture should notify through defVar: {}", + output + ); + } + + #[test] + fn test_live_module_assignments_use_definition_wrapper() { + let output = transform_code_live_module("var y = 1; function inc() { y = y + 1; }"); + assert!( + output.contains("__varRecorder__[\"defVar_test\"](\"y\", \"assignment\", __varRecorder__.y + 1, __varRecorder__)"), + "live assignment should notify through defVar: {}", + output + ); + } + + #[test] + fn test_live_module_destructuring_assignments_update_recorder() { + let output = transform_code_live_module( + "let A, B; async function load(mod) { ({ A, B } = await mod.load()); }", + ); + assert!( + output.contains( + "__varRecorder__.A = __varRecorder__[\"defVar_test\"](\"A\", \"assignment\", _tmp_" + ), + "destructuring assignment should update A through defVar: {}", + output + ); + assert!( + output.contains( + "__varRecorder__.B = __varRecorder__[\"defVar_test\"](\"B\", \"assignment\", _tmp_" + ), + "destructuring assignment should update B through defVar: {}", + output + ); + assert!( + !output.contains("({ A, B }"), + "captured destructuring assignment should be expanded: {}", + output + ); + } + + #[test] + fn test_remote_esm_module_vars_do_not_use_definition_wrapper() { + let output = transform_code_live_module_with_id( + "var x = 23; x = x + 1;", + "esm://ga.jspm.io/npm:@jspm/core@2.1.0/nodelibs/browser/util.js", + ); + assert!( + output.contains("__varRecorder__.x = 23"), + "remote var should still be captured directly: {}", + output + ); + assert!( + !output.contains("defVar_test"), + "remote vars should not go through live defVar wrapper: {}", + output + ); + } + + #[test] + fn test_var_and_func_decls() { + // Babel: 'var z = 3, y = 4; function foo() { var x = 5; }' + // → function hoisted, vars captured, inner var NOT captured + let output = transform_code("var z = 3, y = 4; function foo() { var x = 5; }"); + assert!( + output.contains("__varRecorder__.z = 3"), + "capture z: {}", + output + ); + assert!( + output.contains("__varRecorder__.y = 4"), + "capture y: {}", + output + ); + assert!( + output.contains("__varRecorder__.foo = foo"), + "capture foo: {}", + output + ); + assert!( + !output.contains("__varRecorder__.x"), + "inner var should NOT be captured: {}", + output + ); + } + + #[test] + fn test_function_declaration() { + let output = transform_code("function foo() { return 1; }"); + assert!( + output.contains("__varRecorder__.foo = foo;"), + "should capture function with assignment form: {}", + output + ); + assert!( + output.contains("function foo()"), + "should keep function declaration: {}", + output + ); + } + + #[test] + fn test_excluded_var() { + // Babel: excluded vars should not be captured + let output = transform_code("console.log('hello');"); + assert!( + !output.contains("__varRecorder__.console"), + "excluded var should not be captured: {}", + output + ); + } + + #[test] + fn test_excluded_object_destructuring_binding_is_kept_local() { + let output = transform_code_with_exclusions( + "const { lookupPackage, module, ImportInjector } = scripting; module(System, id);", + vec!["console".to_string(), "module".to_string()], + ); + assert!( + output.contains("const module = _tmp_") && output.contains(".module;"), + "excluded destructured binding should remain local: {}", + output + ); + assert!( + output.contains("module("), + "uses should keep the local binding: {}", + output + ); + assert!( + !output.contains("__varRecorder__.module"), + "excluded destructured binding should not be captured: {}", + output + ); + } + + #[test] + fn test_same_var_decl_initializer_uses_local_binding() { + let output = transform_code("if (true) var xe = 1, Ue = xe + 1;"); + assert!(output.contains("xe + 1"), "same-decl reference: {}", output); + assert!( + !output.contains("__varRecorder__.xe + 1"), + "should use local binding: {}", + output + ); + } + + // --- Nested function scoping --- + + #[test] + fn test_nested_function_uses_captured_ref() { + // Babel: function params shadow, but outer refs are captured + let output = transform_code("var z = 3; function foo(y) { var x = 5 + y + z; }"); + assert!( + output.contains("__varRecorder__.z = 3;"), + "outer var captured with value: {}", + output + ); + assert!( + output.contains("__varRecorder__.foo = foo;"), + "function captured with assignment: {}", + output + ); + // Inside foo, z references the captured var + assert!( + output.contains("__varRecorder__.z;"), + "z ref inside foo uses recorder: {}", + output + ); + assert!( + !output.contains("__varRecorder__.y"), + "param should NOT be captured: {}", + output + ); + assert!( + !output.contains("__varRecorder__.x"), + "inner var should NOT be captured: {}", + output + ); + } + + #[test] + fn test_object_accessor_params_are_local() { + let output = transform_code( + "var x; var o = { get foo() { return x; }, set foo(v) { x = v + 1; } };", + ); + assert!( + output.contains("return __varRecorder__.x;"), + "getter should capture outer x: {}", + output + ); + assert!( + output.contains("__varRecorder__.x = v + 1"), + "setter should update captured outer x from local param: {}", + output + ); + assert!( + !output.contains("__varRecorder__.v"), + "setter param should not be captured: {}", + output + ); + } + + // --- Let + Const --- + + #[test] + fn test_let_const_captured() { + let output = transform_code("let x = 1; const y = 2;"); + assert!( + output.contains("__varRecorder__.x = 1;"), + "let captured with value: {}", + output + ); + assert!( + output.contains("__varRecorder__.y = 2;"), + "const captured with value: {}", + output + ); + } + + // --- Export --- + + #[test] + fn test_export_const() { + // Babel: 'export const x = 23;' → 'export const x = 23;\n_rec.x = x;' + let output = transform_code("export const x = 23;"); + assert!( + output.contains("export const x = 23;"), + "keeps export: {}", + output + ); + assert!( + output.contains("__varRecorder__.x = x"), + "captures export: {}", + output + ); + } + + #[test] + fn test_export_var() { + // Babel: 'var x = 23; export { x };' → '_rec.x = 23; var x = _rec.x; export { x };' + let output = transform_code("var x = 23; export { x };"); + assert!( + output.contains("__varRecorder__.x = 23;"), + "captures var with value: {}", + output + ); + assert!( + output.contains("var x = __varRecorder__.x;"), + "re-declares var from recorder: {}", + output + ); + assert!( + output.contains("export { x }"), + "keeps named export: {}", + output + ); + } + + #[test] + fn test_export_aliased_var() { + // 'var x = 23; export { x as y };' → captures x, renames export local to __export_y__ + let output = transform_code("var x = 23; export { x as y };"); + assert!( + output.contains("__varRecorder__.x = 23;"), + "captures local var with value: {}", + output + ); + assert!( + output.contains("var __export_y__ = __varRecorder__.x;"), + "re-declares with __export_ prefix: {}", + output + ); + assert!( + output.contains("export { __export_y__ as y }"), + "keeps aliased export with renamed local: {}", + output + ); + } + + #[test] + fn test_export_function_decl() { + // Babel: 'export function x() {}' → 'function x() {}\n_rec.x = x;\nexport { x };' + let output = transform_code("export function x() {}"); + assert!( + output.contains("__varRecorder__.x = x;"), + "captures function with assignment: {}", + output + ); + assert!( + output.contains("export function x()"), + "keeps export function declaration: {}", + output + ); + } + + #[test] + fn test_export_default_named_function() { + // Babel: 'export default function x() {}' → 'function x() {}\n_rec.x = x;\nexport default x;' + let output = transform_code("export default function x() {}"); + assert!( + output.contains("__varRecorder__.x = x;"), + "captures default fn with assignment: {}", + output + ); + assert!( + output.contains("export default x;"), + "keeps export default statement: {}", + output + ); + assert!( + output.contains("function x()"), + "keeps function declaration: {}", + output + ); + } + + #[test] + fn test_re_export_named_from_source() { + // SWC materializes re-exports as local aliases so SystemJS emits concrete exports. + let output = transform_code("export { name1, name2 } from \"foo\";"); + assert!( + output.contains("export { __export_name1__ as name1, __export_name2__ as name2 }"), + "keeps re-export with names: {}", + output + ); + assert!( + output.contains("__varRecorder__.name1 = __reexport_name1__"), + "captures re-exported name1: {}", + output + ); + assert!( + output.contains("__varRecorder__.name2 = __reexport_name2__"), + "captures re-exported name2: {}", + output + ); + } + + #[test] + fn test_re_export_aliased_from_source() { + // Babel: 'export { name1 as foo1 } from "foo";' → keeps as-is + let output = transform_code("export { name1 as foo1, name2 as bar2 } from \"foo\";"); + assert!(output.contains("export {"), "keeps re-export: {}", output); + assert!( + output.contains("from \"foo\"") || output.contains("from 'foo'"), + "keeps source module: {}", + output + ); + } + + #[test] + fn test_export_default_expression() { + // Babel: 'export default foo(1, 2, 3);' → 'export default _rec.foo(1, 2, 3);' + // SWC does not capture undeclared globals, so we declare foo first + let output = transform_code("var foo = function() {}; export default foo(1, 2, 3);"); + assert!( + output.contains("__varRecorder__.foo(1, 2, 3)"), + "captures ref in default expr: {}", + output + ); + assert!( + output.contains("export default __varRecorder__.foo(1, 2, 3)"), + "export default uses captured ref: {}", + output + ); + assert!( + output.contains("__varRecorder__.default = __varRecorder__.foo(1, 2, 3)"), + "captures __rec.default: {}", + output + ); + } + + #[test] + fn test_re_export_namespace_import() { + // Babel: 'import * as completions from "./lib/completions.js"; export { completions }' + // → keeps import, adds _rec.completions = completions, keeps export + let output = transform_code( + "import * as completions from \"./lib/completions.js\";\nexport { completions }", + ); + assert!( + output.contains("import * as completions from"), + "keeps namespace import: {}", + output + ); + assert!( + output.contains("__varRecorder__.completions = completions;"), + "captures namespace with assignment form: {}", + output + ); + assert!( + output.contains("export { completions }"), + "keeps named export: {}", + output + ); + } + + // --- Import --- + + #[test] + fn test_import_capture() { + // With captureImports=true, imports get _rec.x = x + let output = transform_code("import { x } from 'foo';"); + assert!( + output.contains("import { x } from 'foo'"), + "keeps import declaration: {}", + output + ); + assert!( + output.contains("__varRecorder__.x = x;"), + "captures import with assignment form: {}", + output + ); + } + + #[test] + fn test_import_default_capture() { + let output = transform_code("import x from 'foo';"); + assert!( + output.contains("import x from 'foo'"), + "keeps default import declaration: {}", + output + ); + assert!( + output.contains("__varRecorder__.x = x;"), + "captures default import with assignment form: {}", + output + ); + } + + #[test] + fn test_import_namespace_capture() { + let output = transform_code("import * as ns from 'foo';"); + assert!( + output.contains("import * as ns from 'foo'"), + "keeps namespace import declaration: {}", + output + ); + assert!( + output.contains("__varRecorder__.ns = ns;"), + "captures namespace import with assignment form: {}", + output + ); + } + + // --- Patterns --- + + #[test] + fn test_destructuring_var() { + let output = transform_code("var { a, b: c } = obj;"); + // SWC desugars to temp variable + property access + assert!( + output.contains("_tmp_"), + "uses temp variable for destructuring: {}", + output + ); + assert!( + output.contains("__varRecorder__.a = _tmp_"), + "captures a from temp: {}", + output + ); + assert!( + output.contains("__varRecorder__.c = _tmp_"), + "captures c (alias of b) from temp: {}", + output + ); + assert!( + !output.contains("__varRecorder__.b"), + "b is a key, not captured: {}", + output + ); + } + + #[test] + fn test_array_destructuring() { + let output = transform_code("var [a, b] = arr;"); + // SWC desugars to temp variable + index access + assert!( + output.contains("_tmp_"), + "uses temp variable for array destructuring: {}", + output + ); + assert!( + output.contains("__varRecorder__.a = _tmp_"), + "captures a from temp: {}", + output + ); + assert!( + output.contains("__varRecorder__.b = _tmp_"), + "captures b from temp: {}", + output + ); + assert!(output.contains("[0]"), "accesses index 0: {}", output); + assert!(output.contains("[1]"), "accesses index 1: {}", output); + } + + // --- For loop let/const scoping --- + + #[test] + fn test_for_loop_let_not_captured() { + let output = transform_code("for (let i = 0; i < 10; i++) {}"); + assert!( + !output.contains("__varRecorder__.i"), + "for-let should NOT be captured: {}", + output + ); + } + + #[test] + fn test_for_in_let_not_captured() { + let output = transform_code("for (let k in obj) {}"); + assert!( + !output.contains("__varRecorder__.k"), + "for-in let should NOT be captured: {}", + output + ); + } + + #[test] + fn test_for_of_let_not_captured() { + let output = transform_code("for (let v of arr) {}"); + assert!( + !output.contains("__varRecorder__.v"), + "for-of let should NOT be captured: {}", + output + ); + } + + // =================================================================== + // Tests translated from lively.source-transform/tests/capturing-test.js + // =================================================================== + + // --- Basic var/func capturing (top-level describe block) --- + + #[test] + fn babel_top_level_var_decls_for_capturing() { + // Babel: 'var y, z = foo + bar; baz.foo(z, 3)' + // SWC only captures declared top-level vars, not undeclared globals like foo/bar/baz. + // We declare them to match Babel behavior more closely. + let output = transform_code("var foo, bar, baz; var y, z = foo + bar; baz.foo(z, 3)"); + assert!( + output.contains("__varRecorder__.y"), + "capture y: {}", + output + ); + assert!( + output.contains("__varRecorder__.z = __varRecorder__.foo + __varRecorder__.bar"), + "capture z with captured refs: {}", + output + ); + assert!( + output.contains("__varRecorder__.baz.foo(__varRecorder__.z, 3)"), + "call site uses captured refs: {}", + output + ); + } + + #[test] + fn babel_top_level_var_and_func_decls_for_capturing() { + // Babel: 'var z = 3, y = 4; function foo() { var x = 5; }' + let output = transform_code("var z = 3, y = 4; function foo() { var x = 5; }"); + assert!( + output.contains("__varRecorder__.z = 3"), + "capture z: {}", + output + ); + assert!( + output.contains("__varRecorder__.y = 4"), + "capture y: {}", + output + ); + assert!( + output.contains("__varRecorder__.foo = foo"), + "capture foo: {}", + output + ); + assert!( + !output.contains("__varRecorder__.x"), + "inner x NOT captured: {}", + output + ); + } + + #[test] + fn babel_top_level_var_decls_and_var_usage() { + // var z = 3, y = 42, obj = {...}; function foo(y) { var x = 5 + y.b(z); } + // inner y is param (not captured), z is outer (captured ref) + let output = transform_code( + "var z = 3, y = 42, obj = {a: '123', b: function b(n) { return 23 + n; }};\nfunction foo(y) { var x = 5 + y.b(z); }" + ); + assert!( + output.contains("__varRecorder__.z = 3;"), + "capture z with value: {}", + output + ); + assert!( + output.contains("__varRecorder__.y = 42;"), + "capture y with value: {}", + output + ); + assert!( + output.contains("__varRecorder__.obj ="), + "capture obj: {}", + output + ); + assert!( + output.contains("__varRecorder__.foo = foo;"), + "capture foo with assignment: {}", + output + ); + // Function is hoisted: __varRecorder__.foo = foo appears before other captures + let foo_pos = output.find("__varRecorder__.foo = foo").unwrap(); + let z_pos = output.find("__varRecorder__.z = 3").unwrap(); + assert!( + foo_pos < z_pos, + "function capture hoisted before var captures: {}", + output + ); + // Inside foo, z should reference __varRecorder__.z + assert!( + output.contains("y.b(__varRecorder__.z)"), + "z ref inside foo uses recorder: {}", + output + ); + // Inner var x and param y should NOT be captured + assert!( + !output.contains("__varRecorder__.x"), + "inner x NOT captured: {}", + output + ); + } + + #[test] + fn babel_captures_global_vars_redefined_in_subscopes() { + // const baz = 42; function bar(y) { const x = baz + 10; if (y > 10) { const baz = 33; return baz + 10 } return x; } + let output = transform_code( + "const baz = 42; function bar(y) { const x = baz + 10; if (y > 10) { const baz = 33; return baz + 10 } return x; }" + ); + assert!( + output.contains("__varRecorder__.baz = 42;"), + "capture baz with value: {}", + output + ); + assert!( + output.contains("__varRecorder__.bar = bar;"), + "capture bar with assignment: {}", + output + ); + // Inside bar, baz references __varRecorder__.baz in the outer scope + assert!( + output.contains("__varRecorder__.baz + 10"), + "baz ref inside bar uses recorder: {}", + output + ); + // The inner const baz = 33 should NOT use the recorder + assert!( + output.contains("const baz = 33"), + "inner baz is a local const: {}", + output + ); + assert!( + output.contains("return baz + 10"), + "inner return uses local baz: {}", + output + ); + } + + #[test] + fn babel_captures_top_level_vars_outside_block_shadow() { + // const p = x => x + 1; function f() { { const p = 3; } return p(2); } + let output = + transform_code("const p = x => x + 1; function f() { { const p = 3; } return p(2); }"); + assert!( + output.contains("__varRecorder__.f = f;"), + "capture f with assignment: {}", + output + ); + assert!( + output.contains("__varRecorder__.p ="), + "capture p: {}", + output + ); + // Inside f(), after the block scope, p(2) should reference __varRecorder__.p + assert!( + output.contains("__varRecorder__.p(2)"), + "p(2) outside block shadow uses recorder: {}", + output + ); + // The block-scoped const p = 3 should NOT use recorder + assert!( + output.contains("const p = 3"), + "inner block const p is local: {}", + output + ); + } + + // --- try-catch --- + + #[test] + fn babel_try_catch_not_transformed() { + // Babel: 'try { throw {} } catch (e) { e }' → not transformed + let output = transform_code("try { throw {} } catch (e) { e }"); + assert!( + !output.contains("__varRecorder__.e"), + "catch param not captured: {}", + output + ); + assert!(output.contains("catch"), "keeps catch: {}", output); + } + + // --- for statement --- + + #[test] + fn babel_standard_for_var_not_rewritten() { + // Babel: 'for (var i = 0; i < 5; i ++) { i; }' → var not captured + let output = transform_code("for (var i = 0; i < 5; i++) { i; }"); + assert!( + !output.contains("__varRecorder__.i"), + "for-var i not captured: {}", + output + ); + } + + #[test] + fn babel_for_in_var_not_rewritten() { + // Babel: 'for (var x in {}) { x; }' → var not captured + let output = transform_code("for (var x in {}) { x; }"); + assert!( + !output.contains("__varRecorder__.x"), + "for-in var not captured: {}", + output + ); + } + + #[test] + fn babel_for_of_captures_iterable_ref() { + // Babel: 'for (let x of foo) { x; }' → 'for (let x of _rec.foo) { x; }' + // SWC: foo is an undeclared global, not captured. Loop var x also not captured. + let output = transform_code("for (let x of foo) { x; }"); + assert!( + !output.contains("__varRecorder__.x"), + "loop var not captured: {}", + output + ); + // With a declared var: for (let x of arr) where arr is declared + let output2 = transform_code("var arr = [1]; for (let x of arr) { x; }"); + assert!( + output2.contains("__varRecorder__.arr"), + "declared iterable captured: {}", + output2 + ); + } + + #[test] + fn babel_for_of_destructured_captures_iterable_ref() { + // Babel: 'for (let [x, y] of foo) { x + y; }' → 'for (let [x, y] of _rec.foo) { x + y; }' + // SWC: foo is undeclared, not captured. Loop vars not captured. + let output = transform_code("for (let [x, y] of foo) { x + y; }"); + assert!( + !output.contains("__varRecorder__.x"), + "loop var x not captured: {}", + output + ); + assert!( + !output.contains("__varRecorder__.y"), + "loop var y not captured: {}", + output + ); + } + + // --- labels --- + + #[test] + fn babel_label_continue() { + // Babel: 'loop1:\nfor (var i = 0; i < 3; i++) continue loop1;' + let output = transform_code("loop1:\nfor (var i = 0; i < 3; i++) continue loop1;"); + assert!(output.contains("loop1:"), "keeps label: {}", output); + assert!( + output.contains("continue loop1"), + "keeps continue: {}", + output + ); + } + + #[test] + fn babel_label_break() { + // Babel: 'loop1:\nfor (var i = 0; i < 3; i++) break loop1;' + let output = transform_code("loop1:\nfor (var i = 0; i < 3; i++) break loop1;"); + assert!(output.contains("loop1:"), "keeps label: {}", output); + assert!(output.contains("break loop1"), "keeps break: {}", output); + } + + // --- es6 let + const --- + + #[test] + fn babel_captures_let_as_var() { + // Babel: 'let x = 23, y = x + 1;' → '_rec.x = 23; _rec.y = _rec.x + 1;' + let output = transform_code("let x = 23, y = x + 1;"); + assert!( + output.contains("__varRecorder__.x = 23;"), + "capture let x with value: {}", + output + ); + assert!( + output.contains("__varRecorder__.y = __varRecorder__.x + 1;"), + "capture let y with captured x ref: {}", + output + ); + } + + #[test] + fn babel_captures_const_as_var() { + // Babel: 'const x = 23, y = x + 1;' → '_rec.x = 23; _rec.y = _rec.x + 1;' + let output = transform_code("const x = 23, y = x + 1;"); + assert!( + output.contains("__varRecorder__.x = 23;"), + "capture const x with value: {}", + output + ); + assert!( + output.contains("__varRecorder__.y = __varRecorder__.x + 1;"), + "capture const y with captured x ref: {}", + output + ); + } + + // --- enhanced object literals --- + + #[test] + fn babel_captures_shorthand_properties() { + // Babel: 'var x = 23, y = {x};' → '_rec.x = 23; _rec.y = { x: _rec.x };' + let output = transform_code("var x = 23, y = {x};"); + assert!( + output.contains("__varRecorder__.x = 23;"), + "capture x with value: {}", + output + ); + // Shorthand {x} expands to {x: __varRecorder__.x} + assert!( + output.contains("x: __varRecorder__.x"), + "shorthand property uses captured ref: {}", + output + ); + assert!( + output.contains("__varRecorder__.y ="), + "capture y: {}", + output + ); + } + + // --- default args --- + + #[test] + fn babel_captures_default_arg() { + // Babel: 'function x(arg = foo) {}' → 'function x(arg = _rec.foo) {} _rec.x = x; x;' + // SWC does not capture undeclared globals, so foo is not captured without declaration + let output = transform_code("function x(arg = foo) {}"); + assert!( + output.contains("__varRecorder__.x = x;"), + "capture func x with assignment: {}", + output + ); + // With declared default arg source: + let output2 = transform_code("var foo = 1; function x(arg = foo) {}"); + assert!( + output2.contains("__varRecorder__.foo = 1;"), + "capture declared foo with value: {}", + output2 + ); + assert!( + output2.contains("__varRecorder__.x = x;"), + "capture func x: {}", + output2 + ); + // Default arg should use captured ref + assert!( + output2.contains("arg = __varRecorder__.foo"), + "default arg uses captured ref: {}", + output2 + ); + } + + // --- class (without classToFunction transform) --- + + #[test] + fn babel_class_def_no_class_to_func() { + // Babel: 'class Foo { a() { return 23; } }' → 'class Foo { a() { return 23; } } _rec.Foo = Foo;' + let output = transform_code("class Foo {\n a() {\n return 23;\n }\n}"); + assert!( + output.contains("class Foo"), + "keeps class declaration: {}", + output + ); + assert!( + output.contains("return 23"), + "keeps method body: {}", + output + ); + assert!( + output.contains("__varRecorder__.Foo = Foo;"), + "captures class Foo with assignment: {}", + output + ); + } + + #[test] + fn babel_exported_class_def_no_class_to_func() { + // Babel: 'export class Foo {}' → 'export class Foo {} _rec.Foo = Foo;' + let output = transform_code("export class Foo {}"); + assert!( + output.contains("export class Foo"), + "keeps export class: {}", + output + ); + assert!( + output.contains("__varRecorder__.Foo = Foo;"), + "captures exported class with assignment: {}", + output + ); + } + + #[test] + fn babel_exported_default_class_no_class_to_func() { + // Babel: 'export default class Foo {}' → 'export default class Foo {} _rec.Foo = Foo;' + let output = transform_code("export default class Foo {}"); + assert!( + output.contains("class Foo"), + "keeps class declaration: {}", + output + ); + assert!( + output.contains("__varRecorder__.Foo = Foo;"), + "captures default exported class with assignment: {}", + output + ); + assert!( + output.contains("__varRecorder__.default = __varRecorder__.Foo"), + "captures __rec.default: {}", + output + ); + assert!( + output.contains("export default __varRecorder__.Foo"), + "export default uses captured ref: {}", + output + ); + } + + #[test] + fn babel_does_not_capture_class_expr() { + // Babel: 'var bar = class Foo {}' → '_rec.bar = class Foo {};' + let output = transform_code("var bar = class Foo {}"); + assert!( + output.contains("__varRecorder__.bar = class Foo"), + "captures var bar with class expr: {}", + output + ); + // Foo as a class expression name should not be separately captured + assert!( + !output.contains("__varRecorder__.Foo"), + "class expr name Foo NOT captured: {}", + output + ); + } + + #[test] + fn babel_captures_var_same_name_as_class_expr() { + // Babel: 'var Foo = class Foo {}; new Foo();' → '_rec.Foo = class Foo {}; new _rec.Foo();' + let output = transform_code("var Foo = class Foo {}; new Foo();"); + assert!( + output.contains("__varRecorder__.Foo = class Foo"), + "captures var Foo with class expr: {}", + output + ); + assert!( + output.contains("new __varRecorder__.Foo()"), + "new uses captured Foo: {}", + output + ); + } + + // --- template strings --- + + #[test] + fn babel_template_string_ref() { + // Babel: '`${foo}`' → '`${ _rec.foo }`;' + // SWC does not capture undeclared globals; test with declared var + let output = transform_code("var foo = 1; `${foo}`;"); + assert!( + output.contains("__varRecorder__.foo = 1;"), + "captures foo with value: {}", + output + ); + assert!( + output.contains("${__varRecorder__.foo}"), + "template string uses captured ref: {}", + output + ); + } + + // --- computed prop in object literal --- + + #[test] + fn babel_computed_prop_in_object() { + // Babel: 'var x = {[x]: y};' → '_rec.x = { [_rec.x]: _rec.y };' + // SWC does not capture undeclared globals; test with all vars declared + let output = transform_code("var x = 1, y = 2; var z = {[x]: y};"); + assert!( + output.contains("__varRecorder__.x = 1;"), + "captures x with value: {}", + output + ); + assert!( + output.contains("__varRecorder__.y = 2;"), + "captures y with value: {}", + output + ); + assert!( + output.contains("[__varRecorder__.x]: __varRecorder__.y"), + "computed prop uses captured refs: {}", + output + ); + assert!( + output.contains("__varRecorder__.z ="), + "captures z: {}", + output + ); + } + + // --- patterns / destructuring --- + + #[test] + fn babel_destructured_obj_var() { + // Babel: 'var {x} = {x: 3};' → 'var destructured_1 = { x: 3 }; _rec.x = destructured_1.x;' + let output = transform_code("var {x} = {x: 3};"); + assert!(output.contains("_tmp_"), "uses temp variable: {}", output); + assert!( + output.contains("__varRecorder__.x ="), + "captures destructured x: {}", + output + ); + assert!( + output.contains(".x;") || output.contains(".x\n"), + "accesses .x property from temp: {}", + output + ); + } + + #[test] + fn babel_destructured_obj_var_with_alias() { + // Babel: 'var {x: y} = foo;' → 'var destructured_1 = _rec.foo; _rec.y = destructured_1.x;' + // SWC does not capture undeclared globals; foo is undeclared + let output = transform_code("var {x: y} = foo;"); + assert!( + output.contains("__varRecorder__.y ="), + "captures alias y: {}", + output + ); + assert!( + !output.contains("__varRecorder__.x"), + "x is a key not a binding, not captured: {}", + output + ); + assert!(output.contains(".x"), "accesses .x from temp: {}", output); + } + + #[test] + fn babel_destructured_list_with_spread() { + // Babel: 'var [a, b, ...rest] = foo;' + // SWC does not capture undeclared globals; foo is undeclared + let output = transform_code("var [a, b, ...rest] = foo;"); + assert!( + output.contains("__varRecorder__.a ="), + "captures a: {}", + output + ); + assert!( + output.contains("__varRecorder__.b ="), + "captures b: {}", + output + ); + assert!( + output.contains("__varRecorder__.rest ="), + "captures rest: {}", + output + ); + assert!(output.contains("[0]"), "accesses index 0: {}", output); + assert!(output.contains("[1]"), "accesses index 1: {}", output); + } + + #[test] + fn babel_destructured_list_with_obj() { + // Babel: 'var [{b}] = foo;' → temp[0].b + // SWC does not capture undeclared globals; foo is undeclared + let output = transform_code("var [{b}] = foo;"); + assert!( + output.contains("__varRecorder__.b ="), + "captures b: {}", + output + ); + assert!(output.contains("[0]"), "accesses index 0: {}", output); + assert!(output.contains(".b"), "accesses .b property: {}", output); + } + + #[test] + fn babel_destructured_list_nested() { + // Babel: 'var [[b]] = foo;' → temp[0][0] + let output = transform_code("var [[b]] = foo;"); + assert!( + output.contains("__varRecorder__.b ="), + "captures b: {}", + output + ); + assert!(output.contains("[0]"), "accesses index: {}", output); + } + + #[test] + fn babel_destructured_obj_with_list() { + // Babel: 'var {x: [y]} = foo, z = 23;' + // SWC does not capture undeclared globals; foo is undeclared + let output = transform_code("var {x: [y]} = foo, z = 23;"); + assert!( + output.contains("__varRecorder__.y ="), + "captures y: {}", + output + ); + assert!( + output.contains("__varRecorder__.z = 23"), + "captures z with value: {}", + output + ); + assert!(output.contains(".x"), "accesses .x property: {}", output); + } + + #[test] + fn babel_destructured_deep() { + // Babel: 'var {x: {x: {x}}, y: {y: x}} = foo;' + // SWC does not capture undeclared globals; foo is undeclared + let output = transform_code("var {x: {x: {x}}, y: {y: x}} = foo;"); + assert!( + output.contains("__varRecorder__.x ="), + "captures x: {}", + output + ); + // Should have multiple temp variables for nested access + assert!( + output.contains("_tmp_"), + "uses temp variables for deep destructuring: {}", + output + ); + } + + #[test] + fn babel_destructured_obj_with_init() { + // Babel: 'var {x = 4} = {x: 3};' + let output = transform_code("var {x = 4} = {x: 3};"); + assert!( + output.contains("__varRecorder__.x ="), + "captures x with default: {}", + output + ); + } + + #[test] + fn babel_destructured_list_with_default() { + // Babel: 'var [a = 3] = foo;' + let output = transform_code("var [a = 3] = foo;"); + assert!( + output.contains("__varRecorder__.a ="), + "captures a with default: {}", + output + ); + } + + #[test] + fn babel_destructured_list_nested_default() { + // Babel: 'var [[a = 3]] = foo;' + let output = transform_code("var [[a = 3]] = foo;"); + assert!( + output.contains("__varRecorder__.a ="), + "captures a with nested default: {}", + output + ); + } + + #[test] + fn babel_destructured_list_obj_deep() { + // Babel: 'var [{b: {c: [a]}}] = foo;' + let output = transform_code("var [{b: {c: [a]}}] = foo;"); + assert!( + output.contains("__varRecorder__.a ="), + "captures a: {}", + output + ); + // Multiple temp variables for deep nesting + assert!(output.contains("_tmp_"), "uses temp variables: {}", output); + } + + #[test] + fn babel_destructured_rest_prop() { + // Babel: 'var {a, b, ...rest} = foo;' + let output = transform_code("var {a, b, ...rest} = foo;"); + assert!( + output.contains("__varRecorder__.a ="), + "captures a: {}", + output + ); + assert!( + output.contains("__varRecorder__.b ="), + "captures b: {}", + output + ); + assert!( + output.contains("__varRecorder__.rest ="), + "captures rest: {}", + output + ); + } + + // --- async --- + + #[test] + fn babel_async_function() { + // Babel: 'async function foo() { return 23 }' → captures foo, hoists capture + let output = transform_code("async function foo() { return 23 }"); + assert!( + output.contains("__varRecorder__.foo = foo;"), + "captures async fn with assignment: {}", + output + ); + assert!( + output.contains("async function foo()"), + "keeps async function declaration: {}", + output + ); + assert!( + output.contains("return 23"), + "keeps function body: {}", + output + ); + // Function capture is hoisted before the function declaration + let capture_pos = output.find("__varRecorder__.foo = foo").unwrap(); + let decl_pos = output.find("async function foo()").unwrap(); + assert!( + capture_pos < decl_pos, + "function capture hoisted before declaration: {}", + output + ); + } + + #[test] + fn babel_await() { + // Babel: 'var x = await foo();' → '_rec.x = await _rec.foo();' + // SWC does not capture undeclared globals; with declared foo, both are captured + let output = transform_code("var foo = async () => 1; var x = await foo();"); + assert!( + output.contains("__varRecorder__.x = await __varRecorder__.foo()"), + "captures x with await and captured foo ref: {}", + output + ); + } + + #[test] + fn babel_exported_async_function() { + // Babel: 'export async function foo() { return 23; }' + let output = transform_code("export async function foo() { return 23; }"); + assert!( + output.contains("__varRecorder__.foo = foo;"), + "captures exported async fn with assignment: {}", + output + ); + assert!( + output.contains("export async function foo()"), + "keeps export async function: {}", + output + ); + } + + #[test] + fn babel_exported_default_async_function() { + // Babel: 'export default async function foo() { return 23; }' + let output = transform_code("export default async function foo() { return 23; }"); + assert!( + output.contains("__varRecorder__.foo = foo;"), + "captures default async fn with assignment: {}", + output + ); + assert!( + output.contains("async function foo()"), + "keeps async function: {}", + output + ); + assert!( + output.contains("__varRecorder__.default = foo;"), + "captures __rec.default: {}", + output + ); + assert!( + output.contains("export default foo;"), + "export default uses name: {}", + output + ); + } + + // --- import --- + + #[test] + fn babel_import_default() { + // Babel: 'import x from "./some-es6-module.js";' → keeps import, adds _rec.x = x + let output = transform_code("import x from \"./some-es6-module.js\";"); + assert!( + output.contains("import x from \"./some-es6-module.js\""), + "keeps full import: {}", + output + ); + assert!( + output.contains("__varRecorder__.x = x;"), + "captures default import with assignment: {}", + output + ); + } + + #[test] + fn babel_import_star() { + // Babel: 'import * as name from "module-name";' → keeps import, adds _rec.name = name + let output = transform_code("import * as name from \"module-name\";"); + assert!( + output.contains("import * as name from \"module-name\""), + "keeps full import: {}", + output + ); + assert!( + output.contains("__varRecorder__.name = name;"), + "captures namespace with assignment: {}", + output + ); + } + + #[test] + fn babel_import_member() { + // Babel: 'import { member } from "module-name";' → keeps import, adds _rec.member = member + let output = transform_code("import { member } from \"module-name\";"); + assert!( + output.contains("import {"), + "keeps import brace: {}", + output + ); + assert!( + output.contains("from \"module-name\""), + "keeps source: {}", + output + ); + assert!( + output.contains("__varRecorder__.member = member;"), + "captures member with assignment: {}", + output + ); + } + + #[test] + fn babel_import_member_with_alias() { + // Babel: 'import { member as alias } from "module-name";' → _rec.alias = alias + let output = transform_code("import { member as alias } from \"module-name\";"); + assert!( + output.contains("member as alias"), + "keeps alias in import: {}", + output + ); + assert!( + output.contains("__varRecorder__.alias = alias;"), + "captures alias with assignment: {}", + output + ); + assert!( + !output.contains("__varRecorder__.member"), + "member not captured separately: {}", + output + ); + } + + #[test] + fn babel_import_multiple_members() { + // Babel: 'import { member1 , member2 } from "module-name";' + let output = transform_code("import { member1, member2 } from \"module-name\";"); + assert!( + output.contains("__varRecorder__.member1 = member1;"), + "captures member1 with assignment: {}", + output + ); + assert!( + output.contains("__varRecorder__.member2 = member2;"), + "captures member2 with assignment: {}", + output + ); + } + + #[test] + fn babel_import_multiple_members_with_alias() { + // Babel: 'import { member1 , member2 as alias} from "module-name";' + let output = transform_code("import { member1, member2 as alias } from \"module-name\";"); + assert!( + output.contains("__varRecorder__.member1 = member1;"), + "captures member1 with assignment: {}", + output + ); + assert!( + output.contains("__varRecorder__.alias = alias;"), + "captures alias with assignment: {}", + output + ); + assert!( + !output.contains("__varRecorder__.member2"), + "member2 not captured (aliased to alias): {}", + output + ); + } + + #[test] + fn babel_import_default_and_member() { + // Babel: 'import defaultMember, { member } from "module-name";' + let output = transform_code("import defaultMember, { member } from \"module-name\";"); + assert!( + output.contains("__varRecorder__.defaultMember = defaultMember;"), + "captures default with assignment: {}", + output + ); + assert!( + output.contains("__varRecorder__.member = member;"), + "captures member with assignment: {}", + output + ); + } + + #[test] + fn babel_import_default_and_star() { + // Babel: 'import defaultMember, * as name from "module-name";' + let output = transform_code("import defaultMember, * as name from \"module-name\";"); + assert!( + output.contains("__varRecorder__.defaultMember = defaultMember;"), + "captures default with assignment: {}", + output + ); + assert!( + output.contains("__varRecorder__.name = name;"), + "captures namespace with assignment: {}", + output + ); + } + + #[test] + fn babel_import_without_binding() { + // Babel: 'import "module-name";' → 'import "module-name";' (no capture) + let output = transform_code("import \"module-name\";"); + assert!( + output.contains("import \"module-name\""), + "keeps bare import: {}", + output + ); + // No capture assignments like __varRecorder__.x = x should appear + assert!( + !output.contains("__varRecorder__."), + "no capture assignment for bare import: {}", + output + ); + } + + // --- export --- + + #[test] + fn babel_export_default_named_var() { + // Babel: 'var x = {x: 23}; export default x;' + // → '_rec.x = { x: 23 }; var x = _rec.x; export default x;' + let output = transform_code("var x = {x: 23}; export default x;"); + assert!( + output.contains("__varRecorder__.x ="), + "captures x: {}", + output + ); + assert!(output.contains("x: 23"), "keeps object literal: {}", output); + assert!( + output.contains("__varRecorder__.default = __varRecorder__.x"), + "captures __rec.default: {}", + output + ); + assert!( + output.contains("export default __varRecorder__.x"), + "export default uses captured ref: {}", + output + ); + } + + #[test] + fn babel_export_var_with_capturing() { + // Babel: 'var a = 23; export var x = a + 1, y = x + 2; export default function f() {}' + let output = transform_code( + "var a = 23;\nexport var x = a + 1, y = x + 2;\nexport default function f() {}", + ); + assert!( + output.contains("__varRecorder__.a = 23;"), + "captures a with value: {}", + output + ); + assert!( + output.contains("__varRecorder__.a + 1"), + "export var x uses captured a ref: {}", + output + ); + assert!( + output.contains("__varRecorder__.x = x;"), + "captures export x: {}", + output + ); + assert!( + output.contains("__varRecorder__.y = y;"), + "captures export y: {}", + output + ); + assert!( + output.contains("__varRecorder__.f = f;"), + "captures default f: {}", + output + ); + assert!( + output.contains("__varRecorder__.default = f;"), + "captures __rec.default: {}", + output + ); + assert!( + output.contains("export default f;"), + "keeps export default: {}", + output + ); + } + + #[test] + fn babel_export_var_statement() { + // Babel: 'var x = 23; export { x };' + let output = transform_code("var x = 23; export { x };"); + assert!( + output.contains("__varRecorder__.x = 23;"), + "captures x with value: {}", + output + ); + assert!( + output.contains("var x = __varRecorder__.x;"), + "re-declares var from recorder: {}", + output + ); + assert!( + output.contains("export { x }"), + "keeps named export: {}", + output + ); + } + + #[test] + fn babel_export_aliased_var_statement() { + // 'var x = 23; export { x as y };' → captures x, renames export local to __export_y__ + let output = transform_code("var x = 23; export { x as y };"); + assert!( + output.contains("__varRecorder__.x = 23;"), + "captures x with value: {}", + output + ); + assert!( + output.contains("var __export_y__ = __varRecorder__.x;"), + "re-declares with __export_ prefix: {}", + output + ); + assert!( + output.contains("export { __export_y__ as y }"), + "keeps aliased export with renamed local: {}", + output + ); + } + + #[test] + fn babel_export_const() { + // Babel: 'export const x = 23;' → 'export const x = 23; _rec.x = x;' + let output = transform_code("export const x = 23;"); + assert!( + output.contains("export const x = 23;"), + "keeps export const declaration: {}", + output + ); + assert!( + output.contains("__varRecorder__.x = x;"), + "captures const x with assignment: {}", + output + ); + } + + #[test] + fn babel_export_function_decl() { + // Babel: 'export function x() {};' → 'function x() {} _rec.x = x; export { x };' + let output = transform_code("export function x() {}"); + assert!( + output.contains("__varRecorder__.x = x;"), + "captures function x with assignment: {}", + output + ); + assert!( + output.contains("export function x()"), + "keeps export function: {}", + output + ); + } + + #[test] + fn babel_export_default_function_decl() { + // Babel: 'export default function x() {};' → 'function x() {} _rec.x = x; export default x;' + let output = transform_code("export default function x() {}"); + assert!( + output.contains("__varRecorder__.x = x;"), + "captures default fn x with assignment: {}", + output + ); + assert!( + output.contains("__varRecorder__.default = x;"), + "captures __rec.default: {}", + output + ); + assert!( + output.contains("export default x;"), + "keeps export default: {}", + output + ); + } + + #[test] + fn babel_export_class_no_class_to_func() { + // Babel: 'export class Foo {};' → 'export class Foo {} _rec.Foo = Foo;' + let output = transform_code("export class Foo {}"); + assert!( + output.contains("export class Foo"), + "keeps export class: {}", + output + ); + assert!( + output.contains("__varRecorder__.Foo = Foo;"), + "captures class Foo with assignment: {}", + output + ); + } + + #[test] + fn babel_export_default_class_no_class_to_func() { + // Babel: 'export default class Foo {};' → 'export default class Foo {} _rec.Foo = Foo;' + let output = transform_code("export default class Foo {}"); + assert!( + output.contains("__varRecorder__.Foo = Foo;"), + "captures default class Foo with assignment: {}", + output + ); + assert!( + output.contains("__varRecorder__.default = __varRecorder__.Foo"), + "captures __rec.default: {}", + output + ); + assert!( + output.contains("export default __varRecorder__.Foo"), + "export default uses captured ref: {}", + output + ); + } + + #[test] + fn babel_export_default_expression() { + // Babel: 'export default foo(1, 2, 3);' → 'export default _rec.foo(1, 2, 3);' + // SWC does not capture undeclared globals; with declared foo it is captured + let output = transform_code("var foo = x => x; export default foo(1, 2, 3);"); + assert!( + output.contains("__varRecorder__.foo(1, 2, 3)"), + "call uses captured foo ref: {}", + output + ); + assert!( + output.contains("export default __varRecorder__.foo(1, 2, 3)"), + "export default uses captured ref: {}", + output + ); + } + + #[test] + fn babel_re_export_namespace_import() { + // Babel: 'import * as completions from "./lib/completions.js"; export { completions }' + let output = transform_code( + "import * as completions from \"./lib/completions.js\";\nexport { completions }", + ); + assert!( + output.contains("import * as completions from"), + "keeps namespace import: {}", + output + ); + assert!( + output.contains("__varRecorder__.completions = completions;"), + "captures completions with assignment: {}", + output + ); + assert!( + output.contains("export { completions }"), + "keeps named export: {}", + output + ); + } + + #[test] + fn babel_re_export_named() { + // SWC materializes re-exports as local aliases so SystemJS emits concrete exports. + let output = transform_code("export { name1, name2 } from \"foo\";"); + assert!( + output.contains("export { __export_name1__ as name1, __export_name2__ as name2 }"), + "keeps re-export with both names: {}", + output + ); + } + + #[test] + fn babel_export_from_named_aliased() { + // Babel: 'export { name1 as foo1, name2 as bar2 } from "foo";' + let output = transform_code("export { name1 as foo1, name2 as bar2 } from \"foo\";"); + assert!(output.contains("export {"), "keeps re-export: {}", output); + } + + #[test] + fn babel_export_bug_1() { + // Babel: 'foo(); export function a() {} export function b() {}' + // → functions hoisted, captures added before foo() call + // SWC does not capture undeclared globals; a and b are captured and hoisted + let output = transform_code("foo();\nexport function a() {}\nexport function b() {}"); + assert!( + output.contains("__varRecorder__.a = a;"), + "captures a with assignment: {}", + output + ); + assert!( + output.contains("__varRecorder__.b = b;"), + "captures b with assignment: {}", + output + ); + // Function captures should be hoisted before foo() call + let a_pos = output.find("__varRecorder__.a = a").unwrap(); + let foo_pos = output.find("foo()").unwrap(); + assert!( + a_pos < foo_pos, + "function capture hoisted before foo() call: {}", + output + ); + } + + #[test] + fn babel_export_bug_2() { + // Babel: 'export { a } from "./package-commands.js"; export function b() {} export function c() {}' + let output = transform_code("export { a } from \"./package-commands.js\";\nexport function b() {}\nexport function c() {}"); + assert!( + output.contains("__varRecorder__.b = b;"), + "captures b with assignment: {}", + output + ); + assert!( + output.contains("__varRecorder__.c = c;"), + "captures c with assignment: {}", + output + ); + assert!( + output.contains("./package-commands.js"), + "keeps re-export source: {}", + output + ); + assert!( + output.contains("export function b()"), + "keeps export function b: {}", + output + ); + assert!( + output.contains("export function c()"), + "keeps export function c: {}", + output + ); + } + + #[test] + fn test_class_with_separate_export() { + // This is the pattern from lively.vm/eval-strategies.js that breaks rollup: + // class is declared, then exported separately via export { } + let output = transform_code("class Foo { eval() { return 1; } }\nexport { Foo };"); + // The class should still be declared (not removed) so rollup can resolve the export + assert!(output.contains("export {"), "keeps export: {}", output); + assert!( + output.contains("class Foo"), + "keeps class declaration: {}", + output + ); + assert!( + output.contains("__varRecorder__.Foo = Foo"), + "captures Foo: {}", + output + ); + } + + // --- Default export capture tests (Divergence O/P) --- + // Babel always sets __rec.default for any form of default export. + // SWC must do the same. + + #[test] + fn test_export_default_named_function_captures_default() { + // Babel: export default function foo() {} → function foo(){} _rec.foo = foo; _rec.default = foo; export default foo; + let output = transform_code("export default function foo() { return 1; }"); + assert!( + output.contains("function foo()"), + "keeps function declaration: {}", + output + ); + assert!( + output.contains("__varRecorder__.foo = foo;"), + "captures foo with assignment: {}", + output + ); + assert!( + output.contains("__varRecorder__.default = foo;"), + "captures __rec.default = foo: {}", + output + ); + assert!( + output.contains("export default foo;"), + "export default uses named binding: {}", + output + ); + } + + #[test] + fn test_export_default_named_class_captures_default() { + // Babel: export default class Foo {} → class Foo {} _rec.Foo = Foo; _rec.default = Foo; export default Foo; + let output = transform_code("export default class Foo { method() {} }"); + assert!( + output.contains("class Foo"), + "keeps class declaration: {}", + output + ); + assert!( + output.contains("__varRecorder__.Foo = Foo;"), + "captures Foo with assignment: {}", + output + ); + assert!( + output.contains("__varRecorder__.default = __varRecorder__.Foo"), + "captures __rec.default from captured Foo: {}", + output + ); + assert!( + output.contains("export default __varRecorder__.Foo"), + "export default uses captured ref: {}", + output + ); + } + + #[test] + fn test_export_default_expression_captures_default() { + // Babel: export default expr → _rec.default = expr; export default expr; + let output = transform_code("var x = 42; export default x;"); + assert!( + output.contains("__varRecorder__.x ="), + "captures x: {}", + output + ); + assert!( + output.contains("__varRecorder__.default = __varRecorder__.x"), + "captures __rec.default from captured x: {}", + output + ); + assert!( + output.contains("export default __varRecorder__.x"), + "export default uses captured ref: {}", + output + ); + } + + #[test] + fn test_export_default_anonymous_function_captures_default() { + // Babel: export default function() {} → _rec.default = function() {}; export default _rec.default; + let output = transform_code("export default function() { return 1; }"); + assert!( + output.contains("__varRecorder__.default") + || output.contains("__varRecorder__[\"default\"]"), + "captures __rec.default for anonymous function: {}", + output + ); + } + + #[test] + fn test_export_default_literal_captures_default() { + // Babel: export default 42 → _rec.default = 42 + let output = transform_code("export default 42;"); + assert!( + output.contains("__varRecorder__.default") + || output.contains("__varRecorder__[\"default\"]"), + "captures __rec.default for literal: {}", + output + ); + } + + // =================================================================== + // Resurrection build: function declaration wrapping + // =================================================================== + + fn transform_code_resurrection(code: &str) -> String { + let cm = Lrc::new(SourceMap::default()); + let fm = cm.new_source_file(FileName::Anon.into(), code.to_string()); + + let mut module = parse_file_as_module( + &fm, + Syntax::Es(Default::default()), + Default::default(), + None, + &mut vec![], + ) + .unwrap(); + + let mut transform = ScopeCapturingTransform::new( + "__varRecorder__".to_string(), + Some(r#"__varRecorder__["test-mod__define__"]"#.to_string()), + vec!["console".to_string()], + false, // captureImports = false for resurrection + true, + true, // resurrection = true + "test-mod".to_string(), + Some("({ pathInPackage: () => \"test-mod\" })".to_string()), + None, + None, + None, + HashMap::new(), + ); + + module.visit_mut_with(&mut transform); + + let mut buf = vec![]; + { + let mut emitter = Emitter { + cfg: Config::default(), + cm: cm.clone(), + comments: None, + wr: JsWriter::new(cm, "\n", &mut buf, None), + }; + + emitter.emit_module(&module).unwrap(); + } + + String::from_utf8(buf).unwrap() + } + + #[test] + fn test_resurrection_func_decl_replaced_with_let_wrapper() { + // Babel: function foo() { return 1; } + // => let __moduleMeta__ = ; + // var foo = wrapper("foo", "function", function() { return 1; }, __moduleMeta__) + // The function declaration should be REPLACED (not kept alongside a capture). + let output = transform_code_resurrection("function foo() { return 1; }"); + // Should have var foo = wrapper(...) + assert!( + output.contains("var foo ="), + "function decl should be replaced with var: {}", + output + ); + assert!( + output.contains("__define__"), + "uses declaration wrapper: {}", + output + ); + assert!( + output.contains("\"foo\""), + "wrapper includes name string: {}", + output + ); + assert!( + output.contains("\"function\""), + "wrapper includes kind string: {}", + output + ); + // The original `function foo` declaration should NOT be present + assert!( + !output.contains("function foo()"), + "original function decl should be removed: {}", + output + ); + // Should have __moduleMeta__ and __module_exports__ declarations + assert!( + output.contains("var __moduleMeta__"), + "has __moduleMeta__ declaration: {}", + output + ); + assert!( + output.contains("__module_exports__"), + "has __module_exports__: {}", + output + ); + } + + #[test] + fn test_resurrection_func_decl_wrapper_uses_module_meta() { + // Divergence B: The 4th argument to the wrapper should be __moduleMeta__, + // not __varRecorder__. __moduleMeta__ is set to the currentModuleAccessor. + let output = transform_code_resurrection("function foo() { return 1; }"); + // Should prepend let __moduleMeta__ = + assert!( + output.contains("var __moduleMeta__ = ("), + "prepends __moduleMeta__ with accessor: {}", + output + ); + assert!( + output.contains("pathInPackage"), + "accessor has pathInPackage: {}", + output + ); + } + + #[test] + fn test_resurrection_declaration_wrapper_expression_is_used_as_callee() { + let output = transform_code_resurrection("function foo() { return 1; }"); + assert!( + output.contains(r#"__varRecorder__["test-mod__define__"]("foo""#), + "wrapper expression should be used directly as callee: {}", + output + ); + assert!( + !output.contains(r#"__varRecorder__["__varRecorder__["#), + "wrapper expression must not be treated as a property key: {}", + output + ); + } + + #[test] + fn test_resurrection_func_decl_non_function_stmts_unchanged() { + // Function declarations should be replaced with var + wrapper. + // Babel's bundler does NOT wrap var declarations with declarationWrapper — + // only function declarations go through insertCapturesForFunctionDeclarations. + let output = transform_code_resurrection("var x = 1; function foo() {} var y = 2;"); + assert!( + output.contains("var foo ="), + "func replaced with var: {}", + output + ); + assert!( + output.contains("\"function\""), + "func wrapper has function kind: {}", + output + ); + // var x and y are captured as plain __varRecorder__ assignments (no __define__ wrapping) + assert!( + output.contains("__varRecorder__.x = 1"), + "var x captured without define: {}", + output + ); + assert!( + output.contains("__varRecorder__.y = 2"), + "var y captured without define: {}", + output + ); + // Function declarations should NOT remain as `function foo` + assert!( + !output.contains("function foo()"), + "original function decl should be removed: {}", + output + ); + } + + // --- Regression tests for function declaration wrapper edge cases --- + + #[test] + fn test_resurrection_exported_function_gets_wrapper() { + // export function foo() {} — the function capture is hoisted and wrapped. + // The export declaration stays (rollup handles it). + let output = transform_code_resurrection("export function foo() { return 1; }"); + assert!( + output.contains("var __moduleMeta__"), + "has module meta declaration: {}", + output + ); + assert!(output.contains("export"), "still exports: {}", output); + assert!( + output.contains("__define__"), + "uses declaration wrapper: {}", + output + ); + assert!( + output.contains("\"foo\""), + "wrapper has function name: {}", + output + ); + } + + #[test] + fn test_resurrection_func_in_nested_scope_not_wrapped() { + // Only top-level function declarations get wrapped, not nested ones + let output = transform_code_resurrection("function outer() { function inner() {} }"); + assert!( + output.contains("var outer ="), + "top-level replaced with var: {}", + output + ); + assert!( + output.contains("\"outer\""), + "wrapper has outer name: {}", + output + ); + assert!( + output.contains("function inner"), + "nested function NOT wrapped, kept as-is: {}", + output + ); + assert!( + !output.contains("let inner"), + "inner should not be replaced with var: {}", + output + ); + } + + #[test] + fn test_resurrection_class_not_affected_by_func_wrapper() { + // Class declarations should NOT be affected by function wrapping + let output = transform_code_resurrection("class Foo {} function bar() {}"); + assert!( + output.contains("var bar ="), + "func replaced with var: {}", + output + ); + assert!( + output.contains("\"bar\""), + "wrapper has bar name: {}", + output + ); + assert!( + output.contains("class Foo") || output.contains("__varRecorder__.Foo"), + "class handled separately: {}", + output + ); + } + + #[test] + fn test_resurrection_async_function_gets_wrapper() { + // async functions are also FunctionDeclarations + let output = transform_code_resurrection("async function fetchData() { return await 1; }"); + assert!( + output.contains("var fetchData ="), + "async func replaced with var: {}", + output + ); + assert!( + output.contains("\"fetchData\""), + "wrapper has function name: {}", + output + ); + assert!( + output.contains("\"function\""), + "wrapper has function kind: {}", + output + ); + assert!( + output.contains("var __moduleMeta__"), + "has module meta declaration: {}", + output + ); + } + + // --- Babel 'declarations' block tests (declarationWrapper behavior) --- + // Babel's bundler only uses declarationWrapper for function declarations + // (via insertCapturesForFunctionDeclarations). Variable declarations and + // assignments are captured without __define__ wrapping. + + #[test] + fn babel_declarations_var_not_wrapped() { + // Babel's bundler does NOT wrap var declarations with __define__. + // Only function declarations get wrapper treatment. + let output = transform_code_resurrection("var x = 23;"); + assert!( + output.contains("__varRecorder__.x = 23"), + "var captured directly: {}", + output + ); + assert!( + !output.contains("__define__"), + "var should NOT use __define__: {}", + output + ); + } + + #[test] + fn babel_declarations_assignment_not_wrapped() { + // Babel's bundler does NOT wrap assignments with __define__. + let output = transform_code_resurrection("var x; x = 23;"); + assert!( + output.contains("__varRecorder__.x = 23"), + "assignment captured directly: {}", + output + ); + // __define__ should only appear if there are function declarations (there are none here) + assert!( + !output.contains("__define__"), + "assignment should NOT use __define__: {}", + output + ); + } + + #[test] + fn babel_declarations_func_decl_wrapped() { + // Babel's insertCapturesForFunctionDeclarations (LAST step) replaces function + // declarations with: let bar = wrapper("bar", "function", function(){}, __moduleMeta__) + // Note: 4th arg is __moduleMeta__ (not __varRecorder__) for function replacement. + let output = transform_code_resurrection("function bar() {}"); + assert!( + output.contains("var bar ="), + "func replaced with var: {}", + output + ); + assert!(output.contains("__define__"), "func wrapped: {}", output); + assert!(output.contains("\"bar\""), "has name: {}", output); + assert!( + output.contains("\"function\""), + "has function kind: {}", + output + ); + assert!( + output.contains("__moduleMeta__)"), + "4th arg is __moduleMeta__: {}", + output + ); + } + + #[test] + fn babel_declarations_export_var_not_wrapped() { + // Babel's bundler does NOT wrap exported var captures with __define__. + let output = transform_code_resurrection("export var x = 23;"); + assert!(output.contains("export"), "keeps export: {}", output); + assert!( + output.contains("__varRecorder__.x = x"), + "exported var captured as ident: {}", + output + ); + assert!( + !output.contains("__define__"), + "exported var should NOT use __define__: {}", + output + ); + } + + #[test] + fn babel_declarations_export_function_wrapped() { + // Babel: 'export function foo() {}' → function declaration gets wrapper treatment + let output = transform_code_resurrection("export function foo() {}"); + assert!(output.contains("export"), "keeps export: {}", output); + assert!(output.contains("__define__"), "wrapped: {}", output); + assert!( + output.contains("\"function\""), + "has function kind: {}", + output + ); + assert!(output.contains("\"foo\""), "has name: {}", output); + } + + #[test] + fn babel_declarations_export_vars_with_separate_export() { + // Babel's bundler does NOT wrap var captures with __define__. + // vars are captured directly, export preserved + let output = transform_code_resurrection("var x, y; x = 23; export { x, y };"); + assert!(output.contains("export {"), "keeps export: {}", output); + assert!( + !output.contains("__define__"), + "vars should NOT use __define__: {}", + output + ); + assert!( + output.contains("__varRecorder__.x"), + "has x capture: {}", + output + ); + assert!( + output.contains("__varRecorder__.y"), + "has y capture: {}", + output + ); + } + + // --- Divergence G/H: no duplicate captures for exported fn/class --- + + #[test] + fn test_no_duplicate_capture_for_exported_function() { + // export function foo() {} should produce exactly ONE __rec.foo capture, + // not two (one from scope capture + one from ExportedImportCapturePass) + let output = transform_code("export function foo() { return 1; }"); + let count = output.matches("__varRecorder__.foo").count(); + // One from hoisted capture, the export itself doesn't add another + assert!( + count <= 2, + "should not have excessive duplicate captures (found {}): {}", + count, + output + ); + } + + #[test] + fn test_no_duplicate_capture_for_exported_class() { + let output = transform_code("export class Foo { method() {} }"); + let count = output.matches("__varRecorder__.Foo").count(); + assert!( + count <= 2, + "should not have excessive duplicate captures (found {}): {}", + count, + output + ); + } + + // --- Divergence Q: __module_exports__ placement --- + + #[test] + fn test_module_exports_placed_after_recorder_init() { + // Babel puts __module_exports__ near the top (after recorder init), + // not at the end of the module body + let output = transform_code_resurrection("export var x = 23; var y = 42;"); + let recorder_pos = output.find("moduleEnv").expect("has recorder init"); + let module_exports_pos = output + .find("__module_exports__") + .expect("has module_exports"); + let last_assignment_pos = output.rfind("__varRecorder__").expect("has assignments"); + assert!( + module_exports_pos < last_assignment_pos, + "__module_exports__ should be near top, not at end: {}", + output + ); + assert!( + module_exports_pos > recorder_pos, + "__module_exports__ should be after recorder init: {}", + output + ); + } + + // --- Divergence S: ExportedImportCapturePass runs for all scope capture --- + // (This is tested via the integration tests in lib.rs which use the full pipeline) + + // =================================================================== + // New tests ported from Babel capturing-test.js + // =================================================================== + + #[test] + fn babel_wraps_literals_exported_as_defaults() { + // Babel line 766: 'export default 32' + // Babel expected: '_rec.$32 = 32; var $32 = _rec.$32; export default $32;' + // In resurrection mode, the declarationWrapper wraps the literal. + let output = transform_code_resurrection("export default 32"); + // The literal 32 should be captured into __varRecorder__.default + assert!( + output.contains("__varRecorder__.default = 32"), + "captures default literal: {}", + output + ); + assert!( + output.contains("export default"), + "keeps export default: {}", + output + ); + // TODO: Babel generates a synthetic $32 variable name for the literal; + // SWC assigns to __varRecorder__.default directly. Both produce correct + // runtime behavior but differ in generated variable names. + } + + #[test] + fn babel_destructuring_not_wrapped_with_declaration_wrapper() { + // Babel's bundler does NOT pass declarationWrapper to the scope capture step. + // Destructured vars are captured directly, not wrapped with __define__. + let output = transform_code_resurrection("var [{x}, y] = foo"); + // SWC uses _tmp_ instead of destructured_1 + assert!( + output.contains("_tmp_"), + "uses temp variable for destructuring: {}", + output + ); + // x and y should be captured without __define__ wrapping + assert!( + output.contains("__varRecorder__.x ="), + "captures destructured x: {}", + output + ); + assert!( + output.contains("__varRecorder__.y ="), + "captures destructured y: {}", + output + ); + // No __define__ wrapping for destructured vars (only function decls get __define__) + assert!( + !output.contains("__define__"), + "destructured vars should NOT use __define__: {}", + output + ); + } + + // --- Divergence S: ExportedImportCapturePass runs for all scope capture --- + // (This is tested via the integration tests in lib.rs which use the full pipeline) + + // --- __module_exports__ alignment tests --- + // These test the exact format of __module_exports__ entries to match Babel. + + #[test] + fn test_module_exports_reexport_uses_resolved_id() { + // Divergence 2: __reexport__ entries must use resolved module IDs, not raw source strings. + // Babel: __reexport__ (e.g. "lively.lang/array.js") + // SWC was producing: __reexport__./array.js (raw source string) + let cm = Lrc::new(SourceMap::default()); + let fm = cm.new_source_file( + FileName::Anon.into(), + "export * from './array.js';".to_string(), + ); + let mut module = parse_file_as_module( + &fm, + Syntax::Es(Default::default()), + Default::default(), + None, + &mut vec![], + ) + .unwrap(); + let mut resolved = HashMap::new(); + resolved.insert("./array.js".to_string(), "lively.lang/array.js".to_string()); + let mut transform = ScopeCapturingTransform::new( + "__varRecorder__".to_string(), + None, + vec![], + false, + true, + true, + "lively.lang/index.js".to_string(), + None, + None, + None, + None, + resolved, + ); + module.visit_mut_with(&mut transform); + let mut buf = vec![]; + { + let mut emitter = Emitter { + cfg: Config::default(), + cm: cm.clone(), + comments: None, + wr: JsWriter::new(cm, "\n", &mut buf, None), + }; + emitter.emit_module(&module).unwrap(); + } + let output = String::from_utf8(buf).unwrap(); + // Must use resolved ID, not raw "./array.js" + assert!( + output.contains("__reexport__lively.lang/array.js"), + "should use resolved ID in __reexport__, not raw source: {}", + output + ); + assert!( + !output.contains("__reexport__./array.js"), + "should NOT use raw source string: {}", + output + ); + } + + #[test] + fn test_module_exports_rename_does_not_duplicate() { + // Divergence 3: export { x as y } should produce ONLY "__rename__x->y", + // NOT both "__rename__x->y" AND "y". Babel uses continue after rename. + let output = transform_code_resurrection("var x = 1; export { x as y };"); + // Count occurrences of "y" in __module_exports__ + let me_start = output + .find("__module_exports__") + .expect("has __module_exports__"); + let me_section = &output[me_start..]; + let me_end = me_section.find(';').unwrap_or(me_section.len()); + let me_content = &me_section[..me_end]; + assert!( + me_content.contains("__rename__x->y"), + "__module_exports__ should have __rename__x->y: {}", + me_content + ); + // "y" should NOT appear as a separate entry. Babel does `continue` after + // __rename__, so ONLY "__rename__x->y" appears, not both that AND "y". + let y_count = me_content.matches("\"y\"").count(); + assert_eq!(y_count, 0, "standalone \"y\" should NOT be in __module_exports__ (Babel uses continue after __rename__): {}", me_content); + } + + #[test] + fn test_module_exports_named_reexport_per_specifier() { + // Divergence 4: export { x, y as z } from 'mod' should produce per-specifier + // entries like Babel, not a blanket __reexport__. + // Babel produces: ["x", "__rename__y->z", "z"] (individual entries) + // SWC was producing: ["__reexport__mod"] (blanket) + let cm = Lrc::new(SourceMap::default()); + let fm = cm.new_source_file( + FileName::Anon.into(), + "export { x, y as z } from 'mod';".to_string(), + ); + let mut module = parse_file_as_module( + &fm, + Syntax::Es(Default::default()), + Default::default(), + None, + &mut vec![], + ) + .unwrap(); + let mut resolved = HashMap::new(); + resolved.insert("mod".to_string(), "resolved/mod.js".to_string()); + let mut transform = ScopeCapturingTransform::new( + "__varRecorder__".to_string(), + None, + vec![], + false, + true, + true, + "test.js".to_string(), + None, + None, + None, + None, + resolved, + ); + module.visit_mut_with(&mut transform); + let mut buf = vec![]; + { + let mut emitter = Emitter { + cfg: Config::default(), + cm: cm.clone(), + comments: None, + wr: JsWriter::new(cm, "\n", &mut buf, None), + }; + emitter.emit_module(&module).unwrap(); + } + let output = String::from_utf8(buf).unwrap(); + // Should have per-specifier entries, not blanket __reexport__ + assert!( + output.contains("\"x\""), + "should have individual 'x' entry: {}", + output + ); + assert!( + output.contains("__rename__y->z"), + "should have __rename__y->z entry: {}", + output + ); + // Should NOT have blanket __reexport__ + assert!( + !output.contains("__reexport__"), + "should NOT use blanket __reexport__ for named re-exports: {}", + output + ); + } + + #[test] + fn test_module_exports_export_all_uses_resolved_id() { + // Same as divergence 2 but for export * from '...' + let cm = Lrc::new(SourceMap::default()); + let fm = cm.new_source_file( + FileName::Anon.into(), + "export * from './morph.js';".to_string(), + ); + let mut module = parse_file_as_module( + &fm, + Syntax::Es(Default::default()), + Default::default(), + None, + &mut vec![], + ) + .unwrap(); + let mut resolved = HashMap::new(); + resolved.insert( + "./morph.js".to_string(), + "lively.morphic/morph.js".to_string(), + ); + let mut transform = ScopeCapturingTransform::new( + "__varRecorder__".to_string(), + None, + vec![], + false, + true, + true, + "lively.morphic/index.js".to_string(), + None, + None, + None, + None, + resolved, + ); + module.visit_mut_with(&mut transform); + let mut buf = vec![]; + { + let mut emitter = Emitter { + cfg: Config::default(), + cm: cm.clone(), + comments: None, + wr: JsWriter::new(cm, "\n", &mut buf, None), + }; + emitter.emit_module(&module).unwrap(); + } + let output = String::from_utf8(buf).unwrap(); + assert!( + output.contains("__reexport__lively.morphic/morph.js"), + "export * should use resolved ID: {}", + output + ); + } + + // --- Divergence 1: Function declaration wrapper hoisting --- + // Babel hoists function declarations to the TOP (putFunctionDeclsInFront) + // before replacing them with let. SWC must do the same to avoid TDZ issues. + + #[test] + fn test_func_decl_wrapper_hoisted_to_top() { + // When a function is declared AFTER code that uses it, the let replacement + // must be hoisted to the top (matching Babel's putFunctionDeclsInFront). + // Otherwise: var x = foo(); ... var foo = wrapper(...) → TDZ error + let output = transform_code_resurrection("var x = foo(); function foo() { return 1; }"); + let let_foo_pos = output.find("var foo").expect("has var foo replacement"); + let var_x_pos = output.find("__varRecorder__.x").expect("has var x capture"); + assert!( + let_foo_pos < var_x_pos, + "var foo must be BEFORE var x (hoisted to top): let_foo={}, var_x={}\n{}", + let_foo_pos, + var_x_pos, + output + ); + } + + #[test] + fn test_func_decl_wrapper_multiple_hoisted_to_top() { + // Multiple functions should all be hoisted to top + let output = transform_code_resurrection( + "var x = 1; function foo() {} var y = 2; function bar() {}", + ); + let let_foo_pos = output.find("var foo").expect("has var foo"); + let let_bar_pos = output.find("var bar").expect("has let bar"); + let var_x_pos = output.find("__varRecorder__.x").expect("has var x"); + let var_y_pos = output.find("__varRecorder__.y").expect("has var y"); + assert!(let_foo_pos < var_x_pos, "foo hoisted before x: {}", output); + assert!(let_bar_pos < var_y_pos, "bar hoisted before y: {}", output); + } + + #[test] + fn test_func_decl_wrapper_after_recorder_init() { + // The hoisted let declarations must come AFTER the recorder init and __moduleMeta__, + // but BEFORE other code + let output = transform_code_resurrection("var x = 1; function foo() {}"); + let recorder_pos = output.find("moduleEnv").expect("has recorder init"); + let meta_pos = output.find("__moduleMeta__").expect("has module meta"); + let let_foo_pos = output.find("var foo").expect("has var foo"); + let var_x_pos = output.find("__varRecorder__.x").expect("has var x"); + assert!(recorder_pos < meta_pos, "recorder before meta: {}", output); + assert!(meta_pos < let_foo_pos, "meta before var foo: {}", output); + assert!(let_foo_pos < var_x_pos, "var foo before var x: {}", output); + } + + #[test] + fn test_func_decl_wrapper_original_position_gets_reference() { + // Babel replaces the original function declaration position with just a reference (foo;) + // This ensures the function body code still has a reference at the original position. + let output = + transform_code_resurrection("var x = 1; function foo() { return 1; } var y = foo();"); + // The var foo should be at the top + assert!( + output.find("var foo").unwrap() < output.find("__varRecorder__.x").unwrap(), + "var foo hoisted: {}", + output + ); + // The original position should have foo; (a reference, not the declaration) + // This is between x and y assignments + let x_pos = output.find("__varRecorder__.x =").unwrap(); + let y_pos = output.find("__varRecorder__.y =").unwrap(); + let between = &output[x_pos..y_pos]; + // Should NOT contain "function foo" (the declaration was hoisted) + assert!( + !between.contains("function foo"), + "original position should not have function declaration: {}", + between + ); + } + + #[test] + fn class_transform_iife_not_wrapped_with_define() { + // Class-to-function transform produces: var Foo = function(superclass) { ... }(undefined) + // The __define__ wrapper must NOT be applied to these IIFEs because: + // 1. initializeES6ClassForLively sets lively-module-meta on the class at runtime + // 2. __define__("Foo", "assignment", Foo, __varRecorder__) would overwrite that + // metadata with __varRecorder__, breaking getPropSettings which destructures + // klass[Symbol.for('lively-module-meta')].package.name + let output = transform_code_resurrection( + "export var Foo = function(superclass) { return superclass; }(undefined)", + ); + // Should have simple capture: __varRecorder__.Foo = Foo + assert!( + output.contains("__varRecorder__.Foo = Foo"), + "should capture class var into recorder: {}", + output + ); + // Should NOT wrap with __define__ for the IIFE export capture + // (there may be __define__ elsewhere but NOT for "Foo", "assignment") + assert!( + !output.contains("__define__\"](\"Foo\", \"assignment\""), + "should NOT wrap class IIFE with __define__: {}", + output + ); + } + + #[test] + fn component_for_not_wrapped_with_define() { + // component.for() returns a ComponentDescriptor whose init() sets + // Symbol.for('lively-module-meta') with correct module/export metadata. + // __define__ would overwrite that with __varRecorder__, causing + // "Cannot read properties of undefined (reading 'exported')" at runtime. + let output = transform_code_resurrection( + "export const Foo = component.for(() => component({}), { module: 'test.cp.js', export: 'Foo' }, System, __varRecorder__, 'Foo')" + ); + // Should have simple capture: __varRecorder__.Foo = Foo + assert!( + output.contains("__varRecorder__.Foo = Foo"), + "should capture component var into recorder: {}", + output + ); + // Should NOT wrap with __define__ + assert!( + !output.contains("__define__\"](\"Foo\", \"assignment\""), + "should NOT wrap component.for() with __define__: {}", + output + ); + } + + #[test] + fn non_exported_component_for_not_wrapped_with_define() { + // Same as above but for non-exported const declarations + let output = transform_code_resurrection( + "const Bar = component.for(() => component({}), { module: 'test.cp.js', export: 'Bar' }, System, __varRecorder__, 'Bar')" + ); + assert!( + output.contains("__varRecorder__.Bar"), + "should capture component var into recorder: {}", + output + ); + assert!( + !output.contains("__define__\"](\"Bar\", \"const\""), + "should NOT wrap component.for() with __define__: {}", + output + ); + } + + #[test] + fn non_exported_class_transform_iife_not_wrapped_with_define() { + // Same as above but for non-exported var declarations + let output = transform_code_resurrection( + "var Bar = function(superclass) { return superclass; }(undefined)", + ); + // Should have capture: __varRecorder__.Bar = + assert!( + output.contains("__varRecorder__.Bar"), + "should capture class var into recorder: {}", + output + ); + // Should NOT wrap with __define__ for the IIFE + assert!( + !output.contains("__define__\"](\"Bar\", \"var\""), + "should NOT wrap class IIFE with __define__: {}", + output + ); + } +} diff --git a/lively.freezer/swc-plugin/src/transforms/systemjs.rs b/lively.freezer/swc-plugin/lively-swc-transforms/src/transforms/systemjs.rs similarity index 91% rename from lively.freezer/swc-plugin/src/transforms/systemjs.rs rename to lively.freezer/swc-plugin/lively-swc-transforms/src/transforms/systemjs.rs index 532cb7dc7c..446893094c 100644 --- a/lively.freezer/swc-plugin/src/transforms/systemjs.rs +++ b/lively.freezer/swc-plugin/lively-swc-transforms/src/transforms/systemjs.rs @@ -1,10 +1,8 @@ -use std::collections::HashSet; -use swc_core::common::DUMMY_SP; -use swc_core::ecma::{ - ast::*, - visit::{VisitMut, VisitMutWith}, -}; use crate::utils::ast_helpers::*; +use std::collections::HashSet; +use swc_common::DUMMY_SP; +use swc_ecma_ast::*; +use swc_ecma_visit::{VisitMut, VisitMutWith}; /// Transform that rewrites SystemJS register calls to capture setters /// @@ -16,7 +14,11 @@ pub struct SystemJsTransform { } impl SystemJsTransform { - pub fn new(capture_obj: String, declaration_wrapper: Option, excluded: Vec) -> Self { + pub fn new( + capture_obj: String, + declaration_wrapper: Option, + excluded: Vec, + ) -> Self { Self { capture_obj, declaration_wrapper, @@ -63,7 +65,7 @@ impl SystemJsTransform { fn wrap_setter_assignment(&self, name: &str, assignment_expr: Expr) -> Expr { if let Some(wrapper) = &self.declaration_wrapper { create_call_expr( - create_ident_expr(wrapper), + create_declaration_wrapper_callee(&self.capture_obj, wrapper), vec![ to_expr_or_spread(create_string_expr(name)), to_expr_or_spread(create_string_expr("var")), @@ -82,14 +84,17 @@ impl SystemJsTransform { let replacement = match stmt { Stmt::Expr(ExprStmt { expr, .. }) => match &**expr { Expr::Assign(assign) if assign.op == AssignOp::Assign => { - let AssignTarget::Simple(SimpleAssignTarget::Ident(binding_ident)) = &assign.left else { + let AssignTarget::Simple(SimpleAssignTarget::Ident(binding_ident)) = + &assign.left + else { continue; }; let var_name = binding_ident.id.sym.to_string(); if self.excluded.contains(&var_name) { continue; } - let rhs = self.wrap_setter_assignment(&var_name, Expr::Assign(assign.clone())); + let rhs = + self.wrap_setter_assignment(&var_name, Expr::Assign(assign.clone())); Some(Stmt::Expr(ExprStmt { span: DUMMY_SP, expr: Box::new(create_assign_expr( @@ -144,7 +149,9 @@ impl SystemJsTransform { let idx = execute_body.stmts.iter().position(|stmt| match stmt { Stmt::Expr(ExprStmt { expr, .. }) => { if let Expr::Assign(assign) = &**expr { - if let AssignTarget::Simple(SimpleAssignTarget::Ident(binding_ident)) = &assign.left { + if let AssignTarget::Simple(SimpleAssignTarget::Ident(binding_ident)) = + &assign.left + { return binding_ident.id.sym.as_ref() == self.capture_obj; } } @@ -191,7 +198,10 @@ impl SystemJsTransform { let Some(return_stmt) = declare_body.stmts.get_mut(return_idx) else { return; }; - let Stmt::Return(ReturnStmt { arg: Some(ret_arg), .. }) = return_stmt else { + let Stmt::Return(ReturnStmt { + arg: Some(ret_arg), .. + }) = return_stmt + else { return; }; let Expr::Object(ret_obj) = &mut **ret_arg else { diff --git a/lively.freezer/swc-plugin/src/utils/ast_helpers.rs b/lively.freezer/swc-plugin/lively-swc-transforms/src/utils/ast_helpers.rs similarity index 76% rename from lively.freezer/swc-plugin/src/utils/ast_helpers.rs rename to lively.freezer/swc-plugin/lively-swc-transforms/src/utils/ast_helpers.rs index b5244e0596..4f99022e63 100644 --- a/lively.freezer/swc-plugin/src/utils/ast_helpers.rs +++ b/lively.freezer/swc-plugin/lively-swc-transforms/src/utils/ast_helpers.rs @@ -1,8 +1,5 @@ -use swc_core::ecma::{ - ast::*, - utils::quote_str, -}; -use swc_core::common::{SyntaxContext, DUMMY_SP}; +use swc_common::{SyntaxContext, DUMMY_SP}; +use swc_ecma_ast::*; /// Create a member expression: obj.prop pub fn create_member_expr(obj: Expr, prop: &str) -> Expr { @@ -30,6 +27,42 @@ pub fn create_ident_expr(name: &str) -> Expr { Expr::Ident(Ident::new(name.into(), DUMMY_SP, SyntaxContext::empty())) } +fn is_identifier_name(source: &str) -> bool { + let mut chars = source.chars(); + let Some(first) = chars.next() else { + return false; + }; + (first.is_ascii_alphabetic() || first == '_' || first == '$') + && chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$') +} + +fn is_identifier_path(source: &str) -> bool { + !source.is_empty() && source.split('.').all(is_identifier_name) +} + +fn looks_like_member_expr(source: &str) -> bool { + if let Some(bracket_pos) = source.find('[') { + return source.ends_with(']') && is_identifier_path(&source[..bracket_pos]); + } + + if let Some(dot_pos) = source.rfind('.') { + return is_identifier_path(&source[..dot_pos]) + && is_identifier_name(&source[dot_pos + 1..]); + } + + false +} + +/// Build a declaration wrapper callee from either an expression-valued config +/// (`__rec["module__define__"]`) or a recorder property name (`defVar_...`). +pub fn create_declaration_wrapper_callee(capture_obj: &str, wrapper: &str) -> Expr { + if looks_like_member_expr(wrapper) { + parse_expr_or_ident(wrapper) + } else { + create_computed_member_expr(create_ident_expr(capture_obj), create_string_expr(wrapper)) + } +} + /// Compatibility helper for expression-valued config fields. /// Without parser support in plugin builds, we preserve historical behavior: /// emit the source string as an identifier-shaped expr node. @@ -56,12 +89,9 @@ pub fn parse_expr_or_ident(source: &str) -> Expr { if let Some(dot_pos) = source.rfind('.') { let obj_str = &source[..dot_pos]; let prop_str = &source[dot_pos + 1..]; - // Only treat as member expr if both parts look like identifiers - if !obj_str.is_empty() && !prop_str.is_empty() - && obj_str.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '$') - && prop_str.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '$') - { - return create_member_expr(create_ident_expr(obj_str), prop_str); + // Only treat as member expr if all parts look like identifiers. + if is_identifier_path(obj_str) && is_identifier_name(prop_str) { + return create_member_expr(parse_expr_or_ident(obj_str), prop_str); } } create_ident_expr(source) @@ -69,7 +99,11 @@ pub fn parse_expr_or_ident(source: &str) -> Expr { /// Create a string literal expression pub fn create_string_expr(value: &str) -> Expr { - Expr::Lit(Lit::Str(quote_str!(value))) + Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: value.into(), + raw: None, + })) } /// Create a call expression: callee(args...) @@ -260,7 +294,9 @@ pub fn expr_to_assign_target(expr: Expr) -> AssignTarget { })), Expr::Paren(paren) => AssignTarget::Simple(SimpleAssignTarget::Paren(paren)), Expr::OptChain(opt_chain) => AssignTarget::Simple(SimpleAssignTarget::OptChain(opt_chain)), - Expr::SuperProp(super_prop) => AssignTarget::Simple(SimpleAssignTarget::SuperProp(super_prop)), + Expr::SuperProp(super_prop) => { + AssignTarget::Simple(SimpleAssignTarget::SuperProp(super_prop)) + } _ => panic!("Cannot convert {:?} to AssignTarget", expr), } } @@ -304,3 +340,57 @@ pub fn create_iife(body: BlockStmt) -> Expr { create_call_expr(func, vec![]) } + +/// Check if a name is a JavaScript reserved keyword that cannot be used as +/// a variable name. +pub fn is_reserved_keyword(name: &str) -> bool { + matches!( + name, + "break" + | "case" + | "catch" + | "continue" + | "debugger" + | "default" + | "delete" + | "do" + | "else" + | "export" + | "extends" + | "finally" + | "for" + | "function" + | "if" + | "import" + | "in" + | "instanceof" + | "new" + | "return" + | "super" + | "switch" + | "this" + | "throw" + | "try" + | "typeof" + | "var" + | "void" + | "while" + | "with" + | "yield" + | "enum" + | "class" + | "const" + | "let" + | "await" + | "implements" + | "interface" + | "package" + | "private" + | "protected" + | "public" + | "static" + | "null" + | "true" + | "false" + ) +} diff --git a/lively.freezer/swc-plugin/src/utils/mod.rs b/lively.freezer/swc-plugin/lively-swc-transforms/src/utils/mod.rs similarity index 100% rename from lively.freezer/swc-plugin/src/utils/mod.rs rename to lively.freezer/swc-plugin/lively-swc-transforms/src/utils/mod.rs diff --git a/lively.freezer/swc-plugin/src/utils/scope_analyzer.rs b/lively.freezer/swc-plugin/lively-swc-transforms/src/utils/scope_analyzer.rs similarity index 97% rename from lively.freezer/swc-plugin/src/utils/scope_analyzer.rs rename to lively.freezer/swc-plugin/lively-swc-transforms/src/utils/scope_analyzer.rs index a0a1749ad8..7c110d9194 100644 --- a/lively.freezer/swc-plugin/src/utils/scope_analyzer.rs +++ b/lively.freezer/swc-plugin/lively-swc-transforms/src/utils/scope_analyzer.rs @@ -1,6 +1,6 @@ use std::collections::{HashMap, HashSet}; -use swc_core::ecma::ast::*; -use swc_core::ecma::visit::{Visit, VisitWith}; +use swc_ecma_ast::*; +use swc_ecma_visit::{Visit, VisitWith}; /// Analyzes variable scope and determines which variables are top-level #[derive(Default)] @@ -237,8 +237,8 @@ fn extract_ids_recursive(pat: &Pat, ids: &mut Vec) { #[cfg(test)] mod tests { use super::*; - use swc_core::ecma::parser::{parse_file_as_module, Syntax}; - use swc_core::common::{FileName, SourceMap, sync::Lrc}; + use swc_common::{sync::Lrc, FileName, SourceMap}; + use swc_ecma_parser::{parse_file_as_module, Syntax}; fn analyze_code(code: &str) -> ScopeAnalyzer { let cm = Lrc::new(SourceMap::default()); diff --git a/lively.freezer/swc-plugin/lively_swc_plugin.wasm b/lively.freezer/swc-plugin/lively_swc_plugin.wasm index 6d7943438e..69c995179a 100755 Binary files a/lively.freezer/swc-plugin/lively_swc_plugin.wasm and b/lively.freezer/swc-plugin/lively_swc_plugin.wasm differ diff --git a/lively.freezer/swc-plugin/src/transforms/scope_capturing.rs b/lively.freezer/swc-plugin/src/transforms/scope_capturing.rs deleted file mode 100644 index ae267e84d0..0000000000 --- a/lively.freezer/swc-plugin/src/transforms/scope_capturing.rs +++ /dev/null @@ -1,3202 +0,0 @@ -use swc_core::common::{SyntaxContext, DUMMY_SP}; -use std::collections::{HashSet, HashMap}; -use swc_core::ecma::{ - ast::*, - visit::{VisitMut, VisitMutWith}, -}; - -use crate::utils::ast_helpers::*; -use crate::utils::scope_analyzer::ScopeAnalyzer; - -/// Transform that captures top-level variables into a recorder object -/// -/// Transforms: -/// - `var x = 1` → `__varRecorder__.x = 1` -/// - `x + 2` → `__varRecorder__.x + 2` -/// - Handles destructuring, function hoisting, and exports -pub struct ScopeCapturingTransform { - /// Name of the capture object (e.g., "__varRecorder__") - capture_obj: String, - - /// Optional wrapper function for declarations (for resurrection builds) - declaration_wrapper: Option, - - /// Variables to exclude from capturing - excluded: HashSet, - - /// Whether to capture imports - capture_imports: bool, - - /// Whether this is a resurrection build - resurrection: bool, - - /// Optional module hash for resurrection builds - module_hash: Option, - - /// Current module id - module_id: String, - - /// Optional current module accessor expression (legacy parity for import.meta). - current_module_accessor: Option, - - /// Top-level variables that should be captured - capturable_vars: HashSet, - - /// Top-level imported bindings - imported_vars: HashSet, - - /// Top-level function capture assignments that must run before body code - hoisted_function_captures: Vec, - - /// Current depth (0 = module level, >0 = nested) - depth: usize, - - /// Lexically declared variable names in nested scopes (for shadowing detection) - scope_stack: Vec, - - /// Collected export metadata for __module_exports__ (resurrection builds only). - /// Entries follow the same format as the Babel path: - /// - `"exportedName"` for regular exports - /// - `"__rename__local->exported"` for renamed exports - /// - `"__reexport__moduleId"` for star re-exports - /// - `"__default__localName"` for default exports - collected_exports: Vec, - - /// Resolved import source specifier → normalized module ID. - /// Used for resurrection namespace transforms (exportsOf calls). - resolved_imports: HashMap, - - /// Names declared by the currently visited variable declaration chain. - /// Needed for parity with legacy transform behavior: references inside a - /// var-declarator initializer must resolve to local bindings declared in - /// the same declaration list, not the recorder member. - current_var_decl_stack: Vec>, -} - -struct ScopeFrame { - names: HashSet, - is_function_scope: bool, -} - -impl ScopeCapturingTransform { - pub fn new( - capture_obj: String, - declaration_wrapper: Option, - excluded: Vec, - capture_imports: bool, - resurrection: bool, - module_id: String, - current_module_accessor: Option, - module_hash: Option, - resolved_imports: HashMap, - ) -> Self { - Self { - capture_obj, - declaration_wrapper, - excluded: excluded.into_iter().collect(), - capture_imports, - resurrection, - module_hash, - module_id, - current_module_accessor, - capturable_vars: HashSet::new(), - imported_vars: HashSet::new(), - hoisted_function_captures: Vec::new(), - collected_exports: Vec::new(), - resolved_imports, - depth: 0, - scope_stack: vec![ScopeFrame { - names: HashSet::new(), - is_function_scope: true, - }], - current_var_decl_stack: Vec::new(), - } - } - - /// Check if an identifier should be captured - fn should_capture(&self, id: &Id) -> bool { - // Never rewrite identifiers that are shadowed in a nested lexical scope. - // This keeps locals (params/vars/lets) untouched while still rewriting - // references to captured top-level bindings in nested functions/blocks. - if self - .scope_stack - .iter() - .skip(1) - .rev() - .any(|scope| scope.names.contains(id.0.as_ref())) - { - return false; - } - - // While walking a variable declaration initializer, do not rewrite - // references to bindings declared by the same declaration statement. - // Example: - // var xe = ..., Ue = get(xe) - // must keep `xe` local here, matching legacy JS transform output. - if self - .current_var_decl_stack - .iter() - .rev() - .any(|names| names.contains(id.0.as_ref())) - { - return false; - } - - // Don't capture excluded variables - if self.excluded.contains(id.0.as_ref()) { - return false; - } - - // Keep imported bindings as plain identifiers unless import capture is enabled. - if !self.capture_imports && self.imported_vars.contains(id) { - return false; - } - - // Check if it's a capturable variable - self.capturable_vars.contains(id) - } - - /// Create a member expression to the capture object: __varRecorder__.name - fn create_captured_member(&self, name: &str) -> Expr { - create_member_expr(create_ident_expr(&self.capture_obj), name) - } - - /// Wrap a declaration with the declaration wrapper if configured. - /// Scope capture wrapper signature: wrapper(name, kind, value, captureObj) - /// Babel tests confirm the 4th arg is _rec (capture obj), not __moduleMeta__. - /// __moduleMeta__ is only used in insertCapturesForFunctionDeclarations (the LAST step). - fn wrap_declaration(&self, name: &str, kind: &str, value: Expr) -> Expr { - if let Some(ref wrapper) = self.declaration_wrapper { - // declarationWrapper("name", "kind", value, __varRecorder__) - create_call_expr( - parse_expr_or_ident(wrapper), - vec![ - to_expr_or_spread(create_string_expr(name)), - to_expr_or_spread(create_string_expr(kind)), - to_expr_or_spread(value), - to_expr_or_spread(create_ident_expr(&self.capture_obj)), - ], - ) - } else { - value - } - } - - /// Check if a pattern includes any capturable identifiers - fn should_capture_pattern(&self, pat: &Pat) -> bool { - extract_idents_from_pat(pat) - .into_iter() - .any(|id| self.should_capture(&id)) - } - - /// Create a default initializer for an uninitialized binding - fn create_default_init(&self, name: &str) -> Expr { - Expr::Bin(BinExpr { - span: DUMMY_SP, - op: BinaryOp::LogicalOr, - left: Box::new(self.create_captured_member(name)), - right: Box::new(create_ident_expr("undefined")), - }) - } - - /// Transform a pattern into statements assigning to the capture object - fn transform_pattern_to_stmts( - &self, - pat: &Pat, - init: Option>, - declaration_kind: &str, - ) -> Vec { - let mut stmts = Vec::new(); - - match pat { - Pat::Ident(BindingIdent { id, .. }) => { - if self.should_capture(&id.to_id()) { - let member = self.create_captured_member(id.sym.as_ref()); - let init_expr = init.unwrap_or_else(|| Box::new(self.create_default_init(id.sym.as_ref()))); - // Babel's bundler does NOT pass declarationWrapper to the scope capture - // step — it's only used in insertCapturesForFunctionDeclarations (a separate - // step that ONLY wraps function declarations). So variable declarations - // are never wrapped with __define__. We match that behavior here. - let value = *init_expr; - stmts.push(Stmt::Expr(ExprStmt { - span: DUMMY_SP, - expr: Box::new(create_assign_expr( - expr_to_assign_target(member), - value, - )), - })); - } - } - Pat::Array(ArrayPat { elems, .. }) => { - // For array destructuring: var [a, b] = arr - // Create temp variable: var _tmp = arr - // Then: a = _tmp[0], b = _tmp[1] - if let Some(init_expr) = init { - let temp_name = format!("_tmp_{}", rand_id()); - let temp_ident = create_ident_expr(&temp_name); - - // First assignment: _tmp = init - stmts.push(Stmt::Decl(create_var_decl( - VarDeclKind::Var, - &temp_name, - Some(*init_expr), - ))); - - // Then assign each element - for (i, elem) in elems.iter().enumerate() { - if let Some(elem_pat) = elem { - if let Pat::Rest(rest_pat) = elem_pat { - let slice_call = create_call_expr( - create_member_expr(temp_ident.clone(), "slice"), - vec![to_expr_or_spread(Expr::Lit(Lit::Num(Number { - span: DUMMY_SP, - value: i as f64, - raw: None, - })))], - ); - let rest_stmts = self.transform_pattern_to_stmts( - &rest_pat.arg, - Some(Box::new(slice_call)), - declaration_kind, - ); - stmts.extend(rest_stmts); - } else { - let indexed = create_computed_member_expr( - temp_ident.clone(), - Expr::Lit(Lit::Num(Number { - span: DUMMY_SP, - value: i as f64, - raw: None, - })), - ); - let elem_stmts = self - .transform_pattern_to_stmts( - elem_pat, - Some(Box::new(indexed)), - declaration_kind, - ); - stmts.extend(elem_stmts); - } - } - } - } - } - Pat::Object(ObjectPat { props, .. }) => { - // For object destructuring: var {a, b: c} = obj - // Create temp variable: var _tmp = obj - // Then: a = _tmp.a, c = _tmp.b - if let Some(init_expr) = init { - let temp_name = format!("_tmp_{}", rand_id()); - let temp_ident = create_ident_expr(&temp_name); - - // First assignment: _tmp = init - stmts.push(Stmt::Decl(create_var_decl( - VarDeclKind::Var, - &temp_name, - Some(*init_expr), - ))); - - let known_keys: Vec = props - .iter() - .filter_map(|prop| match prop { - ObjectPatProp::KeyValue(kv) => match &kv.key { - PropName::Ident(id) => Some(id.sym.to_string()), - PropName::Str(s) => Some(s.value.to_string()), - _ => None, - }, - ObjectPatProp::Assign(assign) => Some(assign.key.sym.to_string()), - ObjectPatProp::Rest(_) => None, - }) - .collect(); - - // Then assign each property - for prop in props { - match prop { - ObjectPatProp::KeyValue(kv) => { - let key_name: String = match &kv.key { - PropName::Ident(id) => (&*id.sym).to_owned(), - PropName::Str(s) => s.value.to_string(), - _ => continue, - }; - - let member = create_member_expr(temp_ident.clone(), &key_name); - let prop_stmts = self.transform_pattern_to_stmts( - &kv.value, - Some(Box::new(member)), - declaration_kind, - ); - stmts.extend(prop_stmts); - } - ObjectPatProp::Assign(assign) => { - let key_name = assign.key.sym.to_string(); - let member = create_member_expr(temp_ident.clone(), &key_name); - - if self.should_capture(&assign.key.to_id()) { - let captured = self.create_captured_member(&key_name); - let value = if let Some(ref default) = assign.value { - // Handle default value: {a = 5} = obj - // Keep parity with legacy transform: - // value === undefined ? default : value - Expr::Cond(CondExpr { - span: DUMMY_SP, - test: Box::new(Expr::Bin(BinExpr { - span: DUMMY_SP, - op: BinaryOp::EqEqEq, - left: Box::new(member.clone()), - right: Box::new(create_ident_expr("undefined")), - })), - cons: default.clone(), - alt: Box::new(member), - }) - } else { - member - }; - - stmts.push(Stmt::Expr(ExprStmt { - span: DUMMY_SP, - expr: Box::new(create_assign_expr( - expr_to_assign_target(captured), - value, - )), - })); - } - } - ObjectPatProp::Rest(rest) => { - // Handle rest properties: {...rest} - // Legacy transform materializes a new object and copies unknown keys. - if let Pat::Ident(BindingIdent { id: rest_ident, .. }) = &*rest.arg { - stmts.push(Stmt::Decl(create_var_decl( - VarDeclKind::Var, - rest_ident.sym.as_ref(), - Some(create_object_lit(vec![])), - ))); - - if self.should_capture(&rest_ident.to_id()) { - let captured = - self.create_captured_member(rest_ident.sym.as_ref()); - stmts.push(Stmt::Expr(ExprStmt { - span: DUMMY_SP, - expr: Box::new(create_assign_expr( - expr_to_assign_target(captured), - Expr::Ident(rest_ident.clone()), - )), - })); - } - - let key_ident = Ident::new( - "__key".into(), - DUMMY_SP, - SyntaxContext::empty(), - ); - let mut for_body_stmts: Vec = known_keys - .iter() - .map(|key| { - Stmt::If(IfStmt { - span: DUMMY_SP, - test: Box::new(Expr::Bin(BinExpr { - span: DUMMY_SP, - op: BinaryOp::EqEqEq, - left: Box::new(Expr::Ident(key_ident.clone())), - right: Box::new(create_string_expr(key)), - })), - cons: Box::new(Stmt::Continue(ContinueStmt { - span: DUMMY_SP, - label: None, - })), - alt: None, - }) - }) - .collect(); - - let rest_member = create_computed_member_expr( - Expr::Ident(rest_ident.clone()), - Expr::Ident(key_ident.clone()), - ); - let source_member = create_computed_member_expr( - temp_ident.clone(), - Expr::Ident(key_ident.clone()), - ); - for_body_stmts.push(Stmt::Expr(ExprStmt { - span: DUMMY_SP, - expr: Box::new(create_assign_expr( - expr_to_assign_target(rest_member), - source_member, - )), - })); - - let for_in = Stmt::ForIn(ForInStmt { - span: DUMMY_SP, - left: ForHead::VarDecl(Box::new(VarDecl { - span: DUMMY_SP, - ctxt: SyntaxContext::empty(), - kind: VarDeclKind::Var, - declare: false, - decls: vec![VarDeclarator { - span: DUMMY_SP, - name: Pat::Ident(BindingIdent { - id: key_ident, - type_ann: None, - }), - init: None, - definite: false, - }], - })), - right: Box::new(temp_ident.clone()), - body: Box::new(Stmt::Block(BlockStmt { - span: DUMMY_SP, - ctxt: SyntaxContext::empty(), - stmts: for_body_stmts, - })), - }); - - let iife = Expr::Call(CallExpr { - span: DUMMY_SP, - ctxt: SyntaxContext::empty(), - callee: Callee::Expr(Box::new(Expr::Fn(FnExpr { - ident: None, - function: Box::new(Function { - params: vec![], - decorators: vec![], - span: DUMMY_SP, - ctxt: SyntaxContext::empty(), - body: Some(BlockStmt { - span: DUMMY_SP, - ctxt: SyntaxContext::empty(), - stmts: vec![for_in], - }), - is_generator: false, - is_async: false, - type_params: None, - return_type: None, - }), - }))), - args: vec![], - type_args: None, - }); - - stmts.push(Stmt::Expr(ExprStmt { - span: DUMMY_SP, - expr: Box::new(iife), - })); - } else { - let rest_stmts = self.transform_pattern_to_stmts( - &rest.arg, - Some(Box::new(temp_ident.clone())), - declaration_kind, - ); - stmts.extend(rest_stmts); - } - } - } - } - } - } - Pat::Rest(RestPat { arg, .. }) => { - // Rest pattern in function params or arrays - let rest_stmts = self.transform_pattern_to_stmts(arg, init, declaration_kind); - stmts.extend(rest_stmts); - } - Pat::Assign(AssignPat { left, right, .. }) => { - // Assignment pattern with default: x = 5 - if let Some(init_expr) = init { - let value = Expr::Cond(CondExpr { - span: DUMMY_SP, - test: Box::new(Expr::Bin(BinExpr { - span: DUMMY_SP, - op: BinaryOp::EqEqEq, - left: init_expr.clone(), - right: Box::new(create_ident_expr("undefined")), - })), - cons: right.clone(), - alt: init_expr, - }); - let assign_stmts = - self.transform_pattern_to_stmts(left, Some(Box::new(value)), declaration_kind); - stmts.extend(assign_stmts); - } else { - let assign_stmts = - self.transform_pattern_to_stmts(left, Some(right.clone()), declaration_kind); - stmts.extend(assign_stmts); - } - } - _ => {} - } - - stmts - } - - /// Enter a new scope - fn enter_scope(&mut self, is_function_scope: bool) { - self.depth += 1; - self.scope_stack.push(ScopeFrame { - names: HashSet::new(), - is_function_scope, - }); - } - - fn declare_in_current_scope(&mut self, name: &str) { - if let Some(scope) = self.scope_stack.last_mut() { - scope.names.insert(name.to_string()); - } - } - - fn declare_in_nearest_function_scope(&mut self, name: &str) { - if let Some(scope) = self - .scope_stack - .iter_mut() - .rev() - .find(|scope| scope.is_function_scope) - { - scope.names.insert(name.to_string()); - } - } - - fn declare_pattern_in_current_scope(&mut self, pat: &Pat) { - for (sym, _) in extract_idents_from_pat(pat) { - self.declare_in_current_scope(sym.as_ref()); - } - } - - /// Replace top-level function declarations with wrapper calls (Babel parity). - /// - /// Transforms: - /// function foo() { ... } - /// into: - /// var foo = wrapper("foo", "function", function() { ... }, __moduleMeta__) - /// - /// Also prepends `let __moduleMeta__ = ` to the module body. - fn replace_function_declarations_with_wrapper(&self, module: &mut Module) { - // Build: let __moduleMeta__ = - let meta_init = if let Some(ref accessor) = self.current_module_accessor { - // The currentModuleAccessor is a complex JS expression (object literal). - // Use SWC's parser to parse it properly. - use swc_core::ecma::parser::{parse_file_as_expr, Syntax}; - use swc_core::common::{sync::Lrc, FileName, SourceMap as SwcSourceMap}; - let cm = Lrc::new(SwcSourceMap::default()); - let fm = cm.new_source_file(FileName::Anon.into(), accessor.clone()); - match parse_file_as_expr( - &fm, - Syntax::Es(Default::default()), - Default::default(), - None, - &mut vec![], - ) { - Ok(expr) => *expr, - Err(_) => parse_expr_or_ident(accessor), - } - } else { - create_ident_expr("undefined") - }; - let meta_decl = ModuleItem::Stmt(Stmt::Decl(create_var_decl( - VarDeclKind::Var, - "__moduleMeta__", - Some(meta_init), - ))); - - // Two-pass approach matching Babel's putFunctionDeclsInFront + - // insertCapturesForFunctionDeclarations: - // 1. Extract all top-level FunctionDeclarations, replace them with references (foo;) - // 2. Create let declarations with wrapper calls, hoisted to top - - let mut hoisted_lets: Vec = Vec::new(); - let mut new_body = Vec::with_capacity(module.body.len() + 1); - - for item in module.body.drain(..) { - match item { - ModuleItem::Stmt(Stmt::Decl(Decl::Fn(fn_decl))) => { - let fn_name = fn_decl.ident.sym.to_string(); - // Create anonymous function expression (id removed) - let anon_fn = Expr::Fn(FnExpr { - ident: None, - function: fn_decl.function, - }); - // Hoisted: var foo = wrapper("foo", "function", function(){...}, __moduleMeta__) - // Babel uses let, but var avoids TDZ issues with rollup circular dep analysis. - let wrapped = create_call_expr( - parse_expr_or_ident(self.declaration_wrapper.as_ref().unwrap()), - vec![ - to_expr_or_spread(create_string_expr(&fn_name)), - to_expr_or_spread(create_string_expr("function")), - to_expr_or_spread(anon_fn), - to_expr_or_spread(create_ident_expr("__moduleMeta__")), - ], - ); - // Use the ORIGINAL ident (preserves SyntaxContext) to avoid - // SWC hygiene renaming the var to Path1/foo1/etc. - let original_ident = fn_decl.ident.clone(); - let var_decl = ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(VarDecl { - span: DUMMY_SP, - kind: VarDeclKind::Var, - decls: vec![VarDeclarator { - span: DUMMY_SP, - name: Pat::Ident(BindingIdent { - id: original_ident.clone(), - type_ann: None, - }), - init: Some(Box::new(wrapped)), - definite: false, - }], - ..Default::default() - })))); - hoisted_lets.push(var_decl); - // Also add recorder capture: __rec.foo = foo - // (hoisted captures are skipped for these when declaration_wrapper is set) - let capture = ModuleItem::Stmt(Stmt::Expr(ExprStmt { - span: DUMMY_SP, - expr: Box::new(create_assign_expr( - expr_to_assign_target(self.create_captured_member(&fn_name)), - Expr::Ident(original_ident.clone()), - )), - })); - hoisted_lets.push(capture); - // Replace original position with reference: foo; - new_body.push(ModuleItem::Stmt(Stmt::Expr(ExprStmt { - span: DUMMY_SP, - expr: Box::new(Expr::Ident(original_ident)), - }))); - } - _ => { - new_body.push(item); - } - } - } - - // Insert hoisted declarations after recorder init: - // imports → recorder init → __moduleMeta__ → hoisted lets → rest of body - let mut final_body = Vec::with_capacity(new_body.len() + hoisted_lets.len() + 1); - let mut inserted = false; - for item in new_body.drain(..) { - final_body.push(item); - if !inserted { - if let ModuleItem::Stmt(Stmt::Decl(Decl::Var(ref var_decl))) = final_body.last().unwrap() { - if var_decl.decls.iter().any(|d| { - matches!(&d.name, Pat::Ident(BindingIdent { id, .. }) if id.sym.as_ref() == self.capture_obj) - }) { - final_body.push(meta_decl.clone()); - for let_decl in hoisted_lets.drain(..) { - final_body.push(let_decl); - } - inserted = true; - } - } - } - } - if !inserted { - // No recorder init found — insert at the beginning - let mut prefix = vec![meta_decl]; - prefix.extend(hoisted_lets); - prefix.extend(final_body); - module.body = prefix; - } else { - module.body = final_body; - } - } - - /// Build the recorder runtime expression: - /// (lively.FreezerRuntime || lively.frozenModules) - /// or, if `lively` is locally bound: - /// (globalThis.lively.FreezerRuntime || globalThis.lively.frozenModules) - fn recorder_runtime_expr(&self, has_local_lively_binding: bool) -> Expr { - let lively_expr = if has_local_lively_binding { - create_member_expr(create_ident_expr("globalThis"), "lively") - } else { - create_ident_expr("lively") - }; - - Expr::Bin(BinExpr { - span: DUMMY_SP, - op: BinaryOp::LogicalOr, - left: Box::new(create_member_expr(lively_expr.clone(), "FreezerRuntime")), - right: Box::new(create_member_expr(lively_expr, "frozenModules")), - }) - } - - /// Build: - /// const __varRecorder__ = (..runtime..).recorderFor("", __contextModule__); - fn create_recorder_init_decl(&self, has_local_lively_binding: bool) -> ModuleItem { - let recorder_call = create_call_expr( - create_member_expr(self.recorder_runtime_expr(has_local_lively_binding), "recorderFor"), - vec![ - to_expr_or_spread(create_string_expr(&self.module_id)), - to_expr_or_spread(create_ident_expr("__contextModule__")), - ], - ); - - ModuleItem::Stmt(Stmt::Decl(create_var_decl( - VarDeclKind::Const, - &self.capture_obj, - Some(recorder_call), - ))) - } - - /// Build import.meta replacement expression. - /// Uses the module_id directly as the URL since it's known at bundle time. - fn create_import_meta_expr(&self) -> Expr { - let url_expr = if !self.module_id.is_empty() { - create_string_expr(&self.module_id) - } else if let Some(accessor) = &self.current_module_accessor { - create_member_expr(parse_expr_or_ident(accessor), "id") - } else { - create_ident_expr(r#"eval("typeof _context !== 'undefined' ? _context : {}").id"#) - }; - create_object_lit(vec![create_prop("url", url_expr)]) - } - - /// Build `__varRecorder__.name = ...` for a function declaration capture. - fn create_function_capture_assignment(&self, fn_decl: &FnDecl) -> ModuleItem { - let fn_name = fn_decl.ident.sym.to_string(); - let member = self.create_captured_member(&fn_name); - // Babel's putFunctionDeclsInFront wraps just the identifier, not the function body. - // The function body replacement happens later in replace_function_declarations_with_wrapper. - let value = self.wrap_declaration( - &fn_name, - "function", - Expr::Ident(fn_decl.ident.clone()), - ); - - ModuleItem::Stmt(Stmt::Expr(ExprStmt { - span: DUMMY_SP, - expr: Box::new(create_assign_expr(expr_to_assign_target(member), value)), - })) - } - - /// Build `__varRecorder__.name = ...` for a class declaration capture. - /// Babel's bundler does NOT wrap class declarations with declarationWrapper — - /// only insertCapturesForFunctionDeclarations wraps function declarations. - fn create_class_capture_assignment(&self, class_decl: &ClassDecl) -> ModuleItem { - let class_name = class_decl.ident.sym.to_string(); - let member = self.create_captured_member(&class_name); - let value = Expr::Ident(class_decl.ident.clone()); - - ModuleItem::Stmt(Stmt::Expr(ExprStmt { - span: DUMMY_SP, - expr: Box::new(create_assign_expr(expr_to_assign_target(member), value)), - })) - } - - /// Collect function captures that need to run before any top-level statements. - /// When resurrection + declaration_wrapper is set, function declarations will - /// be fully replaced by `replace_function_declarations_with_wrapper`, so we - /// skip hoisted captures for plain function declarations (they would be - /// redundant and reference __moduleMeta__ before it's declared). - fn collect_hoisted_function_capture(&mut self, item: &ModuleItem) { - let will_replace_func_decls = self.resurrection && self.declaration_wrapper.is_some(); - match item { - ModuleItem::Stmt(Stmt::Decl(Decl::Fn(fn_decl))) => { - if will_replace_func_decls { - return; // Will be handled by replace_function_declarations_with_wrapper - } - if self.should_capture(&fn_decl.ident.to_id()) { - self.hoisted_function_captures - .push(self.create_function_capture_assignment(fn_decl)); - } - } - ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export_decl)) => { - if let Decl::Fn(fn_decl) = &export_decl.decl { - if self.should_capture(&fn_decl.ident.to_id()) { - self.hoisted_function_captures - .push(self.create_function_capture_assignment(fn_decl)); - } - } - } - _ => {} - } - } - - /// Insert hoisted function capture assignments after imports and recorder init. - fn insert_hoisted_function_captures(&self, module: &mut Module, inserted_recorder_init: bool) { - if self.hoisted_function_captures.is_empty() { - return; - } - - let mut insert_idx = module - .body - .iter() - .take_while(|item| matches!(item, ModuleItem::ModuleDecl(ModuleDecl::Import(_)))) - .count(); - - if inserted_recorder_init { - insert_idx += 1; - } - - for (offset, item) in self.hoisted_function_captures.iter().cloned().enumerate() { - module.body.insert(insert_idx + offset, item); - } - } - - /// Collect export metadata from the module for __module_exports__. - /// Follows the same format as the Babel bundler path. - fn collect_module_exports(&mut self, module: &Module) { - for item in &module.body { - match item { - ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) => { - if export.src.is_some() { - let raw_src = export.src.as_ref().unwrap().value.to_string(); - let resolved_src = self.resolved_imports.get(&raw_src) - .cloned() - .unwrap_or(raw_src); - if export.specifiers.is_empty() { - // export * from '...' (rare, usually ExportAll) - self.collected_exports.push(format!("__reexport__{}", resolved_src)); - } else { - // export { x, y as z } from '...' - // Per-specifier entries (Babel does individual entries, not blanket __reexport__) - for spec in &export.specifiers { - if let ExportSpecifier::Named(named) = spec { - let local_name = match &named.orig { - ModuleExportName::Ident(id) => id.sym.to_string(), - ModuleExportName::Str(s) => s.value.to_string(), - }; - let exported_name = match &named.exported { - Some(ModuleExportName::Ident(id)) => id.sym.to_string(), - Some(ModuleExportName::Str(s)) => s.value.to_string(), - None => local_name.clone(), - }; - if exported_name != local_name && exported_name != "default" { - self.collected_exports.push(format!("__rename__{}->{}",local_name, exported_name)); - continue; // Babel: continue after __rename__ - } - if exported_name == "default" && !local_name.is_empty() { - self.collected_exports.push(format!("__default__{}", local_name)); - } - self.collected_exports.push(exported_name); - } - } - } - } else { - // `export { x, y as z }` - for spec in &export.specifiers { - if let ExportSpecifier::Named(named) = spec { - let local_name = match &named.orig { - ModuleExportName::Ident(id) => id.sym.to_string(), - ModuleExportName::Str(s) => s.value.to_string(), - }; - let exported_name = match &named.exported { - Some(ModuleExportName::Ident(id)) => id.sym.to_string(), - Some(ModuleExportName::Str(s)) => s.value.to_string(), - None => local_name.clone(), - }; - if exported_name != local_name && exported_name != "default" { - self.collected_exports.push(format!("__rename__{}->{}",local_name, exported_name)); - continue; // Babel: continue after __rename__ - } - if exported_name == "default" && !local_name.is_empty() { - self.collected_exports.push(format!("__default__{}", local_name)); - } - self.collected_exports.push(exported_name); - } - } - } - } - ModuleItem::ModuleDecl(ModuleDecl::ExportAll(export_all)) => { - let raw_src = export_all.src.value.to_string(); - let resolved_src = self.resolved_imports.get(&raw_src) - .cloned() - .unwrap_or(raw_src); - self.collected_exports.push(format!("__reexport__{}", resolved_src)); - } - ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export_decl)) => { - match &export_decl.decl { - Decl::Fn(fn_decl) => { - self.collected_exports.push(fn_decl.ident.sym.to_string()); - } - Decl::Class(class_decl) => { - self.collected_exports.push(class_decl.ident.sym.to_string()); - } - Decl::Var(var_decl) => { - for decl in &var_decl.decls { - for (sym, _) in extract_idents_from_pat(&decl.name) { - self.collected_exports.push(sym.to_string()); - } - } - } - _ => {} - } - } - ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(default_decl)) => { - let local = match &default_decl.decl { - DefaultDecl::Fn(f) => f.ident.as_ref().map(|id| id.sym.to_string()), - DefaultDecl::Class(c) => c.ident.as_ref().map(|id| id.sym.to_string()), - _ => None, - }; - if let Some(name) = local { - self.collected_exports.push(format!("__default__{}", name)); - } - self.collected_exports.push("default".to_string()); - } - ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(default_expr)) => { - if let Expr::Ident(ident) = &*default_expr.expr { - self.collected_exports.push(format!("__default__{}", ident.sym)); - } - self.collected_exports.push("default".to_string()); - } - _ => {} - } - } - } - - /// Insert recorder initialization directly after static imports. - fn insert_recorder_init(&self, module: &mut Module, has_local_lively_binding: bool) { - let insert_idx = module - .body - .iter() - .take_while(|item| matches!(item, ModuleItem::ModuleDecl(ModuleDecl::Import(_)))) - .count(); - module - .body - .insert(insert_idx, self.create_recorder_init_decl(has_local_lively_binding)); - // For resurrection builds, insert __module_hash__ right after recorder init - if self.resurrection { - if let Some(hash) = self.module_hash { - let hash_stmt = ModuleItem::Stmt(Stmt::Expr(ExprStmt { - span: DUMMY_SP, - expr: Box::new(Expr::Assign(AssignExpr { - span: DUMMY_SP, - op: AssignOp::Assign, - left: AssignTarget::Simple(SimpleAssignTarget::Member(MemberExpr { - span: DUMMY_SP, - obj: Box::new(create_ident_expr(&self.capture_obj)), - prop: MemberProp::Ident(IdentName::new("__module_hash__".into(), DUMMY_SP)), - })), - right: Box::new(Expr::Lit(Lit::Num(Number { - span: DUMMY_SP, - value: hash as f64, - raw: None, - }))), - })), - })); - module.body.insert(insert_idx + 1, hash_stmt); - } - } - } - - /// Exit a scope - fn exit_scope(&mut self) { - self.depth -= 1; - self.scope_stack.pop(); - } - - - /// Transform a module item at module level, allowing expansion into multiple items - fn transform_module_item(&self, item: ModuleItem) -> Vec { - match item { - ModuleItem::Stmt(Stmt::Decl(Decl::Var(var_decl))) => { - let mut items = Vec::new(); - - for decl in &var_decl.decls { - let mut init = decl.init.clone(); - if self.module_id.ends_with("lively.morphic/config.js") { - if let Some(init_expr) = &init { - if matches!(**init_expr, Expr::Object(_)) { - if let Pat::Ident(BindingIdent { id, .. }) = &decl.name { - let recorder_member = self.create_captured_member(id.sym.as_ref()); - init = Some(Box::new(Expr::Bin(BinExpr { - span: DUMMY_SP, - op: BinaryOp::LogicalOr, - left: Box::new(recorder_member), - right: init_expr.clone(), - }))); - } - } - } - } - if self.should_capture_pattern(&decl.name) { - let declaration_kind = match var_decl.kind { - VarDeclKind::Var => "var", - VarDeclKind::Let => "let", - VarDeclKind::Const => "const", - }; - let stmts = - self.transform_pattern_to_stmts(&decl.name, init.clone(), declaration_kind); - for stmt in stmts { - items.push(ModuleItem::Stmt(stmt)); - } - } else { - let single_var = VarDecl { - span: var_decl.span, - ctxt: var_decl.ctxt, - kind: var_decl.kind, - declare: var_decl.declare, - decls: vec![VarDeclarator { - init, - ..decl.clone() - }], - }; - items.push(ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(single_var))))); - } - } - - items - } - ModuleItem::ModuleDecl(ModuleDecl::Import(import_decl)) => { - // Namespace import handling (import * as ns from 'dep') is done by - // NamespaceTransform (step 4) which runs before scope capture. - // The scope capture just needs to handle capture_imports if enabled. - if !self.capture_imports || import_decl.specifiers.is_empty() { - return vec![ModuleItem::ModuleDecl(ModuleDecl::Import(import_decl))]; - } - let mut items = vec![ModuleItem::ModuleDecl(ModuleDecl::Import(import_decl.clone()))]; - for spec in &import_decl.specifiers { - let local = match spec { - ImportSpecifier::Named(named) => named.local.clone(), - ImportSpecifier::Default(def) => def.local.clone(), - ImportSpecifier::Namespace(ns) => ns.local.clone(), - }; - let member = self.create_captured_member(local.sym.as_ref()); - let assign = create_assign_expr( - expr_to_assign_target(member), - Expr::Ident(local), - ); - items.push(ModuleItem::Stmt(Stmt::Expr(ExprStmt { - span: DUMMY_SP, - expr: Box::new(assign), - }))); - } - items - } - ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export_decl)) => { - // Split `export { x } from '...'` into import + export + recorder capture. - // Only for capture_imports mode (source-map builds). - // Resurrection builds use a different approach below. - if self.capture_imports && export_decl.src.is_some() { - let src = export_decl.src.as_ref().unwrap().value.to_string(); - let mut import_specs = Vec::new(); - let mut export_specs = Vec::new(); - let mut assigns = Vec::new(); - - for spec in &export_decl.specifiers { - if let ExportSpecifier::Named(named) = spec { - let exported_name = match &named.exported { - Some(ModuleExportName::Ident(id)) => id.sym.to_string(), - Some(ModuleExportName::Str(s)) => s.value.to_string(), - None => match &named.orig { - ModuleExportName::Ident(id) => id.sym.to_string(), - ModuleExportName::Str(s) => s.value.to_string(), - }, - }; - - let import_name = match &named.orig { - ModuleExportName::Ident(id) => id.sym.to_string(), - ModuleExportName::Str(s) => s.value.to_string(), - }; - - let local_name = if exported_name == "default" { - format!("__default_export_{}__", import_name) - } else { - exported_name.clone() - }; - - import_specs.push(ImportSpecifier::Named(ImportNamedSpecifier { - span: DUMMY_SP, - local: Ident::new(local_name.as_str().into(), DUMMY_SP, SyntaxContext::empty()), - imported: Some(ModuleExportName::Ident(Ident::new(import_name.as_str().into(), DUMMY_SP, SyntaxContext::empty()))), - is_type_only: false, - })); - - export_specs.push(ExportSpecifier::Named(ExportNamedSpecifier { - span: DUMMY_SP, - orig: ModuleExportName::Ident(Ident::new(local_name.as_str().into(), DUMMY_SP, SyntaxContext::empty())), - exported: if exported_name == local_name { None } else { - Some(ModuleExportName::Ident(Ident::new(exported_name.as_str().into(), DUMMY_SP, SyntaxContext::empty()))) - }, - is_type_only: false, - })); - - let member = self.create_captured_member(&exported_name); - let assign = create_assign_expr( - expr_to_assign_target(member), - Expr::Ident(Ident::new(local_name.as_str().into(), DUMMY_SP, SyntaxContext::empty())), - ); - assigns.push(ModuleItem::Stmt(Stmt::Expr(ExprStmt { - span: DUMMY_SP, - expr: Box::new(assign), - }))); - } - } - - let import_decl = ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { - span: DUMMY_SP, - specifiers: import_specs, - src: Box::new(Str { span: DUMMY_SP, value: src.clone().into(), raw: None }), - type_only: false, - with: None, - phase: Default::default(), - })); - let export_decl = ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(NamedExport { - span: DUMMY_SP, - specifiers: export_specs, - src: None, - type_only: false, - with: None, - })); - - let mut items = vec![import_decl, export_decl]; - items.extend(assigns); - return items; - } - vec![ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export_decl))] - } - ModuleItem::ModuleDecl(ModuleDecl::ExportAll(export_all)) => { - // For capture_imports builds, add namespace import + Object.assign. - // Resurrection builds skip this — NamespaceTransform (step 4) handles it. - if self.capture_imports && !self.resurrection { - let src = export_all.src.value.to_string(); - let resolved_id = self.resolved_imports.get(&src) - .cloned() - .unwrap_or_else(|| src.clone()); - let tmp_name = format!("__captured_export_all_{}__", rand_id()); - let import_decl = ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { - span: DUMMY_SP, - specifiers: vec![ImportSpecifier::Namespace(ImportStarAsSpecifier { - span: DUMMY_SP, - local: Ident::new(tmp_name.as_str().into(), DUMMY_SP, SyntaxContext::empty()), - })], - src: Box::new(Str { span: DUMMY_SP, value: src.into(), raw: None }), - type_only: false, - with: None, - phase: Default::default(), - })); - // Object.assign(recorderFor(resolvedDep), tmp) - let recorder_for = create_call_expr( - create_member_expr( - Expr::Paren(ParenExpr { - span: DUMMY_SP, - expr: Box::new(Expr::Bin(BinExpr { - span: DUMMY_SP, - op: BinaryOp::LogicalOr, - left: Box::new(create_member_expr(create_ident_expr("lively"), "FreezerRuntime")), - right: Box::new(create_member_expr(create_ident_expr("lively"), "frozenModules")), - })), - }), - "recorderFor", - ), - vec![to_expr_or_spread(Expr::Lit(Lit::Str(Str { - span: DUMMY_SP, - value: resolved_id.as_str().into(), - raw: None, - })))], - ); - let assign = ModuleItem::Stmt(Stmt::Expr(ExprStmt { - span: DUMMY_SP, - expr: Box::new(create_call_expr( - create_member_expr(create_ident_expr("Object"), "assign"), - vec![ - to_expr_or_spread(recorder_for), - to_expr_or_spread(create_ident_expr(&tmp_name)), - ], - )), - })); - return vec![ - ModuleItem::ModuleDecl(ModuleDecl::ExportAll(export_all)), - import_decl, - assign, - ]; - } - vec![ModuleItem::ModuleDecl(ModuleDecl::ExportAll(export_all))] - } - ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export_decl)) => { - let mut items = vec![ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export_decl.clone()))]; - - match &export_decl.decl { - Decl::Class(class_decl) => { - if self.should_capture(&class_decl.ident.to_id()) { - items.push(self.create_class_capture_assignment(class_decl)); - } - } - Decl::Var(var_decl) => { - for decl in &var_decl.decls { - let ids = extract_idents_from_pat(&decl.name); - for (sym, ctxt) in ids { - let id = (sym.clone(), ctxt); - if !self.should_capture(&id) { - continue; - } - let name = sym.to_string(); - let ident = Ident::new(sym, DUMMY_SP, ctxt); - let member = self.create_captured_member(&name); - // Babel's bundler does NOT wrap exported var captures with - // declarationWrapper — only function declarations get wrapped. - let value = Expr::Ident(ident); - let assign = create_assign_expr( - expr_to_assign_target(member), - value, - ); - items.push(ModuleItem::Stmt(Stmt::Expr(ExprStmt { - span: DUMMY_SP, - expr: Box::new(assign), - }))); - } - } - } - _ => {} - } - - items - } - ModuleItem::Stmt(Stmt::Decl(Decl::Fn(fn_decl))) => { - vec![ModuleItem::Stmt(Stmt::Decl(Decl::Fn(fn_decl)))] - } - ModuleItem::Stmt(Stmt::Decl(Decl::Class(class_decl))) => { - if self.should_capture(&class_decl.ident.to_id()) { - vec![ - ModuleItem::Stmt(Stmt::Decl(Decl::Class(class_decl.clone()))), - self.create_class_capture_assignment(&class_decl), - ] - } else { - vec![ModuleItem::Stmt(Stmt::Decl(Decl::Class(class_decl)))] - } - } - ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(export_default)) => { - // export default function x() {} → function x() {} __rec.x = x; export default x; - // export default class Foo {} → class Foo {} __rec.Foo = Foo; export default Foo; - let ident = match &export_default.decl { - DefaultDecl::Fn(f) => f.ident.as_ref().cloned(), - DefaultDecl::Class(c) => c.ident.as_ref().cloned(), - _ => None, - }; - if let Some(id) = ident { - let name = id.sym.to_string(); - let decl_item = match export_default.decl.clone() { - DefaultDecl::Fn(f) => { - ModuleItem::Stmt(Stmt::Decl(Decl::Fn(FnDecl { - ident: f.ident.clone().unwrap(), - declare: false, - function: f.function, - }))) - } - DefaultDecl::Class(c) => { - ModuleItem::Stmt(Stmt::Decl(Decl::Class(ClassDecl { - ident: c.ident.clone().unwrap(), - declare: false, - class: c.class, - }))) - } - _ => unreachable!(), - }; - // __rec.name = name - let capture_name = ModuleItem::Stmt(Stmt::Expr(ExprStmt { - span: DUMMY_SP, - expr: Box::new(create_assign_expr( - expr_to_assign_target(self.create_captured_member(&name)), - Expr::Ident(Ident::new(name.as_str().into(), DUMMY_SP, SyntaxContext::empty())), - )), - })); - // __rec.default = name (Babel always captures default) - let capture_default = ModuleItem::Stmt(Stmt::Expr(ExprStmt { - span: DUMMY_SP, - expr: Box::new(create_assign_expr( - expr_to_assign_target(self.create_captured_member("default")), - Expr::Ident(Ident::new(name.as_str().into(), DUMMY_SP, SyntaxContext::empty())), - )), - })); - let export_stmt = ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(ExportDefaultExpr { - span: DUMMY_SP, - expr: Box::new(Expr::Ident(Ident::new(name.as_str().into(), DUMMY_SP, SyntaxContext::empty()))), - })); - vec![decl_item, capture_name, capture_default, export_stmt] - } else { - // Anonymous default export: export default function() {} or export default class {} - // Babel: __rec.default = ; export default __rec.default; - let anon_expr = match export_default.decl { - DefaultDecl::Fn(f) => Expr::Fn(FnExpr { - ident: None, - function: f.function, - }), - DefaultDecl::Class(c) => Expr::Class(ClassExpr { - ident: None, - class: c.class, - }), - _ => { - return vec![ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(export_default))]; - } - }; - let capture_default = ModuleItem::Stmt(Stmt::Expr(ExprStmt { - span: DUMMY_SP, - expr: Box::new(create_assign_expr( - expr_to_assign_target(self.create_captured_member("default")), - anon_expr, - )), - })); - let export_stmt = ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(ExportDefaultExpr { - span: DUMMY_SP, - expr: Box::new(self.create_captured_member("default")), - })); - vec![capture_default, export_stmt] - } - } - // ExportDefaultExpr is kept as-is in transform_module_item. - // The __rec.default capture is added by insert_declarations_for_exports - // which runs after visit_mut transforms all expressions. - other => vec![other], - } - } - - fn collect_declared_top_level_names(&self, module: &Module) -> HashSet { - let mut names = HashSet::new(); - for item in &module.body { - match item { - ModuleItem::Stmt(Stmt::Decl(Decl::Var(var_decl))) => { - for decl in &var_decl.decls { - for (sym, _) in extract_idents_from_pat(&decl.name) { - names.insert(sym.to_string()); - } - } - } - ModuleItem::Stmt(Stmt::Decl(Decl::Fn(fn_decl))) => { - names.insert(fn_decl.ident.sym.to_string()); - } - ModuleItem::Stmt(Stmt::Decl(Decl::Class(class_decl))) => { - names.insert(class_decl.ident.sym.to_string()); - } - ModuleItem::ModuleDecl(ModuleDecl::Import(import_decl)) => { - for spec in &import_decl.specifiers { - match spec { - ImportSpecifier::Named(named) => { - names.insert(named.local.sym.to_string()); - } - ImportSpecifier::Default(def) => { - names.insert(def.local.sym.to_string()); - } - ImportSpecifier::Namespace(ns) => { - names.insert(ns.local.sym.to_string()); - } - } - } - } - ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export_decl)) => match &export_decl.decl { - Decl::Var(var_decl) => { - for decl in &var_decl.decls { - for (sym, _) in extract_idents_from_pat(&decl.name) { - names.insert(sym.to_string()); - } - } - } - Decl::Fn(fn_decl) => { - names.insert(fn_decl.ident.sym.to_string()); - } - Decl::Class(class_decl) => { - names.insert(class_decl.ident.sym.to_string()); - } - _ => {} - }, - _ => {} - } - } - names - } - - /// JS parity: insert binding declarations for export specifiers whose locals - /// were rewritten to recorder assignments (e.g. `export { oe as default }`). - fn insert_declarations_for_exports(&self, module: &mut Module) { - let mut declared = self.collect_declared_top_level_names(module); - let mut new_body = Vec::with_capacity(module.body.len()); - - for item in module.body.drain(..) { - match &item { - ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export_decl)) - if export_decl.src.is_none() && !export_decl.specifiers.is_empty() => - { - for spec in &export_decl.specifiers { - let ExportSpecifier::Named(named) = spec else { - continue; - }; - let ModuleExportName::Ident(local_ident) = &named.orig else { - continue; - }; - let local_name = local_ident.sym.to_string(); - if declared.contains(&local_name) { - continue; - } - // Use the SAME SyntaxContext as the export specifier so - // SWC's codegen doesn't append hygiene suffixes. - let decl = ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(VarDecl { - span: DUMMY_SP, - kind: VarDeclKind::Var, - decls: vec![VarDeclarator { - span: DUMMY_SP, - name: Pat::Ident(BindingIdent { - id: local_ident.clone(), - type_ann: None, - }), - init: Some(Box::new(self.create_captured_member(&local_name))), - definite: false, - }], - ..Default::default() - })))); - new_body.push(decl); - declared.insert(local_name); - } - } - ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(ref export_default)) => { - if let Expr::Ident(local_ident) = &*export_default.expr { - let local_name = local_ident.sym.to_string(); - if !declared.contains(&local_name) { - let decl = ModuleItem::Stmt(Stmt::Decl(create_var_decl( - VarDeclKind::Var, - &local_name, - Some(self.create_captured_member(&local_name)), - ))); - new_body.push(decl); - declared.insert(local_name); - } - } - // Capture __rec.default = (Babel always does this) - let default_capture = ModuleItem::Stmt(Stmt::Expr(ExprStmt { - span: DUMMY_SP, - expr: Box::new(create_assign_expr( - expr_to_assign_target(self.create_captured_member("default")), - *export_default.expr.clone(), - )), - })); - new_body.push(default_capture); - } - _ => {} - } - - new_body.push(item); - } - - module.body = new_body; - } -} - -impl VisitMut for ScopeCapturingTransform { - fn visit_mut_module(&mut self, module: &mut Module) { - // First pass: analyze scope to determine capturable variables - let mut analyzer = ScopeAnalyzer::new(); - use swc_core::ecma::visit::VisitWith; - module.visit_with(&mut analyzer); - - self.imported_vars.clear(); - for item in &module.body { - if let ModuleItem::ModuleDecl(ModuleDecl::Import(import_decl)) = item { - for spec in &import_decl.specifiers { - let id = match spec { - ImportSpecifier::Named(named) => named.local.to_id(), - ImportSpecifier::Default(def) => def.local.to_id(), - ImportSpecifier::Namespace(ns) => ns.local.to_id(), - }; - self.imported_vars.insert(id); - } - } - } - - let has_local_lively_binding = analyzer - .top_level_vars - .iter() - .any(|id| id.0.as_ref() == "lively"); - let has_capture_obj_binding = analyzer - .top_level_vars - .iter() - .any(|id| id.0.as_ref() == self.capture_obj.as_str()); - - self.capturable_vars = analyzer - .top_level_vars - .into_iter() - .filter(|id| !self.excluded.contains(id.0.as_ref())) - .collect(); - - // Collect export metadata for resurrection builds BEFORE the transform - // (since the transform removes export statements from the AST). - // imported_vars is now populated so we can skip re-exported imports. - if self.resurrection { - self.collected_exports.clear(); - self.collect_module_exports(module); - } - - // Second pass: transform the module - self.depth = 0; - self.hoisted_function_captures.clear(); - for item in &module.body { - self.collect_hoisted_function_capture(item); - } - - let mut new_body = Vec::with_capacity(module.body.len()); - for item in module.body.drain(..) { - let mut transformed_items = self.transform_module_item(item); - for transformed in &mut transformed_items { - transformed.visit_mut_children_with(self); - } - new_body.extend(transformed_items); - } - module.body = new_body; - self.insert_declarations_for_exports(module); - - // Ensure the capture object exists for generated __varRecorder__ references. - let mut inserted_recorder_init = false; - if !has_capture_obj_binding { - self.insert_recorder_init(module, has_local_lively_binding); - inserted_recorder_init = true; - } - - self.insert_hoisted_function_captures(module, inserted_recorder_init); - - // For resurrection builds with a declaration wrapper, replace top-level - // function declarations with let bindings wrapped through the wrapper. - // Babel: function foo() {...} => var foo = wrapper("foo", "function", function(){...}, __moduleMeta__) - // This runs LAST (after all scope capture processing) matching the Babel pipeline. - if self.resurrection && self.declaration_wrapper.is_some() { - self.replace_function_declarations_with_wrapper(module); - } - - // For resurrection builds, insert __module_exports__ right after the recorder init - // (matching Babel's placement at the top of the module). - if self.resurrection { - let arr_elems: Vec> = self.collected_exports.iter() - .map(|e| Some(ExprOrSpread { - spread: None, - expr: Box::new(create_string_expr(e)), - })) - .collect(); - let member_expr = Expr::Member(MemberExpr { - span: DUMMY_SP, - obj: Box::new(create_ident_expr(&self.capture_obj)), - prop: MemberProp::Ident(IdentName::new("__module_exports__".into(), DUMMY_SP)), - }); - let module_exports_stmt = ModuleItem::Stmt(Stmt::Expr(ExprStmt { - span: DUMMY_SP, - expr: Box::new(Expr::Assign(AssignExpr { - span: DUMMY_SP, - op: AssignOp::Assign, - left: AssignTarget::Simple(SimpleAssignTarget::Member(MemberExpr { - span: DUMMY_SP, - obj: Box::new(create_ident_expr(&self.capture_obj)), - prop: MemberProp::Ident(IdentName::new("__module_exports__".into(), DUMMY_SP)), - })), - right: Box::new(Expr::Bin(BinExpr { - span: DUMMY_SP, - op: BinaryOp::LogicalOr, - left: Box::new(member_expr), - right: Box::new(Expr::Array(ArrayLit { - span: DUMMY_SP, - elems: arr_elems, - })), - })), - })), - })); - // Insert after the recorder init (first non-import statement) - let insert_pos = module.body.iter().position(|item| { - !matches!(item, ModuleItem::ModuleDecl(ModuleDecl::Import(_))) - }).unwrap_or(0) + 1; // +1 to go after the recorder init - let insert_pos = insert_pos.min(module.body.len()); - module.body.insert(insert_pos, module_exports_stmt); - } - } - - fn visit_mut_expr(&mut self, expr: &mut Expr) { - // Transform identifier references to captured members - match expr { - Expr::Assign(assign) => { - // Keep generated import-capture assignments intact: - // __varRecorder__.x = x - if let ( - AssignTarget::Simple(SimpleAssignTarget::Member(MemberExpr { - obj, - prop: MemberProp::Ident(prop_ident), - .. - })), - Expr::Ident(right_ident), - ) = (&assign.left, &*assign.right) - { - if let Expr::Ident(obj_ident) = &**obj { - if obj_ident.sym.as_ref() == self.capture_obj && prop_ident.sym == right_ident.sym { - return; - } - } - } - } - Expr::Ident(ident) => { - if self.should_capture(&ident.to_id()) { - *expr = self.create_captured_member(ident.sym.as_ref()); - return; - } - } - Expr::MetaProp(MetaPropExpr { - kind: MetaPropKind::ImportMeta, - .. - }) => { - *expr = self.create_import_meta_expr(); - return; - } - _ => {} - } - - expr.visit_mut_children_with(self); - } - - fn visit_mut_assign_expr(&mut self, assign: &mut AssignExpr) { - if let AssignTarget::Simple(SimpleAssignTarget::Ident(binding_ident)) = &assign.left { - let id = binding_ident.id.to_id(); - if self.should_capture(&id) { - let name = binding_ident.id.sym.to_string(); - let member = self.create_captured_member(&name); - assign.left = expr_to_assign_target(member); - // Babel's bundler does NOT wrap assignments with declarationWrapper. - // declarationWrapper is only used in insertCapturesForFunctionDeclarations - // which only handles function declarations. - } - } - - assign.visit_mut_children_with(self); - } - - fn visit_mut_update_expr(&mut self, update: &mut UpdateExpr) { - if let Expr::Ident(ident) = &*update.arg { - if self.should_capture(&ident.to_id()) { - update.arg = Box::new(self.create_captured_member(ident.sym.as_ref())); - } - } - - update.visit_mut_children_with(self); - } - - fn visit_mut_prop(&mut self, prop: &mut Prop) { - if let Prop::Shorthand(ident) = prop { - if self.should_capture(&ident.to_id()) { - let key = PropName::Ident(ident.clone().into()); - let value = Box::new(self.create_captured_member(ident.sym.as_ref())); - *prop = Prop::KeyValue(KeyValueProp { key, value }); - return; - } - } - - prop.visit_mut_children_with(self); - } - - // Scope tracking for nested functions/blocks - fn visit_mut_function(&mut self, func: &mut Function) { - self.enter_scope(true); - for param in &func.params { - self.declare_pattern_in_current_scope(¶m.pat); - } - func.visit_mut_children_with(self); - self.exit_scope(); - } - - fn visit_mut_arrow_expr(&mut self, arrow: &mut ArrowExpr) { - self.enter_scope(true); - for param in &arrow.params { - self.declare_pattern_in_current_scope(param); - } - arrow.visit_mut_children_with(self); - self.exit_scope(); - } - - fn visit_mut_block_stmt(&mut self, block: &mut BlockStmt) { - self.enter_scope(false); - block.visit_mut_children_with(self); - self.exit_scope(); - } - - fn visit_mut_var_decl(&mut self, var_decl: &mut VarDecl) { - let mut current_names = HashSet::new(); - for decl in &var_decl.decls { - for (sym, _) in extract_idents_from_pat(&decl.name) { - current_names.insert(sym.to_string()); - } - } - self.current_var_decl_stack.push(current_names); - - if self.depth > 0 { - for decl in &var_decl.decls { - if matches!(var_decl.kind, VarDeclKind::Var) { - for (sym, _) in extract_idents_from_pat(&decl.name) { - self.declare_in_nearest_function_scope(sym.as_ref()); - } - } else { - self.declare_pattern_in_current_scope(&decl.name); - } - } - } - var_decl.visit_mut_children_with(self); - self.current_var_decl_stack.pop(); - } - - fn visit_mut_fn_expr(&mut self, fn_expr: &mut FnExpr) { - self.enter_scope(true); - if let Some(ident) = &fn_expr.ident { - self.declare_in_current_scope(ident.sym.as_ref()); - } - fn_expr.function.visit_mut_children_with(self); - self.exit_scope(); - } - - fn visit_mut_fn_decl(&mut self, fn_decl: &mut FnDecl) { - if self.depth > 0 { - self.declare_in_current_scope(fn_decl.ident.sym.as_ref()); - } - fn_decl.visit_mut_children_with(self); - } - - fn visit_mut_class_decl(&mut self, class_decl: &mut ClassDecl) { - if self.depth > 0 { - self.declare_in_current_scope(class_decl.ident.sym.as_ref()); - } - class_decl.visit_mut_children_with(self); - } - - fn visit_mut_catch_clause(&mut self, catch: &mut CatchClause) { - self.enter_scope(false); - if let Some(param) = &catch.param { - self.declare_pattern_in_current_scope(param); - } - catch.visit_mut_children_with(self); - self.exit_scope(); - } -} - -// Helper to generate random IDs for temporary variables -fn rand_id() -> String { - use std::sync::atomic::{AtomicUsize, Ordering}; - static COUNTER: AtomicUsize = AtomicUsize::new(0); - format!("{}", COUNTER.fetch_add(1, Ordering::SeqCst)) -} - -#[cfg(test)] -mod tests { - use super::*; - use swc_core::common::{sync::Lrc, FileName, SourceMap}; - use swc_core::ecma::codegen::{text_writer::JsWriter, Emitter, Config}; - use swc_core::ecma::parser::{parse_file_as_module, Syntax}; - - fn transform_code(code: &str) -> String { - let cm = Lrc::new(SourceMap::default()); - let fm = cm.new_source_file(FileName::Anon.into(), code.to_string()); - - let mut module = parse_file_as_module( - &fm, - Syntax::Es(Default::default()), - Default::default(), - None, - &mut vec![], - ) - .unwrap(); - - let mut transform = ScopeCapturingTransform::new( - "__varRecorder__".to_string(), - None, - vec!["console".to_string()], - true, - false, - "test.js".to_string(), - None, - None, - HashMap::new(), - ); - - module.visit_mut_with(&mut transform); - - let mut buf = vec![]; - { - let mut emitter = Emitter { - cfg: Config::default(), - cm: cm.clone(), - comments: None, - wr: JsWriter::new(cm, "\n", &mut buf, None), - }; - - emitter.emit_module(&module).unwrap(); - } - - String::from_utf8(buf).unwrap() - } - - // --- Basic variable capturing (matching Babel capturing-test.js) --- - - #[test] - fn test_simple_var() { - let output = transform_code("var x = 1;"); - assert!(output.contains("__varRecorder__.x = 1;"), "should capture var with value: {}", output); - } - - #[test] - fn test_var_reference() { - let output = transform_code("var x = 1; x + 2;"); - assert!(output.contains("__varRecorder__.x + 2"), "should capture var reference: {}", output); - } - - #[test] - fn test_var_and_func_decls() { - // Babel: 'var z = 3, y = 4; function foo() { var x = 5; }' - // → function hoisted, vars captured, inner var NOT captured - let output = transform_code("var z = 3, y = 4; function foo() { var x = 5; }"); - assert!(output.contains("__varRecorder__.z = 3"), "capture z: {}", output); - assert!(output.contains("__varRecorder__.y = 4"), "capture y: {}", output); - assert!(output.contains("__varRecorder__.foo = foo"), "capture foo: {}", output); - assert!(!output.contains("__varRecorder__.x"), "inner var should NOT be captured: {}", output); - } - - #[test] - fn test_function_declaration() { - let output = transform_code("function foo() { return 1; }"); - assert!(output.contains("__varRecorder__.foo = foo;"), "should capture function with assignment form: {}", output); - assert!(output.contains("function foo()"), "should keep function declaration: {}", output); - } - - #[test] - fn test_excluded_var() { - // Babel: excluded vars should not be captured - let output = transform_code("console.log('hello');"); - assert!(!output.contains("__varRecorder__.console"), "excluded var should not be captured: {}", output); - } - - #[test] - fn test_same_var_decl_initializer_uses_local_binding() { - let output = transform_code("if (true) var xe = 1, Ue = xe + 1;"); - assert!(output.contains("xe + 1"), "same-decl reference: {}", output); - assert!(!output.contains("__varRecorder__.xe + 1"), "should use local binding: {}", output); - } - - // --- Nested function scoping --- - - #[test] - fn test_nested_function_uses_captured_ref() { - // Babel: function params shadow, but outer refs are captured - let output = transform_code("var z = 3; function foo(y) { var x = 5 + y + z; }"); - assert!(output.contains("__varRecorder__.z = 3;"), "outer var captured with value: {}", output); - assert!(output.contains("__varRecorder__.foo = foo;"), "function captured with assignment: {}", output); - // Inside foo, z references the captured var - assert!(output.contains("__varRecorder__.z;"), "z ref inside foo uses recorder: {}", output); - assert!(!output.contains("__varRecorder__.y"), "param should NOT be captured: {}", output); - assert!(!output.contains("__varRecorder__.x"), "inner var should NOT be captured: {}", output); - } - - // --- Let + Const --- - - #[test] - fn test_let_const_captured() { - let output = transform_code("let x = 1; const y = 2;"); - assert!(output.contains("__varRecorder__.x = 1;"), "let captured with value: {}", output); - assert!(output.contains("__varRecorder__.y = 2;"), "const captured with value: {}", output); - } - - // --- Export --- - - #[test] - fn test_export_const() { - // Babel: 'export const x = 23;' → 'export const x = 23;\n_rec.x = x;' - let output = transform_code("export const x = 23;"); - assert!(output.contains("export const x = 23;"), "keeps export: {}", output); - assert!(output.contains("__varRecorder__.x = x"), "captures export: {}", output); - } - - #[test] - fn test_export_var() { - // Babel: 'var x = 23; export { x };' → '_rec.x = 23; var x = _rec.x; export { x };' - let output = transform_code("var x = 23; export { x };"); - assert!(output.contains("__varRecorder__.x = 23;"), "captures var with value: {}", output); - assert!(output.contains("var x = __varRecorder__.x;"), "re-declares var from recorder: {}", output); - assert!(output.contains("export { x }"), "keeps named export: {}", output); - } - - #[test] - fn test_export_aliased_var() { - // Babel: 'var x = 23; export { x as y };' → '_rec.x = 23; var x = _rec.x; export { x as y };' - let output = transform_code("var x = 23; export { x as y };"); - assert!(output.contains("__varRecorder__.x = 23;"), "captures local var with value: {}", output); - assert!(output.contains("var x = __varRecorder__.x;"), "re-declares var from recorder: {}", output); - assert!(output.contains("export { x as y }"), "keeps aliased export: {}", output); - } - - #[test] - fn test_export_function_decl() { - // Babel: 'export function x() {}' → 'function x() {}\n_rec.x = x;\nexport { x };' - let output = transform_code("export function x() {}"); - assert!(output.contains("__varRecorder__.x = x;"), "captures function with assignment: {}", output); - assert!(output.contains("export function x()"), "keeps export function declaration: {}", output); - } - - #[test] - fn test_export_default_named_function() { - // Babel: 'export default function x() {}' → 'function x() {}\n_rec.x = x;\nexport default x;' - let output = transform_code("export default function x() {}"); - assert!(output.contains("__varRecorder__.x = x;"), "captures default fn with assignment: {}", output); - assert!(output.contains("export default x;"), "keeps export default statement: {}", output); - assert!(output.contains("function x()"), "keeps function declaration: {}", output); - } - - #[test] - fn test_re_export_named_from_source() { - // Babel: 'export { name1, name2 } from "foo";' → keeps as-is (no capture in scope phase) - let output = transform_code("export { name1, name2 } from \"foo\";"); - assert!(output.contains("export { name1, name2 }"), "keeps re-export with names: {}", output); - // SWC rewrites re-exports as import+export, capturing the bindings - assert!(output.contains("__varRecorder__.name1 = name1"), "captures re-exported name1: {}", output); - assert!(output.contains("__varRecorder__.name2 = name2"), "captures re-exported name2: {}", output); - } - - #[test] - fn test_re_export_aliased_from_source() { - // Babel: 'export { name1 as foo1 } from "foo";' → keeps as-is - let output = transform_code("export { name1 as foo1, name2 as bar2 } from \"foo\";"); - assert!(output.contains("export {"), "keeps re-export: {}", output); - assert!(output.contains("from \"foo\"") || output.contains("from 'foo'"), "keeps source module: {}", output); - } - - #[test] - fn test_export_default_expression() { - // Babel: 'export default foo(1, 2, 3);' → 'export default _rec.foo(1, 2, 3);' - // SWC does not capture undeclared globals, so we declare foo first - let output = transform_code("var foo = function() {}; export default foo(1, 2, 3);"); - assert!(output.contains("__varRecorder__.foo(1, 2, 3)"), "captures ref in default expr: {}", output); - assert!(output.contains("export default __varRecorder__.foo(1, 2, 3)"), "export default uses captured ref: {}", output); - assert!(output.contains("__varRecorder__.default = __varRecorder__.foo(1, 2, 3)"), "captures __rec.default: {}", output); - } - - #[test] - fn test_re_export_namespace_import() { - // Babel: 'import * as completions from "./lib/completions.js"; export { completions }' - // → keeps import, adds _rec.completions = completions, keeps export - let output = transform_code("import * as completions from \"./lib/completions.js\";\nexport { completions }"); - assert!(output.contains("import * as completions from"), "keeps namespace import: {}", output); - assert!(output.contains("__varRecorder__.completions = completions;"), "captures namespace with assignment form: {}", output); - assert!(output.contains("export { completions }"), "keeps named export: {}", output); - } - - // --- Import --- - - #[test] - fn test_import_capture() { - // With captureImports=true, imports get _rec.x = x - let output = transform_code("import { x } from 'foo';"); - assert!(output.contains("import { x } from 'foo'"), "keeps import declaration: {}", output); - assert!(output.contains("__varRecorder__.x = x;"), "captures import with assignment form: {}", output); - } - - #[test] - fn test_import_default_capture() { - let output = transform_code("import x from 'foo';"); - assert!(output.contains("import x from 'foo'"), "keeps default import declaration: {}", output); - assert!(output.contains("__varRecorder__.x = x;"), "captures default import with assignment form: {}", output); - } - - #[test] - fn test_import_namespace_capture() { - let output = transform_code("import * as ns from 'foo';"); - assert!(output.contains("import * as ns from 'foo'"), "keeps namespace import declaration: {}", output); - assert!(output.contains("__varRecorder__.ns = ns;"), "captures namespace import with assignment form: {}", output); - } - - // --- Patterns --- - - #[test] - fn test_destructuring_var() { - let output = transform_code("var { a, b: c } = obj;"); - // SWC desugars to temp variable + property access - assert!(output.contains("_tmp_"), "uses temp variable for destructuring: {}", output); - assert!(output.contains("__varRecorder__.a = _tmp_"), "captures a from temp: {}", output); - assert!(output.contains("__varRecorder__.c = _tmp_"), "captures c (alias of b) from temp: {}", output); - assert!(!output.contains("__varRecorder__.b"), "b is a key, not captured: {}", output); - } - - #[test] - fn test_array_destructuring() { - let output = transform_code("var [a, b] = arr;"); - // SWC desugars to temp variable + index access - assert!(output.contains("_tmp_"), "uses temp variable for array destructuring: {}", output); - assert!(output.contains("__varRecorder__.a = _tmp_"), "captures a from temp: {}", output); - assert!(output.contains("__varRecorder__.b = _tmp_"), "captures b from temp: {}", output); - assert!(output.contains("[0]"), "accesses index 0: {}", output); - assert!(output.contains("[1]"), "accesses index 1: {}", output); - } - - // --- For loop let/const scoping --- - - #[test] - fn test_for_loop_let_not_captured() { - let output = transform_code("for (let i = 0; i < 10; i++) {}"); - assert!(!output.contains("__varRecorder__.i"), "for-let should NOT be captured: {}", output); - } - - #[test] - fn test_for_in_let_not_captured() { - let output = transform_code("for (let k in obj) {}"); - assert!(!output.contains("__varRecorder__.k"), "for-in let should NOT be captured: {}", output); - } - - #[test] - fn test_for_of_let_not_captured() { - let output = transform_code("for (let v of arr) {}"); - assert!(!output.contains("__varRecorder__.v"), "for-of let should NOT be captured: {}", output); - } - - // =================================================================== - // Tests translated from lively.source-transform/tests/capturing-test.js - // =================================================================== - - // --- Basic var/func capturing (top-level describe block) --- - - #[test] - fn babel_top_level_var_decls_for_capturing() { - // Babel: 'var y, z = foo + bar; baz.foo(z, 3)' - // SWC only captures declared top-level vars, not undeclared globals like foo/bar/baz. - // We declare them to match Babel behavior more closely. - let output = transform_code("var foo, bar, baz; var y, z = foo + bar; baz.foo(z, 3)"); - assert!(output.contains("__varRecorder__.y"), "capture y: {}", output); - assert!(output.contains("__varRecorder__.z = __varRecorder__.foo + __varRecorder__.bar"), "capture z with captured refs: {}", output); - assert!(output.contains("__varRecorder__.baz.foo(__varRecorder__.z, 3)"), "call site uses captured refs: {}", output); - } - - #[test] - fn babel_top_level_var_and_func_decls_for_capturing() { - // Babel: 'var z = 3, y = 4; function foo() { var x = 5; }' - let output = transform_code("var z = 3, y = 4; function foo() { var x = 5; }"); - assert!(output.contains("__varRecorder__.z = 3"), "capture z: {}", output); - assert!(output.contains("__varRecorder__.y = 4"), "capture y: {}", output); - assert!(output.contains("__varRecorder__.foo = foo"), "capture foo: {}", output); - assert!(!output.contains("__varRecorder__.x"), "inner x NOT captured: {}", output); - } - - #[test] - fn babel_top_level_var_decls_and_var_usage() { - // var z = 3, y = 42, obj = {...}; function foo(y) { var x = 5 + y.b(z); } - // inner y is param (not captured), z is outer (captured ref) - let output = transform_code( - "var z = 3, y = 42, obj = {a: '123', b: function b(n) { return 23 + n; }};\nfunction foo(y) { var x = 5 + y.b(z); }" - ); - assert!(output.contains("__varRecorder__.z = 3;"), "capture z with value: {}", output); - assert!(output.contains("__varRecorder__.y = 42;"), "capture y with value: {}", output); - assert!(output.contains("__varRecorder__.obj ="), "capture obj: {}", output); - assert!(output.contains("__varRecorder__.foo = foo;"), "capture foo with assignment: {}", output); - // Function is hoisted: __varRecorder__.foo = foo appears before other captures - let foo_pos = output.find("__varRecorder__.foo = foo").unwrap(); - let z_pos = output.find("__varRecorder__.z = 3").unwrap(); - assert!(foo_pos < z_pos, "function capture hoisted before var captures: {}", output); - // Inside foo, z should reference __varRecorder__.z - assert!(output.contains("y.b(__varRecorder__.z)"), "z ref inside foo uses recorder: {}", output); - // Inner var x and param y should NOT be captured - assert!(!output.contains("__varRecorder__.x"), "inner x NOT captured: {}", output); - } - - #[test] - fn babel_captures_global_vars_redefined_in_subscopes() { - // const baz = 42; function bar(y) { const x = baz + 10; if (y > 10) { const baz = 33; return baz + 10 } return x; } - let output = transform_code( - "const baz = 42; function bar(y) { const x = baz + 10; if (y > 10) { const baz = 33; return baz + 10 } return x; }" - ); - assert!(output.contains("__varRecorder__.baz = 42;"), "capture baz with value: {}", output); - assert!(output.contains("__varRecorder__.bar = bar;"), "capture bar with assignment: {}", output); - // Inside bar, baz references __varRecorder__.baz in the outer scope - assert!(output.contains("__varRecorder__.baz + 10"), "baz ref inside bar uses recorder: {}", output); - // The inner const baz = 33 should NOT use the recorder - assert!(output.contains("const baz = 33"), "inner baz is a local const: {}", output); - assert!(output.contains("return baz + 10"), "inner return uses local baz: {}", output); - } - - #[test] - fn babel_captures_top_level_vars_outside_block_shadow() { - // const p = x => x + 1; function f() { { const p = 3; } return p(2); } - let output = transform_code( - "const p = x => x + 1; function f() { { const p = 3; } return p(2); }" - ); - assert!(output.contains("__varRecorder__.f = f;"), "capture f with assignment: {}", output); - assert!(output.contains("__varRecorder__.p ="), "capture p: {}", output); - // Inside f(), after the block scope, p(2) should reference __varRecorder__.p - assert!(output.contains("__varRecorder__.p(2)"), "p(2) outside block shadow uses recorder: {}", output); - // The block-scoped const p = 3 should NOT use recorder - assert!(output.contains("const p = 3"), "inner block const p is local: {}", output); - } - - // --- try-catch --- - - #[test] - fn babel_try_catch_not_transformed() { - // Babel: 'try { throw {} } catch (e) { e }' → not transformed - let output = transform_code("try { throw {} } catch (e) { e }"); - assert!(!output.contains("__varRecorder__.e"), "catch param not captured: {}", output); - assert!(output.contains("catch"), "keeps catch: {}", output); - } - - // --- for statement --- - - #[test] - fn babel_standard_for_var_not_rewritten() { - // Babel: 'for (var i = 0; i < 5; i ++) { i; }' → var not captured - let output = transform_code("for (var i = 0; i < 5; i++) { i; }"); - assert!(!output.contains("__varRecorder__.i"), "for-var i not captured: {}", output); - } - - #[test] - fn babel_for_in_var_not_rewritten() { - // Babel: 'for (var x in {}) { x; }' → var not captured - let output = transform_code("for (var x in {}) { x; }"); - assert!(!output.contains("__varRecorder__.x"), "for-in var not captured: {}", output); - } - - #[test] - fn babel_for_of_captures_iterable_ref() { - // Babel: 'for (let x of foo) { x; }' → 'for (let x of _rec.foo) { x; }' - // SWC: foo is an undeclared global, not captured. Loop var x also not captured. - let output = transform_code("for (let x of foo) { x; }"); - assert!(!output.contains("__varRecorder__.x"), "loop var not captured: {}", output); - // With a declared var: for (let x of arr) where arr is declared - let output2 = transform_code("var arr = [1]; for (let x of arr) { x; }"); - assert!(output2.contains("__varRecorder__.arr"), "declared iterable captured: {}", output2); - } - - #[test] - fn babel_for_of_destructured_captures_iterable_ref() { - // Babel: 'for (let [x, y] of foo) { x + y; }' → 'for (let [x, y] of _rec.foo) { x + y; }' - // SWC: foo is undeclared, not captured. Loop vars not captured. - let output = transform_code("for (let [x, y] of foo) { x + y; }"); - assert!(!output.contains("__varRecorder__.x"), "loop var x not captured: {}", output); - assert!(!output.contains("__varRecorder__.y"), "loop var y not captured: {}", output); - } - - // --- labels --- - - #[test] - fn babel_label_continue() { - // Babel: 'loop1:\nfor (var i = 0; i < 3; i++) continue loop1;' - let output = transform_code("loop1:\nfor (var i = 0; i < 3; i++) continue loop1;"); - assert!(output.contains("loop1:"), "keeps label: {}", output); - assert!(output.contains("continue loop1"), "keeps continue: {}", output); - } - - #[test] - fn babel_label_break() { - // Babel: 'loop1:\nfor (var i = 0; i < 3; i++) break loop1;' - let output = transform_code("loop1:\nfor (var i = 0; i < 3; i++) break loop1;"); - assert!(output.contains("loop1:"), "keeps label: {}", output); - assert!(output.contains("break loop1"), "keeps break: {}", output); - } - - // --- es6 let + const --- - - #[test] - fn babel_captures_let_as_var() { - // Babel: 'let x = 23, y = x + 1;' → '_rec.x = 23; _rec.y = _rec.x + 1;' - let output = transform_code("let x = 23, y = x + 1;"); - assert!(output.contains("__varRecorder__.x = 23;"), "capture let x with value: {}", output); - assert!(output.contains("__varRecorder__.y = __varRecorder__.x + 1;"), "capture let y with captured x ref: {}", output); - } - - #[test] - fn babel_captures_const_as_var() { - // Babel: 'const x = 23, y = x + 1;' → '_rec.x = 23; _rec.y = _rec.x + 1;' - let output = transform_code("const x = 23, y = x + 1;"); - assert!(output.contains("__varRecorder__.x = 23;"), "capture const x with value: {}", output); - assert!(output.contains("__varRecorder__.y = __varRecorder__.x + 1;"), "capture const y with captured x ref: {}", output); - } - - // --- enhanced object literals --- - - #[test] - fn babel_captures_shorthand_properties() { - // Babel: 'var x = 23, y = {x};' → '_rec.x = 23; _rec.y = { x: _rec.x };' - let output = transform_code("var x = 23, y = {x};"); - assert!(output.contains("__varRecorder__.x = 23;"), "capture x with value: {}", output); - // Shorthand {x} expands to {x: __varRecorder__.x} - assert!(output.contains("x: __varRecorder__.x"), "shorthand property uses captured ref: {}", output); - assert!(output.contains("__varRecorder__.y ="), "capture y: {}", output); - } - - // --- default args --- - - #[test] - fn babel_captures_default_arg() { - // Babel: 'function x(arg = foo) {}' → 'function x(arg = _rec.foo) {} _rec.x = x; x;' - // SWC does not capture undeclared globals, so foo is not captured without declaration - let output = transform_code("function x(arg = foo) {}"); - assert!(output.contains("__varRecorder__.x = x;"), "capture func x with assignment: {}", output); - // With declared default arg source: - let output2 = transform_code("var foo = 1; function x(arg = foo) {}"); - assert!(output2.contains("__varRecorder__.foo = 1;"), "capture declared foo with value: {}", output2); - assert!(output2.contains("__varRecorder__.x = x;"), "capture func x: {}", output2); - // Default arg should use captured ref - assert!(output2.contains("arg = __varRecorder__.foo"), "default arg uses captured ref: {}", output2); - } - - // --- class (without classToFunction transform) --- - - #[test] - fn babel_class_def_no_class_to_func() { - // Babel: 'class Foo { a() { return 23; } }' → 'class Foo { a() { return 23; } } _rec.Foo = Foo;' - let output = transform_code("class Foo {\n a() {\n return 23;\n }\n}"); - assert!(output.contains("class Foo"), "keeps class declaration: {}", output); - assert!(output.contains("return 23"), "keeps method body: {}", output); - assert!(output.contains("__varRecorder__.Foo = Foo;"), "captures class Foo with assignment: {}", output); - } - - #[test] - fn babel_exported_class_def_no_class_to_func() { - // Babel: 'export class Foo {}' → 'export class Foo {} _rec.Foo = Foo;' - let output = transform_code("export class Foo {}"); - assert!(output.contains("export class Foo"), "keeps export class: {}", output); - assert!(output.contains("__varRecorder__.Foo = Foo;"), "captures exported class with assignment: {}", output); - } - - #[test] - fn babel_exported_default_class_no_class_to_func() { - // Babel: 'export default class Foo {}' → 'export default class Foo {} _rec.Foo = Foo;' - let output = transform_code("export default class Foo {}"); - assert!(output.contains("class Foo"), "keeps class declaration: {}", output); - assert!(output.contains("__varRecorder__.Foo = Foo;"), "captures default exported class with assignment: {}", output); - assert!(output.contains("__varRecorder__.default = __varRecorder__.Foo"), "captures __rec.default: {}", output); - assert!(output.contains("export default __varRecorder__.Foo"), "export default uses captured ref: {}", output); - } - - #[test] - fn babel_does_not_capture_class_expr() { - // Babel: 'var bar = class Foo {}' → '_rec.bar = class Foo {};' - let output = transform_code("var bar = class Foo {}"); - assert!(output.contains("__varRecorder__.bar = class Foo"), "captures var bar with class expr: {}", output); - // Foo as a class expression name should not be separately captured - assert!(!output.contains("__varRecorder__.Foo"), "class expr name Foo NOT captured: {}", output); - } - - #[test] - fn babel_captures_var_same_name_as_class_expr() { - // Babel: 'var Foo = class Foo {}; new Foo();' → '_rec.Foo = class Foo {}; new _rec.Foo();' - let output = transform_code("var Foo = class Foo {}; new Foo();"); - assert!(output.contains("__varRecorder__.Foo = class Foo"), "captures var Foo with class expr: {}", output); - assert!(output.contains("new __varRecorder__.Foo()"), "new uses captured Foo: {}", output); - } - - // --- template strings --- - - #[test] - fn babel_template_string_ref() { - // Babel: '`${foo}`' → '`${ _rec.foo }`;' - // SWC does not capture undeclared globals; test with declared var - let output = transform_code("var foo = 1; `${foo}`;"); - assert!(output.contains("__varRecorder__.foo = 1;"), "captures foo with value: {}", output); - assert!(output.contains("${__varRecorder__.foo}"), "template string uses captured ref: {}", output); - } - - // --- computed prop in object literal --- - - #[test] - fn babel_computed_prop_in_object() { - // Babel: 'var x = {[x]: y};' → '_rec.x = { [_rec.x]: _rec.y };' - // SWC does not capture undeclared globals; test with all vars declared - let output = transform_code("var x = 1, y = 2; var z = {[x]: y};"); - assert!(output.contains("__varRecorder__.x = 1;"), "captures x with value: {}", output); - assert!(output.contains("__varRecorder__.y = 2;"), "captures y with value: {}", output); - assert!(output.contains("[__varRecorder__.x]: __varRecorder__.y"), "computed prop uses captured refs: {}", output); - assert!(output.contains("__varRecorder__.z ="), "captures z: {}", output); - } - - // --- patterns / destructuring --- - - #[test] - fn babel_destructured_obj_var() { - // Babel: 'var {x} = {x: 3};' → 'var destructured_1 = { x: 3 }; _rec.x = destructured_1.x;' - let output = transform_code("var {x} = {x: 3};"); - assert!(output.contains("_tmp_"), "uses temp variable: {}", output); - assert!(output.contains("__varRecorder__.x ="), "captures destructured x: {}", output); - assert!(output.contains(".x;") || output.contains(".x\n"), "accesses .x property from temp: {}", output); - } - - #[test] - fn babel_destructured_obj_var_with_alias() { - // Babel: 'var {x: y} = foo;' → 'var destructured_1 = _rec.foo; _rec.y = destructured_1.x;' - // SWC does not capture undeclared globals; foo is undeclared - let output = transform_code("var {x: y} = foo;"); - assert!(output.contains("__varRecorder__.y ="), "captures alias y: {}", output); - assert!(!output.contains("__varRecorder__.x"), "x is a key not a binding, not captured: {}", output); - assert!(output.contains(".x"), "accesses .x from temp: {}", output); - } - - #[test] - fn babel_destructured_list_with_spread() { - // Babel: 'var [a, b, ...rest] = foo;' - // SWC does not capture undeclared globals; foo is undeclared - let output = transform_code("var [a, b, ...rest] = foo;"); - assert!(output.contains("__varRecorder__.a ="), "captures a: {}", output); - assert!(output.contains("__varRecorder__.b ="), "captures b: {}", output); - assert!(output.contains("__varRecorder__.rest ="), "captures rest: {}", output); - assert!(output.contains("[0]"), "accesses index 0: {}", output); - assert!(output.contains("[1]"), "accesses index 1: {}", output); - } - - #[test] - fn babel_destructured_list_with_obj() { - // Babel: 'var [{b}] = foo;' → temp[0].b - // SWC does not capture undeclared globals; foo is undeclared - let output = transform_code("var [{b}] = foo;"); - assert!(output.contains("__varRecorder__.b ="), "captures b: {}", output); - assert!(output.contains("[0]"), "accesses index 0: {}", output); - assert!(output.contains(".b"), "accesses .b property: {}", output); - } - - #[test] - fn babel_destructured_list_nested() { - // Babel: 'var [[b]] = foo;' → temp[0][0] - let output = transform_code("var [[b]] = foo;"); - assert!(output.contains("__varRecorder__.b ="), "captures b: {}", output); - assert!(output.contains("[0]"), "accesses index: {}", output); - } - - #[test] - fn babel_destructured_obj_with_list() { - // Babel: 'var {x: [y]} = foo, z = 23;' - // SWC does not capture undeclared globals; foo is undeclared - let output = transform_code("var {x: [y]} = foo, z = 23;"); - assert!(output.contains("__varRecorder__.y ="), "captures y: {}", output); - assert!(output.contains("__varRecorder__.z = 23"), "captures z with value: {}", output); - assert!(output.contains(".x"), "accesses .x property: {}", output); - } - - #[test] - fn babel_destructured_deep() { - // Babel: 'var {x: {x: {x}}, y: {y: x}} = foo;' - // SWC does not capture undeclared globals; foo is undeclared - let output = transform_code("var {x: {x: {x}}, y: {y: x}} = foo;"); - assert!(output.contains("__varRecorder__.x ="), "captures x: {}", output); - // Should have multiple temp variables for nested access - assert!(output.contains("_tmp_"), "uses temp variables for deep destructuring: {}", output); - } - - #[test] - fn babel_destructured_obj_with_init() { - // Babel: 'var {x = 4} = {x: 3};' - let output = transform_code("var {x = 4} = {x: 3};"); - assert!(output.contains("__varRecorder__.x ="), "captures x with default: {}", output); - } - - #[test] - fn babel_destructured_list_with_default() { - // Babel: 'var [a = 3] = foo;' - let output = transform_code("var [a = 3] = foo;"); - assert!(output.contains("__varRecorder__.a ="), "captures a with default: {}", output); - } - - #[test] - fn babel_destructured_list_nested_default() { - // Babel: 'var [[a = 3]] = foo;' - let output = transform_code("var [[a = 3]] = foo;"); - assert!(output.contains("__varRecorder__.a ="), "captures a with nested default: {}", output); - } - - #[test] - fn babel_destructured_list_obj_deep() { - // Babel: 'var [{b: {c: [a]}}] = foo;' - let output = transform_code("var [{b: {c: [a]}}] = foo;"); - assert!(output.contains("__varRecorder__.a ="), "captures a: {}", output); - // Multiple temp variables for deep nesting - assert!(output.contains("_tmp_"), "uses temp variables: {}", output); - } - - #[test] - fn babel_destructured_rest_prop() { - // Babel: 'var {a, b, ...rest} = foo;' - let output = transform_code("var {a, b, ...rest} = foo;"); - assert!(output.contains("__varRecorder__.a ="), "captures a: {}", output); - assert!(output.contains("__varRecorder__.b ="), "captures b: {}", output); - assert!(output.contains("__varRecorder__.rest ="), "captures rest: {}", output); - } - - // --- async --- - - #[test] - fn babel_async_function() { - // Babel: 'async function foo() { return 23 }' → captures foo, hoists capture - let output = transform_code("async function foo() { return 23 }"); - assert!(output.contains("__varRecorder__.foo = foo;"), "captures async fn with assignment: {}", output); - assert!(output.contains("async function foo()"), "keeps async function declaration: {}", output); - assert!(output.contains("return 23"), "keeps function body: {}", output); - // Function capture is hoisted before the function declaration - let capture_pos = output.find("__varRecorder__.foo = foo").unwrap(); - let decl_pos = output.find("async function foo()").unwrap(); - assert!(capture_pos < decl_pos, "function capture hoisted before declaration: {}", output); - } - - #[test] - fn babel_await() { - // Babel: 'var x = await foo();' → '_rec.x = await _rec.foo();' - // SWC does not capture undeclared globals; with declared foo, both are captured - let output = transform_code("var foo = async () => 1; var x = await foo();"); - assert!(output.contains("__varRecorder__.x = await __varRecorder__.foo()"), "captures x with await and captured foo ref: {}", output); - } - - #[test] - fn babel_exported_async_function() { - // Babel: 'export async function foo() { return 23; }' - let output = transform_code("export async function foo() { return 23; }"); - assert!(output.contains("__varRecorder__.foo = foo;"), "captures exported async fn with assignment: {}", output); - assert!(output.contains("export async function foo()"), "keeps export async function: {}", output); - } - - #[test] - fn babel_exported_default_async_function() { - // Babel: 'export default async function foo() { return 23; }' - let output = transform_code("export default async function foo() { return 23; }"); - assert!(output.contains("__varRecorder__.foo = foo;"), "captures default async fn with assignment: {}", output); - assert!(output.contains("async function foo()"), "keeps async function: {}", output); - assert!(output.contains("__varRecorder__.default = foo;"), "captures __rec.default: {}", output); - assert!(output.contains("export default foo;"), "export default uses name: {}", output); - } - - // --- import --- - - #[test] - fn babel_import_default() { - // Babel: 'import x from "./some-es6-module.js";' → keeps import, adds _rec.x = x - let output = transform_code("import x from \"./some-es6-module.js\";"); - assert!(output.contains("import x from \"./some-es6-module.js\""), "keeps full import: {}", output); - assert!(output.contains("__varRecorder__.x = x;"), "captures default import with assignment: {}", output); - } - - #[test] - fn babel_import_star() { - // Babel: 'import * as name from "module-name";' → keeps import, adds _rec.name = name - let output = transform_code("import * as name from \"module-name\";"); - assert!(output.contains("import * as name from \"module-name\""), "keeps full import: {}", output); - assert!(output.contains("__varRecorder__.name = name;"), "captures namespace with assignment: {}", output); - } - - #[test] - fn babel_import_member() { - // Babel: 'import { member } from "module-name";' → keeps import, adds _rec.member = member - let output = transform_code("import { member } from \"module-name\";"); - assert!(output.contains("import {"), "keeps import brace: {}", output); - assert!(output.contains("from \"module-name\""), "keeps source: {}", output); - assert!(output.contains("__varRecorder__.member = member;"), "captures member with assignment: {}", output); - } - - #[test] - fn babel_import_member_with_alias() { - // Babel: 'import { member as alias } from "module-name";' → _rec.alias = alias - let output = transform_code("import { member as alias } from \"module-name\";"); - assert!(output.contains("member as alias"), "keeps alias in import: {}", output); - assert!(output.contains("__varRecorder__.alias = alias;"), "captures alias with assignment: {}", output); - assert!(!output.contains("__varRecorder__.member"), "member not captured separately: {}", output); - } - - #[test] - fn babel_import_multiple_members() { - // Babel: 'import { member1 , member2 } from "module-name";' - let output = transform_code("import { member1, member2 } from \"module-name\";"); - assert!(output.contains("__varRecorder__.member1 = member1;"), "captures member1 with assignment: {}", output); - assert!(output.contains("__varRecorder__.member2 = member2;"), "captures member2 with assignment: {}", output); - } - - #[test] - fn babel_import_multiple_members_with_alias() { - // Babel: 'import { member1 , member2 as alias} from "module-name";' - let output = transform_code("import { member1, member2 as alias } from \"module-name\";"); - assert!(output.contains("__varRecorder__.member1 = member1;"), "captures member1 with assignment: {}", output); - assert!(output.contains("__varRecorder__.alias = alias;"), "captures alias with assignment: {}", output); - assert!(!output.contains("__varRecorder__.member2"), "member2 not captured (aliased to alias): {}", output); - } - - #[test] - fn babel_import_default_and_member() { - // Babel: 'import defaultMember, { member } from "module-name";' - let output = transform_code("import defaultMember, { member } from \"module-name\";"); - assert!(output.contains("__varRecorder__.defaultMember = defaultMember;"), "captures default with assignment: {}", output); - assert!(output.contains("__varRecorder__.member = member;"), "captures member with assignment: {}", output); - } - - #[test] - fn babel_import_default_and_star() { - // Babel: 'import defaultMember, * as name from "module-name";' - let output = transform_code("import defaultMember, * as name from \"module-name\";"); - assert!(output.contains("__varRecorder__.defaultMember = defaultMember;"), "captures default with assignment: {}", output); - assert!(output.contains("__varRecorder__.name = name;"), "captures namespace with assignment: {}", output); - } - - #[test] - fn babel_import_without_binding() { - // Babel: 'import "module-name";' → 'import "module-name";' (no capture) - let output = transform_code("import \"module-name\";"); - assert!(output.contains("import \"module-name\""), "keeps bare import: {}", output); - // No capture assignments like __varRecorder__.x = x should appear - assert!(!output.contains("__varRecorder__."), "no capture assignment for bare import: {}", output); - } - - // --- export --- - - #[test] - fn babel_export_default_named_var() { - // Babel: 'var x = {x: 23}; export default x;' - // → '_rec.x = { x: 23 }; var x = _rec.x; export default x;' - let output = transform_code("var x = {x: 23}; export default x;"); - assert!(output.contains("__varRecorder__.x ="), "captures x: {}", output); - assert!(output.contains("x: 23"), "keeps object literal: {}", output); - assert!(output.contains("__varRecorder__.default = __varRecorder__.x"), "captures __rec.default: {}", output); - assert!(output.contains("export default __varRecorder__.x"), "export default uses captured ref: {}", output); - } - - #[test] - fn babel_export_var_with_capturing() { - // Babel: 'var a = 23; export var x = a + 1, y = x + 2; export default function f() {}' - let output = transform_code("var a = 23;\nexport var x = a + 1, y = x + 2;\nexport default function f() {}"); - assert!(output.contains("__varRecorder__.a = 23;"), "captures a with value: {}", output); - assert!(output.contains("__varRecorder__.a + 1"), "export var x uses captured a ref: {}", output); - assert!(output.contains("__varRecorder__.x = x;"), "captures export x: {}", output); - assert!(output.contains("__varRecorder__.y = y;"), "captures export y: {}", output); - assert!(output.contains("__varRecorder__.f = f;"), "captures default f: {}", output); - assert!(output.contains("__varRecorder__.default = f;"), "captures __rec.default: {}", output); - assert!(output.contains("export default f;"), "keeps export default: {}", output); - } - - #[test] - fn babel_export_var_statement() { - // Babel: 'var x = 23; export { x };' - let output = transform_code("var x = 23; export { x };"); - assert!(output.contains("__varRecorder__.x = 23;"), "captures x with value: {}", output); - assert!(output.contains("var x = __varRecorder__.x;"), "re-declares var from recorder: {}", output); - assert!(output.contains("export { x }"), "keeps named export: {}", output); - } - - #[test] - fn babel_export_aliased_var_statement() { - // Babel: 'var x = 23; export { x as y };' - let output = transform_code("var x = 23; export { x as y };"); - assert!(output.contains("__varRecorder__.x = 23;"), "captures x with value: {}", output); - assert!(output.contains("var x = __varRecorder__.x;"), "re-declares var from recorder: {}", output); - assert!(output.contains("export { x as y }"), "keeps aliased export: {}", output); - } - - #[test] - fn babel_export_const() { - // Babel: 'export const x = 23;' → 'export const x = 23; _rec.x = x;' - let output = transform_code("export const x = 23;"); - assert!(output.contains("export const x = 23;"), "keeps export const declaration: {}", output); - assert!(output.contains("__varRecorder__.x = x;"), "captures const x with assignment: {}", output); - } - - #[test] - fn babel_export_function_decl() { - // Babel: 'export function x() {};' → 'function x() {} _rec.x = x; export { x };' - let output = transform_code("export function x() {}"); - assert!(output.contains("__varRecorder__.x = x;"), "captures function x with assignment: {}", output); - assert!(output.contains("export function x()"), "keeps export function: {}", output); - } - - #[test] - fn babel_export_default_function_decl() { - // Babel: 'export default function x() {};' → 'function x() {} _rec.x = x; export default x;' - let output = transform_code("export default function x() {}"); - assert!(output.contains("__varRecorder__.x = x;"), "captures default fn x with assignment: {}", output); - assert!(output.contains("__varRecorder__.default = x;"), "captures __rec.default: {}", output); - assert!(output.contains("export default x;"), "keeps export default: {}", output); - } - - #[test] - fn babel_export_class_no_class_to_func() { - // Babel: 'export class Foo {};' → 'export class Foo {} _rec.Foo = Foo;' - let output = transform_code("export class Foo {}"); - assert!(output.contains("export class Foo"), "keeps export class: {}", output); - assert!(output.contains("__varRecorder__.Foo = Foo;"), "captures class Foo with assignment: {}", output); - } - - #[test] - fn babel_export_default_class_no_class_to_func() { - // Babel: 'export default class Foo {};' → 'export default class Foo {} _rec.Foo = Foo;' - let output = transform_code("export default class Foo {}"); - assert!(output.contains("__varRecorder__.Foo = Foo;"), "captures default class Foo with assignment: {}", output); - assert!(output.contains("__varRecorder__.default = __varRecorder__.Foo"), "captures __rec.default: {}", output); - assert!(output.contains("export default __varRecorder__.Foo"), "export default uses captured ref: {}", output); - } - - #[test] - fn babel_export_default_expression() { - // Babel: 'export default foo(1, 2, 3);' → 'export default _rec.foo(1, 2, 3);' - // SWC does not capture undeclared globals; with declared foo it is captured - let output = transform_code("var foo = x => x; export default foo(1, 2, 3);"); - assert!(output.contains("__varRecorder__.foo(1, 2, 3)"), "call uses captured foo ref: {}", output); - assert!(output.contains("export default __varRecorder__.foo(1, 2, 3)"), "export default uses captured ref: {}", output); - } - - #[test] - fn babel_re_export_namespace_import() { - // Babel: 'import * as completions from "./lib/completions.js"; export { completions }' - let output = transform_code("import * as completions from \"./lib/completions.js\";\nexport { completions }"); - assert!(output.contains("import * as completions from"), "keeps namespace import: {}", output); - assert!(output.contains("__varRecorder__.completions = completions;"), "captures completions with assignment: {}", output); - assert!(output.contains("export { completions }"), "keeps named export: {}", output); - } - - #[test] - fn babel_re_export_named() { - // Babel: 'export { name1, name2 } from "foo";' → keeps as-is - let output = transform_code("export { name1, name2 } from \"foo\";"); - assert!(output.contains("export { name1, name2 }"), "keeps re-export with both names: {}", output); - } - - #[test] - fn babel_export_from_named_aliased() { - // Babel: 'export { name1 as foo1, name2 as bar2 } from "foo";' - let output = transform_code("export { name1 as foo1, name2 as bar2 } from \"foo\";"); - assert!(output.contains("export {"), "keeps re-export: {}", output); - } - - #[test] - fn babel_export_bug_1() { - // Babel: 'foo(); export function a() {} export function b() {}' - // → functions hoisted, captures added before foo() call - // SWC does not capture undeclared globals; a and b are captured and hoisted - let output = transform_code("foo();\nexport function a() {}\nexport function b() {}"); - assert!(output.contains("__varRecorder__.a = a;"), "captures a with assignment: {}", output); - assert!(output.contains("__varRecorder__.b = b;"), "captures b with assignment: {}", output); - // Function captures should be hoisted before foo() call - let a_pos = output.find("__varRecorder__.a = a").unwrap(); - let foo_pos = output.find("foo()").unwrap(); - assert!(a_pos < foo_pos, "function capture hoisted before foo() call: {}", output); - } - - #[test] - fn babel_export_bug_2() { - // Babel: 'export { a } from "./package-commands.js"; export function b() {} export function c() {}' - let output = transform_code("export { a } from \"./package-commands.js\";\nexport function b() {}\nexport function c() {}"); - assert!(output.contains("__varRecorder__.b = b;"), "captures b with assignment: {}", output); - assert!(output.contains("__varRecorder__.c = c;"), "captures c with assignment: {}", output); - assert!(output.contains("./package-commands.js"), "keeps re-export source: {}", output); - assert!(output.contains("export function b()"), "keeps export function b: {}", output); - assert!(output.contains("export function c()"), "keeps export function c: {}", output); - } - - #[test] - fn test_class_with_separate_export() { - // This is the pattern from lively.vm/eval-strategies.js that breaks rollup: - // class is declared, then exported separately via export { } - let output = transform_code("class Foo { eval() { return 1; } }\nexport { Foo };"); - // The class should still be declared (not removed) so rollup can resolve the export - assert!(output.contains("export {"), "keeps export: {}", output); - assert!(output.contains("class Foo"), "keeps class declaration: {}", output); - assert!(output.contains("__varRecorder__.Foo = Foo"), "captures Foo: {}", output); - } - - // --- Default export capture tests (Divergence O/P) --- - // Babel always sets __rec.default for any form of default export. - // SWC must do the same. - - #[test] - fn test_export_default_named_function_captures_default() { - // Babel: export default function foo() {} → function foo(){} _rec.foo = foo; _rec.default = foo; export default foo; - let output = transform_code("export default function foo() { return 1; }"); - assert!(output.contains("function foo()"), "keeps function declaration: {}", output); - assert!(output.contains("__varRecorder__.foo = foo;"), "captures foo with assignment: {}", output); - assert!(output.contains("__varRecorder__.default = foo;"), "captures __rec.default = foo: {}", output); - assert!(output.contains("export default foo;"), "export default uses named binding: {}", output); - } - - #[test] - fn test_export_default_named_class_captures_default() { - // Babel: export default class Foo {} → class Foo {} _rec.Foo = Foo; _rec.default = Foo; export default Foo; - let output = transform_code("export default class Foo { method() {} }"); - assert!(output.contains("class Foo"), "keeps class declaration: {}", output); - assert!(output.contains("__varRecorder__.Foo = Foo;"), "captures Foo with assignment: {}", output); - assert!(output.contains("__varRecorder__.default = __varRecorder__.Foo"), "captures __rec.default from captured Foo: {}", output); - assert!(output.contains("export default __varRecorder__.Foo"), "export default uses captured ref: {}", output); - } - - #[test] - fn test_export_default_expression_captures_default() { - // Babel: export default expr → _rec.default = expr; export default expr; - let output = transform_code("var x = 42; export default x;"); - assert!(output.contains("__varRecorder__.x ="), "captures x: {}", output); - assert!(output.contains("__varRecorder__.default = __varRecorder__.x"), - "captures __rec.default from captured x: {}", output); - assert!(output.contains("export default __varRecorder__.x"), "export default uses captured ref: {}", output); - } - - #[test] - fn test_export_default_anonymous_function_captures_default() { - // Babel: export default function() {} → _rec.default = function() {}; export default _rec.default; - let output = transform_code("export default function() { return 1; }"); - assert!(output.contains("__varRecorder__.default") || output.contains("__varRecorder__[\"default\"]"), - "captures __rec.default for anonymous function: {}", output); - } - - #[test] - fn test_export_default_literal_captures_default() { - // Babel: export default 42 → _rec.default = 42 - let output = transform_code("export default 42;"); - assert!(output.contains("__varRecorder__.default") || output.contains("__varRecorder__[\"default\"]"), - "captures __rec.default for literal: {}", output); - } - - // =================================================================== - // Resurrection build: function declaration wrapping - // =================================================================== - - fn transform_code_resurrection(code: &str) -> String { - let cm = Lrc::new(SourceMap::default()); - let fm = cm.new_source_file(FileName::Anon.into(), code.to_string()); - - let mut module = parse_file_as_module( - &fm, - Syntax::Es(Default::default()), - Default::default(), - None, - &mut vec![], - ) - .unwrap(); - - let mut transform = ScopeCapturingTransform::new( - "__varRecorder__".to_string(), - Some(r#"__varRecorder__["test-mod__define__"]"#.to_string()), - vec!["console".to_string()], - false, // captureImports = false for resurrection - true, // resurrection = true - "test-mod".to_string(), - Some("({ pathInPackage: () => \"test-mod\" })".to_string()), - None, - HashMap::new(), - ); - - module.visit_mut_with(&mut transform); - - let mut buf = vec![]; - { - let mut emitter = Emitter { - cfg: Config::default(), - cm: cm.clone(), - comments: None, - wr: JsWriter::new(cm, "\n", &mut buf, None), - }; - - emitter.emit_module(&module).unwrap(); - } - - String::from_utf8(buf).unwrap() - } - - #[test] - fn test_resurrection_func_decl_replaced_with_let_wrapper() { - // Babel: function foo() { return 1; } - // => let __moduleMeta__ = ; - // var foo = wrapper("foo", "function", function() { return 1; }, __moduleMeta__) - // The function declaration should be REPLACED (not kept alongside a capture). - let output = transform_code_resurrection("function foo() { return 1; }"); - // Should have var foo = wrapper(...) - assert!(output.contains("var foo ="), "function decl should be replaced with var: {}", output); - assert!(output.contains("__define__"), "uses declaration wrapper: {}", output); - assert!(output.contains("\"foo\""), "wrapper includes name string: {}", output); - assert!(output.contains("\"function\""), "wrapper includes kind string: {}", output); - // The original `function foo` declaration should NOT be present - assert!(!output.contains("function foo()"), "original function decl should be removed: {}", output); - // Should have __moduleMeta__ and __module_exports__ declarations - assert!(output.contains("var __moduleMeta__"), "has __moduleMeta__ declaration: {}", output); - assert!(output.contains("__module_exports__"), "has __module_exports__: {}", output); - } - - #[test] - fn test_resurrection_func_decl_wrapper_uses_module_meta() { - // Divergence B: The 4th argument to the wrapper should be __moduleMeta__, - // not __varRecorder__. __moduleMeta__ is set to the currentModuleAccessor. - let output = transform_code_resurrection("function foo() { return 1; }"); - // Should prepend let __moduleMeta__ = - assert!(output.contains("var __moduleMeta__ = ("), "prepends __moduleMeta__ with accessor: {}", output); - assert!(output.contains("pathInPackage"), "accessor has pathInPackage: {}", output); - } - - #[test] - fn test_resurrection_func_decl_non_function_stmts_unchanged() { - // Function declarations should be replaced with var + wrapper. - // Babel's bundler does NOT wrap var declarations with declarationWrapper — - // only function declarations go through insertCapturesForFunctionDeclarations. - let output = transform_code_resurrection("var x = 1; function foo() {} var y = 2;"); - assert!(output.contains("var foo ="), "func replaced with var: {}", output); - assert!(output.contains("\"function\""), "func wrapper has function kind: {}", output); - // var x and y are captured as plain __varRecorder__ assignments (no __define__ wrapping) - assert!(output.contains("__varRecorder__.x = 1"), "var x captured without define: {}", output); - assert!(output.contains("__varRecorder__.y = 2"), "var y captured without define: {}", output); - // Function declarations should NOT remain as `function foo` - assert!(!output.contains("function foo()"), "original function decl should be removed: {}", output); - } - - // --- Regression tests for function declaration wrapper edge cases --- - - #[test] - fn test_resurrection_exported_function_gets_wrapper() { - // export function foo() {} — the function capture is hoisted and wrapped. - // The export declaration stays (rollup handles it). - let output = transform_code_resurrection("export function foo() { return 1; }"); - assert!(output.contains("var __moduleMeta__"), "has module meta declaration: {}", output); - assert!(output.contains("export"), "still exports: {}", output); - assert!(output.contains("__define__"), "uses declaration wrapper: {}", output); - assert!(output.contains("\"foo\""), "wrapper has function name: {}", output); - } - - #[test] - fn test_resurrection_func_in_nested_scope_not_wrapped() { - // Only top-level function declarations get wrapped, not nested ones - let output = transform_code_resurrection("function outer() { function inner() {} }"); - assert!(output.contains("var outer ="), "top-level replaced with var: {}", output); - assert!(output.contains("\"outer\""), "wrapper has outer name: {}", output); - assert!(output.contains("function inner"), "nested function NOT wrapped, kept as-is: {}", output); - assert!(!output.contains("let inner"), "inner should not be replaced with var: {}", output); - } - - #[test] - fn test_resurrection_class_not_affected_by_func_wrapper() { - // Class declarations should NOT be affected by function wrapping - let output = transform_code_resurrection("class Foo {} function bar() {}"); - assert!(output.contains("var bar ="), "func replaced with var: {}", output); - assert!(output.contains("\"bar\""), "wrapper has bar name: {}", output); - assert!(output.contains("class Foo") || output.contains("__varRecorder__.Foo"), - "class handled separately: {}", output); - } - - #[test] - fn test_resurrection_async_function_gets_wrapper() { - // async functions are also FunctionDeclarations - let output = transform_code_resurrection("async function fetchData() { return await 1; }"); - assert!(output.contains("var fetchData ="), "async func replaced with var: {}", output); - assert!(output.contains("\"fetchData\""), "wrapper has function name: {}", output); - assert!(output.contains("\"function\""), "wrapper has function kind: {}", output); - assert!(output.contains("var __moduleMeta__"), "has module meta declaration: {}", output); - } - - // --- Babel 'declarations' block tests (declarationWrapper behavior) --- - // Babel's bundler only uses declarationWrapper for function declarations - // (via insertCapturesForFunctionDeclarations). Variable declarations and - // assignments are captured without __define__ wrapping. - - #[test] - fn babel_declarations_var_not_wrapped() { - // Babel's bundler does NOT wrap var declarations with __define__. - // Only function declarations get wrapper treatment. - let output = transform_code_resurrection("var x = 23;"); - assert!(output.contains("__varRecorder__.x = 23"), "var captured directly: {}", output); - assert!(!output.contains("__define__"), "var should NOT use __define__: {}", output); - } - - #[test] - fn babel_declarations_assignment_not_wrapped() { - // Babel's bundler does NOT wrap assignments with __define__. - let output = transform_code_resurrection("var x; x = 23;"); - assert!(output.contains("__varRecorder__.x = 23"), "assignment captured directly: {}", output); - // __define__ should only appear if there are function declarations (there are none here) - assert!(!output.contains("__define__"), "assignment should NOT use __define__: {}", output); - } - - #[test] - fn babel_declarations_func_decl_wrapped() { - // Babel's insertCapturesForFunctionDeclarations (LAST step) replaces function - // declarations with: let bar = wrapper("bar", "function", function(){}, __moduleMeta__) - // Note: 4th arg is __moduleMeta__ (not __varRecorder__) for function replacement. - let output = transform_code_resurrection("function bar() {}"); - assert!(output.contains("var bar ="), "func replaced with var: {}", output); - assert!(output.contains("__define__"), "func wrapped: {}", output); - assert!(output.contains("\"bar\""), "has name: {}", output); - assert!(output.contains("\"function\""), "has function kind: {}", output); - assert!(output.contains("__moduleMeta__)"), "4th arg is __moduleMeta__: {}", output); - } - - #[test] - fn babel_declarations_export_var_not_wrapped() { - // Babel's bundler does NOT wrap exported var captures with __define__. - let output = transform_code_resurrection("export var x = 23;"); - assert!(output.contains("export"), "keeps export: {}", output); - assert!(output.contains("__varRecorder__.x = x"), "exported var captured as ident: {}", output); - assert!(!output.contains("__define__"), "exported var should NOT use __define__: {}", output); - } - - #[test] - fn babel_declarations_export_function_wrapped() { - // Babel: 'export function foo() {}' → function declaration gets wrapper treatment - let output = transform_code_resurrection("export function foo() {}"); - assert!(output.contains("export"), "keeps export: {}", output); - assert!(output.contains("__define__"), "wrapped: {}", output); - assert!(output.contains("\"function\""), "has function kind: {}", output); - assert!(output.contains("\"foo\""), "has name: {}", output); - } - - #[test] - fn babel_declarations_export_vars_with_separate_export() { - // Babel's bundler does NOT wrap var captures with __define__. - // vars are captured directly, export preserved - let output = transform_code_resurrection("var x, y; x = 23; export { x, y };"); - assert!(output.contains("export {"), "keeps export: {}", output); - assert!(!output.contains("__define__"), "vars should NOT use __define__: {}", output); - assert!(output.contains("__varRecorder__.x"), "has x capture: {}", output); - assert!(output.contains("__varRecorder__.y"), "has y capture: {}", output); - } - - // --- Divergence G/H: no duplicate captures for exported fn/class --- - - #[test] - fn test_no_duplicate_capture_for_exported_function() { - // export function foo() {} should produce exactly ONE __rec.foo capture, - // not two (one from scope capture + one from ExportedImportCapturePass) - let output = transform_code("export function foo() { return 1; }"); - let count = output.matches("__varRecorder__.foo").count(); - // One from hoisted capture, the export itself doesn't add another - assert!(count <= 2, "should not have excessive duplicate captures (found {}): {}", count, output); - } - - #[test] - fn test_no_duplicate_capture_for_exported_class() { - let output = transform_code("export class Foo { method() {} }"); - let count = output.matches("__varRecorder__.Foo").count(); - assert!(count <= 2, "should not have excessive duplicate captures (found {}): {}", count, output); - } - - // --- Divergence Q: __module_exports__ placement --- - - #[test] - fn test_module_exports_placed_after_recorder_init() { - // Babel puts __module_exports__ near the top (after recorder init), - // not at the end of the module body - let output = transform_code_resurrection("export var x = 23; var y = 42;"); - let recorder_pos = output.find("recorderFor").expect("has recorder init"); - let module_exports_pos = output.find("__module_exports__").expect("has module_exports"); - let last_assignment_pos = output.rfind("__varRecorder__").expect("has assignments"); - assert!(module_exports_pos < last_assignment_pos, - "__module_exports__ should be near top, not at end: {}", output); - assert!(module_exports_pos > recorder_pos, - "__module_exports__ should be after recorder init: {}", output); - } - - // --- Divergence S: ExportedImportCapturePass runs for all scope capture --- - // (This is tested via the integration tests in lib.rs which use the full pipeline) - - // =================================================================== - // New tests ported from Babel capturing-test.js - // =================================================================== - - #[test] - fn babel_wraps_literals_exported_as_defaults() { - // Babel line 766: 'export default 32' - // Babel expected: '_rec.$32 = 32; var $32 = _rec.$32; export default $32;' - // In resurrection mode, the declarationWrapper wraps the literal. - let output = transform_code_resurrection("export default 32"); - // The literal 32 should be captured into __varRecorder__.default - assert!(output.contains("__varRecorder__.default = 32"), "captures default literal: {}", output); - assert!(output.contains("export default"), "keeps export default: {}", output); - // TODO: Babel generates a synthetic $32 variable name for the literal; - // SWC assigns to __varRecorder__.default directly. Both produce correct - // runtime behavior but differ in generated variable names. - } - - #[test] - fn babel_destructuring_not_wrapped_with_declaration_wrapper() { - // Babel's bundler does NOT pass declarationWrapper to the scope capture step. - // Destructured vars are captured directly, not wrapped with __define__. - let output = transform_code_resurrection("var [{x}, y] = foo"); - // SWC uses _tmp_ instead of destructured_1 - assert!(output.contains("_tmp_"), "uses temp variable for destructuring: {}", output); - // x and y should be captured without __define__ wrapping - assert!(output.contains("__varRecorder__.x ="), "captures destructured x: {}", output); - assert!(output.contains("__varRecorder__.y ="), "captures destructured y: {}", output); - // No __define__ wrapping for destructured vars (only function decls get __define__) - assert!(!output.contains("__define__"), "destructured vars should NOT use __define__: {}", output); - } - - // --- Divergence S: ExportedImportCapturePass runs for all scope capture --- - // (This is tested via the integration tests in lib.rs which use the full pipeline) - - // --- __module_exports__ alignment tests --- - // These test the exact format of __module_exports__ entries to match Babel. - - #[test] - fn test_module_exports_reexport_uses_resolved_id() { - // Divergence 2: __reexport__ entries must use resolved module IDs, not raw source strings. - // Babel: __reexport__ (e.g. "lively.lang/array.js") - // SWC was producing: __reexport__./array.js (raw source string) - let cm = Lrc::new(SourceMap::default()); - let fm = cm.new_source_file(FileName::Anon.into(), "export * from './array.js';".to_string()); - let mut module = parse_file_as_module(&fm, Syntax::Es(Default::default()), Default::default(), None, &mut vec![]).unwrap(); - let mut resolved = HashMap::new(); - resolved.insert("./array.js".to_string(), "lively.lang/array.js".to_string()); - let mut transform = ScopeCapturingTransform::new( - "__varRecorder__".to_string(), None, vec![], false, true, - "lively.lang/index.js".to_string(), None, None, resolved, - ); - module.visit_mut_with(&mut transform); - let mut buf = vec![]; - { let mut emitter = Emitter { cfg: Config::default(), cm: cm.clone(), comments: None, wr: JsWriter::new(cm, "\n", &mut buf, None) }; emitter.emit_module(&module).unwrap(); } - let output = String::from_utf8(buf).unwrap(); - // Must use resolved ID, not raw "./array.js" - assert!(output.contains("__reexport__lively.lang/array.js"), - "should use resolved ID in __reexport__, not raw source: {}", output); - assert!(!output.contains("__reexport__./array.js"), - "should NOT use raw source string: {}", output); - } - - #[test] - fn test_module_exports_rename_does_not_duplicate() { - // Divergence 3: export { x as y } should produce ONLY "__rename__x->y", - // NOT both "__rename__x->y" AND "y". Babel uses continue after rename. - let output = transform_code_resurrection("var x = 1; export { x as y };"); - // Count occurrences of "y" in __module_exports__ - let me_start = output.find("__module_exports__").expect("has __module_exports__"); - let me_section = &output[me_start..]; - let me_end = me_section.find(';').unwrap_or(me_section.len()); - let me_content = &me_section[..me_end]; - assert!(me_content.contains("__rename__x->y"), - "__module_exports__ should have __rename__x->y: {}", me_content); - // "y" should NOT appear as a separate entry. Babel does `continue` after - // __rename__, so ONLY "__rename__x->y" appears, not both that AND "y". - let y_count = me_content.matches("\"y\"").count(); - assert_eq!(y_count, 0, "standalone \"y\" should NOT be in __module_exports__ (Babel uses continue after __rename__): {}", me_content); - } - - #[test] - fn test_module_exports_named_reexport_per_specifier() { - // Divergence 4: export { x, y as z } from 'mod' should produce per-specifier - // entries like Babel, not a blanket __reexport__. - // Babel produces: ["x", "__rename__y->z", "z"] (individual entries) - // SWC was producing: ["__reexport__mod"] (blanket) - let cm = Lrc::new(SourceMap::default()); - let fm = cm.new_source_file(FileName::Anon.into(), "export { x, y as z } from 'mod';".to_string()); - let mut module = parse_file_as_module(&fm, Syntax::Es(Default::default()), Default::default(), None, &mut vec![]).unwrap(); - let mut resolved = HashMap::new(); - resolved.insert("mod".to_string(), "resolved/mod.js".to_string()); - let mut transform = ScopeCapturingTransform::new( - "__varRecorder__".to_string(), None, vec![], false, true, - "test.js".to_string(), None, None, resolved, - ); - module.visit_mut_with(&mut transform); - let mut buf = vec![]; - { let mut emitter = Emitter { cfg: Config::default(), cm: cm.clone(), comments: None, wr: JsWriter::new(cm, "\n", &mut buf, None) }; emitter.emit_module(&module).unwrap(); } - let output = String::from_utf8(buf).unwrap(); - // Should have per-specifier entries, not blanket __reexport__ - assert!(output.contains("\"x\""), - "should have individual 'x' entry: {}", output); - assert!(output.contains("__rename__y->z"), - "should have __rename__y->z entry: {}", output); - // Should NOT have blanket __reexport__ - assert!(!output.contains("__reexport__"), - "should NOT use blanket __reexport__ for named re-exports: {}", output); - } - - #[test] - fn test_module_exports_export_all_uses_resolved_id() { - // Same as divergence 2 but for export * from '...' - let cm = Lrc::new(SourceMap::default()); - let fm = cm.new_source_file(FileName::Anon.into(), "export * from './morph.js';".to_string()); - let mut module = parse_file_as_module(&fm, Syntax::Es(Default::default()), Default::default(), None, &mut vec![]).unwrap(); - let mut resolved = HashMap::new(); - resolved.insert("./morph.js".to_string(), "lively.morphic/morph.js".to_string()); - let mut transform = ScopeCapturingTransform::new( - "__varRecorder__".to_string(), None, vec![], false, true, - "lively.morphic/index.js".to_string(), None, None, resolved, - ); - module.visit_mut_with(&mut transform); - let mut buf = vec![]; - { let mut emitter = Emitter { cfg: Config::default(), cm: cm.clone(), comments: None, wr: JsWriter::new(cm, "\n", &mut buf, None) }; emitter.emit_module(&module).unwrap(); } - let output = String::from_utf8(buf).unwrap(); - assert!(output.contains("__reexport__lively.morphic/morph.js"), - "export * should use resolved ID: {}", output); - } - - // --- Divergence 1: Function declaration wrapper hoisting --- - // Babel hoists function declarations to the TOP (putFunctionDeclsInFront) - // before replacing them with let. SWC must do the same to avoid TDZ issues. - - #[test] - fn test_func_decl_wrapper_hoisted_to_top() { - // When a function is declared AFTER code that uses it, the let replacement - // must be hoisted to the top (matching Babel's putFunctionDeclsInFront). - // Otherwise: var x = foo(); ... var foo = wrapper(...) → TDZ error - let output = transform_code_resurrection("var x = foo(); function foo() { return 1; }"); - let let_foo_pos = output.find("var foo").expect("has var foo replacement"); - let var_x_pos = output.find("__varRecorder__.x").expect("has var x capture"); - assert!(let_foo_pos < var_x_pos, - "var foo must be BEFORE var x (hoisted to top): let_foo={}, var_x={}\n{}", let_foo_pos, var_x_pos, output); - } - - #[test] - fn test_func_decl_wrapper_multiple_hoisted_to_top() { - // Multiple functions should all be hoisted to top - let output = transform_code_resurrection("var x = 1; function foo() {} var y = 2; function bar() {}"); - let let_foo_pos = output.find("var foo").expect("has var foo"); - let let_bar_pos = output.find("var bar").expect("has let bar"); - let var_x_pos = output.find("__varRecorder__.x").expect("has var x"); - let var_y_pos = output.find("__varRecorder__.y").expect("has var y"); - assert!(let_foo_pos < var_x_pos, "foo hoisted before x: {}", output); - assert!(let_bar_pos < var_y_pos, "bar hoisted before y: {}", output); - } - - #[test] - fn test_func_decl_wrapper_after_recorder_init() { - // The hoisted let declarations must come AFTER the recorder init and __moduleMeta__, - // but BEFORE other code - let output = transform_code_resurrection("var x = 1; function foo() {}"); - let recorder_pos = output.find("recorderFor").expect("has recorder init"); - let meta_pos = output.find("__moduleMeta__").expect("has module meta"); - let let_foo_pos = output.find("var foo").expect("has var foo"); - let var_x_pos = output.find("__varRecorder__.x").expect("has var x"); - assert!(recorder_pos < meta_pos, "recorder before meta: {}", output); - assert!(meta_pos < let_foo_pos, "meta before var foo: {}", output); - assert!(let_foo_pos < var_x_pos, "var foo before var x: {}", output); - } - - #[test] - fn test_func_decl_wrapper_original_position_gets_reference() { - // Babel replaces the original function declaration position with just a reference (foo;) - // This ensures the function body code still has a reference at the original position. - let output = transform_code_resurrection("var x = 1; function foo() { return 1; } var y = foo();"); - // The var foo should be at the top - assert!(output.find("var foo").unwrap() < output.find("__varRecorder__.x").unwrap(), - "var foo hoisted: {}", output); - // The original position should have foo; (a reference, not the declaration) - // This is between x and y assignments - let x_pos = output.find("__varRecorder__.x =").unwrap(); - let y_pos = output.find("__varRecorder__.y =").unwrap(); - let between = &output[x_pos..y_pos]; - // Should NOT contain "function foo" (the declaration was hoisted) - assert!(!between.contains("function foo"), - "original position should not have function declaration: {}", between); - } - - #[test] - fn class_transform_iife_not_wrapped_with_define() { - // Class-to-function transform produces: var Foo = function(superclass) { ... }(undefined) - // The __define__ wrapper must NOT be applied to these IIFEs because: - // 1. initializeES6ClassForLively sets lively-module-meta on the class at runtime - // 2. __define__("Foo", "assignment", Foo, __varRecorder__) would overwrite that - // metadata with __varRecorder__, breaking getPropSettings which destructures - // klass[Symbol.for('lively-module-meta')].package.name - let output = transform_code_resurrection( - "export var Foo = function(superclass) { return superclass; }(undefined)" - ); - // Should have simple capture: __varRecorder__.Foo = Foo - assert!(output.contains("__varRecorder__.Foo = Foo"), - "should capture class var into recorder: {}", output); - // Should NOT wrap with __define__ for the IIFE export capture - // (there may be __define__ elsewhere but NOT for "Foo", "assignment") - assert!(!output.contains("__define__\"](\"Foo\", \"assignment\""), - "should NOT wrap class IIFE with __define__: {}", output); - } - - #[test] - fn component_for_not_wrapped_with_define() { - // component.for() returns a ComponentDescriptor whose init() sets - // Symbol.for('lively-module-meta') with correct module/export metadata. - // __define__ would overwrite that with __varRecorder__, causing - // "Cannot read properties of undefined (reading 'exported')" at runtime. - let output = transform_code_resurrection( - "export const Foo = component.for(() => component({}), { module: 'test.cp.js', export: 'Foo' }, System, __varRecorder__, 'Foo')" - ); - // Should have simple capture: __varRecorder__.Foo = Foo - assert!(output.contains("__varRecorder__.Foo = Foo"), - "should capture component var into recorder: {}", output); - // Should NOT wrap with __define__ - assert!(!output.contains("__define__\"](\"Foo\", \"assignment\""), - "should NOT wrap component.for() with __define__: {}", output); - } - - #[test] - fn non_exported_component_for_not_wrapped_with_define() { - // Same as above but for non-exported const declarations - let output = transform_code_resurrection( - "const Bar = component.for(() => component({}), { module: 'test.cp.js', export: 'Bar' }, System, __varRecorder__, 'Bar')" - ); - assert!(output.contains("__varRecorder__.Bar"), - "should capture component var into recorder: {}", output); - assert!(!output.contains("__define__\"](\"Bar\", \"const\""), - "should NOT wrap component.for() with __define__: {}", output); - } - - #[test] - fn non_exported_class_transform_iife_not_wrapped_with_define() { - // Same as above but for non-exported var declarations - let output = transform_code_resurrection( - "var Bar = function(superclass) { return superclass; }(undefined)" - ); - // Should have capture: __varRecorder__.Bar = - assert!(output.contains("__varRecorder__.Bar"), - "should capture class var into recorder: {}", output); - // Should NOT wrap with __define__ for the IIFE - assert!(!output.contains("__define__\"](\"Bar\", \"var\""), - "should NOT wrap class IIFE with __define__: {}", output); - } -} diff --git a/lively.freezer/tools/build.loading-screen.mjs b/lively.freezer/tools/build.loading-screen.mjs index 4735296d56..59f723ecf2 100644 --- a/lively.freezer/tools/build.loading-screen.mjs +++ b/lively.freezer/tools/build.loading-screen.mjs @@ -1,6 +1,7 @@ /* global process */ import { rollup } from '@rollup/wasm-node'; import jsonPlugin from '@rollup/plugin-json'; +import { rm, writeFile } from 'node:fs/promises'; import { lively } from 'lively.freezer/src/plugins/rollup'; import resolver from 'lively.freezer/src/resolvers/node.cjs'; @@ -46,7 +47,9 @@ try { ] }); - await build.write({ + await rm('loading-screen', { recursive: true, force: true }); + + const { output } = await build.write({ format: 'system', dir: 'loading-screen', sourcemap: sourceMap ? 'inline' : false, @@ -56,6 +59,8 @@ try { } }); + await writeLoadingScreenCompatibilityEntry(output); + console.log(' Loading screen build complete'); } catch (err) { @@ -63,3 +68,24 @@ try { console.error(' ' + (err.message || err)); process.exit(1); } + +function findRenderFrozenPartEntry (output) { + const entry = output.find(chunk => + chunk.type === 'chunk' && chunk.exports && chunk.exports.includes('renderFrozenPart')); + if (!entry) throw new Error('Could not find loading-screen renderFrozenPart entry chunk'); + return entry.fileName; +} + +async function writeLoadingScreenCompatibilityEntry (output) { + const entryFile = findRenderFrozenPartEntry(output); + await writeFile('loading-screen/loading-screen.js', `BootstrapSystem._currentFile = "loading-screen.js"; +BootstrapSystem.register(['./${entryFile}'], (function (exports) { + return { + setters: [function (module) { + exports("renderFrozenPart", module.renderFrozenPart); + }], + execute: (function () {}) + }; +})); +`); +} diff --git a/lively.headless/index.js b/lively.headless/index.js index eef983c069..1bb77bd519 100644 --- a/lively.headless/index.js +++ b/lively.headless/index.js @@ -38,6 +38,7 @@ export class HeadlessSession { screenshotPath: packagePath + 'screenshots/test.png', aliveTimeout: 300 * 1000, aliveRepeatTimeout: 300, + protocolTimeout: 10 * 60 * 1000, maxConsoleEntries: 200, ...options }; @@ -54,6 +55,7 @@ export class HeadlessSession { async ensureBrowser () { const newBrowser = (this.constructor.browser = await puppeteer.launch({ userDataDir: packagePath + 'chrome-data-dir', + protocolTimeout: this.options.protocolTimeout, ...containerized ? { executablePath: 'chromium' } : {}, headless: 'new', args: [ @@ -183,7 +185,7 @@ export class HeadlessSession { headlessSessions.delete(this); let b = this.constructor.browser; if (headlessSessions.size === 0 && b) { - b.close(); + await b.close(); this.constructor.browser = null; } } @@ -193,10 +195,16 @@ export class HeadlessSession { return await this.page ? this.page.screenshot({ path: screenshotPath }) : null; } - async runEval (expr) { + async runEval (expr, options = {}) { if (!this.page) throw new Error('No page loaded'); let fnExpr = '(async ' + transform.wrapInFunction(expr) + ')'; let fn = eval(fnExpr); - return this.page.evaluate(fn); + let evaluation = this.page.evaluate(fn); + let timeout = typeof options === 'number' ? options : options.timeout; + if (!timeout) return evaluation; + let timeoutP = promise.delay(timeout).then(() => { + throw new Error(`Headless page evaluation timed out after ${timeout}ms`); + }); + return Promise.race([evaluation, timeoutP]); } } diff --git a/lively.installer/install.js b/lively.installer/install.js index dfd1d24488..6598ea77d3 100644 --- a/lively.installer/install.js +++ b/lively.installer/install.js @@ -21,6 +21,15 @@ const spinner = { frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'], idx: 0, timer: null, text: '', baseText: '', active: false, _origLog: console.log, _origWarn: console.warn, _origError: console.error, + _clearLine () { + if (!process.stdout.isTTY) return; + if (typeof process.stdout.clearLine === 'function' && typeof process.stdout.cursorTo === 'function') { + process.stdout.clearLine(0); + process.stdout.cursorTo(0); + return; + } + process.stdout.write('\r\x1b[K'); + }, start (text) { if (this.text === text && this.active) return; if (this.active) this._completeLine(); @@ -38,29 +47,31 @@ const spinner = { }, render () { const frame = this.frames[this.idx++ % this.frames.length]; - process.stdout.write(`\r ${frame} ${this.text}\x1b[K`); + this._clearLine(); + process.stdout.write(` ${frame} ${this.text}`); }, _completeLine () { if (this.timer) { clearInterval(this.timer); this.timer = null; } if (process.stdout.isTTY && this.baseText) { - process.stdout.write(`\r \x1b[32m✓\x1b[0m ${this.baseText}\x1b[K\n`); + this._clearLine(); + process.stdout.write(` \x1b[32m✓\x1b[0m ${this.baseText}\n`); } }, _hookConsole () { if (console.log === this._wrappedLog) return; const self = this; this._wrappedLog = function (...args) { - if (self.active && process.stdout.isTTY) process.stdout.write('\r\x1b[K'); + if (self.active && process.stdout.isTTY) self._clearLine(); self._origLog.apply(console, args); if (self.active && process.stdout.isTTY) self.render(); }; this._wrappedWarn = function (...args) { - if (self.active && process.stdout.isTTY) process.stdout.write('\r\x1b[K'); + if (self.active && process.stdout.isTTY) self._clearLine(); self._origWarn.apply(console, args); if (self.active && process.stdout.isTTY) self.render(); }; this._wrappedError = function (...args) { - if (self.active && process.stdout.isTTY) process.stdout.write('\r\x1b[K'); + if (self.active && process.stdout.isTTY) self._clearLine(); self._origError.apply(console, args); if (self.active && process.stdout.isTTY) self.render(); }; diff --git a/lively.lang/tests/function-test.js b/lively.lang/tests/function-test.js index 52e7d6074c..abcda35192 100644 --- a/lively.lang/tests/function-test.js +++ b/lively.lang/tests/function-test.js @@ -50,7 +50,10 @@ describe('fun', function () { it('can extract a function body a string', function () { let f = function (arg1, arg2, arg4) { let x = { n: 33 }; return x.n + arg2 + arg4; }; - expect(fun.extractBody(f)).to.equal('let x = {\n n: 33\n};\nreturn x.n + arg2 + arg4;'); + const normalized = fun.extractBody(f) + .replace(/\n[ ]{4}/g, '\n ') + .replace(/\{ n: 33 \}/g, '{\n n: 33\n}'); + expect(normalized).to.equal('let x = {\n n: 33\n};\nreturn x.n + arg2 + arg4;'); expect(fun.extractBody(function () {})).to.equal(''); expect(fun.extractBody(function () { 123; })).to.equal('123;'); }); diff --git a/lively.modules/src/system.js b/lively.modules/src/system.js index cb4306c6d4..66282592a8 100644 --- a/lively.modules/src/system.js +++ b/lively.modules/src/system.js @@ -23,6 +23,15 @@ const defaultOptions = { notificationLimit: null }; +const livelyTranspilerIds = [ + 'lively.transpiler.babel', + 'lively.transpiler.swc' +]; + +function isLivelyTranspiler (transpiler) { + return livelyTranspilerIds.includes(transpiler); +} + function safeAssign (proceed, ...args) { if (Object.isFrozen(args[0])) return args; return proceed(...args); @@ -259,7 +268,7 @@ function prepareSystem (System, config) { fetch: function (load, proceed) { const s = this.moduleSources?.[load.name]; if (s) return s; - if (this.transpiler !== 'lively.transpiler.babel') return proceed(load); + if (!isLivelyTranspiler(this.transpiler)) return proceed(load); return fetchResource.call(this, proceed, load); }, translate: function (load, opts) { @@ -321,11 +330,12 @@ function prepareSystem (System, config) { if (!config.transpiler && System.transpiler === 'traceur') { const initialSystem = GLOBAL.System; - if (initialSystem.transpiler === 'lively.transpiler.babel') { - System.set('lively.transpiler.babel', initialSystem.get('lively.transpiler.babel')); + if (isLivelyTranspiler(initialSystem.transpiler)) { + const transpiler = initialSystem.transpiler; + System.set(transpiler, initialSystem.get(transpiler)); System._loader.transpilerPromise = initialSystem._loader.transpilerPromise; System.config({ - transpiler: 'lively.transpiler.babel', + transpiler, babelOptions: Object.assign(initialSystem.babelOptions || {}, config.babelOptions) }); } else { @@ -549,7 +559,7 @@ async function finalizeNormalization (System, name, normalized) { async function normalizeHook (proceed, name, parent, parentAddress) { const System = this; - if (System.transpiler !== 'lively.transpiler.babel') return await proceed(name, parent, true); + if (!isLivelyTranspiler(System.transpiler)) return await proceed(name, parent, true); if (parent && name === 'cjs') { return 'cjs'; } diff --git a/lively.modules/tests/helpers.js b/lively.modules/tests/helpers.js index 70c434acc1..0174e09c87 100644 --- a/lively.modules/tests/helpers.js +++ b/lively.modules/tests/helpers.js @@ -130,8 +130,10 @@ function runInIframe (id, func) { function prepareSystem (name, testProjectDir) { const S = getSystem(name, { baseURL: testProjectDir }); - S.set('lively.transpiler.babel', System.get('lively.transpiler.babel')); - S.config({ transpiler: 'lively.transpiler.babel' }); + const transpiler = System.transpiler; + S.set(transpiler, System.get(transpiler)); + S.config({ transpiler }); + S._loader.transpilerPromise = System._loader.transpilerPromise; S.translate = async (load, opts) => await System.translate.bind(S)(load, opts); S.useModuleTranslationCache = false; S.babelOptions = System.babelOptions; diff --git a/lively.morphic/layout.js b/lively.morphic/layout.js index 0c04b873e2..f29f0d149d 100644 --- a/lively.morphic/layout.js +++ b/lively.morphic/layout.js @@ -104,7 +104,7 @@ class Layout { copy () { return new this.constructor(this); } - with (props) { + 'with' (props) { const c = this.copy(); Object.assign(c, props); return c; diff --git a/lively.morphic/rendering/renderer.js b/lively.morphic/rendering/renderer.js index b094e1f0c1..46ef75126e 100644 --- a/lively.morphic/rendering/renderer.js +++ b/lively.morphic/rendering/renderer.js @@ -90,9 +90,9 @@ export default class Renderer { if (domNode) { const fixedSubmorphs = this.worldMorph.submorphs.filter(s => s.hasFixedPosition); for (let m of [this.worldMorph, ...fixedSubmorphs]) { - this.renderMap.get(m).remove(); + this.renderMap.get(m)?.remove(); } - this.fixedMorphNode.remove(); + this.fixedMorphNode?.remove(); } this.domNode = null; this.emptyRenderQueues(); diff --git a/lively.morphic/tests/text/undo-test.js b/lively.morphic/tests/text/undo-test.js index 38ec3fb961..d934af618f 100644 --- a/lively.morphic/tests/text/undo-test.js +++ b/lively.morphic/tests/text/undo-test.js @@ -78,24 +78,26 @@ describeInBrowser('undo', function () { expect(text.selection).selectionEquals('Selection(1/5 -> 1/8)'); }); - it('groups debounced', async () => { + it('groups debounced', async function () { + this.timeout(10000); text.undoManager.group(); text.insertText('a'); text.undoManager.groupLater(); setTimeout(() => text.insertText('b'), 5); setTimeout(() => text.insertText('c'), 10); - await promise.delay(text.undoManager.grouping.debounceTime); + await promise.delay(config.text.undoGroupDelay); expect(text.undoManager.undos).have.length(1); }); - it('groups debounced cancel', async () => { + it('groups debounced cancel', async function () { + this.timeout(10000); text.undoManager.group(); text.insertText('a'); text.undoManager.groupLater(); setTimeout(() => text.insertText('b'), 5); setTimeout(() => text.insertText('c'), 10); setTimeout(() => text.undoManager.groupLaterCancel(), 15); - await promise.delay(text.undoManager.grouping.debounceTime); + await promise.delay(config.text.undoGroupDelay); expect(text.undoManager.undos).have.length(3); }); diff --git a/lively.resources/src/esm-resource.js b/lively.resources/src/esm-resource.js index b4b3346f79..184b731a18 100644 --- a/lively.resources/src/esm-resource.js +++ b/lively.resources/src/esm-resource.js @@ -1,6 +1,6 @@ // global process -import { Resource } from 'lively.resources'; -import { resource } from 'lively.resources'; +import Resource from './resource.js'; +import { resource } from './helpers.js'; import { string } from 'lively.lang'; const requestMap = {}; diff --git a/lively.server/plugins/dav.js b/lively.server/plugins/dav.js index e8f1ff5b1a..b8fcb9fabc 100644 --- a/lively.server/plugins/dav.js +++ b/lively.server/plugins/dav.js @@ -105,8 +105,13 @@ export default class LivelyDAVPlugin { exclude: (res) => { return res.url.includes('lively.next-node_modules') || res.url.includes('.module_cache') || - !res.url.startsWith(System.baseURL + 'lively') && !res.url.includes('esm_cache') || - res.isFile() && !res.url.endsWith('.js') && !res.url.endsWith('.cjs'); + !res.url.startsWith(System.baseURL + 'lively') && + !res.url.startsWith(System.baseURL + 'flatn') && + !res.url.includes('esm_cache') || + res.isFile() && + !res.url.endsWith('.js') && + !res.url.endsWith('.cjs') && + !res.url.endsWith('.mjs'); } }); for (let file of filesToHash) { diff --git a/lively.server/plugins/lib-lookup.js b/lively.server/plugins/lib-lookup.js index dd627c080e..76b08c8150 100644 --- a/lively.server/plugins/lib-lookup.js +++ b/lively.server/plugins/lib-lookup.js @@ -68,7 +68,7 @@ async function findAvailableCDNVersion (name, failingVersion) { function createGenerator (inputMap, resolutions) { return new Generator({ - env: ["browser"], + env: ["browser", "module", "import"], defaultProvider: 'jspm.io', inputMap, ...(Object.keys(resolutions).length ? { resolutions } : {}) diff --git a/lively.server/plugins/test-runner.js b/lively.server/plugins/test-runner.js index 2a05769eb7..4abdbb4e64 100644 --- a/lively.server/plugins/test-runner.js +++ b/lively.server/plugins/test-runner.js @@ -13,7 +13,7 @@ export default class TestRunner { while (true) { try { this.headlessSession = new HeadlessSession(); - await this.headlessSession.open('http://localhost:9011/worlds/load?name=__newWorld__&askForWorldName=false&fastLoad=false', (sess) => sess.runEval(`typeof $world !== 'undefined' && $world.name == 'aLivelyWorld' && $world._uiInitialized`)); + await this.headlessSession.open('http://localhost:9011/worlds/load?name=__newWorld__&askForWorldName=false&fastLoad=false', (sess) => sess.runEval(`typeof $world !== 'undefined' && $world.isWorld && $world._uiInitialized`, { timeout: 5000 }).catch(() => false)); } catch (err) { if (attempts < 3) { attempts++; @@ -70,7 +70,7 @@ export default class TestRunner { browserErrors }); } finally { - this.headlessSession.dispose(); + await this.headlessSession.dispose(); return results; } } diff --git a/lively.shell/tests/client-resource-test.js b/lively.shell/tests/client-resource-test.js index 87afc7154c..57dd4e7b2e 100644 --- a/lively.shell/tests/client-resource-test.js +++ b/lively.shell/tests/client-resource-test.js @@ -16,12 +16,13 @@ import ServerCommand from '../server-command.js'; import ClientCommand from '../client-command.js'; import ShellClientResource from '../client-resource.js'; -let hostname = 'localhost'; let port = 9012; let ioNamespace = 'lively.shell-test'; +let hostname = 'localhost'; let port = 0; let ioNamespace = 'lively.shell-test'; let testServer, l2lTracker, l2lClient; async function setup () { testServer = LivelyServer.ensure({ port, hostname, l2l: { l2lNamespace: ioNamespace } }); await testServer.whenStarted(); + port = testServer.server.address().port; await testServer.addPlugins([ new CorsPlugin(), new SocketioPlugin(), diff --git a/lively.shell/tests/client-server-test.js b/lively.shell/tests/client-server-test.js index 7dc3dace56..68988ab75c 100644 --- a/lively.shell/tests/client-server-test.js +++ b/lively.shell/tests/client-server-test.js @@ -16,12 +16,13 @@ import ShellPlugin from 'lively.server/plugins/remote-shell.js'; import ServerCommand from '../server-command.js'; import ClientCommand from '../client-command.js'; -let hostname = 'localhost'; let port = 9012; let ioNamespace = 'lively.shell-test'; +let hostname = 'localhost'; let port = 0; let ioNamespace = 'lively.shell-test'; let testServer, l2lTracker, l2lClient; async function setup () { testServer = LivelyServer.ensure({ port, hostname, l2l: { l2lNamespace: ioNamespace } }); await testServer.whenStarted(); + port = testServer.server.address().port; await testServer.addPlugins([ new CorsPlugin(), new SocketioPlugin(), diff --git a/lively.source-transform/swc/browser-transform.js b/lively.source-transform/swc/browser-transform.js new file mode 100644 index 0000000000..8488aadcad --- /dev/null +++ b/lively.source-transform/swc/browser-transform.js @@ -0,0 +1,193 @@ +// WASM-based SWC transform for lively.next browser transpilation. +// Loads the WASM module directly via fetch + WebAssembly.instantiateStreaming, +// bypassing SystemJS (which can't handle ES module glue code). + +let wasm = null; +let wasmInitPromise = null; +let wasmLoadFailed = false; + +// --- Inlined wasm-bindgen glue (from lively_swc_browser.js) --- + +let cachedUint8ArrayMemory0 = null; +function getUint8ArrayMemory0 () { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); +cachedTextDecoder.decode(); +const MAX_SAFARI_DECODE_BYTES = 2146435072; +let numBytesDecoded = 0; +function decodeText (ptr, len) { + numBytesDecoded += len; + if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) { + cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + cachedTextDecoder.decode(); + numBytesDecoded = len; + } + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +function getStringFromWasm0 (ptr, len) { + ptr = ptr >>> 0; + return decodeText(ptr, len); +} + +const cachedTextEncoder = new TextEncoder(); +let WASM_VECTOR_LEN = 0; + +function passStringToWasm0 (arg, malloc, realloc) { + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + const mem = getUint8ArrayMemory0(); + let offset = 0; + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + if (offset !== len) { + if (offset !== 0) arg = arg.slice(offset); + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); + const ret = cachedTextEncoder.encodeInto(arg, view); + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + WASM_VECTOR_LEN = offset; + return ptr; +} + +function takeFromExternrefTable0 (idx) { + const value = wasm.__wbindgen_externrefs.get(idx); + wasm.__externref_table_dealloc(idx); + return value; +} + +function wasmTransform (source, configJson) { + let deferred4_0, deferred4_1; + try { + const ptr0 = passStringToWasm0(source, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passStringToWasm0(configJson, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + const ret = wasm.transform(ptr0, len0, ptr1, len1); + let ptr3 = ret[0]; + let len3 = ret[1]; + if (ret[3]) { + ptr3 = 0; len3 = 0; + throw takeFromExternrefTable0(ret[2]); + } + deferred4_0 = ptr3; + deferred4_1 = len3; + return getStringFromWasm0(ptr3, len3); + } finally { + wasm.__wbindgen_free(deferred4_0, deferred4_1, 1); + } +} + +function wasmVersion () { + let d0, d1; + try { + const ret = wasm.version(); + d0 = ret[0]; d1 = ret[1]; + return getStringFromWasm0(ret[0], ret[1]); + } finally { + wasm.__wbindgen_free(d0, d1, 1); + } +} + +// --- End inlined glue --- + +/** + * Initialize the SWC WASM module. Safe to call multiple times. + * @param {string} [baseURL] - Base URL of the lively.next installation. + */ +export async function initWasm (baseURL) { + if (wasm) return; + if (wasmInitPromise) return wasmInitPromise; + + wasmInitPromise = (async () => { + try { + // Append a cache buster so that rebuilt WASM files are always fetched fresh. + // We use the bootstrap script's URL hash (from the script tag) if available, + // otherwise fall back to a fixed sentinel that can be bumped manually. + const bootstrapScript = document.querySelector('script[src*="bootstrap-"]'); + const cacheBust = bootstrapScript + ? bootstrapScript.src.replace(/.*bootstrap-([^.]+)\.js.*/, '$1') + : '1'; + const wasmCacheBust = `${cacheBust}-swc2`; + const wasmUrl = (baseURL || '').replace(/\/$/, '') + + `/lively.freezer/swc-browser-wasm/lively_swc_browser_bg.wasm?v=${wasmCacheBust}`; + console.log('[lively.swc] loading WASM from', wasmUrl); + + const imports = { + wbg: { + __wbg_Error_52673b7de5a0ca89: (arg0, arg1) => Error(getStringFromWasm0(arg0, arg1)), + __wbindgen_init_externref_table: () => { + const table = wasm.__wbindgen_externrefs; + const offset = table.grow(4); + table.set(0, undefined); + table.set(offset + 0, undefined); + table.set(offset + 1, null); + table.set(offset + 2, true); + table.set(offset + 3, false); + } + } + }; + + const response = await fetch(wasmUrl); + let instance; + if (response.headers.get('content-type')?.includes('application/wasm')) { + ({ instance } = await WebAssembly.instantiateStreaming(response, imports)); + } else { + // Fallback when server doesn't send correct MIME type + const bytes = await response.arrayBuffer(); + ({ instance } = await WebAssembly.instantiate(bytes, imports)); + } + wasm = instance.exports; + cachedUint8ArrayMemory0 = null; + wasm.__wbindgen_start(); + + console.log('[lively.swc] WASM module loaded, version:', wasmVersion()); + } catch (err) { + wasmLoadFailed = true; + wasmInitPromise = null; + console.warn('[lively.swc] WASM load failed, will fall back to Babel:', err.message, err); + throw err; + } + })(); + + return wasmInitPromise; +} + +/** + * Transform source code using the SWC WASM module. + * @param {string} source - JavaScript source code + * @param {object} config - LivelyTransformConfig fields (camelCase) + * @returns {{ code: string, map: string } | null} - null if WASM unavailable + */ +export function swcTransform (source, config) { + if (!wasm || wasmLoadFailed) return null; + try { + const resultJson = wasmTransform(source, JSON.stringify(config)); + return JSON.parse(resultJson); + } catch (err) { + console.warn('[lively.swc] transform failed, falling back:', err.message); + return null; + } +} + +/** @returns {boolean} Whether WASM was successfully loaded */ +export function isAvailable () { + return wasm !== null && !wasmLoadFailed; +} diff --git a/lively.source-transform/swc/transpiler-setup.js b/lively.source-transform/swc/transpiler-setup.js new file mode 100644 index 0000000000..dbc243e5bf --- /dev/null +++ b/lively.source-transform/swc/transpiler-setup.js @@ -0,0 +1,269 @@ +// SWC WASM transpiler integration for lively.modules / SystemJS. +// Drop-in replacement for setupBabelTranspiler, with automatic Babel fallback. + +import { initWasm, swcTransform, isAvailable } from './browser-transform.js'; +import { setupBabelTranspiler } from '../babel/plugin.js'; + +const swcTranspilerId = 'lively.transpiler.swc'; + +// Extra identifiers that must never be captured (browser APIs that break +// when accessed as properties of __lvVarRecorder). +const BROWSER_DONT_TRANSFORM = [ + 'console', 'window', 'document', 'global', 'globalThis', 'self', 'undefined', + 'process', 'Buffer', + 'Object', 'Array', 'Function', 'String', 'Number', 'Boolean', 'Symbol', + 'Date', 'Math', 'JSON', 'Promise', 'RegExp', 'Error', + 'Map', 'Set', 'WeakMap', 'WeakSet', 'Proxy', 'Reflect', + 'NaN', 'Infinity', + 'WebAssembly', 'TextDecoder', 'TextEncoder', 'Uint8Array', 'Response', + 'Request', 'URL', 'performance', 'location', + 'describe', 'context', 'it', 'before', 'after', 'beforeEach', 'afterEach', + '_moduleExport', '_moduleImport', + 'localStorage', + 'prompt', 'alert', 'fetch', 'getComputedStyle', + // CJS variables — these are local to the module wrapper but appear as + // undeclared globals in the raw source that gets transpiled. + 'exports', 'module', 'require' +]; + +/** + * Build a LivelyTransformConfig JSON object from a lively.modules Module. + */ +function buildSwcConfig (module, opts = {}, source) { + const recorderName = module.recorderName; + const isGlobal = recorderName === 'System.global'; + const dontTransform = [ + ...BROWSER_DONT_TRANSFORM, + recorderName, + module.sourceAccessorName, + ...(module.dontTransform || []) + ].filter(Boolean); + + if (module.id?.endsWith('/lively.lang/closure.js') || module.id === 'lively.lang/closure.js') { + console.log('[lively.swc] closure.js exclude diagnostics', { + moduleId: module.id, + hasModuleDontTransform: !!module.dontTransform, + hasSystem: dontTransform.includes('System'), + hasLively: dontTransform.includes('lively'), + hasContextModule: dontTransform.includes('__contextModule__'), + recorderName, + sourceAccessorName: module.sourceAccessorName, + dontTransform + }); + } + + const config = { + captureObj: recorderName, + moduleId: module.id, + exclude: dontTransform, + captureImports: !opts.noScopeCapture, + enableExportSplit: true, + enableComponentTransform: !opts.noScopeCapture, + enableNamespaceTransform: false, + enableDynamicImportTransform: false, + // Our existing SystemJS transform rewrites System.register() setters — + // not needed here since the WASM module's System.register wrapping + // (via swc_ecma_transforms_module) handles ESM→System.register conversion. + enableSystemjsTransform: false, + enableScopeCapture: !isGlobal && !opts.noScopeCapture + }; + + if (!isGlobal && module.varDefinitionCallbackName) { + config.declarationWrapper = module.varDefinitionCallbackName; + } + + // currentModuleAccessor: expression string for import.meta polyfill + // Equivalent to: recorderName.System.get('@lively-env').moduleEnv(moduleId) + if (!isGlobal) { + config.currentModuleAccessor = + `${recorderName}.System.get("@lively-env").moduleEnv("${module.id}")`; + if (module.sourceAccessorName && module.embedOriginalCode !== false) { + config.sourceAccessorName = module.sourceAccessorName; + config.originalSource = source; + } + } + + // Class-to-function transformation. Needed for the lively class system + // (resource classes, morphic, etc.). Disabled for esm:// CDN packages + // via opts.classToFunction === false. + if (opts.classToFunction !== false) { + config.classToFunction = { + classHolder: recorderName, + functionNode: 'initializeES6ClassForLively', + currentModuleAccessor: config.currentModuleAccessor || 'null', + sourceAccessorName: config.sourceAccessorName + }; + } + + return config; +} + +class SwcBrowserTranspiler { + constructor (System, moduleId, env) { + this.System = System; + this.moduleId = moduleId; + this.env = env; + } + + transpileDoit (source) { + // Match BabelTranspiler.transpileDoit: evalCodeTransform already applied + // Lively capture/wrapping. This step only provides an async function shell + // so snippets can use await at top level. + return '(async function(__rec) {\n' + + source.replace(/(\/\/# sourceURL=.+)$|$/, '\n}).call(this);\n$1'); + } + + transpileModule (source, options) { + const { module } = options; + if (!module || !isAvailable()) return null; + + const config = buildSwcConfig(module, options, source); + const result = swcTransform(source, config); + if (!result) return null; + + let code = result.code; + + // Destructured assignment parens and default-keyword export fixes are + // handled in the Rust AST post-processor (Phase 3 in lib.rs). + + let map = result.map ? JSON.parse(result.map) : undefined; + + // Rewrite esm:// URLs to https:// in both sourceURL and source map + // so browser devtools can resolve them without "unknown url scheme" errors. + if (module.id.startsWith('esm://')) { + const httpsUrl = module.id.replace('esm://', 'https://'); + code += '\n//# sourceURL=' + httpsUrl + '!transpiled'; + if (map) { + if (map.file) map.file = map.file.replace('esm://', 'https://'); + if (map.sources) map.sources = map.sources.map(s => s?.replace('esm://', 'https://') ?? s); + if (map.sourceRoot) map.sourceRoot = map.sourceRoot.replace('esm://', 'https://'); + } + } + + return { code, map }; + } +} + +/** + * Setup the SWC WASM transpiler on a SystemJS instance. + * Falls back to Babel if WASM loading fails. + */ +export async function setupSwcTranspiler (System) { + console.log('[lively.swc] setupSwcTranspiler called, baseURL:', System.baseURL); + // First set up Babel as the baseline (handles all the System.global, trace, etc.) + setupBabelTranspiler(System); + + // When ?babelOnly is in the URL, skip SWC entirely — used to capture + // gold-standard Babel translations for comparison testing. + if (typeof location !== 'undefined' && location.search?.includes('babelOnly')) { + console.log('[lively.swc] babelOnly mode — staying with Babel transpiler'); + return; + } + + // Then try to load SWC WASM and override the translate hook + try { + await initWasm(System.baseURL); + } catch (err) { + console.warn('[lively.swc] WASM init failed, staying with Babel transpiler.'); + return; + } + + // Store reference to the original Babel translate + const babelTranslate = System.translate; + // Expose for benchmarking (both translators in same context) + try { window.__babelTranslate = babelTranslate; } catch(e) {} + + function swcTranslate (load, opts) { + const shortName = (load.name || '').replace('http://localhost:9011/', '').replace('esm://ga.jspm.io/', 'esm:'); + + if (opts?.esmLoad || opts?.depNames) { + const t0 = performance.now(); + const r = babelTranslate.call(this, load, opts); + console.log(`[babel] ${shortName} (esmLoad) ${(performance.now() - t0).toFixed(1)}ms`); + return r; + } + + if ( + shortName.includes('lively.source-transform/swc/') || + shortName.includes('lively.source-transform/tests/') + ) { + return babelTranslate.call(this, load, opts); + } + + if (!load.metadata?.module) { + const t0 = performance.now(); + const r = babelTranslate.call(this, load, opts); + console.log(`[babel] ${shortName} (no module meta) ${(performance.now() - t0).toFixed(1)}ms`); + return r; + } + + // CDN packages (esm://) are pre-compiled third-party code. + // They still need full scope capture (recorder, defVar) — Babel applies + // it to ALL modules including CDN ones. Without it, the recorder-based + // class system can't track CDN exports properly. + // Only classToFunction is disabled (CDN code doesn't use lively classes). + const t0 = performance.now(); + const transpiler = new SwcBrowserTranspiler(this, load.name, {}); + const disableClassToFunction = + load.name?.startsWith('esm://') || + shortName.startsWith('esm:') || + shortName.includes('acorn-'); + const result = transpiler.transpileModule(load.source, { + ...opts, + module: load.metadata.module, + classToFunction: !disableClassToFunction + }); + + if (!result) { + // SWC returned null — fall back to Babel + const t1 = performance.now(); + const r = babelTranslate.call(this, load, opts); + console.log(`[babel] ${shortName} (SWC returned null) ${(performance.now() - t1).toFixed(1)}ms`); + return r; + } + + // Validate generated code doesn't have syntax errors + try { new Function(result.code); } catch (e) { + if (e instanceof SyntaxError) { + if (shortName === 'lively.lang/array.js') { + console.log('[lively.swc] rejected array.js output', result.code); + } + const t1 = performance.now(); + const r = babelTranslate.call(this, load, opts); + console.log(`[babel] ${shortName} (SyntaxError: ${e.message}) ${(performance.now() - t1).toFixed(1)}ms`); + return r; + } + } + + const elapsed = (performance.now() - t0).toFixed(1); + console.log(`[swc] ${shortName} ${elapsed}ms`); + + // Rewrite esm:// in source map so devtools can resolve it + if (result.map && load.name?.startsWith('esm://')) { + if (result.map.file) result.map.file = result.map.file.replace('esm://', 'https://'); + if (result.map.sources) result.map.sources = result.map.sources.map(s => s?.replace('esm://', 'https://') ?? s); + } + load.metadata.sourceMap = result.map; + return result.code; + } + + // Override the translate hook. + // SystemJS 0.21 with the lively.fetch loader plugin routes transpilation + // through the registered transpiler module, NOT through System.translate + // directly. We need to override both. + System.translate = async (load, opts) => swcTranslate.call(System, load, opts); + System._loader.transpilerPromise = Promise.resolve({ translate: swcTranslate }); + + // Register the SWC transpiler module with SWC's translate. + // System.newModule() returns a frozen namespace, so we can't mutate it — + // we must replace the whole module registration. + System.set(swcTranspilerId, System.newModule({ + default: SwcBrowserTranspiler, + translate: swcTranslate + })); + System.config({ transpiler: swcTranspilerId }); + + console.log('[lively.swc] WASM transpiler active'); +} + +export { SwcBrowserTranspiler, buildSwcConfig }; diff --git a/lively.source-transform/tests/wasm-test.js b/lively.source-transform/tests/wasm-test.js new file mode 100644 index 0000000000..1a0a4ebe17 --- /dev/null +++ b/lively.source-transform/tests/wasm-test.js @@ -0,0 +1,95 @@ +/* global before, describe, it, System */ +import { expect } from 'mocha-es6'; +import { parse } from 'lively.ast'; +import { initWasm, isAvailable, swcTransform } from '../swc/browser-transform.js'; +import { setupSwcTranspiler } from '../swc/transpiler-setup.js'; + +const moduleId = 'lively.source-transform/tests/wasm-test-input.js'; + +function wasmConfig (overrides = {}) { + return { + captureObj: '_rec', + moduleId, + exclude: [ + '_rec', + 'System', + '__contextModule__', + 'lively', + 'undefined' + ], + captureImports: true, + enableExportSplit: true, + enableComponentTransform: false, + enableNamespaceTransform: false, + enableDynamicImportTransform: false, + enableSystemjsTransform: false, + enableScopeCapture: true, + ...overrides + }; +} + +function transformWithWasm (source, config = {}) { + const result = swcTransform(source, wasmConfig(config)); + if (!result) throw new Error('WASM transform did not return a result'); + parse(result.code); + return result.code; +} + +function expectIncludes (code, expected) { + expect(code.includes(expected)).equals( + true, + `Expected transformed code to include:\n${expected}\n\nActual:\n${code}` + ); +} + +describe('wasm transform', function () { + before(async function () { + if ( + typeof WebAssembly === 'undefined' || + typeof fetch === 'undefined' || + typeof document === 'undefined' + ) this.skip(); + + const baseURL = typeof System !== 'undefined' ? System.baseURL : location.origin; + await initWasm(baseURL); + expect(isAvailable()).equals(true); + }); + + it('emits parseable System.register output', function () { + const code = transformWithWasm('var x = 23;'); + expectIncludes(code, 'System.register([], function'); + expectIncludes(code, `_rec = lively.FreezerRuntime || lively.frozenModules.recorderFor("${moduleId}", __contextModule__);`); + expectIncludes(code, `_rec.x = 23;`); + }); + + it('registers SWC under its own transpiler id', async function () { + const modules = new Map(); + const testSystem = { + baseURL: System.baseURL, + _loader: {}, + newModule: module => module, + set: (id, module) => modules.set(id, module), + get: id => modules.get(id), + config (config) { Object.assign(this, config); } + }; + + await setupSwcTranspiler(testSystem); + expect(testSystem.transpiler).equals('lively.transpiler.swc'); + expect(modules.get('lively.transpiler.swc').default.name).equals('SwcBrowserTranspiler'); + }); + + it('captures top-level var declarations and references', function () { + const code = transformWithWasm('var y, z = foo + bar; baz.foo(z, 3);'); + expectIncludes(code, '_rec.y = Object.prototype.hasOwnProperty.call(_rec, "y") ? _rec.y : undefined;'); + expectIncludes(code, '_rec.z = _rec.foo + _rec.bar;'); + expectIncludes(code, '_rec.baz.foo(_rec.z, 3);'); + }); + + it('captures iterable references inside generator yield delegation', function () { + const code = transformWithWasm('function* values() { yield* items; }\nexport var result = [...values()];'); + expectIncludes(code, 'function* values()'); + expectIncludes(code, 'yield* _rec.items;'); + expectIncludes(code, '_rec.values = values;'); + expectIncludes(code, '_export("result", result = _rec.result);'); + }); +}); diff --git a/lively.storage/objectdb.js b/lively.storage/objectdb.js index f9c71ade0d..a0f8571604 100644 --- a/lively.storage/objectdb.js +++ b/lively.storage/objectdb.js @@ -891,10 +891,19 @@ class Synchronization { let commitReplicationState = 'not started'; let versionReplicationState = 'not started'; + let finalizing = false; + let sync = this; this.versionReplication = versionReplication; this.commitReplication = commitReplication; this.snapshotReplication = snapshotReplication; + this._finalizeStoppedReplication = () => { + commitReplicationState = 'complete'; + versionReplicationState = 'complete'; + snapshotReplication.stopped = true; + updateState(this); + tryToResolve(this, [], 'versions'); + }; commitChangeListener = remoteCommitDB.pouchdb.changes({ include_docs: true, live: true, conflicts: true }); versionChangeListener = remoteVersionDB.pouchdb.changes({ include_docs: true, live: true, conflicts: true }); @@ -988,7 +997,7 @@ class Synchronization { } finally { snapshotReplication.copyCalls--; updateState(this); - tryToResolve(this, error ? [error] : []); + tryToResolve(this, error ? [error] : [], 'commits'); } }) .on('paused', () => { @@ -1004,12 +1013,12 @@ class Synchronization { .on('error', err => { commitReplicationState = 'complete'; updateState(this); console.error(`${this} commit replication error`, err); - tryToResolve(this, [err]); + tryToResolve(this, [err], 'commits'); }) .on('complete', info => { commitReplicationState = 'complete'; updateState(this); let errors = method === 'sync' ? info.push.errors.concat(info.pull.errors) : info.errors; - tryToResolve(this, errors); + tryToResolve(this, errors, 'commits'); }); versionReplication.on('change', change => { @@ -1039,12 +1048,12 @@ class Synchronization { .on('error', err => { versionReplicationState = 'complete'; updateState(this); console.error(`${this} version replication error`, err); - tryToResolve(this, [err]); + tryToResolve(this, [err], 'versions'); }) .on('complete', info => { versionReplicationState = 'complete'; updateState(this); let errors = method === 'sync' ? info.push.errors.concat(info.pull.errors) : info.errors; - tryToResolve(this, errors); + tryToResolve(this, errors, 'versions'); }); this.state = 'running'; @@ -1054,16 +1063,272 @@ class Synchronization { // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- function updateState (sync) { - if (versionReplicationState === 'paused' && commitReplicationState === 'paused' && snapshotReplication.copyCalls <= 0 && snapshotReplication.copyCallsWaiting <= 0) { return sync.state = 'paused'; } - if (versionReplicationState === 'complete' && commitReplicationState === 'complete' && snapshotReplication.isComplete()) { return sync.state = 'complete'; } + let versionDone = versionReplicationState === 'complete'; + let commitDone = commitReplicationState === 'complete'; + let versionIdle = versionDone || versionReplicationState === 'paused'; + let commitIdle = commitDone || commitReplicationState === 'paused'; + if (versionDone && commitDone && snapshotReplication.isComplete()) { return sync.state = 'complete'; } + if (versionIdle && commitIdle && snapshotReplication.copyCalls <= 0 && snapshotReplication.copyCallsWaiting <= 0) { return sync.state = 'paused'; } return sync.state = 'running'; } - function tryToResolve (sync, errors) { + function isConflictError (err) { + return err && (err.status === 409 || err.name === 'conflict'); + } + + function recordConflictError (sync, kind, err) { + let id = err.id || err.docId; + let conflict = { db: kind, kind, error: err }; + if (id) conflict.id = id; + if (err.rev) conflict.rev = err.rev; + if (id && sync.conflicts.some(ea => ea.db === kind && ea.id === id && ea.rev === err.rev)) return; + sync.conflicts.push(conflict); + } + + function recordVersionChange (sync, direction, id) { + if (sync.changes.some(ea => ea.direction === direction && ea.kind === 'versions' && ea.id === id)) return; + sync.changes.push({ direction, kind: 'versions', id }); + } + + function hasCommitChangeForVersion (sync, direction, id) { + let [type, ...nameParts] = id.split('/'); + let name = nameParts.join('/'); + return sync.changes.some(ea => + ea.direction === direction && + ea.kind === 'commits' && + ea.type === type && + ea.name === name); + } + + function pruneUnmatchedVersionChanges (sync) { + sync.changes = sync.changes.filter(ea => + ea.kind !== 'versions' || hasCommitChangeForVersion(sync, ea.direction, ea.id)); + } + + function recordCommitChange (sync, direction, commit) { + if (sync.changes.some(ea => ea.direction === direction && ea.kind === 'commits' && ea.id === commit._id)) return; + sync.changes.push({ direction, kind: 'commits', id: commit._id, type: commit.type, name: commit.name }); + } + + function shouldReplicateCommitDoc (doc) { + if (!doc || doc._id[0] === '_') return false; + if (!replicationFilter) return true; + if (replicationFilter.onlyIds) return !!replicationFilter.onlyIds[doc._id]; + if (replicationFilter.onlyTypesAndNames) return !!replicationFilter.onlyTypesAndNames[doc.type + '/' + doc.name]; + return true; + } + + function shouldReplicateVersionDoc (doc) { + if (!doc || doc._id[0] === '_') return false; + if (!replicationFilter) return true; + if (replicationFilter.onlyIds) return !!replicationFilter.onlyIds[doc._id]; + if (replicationFilter.onlyTypesAndNames) return !!replicationFilter.onlyTypesAndNames[doc._id]; + return true; + } + + function versionDocData (doc) { + return { + refs: { ...(doc && doc.refs) }, + history: { ...(doc && doc.history) } + }; + } + + function docDataWithoutStorageMeta (doc) { + let data = { ...doc }; + delete data._rev; + delete data._revisions; + return data; + } + + function isAncestorOf (ancestorId, commitId, history, seen = {}) { + if (!ancestorId || !commitId || seen[commitId]) return false; + seen[commitId] = true; + let parents = history[commitId] || []; + if (parents.includes(ancestorId)) return true; + return parents.some(parentId => isAncestorOf(ancestorId, parentId, history, seen)); + } + + function chooseRef (left, right, history) { + if (!left) return right; + if (!right || left === right) return left; + if (isAncestorOf(left, right, history)) return right; + if (isAncestorOf(right, left, history)) return left; + return [left, right].sort().pop(); + } + + function mergeVersionDocs (existingDoc, incomingDoc) { + let existing = versionDocData(existingDoc); + let incoming = versionDocData(incomingDoc); + let history = { ...existing.history, ...incoming.history }; + let refs = {}; + for (let ref of arr.uniq(Object.keys(existing.refs).concat(Object.keys(incoming.refs)))) { + refs[ref] = chooseRef(existing.refs[ref], incoming.refs[ref], history); + } + return { _id: incomingDoc._id, refs, history }; + } + + async function mergeVersionDocInto (toDB, doc, existingDoc) { + let existing = existingDoc || await toDB.get(doc._id); + let merged = mergeVersionDocs(existing, doc); + if (existing && obj.equals(versionDocData(existing), versionDocData(merged))) return false; + await toDB.set(doc._id, merged); + return true; + } + + async function getReplicationDocs (db) { + return Promise.all((await db.docList()) + .filter(({ id }) => id[0] !== '_') + .map(({ id }) => db.pouchdb.get(id, { revs: true }))); + } + + async function copySnapshotsForCommits (commits, direction) { + let resources = commits + .map(commit => ({ commit, path: snapshotPathFor(commit) })) + .filter(({ path }) => !!path); + await promise.parallel(resources.map(({ path }) => () => { + let fromResource = (direction === 'push' ? fromSnapshotLocation : remoteLocation).join(path); + let toResource = (direction === 'push' ? remoteLocation : fromSnapshotLocation).join(path); + return toResource.exists().then(toExists => { + if (toExists) return null; + return fromResource.exists().then(fromExists => { + if (!fromExists) return null; + return tryCopy(0); + }); + }); + + function tryCopy (n = 0) { + return fromResource.copyTo(toResource).catch(err => { + if (n >= 5) throw err; + return tryCopy(n + 1); + }); + } + }), 5); + } + + async function replicateCommitDocsManually (fromDB, toDB, direction) { + let errors = []; + for (let doc of await getReplicationDocs(fromDB)) { + if (!shouldReplicateCommitDoc(doc)) continue; + let existing = await toDB.get(doc._id); + if (existing && obj.equals(docDataWithoutStorageMeta(existing), docDataWithoutStorageMeta(doc))) continue; + let result; + try { + [result] = await toDB.setDocuments([{ ...doc }], { new_edits: false }); + } catch (err) { + result = err; + if (!result.id) result.id = doc._id; + } + + if (result && result.ok) { + recordCommitChange(sync, direction, doc); + try { + await copySnapshotsForCommits([doc], direction); + } catch (err) { + errors.push(err); + } + } else if (isConflictError(result)) { + if (!result.id) result.id = doc._id; + recordConflictError(sync, 'commits', result); + } else if (result) errors.push(result); + } + return errors; + } + + async function replicateVersionDocsManually (fromDB, toDB, direction) { + let errors = []; + for (let doc of await getReplicationDocs(fromDB)) { + if (!shouldReplicateVersionDoc(doc)) continue; + let existing = await toDB.get(doc._id); + if (existing && obj.equals(versionDocData(existing), versionDocData(doc))) continue; + let result; + try { + [result] = await toDB.setDocuments([{ ...doc }], { new_edits: false }); + } catch (err) { + result = err; + if (!result.id) result.id = doc._id; + } + + if (result && result.ok) { + if (!existing || await mergeVersionDocInto(toDB, doc, existing)) { + recordVersionChange(sync, direction, doc._id); + } + } else if (isConflictError(result)) { + if (!result.id) result.id = doc._id; + try { + if (await mergeVersionDocInto(toDB, doc, existing)) { + recordVersionChange(sync, direction, doc._id); + recordConflictError(sync, 'versions', result); + } + } catch (err) { + errors.push(err); + } + } else if (result) errors.push(result); + } + return errors; + } + + async function reconcileCommitDocs () { + let errors = []; + if (method === 'sync') { + errors = errors.concat(await replicateCommitDocsManually(commitDB, remoteCommitDB, 'push')); + await promise.delay(200); + return errors.concat(await replicateCommitDocsManually(remoteCommitDB, commitDB, 'pull')); + } + if (method === 'replicateTo' || method === 'sync') { + errors = errors.concat(await replicateCommitDocsManually(commitDB, remoteCommitDB, 'push')); + } + if (method === 'replicateFrom' || method === 'sync') { + errors = errors.concat(await replicateCommitDocsManually(remoteCommitDB, commitDB, 'pull')); + } + return errors; + } + + async function reconcileVersionDocs () { + let errors = []; + if (method === 'sync') { + errors = errors.concat(await replicateVersionDocsManually(versionDB, remoteVersionDB, 'push')); + await promise.delay(200); + errors = errors.concat(await replicateVersionDocsManually(remoteVersionDB, versionDB, 'pull')); + return errors.concat(await replicateVersionDocsManually(versionDB, remoteVersionDB, 'push')); + } + if (method === 'replicateTo' || method === 'sync') { + errors = errors.concat(await replicateVersionDocsManually(versionDB, remoteVersionDB, 'push')); + } + if (method === 'replicateFrom' || method === 'sync') { + errors = errors.concat(await replicateVersionDocsManually(remoteVersionDB, versionDB, 'pull')); + } + return errors; + } + + function tryToResolve (sync, errors, kind) { + errors = errors || []; + errors = errors.filter(err => { + if (!isConflictError(err)) return true; + recordConflictError(sync, kind, err); + return false; + }); if (!errors.length && (commitReplicationState !== 'complete' || versionReplicationState !== 'complete' || !snapshotReplication.isComplete())) return; + if (finalizing) return; + finalizing = true; + finalize(errors).catch(err => sync.deferred.reject(err)); + } + + async function finalize (errors) { versionChangeListener.cancel(); + commitChangeListener.cancel(); + + if (!errors.length) { + errors = (await reconcileCommitDocs()).concat(await reconcileVersionDocs()); + pruneUnmatchedVersionChanges(sync); + errors = errors.filter(err => { + if (!isConflictError(err)) return true; + recordConflictError(sync, 'versions', err); + return false; + }); + } + let err; if (errors.length) { sync.state = 'complete'; @@ -1091,8 +1356,16 @@ class Synchronization { async safeStop () { if (this.state === 'not started' || !this.isSynchonizing) return this; - await this.whenPaused(); - return this.stop(); + if (this.options.live) { + await promise.delay(200); + await promise.waitFor(5 * 1000, () => + (this.isPaused || this.isComplete) && + this.snapshotReplication.copyCalls <= 0 && + this.snapshotReplication.copyCallsWaiting <= 0, + true); + } else await this.whenPaused(); + this.stop(); + return this.waitForIt(); } stop () { @@ -1100,6 +1373,7 @@ class Synchronization { this.commitReplication.cancel(); this.versionReplication.cancel(); this.snapshotReplication.stopped = true; + this._finalizeStoppedReplication && this._finalizeStoppedReplication(); return this; } diff --git a/lively.storage/tests/objectdb-replication-test.js b/lively.storage/tests/objectdb-replication-test.js index 44ee7e5f69..07e731ed41 100644 --- a/lively.storage/tests/objectdb-replication-test.js +++ b/lively.storage/tests/objectdb-replication-test.js @@ -16,9 +16,15 @@ let replicationLocation = resource('local://lively-morphic-objectdb-test/replica let snapshotLocation2 = resource('local://lively-morphic-objectdb-test/more-snapshots/'); let pouchDBForCommits, pouchDBForHist; +function withoutStorageRev (doc) { + let copy = { ...doc }; + delete copy._rev; + return copy; +} + async function expectDBsHaveSameDocs (db1, db2) { - let docs1 = (await db1.getAll()).filter(ea => !ea._id.startsWith('_')); - let docs2 = (await db2.getAll()).filter(ea => !ea._id.startsWith('_')); + let docs1 = (await db1.getAll()).filter(ea => !ea._id.startsWith('_')).map(withoutStorageRev); + let docs2 = (await db2.getAll()).filter(ea => !ea._id.startsWith('_')).map(withoutStorageRev); expect(docs1).deep.members(docs2); } @@ -135,8 +141,8 @@ describe('replication', function () { ]); await Promise.all([sync1.safeStop(), sync2.safeStop()]); - expect(await objectDB.__versionDB.get('world/foo')) - .deep.equals(await objectDB2.__versionDB.get('world/foo')); + expect(withoutStorageRev(await objectDB.__versionDB.get('world/foo'))) + .deep.equals(withoutStorageRev(await objectDB2.__versionDB.get('world/foo'))); }); it('conflict', async () => { diff --git a/lively.vm/lib/esm-eval.js b/lively.vm/lib/esm-eval.js index ef94859c87..1e6af8e254 100644 --- a/lively.vm/lib/esm-eval.js +++ b/lively.vm/lib/esm-eval.js @@ -6,6 +6,11 @@ const { funcCall, member, id, literal } = nodes; import { arr } from 'lively.lang'; import { runEval as vmRunEval } from './eval.js'; +const livelyTranspilerIds = [ + 'lively.transpiler.babel', + 'lively.transpiler.swc' +]; + // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- // load support @@ -113,13 +118,13 @@ function getEs6Transpiler (System, options, env) { babelPluginTranspilerForAsyncAwaitCode(System, babelPlugin, options.targetModule, env)); } - if (System.transpiler === 'lively.transpiler.babel') { - let Transpiler = System.get('lively.transpiler.babel').default; + if (livelyTranspilerIds.includes(System.transpiler)) { + let Transpiler = System.get(System.transpiler).default; let transpiler = new Transpiler(System, options.targetModule, env); return (source, options) => transpiler.transpileDoit(source, options); } - throw new Error('Sorry, currently only babel is supported as es6 transpiler for runEval!'); + throw new Error('Sorry, currently only babel and swc are supported as es6 transpilers for runEval!'); } function evalEnd (System, code, options, result) { diff --git a/lively.vm/tests/esm-eval-test.js b/lively.vm/tests/esm-eval-test.js index d978b4f398..4859e0c47f 100644 --- a/lively.vm/tests/esm-eval-test.js +++ b/lively.vm/tests/esm-eval-test.js @@ -20,8 +20,10 @@ describe('eval', () => { if (modules) { S = modules.getSystem('test', { baseURL: dir }); S.babelOptions = System.babelOptions; - S.set('lively.transpiler.babel', System.get('lively.transpiler.babel')); - S.config({ transpiler: 'lively.transpiler.babel' }); + const transpiler = System.transpiler; + S.set(transpiler, System.get(transpiler)); + S.config({ transpiler }); + S._loader.transpilerPromise = System._loader.transpilerPromise; S.translate = async (load, opts) => await System.translate.bind(S)(load, opts); } diff --git a/mocha-es6/index.js b/mocha-es6/index.js index ca1d055390..241e560c69 100644 --- a/mocha-es6/index.js +++ b/mocha-es6/index.js @@ -107,14 +107,19 @@ function prepareMocha (mocha, GLOBAL) { GLOBAL.after = context.after || context.suiteTeardown; GLOBAL.beforeEach = context.beforeEach || context.setup; GLOBAL.before = context.before || context.suiteSetup; + GLOBAL.context = context.context || context.describe || context.suite; GLOBAL.describe = context.describe || context.suite; GLOBAL.it = context.it || context.test; GLOBAL.setup = context.setup || context.beforeEach; + GLOBAL.specify = context.specify || context.it || context.test; GLOBAL.suiteSetup = context.suiteSetup || context.before; GLOBAL.suiteTeardown = context.suiteTeardown || context.after; GLOBAL.suite = context.suite || context.describe; GLOBAL.teardown = context.teardown || context.afterEach; GLOBAL.test = context.test || context.it; + GLOBAL.xcontext = context.xcontext || context.xdescribe; + GLOBAL.xdescribe = context.xdescribe || context.describe?.skip; + GLOBAL.xit = context.xit || context.it?.skip || context.specify?.skip; GLOBAL.run = context.run; }); mocha.ui('bdd'); @@ -263,18 +268,51 @@ function recordTestsWhile (file, whileFn, options = {}) { module.define('mocha', m); // put mocha globals in place + const runtimeGlobal = typeof globalThis !== 'undefined' ? globalThis : System.global; + if (!options.global) options.global = runtimeGlobal; prepareMocha(m, options.global); m.suite.emit('pre-require', options.global, file, m); - let result = whileFn(); + const globalNames = [ + 'afterEach', 'after', 'beforeEach', 'before', + 'context', 'describe', 'it', 'setup', 'specify', + 'suiteSetup', 'suiteTeardown', 'suite', 'teardown', + 'test', 'xcontext', 'xdescribe', 'xit', 'run' + ]; + const hadGlobal = new Map(); + const previousGlobals = new Map(); + for (const name of globalNames) { + const hasOwn = Object.prototype.hasOwnProperty.call(runtimeGlobal, name); + hadGlobal.set(name, hasOwn); + previousGlobals.set(name, hasOwn ? runtimeGlobal[name] : undefined); + if (options.global !== runtimeGlobal) runtimeGlobal[name] = options.global[name]; + } + const restoreGlobals = () => { + for (const name of globalNames) { + if (!hadGlobal.get(name)) { + delete runtimeGlobal[name]; + } else { + runtimeGlobal[name] = previousGlobals.get(name); + } + } + }; + let result; + try { + result = whileFn(); + } catch (err) { + restoreGlobals(); + throw err; + } if (result && typeof result.then === 'function') { - return Promise.resolve(result).then(() => { - let imported = System.get(file); - m.suite.emit('require', imported, file, m); - m.suite.emit('post-require', options.global, file, m); - logger.log('[mocha-es6] loaded test module %s with %s tests', - file, gatherTests(m.suite).length); - }); + return Promise.resolve(result) + .then(() => { + let imported = System.get(file); + m.suite.emit('require', imported, file, m); + m.suite.emit('post-require', options.global, file, m); + logger.log('[mocha-es6] loaded test module %s with %s tests', + file, gatherTests(m.suite).length); + }) + .finally(restoreGlobals); } let imported = System.get(file); @@ -282,6 +320,7 @@ function recordTestsWhile (file, whileFn, options = {}) { m.suite.emit('post-require', options.global, file, m); logger.log('[mocha-es6] loaded test module %s with %s tests', file, gatherTests(m.suite).length); + restoreGlobals(); return result; } diff --git a/scripts/test.sh b/scripts/test.sh index e73fc1c43b..b5cebc7054 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -18,6 +18,72 @@ RED_TESTS=0 SKIPPED_TESTS=0 STARTED_SERVER=0 FAILURE=0 +SERVER_READY_URL="${LIVELY_TEST_SERVER_READY_URL:-http://127.0.0.1:9011/}" +SERVER_READY_TIMEOUT="${LIVELY_TEST_SERVER_READY_TIMEOUT:-120000}" +SERVER_START_MAX_ATTEMPTS="${LIVELY_TEST_SERVER_START_MAX_ATTEMPTS:-3}" +TEST_RESULT_MAX_ATTEMPTS="${LIVELY_TEST_RESULT_MAX_ATTEMPTS:-3}" + +start_test_server() { + local attempt=1 + + while (( attempt <= SERVER_START_MAX_ATTEMPTS )); do + # start a new lively.next server + ./start-server.sh > /tmp/lively-next-test-server.log 2>&1 & + SERVER_PID=$! + if node ./scripts/wait-for-server.js "$SERVER_READY_URL" "$SERVER_READY_TIMEOUT" 1000 "$SERVER_PID"; then + return 0 + fi + + echo "Lively server start attempt $attempt/$SERVER_START_MAX_ATTEMPTS failed. Recent server output:" + tail -80 /tmp/lively-next-test-server.log 2>/dev/null || true + stop_test_server + ((attempt++)) + done + + echo "Failed to start lively server for tests after $SERVER_START_MAX_ATTEMPTS attempts." + return 1 +} + +stop_test_server() { + if [ -n "$SERVER_PID" ]; then + kill "$SERVER_PID" 2>/dev/null || true + wait "$SERVER_PID" 2>/dev/null || true + SERVER_PID= + fi + pkill -f "bin/start-server.js.*9011" 2>/dev/null || true + pkill -f "start-server.sh" 2>/dev/null || true +} + +run_package_tests() { + local package="$1" + local attempt=1 + local output + + while true; do + output=$(node --dns-result-order ipv4first ./scripts/test.js "$package") + if ! echo "$output" | grep -Eq "ECONNREFUSED|ECONNRESET|socket hang up"; then + echo "$output" + return 0 + fi + + if (( attempt >= TEST_RESULT_MAX_ATTEMPTS )); then + echo "$output" + return 0 + fi + + echo "Test result request for $package failed due to a transient server connection issue; retrying ($attempt/$TEST_RESULT_MAX_ATTEMPTS)..." >&2 + if [ "$CI" ]; then + stop_test_server >&2 + start_test_server >&2 || { + echo "$output" + return 0 + } + else + node ./scripts/wait-for-server.js "$SERVER_READY_URL" "$SERVER_READY_TIMEOUT" > /dev/null || true + fi + ((attempt++)) + done +} # Clean up any lingering headless Chrome processes and lock files # These can remain from previous test runs and block new browser instances @@ -25,11 +91,13 @@ echo "Cleaning up any lingering Chrome processes..." pkill -f "chrome.*--headless" 2>/dev/null || true pkill -f "chromium.*--headless" 2>/dev/null || true -# Remove Chrome singleton lock file if it exists -CHROME_LOCK_FILE="lively.headless/chrome-data-dir/SingletonLock" -if [ -f "$CHROME_LOCK_FILE" ]; then - echo "Removing stale Chrome lock file: $CHROME_LOCK_FILE" - rm -f "$CHROME_LOCK_FILE" +# Remove Chrome singleton files if they exist +CHROME_PROFILE_DIR="lively.headless/chrome-data-dir" +if [ -d "$CHROME_PROFILE_DIR" ]; then + echo "Resetting Chrome profile directory: $CHROME_PROFILE_DIR" + rm -rf "$CHROME_PROFILE_DIR" + mkdir -p "$CHROME_PROFILE_DIR" + touch "$CHROME_PROFILE_DIR/.gitkeep" fi testfiles=( @@ -104,10 +172,7 @@ then else STARTED_SERVER=1 echo "No local lively server was found on port 9011. Starting a server on port 9011 to run tests on." - # start a new lively.next server - ./start-server.sh > /dev/null 2>&1 & - # wait until server is guaranteed to be running - sleep 30 + start_test_server || exit 1 fi fi @@ -116,13 +181,13 @@ for package in "${testfiles[@]}"; do ((TESTED_PACKAGES++)) if [ "$CI" ]; then - # start a new lively.next server - ./start-server.sh > /dev/null 2>&1 & - # wait until server is guaranteed to be running - sleep 30 + start_test_server || { + ((FAILURE+=1)) + continue + } fi # echo output without the summary stats - output=$(node --dns-result-order ipv4first ./scripts/test.js "$package") + output=$(run_package_tests "$package") if uname | grep 'Linux' > /dev/null; then echo "$output" | sed -s -e 's/SUMMARY.*$//g' @@ -149,19 +214,21 @@ for package in "${testfiles[@]}"; do if [ "$CI" ]; then - pkill -f lively.*start + stop_test_server fi done if [ "$STARTED_SERVER" = "1" ]; then - pkill -f -n lively.*start + stop_test_server fi # Clean up any headless Chrome processes that may have been started during tests echo "Cleaning up Chrome processes after tests..." pkill -f "chrome.*--headless" 2>/dev/null || true pkill -f "chromium.*--headless" 2>/dev/null || true +pkill -f "chrome.*lively.headless/chrome-data-dir" 2>/dev/null || true +pkill -f "chromium.*lively.headless/chrome-data-dir" 2>/dev/null || true ((ALL_TESTS=GREEN_TESTS + RED_TESTS + SKIPPED_TESTS)) if [ ! "$1" ]; diff --git a/scripts/wait-for-server.js b/scripts/wait-for-server.js new file mode 100755 index 0000000000..89e2a3d9b4 --- /dev/null +++ b/scripts/wait-for-server.js @@ -0,0 +1,60 @@ +#!/usr/bin/env node + +const http = require('http'); + +const url = process.argv[2] || 'http://127.0.0.1:9011/'; +const timeoutMs = Number(process.argv[3] || process.env.LIVELY_SERVER_READY_TIMEOUT || 120000); +const intervalMs = Number(process.argv[4] || process.env.LIVELY_SERVER_READY_INTERVAL || 1000); +const watchedPid = Number(process.argv[5] || process.env.LIVELY_SERVER_READY_PID || 0); +const requestTimeoutMs = Number(process.env.LIVELY_SERVER_READY_REQUEST_TIMEOUT || 5000); +const startedAt = Date.now(); +let attempts = 0; +let lastError = ''; + +function elapsed () { return Date.now() - startedAt; } + +function watchedProcessIsAlive () { + if (!watchedPid) return true; + try { + process.kill(watchedPid, 0); + return true; + } catch (err) { + return false; + } +} + +function finishWithFailure () { + console.error(`lively server did not become ready at ${url} within ${timeoutMs}ms${lastError ? `; last error: ${lastError}` : ''}`); + process.exit(1); +} + +function scheduleRetry (reason) { + lastError = reason; + if (!watchedProcessIsAlive()) { + console.error(`lively server process ${watchedPid} exited before ${url} became ready${lastError ? `; last error: ${lastError}` : ''}`); + process.exit(1); + } + if (elapsed() >= timeoutMs) return finishWithFailure(); + setTimeout(check, Math.min(intervalMs, Math.max(0, timeoutMs - elapsed()))); +} + +function check () { + if (!watchedProcessIsAlive()) { + console.error(`lively server process ${watchedPid} exited before ${url} became ready${lastError ? `; last error: ${lastError}` : ''}`); + process.exit(1); + } + attempts++; + const req = http.get(url, { timeout: requestTimeoutMs }, res => { + res.resume(); + if (res.statusCode && res.statusCode < 500) { + console.log(`lively server ready at ${url} after ${attempts} attempt(s)`); + process.exit(0); + } + scheduleRetry(`HTTP ${res.statusCode}`); + }); + + req.on('timeout', () => req.destroy(new Error('request timeout'))); + req.on('error', err => scheduleRetry(err.message || String(err))); +} + +check();