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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
12 changes: 12 additions & 0 deletions scripts/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"]
}
47 changes: 47 additions & 0 deletions scripts/validate-settings-json-runner.ts
Original file line number Diff line number Diff line change
@@ -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}`);
81 changes: 81 additions & 0 deletions scripts/validate-settings-json.mjs
Original file line number Diff line number Diff line change
@@ -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 -- <path-to-json>

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);
});