#!/usr/bin/env node import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import Ajv2020 from 'ajv/dist/2020.js'; import standaloneCode from 'ajv/dist/standalone/index.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const root = path.resolve(__dirname, '..'); const schemasDir = path.join(root, 'schemas'); const output = path.join(root, 'renderers/shared/generated-validators.mjs'); const diagramTypes = ['workflow', 'sequence', 'dataflow', 'lifecycle', 'architecture']; const ajv = new Ajv2020({ allErrors: true, strict: true, code: { source: true, esm: true }, }); ajv.addSchema(JSON.parse(fs.readFileSync(path.join(schemasDir, 'common.schema.json'), 'utf8'))); const schemaIds = {}; for (const type of diagramTypes) { const schema = JSON.parse(fs.readFileSync(path.join(schemasDir, `${type}.schema.json`), 'utf8')); ajv.addSchema(schema); schemaIds[type] = schema.$id; } const banner = '// Generated by scripts/generate-validators.mjs. Do not edit by hand.\n'; const ajvUcs2Import = 'require("ajv/dist/runtime/ucs2length").default'; const inlineUcs2Length = `function ucs2length(str) { const len = str.length; let length = 0; let pos = 0; while (pos < len) { length += 1; const value = str.charCodeAt(pos++); if (value >= 0xd800 && value <= 0xdbff && pos < len && (str.charCodeAt(pos) & 0xfc00) === 0xdc00) pos += 1; } return length; }`; let validatorCode = standaloneCode(ajv, schemaIds); if (!validatorCode.includes(ajvUcs2Import)) { throw new Error('AJV standalone output no longer contains the expected ucs2length helper'); } validatorCode = validatorCode.replaceAll(ajvUcs2Import, inlineUcs2Length); if (validatorCode.includes('require(')) { throw new Error('AJV standalone output contains an unexpected runtime dependency'); } for (const type of diagramTypes) { const exportPattern = new RegExp(`export const ${type} = (validate\\d+);`); const match = validatorCode.match(exportPattern); if (!match) throw new Error(`AJV standalone output no longer exports the ${type} validator as expected`); validatorCode = validatorCode.replace(exportPattern, `const ${type}Schema = ${match[1]};`); } const portableOutputWrappers = diagramTypes.map((type) => `export function ${type}(data, context = undefined) { if (!${type}Schema(data, context)) { ${type}.errors = ${type}Schema.errors; return false; } const output = data?.meta?.output; if (typeof output === 'string') { try { validatePortablePath(output, { profile: 'output' }); } catch (error) { ${type}.errors = [{ instancePath: \`${'${context?.instancePath || \'\'}'}/meta/output\`, schemaPath: 'common.schema.json#/$defs/portableOutputPath', keyword: 'portablePath', params: { reason: error?.reason || 'invalid' }, message: 'must satisfy the portable output path contract', }]; return false; } } ${type}.errors = null; return true; } ${type}.evaluated = ${type}Schema.evaluated;`).join('\n'); const generated = `${banner}import { validatePortablePath } from './portable-path.mjs';\n${validatorCode}\n${portableOutputWrappers}\n`; if (process.argv.includes('--check')) { const current = fs.existsSync(output) ? fs.readFileSync(output, 'utf8').replace(/\r\n?/g, '\n') : ''; if (current !== generated) { console.error('generated validators are stale — run npm run generate:validators'); process.exit(1); } } else { const temporary = `${output}.${process.pid}.tmp`; fs.writeFileSync(temporary, generated); fs.renameSync(temporary, output); console.log(`generated ${path.relative(root, output)}`); }