@svelte-vitals/core 0.1.0 → 0.2.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
@@ -15,6 +15,14 @@ interface Detection {
15
15
  presence: Presence;
16
16
  value: Value;
17
17
  }
18
+ /** Project-wide facts precomputed by the runtime layer for project-scope rules (design §10). */
19
+ interface Project {
20
+ hasRobotsTxt: boolean;
21
+ hasSitemap: boolean;
22
+ /** <html lang> from app.html: presence 'own' when the attribute exists ('none' otherwise); value 'static' if non-empty, 'absent' if empty. */
23
+ htmlLang: Detection;
24
+ }
25
+ declare const defaultProject: Project;
18
26
  /** A single rule finding for one route (or the whole project). */
19
27
  interface Result {
20
28
  /** Rule id, e.g. 'SEO001'. */
@@ -33,10 +41,16 @@ type Scope = 'route' | 'project';
33
41
  type Category = 'seo' | 'performance' | 'a11y' | 'maintainability';
34
42
  /** How dynamic (`{data.title}`) values are treated by scoring (design §4, §12). */
35
43
  type TreatDynamicAs = 'pass' | 'warn' | 'fail';
44
+ /** Per-rule override: disable, or change severity. */
45
+ type RuleSetting = 'off' | Severity;
36
46
  interface Config {
37
47
  treatDynamicAs: TreatDynamicAs;
38
48
  /** Component names treated as meta sources of unknown content (design §11 layer 4). */
39
49
  metaComponents: string[];
50
+ /** Per-rule overrides keyed by rule id (design §6). */
51
+ rules: Record<string, RuleSetting>;
52
+ /** Minimum severity that fails the run / CI (design §6). */
53
+ failOn: Severity;
40
54
  }
41
55
  declare const defaultConfig: Config;
42
56
  /** Merge user config over defaults. Identity helper for config files (design §6). */
@@ -100,6 +114,7 @@ interface HeadProvider {
100
114
  /** Input given to every rule. Mode-independent: rules see only ResolvedHead[] (design §8, §10). */
101
115
  interface RuleContext {
102
116
  heads: ResolvedHead[];
117
+ project: Project;
103
118
  config: Config;
104
119
  }
105
120
  interface Rule {
@@ -122,7 +137,7 @@ interface Rule {
122
137
  *
123
138
  * presence 'none' → penalized (nothing set anywhere)
124
139
  * value 'absent' → penalized (tag present but empty)
125
- * value 'dynamic' → penalized only when treatDynamicAs is 'fail'
140
+ * value 'dynamic' → penalized when treatDynamicAs is not 'pass' (warn or fail)
126
141
  * otherwise (static/inherited) → not penalized
127
142
  */
128
143
  declare function isPenalized(detection: Detection, treatDynamicAs: TreatDynamicAs): boolean;
@@ -141,9 +156,31 @@ declare function runRules(rules: Rule[], ctx: RuleContext): Promise<Result[]>;
141
156
  */
142
157
  declare const seo001Title: Rule;
143
158
 
144
- /** Rule registry. Slice 0 ships SEO001 only; later slices append here. */
159
+ declare const seo002Description: Rule;
160
+ declare const seo003Canonical: Rule;
161
+ declare const seo004OgImage: Rule;
162
+ declare const seo005OgTitle: Rule;
163
+ declare const seo008JsonLd: Rule;
164
+
165
+ declare const seo006Robots: Rule;
166
+ declare const seo007Sitemap: Rule;
167
+ declare const seo009HtmlLang: Rule;
168
+
145
169
  declare const allRules: Rule[];
146
170
 
171
+ interface HeadTagRuleOptions {
172
+ id: string;
173
+ title: string;
174
+ severity: Severity;
175
+ /** Identifies the tag this rule looks for. */
176
+ match: (tag: HeadTag) => boolean;
177
+ /** Short human label, e.g. 'description'. */
178
+ label: string;
179
+ recommendation: string;
180
+ }
181
+ /** Build a route-scope rule asserting the presence of a single head tag (design §11). */
182
+ declare function headTagRule(opts: HeadTagRuleOptions): Rule;
183
+
147
184
  interface Summary {
148
185
  critical: number;
149
186
  warning: number;
@@ -156,15 +193,46 @@ interface Summary {
156
193
  /** Classify a single result for display/scoring (design §7, §12). */
157
194
  type Classification = 'fail' | 'pass' | 'dynamic';
158
195
  declare function classify(result: Result, config: Config): Classification;
196
+ /** A penalized dynamic finding is a warning under treatDynamicAs 'warn'; otherwise the rule's severity. */
197
+ declare function effectiveSeverity(result: Result, config: Config): Severity;
159
198
  declare function summarize(results: Result[], config: Config): Summary;
160
199
  /** Whether the run should fail the build/CI per the minimum failing severity. */
161
200
  declare function hasFailureAtOrAbove(summary: Summary, min: Severity): boolean;
162
201
 
202
+ interface ConsoleReportOptions {
203
+ byRoute?: boolean;
204
+ }
163
205
  /**
164
206
  * Render results as a console report string (design §7). Pure: returns a string,
165
- * the caller is responsible for printing. Slice 0 lists failures grouped by
166
- * severity, then passing routes (with a marker for dynamic values).
207
+ * the caller is responsible for printing. Prepends a score header; when byRoute is
208
+ * set, adds a per-route score tree.
167
209
  */
168
- declare function formatConsoleReport(results: Result[], config: Config): string;
210
+ declare function formatConsoleReport(results: Result[], config: Config, options?: ConsoleReportOptions): string;
211
+
212
+ /** Render results as the documented JSON report string (design §7). */
213
+ declare function formatJsonReport(results: Result[], config: Config, meta: {
214
+ version: string;
215
+ }): string;
216
+
217
+ /** Drop rules disabled via config (design §6). */
218
+ declare function selectRules(rules: Rule[], config: Config): Rule[];
219
+ /** Apply per-rule severity overrides to results (design §6). */
220
+ declare function applyRuleSeverities(results: Result[], config: Config): Result[];
221
+
222
+ interface ScoreModel {
223
+ routeAverage: number;
224
+ sitePenalty: number;
225
+ /** Headline cap value when it actually lowered the score, else null. */
226
+ criticalCap: number | null;
227
+ }
228
+ interface ScoreResult {
229
+ score: number;
230
+ scoreModel: ScoreModel;
231
+ }
232
+ interface ScoreOptions {
233
+ applyCriticalCap?: boolean;
234
+ }
235
+ /** Compute the headline score and its breakdown (design §12). */
236
+ declare function computeScore(results: Result[], config: Config, options?: ScoreOptions): ScoreResult;
169
237
 
170
- export { type Category, type Classification, type Config, type Detection, type HeadProvider, type HeadTag, type Presence, type ResolvedHead, type Result, type Rule, type RuleContext, type Runtime, type Scope, type Severity, type Summary, type TreatDynamicAs, type Value, allRules, classify, defaultConfig, defineConfig, formatConsoleReport, hasFailureAtOrAbove, isPenalized, runRules, seo001Title, summarize };
238
+ export { type Category, type Classification, type Config, type ConsoleReportOptions, type Detection, type HeadProvider, type HeadTag, type Presence, type Project, type ResolvedHead, type Result, type Rule, type RuleContext, type RuleSetting, type Runtime, type Scope, type ScoreModel, type ScoreOptions, type ScoreResult, type Severity, type Summary, type TreatDynamicAs, type Value, allRules, applyRuleSeverities, classify, computeScore, defaultConfig, defaultProject, defineConfig, effectiveSeverity, formatConsoleReport, formatJsonReport, hasFailureAtOrAbove, headTagRule, isPenalized, runRules, selectRules, seo001Title, seo002Description, seo003Canonical, seo004OgImage, seo005OgTitle, seo006Robots, seo007Sitemap, seo008JsonLd, seo009HtmlLang, summarize };
package/dist/index.js CHANGED
@@ -1,7 +1,14 @@
1
1
  // src/types.ts
2
+ var defaultProject = {
3
+ hasRobotsTxt: false,
4
+ hasSitemap: false,
5
+ htmlLang: { presence: "none", value: "absent" }
6
+ };
2
7
  var defaultConfig = {
3
8
  treatDynamicAs: "pass",
4
- metaComponents: []
9
+ metaComponents: [],
10
+ rules: {},
11
+ failOn: "critical"
5
12
  };
6
13
  function defineConfig(config = {}) {
7
14
  return { ...defaultConfig, ...config };
@@ -11,7 +18,7 @@ function defineConfig(config = {}) {
11
18
  function isPenalized(detection, treatDynamicAs) {
12
19
  if (detection.presence === "none") return true;
13
20
  if (detection.value === "absent") return true;
14
- if (detection.value === "dynamic") return treatDynamicAs === "fail";
21
+ if (detection.value === "dynamic") return treatDynamicAs !== "pass";
15
22
  return false;
16
23
  }
17
24
 
@@ -58,8 +65,157 @@ var seo001Title = {
58
65
  }
59
66
  };
60
67
 
68
+ // src/rules/seo/head-tag-rule.ts
69
+ function detect(head, match) {
70
+ const tag = head.tags.find(match);
71
+ return tag ? { presence: tag.presence, value: tag.value } : { presence: "none", value: "absent" };
72
+ }
73
+ function headTagRule(opts) {
74
+ const docsUrl = `https://svelte-vitals.dev/rules/${opts.id}`;
75
+ return {
76
+ id: opts.id,
77
+ title: opts.title,
78
+ category: "seo",
79
+ severity: opts.severity,
80
+ scope: "route",
81
+ async check(ctx) {
82
+ return ctx.heads.map((head) => {
83
+ const detection = detect(head, opts.match);
84
+ const message = detection.presence === "none" ? `Missing ${opts.label}` : detection.value === "absent" ? `Empty ${opts.label}` : opts.label;
85
+ return {
86
+ id: opts.id,
87
+ severity: opts.severity,
88
+ detection,
89
+ route: head.route,
90
+ location: head.file,
91
+ message,
92
+ recommendation: opts.recommendation,
93
+ docsUrl
94
+ };
95
+ });
96
+ }
97
+ };
98
+ }
99
+
100
+ // src/rules/seo/seo002-005-008.ts
101
+ var seo002Description = headTagRule({
102
+ id: "SEO002",
103
+ title: "Description presence",
104
+ severity: "critical",
105
+ match: (t) => t.kind === "meta" && t.name === "description",
106
+ label: '<meta name="description">',
107
+ recommendation: 'Add a <meta name="description"> in <svelte:head>, or set the description on your meta component.'
108
+ });
109
+ var seo003Canonical = headTagRule({
110
+ id: "SEO003",
111
+ title: "Canonical URL",
112
+ severity: "warning",
113
+ match: (t) => t.kind === "link" && t.rel === "canonical",
114
+ label: '<link rel="canonical">',
115
+ recommendation: 'Add <link rel="canonical"> in <svelte:head>, or set the canonical prop on your meta component.'
116
+ });
117
+ var seo004OgImage = headTagRule({
118
+ id: "SEO004",
119
+ title: "Open Graph image",
120
+ severity: "warning",
121
+ match: (t) => t.kind === "meta" && t.property === "og:image",
122
+ label: '<meta property="og:image">',
123
+ recommendation: 'Add <meta property="og:image">, or set openGraph.images on your meta component.'
124
+ });
125
+ var seo005OgTitle = headTagRule({
126
+ id: "SEO005",
127
+ title: "Open Graph title",
128
+ severity: "warning",
129
+ match: (t) => t.kind === "meta" && t.property === "og:title",
130
+ label: '<meta property="og:title">',
131
+ recommendation: 'Add <meta property="og:title">, or set openGraph.title on your meta component.'
132
+ });
133
+ var seo008JsonLd = headTagRule({
134
+ id: "SEO008",
135
+ title: "JSON-LD structured data",
136
+ severity: "info",
137
+ match: (t) => t.kind === "jsonld",
138
+ label: 'JSON-LD (<script type="application/ld+json">)',
139
+ recommendation: "Add JSON-LD structured data, e.g. via <svelte:head> or a JsonLd component."
140
+ });
141
+
142
+ // src/rules/seo/project-rules.ts
143
+ var present = { presence: "own", value: "static" };
144
+ var absent = { presence: "none", value: "absent" };
145
+ var seo006Robots = {
146
+ id: "SEO006",
147
+ title: "robots.txt",
148
+ category: "seo",
149
+ severity: "warning",
150
+ scope: "project",
151
+ async check(ctx) {
152
+ const detection = ctx.project.hasRobotsTxt ? present : absent;
153
+ return [
154
+ {
155
+ id: "SEO006",
156
+ severity: "warning",
157
+ detection,
158
+ message: ctx.project.hasRobotsTxt ? "robots.txt" : "Missing robots.txt",
159
+ recommendation: "Add static/robots.txt or a src/routes/robots.txt/+server endpoint.",
160
+ docsUrl: "https://svelte-vitals.dev/rules/SEO006"
161
+ }
162
+ ];
163
+ }
164
+ };
165
+ var seo007Sitemap = {
166
+ id: "SEO007",
167
+ title: "sitemap.xml",
168
+ category: "seo",
169
+ severity: "warning",
170
+ scope: "project",
171
+ async check(ctx) {
172
+ const detection = ctx.project.hasSitemap ? present : absent;
173
+ return [
174
+ {
175
+ id: "SEO007",
176
+ severity: "warning",
177
+ detection,
178
+ message: ctx.project.hasSitemap ? "sitemap.xml" : "Missing sitemap.xml",
179
+ recommendation: "Add static/sitemap.xml or a src/routes/sitemap.xml/+server endpoint.",
180
+ docsUrl: "https://svelte-vitals.dev/rules/SEO007"
181
+ }
182
+ ];
183
+ }
184
+ };
185
+ var seo009HtmlLang = {
186
+ id: "SEO009",
187
+ title: "<html lang>",
188
+ category: "seo",
189
+ severity: "warning",
190
+ scope: "project",
191
+ async check(ctx) {
192
+ const detection = ctx.project.htmlLang;
193
+ const message = detection.presence === "none" ? "Missing <html lang>" : detection.value === "absent" ? "Empty <html lang>" : "<html lang>";
194
+ return [
195
+ {
196
+ id: "SEO009",
197
+ severity: "warning",
198
+ detection,
199
+ message,
200
+ recommendation: 'Set <html lang="..."> in src/app.html.',
201
+ docsUrl: "https://svelte-vitals.dev/rules/SEO009"
202
+ }
203
+ ];
204
+ }
205
+ };
206
+
61
207
  // src/rules/index.ts
62
- var allRules = [seo001Title];
208
+ var allRules = [
209
+ seo001Title,
210
+ seo002Description,
211
+ seo003Canonical,
212
+ seo004OgImage,
213
+ seo005OgTitle,
214
+ seo006Robots,
215
+ seo007Sitemap,
216
+ seo008JsonLd,
217
+ seo009HtmlLang
218
+ ];
63
219
 
64
220
  // src/summary.ts
65
221
  function classify(result, config) {
@@ -67,12 +223,16 @@ function classify(result, config) {
67
223
  if (result.detection.value === "dynamic") return "dynamic";
68
224
  return "pass";
69
225
  }
226
+ function effectiveSeverity(result, config) {
227
+ if (result.detection.value === "dynamic" && config.treatDynamicAs === "warn") return "warning";
228
+ return result.severity;
229
+ }
70
230
  function summarize(results, config) {
71
231
  const summary = { critical: 0, warning: 0, info: 0, passed: 0, dynamic: 0 };
72
232
  for (const result of results) {
73
233
  const cls = classify(result, config);
74
234
  if (cls === "fail") {
75
- summary[severityKey(result.severity)] += 1;
235
+ summary[effectiveSeverity(result, config)] += 1;
76
236
  } else {
77
237
  summary.passed += 1;
78
238
  if (cls === "dynamic") summary.dynamic += 1;
@@ -83,10 +243,55 @@ function summarize(results, config) {
83
243
  function hasFailureAtOrAbove(summary, min) {
84
244
  const order = ["info", "warning", "critical"];
85
245
  const threshold = order.indexOf(min);
86
- return order.some((sev, idx) => idx >= threshold && summary[severityKey(sev)] > 0);
246
+ return order.some((sev, idx) => idx >= threshold && summary[sev] > 0);
87
247
  }
88
- function severityKey(severity) {
89
- return severity;
248
+
249
+ // src/scoring/score.ts
250
+ var DEDUCTION = { critical: 15, warning: 5, info: 1 };
251
+ var CRITICAL_CAP = 79;
252
+ function clamp(n) {
253
+ return Math.max(0, Math.min(100, n));
254
+ }
255
+ function computeScore(results, config, options = {}) {
256
+ const routeResults = results.filter((r) => r.route !== void 0);
257
+ const projectResults = results.filter((r) => r.route === void 0);
258
+ const routeScores = /* @__PURE__ */ new Map();
259
+ for (const r of routeResults) if (!routeScores.has(r.route)) routeScores.set(r.route, 100);
260
+ let anyCritical = false;
261
+ const routeRuleMax = /* @__PURE__ */ new Map();
262
+ for (const r of routeResults) {
263
+ if (!isPenalized(r.detection, config.treatDynamicAs)) continue;
264
+ const sev = effectiveSeverity(r, config);
265
+ if (sev === "critical") anyCritical = true;
266
+ const route = r.route;
267
+ let perRule = routeRuleMax.get(route);
268
+ if (!perRule) routeRuleMax.set(route, perRule = /* @__PURE__ */ new Map());
269
+ const prev = perRule.get(r.id) ?? 0;
270
+ if (DEDUCTION[sev] > prev) perRule.set(r.id, DEDUCTION[sev]);
271
+ }
272
+ for (const [route, perRule] of routeRuleMax) {
273
+ let deduction = 0;
274
+ for (const d of perRule.values()) deduction += d;
275
+ routeScores.set(route, routeScores.get(route) - deduction);
276
+ }
277
+ const scores = [...routeScores.values()].map(clamp);
278
+ const routeAverage = scores.length ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) : 100;
279
+ const projectRuleMax = /* @__PURE__ */ new Map();
280
+ for (const r of projectResults) {
281
+ if (!isPenalized(r.detection, config.treatDynamicAs)) continue;
282
+ const sev = effectiveSeverity(r, config);
283
+ if (sev === "critical") anyCritical = true;
284
+ const prev = projectRuleMax.get(r.id) ?? 0;
285
+ if (DEDUCTION[sev] > prev) projectRuleMax.set(r.id, DEDUCTION[sev]);
286
+ }
287
+ let sitePenalty = 0;
288
+ for (const deduction of projectRuleMax.values()) sitePenalty += deduction;
289
+ const applyCap = options.applyCriticalCap ?? true;
290
+ const uncapped = routeAverage - sitePenalty;
291
+ const capBinds = applyCap && anyCritical && uncapped > CRITICAL_CAP;
292
+ const criticalCap = capBinds ? CRITICAL_CAP : null;
293
+ const score = capBinds ? CRITICAL_CAP : uncapped;
294
+ return { score: clamp(score), scoreModel: { routeAverage, sitePenalty, criticalCap } };
90
295
  }
91
296
 
92
297
  // src/reporter/console.ts
@@ -96,13 +301,37 @@ var SEVERITY_TITLE = {
96
301
  warning: "Warnings",
97
302
  info: "Info"
98
303
  };
99
- function formatConsoleReport(results, config) {
304
+ function scoreHeader(results, config) {
305
+ const { score, scoreModel } = computeScore(results, config);
306
+ const parts = [`route avg ${scoreModel.routeAverage}`];
307
+ if (scoreModel.sitePenalty > 0) parts.push(`site \u2212${scoreModel.sitePenalty}`);
308
+ if (scoreModel.criticalCap !== null) parts.push(`capped at ${scoreModel.criticalCap}: critical present`);
309
+ return `SEO Score: ${score}/100 (${parts.join(" \xB7 ")})`;
310
+ }
311
+ function byRouteTree(results, config) {
312
+ const routes = /* @__PURE__ */ new Map();
313
+ for (const r of results) {
314
+ if (r.route === void 0) continue;
315
+ if (!routes.has(r.route)) routes.set(r.route, []);
316
+ routes.get(r.route).push(r);
317
+ }
318
+ const lines = ["By route", RULE];
319
+ for (const [route, rs] of [...routes.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
320
+ const { score } = computeScore(rs, config, { applyCriticalCap: false });
321
+ lines.push(`${route.padEnd(28)} ${score}`);
322
+ for (const r of rs.filter((x) => classify(x, config) === "fail")) {
323
+ lines.push(` \u2717 ${r.id} ${r.message}`);
324
+ }
325
+ }
326
+ lines.push("");
327
+ return lines;
328
+ }
329
+ function formatConsoleReport(results, config, options = {}) {
100
330
  const summary = summarize(results, config);
101
- const lines = [];
102
- lines.push("Svelte Vitals \xB7 SEO (static mode)", "");
331
+ const lines = ["Svelte Vitals \xB7 SEO (static mode)", "", scoreHeader(results, config), ""];
103
332
  const failures = results.filter((r) => classify(r, config) === "fail");
104
333
  for (const severity of ["critical", "warning", "info"]) {
105
- const bucket = failures.filter((r) => r.severity === severity);
334
+ const bucket = failures.filter((r) => effectiveSeverity(r, config) === severity);
106
335
  if (bucket.length === 0) continue;
107
336
  lines.push(`${SEVERITY_TITLE[severity]} (${bucket.length})`, RULE);
108
337
  for (const r of bucket) {
@@ -122,20 +351,73 @@ function formatConsoleReport(results, config) {
122
351
  }
123
352
  lines.push("");
124
353
  }
125
- if (summary.dynamic > 0) {
126
- lines.push("\u21AF = set dynamically (verified at runtime).");
127
- }
354
+ if (options.byRoute) lines.push(...byRouteTree(results, config));
355
+ if (summary.dynamic > 0) lines.push("\u21AF = set dynamically (verified at runtime).");
128
356
  return lines.join("\n").replace(/\n+$/, "\n");
129
357
  }
358
+
359
+ // src/reporter/json.ts
360
+ function issueOf(result) {
361
+ return {
362
+ id: result.id,
363
+ title: result.message,
364
+ detection: result.detection,
365
+ location: result.location,
366
+ recommendation: result.recommendation
367
+ };
368
+ }
369
+ function formatJsonReport(results, config, meta) {
370
+ const { score, scoreModel } = computeScore(results, config);
371
+ const summary = summarize(results, config);
372
+ const routeMap = /* @__PURE__ */ new Map();
373
+ for (const r of results) {
374
+ if (r.route === void 0) continue;
375
+ if (!routeMap.has(r.route)) routeMap.set(r.route, { route: r.route, results: [] });
376
+ routeMap.get(r.route).results.push(r);
377
+ }
378
+ const routes = [...routeMap.values()].sort((a, b) => a.route.localeCompare(b.route)).map(({ route, results: rs }) => ({
379
+ route,
380
+ score: computeScore(rs, config, { applyCriticalCap: false }).score,
381
+ issues: rs.filter((r) => isPenalized(r.detection, config.treatDynamicAs)).map((r) => ({ ...issueOf(r), severity: effectiveSeverity(r, config) }))
382
+ }));
383
+ const siteIssues = results.filter((r) => r.route === void 0 && isPenalized(r.detection, config.treatDynamicAs)).map((r) => ({ ...issueOf(r), severity: effectiveSeverity(r, config) }));
384
+ return JSON.stringify({ version: meta.version, score, scoreModel, summary, routes, siteIssues }, null, 2);
385
+ }
386
+
387
+ // src/config-apply.ts
388
+ function selectRules(rules, config) {
389
+ return rules.filter((rule) => config.rules[rule.id] !== "off");
390
+ }
391
+ function applyRuleSeverities(results, config) {
392
+ return results.map((result) => {
393
+ const setting = config.rules[result.id];
394
+ return setting && setting !== "off" ? { ...result, severity: setting } : result;
395
+ });
396
+ }
130
397
  export {
131
398
  allRules,
399
+ applyRuleSeverities,
132
400
  classify,
401
+ computeScore,
133
402
  defaultConfig,
403
+ defaultProject,
134
404
  defineConfig,
405
+ effectiveSeverity,
135
406
  formatConsoleReport,
407
+ formatJsonReport,
136
408
  hasFailureAtOrAbove,
409
+ headTagRule,
137
410
  isPenalized,
138
411
  runRules,
412
+ selectRules,
139
413
  seo001Title,
414
+ seo002Description,
415
+ seo003Canonical,
416
+ seo004OgImage,
417
+ seo005OgTitle,
418
+ seo006Robots,
419
+ seo007Sitemap,
420
+ seo008JsonLd,
421
+ seo009HtmlLang,
140
422
  summarize
141
423
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svelte-vitals/core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Shared, runtime-agnostic core for svelte-vitals (types, rule engine, scorer, reporter).",
5
5
  "type": "module",
6
6
  "license": "MIT",