svelte-shaker 0.13.0 → 0.14.1
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 +46 -26
- package/dist/analyze.d.ts +18 -4
- package/dist/analyze.js +283 -26
- package/dist/engine.d.ts +2 -2
- package/dist/engine.js +2 -2
- package/dist/escape-scan.d.ts +20 -15
- package/dist/escape-scan.js +20 -20
- package/dist/ir.d.ts +4 -3
- package/dist/parse.d.ts +4 -0
- package/dist/svelte_shaker_engine_bg.wasm +0 -0
- package/dist/transform.js +22 -5
- package/dist/unread.js +2 -1
- package/dist/vite.d.ts +44 -19
- package/dist/vite.js +93 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,9 +67,10 @@ import { shaker } from 'svelte-shaker/vite';
|
|
|
67
67
|
|
|
68
68
|
export default defineConfig({
|
|
69
69
|
plugins: [
|
|
70
|
-
// `
|
|
71
|
-
//
|
|
72
|
-
|
|
70
|
+
// `entries` is where the component crawl STARTS, not a file filter. It
|
|
71
|
+
// must cover EVERY call site in the app, or prop elimination would be
|
|
72
|
+
// unsound. Defaults to the Vite root.
|
|
73
|
+
shaker({ entries: ['src'] }),
|
|
73
74
|
svelte(),
|
|
74
75
|
],
|
|
75
76
|
});
|
|
@@ -88,8 +89,9 @@ module); the engine takes an optional `parse` argument if you want to swap it.
|
|
|
88
89
|
|
|
89
90
|
```ts
|
|
90
91
|
shaker({
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
entries: ['src'], // dirs (relative to root) the crawl starts from; they must
|
|
93
|
+
// hold every .svelte call site in the app. Not a glob, not a filter.
|
|
94
|
+
preserve: [], // components whose props must never be folded (see below)
|
|
93
95
|
monomorphize: true, // default on; `false` disables it for faster builds,
|
|
94
96
|
// or { maxVariants: 16, minSavings: 0.05 } to tune
|
|
95
97
|
verbose: false, // true = per-file size breakdown after the build
|
|
@@ -101,6 +103,10 @@ shaker({
|
|
|
101
103
|
});
|
|
102
104
|
```
|
|
103
105
|
|
|
106
|
+
That list is exhaustive: any other key **fails the build**, naming the key and the
|
|
107
|
+
options that do exist. A typo would otherwise be ignored — and a misspelled
|
|
108
|
+
`preserve` ships the component you meant to protect, over-shaken.
|
|
109
|
+
|
|
104
110
|
- **The defaults are the Rust path.** Out of the box svelte-shaker runs the
|
|
105
111
|
native **Rust (WASM) engine** and parses with **rsvelte**, loaded from
|
|
106
112
|
[`@rsvelte/compiler`](https://github.com/baseballyama/rsvelte) (a bundled WASM
|
|
@@ -141,19 +147,30 @@ shaker({
|
|
|
141
147
|
can't (a broken install), the plugin **throws** rather than silently falling
|
|
142
148
|
back — so the same source always shakes the same on every machine. Reinstall
|
|
143
149
|
dependencies, or set `parser: 'svelte'`.
|
|
144
|
-
- **`
|
|
145
|
-
can't see
|
|
146
|
-
|
|
147
|
-
`
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
150
|
+
- **`preserve`** — keep a component's **prop interface** exactly as written, because
|
|
151
|
+
something the shake can't see passes props to it. What is preserved is the props,
|
|
152
|
+
**not** the file's presence in the bundle: this is unrelated to Rollup/Vite's
|
|
153
|
+
`external`, and it never keeps a file out of the bundle or out of the analysis.
|
|
154
|
+
|
|
155
|
+
You need it when the consumer lives outside the `.svelte` graph and the shaker
|
|
156
|
+
can't observe the call site — a `mount()` behind a **non-literal** dynamic
|
|
157
|
+
`import(expr)`, or a module outside the `entries` roots. Consumers reached by a
|
|
158
|
+
static import, `export … from`, or a **literal** `import('./X.svelte')` are found
|
|
159
|
+
by the plugin's own scan of your non-`.svelte` modules, so a plain
|
|
160
|
+
`mount(Component, { props })` is already handled for you.
|
|
161
|
+
|
|
162
|
+
Each entry is a root-relative or absolute path naming a component file (with its
|
|
163
|
+
`.svelte` extension) or a directory of them (same path-prefix basis as `entries`).
|
|
164
|
+
The file stays fully analyzed and its own call sites still count toward its
|
|
165
|
+
children — only that component's own prop folding is turned off. It is not a
|
|
166
|
+
scan-exclusion filter.
|
|
167
|
+
|
|
168
|
+
**When in doubt, list it.** Unlike `entries`, over-listing errs safe: a component
|
|
169
|
+
preserved without needing it is just shaken less, never wrongly.
|
|
170
|
+
|
|
171
|
+
The build **warns** (with the file path) about any module the scan couldn't
|
|
172
|
+
parse — so a mounted component isn't silently left unprotected — and about
|
|
173
|
+
`preserve` entries that matched no component.
|
|
157
174
|
|
|
158
175
|
## What it removes
|
|
159
176
|
|
|
@@ -192,14 +209,17 @@ The whole point is to **never change observable behavior**.
|
|
|
192
209
|
unshaken; distribute via `svelte-package`.
|
|
193
210
|
- **Build only** — whole-program analysis is incompatible with dev/HMR locality,
|
|
194
211
|
so dev is always a pass-through.
|
|
195
|
-
- **`
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
modules
|
|
201
|
-
|
|
202
|
-
|
|
212
|
+
- **`entries` must cover the whole app** — the crawl starts there, and every
|
|
213
|
+
`.svelte` file it finds is a call-site source. A call site outside those roots
|
|
214
|
+
is invisible, so narrowing `entries` does not shake less, it shakes _wrongly_.
|
|
215
|
+
(Components reached _from_ the roots — including library ones in `node_modules`
|
|
216
|
+
— are crawled and shaken without being listed.) Call sites in `.ts`/`.js`
|
|
217
|
+
modules under the roots (e.g. `mount(Component, { props })`) are scanned and the
|
|
218
|
+
component's props are kept automatically; a **non-literal** dynamic `import(expr)`
|
|
219
|
+
can't be followed, so reach for `preserve` there. That scan covers modules
|
|
220
|
+
**under the `entries` roots only** — a library that mounts its own component
|
|
221
|
+
from its own bundled `.js`/`.ts` inside `node_modules` is not scanned, so list
|
|
222
|
+
it in `preserve` (with its resolved path) if you hit that.
|
|
203
223
|
|
|
204
224
|
See [`docs/ARCHITECTURE.md`](https://github.com/baseballyama/svelte-shaker/blob/main/docs/ARCHITECTURE.md)
|
|
205
225
|
for the full design and implementation status.
|
package/dist/analyze.d.ts
CHANGED
|
@@ -18,8 +18,10 @@ export type ReadFileSync = (id: ComponentId) => string;
|
|
|
18
18
|
* never be seen and is safe to drop;
|
|
19
19
|
* - `{ kind: 'all' }` — anything we cannot pin down: a `...rest` (captures
|
|
20
20
|
* undeclared inputs), an Identifier/Array binding (`let p = $props()`),
|
|
21
|
-
* more than one `$props()` call,
|
|
22
|
-
* declarator
|
|
21
|
+
* more than one `$props()` call, `$props()` outside a `let <pat> = …`
|
|
22
|
+
* declarator, or a component that observes slotted content outside `$props()`
|
|
23
|
+
* — a legacy `<slot>` element or a `$$slots` read (both legal in runes mode).
|
|
24
|
+
* Then any input might be observed, so nothing is dropped.
|
|
23
25
|
*/
|
|
24
26
|
export type ReachableInputs = {
|
|
25
27
|
kind: 'all';
|
|
@@ -114,6 +116,17 @@ export interface FileModel {
|
|
|
114
116
|
* the write. We never fold such a prop, exactly like a shadowed one.
|
|
115
117
|
*/
|
|
116
118
|
writtenNames: Set<string>;
|
|
119
|
+
/**
|
|
120
|
+
* Owner-local bindings that are provably a single primitive CONSTANT, keyed by
|
|
121
|
+
* the LOCAL name a forwarded call-site expression references (docs §13.1
|
|
122
|
+
* interprocedural pass-through). Merged into the owner's fold env so that
|
|
123
|
+
* `<Child {count}/>` — where `count` is an unmutated `let count = $state(0)` or
|
|
124
|
+
* a `const count = 0` — folds in the child exactly as a call-site literal would,
|
|
125
|
+
* feeding BOTH constant fold and value-set narrowing. A static property of the
|
|
126
|
+
* source (independent of the fixpoint's plans), so it is computed ONCE here.
|
|
127
|
+
* See {@link computeScriptConstEnv} for the (conservative) admission rules.
|
|
128
|
+
*/
|
|
129
|
+
scriptConstEnv: ReadonlyMap<string, Literal>;
|
|
117
130
|
/**
|
|
118
131
|
* Resolved ids of CHILD components this file leaks as a value (escape, docs
|
|
119
132
|
* §4.1) — e.g. `<svelte:component this={Child}>`. `analyze` unions these
|
|
@@ -264,8 +277,9 @@ export interface UnpassedProp {
|
|
|
264
277
|
* spread that could set it (`readCallSite` already folds `bind:`, known
|
|
265
278
|
* spreads, and `children`/snippet body into `explicit`/`hadSpread`);
|
|
266
279
|
* - a component in `input.escaped` — one the Shell knows has a consumer OUTSIDE
|
|
267
|
-
* the `.svelte` graph (a
|
|
268
|
-
* §4.2) — is skipped, because that consumer may pass a prop
|
|
280
|
+
* the `.svelte` graph (a call site in a non-`.svelte` module, or a user
|
|
281
|
+
* `preserve`, docs §4.2) — is skipped, because that consumer may pass a prop
|
|
282
|
+
* the crawl cannot see.
|
|
269
283
|
*
|
|
270
284
|
* Missing a `.svelte` EDGE (e.g. an unfollowed barrel) only DROPS call sites, so it
|
|
271
285
|
* can only make this UNDER-report (the component looks unused and is skipped). The
|
package/dist/analyze.js
CHANGED
|
@@ -29,22 +29,22 @@ function fixpointIterationBound(componentCount) {
|
|
|
29
29
|
/** Bail reason stamped on a component leaked as a value (docs §4.1 escape). */
|
|
30
30
|
const ESCAPE_REASON = 'escapes as value (e.g. <svelte:component this={X}>)';
|
|
31
31
|
/** Bail reason stamped on a component with a consumer OUTSIDE the analyzed
|
|
32
|
-
* `.svelte` graph — a
|
|
33
|
-
* user-declared `
|
|
34
|
-
* byte-identical to the Rust engine's constant so the two agree. */
|
|
35
|
-
const
|
|
32
|
+
* `.svelte` graph — a call site in a non-`.svelte` module the crawl cannot
|
|
33
|
+
* parse, or a user-declared `preserve` (docs §4.2, {@link AnalyzeInput.escaped}).
|
|
34
|
+
* Kept byte-identical to the Rust engine's constant so the two agree. */
|
|
35
|
+
const MODULE_ESCAPE_REASON = 'has a consumer outside the analyzed .svelte graph';
|
|
36
36
|
/**
|
|
37
|
-
* Stamp {@link
|
|
37
|
+
* Stamp {@link MODULE_ESCAPE_REASON} on every model in `escaped` that exists in
|
|
38
38
|
* the program — the single injection point both the whole-program shake and
|
|
39
39
|
* {@link findNeverPassedProps} share (docs §4.2). Ids not in the program are
|
|
40
|
-
* ignored (a stale `
|
|
41
|
-
*
|
|
40
|
+
* ignored (a stale `preserve` entry or a scanned import to a component outside
|
|
41
|
+
* the crawl is simply a no-op, never an error).
|
|
42
42
|
*/
|
|
43
|
-
function
|
|
43
|
+
function stampModuleEscapes(models, escaped) {
|
|
44
44
|
for (const id of escaped ?? []) {
|
|
45
45
|
const model = models.get(id);
|
|
46
|
-
if (model && !model.bailReasons.includes(
|
|
47
|
-
model.bailReasons.push(
|
|
46
|
+
if (model && !model.bailReasons.includes(MODULE_ESCAPE_REASON))
|
|
47
|
+
model.bailReasons.push(MODULE_ESCAPE_REASON);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
@@ -86,9 +86,10 @@ export function analyzeInput(input, parseCache) {
|
|
|
86
86
|
if (model && !model.bailReasons.includes(ESCAPE_REASON))
|
|
87
87
|
model.bailReasons.push(ESCAPE_REASON);
|
|
88
88
|
}
|
|
89
|
-
// Components with consumers outside the `.svelte` graph (a
|
|
90
|
-
// or a user `
|
|
91
|
-
|
|
89
|
+
// Components with consumers outside the `.svelte` graph (a call site in a
|
|
90
|
+
// non-`.svelte` module or a user `preserve`, docs §4.2) join the same
|
|
91
|
+
// whole-component escape bail.
|
|
92
|
+
stampModuleEscapes(models, input.escaped);
|
|
92
93
|
return { models, plans: planFixpoint(models) };
|
|
93
94
|
}
|
|
94
95
|
/**
|
|
@@ -348,6 +349,27 @@ const EMPTY_ENV = new Map();
|
|
|
348
349
|
const EMPTY_SET_ENV = new Map();
|
|
349
350
|
/** Shared empty {@link OwnerFoldEnv} (no owner, or an owner that folds nothing). */
|
|
350
351
|
const EMPTY_OWNER_ENV = { fold: EMPTY_ENV, narrow: EMPTY_SET_ENV };
|
|
352
|
+
/**
|
|
353
|
+
* Merge an owner's static script constants ({@link FileModel.scriptConstEnv})
|
|
354
|
+
* with its remapped folded props into a single fold env. The two key spaces are
|
|
355
|
+
* DISJOINT by construction: a folded prop is keyed by the LOCAL binding name its
|
|
356
|
+
* `$props()` destructure introduces, and a script const by its top-level
|
|
357
|
+
* declarator name; a top-level `const`/`let` reusing a `$props()` local name is a
|
|
358
|
+
* JS redeclaration error, so no name can appear in both. The merge is therefore
|
|
359
|
+
* order-independent — folded props are applied last purely to document that
|
|
360
|
+
* invariant — and either operand is returned as-is when the other is empty (the
|
|
361
|
+
* common case: no owner-forwarded expressions, or a prop-less component).
|
|
362
|
+
*/
|
|
363
|
+
function mergeScriptConsts(scriptConsts, foldedProps) {
|
|
364
|
+
if (scriptConsts.size === 0)
|
|
365
|
+
return foldedProps;
|
|
366
|
+
if (foldedProps.size === 0)
|
|
367
|
+
return scriptConsts;
|
|
368
|
+
const merged = new Map(scriptConsts);
|
|
369
|
+
for (const [name, value] of foldedProps)
|
|
370
|
+
merged.set(name, value);
|
|
371
|
+
return merged;
|
|
372
|
+
}
|
|
351
373
|
/**
|
|
352
374
|
* Recompute every component's plan from the (cascade-filtered) usage, evaluating
|
|
353
375
|
* forwarded call-site expressions against `prevPlans` — the PREVIOUS fixpoint
|
|
@@ -367,13 +389,21 @@ function buildPlans(models, usage, prevPlans) {
|
|
|
367
389
|
if (cached)
|
|
368
390
|
return cached;
|
|
369
391
|
const model = models.get(owner);
|
|
370
|
-
const plan = prevPlans.get(owner);
|
|
371
392
|
let env = EMPTY_OWNER_ENV;
|
|
372
|
-
if (model
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
393
|
+
if (model) {
|
|
394
|
+
const plan = prevPlans.get(owner);
|
|
395
|
+
// A bailed owner still forwards its own SCRIPT CONSTANTS unchanged — its
|
|
396
|
+
// bail only makes ITS props unobservable, but it keeps rendering its call
|
|
397
|
+
// sites (docs §4.2 "自身のコールサイトは数える"), so `scriptConstEnv` (a
|
|
398
|
+
// static source fact) participates regardless of `plan.bail`. Only the
|
|
399
|
+
// fold/narrow derived from the owner's OWN prop plan is gated on the plan
|
|
400
|
+
// being present and not bailed.
|
|
401
|
+
const foldable = plan !== undefined && !plan.bail;
|
|
402
|
+
const foldedProps = foldable && plan.constFold.size > 0 ? remapToLocalNames(plan.constFold, model) : EMPTY_ENV;
|
|
403
|
+
const narrow = foldable && plan.narrow.size > 0 ? remapToLocalNames(plan.narrow, model) : EMPTY_SET_ENV;
|
|
404
|
+
const fold = mergeScriptConsts(model.scriptConstEnv, foldedProps);
|
|
405
|
+
if (fold.size > 0 || narrow.size > 0)
|
|
406
|
+
env = { fold, narrow };
|
|
377
407
|
}
|
|
378
408
|
envCache.set(owner, env);
|
|
379
409
|
return env;
|
|
@@ -470,8 +500,9 @@ export function deadSpansForPlans(models, plans) {
|
|
|
470
500
|
* spread that could set it (`readCallSite` already folds `bind:`, known
|
|
471
501
|
* spreads, and `children`/snippet body into `explicit`/`hadSpread`);
|
|
472
502
|
* - a component in `input.escaped` — one the Shell knows has a consumer OUTSIDE
|
|
473
|
-
* the `.svelte` graph (a
|
|
474
|
-
* §4.2) — is skipped, because that consumer may pass a prop
|
|
503
|
+
* the `.svelte` graph (a call site in a non-`.svelte` module, or a user
|
|
504
|
+
* `preserve`, docs §4.2) — is skipped, because that consumer may pass a prop
|
|
505
|
+
* the crawl cannot see.
|
|
475
506
|
*
|
|
476
507
|
* Missing a `.svelte` EDGE (e.g. an unfollowed barrel) only DROPS call sites, so it
|
|
477
508
|
* can only make this UNDER-report (the component looks unused and is skipped). The
|
|
@@ -492,9 +523,10 @@ export function findNeverPassedProps(input) {
|
|
|
492
523
|
if (model && !model.bailReasons.includes(ESCAPE_REASON))
|
|
493
524
|
model.bailReasons.push(ESCAPE_REASON);
|
|
494
525
|
}
|
|
495
|
-
//
|
|
496
|
-
// they pass is never mis-reported as
|
|
497
|
-
|
|
526
|
+
// Consumers outside the `.svelte` graph (non-`.svelte` module call sites or
|
|
527
|
+
// `preserve`) escape too, so a prop they pass is never mis-reported as
|
|
528
|
+
// never-passed (docs §4.2).
|
|
529
|
+
stampModuleEscapes(models, input.escaped);
|
|
498
530
|
// Every textual call site counts (no cascade dead-span filtering): a prop passed
|
|
499
531
|
// only at a folded-away site is still author-written, so we do not flag it.
|
|
500
532
|
const usage = buildUsage(models, new Map());
|
|
@@ -637,10 +669,11 @@ function buildModelFromInput(file, edges, parseCache) {
|
|
|
637
669
|
}
|
|
638
670
|
}
|
|
639
671
|
}
|
|
640
|
-
const reachableInputs = computeReachableInputs(instance, props, hasRestProp, propsPattern);
|
|
672
|
+
const reachableInputs = computeReachableInputs(instance, props, hasRestProp, propsPattern, usesLegacySlotInputs(ast));
|
|
641
673
|
const childCalls = collectChildCalls(ast, imports);
|
|
642
674
|
const { shadowedNames, debugNames, writtenNames } = collectTemplateBindings(ast, instance, propsDeclaration);
|
|
643
675
|
const unreadDeclaredProps = computeUnreadDeclaredProps(ast, instance, props, propsPattern, shadowedNames, debugNames, writtenNames);
|
|
676
|
+
const scriptConstEnv = computeScriptConstEnv(ast, instance, ast.module, propsDeclaration, writtenNames);
|
|
644
677
|
// Escape detection (docs §4.1): an imported component referenced as a *value*
|
|
645
678
|
// (most notably `<svelte:component this={X}>`, but also assigned / passed /
|
|
646
679
|
// stored) leaks to a use we cannot follow, so its prop profile is incomplete.
|
|
@@ -662,6 +695,7 @@ function buildModelFromInput(file, edges, parseCache) {
|
|
|
662
695
|
shadowedNames,
|
|
663
696
|
debugNames,
|
|
664
697
|
writtenNames,
|
|
698
|
+
scriptConstEnv,
|
|
665
699
|
escapedComponents,
|
|
666
700
|
bailReasons,
|
|
667
701
|
};
|
|
@@ -743,6 +777,195 @@ function computeUnreadDeclaredProps(ast, instance, props, propsPattern, shadowed
|
|
|
743
777
|
}
|
|
744
778
|
return unread;
|
|
745
779
|
}
|
|
780
|
+
/**
|
|
781
|
+
* Owner-local, provably-constant primitive bindings, keyed by the LOCAL name a
|
|
782
|
+
* forwarded call-site expression references (docs §13.1 interprocedural
|
|
783
|
+
* pass-through). Walks the module then the instance `<script>`'s TOP-LEVEL
|
|
784
|
+
* declarations in order, extending the env sequentially so `const a = 1; const b
|
|
785
|
+
* = a + 1;` both resolve. Every rule is conservative for soundness — a binding
|
|
786
|
+
* is admitted ONLY when its identifier definitely denotes one constant primitive
|
|
787
|
+
* at every call site:
|
|
788
|
+
* - `const x = <expr>` / `let|var x = <expr>` whose `<expr>` constant-evaluates
|
|
789
|
+
* against the env built so far;
|
|
790
|
+
* - `$state(<arg>)` / `$state.raw(<arg>)` are unwrapped to `<arg>` (a bare
|
|
791
|
+
* `$state()` is `undefined`): the reactive wrapper does not change the value a
|
|
792
|
+
* never-written binding forwards. `$derived` / `$props` / any OTHER rune is
|
|
793
|
+
* not unwrapped, so its `CallExpression` never constant-evaluates and is
|
|
794
|
+
* skipped (out of scope);
|
|
795
|
+
* - primitives only — the `Literal` domain excludes object/array initializers,
|
|
796
|
+
* so deep mutation through a `$state` proxy can never be folded away;
|
|
797
|
+
* - the name is NEVER written (reassigned / `++` / destructure-assigned /
|
|
798
|
+
* `bind:`), tested against {@link writtenNames} extended with module-internal
|
|
799
|
+
* writes (docs §4.1: a written binding is not a constant);
|
|
800
|
+
* - the name is bound EXACTLY ONCE across the whole file (its own top-level
|
|
801
|
+
* declarator). A name a template binder (`{#each as}`, snippet param, …) or a
|
|
802
|
+
* nested/duplicate scope also binds is a DIFFERENT entity at some call site,
|
|
803
|
+
* and call-site evaluation ({@link evaluate}) is scope-blind, so folding it
|
|
804
|
+
* could read the wrong entity there (docs §4.1 shadowing; the same soundness
|
|
805
|
+
* argument as {@link isFoldBlockedName}, but on the owner's OWN bindings — for
|
|
806
|
+
* which the file-wide `shadowedNames` cannot be reused: it already contains
|
|
807
|
+
* every top-level script declaration, so it would reject every candidate);
|
|
808
|
+
* - exported bindings (`export const x`) are excluded — they are wrapped in an
|
|
809
|
+
* `ExportNamedDeclaration`, not a bare `VariableDeclaration`, so the top-level
|
|
810
|
+
* scan below skips them; like an escaped component they are reachable from
|
|
811
|
+
* outside the analyzed graph.
|
|
812
|
+
*/
|
|
813
|
+
function computeScriptConstEnv(ast, instance, moduleScript, propsDeclaration, writtenNames) {
|
|
814
|
+
const env = new Map();
|
|
815
|
+
// A name is admissible only if bound EXACTLY ONCE anywhere in the file, so no
|
|
816
|
+
// template binder or nested scope can shadow it at a call site.
|
|
817
|
+
const bindingCounts = new Map();
|
|
818
|
+
countBindingNames(moduleScript?.content, bindingCounts);
|
|
819
|
+
countBindingNames(instance?.content, bindingCounts);
|
|
820
|
+
countBindingNames(ast.fragment, bindingCounts);
|
|
821
|
+
// `writtenNames` (from collectTemplateBindings) scans the instance script and
|
|
822
|
+
// the template but NOT the module script, so a module-internal write
|
|
823
|
+
// (`<script module>let n = 0; function inc(){ n++ }</script>`) would be missed.
|
|
824
|
+
// Close that gap here before admitting any module-level binding.
|
|
825
|
+
const written = new Set(writtenNames);
|
|
826
|
+
collectScriptWrites(moduleScript?.content, written);
|
|
827
|
+
// Module script runs before the instance and its bindings are visible to it,
|
|
828
|
+
// so extend module-first, then instance, each in declaration order.
|
|
829
|
+
for (const program of [moduleScript?.content, instance?.content]) {
|
|
830
|
+
for (const stmt of program?.body ?? []) {
|
|
831
|
+
// Only a bare `VariableDeclaration`; an `export const` is wrapped in an
|
|
832
|
+
// `ExportNamedDeclaration` and is deliberately excluded (see doc comment).
|
|
833
|
+
if (stmt.type !== 'VariableDeclaration' || stmt === propsDeclaration)
|
|
834
|
+
continue;
|
|
835
|
+
for (const decl of stmt.declarations ?? []) {
|
|
836
|
+
// A single-identifier binding only: a destructuring `const { a } = …`
|
|
837
|
+
// has no one primitive name to key, so it never folds.
|
|
838
|
+
if (decl.id?.type !== 'Identifier' || !decl.id.name)
|
|
839
|
+
continue;
|
|
840
|
+
const name = decl.id.name;
|
|
841
|
+
if (written.has(name) || bindingCounts.get(name) !== 1)
|
|
842
|
+
continue;
|
|
843
|
+
const value = evalDeclaratorValue(decl.init, env);
|
|
844
|
+
if (value.known)
|
|
845
|
+
env.set(name, value.value);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
return env;
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Constant value of a declarator initializer for {@link computeScriptConstEnv},
|
|
853
|
+
* unwrapping the two runes whose argument IS the value a never-written binding
|
|
854
|
+
* holds: `$state(<arg>)` / `$state.raw(<arg>)` (a bare `$state()` /
|
|
855
|
+
* `$state.raw()` is `undefined`). Any other initializer — including every other
|
|
856
|
+
* rune call — is evaluated verbatim, so a non-value rune simply falls to unknown.
|
|
857
|
+
*/
|
|
858
|
+
function evalDeclaratorValue(init, env) {
|
|
859
|
+
if (isStateRuneCall(init)) {
|
|
860
|
+
const arg = init?.arguments?.[0];
|
|
861
|
+
if (arg == null)
|
|
862
|
+
return { known: true, value: undefined }; // bare `$state()` -> undefined
|
|
863
|
+
return evaluate(arg, env);
|
|
864
|
+
}
|
|
865
|
+
return evaluate(init, env);
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* `$state(...)` or `$state.raw(...)` — the two runes whose sole argument is the
|
|
869
|
+
* plain value a never-written binding evaluates to. `$state.snapshot`,
|
|
870
|
+
* `$derived`, `$props`, `$bindable`, etc. are intentionally NOT matched: they are
|
|
871
|
+
* not value-preserving wrappers, so they must stay unknown.
|
|
872
|
+
*/
|
|
873
|
+
function isStateRuneCall(node) {
|
|
874
|
+
if (node?.type !== 'CallExpression')
|
|
875
|
+
return false;
|
|
876
|
+
const callee = node.callee;
|
|
877
|
+
if (callee?.type === 'Identifier')
|
|
878
|
+
return callee.name === '$state';
|
|
879
|
+
return (callee?.type === 'MemberExpression' &&
|
|
880
|
+
callee.computed !== true &&
|
|
881
|
+
callee.object?.type === 'Identifier' &&
|
|
882
|
+
callee.object.name === '$state' &&
|
|
883
|
+
callee.property?.type === 'Identifier' &&
|
|
884
|
+
callee.property.name === 'raw');
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Increment `counts` for every name a binding introduces in `root` (a `<script>`
|
|
888
|
+
* Program or the template fragment): variable declarators (including
|
|
889
|
+
* destructuring), function ids and parameters, and every template binder
|
|
890
|
+
* ({@link collectTemplateBindings} covers the same binders). A name whose total
|
|
891
|
+
* count exceeds one is bound in more than one place — a nested/duplicate
|
|
892
|
+
* declaration or a template binder — so it is shadowed at some scope and is
|
|
893
|
+
* disqualified from {@link computeScriptConstEnv}.
|
|
894
|
+
*/
|
|
895
|
+
function countBindingNames(root, counts) {
|
|
896
|
+
if (!root)
|
|
897
|
+
return;
|
|
898
|
+
const bump = (name) => {
|
|
899
|
+
if (name)
|
|
900
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
901
|
+
};
|
|
902
|
+
const bumpPattern = (pattern) => {
|
|
903
|
+
const names = new Set();
|
|
904
|
+
addPatternNames(pattern, names);
|
|
905
|
+
for (const n of names)
|
|
906
|
+
bump(n);
|
|
907
|
+
};
|
|
908
|
+
// A `try {} catch (e) {}` param is intentionally NOT counted: a call-site
|
|
909
|
+
// expression only resolves against the TOP-LEVEL script scope, which a
|
|
910
|
+
// catch-block-scoped name can never enter, so it cannot shadow a fold there.
|
|
911
|
+
walk(root, null, {
|
|
912
|
+
_(node, { next }) {
|
|
913
|
+
switch (node.type) {
|
|
914
|
+
case 'VariableDeclarator':
|
|
915
|
+
bumpPattern(node.id);
|
|
916
|
+
break;
|
|
917
|
+
case 'FunctionDeclaration':
|
|
918
|
+
case 'FunctionExpression':
|
|
919
|
+
case 'ArrowFunctionExpression':
|
|
920
|
+
if (node.id?.type === 'Identifier')
|
|
921
|
+
bump(node.id.name);
|
|
922
|
+
for (const p of node.params ?? [])
|
|
923
|
+
bumpPattern(p);
|
|
924
|
+
break;
|
|
925
|
+
case 'EachBlock':
|
|
926
|
+
bumpPattern(node.context);
|
|
927
|
+
if (typeof node.index === 'string')
|
|
928
|
+
bump(node.index);
|
|
929
|
+
break;
|
|
930
|
+
case 'SnippetBlock':
|
|
931
|
+
if (node.expression?.type === 'Identifier')
|
|
932
|
+
bump(node.expression.name);
|
|
933
|
+
for (const p of node.parameters ?? [])
|
|
934
|
+
bumpPattern(p);
|
|
935
|
+
break;
|
|
936
|
+
case 'AwaitBlock':
|
|
937
|
+
bumpPattern(node.value);
|
|
938
|
+
bumpPattern(node.error);
|
|
939
|
+
break;
|
|
940
|
+
case 'LetDirective':
|
|
941
|
+
bump(node.name);
|
|
942
|
+
break;
|
|
943
|
+
case 'ConstTag':
|
|
944
|
+
for (const d of node.declaration?.declarations ?? [])
|
|
945
|
+
bumpPattern(d.id);
|
|
946
|
+
break;
|
|
947
|
+
}
|
|
948
|
+
next();
|
|
949
|
+
},
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
/** Add every name a `<script>` Program WRITES (bare-identifier assignment /
|
|
953
|
+
* update targets, at any nesting) to `out` — the module-script counterpart of
|
|
954
|
+
* the write collection {@link collectTemplateBindings} runs over the instance. */
|
|
955
|
+
function collectScriptWrites(program, out) {
|
|
956
|
+
if (!program)
|
|
957
|
+
return;
|
|
958
|
+
walk(program, null, {
|
|
959
|
+
AssignmentExpression(node, { next }) {
|
|
960
|
+
collectWrittenNames(node, out);
|
|
961
|
+
next();
|
|
962
|
+
},
|
|
963
|
+
UpdateExpression(node, { next }) {
|
|
964
|
+
collectWrittenNames(node, out);
|
|
965
|
+
next();
|
|
966
|
+
},
|
|
967
|
+
});
|
|
968
|
+
}
|
|
746
969
|
/**
|
|
747
970
|
* Collect every name bound by a TEMPLATE scope (so a same-named prop is a
|
|
748
971
|
* different entity there) and every name used as a `{@debug}` argument.
|
|
@@ -1612,7 +1835,15 @@ function parseModuleBody(code, id) {
|
|
|
1612
1835
|
* `$props.id()` (a member call) is NOT a `$props()` call — the props object never
|
|
1613
1836
|
* leaks through it — so it does not count and does not affect the result.
|
|
1614
1837
|
*/
|
|
1615
|
-
function computeReachableInputs(instance, props, hasRestProp, propsPattern) {
|
|
1838
|
+
function computeReachableInputs(instance, props, hasRestProp, propsPattern, usesSlotInputs) {
|
|
1839
|
+
// A legacy `<slot>` (or a bare `$$slots` read) observes slotted content —
|
|
1840
|
+
// inputs that arrive OUTSIDE `$props()` (in Svelte 5 terms, the synthetic
|
|
1841
|
+
// `children` input and named-slot / `let:` inputs a call site supplies as body
|
|
1842
|
+
// content). The `$props()` shape cannot model them, so the reverse pass must
|
|
1843
|
+
// treat every input as observable, or it would delete the slot-carrying body at
|
|
1844
|
+
// each call site. This holds whether or not an instance script exists.
|
|
1845
|
+
if (usesSlotInputs)
|
|
1846
|
+
return { kind: 'all' };
|
|
1616
1847
|
// No instance script -> no `$props()` -> the component reads no input at all.
|
|
1617
1848
|
if (!instance)
|
|
1618
1849
|
return { kind: 'names', names: new Set() };
|
|
@@ -1633,6 +1864,32 @@ function computeReachableInputs(instance, props, hasRestProp, propsPattern) {
|
|
|
1633
1864
|
return { kind: 'all' };
|
|
1634
1865
|
return { kind: 'names', names: new Set(props.map((p) => p.name)) };
|
|
1635
1866
|
}
|
|
1867
|
+
/** True when the component observes slotted content outside `$props()`: a legacy
|
|
1868
|
+
* `<slot>` element, or a read of the `$$slots` identifier (legal in runes mode,
|
|
1869
|
+
* unlike `$$props`/`$$restProps`). Either signal means {@link
|
|
1870
|
+
* computeReachableInputs} cannot model the inputs and must fall back to ALL.
|
|
1871
|
+
* `$$slots` can appear in the instance script OR a template expression
|
|
1872
|
+
* (`{#if $$slots.default}`), so both trees are scanned; its `$$` prefix cannot be
|
|
1873
|
+
* a user binding, so no shadowing check is needed. */
|
|
1874
|
+
function usesLegacySlotInputs(ast) {
|
|
1875
|
+
return (nodeSignalsSlotInputs(ast.fragment) || (!!ast.instance && nodeSignalsSlotInputs(ast.instance)));
|
|
1876
|
+
}
|
|
1877
|
+
function nodeSignalsSlotInputs(root) {
|
|
1878
|
+
let found = false;
|
|
1879
|
+
walk(root, null, {
|
|
1880
|
+
SlotElement(_node, { stop }) {
|
|
1881
|
+
found = true;
|
|
1882
|
+
stop();
|
|
1883
|
+
},
|
|
1884
|
+
Identifier(node, { stop }) {
|
|
1885
|
+
if (node.name === '$$slots') {
|
|
1886
|
+
found = true;
|
|
1887
|
+
stop();
|
|
1888
|
+
}
|
|
1889
|
+
},
|
|
1890
|
+
});
|
|
1891
|
+
return found;
|
|
1892
|
+
}
|
|
1636
1893
|
/** True when a `$props()` ObjectPattern binds a prop whose external name is not a
|
|
1637
1894
|
* plain identifier (a string-literal or computed key), so {@link declared_props}
|
|
1638
1895
|
* did not capture it. */
|
package/dist/engine.d.ts
CHANGED
|
@@ -47,13 +47,13 @@ export declare class DevShaker {
|
|
|
47
47
|
/** Last-seen source per file, so an update re-reads only what changed. */
|
|
48
48
|
private readonly codeCache;
|
|
49
49
|
/** Components with a consumer outside the `.svelte` graph (docs §4.2): a
|
|
50
|
-
* `.ts`/`.js` call site or a user `
|
|
50
|
+
* `.ts`/`.js` call site or a user `preserve`. The Shell recomputes and
|
|
51
51
|
* {@link setEscaped}s this whenever a non-`.svelte` module changes. */
|
|
52
52
|
private escaped;
|
|
53
53
|
/** Current slimmed output per `.svelte` id (the live shake result). */
|
|
54
54
|
private output;
|
|
55
55
|
constructor(files: ComponentId | ComponentId[], resolve: Resolve, readFile: ReadFile, mode?: DevMode, parse?: Parse, escaped?: ComponentId[]);
|
|
56
|
-
/** Replace the
|
|
56
|
+
/** Replace the module-escape set (docs §4.2). The Shell calls this before
|
|
57
57
|
* {@link update} when a non-`.svelte` module changed the set of components
|
|
58
58
|
* reached from `.ts`/`.js`, so the next shake bails them. */
|
|
59
59
|
setEscaped(escaped: ComponentId[]): void;
|
package/dist/engine.js
CHANGED
|
@@ -24,7 +24,7 @@ export class DevShaker {
|
|
|
24
24
|
/** Last-seen source per file, so an update re-reads only what changed. */
|
|
25
25
|
codeCache = new Map();
|
|
26
26
|
/** Components with a consumer outside the `.svelte` graph (docs §4.2): a
|
|
27
|
-
* `.ts`/`.js` call site or a user `
|
|
27
|
+
* `.ts`/`.js` call site or a user `preserve`. The Shell recomputes and
|
|
28
28
|
* {@link setEscaped}s this whenever a non-`.svelte` module changes. */
|
|
29
29
|
escaped;
|
|
30
30
|
/** Current slimmed output per `.svelte` id (the live shake result). */
|
|
@@ -38,7 +38,7 @@ export class DevShaker {
|
|
|
38
38
|
this.parse = parse;
|
|
39
39
|
this.escaped = escaped;
|
|
40
40
|
}
|
|
41
|
-
/** Replace the
|
|
41
|
+
/** Replace the module-escape set (docs §4.2). The Shell calls this before
|
|
42
42
|
* {@link update} when a non-`.svelte` module changed the set of components
|
|
43
43
|
* reached from `.ts`/`.js`, so the next shake bails them. */
|
|
44
44
|
setEscaped(escaped) {
|
package/dist/escape-scan.d.ts
CHANGED
|
@@ -7,38 +7,43 @@ import type { Resolve, ReadFile } from './analyze.js';
|
|
|
7
7
|
* uses this to decide whether a changed file can shift the escape set (docs §4.2).
|
|
8
8
|
*/
|
|
9
9
|
export declare function isScannableModule(file: string): boolean;
|
|
10
|
-
/** The components a set of `
|
|
11
|
-
* {@link
|
|
10
|
+
/** The components a set of `preserve` prefixes protects (docs §4.2). The
|
|
11
|
+
* {@link partitionPreserve} projection that drops the "matched nothing" diagnostic
|
|
12
12
|
* — kept for callers that only need the ids. */
|
|
13
|
-
export declare function
|
|
13
|
+
export declare function matchPreserve(preserve: string[] | undefined, root: string, components: Iterable<ComponentId>): ComponentId[];
|
|
14
14
|
/**
|
|
15
15
|
* The whole escape set for a build (docs §4.2), plus the diagnostics a Shell must
|
|
16
16
|
* surface. `escaped` is the union of components a non-`.svelte` module imports
|
|
17
|
-
* (found by scanning) and those the user named via `
|
|
17
|
+
* (found by scanning) and those the user named via `preserve`. `unscannable` are
|
|
18
18
|
* modules the scan could not read/parse (a component mounted from one is NOT
|
|
19
|
-
* escaped — the Shell warns and the user lists it in `
|
|
20
|
-
* are `
|
|
21
|
-
* component
|
|
19
|
+
* escaped — the Shell warns and the user lists it in `preserve`); `unmatchedPreserve`
|
|
20
|
+
* are `preserve` entries that matched no component (a typo leaves the intended
|
|
21
|
+
* component unpreserved). Returning them as data lets each Shell — the Vite plugin,
|
|
22
22
|
* or a future `eslint-plugin-svelte` rule — report in its own voice.
|
|
23
23
|
*/
|
|
24
24
|
export interface EscapeScanResult {
|
|
25
25
|
escaped: ComponentId[];
|
|
26
26
|
unscannable: ComponentId[];
|
|
27
|
-
|
|
27
|
+
unmatchedPreserve: string[];
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
|
-
* Compute the {@link EscapeScanResult} from "
|
|
30
|
+
* Compute the {@link EscapeScanResult} from "entry roots + resolver + component
|
|
31
31
|
* set". The one helper a Shell calls (the only escape-scan symbol re-exported from
|
|
32
32
|
* `svelte-shaker/node`).
|
|
33
33
|
*/
|
|
34
34
|
export declare function computeEscapedComponents(opts: {
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Absolute crawl-entry roots (already resolved against the project root) — the
|
|
37
|
+
* Vite plugin's `entries`. The SAME roots drive two different walks: collecting
|
|
38
|
+
* the `.svelte` crawl entries, and (here) collecting the non-`.svelte` modules to
|
|
39
|
+
* scan for call sites that escape the component graph.
|
|
40
|
+
*/
|
|
41
|
+
entryDirs: string[];
|
|
42
|
+
/** Project root, for resolving relative `preserve` entries. */
|
|
38
43
|
root: string;
|
|
39
|
-
/** User-declared `
|
|
40
|
-
|
|
41
|
-
/** The crawled `.svelte` component ids, for matching `
|
|
44
|
+
/** User-declared `preserve` prefixes (root-relative or absolute), if any. */
|
|
45
|
+
preserve?: string[] | undefined;
|
|
46
|
+
/** The crawled `.svelte` component ids, for matching `preserve` prefixes. */
|
|
42
47
|
components: Iterable<ComponentId>;
|
|
43
48
|
resolve: Resolve;
|
|
44
49
|
readFile: ReadFile;
|
package/dist/escape-scan.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ----------------------------------------------------------------------
|
|
2
2
|
// Internal Shell helper (docs/ARCHITECTURE.md §4.2): find the components used from
|
|
3
3
|
// OUTSIDE the analyzed `.svelte` graph — a `.ts`/`.js` call site the crawl cannot
|
|
4
|
-
// parse, or a user-declared `
|
|
4
|
+
// parse, or a user-declared `preserve`. NOT part of the `svelte-shaker/node` public
|
|
5
5
|
// surface: only `computeEscapedComponents` is re-exported there (`scan.ts`). The
|
|
6
6
|
// Vite Shell imports the rest of this module directly.
|
|
7
7
|
// ----------------------------------------------------------------------
|
|
@@ -57,13 +57,13 @@ function collectNonSvelteModules(dir) {
|
|
|
57
57
|
* Every module specifier a JS/TS module statically references: `import … from`,
|
|
58
58
|
* `export … from` / `export *`, and a dynamic `import('…')` whose argument is a
|
|
59
59
|
* STRING LITERAL. A computed dynamic import (`import(expr)`) is unknowable here —
|
|
60
|
-
* the `
|
|
60
|
+
* the `preserve` option (docs §4.2) is the sound fallback for it. Parsed via the
|
|
61
61
|
* Svelte parser's `<script module lang="ts">` wrapper (the same TS-capable parse
|
|
62
62
|
* the engine's barrel-following uses). Returns `null` when the module does NOT
|
|
63
63
|
* parse — the wrapper handles TS but not JSX, and rejects some exotic syntax
|
|
64
64
|
* (`.jsx`/`.tsx` bodies, bleeding-edge TS), and a parse failure hides any call site
|
|
65
65
|
* inside, so the caller must surface it (a mounted component would go un-escaped);
|
|
66
|
-
* `
|
|
66
|
+
* `preserve` is the fix for that file.
|
|
67
67
|
*/
|
|
68
68
|
function moduleImportSpecifiers(code, id) {
|
|
69
69
|
let module;
|
|
@@ -71,7 +71,7 @@ function moduleImportSpecifiers(code, id) {
|
|
|
71
71
|
module = parseSvelte(`<script module lang="ts">\n${code}\n</script>`, id).module;
|
|
72
72
|
}
|
|
73
73
|
catch {
|
|
74
|
-
return null; // unparseable — the caller reports it (soundness hole, `
|
|
74
|
+
return null; // unparseable — the caller reports it (soundness hole, `preserve` fixes it)
|
|
75
75
|
}
|
|
76
76
|
const program = module?.content;
|
|
77
77
|
if (!program)
|
|
@@ -115,7 +115,7 @@ function moduleImportSpecifiers(code, id) {
|
|
|
115
115
|
* A module that cannot be read or parsed is collected into `unscannable` (NOT
|
|
116
116
|
* silently dropped): a call site inside it is invisible, so the caller must warn.
|
|
117
117
|
*/
|
|
118
|
-
async function
|
|
118
|
+
async function collectModuleEscapes(modules, resolve, readFile) {
|
|
119
119
|
const escaped = new Set();
|
|
120
120
|
const unscannable = new Set();
|
|
121
121
|
for (const id of modules) {
|
|
@@ -141,22 +141,22 @@ async function collectExternalEscapes(modules, resolve, readFile) {
|
|
|
141
141
|
return { escaped, unscannable };
|
|
142
142
|
}
|
|
143
143
|
/**
|
|
144
|
-
* Partition user-declared `
|
|
145
|
-
* set into the components they
|
|
144
|
+
* Partition user-declared `preserve` prefixes (docs §4.2) against a known component
|
|
145
|
+
* set into the components they PRESERVE and the entries that matched NOTHING. Each
|
|
146
146
|
* entry is a Vite-root-relative or absolute path naming EITHER a component file
|
|
147
147
|
* (exact match) OR a directory (every component under it) — the same "directory or
|
|
148
|
-
* file prefix" basis as `
|
|
148
|
+
* file prefix" basis as `entries`, with no glob dependency. An entry matching no
|
|
149
149
|
* component is almost always a typo / wrong path (or a missing `.svelte`
|
|
150
150
|
* extension), so it is returned in `unmatched` for the caller to surface rather
|
|
151
|
-
* than being a silent no-op that leaves the intended component
|
|
151
|
+
* than being a silent no-op that leaves the intended component unpreserved.
|
|
152
152
|
*/
|
|
153
|
-
function
|
|
153
|
+
function partitionPreserve(preserve, root, components) {
|
|
154
154
|
const matched = new Set();
|
|
155
155
|
const unmatched = [];
|
|
156
|
-
if (!
|
|
156
|
+
if (!preserve || preserve.length === 0)
|
|
157
157
|
return { matched, unmatched };
|
|
158
158
|
const ids = [...components];
|
|
159
|
-
for (const entry of
|
|
159
|
+
for (const entry of preserve) {
|
|
160
160
|
// `path.resolve` leaves an absolute entry as-is and resolves a relative one
|
|
161
161
|
// against `root` — exactly "Vite-root-relative or absolute".
|
|
162
162
|
const prefix = path.resolve(root, entry);
|
|
@@ -169,25 +169,25 @@ function partitionExternal(external, root, components) {
|
|
|
169
169
|
}
|
|
170
170
|
return { matched, unmatched };
|
|
171
171
|
}
|
|
172
|
-
/** The components a set of `
|
|
173
|
-
* {@link
|
|
172
|
+
/** The components a set of `preserve` prefixes protects (docs §4.2). The
|
|
173
|
+
* {@link partitionPreserve} projection that drops the "matched nothing" diagnostic
|
|
174
174
|
* — kept for callers that only need the ids. */
|
|
175
|
-
export function
|
|
176
|
-
return [...
|
|
175
|
+
export function matchPreserve(preserve, root, components) {
|
|
176
|
+
return [...partitionPreserve(preserve, root, components).matched];
|
|
177
177
|
}
|
|
178
178
|
/**
|
|
179
|
-
* Compute the {@link EscapeScanResult} from "
|
|
179
|
+
* Compute the {@link EscapeScanResult} from "entry roots + resolver + component
|
|
180
180
|
* set". The one helper a Shell calls (the only escape-scan symbol re-exported from
|
|
181
181
|
* `svelte-shaker/node`).
|
|
182
182
|
*/
|
|
183
183
|
export async function computeEscapedComponents(opts) {
|
|
184
|
-
const { escaped, unscannable } = await
|
|
185
|
-
const { matched, unmatched } =
|
|
184
|
+
const { escaped, unscannable } = await collectModuleEscapes(opts.entryDirs.flatMap(collectNonSvelteModules), opts.resolve, opts.readFile);
|
|
185
|
+
const { matched, unmatched } = partitionPreserve(opts.preserve, opts.root, opts.components);
|
|
186
186
|
for (const id of matched)
|
|
187
187
|
escaped.add(id);
|
|
188
188
|
return {
|
|
189
189
|
escaped: [...escaped],
|
|
190
190
|
unscannable: [...unscannable].sort(),
|
|
191
|
-
|
|
191
|
+
unmatchedPreserve: unmatched,
|
|
192
192
|
};
|
|
193
193
|
}
|
package/dist/ir.d.ts
CHANGED
|
@@ -39,12 +39,13 @@ export interface AnalyzeInput {
|
|
|
39
39
|
/**
|
|
40
40
|
* Components with at least one consumer OUTSIDE the analyzed `.svelte` graph —
|
|
41
41
|
* a call site in a `.ts`/`.js` module (`mount(Comp, …)`, a lazy `import()`), or
|
|
42
|
-
* a user-declared `
|
|
42
|
+
* a user-declared `preserve` (docs/ARCHITECTURE.md §4.2). The Shell computes
|
|
43
43
|
* this set (its FS scan cannot parse `.ts` call sites); the engine unions it
|
|
44
44
|
* into the same whole-component escape bail auto-detected escapes use, so these
|
|
45
45
|
* components are never folded and never reported as never-passed — while their
|
|
46
|
-
* OWN call sites still count toward their children. Omitted/`[]` means
|
|
47
|
-
*
|
|
46
|
+
* OWN call sites still count toward their children. Omitted/`[]` means every
|
|
47
|
+
* consumer is inside the crawled `.svelte` graph, keeping the output
|
|
48
|
+
* byte-for-byte unchanged.
|
|
48
49
|
*/
|
|
49
50
|
escaped?: ComponentId[];
|
|
50
51
|
}
|
package/dist/parse.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface AnyNode {
|
|
|
21
21
|
init?: AnyNode | undefined;
|
|
22
22
|
key?: AnyNode | undefined;
|
|
23
23
|
property?: AnyNode | undefined;
|
|
24
|
+
/** MemberExpression object (`$state` in `$state.raw`). */
|
|
25
|
+
object?: AnyNode | undefined;
|
|
24
26
|
source?: AnyNode | undefined;
|
|
25
27
|
local?: AnyNode | undefined;
|
|
26
28
|
/** ImportSpecifier / ExportSpecifier exported-name slot. */
|
|
@@ -46,6 +48,8 @@ export interface AnyNode {
|
|
|
46
48
|
members?: AnyNode[] | undefined;
|
|
47
49
|
body?: AnyNode[] | undefined;
|
|
48
50
|
declarations?: AnyNode[] | undefined;
|
|
51
|
+
/** CallExpression / NewExpression arguments (`0` in `$state(0)`). */
|
|
52
|
+
arguments?: (AnyNode | null)[] | undefined;
|
|
49
53
|
specifiers?: AnyNode[] | undefined;
|
|
50
54
|
nodes?: AnyNode[] | undefined;
|
|
51
55
|
/** SnippetBlock parameters; ArrayPattern elements; DebugTag identifiers. */
|
|
Binary file
|
package/dist/transform.js
CHANGED
|
@@ -19,6 +19,21 @@ export function transformAll(models, plans) {
|
|
|
19
19
|
}
|
|
20
20
|
/** Shared empty local fold env for a bailed owner (nothing forwards a constant). */
|
|
21
21
|
const EMPTY_LOCAL_ENV = new Map();
|
|
22
|
+
/**
|
|
23
|
+
* Merge an owner's static script constants with its remapped folded props for the
|
|
24
|
+
* phase-2 side-effect check. Both are keyed by LOCAL name and are disjoint (a
|
|
25
|
+
* `$props()` local and a top-level script const cannot share a name — JS
|
|
26
|
+
* redeclaration), so the merge is order-independent; either operand is returned
|
|
27
|
+
* as-is when the other is empty (the common case). Mirrors analyze.ts's
|
|
28
|
+
* `mergeScriptConsts` (kept separate so neither module imports the other).
|
|
29
|
+
*/
|
|
30
|
+
function mergeLocalConstEnv(scriptConsts, foldedProps) {
|
|
31
|
+
if (scriptConsts.size === 0)
|
|
32
|
+
return foldedProps;
|
|
33
|
+
if (foldedProps.size === 0)
|
|
34
|
+
return scriptConsts;
|
|
35
|
+
return new Map([...scriptConsts, ...foldedProps]);
|
|
36
|
+
}
|
|
22
37
|
/**
|
|
23
38
|
* Phases 1–2, shared by {@link transformAll} and {@link transformAllWithMono}:
|
|
24
39
|
* fold each component body and drop its folded props (phase 1), then strip the
|
|
@@ -68,11 +83,13 @@ function runBasePhases(models, plans) {
|
|
|
68
83
|
// skipping any call site phase 1 folded away (its attributes went with it).
|
|
69
84
|
for (const model of models.values()) {
|
|
70
85
|
const plan = plans.get(model.id);
|
|
71
|
-
// A forwarded expression (`<Child prop={ownerProp}/>`)
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
86
|
+
// A forwarded expression (`<Child prop={ownerProp}/>`) that the owner proves
|
|
87
|
+
// constant — a folded prop OR an owner-local script constant (docs §13.1) — is
|
|
88
|
+
// side-effect-free, so once the child drops the prop its attribute is as
|
|
89
|
+
// removable as a written literal. Give phase 2 the owner's fold env plus its
|
|
90
|
+
// `scriptConstEnv`, both local-keyed as the expression references props/locals.
|
|
91
|
+
const foldEnv = plan.bail ? EMPTY_LOCAL_ENV : remapToLocalNames(plan.constFold, model);
|
|
92
|
+
const ownerEnv = mergeLocalConstEnv(model.scriptConstEnv, foldEnv);
|
|
76
93
|
removeCallSiteAttributes(model, dropped, strings.get(model.id), editedSpans.get(model.id) ?? [], ownerEnv);
|
|
77
94
|
}
|
|
78
95
|
// Phase 2.5 — reverse (docs §PR4) + unread declared (docs §PR7): delete the
|
package/dist/unread.js
CHANGED
|
@@ -81,7 +81,8 @@ export function collectUnread(models, plans) {
|
|
|
81
81
|
// (b): a prop is droppable when it survived every site's veto (and had the
|
|
82
82
|
// structural gates). A child with NO call sites keeps every `true` here — safe,
|
|
83
83
|
// since the child does not read the prop, so its own render is unchanged whether
|
|
84
|
-
// it is declared or not (and
|
|
84
|
+
// it is declared or not (and a consumer outside the `.svelte` graph, if any,
|
|
85
|
+
// bails the child).
|
|
85
86
|
for (const [id, perProp] of dropEligible) {
|
|
86
87
|
const set = new Set();
|
|
87
88
|
for (const [name, ok] of perProp)
|
package/dist/vite.d.ts
CHANGED
|
@@ -1,32 +1,57 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
2
|
import { type DevMode } from './engine.js';
|
|
3
3
|
import { type MonomorphizeOptions } from './mono.js';
|
|
4
|
+
/**
|
|
5
|
+
* Options for the {@link shaker} plugin. The set below is exhaustive: any other
|
|
6
|
+
* key throws at build start rather than being ignored, because an option we never
|
|
7
|
+
* read is an option the user thinks is applied and is not.
|
|
8
|
+
*/
|
|
4
9
|
export interface ShakerOptions {
|
|
5
10
|
/**
|
|
6
|
-
* Directories (relative to the Vite root)
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
11
|
+
* Directories (relative to the Vite root) the component crawl STARTS from —
|
|
12
|
+
* the same semantics as SvelteKit's `config.kit.prerender.entries`: you list
|
|
13
|
+
* the roots, and everything reachable from them is pulled in by following the
|
|
14
|
+
* import graph. Defaults to the Vite root itself. No glob: each entry is a
|
|
15
|
+
* directory (or a single component file), matched on a plain path-prefix basis.
|
|
16
|
+
*
|
|
17
|
+
* This is NOT an include filter, and it does not bound what gets rewritten: a
|
|
18
|
+
* library component in `node_modules` is never under an entry root, yet it is
|
|
19
|
+
* still crawled and shaken, because a component that IS under one imports it.
|
|
20
|
+
*
|
|
21
|
+
* The soundness contract runs the other way. Every `.svelte` file found is
|
|
22
|
+
* treated as a call-site source, so the union of these roots must cover EVERY
|
|
23
|
+
* call site in the app (docs/ARCHITECTURE.md §4.2). Narrowing `entries` to a
|
|
24
|
+
* subset of the app does not shake less — it hides call sites, and a prop the
|
|
25
|
+
* shaker cannot see being passed is folded away.
|
|
10
26
|
*/
|
|
11
|
-
|
|
27
|
+
entries?: string[];
|
|
12
28
|
/**
|
|
13
|
-
* Components
|
|
14
|
-
*
|
|
15
|
-
* relative or absolute path naming EITHER a component file or a
|
|
16
|
-
* them — the same "directory or file prefix" basis as
|
|
29
|
+
* Components whose PROP INTERFACE must be left exactly as written, because they
|
|
30
|
+
* have a consumer the shake cannot see (docs/ARCHITECTURE.md §4.2). Each entry
|
|
31
|
+
* is a Vite-root-relative or absolute path naming EITHER a component file or a
|
|
32
|
+
* directory of them — the same "directory or file prefix" basis as
|
|
33
|
+
* {@link entries}, no glob.
|
|
34
|
+
*
|
|
35
|
+
* What is preserved is the prop interface, NOT the component's presence in the
|
|
36
|
+
* bundle: this is unrelated to Rollup/Vite's `external`, and it never keeps a
|
|
37
|
+
* file out of the bundle or out of the analysis.
|
|
38
|
+
*
|
|
39
|
+
* Reach for it when a consumer lives outside the `.svelte` graph and the shaker
|
|
40
|
+
* cannot observe that call site: a `mount()` behind a NON-literal dynamic
|
|
41
|
+
* `import(expr)`, or a module outside the {@link entries} roots. Consumers
|
|
42
|
+
* reached by a static `.ts`/`.js` import are already found by the non-`.svelte`
|
|
43
|
+
* module scan, so those need no entry here.
|
|
17
44
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* a
|
|
45
|
+
* A-semantics — listing a component does NOT exclude it from the scan: the file
|
|
46
|
+
* stays fully in the analysis (its own `<Child/>` call sites keep counting toward
|
|
47
|
+
* its children's profiles), and only the component ITSELF has its prop folding /
|
|
48
|
+
* never-passed reporting turned off. It is not a way to make the shaker ignore
|
|
49
|
+
* a file.
|
|
22
50
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* counting toward its children's profiles), and only the component ITSELF has its
|
|
26
|
-
* prop folding / never-passed reporting turned off. It is not a way to make the
|
|
27
|
-
* shaker ignore a file.
|
|
51
|
+
* Over-listing errs SAFE, the opposite of {@link entries}: a component preserved
|
|
52
|
+
* without needing it is merely shaken less, never wrongly.
|
|
28
53
|
*/
|
|
29
|
-
|
|
54
|
+
preserve?: string[];
|
|
30
55
|
/**
|
|
31
56
|
* Per-call-site monomorphization tuning (docs §13.2). Monomorphization is ON
|
|
32
57
|
* by default because it is bail-safe and never bloats (the measured net-win
|
package/dist/vite.js
CHANGED
|
@@ -99,6 +99,62 @@ function resolveMono(options) {
|
|
|
99
99
|
const overrides = typeof options.monomorphize === 'object' ? options.monomorphize : {};
|
|
100
100
|
return { ...DEFAULT_MONO_OPTIONS, enabled: true, ...overrides };
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Options renamed in the pre-1.0 cleanup: stale key -> what to write instead and
|
|
104
|
+
* why the old name described the wrong thing. Both renames dropped a name the
|
|
105
|
+
* ecosystem had already spent on a DIFFERENT meaning, so the message has to say
|
|
106
|
+
* more than "renamed" — a user who reaches for the old key is usually carrying
|
|
107
|
+
* the old key's ecosystem meaning with it.
|
|
108
|
+
*/
|
|
109
|
+
const RENAMED_OPTIONS = {
|
|
110
|
+
include: 'the "include" option was renamed to "entries": rename `include:` to `entries:`. It was ' +
|
|
111
|
+
'never a file filter — it lists the directories the component crawl STARTS from, and ' +
|
|
112
|
+
'everything reachable from them (including library components under node_modules) is ' +
|
|
113
|
+
'shaken whether or not it sits under one.',
|
|
114
|
+
external: 'the "external" option was renamed to "preserve": rename `external:` to `preserve:`. It ' +
|
|
115
|
+
'has nothing to do with Rollup/Vite `external` — it never keeps a file out of the bundle. ' +
|
|
116
|
+
'It names components whose prop interface must be left as written because a consumer the ' +
|
|
117
|
+
'shaker cannot see (a non-literal dynamic `import(expr)`, a module outside `entries`) ' +
|
|
118
|
+
'passes props to them.',
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Every key {@link ShakerOptions} accepts. The `satisfies` is the point of the
|
|
122
|
+
* table: adding an option to the interface without listing it here fails
|
|
123
|
+
* `tsc --noEmit`, so the accept-list can never drift behind the type and start
|
|
124
|
+
* rejecting a valid config.
|
|
125
|
+
*/
|
|
126
|
+
const KNOWN_OPTIONS = {
|
|
127
|
+
entries: true,
|
|
128
|
+
preserve: true,
|
|
129
|
+
monomorphize: true,
|
|
130
|
+
engine: true,
|
|
131
|
+
dev: true,
|
|
132
|
+
parser: true,
|
|
133
|
+
verbose: true,
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Reject anything that is not an option we act on. A Vite config is external
|
|
137
|
+
* input, so this is a boundary check, not a courtesy: an ignored key still
|
|
138
|
+
* builds, but what the user configured quietly does not apply — `entries` falls
|
|
139
|
+
* back to the Vite root, `preserve` to an empty set, and that last one ships an
|
|
140
|
+
* over-shaken component.
|
|
141
|
+
*
|
|
142
|
+
* A typo (`preserv:`) fails in exactly that way, which is why unknown keys are
|
|
143
|
+
* rejected on the same footing as the pre-rename keys ({@link RENAMED_OPTIONS})
|
|
144
|
+
* rather than only the ones we happen to have a migration note for. TypeScript's
|
|
145
|
+
* excess-property check only fires on an object literal written inline, so a
|
|
146
|
+
* config built up in a variable — or any JS config — reaches us unguarded.
|
|
147
|
+
*/
|
|
148
|
+
function assertValidOptions(options) {
|
|
149
|
+
for (const key of Object.keys(options)) {
|
|
150
|
+
if (Object.hasOwn(RENAMED_OPTIONS, key))
|
|
151
|
+
throw new Error(`[vite-plugin-svelte-shaker] ${RENAMED_OPTIONS[key]}`);
|
|
152
|
+
if (!Object.hasOwn(KNOWN_OPTIONS, key))
|
|
153
|
+
throw new Error(`[vite-plugin-svelte-shaker] unknown option "${key}". Valid options are: ` +
|
|
154
|
+
`${Object.keys(KNOWN_OPTIONS).join(', ')}. Check the spelling — an option we ` +
|
|
155
|
+
`do not read is an option that does not apply.`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
102
158
|
/**
|
|
103
159
|
* Source-level Svelte tree-shaking as a Vite plugin (docs/ARCHITECTURE.md §6).
|
|
104
160
|
*
|
|
@@ -119,6 +175,7 @@ function resolveMono(options) {
|
|
|
119
175
|
* variant id (dedup), so they share one compiled module.
|
|
120
176
|
*/
|
|
121
177
|
export function shaker(options = {}) {
|
|
178
|
+
assertValidOptions(options);
|
|
122
179
|
const mono = resolveMono(options);
|
|
123
180
|
let shaken = {};
|
|
124
181
|
/** Variant request id (`<childPath>?shaker_variant=<n>`) -> residual source. */
|
|
@@ -129,22 +186,24 @@ export function shaker(options = {}) {
|
|
|
129
186
|
let log = (msg) => console.info(`[svelte-shaker] ${msg}`);
|
|
130
187
|
let warn = (msg) => console.warn(`[svelte-shaker] ${msg}`);
|
|
131
188
|
// Surface the escape scan's diagnostics (docs §4.2) as build warnings: modules the
|
|
132
|
-
// scan could not parse (a component mounted from one is left
|
|
133
|
-
// soundness hole), and `
|
|
134
|
-
// the intended component
|
|
189
|
+
// scan could not parse (a component mounted from one is left unpreserved — a real
|
|
190
|
+
// soundness hole), and `preserve` entries that matched no component (a typo leaves
|
|
191
|
+
// the intended component unpreserved). Both are actionable and name the offenders,
|
|
135
192
|
// but never fail the build — the shake is still emitted.
|
|
136
193
|
const reportEscapeDiagnostics = (result) => {
|
|
137
194
|
if (result.unscannable.length > 0) {
|
|
138
|
-
warn(`
|
|
139
|
-
|
|
140
|
-
`
|
|
195
|
+
warn(`could not parse ${result.unscannable.length} non-\`.svelte\` module(s), so any ` +
|
|
196
|
+
`component mounted from one of them is invisible to the shake and may be ` +
|
|
197
|
+
`over-shaken (props it is really passed folded away). If any of these mounts a ` +
|
|
198
|
+
`component, list that component in the \`preserve\` option to keep its props:\n` +
|
|
141
199
|
result.unscannable.map((f) => ` - ${path.relative(root, f) || f}`).join('\n'));
|
|
142
200
|
}
|
|
143
|
-
if (result.
|
|
144
|
-
warn(`\`
|
|
145
|
-
`${result.
|
|
146
|
-
`file entry keeps its \`.svelte\` extension); the intended component is NOT
|
|
147
|
-
|
|
201
|
+
if (result.unmatchedPreserve.length > 0) {
|
|
202
|
+
warn(`\`preserve\` matched no component for ${result.unmatchedPreserve.length} entr` +
|
|
203
|
+
`${result.unmatchedPreserve.length === 1 ? 'y' : 'ies'} (check the path, and that a ` +
|
|
204
|
+
`file entry keeps its \`.svelte\` extension); the intended component is NOT ` +
|
|
205
|
+
`preserved, so its props can still be folded:\n` +
|
|
206
|
+
result.unmatchedPreserve.map((e) => ` - ${e}`).join('\n'));
|
|
148
207
|
}
|
|
149
208
|
};
|
|
150
209
|
// Dev (serve) shaking is opt-in (docs §6.2); `null` keeps dev a pass-through.
|
|
@@ -203,22 +262,24 @@ export function shaker(options = {}) {
|
|
|
203
262
|
async configureServer(server) {
|
|
204
263
|
if (!devMode)
|
|
205
264
|
return;
|
|
206
|
-
const dirs = (options.
|
|
207
|
-
|
|
265
|
+
const dirs = (options.entries ?? ['.']).map((p) => path.resolve(root, p));
|
|
266
|
+
// The engine's entry components, collected from the entry DIRS: `entries`
|
|
267
|
+
// names the roots to crawl from, these are the `.svelte` files under them.
|
|
268
|
+
const entryComponents = dirs.flatMap(collectSvelteFiles);
|
|
208
269
|
const read = (id) => fs.readFileSync(id, 'utf-8');
|
|
209
270
|
const underDirs = (file) => dirs.some((d) => file === d || file.startsWith(d + path.sep));
|
|
210
271
|
// Re-scan the non-`.svelte` modules for `.svelte` call sites and re-apply
|
|
211
|
-
// `
|
|
272
|
+
// `preserve` (docs §4.2). Dev uses the same relative-only `fsResolve` the dev
|
|
212
273
|
// crawl uses, so the escape scope matches the crawl scope — dev is never more
|
|
213
274
|
// unsound than build. The sorted-join key lets a change that does not move the
|
|
214
275
|
// escape set skip the reload.
|
|
215
276
|
let escapedKey = '';
|
|
216
277
|
const currentEscaped = async () => {
|
|
217
278
|
const result = await computeEscapedComponents({
|
|
218
|
-
|
|
279
|
+
entryDirs: dirs,
|
|
219
280
|
root,
|
|
220
|
-
|
|
221
|
-
components:
|
|
281
|
+
preserve: options.preserve,
|
|
282
|
+
components: entryComponents,
|
|
222
283
|
resolve: fsResolve,
|
|
223
284
|
readFile: read,
|
|
224
285
|
});
|
|
@@ -226,7 +287,7 @@ export function shaker(options = {}) {
|
|
|
226
287
|
escapedKey = [...result.escaped].sort().join('\n');
|
|
227
288
|
return result.escaped;
|
|
228
289
|
};
|
|
229
|
-
devShaker = new DevShaker(
|
|
290
|
+
devShaker = new DevShaker(entryComponents, fsResolve, read, devMode, getParse(), await currentEscaped());
|
|
230
291
|
shaken = await devShaker.init();
|
|
231
292
|
const applyDelta = (result) => {
|
|
232
293
|
for (const [id, code] of Object.entries(result.changed))
|
|
@@ -301,9 +362,11 @@ export function shaker(options = {}) {
|
|
|
301
362
|
async buildStart() {
|
|
302
363
|
if (devShaker)
|
|
303
364
|
return;
|
|
304
|
-
const dirs = (options.
|
|
305
|
-
|
|
306
|
-
|
|
365
|
+
const dirs = (options.entries ?? ['.']).map((p) => path.resolve(root, p));
|
|
366
|
+
// The engine's entry components, collected from the entry DIRS: `entries`
|
|
367
|
+
// names the roots to crawl from, these are the `.svelte` files under them.
|
|
368
|
+
const entryComponents = dirs.flatMap(collectSvelteFiles);
|
|
369
|
+
if (entryComponents.length === 0) {
|
|
307
370
|
shaken = {};
|
|
308
371
|
variantSources = new Map();
|
|
309
372
|
return;
|
|
@@ -334,13 +397,13 @@ export function shaker(options = {}) {
|
|
|
334
397
|
};
|
|
335
398
|
// Components used from OUTSIDE the `.svelte` graph must not be folded (docs
|
|
336
399
|
// §4.2): those a non-`.svelte` module imports (found by the scan) plus any the
|
|
337
|
-
// user
|
|
338
|
-
// scan's diagnostics (unparseable modules / unmatched `
|
|
400
|
+
// user pinned via `preserve`. Hand the engine that escape set, and surface the
|
|
401
|
+
// scan's diagnostics (unparseable modules / unmatched `preserve`) as warnings.
|
|
339
402
|
const escapeScan = await computeEscapedComponents({
|
|
340
|
-
|
|
403
|
+
entryDirs: dirs,
|
|
341
404
|
root,
|
|
342
|
-
|
|
343
|
-
components:
|
|
405
|
+
preserve: options.preserve,
|
|
406
|
+
components: entryComponents,
|
|
344
407
|
resolve,
|
|
345
408
|
readFile: read,
|
|
346
409
|
});
|
|
@@ -367,12 +430,12 @@ export function shaker(options = {}) {
|
|
|
367
430
|
// Native Rust engine — byte-identical to the JS engine, including
|
|
368
431
|
// monomorphization.
|
|
369
432
|
if (mono.enabled) {
|
|
370
|
-
const result = await svelteShakerWasmWithMono(wasm,
|
|
433
|
+
const result = await svelteShakerWasmWithMono(wasm, entryComponents, resolve, read, mono, getParse(), escaped);
|
|
371
434
|
shaken = result.files;
|
|
372
435
|
variantSources = result.variants;
|
|
373
436
|
}
|
|
374
437
|
else {
|
|
375
|
-
shaken = await svelteShakerWasm(wasm,
|
|
438
|
+
shaken = await svelteShakerWasm(wasm, entryComponents, resolve, read, getParse(), escaped);
|
|
376
439
|
variantSources = new Map();
|
|
377
440
|
}
|
|
378
441
|
reportSizes(shaken, read, root, options.verbose === true, log);
|
|
@@ -381,12 +444,12 @@ export function shaker(options = {}) {
|
|
|
381
444
|
if (!mono.enabled) {
|
|
382
445
|
// JS engine, monomorphization off: byte-for-byte the unused-prop fold /
|
|
383
446
|
// constant fold / value-set narrowing output.
|
|
384
|
-
shaken = await svelteShaker(
|
|
447
|
+
shaken = await svelteShaker(entryComponents, resolve, read, getParse(), escaped);
|
|
385
448
|
variantSources = new Map();
|
|
386
449
|
reportSizes(shaken, read, root, options.verbose === true, log);
|
|
387
450
|
return;
|
|
388
451
|
}
|
|
389
|
-
const result = await svelteShakerWithMono(
|
|
452
|
+
const result = await svelteShakerWithMono(entryComponents, resolve, read, mono, variantSpecifier, getParse(), escaped);
|
|
390
453
|
shaken = result.files;
|
|
391
454
|
variantSources = new Map();
|
|
392
455
|
for (const v of result.mono.variants.values())
|