Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7191d7615 | ||
|
|
28f5585a56 | ||
|
|
a2a97ebcc7 | ||
|
|
6a0c60180c | ||
|
|
4667cafa43 | ||
|
|
72fd295556 | ||
|
|
18bea3a852 | ||
|
|
cc6cf29c79 | ||
|
|
8198886dab | ||
|
|
69d7e26a5b |
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -4,21 +4,25 @@
|
|||||||
"index": {
|
"index": {
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"cacheControl": "no-cache",
|
"cacheControl": "no-cache",
|
||||||
|
"contentTypes": ["text/html"],
|
||||||
"securityHeaders": true
|
"securityHeaders": true
|
||||||
},
|
},
|
||||||
"runtimeConfig": {
|
"runtimeConfig": {
|
||||||
"path": "/config.json",
|
"path": "/config.json",
|
||||||
"cacheControl": "no-store",
|
"cacheControl": "no-store",
|
||||||
|
"contentTypes": ["application/json"],
|
||||||
"securityHeaders": true
|
"securityHeaders": true
|
||||||
},
|
},
|
||||||
"releaseManifest": {
|
"releaseManifest": {
|
||||||
"path": "/release-manifest.json",
|
"path": "/release-manifest.json",
|
||||||
"cacheControl": "no-store",
|
"cacheControl": "no-store",
|
||||||
|
"contentTypes": ["application/json"],
|
||||||
"securityHeaders": true
|
"securityHeaders": true
|
||||||
},
|
},
|
||||||
"hashedAsset": {
|
"hashedAsset": {
|
||||||
"pathPattern": "/assets/*",
|
"pathPattern": "/assets/*",
|
||||||
"cacheControl": "public, max-age=31536000, immutable",
|
"cacheControl": "public, max-age=31536000, immutable",
|
||||||
|
"contentTypes": ["text/javascript", "application/javascript"],
|
||||||
"securityHeaders": false
|
"securityHeaders": false
|
||||||
},
|
},
|
||||||
"sourceMap": {
|
"sourceMap": {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"responses": {
|
"responses": {
|
||||||
"index": {
|
"index": {
|
||||||
"cache-control": "no-cache",
|
"cache-control": "no-cache",
|
||||||
|
"content-type": "text/html; charset=utf-8",
|
||||||
"content-security-policy": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https:; font-src 'self'; upgrade-insecure-requests",
|
"content-security-policy": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https:; font-src 'self'; upgrade-insecure-requests",
|
||||||
"strict-transport-security": "max-age=31536000; includeSubDomains",
|
"strict-transport-security": "max-age=31536000; includeSubDomains",
|
||||||
"x-frame-options": "DENY",
|
"x-frame-options": "DENY",
|
||||||
@@ -12,6 +13,7 @@
|
|||||||
},
|
},
|
||||||
"runtimeConfig": {
|
"runtimeConfig": {
|
||||||
"cache-control": "no-store",
|
"cache-control": "no-store",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
"content-security-policy": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https:; font-src 'self'; upgrade-insecure-requests",
|
"content-security-policy": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https:; font-src 'self'; upgrade-insecure-requests",
|
||||||
"strict-transport-security": "max-age=31536000; includeSubDomains",
|
"strict-transport-security": "max-age=31536000; includeSubDomains",
|
||||||
"x-frame-options": "DENY",
|
"x-frame-options": "DENY",
|
||||||
@@ -21,6 +23,7 @@
|
|||||||
},
|
},
|
||||||
"releaseManifest": {
|
"releaseManifest": {
|
||||||
"cache-control": "no-store",
|
"cache-control": "no-store",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
"content-security-policy": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https:; font-src 'self'; upgrade-insecure-requests",
|
"content-security-policy": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https:; font-src 'self'; upgrade-insecure-requests",
|
||||||
"strict-transport-security": "max-age=31536000; includeSubDomains",
|
"strict-transport-security": "max-age=31536000; includeSubDomains",
|
||||||
"x-frame-options": "DENY",
|
"x-frame-options": "DENY",
|
||||||
@@ -29,7 +32,8 @@
|
|||||||
"permissions-policy": "camera=(), microphone=(), geolocation=()"
|
"permissions-policy": "camera=(), microphone=(), geolocation=()"
|
||||||
},
|
},
|
||||||
"hashedAsset": {
|
"hashedAsset": {
|
||||||
"cache-control": "public, max-age=31536000, immutable"
|
"cache-control": "public, max-age=31536000, immutable",
|
||||||
|
"content-type": "text/javascript; charset=utf-8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ The provider-independent cache defaults are:
|
|||||||
- public source maps: disabled
|
- public source maps: disabled
|
||||||
- service worker/offline cache: disabled
|
- service worker/offline cache: disabled
|
||||||
|
|
||||||
|
HTML, JSON config/manifest, and hashed JavaScript MIME types are also compared
|
||||||
|
to the declared allowlist; a cache-correct response with a mismatched
|
||||||
|
`Content-Type` still fails the hosting gate.
|
||||||
|
|
||||||
`corepack pnpm verify:hosting-headers` uses a deterministic fixture locally.
|
`corepack pnpm verify:hosting-headers` uses a deterministic fixture locally.
|
||||||
Set `HOSTING_BASE_URL` to probe deployed responses; production promotion
|
Set `HOSTING_BASE_URL` to probe deployed responses; production promotion
|
||||||
requires the artifact to report `mode: "live"`.
|
requires the artifact to report `mode: "live"`.
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
"lint": "eslint src scripts tests vite.config.js vitest.config.js playwright.config.js --max-warnings=0",
|
"lint": "eslint src scripts tests vite.config.js vitest.config.js playwright.config.js --max-warnings=0",
|
||||||
"check:architecture": "node scripts/check-architecture.mjs",
|
"check:architecture": "node scripts/check-architecture.mjs",
|
||||||
"check:types": "tsc --allowJs --checkJs --noEmit",
|
"check:types": "tsc --allowJs --checkJs --noEmit",
|
||||||
"check:types:fixture": "tsc --allowJs --checkJs --noEmit --target ES2022 --module NodeNext --moduleResolution NodeNext tests/fixtures/typecheck/invalid-port-call.js",
|
"check:types:fixture": "tsc --ignoreConfig --allowJs --checkJs --noEmit --target ES2022 --module NodeNext --moduleResolution NodeNext tests/fixtures/typecheck/invalid-port-call.js",
|
||||||
"test:runtime-schema": "vitest run tests/runtime-schema --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/runtime-schema.xml --passWithNoTests",
|
"test:runtime-schema": "vitest run tests/runtime-schema --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/runtime-schema.xml --passWithNoTests",
|
||||||
"test:unit": "vitest run tests/unit --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/unit.xml",
|
"test:unit": "vitest run tests/unit --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/unit.xml",
|
||||||
"test:component": "vitest run tests/component --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/component.xml",
|
"test:component": "vitest run tests/component --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/component.xml",
|
||||||
|
|||||||
+22
-15
@@ -1,6 +1,7 @@
|
|||||||
import { readFile, writeFile } from "node:fs/promises";
|
import { readFile, writeFile } from "node:fs/promises";
|
||||||
|
|
||||||
import { evaluateBundleBudget } from "../src/application/policies/performance-budgets.js";
|
import { evaluateBundleBudget } from "../src/application/policies/performance-budgets.js";
|
||||||
|
import { classifyViteJavascript } from "./lib/classify-vite-bundle.mjs";
|
||||||
|
|
||||||
const report =
|
const report =
|
||||||
/** @type {{
|
/** @type {{
|
||||||
@@ -10,7 +11,7 @@ const report =
|
|||||||
JSON.parse(await readFile("artifacts/performance/bundle.json", "utf8"))
|
JSON.parse(await readFile("artifacts/performance/bundle.json", "utf8"))
|
||||||
);
|
);
|
||||||
const viteManifest =
|
const viteManifest =
|
||||||
/** @type {Record<string, { file: string, isEntry?: boolean }>} */ (
|
/** @type {Record<string, { file: string, isEntry?: boolean, imports?: string[] }>} */ (
|
||||||
JSON.parse(await readFile("dist/.vite/manifest.json", "utf8"))
|
JSON.parse(await readFile("dist/.vite/manifest.json", "utf8"))
|
||||||
);
|
);
|
||||||
const budgets =
|
const budgets =
|
||||||
@@ -21,24 +22,19 @@ const budgets =
|
|||||||
const outputByPath = new Map(
|
const outputByPath = new Map(
|
||||||
report.outputs.map((output) => [output.path.replace(/^dist\//, ""), output]),
|
report.outputs.map((output) => [output.path.replace(/^dist\//, ""), output]),
|
||||||
);
|
);
|
||||||
const initialFiles = new Set(
|
const classification = classifyViteJavascript(viteManifest);
|
||||||
Object.values(viteManifest)
|
const initialJsGzipBytes = classification.initialFiles.reduce(
|
||||||
.filter((entry) => entry.isEntry)
|
|
||||||
.map((entry) => entry.file),
|
|
||||||
);
|
|
||||||
const lazyFiles = new Set(
|
|
||||||
Object.values(viteManifest)
|
|
||||||
.filter((entry) => !entry.isEntry && entry.file.endsWith(".js"))
|
|
||||||
.map((entry) => entry.file),
|
|
||||||
);
|
|
||||||
const initialJsGzipBytes = [...initialFiles].reduce(
|
|
||||||
(total, file) => total + (outputByPath.get(file)?.gzipBytes ?? 0),
|
(total, file) => total + (outputByPath.get(file)?.gzipBytes ?? 0),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
const lazyChunks = [...lazyFiles].map((file) => ({
|
const lazyChunks = classification.lazyFiles.map((file) => ({
|
||||||
path: file,
|
path: file,
|
||||||
gzipBytes: outputByPath.get(file)?.gzipBytes ?? 0,
|
gzipBytes: outputByPath.get(file)?.gzipBytes ?? 0,
|
||||||
}));
|
}));
|
||||||
|
const missingOutputs = [
|
||||||
|
...classification.initialFiles,
|
||||||
|
...classification.lazyFiles,
|
||||||
|
].filter((file) => !outputByPath.has(file));
|
||||||
const measurements = { initialJsGzipBytes, lazyChunks };
|
const measurements = { initialJsGzipBytes, lazyChunks };
|
||||||
const result = evaluateBundleBudget(measurements, budgets);
|
const result = evaluateBundleBudget(measurements, budgets);
|
||||||
const fixtures = [
|
const fixtures = [
|
||||||
@@ -70,10 +66,16 @@ const fixtures = [
|
|||||||
).passed,
|
).passed,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const passed = result.passed && fixtures.every((fixture) => fixture.passed);
|
const passed =
|
||||||
|
result.passed &&
|
||||||
|
fixtures.every((fixture) => fixture.passed) &&
|
||||||
|
classification.missingImports.length === 0 &&
|
||||||
|
missingOutputs.length === 0;
|
||||||
const completedReport = {
|
const completedReport = {
|
||||||
...report,
|
...report,
|
||||||
measurements,
|
measurements,
|
||||||
|
classification,
|
||||||
|
missingOutputs,
|
||||||
thresholds: budgets,
|
thresholds: budgets,
|
||||||
results: result,
|
results: result,
|
||||||
fixtures,
|
fixtures,
|
||||||
@@ -85,7 +87,12 @@ await writeFile(
|
|||||||
`${JSON.stringify(completedReport, null, 2)}\n`,
|
`${JSON.stringify(completedReport, null, 2)}\n`,
|
||||||
);
|
);
|
||||||
if (!passed) {
|
if (!passed) {
|
||||||
process.stderr.write("Bundle budget exceeded.\n");
|
process.stderr.write(
|
||||||
|
`Bundle budget or manifest integrity failed: ${[
|
||||||
|
...classification.missingImports,
|
||||||
|
...missingOutputs,
|
||||||
|
].join(", ")}\n`,
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
process.stdout.write(
|
process.stdout.write(
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {{
|
||||||
|
* file: string,
|
||||||
|
* isEntry?: boolean,
|
||||||
|
* imports?: string[]
|
||||||
|
* }} ViteManifestEntry
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static imports of an entry are part of initial JavaScript. Every remaining
|
||||||
|
* JavaScript output is governed by the lazy-chunk budget.
|
||||||
|
*
|
||||||
|
* @param {Record<string, ViteManifestEntry>} manifest
|
||||||
|
*/
|
||||||
|
export function classifyViteJavascript(manifest) {
|
||||||
|
const initialFiles = new Set();
|
||||||
|
const visitedKeys = new Set();
|
||||||
|
const pendingKeys = Object.entries(manifest)
|
||||||
|
.filter(([, entry]) => entry.isEntry)
|
||||||
|
.map(([key]) => key);
|
||||||
|
const missingImports = [];
|
||||||
|
|
||||||
|
while (pendingKeys.length > 0) {
|
||||||
|
const key = /** @type {string} */ (pendingKeys.pop());
|
||||||
|
if (visitedKeys.has(key)) continue;
|
||||||
|
visitedKeys.add(key);
|
||||||
|
const entry = manifest[key];
|
||||||
|
if (!entry) {
|
||||||
|
missingImports.push(key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (entry.file.endsWith(".js")) initialFiles.add(entry.file);
|
||||||
|
pendingKeys.push(...(entry.imports ?? []));
|
||||||
|
}
|
||||||
|
|
||||||
|
const allJavaScript = new Set(
|
||||||
|
Object.values(manifest)
|
||||||
|
.map((entry) => entry.file)
|
||||||
|
.filter((file) => file.endsWith(".js")),
|
||||||
|
);
|
||||||
|
const lazyFiles = [...allJavaScript].filter(
|
||||||
|
(file) => !initialFiles.has(file),
|
||||||
|
);
|
||||||
|
return Object.freeze({
|
||||||
|
initialFiles: Object.freeze([...initialFiles].sort()),
|
||||||
|
lazyFiles: Object.freeze(lazyFiles.sort()),
|
||||||
|
missingImports: Object.freeze(missingImports.sort()),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -7,6 +7,11 @@ const securityPolicy = JSON.parse(
|
|||||||
await readFile("config/hosting/security-headers.json", "utf8"),
|
await readFile("config/hosting/security-headers.json", "utf8"),
|
||||||
);
|
);
|
||||||
const baseUrl = process.env.HOSTING_BASE_URL;
|
const baseUrl = process.env.HOSTING_BASE_URL;
|
||||||
|
const distFiles = (await readdir("dist", { recursive: true })).map(String);
|
||||||
|
const publicSourceMaps = distFiles.filter((file) => file.endsWith(".map"));
|
||||||
|
const publicServiceWorkers = distFiles.filter((file) =>
|
||||||
|
/(?:^|\/)(?:service-worker|sw)(?:[.-][^/]*)?\.js$/i.test(file),
|
||||||
|
);
|
||||||
|
|
||||||
/** @type {Record<string, Record<string, string>>} */
|
/** @type {Record<string, Record<string, string>>} */
|
||||||
let responses;
|
let responses;
|
||||||
@@ -15,13 +20,13 @@ let mode;
|
|||||||
if (baseUrl) {
|
if (baseUrl) {
|
||||||
mode = "live";
|
mode = "live";
|
||||||
const assets = await readdir("dist/assets");
|
const assets = await readdir("dist/assets");
|
||||||
const hashedAsset = assets.find((file) => !file.endsWith(".map"));
|
const hashedJavaScript = assets.find((file) => file.endsWith(".js"));
|
||||||
if (!hashedAsset) throw new Error("No built hashed asset found.");
|
if (!hashedJavaScript) throw new Error("No built hashed JavaScript found.");
|
||||||
const paths = {
|
const paths = {
|
||||||
index: "/",
|
index: "/",
|
||||||
runtimeConfig: "/config.json",
|
runtimeConfig: "/config.json",
|
||||||
releaseManifest: "/release-manifest.json",
|
releaseManifest: "/release-manifest.json",
|
||||||
hashedAsset: `/assets/${hashedAsset}`,
|
hashedAsset: `/assets/${hashedJavaScript}`,
|
||||||
};
|
};
|
||||||
responses = {};
|
responses = {};
|
||||||
for (const [surface, pathname] of Object.entries(paths)) {
|
for (const [surface, pathname] of Object.entries(paths)) {
|
||||||
@@ -51,6 +56,18 @@ for (const [surface, policy] of Object.entries(cachePolicy.surfaces)) {
|
|||||||
observed,
|
observed,
|
||||||
passed: observed === policy.cacheControl,
|
passed: observed === policy.cacheControl,
|
||||||
});
|
});
|
||||||
|
const observedContentType = responses[surface]?.["content-type"];
|
||||||
|
const observedMime = observedContentType
|
||||||
|
?.split(";", 1)[0]
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
results.push({
|
||||||
|
surface,
|
||||||
|
header: "content-type",
|
||||||
|
expected: policy.contentTypes,
|
||||||
|
observed: observedContentType,
|
||||||
|
passed: policy.contentTypes.includes(observedMime),
|
||||||
|
});
|
||||||
if (policy.securityHeaders) {
|
if (policy.securityHeaders) {
|
||||||
for (const [header, expected] of Object.entries(securityPolicy.headers)) {
|
for (const [header, expected] of Object.entries(securityPolicy.headers)) {
|
||||||
const observedSecurity = responses[surface]?.[header.toLowerCase()];
|
const observedSecurity = responses[surface]?.[header.toLowerCase()];
|
||||||
@@ -69,15 +86,19 @@ results.push({
|
|||||||
surface: "sourceMap",
|
surface: "sourceMap",
|
||||||
header: "public",
|
header: "public",
|
||||||
expected: false,
|
expected: false,
|
||||||
observed: cachePolicy.surfaces.sourceMap.public,
|
observed: publicSourceMaps.length > 0,
|
||||||
passed: cachePolicy.surfaces.sourceMap.public === false,
|
passed:
|
||||||
|
cachePolicy.surfaces.sourceMap.public === false &&
|
||||||
|
publicSourceMaps.length === 0,
|
||||||
});
|
});
|
||||||
results.push({
|
results.push({
|
||||||
surface: "serviceWorker",
|
surface: "serviceWorker",
|
||||||
header: "enabled",
|
header: "enabled",
|
||||||
expected: false,
|
expected: false,
|
||||||
observed: cachePolicy.surfaces.serviceWorker.enabled,
|
observed: publicServiceWorkers.length > 0,
|
||||||
passed: cachePolicy.surfaces.serviceWorker.enabled === false,
|
passed:
|
||||||
|
cachePolicy.surfaces.serviceWorker.enabled === false &&
|
||||||
|
publicServiceWorkers.length === 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const passed = results.every((result) => result.passed);
|
const passed = results.every((result) => result.passed);
|
||||||
@@ -100,7 +121,9 @@ await writeFile(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!passed) {
|
if (!passed) {
|
||||||
process.stderr.write("Hosting cache/security header verification failed.\n");
|
process.stderr.write(
|
||||||
|
"Hosting cache/content-type/security header verification failed.\n",
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
process.stdout.write(
|
process.stdout.write(
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import { createHash } from "node:crypto";
|
|||||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||||
|
|
||||||
import { verifyCompatibilityTuple } from "../src/application/policies/compatibility.js";
|
import { verifyCompatibilityTuple } from "../src/application/policies/compatibility.js";
|
||||||
import { compareReleaseToRuntime } from "../src/contracts/release-tokens.js";
|
import {
|
||||||
|
compareReleaseToRuntime,
|
||||||
|
RELEASE_TOKEN_REGISTRY,
|
||||||
|
} from "../src/contracts/release-tokens.js";
|
||||||
|
|
||||||
const fixturesDocument =
|
const fixturesDocument =
|
||||||
/** @type {{
|
/** @type {{
|
||||||
@@ -38,6 +41,14 @@ const actualAssetManifestHash = createHash("sha256")
|
|||||||
|
|
||||||
const artifactComparison = compareReleaseToRuntime(release, runtimeConfig);
|
const artifactComparison = compareReleaseToRuntime(release, runtimeConfig);
|
||||||
const artifactMismatches = [...artifactComparison.mismatches];
|
const artifactMismatches = [...artifactComparison.mismatches];
|
||||||
|
for (const token of Object.keys(RELEASE_TOKEN_REGISTRY)) {
|
||||||
|
if (typeof release[token] !== "string" || release[token].length === 0) {
|
||||||
|
artifactMismatches.push(`releaseToken:${token}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!Number.isFinite(Date.parse(release.builtAt))) {
|
||||||
|
artifactMismatches.push("releaseToken:builtAtFormat");
|
||||||
|
}
|
||||||
if (release.assetManifestHash !== actualAssetManifestHash) {
|
if (release.assetManifestHash !== actualAssetManifestHash) {
|
||||||
artifactMismatches.push("assetManifestContent");
|
artifactMismatches.push("assetManifestContent");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { classifyViteJavascript } from "../../scripts/lib/classify-vite-bundle.mjs";
|
||||||
|
|
||||||
|
describe("Vite bundle classification", () => {
|
||||||
|
it("counts transitive static imports as initial and keeps dynamic chunks lazy", () => {
|
||||||
|
expect(
|
||||||
|
classifyViteJavascript({
|
||||||
|
"index.html": {
|
||||||
|
file: "assets/entry.js",
|
||||||
|
isEntry: true,
|
||||||
|
imports: ["_shared.js"],
|
||||||
|
},
|
||||||
|
"_shared.js": { file: "assets/shared.js", imports: ["_runtime.js"] },
|
||||||
|
"_runtime.js": { file: "assets/runtime.js" },
|
||||||
|
"src/lazy.js": { file: "assets/lazy.js" },
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
initialFiles: [
|
||||||
|
"assets/entry.js",
|
||||||
|
"assets/runtime.js",
|
||||||
|
"assets/shared.js",
|
||||||
|
],
|
||||||
|
lazyFiles: ["assets/lazy.js"],
|
||||||
|
missingImports: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports a manifest import that cannot be resolved", () => {
|
||||||
|
expect(
|
||||||
|
classifyViteJavascript({
|
||||||
|
"index.html": {
|
||||||
|
file: "assets/entry.js",
|
||||||
|
isEntry: true,
|
||||||
|
imports: ["_missing.js"],
|
||||||
|
},
|
||||||
|
}).missingImports,
|
||||||
|
).toEqual(["_missing.js"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user