Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ steps:
`schemaVersion` to the new value and adjust the data shape to satisfy the
new schema.
4. **Register a downward migration** in
[`scripts/build.mjs`](scripts/build.mjs). Add an entry under
[`scripts/build.mts`](scripts/build.mts). Add an entry under
`MIGRATIONS["<name>"]` keyed by the previous major (`N`); the function
projects new-source-shape data into old-schema-shape data.
5. **Mark the previous schema deprecated** by adding `"deprecated": true` at
Expand Down
24 changes: 20 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 12 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@
"prettier:fix": "prettier --write .",
"prettier": "prettier --check .",
"lint:md": "remark . --frail --quiet",
"lint:selectors": "node scripts/lint-selectors.mjs",
"validate": "node scripts/validate-schemas.mjs",
"check": "npm run prettier && npm run lint:md && npm run validate && npm run lint:selectors && npm test",
"test": "node --test scripts/**/*.test.mjs",
"build": "node scripts/build.mjs",
"build:clean": "rm -rf dist && node scripts/build.mjs"
"lint:selectors": "node scripts/lint-selectors.mts",
"typecheck": "tsc --noEmit",
"validate": "node scripts/validate-schemas.mts",
"check": "npm run prettier && npm run typecheck && npm run lint:md && npm run validate && npm run lint:selectors && npm test",
"test": "node --test scripts/**/*.test.mts",
"build": "node scripts/build.mts",
"build:clean": "rm -rf dist && node scripts/build.mts"
},
"engines": {
"node": ">=22",
Expand All @@ -33,16 +34,17 @@
"prettier --check"
],
"maps/**/*.jsonc": [
"node scripts/validate-schemas.mjs"
"node scripts/validate-schemas.mts"
],
"maps/forms/*.jsonc": [
"node scripts/lint-selectors.mjs"
"node scripts/lint-selectors.mts"
],
"*.md": [
"remark --frail --quiet"
]
},
"devDependencies": {
"@types/node": "^22.20.0",
"ajv": "8.18.0",
"ajv-formats": "3.0.1",
"css-what": "8.0.0",
Expand All @@ -55,6 +57,7 @@
"remark-lint-no-trailing-spaces": "4.0.3",
"remark-preset-lint-consistent": "6.0.1",
"remark-preset-lint-recommended": "7.0.1",
"strip-json-comments": "5.0.3"
"strip-json-comments": "5.0.3",
"typescript": "^6.0.3"
}
}
97 changes: 81 additions & 16 deletions scripts/build.mjs β†’ scripts/build.mts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,74 @@ import { readFileSync, writeFileSync, mkdirSync, rmSync, cpSync } from "fs";
import { createHash } from "crypto";
import { basename, dirname, join, relative } from "path";
import { glob } from "node:fs/promises";
import Ajv2020 from "ajv/dist/2020.js";
import addFormats from "ajv-formats";
import Ajv2020Import from "ajv/dist/2020.js";
import addFormatsImport from "ajv-formats";
import stripJsonComments from "strip-json-comments";
import { red, yellow, green, cyan } from "./utils.mjs";
import { red, yellow, green, cyan } from "./utils.mts";

// ajv and ajv-formats are CommonJS; under NodeNext their ESM default import is
// the module namespace, so the constructor/function lives on `.default`.
const Ajv2020 = Ajv2020Import.default;
const addFormats = addFormatsImport.default;

const DIST = "dist";

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

/** Parsed source data for a Map (`<name>.jsonc`). Treated generically here. */
interface MapSourceData {
schemaVersion?: string;
hosts?: Record<string, unknown>;
[key: string]: unknown;
}

/** A migration projecting the latest source shape onto an older major. */
type MigrationFn = (data: MapSourceData) => MapSourceData;

/** The subset of a JSON Schema document the build reads. */
interface SchemaJson {
$id?: string;
deprecated?: boolean;
properties?: { schemaVersion?: { const?: string } };
[key: string]: unknown;
}

interface SchemaEntry {
file: string;
schema: SchemaJson;
major: number;
expectedVersion: string;
}

interface BuildEntry {
target: SchemaEntry;
payload: MapSourceData;
}

interface MapEntry {
name: string;
dir: string;
dataFile: string;
schemas: SchemaEntry[];
builds: BuildEntry[];
}

interface ManifestMapVersion {
filename: string;
cid: string;
schema: string;
deprecated?: boolean;
}

interface Manifest {
buildId: string;
timestamp: string;
gitSha: string;
maps: Record<string, Record<string, ManifestMapVersion>>;
}

// ---------------------------------------------------------------------------
// Per-Map backwards-compatibility migrations
//
Expand Down Expand Up @@ -38,7 +99,7 @@ const DIST = "dist";
// To drop support for an older schema major, either remove its migration
// entry below or delete the corresponding `<name>.v<N>.schema.json` file.
// ---------------------------------------------------------------------------
const MIGRATIONS = {
const MIGRATIONS: Record<string, Record<number, MigrationFn>> = {
forms: {
// 0: (data) => data, // example: latest source projecting to v0
// 1: (data) => data, // example: latest source projecting to v1
Expand All @@ -51,7 +112,7 @@ rmSync(DIST, { recursive: true, force: true });

// Step 1: Discover Maps and their schemas

const mapsByName = new Map();
const mapsByName = new Map<string, MapEntry>();

// Each Map lives one level deep under maps/ (e.g. maps/forms/).
// Schema files are versioned: <name>.v<major>.schema.json.
Expand All @@ -68,7 +129,9 @@ for await (const schemaFile of glob("maps/*/*.v*.schema.json")) {

const major = parseInt(majorMatch[1], 10);

const schemaJson = JSON.parse(readFileSync(schemaFile, "utf-8"));
const schemaJson = JSON.parse(
readFileSync(schemaFile, "utf-8"),
) as SchemaJson;
const expectedVersion = schemaJson?.properties?.schemaVersion?.const;

if (typeof expectedVersion !== "string") {
Expand Down Expand Up @@ -110,10 +173,10 @@ for await (const schemaFile of glob("maps/*/*.v*.schema.json")) {
}

if (!mapsByName.has(name)) {
mapsByName.set(name, { name, dir, dataFile, schemas: [] });
mapsByName.set(name, { name, dir, dataFile, schemas: [], builds: [] });
}

mapsByName.get(name).schemas.push({
mapsByName.get(name)!.schemas.push({
file: schemaFile,
schema: schemaJson,
major,
Expand Down Expand Up @@ -175,7 +238,7 @@ for (const map of maps) {

const sourceData = JSON.parse(
stripJsonComments(readFileSync(map.dataFile, "utf-8")),
);
) as MapSourceData;

// Normalize unicode host keys to punycode (once) and warn on www. prefixes.
if (sourceData.hosts) {
Expand Down Expand Up @@ -232,7 +295,7 @@ for (const map of maps) {
map.builds = [];

for (const target of targets) {
let projectedData;
let projectedData: MapSourceData;
if (target.major === sourceSchema.major) {
projectedData = sourceData;
} else {
Expand All @@ -241,7 +304,7 @@ for (const map of maps) {
console.error(
red(
`${map.name}: no migration registered for source v${sourceSchema.major} β†’ v${target.major}. ` +
`Register MIGRATIONS["${map.name}"][${target.major}] in scripts/build.mjs, ` +
`Register MIGRATIONS["${map.name}"][${target.major}] in scripts/build.mts, ` +
`or delete ${map.dir}/${map.name}.v${target.major}.schema.json to drop support for v${target.major}.`,
),
);
Expand All @@ -260,7 +323,7 @@ for (const map of maps) {
if (!validate(payload)) {
console.error(red(`Validation failed: ${map.dataFile} β†’ ${target.file}`));

for (const err of validate.errors) {
for (const err of validate.errors ?? []) {
console.error(` ${err.instancePath || "/"}: ${err.message}`);
}

Expand Down Expand Up @@ -307,14 +370,14 @@ const gitSha =
}
})();

const manifest = {
const manifest: Manifest = {
buildId,
timestamp: new Date().toISOString(),
gitSha,
maps: {},
};

const checksums = [];
const checksums: string[] = [];

mkdirSync(DIST, { recursive: true });

Expand Down Expand Up @@ -351,13 +414,15 @@ for (const map of maps) {

// Validate the assembled manifest against its schema before writing.
const manifestSchemaSrc = "scripts/manifest.schema.json";
const manifestSchema = JSON.parse(readFileSync(manifestSchemaSrc, "utf-8"));
const manifestSchema = JSON.parse(
readFileSync(manifestSchemaSrc, "utf-8"),
) as SchemaJson;
const validateManifest = ajv.compile(manifestSchema);
if (!validateManifest(manifest)) {
console.error(
red(`Manifest failed validation against ${manifestSchemaSrc}:`),
);
for (const err of validateManifest.errors) {
for (const err of validateManifest.errors ?? []) {
console.error(` ${err.instancePath || "/"}: ${err.message}`);
}
process.exit(1);
Expand Down
Loading