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 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 (`{...obj}` aside). `dynamic` means the attribute had a
85
- * non-literal value (`bind:`, dynamic expression): used, value not statically
86
- * known. `afterLastSpread` records whether this explicit write happened after
87
- * the site's last `{...spread}` only then can a spread not silently override
88
- * it (docs §4.1, "後勝ち順序で救う").
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
- /** Did this site have at least one `{...spread}` attribute? */
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 §4.1).
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 §4.1).
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
- if (attrs[i].type === 'SpreadAttribute')
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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-shaker",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Tree shaking for Svelte components",
5
5
  "keywords": [
6
6
  "dead-code-elimination",