@svelte-vitals/core 0.11.0 → 0.12.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 CHANGED
@@ -105,6 +105,12 @@ interface HeadTag {
105
105
  property?: string;
106
106
  /** <link rel="...">. */
107
107
  rel?: string;
108
+ /** <link as="..."> keyword (e.g. 'font') when statically literal; undefined when absent or dynamically bound. */
109
+ as?: string;
110
+ /** True when a <link> has an `as` attribute at all (literal or dynamic). Distinguishes "no as" from "dynamic as". */
111
+ hasAs?: boolean;
112
+ /** True when a <link> has a `crossorigin` attribute (presence only; value is irrelevant to the checks). */
113
+ hasCrossorigin?: boolean;
108
114
  /** Where this tag was set relative to the route. Never 'none' (absence = no tag). */
109
115
  presence: Exclude<Presence, 'none'>;
110
116
  /** Whether the tag's value is static/dynamic/absent (design §4). */
@@ -229,6 +235,9 @@ declare const seo009HtmlLang: Rule;
229
235
  declare const perf001ImageDimensions: Rule;
230
236
  declare const perf002ImageLoading: Rule;
231
237
 
238
+ declare const perf003PreloadAs: Rule;
239
+ declare const perf004FontPreloadCrossorigin: Rule;
240
+
232
241
  declare const allRules: Rule[];
233
242
 
234
243
  interface RuleInfo {
@@ -275,6 +284,23 @@ interface ImageRuleOptions {
275
284
  /** Build a route-scoped Performance rule that checks each <img> against `ok` (issue #10). */
276
285
  declare function imageRule(opts: ImageRuleOptions): Rule;
277
286
 
287
+ interface LinkRuleOptions {
288
+ id: string;
289
+ title: string;
290
+ severity: Severity;
291
+ /** Noun phrase for messages, e.g. '`as` on a preloaded `<link>`'. */
292
+ label: string;
293
+ recommendation: string;
294
+ rationale: string;
295
+ fix?: Fix;
296
+ /** Which link tags this rule evaluates (e.g. rel === 'preload'). */
297
+ relevant: (tag: HeadTag) => boolean;
298
+ /** Returns true when a relevant link satisfies the rule (passes). */
299
+ ok: (tag: HeadTag) => boolean;
300
+ }
301
+ /** Build a route-scoped Performance rule that checks each relevant <link> in the effective head. */
302
+ declare function linkRule(opts: LinkRuleOptions): Rule;
303
+
278
304
  interface Summary {
279
305
  critical: number;
280
306
  warning: number;
@@ -411,4 +437,4 @@ declare function selectRules(rules: Rule[], config: Config): Rule[];
411
437
  /** Apply per-rule severity overrides to results (design §6). */
412
438
  declare function applyRuleSeverities(results: Result[], config: Config): Result[];
413
439
 
414
- export { BAND_COLOR, type Category, type Classification, type Config, type ConsoleReportOptions, type Detection, type Fix, type HeadProvider, type HeadTag, type HealthResult, type ImageInfo, type JsonReport, type Presence, type Project, ROBOTS_SOURCE_PATHS, type ResolvedHead, 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 Summary, type TreatDynamicAs, type Value, allRules, applyRuleSeverities, buildHtmlDocument, buildJsonReport, classify, computeHealth, computeScore, defaultConfig, defaultProject, defineConfig, docsUrlFor, effectiveSeverity, escapeHtml, explainRule, formatAgentReport, formatConsoleReport, formatGithubReport, formatHtmlReport, formatJsonReport, formatSarifReport, hasFailureAtOrAbove, headTagRule, imageRule, isPenalized, perf001ImageDimensions, perf002ImageLoading, runRules, safeHref, scoreBand, scoresByCategory, selectRules, seo001Title, seo002Description, seo003Canonical, seo004OgImage, seo005OgTitle, seo006Robots, seo007Sitemap, seo008JsonLd, seo009HtmlLang, summarize };
440
+ export { BAND_COLOR, type Category, type Classification, type Config, type ConsoleReportOptions, type Detection, type Fix, type HeadProvider, type HeadTag, type HealthResult, type ImageInfo, type JsonReport, type Presence, type Project, ROBOTS_SOURCE_PATHS, type ResolvedHead, 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 Summary, type TreatDynamicAs, type Value, allRules, applyRuleSeverities, buildHtmlDocument, buildJsonReport, classify, computeHealth, computeScore, defaultConfig, defaultProject, defineConfig, docsUrlFor, effectiveSeverity, escapeHtml, explainRule, formatAgentReport, formatConsoleReport, formatGithubReport, formatHtmlReport, formatJsonReport, formatSarifReport, hasFailureAtOrAbove, headTagRule, imageRule, isPenalized, linkRule, perf001ImageDimensions, perf002ImageLoading, perf003PreloadAs, perf004FontPreloadCrossorigin, runRules, safeHref, scoreBand, scoresByCategory, selectRules, seo001Title, seo002Description, seo003Canonical, seo004OgImage, seo005OgTitle, seo006Robots, seo007Sitemap, seo008JsonLd, seo009HtmlLang, summarize };
package/dist/index.js CHANGED
@@ -373,6 +373,91 @@ var perf002ImageLoading = imageRule({
373
373
  ok: (img) => img.hasLoading
374
374
  });
375
375
 
376
+ // src/rules/perf/link-rule.ts
377
+ function linkRule(opts) {
378
+ const docsUrl = docsUrlFor(opts.id);
379
+ return {
380
+ id: opts.id,
381
+ title: opts.title,
382
+ category: "performance",
383
+ severity: opts.severity,
384
+ scope: "route",
385
+ rationale: opts.rationale,
386
+ ...opts.fix ? { fix: opts.fix } : {},
387
+ async check(ctx) {
388
+ const out = [];
389
+ for (const head of ctx.heads) {
390
+ const links = head.tags.filter((t) => t.kind === "link" && opts.relevant(t));
391
+ if (links.length === 0) continue;
392
+ const bad = links.filter((t) => !opts.ok(t));
393
+ if (bad.length === 0) {
394
+ out.push({
395
+ id: opts.id,
396
+ category: "performance",
397
+ severity: opts.severity,
398
+ detection: { presence: "own", value: "static" },
399
+ route: head.route,
400
+ message: opts.label,
401
+ recommendation: opts.recommendation,
402
+ docsUrl
403
+ });
404
+ continue;
405
+ }
406
+ for (const tag of bad) {
407
+ out.push({
408
+ id: opts.id,
409
+ category: "performance",
410
+ severity: opts.severity,
411
+ detection: { presence: "none", value: "absent" },
412
+ route: head.route,
413
+ // Point at the file the link actually came from (a layout in static
414
+ // mode); fall back to the route's representative file when the tag
415
+ // carries no file (rendered mode).
416
+ location: tag.file ?? head.file,
417
+ message: `Missing ${opts.label}`,
418
+ recommendation: opts.recommendation,
419
+ docsUrl,
420
+ ...opts.fix ? { fix: { ...opts.fix } } : {}
421
+ });
422
+ }
423
+ }
424
+ return out;
425
+ }
426
+ };
427
+ }
428
+
429
+ // src/rules/perf/resource-hints.ts
430
+ var perf003PreloadAs = linkRule({
431
+ id: "PERF003",
432
+ title: "Preload missing as",
433
+ severity: "warning",
434
+ label: "`as` on a preloaded `<link>`",
435
+ recommendation: 'Add an `as` attribute to every `<link rel="preload">` so the browser knows the resource type and can prioritize it.',
436
+ rationale: 'A `<link rel="preload">` without an `as` attribute is ignored by the browser (or fetched a second time), wasting the preload.',
437
+ fix: {
438
+ description: "Add an `as` attribute matching the resource type to the preload link.",
439
+ snippet: '<link rel="preload" href="/app.css" as="style" />',
440
+ lang: "html"
441
+ },
442
+ relevant: (t) => t.rel === "preload",
443
+ ok: (t) => t.hasAs === true
444
+ });
445
+ var perf004FontPreloadCrossorigin = linkRule({
446
+ id: "PERF004",
447
+ title: "Font preload missing crossorigin",
448
+ severity: "warning",
449
+ label: "`crossorigin` on a font preload",
450
+ recommendation: 'Add `crossorigin` to `<link rel="preload" as="font">` \u2014 fonts are fetched in CORS mode, so without it the preload fetches a second, unused copy.',
451
+ rationale: "A font preload without `crossorigin` does not match the actual (CORS) font request, so the preloaded file is never used and the font downloads twice.",
452
+ fix: {
453
+ description: "Add the `crossorigin` attribute to the font preload link.",
454
+ snippet: '<link rel="preload" href="/inter.woff2" as="font" type="font/woff2" crossorigin />',
455
+ lang: "html"
456
+ },
457
+ relevant: (t) => t.rel === "preload" && t.as === "font",
458
+ ok: (t) => t.hasCrossorigin === true
459
+ });
460
+
376
461
  // src/rules/index.ts
377
462
  var allRules = [
378
463
  seo001Title,
@@ -385,7 +470,9 @@ var allRules = [
385
470
  seo008JsonLd,
386
471
  seo009HtmlLang,
387
472
  perf001ImageDimensions,
388
- perf002ImageLoading
473
+ perf002ImageLoading,
474
+ perf003PreloadAs,
475
+ perf004FontPreloadCrossorigin
389
476
  ];
390
477
  function explainRule(id) {
391
478
  const target = id.toUpperCase();
@@ -993,8 +1080,11 @@ export {
993
1080
  headTagRule,
994
1081
  imageRule,
995
1082
  isPenalized,
1083
+ linkRule,
996
1084
  perf001ImageDimensions,
997
1085
  perf002ImageLoading,
1086
+ perf003PreloadAs,
1087
+ perf004FontPreloadCrossorigin,
998
1088
  runRules,
999
1089
  safeHref,
1000
1090
  scoreBand,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svelte-vitals/core",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Shared, runtime-agnostic core for svelte-vitals (types, rule engine, scorer, reporter).",
5
5
  "type": "module",
6
6
  "license": "MIT",