svelte-shaker 0.6.0 → 0.7.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/analyze.d.ts +20 -7
- package/dist/analyze.js +70 -2
- package/dist/parse.d.ts +4 -0
- package/dist/vite.js +39 -0
- package/package.json +1 -1
package/dist/analyze.d.ts
CHANGED
|
@@ -81,11 +81,15 @@ export interface ChildCall {
|
|
|
81
81
|
}
|
|
82
82
|
/**
|
|
83
83
|
* One value passed explicitly to a prop at one call site, after last-write-wins
|
|
84
|
-
* has been resolved (`{
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
84
|
+
* has been resolved. An explicit write is either a real attribute (`a={1}`) OR a
|
|
85
|
+
* key expanded out of a statically-known object-literal spread (`{...{a:1}}`,
|
|
86
|
+
* docs §4.1) — both name a prop and a value the same way. `dynamic` means the
|
|
87
|
+
* value is non-literal (`bind:`, a dynamic expression, or a spread key whose
|
|
88
|
+
* value is non-literal): used, value not statically known. `afterLastSpread`
|
|
89
|
+
* records whether this write happened after the site's last *unknown* spread (one
|
|
90
|
+
* we could not expand) — only then can no spread silently override it (docs §4.1,
|
|
91
|
+
* "後勝ち順序で救う"). A known object-literal spread is expanded, not opaque, so
|
|
92
|
+
* it never counts as an "unknown spread" here.
|
|
89
93
|
*/
|
|
90
94
|
export interface ExplicitProp {
|
|
91
95
|
value: Literal;
|
|
@@ -94,7 +98,12 @@ export interface ExplicitProp {
|
|
|
94
98
|
}
|
|
95
99
|
/** How a child component is called at one `<Child .../>` site. */
|
|
96
100
|
export interface CallSite {
|
|
97
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* Did this site have at least one spread we could NOT statically expand (an
|
|
103
|
+
* identifier / call / `{...{…computed/nested…}}`)? A fully-known object-literal
|
|
104
|
+
* spread is expanded into {@link ExplicitProp} writes instead, so it does not
|
|
105
|
+
* set this — only an opaque spread, which may set any prop, does (docs §4.1).
|
|
106
|
+
*/
|
|
98
107
|
hadSpread: boolean;
|
|
99
108
|
/** Last-write-wins explicit props at this site, keyed by prop name. */
|
|
100
109
|
explicit: Map<string, ExplicitProp>;
|
|
@@ -160,7 +169,11 @@ export declare function remapToLocalNames<V>(map: Map<string, V>, model: FileMod
|
|
|
160
169
|
* Read one `<Child .../>` into a {@link CallSite}. Attributes are in source
|
|
161
170
|
* order, so we resolve last-write-wins (a later `a={…}` overrides an earlier
|
|
162
171
|
* one) and record, per prop, whether its winning write came *after* the last
|
|
163
|
-
* spread — the only case a spread cannot silently override it (docs
|
|
172
|
+
* *unknown* spread — the only case a spread cannot silently override it (docs
|
|
173
|
+
* §4.1). A statically-known object-literal spread (`{...{a:1, b:2}}`) is not
|
|
174
|
+
* opaque: we expand its keys into explicit writes at the spread's position, so it
|
|
175
|
+
* both contributes those literals AND does not poison props it cannot set (docs
|
|
176
|
+
* §4.1, "{...obj} が object literal ならキー展開").
|
|
164
177
|
*/
|
|
165
178
|
export declare function readCallSite(component: AnyNode): CallSite;
|
|
166
179
|
/** Decide what to fold for one component from its global usage. */
|
package/dist/analyze.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseCached, parseSvelte, walk, } from './parse.js';
|
|
2
2
|
import { emptyPlan, } from './ir.js';
|
|
3
3
|
import { computeDeadSpans, inSpans } from './dead.js';
|
|
4
|
+
import { evaluate } from './eval.js';
|
|
4
5
|
const isSvelte = (source) => source.endsWith('.svelte');
|
|
5
6
|
/** Hard cap on fixpoint iterations: convergence is monotone (dead spans only
|
|
6
7
|
* grow as profiles shrink), so this is reached only if something is non-monotone
|
|
@@ -672,18 +673,40 @@ function memberComponentTags(ast) {
|
|
|
672
673
|
* Read one `<Child .../>` into a {@link CallSite}. Attributes are in source
|
|
673
674
|
* order, so we resolve last-write-wins (a later `a={…}` overrides an earlier
|
|
674
675
|
* one) and record, per prop, whether its winning write came *after* the last
|
|
675
|
-
* spread — the only case a spread cannot silently override it (docs
|
|
676
|
+
* *unknown* spread — the only case a spread cannot silently override it (docs
|
|
677
|
+
* §4.1). A statically-known object-literal spread (`{...{a:1, b:2}}`) is not
|
|
678
|
+
* opaque: we expand its keys into explicit writes at the spread's position, so it
|
|
679
|
+
* both contributes those literals AND does not poison props it cannot set (docs
|
|
680
|
+
* §4.1, "{...obj} が object literal ならキー展開").
|
|
676
681
|
*/
|
|
677
682
|
export function readCallSite(component) {
|
|
678
683
|
const attrs = component.attributes ?? [];
|
|
684
|
+
// Only spreads we CANNOT expand are opaque (may set any prop). Classify first
|
|
685
|
+
// so `afterLastSpread` is measured against the last *unknown* spread, not a
|
|
686
|
+
// known object literal we are about to expand into explicit writes.
|
|
679
687
|
let lastSpreadIndex = -1;
|
|
680
688
|
for (let i = 0; i < attrs.length; i++) {
|
|
681
|
-
|
|
689
|
+
const attr = attrs[i];
|
|
690
|
+
if (attr.type === 'SpreadAttribute' && knownSpreadEntries(attr) === null)
|
|
682
691
|
lastSpreadIndex = i;
|
|
683
692
|
}
|
|
684
693
|
const explicit = new Map();
|
|
685
694
|
for (let i = 0; i < attrs.length; i++) {
|
|
686
695
|
const attr = attrs[i];
|
|
696
|
+
if (attr.type === 'SpreadAttribute') {
|
|
697
|
+
// A known object-literal spread expands to one explicit write per key, at
|
|
698
|
+
// this spread's position; an unknown spread is opaque and handled by the
|
|
699
|
+
// `hadSpread`/`afterLastSpread` poisoning in `valueSetFor`.
|
|
700
|
+
const entries = knownSpreadEntries(attr);
|
|
701
|
+
if (entries) {
|
|
702
|
+
for (const [name, value] of entries) {
|
|
703
|
+
explicit.set(name, value.known
|
|
704
|
+
? { value: value.value, dynamic: false, afterLastSpread: i > lastSpreadIndex }
|
|
705
|
+
: dynamicWrite(i, lastSpreadIndex));
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
687
710
|
const name = attr.name;
|
|
688
711
|
if (attr.type === 'BindDirective') {
|
|
689
712
|
// `bind:prop` is a used, dynamic two-way binding (docs §4.1).
|
|
@@ -759,6 +782,51 @@ function dynamicWrite(index, lastSpreadIndex) {
|
|
|
759
782
|
afterLastSpread: index > lastSpreadIndex,
|
|
760
783
|
};
|
|
761
784
|
}
|
|
785
|
+
/**
|
|
786
|
+
* The `[name, value]` entries a spread contributes IF it is a statically-known
|
|
787
|
+
* object literal whose complete key set we can see (docs §4.1 "object literal な
|
|
788
|
+
* spread はキー展開"). Returns `null` for any spread we cannot fully expand — an
|
|
789
|
+
* identifier/call (`{...rest}`), or an object literal carrying a nested spread
|
|
790
|
+
* (`{...{...x}}`), a computed key (`{...{[k]: 1}}`), or a getter/setter/method —
|
|
791
|
+
* because then we do not know the full set of props it sets and must treat it as
|
|
792
|
+
* opaque. Each entry's value is `{known:true,value}` for a literal (so it folds)
|
|
793
|
+
* or `{known:false}` for a non-literal value (key known, value dynamic): both are
|
|
794
|
+
* sound, since the key set is fully known either way.
|
|
795
|
+
*/
|
|
796
|
+
function knownSpreadEntries(attr) {
|
|
797
|
+
const obj = attr.expression;
|
|
798
|
+
if (obj?.type !== 'ObjectExpression')
|
|
799
|
+
return null;
|
|
800
|
+
const entries = [];
|
|
801
|
+
for (const prop of obj.properties ?? []) {
|
|
802
|
+
// A nested spread, computed key, or accessor/method means the full key set is
|
|
803
|
+
// not statically knowable -> the whole spread is opaque.
|
|
804
|
+
if (prop.type !== 'Property')
|
|
805
|
+
return null;
|
|
806
|
+
if (prop.computed === true ||
|
|
807
|
+
prop.kind === 'get' ||
|
|
808
|
+
prop.kind === 'set' ||
|
|
809
|
+
prop.method === true)
|
|
810
|
+
return null;
|
|
811
|
+
const key = prop.key;
|
|
812
|
+
const name = key?.type === 'Identifier'
|
|
813
|
+
? key.name
|
|
814
|
+
: key?.type === 'Literal' &&
|
|
815
|
+
(typeof key.value === 'string' || typeof key.value === 'number')
|
|
816
|
+
? String(key.value)
|
|
817
|
+
: null;
|
|
818
|
+
if (name == null)
|
|
819
|
+
return null;
|
|
820
|
+
entries.push([name, evalToLiteral(prop.value)]);
|
|
821
|
+
}
|
|
822
|
+
return entries;
|
|
823
|
+
}
|
|
824
|
+
/** Constant-evaluate a spread property value with no environment (literals + the
|
|
825
|
+
* tiny pure operator fragment), as `{known:true,value}` or `{known:false}`. */
|
|
826
|
+
function evalToLiteral(node) {
|
|
827
|
+
const r = evaluate(node, new Map());
|
|
828
|
+
return r.known ? { known: true, value: r.value } : { known: false };
|
|
829
|
+
}
|
|
762
830
|
/** Extract a literal from an attribute value, or `{ known:false }`. */
|
|
763
831
|
function literalAttrValue(value) {
|
|
764
832
|
if (value === true)
|
package/dist/parse.d.ts
CHANGED
|
@@ -67,6 +67,10 @@ export interface AnyNode {
|
|
|
67
67
|
computed?: boolean | undefined;
|
|
68
68
|
shorthand?: boolean | undefined;
|
|
69
69
|
elseif?: boolean | undefined;
|
|
70
|
+
/** ObjectExpression `Property` accessor kind (`init` / `get` / `set`). */
|
|
71
|
+
kind?: string | undefined;
|
|
72
|
+
/** ObjectExpression `Property` shorthand-method flag (`{ m() {} }`). */
|
|
73
|
+
method?: boolean | undefined;
|
|
70
74
|
value?: unknown;
|
|
71
75
|
}
|
|
72
76
|
export interface Root extends AnyNode {
|
package/dist/vite.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
+
import { compile } from 'svelte/compiler';
|
|
3
4
|
import { svelteShaker, svelteShakerWithMono } from './index.js';
|
|
4
5
|
import { DevShaker } from './engine.js';
|
|
5
6
|
import { collectSvelteFiles, fsResolve } from './scan.js';
|
|
@@ -9,6 +10,23 @@ import { tryLoadRsvelteParser } from './rsvelte-parse.js';
|
|
|
9
10
|
function formatKB(bytes) {
|
|
10
11
|
return `${(bytes / 1024).toFixed(2)} kB`;
|
|
11
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Compiled-output size proxy: the bytes the Svelte compiler emits for `code`
|
|
15
|
+
* (client JS + scoped CSS). This is the number that actually ships, so it
|
|
16
|
+
* captures what the SOURCE-byte total cannot: a dead `{#if}` arm or a removed
|
|
17
|
+
* `<style>` rule shrinks the compiled output far more than its few source bytes
|
|
18
|
+
* suggest. Reporting-only; `null` if the (always valid) source fails to compile,
|
|
19
|
+
* so the caller can skip it without aborting the build.
|
|
20
|
+
*/
|
|
21
|
+
function compiledSize(code, id) {
|
|
22
|
+
try {
|
|
23
|
+
const { js, css } = compile(code, { generate: 'client', dev: false, filename: id });
|
|
24
|
+
return js.code.length + (css?.code.length ?? 0);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
12
30
|
/**
|
|
13
31
|
* Print what the shake saved. Always emits a one-line whole-program summary;
|
|
14
32
|
* when `verbose`, also lists each shrunk file (largest saving first). Sizes are
|
|
@@ -43,6 +61,27 @@ function reportSizes(shaken, read, root, verbose, log) {
|
|
|
43
61
|
const filePct = ((fileSaved / row.before) * 100).toFixed(1);
|
|
44
62
|
log(` ${rel}: ${formatKB(row.before)} → ${formatKB(row.after)} (-${filePct}%)`);
|
|
45
63
|
}
|
|
64
|
+
// The source-byte delta UNDER-reports the real win: a folded dead branch or a
|
|
65
|
+
// removed `<style>` rule shrinks the compiled output much more than its source.
|
|
66
|
+
// Compiling is costly, so we only do it for the files that actually shrank, and
|
|
67
|
+
// only under `verbose` — it never affects the build, just the visible number.
|
|
68
|
+
let compiledBefore = 0;
|
|
69
|
+
let compiledAfter = 0;
|
|
70
|
+
for (const row of rows) {
|
|
71
|
+
const before = compiledSize(read(row.id), row.id);
|
|
72
|
+
const after = compiledSize(shaken[row.id], row.id);
|
|
73
|
+
if (before === null || after === null)
|
|
74
|
+
continue; // skip the un-compilable
|
|
75
|
+
compiledBefore += before;
|
|
76
|
+
compiledAfter += after;
|
|
77
|
+
}
|
|
78
|
+
if (compiledBefore > 0) {
|
|
79
|
+
const compiledSaved = compiledBefore - compiledAfter;
|
|
80
|
+
const compiledPct = ((compiledSaved / compiledBefore) * 100).toFixed(1);
|
|
81
|
+
log(`compiled output (js+css) of shaken files: ` +
|
|
82
|
+
`${formatKB(compiledBefore)} → ${formatKB(compiledAfter)} ` +
|
|
83
|
+
`(saved ${formatKB(compiledSaved)}, ${compiledPct}%)`);
|
|
84
|
+
}
|
|
46
85
|
}
|
|
47
86
|
/** Query flag a specialized-variant `.svelte` request carries (see below). */
|
|
48
87
|
const VARIANT_QUERY = 'shaker_variant';
|