@svelte-vitals/core 0.19.0 → 0.20.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/index.d.ts +27 -1
- package/dist/index.js +172 -14
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -257,6 +257,11 @@ interface ComponentFacts {
|
|
|
257
257
|
name: string;
|
|
258
258
|
line: number;
|
|
259
259
|
}[];
|
|
260
|
+
/** Mutations of a non-`$bindable` prop from `$props()` — member writes, `delete`, or a mutating method call (CORRECT005). */
|
|
261
|
+
mutatedProps: {
|
|
262
|
+
name: string;
|
|
263
|
+
line: number;
|
|
264
|
+
}[];
|
|
260
265
|
/** Inline `svelte-vitals-disable-next-line` directives found in this file's source — component-rule escape hatch (issue #92). Optional: absent is equivalent to no directives, so existing external constructors of `ComponentFacts` are unaffected. */
|
|
261
266
|
suppressions?: SuppressionDirective[];
|
|
262
267
|
}
|
|
@@ -278,9 +283,28 @@ declare function parseComponentFacts(source: string, filename: string): {
|
|
|
278
283
|
name: string;
|
|
279
284
|
line: number;
|
|
280
285
|
}[];
|
|
286
|
+
mutatedProps: {
|
|
287
|
+
name: string;
|
|
288
|
+
line: number;
|
|
289
|
+
}[];
|
|
281
290
|
suppressions: SuppressionDirective[];
|
|
282
291
|
};
|
|
283
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Fallback facts for a file that fails to read or parse (dev tooling must never
|
|
295
|
+
* throw). This is the single source of truth for the empty-facts shape — add new
|
|
296
|
+
* `ComponentFacts` fields HERE so TypeScript catches every call site that still
|
|
297
|
+
* needs updating.
|
|
298
|
+
*/
|
|
299
|
+
declare function emptyComponentFacts(file: string): ComponentFacts;
|
|
300
|
+
/**
|
|
301
|
+
* Scan every `.svelte` component under `src/` for Correctness/Security/Architecture/
|
|
302
|
+
* Bundle-Performance facts. Independent of route resolution — covers `$lib` and
|
|
303
|
+
* non-route components too. A file that fails to read or parse contributes empty
|
|
304
|
+
* facts instead of aborting the whole scan (dev tooling must never throw).
|
|
305
|
+
*/
|
|
306
|
+
declare function collectComponentFacts(rt: Runtime, cwd: string): Promise<ComponentFacts[]>;
|
|
307
|
+
|
|
284
308
|
type Node = any;
|
|
285
309
|
/**
|
|
286
310
|
* All keys that can bear child nodes in a Svelte AST node.
|
|
@@ -483,6 +507,8 @@ declare const correct003EffectAsOnMount: Rule;
|
|
|
483
507
|
|
|
484
508
|
declare const correct004UnmutatedState: Rule;
|
|
485
509
|
|
|
510
|
+
declare const correct005PropMutation: Rule;
|
|
511
|
+
|
|
486
512
|
declare const sec001Html: Rule;
|
|
487
513
|
declare const sec002JavascriptUrl: Rule;
|
|
488
514
|
|
|
@@ -717,4 +743,4 @@ declare function selectRules(rules: Rule[], config: Config): Rule[];
|
|
|
717
743
|
/** Apply per-rule severity overrides to results (design §6). */
|
|
718
744
|
declare function applyRuleSeverities(results: Result[], config: Config): Result[];
|
|
719
745
|
|
|
720
|
-
export { BAND_COLOR, CHILD_NODE_KEYS, type Category, type Classification, type ComponentFacts, type Config, type ConsoleReportOptions, type Detection, type EachBlockFact, type EffectFact, type Fix, type HeadProvider, type HeadTag, type HeadingInfo, type HealthResult, type ImageInfo, type JsonReport, type Palette, type Presence, type Project, ROBOTS_SOURCE_PATHS, type ResolvedHead, type ResolvedHeadings, type ResolvedImages, type Result, type Rule, type RuleContext, type RuleInfo, type RuleSetting, type Runtime, SITEMAP_SOURCE_PATHS, type Scope, type ScoreModel, type ScoreOptions, type ScoreResult, type Severity, type SourceSpan, type Summary, type SuppressionDirective, type TreatDynamicAs, type Value, allRules, applyRuleSeverities, arch001ComponentSize, arch002PropCount, attrText, attrTextOf, attrValue, attrValueOf, buildHtmlDocument, buildJsonReport, classify, computeHealth, computeScore, correct001EachKey, correct002EffectDerived, correct003EffectAsOnMount, correct004UnmutatedState, defaultConfig, defaultProject, defineConfig, docsUrlFor, effectiveSeverity, escapeHtml, explainRule, findAttr, formatAgentReport, formatConsoleReport, formatGithubReport, formatHtmlReport, formatJsonReport, formatSarifReport, hasFailureAtOrAbove, headTagRule, imageRule, isPenalized, lineOf, linkRule, noColorPalette, parseComponentFacts, perf001ImageDimensions, perf002ImageLoading, perf003PreloadAs, perf004FontPreloadCrossorigin, perf005LcpImage, perf006ResponsiveImage, perf007RenderBlockingScript, perf008Preconnect, perf009HeavyImport, perf010NamespaceImport, runRules, safeHref, scoreBand, scoreColor, scoresByCategory, sec001Html, sec002JavascriptUrl, selectRules, seo001Title, seo002Description, seo003Canonical, seo004OgImage, seo005OgTitle, seo006Robots, seo007Sitemap, seo008JsonLd, seo009HtmlLang, seo010Indexability, seo011TwitterCard, seo012OgDescription, seo013OgUrl, seo014Viewport, seo015SitemapInRobots, seo016JsonLdValidity, seo017DeprecatedType, seo018RelativeUrl, seo019DateFormat, seo020Placeholder, seo021RequiredProps, seo022TitleLength, seo023DescriptionLength, seo024Charset, seo025ImageAlt, seo026Hreflang, seo027Heading, seo028TitleUnique, seo029DescriptionUnique, seo030HeadingOrder, summarize, textFromNodes, valueFromNodes };
|
|
746
|
+
export { BAND_COLOR, CHILD_NODE_KEYS, type Category, type Classification, type ComponentFacts, type Config, type ConsoleReportOptions, type Detection, type EachBlockFact, type EffectFact, type Fix, type HeadProvider, type HeadTag, type HeadingInfo, type HealthResult, type ImageInfo, type JsonReport, type Palette, type Presence, type Project, ROBOTS_SOURCE_PATHS, type ResolvedHead, type ResolvedHeadings, type ResolvedImages, type Result, type Rule, type RuleContext, type RuleInfo, type RuleSetting, type Runtime, SITEMAP_SOURCE_PATHS, type Scope, type ScoreModel, type ScoreOptions, type ScoreResult, type Severity, type SourceSpan, type Summary, type SuppressionDirective, type TreatDynamicAs, type Value, allRules, applyRuleSeverities, arch001ComponentSize, arch002PropCount, attrText, attrTextOf, attrValue, attrValueOf, buildHtmlDocument, buildJsonReport, classify, collectComponentFacts, computeHealth, computeScore, correct001EachKey, correct002EffectDerived, correct003EffectAsOnMount, correct004UnmutatedState, correct005PropMutation, defaultConfig, defaultProject, defineConfig, docsUrlFor, effectiveSeverity, emptyComponentFacts, escapeHtml, explainRule, findAttr, formatAgentReport, formatConsoleReport, formatGithubReport, formatHtmlReport, formatJsonReport, formatSarifReport, hasFailureAtOrAbove, headTagRule, imageRule, isPenalized, lineOf, linkRule, noColorPalette, parseComponentFacts, perf001ImageDimensions, perf002ImageLoading, perf003PreloadAs, perf004FontPreloadCrossorigin, perf005LcpImage, perf006ResponsiveImage, perf007RenderBlockingScript, perf008Preconnect, perf009HeavyImport, perf010NamespaceImport, runRules, safeHref, scoreBand, scoreColor, scoresByCategory, sec001Html, sec002JavascriptUrl, selectRules, seo001Title, seo002Description, seo003Canonical, seo004OgImage, seo005OgTitle, seo006Robots, seo007Sitemap, seo008JsonLd, seo009HtmlLang, seo010Indexability, seo011TwitterCard, seo012OgDescription, seo013OgUrl, seo014Viewport, seo015SitemapInRobots, seo016JsonLdValidity, seo017DeprecatedType, seo018RelativeUrl, seo019DateFormat, seo020Placeholder, seo021RequiredProps, seo022TitleLength, seo023DescriptionLength, seo024Charset, seo025ImageAlt, seo026Hreflang, seo027Heading, seo028TitleUnique, seo029DescriptionUnique, seo030HeadingOrder, summarize, textFromNodes, valueFromNodes };
|
package/dist/index.js
CHANGED
|
@@ -96,7 +96,7 @@ function collectEachBlocks(node, source, acc) {
|
|
|
96
96
|
return;
|
|
97
97
|
}
|
|
98
98
|
if (!node || typeof node !== "object") return;
|
|
99
|
-
if (node.type === "EachBlock" && !isConstantListEach(node)) {
|
|
99
|
+
if (node.type === "EachBlock" && node.context != null && !isConstantListEach(node)) {
|
|
100
100
|
acc.push({ hasKey: node.key != null, line: lineOf(source, node.start) });
|
|
101
101
|
}
|
|
102
102
|
for (const key of CHILD_NODE_KEYS) {
|
|
@@ -175,33 +175,71 @@ function rootObjectName(node) {
|
|
|
175
175
|
while (cur?.type === "MemberExpression") cur = cur.object;
|
|
176
176
|
return cur?.type === "Identifier" ? cur.name : void 0;
|
|
177
177
|
}
|
|
178
|
+
function scopeIntroducedNames(node) {
|
|
179
|
+
const introduced = /* @__PURE__ */ new Set();
|
|
180
|
+
if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
|
|
181
|
+
for (const p of node.params ?? []) addBoundNames(p, introduced);
|
|
182
|
+
} else if (node.type === "CatchClause") {
|
|
183
|
+
addBoundNames(node.param, introduced);
|
|
184
|
+
} else if (node.type === "BlockStatement") {
|
|
185
|
+
for (const stmt of node.body ?? []) {
|
|
186
|
+
if (stmt?.type === "VariableDeclaration" && stmt.kind !== "var") {
|
|
187
|
+
for (const d of stmt.declarations ?? []) addBoundNames(d.id, introduced);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
} else if (node.type === "ForStatement" || node.type === "ForOfStatement" || node.type === "ForInStatement") {
|
|
191
|
+
const decl = node.type === "ForStatement" ? node.init : node.left;
|
|
192
|
+
if (decl?.type === "VariableDeclaration") {
|
|
193
|
+
for (const d of decl.declarations ?? []) addBoundNames(d.id, introduced);
|
|
194
|
+
}
|
|
195
|
+
} else if (node.type === "EachBlock" && node.context) {
|
|
196
|
+
addBoundNames(node.context, introduced);
|
|
197
|
+
}
|
|
198
|
+
return introduced;
|
|
199
|
+
}
|
|
200
|
+
function walkScoped(node, visit, shadowed = /* @__PURE__ */ new Set()) {
|
|
201
|
+
if (Array.isArray(node)) {
|
|
202
|
+
for (const child of node) walkScoped(child, visit, shadowed);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string") return;
|
|
206
|
+
const introduced = scopeIntroducedNames(node);
|
|
207
|
+
const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
|
|
208
|
+
visit(node, scope);
|
|
209
|
+
for (const key of Object.keys(node)) {
|
|
210
|
+
if (key === "type" || key === "start" || key === "end" || key === "loc" || key === "range") continue;
|
|
211
|
+
walkScoped(node[key], visit, scope);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
178
214
|
function collectStateWrites(root, stateNames, acc) {
|
|
179
|
-
|
|
215
|
+
walkScoped(root, (n, scope) => {
|
|
216
|
+
const shadowed = (name) => name === void 0 || scope.has(name);
|
|
180
217
|
if (n?.type === "AssignmentExpression") {
|
|
181
|
-
if (n.left?.type === "Identifier" && stateNames.has(n.left.name)
|
|
182
|
-
|
|
218
|
+
if (n.left?.type === "Identifier" && stateNames.has(n.left.name) && !shadowed(n.left.name)) {
|
|
219
|
+
acc.add(n.left.name);
|
|
220
|
+
} else if (n.left?.type === "MemberExpression") {
|
|
183
221
|
const r = rootObjectName(n.left);
|
|
184
|
-
if (r && stateNames.has(r)) acc.add(r);
|
|
222
|
+
if (r && stateNames.has(r) && !shadowed(r)) acc.add(r);
|
|
185
223
|
} else if (n.left?.type === "ObjectPattern" || n.left?.type === "ArrayPattern") {
|
|
186
224
|
const bound = /* @__PURE__ */ new Set();
|
|
187
225
|
addBoundNames(n.left, bound);
|
|
188
|
-
for (const name of bound) if (stateNames.has(name)) acc.add(name);
|
|
226
|
+
for (const name of bound) if (stateNames.has(name) && !shadowed(name)) acc.add(name);
|
|
189
227
|
}
|
|
190
228
|
} else if (n?.type === "UpdateExpression") {
|
|
191
229
|
const r = rootObjectName(n.argument);
|
|
192
|
-
if (r && stateNames.has(r)) acc.add(r);
|
|
230
|
+
if (r && stateNames.has(r) && !shadowed(r)) acc.add(r);
|
|
193
231
|
} else if (n?.type === "UnaryExpression" && n.operator === "delete") {
|
|
194
232
|
const r = rootObjectName(n.argument);
|
|
195
|
-
if (r && stateNames.has(r)) acc.add(r);
|
|
233
|
+
if (r && stateNames.has(r) && !shadowed(r)) acc.add(r);
|
|
196
234
|
} else if (n?.type === "CallExpression") {
|
|
197
235
|
if (n.callee?.type === "MemberExpression") {
|
|
198
236
|
const r = rootObjectName(n.callee);
|
|
199
|
-
if (r && stateNames.has(r)) acc.add(r);
|
|
237
|
+
if (r && stateNames.has(r) && !shadowed(r)) acc.add(r);
|
|
200
238
|
}
|
|
201
239
|
for (const a of n.arguments ?? []) {
|
|
202
240
|
const arg = a?.type === "SpreadElement" ? a.argument : a;
|
|
203
241
|
const r = rootObjectName(arg);
|
|
204
|
-
if (r && stateNames.has(r)) acc.add(r);
|
|
242
|
+
if (r && stateNames.has(r) && !shadowed(r)) acc.add(r);
|
|
205
243
|
}
|
|
206
244
|
}
|
|
207
245
|
});
|
|
@@ -296,6 +334,71 @@ function collectSecurityFacts(node, source, htmlTags, jsUrls) {
|
|
|
296
334
|
function isPropsCall(node) {
|
|
297
335
|
return node?.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "$props";
|
|
298
336
|
}
|
|
337
|
+
function isBindableCall(node) {
|
|
338
|
+
return node?.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "$bindable";
|
|
339
|
+
}
|
|
340
|
+
function collectNonBindableProps(program) {
|
|
341
|
+
const names = /* @__PURE__ */ new Set();
|
|
342
|
+
let seen = 0;
|
|
343
|
+
let ambiguous = false;
|
|
344
|
+
walkEstree(program, (n) => {
|
|
345
|
+
if (n.type !== "VariableDeclarator" || !n.init || !isPropsCall(n.init)) return;
|
|
346
|
+
seen++;
|
|
347
|
+
if (n.id?.type === "Identifier") {
|
|
348
|
+
names.add(n.id.name);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (n.id?.type !== "ObjectPattern" || !Array.isArray(n.id.properties)) {
|
|
352
|
+
ambiguous = true;
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
for (const p of n.id.properties) {
|
|
356
|
+
if (p?.type === "RestElement") {
|
|
357
|
+
addBoundNames(p.argument, names);
|
|
358
|
+
} else if (p?.type === "Property") {
|
|
359
|
+
if (p.value?.type === "AssignmentPattern") {
|
|
360
|
+
if (!isBindableCall(p.value.right) && p.value.left?.type === "Identifier") names.add(p.value.left.name);
|
|
361
|
+
} else if (p.value?.type === "Identifier") {
|
|
362
|
+
names.add(p.value.name);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
return ambiguous || seen > 1 ? /* @__PURE__ */ new Set() : names;
|
|
368
|
+
}
|
|
369
|
+
var MUTATING_METHODS = /* @__PURE__ */ new Set([
|
|
370
|
+
"push",
|
|
371
|
+
"pop",
|
|
372
|
+
"shift",
|
|
373
|
+
"unshift",
|
|
374
|
+
"splice",
|
|
375
|
+
"sort",
|
|
376
|
+
"reverse",
|
|
377
|
+
"copyWithin",
|
|
378
|
+
"fill",
|
|
379
|
+
"set",
|
|
380
|
+
"add",
|
|
381
|
+
"delete",
|
|
382
|
+
"clear"
|
|
383
|
+
]);
|
|
384
|
+
function collectPropMutations(root, propNames, source, acc) {
|
|
385
|
+
if (propNames.size === 0) return;
|
|
386
|
+
walkScoped(root, (n, scope) => {
|
|
387
|
+
const flag = (r) => {
|
|
388
|
+
if (r && propNames.has(r) && !scope.has(r)) acc.push({ name: r, line: lineOf(source, n.start) });
|
|
389
|
+
};
|
|
390
|
+
if (n.type === "AssignmentExpression" && n.left?.type === "MemberExpression") {
|
|
391
|
+
flag(rootObjectName(n.left));
|
|
392
|
+
} else if (n.type === "UpdateExpression" && n.argument?.type === "MemberExpression") {
|
|
393
|
+
flag(rootObjectName(n.argument));
|
|
394
|
+
} else if (n.type === "UnaryExpression" && n.operator === "delete") {
|
|
395
|
+
flag(rootObjectName(n.argument));
|
|
396
|
+
} else if (n.type === "CallExpression" && n.callee?.type === "MemberExpression") {
|
|
397
|
+
const method = n.callee.property?.type === "Identifier" ? n.callee.property.name : void 0;
|
|
398
|
+
if (method && MUTATING_METHODS.has(method)) flag(rootObjectName(n.callee.object));
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
}
|
|
299
402
|
function countProps(program) {
|
|
300
403
|
let count = 0;
|
|
301
404
|
let seen = 0;
|
|
@@ -364,12 +467,16 @@ function parseComponentFacts(source, filename) {
|
|
|
364
467
|
}
|
|
365
468
|
const effects = [];
|
|
366
469
|
const constableStates = [];
|
|
470
|
+
const mutatedProps = [];
|
|
367
471
|
let propCount = 0;
|
|
368
472
|
const program = ast.instance?.content;
|
|
369
473
|
if (program) {
|
|
370
474
|
collectImportSources(program, imports);
|
|
371
475
|
collectNamespaceImports(program, source, namespaceImports);
|
|
372
476
|
propCount = countProps(program);
|
|
477
|
+
const nonBindableProps = collectNonBindableProps(program);
|
|
478
|
+
collectPropMutations(program, nonBindableProps, source, mutatedProps);
|
|
479
|
+
if (ast.fragment) collectPropMutations(ast.fragment, nonBindableProps, source, mutatedProps);
|
|
373
480
|
const stateNames = /* @__PURE__ */ new Set();
|
|
374
481
|
const reactiveNames = /* @__PURE__ */ new Set();
|
|
375
482
|
const stateDecls = [];
|
|
@@ -412,10 +519,42 @@ function parseComponentFacts(source, filename) {
|
|
|
412
519
|
imports,
|
|
413
520
|
namespaceImports,
|
|
414
521
|
constableStates,
|
|
522
|
+
mutatedProps,
|
|
415
523
|
suppressions
|
|
416
524
|
};
|
|
417
525
|
}
|
|
418
526
|
|
|
527
|
+
// src/component-collect.ts
|
|
528
|
+
function emptyComponentFacts(file) {
|
|
529
|
+
return {
|
|
530
|
+
file,
|
|
531
|
+
eachBlocks: [],
|
|
532
|
+
effects: [],
|
|
533
|
+
htmlTags: [],
|
|
534
|
+
javascriptUrls: [],
|
|
535
|
+
loc: 0,
|
|
536
|
+
propCount: 0,
|
|
537
|
+
imports: [],
|
|
538
|
+
namespaceImports: [],
|
|
539
|
+
constableStates: [],
|
|
540
|
+
mutatedProps: [],
|
|
541
|
+
suppressions: []
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
async function collectComponentFacts(rt, cwd) {
|
|
545
|
+
const files = await rt.glob("src/**/*.svelte", cwd);
|
|
546
|
+
return Promise.all(
|
|
547
|
+
files.sort().map(async (rel) => {
|
|
548
|
+
try {
|
|
549
|
+
const source = await rt.readFile(rt.join(cwd, rel));
|
|
550
|
+
return { file: rel, ...parseComponentFacts(source, rel) };
|
|
551
|
+
} catch {
|
|
552
|
+
return emptyComponentFacts(rel);
|
|
553
|
+
}
|
|
554
|
+
})
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
419
558
|
// src/project-paths.ts
|
|
420
559
|
var ROBOTS_SOURCE_PATHS = [
|
|
421
560
|
"static/robots.txt",
|
|
@@ -1898,7 +2037,7 @@ var correct001EachKey = componentRule({
|
|
|
1898
2037
|
category: "correctness",
|
|
1899
2038
|
label: "Keyed {#each}",
|
|
1900
2039
|
recommendation: "Add a key to the {#each} block, e.g. {#each items as item (item.id)}.",
|
|
1901
|
-
rationale: "An unkeyed {#each}
|
|
2040
|
+
rationale: "An unkeyed {#each} adds/removes nodes at the end and rewrites the data of the DOM nodes in between when the list reorders, so element state/focus sticks to positions instead of items; a key lets Svelte insert, move, and delete the right nodes instead.",
|
|
1902
2041
|
applies: (c) => c.eachBlocks.length > 0,
|
|
1903
2042
|
bad: (c) => c.eachBlocks.filter((e) => !e.hasKey).map((e) => ({ line: e.line, message: "{#each} block has no key" }))
|
|
1904
2043
|
});
|
|
@@ -1939,6 +2078,21 @@ var correct004UnmutatedState = componentRule({
|
|
|
1939
2078
|
}))
|
|
1940
2079
|
});
|
|
1941
2080
|
|
|
2081
|
+
// src/rules/correctness/correct005-prop-mutation.ts
|
|
2082
|
+
var correct005PropMutation = componentRule({
|
|
2083
|
+
id: "CORRECT005",
|
|
2084
|
+
title: "Mutated non-bindable prop",
|
|
2085
|
+
category: "correctness",
|
|
2086
|
+
label: "Prop mutation",
|
|
2087
|
+
recommendation: "Clone the value before mutating it, communicate the change via a callback prop, or declare the prop $bindable if the parent and child should share it.",
|
|
2088
|
+
rationale: "Svelte's docs say plainly: don't mutate props unless they are $bindable. A plain-object prop mutation is a silent no-op (the object isn't a state proxy); a reactive-state-proxy prop mutation works but triggers the ownership_invalid_mutation dev warning only when that code path actually runs. Neither is caught by the compiler, so this rule catches both statically.",
|
|
2089
|
+
applies: (c) => c.mutatedProps.length > 0,
|
|
2090
|
+
bad: (c) => c.mutatedProps.map((m) => ({
|
|
2091
|
+
line: m.line,
|
|
2092
|
+
message: `Prop "${m.name}" is mutated, but it is not declared $bindable`
|
|
2093
|
+
}))
|
|
2094
|
+
});
|
|
2095
|
+
|
|
1942
2096
|
// src/rules/security/sec001-002.ts
|
|
1943
2097
|
var sec001Html = componentRule({
|
|
1944
2098
|
id: "SEC001",
|
|
@@ -2022,8 +2176,8 @@ var perf010NamespaceImport = componentRule({
|
|
|
2022
2176
|
category: "performance",
|
|
2023
2177
|
severity: "info",
|
|
2024
2178
|
label: "No namespace imports",
|
|
2025
|
-
recommendation: "Use named imports (import { x } from 'pkg') instead of import * as
|
|
2026
|
-
rationale: "A namespace import (import * as X) forces the bundler to
|
|
2179
|
+
recommendation: "Use named imports (import { x } from 'pkg') instead of import * as X from 'pkg' \u2014 so the bundle reliably tree-shakes.",
|
|
2180
|
+
rationale: "A namespace import (import * as X) is only tree-shakeable while every access to X stays static; passing X around or indexing it dynamically forces the bundler to keep the whole module. Named imports are reliably shakeable and make the dependency surface explicit.",
|
|
2027
2181
|
applies: (c) => c.namespaceImports.length > 0,
|
|
2028
2182
|
bad: (c) => {
|
|
2029
2183
|
const minLine = /* @__PURE__ */ new Map();
|
|
@@ -2033,7 +2187,7 @@ var perf010NamespaceImport = componentRule({
|
|
|
2033
2187
|
}
|
|
2034
2188
|
return [...minLine.entries()].sort((a, b) => a[1] - b[1]).map(([source, line]) => ({
|
|
2035
2189
|
line,
|
|
2036
|
-
message: `Namespace import "* as \u2026 from '${source}'" \u2014 prefer named imports so the
|
|
2190
|
+
message: `Namespace import "import * as \u2026 from '${source}'" \u2014 prefer named imports so the bundle reliably tree-shakes`
|
|
2037
2191
|
}));
|
|
2038
2192
|
}
|
|
2039
2193
|
});
|
|
@@ -2082,6 +2236,7 @@ var allRules = [
|
|
|
2082
2236
|
correct002EffectDerived,
|
|
2083
2237
|
correct003EffectAsOnMount,
|
|
2084
2238
|
correct004UnmutatedState,
|
|
2239
|
+
correct005PropMutation,
|
|
2085
2240
|
sec001Html,
|
|
2086
2241
|
sec002JavascriptUrl,
|
|
2087
2242
|
arch001ComponentSize,
|
|
@@ -2711,17 +2866,20 @@ export {
|
|
|
2711
2866
|
buildHtmlDocument,
|
|
2712
2867
|
buildJsonReport,
|
|
2713
2868
|
classify,
|
|
2869
|
+
collectComponentFacts,
|
|
2714
2870
|
computeHealth,
|
|
2715
2871
|
computeScore,
|
|
2716
2872
|
correct001EachKey,
|
|
2717
2873
|
correct002EffectDerived,
|
|
2718
2874
|
correct003EffectAsOnMount,
|
|
2719
2875
|
correct004UnmutatedState,
|
|
2876
|
+
correct005PropMutation,
|
|
2720
2877
|
defaultConfig,
|
|
2721
2878
|
defaultProject,
|
|
2722
2879
|
defineConfig,
|
|
2723
2880
|
docsUrlFor,
|
|
2724
2881
|
effectiveSeverity,
|
|
2882
|
+
emptyComponentFacts,
|
|
2725
2883
|
escapeHtml,
|
|
2726
2884
|
explainRule,
|
|
2727
2885
|
findAttr,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svelte-vitals/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Shared, runtime-agnostic core for svelte-vitals (types, rule engine, scorer, reporter).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"homepage": "https://github.com/oekazuma/svelte-vitals#readme",
|
|
23
23
|
"engines": {
|
|
24
|
-
"node": ">=
|
|
24
|
+
"node": ">=22.13.0"
|
|
25
25
|
},
|
|
26
26
|
"sideEffects": false,
|
|
27
27
|
"dependencies": {
|