svelte-shaker 0.14.1 → 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 +47 -6
- package/dist/analyze.js +8 -14
- package/dist/dev-only.d.ts +23 -0
- package/dist/dev-only.js +43 -0
- package/dist/escape-scan.d.ts +8 -0
- package/dist/escape-scan.js +23 -26
- package/dist/parse.d.ts +33 -0
- package/dist/parse.js +87 -0
- package/dist/rsvelte-parse.d.ts +3 -3
- package/dist/rsvelte-parse.js +9 -4
- package/dist/scan.d.ts +12 -1
- package/dist/scan.js +23 -5
- package/dist/vite.d.ts +30 -0
- package/dist/vite.js +20 -4
- package/package.json +4 -2
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
|
|
@@ -92,6 +92,9 @@ shaker({
|
|
|
92
92
|
entries: ['src'], // dirs (relative to root) the crawl starts from; they must
|
|
93
93
|
// hold every .svelte call site in the app. Not a glob, not a filter.
|
|
94
94
|
preserve: [], // components whose props must never be folded (see below)
|
|
95
|
+
devOnly: [...], // glob patterns of files that never ship (tests, stories); they
|
|
96
|
+
// stop counting as call sites. Defaults to tests/mocks/stories; replaces, spread
|
|
97
|
+
// to extend.
|
|
95
98
|
monomorphize: true, // default on; `false` disables it for faster builds,
|
|
96
99
|
// or { maxVariants: 16, minSavings: 0.05 } to tune
|
|
97
100
|
verbose: false, // true = per-file size breakdown after the build
|
|
@@ -100,6 +103,9 @@ shaker({
|
|
|
100
103
|
// out (e.g. if you ever hit a bug in it).
|
|
101
104
|
engine: 'auto', // 'auto' (default: Rust/WASM, else JS) | 'js' | 'rust'
|
|
102
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
|
|
103
109
|
});
|
|
104
110
|
```
|
|
105
111
|
|
|
@@ -147,6 +153,14 @@ options that do exist. A typo would otherwise be ignored — and a misspelled
|
|
|
147
153
|
can't (a broken install), the plugin **throws** rather than silently falling
|
|
148
154
|
back — so the same source always shakes the same on every machine. Reinstall
|
|
149
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.
|
|
150
164
|
- **`preserve`** — keep a component's **prop interface** exactly as written, because
|
|
151
165
|
something the shake can't see passes props to it. What is preserved is the props,
|
|
152
166
|
**not** the file's presence in the bundle: this is unrelated to Rollup/Vite's
|
|
@@ -171,6 +185,32 @@ options that do exist. A typo would otherwise be ignored — and a misspelled
|
|
|
171
185
|
The build **warns** (with the file path) about any module the scan couldn't
|
|
172
186
|
parse — so a mounted component isn't silently left unprotected — and about
|
|
173
187
|
`preserve` entries that matched no component.
|
|
188
|
+
- **`devOnly`** — glob patterns (matched with
|
|
189
|
+
[`picomatch`](https://github.com/micromatch/picomatch) against each file's path
|
|
190
|
+
relative to the Vite root) naming files that **never ship in the production
|
|
191
|
+
bundle** — colocated tests, mocks, Storybook stories. A matched file **stops
|
|
192
|
+
counting as a component consumer** in **both directory scans** (the `.svelte` seed
|
|
193
|
+
scan and the non-`.svelte` escape scan), so a `Foo.test.svelte` or a
|
|
194
|
+
`Button.test.ts` can no longer pessimize the shake. It defaults to:
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
// the built-in default (import DEFAULT_DEV_ONLY to extend it)
|
|
198
|
+
devOnly: ['**/*.test.*', '**/*.spec.*', '**/__tests__/**', '**/__mocks__/**', '**/*.stories.*'];
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Passing `devOnly` **replaces** this list (predictable semantics) — spread it to
|
|
202
|
+
extend: `devOnly: [...DEFAULT_DEV_ONLY, 'src/dev/**']` (import `DEFAULT_DEV_ONLY`
|
|
203
|
+
from `svelte-shaker/vite`). Pass `devOnly: []` to count every file (the pre-`devOnly`
|
|
204
|
+
behavior).
|
|
205
|
+
|
|
206
|
+
**List only files that never ship.** A matched file isn't excluded from the shake —
|
|
207
|
+
one the app actually imports is still crawled and shaken; it just stops _counting_
|
|
208
|
+
as a call site. So a file that really ships but matches a pattern (a `+page.svelte`
|
|
209
|
+
under a route dir named `__tests__`) has its distinct prop values stop blocking
|
|
210
|
+
folds, the same failure mode as leaving it out of `entries` — which is why the
|
|
211
|
+
defaults are narrow. See
|
|
212
|
+
[`docs/ARCHITECTURE.md` §8.1.1](https://github.com/baseballyama/svelte-shaker/blob/main/docs/ARCHITECTURE.md)
|
|
213
|
+
for the full argument.
|
|
174
214
|
|
|
175
215
|
## What it removes
|
|
176
216
|
|
|
@@ -207,8 +247,9 @@ The whole point is to **never change observable behavior**.
|
|
|
207
247
|
scope.
|
|
208
248
|
- **Needs `.svelte` source** — libraries shipping compiled JS pass through
|
|
209
249
|
unshaken; distribute via `svelte-package`.
|
|
210
|
-
- **Build
|
|
211
|
-
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.
|
|
212
253
|
- **`entries` must cover the whole app** — the crawl starts there, and every
|
|
213
254
|
`.svelte` file it finds is a call-site source. A call site outside those roots
|
|
214
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
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ComponentId } from './ir.js';
|
|
2
|
+
/**
|
|
3
|
+
* Files treated as DEV-ONLY by default: colocated tests, mocks, and Storybook
|
|
4
|
+
* stories — files that never ship, so their call sites must not count toward the
|
|
5
|
+
* shake. Discounting them is sound precisely because they never reach production;
|
|
6
|
+
* see docs/ARCHITECTURE.md §8.1.1 for the full argument (why a glob is safe here but
|
|
7
|
+
* not for narrowing app coverage, and the shipped-file-matching failure mode).
|
|
8
|
+
* Passing `devOnly` REPLACES this list; `devOnly: []` counts every file.
|
|
9
|
+
*/
|
|
10
|
+
export declare const DEFAULT_DEV_ONLY: readonly string[];
|
|
11
|
+
/** Predicate over an ABSOLUTE path: `true` when the file is dev-only (discounted by a scan). */
|
|
12
|
+
export type DevOnlyFilter = (file: ComponentId) => boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Compile dev-only `patterns` into a {@link DevOnlyFilter}. Each candidate path is
|
|
15
|
+
* matched (with `picomatch`) as its `base`-relative, posix-normalized form, so the
|
|
16
|
+
* result is OS-independent (backslashes on Windows are folded to `/`). `base` is
|
|
17
|
+
* the Vite root for the plugin and the scanned dir for a standalone `svelte-shaker/node`
|
|
18
|
+
* caller. Compile ONCE and reuse across the whole walk — never per file.
|
|
19
|
+
*
|
|
20
|
+
* Defaults to {@link DEFAULT_DEV_ONLY}; an empty `patterns` array matches nothing, so
|
|
21
|
+
* `devOnly: []` counts every file (the pre-`devOnly` behavior).
|
|
22
|
+
*/
|
|
23
|
+
export declare function compileDevOnly(base: string, patterns?: readonly string[]): DevOnlyFilter;
|
package/dist/dev-only.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// ----------------------------------------------------------------------
|
|
2
|
+
// Shell-side glob for the two directory scans (docs/ARCHITECTURE.md §4.2, §8.1.1):
|
|
3
|
+
// which files are DEV-ONLY — they never ship in the production bundle, so their
|
|
4
|
+
// call sites must not count toward the shake. Kept OUT of the env-free engine
|
|
5
|
+
// core (docs §5): it depends on `picomatch` and `node:path`. Both scans — the
|
|
6
|
+
// `.svelte` seed scan (`collectSvelteFiles`) and the non-`.svelte` escape scan
|
|
7
|
+
// (`collectNonSvelteModules`) — take the SAME compiled predicate, so a dev-only
|
|
8
|
+
// file is discounted by both.
|
|
9
|
+
// ----------------------------------------------------------------------
|
|
10
|
+
import picomatch from 'picomatch';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
/**
|
|
13
|
+
* Files treated as DEV-ONLY by default: colocated tests, mocks, and Storybook
|
|
14
|
+
* stories — files that never ship, so their call sites must not count toward the
|
|
15
|
+
* shake. Discounting them is sound precisely because they never reach production;
|
|
16
|
+
* see docs/ARCHITECTURE.md §8.1.1 for the full argument (why a glob is safe here but
|
|
17
|
+
* not for narrowing app coverage, and the shipped-file-matching failure mode).
|
|
18
|
+
* Passing `devOnly` REPLACES this list; `devOnly: []` counts every file.
|
|
19
|
+
*/
|
|
20
|
+
export const DEFAULT_DEV_ONLY = [
|
|
21
|
+
'**/*.test.*',
|
|
22
|
+
'**/*.spec.*',
|
|
23
|
+
'**/__tests__/**',
|
|
24
|
+
'**/__mocks__/**',
|
|
25
|
+
'**/*.stories.*',
|
|
26
|
+
];
|
|
27
|
+
const NONE_DEV_ONLY = () => false;
|
|
28
|
+
/**
|
|
29
|
+
* Compile dev-only `patterns` into a {@link DevOnlyFilter}. Each candidate path is
|
|
30
|
+
* matched (with `picomatch`) as its `base`-relative, posix-normalized form, so the
|
|
31
|
+
* result is OS-independent (backslashes on Windows are folded to `/`). `base` is
|
|
32
|
+
* the Vite root for the plugin and the scanned dir for a standalone `svelte-shaker/node`
|
|
33
|
+
* caller. Compile ONCE and reuse across the whole walk — never per file.
|
|
34
|
+
*
|
|
35
|
+
* Defaults to {@link DEFAULT_DEV_ONLY}; an empty `patterns` array matches nothing, so
|
|
36
|
+
* `devOnly: []` counts every file (the pre-`devOnly` behavior).
|
|
37
|
+
*/
|
|
38
|
+
export function compileDevOnly(base, patterns = DEFAULT_DEV_ONLY) {
|
|
39
|
+
if (patterns.length === 0)
|
|
40
|
+
return NONE_DEV_ONLY;
|
|
41
|
+
const isMatch = picomatch([...patterns]);
|
|
42
|
+
return (file) => isMatch(path.relative(base, file).split(path.sep).join('/'));
|
|
43
|
+
}
|
package/dist/escape-scan.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ComponentId } from './ir.js';
|
|
2
2
|
import type { Resolve, ReadFile } from './analyze.js';
|
|
3
|
+
import { type DevOnlyFilter } from './dev-only.js';
|
|
3
4
|
/**
|
|
4
5
|
* Is `file` a module the escape scan reads — a non-`.svelte` JS/TS source
|
|
5
6
|
* ({@link NON_SVELTE_MODULE_EXTS}), excluding `.d.ts` declaration files (types-only,
|
|
@@ -45,6 +46,13 @@ export declare function computeEscapedComponents(opts: {
|
|
|
45
46
|
preserve?: string[] | undefined;
|
|
46
47
|
/** The crawled `.svelte` component ids, for matching `preserve` prefixes. */
|
|
47
48
|
components: Iterable<ComponentId>;
|
|
49
|
+
/**
|
|
50
|
+
* Dev-only files to discount in the escape scan (docs §8.1.1) — the SAME predicate
|
|
51
|
+
* the seed scan (`collectSvelteFiles`) applies, so a `Button.test.ts` neither seeds
|
|
52
|
+
* a component nor escapes one. Omitted, it defaults to {@link DEFAULT_DEV_ONLY}
|
|
53
|
+
* matched relative to `root`. Pass `compileDevOnly(root, [])` to scan everything.
|
|
54
|
+
*/
|
|
55
|
+
devOnly?: DevOnlyFilter | undefined;
|
|
48
56
|
resolve: Resolve;
|
|
49
57
|
readFile: ReadFile;
|
|
50
58
|
}): Promise<EscapeScanResult>;
|
package/dist/escape-scan.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
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
|
+
import { compileDevOnly } from './dev-only.js';
|
|
11
12
|
/**
|
|
12
13
|
* The non-`.svelte` module extensions we scan for `.svelte` call sites (docs
|
|
13
14
|
* §4.2). A component imported by any of these has a consumer the `.svelte`-only
|
|
@@ -31,51 +32,43 @@ export function isScannableModule(file) {
|
|
|
31
32
|
* Recursively collect every non-`.svelte` module under `dir` (skipping
|
|
32
33
|
* `node_modules` and dot-directories, mirroring `collectSvelteFiles`). Same
|
|
33
34
|
* include scope as the seed scan — `.ts` inside `node_modules` is deliberately NOT
|
|
34
|
-
* scanned (docs §4.2).
|
|
35
|
+
* scanned (docs §4.2). `devOnly` drops modules that never ship (a `Button.test.ts`)
|
|
36
|
+
* so a colocated test does not mark the component it imports escaped (docs §8.1.1);
|
|
37
|
+
* it is the SAME predicate the seed scan uses, so both scans discount the same files.
|
|
35
38
|
*/
|
|
36
|
-
function collectNonSvelteModules(dir) {
|
|
37
|
-
const out = [];
|
|
39
|
+
function collectNonSvelteModules(dir, devOnly, out) {
|
|
38
40
|
let entries;
|
|
39
41
|
try {
|
|
40
42
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
41
43
|
}
|
|
42
44
|
catch {
|
|
43
|
-
return
|
|
45
|
+
return;
|
|
44
46
|
}
|
|
45
47
|
for (const entry of entries) {
|
|
46
48
|
if (entry.name === 'node_modules' || entry.name.startsWith('.'))
|
|
47
49
|
continue;
|
|
48
50
|
const full = path.join(dir, entry.name);
|
|
49
51
|
if (entry.isDirectory())
|
|
50
|
-
|
|
51
|
-
else if (entry.isFile() && isScannableModule(entry.name))
|
|
52
|
+
collectNonSvelteModules(full, devOnly, out);
|
|
53
|
+
else if (entry.isFile() && isScannableModule(entry.name) && !devOnly(full))
|
|
52
54
|
out.push(full);
|
|
53
55
|
}
|
|
54
|
-
return out;
|
|
55
56
|
}
|
|
56
57
|
/**
|
|
57
58
|
* Every module specifier a JS/TS module statically references: `import … from`,
|
|
58
59
|
* `export … from` / `export *`, and a dynamic `import('…')` whose argument is a
|
|
59
60
|
* STRING LITERAL. A computed dynamic import (`import(expr)`) is unknowable here —
|
|
60
|
-
* the `preserve` option (docs §4.2) is the sound fallback for it. Parsed via
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* `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.
|
|
67
67
|
*/
|
|
68
68
|
function moduleImportSpecifiers(code, id) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
return null; // unparseable — the caller reports it (soundness hole, `preserve` fixes it)
|
|
75
|
-
}
|
|
76
|
-
const program = module?.content;
|
|
77
|
-
if (!program)
|
|
78
|
-
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)
|
|
79
72
|
const specs = [];
|
|
80
73
|
const literalSource = (node) => node?.type === 'Literal' && typeof node.value === 'string' ? node.value : undefined;
|
|
81
74
|
walk(program, null, {
|
|
@@ -181,7 +174,11 @@ export function matchPreserve(preserve, root, components) {
|
|
|
181
174
|
* `svelte-shaker/node`).
|
|
182
175
|
*/
|
|
183
176
|
export async function computeEscapedComponents(opts) {
|
|
184
|
-
const
|
|
177
|
+
const devOnly = opts.devOnly ?? compileDevOnly(opts.root);
|
|
178
|
+
const modules = [];
|
|
179
|
+
for (const dir of opts.entryDirs)
|
|
180
|
+
collectNonSvelteModules(dir, devOnly, modules);
|
|
181
|
+
const { escaped, unscannable } = await collectModuleEscapes(modules, opts.resolve, opts.readFile);
|
|
185
182
|
const { matched, unmatched } = partitionPreserve(opts.preserve, opts.root, opts.components);
|
|
186
183
|
for (const id of matched)
|
|
187
184
|
escaped.add(id);
|
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);
|
package/dist/rsvelte-parse.d.ts
CHANGED
|
@@ -6,9 +6,9 @@ import type { Parse } from './parse.js';
|
|
|
6
6
|
* rsvelte is the default parser, the caller THROWS on `null` rather than silently
|
|
7
7
|
* using svelte/compiler; `parser: 'svelte'` is the explicit opt-out.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* rsvelte's AST positions match svelte/compiler's — UTF-16 code-unit offsets — so
|
|
10
|
+
* the AST feeds the engine directly (`@rsvelte/compiler` <= 0.6 reported UTF-8
|
|
11
|
+
* *byte* offsets and needed a remap; 0.7 emits UTF-16, so the remap is gone).
|
|
12
12
|
* A genuine parse error on a specific file is a real failure and PROPAGATES; only
|
|
13
13
|
* a load/init failure returns `null`.
|
|
14
14
|
*/
|
package/dist/rsvelte-parse.js
CHANGED
|
@@ -8,6 +8,11 @@ import { createRequire } from 'node:module';
|
|
|
8
8
|
// free engine (`index.ts`/`analyze.ts`), so the browser playground build stays
|
|
9
9
|
// clean.
|
|
10
10
|
const require = createRequire(import.meta.url);
|
|
11
|
+
/** The wasm-pack `--target web` bytes shipped in `@rsvelte/compiler`, reached via
|
|
12
|
+
* the package's stable `./wasm` subpath export (added upstream in 0.8.1). Using
|
|
13
|
+
* the public subpath keeps the loader independent of the internal crate name that
|
|
14
|
+
* determines the actual artifact basename. */
|
|
15
|
+
const WASM_FILE = '@rsvelte/compiler/wasm';
|
|
11
16
|
let ready = false;
|
|
12
17
|
/** Require `@rsvelte/compiler` and initialize its wasm once. Throws if the
|
|
13
18
|
* package can't be resolved or the wasm can't be instantiated. */
|
|
@@ -17,7 +22,7 @@ function loadCompiler() {
|
|
|
17
22
|
if (!ready) {
|
|
18
23
|
// A `wasm-pack --target web` module: init once with the wasm bytes (Node has
|
|
19
24
|
// no fetch for file URLs), after which `parse_svelte` is callable.
|
|
20
|
-
const wasmPath = require.resolve(
|
|
25
|
+
const wasmPath = require.resolve(WASM_FILE);
|
|
21
26
|
compiler.initSync({ module: readFileSync(wasmPath) });
|
|
22
27
|
ready = true;
|
|
23
28
|
}
|
|
@@ -30,9 +35,9 @@ function loadCompiler() {
|
|
|
30
35
|
* rsvelte is the default parser, the caller THROWS on `null` rather than silently
|
|
31
36
|
* using svelte/compiler; `parser: 'svelte'` is the explicit opt-out.
|
|
32
37
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
38
|
+
* rsvelte's AST positions match svelte/compiler's — UTF-16 code-unit offsets — so
|
|
39
|
+
* the AST feeds the engine directly (`@rsvelte/compiler` <= 0.6 reported UTF-8
|
|
40
|
+
* *byte* offsets and needed a remap; 0.7 emits UTF-16, so the remap is gone).
|
|
36
41
|
* A genuine parse error on a specific file is a real failure and PROPAGATES; only
|
|
37
42
|
* a load/init failure returns `null`.
|
|
38
43
|
*/
|
package/dist/scan.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ComponentId } from './ir.js';
|
|
2
2
|
import type { Resolve, ReadFile } from './analyze.js';
|
|
3
|
+
import { type DevOnlyFilter } from './dev-only.js';
|
|
3
4
|
export { computeEscapedComponents, type EscapeScanResult } from './escape-scan.js';
|
|
5
|
+
export { DEFAULT_DEV_ONLY, compileDevOnly, type DevOnlyFilter } from './dev-only.js';
|
|
4
6
|
/** Default filesystem resolver: resolve `source` relative to its importer. */
|
|
5
7
|
export declare const fsResolve: Resolve;
|
|
6
8
|
/** Default filesystem reader. */
|
|
@@ -9,5 +11,14 @@ export declare const fsReadFile: ReadFile;
|
|
|
9
11
|
* Recursively collect every `.svelte` file under `dir` (skipping `node_modules`
|
|
10
12
|
* and dot-directories). A Shell helper, kept out of the env-free engine core
|
|
11
13
|
* (docs/ARCHITECTURE.md §5): plugins use it to seed the whole-program crawl.
|
|
14
|
+
*
|
|
15
|
+
* `devOnly` lists files that never ship in the production bundle (tests, stories);
|
|
16
|
+
* a match stops counting as a component consumer, so it is not seeded as an entry
|
|
17
|
+
* and cannot pessimize the shake (docs §8.1.1). A matched file the app actually
|
|
18
|
+
* imports is still crawled and shaken through the normal graph — this only removes
|
|
19
|
+
* it as a SEED. Omitted, it defaults to {@link DEFAULT_DEV_ONLY} matched relative
|
|
20
|
+
* to `dir`; the Vite plugin passes a predicate compiled against the Vite ROOT so a
|
|
21
|
+
* custom pattern is root-relative there. Pass `compileDevOnly(dir, [])` to seed
|
|
22
|
+
* every file.
|
|
12
23
|
*/
|
|
13
|
-
export declare function collectSvelteFiles(dir: string): ComponentId[];
|
|
24
|
+
export declare function collectSvelteFiles(dir: string, devOnly?: DevOnlyFilter): ComponentId[];
|
package/dist/scan.js
CHANGED
|
@@ -5,9 +5,14 @@
|
|
|
5
5
|
// ----------------------------------------------------------------------
|
|
6
6
|
import * as fs from 'node:fs';
|
|
7
7
|
import * as path from 'node:path';
|
|
8
|
+
import { compileDevOnly } from './dev-only.js';
|
|
8
9
|
// The escape-scan machinery lives in `./escape-scan.js` (internal); only the single
|
|
9
10
|
// entry helper is part of the public `svelte-shaker/node` surface.
|
|
10
11
|
export { computeEscapedComponents } from './escape-scan.js';
|
|
12
|
+
// The dev-only glob support (docs §8.1.1) is Shell-side and part of the public
|
|
13
|
+
// `svelte-shaker/node` surface, so a plain-Rollup pipeline can compile the same
|
|
14
|
+
// predicate the Vite plugin does and feed it to both scans.
|
|
15
|
+
export { DEFAULT_DEV_ONLY, compileDevOnly } from './dev-only.js';
|
|
11
16
|
/** Default filesystem resolver: resolve `source` relative to its importer. */
|
|
12
17
|
export const fsResolve = (source, importer) => {
|
|
13
18
|
if (!source.startsWith('.'))
|
|
@@ -20,24 +25,37 @@ export const fsReadFile = (id) => fs.readFileSync(id, 'utf-8');
|
|
|
20
25
|
* Recursively collect every `.svelte` file under `dir` (skipping `node_modules`
|
|
21
26
|
* and dot-directories). A Shell helper, kept out of the env-free engine core
|
|
22
27
|
* (docs/ARCHITECTURE.md §5): plugins use it to seed the whole-program crawl.
|
|
28
|
+
*
|
|
29
|
+
* `devOnly` lists files that never ship in the production bundle (tests, stories);
|
|
30
|
+
* a match stops counting as a component consumer, so it is not seeded as an entry
|
|
31
|
+
* and cannot pessimize the shake (docs §8.1.1). A matched file the app actually
|
|
32
|
+
* imports is still crawled and shaken through the normal graph — this only removes
|
|
33
|
+
* it as a SEED. Omitted, it defaults to {@link DEFAULT_DEV_ONLY} matched relative
|
|
34
|
+
* to `dir`; the Vite plugin passes a predicate compiled against the Vite ROOT so a
|
|
35
|
+
* custom pattern is root-relative there. Pass `compileDevOnly(dir, [])` to seed
|
|
36
|
+
* every file.
|
|
23
37
|
*/
|
|
24
|
-
export function collectSvelteFiles(dir) {
|
|
38
|
+
export function collectSvelteFiles(dir, devOnly = compileDevOnly(dir)) {
|
|
25
39
|
const out = [];
|
|
40
|
+
collectSvelteFilesInto(dir, devOnly, out);
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
/** Recursive worker: the compiled `devOnly` predicate is threaded, never recompiled. */
|
|
44
|
+
function collectSvelteFilesInto(dir, devOnly, out) {
|
|
26
45
|
let entries;
|
|
27
46
|
try {
|
|
28
47
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
29
48
|
}
|
|
30
49
|
catch {
|
|
31
|
-
return
|
|
50
|
+
return;
|
|
32
51
|
}
|
|
33
52
|
for (const entry of entries) {
|
|
34
53
|
if (entry.name === 'node_modules' || entry.name.startsWith('.'))
|
|
35
54
|
continue;
|
|
36
55
|
const full = path.join(dir, entry.name);
|
|
37
56
|
if (entry.isDirectory())
|
|
38
|
-
|
|
39
|
-
else if (entry.isFile() && entry.name.endsWith('.svelte'))
|
|
57
|
+
collectSvelteFilesInto(full, devOnly, out);
|
|
58
|
+
else if (entry.isFile() && entry.name.endsWith('.svelte') && !devOnly(full))
|
|
40
59
|
out.push(full);
|
|
41
60
|
}
|
|
42
|
-
return out;
|
|
43
61
|
}
|
package/dist/vite.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
2
|
import { type DevMode } from './engine.js';
|
|
3
|
+
export { DEFAULT_DEV_ONLY } from './dev-only.js';
|
|
3
4
|
import { type MonomorphizeOptions } from './mono.js';
|
|
4
5
|
/**
|
|
5
6
|
* Options for the {@link shaker} plugin. The set below is exhaustive: any other
|
|
@@ -52,6 +53,35 @@ export interface ShakerOptions {
|
|
|
52
53
|
* without needing it is merely shaken less, never wrongly.
|
|
53
54
|
*/
|
|
54
55
|
preserve?: string[];
|
|
56
|
+
/**
|
|
57
|
+
* Glob patterns naming files that are DEV-ONLY — they never ship in the production
|
|
58
|
+
* bundle (colocated tests, mocks, Storybook stories), so their call sites must not
|
|
59
|
+
* count toward the shake. A matched file stops counting as a component consumer in
|
|
60
|
+
* BOTH directory scans (the `.svelte` seed scan and the non-`.svelte` escape scan);
|
|
61
|
+
* patterns are matched with `picomatch` against the posix-normalized path relative
|
|
62
|
+
* to the Vite root. Defaults to {@link DEFAULT_DEV_ONLY}.
|
|
63
|
+
*
|
|
64
|
+
* List ONLY files that never ship: the option declares a property of the files,
|
|
65
|
+
* which is exactly the soundness contract that makes discounting them safe. A
|
|
66
|
+
* matched file is NOT excluded from the shake — a dev-only `.svelte` file the app
|
|
67
|
+
* actually imports is still crawled and shaken through the normal import graph, so
|
|
68
|
+
* its genuine app call sites still count. This only removes it as a SEED / ESCAPE
|
|
69
|
+
* source; it cannot un-cover reachable app code.
|
|
70
|
+
*
|
|
71
|
+
* The failure mode to avoid: a matched file's call sites simply STOP COUNTING. So
|
|
72
|
+
* if a file that really ships matches a pattern — a `+page.svelte` under a route
|
|
73
|
+
* dir named `__tests__`, a `foo.test.utils.ts` that mounts a component — the
|
|
74
|
+
* distinct prop values it passes no longer block a fold, exactly as if you had left
|
|
75
|
+
* it out of {@link entries}. That is why the defaults are narrow, convention-based
|
|
76
|
+
* patterns rather than an open glob (see docs/ARCHITECTURE.md §8.1.1). It is
|
|
77
|
+
* unrelated to {@link preserve}: that keeps a shipping component's props as written;
|
|
78
|
+
* this declares that a file is not part of the production graph at all.
|
|
79
|
+
*
|
|
80
|
+
* Passing `devOnly` REPLACES the default rather than adding to it (predictable
|
|
81
|
+
* semantics); extend it with `[...DEFAULT_DEV_ONLY, '…']`, and pass `devOnly: []` to
|
|
82
|
+
* count every file (the pre-`devOnly` behavior).
|
|
83
|
+
*/
|
|
84
|
+
devOnly?: string[];
|
|
55
85
|
/**
|
|
56
86
|
* Per-call-site monomorphization tuning (docs §13.2). Monomorphization is ON
|
|
57
87
|
* by default because it is bail-safe and never bloats (the measured net-win
|
package/dist/vite.js
CHANGED
|
@@ -5,6 +5,9 @@ import { svelteShaker, svelteShakerWithMono } from './index.js';
|
|
|
5
5
|
import { DevShaker } from './engine.js';
|
|
6
6
|
import { collectSvelteFiles, fsResolve } from './scan.js';
|
|
7
7
|
import { computeEscapedComponents, isScannableModule, } from './escape-scan.js';
|
|
8
|
+
import { compileDevOnly } from './dev-only.js';
|
|
9
|
+
// Re-export so a user can extend the default dev-only list: `devOnly: [...DEFAULT_DEV_ONLY, '…']`.
|
|
10
|
+
export { DEFAULT_DEV_ONLY } from './dev-only.js';
|
|
8
11
|
import { DEFAULT_MONO_OPTIONS } from './mono.js';
|
|
9
12
|
import { tryLoadRsvelteParser } from './rsvelte-parse.js';
|
|
10
13
|
import { svelteShakerWasm, svelteShakerWasmWithMono, tryLoadWasmEngine } from './wasm-engine.js';
|
|
@@ -126,6 +129,7 @@ const RENAMED_OPTIONS = {
|
|
|
126
129
|
const KNOWN_OPTIONS = {
|
|
127
130
|
entries: true,
|
|
128
131
|
preserve: true,
|
|
132
|
+
devOnly: true,
|
|
129
133
|
monomorphize: true,
|
|
130
134
|
engine: true,
|
|
131
135
|
dev: true,
|
|
@@ -263,9 +267,13 @@ export function shaker(options = {}) {
|
|
|
263
267
|
if (!devMode)
|
|
264
268
|
return;
|
|
265
269
|
const dirs = (options.entries ?? ['.']).map((p) => path.resolve(root, p));
|
|
270
|
+
// One dev-only predicate for the whole dev session, compiled against the Vite
|
|
271
|
+
// root so custom patterns are root-relative regardless of which entry dir a
|
|
272
|
+
// file sits under. Fed to both scans and the watch guards below (docs §8.1.1).
|
|
273
|
+
const isDevOnly = compileDevOnly(root, options.devOnly);
|
|
266
274
|
// The engine's entry components, collected from the entry DIRS: `entries`
|
|
267
275
|
// names the roots to crawl from, these are the `.svelte` files under them.
|
|
268
|
-
const entryComponents = dirs.flatMap(collectSvelteFiles);
|
|
276
|
+
const entryComponents = dirs.flatMap((d) => collectSvelteFiles(d, isDevOnly));
|
|
269
277
|
const read = (id) => fs.readFileSync(id, 'utf-8');
|
|
270
278
|
const underDirs = (file) => dirs.some((d) => file === d || file.startsWith(d + path.sep));
|
|
271
279
|
// Re-scan the non-`.svelte` modules for `.svelte` call sites and re-apply
|
|
@@ -280,6 +288,7 @@ export function shaker(options = {}) {
|
|
|
280
288
|
root,
|
|
281
289
|
preserve: options.preserve,
|
|
282
290
|
components: entryComponents,
|
|
291
|
+
devOnly: isDevOnly,
|
|
283
292
|
resolve: fsResolve,
|
|
284
293
|
readFile: read,
|
|
285
294
|
});
|
|
@@ -301,7 +310,10 @@ export function shaker(options = {}) {
|
|
|
301
310
|
// un-shake a child; a removed one can re-shake it — docs §4). Re-shake and
|
|
302
311
|
// full-reload: over-invalidation is always sound, and add/remove is rare
|
|
303
312
|
// enough that fine-grained HMR for it is not worth the complexity here.
|
|
304
|
-
|
|
313
|
+
// Dev-only files are discounted by both scans, so an edit to one never moves
|
|
314
|
+
// the shake — skip them here too, or a `Foo.test.svelte` save would retrigger a
|
|
315
|
+
// full re-shake for no effect.
|
|
316
|
+
const isOurs = (file) => file.endsWith('.svelte') && underDirs(file) && !isDevOnly(file);
|
|
305
317
|
const onGraphChange = async (file, kind) => {
|
|
306
318
|
if (!devShaker || !isOurs(file))
|
|
307
319
|
return;
|
|
@@ -315,7 +327,7 @@ export function shaker(options = {}) {
|
|
|
315
327
|
// we re-shake and full-reload (over-invalidation is sound, but pointless
|
|
316
328
|
// reloads on unrelated `.ts` edits are avoided).
|
|
317
329
|
const onModuleChange = async (file) => {
|
|
318
|
-
if (!devShaker || !isScannableModule(file) || !underDirs(file))
|
|
330
|
+
if (!devShaker || !isScannableModule(file) || !underDirs(file) || isDevOnly(file))
|
|
319
331
|
return;
|
|
320
332
|
const before = escapedKey;
|
|
321
333
|
devShaker.setEscaped(await currentEscaped());
|
|
@@ -363,9 +375,12 @@ export function shaker(options = {}) {
|
|
|
363
375
|
if (devShaker)
|
|
364
376
|
return;
|
|
365
377
|
const dirs = (options.entries ?? ['.']).map((p) => path.resolve(root, p));
|
|
378
|
+
// One dev-only predicate for both scans, compiled against the Vite root so
|
|
379
|
+
// custom patterns are root-relative regardless of the entry dir (docs §8.1.1).
|
|
380
|
+
const isDevOnly = compileDevOnly(root, options.devOnly);
|
|
366
381
|
// The engine's entry components, collected from the entry DIRS: `entries`
|
|
367
382
|
// names the roots to crawl from, these are the `.svelte` files under them.
|
|
368
|
-
const entryComponents = dirs.flatMap(collectSvelteFiles);
|
|
383
|
+
const entryComponents = dirs.flatMap((d) => collectSvelteFiles(d, isDevOnly));
|
|
369
384
|
if (entryComponents.length === 0) {
|
|
370
385
|
shaken = {};
|
|
371
386
|
variantSources = new Map();
|
|
@@ -404,6 +419,7 @@ export function shaker(options = {}) {
|
|
|
404
419
|
root,
|
|
405
420
|
preserve: options.preserve,
|
|
406
421
|
components: entryComponents,
|
|
422
|
+
devOnly: isDevOnly,
|
|
407
423
|
resolve,
|
|
408
424
|
readFile: read,
|
|
409
425
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-shaker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"description": "Tree shaking for Svelte components",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dead-code-elimination",
|
|
@@ -49,12 +49,14 @@
|
|
|
49
49
|
"access": "public"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@rsvelte/compiler": "0.
|
|
52
|
+
"@rsvelte/compiler": "0.8.1",
|
|
53
53
|
"magic-string": "1.0.0",
|
|
54
|
+
"picomatch": "4.0.5",
|
|
54
55
|
"zimmerframe": "1.1.4"
|
|
55
56
|
},
|
|
56
57
|
"devDependencies": {
|
|
57
58
|
"@sveltejs/vite-plugin-svelte": "^7.2.0",
|
|
59
|
+
"@types/picomatch": "4.0.2",
|
|
58
60
|
"svelte": "^5.56.6",
|
|
59
61
|
"vite": "^8.1.5"
|
|
60
62
|
},
|