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 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, or `$props()` outside a `let <pat> = …`
22
- * declarator. Then any input might be observed, so nothing is dropped.
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 `.ts`/`.js` call site, or a user `preserve`, docs
279
- * §4.2) — is skipped, because that consumer may pass a prop the crawl cannot see.
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 `.ts`/`.js` call site the crawl cannot parse, or a
33
- * user-declared `preserve` (docs §4.2, {@link AnalyzeInput.escaped}). Kept
34
- * byte-identical to the Rust engine's constant so the two agree. */
35
- const EXTERNAL_ESCAPE_REASON = 'has a consumer outside the analyzed .svelte graph';
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 EXTERNAL_ESCAPE_REASON} on every model in `escaped` that exists in
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 `.ts` import to a component
41
- * outside the crawl is simply a no-op, never an error).
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 stampExternalEscapes(models, escaped) {
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(EXTERNAL_ESCAPE_REASON))
47
- model.bailReasons.push(EXTERNAL_ESCAPE_REASON);
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 `.ts`/`.js` call site
90
- // or a user `preserve`, docs §4.2) join the same whole-component escape bail.
91
- stampExternalEscapes(models, input.escaped);
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 `.ts`/`.js` call site, or a user `preserve`, docs
503
- * §4.2) — is skipped, because that consumer may pass a prop the crawl cannot see.
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
- // External consumers (`.ts`/`.js` call sites or `preserve`) escape too, so a prop
525
- // they pass is never mis-reported as never-passed (docs §4.2).
526
- stampExternalEscapes(models, input.escaped);
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 external-escape set (docs §4.2). The Shell calls this before
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 external-escape set (docs §4.2). The Shell calls this before
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) {
@@ -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 collectExternalEscapes(modules, resolve, readFile) {
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 collectExternalEscapes(opts.entryDirs.flatMap(collectNonSvelteModules), opts.resolve, opts.readFile);
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 "no
47
- * external consumers", keeping the output byte-for-byte unchanged.
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 external consumers, if any, bail the child).
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-shaker",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "description": "Tree shaking for Svelte components",
5
5
  "keywords": [
6
6
  "dead-code-elimination",