svelte-shaker 0.15.0 → 0.15.2
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 +18 -6
- package/dist/analyze.js +37 -20
- package/dist/escape-scan.js +10 -18
- package/dist/eval.d.ts +15 -0
- package/dist/eval.js +30 -2
- package/dist/parse.d.ts +33 -0
- package/dist/parse.js +87 -0
- package/dist/svelte_shaker_engine_bg.wasm +0 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,10 +54,10 @@ binary). `parser: 'svelte'` falls back to svelte/compiler if you ever need it
|
|
|
54
54
|
|
|
55
55
|
## Usage (Vite)
|
|
56
56
|
|
|
57
|
-
Add the plugin **before** `svelte()`.
|
|
58
|
-
a pass-through
|
|
59
|
-
|
|
60
|
-
[Options](#options)).
|
|
57
|
+
Add the plugin **before** `svelte()`. By default it runs only in `vite build` —
|
|
58
|
+
dev/HMR is a pass-through (opt into dev shaking with the `dev` option, see
|
|
59
|
+
[Options](#options)). Out of the box it runs the native **Rust (WASM) engine**
|
|
60
|
+
and parses with **rsvelte**; both fall back cleanly (see [Options](#options)).
|
|
61
61
|
|
|
62
62
|
```ts
|
|
63
63
|
// vite.config.ts
|
|
@@ -103,6 +103,9 @@ shaker({
|
|
|
103
103
|
// out (e.g. if you ever hit a bug in it).
|
|
104
104
|
engine: 'auto', // 'auto' (default: Rust/WASM, else JS) | 'js' | 'rust'
|
|
105
105
|
parser: 'rsvelte', // 'rsvelte' (default) | 'svelte' (fallback)
|
|
106
|
+
|
|
107
|
+
dev: false, // default off: dev is a pass-through. 'incremental' (re-parse only
|
|
108
|
+
// changed files) | 'coarse' (re-analyze everything) opts in; never monomorphizes
|
|
106
109
|
});
|
|
107
110
|
```
|
|
108
111
|
|
|
@@ -150,6 +153,14 @@ options that do exist. A typo would otherwise be ignored — and a misspelled
|
|
|
150
153
|
can't (a broken install), the plugin **throws** rather than silently falling
|
|
151
154
|
back — so the same source always shakes the same on every machine. Reinstall
|
|
152
155
|
dependencies, or set `parser: 'svelte'`.
|
|
156
|
+
- **`dev`** — whether to shake in `vite dev` too. **Off** by default: dev is a
|
|
157
|
+
pass-through, which is always correct and keeps HMR simple. Opt in with
|
|
158
|
+
`dev: 'incremental'` — re-parses only the changed files and re-runs the
|
|
159
|
+
whole-program fixpoint over a long-lived incremental engine (the intended mode)
|
|
160
|
+
— or `dev: 'coarse'`, which re-analyzes the whole program on every change (the
|
|
161
|
+
slow but trivially-correct safety valve). Monomorphization is **never** applied
|
|
162
|
+
in dev; only the always-on passes (unused-prop fold / constant fold / value-set
|
|
163
|
+
narrowing) run.
|
|
153
164
|
- **`preserve`** — keep a component's **prop interface** exactly as written, because
|
|
154
165
|
something the shake can't see passes props to it. What is preserved is the props,
|
|
155
166
|
**not** the file's presence in the bundle: this is unrelated to Rollup/Vite's
|
|
@@ -236,8 +247,9 @@ The whole point is to **never change observable behavior**.
|
|
|
236
247
|
scope.
|
|
237
248
|
- **Needs `.svelte` source** — libraries shipping compiled JS pass through
|
|
238
249
|
unshaken; distribute via `svelte-package`.
|
|
239
|
-
- **Build
|
|
240
|
-
so dev is
|
|
250
|
+
- **Build-first** — whole-program analysis is incompatible with dev/HMR locality,
|
|
251
|
+
so dev is a pass-through by default; opt into incremental dev shaking with the
|
|
252
|
+
`dev` option.
|
|
241
253
|
- **`entries` must cover the whole app** — the crawl starts there, and every
|
|
242
254
|
`.svelte` file it finds is a call-site source. A call site outside those roots
|
|
243
255
|
is invisible, so narrowing `entries` does not shake less, it shakes _wrongly_.
|
package/dist/analyze.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { parseCached,
|
|
1
|
+
import { parseCached, parseModuleProgram, walk, } from './parse.js';
|
|
2
2
|
import { emptyPlan, } from './ir.js';
|
|
3
3
|
import { computeDeadSpans, inSpans } from './dead.js';
|
|
4
|
-
import { evaluate, setVar } from './eval.js';
|
|
4
|
+
import { evaluate, 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'
|
|
1085
|
-
|
|
1086
|
-
|
|
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'
|
|
1471
|
-
|
|
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 { known: true, value: expr.value };
|
|
1472
1492
|
}
|
|
1473
1493
|
return { known: false };
|
|
1474
1494
|
}
|
|
@@ -1608,6 +1628,9 @@ function valueSetFor(decl, sites, ownerEnv) {
|
|
|
1608
1628
|
return { values, dynamic, top };
|
|
1609
1629
|
}
|
|
1610
1630
|
function literalDefault(expr) {
|
|
1631
|
+
// A default like `= 500 as const` reaches here as a `TSAsExpression`; the
|
|
1632
|
+
// assertion erases at runtime, so read through it to the bare default value.
|
|
1633
|
+
expr = unwrapTsAssertions(expr) ?? undefined;
|
|
1611
1634
|
if (!expr)
|
|
1612
1635
|
return { known: true, value: undefined }; // omitted default -> undefined
|
|
1613
1636
|
if (expr.type === 'Literal')
|
|
@@ -1809,21 +1832,15 @@ function followLocalImport(body, localName) {
|
|
|
1809
1832
|
return null;
|
|
1810
1833
|
}
|
|
1811
1834
|
/**
|
|
1812
|
-
* Parse a `.js`/`.ts`
|
|
1813
|
-
*
|
|
1814
|
-
*
|
|
1815
|
-
* specifiers
|
|
1816
|
-
*
|
|
1817
|
-
*
|
|
1835
|
+
* Parse a `.js`/`.ts` barrel's top-level body via {@link parseModuleProgram}
|
|
1836
|
+
* (the engine has no standalone JS parser; the shared helper wraps the source in
|
|
1837
|
+
* a `<script module lang="ts">` so TypeScript barrels — `export type { … }`,
|
|
1838
|
+
* type-only specifiers — parse, and neutralizes any `</script>` in the text so a
|
|
1839
|
+
* valid module that merely mentions it still parses, issue #146). Returns `null`
|
|
1840
|
+
* if it cannot be parsed — callers then leave the barrel unfollowed.
|
|
1818
1841
|
*/
|
|
1819
1842
|
function parseModuleBody(code, id) {
|
|
1820
|
-
|
|
1821
|
-
const ast = parseSvelte(`<script module lang="ts">\n${code}\n</script>`, id);
|
|
1822
|
-
return ast.module?.content?.body ?? null;
|
|
1823
|
-
}
|
|
1824
|
-
catch {
|
|
1825
|
-
return null;
|
|
1826
|
-
}
|
|
1843
|
+
return parseModuleProgram(code, id)?.body ?? null;
|
|
1827
1844
|
}
|
|
1828
1845
|
/**
|
|
1829
1846
|
* Derive the child's {@link ReachableInputs} from its `$props()` shape (docs
|
package/dist/escape-scan.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// ----------------------------------------------------------------------
|
|
8
8
|
import * as fs from 'node:fs';
|
|
9
9
|
import * as path from 'node:path';
|
|
10
|
-
import {
|
|
10
|
+
import { parseModuleProgram, walk } from './parse.js';
|
|
11
11
|
import { compileDevOnly } from './dev-only.js';
|
|
12
12
|
/**
|
|
13
13
|
* The non-`.svelte` module extensions we scan for `.svelte` call sites (docs
|
|
@@ -58,25 +58,17 @@ function collectNonSvelteModules(dir, devOnly, out) {
|
|
|
58
58
|
* Every module specifier a JS/TS module statically references: `import … from`,
|
|
59
59
|
* `export … from` / `export *`, and a dynamic `import('…')` whose argument is a
|
|
60
60
|
* STRING LITERAL. A computed dynamic import (`import(expr)`) is unknowable here —
|
|
61
|
-
* the `preserve` option (docs §4.2) is the sound fallback for it. Parsed via
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* `preserve` is the fix for that file.
|
|
61
|
+
* the `preserve` option (docs §4.2) is the sound fallback for it. Parsed via
|
|
62
|
+
* {@link parseModuleProgram} (the same TS-capable parse the engine's
|
|
63
|
+
* barrel-following uses). Returns `null` when the module does NOT parse — a JSX
|
|
64
|
+
* body or exotic/bleeding-edge TS the wrapper rejects — because a parse failure
|
|
65
|
+
* hides any call site inside, so the caller must surface it (a mounted component
|
|
66
|
+
* would go un-escaped); `preserve` is the fix for that file.
|
|
68
67
|
*/
|
|
69
68
|
function moduleImportSpecifiers(code, id) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
return null; // unparseable — the caller reports it (soundness hole, `preserve` fixes it)
|
|
76
|
-
}
|
|
77
|
-
const program = module?.content;
|
|
78
|
-
if (!program)
|
|
79
|
-
return []; // parsed, no module body — nothing to follow (not a failure)
|
|
69
|
+
const program = parseModuleProgram(code, id);
|
|
70
|
+
if (program === null)
|
|
71
|
+
return null; // unparseable — the caller reports it (`preserve` fixes it)
|
|
80
72
|
const specs = [];
|
|
81
73
|
const literalSource = (node) => node?.type === 'Literal' && typeof node.value === 'string' ? node.value : undefined;
|
|
82
74
|
walk(program, null, {
|
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
|
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,6 +32,7 @@ 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) {
|
|
@@ -126,6 +150,7 @@ export function evaluateWithSets(node, constEnv, setEnv) {
|
|
|
126
150
|
}
|
|
127
151
|
/** Evaluate a boolean condition to a Kleene truth over the value sets. */
|
|
128
152
|
function evalTri(node, constEnv, setEnv) {
|
|
153
|
+
node = unwrapTsAssertions(node);
|
|
129
154
|
if (!node)
|
|
130
155
|
return 'unknown';
|
|
131
156
|
switch (node.type) {
|
|
@@ -193,8 +218,11 @@ function equalityTri(left, right, constEnv, setEnv, loose) {
|
|
|
193
218
|
* set pass-through (analyze.ts) all decide it through this, so they stay in lockstep.
|
|
194
219
|
*/
|
|
195
220
|
export function setVar(node, setEnv) {
|
|
196
|
-
|
|
197
|
-
|
|
221
|
+
// A `variant as const` / `variant!` reference is still a bare read of `variant`
|
|
222
|
+
// (the assertion erases at runtime), so it narrows like the identifier it wraps.
|
|
223
|
+
const bare = unwrapTsAssertions(node);
|
|
224
|
+
if (bare?.type === 'Identifier' && bare.name && setEnv.has(bare.name))
|
|
225
|
+
return setEnv.get(bare.name);
|
|
198
226
|
return null;
|
|
199
227
|
}
|
|
200
228
|
/**
|
package/dist/parse.d.ts
CHANGED
|
@@ -81,6 +81,39 @@ export interface Root extends AnyNode {
|
|
|
81
81
|
fragment: AnyNode;
|
|
82
82
|
}
|
|
83
83
|
export declare function parseSvelte(code: string, filename: string): Root;
|
|
84
|
+
/**
|
|
85
|
+
* Parse a plain `.js`/`.ts` module (NOT a `.svelte` component) and return its
|
|
86
|
+
* top-level ESTree Program, or `null` if it does not parse. The engine has no
|
|
87
|
+
* standalone JS parser, so we reuse the Svelte parser via a `<script module
|
|
88
|
+
* lang="ts">` wrapper: `lang="ts"` is what lets a TypeScript barrel — `export
|
|
89
|
+
* type { … }`, type-only specifiers, annotations, the norm for a design-system
|
|
90
|
+
* `index.ts` — parse where a plain-JS parse would throw.
|
|
91
|
+
*
|
|
92
|
+
* The Svelte parser ends a `<script>` at the first `</script…>` it finds in the
|
|
93
|
+
* RAW text (`read_until(/<\/script\s*>/)`), so a valid module whose text merely
|
|
94
|
+
* MENTIONS `</script>` — in a comment, string, regex or template literal, as an
|
|
95
|
+
* HTML sanitizer or a markdown pipeline routinely does — would close the wrapper
|
|
96
|
+
* early and fail to parse (issue #146). We break every `</script` in the body
|
|
97
|
+
* before wrapping by inserting a space after the `<`. In valid JS/TS `</script`
|
|
98
|
+
* only ever occurs inside a comment or a string/regex/template literal (it is
|
|
99
|
+
* never part of executable syntax), so this alters only inert text — with ONE
|
|
100
|
+
* exception a caller does read: a module SPECIFIER that itself contains
|
|
101
|
+
* `</script` (`import x from './a</script>b.js'`). Rewriting it would silently
|
|
102
|
+
* change which file it resolves to, turning a parse failure into a partial one
|
|
103
|
+
* (a missed escape / an unfollowed export). Rather than claim such a specifier
|
|
104
|
+
* cannot exist, we detect a rewritten one after parsing and fail the whole
|
|
105
|
+
* module (return `null`), the same loud degrade a parse failure gives — the
|
|
106
|
+
* specifier is absurd (`<`/`>` are invalid in real paths and package names), so
|
|
107
|
+
* this only trades one conservative outcome for another. The inserted spaces
|
|
108
|
+
* shift byte offsets, but no caller reads spans off this AST — only specifier
|
|
109
|
+
* names and string values.
|
|
110
|
+
*
|
|
111
|
+
* Returns `null` on a genuine parse failure (a JSX body, exotic/bleeding-edge
|
|
112
|
+
* TS) or a specifier the neutralization would corrupt: a call site hidden inside
|
|
113
|
+
* then goes unfollowed, which each caller handles conservatively (escape scan
|
|
114
|
+
* reports the file as unscannable; barrel-following leaves the barrel unfollowed).
|
|
115
|
+
*/
|
|
116
|
+
export declare function parseModuleProgram(code: string, filename: string): AnyNode | null;
|
|
84
117
|
/**
|
|
85
118
|
* Content-keyed parse cache: a hit returns the IDENTICAL AST for unchanged
|
|
86
119
|
* source, so the dev engine re-parses only the files that actually changed
|
package/dist/parse.js
CHANGED
|
@@ -3,6 +3,93 @@ import { walk as zfWalk } from 'zimmerframe';
|
|
|
3
3
|
export function parseSvelte(code, filename) {
|
|
4
4
|
return parse(code, { modern: true, filename });
|
|
5
5
|
}
|
|
6
|
+
/** Matches every `</script` in module text (any case), the sequence that would
|
|
7
|
+
* otherwise close the {@link parseModuleProgram} wrapper early. */
|
|
8
|
+
const CLOSING_SCRIPT = /<\/script/gi;
|
|
9
|
+
/** What `CLOSING_SCRIPT` is rewritten to: a space after the `<` breaks the
|
|
10
|
+
* `</script\s*>` the Svelte parser scans for, while staying a recognizable
|
|
11
|
+
* marker so a corrupted specifier can be detected afterwards. */
|
|
12
|
+
const NEUTRALIZED_SCRIPT = '< /script';
|
|
13
|
+
/**
|
|
14
|
+
* Parse a plain `.js`/`.ts` module (NOT a `.svelte` component) and return its
|
|
15
|
+
* top-level ESTree Program, or `null` if it does not parse. The engine has no
|
|
16
|
+
* standalone JS parser, so we reuse the Svelte parser via a `<script module
|
|
17
|
+
* lang="ts">` wrapper: `lang="ts"` is what lets a TypeScript barrel — `export
|
|
18
|
+
* type { … }`, type-only specifiers, annotations, the norm for a design-system
|
|
19
|
+
* `index.ts` — parse where a plain-JS parse would throw.
|
|
20
|
+
*
|
|
21
|
+
* The Svelte parser ends a `<script>` at the first `</script…>` it finds in the
|
|
22
|
+
* RAW text (`read_until(/<\/script\s*>/)`), so a valid module whose text merely
|
|
23
|
+
* MENTIONS `</script>` — in a comment, string, regex or template literal, as an
|
|
24
|
+
* HTML sanitizer or a markdown pipeline routinely does — would close the wrapper
|
|
25
|
+
* early and fail to parse (issue #146). We break every `</script` in the body
|
|
26
|
+
* before wrapping by inserting a space after the `<`. In valid JS/TS `</script`
|
|
27
|
+
* only ever occurs inside a comment or a string/regex/template literal (it is
|
|
28
|
+
* never part of executable syntax), so this alters only inert text — with ONE
|
|
29
|
+
* exception a caller does read: a module SPECIFIER that itself contains
|
|
30
|
+
* `</script` (`import x from './a</script>b.js'`). Rewriting it would silently
|
|
31
|
+
* change which file it resolves to, turning a parse failure into a partial one
|
|
32
|
+
* (a missed escape / an unfollowed export). Rather than claim such a specifier
|
|
33
|
+
* cannot exist, we detect a rewritten one after parsing and fail the whole
|
|
34
|
+
* module (return `null`), the same loud degrade a parse failure gives — the
|
|
35
|
+
* specifier is absurd (`<`/`>` are invalid in real paths and package names), so
|
|
36
|
+
* this only trades one conservative outcome for another. The inserted spaces
|
|
37
|
+
* shift byte offsets, but no caller reads spans off this AST — only specifier
|
|
38
|
+
* names and string values.
|
|
39
|
+
*
|
|
40
|
+
* Returns `null` on a genuine parse failure (a JSX body, exotic/bleeding-edge
|
|
41
|
+
* TS) or a specifier the neutralization would corrupt: a call site hidden inside
|
|
42
|
+
* then goes unfollowed, which each caller handles conservatively (escape scan
|
|
43
|
+
* reports the file as unscannable; barrel-following leaves the barrel unfollowed).
|
|
44
|
+
*/
|
|
45
|
+
export function parseModuleProgram(code, filename) {
|
|
46
|
+
const wrapped = `<script module lang="ts">\n${code.replace(CLOSING_SCRIPT, NEUTRALIZED_SCRIPT)}\n</script>`;
|
|
47
|
+
let program;
|
|
48
|
+
try {
|
|
49
|
+
program = parseSvelte(wrapped, filename).module?.content ?? null;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null; // unparseable — the caller decides how to degrade (see above)
|
|
53
|
+
}
|
|
54
|
+
// A specifier that carried `</script` was rewritten by the neutralization and
|
|
55
|
+
// no longer names the real module; degrade loudly rather than resolve a lie.
|
|
56
|
+
if (program && hasNeutralizedSpecifier(program))
|
|
57
|
+
return null;
|
|
58
|
+
return program;
|
|
59
|
+
}
|
|
60
|
+
/** True when any static import/export/dynamic-`import()` specifier string in
|
|
61
|
+
* `program` contains the {@link NEUTRALIZED_SCRIPT} marker — i.e. the source
|
|
62
|
+
* originally held `</script` and was rewritten by {@link parseModuleProgram}, so
|
|
63
|
+
* it can no longer be trusted to resolve. */
|
|
64
|
+
function hasNeutralizedSpecifier(program) {
|
|
65
|
+
let corrupted = false;
|
|
66
|
+
const check = (source) => {
|
|
67
|
+
if (source?.type === 'Literal' &&
|
|
68
|
+
typeof source.value === 'string' &&
|
|
69
|
+
source.value.includes(NEUTRALIZED_SCRIPT)) {
|
|
70
|
+
corrupted = true;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
walk(program, null, {
|
|
74
|
+
ImportDeclaration(node, { next }) {
|
|
75
|
+
check(node.source);
|
|
76
|
+
next();
|
|
77
|
+
},
|
|
78
|
+
ExportNamedDeclaration(node, { next }) {
|
|
79
|
+
check(node.source);
|
|
80
|
+
next();
|
|
81
|
+
},
|
|
82
|
+
ExportAllDeclaration(node, { next }) {
|
|
83
|
+
check(node.source);
|
|
84
|
+
next();
|
|
85
|
+
},
|
|
86
|
+
ImportExpression(node, { next }) {
|
|
87
|
+
check(node.source);
|
|
88
|
+
next();
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
return corrupted;
|
|
92
|
+
}
|
|
6
93
|
export function parseCached(filename, code, cache, parse = parseSvelte) {
|
|
7
94
|
if (!cache)
|
|
8
95
|
return parse(code, filename);
|
|
Binary file
|