svelte-shaker 0.14.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/dist/analyze.d.ts +7 -4
- package/dist/analyze.js +57 -20
- package/dist/engine.d.ts +1 -1
- package/dist/engine.js +1 -1
- package/dist/escape-scan.js +2 -2
- package/dist/ir.d.ts +3 -2
- package/dist/unread.js +2 -1
- package/package.json +1 -1
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';
|
|
@@ -275,8 +277,9 @@ export interface UnpassedProp {
|
|
|
275
277
|
* spread that could set it (`readCallSite` already folds `bind:`, known
|
|
276
278
|
* spreads, and `children`/snippet body into `explicit`/`hadSpread`);
|
|
277
279
|
* - a component in `input.escaped` — one the Shell knows has a consumer OUTSIDE
|
|
278
|
-
* the `.svelte` graph (a
|
|
279
|
-
* §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.
|
|
280
283
|
*
|
|
281
284
|
* Missing a `.svelte` EDGE (e.g. an unfollowed barrel) only DROPS call sites, so it
|
|
282
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 `preserve` (docs §4.2, {@link AnalyzeInput.escaped}).
|
|
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 `preserve` entry or a scanned
|
|
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 `preserve`, docs §4.2) join the same
|
|
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
|
/**
|
|
@@ -499,8 +500,9 @@ export function deadSpansForPlans(models, plans) {
|
|
|
499
500
|
* spread that could set it (`readCallSite` already folds `bind:`, known
|
|
500
501
|
* spreads, and `children`/snippet body into `explicit`/`hadSpread`);
|
|
501
502
|
* - a component in `input.escaped` — one the Shell knows has a consumer OUTSIDE
|
|
502
|
-
* the `.svelte` graph (a
|
|
503
|
-
* §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.
|
|
504
506
|
*
|
|
505
507
|
* Missing a `.svelte` EDGE (e.g. an unfollowed barrel) only DROPS call sites, so it
|
|
506
508
|
* can only make this UNDER-report (the component looks unused and is skipped). The
|
|
@@ -521,9 +523,10 @@ export function findNeverPassedProps(input) {
|
|
|
521
523
|
if (model && !model.bailReasons.includes(ESCAPE_REASON))
|
|
522
524
|
model.bailReasons.push(ESCAPE_REASON);
|
|
523
525
|
}
|
|
524
|
-
//
|
|
525
|
-
// they pass is never mis-reported as
|
|
526
|
-
|
|
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);
|
|
527
530
|
// Every textual call site counts (no cascade dead-span filtering): a prop passed
|
|
528
531
|
// only at a folded-away site is still author-written, so we do not flag it.
|
|
529
532
|
const usage = buildUsage(models, new Map());
|
|
@@ -666,7 +669,7 @@ function buildModelFromInput(file, edges, parseCache) {
|
|
|
666
669
|
}
|
|
667
670
|
}
|
|
668
671
|
}
|
|
669
|
-
const reachableInputs = computeReachableInputs(instance, props, hasRestProp, propsPattern);
|
|
672
|
+
const reachableInputs = computeReachableInputs(instance, props, hasRestProp, propsPattern, usesLegacySlotInputs(ast));
|
|
670
673
|
const childCalls = collectChildCalls(ast, imports);
|
|
671
674
|
const { shadowedNames, debugNames, writtenNames } = collectTemplateBindings(ast, instance, propsDeclaration);
|
|
672
675
|
const unreadDeclaredProps = computeUnreadDeclaredProps(ast, instance, props, propsPattern, shadowedNames, debugNames, writtenNames);
|
|
@@ -1832,7 +1835,15 @@ function parseModuleBody(code, id) {
|
|
|
1832
1835
|
* `$props.id()` (a member call) is NOT a `$props()` call — the props object never
|
|
1833
1836
|
* leaks through it — so it does not count and does not affect the result.
|
|
1834
1837
|
*/
|
|
1835
|
-
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' };
|
|
1836
1847
|
// No instance script -> no `$props()` -> the component reads no input at all.
|
|
1837
1848
|
if (!instance)
|
|
1838
1849
|
return { kind: 'names', names: new Set() };
|
|
@@ -1853,6 +1864,32 @@ function computeReachableInputs(instance, props, hasRestProp, propsPattern) {
|
|
|
1853
1864
|
return { kind: 'all' };
|
|
1854
1865
|
return { kind: 'names', names: new Set(props.map((p) => p.name)) };
|
|
1855
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
|
+
}
|
|
1856
1893
|
/** True when a `$props()` ObjectPattern binds a prop whose external name is not a
|
|
1857
1894
|
* plain identifier (a string-literal or computed key), so {@link declared_props}
|
|
1858
1895
|
* did not capture it. */
|
package/dist/engine.d.ts
CHANGED
|
@@ -53,7 +53,7 @@ export declare class DevShaker {
|
|
|
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
|
@@ -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.js
CHANGED
|
@@ -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) {
|
|
@@ -181,7 +181,7 @@ export function matchPreserve(preserve, root, components) {
|
|
|
181
181
|
* `svelte-shaker/node`).
|
|
182
182
|
*/
|
|
183
183
|
export async function computeEscapedComponents(opts) {
|
|
184
|
-
const { escaped, unscannable } = await
|
|
184
|
+
const { escaped, unscannable } = await collectModuleEscapes(opts.entryDirs.flatMap(collectNonSvelteModules), opts.resolve, opts.readFile);
|
|
185
185
|
const { matched, unmatched } = partitionPreserve(opts.preserve, opts.root, opts.components);
|
|
186
186
|
for (const id of matched)
|
|
187
187
|
escaped.add(id);
|
package/dist/ir.d.ts
CHANGED
|
@@ -43,8 +43,9 @@ export interface AnalyzeInput {
|
|
|
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/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)
|