116 lines · 4.2 KB · JavaScript
RawDownload
/* eslint-disable @typescript-eslint/explicit-function-return-type */import { readFile, writeFile } from 'node:fs/promises'import { resolve } from 'node:path'import { format } from 'prettier'import ts from 'typescript' const root = resolve(import.meta.dirname, '..')const catalogPath = resolve(root, 'src/shared/renderer-contract-catalog.ts')const outputPath = resolve(root, 'src/shared/web-api-map.generated.ts') const sourceText = await readFile(catalogPath, 'utf8')const source = ts.createSourceFile(catalogPath, sourceText, ts.ScriptTarget.Latest, true) const stringLiteral = (node, label) => {  if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text  if (ts.isIdentifier(node)) return node.text  throw new Error(`Renderer contract ${label} must be a string literal.`)} // prettier-ignoreconst invokeProfiles = new Set(['WEB', 'LOCAL', 'MAPPED_ELECTRON', 'MAPPED_NATIVE', 'DELEGATED_NATIVE'])const eventProfiles = new Set(['EVENT'])// prettier-ignoreconst unprojectedProfiles = new Set(['ELECTRON', 'SEND', 'WINDOW_FIND_READY', 'ELECTRON_EVENT', 'CLOSE_PANE_EVENT', 'NATIVE']) const projectionFor = (node) => {  if (!node) return 'invoke'  if (!ts.isIdentifier(node)) throw new Error('Renderer contract profile must be an identifier.')  if (invokeProfiles.has(node.text)) return 'invoke'  if (eventProfiles.has(node.text)) return 'event'  if (unprojectedProfiles.has(node.text)) return 'none'  throw new Error(`Unknown renderer contract profile: ${node.text}`)} const invoke = {}const events = {} let contractconst findCatalog = (node) => {  const argument =    ts.isVariableDeclaration(node) &&    node.initializer &&    ts.isCallExpression(node.initializer) &&    node.initializer.arguments[0]      ? node.initializer.arguments[0]      : undefined  const value = argument && ts.isAsExpression(argument) ? argument.expression : argument  if (    ts.isVariableDeclaration(node) &&    ts.isIdentifier(node.name) &&    node.name.text === 'RENDERER_API_CONTRACT' &&    value &&    ts.isObjectLiteralExpression(value)  ) {    contract = value  }  ts.forEachChild(node, findCatalog)}findCatalog(source)if (!contract) throw new Error('Typed renderer contract was not found.') for (const property of contract.properties) {  if (!ts.isPropertyAssignment(property)) {    throw new Error('Renderer contract may only contain property assignments.')  }  const path = stringLiteral(property.name, 'public path')  const initializer = property.initializer  if (    !ts.isCallExpression(initializer) ||    !ts.isCallExpression(initializer.expression) ||    !ts.isIdentifier(initializer.expression.expression)  ) {    throw new Error(`Renderer contract entry must use a typed builder: ${path}`)  }  const builder = initializer.expression.expression.text  if (builder === 'value') continue  if (builder !== 'callable') throw new Error(`Unknown renderer contract builder: ${builder}`)  const metadata = initializer.arguments[1]  if (!metadata || !ts.isArrayLiteralExpression(metadata)) {    throw new Error(`Renderer contract metadata must be a tuple: ${path}`)  }  const projection = projectionFor(metadata.elements[1])  if (projection === 'none') continue  const channel = stringLiteral(metadata.elements[0], 'channel')  const target = projection === 'invoke' ? invoke : events  if (Object.hasOwn(target, path)) throw new Error(`Duplicate projected renderer path: ${path}`)  target[path] = channel} const sorted = (value) =>  Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)))const output = await format(  `// Generated by scripts/generate-web-api-map.mjs. Do not edit by hand.export const WEB_INVOKE_CHANNELS = ${JSON.stringify(sorted(invoke), null, 2)} as const export const WEB_EVENT_CHANNELS = ${JSON.stringify(sorted(events), null, 2)} as const`,  {    parser: 'typescript',    singleQuote: true,    semi: false,    printWidth: 100,    trailingComma: 'none'  }) if (process.argv.includes('--check')) {  const current = await readFile(outputPath, 'utf8').catch(() => '')  if (current !== output) {    console.error('Web API map is stale. Run npm run gen:web-api-map.')    process.exitCode = 1  }} else {  await writeFile(outputPath, output, 'utf8')  console.log(`Generated ${outputPath}`)}