Skip to content

Commit 3cb114a

Browse files
Robins Kisteclaude
andcommitted
❄️🔁: browser WASM SWC transpiler — replaces Babel for fastLoad=false world loads
Compile the existing Rust SWC transforms into a standalone browser WASM module that replaces Babel for in-browser module transpilation. Falls back to Babel automatically if WASM loading fails. Cargo workspace with 3 crates sharing transform code: - lively-swc-transforms: shared library (7 AST visitors) - lively-swc-plugin: existing server-side plugin (wasm32-wasip1) - lively-swc-browser: new browser WASM entry (wasm32-unknown-unknown) JS integration layer (lively.source-transform/swc/): - browser-transform.js: WASM loader + transform wrapper - transpiler-setup.js: SystemJS registration, config mapping, Babel fallback Key post-processing passes for Babel-compatible System.register output: - Hoist recorder init, rewrite setters with defVar/default params - Fix system_js bugs: async execute, shadowed export calls, nested fn export leaking (let/const shadowing exported names) - Export alias desugaring (__export_X__ prefix) to prevent system_js hoisting Performance (176 modules, 0 Babel fallbacks, 0 errors): - SWC: 34s total load, 3.1s transpile - Babel: 46s total load - ~1.3x faster overall, ~5x faster transpilation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b86076e commit 3cb114a

30 files changed

Lines changed: 3229 additions & 210 deletions

lively.freezer/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,11 @@
3434
"build-landing-page": "./tools/build-landing-page.sh",
3535
"build-unified": "./tools/build-unified.sh",
3636
"build": "./tools/build-unified.sh",
37-
"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",
38-
"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",
37+
"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",
38+
"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",
39+
"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",
3940
"example-swc-bundler": "./tools/run-swc-example.sh",
40-
"test-swc-plugin": "cd swc-plugin && cargo test"
41+
"test-swc-plugin": "cd swc-plugin && cargo test -p lively-swc-transforms"
4142
},
4243
"systemjs": {
4344
"map": {

lively.freezer/src/bundler-swc.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,10 @@ export class LivelySwcTransform {
100100
captureImports,
101101
resurrection,
102102
moduleId,
103-
currentModuleAccessor: currentModuleAccessor || classToFunctionConfig?.currentModuleAccessor || null,
103+
// Scope capture in bundle mode always uses FreezerRuntime.recorderFor().
104+
// The class transform has its own currentModuleAccessor via classToFunction.currentModuleAccessor.
105+
// Setting this to null ensures the FreezerRuntime path is used (not System.get("@lively-env").moduleEnv()).
106+
currentModuleAccessor: null,
104107
packageName,
105108
packageVersion,
106109
enableComponentTransform: true,

lively.freezer/src/util/bootstrap.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { updateBundledModules } from 'lively.modules/src/module.js';
1010
import { Project } from 'lively.project/project.js';
1111
import { pathForBrowserHistory } from 'lively.morphic/helpers.js';
1212
import { installLinter } from 'lively.ide/js/linter.js';
13-
import { setupBabelTranspiler } from 'lively.source-transform/babel/plugin.js';
13+
import { setupSwcTranspiler } from 'lively.source-transform/swc/transpiler-setup.js';
1414
import untar from 'js-untar';
1515
import bowser from 'bowser';
1616

@@ -233,7 +233,7 @@ function bootstrapLivelySystem (progress, fastLoad = query.fastLoad !== false ||
233233
$world.env.installSystemChangeHandlers();
234234

235235
installLinter(System);
236-
setupBabelTranspiler(System);
236+
await setupSwcTranspiler(System);
237237
logInfo('Setup SystemJS:', Date.now() - ts + 'ms');
238238

239239
// load packages
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/* tslint:disable */
2+
/* eslint-disable */
3+
4+
/**
5+
* Transform JavaScript source code using lively.next's SWC transforms,
6+
* then wrap in System.register() format for SystemJS module loading.
7+
*
8+
* # Arguments
9+
* * `source` - The JavaScript source code to transform
10+
* * `config_json` - JSON string matching `LivelyTransformConfig`
11+
*
12+
* # Returns
13+
* JSON string: `{ "code": "...", "map": "..." }`
14+
*/
15+
export function transform(source: string, config_json: string): string;
16+
17+
/**
18+
* Returns the version of the transforms library.
19+
*/
20+
export function version(): string;
21+
22+
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
23+
24+
export interface InitOutput {
25+
readonly memory: WebAssembly.Memory;
26+
readonly transform: (a: number, b: number, c: number, d: number) => [number, number, number, number];
27+
readonly version: () => [number, number];
28+
readonly __wbindgen_externrefs: WebAssembly.Table;
29+
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
30+
readonly __wbindgen_malloc: (a: number, b: number) => number;
31+
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
32+
readonly __externref_table_dealloc: (a: number) => void;
33+
readonly __wbindgen_start: () => void;
34+
}
35+
36+
export type SyncInitInput = BufferSource | WebAssembly.Module;
37+
38+
/**
39+
* Instantiates the given `module`, which can either be bytes or
40+
* a precompiled `WebAssembly.Module`.
41+
*
42+
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
43+
*
44+
* @returns {InitOutput}
45+
*/
46+
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
47+
48+
/**
49+
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
50+
* for everything else, calls `WebAssembly.instantiate` directly.
51+
*
52+
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
53+
*
54+
* @returns {Promise<InitOutput>}
55+
*/
56+
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
let wasm;
2+
3+
function getStringFromWasm0(ptr, len) {
4+
ptr = ptr >>> 0;
5+
return decodeText(ptr, len);
6+
}
7+
8+
let cachedUint8ArrayMemory0 = null;
9+
function getUint8ArrayMemory0() {
10+
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
11+
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
12+
}
13+
return cachedUint8ArrayMemory0;
14+
}
15+
16+
function passStringToWasm0(arg, malloc, realloc) {
17+
if (realloc === undefined) {
18+
const buf = cachedTextEncoder.encode(arg);
19+
const ptr = malloc(buf.length, 1) >>> 0;
20+
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
21+
WASM_VECTOR_LEN = buf.length;
22+
return ptr;
23+
}
24+
25+
let len = arg.length;
26+
let ptr = malloc(len, 1) >>> 0;
27+
28+
const mem = getUint8ArrayMemory0();
29+
30+
let offset = 0;
31+
32+
for (; offset < len; offset++) {
33+
const code = arg.charCodeAt(offset);
34+
if (code > 0x7F) break;
35+
mem[ptr + offset] = code;
36+
}
37+
if (offset !== len) {
38+
if (offset !== 0) {
39+
arg = arg.slice(offset);
40+
}
41+
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
42+
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
43+
const ret = cachedTextEncoder.encodeInto(arg, view);
44+
45+
offset += ret.written;
46+
ptr = realloc(ptr, len, offset, 1) >>> 0;
47+
}
48+
49+
WASM_VECTOR_LEN = offset;
50+
return ptr;
51+
}
52+
53+
function takeFromExternrefTable0(idx) {
54+
const value = wasm.__wbindgen_externrefs.get(idx);
55+
wasm.__externref_table_dealloc(idx);
56+
return value;
57+
}
58+
59+
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
60+
cachedTextDecoder.decode();
61+
const MAX_SAFARI_DECODE_BYTES = 2146435072;
62+
let numBytesDecoded = 0;
63+
function decodeText(ptr, len) {
64+
numBytesDecoded += len;
65+
if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) {
66+
cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
67+
cachedTextDecoder.decode();
68+
numBytesDecoded = len;
69+
}
70+
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
71+
}
72+
73+
const cachedTextEncoder = new TextEncoder();
74+
75+
if (!('encodeInto' in cachedTextEncoder)) {
76+
cachedTextEncoder.encodeInto = function (arg, view) {
77+
const buf = cachedTextEncoder.encode(arg);
78+
view.set(buf);
79+
return {
80+
read: arg.length,
81+
written: buf.length
82+
};
83+
}
84+
}
85+
86+
let WASM_VECTOR_LEN = 0;
87+
88+
/**
89+
* Transform JavaScript source code using lively.next's SWC transforms,
90+
* then wrap in System.register() format for SystemJS module loading.
91+
*
92+
* # Arguments
93+
* * `source` - The JavaScript source code to transform
94+
* * `config_json` - JSON string matching `LivelyTransformConfig`
95+
*
96+
* # Returns
97+
* JSON string: `{ "code": "...", "map": "..." }`
98+
* @param {string} source
99+
* @param {string} config_json
100+
* @returns {string}
101+
*/
102+
export function transform(source, config_json) {
103+
let deferred4_0;
104+
let deferred4_1;
105+
try {
106+
const ptr0 = passStringToWasm0(source, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
107+
const len0 = WASM_VECTOR_LEN;
108+
const ptr1 = passStringToWasm0(config_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
109+
const len1 = WASM_VECTOR_LEN;
110+
const ret = wasm.transform(ptr0, len0, ptr1, len1);
111+
var ptr3 = ret[0];
112+
var len3 = ret[1];
113+
if (ret[3]) {
114+
ptr3 = 0; len3 = 0;
115+
throw takeFromExternrefTable0(ret[2]);
116+
}
117+
deferred4_0 = ptr3;
118+
deferred4_1 = len3;
119+
return getStringFromWasm0(ptr3, len3);
120+
} finally {
121+
wasm.__wbindgen_free(deferred4_0, deferred4_1, 1);
122+
}
123+
}
124+
125+
/**
126+
* Returns the version of the transforms library.
127+
* @returns {string}
128+
*/
129+
export function version() {
130+
let deferred1_0;
131+
let deferred1_1;
132+
try {
133+
const ret = wasm.version();
134+
deferred1_0 = ret[0];
135+
deferred1_1 = ret[1];
136+
return getStringFromWasm0(ret[0], ret[1]);
137+
} finally {
138+
wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
139+
}
140+
}
141+
142+
const EXPECTED_RESPONSE_TYPES = new Set(['basic', 'cors', 'default']);
143+
144+
async function __wbg_load(module, imports) {
145+
if (typeof Response === 'function' && module instanceof Response) {
146+
if (typeof WebAssembly.instantiateStreaming === 'function') {
147+
try {
148+
return await WebAssembly.instantiateStreaming(module, imports);
149+
} catch (e) {
150+
const validResponse = module.ok && EXPECTED_RESPONSE_TYPES.has(module.type);
151+
152+
if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') {
153+
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);
154+
155+
} else {
156+
throw e;
157+
}
158+
}
159+
}
160+
161+
const bytes = await module.arrayBuffer();
162+
return await WebAssembly.instantiate(bytes, imports);
163+
} else {
164+
const instance = await WebAssembly.instantiate(module, imports);
165+
166+
if (instance instanceof WebAssembly.Instance) {
167+
return { instance, module };
168+
} else {
169+
return instance;
170+
}
171+
}
172+
}
173+
174+
function __wbg_get_imports() {
175+
const imports = {};
176+
imports.wbg = {};
177+
imports.wbg.__wbg_Error_52673b7de5a0ca89 = function(arg0, arg1) {
178+
const ret = Error(getStringFromWasm0(arg0, arg1));
179+
return ret;
180+
};
181+
imports.wbg.__wbindgen_init_externref_table = function() {
182+
const table = wasm.__wbindgen_externrefs;
183+
const offset = table.grow(4);
184+
table.set(0, undefined);
185+
table.set(offset + 0, undefined);
186+
table.set(offset + 1, null);
187+
table.set(offset + 2, true);
188+
table.set(offset + 3, false);
189+
};
190+
191+
return imports;
192+
}
193+
194+
function __wbg_finalize_init(instance, module) {
195+
wasm = instance.exports;
196+
__wbg_init.__wbindgen_wasm_module = module;
197+
cachedUint8ArrayMemory0 = null;
198+
199+
200+
wasm.__wbindgen_start();
201+
return wasm;
202+
}
203+
204+
function initSync(module) {
205+
if (wasm !== undefined) return wasm;
206+
207+
208+
if (typeof module !== 'undefined') {
209+
if (Object.getPrototypeOf(module) === Object.prototype) {
210+
({module} = module)
211+
} else {
212+
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
213+
}
214+
}
215+
216+
const imports = __wbg_get_imports();
217+
if (!(module instanceof WebAssembly.Module)) {
218+
module = new WebAssembly.Module(module);
219+
}
220+
const instance = new WebAssembly.Instance(module, imports);
221+
return __wbg_finalize_init(instance, module);
222+
}
223+
224+
async function __wbg_init(module_or_path) {
225+
if (wasm !== undefined) return wasm;
226+
227+
228+
if (typeof module_or_path !== 'undefined') {
229+
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
230+
({module_or_path} = module_or_path)
231+
} else {
232+
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
233+
}
234+
}
235+
236+
if (typeof module_or_path === 'undefined') {
237+
module_or_path = new URL('lively_swc_browser_bg.wasm', import.meta.url);
238+
}
239+
const imports = __wbg_get_imports();
240+
241+
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
242+
module_or_path = fetch(module_or_path);
243+
}
244+
245+
const { instance, module } = await __wbg_load(await module_or_path, imports);
246+
247+
return __wbg_finalize_init(instance, module);
248+
}
249+
250+
export { initSync };
251+
export default __wbg_init;
4.85 MB
Binary file not shown.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/* tslint:disable */
2+
/* eslint-disable */
3+
export const memory: WebAssembly.Memory;
4+
export const transform: (a: number, b: number, c: number, d: number) => [number, number, number, number];
5+
export const version: () => [number, number];
6+
export const __wbindgen_externrefs: WebAssembly.Table;
7+
export const __wbindgen_free: (a: number, b: number, c: number) => void;
8+
export const __wbindgen_malloc: (a: number, b: number) => number;
9+
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
10+
export const __externref_table_dealloc: (a: number) => void;
11+
export const __wbindgen_start: () => void;

0 commit comments

Comments
 (0)