svelte-shaker 0.6.0 → 0.8.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 +14 -6
- package/dist/analyze.d.ts +20 -7
- package/dist/analyze.js +70 -2
- package/dist/mono.js +52 -28
- package/dist/parse.d.ts +4 -0
- package/dist/svelte_shaker_engine.js +170 -0
- package/dist/svelte_shaker_engine_bg.wasm +0 -0
- package/dist/vite.d.ts +26 -6
- package/dist/vite.js +75 -6
- package/dist/wasm-engine.d.ts +33 -0
- package/dist/wasm-engine.js +69 -0
- package/package.json +2 -2
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:
|
|
103
|
-
monomorphize:
|
|
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
|
-
//
|
|
108
|
-
|
|
109
|
-
shaker({ include: ['src'], level:
|
|
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
|
+
// 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' });
|
|
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`) |
|
|
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/analyze.d.ts
CHANGED
|
@@ -81,11 +81,15 @@ export interface ChildCall {
|
|
|
81
81
|
}
|
|
82
82
|
/**
|
|
83
83
|
* One value passed explicitly to a prop at one call site, after last-write-wins
|
|
84
|
-
* has been resolved (`{
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
84
|
+
* has been resolved. An explicit write is either a real attribute (`a={1}`) OR a
|
|
85
|
+
* key expanded out of a statically-known object-literal spread (`{...{a:1}}`,
|
|
86
|
+
* docs §4.1) — both name a prop and a value the same way. `dynamic` means the
|
|
87
|
+
* value is non-literal (`bind:`, a dynamic expression, or a spread key whose
|
|
88
|
+
* value is non-literal): used, value not statically known. `afterLastSpread`
|
|
89
|
+
* records whether this write happened after the site's last *unknown* spread (one
|
|
90
|
+
* we could not expand) — only then can no spread silently override it (docs §4.1,
|
|
91
|
+
* "後勝ち順序で救う"). A known object-literal spread is expanded, not opaque, so
|
|
92
|
+
* it never counts as an "unknown spread" here.
|
|
89
93
|
*/
|
|
90
94
|
export interface ExplicitProp {
|
|
91
95
|
value: Literal;
|
|
@@ -94,7 +98,12 @@ export interface ExplicitProp {
|
|
|
94
98
|
}
|
|
95
99
|
/** How a child component is called at one `<Child .../>` site. */
|
|
96
100
|
export interface CallSite {
|
|
97
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* Did this site have at least one spread we could NOT statically expand (an
|
|
103
|
+
* identifier / call / `{...{…computed/nested…}}`)? A fully-known object-literal
|
|
104
|
+
* spread is expanded into {@link ExplicitProp} writes instead, so it does not
|
|
105
|
+
* set this — only an opaque spread, which may set any prop, does (docs §4.1).
|
|
106
|
+
*/
|
|
98
107
|
hadSpread: boolean;
|
|
99
108
|
/** Last-write-wins explicit props at this site, keyed by prop name. */
|
|
100
109
|
explicit: Map<string, ExplicitProp>;
|
|
@@ -160,7 +169,11 @@ export declare function remapToLocalNames<V>(map: Map<string, V>, model: FileMod
|
|
|
160
169
|
* Read one `<Child .../>` into a {@link CallSite}. Attributes are in source
|
|
161
170
|
* order, so we resolve last-write-wins (a later `a={…}` overrides an earlier
|
|
162
171
|
* one) and record, per prop, whether its winning write came *after* the last
|
|
163
|
-
* spread — the only case a spread cannot silently override it (docs
|
|
172
|
+
* *unknown* spread — the only case a spread cannot silently override it (docs
|
|
173
|
+
* §4.1). A statically-known object-literal spread (`{...{a:1, b:2}}`) is not
|
|
174
|
+
* opaque: we expand its keys into explicit writes at the spread's position, so it
|
|
175
|
+
* both contributes those literals AND does not poison props it cannot set (docs
|
|
176
|
+
* §4.1, "{...obj} が object literal ならキー展開").
|
|
164
177
|
*/
|
|
165
178
|
export declare function readCallSite(component: AnyNode): CallSite;
|
|
166
179
|
/** Decide what to fold for one component from its global usage. */
|
package/dist/analyze.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseCached, parseSvelte, walk, } from './parse.js';
|
|
2
2
|
import { emptyPlan, } from './ir.js';
|
|
3
3
|
import { computeDeadSpans, inSpans } from './dead.js';
|
|
4
|
+
import { evaluate } from './eval.js';
|
|
4
5
|
const isSvelte = (source) => source.endsWith('.svelte');
|
|
5
6
|
/** Hard cap on fixpoint iterations: convergence is monotone (dead spans only
|
|
6
7
|
* grow as profiles shrink), so this is reached only if something is non-monotone
|
|
@@ -672,18 +673,40 @@ function memberComponentTags(ast) {
|
|
|
672
673
|
* Read one `<Child .../>` into a {@link CallSite}. Attributes are in source
|
|
673
674
|
* order, so we resolve last-write-wins (a later `a={…}` overrides an earlier
|
|
674
675
|
* one) and record, per prop, whether its winning write came *after* the last
|
|
675
|
-
* spread — the only case a spread cannot silently override it (docs
|
|
676
|
+
* *unknown* spread — the only case a spread cannot silently override it (docs
|
|
677
|
+
* §4.1). A statically-known object-literal spread (`{...{a:1, b:2}}`) is not
|
|
678
|
+
* opaque: we expand its keys into explicit writes at the spread's position, so it
|
|
679
|
+
* both contributes those literals AND does not poison props it cannot set (docs
|
|
680
|
+
* §4.1, "{...obj} が object literal ならキー展開").
|
|
676
681
|
*/
|
|
677
682
|
export function readCallSite(component) {
|
|
678
683
|
const attrs = component.attributes ?? [];
|
|
684
|
+
// Only spreads we CANNOT expand are opaque (may set any prop). Classify first
|
|
685
|
+
// so `afterLastSpread` is measured against the last *unknown* spread, not a
|
|
686
|
+
// known object literal we are about to expand into explicit writes.
|
|
679
687
|
let lastSpreadIndex = -1;
|
|
680
688
|
for (let i = 0; i < attrs.length; i++) {
|
|
681
|
-
|
|
689
|
+
const attr = attrs[i];
|
|
690
|
+
if (attr.type === 'SpreadAttribute' && knownSpreadEntries(attr) === null)
|
|
682
691
|
lastSpreadIndex = i;
|
|
683
692
|
}
|
|
684
693
|
const explicit = new Map();
|
|
685
694
|
for (let i = 0; i < attrs.length; i++) {
|
|
686
695
|
const attr = attrs[i];
|
|
696
|
+
if (attr.type === 'SpreadAttribute') {
|
|
697
|
+
// A known object-literal spread expands to one explicit write per key, at
|
|
698
|
+
// this spread's position; an unknown spread is opaque and handled by the
|
|
699
|
+
// `hadSpread`/`afterLastSpread` poisoning in `valueSetFor`.
|
|
700
|
+
const entries = knownSpreadEntries(attr);
|
|
701
|
+
if (entries) {
|
|
702
|
+
for (const [name, value] of entries) {
|
|
703
|
+
explicit.set(name, value.known
|
|
704
|
+
? { value: value.value, dynamic: false, afterLastSpread: i > lastSpreadIndex }
|
|
705
|
+
: dynamicWrite(i, lastSpreadIndex));
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
687
710
|
const name = attr.name;
|
|
688
711
|
if (attr.type === 'BindDirective') {
|
|
689
712
|
// `bind:prop` is a used, dynamic two-way binding (docs §4.1).
|
|
@@ -759,6 +782,51 @@ function dynamicWrite(index, lastSpreadIndex) {
|
|
|
759
782
|
afterLastSpread: index > lastSpreadIndex,
|
|
760
783
|
};
|
|
761
784
|
}
|
|
785
|
+
/**
|
|
786
|
+
* The `[name, value]` entries a spread contributes IF it is a statically-known
|
|
787
|
+
* object literal whose complete key set we can see (docs §4.1 "object literal な
|
|
788
|
+
* spread はキー展開"). Returns `null` for any spread we cannot fully expand — an
|
|
789
|
+
* identifier/call (`{...rest}`), or an object literal carrying a nested spread
|
|
790
|
+
* (`{...{...x}}`), a computed key (`{...{[k]: 1}}`), or a getter/setter/method —
|
|
791
|
+
* because then we do not know the full set of props it sets and must treat it as
|
|
792
|
+
* opaque. Each entry's value is `{known:true,value}` for a literal (so it folds)
|
|
793
|
+
* or `{known:false}` for a non-literal value (key known, value dynamic): both are
|
|
794
|
+
* sound, since the key set is fully known either way.
|
|
795
|
+
*/
|
|
796
|
+
function knownSpreadEntries(attr) {
|
|
797
|
+
const obj = attr.expression;
|
|
798
|
+
if (obj?.type !== 'ObjectExpression')
|
|
799
|
+
return null;
|
|
800
|
+
const entries = [];
|
|
801
|
+
for (const prop of obj.properties ?? []) {
|
|
802
|
+
// A nested spread, computed key, or accessor/method means the full key set is
|
|
803
|
+
// not statically knowable -> the whole spread is opaque.
|
|
804
|
+
if (prop.type !== 'Property')
|
|
805
|
+
return null;
|
|
806
|
+
if (prop.computed === true ||
|
|
807
|
+
prop.kind === 'get' ||
|
|
808
|
+
prop.kind === 'set' ||
|
|
809
|
+
prop.method === true)
|
|
810
|
+
return null;
|
|
811
|
+
const key = prop.key;
|
|
812
|
+
const name = key?.type === 'Identifier'
|
|
813
|
+
? key.name
|
|
814
|
+
: key?.type === 'Literal' &&
|
|
815
|
+
(typeof key.value === 'string' || typeof key.value === 'number')
|
|
816
|
+
? String(key.value)
|
|
817
|
+
: null;
|
|
818
|
+
if (name == null)
|
|
819
|
+
return null;
|
|
820
|
+
entries.push([name, evalToLiteral(prop.value)]);
|
|
821
|
+
}
|
|
822
|
+
return entries;
|
|
823
|
+
}
|
|
824
|
+
/** Constant-evaluate a spread property value with no environment (literals + the
|
|
825
|
+
* tiny pure operator fragment), as `{known:true,value}` or `{known:false}`. */
|
|
826
|
+
function evalToLiteral(node) {
|
|
827
|
+
const r = evaluate(node, new Map());
|
|
828
|
+
return r.known ? { known: true, value: r.value } : { known: false };
|
|
829
|
+
}
|
|
762
830
|
/** Extract a literal from an attribute value, or `{ known:false }`. */
|
|
763
831
|
function literalAttrValue(value) {
|
|
764
832
|
if (value === true)
|
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
|
-
// ---
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
|
284
|
-
const
|
|
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 (
|
|
289
|
+
if (specComponents.has(id))
|
|
299
290
|
continue;
|
|
300
|
-
|
|
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 (
|
|
300
|
+
if (specVariants.has(vid))
|
|
310
301
|
continue;
|
|
311
|
-
|
|
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
|
-
|
|
319
|
-
for
|
|
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
|
-
|
|
323
|
+
return false; // un-sizable -> decline
|
|
324
|
+
baseSide += size;
|
|
324
325
|
}
|
|
325
|
-
for (const vid of
|
|
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
|
-
|
|
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
|
|
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
|
package/dist/parse.d.ts
CHANGED
|
@@ -67,6 +67,10 @@ export interface AnyNode {
|
|
|
67
67
|
computed?: boolean | undefined;
|
|
68
68
|
shorthand?: boolean | undefined;
|
|
69
69
|
elseif?: boolean | undefined;
|
|
70
|
+
/** ObjectExpression `Property` accessor kind (`init` / `get` / `set`). */
|
|
71
|
+
kind?: string | undefined;
|
|
72
|
+
/** ObjectExpression `Property` shorthand-method flag (`{ m() {} }`). */
|
|
73
|
+
method?: boolean | undefined;
|
|
70
74
|
value?: unknown;
|
|
71
75
|
}
|
|
72
76
|
export interface Root extends AnyNode {
|
|
@@ -0,0 +1,170 @@
|
|
|
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
|
+
function __wbg_get_imports() {
|
|
77
|
+
const import0 = {
|
|
78
|
+
__proto__: null,
|
|
79
|
+
__wbindgen_init_externref_table: function() {
|
|
80
|
+
const table = wasm.__wbindgen_externrefs;
|
|
81
|
+
const offset = table.grow(4);
|
|
82
|
+
table.set(0, undefined);
|
|
83
|
+
table.set(offset + 0, undefined);
|
|
84
|
+
table.set(offset + 1, null);
|
|
85
|
+
table.set(offset + 2, true);
|
|
86
|
+
table.set(offset + 3, false);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
return {
|
|
90
|
+
__proto__: null,
|
|
91
|
+
"./svelte_shaker_engine_bg.js": import0,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getStringFromWasm0(ptr, len) {
|
|
96
|
+
return decodeText(ptr >>> 0, len);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let cachedUint8ArrayMemory0 = null;
|
|
100
|
+
function getUint8ArrayMemory0() {
|
|
101
|
+
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
|
|
102
|
+
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
|
103
|
+
}
|
|
104
|
+
return cachedUint8ArrayMemory0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function passStringToWasm0(arg, malloc, realloc) {
|
|
108
|
+
if (realloc === undefined) {
|
|
109
|
+
const buf = cachedTextEncoder.encode(arg);
|
|
110
|
+
const ptr = malloc(buf.length, 1) >>> 0;
|
|
111
|
+
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
|
|
112
|
+
WASM_VECTOR_LEN = buf.length;
|
|
113
|
+
return ptr;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let len = arg.length;
|
|
117
|
+
let ptr = malloc(len, 1) >>> 0;
|
|
118
|
+
|
|
119
|
+
const mem = getUint8ArrayMemory0();
|
|
120
|
+
|
|
121
|
+
let offset = 0;
|
|
122
|
+
|
|
123
|
+
for (; offset < len; offset++) {
|
|
124
|
+
const code = arg.charCodeAt(offset);
|
|
125
|
+
if (code > 0x7F) break;
|
|
126
|
+
mem[ptr + offset] = code;
|
|
127
|
+
}
|
|
128
|
+
if (offset !== len) {
|
|
129
|
+
if (offset !== 0) {
|
|
130
|
+
arg = arg.slice(offset);
|
|
131
|
+
}
|
|
132
|
+
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
|
|
133
|
+
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
|
134
|
+
const ret = cachedTextEncoder.encodeInto(arg, view);
|
|
135
|
+
|
|
136
|
+
offset += ret.written;
|
|
137
|
+
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
WASM_VECTOR_LEN = offset;
|
|
141
|
+
return ptr;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
|
145
|
+
cachedTextDecoder.decode();
|
|
146
|
+
function decodeText(ptr, len) {
|
|
147
|
+
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const cachedTextEncoder = new TextEncoder();
|
|
151
|
+
|
|
152
|
+
if (!('encodeInto' in cachedTextEncoder)) {
|
|
153
|
+
cachedTextEncoder.encodeInto = function (arg, view) {
|
|
154
|
+
const buf = cachedTextEncoder.encode(arg);
|
|
155
|
+
view.set(buf);
|
|
156
|
+
return {
|
|
157
|
+
read: arg.length,
|
|
158
|
+
written: buf.length
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let WASM_VECTOR_LEN = 0;
|
|
164
|
+
|
|
165
|
+
const wasmPath = `${__dirname}/svelte_shaker_engine_bg.wasm`;
|
|
166
|
+
const wasmBytes = require('fs').readFileSync(wasmPath);
|
|
167
|
+
const wasmModule = new WebAssembly.Module(wasmBytes);
|
|
168
|
+
let wasmInstance = new WebAssembly.Instance(wasmModule, __wbg_get_imports());
|
|
169
|
+
let wasm = wasmInstance.exports;
|
|
170
|
+
wasm.__wbindgen_start();
|
|
Binary file
|
package/dist/vite.d.ts
CHANGED
|
@@ -10,17 +10,37 @@ export interface ShakerOptions {
|
|
|
10
10
|
*/
|
|
11
11
|
include?: string[];
|
|
12
12
|
/**
|
|
13
|
-
* Optimization level (docs §3). L0/L1/L1.5 are always on
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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, or to use the Rust {@link
|
|
18
|
+
* engine} (which implements L0/L1/L1.5 only).
|
|
16
19
|
*/
|
|
17
20
|
level?: 0 | 1 | 2;
|
|
18
21
|
/**
|
|
19
|
-
* L2 monomorphization tuning (docs §13.2).
|
|
20
|
-
* `true
|
|
21
|
-
*
|
|
22
|
+
* L2 monomorphization tuning (docs §13.2). Consulted whenever L2 is active
|
|
23
|
+
* (i.e. not turned off via `level: 1`/`0`). `true`/omitted enables it with
|
|
24
|
+
* defaults; an object overrides `maxVariants` / `minSavings`; `false` turns L2
|
|
25
|
+
* OFF (same as `level: 1`). Raising `maxVariants` lets children with more
|
|
26
|
+
* distinct call-site shapes be specialized — affordable now that the net-win
|
|
27
|
+
* gate only sizes the modules that actually differ (docs §13.2).
|
|
22
28
|
*/
|
|
23
29
|
monomorphize?: boolean | Partial<Omit<MonomorphizeOptions, 'enabled'>>;
|
|
30
|
+
/**
|
|
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.
|
|
42
|
+
*/
|
|
43
|
+
engine?: 'auto' | 'js' | 'rust';
|
|
24
44
|
/**
|
|
25
45
|
* Whether to shake in `vite dev` too (docs/RUST-MIGRATION.md §3 M2,
|
|
26
46
|
* ARCHITECTURE §6.2). Default `false` — dev is a pass-through, which is always
|
package/dist/vite.js
CHANGED
|
@@ -1,14 +1,33 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
+
import { compile } from 'svelte/compiler';
|
|
3
4
|
import { svelteShaker, svelteShakerWithMono } from './index.js';
|
|
4
5
|
import { DevShaker } from './engine.js';
|
|
5
6
|
import { collectSvelteFiles, fsResolve } from './scan.js';
|
|
6
7
|
import { DEFAULT_MONO_OPTIONS } from './mono.js';
|
|
7
8
|
import { tryLoadRsvelteParser } from './rsvelte-parse.js';
|
|
9
|
+
import { svelteShakerWasm, tryLoadWasmEngine } from './wasm-engine.js';
|
|
8
10
|
/** kB with two decimals, the unit Vite itself uses in its build size report. */
|
|
9
11
|
function formatKB(bytes) {
|
|
10
12
|
return `${(bytes / 1024).toFixed(2)} kB`;
|
|
11
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Compiled-output size proxy: the bytes the Svelte compiler emits for `code`
|
|
16
|
+
* (client JS + scoped CSS). This is the number that actually ships, so it
|
|
17
|
+
* captures what the SOURCE-byte total cannot: a dead `{#if}` arm or a removed
|
|
18
|
+
* `<style>` rule shrinks the compiled output far more than its few source bytes
|
|
19
|
+
* suggest. Reporting-only; `null` if the (always valid) source fails to compile,
|
|
20
|
+
* so the caller can skip it without aborting the build.
|
|
21
|
+
*/
|
|
22
|
+
function compiledSize(code, id) {
|
|
23
|
+
try {
|
|
24
|
+
const { js, css } = compile(code, { generate: 'client', dev: false, filename: id });
|
|
25
|
+
return js.code.length + (css?.code.length ?? 0);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
12
31
|
/**
|
|
13
32
|
* Print what the shake saved. Always emits a one-line whole-program summary;
|
|
14
33
|
* when `verbose`, also lists each shrunk file (largest saving first). Sizes are
|
|
@@ -43,16 +62,39 @@ function reportSizes(shaken, read, root, verbose, log) {
|
|
|
43
62
|
const filePct = ((fileSaved / row.before) * 100).toFixed(1);
|
|
44
63
|
log(` ${rel}: ${formatKB(row.before)} → ${formatKB(row.after)} (-${filePct}%)`);
|
|
45
64
|
}
|
|
65
|
+
// The source-byte delta UNDER-reports the real win: a folded dead branch or a
|
|
66
|
+
// removed `<style>` rule shrinks the compiled output much more than its source.
|
|
67
|
+
// Compiling is costly, so we only do it for the files that actually shrank, and
|
|
68
|
+
// only under `verbose` — it never affects the build, just the visible number.
|
|
69
|
+
let compiledBefore = 0;
|
|
70
|
+
let compiledAfter = 0;
|
|
71
|
+
for (const row of rows) {
|
|
72
|
+
const before = compiledSize(read(row.id), row.id);
|
|
73
|
+
const after = compiledSize(shaken[row.id], row.id);
|
|
74
|
+
if (before === null || after === null)
|
|
75
|
+
continue; // skip the un-compilable
|
|
76
|
+
compiledBefore += before;
|
|
77
|
+
compiledAfter += after;
|
|
78
|
+
}
|
|
79
|
+
if (compiledBefore > 0) {
|
|
80
|
+
const compiledSaved = compiledBefore - compiledAfter;
|
|
81
|
+
const compiledPct = ((compiledSaved / compiledBefore) * 100).toFixed(1);
|
|
82
|
+
log(`compiled output (js+css) of shaken files: ` +
|
|
83
|
+
`${formatKB(compiledBefore)} → ${formatKB(compiledAfter)} ` +
|
|
84
|
+
`(saved ${formatKB(compiledSaved)}, ${compiledPct}%)`);
|
|
85
|
+
}
|
|
46
86
|
}
|
|
47
87
|
/** Query flag a specialized-variant `.svelte` request carries (see below). */
|
|
48
88
|
const VARIANT_QUERY = 'shaker_variant';
|
|
49
89
|
/**
|
|
50
90
|
* Resolve the {@link MonomorphizeOptions} from the public option surface. L2 is
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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`).
|
|
53
94
|
*/
|
|
54
95
|
function resolveMono(options) {
|
|
55
|
-
|
|
96
|
+
const optedOut = options.level === 0 || options.level === 1 || options.monomorphize === false;
|
|
97
|
+
if (optedOut)
|
|
56
98
|
return DEFAULT_MONO_OPTIONS;
|
|
57
99
|
const overrides = typeof options.monomorphize === 'object' ? options.monomorphize : {};
|
|
58
100
|
return { ...DEFAULT_MONO_OPTIONS, enabled: true, ...overrides };
|
|
@@ -227,14 +269,41 @@ export function shaker(options = {}) {
|
|
|
227
269
|
return null;
|
|
228
270
|
return resolved.id.split('?')[0];
|
|
229
271
|
};
|
|
230
|
-
|
|
231
|
-
|
|
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.
|
|
275
|
+
const engineChoice = options.engine ?? 'auto';
|
|
276
|
+
let effectiveMono = mono;
|
|
277
|
+
let wasm = null;
|
|
278
|
+
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
|
+
wasm = tryLoadWasmEngine();
|
|
284
|
+
if (!wasm)
|
|
285
|
+
throw new Error('[vite-plugin-svelte-shaker] engine: "rust" was requested but the WASM engine ' +
|
|
286
|
+
'could not be loaded. Remove the option (or use engine: "js") to use the JS engine.');
|
|
287
|
+
}
|
|
288
|
+
else if (engineChoice === 'auto' && !mono.enabled) {
|
|
289
|
+
// Auto: prefer the native engine on the L2-off path; fall back to JS silently.
|
|
290
|
+
wasm = tryLoadWasmEngine();
|
|
291
|
+
}
|
|
292
|
+
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();
|
|
296
|
+
reportSizes(shaken, read, root, options.verbose === true, log);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (!effectiveMono.enabled) {
|
|
300
|
+
// JS engine, L2 off: byte-for-byte the L0/L1/L1.5 output.
|
|
232
301
|
shaken = await svelteShaker(entries, resolve, read, getParse());
|
|
233
302
|
variantSources = new Map();
|
|
234
303
|
reportSizes(shaken, read, root, options.verbose === true, log);
|
|
235
304
|
return;
|
|
236
305
|
}
|
|
237
|
-
const result = await svelteShakerWithMono(entries, resolve, read,
|
|
306
|
+
const result = await svelteShakerWithMono(entries, resolve, read, effectiveMono, variantSpecifier, getParse());
|
|
238
307
|
shaken = result.files;
|
|
239
308
|
variantSources = new Map();
|
|
240
309
|
for (const v of result.mono.variants.values())
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type ReadFile, type Resolve } from './analyze.js';
|
|
2
|
+
import { type Parse } from './parse.js';
|
|
3
|
+
import type { ComponentId } from './ir.js';
|
|
4
|
+
/** The subset of the WASM exports the plugin uses (docs/RUST-MIGRATION.md M5). */
|
|
5
|
+
interface WasmEngine {
|
|
6
|
+
/** Whole-program L0/L1/L1.5 shake: input JSON in, `{id: shakenCode}` JSON out. */
|
|
7
|
+
shake_program: (inputJson: string) => string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Load the native Rust (WASM) engine, or `null` if it can't be loaded (no built
|
|
11
|
+
* artifact for this install). Two locations are tried in order:
|
|
12
|
+
* - `./svelte_shaker_engine.js` — the PUBLISHED layout, where `build` copies the
|
|
13
|
+
* artifact next to `dist/wasm-engine.js` (wasm-pack's `pkg/.gitignore` of `*`
|
|
14
|
+
* keeps `engine-rs/pkg` out of the npm tarball, so we ship it via `dist/`).
|
|
15
|
+
* - `../engine-rs/pkg/svelte_shaker_engine.js` — the REPO layout, used when
|
|
16
|
+
* running from source in the package's own tests (no copy step has run).
|
|
17
|
+
*/
|
|
18
|
+
export declare function tryLoadWasmEngine(): WasmEngine | null;
|
|
19
|
+
/**
|
|
20
|
+
* Whole-program shake via the native Rust engine — the L0/L1/L1.5 counterpart of
|
|
21
|
+
* {@link svelteShaker} (L2 lives only in the JS engine). The crawl/resolution
|
|
22
|
+
* stays in JS ({@link buildAnalyzeInput}); we hand the Rust engine the resolved
|
|
23
|
+
* graph plus each file's AST and source as JSON, exactly as the differential
|
|
24
|
+
* `wasm-shake` test does — so the output is byte-identical to the JS engine.
|
|
25
|
+
*
|
|
26
|
+
* The same parse cache feeds the crawl and the program input, so each file is
|
|
27
|
+
* parsed once. A final self-check mirrors the JS engine's `revertUnparseable`:
|
|
28
|
+
* any emitted file that no longer parses is reverted to its original (a sound
|
|
29
|
+
* "did not shake this component"), so a single mishandled shape can never break
|
|
30
|
+
* the build.
|
|
31
|
+
*/
|
|
32
|
+
export declare function svelteShakerWasm(engine: WasmEngine, entries: ComponentId | ComponentId[], resolve: Resolve, readFile: ReadFile, parse?: Parse): Promise<Record<ComponentId, string>>;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { buildAnalyzeInput } from './analyze.js';
|
|
3
|
+
import { parseCached, parseSvelte } from './parse.js';
|
|
4
|
+
// NODE-ONLY: loads the native Rust (WASM) engine and drives it from the Vite
|
|
5
|
+
// plugin. Imported only by `vite.ts` (a Node entry), never by the environment-free
|
|
6
|
+
// engine (`index.ts`/`analyze.ts`), so the browser playground build stays clean.
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
/**
|
|
9
|
+
* Load the native Rust (WASM) engine, or `null` if it can't be loaded (no built
|
|
10
|
+
* artifact for this install). Two locations are tried in order:
|
|
11
|
+
* - `./svelte_shaker_engine.js` — the PUBLISHED layout, where `build` copies the
|
|
12
|
+
* artifact next to `dist/wasm-engine.js` (wasm-pack's `pkg/.gitignore` of `*`
|
|
13
|
+
* keeps `engine-rs/pkg` out of the npm tarball, so we ship it via `dist/`).
|
|
14
|
+
* - `../engine-rs/pkg/svelte_shaker_engine.js` — the REPO layout, used when
|
|
15
|
+
* running from source in the package's own tests (no copy step has run).
|
|
16
|
+
*/
|
|
17
|
+
export function tryLoadWasmEngine() {
|
|
18
|
+
for (const spec of ['./svelte_shaker_engine.js', '../engine-rs/pkg/svelte_shaker_engine.js']) {
|
|
19
|
+
try {
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
21
|
+
const mod = require(spec);
|
|
22
|
+
if (typeof mod.shake_program === 'function')
|
|
23
|
+
return { shake_program: mod.shake_program.bind(mod) };
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Try the next location.
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Whole-program shake via the native Rust engine — the L0/L1/L1.5 counterpart of
|
|
33
|
+
* {@link svelteShaker} (L2 lives only in the JS engine). The crawl/resolution
|
|
34
|
+
* stays in JS ({@link buildAnalyzeInput}); we hand the Rust engine the resolved
|
|
35
|
+
* graph plus each file's AST and source as JSON, exactly as the differential
|
|
36
|
+
* `wasm-shake` test does — so the output is byte-identical to the JS engine.
|
|
37
|
+
*
|
|
38
|
+
* The same parse cache feeds the crawl and the program input, so each file is
|
|
39
|
+
* parsed once. A final self-check mirrors the JS engine's `revertUnparseable`:
|
|
40
|
+
* any emitted file that no longer parses is reverted to its original (a sound
|
|
41
|
+
* "did not shake this component"), so a single mishandled shape can never break
|
|
42
|
+
* the build.
|
|
43
|
+
*/
|
|
44
|
+
export async function svelteShakerWasm(engine, entries, resolve, readFile, parse) {
|
|
45
|
+
const cache = new Map();
|
|
46
|
+
const input = await buildAnalyzeInput(entries, resolve, readFile, cache, parse);
|
|
47
|
+
const programInput = {
|
|
48
|
+
files: input.files.map((f) => ({
|
|
49
|
+
id: f.id,
|
|
50
|
+
ast: parseCached(f.id, f.code, cache, parse),
|
|
51
|
+
code: f.code,
|
|
52
|
+
})),
|
|
53
|
+
edges: input.edges,
|
|
54
|
+
entries: input.entries,
|
|
55
|
+
};
|
|
56
|
+
const out = JSON.parse(engine.shake_program(JSON.stringify(programInput)));
|
|
57
|
+
for (const file of input.files) {
|
|
58
|
+
const code = out[file.id];
|
|
59
|
+
if (code === undefined || code === file.code)
|
|
60
|
+
continue;
|
|
61
|
+
try {
|
|
62
|
+
parseSvelte(code, file.id);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
out[file.id] = file.code;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-shaker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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",
|