svelte-shaker 0.9.2 → 0.10.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 +43 -0
- package/dist/analyze.js +179 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/analyze.d.ts
CHANGED
|
@@ -3,6 +3,11 @@ import { type AnalyzeInput, type ComponentId, type ComponentPlan, type Literal }
|
|
|
3
3
|
import { type Span } from './dead.js';
|
|
4
4
|
export type Resolve = (source: string, importer: ComponentId) => Promise<ComponentId | null> | ComponentId | null;
|
|
5
5
|
export type ReadFile = (id: ComponentId) => Promise<string> | string;
|
|
6
|
+
/** Synchronous variants of {@link Resolve}/{@link ReadFile} for callers that
|
|
7
|
+
* cannot await — e.g. an ESLint rule, which runs synchronously. Used by
|
|
8
|
+
* {@link buildAnalyzeInputSync}. */
|
|
9
|
+
export type ResolveSync = (source: string, importer: ComponentId) => ComponentId | null;
|
|
10
|
+
export type ReadFileSync = (id: ComponentId) => string;
|
|
6
11
|
/** One declared prop in a `$props()` destructuring. */
|
|
7
12
|
export interface PropDecl {
|
|
8
13
|
/** The EXTERNAL prop name — the destructure KEY (`prop` in `prop: alias`).
|
|
@@ -148,12 +153,50 @@ export declare function analyzeInput(input: AnalyzeInput, parseCache?: ParseCach
|
|
|
148
153
|
* value set), keeping the produced model set — and thus the output — identical.
|
|
149
154
|
*/
|
|
150
155
|
export declare function buildAnalyzeInput(entries: ComponentId | ComponentId[], resolve: Resolve, readFile: ReadFile, parseCache?: ParseCache, parse?: Parse): Promise<AnalyzeInput>;
|
|
156
|
+
/**
|
|
157
|
+
* Synchronous twin of {@link buildAnalyzeInput} for callers that cannot await
|
|
158
|
+
* (an ESLint rule runs synchronously). Byte-for-byte the same crawl with sync
|
|
159
|
+
* `resolve`/`readFile`; the `tests/build-analyze-input-sync` differential test
|
|
160
|
+
* pins it identical to the async path, so keep the two bodies in lockstep.
|
|
161
|
+
*/
|
|
162
|
+
export declare function buildAnalyzeInputSync(entries: ComponentId | ComponentId[], resolve: ResolveSync, readFile: ReadFileSync, parseCache?: ParseCache, parse?: Parse): AnalyzeInput;
|
|
151
163
|
/**
|
|
152
164
|
* Dead `{#if}` spans per component implied by `plans`, via the SAME shared
|
|
153
165
|
* predicate the transform uses ({@link computeDeadSpans}). A bailed component
|
|
154
166
|
* folds nothing, so it has no dead spans.
|
|
155
167
|
*/
|
|
156
168
|
export declare function deadSpansForPlans(models: Map<ComponentId, FileModel>, plans: Map<ComponentId, ComponentPlan>): Map<ComponentId, Span[]>;
|
|
169
|
+
/** One declared prop that no call site in the program ever passes. `start`/`end`
|
|
170
|
+
* are UTF-16 offsets of the prop's `$props()` destructuring property, for direct
|
|
171
|
+
* source mapping by a consumer (e.g. an ESLint rule). */
|
|
172
|
+
export interface UnpassedProp {
|
|
173
|
+
/** The external prop name (what a caller would pass). */
|
|
174
|
+
name: string;
|
|
175
|
+
start: number;
|
|
176
|
+
end: number;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Declared props that NO call site in the analyzed program ever passes — neither
|
|
180
|
+
* explicitly (`<C p=…>` / `bind:p`), via a spread, nor as body content/`{#snippet}`.
|
|
181
|
+
* These are "dead" from the consumer side: the component declares an input no one
|
|
182
|
+
* supplies, so it is always its default. A lint-oriented counterpart to the
|
|
183
|
+
* build-time fold (svelte-shaker would const-fold such a prop to its default).
|
|
184
|
+
*
|
|
185
|
+
* Soundness — only HIGH-CONFIDENCE reports, mirroring the folder's own caution:
|
|
186
|
+
* - a component that BAILED (escaped as a value, `accessors`, etc.) is skipped —
|
|
187
|
+
* its prop profile is unknowable;
|
|
188
|
+
* - a component with ZERO call sites is skipped — it is an entry/route/unused
|
|
189
|
+
* component whose props may be supplied OUTSIDE the analyzed graph (a SvelteKit
|
|
190
|
+
* `+page.svelte`'s `data`, a framework mount, a not-yet-rendered component);
|
|
191
|
+
* - a prop is reported only when EVERY call site neither names it nor carries a
|
|
192
|
+
* spread that could set it (`readCallSite` already folds `bind:`, known
|
|
193
|
+
* spreads, and `children`/snippet body into `explicit`/`hadSpread`).
|
|
194
|
+
*
|
|
195
|
+
* Because missing an edge (e.g. an unfollowed barrel) only DROPS call sites, it
|
|
196
|
+
* can only make this UNDER-report (the component looks unused and is skipped),
|
|
197
|
+
* never over-report — so an incomplete crawl stays false-positive-free.
|
|
198
|
+
*/
|
|
199
|
+
export declare function findNeverPassedProps(input: AnalyzeInput): Map<ComponentId, UnpassedProp[]>;
|
|
157
200
|
/**
|
|
158
201
|
* Remap a plan map keyed by EXTERNAL prop name (`constFold` / `narrow`) to one
|
|
159
202
|
* keyed by the LOCAL binding name each prop introduces. Call-site analysis and
|
package/dist/analyze.js
CHANGED
|
@@ -174,6 +174,72 @@ export async function buildAnalyzeInput(entries, resolve, readFile, parseCache,
|
|
|
174
174
|
}
|
|
175
175
|
return { files, edges, entries: entryList };
|
|
176
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Synchronous twin of {@link buildAnalyzeInput} for callers that cannot await
|
|
179
|
+
* (an ESLint rule runs synchronously). Byte-for-byte the same crawl with sync
|
|
180
|
+
* `resolve`/`readFile`; the `tests/build-analyze-input-sync` differential test
|
|
181
|
+
* pins it identical to the async path, so keep the two bodies in lockstep.
|
|
182
|
+
*/
|
|
183
|
+
export function buildAnalyzeInputSync(entries, resolve, readFile, parseCache, parse) {
|
|
184
|
+
const entryList = Array.isArray(entries) ? [...entries] : [entries];
|
|
185
|
+
const files = [];
|
|
186
|
+
const edges = [];
|
|
187
|
+
const queue = [...entryList];
|
|
188
|
+
const seen = new Set(queue);
|
|
189
|
+
while (queue.length > 0) {
|
|
190
|
+
const id = queue.shift();
|
|
191
|
+
const code = readFile(id);
|
|
192
|
+
files.push({ id, code });
|
|
193
|
+
const ast = parseCached(id, code, parseCache, parse);
|
|
194
|
+
const instance = ast.instance;
|
|
195
|
+
if (!instance)
|
|
196
|
+
continue;
|
|
197
|
+
const barrelLocals = new Map();
|
|
198
|
+
const namespaceSources = new Map();
|
|
199
|
+
const directChildren = [];
|
|
200
|
+
for (const imp of importSources(instance)) {
|
|
201
|
+
if (imp.imported === '*') {
|
|
202
|
+
namespaceSources.set(imp.local, imp.value);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (imp.imported === 'default' && isSvelte(imp.value)) {
|
|
206
|
+
const childId = resolve(imp.value, id);
|
|
207
|
+
if (childId) {
|
|
208
|
+
edges.push({ from: id, local: imp.local, to: childId, kind: 'default-svelte' });
|
|
209
|
+
directChildren.push(childId);
|
|
210
|
+
}
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const childId = resolveThroughBarrelSync(imp.value, imp.imported, id, resolve, readFile);
|
|
214
|
+
if (childId) {
|
|
215
|
+
edges.push({ from: id, local: imp.local, to: childId, kind: 'barrel' });
|
|
216
|
+
barrelLocals.set(imp.local, childId);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const nsChildren = [];
|
|
220
|
+
if (namespaceSources.size > 0) {
|
|
221
|
+
for (const tag of memberComponentTags(ast)) {
|
|
222
|
+
const dot = tag.indexOf('.');
|
|
223
|
+
const source = namespaceSources.get(tag.slice(0, dot));
|
|
224
|
+
if (source == null)
|
|
225
|
+
continue;
|
|
226
|
+
const childId = resolveThroughBarrelSync(source, tag.slice(dot + 1), id, resolve, readFile);
|
|
227
|
+
if (childId) {
|
|
228
|
+
edges.push({ from: id, local: tag, to: childId, kind: 'namespace' });
|
|
229
|
+
nsChildren.push(childId);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const rendered = collectBarrelChildIds(ast, barrelLocals);
|
|
234
|
+
for (const childId of [...directChildren, ...rendered, ...nsChildren]) {
|
|
235
|
+
if (!seen.has(childId)) {
|
|
236
|
+
seen.add(childId);
|
|
237
|
+
queue.push(childId);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return { files, edges, entries: entryList };
|
|
242
|
+
}
|
|
177
243
|
/**
|
|
178
244
|
* Aggregate every component's call sites into per-child {@link Usage}, EXCLUDING
|
|
179
245
|
* any `<Child/>` whose node falls inside a dead `{#if}` span of its containing
|
|
@@ -279,6 +345,67 @@ export function deadSpansForPlans(models, plans) {
|
|
|
279
345
|
}
|
|
280
346
|
return out;
|
|
281
347
|
}
|
|
348
|
+
/**
|
|
349
|
+
* Declared props that NO call site in the analyzed program ever passes — neither
|
|
350
|
+
* explicitly (`<C p=…>` / `bind:p`), via a spread, nor as body content/`{#snippet}`.
|
|
351
|
+
* These are "dead" from the consumer side: the component declares an input no one
|
|
352
|
+
* supplies, so it is always its default. A lint-oriented counterpart to the
|
|
353
|
+
* build-time fold (svelte-shaker would const-fold such a prop to its default).
|
|
354
|
+
*
|
|
355
|
+
* Soundness — only HIGH-CONFIDENCE reports, mirroring the folder's own caution:
|
|
356
|
+
* - a component that BAILED (escaped as a value, `accessors`, etc.) is skipped —
|
|
357
|
+
* its prop profile is unknowable;
|
|
358
|
+
* - a component with ZERO call sites is skipped — it is an entry/route/unused
|
|
359
|
+
* component whose props may be supplied OUTSIDE the analyzed graph (a SvelteKit
|
|
360
|
+
* `+page.svelte`'s `data`, a framework mount, a not-yet-rendered component);
|
|
361
|
+
* - a prop is reported only when EVERY call site neither names it nor carries a
|
|
362
|
+
* spread that could set it (`readCallSite` already folds `bind:`, known
|
|
363
|
+
* spreads, and `children`/snippet body into `explicit`/`hadSpread`).
|
|
364
|
+
*
|
|
365
|
+
* Because missing an edge (e.g. an unfollowed barrel) only DROPS call sites, it
|
|
366
|
+
* can only make this UNDER-report (the component looks unused and is skipped),
|
|
367
|
+
* never over-report — so an incomplete crawl stays false-positive-free.
|
|
368
|
+
*/
|
|
369
|
+
export function findNeverPassedProps(input) {
|
|
370
|
+
const models = buildModels(input);
|
|
371
|
+
// Stamp escape bails up front (same union as `analyzeInput`) so escaped
|
|
372
|
+
// components are skipped below.
|
|
373
|
+
const escaped = new Set();
|
|
374
|
+
for (const model of models.values())
|
|
375
|
+
for (const id of model.escapedComponents)
|
|
376
|
+
escaped.add(id);
|
|
377
|
+
for (const id of escaped) {
|
|
378
|
+
const model = models.get(id);
|
|
379
|
+
if (model && !model.bailReasons.includes(ESCAPE_REASON))
|
|
380
|
+
model.bailReasons.push(ESCAPE_REASON);
|
|
381
|
+
}
|
|
382
|
+
// Every textual call site counts (no cascade dead-span filtering): a prop passed
|
|
383
|
+
// only at a folded-away site is still author-written, so we do not flag it.
|
|
384
|
+
const usage = buildUsage(models, new Map());
|
|
385
|
+
const out = new Map();
|
|
386
|
+
for (const model of models.values()) {
|
|
387
|
+
if (model.bailReasons.length > 0)
|
|
388
|
+
continue;
|
|
389
|
+
if (!model.props || model.props.length === 0)
|
|
390
|
+
continue;
|
|
391
|
+
const sites = usage.get(model.id)?.sites ?? [];
|
|
392
|
+
if (sites.length === 0)
|
|
393
|
+
continue;
|
|
394
|
+
const unpassed = [];
|
|
395
|
+
for (const decl of model.props) {
|
|
396
|
+
const maybePassed = sites.some((s) => s.explicit.has(decl.name) || s.hadSpread);
|
|
397
|
+
if (maybePassed)
|
|
398
|
+
continue;
|
|
399
|
+
const prop = decl.property;
|
|
400
|
+
if (typeof prop.start !== 'number' || typeof prop.end !== 'number')
|
|
401
|
+
continue;
|
|
402
|
+
unpassed.push({ name: decl.name, start: prop.start, end: prop.end });
|
|
403
|
+
}
|
|
404
|
+
if (unpassed.length > 0)
|
|
405
|
+
out.set(model.id, unpassed);
|
|
406
|
+
}
|
|
407
|
+
return out;
|
|
408
|
+
}
|
|
282
409
|
/**
|
|
283
410
|
* Remap a plan map keyed by EXTERNAL prop name (`constFold` / `narrow`) to one
|
|
284
411
|
* keyed by the LOCAL binding name each prop introduces. Call-site analysis and
|
|
@@ -1103,6 +1230,58 @@ async function resolveThroughBarrel(source, imported, importer, resolve, readFil
|
|
|
1103
1230
|
}
|
|
1104
1231
|
return null;
|
|
1105
1232
|
}
|
|
1233
|
+
/** Synchronous twin of {@link resolveThroughBarrel} (see {@link
|
|
1234
|
+
* buildAnalyzeInputSync}). Keep in lockstep with the async body above. */
|
|
1235
|
+
function resolveThroughBarrelSync(source, imported, importer, resolve, readFile, hops = 0) {
|
|
1236
|
+
if (hops > MAX_BARREL_HOPS)
|
|
1237
|
+
return null;
|
|
1238
|
+
const targetId = resolve(source, importer);
|
|
1239
|
+
if (!targetId)
|
|
1240
|
+
return null;
|
|
1241
|
+
if (isSvelte(source) || isSvelte(targetId)) {
|
|
1242
|
+
return imported === 'default' || imported === '*' ? targetId : null;
|
|
1243
|
+
}
|
|
1244
|
+
let code;
|
|
1245
|
+
try {
|
|
1246
|
+
code = readFile(targetId);
|
|
1247
|
+
}
|
|
1248
|
+
catch {
|
|
1249
|
+
return null;
|
|
1250
|
+
}
|
|
1251
|
+
const body = parseModuleBody(code, targetId);
|
|
1252
|
+
if (!body)
|
|
1253
|
+
return null;
|
|
1254
|
+
for (const stmt of body) {
|
|
1255
|
+
if (stmt.type === 'ExportNamedDeclaration' && stmt.source?.value) {
|
|
1256
|
+
for (const spec of stmt.specifiers ?? []) {
|
|
1257
|
+
if (specName(spec.exported) !== imported)
|
|
1258
|
+
continue;
|
|
1259
|
+
return resolveThroughBarrelSync(String(stmt.source.value), specName(spec.local) ?? 'default', targetId, resolve, readFile, hops + 1);
|
|
1260
|
+
}
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
if (stmt.type === 'ExportNamedDeclaration' && !stmt.source) {
|
|
1264
|
+
for (const spec of stmt.specifiers ?? []) {
|
|
1265
|
+
if (specName(spec.exported) !== imported)
|
|
1266
|
+
continue;
|
|
1267
|
+
const localName = specName(spec.local);
|
|
1268
|
+
if (!localName)
|
|
1269
|
+
continue;
|
|
1270
|
+
const found = followLocalImport(body, localName);
|
|
1271
|
+
if (!found)
|
|
1272
|
+
return null;
|
|
1273
|
+
return resolveThroughBarrelSync(found.value, found.imported, targetId, resolve, readFile, hops + 1);
|
|
1274
|
+
}
|
|
1275
|
+
continue;
|
|
1276
|
+
}
|
|
1277
|
+
if (stmt.type === 'ExportAllDeclaration' && stmt.source?.value) {
|
|
1278
|
+
const via = resolveThroughBarrelSync(String(stmt.source.value), imported, targetId, resolve, readFile, hops + 1);
|
|
1279
|
+
if (via)
|
|
1280
|
+
return via;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
return null;
|
|
1284
|
+
}
|
|
1106
1285
|
/** Find the import in `body` that binds `localName`, as an {@link ImportInfo}. */
|
|
1107
1286
|
function followLocalImport(body, localName) {
|
|
1108
1287
|
for (const stmt of body) {
|
package/dist/index.d.ts
CHANGED
|
@@ -3,9 +3,10 @@ import { type Parse } from './parse.js';
|
|
|
3
3
|
import { type MonomorphizeOptions, type MonomorphizeResult } 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
|
-
export type { Resolve, ReadFile } from './analyze.js';
|
|
6
|
+
export type { Resolve, ReadFile, ResolveSync, ReadFileSync } from './analyze.js';
|
|
7
7
|
export type { Parse, Root } from './parse.js';
|
|
8
|
-
export { analyze, analyzeInput, buildAnalyzeInput } from './analyze.js';
|
|
8
|
+
export { analyze, analyzeInput, buildAnalyzeInput, buildAnalyzeInputSync, deadSpansForPlans, findNeverPassedProps, } from './analyze.js';
|
|
9
|
+
export type { UnpassedProp } from './analyze.js';
|
|
9
10
|
export { DevShaker, type DevMode, type DevShakerChange } from './engine.js';
|
|
10
11
|
export { transformAll, transformAllWithMono } from './transform.js';
|
|
11
12
|
export { monomorphize, DEFAULT_MONO_OPTIONS, type MonomorphizeOptions, type MonomorphizeResult, type Variant, type CallSiteBinding, } from './mono.js';
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { analyze, analyzeInput, buildAnalyzeInput, } from './analyze.js';
|
|
|
2
2
|
import { parseSvelte } from './parse.js';
|
|
3
3
|
import { transformAll, transformAllWithMono } from './transform.js';
|
|
4
4
|
import { monomorphize, DEFAULT_MONO_OPTIONS, } from './mono.js';
|
|
5
|
-
export { analyze, analyzeInput, buildAnalyzeInput } from './analyze.js';
|
|
5
|
+
export { analyze, analyzeInput, buildAnalyzeInput, buildAnalyzeInputSync, deadSpansForPlans, findNeverPassedProps, } from './analyze.js';
|
|
6
6
|
export { DevShaker } from './engine.js';
|
|
7
7
|
export { transformAll, transformAllWithMono } from './transform.js';
|
|
8
8
|
export { monomorphize, DEFAULT_MONO_OPTIONS, } from './mono.js';
|