|
| 1 | +/** |
| 2 | + * Generates TypeScript types from OGC TMS 2.0 JSON Schema files. |
| 3 | + * |
| 4 | + * Each schema is compiled independently with `declareExternallyReferenced: false` |
| 5 | + * so that $ref'd types appear as bare names. We then prepend the correct import |
| 6 | + * statements based on the known dependency graph. |
| 7 | + * |
| 8 | + * Only the schemas reachable from the four public exports (BoundingBox, CRS, |
| 9 | + * TileMatrix, TileMatrixSet) are generated. The projJSON.json schema (~1000 |
| 10 | + * lines of CRS subtypes) is replaced with an opaque `ProjJSON` type alias. |
| 11 | + */ |
| 12 | + |
| 13 | +import { mkdirSync, writeFileSync } from "node:fs"; |
| 14 | +import { dirname, resolve } from "node:path"; |
| 15 | +import { fileURLToPath } from "node:url"; |
| 16 | +import { compileFromFile } from "json-schema-to-typescript"; |
| 17 | + |
| 18 | +const __dirname = dirname(fileURLToPath(import.meta.url)); |
| 19 | +const ROOT = resolve(__dirname, ".."); |
| 20 | +const SCHEMA_DIR = resolve(ROOT, "spec/schemas/tms/2.0/json"); |
| 21 | +const OUT_DIR = resolve(ROOT, "src/types/spec"); |
| 22 | + |
| 23 | +const BANNER = `/* This file was automatically generated from OGC TMS 2.0 JSON Schema. */ |
| 24 | +/* DO NOT MODIFY IT BY HAND. Instead, modify the source JSON Schema file */ |
| 25 | +/* and run \`pnpm run generate-types\` to regenerate. */`; |
| 26 | + |
| 27 | +/** |
| 28 | + * Map from schema filename (without .json) to the type names it exports. |
| 29 | + * Only schemas in the transitive closure of the four public types are listed. |
| 30 | + */ |
| 31 | +const SCHEMA_EXPORTS: Record<string, string[]> = { |
| 32 | + "2DPoint": ["DPoint"], |
| 33 | + "2DBoundingBox": ["DBoundingBox"], |
| 34 | + crs: ["CRS"], |
| 35 | + variableMatrixWidth: ["VariableMatrixWidth"], |
| 36 | + tileMatrix: ["TileMatrix"], |
| 37 | + tileMatrixSet: ["TileMatrixSetDefinition"], |
| 38 | +}; |
| 39 | + |
| 40 | +/** |
| 41 | + * Map from schema filename to the schema files it $ref's. |
| 42 | + * projJSON is excluded — handled separately as an opaque type. |
| 43 | + */ |
| 44 | +const SCHEMA_DEPS: Record<string, string[]> = { |
| 45 | + "2DPoint": [], |
| 46 | + "2DBoundingBox": ["2DPoint", "crs"], |
| 47 | + crs: [], |
| 48 | + variableMatrixWidth: [], |
| 49 | + tileMatrix: ["2DPoint", "variableMatrixWidth"], |
| 50 | + tileMatrixSet: ["crs", "2DBoundingBox", "tileMatrix"], |
| 51 | +}; |
| 52 | + |
| 53 | +/** Build an import block, only including types that appear in the generated code. */ |
| 54 | +function buildImports(schemaName: string, generatedCode: string): string { |
| 55 | + const deps = SCHEMA_DEPS[schemaName]; |
| 56 | + if (!deps || deps.length === 0) return ""; |
| 57 | + |
| 58 | + const lines: string[] = []; |
| 59 | + for (const dep of deps) { |
| 60 | + const types = SCHEMA_EXPORTS[dep]; |
| 61 | + if (!types) throw new Error(`Unknown schema dependency: ${dep}`); |
| 62 | + // Only import types that are actually referenced in the generated output |
| 63 | + const usedTypes = types.filter((t) => |
| 64 | + new RegExp(`\\b${t}\\b`).test(generatedCode), |
| 65 | + ); |
| 66 | + if (usedTypes.length > 0) { |
| 67 | + lines.push(`import type { ${usedTypes.join(", ")} } from "./${dep}.js";`); |
| 68 | + } |
| 69 | + } |
| 70 | + |
| 71 | + return lines.length > 0 ? `${lines.join("\n")}\n\n` : ""; |
| 72 | +} |
| 73 | + |
| 74 | +/** Compile a single schema file, returning the generated TypeScript. */ |
| 75 | +async function compileSchema(schemaName: string): Promise<string> { |
| 76 | + const ts = await compileFromFile(`${SCHEMA_DIR}/${schemaName}.json`, { |
| 77 | + cwd: SCHEMA_DIR, |
| 78 | + declareExternallyReferenced: false, |
| 79 | + format: false, |
| 80 | + bannerComment: "", |
| 81 | + $refOptions: { |
| 82 | + resolve: { |
| 83 | + projjson: { |
| 84 | + order: 1, |
| 85 | + canRead: /projJSON/, |
| 86 | + read: () => JSON.stringify({ type: "object", title: "ProjJSON" }), |
| 87 | + }, |
| 88 | + }, |
| 89 | + }, |
| 90 | + }); |
| 91 | + |
| 92 | + // The OGC schemas attach descriptions to $ref properties via |
| 93 | + // allOf: [{ description }, { $ref }]. json-schema-to-typescript renders |
| 94 | + // the description-only object as { [k: string]: unknown } and intersects |
| 95 | + // it with the $ref'd type. That intersection makes the type incompatible |
| 96 | + // with plain JSON imports. Strip it — only the $ref'd type name matters. |
| 97 | + return ts.replace(/\(\{\s*\[k: string\]: unknown\s*\} & (\w+)\)/g, "$1"); |
| 98 | +} |
| 99 | + |
| 100 | +async function main() { |
| 101 | + mkdirSync(OUT_DIR, { recursive: true }); |
| 102 | + |
| 103 | + // Generate the opaque ProjJSON type file |
| 104 | + const projJsonContent = [ |
| 105 | + BANNER, |
| 106 | + "", |
| 107 | + "/** Opaque type for PROJ JSON coordinate reference system definitions. */", |
| 108 | + "export type ProjJSON = Record<string, unknown>;", |
| 109 | + "", |
| 110 | + ].join("\n"); |
| 111 | + writeFileSync(resolve(OUT_DIR, "projJSON.ts"), projJsonContent); |
| 112 | + |
| 113 | + const schemaNames = Object.keys(SCHEMA_EXPORTS); |
| 114 | + |
| 115 | + for (const name of schemaNames) { |
| 116 | + const rawTs = await compileSchema(name); |
| 117 | + const imports = buildImports(name, rawTs); |
| 118 | + |
| 119 | + // crs.ts needs the ProjJSON import since we stubbed out the schema |
| 120 | + const projImport = |
| 121 | + name === "crs" |
| 122 | + ? 'import type { ProjJSON } from "./projJSON.js";\n\n' |
| 123 | + : ""; |
| 124 | + |
| 125 | + const content = [BANNER, "", projImport + imports + rawTs].join("\n"); |
| 126 | + const outPath = resolve(OUT_DIR, `${name}.ts`); |
| 127 | + writeFileSync(outPath, content); |
| 128 | + console.log(` ${name}.ts`); |
| 129 | + } |
| 130 | + |
| 131 | + // Generate barrel index.ts |
| 132 | + const reexports = ["projJSON", ...schemaNames] |
| 133 | + .map((name) => `export * from "./${name}.js";`) |
| 134 | + .join("\n"); |
| 135 | + const indexContent = [BANNER, "", reexports, ""].join("\n"); |
| 136 | + writeFileSync(resolve(OUT_DIR, "index.ts"), indexContent); |
| 137 | + console.log(" index.ts"); |
| 138 | + |
| 139 | + console.log(`\nGenerated ${schemaNames.length + 2} files in src/types/spec/`); |
| 140 | +} |
| 141 | + |
| 142 | +main().catch((err) => { |
| 143 | + console.error(err); |
| 144 | + process.exit(1); |
| 145 | +}); |
0 commit comments