Skip to content

Commit fdbab06

Browse files
authored
Merge pull request #30 from bitwarden/ts-migrate/PM-35418
Migrate scripts/ from JavaScript to TypeScript
2 parents d322e75 + 6fc1bd9 commit fdbab06

12 files changed

Lines changed: 390 additions & 127 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ steps:
235235
`schemaVersion` to the new value and adjust the data shape to satisfy the
236236
new schema.
237237
4. **Register a downward migration** in
238-
[`scripts/build.mjs`](scripts/build.mjs). Add an entry under
238+
[`scripts/build.mts`](scripts/build.mts). Add an entry under
239239
`MIGRATIONS["<name>"]` keyed by the previous major (`N`); the function
240240
projects new-source-shape data into old-schema-shape data.
241241
5. **Mark the previous schema deprecated** by adding `"deprecated": true` at

package-lock.json

Lines changed: 20 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@
1717
"prettier:fix": "prettier --write .",
1818
"prettier": "prettier --check .",
1919
"lint:md": "remark . --frail --quiet",
20-
"lint:selectors": "node scripts/lint-selectors.mjs",
21-
"validate": "node scripts/validate-schemas.mjs",
22-
"check": "npm run prettier && npm run lint:md && npm run validate && npm run lint:selectors && npm test",
23-
"test": "node --test scripts/**/*.test.mjs",
24-
"build": "node scripts/build.mjs",
25-
"build:clean": "rm -rf dist && node scripts/build.mjs"
20+
"lint:selectors": "node scripts/lint-selectors.mts",
21+
"typecheck": "tsc --noEmit",
22+
"validate": "node scripts/validate-schemas.mts",
23+
"check": "npm run prettier && npm run typecheck && npm run lint:md && npm run validate && npm run lint:selectors && npm test",
24+
"test": "node --test scripts/**/*.test.mts",
25+
"build": "node scripts/build.mts",
26+
"build:clean": "rm -rf dist && node scripts/build.mts"
2627
},
2728
"engines": {
2829
"node": ">=22",
@@ -33,16 +34,17 @@
3334
"prettier --check"
3435
],
3536
"maps/**/*.jsonc": [
36-
"node scripts/validate-schemas.mjs"
37+
"node scripts/validate-schemas.mts"
3738
],
3839
"maps/forms/*.jsonc": [
39-
"node scripts/lint-selectors.mjs"
40+
"node scripts/lint-selectors.mts"
4041
],
4142
"*.md": [
4243
"remark --frail --quiet"
4344
]
4445
},
4546
"devDependencies": {
47+
"@types/node": "^22.20.0",
4648
"ajv": "8.18.0",
4749
"ajv-formats": "3.0.1",
4850
"css-what": "8.0.0",
@@ -55,6 +57,7 @@
5557
"remark-lint-no-trailing-spaces": "4.0.3",
5658
"remark-preset-lint-consistent": "6.0.1",
5759
"remark-preset-lint-recommended": "7.0.1",
58-
"strip-json-comments": "5.0.3"
60+
"strip-json-comments": "5.0.3",
61+
"typescript": "^6.0.3"
5962
}
6063
}
Lines changed: 81 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,74 @@ import { readFileSync, writeFileSync, mkdirSync, rmSync, cpSync } from "fs";
33
import { createHash } from "crypto";
44
import { basename, dirname, join, relative } from "path";
55
import { glob } from "node:fs/promises";
6-
import Ajv2020 from "ajv/dist/2020.js";
7-
import addFormats from "ajv-formats";
6+
import Ajv2020Import from "ajv/dist/2020.js";
7+
import addFormatsImport from "ajv-formats";
88
import stripJsonComments from "strip-json-comments";
9-
import { red, yellow, green, cyan } from "./utils.mjs";
9+
import { red, yellow, green, cyan } from "./utils.mts";
10+
11+
// ajv and ajv-formats are CommonJS; under NodeNext their ESM default import is
12+
// the module namespace, so the constructor/function lives on `.default`.
13+
const Ajv2020 = Ajv2020Import.default;
14+
const addFormats = addFormatsImport.default;
1015

1116
const DIST = "dist";
1217

18+
// ---------------------------------------------------------------------------
19+
// Types
20+
// ---------------------------------------------------------------------------
21+
22+
/** Parsed source data for a Map (`<name>.jsonc`). Treated generically here. */
23+
interface MapSourceData {
24+
schemaVersion?: string;
25+
hosts?: Record<string, unknown>;
26+
[key: string]: unknown;
27+
}
28+
29+
/** A migration projecting the latest source shape onto an older major. */
30+
type MigrationFn = (data: MapSourceData) => MapSourceData;
31+
32+
/** The subset of a JSON Schema document the build reads. */
33+
interface SchemaJson {
34+
$id?: string;
35+
deprecated?: boolean;
36+
properties?: { schemaVersion?: { const?: string } };
37+
[key: string]: unknown;
38+
}
39+
40+
interface SchemaEntry {
41+
file: string;
42+
schema: SchemaJson;
43+
major: number;
44+
expectedVersion: string;
45+
}
46+
47+
interface BuildEntry {
48+
target: SchemaEntry;
49+
payload: MapSourceData;
50+
}
51+
52+
interface MapEntry {
53+
name: string;
54+
dir: string;
55+
dataFile: string;
56+
schemas: SchemaEntry[];
57+
builds: BuildEntry[];
58+
}
59+
60+
interface ManifestMapVersion {
61+
filename: string;
62+
cid: string;
63+
schema: string;
64+
deprecated?: boolean;
65+
}
66+
67+
interface Manifest {
68+
buildId: string;
69+
timestamp: string;
70+
gitSha: string;
71+
maps: Record<string, Record<string, ManifestMapVersion>>;
72+
}
73+
1374
// ---------------------------------------------------------------------------
1475
// Per-Map backwards-compatibility migrations
1576
//
@@ -38,7 +99,7 @@ const DIST = "dist";
3899
// To drop support for an older schema major, either remove its migration
39100
// entry below or delete the corresponding `<name>.v<N>.schema.json` file.
40101
// ---------------------------------------------------------------------------
41-
const MIGRATIONS = {
102+
const MIGRATIONS: Record<string, Record<number, MigrationFn>> = {
42103
forms: {
43104
// 0: (data) => data, // example: latest source projecting to v0
44105
// 1: (data) => data, // example: latest source projecting to v1
@@ -51,7 +112,7 @@ rmSync(DIST, { recursive: true, force: true });
51112

52113
// Step 1: Discover Maps and their schemas
53114

54-
const mapsByName = new Map();
115+
const mapsByName = new Map<string, MapEntry>();
55116

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

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

71-
const schemaJson = JSON.parse(readFileSync(schemaFile, "utf-8"));
132+
const schemaJson = JSON.parse(
133+
readFileSync(schemaFile, "utf-8"),
134+
) as SchemaJson;
72135
const expectedVersion = schemaJson?.properties?.schemaVersion?.const;
73136

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

112175
if (!mapsByName.has(name)) {
113-
mapsByName.set(name, { name, dir, dataFile, schemas: [] });
176+
mapsByName.set(name, { name, dir, dataFile, schemas: [], builds: [] });
114177
}
115178

116-
mapsByName.get(name).schemas.push({
179+
mapsByName.get(name)!.schemas.push({
117180
file: schemaFile,
118181
schema: schemaJson,
119182
major,
@@ -175,7 +238,7 @@ for (const map of maps) {
175238

176239
const sourceData = JSON.parse(
177240
stripJsonComments(readFileSync(map.dataFile, "utf-8")),
178-
);
241+
) as MapSourceData;
179242

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

234297
for (const target of targets) {
235-
let projectedData;
298+
let projectedData: MapSourceData;
236299
if (target.major === sourceSchema.major) {
237300
projectedData = sourceData;
238301
} else {
@@ -241,7 +304,7 @@ for (const map of maps) {
241304
console.error(
242305
red(
243306
`${map.name}: no migration registered for source v${sourceSchema.major} → v${target.major}. ` +
244-
`Register MIGRATIONS["${map.name}"][${target.major}] in scripts/build.mjs, ` +
307+
`Register MIGRATIONS["${map.name}"][${target.major}] in scripts/build.mts, ` +
245308
`or delete ${map.dir}/${map.name}.v${target.major}.schema.json to drop support for v${target.major}.`,
246309
),
247310
);
@@ -260,7 +323,7 @@ for (const map of maps) {
260323
if (!validate(payload)) {
261324
console.error(red(`Validation failed: ${map.dataFile}${target.file}`));
262325

263-
for (const err of validate.errors) {
326+
for (const err of validate.errors ?? []) {
264327
console.error(` ${err.instancePath || "/"}: ${err.message}`);
265328
}
266329

@@ -307,14 +370,14 @@ const gitSha =
307370
}
308371
})();
309372

310-
const manifest = {
373+
const manifest: Manifest = {
311374
buildId,
312375
timestamp: new Date().toISOString(),
313376
gitSha,
314377
maps: {},
315378
};
316379

317-
const checksums = [];
380+
const checksums: string[] = [];
318381

319382
mkdirSync(DIST, { recursive: true });
320383

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

352415
// Validate the assembled manifest against its schema before writing.
353416
const manifestSchemaSrc = "scripts/manifest.schema.json";
354-
const manifestSchema = JSON.parse(readFileSync(manifestSchemaSrc, "utf-8"));
417+
const manifestSchema = JSON.parse(
418+
readFileSync(manifestSchemaSrc, "utf-8"),
419+
) as SchemaJson;
355420
const validateManifest = ajv.compile(manifestSchema);
356421
if (!validateManifest(manifest)) {
357422
console.error(
358423
red(`Manifest failed validation against ${manifestSchemaSrc}:`),
359424
);
360-
for (const err of validateManifest.errors) {
425+
for (const err of validateManifest.errors ?? []) {
361426
console.error(` ${err.instancePath || "/"}: ${err.message}`);
362427
}
363428
process.exit(1);

0 commit comments

Comments
 (0)