Compare commits

..
20 changed files with 192 additions and 121 deletions
-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
+1 -8
View File
@@ -124,10 +124,7 @@
"logPath": "artifacts/quality/gates/FE-GATE-009.txt", "logPath": "artifacts/quality/gates/FE-GATE-009.txt",
"evidence": [ "evidence": [
"artifacts/tests/a11y.json", "artifacts/tests/a11y.json",
"artifacts/tests/a11y-manual/APP_HOME.md", "artifacts/tests/a11y-manual/APP_HOME.md"
"artifacts/tests/a11y-manual/SAMPLE_RESOURCE_LIST.md",
"artifacts/tests/a11y-manual/NOT_FOUND.md",
"artifacts/tests/a11y-manual/report.json"
], ],
"retentionClass": "merge-cycle" "retentionClass": "merge-cycle"
}, },
@@ -211,10 +208,6 @@
}, },
"FE-GATE-018": { "FE-GATE-018": {
"name": "field-web-vitals", "name": "field-web-vitals",
"requiresEnvironment": [
"FIELD_WEB_VITALS_INPUT",
"MIN_ELIGIBLE_SAMPLES"
],
"steps": [ "steps": [
{ "script": "collect:web-vitals-evidence", "expect": "pass" } { "script": "collect:web-vitals-evidence", "expect": "pass" }
], ],
+4
View File
@@ -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": {
+5 -1
View File
@@ -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"
} }
} }
} }
-4
View File
@@ -1,9 +1,5 @@
# Architecture overview # Architecture overview
This Mermaid view is a repository-local implementation projection. The
`PASS_SCOPED` reviewer evidence applies to the canonical draw.io diagram named
in `review-ledger.json`, not automatically to edits in this file.
```mermaid ```mermaid
flowchart LR flowchart LR
Bootstrap[bootstrap / composition root] --> Presentation[presentation] Bootstrap[bootstrap / composition root] --> Presentation[presentation]
-22
View File
@@ -1,22 +0,0 @@
# Imported scoped diagram review evidence
This ledger entry consumes the canonical evidence already recorded by the
`ca-skeleton-frontend-operational-contract` project note. It does not claim
review of the repository-local Mermaid projections or of the complete
production deployment topology.
- Reviewer: `wiki-diagram-reviewer`
- Standard: `rules/diagram-standards.md` v2
- Canonical report:
`docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/diagram-review.md`
- Canonical report SHA-256:
`b4d2a35e4f07e176717786408f98dab5cee1047f77f6ff61f5faeddfccd78a29`
| Canonical diagram | SHA-256 | Score | Verdict | Reviewed scope |
| --- | --- | ---: | --- | --- |
| `raw/diagrams/ca-skeleton-frontend/architecture-overview-2026-07-18.drawio` | `c0ae56c9c964c5c6e698ab7dcc91736b9b811b2b834381817905db81c4230ba0` | 100 | PASS | Clean Architecture compile-time dependency ownership |
| `raw/diagrams/ca-skeleton-frontend/architecture-deployment-2026-07-18.drawio` | `9a654326fb840ddf24b832221ff7eec4b8fadd9f87ad84174fccfa3bfcd1a25b` | 100 | PASS | immutable static assets and mutable `/config.json` delivery |
The canonical report explicitly limits this `PASS_SCOPED`: it does not verify
the complete release/rollback topology, the implementation topology, or live
hosting state.
+8 -20
View File
@@ -1,29 +1,17 @@
{ {
"schemaVersion": 1, "schemaVersion": 1,
"status": "PASS_SCOPED", "status": "pending-review",
"reviewer": "wiki-diagram-reviewer", "reviewerThreshold": null,
"standard": "rules/diagram-standards.md v2",
"evidenceReport": {
"repoPath": "docs/architecture/review-evidence.md",
"canonicalPath": "docs/superpowers/specs/2026-07-18-ca-skeleton-frontend-operational-contract-review/diagram-review.md",
"canonicalSha256": "b4d2a35e4f07e176717786408f98dab5cee1047f77f6ff61f5faeddfccd78a29"
},
"reviews": { "reviews": {
"overview": { "overview": {
"sourcePath": "raw/diagrams/ca-skeleton-frontend/architecture-overview-2026-07-18.drawio", "path": "docs/architecture/overview.md",
"sha256": "c0ae56c9c964c5c6e698ab7dcc91736b9b811b2b834381817905db81c4230ba0", "reviewer": null,
"score": 100, "score": null
"verdict": "PASS",
"thresholdSatisfied": true,
"scope": "Clean Architecture compile-time dependency ownership"
}, },
"staticDelivery": { "staticDelivery": {
"sourcePath": "raw/diagrams/ca-skeleton-frontend/architecture-deployment-2026-07-18.drawio", "path": "docs/architecture/static-delivery.md",
"sha256": "9a654326fb840ddf24b832221ff7eec4b8fadd9f87ad84174fccfa3bfcd1a25b", "reviewer": null,
"score": 100, "score": null
"verdict": "PASS",
"thresholdSatisfied": true,
"scope": "immutable static assets and mutable /config.json delivery"
} }
} }
} }
-4
View File
@@ -1,9 +1,5 @@
# Static asset and runtime-config delivery # Static asset and runtime-config delivery
This Mermaid view is a repository-local implementation projection. The
`PASS_SCOPED` reviewer evidence applies only to the canonical static-delivery
draw.io scope recorded in `review-ledger.json`.
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
participant CI participant CI
+3 -4
View File
@@ -18,10 +18,9 @@ DOCUMENTATION_READY (off-chain)
Pull requests and `develop` pushes evaluate merge readiness. Version tags Pull requests and `develop` pushes evaluate merge readiness. Version tags
evaluate merge then release readiness. Production and field evaluation require evaluate merge then release readiness. Production and field evaluation require
an explicit workflow dispatch. The field tier cannot pass until the 28-day an explicit workflow dispatch. The field tier cannot pass until the 28-day
sample threshold decision is recorded. Documentation readiness consumes the sample threshold decision is recorded. Documentation readiness cannot pass
canonical project-note evidence in which both scoped diagrams already received until both scoped diagrams have a recorded reviewer threshold and signed
100/100 `PASS_SCOPED`; the repo ledger preserves the evidence scope and scores.
canonical digests.
All jobs upload the shared `artifacts/` tree even after failure. Numeric All jobs upload the shared `artifacts/` tree even after failure. Numeric
retention remains an organization/provider decision; the workflow intentionally retention remains an organization/provider decision; the workflow intentionally
@@ -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
View File
@@ -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
View File
@@ -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(
+49
View File
@@ -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()),
});
}
+12 -29
View File
@@ -3,43 +3,29 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
const ledger = JSON.parse( const ledger = JSON.parse(
await readFile("docs/architecture/review-ledger.json", "utf8"), await readFile("docs/architecture/review-ledger.json", "utf8"),
); );
const evidence = await readFile(ledger.evidenceReport.repoPath, "utf8");
const results = []; const results = [];
for (const [diagram, review] of Object.entries(ledger.reviews)) { for (const [diagram, review] of Object.entries(ledger.reviews)) {
const sourceReferenced = evidence.includes(review.sourcePath); const content = await readFile(review.path, "utf8");
const digestReferenced = const hasDiagram = /```mermaid[\s\S]+```/.test(content);
/^[0-9a-f]{64}$/.test(review.sha256) &&
evidence.includes(review.sha256);
const scorePass = const scorePass =
review.thresholdSatisfied === true && typeof ledger.reviewerThreshold === "number" &&
review.verdict === "PASS" &&
typeof review.score === "number" && typeof review.score === "number" &&
evidence.includes(`| ${review.score} | PASS |`); review.score >= ledger.reviewerThreshold;
results.push({ results.push({
diagram, diagram,
sourcePath: review.sourcePath, path: review.path,
sha256: review.sha256, hasDiagram,
sourceReferenced, reviewer: review.reviewer,
digestReferenced,
reviewer: ledger.reviewer,
score: review.score, score: review.score,
scorePass, scorePass,
passed: passed:
sourceReferenced && hasDiagram &&
digestReferenced && Boolean(review.reviewer) &&
ledger.reviewer === "wiki-diagram-reviewer" &&
ledger.standard === "rules/diagram-standards.md v2" &&
scorePass && scorePass &&
ledger.status === "PASS_SCOPED", ledger.status === "PASS_SCOPED",
}); });
} }
const reportDigestValid = const passed = results.every((result) => result.passed);
/^[0-9a-f]{64}$/.test(ledger.evidenceReport.canonicalSha256) &&
evidence.includes(ledger.evidenceReport.canonicalSha256);
const passed =
reportDigestValid &&
results.length === 2 &&
results.every((result) => result.passed);
await mkdir("artifacts/quality", { recursive: true }); await mkdir("artifacts/quality", { recursive: true });
await writeFile( await writeFile(
"artifacts/quality/documentation-review.json", "artifacts/quality/documentation-review.json",
@@ -48,10 +34,7 @@ await writeFile(
schemaVersion: 1, schemaVersion: 1,
generatedAt: new Date().toISOString(), generatedAt: new Date().toISOString(),
status: ledger.status, status: ledger.status,
reviewer: ledger.reviewer, reviewerThreshold: ledger.reviewerThreshold,
standard: ledger.standard,
evidenceReport: ledger.evidenceReport,
reportDigestValid,
results, results,
passed, passed,
}, },
@@ -61,7 +44,7 @@ await writeFile(
); );
if (!passed) { if (!passed) {
process.stderr.write( process.stderr.write(
"Documentation readiness: FAIL_UNVERIFIED (canonical scoped-review evidence is incomplete)\n", "Documentation readiness: FAIL_UNVERIFIED (reviewer threshold and signed reviews required)\n",
); );
process.exit(1); process.exit(1);
} }
+31 -8
View File
@@ -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(
+12 -1
View File
@@ -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");
} }
+40
View File
@@ -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"]);
});
});