UNPKG

@atlaskit/adf-schema

Version:

Shared package that contains the ADF-schema (json) and ProseMirror node/mark specs

174 lines (164 loc) 6.12 kB
import path from 'path'; import { adfToValidatorSpec } from '@atlaskit/adf-schema-generator'; import adfNode from '../src/next-schema/full-schema.adf'; import { writeToFile } from './helpers/writeToFile'; const VALID_JS_IDENTIFIER_REGEX = /^[a-zA-Z_$][\w$]*$/u; const outputPath = path.join('src', 'validator-schema', 'generated'); const indent = (level: number) => '\n' + ' '.repeat(level); /** * Emit TypeScript object/array literals. * * Arrays are intentionally NOT marked `as const`. With `as const`, TypeScript * infers `readonly` tuple types for every array, which then fail to satisfy * the `Array<string>` (mutable) shape used in the consumer's `ValidatorSpec` * type — this surfaced as a TS2352 on `as Record<string, ValidatorSpec>` in * `ai-model-io/.../specs/index.ts`. * * Plain object/array literals (without `as const`) are also fully derivable * by `--isolatedDeclarations`, so dropping `as const` is safe for both the * adf-schema build and any downstream consumer. */ function serializeValidatorJson(value: unknown, depth = 0): string { if (value === null) { return 'null'; } if (value === undefined) { return 'undefined'; } if (typeof value === 'string') { return JSON.stringify(value); } if (typeof value === 'number' || typeof value === 'boolean') { return String(value); } if (Array.isArray(value)) { if (value.length === 0) { return '[]'; } const items = value.filter((item) => item !== undefined); if (items.length === 0) { return '[]'; } const inner = items .map((item) => serializeValidatorJson(item, depth + 1)) .join(`,${indent(depth + 1)}`); return `[${indent(depth + 1)}${inner}${indent(depth)}]`; } if (typeof value === 'object') { const entries = Object.entries(value as Record<string, unknown>).sort(([a], [b]) => a.localeCompare(b), ); if (entries.length === 0) { return '{}'; } const inner = entries .map(([k, v]) => { const keyName = VALID_JS_IDENTIFIER_REGEX.test(k) ? k : JSON.stringify(k); return `${keyName}: ${serializeValidatorJson(v, depth + 1)}`; }) .join(`,${indent(depth + 1)}`); return `{${indent(depth + 1)}${inner}${indent(depth)}}`; } throw new Error(`Unsupported value in validator spec: ${typeof value}`); } function isPlainObject(value: unknown): value is Record<string, unknown> { return typeof value === 'object' && value !== null && !Array.isArray(value); } /** * Emit the TypeScript type annotation for a value, widening all primitive * literals to their base types (e.g. `'enum'` → `string`, `100` → `number`). * * This produces an explicit type annotation on each generated `export const` * so that: * 1. Running the codegen never silently removes type information. * 2. Consumers that use `--isolatedDeclarations` get an explicit declaration * rather than relying on type inference from a deeply-nested literal. * * For arrays whose elements all share the same structural type, the result is * `T[]`. For heterogeneous arrays (e.g. an extends tuple like * `[string, ValidatorSpecNode]` or an anyOf attrs list), the result is * `(T1 | T2 | ...)[]`, deduplicating structurally identical element types. * * The `siblings` argument carries parallel values from sibling array branches * at the current path — used to produce a discriminated-union style annotation * for object branches that have non-overlapping key sets. When an object * branch is missing a key that another sibling branch has, the missing key is * emitted as `key?: undefined`. This matches the TypeScript pattern used to * make `anyOf`-style attribute unions mutually exclusive (e.g. `media`'s * `file` vs `link` attrs, `blockCard`'s three attribute variants). */ function serializeValidatorType(value: unknown, depth = 0, siblings: unknown[] = []): string { if (value === null) { return 'null'; } if (value === undefined) { return 'undefined'; } if (typeof value === 'string') { return 'string'; } if (typeof value === 'number') { return 'number'; } if (typeof value === 'boolean') { return 'boolean'; } if (Array.isArray(value)) { if (value.length === 0) { return 'unknown[]'; } const items = value.filter((item) => item !== undefined); if (items.length === 0) { return 'unknown[]'; } const elementTypes = items.map((item, i) => { const otherElements = items.filter((_, j) => j !== i); return serializeValidatorType(item, depth + 1, otherElements); }); const uniqueTypes = [...new Set(elementTypes)]; const elementType = uniqueTypes.length === 1 ? uniqueTypes[0] : `(${uniqueTypes.join(' | ')})`; return `${elementType}[]`; } if (typeof value === 'object') { const selfRecord = value as Record<string, unknown>; const siblingObjects = siblings.filter(isPlainObject); const ownKeys = Object.keys(selfRecord); const allKeys = new Set<string>(ownKeys); for (const sibling of siblingObjects) { for (const key of Object.keys(sibling)) { allKeys.add(key); } } if (allKeys.size === 0) { return '{}'; } const sortedKeys = [...allKeys].sort((a, b) => a.localeCompare(b)); const inner = sortedKeys .map((k) => { const keyName = VALID_JS_IDENTIFIER_REGEX.test(k) ? k : JSON.stringify(k); if (k in selfRecord) { const siblingsForK = siblingObjects.filter((s) => k in s).map((s) => s[k]); return `${keyName}: ${serializeValidatorType(selfRecord[k], depth + 1, siblingsForK)}`; } return `${keyName}?: undefined`; }) .join(`;${indent(depth + 1)}`); return `{${indent(depth + 1)}${inner};${indent(depth)}}`; } throw new Error(`Unsupported value in validator type: ${typeof value}`); } function generateValidatorSpec() { const output = adfToValidatorSpec(adfNode); return Object.entries(output) .map(([key, entry]) => { const { json } = entry as { json: unknown }; const type = serializeValidatorType(json); return `export const ${key}: ${type} = ${serializeValidatorJson(json)};`; }) .join('\n\n'); } function main() { const spec = generateValidatorSpec(); writeToFile('validatorSpec.ts', spec, outputPath); } main();