/* 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}`)}
/* 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}`)
}