persistence-schema.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. /** Extract complete persistent Session record types from one source-only compiler program. */
  2. import { readFileSync } from 'node:fs'
  3. import { dirname, extname, relative, resolve, sep } from 'node:path'
  4. import ts from 'typescript'
  5. import { collectLogEvents } from './persistence-catalog-source.ts'
  6. import {
  7. canonicalizeSchema,
  8. schemaChildren,
  9. schemaDigest,
  10. type PersistenceRoot,
  11. type PersistenceSchemaInventory,
  12. type PersistenceType,
  13. type SchemaNode,
  14. type SchemaProperty,
  15. } from './persistence-schema-model.ts'
  16. interface DeclarationMetadata {
  17. readonly names: Set<string>
  18. readonly sources: Set<string>
  19. }
  20. interface RootInput {
  21. readonly key: string
  22. readonly kind: PersistenceRoot['kind']
  23. readonly event?: string
  24. readonly surface?: boolean
  25. readonly node: number
  26. }
  27. /** A reachable TypeScript type that cannot be represented as a persisted JSON type. */
  28. export class PersistenceSchemaError extends Error {
  29. override name = 'PersistenceSchemaError'
  30. }
  31. /**
  32. * Extract every repository event, its complete envelope, and the Session header.
  33. * @param root - repository root, with source paths in tsconfig.host.json.
  34. * @returns canonical root fingerprints and all reachable type definitions.
  35. */
  36. export function extractPersistenceSchema(root: string): PersistenceSchemaInventory {
  37. root = resolve(root)
  38. const events = collectLogEvents(root).sort((left, right) => compare(left.name, right.name))
  39. if (events.length === 0) throw new PersistenceSchemaError('persistence schema: no Session events were discovered')
  40. const filename = resolve(root, 'scripts/__persistence_schema_roots__.ts')
  41. const source = [
  42. "import type { SessionHeader, SessionEvent, SessionEventMap, SurfaceEventType } from '@deepseek-ai/dsh-session/types'",
  43. 'export type HeaderRoot = SessionHeader',
  44. 'export type SurfaceRoot = SurfaceEventType',
  45. 'export type EventNamesRoot = keyof SessionEventMap',
  46. ...events.map((event, index) => `export type EventRoot${String(index)} = SessionEvent<${JSON.stringify(event.name)}>`),
  47. '',
  48. ].join('\n')
  49. const configPath = resolve(root, 'tsconfig.host.json')
  50. const config = ts.readConfigFile(configPath, file => readFileSync(file, 'utf8'))
  51. if (config.error !== undefined) throw new PersistenceSchemaError(diagnosticText(config.error))
  52. const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, root)
  53. const configErrors = parsed.errors.filter(error => error.code !== 18003)
  54. if (configErrors.length > 0) throw new PersistenceSchemaError(configErrors.map(diagnosticText).join('\n'))
  55. const options: ts.CompilerOptions = {
  56. ...parsed.options,
  57. composite: false,
  58. incremental: false,
  59. noEmit: true,
  60. rootDir: root,
  61. noUnusedLocals: false,
  62. noUnusedParameters: false,
  63. // Reachable declaration-file errors must not become opaque any values.
  64. skipLibCheck: false,
  65. strict: true,
  66. exactOptionalPropertyTypes: true,
  67. }
  68. const host = ts.createCompilerHost(options)
  69. const originalGetSourceFile = host.getSourceFile.bind(host)
  70. host.getSourceFile = (path, languageVersion, onError, shouldCreateNewSourceFile) =>
  71. resolve(path) === filename
  72. ? ts.createSourceFile(filename, source, languageVersion, true)
  73. : originalGetSourceFile(path, languageVersion, onError, shouldCreateNewSourceFile)
  74. const files = [...new Set([
  75. ...hostSourceFiles(root, configPath),
  76. ...events.map(event => resolve(root, event.source.slice(0, event.source.lastIndexOf(':')))),
  77. ])]
  78. const program = ts.createProgram({ rootNames: [filename, ...files], options, host })
  79. const rootSource = program.getSourceFile(filename)
  80. if (rootSource === undefined) throw new PersistenceSchemaError('persistence schema: compiler omitted the requested roots')
  81. const diagnostics = [
  82. ...program.getOptionsDiagnostics(), ...program.getSyntacticDiagnostics(), ...program.getSemanticDiagnostics(rootSource),
  83. ]
  84. if (diagnostics.length > 0) throw new PersistenceSchemaError(diagnostics.map(diagnosticText).join('\n'))
  85. const checker = program.getTypeChecker()
  86. const declarations = new Map(rootSource.statements.filter(ts.isTypeAliasDeclaration)
  87. .map(declaration => [declaration.name.text, declaration]))
  88. const declaration = (name: string): ts.TypeAliasDeclaration => {
  89. const found = declarations.get(name)
  90. if (found === undefined) throw new PersistenceSchemaError(`persistence schema: missing compiler root ${name}`)
  91. return found
  92. }
  93. const eventNamesDeclaration = declaration('EventNamesRoot')
  94. const compiledEvents = stringLiterals(checker.getTypeFromTypeNode(eventNamesDeclaration.type), 'keyof SessionEventMap')
  95. const discoveredEvents = new Set(events.map(event => event.name))
  96. const omitted = [...compiledEvents].filter(name => !discoveredEvents.has(name))
  97. const uncompiled = [...discoveredEvents].filter(name => !compiledEvents.has(name))
  98. if (omitted.length > 0 || uncompiled.length > 0) {
  99. throw new PersistenceSchemaError(`persistence schema: compiler and source event discovery disagree; omitted: ${omitted.join(', ')}; uncompiled: ${uncompiled.join(', ')}`)
  100. }
  101. const surfaceDeclaration = declaration('SurfaceRoot')
  102. const surface = stringLiterals(checker.getTypeFromTypeNode(surfaceDeclaration.type), 'SurfaceEventType')
  103. for (const name of surface) {
  104. if (!events.some(event => event.name === name)) throw new PersistenceSchemaError(`persistence schema: surface event ${name} has no declaration`)
  105. }
  106. const header = declaration('HeaderRoot')
  107. const physicalFile = resolve(root, 'packages/session/session-persistence-jsonl/src/format.ts')
  108. const physical = program.getSourceFile(physicalFile)?.statements
  109. .filter((node): node is ts.InterfaceDeclaration | ts.TypeAliasDeclaration =>
  110. ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node))
  111. .filter(declaration => declaration.name.text === 'HeaderLine')
  112. if (physical?.length !== 1) throw new PersistenceSchemaError('persistence schema: expected one current JSONL HeaderLine declaration')
  113. const physicalHeader = physical[0] as ts.InterfaceDeclaration | ts.TypeAliasDeclaration
  114. const declarationSources = validateReachableDeclarations(program, root, [...declarations.values(), physicalHeader])
  115. const extraction = new SchemaExtractor(root, program, declarationSources)
  116. const roots: RootInput[] = [{
  117. key: 'SessionHeader',
  118. kind: 'header',
  119. node: extraction.convert(checker.getTypeFromTypeNode(header.type), header),
  120. }]
  121. roots.push({ key: 'JsonlHeaderLine', kind: 'header', node: extraction.convert(checker.getTypeAtLocation(physicalHeader), physicalHeader) })
  122. const envelopes: number[] = []
  123. for (const [index, event] of events.entries()) {
  124. const item = declaration(`EventRoot${String(index)}`)
  125. const node = extraction.convert(checker.getTypeFromTypeNode(item.type), item)
  126. roots.push({ key: `event:${event.name}`, kind: 'event', event: event.name, surface: surface.has(event.name), node })
  127. envelopes.push(extraction.envelope(node, event.name))
  128. }
  129. roots.splice(2, 0, { key: 'SessionEventEnvelope', kind: 'envelope', node: extraction.union(envelopes) })
  130. return extraction.inventory(roots)
  131. }
  132. function hostSourceFiles(root: string, configPath: string, seen = new Set<string>()): string[] {
  133. configPath = resolve(configPath)
  134. if (seen.has(configPath)) return []
  135. seen.add(configPath)
  136. const read = ts.readConfigFile(configPath, file => readFileSync(file, 'utf8'))
  137. if (read.error !== undefined) throw new PersistenceSchemaError(diagnosticText(read.error))
  138. const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, dirname(configPath), undefined, configPath)
  139. const errors = parsed.errors.filter(error => error.code !== 18003)
  140. if (errors.length > 0) throw new PersistenceSchemaError(errors.map(diagnosticText).join('\n'))
  141. return [
  142. ...parsed.fileNames.filter(file => /^packages\/[^/]+\/[^/]+\/src\//.test(slash(relative(root, file)))),
  143. ...(parsed.projectReferences ?? []).flatMap(reference =>
  144. hostSourceFiles(root, extname(reference.path) === '.json' ? reference.path : resolve(reference.path, 'tsconfig.json'), seen)),
  145. ]
  146. }
  147. function validateReachableDeclarations(
  148. program: ts.Program,
  149. root: string,
  150. roots: readonly ts.Node[],
  151. ): ReadonlyMap<ts.Type, readonly ts.Node[]> {
  152. const checker = program.getTypeChecker()
  153. const visited = new Set<ts.Node>()
  154. const declarations = new Map<ts.SourceFile, ts.Node[]>()
  155. const definitions = new Map<ts.Type, ts.Node[]>()
  156. const visit = (node: ts.Node): void => {
  157. if (visited.has(node)) return
  158. visited.add(node)
  159. const file = node.getSourceFile()
  160. if (trackedSource(slash(relative(root, file.fileName)))) {
  161. const scopes = declarations.get(file) ?? []
  162. scopes.push(node)
  163. declarations.set(file, scopes)
  164. if (isNamedTypeDeclaration(node) || ts.isTypeLiteralNode(node)) {
  165. // Primitive aliases share checker types with unaliased properties.
  166. const type = checker.getTypeAtLocation(node)
  167. const entries = definitions.get(type) ?? []
  168. entries.push(node)
  169. definitions.set(type, entries)
  170. }
  171. }
  172. const target = ts.isTypeReferenceNode(node) ? node.typeName
  173. : ts.isExpressionWithTypeArguments(node) ? node.expression
  174. : ts.isTypeQueryNode(node) ? node.exprName
  175. : ts.isImportTypeNode(node) ? node.qualifier
  176. : undefined
  177. if (target !== undefined) {
  178. let symbol = checker.getSymbolAtLocation(target)
  179. if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias)) symbol = checker.getAliasedSymbol(symbol)
  180. for (const declaration of symbol?.declarations ?? []) {
  181. if (trackedSource(slash(relative(root, declaration.getSourceFile().fileName)))) visit(declaration)
  182. }
  183. }
  184. if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) return
  185. ts.forEachChild(node, visit)
  186. }
  187. for (const root of roots) visit(root)
  188. const errors = [...declarations].flatMap(([file, scopes]) => program.getSemanticDiagnostics(file)
  189. .filter((error) => {
  190. const start = error.start
  191. return start !== undefined && scopes.some(scope => start >= scope.getStart() && start < scope.end)
  192. }))
  193. if (errors.length > 0) throw new PersistenceSchemaError(errors.map(diagnosticText).join('\n'))
  194. return definitions
  195. }
  196. class SchemaExtractor {
  197. readonly nodes: SchemaNode[] = []
  198. private readonly cache = new Map<ts.Type, number>()
  199. private readonly declarationMetadata = new Map<number, DeclarationMetadata>()
  200. private readonly checker: ts.TypeChecker
  201. constructor(
  202. private readonly root: string,
  203. program: ts.Program,
  204. private readonly declarationSources: ReadonlyMap<ts.Type, readonly ts.Node[]>,
  205. ) {
  206. this.checker = program.getTypeChecker()
  207. }
  208. convert(type: ts.Type, site: ts.Node): number {
  209. const cached = this.cache.get(type)
  210. if (cached !== undefined) {
  211. this.record(cached, type)
  212. return cached
  213. }
  214. const id = this.nodes.length
  215. this.nodes.push({ kind: 'primitive', type: 'never' })
  216. this.cache.set(type, id)
  217. this.record(id, type)
  218. const add = (node: SchemaNode): number => { this.nodes[id] = node; return id }
  219. const flags = type.flags
  220. if (flags & ts.TypeFlags.Any) {
  221. if ((type as ts.Type & { intrinsicName?: string }).intrinsicName === 'error') this.fail(type, site, 'unresolved compiler type')
  222. return add({ kind: 'opaque', reason: 'any' })
  223. }
  224. if (flags & ts.TypeFlags.Unknown) return add({ kind: 'opaque', reason: 'unknown' })
  225. if (flags & ts.TypeFlags.Never) return add({ kind: 'primitive', type: 'never' })
  226. if (flags & ts.TypeFlags.Null) return add({ kind: 'primitive', type: 'null' })
  227. if (flags & ts.TypeFlags.String) return add({ kind: 'primitive', type: 'string' })
  228. if (flags & ts.TypeFlags.Number) return add({ kind: 'primitive', type: 'number' })
  229. if (flags & ts.TypeFlags.Boolean) return add({ kind: 'primitive', type: 'boolean' })
  230. if (flags & ts.TypeFlags.StringLiteral) return add({ kind: 'literal', value: (type as ts.StringLiteralType).value })
  231. if (flags & ts.TypeFlags.NumberLiteral) return add({ kind: 'literal', value: (type as ts.NumberLiteralType).value })
  232. if (flags & ts.TypeFlags.BooleanLiteral) return add({ kind: 'literal', value: this.checker.typeToString(type) === 'true' })
  233. if (type.isUnion()) return add(this.unionNode(type.types.map(member => this.convert(member, site))))
  234. if (type.isIntersection()) {
  235. const material = type.types.filter(member => !this.phantom(member))
  236. for (const member of material) this.rejectClass(member, site)
  237. if (material.length === 1) {
  238. const target = this.convert(material[0] as ts.Type, site)
  239. return add({ kind: 'union', types: [target] })
  240. }
  241. if (material.length === 0 || material.some(member => (member.flags & ts.TypeFlags.Object) === 0)) {
  242. this.fail(type, site, 'unsupported material intersection')
  243. }
  244. return add(this.object(type, site))
  245. }
  246. if (this.checker.isTupleType(type)) {
  247. const reference = type as ts.TypeReference
  248. const target = reference.target as ts.TupleType
  249. return add({
  250. kind: 'tuple',
  251. elements: this.checker.getTypeArguments(reference).map((element, index) => {
  252. const flags = target.elementFlags[index] ?? ts.ElementFlags.Required
  253. const optional = (flags & ts.ElementFlags.Optional) !== 0
  254. const rest = (flags & (ts.ElementFlags.Rest | ts.ElementFlags.Variadic)) !== 0
  255. return { type: this.valueType(element, site, optional), optional, rest }
  256. }),
  257. })
  258. }
  259. if (this.checker.isArrayType(type) || this.checker.isArrayLikeType(type)) {
  260. const element = this.checker.getIndexTypeOfType(type, ts.IndexKind.Number)
  261. if (element === undefined) this.fail(type, site, 'array element is unavailable')
  262. return add({ kind: 'array', element: this.convert(element, site) })
  263. }
  264. if (flags & ts.TypeFlags.Object) return add(this.object(type, site))
  265. this.fail(type, site, 'type has no supported JSON representation')
  266. }
  267. private valueType(type: ts.Type, site: ts.Node, optional: boolean): number {
  268. if (!optional) return this.convert(type, site)
  269. const members = (type.isUnion() ? type.types : [type]).filter(member => !(member.flags & ts.TypeFlags.Undefined))
  270. return this.union(members.map(member => this.convert(member, site)))
  271. }
  272. private object(type: ts.Type, site: ts.Node): SchemaNode {
  273. this.rejectClass(type, site)
  274. if (type.getCallSignatures().length > 0 || type.getConstructSignatures().length > 0) this.fail(type, site, 'callable data is not JSON')
  275. const properties: SchemaProperty[] = []
  276. for (const property of this.checker.getPropertiesOfType(type).sort((left, right) => compare(left.name, right.name))) {
  277. if (property.getName().startsWith('__@')) this.fail(type, site, 'symbol-keyed data is not a JSON field')
  278. const declaration = property.valueDeclaration ?? property.declarations?.[0] ?? site
  279. const propertyType = this.checker.getTypeOfSymbolAtLocation(property, declaration)
  280. const optional = (property.flags & ts.SymbolFlags.Optional) !== 0
  281. if (optional && (propertyType.isUnion() ? propertyType.types : [propertyType])
  282. .every(member => (member.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Never)) !== 0)) continue
  283. const child = this.valueType(propertyType, declaration, optional)
  284. properties.push({ name: property.getName(), type: child, optional })
  285. }
  286. const indices = this.checker.getIndexInfosOfType(type).map(info => ({
  287. key: this.convert(info.keyType, site),
  288. value: this.convert(info.type, site),
  289. }))
  290. if (properties.length === 0 && indices.length === 0) this.fail(type, site, 'unconstrained empty object type is not an explicit JSON record')
  291. return { kind: 'object', properties, indices }
  292. }
  293. private rejectClass(type: ts.Type, site: ts.Node): void {
  294. const symbol = type.getSymbol()
  295. if (symbol?.declarations?.some(declaration => ts.isClassDeclaration(declaration) || ts.isClassExpression(declaration))) {
  296. this.fail(type, site, 'class instances require an explicit persisted representation')
  297. }
  298. }
  299. private phantom(type: ts.Type): boolean {
  300. if (!(type.flags & ts.TypeFlags.Object)) return false
  301. const properties = this.checker.getPropertiesOfType(type)
  302. return properties.length > 0 && properties.every(property => property.getName().startsWith('__@'))
  303. && type.getCallSignatures().length === 0 && type.getConstructSignatures().length === 0
  304. && this.checker.getIndexInfosOfType(type).length === 0
  305. }
  306. union(types: readonly number[]): number {
  307. const node = this.unionNode(types)
  308. if (node.kind === 'union' && node.types.length === 1) return node.types[0] as number
  309. this.nodes.push(node)
  310. return this.nodes.length - 1
  311. }
  312. private unionNode(types: readonly number[]): SchemaNode {
  313. if (types.length === 0) return { kind: 'primitive', type: 'never' }
  314. return { kind: 'union', types: [...new Set(types)] }
  315. }
  316. envelope(id: number, event: string): number {
  317. const node = this.nodes[id] as SchemaNode
  318. if (node.kind === 'union') return this.union(node.types.map(type => this.envelope(type, event)))
  319. if (node.kind !== 'object') throw new PersistenceSchemaError('persistence schema: SessionEvent must resolve to object alternatives')
  320. const tag = node.properties.find(property => property.name === 'type')
  321. const data = node.properties.find(property => property.name === 'data')
  322. const literal = tag === undefined ? undefined : canonicalizeSchema(this.nodes, tag.type).nodes[0]
  323. if (tag?.optional !== false || data?.optional !== false || literal?.kind !== 'literal' || literal.value !== event) {
  324. throw new PersistenceSchemaError(`persistence schema: SessionEvent<${JSON.stringify(event)}> must retain its required type and data fields`)
  325. }
  326. const string = this.nodes.length
  327. this.nodes.push({ kind: 'primitive', type: 'string' })
  328. const envelope: SchemaNode = {
  329. kind: 'object',
  330. properties: node.properties.filter(property => property.name !== 'data')
  331. .map(property => property.name === 'type' ? { ...property, type: string } : property),
  332. indices: node.indices,
  333. }
  334. this.nodes.push(envelope)
  335. return this.nodes.length - 1
  336. }
  337. inventory(inputs: readonly RootInput[]): PersistenceSchemaInventory {
  338. const roots = inputs.map((input) => {
  339. const schema = canonicalizeSchema(this.nodes, input.node)
  340. return {
  341. key: input.key,
  342. kind: input.kind,
  343. ...(input.event === undefined ? {} : { event: input.event }),
  344. ...(input.surface === undefined ? {} : { surface: input.surface }),
  345. digest: schemaDigest(schema),
  346. schema,
  347. }
  348. })
  349. const found = new Set<number>()
  350. const paths = new Map<number, Set<string>>()
  351. const visit = (id: number, path: string): void => {
  352. const names = paths.get(id) ?? new Set<string>()
  353. names.add(path)
  354. paths.set(id, names)
  355. if (found.has(id)) return
  356. found.add(id)
  357. const node = this.nodes[id] as SchemaNode
  358. if (node.kind === 'object') {
  359. for (const property of node.properties) visit(property.type, `${path}.${property.name}`)
  360. for (const index of node.indices) { visit(index.key, `${path}.[key]`); visit(index.value, `${path}.[value]`) }
  361. } else {
  362. for (const [index, child] of schemaChildren(node).entries()) visit(child, `${path}[${String(index)}]`)
  363. }
  364. }
  365. for (const input of inputs) visit(input.node, input.key)
  366. const types = new Map<string, { schema: PersistenceType['schema']; names: Set<string>; sources: Set<string> }>()
  367. for (const id of found) {
  368. const schema = canonicalizeSchema(this.nodes, id)
  369. const digest = schemaDigest(schema)
  370. const item = types.get(digest) ?? { schema, names: new Set<string>(), sources: new Set<string>() }
  371. const declarationMetadata = this.declarationMetadata.get(id)
  372. for (const name of declarationMetadata?.names ?? []) item.names.add(name)
  373. for (const source of declarationMetadata?.sources ?? []) item.sources.add(source)
  374. const kind = schema.nodes[0]?.kind
  375. if (kind !== 'primitive' && kind !== 'literal' && (declarationMetadata === undefined || declarationMetadata.names.size === 0)) {
  376. for (const path of paths.get(id) ?? []) item.names.add(path)
  377. }
  378. types.set(digest, item)
  379. }
  380. return {
  381. formatVersion: 1,
  382. roots,
  383. types: [...types].sort(([left], [right]) => compare(left, right)).map(([digest, item]) => ({
  384. digest,
  385. schema: item.schema,
  386. names: [...item.names].sort(compare),
  387. sources: [...item.sources].sort(compare),
  388. })),
  389. }
  390. }
  391. private record(id: number, type: ts.Type): void {
  392. const item = this.declarationMetadata.get(id) ?? { names: new Set<string>(), sources: new Set<string>() }
  393. const declarations = new Set([
  394. ...this.declarationSources.get(type) ?? [],
  395. ...type.aliasSymbol?.declarations ?? [],
  396. ...type.getSymbol()?.declarations ?? [],
  397. ])
  398. for (const declaration of declarations) {
  399. if (!isNamedTypeDeclaration(declaration) && !ts.isTypeLiteralNode(declaration) && !ts.isEnumMember(declaration)) continue
  400. const source = declaration.getSourceFile()
  401. const file = slash(relative(this.root, source.fileName))
  402. if (!trackedSource(file)) continue
  403. if (isNamedTypeDeclaration(declaration)) item.names.add(`${file}#${declaration.name.text}`)
  404. const position = source.getLineAndCharacterOfPosition(declaration.getStart())
  405. item.sources.add(`${file}:${String(position.line + 1)}`)
  406. }
  407. this.declarationMetadata.set(id, item)
  408. }
  409. private fail(type: ts.Type, site: ts.Node, reason: string): never {
  410. const source = slash(relative(this.root, site.getSourceFile().fileName))
  411. const position = site.getSourceFile().getLineAndCharacterOfPosition(site.getStart())
  412. throw new PersistenceSchemaError(`persistence schema: ${source}:${String(position.line + 1)}: ${reason}: ${this.checker.typeToString(type)}`)
  413. }
  414. }
  415. function isNamedTypeDeclaration(node: ts.Node): node is ts.TypeAliasDeclaration | ts.InterfaceDeclaration | ts.EnumDeclaration {
  416. return ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isEnumDeclaration(node)
  417. }
  418. function stringLiterals(type: ts.Type, name: string): Set<string> {
  419. if (type.flags & ts.TypeFlags.Never) return new Set()
  420. const members = type.isUnion() ? type.types : [type]
  421. if (members.some(member => !(member.flags & ts.TypeFlags.StringLiteral))) throw new PersistenceSchemaError(`persistence schema: ${name} is not a closed string-literal union`)
  422. return new Set(members.map(member => (member as ts.StringLiteralType).value))
  423. }
  424. function diagnosticText(diagnostic: ts.Diagnostic): string {
  425. return `persistence schema: TypeScript TS${String(diagnostic.code)}: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}`
  426. }
  427. function compare(left: string, right: string): number {
  428. return left < right ? -1 : left > right ? 1 : 0
  429. }
  430. function slash(path: string): string {
  431. return path.split(sep).join('/')
  432. }
  433. function trackedSource(file: string): boolean {
  434. return (file.startsWith('packages/') || file.startsWith('vendor/')) && !file.includes('/node_modules/')
  435. }