/**
* Generate the dev-invariants scoped-event resolver map from the
* repository TypeScript Program.
*
* A scoped event declares `this: Scoped`. Real `scopeTarget(base, key)`
* calls establish the routing-key type for that base. The generator searches
* every event payload parameter and one property level for exactly one type
* equivalent to that key. Each generated resolver compiles against the merged
* `Events` parameter tuple. Zero matches require `@dshScopeScan unsupported`;
* multiple matches are ambiguous and always fail loud.
*
* `tsx scripts/gen-scoped-events.ts` -> write the generated source
* `tsx scripts/gen-scoped-events.ts --check` -> exit 1 when it is stale
*/
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
import { resolve } from 'node:path'
import ts from 'typescript'
import { pointer, rawJsDoc } from './jsdoc.ts'
import { TypeScriptProject } from './ts-project.ts'
const root = resolve(import.meta.dirname, '..')
const OUT = 'packages/support/invariants/src/scoped-events.generated.ts'
const SCOPE_DOC_MARKER = 'Scope-filtered dispatch'
interface ScopeTargetContract {
baseType: ts.Type
keyType: ts.Type
source: string
}
interface SubjectCandidate {
path: string
parameter: number
property?: string
type: ts.Type
}
interface ScopedEventResolver {
event: string
candidate: SubjectCandidate | null
ownerPackage: string
}
interface ScopeTag {
present: boolean
unsupported: boolean
}
/** Program-backed analyzer and renderer for the generated scoped-event resolvers. */
class ScopedEventGenerator {
private readonly checker: ts.TypeChecker
private readonly packageSources: ts.SourceFile[]
private readonly scopeTargetDeclaration: ts.FunctionDeclaration
private readonly scopedSymbol: ts.Symbol
private readonly violations: string[] = []
private readonly packageNames = new Map()
constructor(private readonly project: TypeScriptProject) {
this.checker = project.checker
this.packageSources = project.sourceFiles().filter((sourceFile) => {
return /^packages\/[^/]+\/[^/]+\/src\/.+\.ts$/.test(project.relativePath(sourceFile))
})
this.scopeTargetDeclaration = this.functionDeclaration(
'packages/core/scope/src/index.ts',
'scopeTarget',
)
this.scopedSymbol = this.typeAliasSymbol(
'packages/core/scope/src/index.ts',
'Scoped',
)
}
/** Render the complete generated TypeScript module or throw every contract violation. */
render(): string {
const contracts = this.collectScopeTargetContracts()
const resolvers = this.collectScopedEventResolvers(contracts)
if (this.violations.length > 0) {
throw new Error(
`gen-scoped-events: ${this.violations.length} scoped-event contract violation(s):\n`
+ this.violations.map(violation => ` - ${violation}`).join('\n'),
)
}
const ownerImports = [...new Set(resolvers.map(resolver => resolver.ownerPackage))]
.sort()
.map(packageName => `import type {} from ${quote(packageName)}`)
return [
'/**',
' * Generated scoped-event routing-subject resolvers for dsh-invariants.',
' * Do not edit by hand; run `pnpm run gen-scoped-events`.',
' *',
' * @module @deepseek-ai/dsh-invariants/scoped-events.generated',
' */',
'',
"import type { Events } from 'cordis'",
"import type { Scoped } from '@deepseek-ai/dsh-scope'",
...ownerImports,
'',
'type ScopedEventName = {',
' [K in keyof Events]: ThisParameterType extends Scoped