@svelte-vitals/core 0.55.2 → 0.55.3
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 +1 -1
- package/dist/index.js +1 -2
- package/dist/internal.d.ts +487 -11
- package/dist/internal.js +387 -98
- package/dist/{json-BQf9Qb2D.d.ts → json-DmLgFEwV.d.ts} +85 -10
- package/dist/{markdown-DgHBAvYl.js → markdown-CsQNXeWj.js} +895 -158
- package/package.json +4 -4
|
@@ -152,6 +152,168 @@ const seoTitlePresence = {
|
|
|
152
152
|
}
|
|
153
153
|
};
|
|
154
154
|
//#endregion
|
|
155
|
+
//#region src/kit-module.ts
|
|
156
|
+
/** The root layout — its page options cover the whole app. */
|
|
157
|
+
const ROOT_LAYOUT_RE = /^src\/routes\/\+layout(\.server)?\.(ts|js)$/;
|
|
158
|
+
/** Files whose `ssr` export is a page option — it has no effect in `+server` endpoints or hooks files. */
|
|
159
|
+
const PAGE_OPTION_FILE_RE = /\+(page|layout)(\.server)?\.(ts|js)$/;
|
|
160
|
+
const appSsrCache = /* @__PURE__ */ new WeakMap();
|
|
161
|
+
/**
|
|
162
|
+
* Whether no page is ever server-rendered: the hash router, or a root layout exporting `ssr = false`
|
|
163
|
+
* that no page-option file might turn back on. Universal loads and components then run only in the
|
|
164
|
+
* browser; server files (`+page.server`, `+server`, hooks) still run on the server.
|
|
165
|
+
*/
|
|
166
|
+
function appSsrDisabled(scope) {
|
|
167
|
+
if (scope.project?.kitHashRouter) return true;
|
|
168
|
+
const kitModules = scope.kitModules;
|
|
169
|
+
if (!kitModules) return false;
|
|
170
|
+
let hit = appSsrCache.get(kitModules);
|
|
171
|
+
if (hit === void 0) {
|
|
172
|
+
hit = kitModules.some((m) => m.ssrDisabled && ROOT_LAYOUT_RE.test(m.file)) && !kitModules.some((m) => PAGE_OPTION_FILE_RE.test(m.file) && (m.ssrEnabled || m.parseFailed));
|
|
173
|
+
appSsrCache.set(kitModules, hit);
|
|
174
|
+
}
|
|
175
|
+
return hit;
|
|
176
|
+
}
|
|
177
|
+
const ROUTES_DIR$1 = "src/routes";
|
|
178
|
+
/** A `+page`/`+layout` file: its directory, kind, and the `@segment` layout reset (on `.svelte` names). */
|
|
179
|
+
const ROUTE_FILE_RE = /^(src\/routes(?:\/.+)?)\/\+(page|layout)(?:@([^/]*))?(?:\.server)?\.(?:svelte|ts|js)$/;
|
|
180
|
+
const parentDir = (dir) => dir === ROUTES_DIR$1 ? void 0 : dir.slice(0, dir.lastIndexOf("/"));
|
|
181
|
+
/** The nearest ancestor-or-self directory a `@segment` reset names (`''` is the root). */
|
|
182
|
+
function resetTarget(dir, segment) {
|
|
183
|
+
for (let d = dir; d !== void 0; d = parentDir(d)) if (segment === "" ? d === ROUTES_DIR$1 : d.slice(d.lastIndexOf("/") + 1) === segment) return d;
|
|
184
|
+
}
|
|
185
|
+
const routeOptionCache = {
|
|
186
|
+
ssr: /* @__PURE__ */ new WeakMap(),
|
|
187
|
+
csr: /* @__PURE__ */ new WeakMap()
|
|
188
|
+
};
|
|
189
|
+
const optionOn = (m, option) => m.parseFailed || (option === "ssr" ? m.ssrEnabled : m.csrEnabled);
|
|
190
|
+
const optionOff = (m, option) => (option === "ssr" ? m.ssrDisabled : m.csrDisabled) !== void 0;
|
|
191
|
+
/**
|
|
192
|
+
* Per route directory, whether `ssr` or `csr` is off for its page (`page:<dir>`) or layout
|
|
193
|
+
* (`layout:<dir>`). A page takes the nearest export of the option along its own files and then its
|
|
194
|
+
* layout chain, following `+page@x`/`+layout@x` resets as SvelteKit does; an export other than
|
|
195
|
+
* `false` (or `dev`), or an unparsed option file, counts as on. A layout is off only when every page
|
|
196
|
+
* using it is — and the root layout also needs its own opt-out, since it wraps the 404 page.
|
|
197
|
+
*/
|
|
198
|
+
function routeOptionOff(scope, which) {
|
|
199
|
+
const kitModules = scope.kitModules ?? [];
|
|
200
|
+
let hit = routeOptionCache[which].get(scope);
|
|
201
|
+
if (hit) return hit;
|
|
202
|
+
const own = {
|
|
203
|
+
universal: /* @__PURE__ */ new Map(),
|
|
204
|
+
server: /* @__PURE__ */ new Map()
|
|
205
|
+
};
|
|
206
|
+
for (const m of kitModules) {
|
|
207
|
+
const match = ROUTE_FILE_RE.exec(m.file);
|
|
208
|
+
if (!match) continue;
|
|
209
|
+
const key = `${match[2]}:${match[1]}`;
|
|
210
|
+
if (optionOn(m, which)) own[m.kind].set(key, "on");
|
|
211
|
+
else if (optionOff(m, which)) own[m.kind].set(key, "off");
|
|
212
|
+
}
|
|
213
|
+
const option = new Map([...own.server, ...own.universal]);
|
|
214
|
+
hit = /* @__PURE__ */ new Map();
|
|
215
|
+
for (const [dir, chain] of pageChains(scope)) {
|
|
216
|
+
const off = [`page:${dir}`, ...chain.map((l) => `layout:${l}`)].map((k) => option.get(k)).find(Boolean) === "off";
|
|
217
|
+
hit.set(`page:${dir}`, off);
|
|
218
|
+
for (const l of chain) hit.set(`layout:${l}`, (hit.get(`layout:${l}`) ?? true) && off);
|
|
219
|
+
}
|
|
220
|
+
if (option.get(`layout:${ROUTES_DIR$1}`) !== "off") hit.delete(`layout:${ROUTES_DIR$1}`);
|
|
221
|
+
routeOptionCache[which].set(scope, hit);
|
|
222
|
+
return hit;
|
|
223
|
+
}
|
|
224
|
+
const chainCache = /* @__PURE__ */ new WeakMap();
|
|
225
|
+
/** Per page directory, the layout directories that wrap it, nearest first, following `+page@x`/`+layout@x` resets as SvelteKit does. */
|
|
226
|
+
function pageChains(scope) {
|
|
227
|
+
let hit = chainCache.get(scope);
|
|
228
|
+
if (hit) return hit;
|
|
229
|
+
const pages = /* @__PURE__ */ new Map();
|
|
230
|
+
const layouts = /* @__PURE__ */ new Map();
|
|
231
|
+
const files = [
|
|
232
|
+
...scope.sourceFiles ?? [],
|
|
233
|
+
...(scope.components ?? []).map((c) => c.file),
|
|
234
|
+
...(scope.kitModules ?? []).map((m) => m.file)
|
|
235
|
+
];
|
|
236
|
+
for (const file of files) {
|
|
237
|
+
const match = ROUTE_FILE_RE.exec(file);
|
|
238
|
+
if (!match) continue;
|
|
239
|
+
const map = match[2] === "page" ? pages : layouts;
|
|
240
|
+
if (match[3] !== void 0 || !map.has(match[1])) map.set(match[1], match[3]);
|
|
241
|
+
}
|
|
242
|
+
hit = /* @__PURE__ */ new Map();
|
|
243
|
+
for (const [dir, reset] of pages) {
|
|
244
|
+
const chain = [];
|
|
245
|
+
for (let d = reset === void 0 ? dir : resetTarget(dir, reset); d !== void 0;) {
|
|
246
|
+
const layoutReset = layouts.get(d);
|
|
247
|
+
if (layouts.has(d)) chain.push(d);
|
|
248
|
+
d = layoutReset === void 0 ? parentDir(d) : resetTarget(parentDir(d), layoutReset);
|
|
249
|
+
}
|
|
250
|
+
hit.set(dir, chain);
|
|
251
|
+
}
|
|
252
|
+
chainCache.set(scope, hit);
|
|
253
|
+
return hit;
|
|
254
|
+
}
|
|
255
|
+
/** Whether a `+page` file's route sends requests without a session away: its own load or a wrapping layout's does (`loadGated`). */
|
|
256
|
+
function routeGated(file, scope) {
|
|
257
|
+
const match = ROUTE_FILE_RE.exec(file);
|
|
258
|
+
if (!match || match[2] !== "page") return false;
|
|
259
|
+
const gated = /* @__PURE__ */ new Set();
|
|
260
|
+
for (const m of scope.kitModules ?? []) {
|
|
261
|
+
const g = m.loadGated && m.kind === "server" ? ROUTE_FILE_RE.exec(m.file) : null;
|
|
262
|
+
if (g) gated.add(`${g[2]}:${g[1]}`);
|
|
263
|
+
}
|
|
264
|
+
if (gated.size === 0) return false;
|
|
265
|
+
const chain = pageChains(scope).get(match[1]) ?? [];
|
|
266
|
+
return gated.has(`page:${match[1]}`) || chain.some((l) => gated.has(`layout:${l}`));
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Whether a `+page` file's route never renders because a layout above it redirects or errors in its
|
|
270
|
+
* `load` on every call (`loadNeverRenders`). Not the root layout: one that sent every request away
|
|
271
|
+
* would leave the app nothing to serve, so that reading is more likely wrong than the app, and acting
|
|
272
|
+
* on it would silence every route.
|
|
273
|
+
*/
|
|
274
|
+
function layoutNeverRenders(file, scope) {
|
|
275
|
+
const match = ROUTE_FILE_RE.exec(file);
|
|
276
|
+
if (!match || match[2] !== "page") return false;
|
|
277
|
+
const dead = /* @__PURE__ */ new Set();
|
|
278
|
+
for (const m of scope.kitModules ?? []) {
|
|
279
|
+
const layout = m.loadNeverRenders ? ROUTE_FILE_RE.exec(m.file) : null;
|
|
280
|
+
if (layout?.[2] === "layout" && layout[1] !== ROUTES_DIR$1) dead.add(layout[1]);
|
|
281
|
+
}
|
|
282
|
+
return dead.size > 0 && (pageChains(scope).get(match[1]) ?? []).some((dir) => dead.has(dir));
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Whether a `+page` file's route never renders because its own `load` redirects or errors on every
|
|
286
|
+
* request, a build-time `if (building)` aside (`loadNeverRendersOutsideBuild`), and nothing on its chain
|
|
287
|
+
* exports `prerender`, so the build never renders it either.
|
|
288
|
+
*/
|
|
289
|
+
function pageRendersOnlyWhenBuilding(file, scope) {
|
|
290
|
+
const match = ROUTE_FILE_RE.exec(file);
|
|
291
|
+
if (!match || match[2] !== "page") return false;
|
|
292
|
+
const at = (m) => ROUTE_FILE_RE.exec(m.file);
|
|
293
|
+
const modules = scope.kitModules ?? [];
|
|
294
|
+
if (!modules.some((m) => m.loadNeverRendersOutsideBuild && at(m)?.[2] === "page" && at(m)?.[1] === match[1])) return false;
|
|
295
|
+
const layouts = /* @__PURE__ */ new Set([match[1], ...pageChains(scope).get(match[1]) ?? []]);
|
|
296
|
+
return !modules.some((m) => {
|
|
297
|
+
const where = m.prerenderExported ? at(m) : null;
|
|
298
|
+
return !!where && (where[2] === "page" ? where[1] === match[1] : layouts.has(where[1]));
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
/** Whether a `+page`/`+layout` file (module or component) belongs to a route that is never server-rendered. */
|
|
302
|
+
function routeNeverSsr(file, scope) {
|
|
303
|
+
const match = ROUTE_FILE_RE.exec(file);
|
|
304
|
+
return !!match && (appSsrDisabled(scope) || routeOptionOff(scope, "ssr").get(`${match[2]}:${match[1]}`) === true);
|
|
305
|
+
}
|
|
306
|
+
/** Whether a universal `+page`/`+layout` module never runs in the browser: every page it serves has `csr` off, its own or inherited. */
|
|
307
|
+
function universalNeverCsr(m, scope) {
|
|
308
|
+
if (m.kind !== "universal") return false;
|
|
309
|
+
const match = ROUTE_FILE_RE.exec(m.file);
|
|
310
|
+
return !!match && routeOptionOff(scope, "csr").get(`${match[2]}:${match[1]}`) === true;
|
|
311
|
+
}
|
|
312
|
+
/** Whether a universal `+page`/`+layout` module never runs on the server — its own `ssr = false`, an app-wide one, or one its route inherits. */
|
|
313
|
+
function universalNeverSsr(m, scope) {
|
|
314
|
+
return m.kind === "universal" && (m.ssrDisabled !== void 0 || appSsrDisabled(scope) || routeNeverSsr(m.file, scope));
|
|
315
|
+
}
|
|
316
|
+
//#endregion
|
|
155
317
|
//#region src/rules/seo/head-tag-rule.ts
|
|
156
318
|
/** Open Graph keys are read from `property=`; `name="og:*"` is the common slip. */
|
|
157
319
|
function ogMisspelled(key) {
|
|
@@ -179,6 +341,7 @@ function detect(head, match) {
|
|
|
179
341
|
value: "absent"
|
|
180
342
|
};
|
|
181
343
|
}
|
|
344
|
+
const GATED_NOTE = "Reported as info: this route's load turns away visitors without a session (a redirect or a 401/403), so search engines never index it.";
|
|
182
345
|
/** Build a route-scope rule asserting the presence of a single head tag (design §11). */
|
|
183
346
|
function headTagRule(opts) {
|
|
184
347
|
const docsUrl = docsUrlFor(opts.id);
|
|
@@ -195,15 +358,16 @@ function headTagRule(opts) {
|
|
|
195
358
|
const detection = detect(head, opts.match);
|
|
196
359
|
const misspelled = detection.presence === "none" && opts.misspelled && head.tags.some(opts.misspelled.match) ? opts.misspelled : void 0;
|
|
197
360
|
const message = misspelled ? misspelled.label : detection.presence === "none" ? `Missing ${opts.label}` : detection.value === "absent" ? `Empty ${opts.label}` : opts.label;
|
|
361
|
+
const gated = opts.infoWhenGated && routeGated(head.file, ctx);
|
|
198
362
|
return {
|
|
199
363
|
id: opts.id,
|
|
200
364
|
category: "seo",
|
|
201
|
-
severity: opts.severity,
|
|
365
|
+
severity: gated ? "info" : opts.severity,
|
|
202
366
|
detection,
|
|
203
367
|
route: head.route,
|
|
204
368
|
location: head.file,
|
|
205
369
|
message,
|
|
206
|
-
recommendation: misspelled?.recommendation ?? opts.recommendation,
|
|
370
|
+
recommendation: misspelled?.recommendation ?? (gated ? `${opts.recommendation} ${GATED_NOTE}` : opts.recommendation),
|
|
207
371
|
docsUrl,
|
|
208
372
|
...misspelled ? { fix: { ...misspelled.fix } } : opts.fix ? { fix: { ...opts.fix } } : {}
|
|
209
373
|
};
|
|
@@ -217,6 +381,7 @@ const seoDescriptionPresence = headTagRule({
|
|
|
217
381
|
id: "seo/description-presence",
|
|
218
382
|
title: "Description presence",
|
|
219
383
|
severity: "warning",
|
|
384
|
+
infoWhenGated: true,
|
|
220
385
|
match: (t) => t.kind === "meta" && t.name === "description",
|
|
221
386
|
label: "<meta name=\"description\">",
|
|
222
387
|
recommendation: "Add a <meta name=\"description\"> in <svelte:head>, or set the description on your meta component.",
|
|
@@ -228,12 +393,50 @@ const seoDescriptionPresence = headTagRule({
|
|
|
228
393
|
}
|
|
229
394
|
});
|
|
230
395
|
//#endregion
|
|
396
|
+
//#region src/head.ts
|
|
397
|
+
/** Whether a `<link>`'s `rel` holds `token`: `rel` is a set of space-separated keywords (`dns-prefetch preconnect`). */
|
|
398
|
+
function hasRel(tag, token) {
|
|
399
|
+
return tag.rel?.split(/\s+/).includes(token) ?? false;
|
|
400
|
+
}
|
|
401
|
+
const JS_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
402
|
+
"application/ecmascript",
|
|
403
|
+
"application/javascript",
|
|
404
|
+
"application/x-ecmascript",
|
|
405
|
+
"application/x-javascript",
|
|
406
|
+
"text/ecmascript",
|
|
407
|
+
"text/javascript",
|
|
408
|
+
"text/javascript1.0",
|
|
409
|
+
"text/javascript1.1",
|
|
410
|
+
"text/javascript1.2",
|
|
411
|
+
"text/javascript1.3",
|
|
412
|
+
"text/javascript1.4",
|
|
413
|
+
"text/javascript1.5",
|
|
414
|
+
"text/jscript",
|
|
415
|
+
"text/livescript",
|
|
416
|
+
"text/x-ecmascript",
|
|
417
|
+
"text/x-javascript"
|
|
418
|
+
]);
|
|
419
|
+
/** HTML strips only ASCII whitespace from a `type` before matching — `trim()` would also strip
|
|
420
|
+
* U+00A0 etc. and misread a data block as a classic script. */
|
|
421
|
+
const ASCII_WHITESPACE_EDGES = /^[\t\n\f\r ]+|[\t\n\f\r ]+$/g;
|
|
422
|
+
/**
|
|
423
|
+
* Whether a `type` attribute (undefined = absent) makes a `<script>` a classic,
|
|
424
|
+
* render-blocking-capable script. Per the HTML spec, absent and the literal empty string are
|
|
425
|
+
* classic; any other value is ASCII-whitespace-stripped and must match a JavaScript MIME type —
|
|
426
|
+
* so a whitespace-only `type` is a data block, not a classic script.
|
|
427
|
+
*/
|
|
428
|
+
function isClassicScriptType(type) {
|
|
429
|
+
if (type === void 0 || type === "") return true;
|
|
430
|
+
return JS_MIME_TYPES.has(type.replace(ASCII_WHITESPACE_EDGES, "").toLowerCase());
|
|
431
|
+
}
|
|
432
|
+
//#endregion
|
|
231
433
|
//#region src/rules/seo/canonical-url.ts
|
|
232
434
|
const seoCanonicalUrl = headTagRule({
|
|
233
435
|
id: "seo/canonical-url",
|
|
234
436
|
title: "Canonical URL",
|
|
235
437
|
severity: "warning",
|
|
236
|
-
|
|
438
|
+
infoWhenGated: true,
|
|
439
|
+
match: (t) => t.kind === "link" && hasRel(t, "canonical"),
|
|
237
440
|
label: "<link rel=\"canonical\">",
|
|
238
441
|
recommendation: "Add <link rel=\"canonical\"> in <svelte:head>, or set the canonical prop on your meta component.",
|
|
239
442
|
rationale: "A canonical URL tells search engines which URL is authoritative, preventing duplicate-content dilution across query-string variants of the same page.",
|
|
@@ -557,7 +760,7 @@ const performancePreloadMissingAs = linkRule({
|
|
|
557
760
|
snippet: "<link rel=\"preload\" href=\"/app.css\" as=\"style\" />",
|
|
558
761
|
lang: "html"
|
|
559
762
|
},
|
|
560
|
-
relevant: (t) => t
|
|
763
|
+
relevant: (t) => hasRel(t, "preload"),
|
|
561
764
|
ok: (t) => t.hasAs === true
|
|
562
765
|
});
|
|
563
766
|
//#endregion
|
|
@@ -574,7 +777,7 @@ const performanceFontPreloadCrossorigin = linkRule({
|
|
|
574
777
|
snippet: "<link rel=\"preload\" href=\"/inter.woff2\" as=\"font\" type=\"font/woff2\" crossorigin />",
|
|
575
778
|
lang: "html"
|
|
576
779
|
},
|
|
577
|
-
relevant: (t) => t
|
|
780
|
+
relevant: (t) => hasRel(t, "preload") && t.as === "font",
|
|
578
781
|
ok: (t) => t.hasCrossorigin === true
|
|
579
782
|
});
|
|
580
783
|
//#endregion
|
|
@@ -663,7 +866,8 @@ const performanceRenderBlockingScript = {
|
|
|
663
866
|
async check(ctx) {
|
|
664
867
|
const out = [];
|
|
665
868
|
for (const head of ctx.heads) {
|
|
666
|
-
const
|
|
869
|
+
const clientHead = routeNeverSsr(head.file, ctx);
|
|
870
|
+
const scripts = head.tags.filter((t) => t.kind === "script" && !(clientHead && t.file !== "src/app.html"));
|
|
667
871
|
if (scripts.length === 0) continue;
|
|
668
872
|
const blocking = scripts.filter((t) => t.blocking);
|
|
669
873
|
if (blocking.length > 0) for (const tag of blocking) out.push({
|
|
@@ -1175,7 +1379,7 @@ const performancePreconnect = {
|
|
|
1175
1379
|
if (tag.kind !== "link" && tag.kind !== "script" || typeof tag.href !== "string") continue;
|
|
1176
1380
|
const host = hostOf(tag.href);
|
|
1177
1381
|
if (!host) continue;
|
|
1178
|
-
if (tag.kind === "link" && (tag
|
|
1382
|
+
if (tag.kind === "link" && (hasRel(tag, "preconnect") || hasRel(tag, "dns-prefetch"))) {
|
|
1179
1383
|
covered.add(host);
|
|
1180
1384
|
continue;
|
|
1181
1385
|
}
|
|
@@ -2060,7 +2264,7 @@ const seoHreflang = {
|
|
|
2060
2264
|
async check(ctx) {
|
|
2061
2265
|
const out = [];
|
|
2062
2266
|
for (const head of ctx.heads) {
|
|
2063
|
-
const alternates = head.tags.filter((t) => t.kind === "link" && t
|
|
2267
|
+
const alternates = head.tags.filter((t) => t.kind === "link" && hasRel(t, "alternate") && typeof t.hreflang === "string");
|
|
2064
2268
|
if (alternates.length === 0) continue;
|
|
2065
2269
|
const values = alternates.map((t) => t.hreflang);
|
|
2066
2270
|
const badTag = alternates.find((t) => !isValidHreflang(t.hreflang));
|
|
@@ -2342,7 +2546,7 @@ const multipleRecommendation = "A single, clear <h1> is the conventional signal
|
|
|
2342
2546
|
* `<svelte:boundary>` contribute only their largest arm. Separate blocks still sum, since both may be true.
|
|
2343
2547
|
*/
|
|
2344
2548
|
function renderableH1s(headings) {
|
|
2345
|
-
const nodes = headings.filter((h) => h.level === 1).map((heading) => ({
|
|
2549
|
+
const nodes = headings.filter((h) => h.level === 1 && !h.aria).map((heading) => ({
|
|
2346
2550
|
key: "h1",
|
|
2347
2551
|
path: heading.path ?? [],
|
|
2348
2552
|
repeatable: false,
|
|
@@ -2371,7 +2575,7 @@ const seoSingleH1 = {
|
|
|
2371
2575
|
severity: "warning",
|
|
2372
2576
|
recommendation: missingRecommendation
|
|
2373
2577
|
};
|
|
2374
|
-
const first = route.headings
|
|
2578
|
+
const first = route.headings.find((h) => !h.aria);
|
|
2375
2579
|
if (first) where = {
|
|
2376
2580
|
location: first.file,
|
|
2377
2581
|
...first.line > 0 ? { line: first.line } : {}
|
|
@@ -2496,6 +2700,12 @@ const seoDuplicateDescription = uniquenessRule({
|
|
|
2496
2700
|
});
|
|
2497
2701
|
//#endregion
|
|
2498
2702
|
//#region src/headings.ts
|
|
2703
|
+
/** Sort comparator for `order`: element by element, a prefix first; headings without one keep their place. */
|
|
2704
|
+
function documentOrder(a, b) {
|
|
2705
|
+
if (!a.order || !b.order) return 0;
|
|
2706
|
+
for (let i = 0; i < Math.min(a.order.length, b.order.length); i++) if (a.order[i] !== b.order[i]) return a.order[i] - b.order[i];
|
|
2707
|
+
return a.order.length - b.order.length;
|
|
2708
|
+
}
|
|
2499
2709
|
/** Two headings can never render together: different arms of one block. */
|
|
2500
2710
|
function exclusiveHeadings(a, b) {
|
|
2501
2711
|
if (!a.path || !b.path) return false;
|
|
@@ -2504,6 +2714,8 @@ function exclusiveHeadings(a, b) {
|
|
|
2504
2714
|
//#endregion
|
|
2505
2715
|
//#region src/rules/seo/heading-level-skip.ts
|
|
2506
2716
|
const docsUrl$5 = docsUrlFor("seo/heading-level-skip");
|
|
2717
|
+
/** Every arm `p` sits in that can be skipped, `h` sits in too: whenever `h` renders, so does `p` (or one like it). */
|
|
2718
|
+
const rendersWith = (p, h) => (p.path ?? []).every((s) => s.always || (h.path ?? []).some((t) => t.group === s.group && t.branch === s.branch));
|
|
2507
2719
|
const recommendation$5 = "Increase heading levels one step at a time (do not jump, e.g. from <h2> straight to <h4>).";
|
|
2508
2720
|
/**
|
|
2509
2721
|
* seo/heading-level-skip — Skipped heading level. Walking a route's body headings in
|
|
@@ -2522,13 +2734,33 @@ const seoHeadingLevelSkip = {
|
|
|
2522
2734
|
const out = [];
|
|
2523
2735
|
for (const route of ctx.headings ?? []) {
|
|
2524
2736
|
if (route.headings.length === 0) continue;
|
|
2737
|
+
const placed = (route.componentHeadings ?? []).filter((h) => h.order);
|
|
2738
|
+
const own = new Set(route.headings);
|
|
2739
|
+
const headings = [...route.headings, ...placed].sort(documentOrder);
|
|
2525
2740
|
let skip;
|
|
2526
|
-
for (let i = 1; i <
|
|
2527
|
-
const h =
|
|
2528
|
-
|
|
2529
|
-
|
|
2741
|
+
for (let i = 1; i < headings.length && !skip; i++) {
|
|
2742
|
+
const h = headings[i];
|
|
2743
|
+
if (!own.has(h)) continue;
|
|
2744
|
+
const before = headings.slice(0, i).filter((p) => !exclusiveHeadings(p, h) && (own.has(p) || rendersWith(p, h)));
|
|
2745
|
+
const prev = before.at(-1);
|
|
2746
|
+
if (!prev) continue;
|
|
2747
|
+
const levels = [prev.level];
|
|
2748
|
+
const arms = /* @__PURE__ */ new Set();
|
|
2749
|
+
const looped = prev.path?.some((s) => s.repeat && !h.path?.some((t) => t.group === s.group));
|
|
2750
|
+
for (let j = looped ? -1 : before.length - 2; j >= 0; j--) {
|
|
2751
|
+
const q = before[j];
|
|
2752
|
+
const step = q.path?.find((s) => prev.path?.some((t) => t.group === s.group && t.branch !== s.branch));
|
|
2753
|
+
const arm = step && `${step.group}:${step.branch}`;
|
|
2754
|
+
if (arm && !arms.has(arm)) {
|
|
2755
|
+
arms.add(arm);
|
|
2756
|
+
levels.push(q.level);
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
const known = levels.filter((level) => level > 0);
|
|
2760
|
+
const lowest = Math.min(...known);
|
|
2761
|
+
if (known.length === levels.length && h.level > lowest + 1) skip = {
|
|
2530
2762
|
level: h.level,
|
|
2531
|
-
prev:
|
|
2763
|
+
prev: lowest,
|
|
2532
2764
|
line: h.line,
|
|
2533
2765
|
file: h.file
|
|
2534
2766
|
};
|
|
@@ -2661,93 +2893,6 @@ function kitModuleRule(opts) {
|
|
|
2661
2893
|
});
|
|
2662
2894
|
}
|
|
2663
2895
|
//#endregion
|
|
2664
|
-
//#region src/kit-module.ts
|
|
2665
|
-
/** The root layout — its page options cover the whole app. */
|
|
2666
|
-
const ROOT_LAYOUT_RE = /^src\/routes\/\+layout(\.server)?\.(ts|js)$/;
|
|
2667
|
-
/** Files whose `ssr` export is a page option — it has no effect in `+server` endpoints or hooks files. */
|
|
2668
|
-
const PAGE_OPTION_FILE_RE = /\+(page|layout)(\.server)?\.(ts|js)$/;
|
|
2669
|
-
const appSsrCache = /* @__PURE__ */ new WeakMap();
|
|
2670
|
-
/**
|
|
2671
|
-
* Whether no page is ever server-rendered: the root layout exports `ssr = false` and no page-option
|
|
2672
|
-
* file might turn it back on. Universal loads and components then run only in the browser; server
|
|
2673
|
-
* files (`+page.server`, `+server`, hooks) still run on the server.
|
|
2674
|
-
*/
|
|
2675
|
-
function appSsrDisabled(kitModules) {
|
|
2676
|
-
if (!kitModules) return false;
|
|
2677
|
-
let hit = appSsrCache.get(kitModules);
|
|
2678
|
-
if (hit === void 0) {
|
|
2679
|
-
hit = kitModules.some((m) => m.ssrDisabled && ROOT_LAYOUT_RE.test(m.file)) && !kitModules.some((m) => PAGE_OPTION_FILE_RE.test(m.file) && (m.ssrEnabled || m.parseFailed));
|
|
2680
|
-
appSsrCache.set(kitModules, hit);
|
|
2681
|
-
}
|
|
2682
|
-
return hit;
|
|
2683
|
-
}
|
|
2684
|
-
const ROUTES_DIR$1 = "src/routes";
|
|
2685
|
-
/** A `+page`/`+layout` file: its directory, kind, and the `@segment` layout reset (on `.svelte` names). */
|
|
2686
|
-
const ROUTE_FILE_RE = /^(src\/routes(?:\/.+)?)\/\+(page|layout)(?:@([^/]*))?(?:\.server)?\.(?:svelte|ts|js)$/;
|
|
2687
|
-
const parentDir = (dir) => dir === ROUTES_DIR$1 ? void 0 : dir.slice(0, dir.lastIndexOf("/"));
|
|
2688
|
-
/** The nearest ancestor-or-self directory a `@segment` reset names (`''` is the root). */
|
|
2689
|
-
function resetTarget(dir, segment) {
|
|
2690
|
-
for (let d = dir; d !== void 0; d = parentDir(d)) if (segment === "" ? d === ROUTES_DIR$1 : d.slice(d.lastIndexOf("/") + 1) === segment) return d;
|
|
2691
|
-
}
|
|
2692
|
-
const routeSsrCache = /* @__PURE__ */ new WeakMap();
|
|
2693
|
-
/**
|
|
2694
|
-
* Per route directory, whether its page (`page:<dir>`) or layout (`layout:<dir>`) is never
|
|
2695
|
-
* server-rendered. A page takes the nearest `ssr` export along its own files and then its layout
|
|
2696
|
-
* chain, following `+page@x`/`+layout@x` resets as SvelteKit does; an export other than a literal
|
|
2697
|
-
* `false`, or an unparsed option file, counts as SSR on. A layout is off only when every page using
|
|
2698
|
-
* it is — and the root layout also needs its own `ssr = false`, since it wraps the 404 page.
|
|
2699
|
-
*/
|
|
2700
|
-
function routeSsrOff(scope) {
|
|
2701
|
-
const kitModules = scope.kitModules ?? [];
|
|
2702
|
-
let hit = routeSsrCache.get(scope);
|
|
2703
|
-
if (hit) return hit;
|
|
2704
|
-
const pages = /* @__PURE__ */ new Map();
|
|
2705
|
-
const layouts = /* @__PURE__ */ new Map();
|
|
2706
|
-
const files = [
|
|
2707
|
-
...scope.sourceFiles ?? [],
|
|
2708
|
-
...(scope.components ?? []).map((c) => c.file),
|
|
2709
|
-
...kitModules.map((m) => m.file)
|
|
2710
|
-
];
|
|
2711
|
-
for (const file of files) {
|
|
2712
|
-
const match = ROUTE_FILE_RE.exec(file);
|
|
2713
|
-
if (!match) continue;
|
|
2714
|
-
const map = match[2] === "page" ? pages : layouts;
|
|
2715
|
-
if (match[3] !== void 0 || !map.has(match[1])) map.set(match[1], match[3]);
|
|
2716
|
-
}
|
|
2717
|
-
const option = /* @__PURE__ */ new Map();
|
|
2718
|
-
for (const m of kitModules) {
|
|
2719
|
-
const match = ROUTE_FILE_RE.exec(m.file);
|
|
2720
|
-
if (!match) continue;
|
|
2721
|
-
const key = `${match[2]}:${match[1]}`;
|
|
2722
|
-
if (m.ssrEnabled || m.parseFailed) option.set(key, "on");
|
|
2723
|
-
else if (m.ssrDisabled && !option.has(key)) option.set(key, "off");
|
|
2724
|
-
}
|
|
2725
|
-
hit = /* @__PURE__ */ new Map();
|
|
2726
|
-
for (const [dir, reset] of pages) {
|
|
2727
|
-
const chain = [];
|
|
2728
|
-
for (let d = reset === void 0 ? dir : resetTarget(dir, reset); d !== void 0;) {
|
|
2729
|
-
const layoutReset = layouts.get(d);
|
|
2730
|
-
if (layouts.has(d)) chain.push(d);
|
|
2731
|
-
d = layoutReset === void 0 ? parentDir(d) : resetTarget(parentDir(d), layoutReset);
|
|
2732
|
-
}
|
|
2733
|
-
const off = [`page:${dir}`, ...chain.map((l) => `layout:${l}`)].map((k) => option.get(k)).find(Boolean) === "off";
|
|
2734
|
-
hit.set(`page:${dir}`, off);
|
|
2735
|
-
for (const l of chain) hit.set(`layout:${l}`, (hit.get(`layout:${l}`) ?? true) && off);
|
|
2736
|
-
}
|
|
2737
|
-
if (option.get(`layout:${ROUTES_DIR$1}`) !== "off") hit.delete(`layout:${ROUTES_DIR$1}`);
|
|
2738
|
-
routeSsrCache.set(scope, hit);
|
|
2739
|
-
return hit;
|
|
2740
|
-
}
|
|
2741
|
-
/** Whether a `+page`/`+layout` file (module or component) belongs to a route that is never server-rendered. */
|
|
2742
|
-
function routeNeverSsr(file, scope) {
|
|
2743
|
-
const match = ROUTE_FILE_RE.exec(file);
|
|
2744
|
-
return !!match && routeSsrOff(scope).get(`${match[2]}:${match[1]}`) === true;
|
|
2745
|
-
}
|
|
2746
|
-
/** Whether a universal `+page`/`+layout` module never runs on the server — its own `ssr = false`, an app-wide one, or one its route inherits. */
|
|
2747
|
-
function universalNeverSsr(m, scope) {
|
|
2748
|
-
return m.kind === "universal" && (m.ssrDisabled !== void 0 || appSsrDisabled(scope.kitModules) || routeNeverSsr(m.file, scope));
|
|
2749
|
-
}
|
|
2750
|
-
//#endregion
|
|
2751
2896
|
//#region src/rules/seo/ssr-disabled.ts
|
|
2752
2897
|
const seoSsrDisabled = kitModuleRule({
|
|
2753
2898
|
id: "seo/ssr-disabled",
|
|
@@ -3244,7 +3389,7 @@ const correctnessInstanceBrowserGlobal = componentRule({
|
|
|
3244
3389
|
label: "Server-safe component init",
|
|
3245
3390
|
recommendation: "Move browser-only code into onMount or $effect (they never run on the server), or guard it with browser from $app/environment (or a typeof check).",
|
|
3246
3391
|
rationale: "A component instance script runs on the server on every SSR render, where window/document/localStorage do not exist. Warning, not critical: a component rendered only behind a parent {#if browser} (or a client-only dynamic import) is a legitimate pattern that static analysis cannot prove cross-file.",
|
|
3247
|
-
applies: (c, _o, ctx) => (c.browserGlobalRefs ?? []).some((r) => r.context === "instance") && !appSsrDisabled(ctx
|
|
3392
|
+
applies: (c, _o, ctx) => (c.browserGlobalRefs ?? []).some((r) => r.context === "instance") && !appSsrDisabled(ctx) && !routeNeverSsr(c.file, ctx),
|
|
3248
3393
|
bad: (c) => (c.browserGlobalRefs ?? []).filter((r) => r.context === "instance").map((r) => ({
|
|
3249
3394
|
line: r.line,
|
|
3250
3395
|
message: `${r.name} is accessed during component initialisation — during SSR this runs on the server, where ${r.name} is not defined`
|
|
@@ -3432,7 +3577,8 @@ const PREPROCESSED_STYLE_RE = /(<style\b[^>]*\slang\s*=\s*['"]?[^'"\s>]+['"]?[^>
|
|
|
3432
3577
|
* the text scan's imprecision cannot reach it. A file that does not parse is already a hard failure,
|
|
3433
3578
|
* which the retry can only improve on.
|
|
3434
3579
|
*/
|
|
3435
|
-
function parseSvelte(
|
|
3580
|
+
function parseSvelte(raw, filename) {
|
|
3581
|
+
const source = raw.charCodeAt(0) === 65279 ? ` ${raw.slice(1)}` : raw;
|
|
3436
3582
|
try {
|
|
3437
3583
|
return parse(source, {
|
|
3438
3584
|
modern: true,
|
|
@@ -3498,12 +3644,25 @@ function attrValue(attributes, name) {
|
|
|
3498
3644
|
const attr = findAttr(attributes, name);
|
|
3499
3645
|
return attr ? attrValueOf(attr) : "absent";
|
|
3500
3646
|
}
|
|
3647
|
+
const lineStarts = /* @__PURE__ */ new Map();
|
|
3501
3648
|
function lineOf(source, offset) {
|
|
3502
3649
|
if (typeof offset !== "number" || offset < 0) return 0;
|
|
3503
|
-
let
|
|
3650
|
+
let starts = lineStarts.get(source);
|
|
3651
|
+
if (!starts) {
|
|
3652
|
+
starts = [0];
|
|
3653
|
+
for (let i = source.indexOf("\n"); i !== -1; i = source.indexOf("\n", i + 1)) starts.push(i + 1);
|
|
3654
|
+
if (lineStarts.size >= 8) lineStarts.delete(lineStarts.keys().next().value);
|
|
3655
|
+
lineStarts.set(source, starts);
|
|
3656
|
+
}
|
|
3504
3657
|
const end = Math.min(offset, source.length);
|
|
3505
|
-
|
|
3506
|
-
|
|
3658
|
+
let lo = 0;
|
|
3659
|
+
let hi = starts.length;
|
|
3660
|
+
while (lo < hi) {
|
|
3661
|
+
const mid = lo + hi >> 1;
|
|
3662
|
+
if (starts[mid] <= end) lo = mid + 1;
|
|
3663
|
+
else hi = mid;
|
|
3664
|
+
}
|
|
3665
|
+
return lo;
|
|
3507
3666
|
}
|
|
3508
3667
|
/**
|
|
3509
3668
|
* An element's attribute by name, matched **case-insensitively**: HTML attribute names are, so
|
|
@@ -3536,7 +3695,7 @@ function attrTextOf(attr) {
|
|
|
3536
3695
|
//#region src/module-ast.ts
|
|
3537
3696
|
function unwrapTs(expr) {
|
|
3538
3697
|
let cur = expr;
|
|
3539
|
-
while (cur
|
|
3698
|
+
while (cur != null && (cur.type === "TSSatisfiesExpression" || cur.type === "TSAsExpression" || cur.type === "TSNonNullExpression")) cur = cur.expression;
|
|
3540
3699
|
return cur;
|
|
3541
3700
|
}
|
|
3542
3701
|
/** AST metadata keys every walker skips — never traversed as child nodes. Shared with the Kit-module parser (the security kit-module rules). */
|
|
@@ -3589,8 +3748,8 @@ function addBoundNames(id, acc) {
|
|
|
3589
3748
|
}
|
|
3590
3749
|
/** The base identifier name of a (possibly nested) member expression or identifier, else undefined. Shared with the Kit-module parser (the security kit-module rules). */
|
|
3591
3750
|
function rootObjectName(node) {
|
|
3592
|
-
let cur = node;
|
|
3593
|
-
while (cur?.type === "MemberExpression") cur = cur.object;
|
|
3751
|
+
let cur = unwrapTs(node);
|
|
3752
|
+
while (cur?.type === "MemberExpression") cur = unwrapTs(cur.object);
|
|
3594
3753
|
return cur?.type === "Identifier" ? cur.name : void 0;
|
|
3595
3754
|
}
|
|
3596
3755
|
/**
|
|
@@ -3842,6 +4001,11 @@ function collectProgramBindings(program) {
|
|
|
3842
4001
|
for (const s of stmt.specifiers ?? []) if (s?.local?.type === "Identifier") bound.add(s.local.name);
|
|
3843
4002
|
continue;
|
|
3844
4003
|
}
|
|
4004
|
+
if (stmt?.type === "LabeledStatement" && stmt.label?.name === "$") {
|
|
4005
|
+
const e = stmt.body?.type === "ExpressionStatement" ? stmt.body.expression : void 0;
|
|
4006
|
+
if (e?.type === "AssignmentExpression" && e.operator === "=") addBoundNames(e.left, bound);
|
|
4007
|
+
continue;
|
|
4008
|
+
}
|
|
3845
4009
|
const decl = unwrapExport(stmt);
|
|
3846
4010
|
if (decl?.type === "VariableDeclaration") for (const d of decl.declarations ?? []) addBoundNames(d?.id, bound);
|
|
3847
4011
|
else if ((decl?.type === "FunctionDeclaration" || decl?.type === "ClassDeclaration") && decl.id?.type === "Identifier") bound.add(decl.id.name);
|
|
@@ -3886,6 +4050,13 @@ function isBrowserGuardTest(test, guardBindings) {
|
|
|
3886
4050
|
});
|
|
3887
4051
|
return guarded;
|
|
3888
4052
|
}
|
|
4053
|
+
/** The tracked global `G` of a `typeof G === 'undefined'` test: the test is false only where `G` is defined. */
|
|
4054
|
+
function missingGlobalOf(n) {
|
|
4055
|
+
if (n?.type !== "BinaryExpression" || n.operator !== "===" && n.operator !== "==") return void 0;
|
|
4056
|
+
const sides = [n.left, n.right];
|
|
4057
|
+
if (!sides.some((s) => s?.type === "Literal" && s.value === "undefined")) return void 0;
|
|
4058
|
+
return sides.find((s) => s?.type === "UnaryExpression" && s.operator === "typeof" && s.argument?.type === "Identifier" && BROWSER_GLOBALS.has(s.argument.name))?.argument.name;
|
|
4059
|
+
}
|
|
3889
4060
|
/**
|
|
3890
4061
|
* Names of derived guard bindings (one level, no fixpoint chasing): a top-level
|
|
3891
4062
|
* (export-unwrapped) `const`/`let` declarator whose init passes `isBrowserGuardTest` —
|
|
@@ -3905,6 +4076,95 @@ function collectDerivedGuardBindings(program, guards) {
|
|
|
3905
4076
|
}
|
|
3906
4077
|
return derived;
|
|
3907
4078
|
}
|
|
4079
|
+
const DOM_ONLY_GLOBALS = /* @__PURE__ */ new Set(["window", "document"]);
|
|
4080
|
+
/**
|
|
4081
|
+
* Unlike `isBrowserGuardTest` this keeps the polarity and each outcome apart, because a write the
|
|
4082
|
+
* guard leaves reachable on the server must still be reported: `!browser && url.search` being falsy
|
|
4083
|
+
* proves nothing.
|
|
4084
|
+
*/
|
|
4085
|
+
function guardImplies(test, guards, shadowed) {
|
|
4086
|
+
if (test?.type === "Identifier") return !shadowed.has(test.name) && guards.get(test.name) || {};
|
|
4087
|
+
if (test?.type === "UnaryExpression" && test.operator === "!") {
|
|
4088
|
+
const inner = guardImplies(test.argument, guards, shadowed);
|
|
4089
|
+
return {
|
|
4090
|
+
truthy: inner.falsy,
|
|
4091
|
+
falsy: inner.truthy
|
|
4092
|
+
};
|
|
4093
|
+
}
|
|
4094
|
+
if (test?.type === "LogicalExpression" && (test.operator === "&&" || test.operator === "||")) {
|
|
4095
|
+
const a = guardImplies(test.left, guards, shadowed);
|
|
4096
|
+
const b = guardImplies(test.right, guards, shadowed);
|
|
4097
|
+
const agree = (x, y) => x === y ? x : void 0;
|
|
4098
|
+
return test.operator === "&&" ? {
|
|
4099
|
+
truthy: a.truthy ?? b.truthy,
|
|
4100
|
+
falsy: agree(a.falsy, b.falsy)
|
|
4101
|
+
} : {
|
|
4102
|
+
truthy: agree(a.truthy, b.truthy),
|
|
4103
|
+
falsy: a.falsy ?? b.falsy
|
|
4104
|
+
};
|
|
4105
|
+
}
|
|
4106
|
+
if (test?.type === "BinaryExpression" && [
|
|
4107
|
+
"===",
|
|
4108
|
+
"!==",
|
|
4109
|
+
"==",
|
|
4110
|
+
"!="
|
|
4111
|
+
].includes(test.operator)) {
|
|
4112
|
+
const sides = [test.left, test.right];
|
|
4113
|
+
const hasTypeofGlobal = sides.some((s) => s?.type === "UnaryExpression" && s.operator === "typeof" && s.argument?.type === "Identifier" && DOM_ONLY_GLOBALS.has(s.argument.name) && !shadowed.has(s.argument.name));
|
|
4114
|
+
const type = sides.find((s) => s?.type === "Literal" && typeof s.value === "string")?.value;
|
|
4115
|
+
if (!hasTypeofGlobal || type === void 0) return {};
|
|
4116
|
+
const equal = test.operator === "===" || test.operator === "==";
|
|
4117
|
+
if (type === "undefined") return equal ? {
|
|
4118
|
+
truthy: "server",
|
|
4119
|
+
falsy: "browser"
|
|
4120
|
+
} : {
|
|
4121
|
+
truthy: "browser",
|
|
4122
|
+
falsy: "server"
|
|
4123
|
+
};
|
|
4124
|
+
return equal ? { truthy: "browser" } : { falsy: "browser" };
|
|
4125
|
+
}
|
|
4126
|
+
return {};
|
|
4127
|
+
}
|
|
4128
|
+
/**
|
|
4129
|
+
* Source ranges only the browser reaches: the arm of an `if`/ternary its browser guard selects, the
|
|
4130
|
+
* right side of `browser && x` or `!browser || x`, and the statements after a terminating early return
|
|
4131
|
+
* the server always takes (`if (!browser) return;`) in their block. A top-level `const` derived from a
|
|
4132
|
+
* guard (`const serverOnly = !browser`) carries its own polarity.
|
|
4133
|
+
*/
|
|
4134
|
+
function browserOnlyRanges(program, envGuards) {
|
|
4135
|
+
const guards = new Map([...envGuards].map((name) => [name, {
|
|
4136
|
+
truthy: "browser",
|
|
4137
|
+
falsy: "server"
|
|
4138
|
+
}]));
|
|
4139
|
+
const bound = collectProgramBindings(program);
|
|
4140
|
+
for (const name of envGuards) bound.delete(name);
|
|
4141
|
+
for (const stmt of program.body ?? []) {
|
|
4142
|
+
const decl = unwrapExport(stmt);
|
|
4143
|
+
if (decl?.type !== "VariableDeclaration" || decl.kind !== "const") continue;
|
|
4144
|
+
for (const d of decl.declarations ?? []) {
|
|
4145
|
+
const implied = d?.id?.type === "Identifier" && d.init ? guardImplies(d.init, guards, bound) : {};
|
|
4146
|
+
if (implied.truthy || implied.falsy) guards.set(d.id.name, implied);
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
for (const name of guards.keys()) bound.delete(name);
|
|
4150
|
+
const out = [];
|
|
4151
|
+
const push = (n) => n && out.push([n.start, n.end]);
|
|
4152
|
+
walkScoped(program, (n, shadowed) => {
|
|
4153
|
+
const implies = (test) => guardImplies(test, guards, shadowed);
|
|
4154
|
+
if (n.type === "IfStatement" || n.type === "ConditionalExpression") {
|
|
4155
|
+
const implied = implies(n.test);
|
|
4156
|
+
if (implied.truthy === "browser") push(n.consequent);
|
|
4157
|
+
if (implied.falsy === "browser") push(n.alternate);
|
|
4158
|
+
} else if (n.type === "LogicalExpression" && (n.operator === "&&" || n.operator === "||")) {
|
|
4159
|
+
if (implies(n.left)[n.operator === "&&" ? "truthy" : "falsy"] === "browser") push(n.right);
|
|
4160
|
+
} else if (n.type === "BlockStatement" || n.type === "Program") {
|
|
4161
|
+
const body = n.body ?? [];
|
|
4162
|
+
const at = body.findIndex((stmt) => stmt?.type === "IfStatement" && implies(stmt.test).falsy === "browser" && guardTerminates(stmt.consequent));
|
|
4163
|
+
if (at >= 0 && at < body.length - 1) out.push([body[at + 1].start, body[body.length - 1].end]);
|
|
4164
|
+
}
|
|
4165
|
+
}, bound);
|
|
4166
|
+
return out;
|
|
4167
|
+
}
|
|
3908
4168
|
/**
|
|
3909
4169
|
* Browser-global reads in code that executes when `program` (or a passed function body)
|
|
3910
4170
|
* is evaluated (correctness/server-browser-global, correctness/instance-browser-global). Position-aware — only read positions match: never a
|
|
@@ -3940,6 +4200,12 @@ function collectBrowserGlobalRefs(program, source, extra) {
|
|
|
3940
4200
|
if (EVAL_SCOPE_BOUNDARIES.has(n.type)) return;
|
|
3941
4201
|
if ((n.type === "IfStatement" || n.type === "ConditionalExpression") && isBrowserGuardTest(n.test, guards)) return;
|
|
3942
4202
|
if (n.type === "LogicalExpression" && (n.operator === "&&" ? isBrowserGuardTest(n.left, guards) : n.left?.type === "UnaryExpression" && n.left.operator === "!" && isBrowserGuardTest(n.left.argument, guards))) return;
|
|
4203
|
+
const missing = n.type === "LogicalExpression" && n.operator === "||" ? missingGlobalOf(n.left) : void 0;
|
|
4204
|
+
if (missing && !bound.has(missing) && !shadowed.has(missing)) {
|
|
4205
|
+
visit(n.left, shadowed);
|
|
4206
|
+
if (!DOM_ONLY_GLOBALS.has(missing)) visit(n.right, /* @__PURE__ */ new Set([...shadowed, missing]));
|
|
4207
|
+
return;
|
|
4208
|
+
}
|
|
3943
4209
|
const introduced = scopeIntroducedNames(n);
|
|
3944
4210
|
const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
|
|
3945
4211
|
switch (n.type) {
|
|
@@ -4034,6 +4300,11 @@ const HANDLER_NAMES = /* @__PURE__ */ new Set([
|
|
|
4034
4300
|
"OPTIONS",
|
|
4035
4301
|
"fallback"
|
|
4036
4302
|
]);
|
|
4303
|
+
/** Whether `arg` can be a store updater: a function literal, or a function this module declares. */
|
|
4304
|
+
function isUpdaterArgument(arg, localFunctions) {
|
|
4305
|
+
const e = arg ? unwrapTs(arg) : void 0;
|
|
4306
|
+
return isFunctionNode(e) || e?.type === "Identifier" && localFunctions.has(e.name);
|
|
4307
|
+
}
|
|
4037
4308
|
function isFunctionNode(n) {
|
|
4038
4309
|
return n?.type === "FunctionDeclaration" || n?.type === "FunctionExpression" || n?.type === "ArrowFunctionExpression";
|
|
4039
4310
|
}
|
|
@@ -4135,7 +4406,7 @@ function collectStartupFunctions(program) {
|
|
|
4135
4406
|
}
|
|
4136
4407
|
/**
|
|
4137
4408
|
* The `export const ssr = false` / `export const csr = false` opt-out, when
|
|
4138
|
-
* present: inline form (`satisfies`/`as` unwrapped) or same-file alias export
|
|
4409
|
+
* present (`= dev` too): inline form (`satisfies`/`as` unwrapped) or same-file alias export
|
|
4139
4410
|
* (`const ssr = false; export { ssr };`). Returns the declaration's line in the
|
|
4140
4411
|
* WRAPPED source (the caller applies the −1 shift). An `ssr = false` file never
|
|
4141
4412
|
* runs on the server — correctness/server-browser-global skips its browser-global scan, and seo/ssr-disabled
|
|
@@ -4143,10 +4414,27 @@ function collectStartupFunctions(program) {
|
|
|
4143
4414
|
* performance/load-waterfall exempts it (see `csrDisabled` on `KitModuleFacts`).
|
|
4144
4415
|
*/
|
|
4145
4416
|
function findFalseOptOut(program, source, name) {
|
|
4417
|
+
const dev = collectNamedImportAliases(program, "$app/environment", DEV_NAMES);
|
|
4146
4418
|
let hit;
|
|
4147
4419
|
forEachNamedExport(program, (exported, value, anchor) => {
|
|
4148
|
-
|
|
4149
|
-
|
|
4420
|
+
const literal = value?.type === "Literal" && value.value === false;
|
|
4421
|
+
if (exported !== name || !(literal || value?.type === "Identifier" && dev.has(value.name))) return void 0;
|
|
4422
|
+
hit = {
|
|
4423
|
+
line: lineOf(source, anchor.start),
|
|
4424
|
+
literal
|
|
4425
|
+
};
|
|
4426
|
+
return true;
|
|
4427
|
+
});
|
|
4428
|
+
return hit;
|
|
4429
|
+
}
|
|
4430
|
+
const DEV_NAMES = /* @__PURE__ */ new Set(["dev"]);
|
|
4431
|
+
const BUILDING_NAMES = /* @__PURE__ */ new Set(["building"]);
|
|
4432
|
+
/** Whether the module exports `name` as the literal `true` (inline or same-file alias export). */
|
|
4433
|
+
function exportsTrue(program, name) {
|
|
4434
|
+
let hit = false;
|
|
4435
|
+
forEachNamedExport(program, (exported, value) => {
|
|
4436
|
+
if (exported !== name) return void 0;
|
|
4437
|
+
hit = value?.type === "Literal" && value.value === true;
|
|
4150
4438
|
return true;
|
|
4151
4439
|
});
|
|
4152
4440
|
return hit;
|
|
@@ -4197,8 +4485,18 @@ const MUTATING_METHODS = /* @__PURE__ */ new Set([
|
|
|
4197
4485
|
"add",
|
|
4198
4486
|
"set"
|
|
4199
4487
|
]);
|
|
4488
|
+
const ITERATING_METHODS = /* @__PURE__ */ new Set([
|
|
4489
|
+
"forEach",
|
|
4490
|
+
"map",
|
|
4491
|
+
"flatMap",
|
|
4492
|
+
"filter",
|
|
4493
|
+
"some",
|
|
4494
|
+
"every",
|
|
4495
|
+
"reduce"
|
|
4496
|
+
]);
|
|
4200
4497
|
const REDIRECT_NAMES = /* @__PURE__ */ new Set(["redirect"]);
|
|
4201
4498
|
const EXIT_NAMES = /* @__PURE__ */ new Set(["redirect", "error"]);
|
|
4499
|
+
const ERROR_NAMES = /* @__PURE__ */ new Set(["error"]);
|
|
4202
4500
|
/**
|
|
4203
4501
|
* Root-relative `redirect(status, '/…')` targets (correctness/base-path-navigation). Argument 1
|
|
4204
4502
|
* is the location (argument 0 is the status). Only a plain string literal counts, which
|
|
@@ -4218,6 +4516,116 @@ function collectRedirectCalls(node, locals, out = []) {
|
|
|
4218
4516
|
}
|
|
4219
4517
|
return out;
|
|
4220
4518
|
}
|
|
4519
|
+
/**
|
|
4520
|
+
* The string literals a `transformPageChunk` option replaces in the page HTML (`html.replace(TOKEN, …)`,
|
|
4521
|
+
* `.replaceAll`), wherever the file passes one to `resolve`, a same-file `const` holding the string
|
|
4522
|
+
* included. A token the hook builds at run time is not read.
|
|
4523
|
+
*/
|
|
4524
|
+
function pageChunkTokens(program) {
|
|
4525
|
+
const consts = /* @__PURE__ */ new Map();
|
|
4526
|
+
for (const stmt of program.body ?? []) {
|
|
4527
|
+
const decl = stmt?.type === "ExportNamedDeclaration" ? stmt.declaration : stmt;
|
|
4528
|
+
if (decl?.type !== "VariableDeclaration" || decl.kind !== "const") continue;
|
|
4529
|
+
for (const d of decl.declarations ?? []) {
|
|
4530
|
+
const init = unwrapTs(d.init);
|
|
4531
|
+
if (d.id?.type === "Identifier" && init?.type === "Literal" && typeof init.value === "string") consts.set(d.id.name, init.value);
|
|
4532
|
+
}
|
|
4533
|
+
}
|
|
4534
|
+
const out = /* @__PURE__ */ new Set();
|
|
4535
|
+
const visit = (n, inChunk) => {
|
|
4536
|
+
if (Array.isArray(n)) return n.forEach((c) => visit(c, inChunk));
|
|
4537
|
+
if (!n || typeof n !== "object" || typeof n.type !== "string") return;
|
|
4538
|
+
const chunk = inChunk || n.type === "Property" && !n.computed && n.key?.type === "Identifier" && n.key.name === "transformPageChunk";
|
|
4539
|
+
if (chunk && n.type === "CallExpression" && n.callee?.type === "MemberExpression" && !n.callee.computed) {
|
|
4540
|
+
const method = n.callee.property?.name;
|
|
4541
|
+
const arg = unwrapTs(n.arguments?.[0]);
|
|
4542
|
+
const token = arg?.type === "Literal" && typeof arg.value === "string" ? arg.value : arg?.type === "Identifier" ? consts.get(arg.name) : void 0;
|
|
4543
|
+
if ((method === "replace" || method === "replaceAll") && token) out.add(token);
|
|
4544
|
+
}
|
|
4545
|
+
for (const key of Object.keys(n)) if (!WALK_IGNORED_KEYS.has(key)) visit(n[key], chunk);
|
|
4546
|
+
};
|
|
4547
|
+
visit(program, false);
|
|
4548
|
+
return [...out];
|
|
4549
|
+
}
|
|
4550
|
+
/** Whether `node` calls `name` outside nested functions. */
|
|
4551
|
+
function callsOutsideFunctions(node, name) {
|
|
4552
|
+
if (Array.isArray(node)) return node.some((n) => callsOutsideFunctions(n, name));
|
|
4553
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string") return false;
|
|
4554
|
+
if (node.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === name) return true;
|
|
4555
|
+
return Object.keys(node).some((key) => !WALK_IGNORED_KEYS.has(key) && callsOutsideFunctions(node[key], name));
|
|
4556
|
+
}
|
|
4557
|
+
/**
|
|
4558
|
+
* The request paths the exported `handle` answers on every request, before calling `resolve`: an `if` on
|
|
4559
|
+
* the path alone (`pathname === '/x'`, `pathname.startsWith('/x')`, or an `||` of those, with `pathname`
|
|
4560
|
+
* read from the `event` parameter, directly or through a `const`) whose body returns or throws, among the
|
|
4561
|
+
* statements before the first one that calls `resolve`. A test that also reads anything else, a `sequence(…)`
|
|
4562
|
+
* handle and a guard in another function are not read.
|
|
4563
|
+
*/
|
|
4564
|
+
function handleAnsweredPaths(program, exits) {
|
|
4565
|
+
let handle;
|
|
4566
|
+
forEachNamedExport(program, (name, value) => {
|
|
4567
|
+
if (name === "handle" && isFunctionNode(value)) handle = value;
|
|
4568
|
+
return handle !== void 0;
|
|
4569
|
+
});
|
|
4570
|
+
const param = handle?.params?.[0];
|
|
4571
|
+
if (handle?.body?.type !== "BlockStatement" || param?.type !== "ObjectPattern") return [];
|
|
4572
|
+
const local = (key) => {
|
|
4573
|
+
const p = param.properties?.find((q) => q.type === "Property" && q.key?.type === "Identifier" && q.key.name === key);
|
|
4574
|
+
return p?.value?.type === "Identifier" ? p.value.name : void 0;
|
|
4575
|
+
};
|
|
4576
|
+
const event = local("event");
|
|
4577
|
+
const resolve = local("resolve");
|
|
4578
|
+
if (!event || !resolve) return [];
|
|
4579
|
+
const isUrl = (n) => n?.type === "MemberExpression" && !n.computed && n.property?.name === "url" && n.object?.type === "Identifier" && n.object.name === event;
|
|
4580
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
4581
|
+
const isPath = (n) => {
|
|
4582
|
+
const e = unwrapTs(n);
|
|
4583
|
+
if (e?.type === "Identifier") return aliases.has(e.name);
|
|
4584
|
+
return e?.type === "MemberExpression" && !e.computed && e.property?.name === "pathname" && isUrl(e.object);
|
|
4585
|
+
};
|
|
4586
|
+
const literal = (n) => n?.type === "Literal" && typeof n.value === "string" ? n.value : void 0;
|
|
4587
|
+
const conds = (t) => {
|
|
4588
|
+
if (t?.type === "LogicalExpression" && t.operator === "||") {
|
|
4589
|
+
const l = conds(t.left);
|
|
4590
|
+
const r = conds(t.right);
|
|
4591
|
+
return l && r ? [...l, ...r] : void 0;
|
|
4592
|
+
}
|
|
4593
|
+
if (t?.type === "BinaryExpression" && (t.operator === "===" || t.operator === "==")) {
|
|
4594
|
+
const v = isPath(t.left) ? literal(t.right) : isPath(t.right) ? literal(t.left) : void 0;
|
|
4595
|
+
return v === void 0 ? void 0 : [{
|
|
4596
|
+
op: "eq",
|
|
4597
|
+
v
|
|
4598
|
+
}];
|
|
4599
|
+
}
|
|
4600
|
+
const callee = t?.type === "CallExpression" ? t.callee : void 0;
|
|
4601
|
+
const v = literal(t?.arguments?.[0]);
|
|
4602
|
+
return callee?.type === "MemberExpression" && callee.property?.name === "startsWith" && isPath(callee.object) && v !== void 0 ? [{
|
|
4603
|
+
op: "starts",
|
|
4604
|
+
v
|
|
4605
|
+
}] : void 0;
|
|
4606
|
+
};
|
|
4607
|
+
const exitsAlways = (s) => {
|
|
4608
|
+
if (s?.type === "BlockStatement") return s.body.length > 0 && exitsAlways(s.body[s.body.length - 1]);
|
|
4609
|
+
if (s?.type === "ReturnStatement" || s?.type === "ThrowStatement") return true;
|
|
4610
|
+
const call = s?.type === "ExpressionStatement" ? unwrapTs(s.expression) : void 0;
|
|
4611
|
+
return call?.type === "CallExpression" && call.callee?.type === "Identifier" && exits.has(call.callee.name);
|
|
4612
|
+
};
|
|
4613
|
+
const out = [];
|
|
4614
|
+
for (const stmt of handle.body.body) {
|
|
4615
|
+
if (callsOutsideFunctions(stmt, resolve)) break;
|
|
4616
|
+
if (stmt.type === "VariableDeclaration" && stmt.kind === "const") for (const d of stmt.declarations ?? []) {
|
|
4617
|
+
if (d.id?.type === "Identifier" && isPath(d.init)) aliases.add(d.id.name);
|
|
4618
|
+
if (d.id?.type === "ObjectPattern" && isUrl(unwrapTs(d.init))) {
|
|
4619
|
+
const p = d.id.properties?.find((q) => q.type === "Property" && q.key?.name === "pathname");
|
|
4620
|
+
if (p?.value?.type === "Identifier") aliases.add(p.value.name);
|
|
4621
|
+
}
|
|
4622
|
+
}
|
|
4623
|
+
if (stmt.type !== "IfStatement" || !exitsAlways(stmt.consequent)) continue;
|
|
4624
|
+
const c = conds(stmt.test);
|
|
4625
|
+
if (c) out.push(...c);
|
|
4626
|
+
}
|
|
4627
|
+
return out;
|
|
4628
|
+
}
|
|
4221
4629
|
/** Whether `node` holds a `return` outside nested functions. */
|
|
4222
4630
|
function containsReturn(node) {
|
|
4223
4631
|
if (Array.isArray(node)) return node.some(containsReturn);
|
|
@@ -4232,9 +4640,16 @@ function containsReturn(node) {
|
|
|
4232
4640
|
* of the same file that itself always redirects. A redirect under an `if` without an `else`, in a
|
|
4233
4641
|
* loop, or in a function declared inside `load` does not count.
|
|
4234
4642
|
*/
|
|
4235
|
-
function loadNeverRenders(program, locals) {
|
|
4643
|
+
function loadNeverRenders(program, locals, building = /* @__PURE__ */ new Set()) {
|
|
4236
4644
|
const load = locals.size > 0 ? findLoadFunction(program) : void 0;
|
|
4237
|
-
|
|
4645
|
+
return !!load?.body && alwaysExits(program, load, locals, building);
|
|
4646
|
+
}
|
|
4647
|
+
/**
|
|
4648
|
+
* Whether `fn`, a function of `program`, redirects or errors on every call (see `loadNeverRenders`).
|
|
4649
|
+
* `building` holds the names `building` from `$app/environment` is imported as: an `if` on one of them
|
|
4650
|
+
* is read with the test false, as it is on every request.
|
|
4651
|
+
*/
|
|
4652
|
+
function alwaysExits(program, fn, locals, building = /* @__PURE__ */ new Set()) {
|
|
4238
4653
|
const topLevel = collectTopLevelBindings(program);
|
|
4239
4654
|
const fnRedirects = (fn, seen, observed) => {
|
|
4240
4655
|
const shadow = /* @__PURE__ */ new Set();
|
|
@@ -4276,15 +4691,183 @@ function loadNeverRenders(program, locals) {
|
|
|
4276
4691
|
}
|
|
4277
4692
|
return false;
|
|
4278
4693
|
};
|
|
4694
|
+
const plainReturn = (node, shadow, frame) => {
|
|
4695
|
+
if (Array.isArray(node)) return node.some((n) => plainReturn(n, shadow, frame));
|
|
4696
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string" || isFunctionNode(node)) return false;
|
|
4697
|
+
if (node.type === "ReturnStatement") return !isRedirect(node.argument, shadow, frame, frame.observed);
|
|
4698
|
+
const scope = node.type === "BlockStatement" ? within(node.body, shadow) : shadow;
|
|
4699
|
+
return Object.keys(node).some((key) => !WALK_IGNORED_KEYS.has(key) && plainReturn(node[key], scope, frame));
|
|
4700
|
+
};
|
|
4279
4701
|
const always = (body, outer, rethrow, frame) => {
|
|
4280
4702
|
const shadow = within(body, outer);
|
|
4281
|
-
for (
|
|
4703
|
+
for (let stmt of body ?? []) {
|
|
4704
|
+
const test = stmt?.type === "IfStatement" ? unwrapTs(stmt.test) : void 0;
|
|
4705
|
+
if (test?.type === "Identifier" && building.has(test.name) && !shadow.has(test.name)) {
|
|
4706
|
+
if (!stmt.alternate) continue;
|
|
4707
|
+
stmt = stmt.alternate;
|
|
4708
|
+
}
|
|
4282
4709
|
if (redirects(stmt, shadow, rethrow, frame)) return true;
|
|
4283
|
-
if (
|
|
4710
|
+
if (plainReturn(stmt, shadow, frame)) return false;
|
|
4284
4711
|
}
|
|
4285
4712
|
return false;
|
|
4286
4713
|
};
|
|
4287
|
-
return fnRedirects(
|
|
4714
|
+
return fnRedirects(fn, /* @__PURE__ */ new Set(), true);
|
|
4715
|
+
}
|
|
4716
|
+
/**
|
|
4717
|
+
* The binding a load's leading flag guard reads: a top-level `if (!FLAG) <exit>` (or `!NS.FLAG`) before
|
|
4718
|
+
* any `return`, where `FLAG` is imported from a repo-local module. When that export is the literal
|
|
4719
|
+
* `false`, every request takes the exit; `collectKitModuleFacts` reads the module to decide.
|
|
4720
|
+
*/
|
|
4721
|
+
function leadingFlagGuard(program, exits, imported) {
|
|
4722
|
+
const load = exits.size > 0 ? findLoadFunction(program) : void 0;
|
|
4723
|
+
if (load?.body?.type !== "BlockStatement") return void 0;
|
|
4724
|
+
const local = /* @__PURE__ */ new Set();
|
|
4725
|
+
for (const param of load.params ?? []) addBoundNames(param, local);
|
|
4726
|
+
for (const stmt of load.body.body) if (stmt?.type === "VariableDeclaration") for (const d of stmt.declarations ?? []) addBoundNames(d?.id, local);
|
|
4727
|
+
else if ((stmt?.type === "FunctionDeclaration" || stmt?.type === "ClassDeclaration") && stmt.id) local.add(stmt.id.name);
|
|
4728
|
+
const exitCall = (e) => {
|
|
4729
|
+
const x = unwrapTs(e);
|
|
4730
|
+
return x?.type === "CallExpression" && x.callee?.type === "Identifier" && exits.has(x.callee.name) && !local.has(x.callee.name);
|
|
4731
|
+
};
|
|
4732
|
+
const exitsFirst = (stmt) => {
|
|
4733
|
+
if (stmt?.type === "BlockStatement") {
|
|
4734
|
+
for (const inner of stmt.body) {
|
|
4735
|
+
if (exitsFirst(inner)) return true;
|
|
4736
|
+
if (containsReturn(inner)) return false;
|
|
4737
|
+
}
|
|
4738
|
+
return false;
|
|
4739
|
+
}
|
|
4740
|
+
return stmt?.type === "ExpressionStatement" && exitCall(stmt.expression) || (stmt?.type === "ThrowStatement" || stmt?.type === "ReturnStatement") && exitCall(stmt.argument);
|
|
4741
|
+
};
|
|
4742
|
+
for (const stmt of load.body.body) {
|
|
4743
|
+
const test = stmt?.type === "IfStatement" && exitsFirst(stmt.consequent) ? unwrapTs(stmt.test) : void 0;
|
|
4744
|
+
if (test?.type === "UnaryExpression" && test.operator === "!") {
|
|
4745
|
+
const arg = unwrapTs(test.argument);
|
|
4746
|
+
const root = arg?.type === "MemberExpression" && !arg.computed ? unwrapTs(arg.object) : arg;
|
|
4747
|
+
const member = arg !== root && arg.property?.type === "Identifier" ? arg.property.name : void 0;
|
|
4748
|
+
const from = root?.type === "Identifier" && !local.has(root.name) && (arg === root || member) ? imported(root.name) : void 0;
|
|
4749
|
+
if (from) return {
|
|
4750
|
+
...from,
|
|
4751
|
+
...member ? { member } : {}
|
|
4752
|
+
};
|
|
4753
|
+
}
|
|
4754
|
+
if (containsReturn(stmt)) return void 0;
|
|
4755
|
+
}
|
|
4756
|
+
}
|
|
4757
|
+
/** The imported function `export const load = factory(…)` calls, for `collectKitModuleFacts` to read. */
|
|
4758
|
+
function loadFactory(program, imported) {
|
|
4759
|
+
let call;
|
|
4760
|
+
forEachNamedExport(program, (name, value) => {
|
|
4761
|
+
if (name !== "load") return void 0;
|
|
4762
|
+
call = unwrapTs(value);
|
|
4763
|
+
return true;
|
|
4764
|
+
});
|
|
4765
|
+
return call?.type === "CallExpression" && call.callee?.type === "Identifier" ? imported(call.callee.name) : void 0;
|
|
4766
|
+
}
|
|
4767
|
+
/**
|
|
4768
|
+
* The exported functions of a module that return a function which redirects or errors on every call
|
|
4769
|
+
* (`createRedirect = (to) => () => redirect(307, to)`), read like a `load` that does.
|
|
4770
|
+
*/
|
|
4771
|
+
function parseExitFactoryExports(source, filename) {
|
|
4772
|
+
const { program } = parseModuleProgram(source, filename);
|
|
4773
|
+
const exits = collectNamedImportAliases(program, "@sveltejs/kit", EXIT_NAMES);
|
|
4774
|
+
const names = /* @__PURE__ */ new Set();
|
|
4775
|
+
if (exits.size === 0) return names;
|
|
4776
|
+
forEachNamedExport(program, (name, value) => {
|
|
4777
|
+
if (!isFunctionNode(value)) return void 0;
|
|
4778
|
+
const first = (value.body?.type === "BlockStatement" ? value.body.body : [{
|
|
4779
|
+
type: "ReturnStatement",
|
|
4780
|
+
argument: value.body
|
|
4781
|
+
}]).find((stmt) => containsReturn(stmt));
|
|
4782
|
+
const returned = first?.type === "ReturnStatement" ? first.argument : void 0;
|
|
4783
|
+
if (isFunctionNode(returned) && alwaysExits(program, returned, exits)) names.add(name);
|
|
4784
|
+
});
|
|
4785
|
+
return names;
|
|
4786
|
+
}
|
|
4787
|
+
/** The exports of a module that are the literal `false`, as `NAME` or `NAME.member` (an object literal's own property). */
|
|
4788
|
+
function parseFalseExports(source, filename) {
|
|
4789
|
+
return collectFalseExports(parseModuleProgram(source, filename).program);
|
|
4790
|
+
}
|
|
4791
|
+
/** `parseFalseExports` over a parsed module. */
|
|
4792
|
+
function collectFalseExports(program) {
|
|
4793
|
+
const names = /* @__PURE__ */ new Set();
|
|
4794
|
+
for (const stmt of program?.body ?? []) {
|
|
4795
|
+
if (stmt?.type !== "ExportNamedDeclaration" || stmt.declaration?.type !== "VariableDeclaration") continue;
|
|
4796
|
+
if (stmt.declaration.kind !== "const") continue;
|
|
4797
|
+
for (const d of stmt.declaration.declarations ?? []) {
|
|
4798
|
+
const init = unwrapTs(d?.init);
|
|
4799
|
+
if (d?.id?.type !== "Identifier") continue;
|
|
4800
|
+
if (init?.type === "Literal" && init.value === false) names.add(d.id.name);
|
|
4801
|
+
if (init?.type === "ObjectExpression") {
|
|
4802
|
+
const members = /* @__PURE__ */ new Map();
|
|
4803
|
+
for (const p of init.properties ?? []) {
|
|
4804
|
+
const v = unwrapTs(p?.value);
|
|
4805
|
+
if (p?.type !== "Property" || p.computed || p.key?.type !== "Identifier") members.clear();
|
|
4806
|
+
else members.set(p.key.name, v?.type === "Literal" && v.value === false);
|
|
4807
|
+
}
|
|
4808
|
+
for (const [key, isFalse] of members) if (isFalse) names.add(`${d.id.name}.${key}`);
|
|
4809
|
+
}
|
|
4810
|
+
}
|
|
4811
|
+
}
|
|
4812
|
+
return names;
|
|
4813
|
+
}
|
|
4814
|
+
/**
|
|
4815
|
+
* Whether the exported `load` sends a request without a session away before rendering: a top-level
|
|
4816
|
+
* `if` whose test is only negated reads of the request's `locals` (`!locals.user`,
|
|
4817
|
+
* `!(await locals.auth())`, or a `const` taken from one earlier in the body) and whose branch
|
|
4818
|
+
* redirects or errors 401/403. A test that also reads anything else — the URL, params — gates only
|
|
4819
|
+
* some requests and does not count; neither does a gate after a `return`.
|
|
4820
|
+
*/
|
|
4821
|
+
function loadGatesOnLocals(program) {
|
|
4822
|
+
const redirects = collectNamedImportAliases(program, "@sveltejs/kit", REDIRECT_NAMES);
|
|
4823
|
+
const errors = collectNamedImportAliases(program, "@sveltejs/kit", ERROR_NAMES);
|
|
4824
|
+
const load = redirects.size + errors.size > 0 ? findLoadFunction(program) : void 0;
|
|
4825
|
+
if (load?.body?.type !== "BlockStatement") return false;
|
|
4826
|
+
const param = load.params?.[0];
|
|
4827
|
+
const event = param?.type === "Identifier" ? param.name : void 0;
|
|
4828
|
+
const derived = /* @__PURE__ */ new Set();
|
|
4829
|
+
if (param?.type === "ObjectPattern") {
|
|
4830
|
+
for (const p of param.properties ?? []) if (p?.type === "Property" && (p.key?.name ?? p.key?.value) === "locals") addBoundNames(p.value, derived);
|
|
4831
|
+
}
|
|
4832
|
+
const fromLocals = (expr) => {
|
|
4833
|
+
let cur = unwrapTs(expr);
|
|
4834
|
+
let prev;
|
|
4835
|
+
for (;;) if (cur?.type === "AwaitExpression" || cur?.type === "ChainExpression") cur = unwrapTs(cur.argument ?? cur.expression);
|
|
4836
|
+
else if (cur?.type === "CallExpression" && (cur.arguments ?? []).length === 0) cur = unwrapTs(cur.callee);
|
|
4837
|
+
else if (cur?.type === "MemberExpression" && !cur.computed) [prev, cur] = [cur, unwrapTs(cur.object)];
|
|
4838
|
+
else break;
|
|
4839
|
+
if (cur?.type !== "Identifier") return false;
|
|
4840
|
+
return derived.has(cur.name) || cur.name === event && prev?.computed === false && prev.property?.name === "locals";
|
|
4841
|
+
};
|
|
4842
|
+
const negated = (test) => {
|
|
4843
|
+
const t = unwrapTs(test);
|
|
4844
|
+
if (t?.type === "LogicalExpression") return t.operator === "||" && negated(t.left) && negated(t.right);
|
|
4845
|
+
return t?.type === "UnaryExpression" && t.operator === "!" && fromLocals(t.argument);
|
|
4846
|
+
};
|
|
4847
|
+
const exitCall = (expr) => {
|
|
4848
|
+
const e = unwrapTs(expr);
|
|
4849
|
+
if (e?.type !== "CallExpression" || e.callee?.type !== "Identifier") return false;
|
|
4850
|
+
if (redirects.has(e.callee.name)) return true;
|
|
4851
|
+
if (!errors.has(e.callee.name)) return false;
|
|
4852
|
+
const status = e.arguments?.[0];
|
|
4853
|
+
return status?.type === "Literal" && (status.value === 401 || status.value === 403);
|
|
4854
|
+
};
|
|
4855
|
+
const exitsNow = (stmt) => {
|
|
4856
|
+
if (stmt?.type !== "BlockStatement") return stmt?.type === "ExpressionStatement" && exitCall(stmt.expression) || (stmt?.type === "ThrowStatement" || stmt?.type === "ReturnStatement") && exitCall(stmt.argument);
|
|
4857
|
+
for (const inner of stmt.body) {
|
|
4858
|
+
if (exitsNow(inner)) return true;
|
|
4859
|
+
if (containsReturn(inner)) return false;
|
|
4860
|
+
}
|
|
4861
|
+
return false;
|
|
4862
|
+
};
|
|
4863
|
+
for (const stmt of load.body.body) {
|
|
4864
|
+
if (stmt?.type === "IfStatement" && negated(stmt.test) && exitsNow(stmt.consequent)) return true;
|
|
4865
|
+
if (containsReturn(stmt)) return false;
|
|
4866
|
+
if (stmt?.type === "VariableDeclaration" && stmt.kind === "const") {
|
|
4867
|
+
for (const d of stmt.declarations ?? []) if (d?.init && fromLocals(d.init)) addBoundNames(d.id, derived);
|
|
4868
|
+
}
|
|
4869
|
+
}
|
|
4870
|
+
return false;
|
|
4288
4871
|
}
|
|
4289
4872
|
/**
|
|
4290
4873
|
* Whether `await`'s argument is a `parent()` / `<x>.parent()` call (Kit's parent-load
|
|
@@ -4306,6 +4889,7 @@ const BODY_METHODS = /* @__PURE__ */ new Set([
|
|
|
4306
4889
|
"formData",
|
|
4307
4890
|
"bytes"
|
|
4308
4891
|
]);
|
|
4892
|
+
const PROMISE_COMBINATORS = /* @__PURE__ */ new Set(["all", "allSettled"]);
|
|
4309
4893
|
/**
|
|
4310
4894
|
* Whether `await`'s argument is a response-body read (`res.json()`, `res.text()`, …):
|
|
4311
4895
|
* parsing an already-received body costs no extra round trip, so it is not a
|
|
@@ -4314,9 +4898,12 @@ const BODY_METHODS = /* @__PURE__ */ new Set([
|
|
|
4314
4898
|
*/
|
|
4315
4899
|
function isBodyParseCall(arg) {
|
|
4316
4900
|
const e = unwrapTs(arg);
|
|
4317
|
-
if (e?.type !== "CallExpression"
|
|
4901
|
+
if (e?.type !== "CallExpression") return false;
|
|
4318
4902
|
const callee = e.callee;
|
|
4319
|
-
|
|
4903
|
+
if (callee?.type !== "MemberExpression" || callee.computed || callee.property.type !== "Identifier") return false;
|
|
4904
|
+
if (BODY_METHODS.has(callee.property.name)) return !e.arguments?.length;
|
|
4905
|
+
const list = e.arguments?.length === 1 ? unwrapTs(e.arguments[0]) : void 0;
|
|
4906
|
+
return callee.object.type === "Identifier" && callee.object.name === "Promise" && PROMISE_COMBINATORS.has(callee.property.name) && list?.type === "ArrayExpression" && list.elements.length > 0 && list.elements.every((el) => el && isBodyParseCall(el));
|
|
4320
4907
|
}
|
|
4321
4908
|
const WORK_STARTING = /* @__PURE__ */ new Set([
|
|
4322
4909
|
"CallExpression",
|
|
@@ -4413,6 +5000,19 @@ function collectLoadWaterfalls(program, wrapped) {
|
|
|
4413
5000
|
};
|
|
4414
5001
|
const line = (start) => Math.max(0, lineOf(wrapped, start) - 1);
|
|
4415
5002
|
const tainted = /* @__PURE__ */ new Set();
|
|
5003
|
+
const ownBindings = /* @__PURE__ */ new Set();
|
|
5004
|
+
const collectOwn = (body) => {
|
|
5005
|
+
for (const stmt of body ?? []) {
|
|
5006
|
+
if (stmt?.type === "TryStatement") collectOwn(stmt.block?.body);
|
|
5007
|
+
if (stmt?.type !== "VariableDeclaration") continue;
|
|
5008
|
+
for (const d of stmt.declarations ?? []) {
|
|
5009
|
+
let init = unwrapTs(d?.init);
|
|
5010
|
+
if (init?.type === "AwaitExpression") init = unwrapTs(init.argument);
|
|
5011
|
+
if (init?.type === "CallExpression" || init?.type === "NewExpression") addBoundNames(d?.id, ownBindings);
|
|
5012
|
+
}
|
|
5013
|
+
}
|
|
5014
|
+
};
|
|
5015
|
+
collectOwn(load.body.body);
|
|
4416
5016
|
const syncTainted = /* @__PURE__ */ new Set();
|
|
4417
5017
|
let sawAwaitSite = false;
|
|
4418
5018
|
const taintTarget = (target, rhs) => {
|
|
@@ -4435,6 +5035,26 @@ function collectLoadWaterfalls(program, wrapped) {
|
|
|
4435
5035
|
const root = rootObjectName(callee);
|
|
4436
5036
|
if (root) tainted.add(root);
|
|
4437
5037
|
};
|
|
5038
|
+
const taintLoopFill = (call) => {
|
|
5039
|
+
const callee = unwrapTs(call?.callee);
|
|
5040
|
+
if (call?.type !== "CallExpression" || callee?.type !== "MemberExpression" || callee.computed) return;
|
|
5041
|
+
if (!ITERATING_METHODS.has(callee.property?.name) || !refsTainted(callee.object, tainted)) return;
|
|
5042
|
+
const fill = (node) => {
|
|
5043
|
+
if (Array.isArray(node)) return node.forEach(fill);
|
|
5044
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string") return;
|
|
5045
|
+
if (isFunctionNode(node)) return;
|
|
5046
|
+
const c = node.type === "CallExpression" ? unwrapTs(node.callee) : void 0;
|
|
5047
|
+
if (c?.type === "MemberExpression" && !c.computed && MUTATING_METHODS.has(c.property?.name)) {
|
|
5048
|
+
const root = rootObjectName(c);
|
|
5049
|
+
if (root) tainted.add(root);
|
|
5050
|
+
}
|
|
5051
|
+
if (c?.type === "MemberExpression" && !c.computed && ITERATING_METHODS.has(c.property?.name)) {
|
|
5052
|
+
for (const arg of node.arguments ?? []) if (isFunctionNode(arg)) fill(arg.body);
|
|
5053
|
+
}
|
|
5054
|
+
for (const key of Object.keys(node)) if (!WALK_IGNORED_KEYS.has(key)) fill(node[key]);
|
|
5055
|
+
};
|
|
5056
|
+
for (const arg of call.arguments ?? []) if (isFunctionNode(arg)) fill(arg.body);
|
|
5057
|
+
};
|
|
4438
5058
|
const taintOnly = (node) => {
|
|
4439
5059
|
if (Array.isArray(node)) {
|
|
4440
5060
|
for (const child of node) taintOnly(child);
|
|
@@ -4443,13 +5063,18 @@ function collectLoadWaterfalls(program, wrapped) {
|
|
|
4443
5063
|
if (!node || typeof node !== "object" || typeof node.type !== "string") return;
|
|
4444
5064
|
if (isFunctionNode(node)) return;
|
|
4445
5065
|
if (node.type === "AssignmentExpression") taintTarget(node.left, node.right);
|
|
4446
|
-
else if (node.type === "CallExpression")
|
|
4447
|
-
|
|
5066
|
+
else if (node.type === "CallExpression") {
|
|
5067
|
+
taintMutation(node);
|
|
5068
|
+
taintLoopFill(node);
|
|
5069
|
+
} else if ((node.type === "ForOfStatement" || node.type === "ForInStatement") && refsTainted(node.right, tainted)) {
|
|
4448
5070
|
const names = /* @__PURE__ */ new Set();
|
|
4449
5071
|
addBoundNames(node.left?.type === "VariableDeclaration" ? node.left.declarations?.[0]?.id : node.left, names);
|
|
4450
5072
|
for (const name of names) tainted.add(name);
|
|
4451
5073
|
} else if (node.type === "VariableDeclaration") {
|
|
4452
|
-
for (const d of node.declarations ?? []) if (d?.id && d.init)
|
|
5074
|
+
for (const d of node.declarations ?? []) if (d?.id && d.init) {
|
|
5075
|
+
taintTarget(d.id, d.init);
|
|
5076
|
+
taintLoopFill(unwrapTs(d.init));
|
|
5077
|
+
}
|
|
4453
5078
|
}
|
|
4454
5079
|
for (const key of Object.keys(node)) {
|
|
4455
5080
|
if (WALK_IGNORED_KEYS.has(key)) continue;
|
|
@@ -4483,11 +5108,21 @@ function collectLoadWaterfalls(program, wrapped) {
|
|
|
4483
5108
|
sawAwaitSite = true;
|
|
4484
5109
|
}
|
|
4485
5110
|
if (stmt.type === "VariableDeclaration") {
|
|
4486
|
-
for (const d of stmt.declarations ?? []) if (d?.id && d.init)
|
|
5111
|
+
for (const d of stmt.declarations ?? []) if (d?.id && d.init) {
|
|
5112
|
+
taintTarget(d.id, d.init);
|
|
5113
|
+
taintLoopFill(unwrapTs(d.init));
|
|
5114
|
+
}
|
|
4487
5115
|
} else if (stmt.type === "ExpressionStatement") {
|
|
4488
5116
|
const expr = unwrapTs(stmt.expression);
|
|
4489
5117
|
if (expr?.type === "AssignmentExpression") taintTarget(expr.left, expr.right);
|
|
4490
|
-
else
|
|
5118
|
+
else {
|
|
5119
|
+
taintMutation(expr);
|
|
5120
|
+
taintLoopFill(expr);
|
|
5121
|
+
}
|
|
5122
|
+
const call = expr?.type === "AwaitExpression" ? unwrapTs(expr.argument) : void 0;
|
|
5123
|
+
const callee = call?.type === "CallExpression" ? unwrapTs(call.callee) : void 0;
|
|
5124
|
+
const owner = callee?.type === "MemberExpression" && !callee.computed ? unwrapTs(callee.object) : void 0;
|
|
5125
|
+
if (owner?.type === "Identifier" && ownBindings.has(owner.name)) tainted.add(owner.name);
|
|
4491
5126
|
}
|
|
4492
5127
|
} else taintOnly(stmt);
|
|
4493
5128
|
}
|
|
@@ -4584,7 +5219,7 @@ function aliasMatches(entry, spec) {
|
|
|
4584
5219
|
*/
|
|
4585
5220
|
function resolveRepoLocalPath(spec, importerFile, aliases = DEFAULT_KIT_ALIASES) {
|
|
4586
5221
|
let path;
|
|
4587
|
-
if (spec.startsWith("./") || spec.startsWith("../")) path = `${importerFile.split("/").slice(0, -1).join("/")}/${spec}`;
|
|
5222
|
+
if (spec === "." || spec === ".." || spec.startsWith("./") || spec.startsWith("../")) path = `${importerFile.split("/").slice(0, -1).join("/")}/${spec}`;
|
|
4588
5223
|
else {
|
|
4589
5224
|
const entry = aliases.find((a) => aliasMatches(a, spec));
|
|
4590
5225
|
if (entry?.replacement == null) return void 0;
|
|
@@ -4650,13 +5285,9 @@ function libServerRoot(aliases) {
|
|
|
4650
5285
|
* risks a false positive in this default-on security rule, and staying silent only costs a
|
|
4651
5286
|
* missed finding.
|
|
4652
5287
|
*/
|
|
4653
|
-
/** The resolved repo path
|
|
4654
|
-
function
|
|
4655
|
-
|
|
4656
|
-
if (serverRoot === void 0) return void 0;
|
|
4657
|
-
const path = resolveRepoLocalPath(spec, importerFile, aliases);
|
|
4658
|
-
if (path === void 0) return void 0;
|
|
4659
|
-
return path === serverRoot || path.startsWith(`${serverRoot}/`) ? path : void 0;
|
|
5288
|
+
/** The resolved repo path of a repo-local `spec`, or undefined when it is a package or the `$lib` root is opaque. */
|
|
5289
|
+
function localModulePath(spec, importerFile, aliases) {
|
|
5290
|
+
return libServerRoot(aliases) === void 0 ? void 0 : resolveRepoLocalPath(spec, importerFile, aliases);
|
|
4660
5291
|
}
|
|
4661
5292
|
function isLocalStateSpecifier(spec, importerFile, aliases) {
|
|
4662
5293
|
const serverRoot = libServerRoot(aliases);
|
|
@@ -4672,23 +5303,60 @@ const IN_MEMORY_CTORS = /* @__PURE__ */ new Set([
|
|
|
4672
5303
|
"WeakMap",
|
|
4673
5304
|
"WeakSet"
|
|
4674
5305
|
]);
|
|
5306
|
+
const isContainerCtor = (n) => n?.type === "NewExpression" && n.callee?.type === "Identifier" && IN_MEMORY_CTORS.has(n.callee.name);
|
|
4675
5307
|
/**
|
|
4676
5308
|
* Whether an initializer positively identifies an in-memory container. An object literal with a
|
|
4677
5309
|
* spread is a composite of values this parse cannot see — typically a facade over clients and
|
|
4678
5310
|
* imported modules (`{ ...prismaModels, ...handlers }`) — so it counts only when one of its own
|
|
4679
|
-
* properties is itself a container.
|
|
5311
|
+
* properties is itself a container. One whose properties are all functions (`{ get, set }` of the
|
|
5312
|
+
* module's own functions) holds no data: `.set()` on it calls its own method.
|
|
4680
5313
|
*/
|
|
4681
|
-
function isInMemoryInit(init) {
|
|
5314
|
+
function isInMemoryInit(init, fns = /* @__PURE__ */ new Set()) {
|
|
4682
5315
|
if (!init) return false;
|
|
4683
5316
|
if (init.type === "ArrayExpression") return true;
|
|
4684
5317
|
if (init.type === "ObjectExpression") {
|
|
4685
5318
|
const props = init.properties ?? [];
|
|
5319
|
+
const isFn = (p) => p?.type === "Property" && (p.method || p.value?.type === "FunctionExpression" || p.value?.type === "ArrowFunctionExpression" || p.value?.type === "Identifier" && fns.has(p.value.name));
|
|
5320
|
+
if (props.length > 0 && props.every(isFn)) return false;
|
|
4686
5321
|
if (!props.some((p) => p?.type === "SpreadElement")) return true;
|
|
4687
|
-
return props.some((p) => p?.type === "Property" && isInMemoryInit(p.value));
|
|
5322
|
+
return props.some((p) => p?.type === "Property" && isInMemoryInit(p.value, fns));
|
|
4688
5323
|
}
|
|
4689
5324
|
return init.type === "NewExpression" && init.callee?.type === "Identifier" && IN_MEMORY_CTORS.has(init.callee.name);
|
|
4690
5325
|
}
|
|
4691
5326
|
/**
|
|
5327
|
+
* The exported bindings of a module that are a persistence or API client: `new C()` of a class imported
|
|
5328
|
+
* from an installed package, or of a class this module declares that neither extends another class nor
|
|
5329
|
+
* keeps a `Map`/`Set`/`WeakMap`/`WeakSet` field; or a call into an installed package (`drizzle(…)`,
|
|
5330
|
+
* `createClient(…)`). A write through one of them reaches a database or service, not module state.
|
|
5331
|
+
*/
|
|
5332
|
+
function parseClientExports(source, filename, aliases) {
|
|
5333
|
+
const names = /* @__PURE__ */ new Set();
|
|
5334
|
+
const { program } = parseModuleProgram(source, filename);
|
|
5335
|
+
const fromPackage = /* @__PURE__ */ new Set();
|
|
5336
|
+
for (const stmt of program?.body ?? []) {
|
|
5337
|
+
const src = stmt?.type === "ImportDeclaration" ? String(stmt.source?.value ?? "") : "";
|
|
5338
|
+
if (!(!src || /^[./$~#]|^@\//.test(src) || resolveRepoLocalPath(src, filename, aliases) !== void 0) && !/^svelte|store/i.test(src)) for (const sp of stmt.specifiers ?? []) fromPackage.add(sp.local.name);
|
|
5339
|
+
}
|
|
5340
|
+
const plainClasses = /* @__PURE__ */ new Set();
|
|
5341
|
+
for (const stmt of program?.body ?? []) {
|
|
5342
|
+
const decl = unwrapExport(stmt);
|
|
5343
|
+
if (decl?.type !== "ClassDeclaration" || !decl.id || decl.superClass) continue;
|
|
5344
|
+
if (!(decl.body?.body ?? []).some((m) => isContainerCtor(m?.value) || m?.kind === "constructor" && m.value?.body?.body?.some((st) => st?.type === "ExpressionStatement" && isContainerCtor(st.expression?.right)))) plainClasses.add(decl.id.name);
|
|
5345
|
+
}
|
|
5346
|
+
for (const stmt of program?.body ?? []) {
|
|
5347
|
+
if (stmt?.type !== "ExportNamedDeclaration" || stmt.declaration?.type !== "VariableDeclaration") continue;
|
|
5348
|
+
for (const d of stmt.declaration.declarations ?? []) {
|
|
5349
|
+
let init = unwrapTs(d?.init);
|
|
5350
|
+
if (init?.type === "AwaitExpression") init = unwrapTs(init.argument);
|
|
5351
|
+
if (d?.id?.type !== "Identifier") continue;
|
|
5352
|
+
const ctor = init?.type === "NewExpression" && init.callee?.type === "Identifier" ? init.callee.name : void 0;
|
|
5353
|
+
if (ctor !== void 0 && (fromPackage.has(ctor) || plainClasses.has(ctor))) names.add(d.id.name);
|
|
5354
|
+
if (init?.type === "CallExpression" && fromPackage.has(rootObjectName(init.callee) ?? "")) names.add(d.id.name);
|
|
5355
|
+
}
|
|
5356
|
+
}
|
|
5357
|
+
return names;
|
|
5358
|
+
}
|
|
5359
|
+
/**
|
|
4692
5360
|
* The exported bindings of a module that are initialized to an in-memory container — an object
|
|
4693
5361
|
* or array literal (a spread literal only as `isInMemoryInit` allows), or `new Map`/`Set`/`WeakMap`/`WeakSet`. Used to arbitrate a `.set()` call on
|
|
4694
5362
|
* an import from under the `$lib` server root, where the call shape alone cannot separate a
|
|
@@ -4702,11 +5370,16 @@ function isInMemoryInit(init) {
|
|
|
4702
5370
|
function parseInMemoryExports(source, filename) {
|
|
4703
5371
|
const names = /* @__PURE__ */ new Set();
|
|
4704
5372
|
const { program } = parseModuleProgram(source, filename);
|
|
5373
|
+
const fns = /* @__PURE__ */ new Set();
|
|
5374
|
+
for (const stmt of program?.body ?? []) {
|
|
5375
|
+
const decl = unwrapExport(stmt);
|
|
5376
|
+
if (decl?.type === "FunctionDeclaration" && decl.id) fns.add(decl.id.name);
|
|
5377
|
+
}
|
|
4705
5378
|
for (const stmt of program?.body ?? []) {
|
|
4706
5379
|
if (stmt?.type !== "ExportNamedDeclaration" || !stmt.declaration) continue;
|
|
4707
5380
|
const decl = stmt.declaration;
|
|
4708
5381
|
if (decl.type !== "VariableDeclaration") continue;
|
|
4709
|
-
for (const d of decl.declarations ?? []) if (d?.id?.type === "Identifier" && isInMemoryInit(d.init)) names.add(d.id.name);
|
|
5382
|
+
for (const d of decl.declarations ?? []) if (d?.id?.type === "Identifier" && isInMemoryInit(d.init, fns)) names.add(d.id.name);
|
|
4710
5383
|
}
|
|
4711
5384
|
return names;
|
|
4712
5385
|
}
|
|
@@ -4722,6 +5395,7 @@ function parseKitModuleFacts(source, filename, aliases) {
|
|
|
4722
5395
|
const importedStateWrites = [];
|
|
4723
5396
|
const importedStateWritesOutsideHandlers = [];
|
|
4724
5397
|
const pendingServerStoreWrites = [];
|
|
5398
|
+
const storeWriteTargets = [];
|
|
4725
5399
|
const runesModuleImports = [];
|
|
4726
5400
|
const lifecycleCalls = [];
|
|
4727
5401
|
const browserGlobalRefs = [];
|
|
@@ -4740,6 +5414,11 @@ function parseKitModuleFacts(source, filename, aliases) {
|
|
|
4740
5414
|
const importedSpecifiers = /* @__PURE__ */ new Map();
|
|
4741
5415
|
const importedNames = /* @__PURE__ */ new Map();
|
|
4742
5416
|
const namespaceImports = /* @__PURE__ */ new Set();
|
|
5417
|
+
const localFunctions = /* @__PURE__ */ new Set();
|
|
5418
|
+
walkEstree(program, (n) => {
|
|
5419
|
+
if (n.type === "FunctionDeclaration" && n.id?.name) localFunctions.add(n.id.name);
|
|
5420
|
+
if (n.type === "VariableDeclarator" && n.id?.type === "Identifier" && n.init && isFunctionNode(unwrapTs(n.init))) localFunctions.add(n.id.name);
|
|
5421
|
+
});
|
|
4743
5422
|
for (const stmt of program.body ?? []) {
|
|
4744
5423
|
if (stmt?.type !== "ImportDeclaration" || stmt.importKind === "type") continue;
|
|
4745
5424
|
const spec = typeof stmt.source?.value === "string" ? stmt.source.value : "";
|
|
@@ -4771,7 +5450,7 @@ function parseKitModuleFacts(source, filename, aliases) {
|
|
|
4771
5450
|
const ssrOptOut = findFalseOptOut(program, wrapped, "ssr");
|
|
4772
5451
|
const csrOptOut = findFalseOptOut(program, wrapped, "csr");
|
|
4773
5452
|
const waterfalls = collectLoadWaterfalls(program, wrapped);
|
|
4774
|
-
if (!ssrOptOut) {
|
|
5453
|
+
if (!ssrOptOut?.literal) {
|
|
4775
5454
|
const shiftLine = (l) => Math.max(0, l - 1);
|
|
4776
5455
|
const browserImports = collectBrowserGuardImports(program);
|
|
4777
5456
|
const guards = /* @__PURE__ */ new Set([...browserImports, ...collectDerivedGuardBindings(program, browserImports)]);
|
|
@@ -4803,10 +5482,12 @@ function parseKitModuleFacts(source, filename, aliases) {
|
|
|
4803
5482
|
scanFn(fn, false);
|
|
4804
5483
|
}
|
|
4805
5484
|
}
|
|
5485
|
+
const browserOnly = browserOnlyRanges(program, collectBrowserGuardImports(program));
|
|
5486
|
+
const reachedOnlyByBrowser = (at) => browserOnly.some(([from, to]) => at >= from && at < to);
|
|
4806
5487
|
walkKit(program, handlerFns, startupFns, (n, shadowed, inFunction, inHandler, inStartup) => {
|
|
4807
5488
|
if (inFunction && !inStartup) {
|
|
4808
5489
|
const flagLet = (name) => {
|
|
4809
|
-
if (name && !shadowed.has(name) && moduleLets.has(name)) moduleStateReassignments.push({
|
|
5490
|
+
if (name && !shadowed.has(name) && moduleLets.has(name) && !reachedOnlyByBrowser(n.start)) moduleStateReassignments.push({
|
|
4810
5491
|
name,
|
|
4811
5492
|
line: line(n.start),
|
|
4812
5493
|
inHandler
|
|
@@ -4847,23 +5528,25 @@ function parseKitModuleFacts(source, filename, aliases) {
|
|
|
4847
5528
|
} else if (n.type === "CallExpression" && n.callee?.type === "MemberExpression") {
|
|
4848
5529
|
const method = n.callee.property?.type === "Identifier" ? n.callee.property.name : void 0;
|
|
4849
5530
|
const nsFunction = n.callee.object?.type === "Identifier" && namespaceImports.has(n.callee.object.name);
|
|
4850
|
-
|
|
5531
|
+
const updater = method !== "update" || isUpdaterArgument(n.arguments?.[0], localFunctions);
|
|
5532
|
+
if ((method === "set" || method === "update") && !nsFunction && updater) {
|
|
4851
5533
|
const r = importedRoot(n.callee.object);
|
|
4852
5534
|
const spec = r ? importedSpecifiers.get(r) : void 0;
|
|
4853
5535
|
if (r && spec !== void 0) {
|
|
4854
|
-
|
|
5536
|
+
const resolved = localModulePath(spec, filename, aliases);
|
|
5537
|
+
const target = {
|
|
4855
5538
|
name: r,
|
|
4856
|
-
|
|
5539
|
+
imported: importedNames.get(r) ?? r,
|
|
5540
|
+
resolved,
|
|
5541
|
+
line: line(n.start)
|
|
4857
5542
|
};
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
if (resolved !== void 0 && inHandler) pendingServerStoreWrites.push({
|
|
5543
|
+
if (isLocalStateSpecifier(spec, filename, aliases)) {
|
|
5544
|
+
write = {
|
|
4861
5545
|
name: r,
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
}
|
|
5546
|
+
via: "set-call"
|
|
5547
|
+
};
|
|
5548
|
+
if (inHandler) storeWriteTargets.push(target);
|
|
5549
|
+
} else if (resolved !== void 0 && inHandler) pendingServerStoreWrites.push(target);
|
|
4867
5550
|
}
|
|
4868
5551
|
}
|
|
4869
5552
|
} else if (n.type === "AssignmentExpression" && (n.left?.type === "ObjectPattern" || n.left?.type === "ArrayPattern")) {
|
|
@@ -4884,6 +5567,7 @@ function parseKitModuleFacts(source, filename, aliases) {
|
|
|
4884
5567
|
};
|
|
4885
5568
|
scanPatternTargets(n.left);
|
|
4886
5569
|
}
|
|
5570
|
+
if (write && reachedOnlyByBrowser(n.start)) write = void 0;
|
|
4887
5571
|
if (write) {
|
|
4888
5572
|
if (inHandler) importedStateWrites.push({
|
|
4889
5573
|
...write,
|
|
@@ -4915,19 +5599,51 @@ function parseKitModuleFacts(source, filename, aliases) {
|
|
|
4915
5599
|
line: line(call.start)
|
|
4916
5600
|
});
|
|
4917
5601
|
}
|
|
5602
|
+
const exitLocals = collectNamedImportAliases(program, "@sveltejs/kit", EXIT_NAMES);
|
|
5603
|
+
const neverRenders = loadNeverRenders(program, exitLocals);
|
|
5604
|
+
const building = collectNamedImportAliases(program, "$app/environment", BUILDING_NAMES);
|
|
5605
|
+
const hooks = /(^|\/)hooks\.server\.(ts|js)$/.test(filename);
|
|
5606
|
+
const answered = hooks ? handleAnsweredPaths(program, exitLocals) : [];
|
|
5607
|
+
const chunkTokens = hooks ? pageChunkTokens(program) : [];
|
|
5608
|
+
const flagGuard = leadingFlagGuard(program, exitLocals, (local) => {
|
|
5609
|
+
const spec = namespaceImports.has(local) ? void 0 : importedSpecifiers.get(local);
|
|
5610
|
+
const resolved = spec === void 0 ? void 0 : localModulePath(spec, filename, aliases);
|
|
5611
|
+
return resolved === void 0 ? void 0 : {
|
|
5612
|
+
resolved,
|
|
5613
|
+
name: importedNames.get(local) ?? local
|
|
5614
|
+
};
|
|
5615
|
+
});
|
|
5616
|
+
const factory = loadFactory(program, (local) => {
|
|
5617
|
+
const spec = namespaceImports.has(local) ? void 0 : importedSpecifiers.get(local);
|
|
5618
|
+
const resolved = spec === void 0 ? void 0 : localModulePath(spec, filename, aliases);
|
|
5619
|
+
return resolved === void 0 ? void 0 : {
|
|
5620
|
+
resolved,
|
|
5621
|
+
name: importedNames.get(local) ?? local
|
|
5622
|
+
};
|
|
5623
|
+
});
|
|
4918
5624
|
return {
|
|
4919
5625
|
moduleStateReassignments: byLine(moduleStateReassignments),
|
|
4920
5626
|
importedStateWrites: byLine(importedStateWrites),
|
|
4921
5627
|
importedStateWritesOutsideHandlers: byLine(importedStateWritesOutsideHandlers),
|
|
4922
5628
|
pendingServerStoreWrites: byLine(pendingServerStoreWrites),
|
|
5629
|
+
...storeWriteTargets.length > 0 ? { storeWriteTargets } : {},
|
|
4923
5630
|
runesModuleImports: byLine(runesModuleImports),
|
|
4924
5631
|
lifecycleCalls: byLine(lifecycleCalls),
|
|
4925
5632
|
browserGlobalRefs: byLine(browserGlobalRefs),
|
|
4926
5633
|
basePathLinks: byLine(basePathLinks),
|
|
4927
5634
|
...ssrOptOut ? { ssrDisabled: { line: Math.max(0, ssrOptOut.line - 1) } } : {},
|
|
4928
5635
|
...!ssrOptOut && exportsName(program, "ssr") ? { ssrEnabled: true } : {},
|
|
5636
|
+
...!ssrOptOut && exportsTrue(program, "ssr") ? { ssrTrue: true } : {},
|
|
4929
5637
|
...csrOptOut ? { csrDisabled: { line: Math.max(0, csrOptOut.line - 1) } } : {},
|
|
4930
|
-
|
|
5638
|
+
...!csrOptOut && exportsName(program, "csr") ? { csrEnabled: true } : {},
|
|
5639
|
+
...neverRenders ? { loadNeverRenders: true } : {},
|
|
5640
|
+
...!neverRenders && building.size > 0 && loadNeverRenders(program, exitLocals, building) ? { loadNeverRendersOutsideBuild: true } : {},
|
|
5641
|
+
...exportsName(program, "prerender") ? { prerenderExported: true } : {},
|
|
5642
|
+
...answered.length > 0 ? { answeredPaths: answered } : {},
|
|
5643
|
+
...chunkTokens.length > 0 ? { pageChunkTokens: chunkTokens } : {},
|
|
5644
|
+
...flagGuard ? { pendingFlagGuard: flagGuard } : {},
|
|
5645
|
+
...factory ? { pendingLoadFactory: factory } : {},
|
|
5646
|
+
...loadGatesOnLocals(program) ? { loadGated: true } : {},
|
|
4931
5647
|
...waterfalls.dependentLines.length > 0 || waterfalls.independentLines.length > 0 ? { loadWaterfalls: waterfalls } : {},
|
|
4932
5648
|
suppressions
|
|
4933
5649
|
};
|
|
@@ -6399,8 +7115,8 @@ const MESSAGE$1 = "Sequential dependent awaits in a universal load create a clie
|
|
|
6399
7115
|
/**
|
|
6400
7116
|
* performance/load-waterfall — dependent await chains in universal loads. Server loads are exempt:
|
|
6401
7117
|
* a dependent chain cannot be parallelized, and on the server there is no better
|
|
6402
|
-
* placement to suggest. csr = false
|
|
6403
|
-
* runtime the universal load only runs during SSR.
|
|
7118
|
+
* placement to suggest. Loads on a route with `csr = false` (its own or a layout's) are exempt
|
|
7119
|
+
* too — without a client runtime the universal load only runs during SSR.
|
|
6404
7120
|
*/
|
|
6405
7121
|
const performanceLoadWaterfall = kitModuleRule({
|
|
6406
7122
|
id: "performance/load-waterfall",
|
|
@@ -6415,7 +7131,7 @@ const performanceLoadWaterfall = kitModuleRule({
|
|
|
6415
7131
|
snippet: "// +page.server.ts — same chain, server-side hops\nexport async function load({ fetch }) {\n const user = await fetch(`/api/user`).then((r) => r.json());\n const posts = await fetch(`/api/posts/${user.id}`).then((r) => r.json());\n return { user, posts };\n}",
|
|
6416
7132
|
lang: "ts"
|
|
6417
7133
|
},
|
|
6418
|
-
applies: (m) => m.kind === "universal" &&
|
|
7134
|
+
applies: (m, ctx) => m.kind === "universal" && (m.loadWaterfalls?.dependentLines.length ?? 0) > 0 && !universalNeverCsr(m, ctx),
|
|
6419
7135
|
bad: (m) => m.loadWaterfalls.dependentLines.map((line) => ({
|
|
6420
7136
|
line,
|
|
6421
7137
|
message: MESSAGE$1
|
|
@@ -7088,7 +7804,7 @@ const a11yPatternTitle = componentRule({
|
|
|
7088
7804
|
//#endregion
|
|
7089
7805
|
//#region src/html-spec/generated.ts
|
|
7090
7806
|
/*!
|
|
7091
|
-
* HTML spec data projected from @markuplint/html-spec@5.0.
|
|
7807
|
+
* HTML spec data projected from @markuplint/html-spec@5.0.1 — https://github.com/markuplint/markuplint
|
|
7092
7808
|
*
|
|
7093
7809
|
* MIT License
|
|
7094
7810
|
*
|
|
@@ -8481,8 +9197,29 @@ function buildJsonReport(results, config, meta, ruleIds, examined, skipped) {
|
|
|
8481
9197
|
};
|
|
8482
9198
|
}
|
|
8483
9199
|
/** Render results as the documented JSON report string (design §7). */
|
|
8484
|
-
function formatJsonReport(
|
|
8485
|
-
return
|
|
9200
|
+
function formatJsonReport(...args) {
|
|
9201
|
+
return [...formatJsonReportChunks(...args)].join("");
|
|
9202
|
+
}
|
|
9203
|
+
/**
|
|
9204
|
+
* `formatJsonReport`'s text in pieces, one route at a time: a large prerendered site's report is
|
|
9205
|
+
* longer than the longest string V8 can hold.
|
|
9206
|
+
*/
|
|
9207
|
+
function* formatJsonReportChunks(results, config, meta, ruleIds, examined, skipped) {
|
|
9208
|
+
const report = buildJsonReport(results, config, meta, ruleIds, examined, skipped);
|
|
9209
|
+
const marker = JSON.stringify("\0routes");
|
|
9210
|
+
const text = JSON.stringify({
|
|
9211
|
+
...report,
|
|
9212
|
+
routes: "\0routes"
|
|
9213
|
+
}, null, 2);
|
|
9214
|
+
const at = text.indexOf(marker);
|
|
9215
|
+
yield text.slice(0, at);
|
|
9216
|
+
if (report.routes.length === 0) yield "[]";
|
|
9217
|
+
else {
|
|
9218
|
+
yield "[\n";
|
|
9219
|
+
for (const [i, route] of report.routes.entries()) yield `${i > 0 ? ",\n" : ""} ${JSON.stringify(route, null, 2).replaceAll("\n", "\n ")}`;
|
|
9220
|
+
yield "\n ]";
|
|
9221
|
+
}
|
|
9222
|
+
yield text.slice(at + marker.length);
|
|
8486
9223
|
}
|
|
8487
9224
|
//#endregion
|
|
8488
9225
|
//#region src/reporter/sanitize.ts
|
|
@@ -8628,4 +9365,4 @@ function formatMarkdownReport(results, config, meta) {
|
|
|
8628
9365
|
return lines.join("\n");
|
|
8629
9366
|
}
|
|
8630
9367
|
//#endregion
|
|
8631
|
-
export {
|
|
9368
|
+
export { performanceLoadWaterfall as $, shouldSkipRangeCheck as $n, securityJavascriptUrl as $t, a11yPatternTitle as A, splitTokens as An, seoTitlePresence as Ar, parseModuleProgram as At, a11yPlaceholderLabelOption as B, seoJsonLdDateFormat as Bn, defineConfig as Br, attrTextOf as Bt, a11yPermittedContents as C, IDREF_ATTRS as Cn, seoCanonicalUrl as Cr, collectBrowserGuardImports as Ct, a11yDeprecatedAttr as D, foldOccurrences as Dn, layoutNeverRenders as Dr, collectSuppressions as Dt, a11yDisallowedAriaProps as E, decodeFragmentId as En, seoDescriptionPresence as Er, collectProgramBindings as Et, a11yPositiveTabindex as F, seoCharset as Fn, docsUrlFor as Fr, walkEstree as Ft, a11yInteractiveNesting as G, seoViewport as Gn, parseSvelte as Gt, a11yLabelHasControl as H, seoJsonLdDeprecatedType as Hn, attrValueOf as Ht, isInteractiveContainer as I, seoDescriptionLength as In, isPenalized as Ir, walkEvalScope as It, a11yUnknownAriaAttribute as J, seoTwitterCard as Jn, architecturePropCount as Jt, a11yInvalidAriaValue as K, seoOgUrl as Kn, textFromNodes as Kt, isInteractiveElement as L, seoTitleLength as Ln, CATEGORIES as Lr, walkScoped as Lt, a11yNoDuplicateDt as M, resolveRole as Mn, effectiveSeverity as Mr, scopeIntroducedNames as Mt, a11yNoAutofocus as N, seoHreflang as Nn, hasFailureAtOrAbove as Nr, unwrapExport as Nt, a11yDeprecatedElement as O, isTopFragment as On, pageRendersOnlyWhenBuilding as Or, collectSvelteLifecycleImports as Ot, a11yNoAccesskey as P, seoImageAlt as Pn, summarize as Pr, unwrapTs as Pt, performanceSequentialAwaits as Q, resolveRuleOptions as Qn, securityHandlerStateWrite as Qt, literalTabindexValue as R, seoJsonLdRequiredProps as Rn, defaultConfig as Rr, CHILD_NODE_KEYS as Rt, a11yRequiredElement as S, ASIDE_DEMOTING_TAGS as Sn, seoOgImage as Sr, collectBrowserGlobalRefs as St, a11yDeprecatedAria as T, SECTIONING_TAGS as Tn, isClassicScriptType as Tr, collectNamedImportAliases as Tt, a11yAccessibleName as U, seoJsonLdValidity as Un, findAttr as Ut, a11yUseList as V, seoJsonLdRelativeUrl as Vn, attrValue as Vt, a11yAriaHiddenFocus as W, seoSitemapInRobots as Wn, lineOf as Wt, performanceIframeLoading as X, performancePreconnect as Xn, securitySharedStateImport as Xt, a11yInvalidRole as Y, seoIndexability as Yn, architectureComponentSize as Yt, performanceStateRaw as Z, isPlainObject as Zn, securityServerModuleState as Zt, a11yNoMissingIdRef as _, seoHeadingLevelSkip as _n, seoHtmlLang as _r, parseKitModuleFacts as _t, formatJsonReport as a, correctnessOrphanEffect as an, formatFailedRuleWarning as ar, architectureReservedNamePlacement as at, a11yDuplicateLandmark as b, seoSingleH1 as bn, seoJsonLd as br, WALK_IGNORED_KEYS as bt, computeScore as c, correctnessNonreactiveBuiltinState as cn, withFailedRulesOff as cr, architectureUnitEntryFile as ct, messageText as d, correctnessUnmutatedState as dn, performanceLcpImage as dr, collectFalseExports as dt, securityRawHtml as en, validateRuleSetting as er, performanceMinifyDisabled as et, ruleMetaById as f, correctnessEffectAsOnMount as fn, performanceFontPreloadCrossorigin as fr, collectTopLevelBindings as ft, a11yUnverifiedIdRef as g, seoSsrDisabled as gn, performanceImageDimensions as gr, parseInMemoryExports as gt, explainRule as h, correctnessEachKey as hn, performanceImageLoadingHint as hr, parseFalseExports as ht, buildJsonReport as i, correctnessOrphanLifecycle as in, compileOverrides as ir, architectureRouteComponentImport as it, a11yAbbrTitle as j, stripTextDirective as jn, classify as jr, rootObjectName as jt, HTML_SPEC as k, resolveLandmark as kn, routeNeverSsr as kr, matchLifecycleCall as kt, formatGithubReport as l, correctnessStalePropDerivation as ln, withIndexableOff as lr, architecturePrivateScopeImport as lt, allRules as m, correctnessEachIndexKey as mn, performanceResponsiveImage as mr, parseExitFactoryExports as mt, mdEscape as n, correctnessServerBrowserGlobal as nn, applyOverrides as nr, performanceHeavyImport as nt, formatJsonReportChunks as o, correctnessAutoplayMuted as on, selectRules as or, architectureReservedDirectoryNames as ot, severityToSarifLevel as p, correctnessEffectAsDerived as pn, performancePreloadMissingAs as pr, parseClientExports as pt, a11yRequiredAriaProps as q, seoOgDescription as qn, valueFromNodes as qt, terminalSafe as r, correctnessBasePathNavigation as rn, applyRuleSeverities as rr, architectureDocLinkTarget as rt, computeHealth as s, correctnessCheckableBindValue as sn, settingSeverity as sr, architectureDirectoryNaming as st, formatMarkdownReport as t, correctnessInstanceBrowserGlobal as tn, SEARCH_RESULT_RULES as tr, performanceNamespaceImport as tt, SEVERITY_RANK as u, correctnessPropMutation as un, performanceRenderBlockingScript as ur, DEFAULT_KIT_ALIASES as ut, a11yIdDuplication as v, seoDuplicateDescription as vn, seoSitemapXml as vr, resolveRepoLocalPath as vt, a11yDisallowedElement as w, NAMING_ATTRS$1 as wn, hasRel as wr, collectDerivedGuardBindings as wt, a11yDoctype as x, ANCESTRY_DEPENDENT_TAGS as xn, seoOgTitle as xr, addBoundNames as xt, a11yTopLevelLandmark as y, seoDuplicateTitle as yn, seoRobotsTxt as yr, isRootRelativePath as yt, a11yRequireDatetime as z, seoJsonLdPlaceholder as zn, defaultProject as zr, attrText as zt };
|