@svelte-vitals/core 0.18.0 → 0.19.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 +58 -1
- package/dist/index.js +417 -2
- package/package.json +4 -1
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,58 @@ interface ComponentFacts {
|
|
|
250
257
|
name: string;
|
|
251
258
|
line: number;
|
|
252
259
|
}[];
|
|
260
|
+
/** 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
|
+
suppressions?: SuppressionDirective[];
|
|
253
262
|
}
|
|
254
263
|
|
|
264
|
+
/** Parse a component's reactivity/correctness + security + architecture facts (CLI/static + vite build mode). */
|
|
265
|
+
declare function parseComponentFacts(source: string, filename: string): {
|
|
266
|
+
eachBlocks: EachBlockFact[];
|
|
267
|
+
effects: EffectFact[];
|
|
268
|
+
htmlTags: SourceSpan[];
|
|
269
|
+
javascriptUrls: SourceSpan[];
|
|
270
|
+
loc: number;
|
|
271
|
+
propCount: number;
|
|
272
|
+
imports: string[];
|
|
273
|
+
namespaceImports: {
|
|
274
|
+
source: string;
|
|
275
|
+
line: number;
|
|
276
|
+
}[];
|
|
277
|
+
constableStates: {
|
|
278
|
+
name: string;
|
|
279
|
+
line: number;
|
|
280
|
+
}[];
|
|
281
|
+
suppressions: SuppressionDirective[];
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
type Node = any;
|
|
285
|
+
/**
|
|
286
|
+
* All keys that can bear child nodes in a Svelte AST node.
|
|
287
|
+
* Covers if/each/await blocks (pending/then/catch/fallback) as well as
|
|
288
|
+
* the standard fragment, nodes, consequent, alternate, and body keys.
|
|
289
|
+
*/
|
|
290
|
+
declare const CHILD_NODE_KEYS: string[];
|
|
291
|
+
/**
|
|
292
|
+
* Determine a value's kind from a list of child/text nodes (design §4, §11):
|
|
293
|
+
* - any ExpressionTag present → 'dynamic' (e.g. {data.title}); we do NOT
|
|
294
|
+
* follow the expression — that would turn this into runtime analysis.
|
|
295
|
+
* - non-whitespace Text only → 'static'
|
|
296
|
+
* - empty / whitespace only → 'absent'
|
|
297
|
+
*/
|
|
298
|
+
declare function valueFromNodes(nodes: Node[]): Value;
|
|
299
|
+
/** The literal text of a node list when fully static (no ExpressionTag), else undefined. */
|
|
300
|
+
declare function textFromNodes(nodes: Node[]): string | undefined;
|
|
301
|
+
/** Static string of an attribute (e.g. name="description"), or undefined if dynamic/absent. */
|
|
302
|
+
declare function attrText(attributes: Node[], name: string): string | undefined;
|
|
303
|
+
/** Value kind of an attribute's content (e.g. the `content` of a <meta>). */
|
|
304
|
+
declare function attrValue(attributes: Node[], name: string): Value;
|
|
305
|
+
declare function lineOf(source: string, offset: unknown): number;
|
|
306
|
+
declare function findAttr(attributes: Node[], name: string): Node | undefined;
|
|
307
|
+
/** Value kind of a single attribute (e.g. a component prop). */
|
|
308
|
+
declare function attrValueOf(attr: Node): Value;
|
|
309
|
+
/** Literal static text of a single attribute node (e.g. a component prop), or undefined if dynamic/absent. */
|
|
310
|
+
declare function attrTextOf(attr: Node): string | undefined;
|
|
311
|
+
|
|
255
312
|
/**
|
|
256
313
|
* Source-file locations that satisfy the project-scope rules, shared by every
|
|
257
314
|
* mode so the static (CLI) and rendered (plugin) collectors never drift. This
|
|
@@ -660,4 +717,4 @@ declare function selectRules(rules: Rule[], config: Config): Rule[];
|
|
|
660
717
|
/** Apply per-rule severity overrides to results (design §6). */
|
|
661
718
|
declare function applyRuleSeverities(results: Result[], config: Config): Result[];
|
|
662
719
|
|
|
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 };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,408 @@ 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" && !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 collectStateWrites(root, stateNames, acc) {
|
|
179
|
+
walkEstree(root, (n) => {
|
|
180
|
+
if (n?.type === "AssignmentExpression") {
|
|
181
|
+
if (n.left?.type === "Identifier" && stateNames.has(n.left.name)) acc.add(n.left.name);
|
|
182
|
+
else if (n.left?.type === "MemberExpression") {
|
|
183
|
+
const r = rootObjectName(n.left);
|
|
184
|
+
if (r && stateNames.has(r)) acc.add(r);
|
|
185
|
+
} else if (n.left?.type === "ObjectPattern" || n.left?.type === "ArrayPattern") {
|
|
186
|
+
const bound = /* @__PURE__ */ new Set();
|
|
187
|
+
addBoundNames(n.left, bound);
|
|
188
|
+
for (const name of bound) if (stateNames.has(name)) acc.add(name);
|
|
189
|
+
}
|
|
190
|
+
} else if (n?.type === "UpdateExpression") {
|
|
191
|
+
const r = rootObjectName(n.argument);
|
|
192
|
+
if (r && stateNames.has(r)) acc.add(r);
|
|
193
|
+
} else if (n?.type === "UnaryExpression" && n.operator === "delete") {
|
|
194
|
+
const r = rootObjectName(n.argument);
|
|
195
|
+
if (r && stateNames.has(r)) acc.add(r);
|
|
196
|
+
} else if (n?.type === "CallExpression") {
|
|
197
|
+
if (n.callee?.type === "MemberExpression") {
|
|
198
|
+
const r = rootObjectName(n.callee);
|
|
199
|
+
if (r && stateNames.has(r)) acc.add(r);
|
|
200
|
+
}
|
|
201
|
+
for (const a of n.arguments ?? []) {
|
|
202
|
+
const arg = a?.type === "SpreadElement" ? a.argument : a;
|
|
203
|
+
const r = rootObjectName(arg);
|
|
204
|
+
if (r && stateNames.has(r)) acc.add(r);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
var COMPONENT_LIKE_TYPES = /* @__PURE__ */ new Set(["Component", "SvelteComponent", "SvelteSelf"]);
|
|
210
|
+
function collectTemplateEscapes(node, stateNames, acc) {
|
|
211
|
+
if (Array.isArray(node)) {
|
|
212
|
+
for (const c of node) collectTemplateEscapes(c, stateNames, acc);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string") return;
|
|
216
|
+
if (Array.isArray(node.attributes)) {
|
|
217
|
+
for (const attr of node.attributes) {
|
|
218
|
+
if (attr?.type === "BindDirective") {
|
|
219
|
+
const r = rootObjectName(attr.expression);
|
|
220
|
+
if (r && stateNames.has(r)) acc.add(r);
|
|
221
|
+
} else if (COMPONENT_LIKE_TYPES.has(node.type)) {
|
|
222
|
+
walkEstree(attr, (m) => {
|
|
223
|
+
if (m?.type === "Identifier" && stateNames.has(m.name)) acc.add(m.name);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
for (const key of CHILD_NODE_KEYS) {
|
|
229
|
+
if (key in node) collectTemplateEscapes(node[key], stateNames, acc);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
var RUNE_NAMES = /* @__PURE__ */ new Set(["$state", "$derived", "$effect", "$props", "$bindable", "$inspect", "$host"]);
|
|
233
|
+
function bodyReadsReactive(fn, reactiveNames) {
|
|
234
|
+
let reads = false;
|
|
235
|
+
const IGNORED_KEYS = /* @__PURE__ */ new Set(["type", "start", "end", "loc", "range"]);
|
|
236
|
+
const visit = (n) => {
|
|
237
|
+
if (reads || !n) return;
|
|
238
|
+
if (Array.isArray(n)) {
|
|
239
|
+
for (const c of n) visit(c);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (typeof n !== "object" || typeof n.type !== "string") return;
|
|
243
|
+
if (n.type === "Identifier") {
|
|
244
|
+
if (reactiveNames.has(n.name) || n.name.startsWith("$") && !RUNE_NAMES.has(n.name)) reads = true;
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (n.type === "CallExpression" && n.callee?.type === "Identifier") {
|
|
248
|
+
reads = true;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (n.type === "MemberExpression") {
|
|
252
|
+
visit(n.object);
|
|
253
|
+
if (n.computed) visit(n.property);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (n.type === "Property") {
|
|
257
|
+
if (n.computed) visit(n.key);
|
|
258
|
+
visit(n.value);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
for (const key of Object.keys(n)) {
|
|
262
|
+
if (!IGNORED_KEYS.has(key)) visit(n[key]);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
visit(fn.body);
|
|
266
|
+
return reads;
|
|
267
|
+
}
|
|
268
|
+
function bodyIsEmpty(fn) {
|
|
269
|
+
const body = fn?.body;
|
|
270
|
+
if (!body) return true;
|
|
271
|
+
if (body.type === "BlockStatement") return (body.body ?? []).length === 0;
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
var URL_ATTRS = ["href", "src", "action", "formaction"];
|
|
275
|
+
function collectSecurityFacts(node, source, htmlTags, jsUrls) {
|
|
276
|
+
if (Array.isArray(node)) {
|
|
277
|
+
for (const child of node) collectSecurityFacts(child, source, htmlTags, jsUrls);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (!node || typeof node !== "object") return;
|
|
281
|
+
if (node.type === "HtmlTag") htmlTags.push({ line: lineOf(source, node.start) });
|
|
282
|
+
if ((node.type === "RegularElement" || node.type === "SvelteElement") && Array.isArray(node.attributes)) {
|
|
283
|
+
for (const name of URL_ATTRS) {
|
|
284
|
+
const attr = findAttr(node.attributes, name);
|
|
285
|
+
if (!attr) continue;
|
|
286
|
+
const value = attrTextOf(attr);
|
|
287
|
+
if (value !== void 0 && /^\s*javascript:/i.test(value)) {
|
|
288
|
+
jsUrls.push({ line: lineOf(source, attr.start ?? node.start) });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
for (const key of CHILD_NODE_KEYS) {
|
|
293
|
+
if (key in node) collectSecurityFacts(node[key], source, htmlTags, jsUrls);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function isPropsCall(node) {
|
|
297
|
+
return node?.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "$props";
|
|
298
|
+
}
|
|
299
|
+
function countProps(program) {
|
|
300
|
+
let count = 0;
|
|
301
|
+
let seen = 0;
|
|
302
|
+
let uncountable = false;
|
|
303
|
+
walkEstree(program, (n) => {
|
|
304
|
+
if (n.type !== "VariableDeclarator" || !n.init || !isPropsCall(n.init)) return;
|
|
305
|
+
seen++;
|
|
306
|
+
const props = n.id?.type === "ObjectPattern" ? n.id.properties : void 0;
|
|
307
|
+
if (!Array.isArray(props) || props.some((p) => p?.type === "RestElement")) {
|
|
308
|
+
uncountable = true;
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
count = props.filter((p) => p?.type === "Property").length;
|
|
312
|
+
});
|
|
313
|
+
return uncountable || seen > 1 ? 0 : count;
|
|
314
|
+
}
|
|
315
|
+
function countLines(source) {
|
|
316
|
+
if (source.length === 0) return 0;
|
|
317
|
+
return source.split("\n").length - (source.endsWith("\n") ? 1 : 0);
|
|
318
|
+
}
|
|
319
|
+
function collectImportSources(program, acc) {
|
|
320
|
+
walkEstree(program, (n) => {
|
|
321
|
+
if (n.type === "ImportDeclaration" && typeof n.source?.value === "string") acc.push(n.source.value);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
function isBareSpecifier(s) {
|
|
325
|
+
return !/^[./$#]/.test(s);
|
|
326
|
+
}
|
|
327
|
+
function collectNamespaceImports(program, source, acc) {
|
|
328
|
+
walkEstree(program, (n) => {
|
|
329
|
+
if (n.type !== "ImportDeclaration" || n.importKind === "type") return;
|
|
330
|
+
const spec = n.source?.value;
|
|
331
|
+
if (typeof spec !== "string" || !isBareSpecifier(spec)) return;
|
|
332
|
+
if (Array.isArray(n.specifiers) && n.specifiers.some((s) => s?.type === "ImportNamespaceSpecifier")) {
|
|
333
|
+
acc.push({ source: spec, line: lineOf(source, n.start) });
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
var JS_DIRECTIVE = /^\s*\/\/\s*svelte-vitals-disable-next-line(?:\s+([A-Za-z]+\d+(?:\s*,\s*[A-Za-z]+\d+)*))?\s*$/;
|
|
338
|
+
var HTML_DIRECTIVE = /^\s*<!--\s*svelte-vitals-disable-next-line(?:\s+([A-Za-z]+\d+(?:\s*,\s*[A-Za-z]+\d+)*))?\s*-->\s*$/;
|
|
339
|
+
function collectSuppressions(source) {
|
|
340
|
+
const out = [];
|
|
341
|
+
const lines = source.split("\n");
|
|
342
|
+
lines.forEach((line, i) => {
|
|
343
|
+
const m = JS_DIRECTIVE.exec(line) ?? HTML_DIRECTIVE.exec(line);
|
|
344
|
+
if (!m) return;
|
|
345
|
+
const ruleIds = m[1]?.split(",").map((s) => s.trim().toUpperCase());
|
|
346
|
+
out.push({ line: i + 2, ruleIds });
|
|
347
|
+
});
|
|
348
|
+
return out;
|
|
349
|
+
}
|
|
350
|
+
function parseComponentFacts(source, filename) {
|
|
351
|
+
const ast = parse(source, { modern: true, filename });
|
|
352
|
+
const eachBlocks = [];
|
|
353
|
+
collectEachBlocks(ast.fragment ?? ast, source, eachBlocks);
|
|
354
|
+
const htmlTags = [];
|
|
355
|
+
const javascriptUrls = [];
|
|
356
|
+
collectSecurityFacts(ast.fragment ?? ast, source, htmlTags, javascriptUrls);
|
|
357
|
+
const loc = countLines(source);
|
|
358
|
+
const suppressions = collectSuppressions(source);
|
|
359
|
+
const imports = [];
|
|
360
|
+
const namespaceImports = [];
|
|
361
|
+
if (ast.module?.content) {
|
|
362
|
+
collectImportSources(ast.module.content, imports);
|
|
363
|
+
collectNamespaceImports(ast.module.content, source, namespaceImports);
|
|
364
|
+
}
|
|
365
|
+
const effects = [];
|
|
366
|
+
const constableStates = [];
|
|
367
|
+
let propCount = 0;
|
|
368
|
+
const program = ast.instance?.content;
|
|
369
|
+
if (program) {
|
|
370
|
+
collectImportSources(program, imports);
|
|
371
|
+
collectNamespaceImports(program, source, namespaceImports);
|
|
372
|
+
propCount = countProps(program);
|
|
373
|
+
const stateNames = /* @__PURE__ */ new Set();
|
|
374
|
+
const reactiveNames = /* @__PURE__ */ new Set();
|
|
375
|
+
const stateDecls = [];
|
|
376
|
+
walkEstree(program, (n) => {
|
|
377
|
+
if (n.type !== "VariableDeclarator" || !n.init) return;
|
|
378
|
+
if (isStateDeclaration(n.init) && n.id?.type === "Identifier") {
|
|
379
|
+
stateNames.add(n.id.name);
|
|
380
|
+
stateDecls.push({ name: n.id.name, line: lineOf(source, n.start) });
|
|
381
|
+
}
|
|
382
|
+
if (isStateDeclaration(n.init) || isDerivedDeclaration(n.init) || isPropsCall(n.init))
|
|
383
|
+
addBoundNames(n.id, reactiveNames);
|
|
384
|
+
});
|
|
385
|
+
walkEstree(program, (n) => {
|
|
386
|
+
if (n.type !== "CallExpression" || !isEffectCall(n)) return;
|
|
387
|
+
const fn = n.arguments?.[0];
|
|
388
|
+
const isFn = fn?.type === "ArrowFunctionExpression" || fn?.type === "FunctionExpression";
|
|
389
|
+
effects.push({
|
|
390
|
+
line: lineOf(source, n.start),
|
|
391
|
+
assignsOnlyState: isFn ? bodyOnlyAssignsState(fn, stateNames) : false,
|
|
392
|
+
mountOnly: isFn ? !bodyIsEmpty(fn) && !bodyReadsReactive(fn, reactiveNames) : false
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
const writtenOrEscaped = /* @__PURE__ */ new Set();
|
|
396
|
+
collectStateWrites(program, stateNames, writtenOrEscaped);
|
|
397
|
+
if (ast.fragment) {
|
|
398
|
+
collectStateWrites(ast.fragment, stateNames, writtenOrEscaped);
|
|
399
|
+
collectTemplateEscapes(ast.fragment, stateNames, writtenOrEscaped);
|
|
400
|
+
}
|
|
401
|
+
for (const d of stateDecls) {
|
|
402
|
+
if (!writtenOrEscaped.has(d.name)) constableStates.push(d);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
eachBlocks,
|
|
407
|
+
effects,
|
|
408
|
+
htmlTags,
|
|
409
|
+
javascriptUrls,
|
|
410
|
+
loc,
|
|
411
|
+
propCount,
|
|
412
|
+
imports,
|
|
413
|
+
namespaceImports,
|
|
414
|
+
constableStates,
|
|
415
|
+
suppressions
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
17
419
|
// src/project-paths.ts
|
|
18
420
|
var ROBOTS_SOURCE_PATHS = [
|
|
19
421
|
"static/robots.txt",
|
|
@@ -1438,6 +1840,9 @@ var seo030HeadingOrder = {
|
|
|
1438
1840
|
// src/rules/component-rule.ts
|
|
1439
1841
|
var PENALIZED2 = { presence: "none", value: "absent" };
|
|
1440
1842
|
var PASS2 = { presence: "own", value: "static" };
|
|
1843
|
+
function isSuppressed(c, ruleId, line) {
|
|
1844
|
+
return (c.suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ruleId)));
|
|
1845
|
+
}
|
|
1441
1846
|
function componentRule(opts) {
|
|
1442
1847
|
const docsUrl7 = docsUrlFor(opts.id);
|
|
1443
1848
|
const severity = opts.severity ?? "warning";
|
|
@@ -1452,7 +1857,7 @@ function componentRule(opts) {
|
|
|
1452
1857
|
const out = [];
|
|
1453
1858
|
for (const c of ctx.components ?? []) {
|
|
1454
1859
|
if (!opts.applies(c)) continue;
|
|
1455
|
-
const bad = opts.bad(c);
|
|
1860
|
+
const bad = opts.bad(c).filter((b) => !(b.line > 0 && isSuppressed(c, opts.id, b.line)));
|
|
1456
1861
|
if (bad.length === 0) {
|
|
1457
1862
|
out.push({
|
|
1458
1863
|
id: opts.id,
|
|
@@ -2292,12 +2697,17 @@ function applyRuleSeverities(results, config) {
|
|
|
2292
2697
|
}
|
|
2293
2698
|
export {
|
|
2294
2699
|
BAND_COLOR,
|
|
2700
|
+
CHILD_NODE_KEYS,
|
|
2295
2701
|
ROBOTS_SOURCE_PATHS,
|
|
2296
2702
|
SITEMAP_SOURCE_PATHS,
|
|
2297
2703
|
allRules,
|
|
2298
2704
|
applyRuleSeverities,
|
|
2299
2705
|
arch001ComponentSize,
|
|
2300
2706
|
arch002PropCount,
|
|
2707
|
+
attrText,
|
|
2708
|
+
attrTextOf,
|
|
2709
|
+
attrValue,
|
|
2710
|
+
attrValueOf,
|
|
2301
2711
|
buildHtmlDocument,
|
|
2302
2712
|
buildJsonReport,
|
|
2303
2713
|
classify,
|
|
@@ -2314,6 +2724,7 @@ export {
|
|
|
2314
2724
|
effectiveSeverity,
|
|
2315
2725
|
escapeHtml,
|
|
2316
2726
|
explainRule,
|
|
2727
|
+
findAttr,
|
|
2317
2728
|
formatAgentReport,
|
|
2318
2729
|
formatConsoleReport,
|
|
2319
2730
|
formatGithubReport,
|
|
@@ -2324,8 +2735,10 @@ export {
|
|
|
2324
2735
|
headTagRule,
|
|
2325
2736
|
imageRule,
|
|
2326
2737
|
isPenalized,
|
|
2738
|
+
lineOf,
|
|
2327
2739
|
linkRule,
|
|
2328
2740
|
noColorPalette,
|
|
2741
|
+
parseComponentFacts,
|
|
2329
2742
|
perf001ImageDimensions,
|
|
2330
2743
|
perf002ImageLoading,
|
|
2331
2744
|
perf003PreloadAs,
|
|
@@ -2374,5 +2787,7 @@ export {
|
|
|
2374
2787
|
seo028TitleUnique,
|
|
2375
2788
|
seo029DescriptionUnique,
|
|
2376
2789
|
seo030HeadingOrder,
|
|
2377
|
-
summarize
|
|
2790
|
+
summarize,
|
|
2791
|
+
textFromNodes,
|
|
2792
|
+
valueFromNodes
|
|
2378
2793
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svelte-vitals/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Shared, runtime-agnostic core for svelte-vitals (types, rule engine, scorer, reporter).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
"node": ">=18.20.8"
|
|
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",
|