svelte-shaker 0.14.1 → 0.15.0
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 +29 -0
- 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 +13 -8
- 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
|
@@ -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
|
|
@@ -171,6 +174,32 @@ options that do exist. A typo would otherwise be ignored — and a misspelled
|
|
|
171
174
|
The build **warns** (with the file path) about any module the scan couldn't
|
|
172
175
|
parse — so a mounted component isn't silently left unprotected — and about
|
|
173
176
|
`preserve` entries that matched no component.
|
|
177
|
+
- **`devOnly`** — glob patterns (matched with
|
|
178
|
+
[`picomatch`](https://github.com/micromatch/picomatch) against each file's path
|
|
179
|
+
relative to the Vite root) naming files that **never ship in the production
|
|
180
|
+
bundle** — colocated tests, mocks, Storybook stories. A matched file **stops
|
|
181
|
+
counting as a component consumer** in **both directory scans** (the `.svelte` seed
|
|
182
|
+
scan and the non-`.svelte` escape scan), so a `Foo.test.svelte` or a
|
|
183
|
+
`Button.test.ts` can no longer pessimize the shake. It defaults to:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
// the built-in default (import DEFAULT_DEV_ONLY to extend it)
|
|
187
|
+
devOnly: ['**/*.test.*', '**/*.spec.*', '**/__tests__/**', '**/__mocks__/**', '**/*.stories.*'];
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Passing `devOnly` **replaces** this list (predictable semantics) — spread it to
|
|
191
|
+
extend: `devOnly: [...DEFAULT_DEV_ONLY, 'src/dev/**']` (import `DEFAULT_DEV_ONLY`
|
|
192
|
+
from `svelte-shaker/vite`). Pass `devOnly: []` to count every file (the pre-`devOnly`
|
|
193
|
+
behavior).
|
|
194
|
+
|
|
195
|
+
**List only files that never ship.** A matched file isn't excluded from the shake —
|
|
196
|
+
one the app actually imports is still crawled and shaken; it just stops _counting_
|
|
197
|
+
as a call site. So a file that really ships but matches a pattern (a `+page.svelte`
|
|
198
|
+
under a route dir named `__tests__`) has its distinct prop values stop blocking
|
|
199
|
+
folds, the same failure mode as leaving it out of `entries` — which is why the
|
|
200
|
+
defaults are narrow. See
|
|
201
|
+
[`docs/ARCHITECTURE.md` §8.1.1](https://github.com/baseballyama/svelte-shaker/blob/main/docs/ARCHITECTURE.md)
|
|
202
|
+
for the full argument.
|
|
174
203
|
|
|
175
204
|
## What it removes
|
|
176
205
|
|
|
@@ -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
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import * as fs from 'node:fs';
|
|
9
9
|
import * as path from 'node:path';
|
|
10
10
|
import { parseSvelte, 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,27 +32,27 @@ 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`,
|
|
@@ -181,7 +182,11 @@ export function matchPreserve(preserve, root, components) {
|
|
|
181
182
|
* `svelte-shaker/node`).
|
|
182
183
|
*/
|
|
183
184
|
export async function computeEscapedComponents(opts) {
|
|
184
|
-
const
|
|
185
|
+
const devOnly = opts.devOnly ?? compileDevOnly(opts.root);
|
|
186
|
+
const modules = [];
|
|
187
|
+
for (const dir of opts.entryDirs)
|
|
188
|
+
collectNonSvelteModules(dir, devOnly, modules);
|
|
189
|
+
const { escaped, unscannable } = await collectModuleEscapes(modules, opts.resolve, opts.readFile);
|
|
185
190
|
const { matched, unmatched } = partitionPreserve(opts.preserve, opts.root, opts.components);
|
|
186
191
|
for (const id of matched)
|
|
187
192
|
escaped.add(id);
|
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.0",
|
|
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
|
},
|