diff --git a/package.json b/package.json index 143b3e86..d39286b0 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,8 @@ "publish-appbuilder": "bash scripts/build-appbuilder.sh 1 v1/main", "publish-appbuilder-platform": "bash scripts/build-appbuilder.sh 1 app/builder/v1/main", "publish": "bash scripts/build-appbuilder.sh 1", - "generate:mantine-props-zod": "node scripts/generate-mantine-props-zod.mjs" + "generate:mantine-props-zod": "node scripts/generate-mantine-props-zod.mjs", + "validate:settings": "node scripts/validate-settings-json.mjs" }, "browserslist": { "production": [ diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json new file mode 100644 index 00000000..ccf76396 --- /dev/null +++ b/scripts/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["validate-settings-json-runner.ts"] +} diff --git a/scripts/validate-settings-json-runner.ts b/scripts/validate-settings-json-runner.ts new file mode 100644 index 00000000..b882fb7a --- /dev/null +++ b/scripts/validate-settings-json-runner.ts @@ -0,0 +1,47 @@ +/** + * Fast settings JSON validation (no Jest). Used by validate-settings-json.mjs. + */ +import {readFileSync} from "node:fs"; +import process from "node:process"; + +/** Minimal surface from appbuildertypecheck.ts (dynamic import). */ +interface AppBuilderTypecheckModule { + validateAppBuilderSettingsJson: ( + value: unknown, + ) => {success: true; data: unknown} | {success: false; error: unknown}; + formatAppBuilderZodError: (error: unknown) => string; +} + +const file = process.env.VALIDATE_SETTINGS_FILE; +if (!file) { + console.error( + "validate-settings-json-runner: VALIDATE_SETTINGS_FILE not set", + ); + process.exit(1); +} + +const {formatAppBuilderZodError, validateAppBuilderSettingsJson} = + (await import( + import.meta + .resolve("../src/shared/features/appbuilder/config/appbuildertypecheck.ts") + )) as AppBuilderTypecheckModule; + +let json: unknown; +try { + json = JSON.parse(readFileSync(file, "utf8")); +} catch (error) { + console.error( + `Invalid JSON in ${file}: ${error instanceof Error ? error.message : String(error)}`, + ); + process.exit(1); +} + +const result = validateAppBuilderSettingsJson(json); +if (!result.success) { + console.error( + `Settings JSON invalid (${file}):\n${formatAppBuilderZodError(result.error)}`, + ); + process.exit(1); +} + +console.log(`Settings JSON valid: ${file}`); diff --git a/scripts/validate-settings-json.mjs b/scripts/validate-settings-json.mjs new file mode 100644 index 00000000..c0744fb1 --- /dev/null +++ b/scripts/validate-settings-json.mjs @@ -0,0 +1,81 @@ +#!/usr/bin/env node +/** + * Validate an App Builder settings JSON file (agent / CLI). + * + * Usage (from repo root): + * pnpm run validate:settings -- public/my-config.json + * node scripts/validate-settings-json.mjs public/my-config.json + * + * Fast path (~5–15s): tsx runner, no Jest. Times out after 30s. + * Exits 0 when valid, 1 when invalid, missing file, or timeout. + */ + +import {spawn} from "node:child_process"; +import {existsSync} from "node:fs"; +import {createRequire} from "node:module"; +import {dirname, join, resolve} from "node:path"; +import process from "node:process"; +import {fileURLToPath} from "node:url"; + +const repoRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); +const RUNNER = join(repoRoot, "scripts/validate-settings-json-runner.ts"); +const TIMEOUT_MS = 30_000; + +const require = createRequire(join(repoRoot, "package.json")); +const tsxCli = join( + dirname(require.resolve("tsx/package.json")), + "dist/cli.mjs", +); + +function printUsage() { + console.error(`Usage: pnpm run validate:settings -- + +Examples: + pnpm run validate:settings -- public/my-config.json + pnpm run validate:settings -- public/chair-brand.json + +Path is relative to the repo root or absolute. + +Do NOT run full pnpm test for single-file checks — use this command only. +Expected runtime: under 30 seconds.`); +} + +const arg = process.argv.slice(2).find((a) => a !== "--"); + +if (!arg || arg === "--help" || arg === "-h") { + printUsage(); + process.exit(arg ? 0 : 1); +} + +const absPath = resolve(repoRoot, arg); + +if (!existsSync(absPath)) { + console.error(`validate-settings-json: file not found: ${absPath}`); + process.exit(1); +} + +const childEnv = {...process.env, VALIDATE_SETTINGS_FILE: absPath}; +const child = spawn(process.execPath, [tsxCli, RUNNER], { + cwd: repoRoot, + env: childEnv, + stdio: "inherit", +}); + +const timer = setTimeout(() => { + console.error( + `\nvalidate-settings-json: timed out after ${TIMEOUT_MS / 1000}s — aborting.`, + ); + child.kill("SIGTERM"); + process.exit(1); +}, TIMEOUT_MS); + +child.on("close", (code) => { + clearTimeout(timer); + process.exit(code === 0 ? 0 : 1); +}); + +child.on("error", (error) => { + clearTimeout(timer); + console.error(`validate-settings-json: failed to start: ${error.message}`); + process.exit(1); +});