svelte-shaker 0.15.1 → 0.15.3

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.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { parseCached, parseModuleProgram, walk, } from './parse.js';
2
- import { emptyPlan, } from './ir.js';
2
+ import { emptyPlan, isFoldableValue, } from './ir.js';
3
3
  import { computeDeadSpans, inSpans } from './dead.js';
4
- import { evaluate, setVar } from './eval.js';
4
+ import { evaluate, literalValue, setVar, unwrapTsAssertions } from './eval.js';
5
5
  const isSvelte = (source) => source.endsWith('.svelte');
6
6
  /** Floor for the fixpoint iteration bound (see {@link fixpointIterationBound}). */
7
7
  const MIN_FIXPOINT_ITERATIONS = 10;
@@ -856,6 +856,11 @@ function computeScriptConstEnv(ast, instance, moduleScript, propsDeclaration, wr
856
856
  * rune call — is evaluated verbatim, so a non-value rune simply falls to unknown.
857
857
  */
858
858
  function evalDeclaratorValue(init, env) {
859
+ // `const x = $state(0) as T` wraps the rune in a `TSAsExpression`; erase the
860
+ // (runtime-transparent) assertion first so the `$state`/`$state.raw` unwrap
861
+ // below and `evaluate` both see the real initializer, same as a parser that
862
+ // strips the assertion would.
863
+ init = unwrapTsAssertions(init) ?? undefined;
859
864
  if (isStateRuneCall(init)) {
860
865
  const arg = init?.arguments?.[0];
861
866
  if (arg == null)
@@ -1081,9 +1086,13 @@ function collectTemplateBindings(ast, instance, propsDeclaration) {
1081
1086
  function collectWrittenNames(node, out) {
1082
1087
  if (node.type === 'AssignmentExpression')
1083
1088
  addPatternNames(node.left, out);
1084
- else if (node.type === 'UpdateExpression' && node.argument?.type === 'Identifier') {
1085
- if (node.argument.name)
1086
- out.add(node.argument.name);
1089
+ else if (node.type === 'UpdateExpression') {
1090
+ // `x!++` keeps the `!` as a `TSNonNullExpression` around the target (only a
1091
+ // bare `x = …` LHS is normalized to the identifier), so read through it or the
1092
+ // write goes uncounted and the name is wrongly admitted as a constant.
1093
+ const arg = unwrapTsAssertions(node.argument);
1094
+ if (arg?.type === 'Identifier' && arg.name)
1095
+ out.add(arg.name);
1087
1096
  }
1088
1097
  }
1089
1098
  /**
@@ -1091,6 +1100,11 @@ function collectWrittenNames(node, out) {
1091
1100
  * Handles bare identifiers, object/array destructuring, defaults and rest.
1092
1101
  */
1093
1102
  function addPatternNames(pattern, out) {
1103
+ // A non-null-asserted assignment target keeps its `TSNonNullExpression` wrapper
1104
+ // in every position but a bare `x = …` LHS — `x! += 1`, `[x!] = a`, `({k: x!} =
1105
+ // o)` — so peel it here (the one choke point every pattern position recurses
1106
+ // through) to count the write against the bare name.
1107
+ pattern = unwrapTsAssertions(pattern) ?? undefined;
1094
1108
  if (!pattern)
1095
1109
  return;
1096
1110
  switch (pattern.type) {
@@ -1467,8 +1481,14 @@ function literalAttrValue(value) {
1467
1481
  const part = parts[0];
1468
1482
  if (part.type === 'Text')
1469
1483
  return { known: true, value: (part.data ?? part.raw ?? '') };
1470
- if (part.type === 'ExpressionTag' && part.expression?.type === 'Literal') {
1471
- return { known: true, value: part.expression.value };
1484
+ if (part.type === 'ExpressionTag') {
1485
+ // Recognize `prop={'x' as const}` as the literal it wraps, so the write is
1486
+ // classified NON-dynamic identically to a parser that strips the assertion.
1487
+ // Both paths already fold the value (via the expr fallback), but the
1488
+ // `dynamic` flag itself must match — mono's `specializableShape` reads it.
1489
+ const expr = unwrapTsAssertions(part.expression);
1490
+ if (expr?.type === 'Literal')
1491
+ return literalValue(expr);
1472
1492
  }
1473
1493
  return { known: false };
1474
1494
  }
@@ -1529,7 +1549,9 @@ function buildPlan(model, u, ownerEnv) {
1529
1549
  continue;
1530
1550
  // constant fold: a clean singleton value set is the foldable case.
1531
1551
  if (set.values.length === 1) {
1532
- plan.constFold.set(decl.name, set.values[0]);
1552
+ const only = set.values[0];
1553
+ if (isFoldableValue(only))
1554
+ plan.constFold.set(decl.name, only);
1533
1555
  continue;
1534
1556
  }
1535
1557
  // value-set narrowing: >= 2 distinct literals with no dynamic/⊤ contribution is a fully
@@ -1608,10 +1630,18 @@ function valueSetFor(decl, sites, ownerEnv) {
1608
1630
  return { values, dynamic, top };
1609
1631
  }
1610
1632
  function literalDefault(expr) {
1633
+ // A default like `= 500 as const` reaches here as a `TSAsExpression`; the
1634
+ // assertion erases at runtime, so read through it to the bare default value.
1635
+ expr = unwrapTsAssertions(expr) ?? undefined;
1611
1636
  if (!expr)
1612
1637
  return { known: true, value: undefined }; // omitted default -> undefined
1638
+ // `literalValue` is the same gate `literalAttrValue` and `evaluate` use: a
1639
+ // BigInt/RegExp default (no faithful source form) or a `null` that is really
1640
+ // `1e999` surviving JSON transport must stay UNKNOWN here too, or a call site
1641
+ // that merely omits the prop reintroduces the crash/misfold this default path
1642
+ // exists to prevent.
1613
1643
  if (expr.type === 'Literal')
1614
- return { known: true, value: expr.value };
1644
+ return literalValue(expr);
1615
1645
  if (expr.type === 'Identifier' && expr.name === 'undefined')
1616
1646
  return { known: true, value: undefined };
1617
1647
  return { known: false };
package/dist/eval.d.ts CHANGED
@@ -6,6 +6,21 @@ export type EvalResult = {
6
6
  } | {
7
7
  known: false;
8
8
  };
9
+ /**
10
+ * Strip TypeScript assertion wrappers — `x as T` (`TSAsExpression`), `x!`
11
+ * (`TSNonNullExpression`), `x satisfies T` (`TSSatisfiesExpression`) — down to
12
+ * the runtime expression they wrap, recursing so `('a' as const)!` becomes `'a'`.
13
+ *
14
+ * These are compile-time-only type operators: they are erased before any code
15
+ * runs and do not touch the operand's value, so interpreting the operand IS
16
+ * interpreting the whole expression (docs/ARCHITECTURE.md §2.2 — the value the
17
+ * expression abstracts to is exactly the operand's). The engine runs on the
18
+ * `lang="ts"` source with types preserved (§6.3), so svelte/compiler hands it
19
+ * these nodes verbatim; a parser that erases them (rsvelte today) yields the bare
20
+ * operand instead. Unwrapping here folds both ASTs identically — the
21
+ * parser-neutrality contract every value-interpreting entry point relies on.
22
+ */
23
+ export declare function unwrapTsAssertions(node: AnyNode | null | undefined): AnyNode | null | undefined;
9
24
  /**
10
25
  * A deliberately tiny, total constant evaluator over an ESTree expression,
11
26
  * given an environment of statically-known identifiers. It never throws and
@@ -16,6 +31,21 @@ export type EvalResult = {
16
31
  * unknown on non-distributive ops), just without the interprocedural lattice.
17
32
  */
18
33
  export declare function evaluate(node: AnyNode | null | undefined, env: ReadonlyMap<string, Literal>): EvalResult;
34
+ /**
35
+ * The `Literal` value of an ESTree `Literal` node, or unknown when the node
36
+ * carries a value outside that union. This is the ONLY door through which a
37
+ * parsed value enters the engine, so it is where the union is enforced:
38
+ *
39
+ * - **RegExp / BigInt literals** (`/x/g`, `1n`) hold a `RegExp` / `bigint` in
40
+ * `value` — no faithful literal source form, and `JSON.stringify` throws on
41
+ * the latter and flattens the former to `{}`. Both carry a discriminator.
42
+ * - **`value: null`** is ambiguous: an AST that reached us as JSON (the rsvelte
43
+ * parser crosses a WASM boundary that way) cannot represent `Infinity`, so
44
+ * `1e999` arrives indistinguishable from a genuine `null` except by `raw`.
45
+ *
46
+ * Anything unproven stays unknown, which costs one fold — never a wrong one.
47
+ */
48
+ export declare function literalValue(node: AnyNode): EvalResult;
19
49
  /**
20
50
  * Sound set-aware predicate. `constEnv` holds props collapsed to a single
21
51
  * literal (`constFold`); `setEnv` holds props whose reachable value set is known
package/dist/eval.js CHANGED
@@ -1,4 +1,27 @@
1
1
  const UNKNOWN = { known: false };
2
+ /**
3
+ * Strip TypeScript assertion wrappers — `x as T` (`TSAsExpression`), `x!`
4
+ * (`TSNonNullExpression`), `x satisfies T` (`TSSatisfiesExpression`) — down to
5
+ * the runtime expression they wrap, recursing so `('a' as const)!` becomes `'a'`.
6
+ *
7
+ * These are compile-time-only type operators: they are erased before any code
8
+ * runs and do not touch the operand's value, so interpreting the operand IS
9
+ * interpreting the whole expression (docs/ARCHITECTURE.md §2.2 — the value the
10
+ * expression abstracts to is exactly the operand's). The engine runs on the
11
+ * `lang="ts"` source with types preserved (§6.3), so svelte/compiler hands it
12
+ * these nodes verbatim; a parser that erases them (rsvelte today) yields the bare
13
+ * operand instead. Unwrapping here folds both ASTs identically — the
14
+ * parser-neutrality contract every value-interpreting entry point relies on.
15
+ */
16
+ export function unwrapTsAssertions(node) {
17
+ let current = node;
18
+ while (current?.type === 'TSAsExpression' ||
19
+ current?.type === 'TSNonNullExpression' ||
20
+ current?.type === 'TSSatisfiesExpression') {
21
+ current = current.expression;
22
+ }
23
+ return current;
24
+ }
2
25
  /**
3
26
  * A deliberately tiny, total constant evaluator over an ESTree expression,
4
27
  * given an environment of statically-known identifiers. It never throws and
@@ -9,11 +32,12 @@ const UNKNOWN = { known: false };
9
32
  * unknown on non-distributive ops), just without the interprocedural lattice.
10
33
  */
11
34
  export function evaluate(node, env) {
35
+ node = unwrapTsAssertions(node);
12
36
  if (!node)
13
37
  return UNKNOWN;
14
38
  switch (node.type) {
15
39
  case 'Literal':
16
- return { known: true, value: node.value };
40
+ return literalValue(node);
17
41
  case 'Identifier': {
18
42
  const name = node.name ?? '';
19
43
  if (name === 'undefined')
@@ -108,6 +132,30 @@ export function evaluate(node, env) {
108
132
  return UNKNOWN;
109
133
  }
110
134
  }
135
+ /**
136
+ * The `Literal` value of an ESTree `Literal` node, or unknown when the node
137
+ * carries a value outside that union. This is the ONLY door through which a
138
+ * parsed value enters the engine, so it is where the union is enforced:
139
+ *
140
+ * - **RegExp / BigInt literals** (`/x/g`, `1n`) hold a `RegExp` / `bigint` in
141
+ * `value` — no faithful literal source form, and `JSON.stringify` throws on
142
+ * the latter and flattens the former to `{}`. Both carry a discriminator.
143
+ * - **`value: null`** is ambiguous: an AST that reached us as JSON (the rsvelte
144
+ * parser crosses a WASM boundary that way) cannot represent `Infinity`, so
145
+ * `1e999` arrives indistinguishable from a genuine `null` except by `raw`.
146
+ *
147
+ * Anything unproven stays unknown, which costs one fold — never a wrong one.
148
+ */
149
+ export function literalValue(node) {
150
+ if (node.regex !== undefined || node.bigint !== undefined)
151
+ return UNKNOWN;
152
+ const v = node.value;
153
+ if (v === null)
154
+ return node.raw === 'null' ? { known: true, value: null } : UNKNOWN;
155
+ if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean')
156
+ return { known: true, value: v };
157
+ return UNKNOWN;
158
+ }
111
159
  /**
112
160
  * Sound set-aware predicate. `constEnv` holds props collapsed to a single
113
161
  * literal (`constFold`); `setEnv` holds props whose reachable value set is known
@@ -126,6 +174,7 @@ export function evaluateWithSets(node, constEnv, setEnv) {
126
174
  }
127
175
  /** Evaluate a boolean condition to a Kleene truth over the value sets. */
128
176
  function evalTri(node, constEnv, setEnv) {
177
+ node = unwrapTsAssertions(node);
129
178
  if (!node)
130
179
  return 'unknown';
131
180
  switch (node.type) {
@@ -193,8 +242,11 @@ function equalityTri(left, right, constEnv, setEnv, loose) {
193
242
  * set pass-through (analyze.ts) all decide it through this, so they stay in lockstep.
194
243
  */
195
244
  export function setVar(node, setEnv) {
196
- if (node?.type === 'Identifier' && node.name && setEnv.has(node.name))
197
- return setEnv.get(node.name);
245
+ // A `variant as const` / `variant!` reference is still a bare read of `variant`
246
+ // (the assertion erases at runtime), so it narrows like the identifier it wraps.
247
+ const bare = unwrapTsAssertions(node);
248
+ if (bare?.type === 'Identifier' && bare.name && setEnv.has(bare.name))
249
+ return setEnv.get(bare.name);
198
250
  return null;
199
251
  }
200
252
  /**
package/dist/ir.d.ts CHANGED
@@ -131,4 +131,18 @@ export interface ComponentPlan {
131
131
  */
132
132
  valueSets: Map<string, PropValueSet>;
133
133
  }
134
+ /**
135
+ * Whether a proven value may be SUBSTITUTED into the residual as a constant.
136
+ *
137
+ * Every member of {@link Literal} has a faithful source form except `-0`: the
138
+ * Svelte compiler constant-folds the expression it is spliced into and loses the
139
+ * sign of zero there (`{1 / n}` renders `-Infinity` with `n = -0` at runtime,
140
+ * but the folded `{1 / (-0)}` compiles to a literal `Infinity`). Emitting it
141
+ * would therefore change what renders even though the value we proved is right,
142
+ * so a `-0`-valued prop is simply left alone — one missed fold, no risk.
143
+ *
144
+ * Narrowing is unaffected: a narrowed prop stays dynamic and is never
145
+ * substituted, and the comparisons it feeds use real JS semantics.
146
+ */
147
+ export declare function isFoldableValue(value: Literal): boolean;
134
148
  export declare function emptyPlan(id: ComponentId): ComponentPlan;
package/dist/ir.js CHANGED
@@ -3,6 +3,22 @@
3
3
  // See docs/ARCHITECTURE.md §5.1. This is the M0 (walking-skeleton) subset:
4
4
  // only the pieces basic1 exercises, but shaped so later levels slot in.
5
5
  // ----------------------------------------------------------------------
6
+ /**
7
+ * Whether a proven value may be SUBSTITUTED into the residual as a constant.
8
+ *
9
+ * Every member of {@link Literal} has a faithful source form except `-0`: the
10
+ * Svelte compiler constant-folds the expression it is spliced into and loses the
11
+ * sign of zero there (`{1 / n}` renders `-Infinity` with `n = -0` at runtime,
12
+ * but the folded `{1 / (-0)}` compiles to a literal `Infinity`). Emitting it
13
+ * would therefore change what renders even though the value we proved is right,
14
+ * so a `-0`-valued prop is simply left alone — one missed fold, no risk.
15
+ *
16
+ * Narrowing is unaffected: a narrowed prop stays dynamic and is never
17
+ * substituted, and the comparisons it feeds use real JS semantics.
18
+ */
19
+ export function isFoldableValue(value) {
20
+ return !Object.is(value, -0);
21
+ }
6
22
  export function emptyPlan(id) {
7
23
  return {
8
24
  id,
package/dist/mono.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type FileModel } from './analyze.js';
2
2
  import { type AnyNode } from './parse.js';
3
- import type { ComponentId, ComponentPlan, Literal } from './ir.js';
3
+ import { type ComponentId, type ComponentPlan, type Literal } from './ir.js';
4
4
  /** Tuning knobs for monomorphization (docs §8.1, §13.2). All have sound defaults. */
5
5
  export interface MonomorphizeOptions {
6
6
  /** Master switch. Default OFF — every existing behavior is unchanged. */
package/dist/mono.js CHANGED
@@ -60,6 +60,7 @@ import { inSpans } from './dead.js';
60
60
  import { shakeBody } from './transform.js';
61
61
  import { readCallSite, deadSpansForPlans, isFoldBlockedName, } from './analyze.js';
62
62
  import { parseSvelte, walk } from './parse.js';
63
+ import { isFoldableValue } from './ir.js';
63
64
  export const DEFAULT_MONO_OPTIONS = {
64
65
  enabled: false,
65
66
  maxVariants: 8,
@@ -418,6 +419,8 @@ function specializableShape(node, child, plan) {
418
419
  // override — exactly the analysis's "safely explicit" condition.
419
420
  if (explicit.dynamic || !explicit.afterLastSpread)
420
421
  continue;
422
+ if (!isFoldableValue(explicit.value))
423
+ continue; // no safe source form — exactly as constant fold
421
424
  shape.set(name, explicit.value);
422
425
  }
423
426
  return shape;
package/dist/parse.d.ts CHANGED
@@ -75,6 +75,13 @@ export interface AnyNode {
75
75
  kind?: string | undefined;
76
76
  /** ObjectExpression `Property` shorthand-method flag (`{ m() {} }`). */
77
77
  method?: boolean | undefined;
78
+ /** ESTree `Literal` discriminators, present only on a RegExp (`/x/g`) or
79
+ * BigInt (`1n`) literal — neither of which carries a foldable `value`. */
80
+ regex?: {
81
+ pattern: string;
82
+ flags: string;
83
+ } | undefined;
84
+ bigint?: string | undefined;
78
85
  value?: unknown;
79
86
  }
80
87
  export interface Root extends AnyNode {
Binary file
package/dist/transform.js CHANGED
@@ -1014,8 +1014,29 @@ function setDefault(map, key) {
1014
1014
  function isSpace(ch) {
1015
1015
  return ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r';
1016
1016
  }
1017
+ /** Source text for a folded value, faithful for every member of {@link Literal}
1018
+ * (the union {@link evaluate} admits). Always an expression, so it drops into
1019
+ * both substitution positions unchanged. */
1017
1020
  function literalSource(value) {
1018
1021
  if (value === undefined)
1019
1022
  return 'undefined';
1023
+ if (typeof value === 'number')
1024
+ return numberSource(value);
1025
+ return JSON.stringify(value);
1026
+ }
1027
+ /**
1028
+ * `JSON.stringify` flattens `Infinity`/`-Infinity`/`NaN` to `null`, so those get
1029
+ * an explicit form. Written as arithmetic rather than the `Infinity`/`NaN`
1030
+ * globals because the substituted text lands in the CALLEE's scope, where a
1031
+ * local of either name would silently capture it; `(0/0)` cannot be shadowed.
1032
+ * (`-0`, the fourth lossy case, never reaches here — see `isFoldableValue`.)
1033
+ */
1034
+ function numberSource(value) {
1035
+ if (Number.isNaN(value))
1036
+ return '(0/0)';
1037
+ if (value === Infinity)
1038
+ return '(1/0)';
1039
+ if (value === -Infinity)
1040
+ return '(-1/0)';
1020
1041
  return JSON.stringify(value);
1021
1042
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-shaker",
3
- "version": "0.15.1",
3
+ "version": "0.15.3",
4
4
  "description": "Tree shaking for Svelte components",
5
5
  "keywords": [
6
6
  "dead-code-elimination",