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/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
+ }
@@ -1,4 +1,4 @@
1
- import type { Parse } from './parse';
1
+ import type { Parse } from './parse.js';
2
2
  /**
3
3
  * Build a {@link Parse} backed by rsvelte's native parser
4
4
  * (`@rsvelte/vite-plugin-svelte-native`), or `null` if that OPTIONAL peer package
@@ -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
- import*as e from"node:fs";import*as t from"node:path";const n=(e,n)=>e.startsWith(".")?t.resolve(t.dirname(n),e):null,o=t=>e.readFileSync(t,"utf-8");function r(n){const o=[];let s;try{s=e.readdirSync(n,{withFileTypes:!0})}catch{return o}for(const e of s){if("node_modules"===e.name||e.name.startsWith("."))continue;const s=t.join(n,e.name);e.isDirectory()?o.push(...r(s)):e.isFile()&&e.name.endsWith(".svelte")&&o.push(s)}return o}export{r as collectSvelteFiles,o as fsReadFile,n as fsResolve};
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
  *