@svelte-vitals/core 0.18.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 +84 -1
- package/dist/index.js +579 -6
- package/package.json +5 -2
package/dist/index.d.ts
CHANGED
|
@@ -224,6 +224,13 @@ interface SourceSpan {
|
|
|
224
224
|
/** 1-based source line, or 0 if unknown. */
|
|
225
225
|
line: number;
|
|
226
226
|
}
|
|
227
|
+
/** An inline `svelte-vitals-disable-next-line` directive found in the component's source (issue #92). */
|
|
228
|
+
interface SuppressionDirective {
|
|
229
|
+
/** 1-based line the directive suppresses (the line immediately after the comment). */
|
|
230
|
+
line: number;
|
|
231
|
+
/** Rule ids suppressed on that line; undefined = suppress every rule on that line. */
|
|
232
|
+
ruleIds?: string[];
|
|
233
|
+
}
|
|
227
234
|
/** Reactivity/correctness + security + architecture facts parsed from one `.svelte` component. */
|
|
228
235
|
interface ComponentFacts {
|
|
229
236
|
/** Source file the component came from. */
|
|
@@ -250,8 +257,82 @@ interface ComponentFacts {
|
|
|
250
257
|
name: string;
|
|
251
258
|
line: number;
|
|
252
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
|
+
}[];
|
|
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. */
|
|
266
|
+
suppressions?: SuppressionDirective[];
|
|
253
267
|
}
|
|
254
268
|
|
|
269
|
+
/** Parse a component's reactivity/correctness + security + architecture facts (CLI/static + vite build mode). */
|
|
270
|
+
declare function parseComponentFacts(source: string, filename: string): {
|
|
271
|
+
eachBlocks: EachBlockFact[];
|
|
272
|
+
effects: EffectFact[];
|
|
273
|
+
htmlTags: SourceSpan[];
|
|
274
|
+
javascriptUrls: SourceSpan[];
|
|
275
|
+
loc: number;
|
|
276
|
+
propCount: number;
|
|
277
|
+
imports: string[];
|
|
278
|
+
namespaceImports: {
|
|
279
|
+
source: string;
|
|
280
|
+
line: number;
|
|
281
|
+
}[];
|
|
282
|
+
constableStates: {
|
|
283
|
+
name: string;
|
|
284
|
+
line: number;
|
|
285
|
+
}[];
|
|
286
|
+
mutatedProps: {
|
|
287
|
+
name: string;
|
|
288
|
+
line: number;
|
|
289
|
+
}[];
|
|
290
|
+
suppressions: SuppressionDirective[];
|
|
291
|
+
};
|
|
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
|
+
|
|
308
|
+
type Node = any;
|
|
309
|
+
/**
|
|
310
|
+
* All keys that can bear child nodes in a Svelte AST node.
|
|
311
|
+
* Covers if/each/await blocks (pending/then/catch/fallback) as well as
|
|
312
|
+
* the standard fragment, nodes, consequent, alternate, and body keys.
|
|
313
|
+
*/
|
|
314
|
+
declare const CHILD_NODE_KEYS: string[];
|
|
315
|
+
/**
|
|
316
|
+
* Determine a value's kind from a list of child/text nodes (design §4, §11):
|
|
317
|
+
* - any ExpressionTag present → 'dynamic' (e.g. {data.title}); we do NOT
|
|
318
|
+
* follow the expression — that would turn this into runtime analysis.
|
|
319
|
+
* - non-whitespace Text only → 'static'
|
|
320
|
+
* - empty / whitespace only → 'absent'
|
|
321
|
+
*/
|
|
322
|
+
declare function valueFromNodes(nodes: Node[]): Value;
|
|
323
|
+
/** The literal text of a node list when fully static (no ExpressionTag), else undefined. */
|
|
324
|
+
declare function textFromNodes(nodes: Node[]): string | undefined;
|
|
325
|
+
/** Static string of an attribute (e.g. name="description"), or undefined if dynamic/absent. */
|
|
326
|
+
declare function attrText(attributes: Node[], name: string): string | undefined;
|
|
327
|
+
/** Value kind of an attribute's content (e.g. the `content` of a <meta>). */
|
|
328
|
+
declare function attrValue(attributes: Node[], name: string): Value;
|
|
329
|
+
declare function lineOf(source: string, offset: unknown): number;
|
|
330
|
+
declare function findAttr(attributes: Node[], name: string): Node | undefined;
|
|
331
|
+
/** Value kind of a single attribute (e.g. a component prop). */
|
|
332
|
+
declare function attrValueOf(attr: Node): Value;
|
|
333
|
+
/** Literal static text of a single attribute node (e.g. a component prop), or undefined if dynamic/absent. */
|
|
334
|
+
declare function attrTextOf(attr: Node): string | undefined;
|
|
335
|
+
|
|
255
336
|
/**
|
|
256
337
|
* Source-file locations that satisfy the project-scope rules, shared by every
|
|
257
338
|
* mode so the static (CLI) and rendered (plugin) collectors never drift. This
|
|
@@ -426,6 +507,8 @@ declare const correct003EffectAsOnMount: Rule;
|
|
|
426
507
|
|
|
427
508
|
declare const correct004UnmutatedState: Rule;
|
|
428
509
|
|
|
510
|
+
declare const correct005PropMutation: Rule;
|
|
511
|
+
|
|
429
512
|
declare const sec001Html: Rule;
|
|
430
513
|
declare const sec002JavascriptUrl: Rule;
|
|
431
514
|
|
|
@@ -660,4 +743,4 @@ declare function selectRules(rules: Rule[], config: Config): Rule[];
|
|
|
660
743
|
/** Apply per-rule severity overrides to results (design §6). */
|
|
661
744
|
declare function applyRuleSeverities(results: Result[], config: Config): Result[];
|
|
662
745
|
|
|
663
|
-
export { BAND_COLOR, 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 TreatDynamicAs, type Value, allRules, applyRuleSeverities, arch001ComponentSize, arch002PropCount, buildHtmlDocument, buildJsonReport, classify, computeHealth, computeScore, correct001EachKey, correct002EffectDerived, correct003EffectAsOnMount, correct004UnmutatedState, defaultConfig, defaultProject, defineConfig, docsUrlFor, effectiveSeverity, escapeHtml, explainRule, formatAgentReport, formatConsoleReport, formatGithubReport, formatHtmlReport, formatJsonReport, formatSarifReport, hasFailureAtOrAbove, headTagRule, imageRule, isPenalized, linkRule, noColorPalette, 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 };
|
|
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
|
@@ -14,6 +14,547 @@ function defineConfig(config = {}) {
|
|
|
14
14
|
return { ...defaultConfig, ...config };
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// src/component-parse.ts
|
|
18
|
+
import { parse } from "svelte/compiler";
|
|
19
|
+
|
|
20
|
+
// src/svelte-ast.ts
|
|
21
|
+
var CHILD_NODE_KEYS = [
|
|
22
|
+
"fragment",
|
|
23
|
+
"nodes",
|
|
24
|
+
"consequent",
|
|
25
|
+
"alternate",
|
|
26
|
+
"body",
|
|
27
|
+
"pending",
|
|
28
|
+
"then",
|
|
29
|
+
"catch",
|
|
30
|
+
"fallback"
|
|
31
|
+
];
|
|
32
|
+
function valueFromNodes(nodes) {
|
|
33
|
+
if (!Array.isArray(nodes)) return "absent";
|
|
34
|
+
if (nodes.some((n) => n?.type === "ExpressionTag")) return "dynamic";
|
|
35
|
+
const text = nodes.filter((n) => n?.type === "Text").map((n) => String(n.data ?? "")).join("");
|
|
36
|
+
return text.trim().length > 0 ? "static" : "absent";
|
|
37
|
+
}
|
|
38
|
+
function textFromNodes(nodes) {
|
|
39
|
+
if (!Array.isArray(nodes) || nodes.some((n) => n?.type === "ExpressionTag")) return void 0;
|
|
40
|
+
const text = nodes.filter((n) => n?.type === "Text").map((n) => String(n.data ?? "")).join("");
|
|
41
|
+
return text.trim().length > 0 ? text : void 0;
|
|
42
|
+
}
|
|
43
|
+
function attrText(attributes, name) {
|
|
44
|
+
const attr = findAttr(attributes, name);
|
|
45
|
+
if (!attr) return void 0;
|
|
46
|
+
const v = attr.value;
|
|
47
|
+
if (v === true) return "";
|
|
48
|
+
if (Array.isArray(v)) {
|
|
49
|
+
if (v.some((n) => n?.type === "ExpressionTag")) return void 0;
|
|
50
|
+
return v.filter((n) => n?.type === "Text").map((n) => String(n.data ?? "")).join("");
|
|
51
|
+
}
|
|
52
|
+
return void 0;
|
|
53
|
+
}
|
|
54
|
+
function attrValue(attributes, name) {
|
|
55
|
+
const attr = findAttr(attributes, name);
|
|
56
|
+
if (!attr) return "absent";
|
|
57
|
+
const v = attr.value;
|
|
58
|
+
if (v === true) return "absent";
|
|
59
|
+
if (Array.isArray(v)) return valueFromNodes(v);
|
|
60
|
+
if (v && v.type === "ExpressionTag") return "dynamic";
|
|
61
|
+
return "absent";
|
|
62
|
+
}
|
|
63
|
+
function lineOf(source, offset) {
|
|
64
|
+
if (typeof offset !== "number" || offset < 0) return 0;
|
|
65
|
+
let line = 1;
|
|
66
|
+
const end = Math.min(offset, source.length);
|
|
67
|
+
for (let i = 0; i < end; i++) if (source[i] === "\n") line++;
|
|
68
|
+
return line;
|
|
69
|
+
}
|
|
70
|
+
function findAttr(attributes, name) {
|
|
71
|
+
if (!Array.isArray(attributes)) return void 0;
|
|
72
|
+
return attributes.find((a) => a?.type === "Attribute" && a.name === name);
|
|
73
|
+
}
|
|
74
|
+
function attrValueOf(attr) {
|
|
75
|
+
const v = attr?.value;
|
|
76
|
+
if (v === true) return "absent";
|
|
77
|
+
if (Array.isArray(v)) return valueFromNodes(v);
|
|
78
|
+
if (v && v.type === "ExpressionTag") return "dynamic";
|
|
79
|
+
return "absent";
|
|
80
|
+
}
|
|
81
|
+
function attrTextOf(attr) {
|
|
82
|
+
const v = attr?.value;
|
|
83
|
+
if (!Array.isArray(v) || v.some((n) => n?.type === "ExpressionTag")) return void 0;
|
|
84
|
+
const text = v.filter((n) => n?.type === "Text").map((n) => String(n.data ?? "")).join("");
|
|
85
|
+
return text.trim().length > 0 ? text : void 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/component-parse.ts
|
|
89
|
+
function isConstantListEach(node) {
|
|
90
|
+
const expr = node?.expression;
|
|
91
|
+
return expr?.type === "ArrayExpression" && Array.isArray(expr.elements) && !expr.elements.some((el) => el?.type === "SpreadElement");
|
|
92
|
+
}
|
|
93
|
+
function collectEachBlocks(node, source, acc) {
|
|
94
|
+
if (Array.isArray(node)) {
|
|
95
|
+
for (const child of node) collectEachBlocks(child, source, acc);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (!node || typeof node !== "object") return;
|
|
99
|
+
if (node.type === "EachBlock" && node.context != null && !isConstantListEach(node)) {
|
|
100
|
+
acc.push({ hasKey: node.key != null, line: lineOf(source, node.start) });
|
|
101
|
+
}
|
|
102
|
+
for (const key of CHILD_NODE_KEYS) {
|
|
103
|
+
if (key in node) collectEachBlocks(node[key], source, acc);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function walkEstree(node, visit) {
|
|
107
|
+
if (Array.isArray(node)) {
|
|
108
|
+
for (const child of node) walkEstree(child, visit);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string") return;
|
|
112
|
+
visit(node);
|
|
113
|
+
for (const key of Object.keys(node)) {
|
|
114
|
+
if (key === "type" || key === "start" || key === "end" || key === "loc" || key === "range") continue;
|
|
115
|
+
walkEstree(node[key], visit);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function isEffectCall(node) {
|
|
119
|
+
const c = node?.callee;
|
|
120
|
+
if (c?.type === "Identifier") return c.name === "$effect";
|
|
121
|
+
if (c?.type === "MemberExpression" && c.object?.type === "Identifier" && c.object.name === "$effect") {
|
|
122
|
+
return c.property?.type === "Identifier" && c.property.name === "pre";
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
function isStateDeclaration(node) {
|
|
127
|
+
const c = node?.callee;
|
|
128
|
+
if (c?.type === "Identifier") return c.name === "$state";
|
|
129
|
+
if (c?.type === "MemberExpression" && c.object?.type === "Identifier" && c.object.name === "$state") {
|
|
130
|
+
return c.property?.type === "Identifier" && (c.property.name === "raw" || c.property.name === "frozen");
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
function bodyOnlyAssignsState(fn, stateNames) {
|
|
135
|
+
const isStateAssign = (expr) => expr?.type === "AssignmentExpression" && expr.operator === "=" && expr.left?.type === "Identifier" && stateNames.has(expr.left.name);
|
|
136
|
+
const body = fn?.body;
|
|
137
|
+
if (!body) return false;
|
|
138
|
+
if (body.type !== "BlockStatement") return isStateAssign(body);
|
|
139
|
+
if (body.body.length === 0) return false;
|
|
140
|
+
return body.body.every((s) => s?.type === "ExpressionStatement" && isStateAssign(s.expression));
|
|
141
|
+
}
|
|
142
|
+
function isDerivedDeclaration(node) {
|
|
143
|
+
const c = node?.callee;
|
|
144
|
+
if (c?.type === "Identifier") return c.name === "$derived";
|
|
145
|
+
if (c?.type === "MemberExpression" && c.object?.type === "Identifier" && c.object.name === "$derived") {
|
|
146
|
+
return c.property?.type === "Identifier" && c.property.name === "by";
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
function addBoundNames(id, acc) {
|
|
151
|
+
if (!id) return;
|
|
152
|
+
switch (id.type) {
|
|
153
|
+
case "Identifier":
|
|
154
|
+
acc.add(id.name);
|
|
155
|
+
break;
|
|
156
|
+
case "ObjectPattern":
|
|
157
|
+
for (const p of id.properties ?? []) {
|
|
158
|
+
if (p?.type === "Property") addBoundNames(p.value, acc);
|
|
159
|
+
else if (p?.type === "RestElement") addBoundNames(p.argument, acc);
|
|
160
|
+
}
|
|
161
|
+
break;
|
|
162
|
+
case "ArrayPattern":
|
|
163
|
+
for (const el of id.elements ?? []) addBoundNames(el, acc);
|
|
164
|
+
break;
|
|
165
|
+
case "AssignmentPattern":
|
|
166
|
+
addBoundNames(id.left, acc);
|
|
167
|
+
break;
|
|
168
|
+
case "RestElement":
|
|
169
|
+
addBoundNames(id.argument, acc);
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function rootObjectName(node) {
|
|
174
|
+
let cur = node;
|
|
175
|
+
while (cur?.type === "MemberExpression") cur = cur.object;
|
|
176
|
+
return cur?.type === "Identifier" ? cur.name : void 0;
|
|
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
|
+
}
|
|
214
|
+
function collectStateWrites(root, stateNames, acc) {
|
|
215
|
+
walkScoped(root, (n, scope) => {
|
|
216
|
+
const shadowed = (name) => name === void 0 || scope.has(name);
|
|
217
|
+
if (n?.type === "AssignmentExpression") {
|
|
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") {
|
|
221
|
+
const r = rootObjectName(n.left);
|
|
222
|
+
if (r && stateNames.has(r) && !shadowed(r)) acc.add(r);
|
|
223
|
+
} else if (n.left?.type === "ObjectPattern" || n.left?.type === "ArrayPattern") {
|
|
224
|
+
const bound = /* @__PURE__ */ new Set();
|
|
225
|
+
addBoundNames(n.left, bound);
|
|
226
|
+
for (const name of bound) if (stateNames.has(name) && !shadowed(name)) acc.add(name);
|
|
227
|
+
}
|
|
228
|
+
} else if (n?.type === "UpdateExpression") {
|
|
229
|
+
const r = rootObjectName(n.argument);
|
|
230
|
+
if (r && stateNames.has(r) && !shadowed(r)) acc.add(r);
|
|
231
|
+
} else if (n?.type === "UnaryExpression" && n.operator === "delete") {
|
|
232
|
+
const r = rootObjectName(n.argument);
|
|
233
|
+
if (r && stateNames.has(r) && !shadowed(r)) acc.add(r);
|
|
234
|
+
} else if (n?.type === "CallExpression") {
|
|
235
|
+
if (n.callee?.type === "MemberExpression") {
|
|
236
|
+
const r = rootObjectName(n.callee);
|
|
237
|
+
if (r && stateNames.has(r) && !shadowed(r)) acc.add(r);
|
|
238
|
+
}
|
|
239
|
+
for (const a of n.arguments ?? []) {
|
|
240
|
+
const arg = a?.type === "SpreadElement" ? a.argument : a;
|
|
241
|
+
const r = rootObjectName(arg);
|
|
242
|
+
if (r && stateNames.has(r) && !shadowed(r)) acc.add(r);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
var COMPONENT_LIKE_TYPES = /* @__PURE__ */ new Set(["Component", "SvelteComponent", "SvelteSelf"]);
|
|
248
|
+
function collectTemplateEscapes(node, stateNames, acc) {
|
|
249
|
+
if (Array.isArray(node)) {
|
|
250
|
+
for (const c of node) collectTemplateEscapes(c, stateNames, acc);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string") return;
|
|
254
|
+
if (Array.isArray(node.attributes)) {
|
|
255
|
+
for (const attr of node.attributes) {
|
|
256
|
+
if (attr?.type === "BindDirective") {
|
|
257
|
+
const r = rootObjectName(attr.expression);
|
|
258
|
+
if (r && stateNames.has(r)) acc.add(r);
|
|
259
|
+
} else if (COMPONENT_LIKE_TYPES.has(node.type)) {
|
|
260
|
+
walkEstree(attr, (m) => {
|
|
261
|
+
if (m?.type === "Identifier" && stateNames.has(m.name)) acc.add(m.name);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
for (const key of CHILD_NODE_KEYS) {
|
|
267
|
+
if (key in node) collectTemplateEscapes(node[key], stateNames, acc);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
var RUNE_NAMES = /* @__PURE__ */ new Set(["$state", "$derived", "$effect", "$props", "$bindable", "$inspect", "$host"]);
|
|
271
|
+
function bodyReadsReactive(fn, reactiveNames) {
|
|
272
|
+
let reads = false;
|
|
273
|
+
const IGNORED_KEYS = /* @__PURE__ */ new Set(["type", "start", "end", "loc", "range"]);
|
|
274
|
+
const visit = (n) => {
|
|
275
|
+
if (reads || !n) return;
|
|
276
|
+
if (Array.isArray(n)) {
|
|
277
|
+
for (const c of n) visit(c);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (typeof n !== "object" || typeof n.type !== "string") return;
|
|
281
|
+
if (n.type === "Identifier") {
|
|
282
|
+
if (reactiveNames.has(n.name) || n.name.startsWith("$") && !RUNE_NAMES.has(n.name)) reads = true;
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (n.type === "CallExpression" && n.callee?.type === "Identifier") {
|
|
286
|
+
reads = true;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (n.type === "MemberExpression") {
|
|
290
|
+
visit(n.object);
|
|
291
|
+
if (n.computed) visit(n.property);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (n.type === "Property") {
|
|
295
|
+
if (n.computed) visit(n.key);
|
|
296
|
+
visit(n.value);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
for (const key of Object.keys(n)) {
|
|
300
|
+
if (!IGNORED_KEYS.has(key)) visit(n[key]);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
visit(fn.body);
|
|
304
|
+
return reads;
|
|
305
|
+
}
|
|
306
|
+
function bodyIsEmpty(fn) {
|
|
307
|
+
const body = fn?.body;
|
|
308
|
+
if (!body) return true;
|
|
309
|
+
if (body.type === "BlockStatement") return (body.body ?? []).length === 0;
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
var URL_ATTRS = ["href", "src", "action", "formaction"];
|
|
313
|
+
function collectSecurityFacts(node, source, htmlTags, jsUrls) {
|
|
314
|
+
if (Array.isArray(node)) {
|
|
315
|
+
for (const child of node) collectSecurityFacts(child, source, htmlTags, jsUrls);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (!node || typeof node !== "object") return;
|
|
319
|
+
if (node.type === "HtmlTag") htmlTags.push({ line: lineOf(source, node.start) });
|
|
320
|
+
if ((node.type === "RegularElement" || node.type === "SvelteElement") && Array.isArray(node.attributes)) {
|
|
321
|
+
for (const name of URL_ATTRS) {
|
|
322
|
+
const attr = findAttr(node.attributes, name);
|
|
323
|
+
if (!attr) continue;
|
|
324
|
+
const value = attrTextOf(attr);
|
|
325
|
+
if (value !== void 0 && /^\s*javascript:/i.test(value)) {
|
|
326
|
+
jsUrls.push({ line: lineOf(source, attr.start ?? node.start) });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
for (const key of CHILD_NODE_KEYS) {
|
|
331
|
+
if (key in node) collectSecurityFacts(node[key], source, htmlTags, jsUrls);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function isPropsCall(node) {
|
|
335
|
+
return node?.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "$props";
|
|
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
|
+
}
|
|
402
|
+
function countProps(program) {
|
|
403
|
+
let count = 0;
|
|
404
|
+
let seen = 0;
|
|
405
|
+
let uncountable = false;
|
|
406
|
+
walkEstree(program, (n) => {
|
|
407
|
+
if (n.type !== "VariableDeclarator" || !n.init || !isPropsCall(n.init)) return;
|
|
408
|
+
seen++;
|
|
409
|
+
const props = n.id?.type === "ObjectPattern" ? n.id.properties : void 0;
|
|
410
|
+
if (!Array.isArray(props) || props.some((p) => p?.type === "RestElement")) {
|
|
411
|
+
uncountable = true;
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
count = props.filter((p) => p?.type === "Property").length;
|
|
415
|
+
});
|
|
416
|
+
return uncountable || seen > 1 ? 0 : count;
|
|
417
|
+
}
|
|
418
|
+
function countLines(source) {
|
|
419
|
+
if (source.length === 0) return 0;
|
|
420
|
+
return source.split("\n").length - (source.endsWith("\n") ? 1 : 0);
|
|
421
|
+
}
|
|
422
|
+
function collectImportSources(program, acc) {
|
|
423
|
+
walkEstree(program, (n) => {
|
|
424
|
+
if (n.type === "ImportDeclaration" && typeof n.source?.value === "string") acc.push(n.source.value);
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
function isBareSpecifier(s) {
|
|
428
|
+
return !/^[./$#]/.test(s);
|
|
429
|
+
}
|
|
430
|
+
function collectNamespaceImports(program, source, acc) {
|
|
431
|
+
walkEstree(program, (n) => {
|
|
432
|
+
if (n.type !== "ImportDeclaration" || n.importKind === "type") return;
|
|
433
|
+
const spec = n.source?.value;
|
|
434
|
+
if (typeof spec !== "string" || !isBareSpecifier(spec)) return;
|
|
435
|
+
if (Array.isArray(n.specifiers) && n.specifiers.some((s) => s?.type === "ImportNamespaceSpecifier")) {
|
|
436
|
+
acc.push({ source: spec, line: lineOf(source, n.start) });
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
var JS_DIRECTIVE = /^\s*\/\/\s*svelte-vitals-disable-next-line(?:\s+([A-Za-z]+\d+(?:\s*,\s*[A-Za-z]+\d+)*))?\s*$/;
|
|
441
|
+
var HTML_DIRECTIVE = /^\s*<!--\s*svelte-vitals-disable-next-line(?:\s+([A-Za-z]+\d+(?:\s*,\s*[A-Za-z]+\d+)*))?\s*-->\s*$/;
|
|
442
|
+
function collectSuppressions(source) {
|
|
443
|
+
const out = [];
|
|
444
|
+
const lines = source.split("\n");
|
|
445
|
+
lines.forEach((line, i) => {
|
|
446
|
+
const m = JS_DIRECTIVE.exec(line) ?? HTML_DIRECTIVE.exec(line);
|
|
447
|
+
if (!m) return;
|
|
448
|
+
const ruleIds = m[1]?.split(",").map((s) => s.trim().toUpperCase());
|
|
449
|
+
out.push({ line: i + 2, ruleIds });
|
|
450
|
+
});
|
|
451
|
+
return out;
|
|
452
|
+
}
|
|
453
|
+
function parseComponentFacts(source, filename) {
|
|
454
|
+
const ast = parse(source, { modern: true, filename });
|
|
455
|
+
const eachBlocks = [];
|
|
456
|
+
collectEachBlocks(ast.fragment ?? ast, source, eachBlocks);
|
|
457
|
+
const htmlTags = [];
|
|
458
|
+
const javascriptUrls = [];
|
|
459
|
+
collectSecurityFacts(ast.fragment ?? ast, source, htmlTags, javascriptUrls);
|
|
460
|
+
const loc = countLines(source);
|
|
461
|
+
const suppressions = collectSuppressions(source);
|
|
462
|
+
const imports = [];
|
|
463
|
+
const namespaceImports = [];
|
|
464
|
+
if (ast.module?.content) {
|
|
465
|
+
collectImportSources(ast.module.content, imports);
|
|
466
|
+
collectNamespaceImports(ast.module.content, source, namespaceImports);
|
|
467
|
+
}
|
|
468
|
+
const effects = [];
|
|
469
|
+
const constableStates = [];
|
|
470
|
+
const mutatedProps = [];
|
|
471
|
+
let propCount = 0;
|
|
472
|
+
const program = ast.instance?.content;
|
|
473
|
+
if (program) {
|
|
474
|
+
collectImportSources(program, imports);
|
|
475
|
+
collectNamespaceImports(program, source, namespaceImports);
|
|
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);
|
|
480
|
+
const stateNames = /* @__PURE__ */ new Set();
|
|
481
|
+
const reactiveNames = /* @__PURE__ */ new Set();
|
|
482
|
+
const stateDecls = [];
|
|
483
|
+
walkEstree(program, (n) => {
|
|
484
|
+
if (n.type !== "VariableDeclarator" || !n.init) return;
|
|
485
|
+
if (isStateDeclaration(n.init) && n.id?.type === "Identifier") {
|
|
486
|
+
stateNames.add(n.id.name);
|
|
487
|
+
stateDecls.push({ name: n.id.name, line: lineOf(source, n.start) });
|
|
488
|
+
}
|
|
489
|
+
if (isStateDeclaration(n.init) || isDerivedDeclaration(n.init) || isPropsCall(n.init))
|
|
490
|
+
addBoundNames(n.id, reactiveNames);
|
|
491
|
+
});
|
|
492
|
+
walkEstree(program, (n) => {
|
|
493
|
+
if (n.type !== "CallExpression" || !isEffectCall(n)) return;
|
|
494
|
+
const fn = n.arguments?.[0];
|
|
495
|
+
const isFn = fn?.type === "ArrowFunctionExpression" || fn?.type === "FunctionExpression";
|
|
496
|
+
effects.push({
|
|
497
|
+
line: lineOf(source, n.start),
|
|
498
|
+
assignsOnlyState: isFn ? bodyOnlyAssignsState(fn, stateNames) : false,
|
|
499
|
+
mountOnly: isFn ? !bodyIsEmpty(fn) && !bodyReadsReactive(fn, reactiveNames) : false
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
const writtenOrEscaped = /* @__PURE__ */ new Set();
|
|
503
|
+
collectStateWrites(program, stateNames, writtenOrEscaped);
|
|
504
|
+
if (ast.fragment) {
|
|
505
|
+
collectStateWrites(ast.fragment, stateNames, writtenOrEscaped);
|
|
506
|
+
collectTemplateEscapes(ast.fragment, stateNames, writtenOrEscaped);
|
|
507
|
+
}
|
|
508
|
+
for (const d of stateDecls) {
|
|
509
|
+
if (!writtenOrEscaped.has(d.name)) constableStates.push(d);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
eachBlocks,
|
|
514
|
+
effects,
|
|
515
|
+
htmlTags,
|
|
516
|
+
javascriptUrls,
|
|
517
|
+
loc,
|
|
518
|
+
propCount,
|
|
519
|
+
imports,
|
|
520
|
+
namespaceImports,
|
|
521
|
+
constableStates,
|
|
522
|
+
mutatedProps,
|
|
523
|
+
suppressions
|
|
524
|
+
};
|
|
525
|
+
}
|
|
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
|
+
|
|
17
558
|
// src/project-paths.ts
|
|
18
559
|
var ROBOTS_SOURCE_PATHS = [
|
|
19
560
|
"static/robots.txt",
|
|
@@ -1438,6 +1979,9 @@ var seo030HeadingOrder = {
|
|
|
1438
1979
|
// src/rules/component-rule.ts
|
|
1439
1980
|
var PENALIZED2 = { presence: "none", value: "absent" };
|
|
1440
1981
|
var PASS2 = { presence: "own", value: "static" };
|
|
1982
|
+
function isSuppressed(c, ruleId, line) {
|
|
1983
|
+
return (c.suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ruleId)));
|
|
1984
|
+
}
|
|
1441
1985
|
function componentRule(opts) {
|
|
1442
1986
|
const docsUrl7 = docsUrlFor(opts.id);
|
|
1443
1987
|
const severity = opts.severity ?? "warning";
|
|
@@ -1452,7 +1996,7 @@ function componentRule(opts) {
|
|
|
1452
1996
|
const out = [];
|
|
1453
1997
|
for (const c of ctx.components ?? []) {
|
|
1454
1998
|
if (!opts.applies(c)) continue;
|
|
1455
|
-
const bad = opts.bad(c);
|
|
1999
|
+
const bad = opts.bad(c).filter((b) => !(b.line > 0 && isSuppressed(c, opts.id, b.line)));
|
|
1456
2000
|
if (bad.length === 0) {
|
|
1457
2001
|
out.push({
|
|
1458
2002
|
id: opts.id,
|
|
@@ -1493,7 +2037,7 @@ var correct001EachKey = componentRule({
|
|
|
1493
2037
|
category: "correctness",
|
|
1494
2038
|
label: "Keyed {#each}",
|
|
1495
2039
|
recommendation: "Add a key to the {#each} block, e.g. {#each items as item (item.id)}.",
|
|
1496
|
-
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.",
|
|
1497
2041
|
applies: (c) => c.eachBlocks.length > 0,
|
|
1498
2042
|
bad: (c) => c.eachBlocks.filter((e) => !e.hasKey).map((e) => ({ line: e.line, message: "{#each} block has no key" }))
|
|
1499
2043
|
});
|
|
@@ -1534,6 +2078,21 @@ var correct004UnmutatedState = componentRule({
|
|
|
1534
2078
|
}))
|
|
1535
2079
|
});
|
|
1536
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
|
+
|
|
1537
2096
|
// src/rules/security/sec001-002.ts
|
|
1538
2097
|
var sec001Html = componentRule({
|
|
1539
2098
|
id: "SEC001",
|
|
@@ -1617,8 +2176,8 @@ var perf010NamespaceImport = componentRule({
|
|
|
1617
2176
|
category: "performance",
|
|
1618
2177
|
severity: "info",
|
|
1619
2178
|
label: "No namespace imports",
|
|
1620
|
-
recommendation: "Use named imports (import { x } from 'pkg') instead of import * as
|
|
1621
|
-
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.",
|
|
1622
2181
|
applies: (c) => c.namespaceImports.length > 0,
|
|
1623
2182
|
bad: (c) => {
|
|
1624
2183
|
const minLine = /* @__PURE__ */ new Map();
|
|
@@ -1628,7 +2187,7 @@ var perf010NamespaceImport = componentRule({
|
|
|
1628
2187
|
}
|
|
1629
2188
|
return [...minLine.entries()].sort((a, b) => a[1] - b[1]).map(([source, line]) => ({
|
|
1630
2189
|
line,
|
|
1631
|
-
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`
|
|
1632
2191
|
}));
|
|
1633
2192
|
}
|
|
1634
2193
|
});
|
|
@@ -1677,6 +2236,7 @@ var allRules = [
|
|
|
1677
2236
|
correct002EffectDerived,
|
|
1678
2237
|
correct003EffectAsOnMount,
|
|
1679
2238
|
correct004UnmutatedState,
|
|
2239
|
+
correct005PropMutation,
|
|
1680
2240
|
sec001Html,
|
|
1681
2241
|
sec002JavascriptUrl,
|
|
1682
2242
|
arch001ComponentSize,
|
|
@@ -2292,28 +2852,37 @@ function applyRuleSeverities(results, config) {
|
|
|
2292
2852
|
}
|
|
2293
2853
|
export {
|
|
2294
2854
|
BAND_COLOR,
|
|
2855
|
+
CHILD_NODE_KEYS,
|
|
2295
2856
|
ROBOTS_SOURCE_PATHS,
|
|
2296
2857
|
SITEMAP_SOURCE_PATHS,
|
|
2297
2858
|
allRules,
|
|
2298
2859
|
applyRuleSeverities,
|
|
2299
2860
|
arch001ComponentSize,
|
|
2300
2861
|
arch002PropCount,
|
|
2862
|
+
attrText,
|
|
2863
|
+
attrTextOf,
|
|
2864
|
+
attrValue,
|
|
2865
|
+
attrValueOf,
|
|
2301
2866
|
buildHtmlDocument,
|
|
2302
2867
|
buildJsonReport,
|
|
2303
2868
|
classify,
|
|
2869
|
+
collectComponentFacts,
|
|
2304
2870
|
computeHealth,
|
|
2305
2871
|
computeScore,
|
|
2306
2872
|
correct001EachKey,
|
|
2307
2873
|
correct002EffectDerived,
|
|
2308
2874
|
correct003EffectAsOnMount,
|
|
2309
2875
|
correct004UnmutatedState,
|
|
2876
|
+
correct005PropMutation,
|
|
2310
2877
|
defaultConfig,
|
|
2311
2878
|
defaultProject,
|
|
2312
2879
|
defineConfig,
|
|
2313
2880
|
docsUrlFor,
|
|
2314
2881
|
effectiveSeverity,
|
|
2882
|
+
emptyComponentFacts,
|
|
2315
2883
|
escapeHtml,
|
|
2316
2884
|
explainRule,
|
|
2885
|
+
findAttr,
|
|
2317
2886
|
formatAgentReport,
|
|
2318
2887
|
formatConsoleReport,
|
|
2319
2888
|
formatGithubReport,
|
|
@@ -2324,8 +2893,10 @@ export {
|
|
|
2324
2893
|
headTagRule,
|
|
2325
2894
|
imageRule,
|
|
2326
2895
|
isPenalized,
|
|
2896
|
+
lineOf,
|
|
2327
2897
|
linkRule,
|
|
2328
2898
|
noColorPalette,
|
|
2899
|
+
parseComponentFacts,
|
|
2329
2900
|
perf001ImageDimensions,
|
|
2330
2901
|
perf002ImageLoading,
|
|
2331
2902
|
perf003PreloadAs,
|
|
@@ -2374,5 +2945,7 @@ export {
|
|
|
2374
2945
|
seo028TitleUnique,
|
|
2375
2946
|
seo029DescriptionUnique,
|
|
2376
2947
|
seo030HeadingOrder,
|
|
2377
|
-
summarize
|
|
2948
|
+
summarize,
|
|
2949
|
+
textFromNodes,
|
|
2950
|
+
valueFromNodes
|
|
2378
2951
|
};
|
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,9 +21,12 @@
|
|
|
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
|
+
"dependencies": {
|
|
28
|
+
"svelte": "^5.56.4"
|
|
29
|
+
},
|
|
27
30
|
"exports": {
|
|
28
31
|
".": {
|
|
29
32
|
"types": "./dist/index.d.ts",
|