@atlaskit/adf-schema
Version:
Shared package that contains the ADF-schema (json) and ProseMirror node/mark specs
174 lines (164 loc) • 6.12 kB
text/typescript
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();