svelte-shaker 0.8.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
@@ -110,11 +110,11 @@ shaker({
110
110
  shaker({ include: ['src'], level: 1 }); // L2 off (L0/L1/L1.5 only)
111
111
  shaker({ include: ['src'], monomorphize: { maxVariants: 16 } }); // raise the variant cap
112
112
 
113
- // Run the analysis + transform in the native Rust (WASM) engine. It implements
114
- // L0/L1/L1.5 and is differentially tested to be byte-identical to the JS engine,
115
- // so it only changes speed. L2 lives only in the JS engine, so the Rust engine
116
- // skips it pair it with `level: 1`:
117
- shaker({ include: ['src'], level: 1, engine: 'rust' });
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'
118
118
 
119
119
  // Opt into the faster rsvelte parser (~1.46x full build, ~2.2x parse).
120
120
  // Requires the optional peer `@rsvelte/vite-plugin-svelte-native` (install it
@@ -73,9 +73,56 @@ function shake_program(input_json) {
73
73
  }
74
74
  }
75
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;
76
105
  function __wbg_get_imports() {
77
106
  const import0 = {
78
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
+ },
79
126
  __wbindgen_init_externref_table: function() {
80
127
  const table = wasm.__wbindgen_externrefs;
81
128
  const offset = table.grow(4);
@@ -92,6 +139,20 @@ function __wbg_get_imports() {
92
139
  };
93
140
  }
94
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
+
95
156
  function getStringFromWasm0(ptr, len) {
96
157
  return decodeText(ptr >>> 0, len);
97
158
  }
@@ -104,6 +165,19 @@ function getUint8ArrayMemory0() {
104
165
  return cachedUint8ArrayMemory0;
105
166
  }
106
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
+
107
181
  function passStringToWasm0(arg, malloc, realloc) {
108
182
  if (realloc === undefined) {
109
183
  const buf = cachedTextEncoder.encode(arg);
Binary file
package/dist/vite.d.ts CHANGED
@@ -14,8 +14,7 @@ export interface ShakerOptions {
14
14
  * additionally enables L2 per-call-site monomorphization. **Default `2`** — L2
15
15
  * is ON by default because it is bail-safe and never bloats (the measured
16
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, or to use the Rust {@link
18
- * engine} (which implements L0/L1/L1.5 only).
17
+ * trade a little compression for faster builds.
19
18
  */
20
19
  level?: 0 | 1 | 2;
21
20
  /**
@@ -28,17 +27,16 @@ export interface ShakerOptions {
28
27
  */
29
28
  monomorphize?: boolean | Partial<Omit<MonomorphizeOptions, 'enabled'>>;
30
29
  /**
31
- * Which engine runs the L0/L1/L1.5 analysis + transform. Default `'auto'`.
32
- * - `'auto'` — use the native Rust (WASM) engine when it can be loaded AND L2
33
- * is off; otherwise fall back to the JS engine. Since L2 lives only in the
34
- * JS engine, a build with L2 active (the default) runs on JS; turn L2 off
35
- * (`level: 1`) to get the faster Rust path.
36
- * - `'rust'` — force the Rust engine. L2 is not available there, so it is
37
- * skipped (a one-time notice is logged) even if requested. Throws if the
38
- * WASM module cannot be loaded.
39
- * - `'js'` force the JS engine (the established, L2-capable path).
40
- * The Rust engine is differentially tested to produce byte-identical output to
41
- * the JS engine, so the choice only affects speed, never what is shaken.
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.
42
40
  */
43
41
  engine?: 'auto' | 'js' | 'rust';
44
42
  /**
package/dist/vite.js CHANGED
@@ -6,7 +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, tryLoadWasmEngine } from './wasm-engine.js';
9
+ import { svelteShakerWasm, svelteShakerWasmWithMono, tryLoadWasmEngine } from './wasm-engine.js';
10
10
  /** kB with two decimals, the unit Vite itself uses in its build size report. */
11
11
  function formatKB(bytes) {
12
12
  return `${(bytes / 1024).toFixed(2)} kB`;
@@ -269,41 +269,44 @@ export function shaker(options = {}) {
269
269
  return null;
270
270
  return resolved.id.split('?')[0];
271
271
  };
272
- // Decide the engine. L2 (the default) lives only in the JS engine, so it
273
- // takes precedence; the native Rust engine drives the L0/L1/L1.5 path when
274
- // selected (`engine: 'rust'`) or under `'auto'` whenever L2 is off.
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.
275
277
  const engineChoice = options.engine ?? 'auto';
276
- let effectiveMono = mono;
277
278
  let wasm = null;
278
279
  if (engineChoice === 'rust') {
279
- if (mono.enabled) {
280
- log('engine: "rust" runs L0/L1/L1.5 only — L2 monomorphization (JS-only) is skipped');
281
- effectiveMono = DEFAULT_MONO_OPTIONS;
282
- }
283
280
  wasm = tryLoadWasmEngine();
284
281
  if (!wasm)
285
282
  throw new Error('[vite-plugin-svelte-shaker] engine: "rust" was requested but the WASM engine ' +
286
283
  'could not be loaded. Remove the option (or use engine: "js") to use the JS engine.');
287
284
  }
288
- else if (engineChoice === 'auto' && !mono.enabled) {
289
- // Auto: prefer the native engine on the L2-off path; fall back to JS silently.
285
+ else if (engineChoice === 'auto') {
290
286
  wasm = tryLoadWasmEngine();
291
287
  }
292
288
  if (wasm) {
293
- // Native Rust L0/L1/L1.5 — byte-identical to the JS engine (no L2 here).
294
- shaken = await svelteShakerWasm(wasm, entries, resolve, read, getParse());
295
- variantSources = new Map();
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
+ }
296
299
  reportSizes(shaken, read, root, options.verbose === true, log);
297
300
  return;
298
301
  }
299
- if (!effectiveMono.enabled) {
302
+ if (!mono.enabled) {
300
303
  // JS engine, L2 off: byte-for-byte the L0/L1/L1.5 output.
301
304
  shaken = await svelteShaker(entries, resolve, read, getParse());
302
305
  variantSources = new Map();
303
306
  reportSizes(shaken, read, root, options.verbose === true, log);
304
307
  return;
305
308
  }
306
- const result = await svelteShakerWithMono(entries, resolve, read, effectiveMono, variantSpecifier, getParse());
309
+ const result = await svelteShakerWithMono(entries, resolve, read, mono, variantSpecifier, getParse());
307
310
  shaken = result.files;
308
311
  variantSources = new Map();
309
312
  for (const v of result.mono.variants.values())
@@ -1,10 +1,18 @@
1
1
  import { type ReadFile, type Resolve } from './analyze.js';
2
2
  import { type Parse } from './parse.js';
3
+ import { type MonomorphizeOptions } from './mono.js';
3
4
  import type { ComponentId } from './ir.js';
4
- /** The subset of the WASM exports the plugin uses (docs/RUST-MIGRATION.md M5). */
5
+ /** The subset of the WASM exports the plugin uses (docs/RUST-MIGRATION.md M5+). */
5
6
  interface WasmEngine {
6
7
  /** Whole-program L0/L1/L1.5 shake: input JSON in, `{id: shakenCode}` JSON out. */
7
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;
8
16
  }
9
17
  /**
10
18
  * Load the native Rust (WASM) engine, or `null` if it can't be loaded (no built
@@ -30,4 +38,19 @@ export declare function tryLoadWasmEngine(): WasmEngine | null;
30
38
  * the build.
31
39
  */
32
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>;
33
56
  export {};
@@ -1,6 +1,8 @@
1
1
  import { createRequire } from 'node:module';
2
+ import { compile } from 'svelte/compiler';
2
3
  import { buildAnalyzeInput } from './analyze.js';
3
4
  import { parseCached, parseSvelte } from './parse.js';
5
+ import {} from './mono.js';
4
6
  // NODE-ONLY: loads the native Rust (WASM) engine and drives it from the Vite
5
7
  // plugin. Imported only by `vite.ts` (a Node entry), never by the environment-free
6
8
  // engine (`index.ts`/`analyze.ts`), so the browser playground build stays clean.
@@ -19,8 +21,12 @@ export function tryLoadWasmEngine() {
19
21
  try {
20
22
  // eslint-disable-next-line @typescript-eslint/no-require-imports
21
23
  const mod = require(spec);
22
- if (typeof mod.shake_program === 'function')
23
- return { shake_program: mod.shake_program.bind(mod) };
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
+ };
24
30
  }
25
31
  catch {
26
32
  // Try the next location.
@@ -28,6 +34,16 @@ export function tryLoadWasmEngine() {
28
34
  }
29
35
  return null;
30
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
+ }
31
47
  /**
32
48
  * Whole-program shake via the native Rust engine — the L0/L1/L1.5 counterpart of
33
49
  * {@link svelteShaker} (L2 lives only in the JS engine). The crawl/resolution
@@ -54,7 +70,43 @@ export async function svelteShakerWasm(engine, entries, resolve, readFile, parse
54
70
  entries: input.entries,
55
71
  };
56
72
  const out = JSON.parse(engine.shake_program(JSON.stringify(programInput)));
57
- for (const file of input.files) {
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) {
58
110
  const code = out[file.id];
59
111
  if (code === undefined || code === file.code)
60
112
  continue;
@@ -65,5 +117,4 @@ export async function svelteShakerWasm(engine, entries, resolve, readFile, parse
65
117
  out[file.id] = file.code;
66
118
  }
67
119
  }
68
- return out;
69
120
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-shaker",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Tree shaking for Svelte components",
5
5
  "keywords": [
6
6
  "dead-code-elimination",