svelte-shaker 0.5.2 → 0.6.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/dist/{types/src/analyze.d.ts → analyze.d.ts} +3 -3
- package/dist/analyze.js +1072 -0
- package/dist/{types/src/css.d.ts → css.d.ts} +2 -2
- package/dist/css.js +256 -0
- package/dist/{types/src/dead.d.ts → dead.d.ts} +2 -2
- package/dist/dead.js +195 -0
- package/dist/{types/src/engine.d.ts → engine.d.ts} +3 -3
- package/dist/engine.js +109 -0
- package/dist/{types/src/eval.d.ts → eval.d.ts} +2 -2
- package/dist/eval.js +222 -0
- package/dist/{types/src/index.d.ts → index.d.ts} +11 -11
- package/dist/index.js +86 -1
- package/dist/ir.js +15 -0
- package/dist/{types/src/mono.d.ts → mono.d.ts} +3 -3
- package/dist/mono.js +451 -0
- package/dist/parse.js +19 -0
- package/dist/{types/src/rsvelte-parse.d.ts → rsvelte-parse.d.ts} +1 -1
- package/dist/rsvelte-parse.js +31 -0
- package/dist/{types/src/scan.d.ts → scan.d.ts} +2 -2
- package/dist/scan.js +40 -1
- package/dist/{types/src/transform.d.ts → transform.d.ts} +4 -4
- package/dist/transform.js +825 -0
- package/dist/{types/src/vite.d.ts → vite.d.ts} +10 -2
- package/dist/vite.js +289 -1
- package/package.json +9 -18
- package/dist/index.cjs +0 -1
- /package/dist/{types/src/ir.d.ts → ir.d.ts} +0 -0
- /package/dist/{types/src/parse.d.ts → parse.d.ts} +0 -0
package/dist/mono.js
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
// ----------------------------------------------------------------------
|
|
2
|
+
// L2 per-call-site monomorphization (docs/ARCHITECTURE.md §3 "L2", §11, §13.2).
|
|
3
|
+
//
|
|
4
|
+
// OPT-IN, BAIL-SAFE, and — the property this module guarantees — NEVER BLOATING.
|
|
5
|
+
// Where L1 folds a prop only when it is the SAME constant across the whole app,
|
|
6
|
+
// and L1.5 narrows a multi-valued prop without folding it, L2 specializes a call
|
|
7
|
+
// site: `<Btn variant="primary"/>` could get a private copy of `Btn` in which
|
|
8
|
+
// `variant` is the constant `'primary'`, so every non-primary branch and CSS rule
|
|
9
|
+
// folds away — even though `variant` is app-wide multi-valued and therefore NOT
|
|
10
|
+
// foldable by L1/L1.5.
|
|
11
|
+
//
|
|
12
|
+
// THE KEY INSIGHT (docs §3 L2, §11) — why we do NOT specialize everything:
|
|
13
|
+
// L1.5 already removes every arm that is dead APP-WIDE. So splitting a
|
|
14
|
+
// component into per-shape copies only ever SHRINKS the bundle when the
|
|
15
|
+
// specialization makes a whole MODULE become globally unreferenced — which
|
|
16
|
+
// happens for CORRELATED multi-prop conditions that L1.5's independent
|
|
17
|
+
// per-prop narrowing cannot kill. Canonical example:
|
|
18
|
+
// Child: {#if a === 1 && b === 1}<Heavy/>{/if}<p>base</p>
|
|
19
|
+
// app-wide a∈{0,1}, b∈{0,1}, sites are only <Child a={0} b={1}/> and
|
|
20
|
+
// <Child a={1} b={0}/> — never (1,1).
|
|
21
|
+
// L1.5 keeps <Heavy/> (it narrows a and b independently and cannot prove
|
|
22
|
+
// `a && b` is never both 1), so Heavy stays in the bundle. L2 specializes each
|
|
23
|
+
// site (a or b becomes a constant) -> in BOTH variants `{#if a===1&&b===1}`
|
|
24
|
+
// folds false -> <Heavy/> is gone from every variant -> Heavy is globally
|
|
25
|
+
// unreferenced -> the bundler drops Heavy entirely. THAT is the win.
|
|
26
|
+
// Conversely a plain `variant∈{a,b}` with inline arms and no module elimination
|
|
27
|
+
// MUST NOT be specialized: per-shape copies just duplicate scaffolding and GROW
|
|
28
|
+
// the bundle.
|
|
29
|
+
//
|
|
30
|
+
// SOUNDNESS (the whole contract — docs §13.2):
|
|
31
|
+
// * We only specialize a LIVE call site (never one inside a dead `{#if}` span
|
|
32
|
+
// — same predicate the fixpoint uses).
|
|
33
|
+
// * We only fold a prop at a site when its value is a literal no spread can
|
|
34
|
+
// override (`afterLastSpread && !dynamic`) — the partial-bail rule (docs §4.1).
|
|
35
|
+
// * We never specialize a BAILED component (escape / barrel / accessors), a
|
|
36
|
+
// prop shadowed by a binding, a `{@debug}` prop, or a prop already folded by
|
|
37
|
+
// L1. The residual is produced by the SAME audited body pipeline as
|
|
38
|
+
// L0/L1/L1.5 ({@link shakeBody}); L2 only chooses a richer fold environment.
|
|
39
|
+
//
|
|
40
|
+
// NEVER-BLOAT, MEASURED NET-WIN GATE (docs §3 L2, §13.2 — replaces the old
|
|
41
|
+
// "specialize any folding site" behaviour):
|
|
42
|
+
// 1. ALL-SITES-OR-NOTHING per child C: we only consider specializing C if
|
|
43
|
+
// EVERY live call site of C across the whole program maps to a NON-base
|
|
44
|
+
// residual (a real variant). If any live site would keep C's base, we do
|
|
45
|
+
// NOT specialize C — otherwise the base module stays referenced AND variants
|
|
46
|
+
// are added, which is pure bloat. All-sites means C's base becomes globally
|
|
47
|
+
// unreferenced, so the bundler can drop it.
|
|
48
|
+
// 2. We build the whole-program LIVE RENDER graph (component -> components it
|
|
49
|
+
// renders) and measure, with a per-module byte proxy ({@link ownSize}:
|
|
50
|
+
// compiled client JS length), the total module bytes reachable from the
|
|
51
|
+
// shake entries in two scenarios: BASE (C unspecialized) and SPEC (C's sites
|
|
52
|
+
// render the variants, C.base removed, each variant renders its own live
|
|
53
|
+
// children). We specialize C IFF Sigma_spec < Sigma_base * (1 - minSavings)
|
|
54
|
+
// — a strict, measured net reduction. When in doubt we keep the base.
|
|
55
|
+
// ----------------------------------------------------------------------
|
|
56
|
+
import MagicString from 'magic-string';
|
|
57
|
+
import { compile } from 'svelte/compiler';
|
|
58
|
+
import { inSpans } from './dead.js';
|
|
59
|
+
import { shakeBody } from './transform.js';
|
|
60
|
+
import { readCallSite, deadSpansForPlans, isFoldBlockedName, } from './analyze.js';
|
|
61
|
+
import { parseSvelte, walk } from './parse.js';
|
|
62
|
+
export const DEFAULT_MONO_OPTIONS = {
|
|
63
|
+
enabled: false,
|
|
64
|
+
maxVariants: 8,
|
|
65
|
+
minSavings: 0,
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Compute per-call-site specialized residuals and the dedup'd variant set under
|
|
69
|
+
* a MEASURED, never-bloat net-win gate (docs §3 L2, §13.2).
|
|
70
|
+
*
|
|
71
|
+
* Pure over `models`/`plans`: it reads the already-computed plans, finds the
|
|
72
|
+
* live call sites whose extra literal props fold, groups them per child, and —
|
|
73
|
+
* for each child — only specializes when (a) EVERY live site of that child gets
|
|
74
|
+
* a non-base residual (all-sites-or-nothing, so the base module becomes
|
|
75
|
+
* unreferenced) and (b) replacing the child with its variants strictly shrinks
|
|
76
|
+
* the whole-program module bytes reachable from `entries`. It never mutates the
|
|
77
|
+
* inputs and never touches the base transform, so with L2 off (or when no child
|
|
78
|
+
* passes the gate) the default whole-program output is byte-for-byte unchanged.
|
|
79
|
+
*/
|
|
80
|
+
export function monomorphize(models, plans, options = DEFAULT_MONO_OPTIONS, entries = []) {
|
|
81
|
+
const variants = new Map();
|
|
82
|
+
const bindings = [];
|
|
83
|
+
if (!options.enabled)
|
|
84
|
+
return { variants, bindings };
|
|
85
|
+
// Exclude call sites inside a dead `{#if}` span — the SAME predicate the
|
|
86
|
+
// fixpoint and transform use, so we never specialize a site that vanishes.
|
|
87
|
+
const deadSpans = deadSpansForPlans(models, plans);
|
|
88
|
+
// (1) Gather, per child, EVERY live `<Child .../>` site and whether it folds
|
|
89
|
+
// anything extra. A child is only a specialization candidate if every one of
|
|
90
|
+
// its live sites folds a non-base residual (all-sites-or-nothing).
|
|
91
|
+
const liveSitesByChild = new Map();
|
|
92
|
+
const ineligible = new Set(); // a live site that cannot specialize
|
|
93
|
+
for (const owner of models.values()) {
|
|
94
|
+
const dead = deadSpans.get(owner.id) ?? [];
|
|
95
|
+
for (const call of owner.childCalls) {
|
|
96
|
+
if (dead.length > 0 && inSpans(call.node, dead))
|
|
97
|
+
continue; // dead site
|
|
98
|
+
const child = models.get(call.childId);
|
|
99
|
+
const childPlan = plans.get(call.childId);
|
|
100
|
+
if (!child || !childPlan)
|
|
101
|
+
continue;
|
|
102
|
+
// Never specialize a fully-bailed child (escape/barrel/accessors): its
|
|
103
|
+
// prop profile is unobservable, so a "specialized" copy could be wrong.
|
|
104
|
+
if (childPlan.bail || !child.props || child.props.length === 0) {
|
|
105
|
+
ineligible.add(call.childId);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const shape = specializableShape(call.node, child, childPlan);
|
|
109
|
+
if (shape.size === 0) {
|
|
110
|
+
// This live site keeps the base — so we can never make the base module
|
|
111
|
+
// unreferenced. Disqualify the whole child (all-sites-or-nothing).
|
|
112
|
+
ineligible.add(call.childId);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const code = renderResidual(child, childPlan, shape);
|
|
116
|
+
if (code === baseResidual(child, childPlan)) {
|
|
117
|
+
// The extra literals fold nothing the base did not -> base residual ->
|
|
118
|
+
// this site keeps the base. Disqualify the child.
|
|
119
|
+
ineligible.add(call.childId);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const list = liveSitesByChild.get(call.childId);
|
|
123
|
+
if (list)
|
|
124
|
+
list.push({ owner: owner.id, node: call.node, shape, code });
|
|
125
|
+
else
|
|
126
|
+
liveSitesByChild.set(call.childId, [{ owner: owner.id, node: call.node, shape, code }]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// (2) Build the whole-program base render graph + the base module sizes, and
|
|
130
|
+
// the set of components reachable from the shake entries. `ownSize` is
|
|
131
|
+
// memoized (compile is the hot cost) and a compile error makes a component
|
|
132
|
+
// non-specializable (we treat it as un-sizable -> skip any child involved).
|
|
133
|
+
const baseSource = baseSourceMap(models, plans);
|
|
134
|
+
const baseChildrenOf = new Map();
|
|
135
|
+
for (const model of models.values())
|
|
136
|
+
baseChildrenOf.set(model.id, liveChildIds(baseSource.get(model.id), model));
|
|
137
|
+
// Reachability roots = the shake entries (docs §3 L2, §13.2), narrowed to the
|
|
138
|
+
// TRUE import-graph roots. The Shell seeds the crawl with EVERY `.svelte` file
|
|
139
|
+
// (so it can attribute every call site), which would make every module its own
|
|
140
|
+
// root and defeat reachability — so we drop any entry that is itself rendered
|
|
141
|
+
// by another component. What remains are the real app entry points, and a
|
|
142
|
+
// module reachable only through a folded-away edge becomes orphan-able.
|
|
143
|
+
const incoming = new Set();
|
|
144
|
+
for (const children of baseChildrenOf.values())
|
|
145
|
+
for (const c of children)
|
|
146
|
+
incoming.add(c);
|
|
147
|
+
const entryList = (Array.isArray(entries) ? entries : [entries]).filter((e) => models.has(e));
|
|
148
|
+
const roots = entryList.filter((e) => !incoming.has(e));
|
|
149
|
+
const sizeCache = new Map();
|
|
150
|
+
const ownSize = (id, source) => {
|
|
151
|
+
const cached = sizeCache.get(source);
|
|
152
|
+
if (cached !== undefined)
|
|
153
|
+
return cached;
|
|
154
|
+
let size;
|
|
155
|
+
try {
|
|
156
|
+
const { js } = compile(source, {
|
|
157
|
+
generate: 'client',
|
|
158
|
+
dev: false,
|
|
159
|
+
filename: id,
|
|
160
|
+
});
|
|
161
|
+
size = js.code.length;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
size = null; // un-sizable -> caller skips the child
|
|
165
|
+
}
|
|
166
|
+
if (size !== null)
|
|
167
|
+
sizeCache.set(source, size);
|
|
168
|
+
return size;
|
|
169
|
+
};
|
|
170
|
+
// The children that are still in the running after the all-sites filter. A
|
|
171
|
+
// child whose live-site OWNER is itself such a candidate must NOT be
|
|
172
|
+
// specialized: when the owner is specialized, its live `<Child .../>` site
|
|
173
|
+
// moves into the owner's variant residual (un-rewritten -> renders the BASE
|
|
174
|
+
// child), so the base child stays referenced. Specializing it anyway would
|
|
175
|
+
// emit its variants AND keep its base = bloat. Declining nested
|
|
176
|
+
// specialization is the conservative, never-bloat choice (candidate
|
|
177
|
+
// interactions are a documented followup, docs §3 L2 / §13.2); the owner's own
|
|
178
|
+
// net-win already accounts for rendering the base child.
|
|
179
|
+
const candidateChildren = new Set();
|
|
180
|
+
for (const childId of liveSitesByChild.keys())
|
|
181
|
+
if (!ineligible.has(childId))
|
|
182
|
+
candidateChildren.add(childId);
|
|
183
|
+
// (3) Decide each candidate child independently against the base scenario
|
|
184
|
+
// (docs §13.2: candidate interactions are a followup; independence is sound
|
|
185
|
+
// because every decision is measured against the SAME base and only applied on
|
|
186
|
+
// a strict net reduction, so the union can never bloat past base).
|
|
187
|
+
for (const [childId, sites] of liveSitesByChild) {
|
|
188
|
+
if (ineligible.has(childId))
|
|
189
|
+
continue; // some live site keeps the base
|
|
190
|
+
// A live-site owner is itself specializable -> declining avoids base+variant
|
|
191
|
+
// bloat of THIS child (see `candidateChildren`).
|
|
192
|
+
if (sites.some((s) => s.owner !== childId && candidateChildren.has(s.owner)))
|
|
193
|
+
continue;
|
|
194
|
+
// Dedup the candidate sites' residuals into the distinct variant set.
|
|
195
|
+
const residualToVariant = new Map();
|
|
196
|
+
const variantSources = [];
|
|
197
|
+
let overCap = false;
|
|
198
|
+
for (const site of sites) {
|
|
199
|
+
if (residualToVariant.has(site.code))
|
|
200
|
+
continue;
|
|
201
|
+
if (variantSources.length >= options.maxVariants) {
|
|
202
|
+
overCap = true; // can't give every distinct shape a variant -> bail child
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
const vid = `${childId}::v${variantSources.length}`;
|
|
206
|
+
residualToVariant.set(site.code, vid);
|
|
207
|
+
variantSources.push({ id: vid, code: site.code });
|
|
208
|
+
}
|
|
209
|
+
if (overCap)
|
|
210
|
+
continue; // exceeding the cap means we cannot specialize all-sites
|
|
211
|
+
// Measure: does replacing the base child with its variants strictly shrink
|
|
212
|
+
// the whole-program reachable module bytes?
|
|
213
|
+
if (!netWin(childId, variantSources, models, baseSource, baseChildrenOf, roots, ownSize, options.minSavings))
|
|
214
|
+
continue;
|
|
215
|
+
// The gate passed: emit the variants and bind every live site.
|
|
216
|
+
for (const v of variantSources) {
|
|
217
|
+
const site = sites.find((s) => s.code === v.code);
|
|
218
|
+
variants.set(v.id, {
|
|
219
|
+
id: v.id,
|
|
220
|
+
childId,
|
|
221
|
+
code: v.code,
|
|
222
|
+
foldedProps: site.shape,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
for (const site of sites) {
|
|
226
|
+
bindings.push({
|
|
227
|
+
owner: site.owner,
|
|
228
|
+
childId,
|
|
229
|
+
node: site.node,
|
|
230
|
+
variantId: residualToVariant.get(site.code),
|
|
231
|
+
foldedProps: site.shape,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return { variants, bindings };
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* The measured net-win gate (docs §3 L2, §13.2). Returns true iff replacing the
|
|
239
|
+
* base child `childId` with its `variantSources` strictly shrinks the total
|
|
240
|
+
* module bytes reachable from `roots`:
|
|
241
|
+
*
|
|
242
|
+
* Sigma_base = sum over the BASE-reachable component set of ownSize(base).
|
|
243
|
+
* Sigma_spec = same reachability but with the child expanded into its
|
|
244
|
+
* variants: the child's incoming edges go to the variants, the
|
|
245
|
+
* child's base module is gone, and each variant renders its OWN
|
|
246
|
+
* live children. ownSize(variant) for variants, base size for
|
|
247
|
+
* everything else.
|
|
248
|
+
*
|
|
249
|
+
* Specialize IFF Sigma_spec < Sigma_base * (1 - minSavings). Any compile error
|
|
250
|
+
* (un-sizable module) makes us decline — never bloat.
|
|
251
|
+
*/
|
|
252
|
+
function netWin(childId, variantSources, models, baseSource, baseChildrenOf, roots, ownSize, minSavings) {
|
|
253
|
+
// The variants' OWN live children, parsed from each variant residual via the
|
|
254
|
+
// child's import map (variants never add imports — they only fold/remove).
|
|
255
|
+
const childModel = models.get(childId);
|
|
256
|
+
const variantChildren = new Map();
|
|
257
|
+
for (const v of variantSources)
|
|
258
|
+
variantChildren.set(v.id, liveChildIds(v.code, childModel));
|
|
259
|
+
// --- Sigma_base: reachable set in the BASE scenario, sized by base residual.
|
|
260
|
+
const baseReached = new Set();
|
|
261
|
+
const stackB = [...roots];
|
|
262
|
+
while (stackB.length > 0) {
|
|
263
|
+
const id = stackB.pop();
|
|
264
|
+
if (baseReached.has(id))
|
|
265
|
+
continue;
|
|
266
|
+
baseReached.add(id);
|
|
267
|
+
for (const c of baseChildrenOf.get(id) ?? [])
|
|
268
|
+
stackB.push(c);
|
|
269
|
+
}
|
|
270
|
+
let sigmaBase = 0;
|
|
271
|
+
for (const id of baseReached) {
|
|
272
|
+
const size = ownSize(id, baseSource.get(id));
|
|
273
|
+
if (size === null)
|
|
274
|
+
return false; // un-sizable -> decline
|
|
275
|
+
sigmaBase += size;
|
|
276
|
+
}
|
|
277
|
+
// --- Sigma_spec: reachability with the child expanded into its variants.
|
|
278
|
+
// * reached non-variant components are sized by their base residual,
|
|
279
|
+
// * each reached variant by its own residual,
|
|
280
|
+
// * the child's base module is NOT a node (its edges redirect to variants).
|
|
281
|
+
// A worklist over a tagged node: either a real component id or a variant id.
|
|
282
|
+
const allVariantIds = variantSources.map((v) => v.id);
|
|
283
|
+
const reachedComponents = new Set();
|
|
284
|
+
const reachedVariants = new Set();
|
|
285
|
+
// Redirect any edge into the child to ALL its variants (all-sites means every
|
|
286
|
+
// live caller now renders a variant; for a sound upper bound we keep them all).
|
|
287
|
+
const expand = (id) => id === childId ? { comps: [], vars: allVariantIds } : { comps: [id], vars: [] };
|
|
288
|
+
const compStack = [];
|
|
289
|
+
const varStack = [];
|
|
290
|
+
for (const r of roots) {
|
|
291
|
+
const e = expand(r);
|
|
292
|
+
compStack.push(...e.comps);
|
|
293
|
+
varStack.push(...e.vars);
|
|
294
|
+
}
|
|
295
|
+
while (compStack.length > 0 || varStack.length > 0) {
|
|
296
|
+
if (compStack.length > 0) {
|
|
297
|
+
const id = compStack.pop();
|
|
298
|
+
if (reachedComponents.has(id))
|
|
299
|
+
continue;
|
|
300
|
+
reachedComponents.add(id);
|
|
301
|
+
for (const c of baseChildrenOf.get(id) ?? []) {
|
|
302
|
+
const e = expand(c);
|
|
303
|
+
compStack.push(...e.comps);
|
|
304
|
+
varStack.push(...e.vars);
|
|
305
|
+
}
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const vid = varStack.pop();
|
|
309
|
+
if (reachedVariants.has(vid))
|
|
310
|
+
continue;
|
|
311
|
+
reachedVariants.add(vid);
|
|
312
|
+
for (const c of variantChildren.get(vid) ?? []) {
|
|
313
|
+
const e = expand(c);
|
|
314
|
+
compStack.push(...e.comps);
|
|
315
|
+
varStack.push(...e.vars);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
let sigmaSpec = 0;
|
|
319
|
+
for (const id of reachedComponents) {
|
|
320
|
+
const size = ownSize(id, baseSource.get(id));
|
|
321
|
+
if (size === null)
|
|
322
|
+
return false;
|
|
323
|
+
sigmaSpec += size;
|
|
324
|
+
}
|
|
325
|
+
for (const vid of reachedVariants) {
|
|
326
|
+
const src = variantSources.find((v) => v.id === vid).code;
|
|
327
|
+
const size = ownSize(vid, src);
|
|
328
|
+
if (size === null)
|
|
329
|
+
return false;
|
|
330
|
+
sigmaSpec += size;
|
|
331
|
+
}
|
|
332
|
+
return sigmaSpec < sigmaBase * (1 - minSavings);
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* The live child component ids a `.svelte` SOURCE renders: parse it and resolve
|
|
336
|
+
* every `<Child .../>` tag through `model.imports` (local name -> child id).
|
|
337
|
+
* Folding removes dead `<Child/>` tags from the source, so parsing the RESIDUAL
|
|
338
|
+
* (not the original) yields exactly the edges that survive — which is what makes
|
|
339
|
+
* a correlated condition able to orphan a whole module. A parse error yields no
|
|
340
|
+
* edges (sound: it only ever shrinks the reachable set, never grows it; and a
|
|
341
|
+
* residual that fails to parse would also fail to compile in {@link ownSize},
|
|
342
|
+
* declining the child).
|
|
343
|
+
*/
|
|
344
|
+
function liveChildIds(source, model) {
|
|
345
|
+
let ast;
|
|
346
|
+
try {
|
|
347
|
+
ast = parseSvelte(source, model.id);
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return [];
|
|
351
|
+
}
|
|
352
|
+
const ids = [];
|
|
353
|
+
walk(ast.fragment, null, {
|
|
354
|
+
Component(node, { next }) {
|
|
355
|
+
const childId = node.name ? model.imports.get(node.name) : undefined;
|
|
356
|
+
if (childId)
|
|
357
|
+
ids.push(childId);
|
|
358
|
+
next();
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
return ids;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* The set of EXTRA props this call site freezes to a literal — props that:
|
|
365
|
+
* - are declared by the child (`...rest` only holds undeclared props, so a
|
|
366
|
+
* declared prop is always safe to fold — same as L0/L1),
|
|
367
|
+
* - are passed a literal at this site that no spread can override
|
|
368
|
+
* (`afterLastSpread && !dynamic`) — the partial-bail rule (docs §4.1),
|
|
369
|
+
* - are NOT already folded by L1 (`constFold`) — those carry no extra info,
|
|
370
|
+
* - are NOT shadowed by a template/instance binding or used in `{@debug}` —
|
|
371
|
+
* the analysis already refuses to fold those, and so must we.
|
|
372
|
+
*
|
|
373
|
+
* Returns name -> literal value. Empty means this site cannot be specialized
|
|
374
|
+
* beyond what the base component already does.
|
|
375
|
+
*/
|
|
376
|
+
function specializableShape(node, child, plan) {
|
|
377
|
+
const site = readCallSite(node);
|
|
378
|
+
const declared = new Map();
|
|
379
|
+
for (const d of child.props ?? [])
|
|
380
|
+
declared.set(d.name, d);
|
|
381
|
+
const shape = new Map();
|
|
382
|
+
for (const [name, explicit] of site.explicit) {
|
|
383
|
+
const decl = declared.get(name);
|
|
384
|
+
if (!decl)
|
|
385
|
+
continue; // undeclared -> flows to `...rest`, skip
|
|
386
|
+
if (plan.constFold.has(name))
|
|
387
|
+
continue; // already an app-wide L1 constant
|
|
388
|
+
// A nested-pattern entry (`null` local) is unfoldable, and a prop whose LOCAL
|
|
389
|
+
// binding is shadowed / used in `{@debug}` must not fold — both exactly as L1.
|
|
390
|
+
if (decl.local === null || isFoldBlockedName(child, decl.local))
|
|
391
|
+
continue;
|
|
392
|
+
// The value must be a literal this site genuinely passes and no spread can
|
|
393
|
+
// override — exactly the analysis's "safely explicit" condition.
|
|
394
|
+
if (explicit.dynamic || !explicit.afterLastSpread)
|
|
395
|
+
continue;
|
|
396
|
+
shape.set(name, explicit.value);
|
|
397
|
+
}
|
|
398
|
+
return shape;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Render a component's residual for an augmented fold environment. Reuses the
|
|
402
|
+
* exact L0/L1/L1.5 pipeline: `env` = the child's app-wide L1 constants PLUS this
|
|
403
|
+
* site's extra literals; `setEnv` = the child's narrow sets MINUS any prop now
|
|
404
|
+
* frozen by `env` (a frozen prop is a constant, no longer a set).
|
|
405
|
+
*/
|
|
406
|
+
function renderResidual(child, plan, extra) {
|
|
407
|
+
const env = new Map(plan.constFold);
|
|
408
|
+
for (const [name, value] of extra)
|
|
409
|
+
env.set(name, value);
|
|
410
|
+
const setEnv = new Map();
|
|
411
|
+
for (const [name, set] of plan.narrow)
|
|
412
|
+
if (!env.has(name))
|
|
413
|
+
setEnv.set(name, set);
|
|
414
|
+
const s = new MagicString(child.code);
|
|
415
|
+
shakeBody(child, env, setEnv, plan, s);
|
|
416
|
+
return s.toString();
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* The child's BASE residual (what the whole-program transform already emits for
|
|
420
|
+
* it under L1/L1.5 alone). Used to detect a no-op specialization: if the extra
|
|
421
|
+
* literals fold nothing the base did not, the residual equals this and the site
|
|
422
|
+
* keeps the base component (no pointless variant). Memoized per child.
|
|
423
|
+
*/
|
|
424
|
+
const baseCache = new WeakMap();
|
|
425
|
+
function baseResidual(child, plan) {
|
|
426
|
+
const cached = baseCache.get(child);
|
|
427
|
+
if (cached !== undefined)
|
|
428
|
+
return cached;
|
|
429
|
+
const s = new MagicString(child.code);
|
|
430
|
+
shakeBody(child, plan.constFold, plan.narrow, plan, s);
|
|
431
|
+
const code = s.toString();
|
|
432
|
+
baseCache.set(child, code);
|
|
433
|
+
return code;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* The base residual SOURCE per component — the same body shake the whole-program
|
|
437
|
+
* transform emits ({@link baseResidual}), keyed by component id. This is the
|
|
438
|
+
* per-module source the net-win gate sizes and the source whose `<Child/>` tags
|
|
439
|
+
* define the base render graph. (It omits the owner's call-site attribute
|
|
440
|
+
* stripping, which never changes the child's own rendered children or the
|
|
441
|
+
* component's compiled size in a way that affects the comparison — both
|
|
442
|
+
* scenarios share it.)
|
|
443
|
+
*/
|
|
444
|
+
function baseSourceMap(models, plans) {
|
|
445
|
+
const out = new Map();
|
|
446
|
+
for (const model of models.values()) {
|
|
447
|
+
const plan = plans.get(model.id);
|
|
448
|
+
out.set(model.id, plan.bail ? model.code : baseResidual(model, plan));
|
|
449
|
+
}
|
|
450
|
+
return out;
|
|
451
|
+
}
|
package/dist/parse.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { parse } from 'svelte/compiler';
|
|
2
|
+
import { walk as zfWalk } from 'zimmerframe';
|
|
3
|
+
export function parseSvelte(code, filename) {
|
|
4
|
+
return parse(code, { modern: true, filename });
|
|
5
|
+
}
|
|
6
|
+
export function parseCached(filename, code, cache, parse = parseSvelte) {
|
|
7
|
+
if (!cache)
|
|
8
|
+
return parse(code, filename);
|
|
9
|
+
const hit = cache.get(filename);
|
|
10
|
+
if (hit && hit.code === code)
|
|
11
|
+
return hit.ast;
|
|
12
|
+
const ast = parse(code, filename);
|
|
13
|
+
cache.set(filename, { code, ast });
|
|
14
|
+
return ast;
|
|
15
|
+
}
|
|
16
|
+
/** `zimmerframe.walk`, narrowed to our loose node type. */
|
|
17
|
+
export function walk(root, state, visitors) {
|
|
18
|
+
zfWalk(root, state, visitors);
|
|
19
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
// NODE-ONLY: dynamically loads the OPTIONAL native rsvelte parser. Imported only
|
|
3
|
+
// by the Vite plugin (`vite.ts`, an ESM/Node entry), never by the environment-free
|
|
4
|
+
// engine (`index.ts`/`analyze.ts`), so the browser playground build stays clean.
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
/**
|
|
7
|
+
* Build a {@link Parse} backed by rsvelte's native parser
|
|
8
|
+
* (`@rsvelte/vite-plugin-svelte-native`), or `null` if that OPTIONAL peer package
|
|
9
|
+
* can't be loaded (not installed / unsupported platform) — the caller then falls
|
|
10
|
+
* back to svelte/compiler.
|
|
11
|
+
*
|
|
12
|
+
* `skipExpressionLoc: true` is REQUIRED, not cosmetic: the per-expression `loc`
|
|
13
|
+
* blocks roughly double the AST and make the engine's walk the dominant cost
|
|
14
|
+
* (full-pipeline 0.72x WITH them vs 1.46x WITHOUT) — and the engine reads only
|
|
15
|
+
* UTF-16 `start`/`end`, never `loc`, so dropping them leaves the output identical
|
|
16
|
+
* (docs/RUST-MIGRATION.md §6).
|
|
17
|
+
*/
|
|
18
|
+
export function tryLoadRsvelteParser() {
|
|
19
|
+
let native;
|
|
20
|
+
try {
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
22
|
+
native = require('@rsvelte/vite-plugin-svelte-native');
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const parse = native.parse;
|
|
28
|
+
if (typeof parse !== 'function')
|
|
29
|
+
return null;
|
|
30
|
+
return (code) => JSON.parse(parse(code, { skipExpressionLoc: true }));
|
|
31
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ComponentId } from './ir';
|
|
2
|
-
import type { Resolve, ReadFile } from './analyze';
|
|
1
|
+
import type { ComponentId } from './ir.js';
|
|
2
|
+
import type { Resolve, ReadFile } from './analyze.js';
|
|
3
3
|
/** Default filesystem resolver: resolve `source` relative to its importer. */
|
|
4
4
|
export declare const fsResolve: Resolve;
|
|
5
5
|
/** Default filesystem reader. */
|
package/dist/scan.js
CHANGED
|
@@ -1 +1,40 @@
|
|
|
1
|
-
|
|
1
|
+
// ----------------------------------------------------------------------
|
|
2
|
+
// Node-only Shell glue (docs/ARCHITECTURE.md §5). Kept OUT of `index.ts` so the
|
|
3
|
+
// engine core has no `node:*` imports and runs unchanged in the browser.
|
|
4
|
+
// Exposed to Node consumers via the `svelte-shaker/node` entry point.
|
|
5
|
+
// ----------------------------------------------------------------------
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
/** Default filesystem resolver: resolve `source` relative to its importer. */
|
|
9
|
+
export const fsResolve = (source, importer) => {
|
|
10
|
+
if (!source.startsWith('.'))
|
|
11
|
+
return null; // bare imports aren't local components
|
|
12
|
+
return path.resolve(path.dirname(importer), source);
|
|
13
|
+
};
|
|
14
|
+
/** Default filesystem reader. */
|
|
15
|
+
export const fsReadFile = (id) => fs.readFileSync(id, 'utf-8');
|
|
16
|
+
/**
|
|
17
|
+
* Recursively collect every `.svelte` file under `dir` (skipping `node_modules`
|
|
18
|
+
* and dot-directories). A Shell helper, kept out of the env-free engine core
|
|
19
|
+
* (docs/ARCHITECTURE.md §5): plugins use it to seed the whole-program crawl.
|
|
20
|
+
*/
|
|
21
|
+
export function collectSvelteFiles(dir) {
|
|
22
|
+
const out = [];
|
|
23
|
+
let entries;
|
|
24
|
+
try {
|
|
25
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.'))
|
|
32
|
+
continue;
|
|
33
|
+
const full = path.join(dir, entry.name);
|
|
34
|
+
if (entry.isDirectory())
|
|
35
|
+
out.push(...collectSvelteFiles(full));
|
|
36
|
+
else if (entry.isFile() && entry.name.endsWith('.svelte'))
|
|
37
|
+
out.push(full);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import MagicString from 'magic-string';
|
|
2
|
-
import { type AnyNode } from './parse';
|
|
3
|
-
import type { ComponentId, ComponentPlan, Literal } from './ir';
|
|
4
|
-
import { type FileModel } from './analyze';
|
|
5
|
-
import { type Span } from './dead';
|
|
2
|
+
import { type AnyNode } from './parse.js';
|
|
3
|
+
import type { ComponentId, ComponentPlan, Literal } from './ir.js';
|
|
4
|
+
import { type FileModel } from './analyze.js';
|
|
5
|
+
import { type Span } from './dead.js';
|
|
6
6
|
/**
|
|
7
7
|
* Apply every plan to every component and return the shaken source per file.
|
|
8
8
|
*
|