svelte-shaker 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -99,14 +99,22 @@ pipelines; the Vite plugin is preferred for apps.
99
99
  ```ts
100
100
  shaker({
101
101
  include: ['src'], // dirs (relative to root) holding every .svelte call site
102
- level: 1, // 0 | 1 | 2 — default 1 (L0/L1/L1.5 always on). 2 = opt-in L2.
103
- monomorphize: false, // L2 tuning; only consulted when level: 2.
102
+ level: 2, // 0 | 1 | 2 — default 2 (L0/L1/L1.5 always on; 2 also enables L2).
103
+ monomorphize: true, // L2 tuning; on by default object overrides maxVariants/minSavings.
104
+ engine: 'auto', // 'auto' (default) | 'js' | 'rust' — see below.
104
105
  parser: 'svelte', // 'svelte' (default) | 'rsvelte' — see below.
105
106
  });
106
107
 
107
- // Opt into L2 per-call-site monomorphization:
108
- shaker({ include: ['src'], level: 2, monomorphize: true });
109
- shaker({ include: ['src'], level: 2, monomorphize: { maxVariants: 16 } });
108
+ // L2 is ON by default (it is bail-safe and never bloats). Turn it OFF for faster
109
+ // builds, or tune it:
110
+ shaker({ include: ['src'], level: 1 }); // L2 off (L0/L1/L1.5 only)
111
+ shaker({ include: ['src'], monomorphize: { maxVariants: 16 } }); // raise the variant cap
112
+
113
+ // Engine: the native Rust (WASM) engine runs the whole shake INCLUDING L2 (it
114
+ // calls back to JS only for the net-win gate's compiled-size proxy) and is
115
+ // differentially tested to be byte-identical to the JS engine, so it only changes
116
+ // speed. It is the default ('auto'); force it (or the JS engine) explicitly with:
117
+ shaker({ include: ['src'], engine: 'rust' }); // or engine: 'js'
110
118
 
111
119
  // Opt into the faster rsvelte parser (~1.46x full build, ~2.2x parse).
112
120
  // Requires the optional peer `@rsvelte/vite-plugin-svelte-native` (install it
@@ -154,7 +162,7 @@ for the design.
154
162
  | **L1** | Props that collapse to one constant app-wide → fold + drop + strip every call site's attribute | on |
155
163
  | **L1.5** | Value-set **narrowing**: with `variant ∈ {primary, secondary}`, delete provably-dead `{#if}`/`{:else if}` arms (prop stays in the signature) | on |
156
164
  | **CSS** | `<style>` rules whose class can never be produced given the value sets — the bundler-can't differentiator | on |
157
- | **L2** | Per-call-site monomorphization: specialize a component per prop shape (deduped by residual, capped by `maxVariants`) | **opt-in** |
165
+ | **L2** | Per-call-site monomorphization: specialize a component per prop shape (deduped by residual, capped by `maxVariants`) | on (set `level: 1` to disable) |
158
166
 
159
167
  Folding also reaches template ternaries (`{cond ? a : b}`) and class-string
160
168
  interpolation when the condition/parts are provable constants.
package/dist/mono.js CHANGED
@@ -256,7 +256,9 @@ function netWin(childId, variantSources, models, baseSource, baseChildrenOf, roo
256
256
  const variantChildren = new Map();
257
257
  for (const v of variantSources)
258
258
  variantChildren.set(v.id, liveChildIds(v.code, childModel));
259
- // --- Sigma_base: reachable set in the BASE scenario, sized by base residual.
259
+ // --- Reachability is graph-only (NO compile): the costly `ownSize` is deferred
260
+ // until we know which modules actually differ between the two scenarios.
261
+ // BASE scenario: components reachable from the roots through the base graph.
260
262
  const baseReached = new Set();
261
263
  const stackB = [...roots];
262
264
  while (stackB.length > 0) {
@@ -267,23 +269,12 @@ function netWin(childId, variantSources, models, baseSource, baseChildrenOf, roo
267
269
  for (const c of baseChildrenOf.get(id) ?? [])
268
270
  stackB.push(c);
269
271
  }
270
- let sigmaBase = 0;
271
- for (const id of baseReached) {
272
- const size = ownSize(id, baseSource.get(id));
273
- if (size === null)
274
- return false; // un-sizable -> decline
275
- sigmaBase += size;
276
- }
277
- // --- Sigma_spec: reachability with the child expanded into its variants.
278
- // * reached non-variant components are sized by their base residual,
279
- // * each reached variant by its own residual,
280
- // * the child's base module is NOT a node (its edges redirect to variants).
281
- // A worklist over a tagged node: either a real component id or a variant id.
272
+ // SPEC scenario: the same graph, but every edge into the child redirects to ALL
273
+ // its variants (all-sites means each live caller now renders a variant); the
274
+ // child's base module is gone and each variant renders its OWN live children.
282
275
  const allVariantIds = variantSources.map((v) => v.id);
283
- const reachedComponents = new Set();
284
- const reachedVariants = new Set();
285
- // Redirect any edge into the child to ALL its variants (all-sites means every
286
- // live caller now renders a variant; for a sound upper bound we keep them all).
276
+ const specComponents = new Set();
277
+ const specVariants = new Set();
287
278
  const expand = (id) => id === childId ? { comps: [], vars: allVariantIds } : { comps: [id], vars: [] };
288
279
  const compStack = [];
289
280
  const varStack = [];
@@ -295,9 +286,9 @@ function netWin(childId, variantSources, models, baseSource, baseChildrenOf, roo
295
286
  while (compStack.length > 0 || varStack.length > 0) {
296
287
  if (compStack.length > 0) {
297
288
  const id = compStack.pop();
298
- if (reachedComponents.has(id))
289
+ if (specComponents.has(id))
299
290
  continue;
300
- reachedComponents.add(id);
291
+ specComponents.add(id);
301
292
  for (const c of baseChildrenOf.get(id) ?? []) {
302
293
  const e = expand(c);
303
294
  compStack.push(...e.comps);
@@ -306,30 +297,63 @@ function netWin(childId, variantSources, models, baseSource, baseChildrenOf, roo
306
297
  continue;
307
298
  }
308
299
  const vid = varStack.pop();
309
- if (reachedVariants.has(vid))
300
+ if (specVariants.has(vid))
310
301
  continue;
311
- reachedVariants.add(vid);
302
+ specVariants.add(vid);
312
303
  for (const c of variantChildren.get(vid) ?? []) {
313
304
  const e = expand(c);
314
305
  compStack.push(...e.comps);
315
306
  varStack.push(...e.vars);
316
307
  }
317
308
  }
318
- let sigmaSpec = 0;
319
- for (const id of reachedComponents) {
309
+ // A component reachable in BOTH scenarios has the same base size on each side, so
310
+ // for the default `minSavings === 0` it cancels out of `Σ_spec < Σ_base` — we
311
+ // only have to size the SYMMETRIC DIFFERENCE: the variants (spec-only), and the
312
+ // child base plus any now-orphaned modules (base-only). Compiling only the diff
313
+ // — instead of the whole reachable program, once per candidate — is what keeps a
314
+ // large `maxVariants` affordable: a child that orphans nothing sizes just its
315
+ // variants vs. its base and declines without touching the rest of the graph.
316
+ let baseSide = 0; // Σ over base-only modules (child base + orphaned)
317
+ let specSide = 0; // Σ over spec-only modules (the variants, + any spec-only comp)
318
+ for (const id of baseReached) {
319
+ if (specComponents.has(id))
320
+ continue; // shared -> cancels
320
321
  const size = ownSize(id, baseSource.get(id));
321
322
  if (size === null)
322
- return false;
323
- sigmaSpec += size;
323
+ return false; // un-sizable -> decline
324
+ baseSide += size;
324
325
  }
325
- for (const vid of reachedVariants) {
326
+ for (const vid of specVariants) {
326
327
  const src = variantSources.find((v) => v.id === vid).code;
327
328
  const size = ownSize(vid, src);
328
329
  if (size === null)
329
330
  return false;
330
- sigmaSpec += size;
331
+ specSide += size;
332
+ }
333
+ // Folding never adds reachability, so this set is normally empty; sizing it keeps
334
+ // the comparison sound regardless.
335
+ for (const id of specComponents) {
336
+ if (baseReached.has(id))
337
+ continue;
338
+ const size = ownSize(id, baseSource.get(id));
339
+ if (size === null)
340
+ return false;
341
+ specSide += size;
342
+ }
343
+ // Default gate: shared modules cancel, so the strict net win is `specSide < baseSide`.
344
+ if (minSavings === 0)
345
+ return specSide < baseSide;
346
+ // A non-zero `minSavings` needs the ABSOLUTE base total, so size the shared set too.
347
+ let shared = 0;
348
+ for (const id of baseReached) {
349
+ if (!specComponents.has(id))
350
+ continue;
351
+ const size = ownSize(id, baseSource.get(id));
352
+ if (size === null)
353
+ return false;
354
+ shared += size;
331
355
  }
332
- return sigmaSpec < sigmaBase * (1 - minSavings);
356
+ return specSide + shared < (baseSide + shared) * (1 - minSavings);
333
357
  }
334
358
  /**
335
359
  * The live child component ids a `.svelte` SOURCE renders: parse it and resolve
@@ -0,0 +1,244 @@
1
+ /* @ts-self-types="./svelte_shaker_engine.d.ts" */
2
+
3
+ /**
4
+ * Analyze one component AST (JSON) given its resolved outgoing edges (JSON), and
5
+ * return the per-file model fields ported so far: declared props, `...rest`
6
+ * presence, shadowed / `{@debug}` fold-blocking names, the `<svelte:options>`
7
+ * bail, the rendered child calls, and escaped components. `{"error": "..."}` on
8
+ * malformed input.
9
+ * @param {string} ast_json
10
+ * @param {string} edges_json
11
+ * @returns {string}
12
+ */
13
+ function analyze_component(ast_json, edges_json) {
14
+ let deferred3_0;
15
+ let deferred3_1;
16
+ try {
17
+ const ptr0 = passStringToWasm0(ast_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
18
+ const len0 = WASM_VECTOR_LEN;
19
+ const ptr1 = passStringToWasm0(edges_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
20
+ const len1 = WASM_VECTOR_LEN;
21
+ const ret = wasm.analyze_component(ptr0, len0, ptr1, len1);
22
+ deferred3_0 = ret[0];
23
+ deferred3_1 = ret[1];
24
+ return getStringFromWasm0(ret[0], ret[1]);
25
+ } finally {
26
+ wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
27
+ }
28
+ }
29
+ exports.analyze_component = analyze_component;
30
+
31
+ /**
32
+ * Whole-program analysis entry: `input` is `{ files: [{id, ast}], edges:
33
+ * [{from, local, to, kind}], entries }` (the AST is parsed on the JS side).
34
+ * Returns `{ id: plan }` for every component.
35
+ * @param {string} input_json
36
+ * @returns {string}
37
+ */
38
+ function analyze_program(input_json) {
39
+ let deferred2_0;
40
+ let deferred2_1;
41
+ try {
42
+ const ptr0 = passStringToWasm0(input_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
43
+ const len0 = WASM_VECTOR_LEN;
44
+ const ret = wasm.analyze_program(ptr0, len0);
45
+ deferred2_0 = ret[0];
46
+ deferred2_1 = ret[1];
47
+ return getStringFromWasm0(ret[0], ret[1]);
48
+ } finally {
49
+ wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
50
+ }
51
+ }
52
+ exports.analyze_program = analyze_program;
53
+
54
+ /**
55
+ * Whole-program shake: analyze + transform. `input` is `{ files: [{id, ast,
56
+ * code}], edges, entries }`. Returns `{ id: slimmedSource }` for every file —
57
+ * byte-for-byte the L0/L1/L1.5 output (the `svelteShaker` equivalent).
58
+ * @param {string} input_json
59
+ * @returns {string}
60
+ */
61
+ function shake_program(input_json) {
62
+ let deferred2_0;
63
+ let deferred2_1;
64
+ try {
65
+ const ptr0 = passStringToWasm0(input_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
66
+ const len0 = WASM_VECTOR_LEN;
67
+ const ret = wasm.shake_program(ptr0, len0);
68
+ deferred2_0 = ret[0];
69
+ deferred2_1 = ret[1];
70
+ return getStringFromWasm0(ret[0], ret[1]);
71
+ } finally {
72
+ wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
73
+ }
74
+ }
75
+ exports.shake_program = shake_program;
76
+
77
+ /**
78
+ * Whole-program shake WITH L2 monomorphization. `input` is the same shape as
79
+ * `shake_program`; `options_json` is `{enabled, maxVariants, minSavings}`;
80
+ * `own_size(source) -> number | null` is the per-module compiled-byte proxy the
81
+ * net-win gate uses (the JS side runs svelte/compiler, so decisions match the TS
82
+ * engine). Returns `{ files: {id: code}, variants: {specifier: code} }`.
83
+ * @param {string} input_json
84
+ * @param {string} options_json
85
+ * @param {Function} own_size
86
+ * @returns {string}
87
+ */
88
+ function shake_program_with_mono(input_json, options_json, own_size) {
89
+ let deferred3_0;
90
+ let deferred3_1;
91
+ try {
92
+ const ptr0 = passStringToWasm0(input_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
93
+ const len0 = WASM_VECTOR_LEN;
94
+ const ptr1 = passStringToWasm0(options_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
95
+ const len1 = WASM_VECTOR_LEN;
96
+ const ret = wasm.shake_program_with_mono(ptr0, len0, ptr1, len1, own_size);
97
+ deferred3_0 = ret[0];
98
+ deferred3_1 = ret[1];
99
+ return getStringFromWasm0(ret[0], ret[1]);
100
+ } finally {
101
+ wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
102
+ }
103
+ }
104
+ exports.shake_program_with_mono = shake_program_with_mono;
105
+ function __wbg_get_imports() {
106
+ const import0 = {
107
+ __proto__: null,
108
+ __wbg___wbindgen_number_get_9bb1761122181af2: function(arg0, arg1) {
109
+ const obj = arg1;
110
+ const ret = typeof(obj) === 'number' ? obj : undefined;
111
+ getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true);
112
+ getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);
113
+ },
114
+ __wbg___wbindgen_throw_1506f2235d1bdba0: function(arg0, arg1) {
115
+ throw new Error(getStringFromWasm0(arg0, arg1));
116
+ },
117
+ __wbg_call_40e4174f169eaca7: function() { return handleError(function (arg0, arg1, arg2, arg3) {
118
+ const ret = arg0.call(arg1, arg2, arg3);
119
+ return ret;
120
+ }, arguments); },
121
+ __wbindgen_cast_0000000000000001: function(arg0, arg1) {
122
+ // Cast intrinsic for `Ref(String) -> Externref`.
123
+ const ret = getStringFromWasm0(arg0, arg1);
124
+ return ret;
125
+ },
126
+ __wbindgen_init_externref_table: function() {
127
+ const table = wasm.__wbindgen_externrefs;
128
+ const offset = table.grow(4);
129
+ table.set(0, undefined);
130
+ table.set(offset + 0, undefined);
131
+ table.set(offset + 1, null);
132
+ table.set(offset + 2, true);
133
+ table.set(offset + 3, false);
134
+ },
135
+ };
136
+ return {
137
+ __proto__: null,
138
+ "./svelte_shaker_engine_bg.js": import0,
139
+ };
140
+ }
141
+
142
+ function addToExternrefTable0(obj) {
143
+ const idx = wasm.__externref_table_alloc();
144
+ wasm.__wbindgen_externrefs.set(idx, obj);
145
+ return idx;
146
+ }
147
+
148
+ let cachedDataViewMemory0 = null;
149
+ function getDataViewMemory0() {
150
+ if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
151
+ cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
152
+ }
153
+ return cachedDataViewMemory0;
154
+ }
155
+
156
+ function getStringFromWasm0(ptr, len) {
157
+ return decodeText(ptr >>> 0, len);
158
+ }
159
+
160
+ let cachedUint8ArrayMemory0 = null;
161
+ function getUint8ArrayMemory0() {
162
+ if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
163
+ cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
164
+ }
165
+ return cachedUint8ArrayMemory0;
166
+ }
167
+
168
+ function handleError(f, args) {
169
+ try {
170
+ return f.apply(this, args);
171
+ } catch (e) {
172
+ const idx = addToExternrefTable0(e);
173
+ wasm.__wbindgen_exn_store(idx);
174
+ }
175
+ }
176
+
177
+ function isLikeNone(x) {
178
+ return x === undefined || x === null;
179
+ }
180
+
181
+ function passStringToWasm0(arg, malloc, realloc) {
182
+ if (realloc === undefined) {
183
+ const buf = cachedTextEncoder.encode(arg);
184
+ const ptr = malloc(buf.length, 1) >>> 0;
185
+ getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
186
+ WASM_VECTOR_LEN = buf.length;
187
+ return ptr;
188
+ }
189
+
190
+ let len = arg.length;
191
+ let ptr = malloc(len, 1) >>> 0;
192
+
193
+ const mem = getUint8ArrayMemory0();
194
+
195
+ let offset = 0;
196
+
197
+ for (; offset < len; offset++) {
198
+ const code = arg.charCodeAt(offset);
199
+ if (code > 0x7F) break;
200
+ mem[ptr + offset] = code;
201
+ }
202
+ if (offset !== len) {
203
+ if (offset !== 0) {
204
+ arg = arg.slice(offset);
205
+ }
206
+ ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
207
+ const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
208
+ const ret = cachedTextEncoder.encodeInto(arg, view);
209
+
210
+ offset += ret.written;
211
+ ptr = realloc(ptr, len, offset, 1) >>> 0;
212
+ }
213
+
214
+ WASM_VECTOR_LEN = offset;
215
+ return ptr;
216
+ }
217
+
218
+ let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
219
+ cachedTextDecoder.decode();
220
+ function decodeText(ptr, len) {
221
+ return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
222
+ }
223
+
224
+ const cachedTextEncoder = new TextEncoder();
225
+
226
+ if (!('encodeInto' in cachedTextEncoder)) {
227
+ cachedTextEncoder.encodeInto = function (arg, view) {
228
+ const buf = cachedTextEncoder.encode(arg);
229
+ view.set(buf);
230
+ return {
231
+ read: arg.length,
232
+ written: buf.length
233
+ };
234
+ };
235
+ }
236
+
237
+ let WASM_VECTOR_LEN = 0;
238
+
239
+ const wasmPath = `${__dirname}/svelte_shaker_engine_bg.wasm`;
240
+ const wasmBytes = require('fs').readFileSync(wasmPath);
241
+ const wasmModule = new WebAssembly.Module(wasmBytes);
242
+ let wasmInstance = new WebAssembly.Instance(wasmModule, __wbg_get_imports());
243
+ let wasm = wasmInstance.exports;
244
+ wasm.__wbindgen_start();
package/dist/vite.d.ts CHANGED
@@ -10,17 +10,35 @@ export interface ShakerOptions {
10
10
  */
11
11
  include?: string[];
12
12
  /**
13
- * Optimization level (docs §3). L0/L1/L1.5 are always on (`level >= 1`); only
14
- * `level: 2` additionally enables L2 per-call-site monomorphization, which is
15
- * OPT-IN. Default `1` behavior with the level unset is unchanged.
13
+ * Optimization level (docs §3). L0/L1/L1.5 are always on; `level: 2`
14
+ * additionally enables L2 per-call-site monomorphization. **Default `2`** — L2
15
+ * is ON by default because it is bail-safe and never bloats (the measured
16
+ * net-win gate, docs §3 L2). Set `level: 1` (or `0`) to turn L2 OFF, e.g. to
17
+ * trade a little compression for faster builds.
16
18
  */
17
19
  level?: 0 | 1 | 2;
18
20
  /**
19
- * L2 monomorphization tuning (docs §13.2). Only consulted when `level: 2`.
20
- * `true` enables it with defaults; an object overrides `maxVariants`. Defaults
21
- * to OFF.
21
+ * L2 monomorphization tuning (docs §13.2). Consulted whenever L2 is active
22
+ * (i.e. not turned off via `level: 1`/`0`). `true`/omitted enables it with
23
+ * defaults; an object overrides `maxVariants` / `minSavings`; `false` turns L2
24
+ * OFF (same as `level: 1`). Raising `maxVariants` lets children with more
25
+ * distinct call-site shapes be specialized — affordable now that the net-win
26
+ * gate only sizes the modules that actually differ (docs §13.2).
22
27
  */
23
28
  monomorphize?: boolean | Partial<Omit<MonomorphizeOptions, 'enabled'>>;
29
+ /**
30
+ * Which engine runs the whole shake (analysis + transform, INCLUDING L2).
31
+ * Default `'auto'`. The native Rust (WASM) engine implements every level — for
32
+ * L2 it calls back into JS only for the per-module compiled-size proxy the
33
+ * net-win gate needs — so it is the default fast path:
34
+ * - `'auto'` — use the native Rust engine when it can be loaded; otherwise fall
35
+ * back to the JS engine.
36
+ * - `'rust'` — force the Rust engine; throws if the WASM module can't be loaded.
37
+ * - `'js'` — force the JS engine.
38
+ * Both engines are differentially tested to produce byte-identical output, so the
39
+ * choice only affects speed, never what is shaken.
40
+ */
41
+ engine?: 'auto' | 'js' | 'rust';
24
42
  /**
25
43
  * Whether to shake in `vite dev` too (docs/RUST-MIGRATION.md §3 M2,
26
44
  * ARCHITECTURE §6.2). Default `false` — dev is a pass-through, which is always
package/dist/vite.js CHANGED
@@ -6,6 +6,7 @@ import { DevShaker } from './engine.js';
6
6
  import { collectSvelteFiles, fsResolve } from './scan.js';
7
7
  import { DEFAULT_MONO_OPTIONS } from './mono.js';
8
8
  import { tryLoadRsvelteParser } from './rsvelte-parse.js';
9
+ import { svelteShakerWasm, svelteShakerWasmWithMono, tryLoadWasmEngine } from './wasm-engine.js';
9
10
  /** kB with two decimals, the unit Vite itself uses in its build size report. */
10
11
  function formatKB(bytes) {
11
12
  return `${(bytes / 1024).toFixed(2)} kB`;
@@ -87,11 +88,13 @@ function reportSizes(shaken, read, root, verbose, log) {
87
88
  const VARIANT_QUERY = 'shaker_variant';
88
89
  /**
89
90
  * Resolve the {@link MonomorphizeOptions} from the public option surface. L2 is
90
- * active only when `level: 2` AND `monomorphize` is truthy; anything else leaves
91
- * it disabled (the default), so existing configs are unaffected.
91
+ * ON by default (it is bail-safe and never bloats); it is disabled only by an
92
+ * explicit opt-out `level: 1`/`0` or `monomorphize: false`. An object
93
+ * `monomorphize` overrides the tuning knobs (`maxVariants` / `minSavings`).
92
94
  */
93
95
  function resolveMono(options) {
94
- if (options.level !== 2 || !options.monomorphize)
96
+ const optedOut = options.level === 0 || options.level === 1 || options.monomorphize === false;
97
+ if (optedOut)
95
98
  return DEFAULT_MONO_OPTIONS;
96
99
  const overrides = typeof options.monomorphize === 'object' ? options.monomorphize : {};
97
100
  return { ...DEFAULT_MONO_OPTIONS, enabled: true, ...overrides };
@@ -266,8 +269,38 @@ export function shaker(options = {}) {
266
269
  return null;
267
270
  return resolved.id.split('?')[0];
268
271
  };
272
+ // Decide the engine. The native Rust engine now implements every level
273
+ // INCLUDING L2 (it calls back to JS only for the compiled-size proxy), so it
274
+ // is the default: `'auto'` uses it whenever it can be loaded and falls back
275
+ // to JS otherwise, `'rust'` forces it (throwing if it can't load), `'js'`
276
+ // forces the JS engine. Both engines produce byte-identical output.
277
+ const engineChoice = options.engine ?? 'auto';
278
+ let wasm = null;
279
+ if (engineChoice === 'rust') {
280
+ wasm = tryLoadWasmEngine();
281
+ if (!wasm)
282
+ throw new Error('[vite-plugin-svelte-shaker] engine: "rust" was requested but the WASM engine ' +
283
+ 'could not be loaded. Remove the option (or use engine: "js") to use the JS engine.');
284
+ }
285
+ else if (engineChoice === 'auto') {
286
+ wasm = tryLoadWasmEngine();
287
+ }
288
+ if (wasm) {
289
+ // Native Rust engine — byte-identical to the JS engine, including L2.
290
+ if (mono.enabled) {
291
+ const result = await svelteShakerWasmWithMono(wasm, entries, resolve, read, mono, getParse());
292
+ shaken = result.files;
293
+ variantSources = result.variants;
294
+ }
295
+ else {
296
+ shaken = await svelteShakerWasm(wasm, entries, resolve, read, getParse());
297
+ variantSources = new Map();
298
+ }
299
+ reportSizes(shaken, read, root, options.verbose === true, log);
300
+ return;
301
+ }
269
302
  if (!mono.enabled) {
270
- // Default path: byte-for-byte the L0/L1/L1.5 output (no L2 at all).
303
+ // JS engine, L2 off: byte-for-byte the L0/L1/L1.5 output.
271
304
  shaken = await svelteShaker(entries, resolve, read, getParse());
272
305
  variantSources = new Map();
273
306
  reportSizes(shaken, read, root, options.verbose === true, log);
@@ -0,0 +1,56 @@
1
+ import { type ReadFile, type Resolve } from './analyze.js';
2
+ import { type Parse } from './parse.js';
3
+ import { type MonomorphizeOptions } from './mono.js';
4
+ import type { ComponentId } from './ir.js';
5
+ /** The subset of the WASM exports the plugin uses (docs/RUST-MIGRATION.md M5+). */
6
+ interface WasmEngine {
7
+ /** Whole-program L0/L1/L1.5 shake: input JSON in, `{id: shakenCode}` JSON out. */
8
+ shake_program: (inputJson: string) => string;
9
+ /**
10
+ * Whole-program shake WITH L2 monomorphization. `ownSize(id, source)` is the
11
+ * per-module compiled-byte proxy the net-win gate calls back into JS for (the
12
+ * Svelte compiler has no in-WASM equivalent); returns
13
+ * `{ files: {id: code}, variants: {specifier: code} }` JSON.
14
+ */
15
+ shake_program_with_mono: (inputJson: string, optionsJson: string, ownSize: (id: string, source: string) => number | null) => string;
16
+ }
17
+ /**
18
+ * Load the native Rust (WASM) engine, or `null` if it can't be loaded (no built
19
+ * artifact for this install). Two locations are tried in order:
20
+ * - `./svelte_shaker_engine.js` — the PUBLISHED layout, where `build` copies the
21
+ * artifact next to `dist/wasm-engine.js` (wasm-pack's `pkg/.gitignore` of `*`
22
+ * keeps `engine-rs/pkg` out of the npm tarball, so we ship it via `dist/`).
23
+ * - `../engine-rs/pkg/svelte_shaker_engine.js` — the REPO layout, used when
24
+ * running from source in the package's own tests (no copy step has run).
25
+ */
26
+ export declare function tryLoadWasmEngine(): WasmEngine | null;
27
+ /**
28
+ * Whole-program shake via the native Rust engine — the L0/L1/L1.5 counterpart of
29
+ * {@link svelteShaker} (L2 lives only in the JS engine). The crawl/resolution
30
+ * stays in JS ({@link buildAnalyzeInput}); we hand the Rust engine the resolved
31
+ * graph plus each file's AST and source as JSON, exactly as the differential
32
+ * `wasm-shake` test does — so the output is byte-identical to the JS engine.
33
+ *
34
+ * The same parse cache feeds the crawl and the program input, so each file is
35
+ * parsed once. A final self-check mirrors the JS engine's `revertUnparseable`:
36
+ * any emitted file that no longer parses is reverted to its original (a sound
37
+ * "did not shake this component"), so a single mishandled shape can never break
38
+ * the build.
39
+ */
40
+ export declare function svelteShakerWasm(engine: WasmEngine, entries: ComponentId | ComponentId[], resolve: Resolve, readFile: ReadFile, parse?: Parse): Promise<Record<ComponentId, string>>;
41
+ /** The output of a Rust L2 shake: the wired owner files + the variant residuals
42
+ * keyed by their request specifier (what the Shell's `load` hook serves). */
43
+ export interface WasmMonoResult {
44
+ files: Record<ComponentId, string>;
45
+ variants: Map<string, string>;
46
+ }
47
+ /**
48
+ * Whole-program shake WITH L2 monomorphization, run entirely in the native Rust
49
+ * engine — the counterpart of {@link svelteShakerWithMono}. The crawl/resolution
50
+ * stays in JS; the Rust engine does the analysis, the L2 graph/gate, and the
51
+ * call-site rewrite, calling back into JS only for {@link ownSize} (the Svelte
52
+ * compiler). Feeding it the same compiler the JS engine uses makes the result
53
+ * byte-identical (pinned by the differential `wasm-mono` test).
54
+ */
55
+ export declare function svelteShakerWasmWithMono(engine: WasmEngine, entries: ComponentId | ComponentId[], resolve: Resolve, readFile: ReadFile, mono: MonomorphizeOptions, parse?: Parse): Promise<WasmMonoResult>;
56
+ export {};
@@ -0,0 +1,120 @@
1
+ import { createRequire } from 'node:module';
2
+ import { compile } from 'svelte/compiler';
3
+ import { buildAnalyzeInput } from './analyze.js';
4
+ import { parseCached, parseSvelte } from './parse.js';
5
+ import {} from './mono.js';
6
+ // NODE-ONLY: loads the native Rust (WASM) engine and drives it from the Vite
7
+ // plugin. Imported only by `vite.ts` (a Node entry), never by the environment-free
8
+ // engine (`index.ts`/`analyze.ts`), so the browser playground build stays clean.
9
+ const require = createRequire(import.meta.url);
10
+ /**
11
+ * Load the native Rust (WASM) engine, or `null` if it can't be loaded (no built
12
+ * artifact for this install). Two locations are tried in order:
13
+ * - `./svelte_shaker_engine.js` — the PUBLISHED layout, where `build` copies the
14
+ * artifact next to `dist/wasm-engine.js` (wasm-pack's `pkg/.gitignore` of `*`
15
+ * keeps `engine-rs/pkg` out of the npm tarball, so we ship it via `dist/`).
16
+ * - `../engine-rs/pkg/svelte_shaker_engine.js` — the REPO layout, used when
17
+ * running from source in the package's own tests (no copy step has run).
18
+ */
19
+ export function tryLoadWasmEngine() {
20
+ for (const spec of ['./svelte_shaker_engine.js', '../engine-rs/pkg/svelte_shaker_engine.js']) {
21
+ try {
22
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
23
+ const mod = require(spec);
24
+ if (typeof mod.shake_program === 'function' &&
25
+ typeof mod.shake_program_with_mono === 'function')
26
+ return {
27
+ shake_program: mod.shake_program.bind(mod),
28
+ shake_program_with_mono: mod.shake_program_with_mono.bind(mod),
29
+ };
30
+ }
31
+ catch {
32
+ // Try the next location.
33
+ }
34
+ }
35
+ return null;
36
+ }
37
+ /** The compiled-byte size proxy the L2 net-win gate uses — the same call
38
+ * `mono.ts` makes, so the Rust gate decides byte-for-byte like the JS engine. */
39
+ function ownSize(id, source) {
40
+ try {
41
+ return compile(source, { generate: 'client', dev: false, filename: id }).js.code.length;
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
47
+ /**
48
+ * Whole-program shake via the native Rust engine — the L0/L1/L1.5 counterpart of
49
+ * {@link svelteShaker} (L2 lives only in the JS engine). The crawl/resolution
50
+ * stays in JS ({@link buildAnalyzeInput}); we hand the Rust engine the resolved
51
+ * graph plus each file's AST and source as JSON, exactly as the differential
52
+ * `wasm-shake` test does — so the output is byte-identical to the JS engine.
53
+ *
54
+ * The same parse cache feeds the crawl and the program input, so each file is
55
+ * parsed once. A final self-check mirrors the JS engine's `revertUnparseable`:
56
+ * any emitted file that no longer parses is reverted to its original (a sound
57
+ * "did not shake this component"), so a single mishandled shape can never break
58
+ * the build.
59
+ */
60
+ export async function svelteShakerWasm(engine, entries, resolve, readFile, parse) {
61
+ const cache = new Map();
62
+ const input = await buildAnalyzeInput(entries, resolve, readFile, cache, parse);
63
+ const programInput = {
64
+ files: input.files.map((f) => ({
65
+ id: f.id,
66
+ ast: parseCached(f.id, f.code, cache, parse),
67
+ code: f.code,
68
+ })),
69
+ edges: input.edges,
70
+ entries: input.entries,
71
+ };
72
+ const out = JSON.parse(engine.shake_program(JSON.stringify(programInput)));
73
+ revertUnparseable(out, input.files);
74
+ return out;
75
+ }
76
+ /**
77
+ * Whole-program shake WITH L2 monomorphization, run entirely in the native Rust
78
+ * engine — the counterpart of {@link svelteShakerWithMono}. The crawl/resolution
79
+ * stays in JS; the Rust engine does the analysis, the L2 graph/gate, and the
80
+ * call-site rewrite, calling back into JS only for {@link ownSize} (the Svelte
81
+ * compiler). Feeding it the same compiler the JS engine uses makes the result
82
+ * byte-identical (pinned by the differential `wasm-mono` test).
83
+ */
84
+ export async function svelteShakerWasmWithMono(engine, entries, resolve, readFile, mono, parse) {
85
+ const cache = new Map();
86
+ const input = await buildAnalyzeInput(entries, resolve, readFile, cache, parse);
87
+ const programInput = {
88
+ files: input.files.map((f) => ({
89
+ id: f.id,
90
+ ast: parseCached(f.id, f.code, cache, parse),
91
+ code: f.code,
92
+ })),
93
+ edges: input.edges,
94
+ entries: input.entries,
95
+ };
96
+ const options = JSON.stringify({
97
+ enabled: mono.enabled,
98
+ maxVariants: mono.maxVariants,
99
+ minSavings: mono.minSavings,
100
+ });
101
+ const result = JSON.parse(engine.shake_program_with_mono(JSON.stringify(programInput), options, ownSize));
102
+ revertUnparseable(result.files, input.files);
103
+ return { files: result.files, variants: new Map(Object.entries(result.variants)) };
104
+ }
105
+ /** Self-check: revert any emitted file that no longer parses to its original — a
106
+ * sound "did not shake this component", mirroring the JS engine's last line of
107
+ * defense ({@link svelteShaker}'s `revertUnparseable`). */
108
+ function revertUnparseable(out, files) {
109
+ for (const file of files) {
110
+ const code = out[file.id];
111
+ if (code === undefined || code === file.code)
112
+ continue;
113
+ try {
114
+ parseSvelte(code, file.id);
115
+ }
116
+ catch {
117
+ out[file.id] = file.code;
118
+ }
119
+ }
120
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-shaker",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Tree shaking for Svelte components",
5
5
  "keywords": [
6
6
  "dead-code-elimination",
@@ -69,7 +69,7 @@
69
69
  "node": ">=22"
70
70
  },
71
71
  "scripts": {
72
- "build": "tsc -p tsconfig.build.json",
72
+ "build": "tsc -p tsconfig.build.json && node scripts/copy-wasm.mjs",
73
73
  "build:wasm": "wasm-pack build engine-rs --target nodejs --release --out-dir pkg",
74
74
  "dev": "tsc -p tsconfig.build.json -w",
75
75
  "test:watch": "vitest",