diff --git a/package.json b/package.json index af55d95e..e3ae27c3 100644 --- a/package.json +++ b/package.json @@ -18,24 +18,24 @@ "build:prod": "node esbuild.config.mjs --production", "dev": "node esbuild.config.mjs --watch", "format": "node scripts/format.mjs", - "format:check": "node scripts/format.mjs --check --cache --cache-location node_modules/.cache/prettier/.prettier-cache", + "format:check": "node scripts/format.mjs --check --cache", "format:check:ci": "node scripts/format.mjs --check", - "api:baseline": "node scripts/report-api-baseline.mjs", - "api:baseline:check": "node scripts/report-api-baseline.mjs --check", - "generate:app-server-types": "node scripts/clean-generated-types.mjs && codex app-server generate-ts --experimental --out src/generated/app-server && node scripts/normalize-generated-types.mjs", + "api:baseline": "node scripts/api-baseline.mjs", + "api:baseline:check": "node scripts/api-baseline.mjs --check", + "generate:app-server-types": "node scripts/generate-app-server-types.mjs", "lint": "node scripts/run-parallel.mjs lint:ts lint:css", "lint:css": "stylelint \"*.css\" \"src/**/*.css\" --max-warnings=0", "lint:ts": "eslint src tests scripts \"*.config.ts\" \"*.config.mjs\" --max-warnings=0 --cache --cache-location node_modules/.cache/eslint/ --cache-strategy content", "lint:ts:ci": "eslint src tests scripts \"*.config.ts\" \"*.config.mjs\" --max-warnings=0", - "release:check": "node scripts/check-release.mjs", - "release:preflight": "node scripts/preflight-release.mjs", - "release:prepare": "node scripts/prepare-release.mjs", + "release:check": "node scripts/release/check.mjs", + "release:preflight": "node scripts/release/preflight.mjs", + "release:prepare": "node scripts/release/prepare.mjs", "test": "vitest run --cache", "test:ci": "vitest run", "typecheck": "tsc -p tsconfig.json --noEmit --incremental --tsBuildInfoFile node_modules/.cache/typescript/tsconfig.tsbuildinfo", "typecheck:ci": "tsc -p tsconfig.json --noEmit", - "check": "node scripts/run-parallel.mjs typecheck test lint:ts lint:css format:check && npm run build:prod", - "check:ci": "npm run typecheck:ci && npm run test:ci && npm run lint:ts:ci && npm run lint:css && npm run format:check:ci && npm run build:prod" + "check": "node scripts/check.mjs", + "check:ci": "node scripts/check.mjs --ci" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/scripts/report-api-baseline.mjs b/scripts/api-baseline.mjs similarity index 97% rename from scripts/report-api-baseline.mjs rename to scripts/api-baseline.mjs index b9b63da2..43e8902b 100644 --- a/scripts/report-api-baseline.mjs +++ b/scripts/api-baseline.mjs @@ -1,6 +1,6 @@ import { readFile } from "node:fs/promises"; import { spawnSync } from "node:child_process"; -import { readJson } from "./release-utils.mjs"; +import { readJson } from "./release/utils.mjs"; const args = new Set(process.argv.slice(2)); const shouldCheck = args.has("--check"); @@ -118,6 +118,7 @@ const manifestJson = await readJson("manifest.json"); const versionsJson = await readJson("versions.json"); const readme = await readFile("README.md", "utf8"); const clientSource = await readFile("src/app-server/client.ts", "utf8"); +const appServerGenerateSource = await readFile("scripts/generate-app-server-types.mjs", "utf8"); const readmeBaselines = readCompatibilityBaselines(readme); const codexReadmeVersion = readmeBaselines.codexTestedCliVersion; @@ -134,9 +135,10 @@ const obsidianSpecSemver = parseSemver(obsidianSpec); const obsidianLockSemver = parseSemver(obsidianLockVersion); const obsidianMinSemver = parseSemver(obsidianMinVersion); -const appServerGenerateScript = packageJson.scripts?.["generate:app-server-types"] ?? ""; const appServerGenerationExperimental = - appServerGenerateScript.includes("codex app-server generate-ts") && appServerGenerateScript.includes("--experimental"); + appServerGenerateSource.includes("app-server") && + appServerGenerateSource.includes("generate-ts") && + appServerGenerateSource.includes("--experimental"); const initializeExperimentalApi = /experimentalApi:\s*true/.test(clientSource); const initializeRequestAttestationDisabled = /requestAttestation:\s*false/.test(clientSource); diff --git a/scripts/check.mjs b/scripts/check.mjs new file mode 100644 index 00000000..764cc77a --- /dev/null +++ b/scripts/check.mjs @@ -0,0 +1,32 @@ +import { spawnSync } from "node:child_process"; + +const args = new Set(process.argv.slice(2)); +const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm"; + +for (const arg of args) { + if (arg !== "--ci") { + console.error("Usage: node scripts/check.mjs [--ci]"); + process.exit(1); + } +} + +if (args.has("--ci")) { + for (const script of ["typecheck:ci", "test:ci", "lint:ts:ci", "lint:css", "format:check:ci", "build:prod"]) { + run(npmCommand, ["run", script]); + } +} else { + run("node", ["scripts/run-parallel.mjs", "typecheck", "test", "lint:ts", "lint:css", "format:check"]); + run(npmCommand, ["run", "build:prod"]); +} + +function run(command, args) { + const result = spawnSync(command, args, { + stdio: "inherit", + shell: false, + }); + if (result.error) { + console.error(`Failed to run ${command} ${args.join(" ")}: ${result.error.message}`); + process.exit(1); + } + if (result.status !== 0) process.exit(result.status ?? 1); +} diff --git a/scripts/clean-generated-types.mjs b/scripts/clean-generated-types.mjs deleted file mode 100644 index de4b3b24..00000000 --- a/scripts/clean-generated-types.mjs +++ /dev/null @@ -1,7 +0,0 @@ -import { mkdir, rm } from "node:fs/promises"; -import path from "node:path"; - -const generatedDir = path.resolve("src/generated/app-server"); - -await rm(generatedDir, { recursive: true, force: true }); -await mkdir(generatedDir, { recursive: true }); diff --git a/scripts/generate-app-server-types.mjs b/scripts/generate-app-server-types.mjs new file mode 100644 index 00000000..c1cd0704 --- /dev/null +++ b/scripts/generate-app-server-types.mjs @@ -0,0 +1,68 @@ +import { spawnSync } from "node:child_process"; +import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises"; +import path from "node:path"; + +const generatedDir = path.resolve("src/generated/app-server"); +const generatedHeader = "// GENERATED CODE! DO NOT MODIFY BY HAND!"; +const normalizationNotice = "// This file was mechanically normalized after generation by scripts/generate-app-server-types.mjs."; + +await cleanGeneratedTypes(); +run("codex", ["app-server", "generate-ts", "--experimental", "--out", "src/generated/app-server"]); +await normalizeGeneratedTypes(); + +async function cleanGeneratedTypes() { + await rm(generatedDir, { recursive: true, force: true }); + await mkdir(generatedDir, { recursive: true }); +} + +async function normalizeGeneratedTypes() { + const files = await listTypeScriptFiles(generatedDir); + + await Promise.all( + files.map(async (file) => { + const source = await readFile(file, "utf8"); + const normalized = normalizeSource(source); + const nextSource = normalized === source ? source : addNormalizationNotice(normalized); + if (nextSource !== source) await writeFile(file, nextSource); + }), + ); +} + +async function listTypeScriptFiles(dir) { + const entries = await readdir(dir, { withFileTypes: true }); + const files = await Promise.all( + entries.map((entry) => { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) return listTypeScriptFiles(fullPath); + return entry.isFile() && entry.name.endsWith(".ts") ? [fullPath] : []; + }), + ); + return files.flat(); +} + +function normalizeSource(source) { + let normalized = source; + do { + source = normalized; + normalized = source.replaceAll("| null | null", "| null"); + } while (normalized !== source); + return normalized; +} + +function addNormalizationNotice(source) { + if (source.includes(normalizationNotice)) return source; + if (!source.startsWith(generatedHeader)) return source; + return source.replace(generatedHeader, `${generatedHeader}\n${normalizationNotice}`); +} + +function run(command, args) { + const result = spawnSync(command, args, { + stdio: "inherit", + shell: false, + }); + if (result.error) { + console.error(`Failed to run ${command} ${args.join(" ")}: ${result.error.message}`); + process.exit(1); + } + if (result.status !== 0) process.exit(result.status ?? 1); +} diff --git a/scripts/normalize-generated-types.mjs b/scripts/normalize-generated-types.mjs deleted file mode 100644 index 79728707..00000000 --- a/scripts/normalize-generated-types.mjs +++ /dev/null @@ -1,44 +0,0 @@ -import { readdir, readFile, writeFile } from "node:fs/promises"; -import path from "node:path"; - -const generatedDir = path.resolve("src/generated/app-server"); -const generatedHeader = "// GENERATED CODE! DO NOT MODIFY BY HAND!"; -const normalizationNotice = "// This file was mechanically normalized after generation by scripts/normalize-generated-types.mjs."; - -async function listTypeScriptFiles(dir) { - const entries = await readdir(dir, { withFileTypes: true }); - const files = await Promise.all( - entries.map((entry) => { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) return listTypeScriptFiles(fullPath); - return entry.isFile() && entry.name.endsWith(".ts") ? [fullPath] : []; - }), - ); - return files.flat(); -} - -function normalizeSource(source) { - let normalized = source; - do { - source = normalized; - normalized = source.replaceAll("| null | null", "| null"); - } while (normalized !== source); - return normalized; -} - -function addNormalizationNotice(source) { - if (source.includes(normalizationNotice)) return source; - if (!source.startsWith(generatedHeader)) return source; - return source.replace(generatedHeader, `${generatedHeader}\n${normalizationNotice}`); -} - -const files = await listTypeScriptFiles(generatedDir); - -await Promise.all( - files.map(async (file) => { - const source = await readFile(file, "utf8"); - const normalized = normalizeSource(source); - const nextSource = normalized === source ? source : addNormalizationNotice(normalized); - if (nextSource !== source) await writeFile(file, nextSource); - }), -); diff --git a/scripts/check-release.mjs b/scripts/release/check.mjs similarity index 98% rename from scripts/check-release.mjs rename to scripts/release/check.mjs index fd806033..e9d40199 100644 --- a/scripts/check-release.mjs +++ b/scripts/release/check.mjs @@ -1,6 +1,6 @@ import { readFile } from "node:fs/promises"; import path from "node:path"; -import { compareVersions, isExpectedNextVersion, parseVersion, readJson } from "./release-utils.mjs"; +import { compareVersions, isExpectedNextVersion, parseVersion, readJson } from "./utils.mjs"; function fail(message) { console.error(`release check failed: ${message}`); diff --git a/scripts/preflight-release.mjs b/scripts/release/preflight.mjs similarity index 100% rename from scripts/preflight-release.mjs rename to scripts/release/preflight.mjs diff --git a/scripts/prepare-release.mjs b/scripts/release/prepare.mjs similarity index 98% rename from scripts/prepare-release.mjs rename to scripts/release/prepare.mjs index cec2c590..b9f8edba 100644 --- a/scripts/prepare-release.mjs +++ b/scripts/release/prepare.mjs @@ -1,6 +1,6 @@ import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; -import { compareVersions, isExpectedNextVersion, parseVersion, readJson, writeJson } from "./release-utils.mjs"; +import { compareVersions, isExpectedNextVersion, parseVersion, readJson, writeJson } from "./utils.mjs"; function fail(message) { console.error(`release prepare failed: ${message}`); @@ -17,6 +17,8 @@ const packageJson = await readJson("package.json"); const packageLockJson = await readJson("package-lock.json"); const manifestJson = await readJson("manifest.json"); const versionsJson = await readJson("versions.json"); +const notesDir = path.join(".github", "release-notes"); +const notesPath = path.join(notesDir, `${releaseVersion}.md`); const versionKeys = Object.keys(versionsJson); const previousVersionKey = versionKeys.at(-1); @@ -29,6 +31,12 @@ if (!isExpectedNextVersion(previousVersion, currentVersion)) { fail(`release version ${releaseVersion} is not the expected next version after ${previousVersionKey}`); } if (versionsJson[releaseVersion] !== undefined) fail(`versions.json already contains ${releaseVersion}`); +try { + await readFile(notesPath, "utf8"); + fail(`${notesPath} already exists`); +} catch (error) { + if (error.code !== "ENOENT") throw error; +} packageJson.version = releaseVersion; packageLockJson.version = releaseVersion; @@ -42,15 +50,7 @@ await writeJson("package-lock.json", packageLockJson); await writeJson("manifest.json", manifestJson); await writeJson("versions.json", versionsJson); -const notesDir = path.join(".github", "release-notes"); -const notesPath = path.join(notesDir, `${releaseVersion}.md`); await mkdir(notesDir, { recursive: true }); -try { - await readFile(notesPath, "utf8"); - fail(`${notesPath} already exists`); -} catch (error) { - if (error.code !== "ENOENT") throw error; -} await writeFile(notesPath, "## Changes\n\n- \n"); console.log(`prepared release ${releaseVersion}`); diff --git a/scripts/release-utils.mjs b/scripts/release/utils.mjs similarity index 100% rename from scripts/release-utils.mjs rename to scripts/release/utils.mjs