svelte-shaker 0.15.0 → 0.15.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/README.md +18 -6
- package/dist/analyze.js +8 -14
- package/dist/escape-scan.js +10 -18
- package/dist/parse.d.ts +33 -0
- package/dist/parse.js +87 -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,4 +1,4 @@
|
|
|
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
4
|
import { evaluate, setVar } from './eval.js';
|
|
@@ -1809,21 +1809,15 @@ function followLocalImport(body, localName) {
|
|
|
1809
1809
|
return null;
|
|
1810
1810
|
}
|
|
1811
1811
|
/**
|
|
1812
|
-
* Parse a `.js`/`.ts`
|
|
1813
|
-
*
|
|
1814
|
-
*
|
|
1815
|
-
* specifiers
|
|
1816
|
-
*
|
|
1817
|
-
*
|
|
1812
|
+
* Parse a `.js`/`.ts` barrel's top-level body via {@link parseModuleProgram}
|
|
1813
|
+
* (the engine has no standalone JS parser; the shared helper wraps the source in
|
|
1814
|
+
* a `<script module lang="ts">` so TypeScript barrels — `export type { … }`,
|
|
1815
|
+
* type-only specifiers — parse, and neutralizes any `</script>` in the text so a
|
|
1816
|
+
* valid module that merely mentions it still parses, issue #146). Returns `null`
|
|
1817
|
+
* if it cannot be parsed — callers then leave the barrel unfollowed.
|
|
1818
1818
|
*/
|
|
1819
1819
|
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
|
-
}
|
|
1820
|
+
return parseModuleProgram(code, id)?.body ?? null;
|
|
1827
1821
|
}
|
|
1828
1822
|
/**
|
|
1829
1823
|
* 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/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);
|