svelte-shaker 0.15.2 → 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, unwrapTsAssertions } 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;
@@ -1488,7 +1488,7 @@ function literalAttrValue(value) {
1488
1488
  // `dynamic` flag itself must match — mono's `specializableShape` reads it.
1489
1489
  const expr = unwrapTsAssertions(part.expression);
1490
1490
  if (expr?.type === 'Literal')
1491
- return { known: true, value: expr.value };
1491
+ return literalValue(expr);
1492
1492
  }
1493
1493
  return { known: false };
1494
1494
  }
@@ -1549,7 +1549,9 @@ function buildPlan(model, u, ownerEnv) {
1549
1549
  continue;
1550
1550
  // constant fold: a clean singleton value set is the foldable case.
1551
1551
  if (set.values.length === 1) {
1552
- 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);
1553
1555
  continue;
1554
1556
  }
1555
1557
  // value-set narrowing: >= 2 distinct literals with no dynamic/⊤ contribution is a fully
@@ -1633,8 +1635,13 @@ function literalDefault(expr) {
1633
1635
  expr = unwrapTsAssertions(expr) ?? undefined;
1634
1636
  if (!expr)
1635
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.
1636
1643
  if (expr.type === 'Literal')
1637
- return { known: true, value: expr.value };
1644
+ return literalValue(expr);
1638
1645
  if (expr.type === 'Identifier' && expr.name === 'undefined')
1639
1646
  return { known: true, value: undefined };
1640
1647
  return { known: false };
package/dist/eval.d.ts CHANGED
@@ -31,6 +31,21 @@ export declare function unwrapTsAssertions(node: AnyNode | null | undefined): An
31
31
  * unknown on non-distributive ops), just without the interprocedural lattice.
32
32
  */
33
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;
34
49
  /**
35
50
  * Sound set-aware predicate. `constEnv` holds props collapsed to a single
36
51
  * literal (`constFold`); `setEnv` holds props whose reachable value set is known
package/dist/eval.js CHANGED
@@ -37,7 +37,7 @@ export function evaluate(node, env) {
37
37
  return UNKNOWN;
38
38
  switch (node.type) {
39
39
  case 'Literal':
40
- return { known: true, value: node.value };
40
+ return literalValue(node);
41
41
  case 'Identifier': {
42
42
  const name = node.name ?? '';
43
43
  if (name === 'undefined')
@@ -132,6 +132,30 @@ export function evaluate(node, env) {
132
132
  return UNKNOWN;
133
133
  }
134
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
+ }
135
159
  /**
136
160
  * Sound set-aware predicate. `constEnv` holds props collapsed to a single
137
161
  * literal (`constFold`); `setEnv` holds props whose reachable value set is known
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 {
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.2",
3
+ "version": "0.15.3",
4
4
  "description": "Tree shaking for Svelte components",
5
5
  "keywords": [
6
6
  "dead-code-elimination",