svelte-shaker 0.16.0 → 0.17.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 +41 -33
- package/dist/analyze.d.ts +44 -2
- package/dist/analyze.js +32 -30
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/mono.d.ts +14 -1
- package/dist/mono.js +7 -14
- package/dist/native-engine.d.ts +59 -0
- package/dist/native-engine.js +141 -0
- package/dist/rsvelte-parse.d.ts +10 -0
- package/dist/rsvelte-parse.js +21 -0
- package/dist/vite.d.ts +33 -24
- package/dist/vite.js +64 -16
- package/dist/wasm-engine.d.ts +5 -5
- package/dist/wasm-engine.js +4 -15
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -47,19 +47,21 @@ reachable value set of `variant` and removes the `.btn-danger` rule.
|
|
|
47
47
|
npm i -D svelte-shaker # requires svelte@^5
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
Nothing else to install. The plugin picks its engine
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
Nothing else to install. The plugin picks its engine automatically (see
|
|
51
|
+
[Options](#options)): if a **native (napi) Rust** binary loads it runs there —
|
|
52
|
+
parsing with rsvelte in process, fastest, no size ceiling; otherwise a small/medium
|
|
53
|
+
app runs on the **WASM Rust engine** (parsing with **rsvelte** from
|
|
54
|
+
`@rsvelte/compiler`, a bundled WASM dependency — no peer, no platform-specific
|
|
55
|
+
binary) and a large app on the **JS engine** with **svelte/compiler**. `engine` and
|
|
56
|
+
`parser` let you pin the choice.
|
|
55
57
|
|
|
56
58
|
## Usage (Vite)
|
|
57
59
|
|
|
58
60
|
Add the plugin **before** `svelte()`. By default it runs only in `vite build` —
|
|
59
61
|
dev/HMR is a pass-through (opt into dev shaking with the `dev` option, see
|
|
60
|
-
[Options](#options)). The engine
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
[Options](#options)). The engine is chosen automatically (native Rust if a binary
|
|
63
|
+
loads, else WASM Rust for small/medium or JS for large), and every path falls back
|
|
64
|
+
cleanly (see [Options](#options)).
|
|
63
65
|
|
|
64
66
|
```ts
|
|
65
67
|
// vite.config.ts
|
|
@@ -104,10 +106,10 @@ shaker({
|
|
|
104
106
|
// or { maxVariants: 16, minSavings: 0.05 } to tune
|
|
105
107
|
verbose: false, // true = per-file size breakdown after the build
|
|
106
108
|
|
|
107
|
-
// Engine
|
|
108
|
-
engine: 'auto', // 'auto' (Rust
|
|
109
|
-
parser: undefined, //
|
|
110
|
-
// set 'rsvelte' | 'svelte' to pin
|
|
109
|
+
// Engine is auto-selected; set these only to pin.
|
|
110
|
+
engine: 'auto', // 'auto' (native Rust if it loads, else WASM if <=~300, else JS) | 'js' | 'rust'
|
|
111
|
+
parser: undefined, // JS/WASM parse only (native always uses in-process rsvelte);
|
|
112
|
+
// default follows the engine (WASM->rsvelte, JS->svelte); set 'rsvelte' | 'svelte' to pin
|
|
111
113
|
|
|
112
114
|
dev: false, // default off: dev is a pass-through. 'incremental' (re-parse only
|
|
113
115
|
// changed files) | 'coarse' (re-analyze everything) opts in; never monomorphizes
|
|
@@ -118,27 +120,33 @@ That list is exhaustive: any other key **fails the build**, naming the key and t
|
|
|
118
120
|
options that do exist. A typo would otherwise be ignored — and a misspelled
|
|
119
121
|
`preserve` ships the component you meant to protect, over-shaken.
|
|
120
122
|
|
|
121
|
-
- **`engine`** — which engine runs the shake.
|
|
122
|
-
|
|
123
|
-
**
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
**
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
123
|
+
- **`engine`** — which engine runs the shake. There are two Rust engines (the same
|
|
124
|
+
analysis behind two frontends) plus the JS engine. The **native (napi)** engine
|
|
125
|
+
parses with rsvelte **in process** and keeps the ASTs Rust-side, so no
|
|
126
|
+
whole-program AST crosses a boundary — fastest, no size ceiling — but it ships as
|
|
127
|
+
a per-platform prebuilt binary that may not exist for every install. The **WASM**
|
|
128
|
+
engine is the same Rust engine, but the whole-program AST must cross the JS↔WASM
|
|
129
|
+
boundary as JSON (tens of MB past a few hundred components), so it only wins for
|
|
130
|
+
small/medium apps; the **JS** engine needs no boundary crossing, so it wins for a
|
|
131
|
+
large app when the native binary isn't available. **`'auto'`** (default) uses the
|
|
132
|
+
native engine if a binary loads (no size gate), else the WASM engine for a
|
|
133
|
+
small/medium app or the JS engine for a large one. **`'rust'`** forces a Rust
|
|
134
|
+
engine — native if it loads, else WASM (throwing if `@rsvelte/compiler` can't load
|
|
135
|
+
for the WASM fallback); **`'js'`** forces the JS engine. All three are
|
|
136
|
+
differentially tested to shake **byte-identically**, so this is **speed-only** — it
|
|
137
|
+
never changes what ships.
|
|
138
|
+
- **`parser`** — how the **JS / WASM** engines parse `.svelte`. It does **not** apply
|
|
139
|
+
to the native engine, which always parses with rsvelte in process. The default
|
|
140
|
+
**follows the engine**: **rsvelte** on the WASM engine (its AST crosses into Rust
|
|
141
|
+
directly), **svelte/compiler** on the JS engine (where rsvelte's parse is ~2× slower
|
|
142
|
+
with no downstream benefit). The choice is **soundness-neutral** — the engine reads
|
|
143
|
+
only UTF-16 `start`/`end`, so both parsers are differentially tested to produce
|
|
144
|
+
**byte-identical** output, never changing what renders. `parser: 'svelte'` also
|
|
145
|
+
forces the native engine **off** (it can't honor svelte/compiler), so the shake uses
|
|
146
|
+
a JS/WASM engine with svelte/compiler. When rsvelte is the resolved JS-side parser
|
|
147
|
+
(`parser: 'rsvelte'`, or the WASM engine's default) and `@rsvelte/compiler` can't
|
|
148
|
+
load, the plugin **throws** rather than silently swapping (so the same source can't
|
|
149
|
+
shake differently on another machine); `parser: 'svelte'` is the explicit opt-out.
|
|
142
150
|
- **`monomorphize`** — the one shaking knob, **on** by default. A measured
|
|
143
151
|
net-win gate only specializes a component when that strictly shrinks the whole
|
|
144
152
|
program, so monomorphization **never bloats**: whatever the knobs are set to,
|
package/dist/analyze.d.ts
CHANGED
|
@@ -237,14 +237,32 @@ export declare function planFixpoint(models: Map<ComponentId, FileModel>): Map<C
|
|
|
237
237
|
* import is never crawled — its `<Comp/>` site cannot exist, so it cannot taint a
|
|
238
238
|
* value set), keeping the produced model set — and thus the output — identical.
|
|
239
239
|
*/
|
|
240
|
-
|
|
240
|
+
/**
|
|
241
|
+
* The three per-file facts the crawl needs to resolve edges — import specifiers,
|
|
242
|
+
* rendered `<Local>` tag names, and `<ns.X>` member tag names. `null` when the file
|
|
243
|
+
* has no instance script (nothing to attribute), matching the crawl's skip.
|
|
244
|
+
*/
|
|
245
|
+
export interface CrawlFacts {
|
|
246
|
+
imports: ImportInfo[];
|
|
247
|
+
renderedTags: Set<string>;
|
|
248
|
+
memberTags: Set<string>;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Source of {@link CrawlFacts} for one `(id, code)`. The default is a JS parse +
|
|
252
|
+
* extraction ({@link jsCrawlFacts}); the native engine passes a provider backed by
|
|
253
|
+
* `ShakeSession` facts, so the crawl resolves the SAME edges without the JS parse.
|
|
254
|
+
*/
|
|
255
|
+
export type FactsProvider = (id: ComponentId, code: string) => CrawlFacts | null;
|
|
256
|
+
/** The default facts provider: parse (svelte/compiler or rsvelte) and extract. */
|
|
257
|
+
export declare function jsCrawlFacts(id: ComponentId, code: string, parseCache?: ParseCache, parse?: Parse): CrawlFacts | null;
|
|
258
|
+
export declare function buildAnalyzeInput(entries: ComponentId | ComponentId[], resolve: Resolve, readFile: ReadFile, parseCache?: ParseCache, parse?: Parse, escaped?: ComponentId[], factsProvider?: FactsProvider): Promise<AnalyzeInput>;
|
|
241
259
|
/**
|
|
242
260
|
* Synchronous twin of {@link buildAnalyzeInput} for callers that cannot await
|
|
243
261
|
* (an ESLint rule runs synchronously). Byte-for-byte the same crawl with sync
|
|
244
262
|
* `resolve`/`readFile`; the `tests/build-analyze-input-sync` differential test
|
|
245
263
|
* pins it identical to the async path, so keep the two bodies in lockstep.
|
|
246
264
|
*/
|
|
247
|
-
export declare function buildAnalyzeInputSync(entries: ComponentId | ComponentId[], resolve: ResolveSync, readFile: ReadFileSync, parseCache?: ParseCache, parse?: Parse, escaped?: ComponentId[]): AnalyzeInput;
|
|
265
|
+
export declare function buildAnalyzeInputSync(entries: ComponentId | ComponentId[], resolve: ResolveSync, readFile: ReadFileSync, parseCache?: ParseCache, parse?: Parse, escaped?: ComponentId[], factsProvider?: FactsProvider): AnalyzeInput;
|
|
248
266
|
/**
|
|
249
267
|
* Dead `{#if}` spans per component implied by `plans`, via the SAME shared
|
|
250
268
|
* predicate the transform uses ({@link computeDeadSpans}). A bailed component
|
|
@@ -299,6 +317,22 @@ export declare function findNeverPassedProps(input: AnalyzeInput): Map<Component
|
|
|
299
317
|
* maps cleanly; an external name with no matching declared local is dropped.
|
|
300
318
|
*/
|
|
301
319
|
export declare function remapToLocalNames<V>(map: Map<string, V>, model: FileModel): Map<string, V>;
|
|
320
|
+
/**
|
|
321
|
+
* The bare component tag names this file RENDERS (`<Local/>`, excluding dotted
|
|
322
|
+
* `<ns.X/>` member tags). The Shell crawl uses this to resolve a barrel (a
|
|
323
|
+
* `.js`/`.ts` re-export, which costs a module read+parse) only for named imports
|
|
324
|
+
* actually rendered as a component — a value-only named import (a helper / type)
|
|
325
|
+
* is never a `<Local>` call site, so following it would read+parse a module for
|
|
326
|
+
* nothing. Skipping it only ever drops a non-call-site, so attribution (and the
|
|
327
|
+
* resulting models) are unchanged.
|
|
328
|
+
*/
|
|
329
|
+
export declare function renderedComponentTagNames(ast: Root): Set<string>;
|
|
330
|
+
/**
|
|
331
|
+
* Every dotted component tag a file renders (`<ns.Child/>` -> `"ns.Child"`). The
|
|
332
|
+
* Shell resolves each through its namespace import's barrel; bare `<Child/>` tags
|
|
333
|
+
* have no dot and are bound by the plain import maps instead.
|
|
334
|
+
*/
|
|
335
|
+
export declare function memberComponentTags(ast: Root): Set<string>;
|
|
302
336
|
/**
|
|
303
337
|
* Read one `<Child .../>` into a {@link CallSite}. Attributes are in source
|
|
304
338
|
* order, so we resolve last-write-wins (a later `a={…}` overrides an earlier
|
|
@@ -323,3 +357,11 @@ export declare function readCallSite(component: AnyNode, owner?: ComponentId): C
|
|
|
323
357
|
* monomorphization specialization (mono.ts) must honor this identically.
|
|
324
358
|
*/
|
|
325
359
|
export declare function isFoldBlockedName(model: FileModel, name: string): boolean;
|
|
360
|
+
export interface ImportInfo {
|
|
361
|
+
value: string;
|
|
362
|
+
local: string;
|
|
363
|
+
/** `default` for a default import, the exported name for a named import, or
|
|
364
|
+
* `*` for a namespace import. */
|
|
365
|
+
imported: string;
|
|
366
|
+
}
|
|
367
|
+
export declare function importSources(instance: AnyNode): Generator<ImportInfo>;
|
package/dist/analyze.js
CHANGED
|
@@ -150,20 +150,23 @@ function buildModels(input, parseCache) {
|
|
|
150
150
|
}
|
|
151
151
|
return models;
|
|
152
152
|
}
|
|
153
|
-
/**
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
153
|
+
/** The default facts provider: parse (svelte/compiler or rsvelte) and extract. */
|
|
154
|
+
export function jsCrawlFacts(id, code, parseCache, parse) {
|
|
155
|
+
const ast = parseCached(id, code, parseCache, parse);
|
|
156
|
+
const instance = ast.instance;
|
|
157
|
+
if (!instance)
|
|
158
|
+
return null;
|
|
159
|
+
return {
|
|
160
|
+
imports: [...importSources(instance)],
|
|
161
|
+
renderedTags: renderedComponentTagNames(ast),
|
|
162
|
+
memberTags: memberComponentTags(ast),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
export async function buildAnalyzeInput(entries, resolve, readFile, parseCache, parse, escaped = [],
|
|
166
|
+
// The facts source. Defaults to a JS parse + extract; the native engine injects a
|
|
167
|
+
// `ShakeSession`-backed provider (docs M3). Internal — the public crawl is unchanged.
|
|
168
|
+
factsProvider) {
|
|
169
|
+
const getFacts = factsProvider ?? ((id, code) => jsCrawlFacts(id, code, parseCache, parse));
|
|
167
170
|
const entryList = Array.isArray(entries) ? [...entries] : [entries];
|
|
168
171
|
const files = [];
|
|
169
172
|
const edges = [];
|
|
@@ -176,16 +179,15 @@ export async function buildAnalyzeInput(entries, resolve, readFile, parseCache,
|
|
|
176
179
|
const id = queue.shift();
|
|
177
180
|
const code = await readFile(id);
|
|
178
181
|
files.push({ id, code });
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
if (!instance)
|
|
182
|
+
const facts = getFacts(id, code);
|
|
183
|
+
if (!facts)
|
|
182
184
|
continue;
|
|
183
185
|
// The bare component tags this file renders (`<Local …>`). Resolving a barrel
|
|
184
186
|
// (a `.js`/`.ts` re-export) means READING and PARSING the target module to
|
|
185
187
|
// chase the export, so we do it ONLY for named imports actually rendered as a
|
|
186
188
|
// component here — a named import used as a value (a helper / type) can never
|
|
187
189
|
// be a `<Local>` call site, so chasing it is pure waste.
|
|
188
|
-
const renderedTags =
|
|
190
|
+
const renderedTags = facts.renderedTags;
|
|
189
191
|
// Resolve this file's imports into the three attributable edge kinds. Direct
|
|
190
192
|
// default `.svelte` and simple barrel/named imports bind a bare local; a
|
|
191
193
|
// namespace import (`import * as ns`) binds no single component, so it is
|
|
@@ -193,7 +195,7 @@ export async function buildAnalyzeInput(entries, resolve, readFile, parseCache,
|
|
|
193
195
|
const barrelLocals = new Map();
|
|
194
196
|
const namespaceSources = new Map();
|
|
195
197
|
const directChildren = [];
|
|
196
|
-
for (const imp of
|
|
198
|
+
for (const imp of facts.imports) {
|
|
197
199
|
if (imp.imported === '*') {
|
|
198
200
|
namespaceSources.set(imp.local, imp.value);
|
|
199
201
|
continue;
|
|
@@ -223,7 +225,7 @@ export async function buildAnalyzeInput(entries, resolve, readFile, parseCache,
|
|
|
223
225
|
// renders, so the engine attributes `<ns.X .../>` by name lookup.
|
|
224
226
|
const nsChildren = [];
|
|
225
227
|
if (namespaceSources.size > 0) {
|
|
226
|
-
for (const tag of
|
|
228
|
+
for (const tag of facts.memberTags) {
|
|
227
229
|
const dot = tag.indexOf('.');
|
|
228
230
|
const source = namespaceSources.get(tag.slice(0, dot));
|
|
229
231
|
if (source == null)
|
|
@@ -253,7 +255,8 @@ export async function buildAnalyzeInput(entries, resolve, readFile, parseCache,
|
|
|
253
255
|
* `resolve`/`readFile`; the `tests/build-analyze-input-sync` differential test
|
|
254
256
|
* pins it identical to the async path, so keep the two bodies in lockstep.
|
|
255
257
|
*/
|
|
256
|
-
export function buildAnalyzeInputSync(entries, resolve, readFile, parseCache, parse, escaped = []) {
|
|
258
|
+
export function buildAnalyzeInputSync(entries, resolve, readFile, parseCache, parse, escaped = [], factsProvider) {
|
|
259
|
+
const getFacts = factsProvider ?? ((id, code) => jsCrawlFacts(id, code, parseCache, parse));
|
|
257
260
|
const entryList = Array.isArray(entries) ? [...entries] : [entries];
|
|
258
261
|
const files = [];
|
|
259
262
|
const edges = [];
|
|
@@ -264,18 +267,17 @@ export function buildAnalyzeInputSync(entries, resolve, readFile, parseCache, pa
|
|
|
264
267
|
const id = queue.shift();
|
|
265
268
|
const code = readFile(id);
|
|
266
269
|
files.push({ id, code });
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
if (!instance)
|
|
270
|
+
const facts = getFacts(id, code);
|
|
271
|
+
if (!facts)
|
|
270
272
|
continue;
|
|
271
273
|
// See {@link buildAnalyzeInput}: resolve a barrel only for named imports
|
|
272
274
|
// actually rendered as a `<Local>` component here, to avoid reading+parsing
|
|
273
275
|
// modules behind value-only named imports.
|
|
274
|
-
const renderedTags =
|
|
276
|
+
const renderedTags = facts.renderedTags;
|
|
275
277
|
const barrelLocals = new Map();
|
|
276
278
|
const namespaceSources = new Map();
|
|
277
279
|
const directChildren = [];
|
|
278
|
-
for (const imp of
|
|
280
|
+
for (const imp of facts.imports) {
|
|
279
281
|
if (imp.imported === '*') {
|
|
280
282
|
namespaceSources.set(imp.local, imp.value);
|
|
281
283
|
continue;
|
|
@@ -298,7 +300,7 @@ export function buildAnalyzeInputSync(entries, resolve, readFile, parseCache, pa
|
|
|
298
300
|
}
|
|
299
301
|
const nsChildren = [];
|
|
300
302
|
if (namespaceSources.size > 0) {
|
|
301
|
-
for (const tag of
|
|
303
|
+
for (const tag of facts.memberTags) {
|
|
302
304
|
const dot = tag.indexOf('.');
|
|
303
305
|
const source = namespaceSources.get(tag.slice(0, dot));
|
|
304
306
|
if (source == null)
|
|
@@ -1269,7 +1271,7 @@ function collectChildCalls(ast, imports) {
|
|
|
1269
1271
|
* nothing. Skipping it only ever drops a non-call-site, so attribution (and the
|
|
1270
1272
|
* resulting models) are unchanged.
|
|
1271
1273
|
*/
|
|
1272
|
-
function renderedComponentTagNames(ast) {
|
|
1274
|
+
export function renderedComponentTagNames(ast) {
|
|
1273
1275
|
const names = new Set();
|
|
1274
1276
|
walk(ast.fragment, null, {
|
|
1275
1277
|
Component(node, { next }) {
|
|
@@ -1286,7 +1288,7 @@ function renderedComponentTagNames(ast) {
|
|
|
1286
1288
|
* Shell resolves each through its namespace import's barrel; bare `<Child/>` tags
|
|
1287
1289
|
* have no dot and are bound by the plain import maps instead.
|
|
1288
1290
|
*/
|
|
1289
|
-
function memberComponentTags(ast) {
|
|
1291
|
+
export function memberComponentTags(ast) {
|
|
1290
1292
|
const tags = new Set();
|
|
1291
1293
|
walk(ast.fragment, null, {
|
|
1292
1294
|
Component(node, { next }) {
|
|
@@ -1650,7 +1652,7 @@ function literalDefault(expr) {
|
|
|
1650
1652
|
return { known: true, value: undefined };
|
|
1651
1653
|
return { known: false };
|
|
1652
1654
|
}
|
|
1653
|
-
function* importSources(instance) {
|
|
1655
|
+
export function* importSources(instance) {
|
|
1654
1656
|
const program = instance.content;
|
|
1655
1657
|
for (const stmt of program?.body ?? []) {
|
|
1656
1658
|
if (stmt.type !== 'ImportDeclaration')
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ReadFile, type Resolve } from './analyze.js';
|
|
2
2
|
import { type Parse } from './parse.js';
|
|
3
|
-
import { type MonomorphizeOptions, type MonomorphizeResult } from './mono.js';
|
|
3
|
+
import { type MonomorphizeOptions, type MonomorphizeResult, type OwnSize } from './mono.js';
|
|
4
4
|
import type { ComponentId } from './ir.js';
|
|
5
5
|
export type { ComponentId, AnalyzeInput, InputFile, ResolvedEdge, EdgeKind, EditResult, } from './ir.js';
|
|
6
6
|
export type { Resolve, ReadFile, ResolveSync, ReadFileSync } from './analyze.js';
|
|
@@ -9,7 +9,7 @@ export { analyze, analyzeInput, buildAnalyzeInput, buildAnalyzeInputSync, deadSp
|
|
|
9
9
|
export type { UnpassedProp } from './analyze.js';
|
|
10
10
|
export { DevShaker, type DevMode, type DevShakerChange } from './engine.js';
|
|
11
11
|
export { transformAll, transformAllWithMono } from './transform.js';
|
|
12
|
-
export { monomorphize, DEFAULT_MONO_OPTIONS, type MonomorphizeOptions, type MonomorphizeResult, type Variant, type CallSiteBinding, } from './mono.js';
|
|
12
|
+
export { monomorphize, DEFAULT_MONO_OPTIONS, type MonomorphizeOptions, type MonomorphizeResult, type OwnSize, type Variant, type CallSiteBinding, } from './mono.js';
|
|
13
13
|
/**
|
|
14
14
|
* Whole-program shake: crawl the component graph from `entry`, decide what to
|
|
15
15
|
* fold, and return the shaken source for every reachable `.svelte` file.
|
|
@@ -45,4 +45,4 @@ export type VariantSpecifier = (variantId: string) => string;
|
|
|
45
45
|
* specialized and `files` equals the unused-prop fold / constant fold / value-set narrowing output exactly — a strict
|
|
46
46
|
* superset of the default behavior, so existing consumers are unaffected.
|
|
47
47
|
*/
|
|
48
|
-
export declare function svelteShakerWithMono(entries: ComponentId | ComponentId[], resolve: Resolve, readFile: ReadFile, mono?: MonomorphizeOptions, variantSpecifier?: VariantSpecifier, parse?: Parse, escaped?: ComponentId[]): Promise<ShakeResult>;
|
|
48
|
+
export declare function svelteShakerWithMono(entries: ComponentId | ComponentId[], resolve: Resolve, readFile: ReadFile, mono?: MonomorphizeOptions, variantSpecifier?: VariantSpecifier, parse?: Parse, escaped?: ComponentId[], ownSize?: OwnSize): Promise<ShakeResult>;
|
package/dist/index.js
CHANGED
|
@@ -46,7 +46,7 @@ async function analyzeWith(entries, resolve, readFile, parse, escaped = []) {
|
|
|
46
46
|
* specialized and `files` equals the unused-prop fold / constant fold / value-set narrowing output exactly — a strict
|
|
47
47
|
* superset of the default behavior, so existing consumers are unaffected.
|
|
48
48
|
*/
|
|
49
|
-
export async function svelteShakerWithMono(entries, resolve, readFile, mono = DEFAULT_MONO_OPTIONS, variantSpecifier = (id) => id, parse, escaped = []) {
|
|
49
|
+
export async function svelteShakerWithMono(entries, resolve, readFile, mono = DEFAULT_MONO_OPTIONS, variantSpecifier = (id) => id, parse, escaped = [], ownSize) {
|
|
50
50
|
const { models, plans } = await analyzeWith(entries, resolve, readFile, parse, escaped);
|
|
51
51
|
// The cascade may re-run the transform with force-bailed plans, so recompute
|
|
52
52
|
// monomorphization inside it: a bailed component must not be specialized either.
|
|
@@ -58,7 +58,7 @@ export async function svelteShakerWithMono(entries, resolve, readFile, mono = DE
|
|
|
58
58
|
const files = shakeWithRevertCascade(models, plans, (p) => {
|
|
59
59
|
// Thread the shake entries through so the net-win gate can compute module
|
|
60
60
|
// reachability from them (docs §3 monomorphization, §13.2).
|
|
61
|
-
lastResult = monomorphize(models, p, mono, entries);
|
|
61
|
+
lastResult = monomorphize(models, p, mono, entries, ownSize);
|
|
62
62
|
// With no bindings the wired pass and the base pass are identical, so reuse
|
|
63
63
|
// the plain transform to keep the default path byte-for-byte unchanged.
|
|
64
64
|
return lastResult.bindings.length === 0
|
package/dist/mono.d.ts
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { type FileModel } from './analyze.js';
|
|
2
2
|
import { type AnyNode } from './parse.js';
|
|
3
3
|
import { type ComponentId, type ComponentPlan, type Literal } from './ir.js';
|
|
4
|
+
/**
|
|
5
|
+
* The monomorphization net-win gate's per-module compiled-byte size proxy (docs §3):
|
|
6
|
+
* `(id, source) -> byte count | null`, where `null` means the module can't be sized
|
|
7
|
+
* (a compile error) and the gate declines the child rather than bloat.
|
|
8
|
+
*
|
|
9
|
+
* INJECTED by the Shell so the environment-free engine holds no compiler: the Vite
|
|
10
|
+
* plugin passes an rsvelte-backed sizer (`@rsvelte/compiler`'s `compile_client`),
|
|
11
|
+
* and the native engine computes the SAME proxy in-process (`session::own_size`), so
|
|
12
|
+
* all three engines' gates decide byte-for-byte alike (parity is test-gated). The
|
|
13
|
+
* default (`() => null`) sizes nothing, so an environment without a sizer simply
|
|
14
|
+
* specializes nothing — sound, just unoptimized.
|
|
15
|
+
*/
|
|
16
|
+
export type OwnSize = (id: ComponentId, source: string) => number | null;
|
|
4
17
|
/** Tuning knobs for monomorphization (docs §8.1, §13.2). All have sound defaults. */
|
|
5
18
|
export interface MonomorphizeOptions {
|
|
6
19
|
/** Master switch. Default OFF — every existing behavior is unchanged. */
|
|
@@ -79,4 +92,4 @@ export interface MonomorphizeResult {
|
|
|
79
92
|
* inputs and never touches the base transform, so with monomorphization off (or when no child
|
|
80
93
|
* passes the gate) the default whole-program output is byte-for-byte unchanged.
|
|
81
94
|
*/
|
|
82
|
-
export declare function monomorphize(models: Map<ComponentId, FileModel>, plans: Map<ComponentId, ComponentPlan>, options?: MonomorphizeOptions, entries?: ComponentId | ComponentId[]): MonomorphizeResult;
|
|
95
|
+
export declare function monomorphize(models: Map<ComponentId, FileModel>, plans: Map<ComponentId, ComponentPlan>, options?: MonomorphizeOptions, entries?: ComponentId | ComponentId[], measureSize?: OwnSize): MonomorphizeResult;
|
package/dist/mono.js
CHANGED
|
@@ -55,7 +55,6 @@
|
|
|
55
55
|
// — a strict, measured net reduction. When in doubt we keep the base.
|
|
56
56
|
// ----------------------------------------------------------------------
|
|
57
57
|
import MagicString from 'magic-string';
|
|
58
|
-
import { compile } from 'svelte/compiler';
|
|
59
58
|
import { inSpans } from './dead.js';
|
|
60
59
|
import { shakeBody } from './transform.js';
|
|
61
60
|
import { readCallSite, deadSpansForPlans, isFoldBlockedName, } from './analyze.js';
|
|
@@ -79,7 +78,7 @@ export const DEFAULT_MONO_OPTIONS = {
|
|
|
79
78
|
* inputs and never touches the base transform, so with monomorphization off (or when no child
|
|
80
79
|
* passes the gate) the default whole-program output is byte-for-byte unchanged.
|
|
81
80
|
*/
|
|
82
|
-
export function monomorphize(models, plans, options = DEFAULT_MONO_OPTIONS, entries = []) {
|
|
81
|
+
export function monomorphize(models, plans, options = DEFAULT_MONO_OPTIONS, entries = [], measureSize = () => null) {
|
|
83
82
|
const variants = new Map();
|
|
84
83
|
const bindings = [];
|
|
85
84
|
if (!options.enabled)
|
|
@@ -163,23 +162,17 @@ export function monomorphize(models, plans, options = DEFAULT_MONO_OPTIONS, entr
|
|
|
163
162
|
incoming.add(c);
|
|
164
163
|
const entryList = (Array.isArray(entries) ? entries : [entries]).filter((e) => models.has(e));
|
|
165
164
|
const roots = entryList.filter((e) => !incoming.has(e));
|
|
165
|
+
// Memoize the injected size proxy by source string (measuring is the hot cost).
|
|
166
|
+
// `measureSize` is the rsvelte compiled-byte proxy the Shell injects (docs §3);
|
|
167
|
+
// its default (`() => null`) makes every module un-sizable, so the gate below
|
|
168
|
+
// declines every child — an environment with no sizer specializes nothing, which
|
|
169
|
+
// is sound (never bloat), just unoptimized.
|
|
166
170
|
const sizeCache = new Map();
|
|
167
171
|
const ownSize = (id, source) => {
|
|
168
172
|
const cached = sizeCache.get(source);
|
|
169
173
|
if (cached !== undefined)
|
|
170
174
|
return cached;
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
const { js } = compile(source, {
|
|
174
|
-
generate: 'client',
|
|
175
|
-
dev: false,
|
|
176
|
-
filename: id,
|
|
177
|
-
});
|
|
178
|
-
size = js.code.length;
|
|
179
|
-
}
|
|
180
|
-
catch {
|
|
181
|
-
size = null; // un-sizable -> caller skips the child
|
|
182
|
-
}
|
|
175
|
+
const size = measureSize(id, source);
|
|
183
176
|
if (size !== null)
|
|
184
177
|
sizeCache.set(source, size);
|
|
185
178
|
return size;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type ReadFile, type Resolve } from './analyze.js';
|
|
2
|
+
import { type MonomorphizeOptions } from './mono.js';
|
|
3
|
+
import type { ComponentId } from './ir.js';
|
|
4
|
+
/** The long-lived native session: parse + retain ASTs, then shake to edits. The
|
|
5
|
+
* shake needs no size callback — the monomorphization net-win gate's compiled-byte
|
|
6
|
+
* proxy is computed in-process by rsvelte (`session::own_size`), so the native path
|
|
7
|
+
* never calls back into a JS compiler. */
|
|
8
|
+
interface ShakeSession {
|
|
9
|
+
parse: (inputJson: string) => string;
|
|
10
|
+
parseMore: (inputJson: string) => string;
|
|
11
|
+
shake: (configJson: string) => string;
|
|
12
|
+
}
|
|
13
|
+
/** The subset of the napi addon the plugin uses. */
|
|
14
|
+
interface NativeEngine {
|
|
15
|
+
ShakeSession: new () => ShakeSession;
|
|
16
|
+
engineApiVersion: () => number;
|
|
17
|
+
}
|
|
18
|
+
/** Whether a loaded module is a native addon this engine can drive: the right ABI
|
|
19
|
+
* generation AND the `ShakeSession` shape (`parse` + `parseMore` + `shake`). An OLDER
|
|
20
|
+
* published `svelte-shaker-engine-scan-native` — one with no `engineApiVersion`, a
|
|
21
|
+
* different generation, or predating `parseMore` — is REJECTED so the caller degrades
|
|
22
|
+
* to WASM/JS instead of mis-calling an incompatible binary and crashing the build. */
|
|
23
|
+
export declare function hasSessionApi(mod: Partial<NativeEngine>): mod is NativeEngine;
|
|
24
|
+
/**
|
|
25
|
+
* Load the native Rust (napi) engine, or `null` if it can't be loaded (then the caller
|
|
26
|
+
* falls back to the WASM / JS engine). Two locations are tried in order:
|
|
27
|
+
* - the published `svelte-shaker-engine-scan-native` package (an
|
|
28
|
+
* `optionalDependency` — installed only when a prebuilt binary exists for this
|
|
29
|
+
* platform); this is the layout an npm consumer gets.
|
|
30
|
+
* - `../engine-scan-native/index.cjs` — the in-repo package, used from source in
|
|
31
|
+
* this package's own tests / dev (where the npm package isn't a dependency).
|
|
32
|
+
* Either loader resolves the platform `.node`; a missing binary throws (swallowed).
|
|
33
|
+
* A resolved-but-API-incompatible module (an older publish) is rejected so a version
|
|
34
|
+
* skew degrades to a fallback engine, never a crash.
|
|
35
|
+
*/
|
|
36
|
+
export declare function tryLoadNativeEngine(): NativeEngine | null;
|
|
37
|
+
/** The output of a native monomorphization shake: the wired owner files + the variant
|
|
38
|
+
* residuals keyed by their request specifier (what the Shell's `load` hook serves). */
|
|
39
|
+
export interface NativeMonoResult {
|
|
40
|
+
files: Record<ComponentId, string>;
|
|
41
|
+
variants: Map<string, string>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Whole-program shake via the native Rust engine — the counterpart of
|
|
45
|
+
* {@link svelteShakerWithMono} / {@link svelteShakerWasmWithMono}, handling
|
|
46
|
+
* monomorphization too (mono off → an empty variant set).
|
|
47
|
+
*
|
|
48
|
+
* The seed components are parsed ONCE by the session (a batched, parallel rsvelte
|
|
49
|
+
* parse); the crawl then resolves edges reading those facts instead of re-parsing in
|
|
50
|
+
* JS, parsing any file discovered outside the seed on demand (`parseMore`). The
|
|
51
|
+
* session retains every AST, so the shake needs no AST at the boundary — only the
|
|
52
|
+
* resolved graph crosses, and the monomorphization size proxy is computed in-process
|
|
53
|
+
* by rsvelte (`session::own_size`), so nothing calls back into a JS compiler. A final
|
|
54
|
+
* svelte/compiler revert cascade (the AUTHORITY) force-bails any residual unparseable
|
|
55
|
+
* output; the session's own inner rsvelte cascade means valid programs settle in one
|
|
56
|
+
* outer pass.
|
|
57
|
+
*/
|
|
58
|
+
export declare function svelteShakerNativeWithMono(engine: NativeEngine, entries: ComponentId | ComponentId[], resolve: Resolve, readFile: ReadFile, mono: MonomorphizeOptions, escaped?: ComponentId[]): Promise<NativeMonoResult>;
|
|
59
|
+
export {};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { buildAnalyzeInput, } from './analyze.js';
|
|
3
|
+
import {} from './mono.js';
|
|
4
|
+
import { revertCascade } from './revert-cascade.js';
|
|
5
|
+
// NODE-ONLY: loads the native Rust (napi) engine and drives it from the Vite plugin.
|
|
6
|
+
// Imported only by `vite.ts` (a Node entry), never by the environment-free engine, so
|
|
7
|
+
// the browser playground build stays clean. Unlike the WASM engine, this parses with
|
|
8
|
+
// rsvelte IN PROCESS and keeps the ASTs Rust-side (a `ShakeSession`), so no
|
|
9
|
+
// whole-program AST ever crosses the JS boundary — the crawl reads per-file FACTS from
|
|
10
|
+
// the session instead of re-parsing in JS (docs/RUST-MIGRATION.md M3).
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
/** The addon ABI generation this driver speaks (native `engine_api_version`). Bump
|
|
13
|
+
* in lockstep with a breaking change to the exported methods. The 0.2.x addon's
|
|
14
|
+
* `shake` took a JS `ownSize` callback; 0.3.x computes the size proxy in Rust and
|
|
15
|
+
* `shake` takes ONE argument — calling a 0.2.x binary the 0.3.x way throws a napi
|
|
16
|
+
* TypeError that crashes `vite build`, so a version mismatch MUST be rejected here. */
|
|
17
|
+
const EXPECTED_ENGINE_API_VERSION = 3;
|
|
18
|
+
/** Whether a loaded module is a native addon this engine can drive: the right ABI
|
|
19
|
+
* generation AND the `ShakeSession` shape (`parse` + `parseMore` + `shake`). An OLDER
|
|
20
|
+
* published `svelte-shaker-engine-scan-native` — one with no `engineApiVersion`, a
|
|
21
|
+
* different generation, or predating `parseMore` — is REJECTED so the caller degrades
|
|
22
|
+
* to WASM/JS instead of mis-calling an incompatible binary and crashing the build. */
|
|
23
|
+
export function hasSessionApi(mod) {
|
|
24
|
+
const versionFn = mod.engineApiVersion;
|
|
25
|
+
// The 0.2.x addon has no `engineApiVersion` export, so this rejects it — the exact
|
|
26
|
+
// skew that would otherwise throw on the new one-argument `shake` call.
|
|
27
|
+
if (typeof versionFn !== 'function' || versionFn() !== EXPECTED_ENGINE_API_VERSION)
|
|
28
|
+
return false;
|
|
29
|
+
const ctor = mod.ShakeSession;
|
|
30
|
+
const proto = ctor?.prototype;
|
|
31
|
+
return (typeof ctor === 'function' &&
|
|
32
|
+
typeof proto?.parse === 'function' &&
|
|
33
|
+
typeof proto?.parseMore === 'function' &&
|
|
34
|
+
typeof proto?.shake === 'function');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Load the native Rust (napi) engine, or `null` if it can't be loaded (then the caller
|
|
38
|
+
* falls back to the WASM / JS engine). Two locations are tried in order:
|
|
39
|
+
* - the published `svelte-shaker-engine-scan-native` package (an
|
|
40
|
+
* `optionalDependency` — installed only when a prebuilt binary exists for this
|
|
41
|
+
* platform); this is the layout an npm consumer gets.
|
|
42
|
+
* - `../engine-scan-native/index.cjs` — the in-repo package, used from source in
|
|
43
|
+
* this package's own tests / dev (where the npm package isn't a dependency).
|
|
44
|
+
* Either loader resolves the platform `.node`; a missing binary throws (swallowed).
|
|
45
|
+
* A resolved-but-API-incompatible module (an older publish) is rejected so a version
|
|
46
|
+
* skew degrades to a fallback engine, never a crash.
|
|
47
|
+
*/
|
|
48
|
+
export function tryLoadNativeEngine() {
|
|
49
|
+
for (const spec of ['svelte-shaker-engine-scan-native', '../engine-scan-native/index.cjs']) {
|
|
50
|
+
try {
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
52
|
+
const mod = require(spec);
|
|
53
|
+
if (hasSessionApi(mod))
|
|
54
|
+
return { ShakeSession: mod.ShakeSession, engineApiVersion: mod.engineApiVersion };
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Not resolvable here — try the next location.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* One native facts record → the crawl's {@link CrawlFacts}. An unparseable file is
|
|
64
|
+
* treated as contributing nothing (its retained AST is Null, so the shake skips it —
|
|
65
|
+
* sound under-shake), matching the session's own parse-error handling; a file with no
|
|
66
|
+
* instance script simply has empty imports, so it resolves no edges either way.
|
|
67
|
+
*/
|
|
68
|
+
function toCrawlFacts(f) {
|
|
69
|
+
if (f.parseError)
|
|
70
|
+
return null;
|
|
71
|
+
return {
|
|
72
|
+
imports: f.imports.map((i) => ({ value: i.source, local: i.local, imported: i.imported })),
|
|
73
|
+
renderedTags: new Set(f.renderedTags),
|
|
74
|
+
memberTags: new Set(f.memberTags),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Whole-program shake via the native Rust engine — the counterpart of
|
|
79
|
+
* {@link svelteShakerWithMono} / {@link svelteShakerWasmWithMono}, handling
|
|
80
|
+
* monomorphization too (mono off → an empty variant set).
|
|
81
|
+
*
|
|
82
|
+
* The seed components are parsed ONCE by the session (a batched, parallel rsvelte
|
|
83
|
+
* parse); the crawl then resolves edges reading those facts instead of re-parsing in
|
|
84
|
+
* JS, parsing any file discovered outside the seed on demand (`parseMore`). The
|
|
85
|
+
* session retains every AST, so the shake needs no AST at the boundary — only the
|
|
86
|
+
* resolved graph crosses, and the monomorphization size proxy is computed in-process
|
|
87
|
+
* by rsvelte (`session::own_size`), so nothing calls back into a JS compiler. A final
|
|
88
|
+
* svelte/compiler revert cascade (the AUTHORITY) force-bails any residual unparseable
|
|
89
|
+
* output; the session's own inner rsvelte cascade means valid programs settle in one
|
|
90
|
+
* outer pass.
|
|
91
|
+
*/
|
|
92
|
+
export async function svelteShakerNativeWithMono(engine, entries, resolve, readFile, mono, escaped = []) {
|
|
93
|
+
const seedIds = Array.isArray(entries) ? entries : [entries];
|
|
94
|
+
// Batch-read + batch-parse the seed. Cache the source so the crawl's `readFile`
|
|
95
|
+
// does not read the seed a second time.
|
|
96
|
+
const codeCache = new Map();
|
|
97
|
+
const seedFiles = await Promise.all(seedIds.map(async (id) => {
|
|
98
|
+
const code = await readFile(id);
|
|
99
|
+
codeCache.set(id, code);
|
|
100
|
+
return { id, code };
|
|
101
|
+
}));
|
|
102
|
+
const session = new engine.ShakeSession();
|
|
103
|
+
const seedFacts = JSON.parse(session.parse(JSON.stringify({ files: seedFiles })));
|
|
104
|
+
const factsById = new Map();
|
|
105
|
+
for (const f of seedFacts.files)
|
|
106
|
+
factsById.set(f.id, toCrawlFacts(f));
|
|
107
|
+
const cachedRead = async (id) => {
|
|
108
|
+
const hit = codeCache.get(id);
|
|
109
|
+
if (hit !== undefined)
|
|
110
|
+
return hit;
|
|
111
|
+
const code = await readFile(id);
|
|
112
|
+
codeCache.set(id, code);
|
|
113
|
+
return code;
|
|
114
|
+
};
|
|
115
|
+
// Facts source for the crawl: a seed hit, or parse the newly discovered file into the
|
|
116
|
+
// session now (retaining its AST for the shake) and cache its facts.
|
|
117
|
+
const provider = (id, code) => {
|
|
118
|
+
if (factsById.has(id))
|
|
119
|
+
return factsById.get(id);
|
|
120
|
+
const res = JSON.parse(session.parseMore(JSON.stringify({ files: [{ id, code }] })));
|
|
121
|
+
const facts = res.files.length > 0 ? toCrawlFacts(res.files[0]) : null;
|
|
122
|
+
factsById.set(id, facts);
|
|
123
|
+
return facts;
|
|
124
|
+
};
|
|
125
|
+
const input = await buildAnalyzeInput(entries, resolve, cachedRead, undefined, undefined, escaped, provider);
|
|
126
|
+
const config = {
|
|
127
|
+
edges: input.edges,
|
|
128
|
+
entries: input.entries,
|
|
129
|
+
escaped: input.escaped ?? [],
|
|
130
|
+
mono,
|
|
131
|
+
};
|
|
132
|
+
let last;
|
|
133
|
+
const files = revertCascade(input.files, (forceBail) => {
|
|
134
|
+
last = JSON.parse(session.shake(JSON.stringify({ ...config, forceBail: [...forceBail] })));
|
|
135
|
+
return last.files;
|
|
136
|
+
});
|
|
137
|
+
// The engine already keys each variant by its `?shaker_variant=` request specifier
|
|
138
|
+
// (mono.rs `variant_specifier`), the same key the `load` hook serves — so pass it
|
|
139
|
+
// through, exactly as the WASM engine does.
|
|
140
|
+
return { files, variants: new Map(Object.entries(last.variants)) };
|
|
141
|
+
}
|
package/dist/rsvelte-parse.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Parse } from './parse.js';
|
|
2
|
+
import type { OwnSize } from './mono.js';
|
|
2
3
|
/**
|
|
3
4
|
* Build a {@link Parse} backed by rsvelte's parser (`@rsvelte/compiler`, a
|
|
4
5
|
* bundled WASM dependency), or `null` if that module can't be loaded/initialized
|
|
@@ -13,3 +14,12 @@ import type { Parse } from './parse.js';
|
|
|
13
14
|
* a load/init failure returns `null`.
|
|
14
15
|
*/
|
|
15
16
|
export declare function tryLoadRsvelteParser(): Parse | null;
|
|
17
|
+
/**
|
|
18
|
+
* Build the rsvelte-backed {@link OwnSize} for the JS/WASM engines, or `null` if
|
|
19
|
+
* `@rsvelte/compiler` can't be loaded/initialized. The native engine computes the
|
|
20
|
+
* SAME proxy in-process (`session::own_size` over the pinned rsvelte crate), so all
|
|
21
|
+
* three engines' monomorphization gates decide byte-for-byte alike (parity is
|
|
22
|
+
* test-gated). `name` is passed as the component id — its exact string is immaterial
|
|
23
|
+
* to the SIZE as long as every engine passes the same one, which they do.
|
|
24
|
+
*/
|
|
25
|
+
export declare function tryLoadRsvelteOwnSize(): OwnSize | null;
|
package/dist/rsvelte-parse.js
CHANGED
|
@@ -56,3 +56,24 @@ export function tryLoadRsvelteParser() {
|
|
|
56
56
|
return JSON.parse(result.ast);
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Build the rsvelte-backed {@link OwnSize} for the JS/WASM engines, or `null` if
|
|
61
|
+
* `@rsvelte/compiler` can't be loaded/initialized. The native engine computes the
|
|
62
|
+
* SAME proxy in-process (`session::own_size` over the pinned rsvelte crate), so all
|
|
63
|
+
* three engines' monomorphization gates decide byte-for-byte alike (parity is
|
|
64
|
+
* test-gated). `name` is passed as the component id — its exact string is immaterial
|
|
65
|
+
* to the SIZE as long as every engine passes the same one, which they do.
|
|
66
|
+
*/
|
|
67
|
+
export function tryLoadRsvelteOwnSize() {
|
|
68
|
+
let compiler;
|
|
69
|
+
try {
|
|
70
|
+
compiler = loadCompiler();
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return (id, source) => {
|
|
76
|
+
const result = compiler.compile_client(source, id);
|
|
77
|
+
return result.success ? result.js.length : null;
|
|
78
|
+
};
|
|
79
|
+
}
|
package/dist/vite.d.ts
CHANGED
|
@@ -118,18 +118,24 @@ export interface ShakerOptions {
|
|
|
118
118
|
monomorphize?: boolean | Partial<Omit<MonomorphizeOptions, 'enabled'>>;
|
|
119
119
|
/**
|
|
120
120
|
* Which engine runs the whole shake (analysis + transform, including
|
|
121
|
-
* monomorphization). Default `'auto'`.
|
|
122
|
-
*
|
|
123
|
-
* the
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* -
|
|
121
|
+
* monomorphization). Default `'auto'`. There are two Rust engines — the same
|
|
122
|
+
* analysis behind two frontends — plus the JS engine:
|
|
123
|
+
* - the NATIVE (napi) engine parses with rsvelte IN PROCESS and keeps the ASTs
|
|
124
|
+
* Rust-side, so no whole-program AST crosses a boundary. It is the fastest and
|
|
125
|
+
* has no size ceiling, but ships as a per-platform prebuilt binary that may not
|
|
126
|
+
* exist for every install;
|
|
127
|
+
* - the WASM engine is the same Rust engine, but the whole-program AST must cross
|
|
128
|
+
* the JS<->WASM boundary as JSON — tens of MB past a few hundred components — so
|
|
129
|
+
* it only wins for small/medium apps;
|
|
130
|
+
* - the JS engine needs no boundary crossing at all, so it wins for a large app
|
|
131
|
+
* when the native engine isn't available.
|
|
132
|
+
*
|
|
133
|
+
* - `'auto'` — the native engine if a binary loads (no size gate); otherwise the
|
|
134
|
+
* WASM engine for a small/medium app, or the JS engine for a large one.
|
|
135
|
+
* - `'rust'` — force a Rust engine: native if it loads, else WASM; throws if
|
|
136
|
+
* neither can be loaded.
|
|
131
137
|
* - `'js'` — force the JS engine.
|
|
132
|
-
*
|
|
138
|
+
* All three are differentially tested to produce byte-identical output, so the
|
|
133
139
|
* choice only affects speed, never what is shaken.
|
|
134
140
|
*/
|
|
135
141
|
engine?: 'auto' | 'js' | 'rust';
|
|
@@ -146,20 +152,23 @@ export interface ShakerOptions {
|
|
|
146
152
|
*/
|
|
147
153
|
dev?: false | DevMode;
|
|
148
154
|
/**
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* `'
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
+
* How the JS / WASM engines parse `.svelte`. Does NOT apply to the native engine,
|
|
156
|
+
* which always parses with rsvelte IN PROCESS (there is no JS-side parse to pick).
|
|
157
|
+
* Defaults to FOLLOW THE ENGINE: `'rsvelte'` on the WASM engine — its AST crosses
|
|
158
|
+
* the boundary directly — and `'svelte'` (svelte/compiler) on the JS engine, where
|
|
159
|
+
* rsvelte's parse is pure overhead (~2x slower) with no downstream benefit. The
|
|
160
|
+
* JS-side rsvelte parser loads from `@rsvelte/compiler` (a bundled WASM dependency —
|
|
161
|
+
* nothing extra to install, no platform-specific binary).
|
|
155
162
|
*
|
|
156
|
-
* The engine reads only UTF-16 `start`/`end`, never `loc`, so the choice can
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
* (
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
+
* The engine reads only UTF-16 `start`/`end`, never `loc`, so the choice can never
|
|
164
|
+
* affect what renders — it is soundness-neutral, differentially tested to produce
|
|
165
|
+
* byte-identical output either way. `parser: 'svelte'` also forces the native
|
|
166
|
+
* engine OFF (it cannot honor svelte/compiler), so the shake uses a JS/WASM engine
|
|
167
|
+
* that parses with svelte/compiler. When rsvelte IS the resolved JS-side parser (an
|
|
168
|
+
* explicit `parser: 'rsvelte'`, or the WASM engine's default) and `@rsvelte/compiler`
|
|
169
|
+
* can't be loaded, the plugin THROWS rather than silently swapping to svelte/compiler,
|
|
170
|
+
* so the same source can't shake differently on another machine: set
|
|
171
|
+
* `parser: 'svelte'` to opt out.
|
|
163
172
|
*/
|
|
164
173
|
parser?: 'svelte' | 'rsvelte';
|
|
165
174
|
/**
|
package/dist/vite.js
CHANGED
|
@@ -10,8 +10,9 @@ import { compileExclude } from './exclude.js';
|
|
|
10
10
|
// Re-export so a user can extend the default dev-only list: `devOnly: [...DEFAULT_DEV_ONLY, '…']`.
|
|
11
11
|
export { DEFAULT_DEV_ONLY } from './dev-only.js';
|
|
12
12
|
import { DEFAULT_MONO_OPTIONS } from './mono.js';
|
|
13
|
-
import { tryLoadRsvelteParser } from './rsvelte-parse.js';
|
|
13
|
+
import { tryLoadRsvelteParser, tryLoadRsvelteOwnSize } from './rsvelte-parse.js';
|
|
14
14
|
import { svelteShakerWasm, svelteShakerWasmWithMono, tryLoadWasmEngine } from './wasm-engine.js';
|
|
15
|
+
import { svelteShakerNativeWithMono, tryLoadNativeEngine } from './native-engine.js';
|
|
15
16
|
/** kB with two decimals, the unit Vite itself uses in its build size report. */
|
|
16
17
|
function formatKB(bytes) {
|
|
17
18
|
return `${(bytes / 1024).toFixed(2)} kB`;
|
|
@@ -473,35 +474,82 @@ export function shaker(options = {}) {
|
|
|
473
474
|
});
|
|
474
475
|
reportEscapeDiagnostics(escapeScan);
|
|
475
476
|
const escaped = escapeScan.escaped;
|
|
477
|
+
// The monomorphization net-win gate's size proxy, computed by rsvelte's client
|
|
478
|
+
// codegen (`@rsvelte/compiler`), for the JS and WASM engines so their gate decides
|
|
479
|
+
// byte-for-byte like the native engine (which computes the SAME proxy in-process).
|
|
480
|
+
// Loaded lazily by the js/wasm mono branches only — the native path sizes in Rust
|
|
481
|
+
// and never needs it. If `@rsvelte/compiler` can't load, sizing returns null and
|
|
482
|
+
// the gate specializes nothing — sound (never bloat), just unoptimized.
|
|
483
|
+
const loadOwnSize = () => tryLoadRsvelteOwnSize() ?? (() => null);
|
|
476
484
|
// Decide the engine. The native Rust engine now implements every pass
|
|
477
485
|
// INCLUDING monomorphization (it calls back to JS only for the compiled-size
|
|
478
486
|
// proxy), so it
|
|
479
487
|
// is the default: `'auto'` uses it whenever it can be loaded and falls back
|
|
480
488
|
// to JS otherwise, `'rust'` forces it (throwing if it can't load), `'js'`
|
|
481
489
|
// forces the JS engine. Both engines produce byte-identical output.
|
|
490
|
+
// Decide the engine. The native (napi) Rust engine parses with rsvelte IN
|
|
491
|
+
// PROCESS and keeps the ASTs Rust-side, so no whole-program AST crosses a
|
|
492
|
+
// boundary — it is the fastest and has no size ceiling. The WASM engine is the
|
|
493
|
+
// SAME Rust engine but marshals the whole-program AST across the JS<->WASM
|
|
494
|
+
// boundary as JSON (tens of MB past a few hundred components), so `auto` only
|
|
495
|
+
// uses it below the size gate; past it the boundary-free JS engine wins.
|
|
496
|
+
// `'rust'` — native if it loads, else WASM (throws if neither can load).
|
|
497
|
+
// `'auto'` — native if it loads; else WASM below the gate; else JS.
|
|
498
|
+
// `'js'` — the JS engine.
|
|
499
|
+
// All three are differentially tested to emit byte-identical output.
|
|
482
500
|
const engineChoice = options.engine ?? 'auto';
|
|
501
|
+
// `parser: 'svelte'` asks for svelte/compiler, which the native engine (always
|
|
502
|
+
// in-process rsvelte) cannot honor — so it forces the native engine off, and the
|
|
503
|
+
// shake falls back to a WASM/JS engine that parses with the requested parser.
|
|
504
|
+
const nativeAllowed = options.parser !== 'svelte';
|
|
505
|
+
let native = null;
|
|
483
506
|
let wasm = null;
|
|
484
507
|
if (engineChoice === 'rust') {
|
|
485
|
-
|
|
486
|
-
if (!
|
|
487
|
-
|
|
488
|
-
|
|
508
|
+
native = nativeAllowed ? tryLoadNativeEngine() : null;
|
|
509
|
+
if (!native) {
|
|
510
|
+
wasm = tryLoadWasmEngine();
|
|
511
|
+
if (!wasm)
|
|
512
|
+
throw new Error('[vite-plugin-svelte-shaker] engine: "rust" was requested but neither the native ' +
|
|
513
|
+
'nor the WASM Rust engine could be loaded. Remove the option (or use engine: "js") ' +
|
|
514
|
+
'to use the JS engine.');
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
else if (engineChoice === 'auto') {
|
|
518
|
+
native = nativeAllowed ? tryLoadNativeEngine() : null;
|
|
519
|
+
if (!native && entryComponents.length <= RUST_ENGINE_MAX_COMPONENTS) {
|
|
520
|
+
wasm = tryLoadWasmEngine();
|
|
521
|
+
}
|
|
489
522
|
}
|
|
490
|
-
|
|
491
|
-
//
|
|
492
|
-
//
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
|
|
523
|
+
if (native) {
|
|
524
|
+
// Native Rust engine (napi): parses with rsvelte in process, shakes over the
|
|
525
|
+
// retained ASTs, and returns only the edits — byte-identical to the JS engine,
|
|
526
|
+
// monomorphization included (mono off -> an empty variant set). The `parser`
|
|
527
|
+
// option does not apply here (the session always parses in-process rsvelte).
|
|
528
|
+
//
|
|
529
|
+
// Defense in depth: `tryLoadNativeEngine` already rejects an ABI-incompatible
|
|
530
|
+
// binary (`engineApiVersion`), but any OTHER native failure (a napi marshaling
|
|
531
|
+
// error, an unforeseen throw) must NOT crash the build — degrade to the JS
|
|
532
|
+
// engine with a warning. `session.shake` is `catch_unwind`, so a native panic
|
|
533
|
+
// surfaces here as a throw rather than aborting the process.
|
|
534
|
+
try {
|
|
535
|
+
const result = await svelteShakerNativeWithMono(native, entryComponents, resolve, read, mono, escaped);
|
|
536
|
+
shaken = result.files;
|
|
537
|
+
variantSources = result.variants;
|
|
538
|
+
reportSizes(shaken, read, root, options.verbose === true, log);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
catch (err) {
|
|
542
|
+
warn(`the native engine failed at runtime (${err instanceof Error ? err.message : String(err)}); ` +
|
|
543
|
+
`falling back to the JS engine for this build`);
|
|
544
|
+
native = null;
|
|
545
|
+
wasm = null; // fall through to the always-available JS engine below
|
|
546
|
+
}
|
|
499
547
|
}
|
|
500
548
|
if (wasm) {
|
|
501
549
|
// Native Rust engine — byte-identical to the JS engine, including
|
|
502
550
|
// monomorphization.
|
|
503
551
|
if (mono.enabled) {
|
|
504
|
-
const result = await svelteShakerWasmWithMono(wasm, entryComponents, resolve, read, mono, getParse(true), escaped);
|
|
552
|
+
const result = await svelteShakerWasmWithMono(wasm, entryComponents, resolve, read, mono, loadOwnSize(), getParse(true), escaped);
|
|
505
553
|
shaken = result.files;
|
|
506
554
|
variantSources = result.variants;
|
|
507
555
|
}
|
|
@@ -520,7 +568,7 @@ export function shaker(options = {}) {
|
|
|
520
568
|
reportSizes(shaken, read, root, options.verbose === true, log);
|
|
521
569
|
return;
|
|
522
570
|
}
|
|
523
|
-
const result = await svelteShakerWithMono(entryComponents, resolve, read, mono, variantSpecifier, getParse(false), escaped);
|
|
571
|
+
const result = await svelteShakerWithMono(entryComponents, resolve, read, mono, variantSpecifier, getParse(false), escaped, loadOwnSize());
|
|
524
572
|
shaken = result.files;
|
|
525
573
|
variantSources = new Map();
|
|
526
574
|
for (const v of result.mono.variants.values())
|
package/dist/wasm-engine.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ReadFile, type Resolve } from './analyze.js';
|
|
2
2
|
import { type Parse } from './parse.js';
|
|
3
|
-
import { type MonomorphizeOptions } from './mono.js';
|
|
3
|
+
import { type MonomorphizeOptions, type OwnSize } from './mono.js';
|
|
4
4
|
import type { ComponentId } from './ir.js';
|
|
5
5
|
/** The subset of the WASM exports the plugin uses (docs/RUST-MIGRATION.md M5+). */
|
|
6
6
|
interface WasmEngine {
|
|
@@ -48,9 +48,9 @@ export interface WasmMonoResult {
|
|
|
48
48
|
* Whole-program shake WITH monomorphization, run entirely in the native Rust
|
|
49
49
|
* engine — the counterpart of {@link svelteShakerWithMono}. The crawl/resolution
|
|
50
50
|
* stays in JS; the Rust engine does the analysis, the monomorphization graph/gate, and the
|
|
51
|
-
* call-site rewrite, calling back into JS only for
|
|
52
|
-
*
|
|
53
|
-
* byte-identical (pinned by the differential `wasm-mono`
|
|
51
|
+
* call-site rewrite, calling back into JS only for `ownSize` (the injected rsvelte
|
|
52
|
+
* size proxy). The native engine computes the SAME proxy in-process, so the results
|
|
53
|
+
* are byte-identical (pinned by the differential `wasm-mono` / native parity tests).
|
|
54
54
|
*/
|
|
55
|
-
export declare function svelteShakerWasmWithMono(engine: WasmEngine, entries: ComponentId | ComponentId[], resolve: Resolve, readFile: ReadFile, mono: MonomorphizeOptions, parse?: Parse, escaped?: ComponentId[]): Promise<WasmMonoResult>;
|
|
55
|
+
export declare function svelteShakerWasmWithMono(engine: WasmEngine, entries: ComponentId | ComponentId[], resolve: Resolve, readFile: ReadFile, mono: MonomorphizeOptions, ownSize: OwnSize, parse?: Parse, escaped?: ComponentId[]): Promise<WasmMonoResult>;
|
|
56
56
|
export {};
|
package/dist/wasm-engine.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
|
-
import { compile } from 'svelte/compiler';
|
|
3
2
|
import { buildAnalyzeInput } from './analyze.js';
|
|
4
3
|
import { parseCached } from './parse.js';
|
|
5
4
|
import {} from './mono.js';
|
|
@@ -35,16 +34,6 @@ export function tryLoadWasmEngine() {
|
|
|
35
34
|
}
|
|
36
35
|
return null;
|
|
37
36
|
}
|
|
38
|
-
/** The compiled-byte size proxy the monomorphization net-win gate uses — the same call
|
|
39
|
-
* `mono.ts` makes, so the Rust gate decides byte-for-byte like the JS engine. */
|
|
40
|
-
function ownSize(id, source) {
|
|
41
|
-
try {
|
|
42
|
-
return compile(source, { generate: 'client', dev: false, filename: id }).js.code.length;
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
37
|
/**
|
|
49
38
|
* Whole-program shake via the native Rust engine — the unused-prop fold / constant fold / value-set narrowing counterpart of
|
|
50
39
|
* {@link svelteShaker} (monomorphization lives only in the JS engine). The crawl/resolution
|
|
@@ -77,11 +66,11 @@ export async function svelteShakerWasm(engine, entries, resolve, readFile, parse
|
|
|
77
66
|
* Whole-program shake WITH monomorphization, run entirely in the native Rust
|
|
78
67
|
* engine — the counterpart of {@link svelteShakerWithMono}. The crawl/resolution
|
|
79
68
|
* stays in JS; the Rust engine does the analysis, the monomorphization graph/gate, and the
|
|
80
|
-
* call-site rewrite, calling back into JS only for
|
|
81
|
-
*
|
|
82
|
-
* byte-identical (pinned by the differential `wasm-mono`
|
|
69
|
+
* call-site rewrite, calling back into JS only for `ownSize` (the injected rsvelte
|
|
70
|
+
* size proxy). The native engine computes the SAME proxy in-process, so the results
|
|
71
|
+
* are byte-identical (pinned by the differential `wasm-mono` / native parity tests).
|
|
83
72
|
*/
|
|
84
|
-
export async function svelteShakerWasmWithMono(engine, entries, resolve, readFile, mono, parse, escaped = []) {
|
|
73
|
+
export async function svelteShakerWasmWithMono(engine, entries, resolve, readFile, mono, ownSize, parse, escaped = []) {
|
|
85
74
|
const cache = new Map();
|
|
86
75
|
const input = await buildAnalyzeInput(entries, resolve, readFile, cache, parse, escaped);
|
|
87
76
|
const programInput = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-shaker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Tree shaking for Svelte components",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dead-code-elimination",
|
|
@@ -63,6 +63,9 @@
|
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"svelte": "^5.56.6"
|
|
65
65
|
},
|
|
66
|
+
"optionalDependencies": {
|
|
67
|
+
"svelte-shaker-engine-scan-native": "~0.3.0"
|
|
68
|
+
},
|
|
66
69
|
"engines": {
|
|
67
70
|
"node": ">=22.23.1"
|
|
68
71
|
},
|