/* 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-ignore const invokeProfiles = new Set(['WEB', 'LOCAL', 'MAPPED_ELECTRON', 'MAPPED_NATIVE', 'DELEGATED_NATIVE']) const eventProfiles = new Set(['EVENT']) // prettier-ignore const 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 contract const 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}`) }