on-zero 0.16.17 → 0.16.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/dist/cjs/createZeroClient.cjs +44 -11
  2. package/dist/cjs/createZeroClient.native.js +54 -17
  3. package/dist/cjs/createZeroClient.native.js.map +1 -1
  4. package/dist/cjs/createZeroClient.recovery.test.cjs +49 -0
  5. package/dist/cjs/createZeroClient.recovery.test.native.js +53 -0
  6. package/dist/cjs/createZeroClient.recovery.test.native.js.map +1 -1
  7. package/dist/cjs/generate-helpers.cjs +28 -0
  8. package/dist/cjs/generate-helpers.native.js +36 -0
  9. package/dist/cjs/generate-helpers.native.js.map +1 -1
  10. package/dist/cjs/generate-layout.cjs +62 -53
  11. package/dist/cjs/generate-layout.native.js +62 -54
  12. package/dist/cjs/generate-layout.native.js.map +1 -1
  13. package/dist/cjs/generate-lite.cjs +28 -0
  14. package/dist/cjs/generate-lite.native.js +89 -33
  15. package/dist/cjs/generate-lite.native.js.map +1 -1
  16. package/dist/cjs/generate-lite.test.cjs +46 -0
  17. package/dist/cjs/generate-lite.test.native.js +46 -0
  18. package/dist/cjs/generate-lite.test.native.js.map +1 -1
  19. package/dist/cjs/generate.cjs +237 -243
  20. package/dist/cjs/generate.native.js +352 -353
  21. package/dist/cjs/generate.native.js.map +1 -1
  22. package/dist/cjs/generate.test.cjs +35 -0
  23. package/dist/cjs/generate.test.native.js +35 -0
  24. package/dist/cjs/generate.test.native.js.map +1 -1
  25. package/dist/cjs/helpers/createMutators.cjs +2 -1
  26. package/dist/cjs/helpers/createMutators.native.js +3 -1
  27. package/dist/cjs/helpers/createMutators.native.js.map +1 -1
  28. package/dist/cjs/helpers/recoverZeroClient.cjs +8 -2
  29. package/dist/cjs/helpers/recoverZeroClient.native.js +10 -2
  30. package/dist/cjs/helpers/recoverZeroClient.native.js.map +1 -1
  31. package/dist/cjs/helpers/recoverZeroClient.test.cjs +53 -0
  32. package/dist/cjs/helpers/recoverZeroClient.test.native.js +55 -0
  33. package/dist/cjs/helpers/recoverZeroClient.test.native.js.map +1 -1
  34. package/dist/cjs/native-typescript.cjs +190 -0
  35. package/dist/cjs/native-typescript.native.js +255 -0
  36. package/dist/cjs/native-typescript.native.js.map +1 -0
  37. package/dist/esm/createZeroClient.mjs +44 -11
  38. package/dist/esm/createZeroClient.mjs.map +1 -1
  39. package/dist/esm/createZeroClient.native.js +54 -17
  40. package/dist/esm/createZeroClient.native.js.map +1 -1
  41. package/dist/esm/createZeroClient.recovery.test.mjs +50 -1
  42. package/dist/esm/createZeroClient.recovery.test.mjs.map +1 -1
  43. package/dist/esm/createZeroClient.recovery.test.native.js +54 -1
  44. package/dist/esm/createZeroClient.recovery.test.native.js.map +1 -1
  45. package/dist/esm/generate-helpers.mjs +27 -1
  46. package/dist/esm/generate-helpers.mjs.map +1 -1
  47. package/dist/esm/generate-helpers.native.js +35 -1
  48. package/dist/esm/generate-helpers.native.js.map +1 -1
  49. package/dist/esm/generate-layout.mjs +52 -54
  50. package/dist/esm/generate-layout.mjs.map +1 -1
  51. package/dist/esm/generate-layout.native.js +52 -55
  52. package/dist/esm/generate-layout.native.js.map +1 -1
  53. package/dist/esm/generate-lite.mjs +29 -1
  54. package/dist/esm/generate-lite.mjs.map +1 -1
  55. package/dist/esm/generate-lite.native.js +90 -34
  56. package/dist/esm/generate-lite.native.js.map +1 -1
  57. package/dist/esm/generate-lite.test.mjs +46 -0
  58. package/dist/esm/generate-lite.test.mjs.map +1 -1
  59. package/dist/esm/generate-lite.test.native.js +46 -0
  60. package/dist/esm/generate-lite.test.native.js.map +1 -1
  61. package/dist/esm/generate.mjs +238 -244
  62. package/dist/esm/generate.mjs.map +1 -1
  63. package/dist/esm/generate.native.js +353 -354
  64. package/dist/esm/generate.native.js.map +1 -1
  65. package/dist/esm/generate.test.mjs +35 -0
  66. package/dist/esm/generate.test.mjs.map +1 -1
  67. package/dist/esm/generate.test.native.js +35 -0
  68. package/dist/esm/generate.test.native.js.map +1 -1
  69. package/dist/esm/helpers/createMutators.mjs +2 -1
  70. package/dist/esm/helpers/createMutators.mjs.map +1 -1
  71. package/dist/esm/helpers/createMutators.native.js +3 -1
  72. package/dist/esm/helpers/createMutators.native.js.map +1 -1
  73. package/dist/esm/helpers/recoverZeroClient.mjs +8 -2
  74. package/dist/esm/helpers/recoverZeroClient.mjs.map +1 -1
  75. package/dist/esm/helpers/recoverZeroClient.native.js +10 -2
  76. package/dist/esm/helpers/recoverZeroClient.native.js.map +1 -1
  77. package/dist/esm/helpers/recoverZeroClient.test.mjs +53 -0
  78. package/dist/esm/helpers/recoverZeroClient.test.mjs.map +1 -1
  79. package/dist/esm/helpers/recoverZeroClient.test.native.js +55 -0
  80. package/dist/esm/helpers/recoverZeroClient.test.native.js.map +1 -1
  81. package/dist/esm/index.native.js.map +1 -1
  82. package/dist/esm/native-typescript.mjs +164 -0
  83. package/dist/esm/native-typescript.mjs.map +1 -0
  84. package/dist/esm/native-typescript.native.js +226 -0
  85. package/dist/esm/native-typescript.native.js.map +1 -0
  86. package/package.json +4 -3
  87. package/src/createZeroClient.recovery.test.tsx +56 -1
  88. package/src/createZeroClient.tsx +54 -15
  89. package/src/generate-helpers.ts +79 -0
  90. package/src/generate-layout.ts +64 -108
  91. package/src/generate-lite.test.ts +49 -0
  92. package/src/generate-lite.ts +44 -0
  93. package/src/generate.test.ts +52 -0
  94. package/src/generate.ts +313 -366
  95. package/src/helpers/createMutators.ts +14 -3
  96. package/src/helpers/recoverZeroClient.test.ts +50 -0
  97. package/src/helpers/recoverZeroClient.ts +12 -2
  98. package/src/native-typescript.ts +176 -0
  99. package/types/createZeroClient.d.ts.map +1 -1
  100. package/types/generate-helpers.d.ts +13 -0
  101. package/types/generate-helpers.d.ts.map +1 -1
  102. package/types/generate-layout.d.ts +2 -1
  103. package/types/generate-layout.d.ts.map +1 -1
  104. package/types/generate-lite.d.ts +1 -0
  105. package/types/generate-lite.d.ts.map +1 -1
  106. package/types/generate.d.ts.map +1 -1
  107. package/types/helpers/createMutators.d.ts.map +1 -1
  108. package/types/helpers/recoverZeroClient.d.ts.map +1 -1
  109. package/types/native-typescript.d.ts +11 -0
  110. package/types/native-typescript.d.ts.map +1 -0
package/src/generate.ts CHANGED
@@ -2,6 +2,16 @@ import { createHash } from 'node:crypto'
2
2
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
3
3
  import { basename, dirname, resolve } from 'node:path'
4
4
 
5
+ import * as ts from 'typescript/unstable/ast'
6
+ import {
7
+ ObjectFlags,
8
+ SymbolFlags,
9
+ TypeFlags,
10
+ type Project,
11
+ type Symbol,
12
+ type Type,
13
+ } from 'typescript/unstable/async'
14
+
5
15
  import {
6
16
  CRUD_MUTATION_NAMES,
7
17
  formatObjectKey,
@@ -16,15 +26,21 @@ import {
16
26
  generateTypesFile,
17
27
  parseColumnType,
18
28
  parseTypeString,
29
+ renderDrizzleZeroSqliteSchemaModule,
19
30
  shouldSkipObjectKey,
20
31
  } from './generate-helpers'
21
32
  import { discoverDataLayout, namespaceImportPath } from './generate-layout'
33
+ import {
34
+ collectTypeScriptSourcePaths,
35
+ createNativeTypeScriptProject,
36
+ type NativeTypeScriptProject,
37
+ } from './native-typescript'
22
38
 
23
39
  import type { ExtractedMutation, ModelMutations, SchemaColumn } from './generate-helpers'
24
40
  import type { DataLayout } from './generate-layout'
25
41
 
26
42
  const hash = (s: string) => createHash('sha256').update(s).digest('hex')
27
- const GENERATOR_CACHE_VERSION = '6'
43
+ const GENERATOR_CACHE_VERSION = '7'
28
44
 
29
45
  const isGeneratorSourceFile = (name: string) =>
30
46
  name.endsWith('.ts') &&
@@ -105,9 +121,19 @@ function saveCache() {
105
121
 
106
122
  function writeFileIfChanged(filePath: string, content: string): boolean {
107
123
  const contentHash = hash(content)
108
- const cachedHash = generateCache[filePath]
109
124
 
110
- if (cachedHash === contentHash && existsSync(filePath)) {
125
+ // compare against the bytes actually on disk, never against the cache. the
126
+ // cache records what this generator last WROTE to a path, and a checkout,
127
+ // merge, revert or hand edit replaces the file without touching it, so a
128
+ // cache hit does not mean the file matches. trusting it skipped the write
129
+ // and left stale generated output that the next build silently consumed.
130
+ let onDisk: string | null = null
131
+ try {
132
+ onDisk = readFileSync(filePath, 'utf-8')
133
+ } catch {}
134
+
135
+ if (onDisk !== null && hash(onDisk) === contentHash) {
136
+ generateCache[filePath] = contentHash
111
137
  return false
112
138
  }
113
139
 
@@ -119,91 +145,33 @@ function writeFileIfChanged(filePath: string, content: string): boolean {
119
145
  // file-content emitters and valibot helpers are imported from ./generate-helpers
120
146
  // so they can be shared with the browser-safe generate-lite entry point.
121
147
 
122
- // creates a TypeChecker that can resolve type references across files
123
- function createTypeResolver(
124
- ts: typeof import('typescript'),
125
- files: Array<{ path: string; content: string }>,
126
- dir: string
127
- ) {
128
- // find tsconfig if it exists for path alias resolution
129
- const configPath = ts.findConfigFile(dir, ts.sys.fileExists, 'tsconfig.json')
130
- let compilerOptions: import('typescript').CompilerOptions = {
131
- target: ts.ScriptTarget.Latest,
132
- module: ts.ModuleKind.ESNext,
133
- moduleResolution: ts.ModuleResolutionKind.Bundler,
134
- strict: false,
135
- skipLibCheck: true,
136
- noEmit: true,
137
- }
138
-
139
- if (configPath) {
140
- const configFile = ts.readConfigFile(configPath, ts.sys.readFile)
141
- if (configFile.config) {
142
- // use tsconfig's directory as base for path resolution (not the models dir)
143
- const parsed = ts.parseJsonConfigFileContent(
144
- configFile.config,
145
- ts.sys,
146
- dirname(configPath)
147
- )
148
- compilerOptions = { ...compilerOptions, ...parsed.options }
149
- }
150
- }
151
-
152
- // create a virtual file system host backed by real files + our content map
153
- const fileMap = new Map<string, string>()
154
- for (const f of files) {
155
- fileMap.set(f.path, f.content)
156
- }
157
-
158
- const host = ts.createCompilerHost(compilerOptions)
159
- const originalGetSourceFile = host.getSourceFile.bind(host)
160
- host.getSourceFile = (fileName, languageVersion, onError) => {
161
- const content = fileMap.get(fileName)
162
- if (content !== undefined) {
163
- return ts.createSourceFile(fileName, content, languageVersion, true)
164
- }
165
- return originalGetSourceFile(fileName, languageVersion, onError)
166
- }
167
- host.fileExists = (fileName) => fileMap.has(fileName) || ts.sys.fileExists(fileName)
168
- host.readFile = (fileName) => fileMap.get(fileName) ?? ts.sys.readFile(fileName)
169
-
170
- const program = ts.createProgram(
171
- files.map((f) => f.path),
172
- compilerOptions,
173
- host
174
- )
175
- const checker = program.getTypeChecker()
176
-
148
+ function createTypeResolver(project: Project) {
177
149
  return {
178
- program,
179
- checker,
180
- // resolve a type annotation node to a ts.Type
181
- resolveType(node: import('typescript').TypeNode): import('typescript').Type | null {
150
+ project,
151
+ async resolveType(node: ts.TypeNode): Promise<Type | null> {
182
152
  try {
183
- return checker.getTypeFromTypeNode(node)
153
+ return (await project.checker.getTypeFromTypeNode(node)) ?? null
184
154
  } catch {
185
155
  return null
186
156
  }
187
157
  },
188
- // convert a resolved type to valibot code
189
- typeToValibot(type: import('typescript').Type): string {
190
- return tsTypeToValibot(ts, checker, type)
158
+ async typeToValibot(type: Type): Promise<string> {
159
+ return tsTypeToValibot(project, type)
191
160
  },
192
161
  }
193
162
  }
194
163
 
195
164
  // find a specific exported arrow function's Nth parameter type in a checker-owned source file
196
- function resolveParamType(
197
- ts: typeof import('typescript'),
165
+ async function resolveParamType(
198
166
  resolver: ReturnType<typeof createTypeResolver>,
199
- sourceFile: import('typescript').SourceFile,
167
+ sourceFile: ts.SourceFile,
200
168
  exportName: string,
201
169
  paramIndex: number
202
- ): import('typescript').Type | null {
203
- let result: import('typescript').Type | null = null
170
+ ): Promise<Type | null> {
171
+ let typeNode: ts.TypeNode | null = null
204
172
 
205
- ts.forEachChild(sourceFile, (node) => {
206
- if (result) return
173
+ sourceFile.forEachChild((node) => {
174
+ if (typeNode) return
207
175
  if (!ts.isVariableStatement(node)) return
208
176
  if (!node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) return
209
177
 
@@ -214,12 +182,12 @@ function resolveParamType(
214
182
  if (decl.initializer && ts.isArrowFunction(decl.initializer)) {
215
183
  const param = decl.initializer.parameters[paramIndex]
216
184
  if (param?.type) {
217
- result = resolver.resolveType(param.type)
185
+ typeNode = param.type
218
186
  }
219
187
  }
220
188
  })
221
189
 
222
- return result
190
+ return typeNode ? resolver.resolveType(typeNode) : null
223
191
  }
224
192
 
225
193
  // positional shape of a `mutations(...)` call, matching the runtime overloads:
@@ -229,13 +197,10 @@ function resolveParamType(
229
197
  // mutations(table, permissions, handlers, { crud: false })
230
198
  // arg 1 is always permissions and arg 3 is always options, so neither may ever
231
199
  // be read as the handlers object.
232
- function readMutationsCall(
233
- ts: typeof import('typescript'),
234
- call: import('typescript').CallExpression
235
- ): {
200
+ function readMutationsCall(call: ts.CallExpression): {
236
201
  hasTable: boolean
237
202
  crud: boolean
238
- handlersArg: import('typescript').ObjectLiteralExpression | null
203
+ handlersArg: ts.ObjectLiteralExpression | null
239
204
  } {
240
205
  const args = call.arguments
241
206
 
@@ -275,14 +240,13 @@ function readMutationsCall(
275
240
 
276
241
  // find mutation handler param types in a resolver-owned source file
277
242
  // walks `export const mutate = mutations(..., { handlerName: async (ctx, param: Type) => ... })`
278
- function resolveMutationParamTypes(
279
- ts: typeof import('typescript'),
243
+ async function resolveMutationParamTypes(
280
244
  resolver: ReturnType<typeof createTypeResolver>,
281
- sourceFile: import('typescript').SourceFile
282
- ): Map<string, import('typescript').Type> {
283
- const resolved = new Map<string, import('typescript').Type>()
245
+ sourceFile: ts.SourceFile
246
+ ): Promise<Map<string, Type>> {
247
+ const nodes = new Map<string, ts.TypeNode>()
284
248
 
285
- ts.forEachChild(sourceFile, (node) => {
249
+ sourceFile.forEachChild((node) => {
286
250
  if (!ts.isVariableStatement(node)) return
287
251
  if (!node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) return
288
252
 
@@ -292,7 +256,7 @@ function resolveMutationParamTypes(
292
256
 
293
257
  if (!decl.initializer || !ts.isCallExpression(decl.initializer)) return
294
258
 
295
- const { handlersArg } = readMutationsCall(ts, decl.initializer)
259
+ const { handlersArg } = readMutationsCall(decl.initializer)
296
260
  if (!handlersArg) return
297
261
 
298
262
  for (const prop of handlersArg.properties) {
@@ -300,9 +264,7 @@ function resolveMutationParamTypes(
300
264
  const name = prop.name?.getText(sourceFile)
301
265
  if (!name) continue
302
266
 
303
- let params:
304
- | import('typescript').NodeArray<import('typescript').ParameterDeclaration>
305
- | null = null
267
+ let params: ts.NodeArray<ts.ParameterDeclaration> | null = null
306
268
  if (ts.isPropertyAssignment(prop)) {
307
269
  const init = prop.initializer
308
270
  if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) {
@@ -316,30 +278,30 @@ function resolveMutationParamTypes(
316
278
  const typeNode = params[1]!.type
317
279
  if (!typeNode) continue
318
280
 
319
- const expanded = resolver.resolveType(typeNode)
320
- if (expanded) {
321
- resolved.set(name, expanded)
322
- }
281
+ nodes.set(name, typeNode)
323
282
  }
324
283
  })
325
284
 
285
+ const resolved = new Map<string, Type>()
286
+ for (const [name, node] of nodes) {
287
+ const expanded = await resolver.resolveType(node)
288
+ if (expanded) resolved.set(name, expanded)
289
+ }
326
290
  return resolved
327
291
  }
328
292
 
329
293
  function extractMutationsFromModel(
330
- ts: typeof import('typescript'),
331
- sourceFile: ReturnType<typeof ts.createSourceFile>,
294
+ sourceFile: ts.SourceFile,
332
295
  content: string,
333
296
  fileName: string,
334
297
  silent: boolean,
335
298
  typeToValibot: (typeString: string) => string | null,
336
- resolvedTypes?: Map<string, import('typescript').Type>,
337
- resolvedTypeToValibot?: (type: import('typescript').Type) => string
299
+ resolvedValibot?: Map<string, string>
338
300
  ): ModelMutations | null {
339
- let mutateNode: import('typescript').CallExpression | null = null
301
+ let mutateNode: ts.CallExpression | null = null
340
302
 
341
303
  // find `export const mutate = mutations(...)`
342
- ts.forEachChild(sourceFile, (node) => {
304
+ sourceFile.forEachChild((node) => {
343
305
  if (!ts.isVariableStatement(node)) return
344
306
  if (!node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) return
345
307
  const decl = node.declarationList.declarations[0]
@@ -360,11 +322,11 @@ function extractMutationsFromModel(
360
322
  }
361
323
  }
362
324
 
363
- const call = mutateNode as import('typescript').CallExpression
325
+ const call = mutateNode as ts.CallExpression
364
326
 
365
327
  // string-named and table-builder registrations behave identically at runtime:
366
328
  // any 2+ arg call registers permissions and generates crud unless it opted out
367
- const { hasTable, crud, handlersArg } = readMutationsCall(ts, call)
329
+ const { hasTable, crud, handlersArg } = readMutationsCall(call)
368
330
  const hasCRUD = hasTable && crud
369
331
 
370
332
  // extract schema columns for CRUD generation
@@ -373,7 +335,7 @@ function extractMutationsFromModel(
373
335
 
374
336
  if (hasCRUD) {
375
337
  // parse schema columns from file content
376
- extractSchemaColumns(ts, sourceFile, columns, primaryKeys)
338
+ extractSchemaColumns(sourceFile, columns, primaryKeys)
377
339
  }
378
340
 
379
341
  // extract custom mutation param types
@@ -387,9 +349,7 @@ function extractMutationsFromModel(
387
349
  if (!name) continue
388
350
 
389
351
  // find the arrow function or method
390
- let params:
391
- | import('typescript').NodeArray<import('typescript').ParameterDeclaration>
392
- | null = null
352
+ let params: ts.NodeArray<ts.ParameterDeclaration> | null = null
393
353
 
394
354
  if (ts.isPropertyAssignment(prop)) {
395
355
  const init = prop.initializer
@@ -419,11 +379,8 @@ function extractMutationsFromModel(
419
379
  let valibotCode = typeToValibot(paramType)
420
380
 
421
381
  // if direct parse failed (unresolved reference), use checker-resolved types
422
- if (!valibotCode && resolvedTypes && resolvedTypeToValibot) {
423
- const resolvedType = resolvedTypes.get(name)
424
- if (resolvedType) {
425
- valibotCode = resolvedTypeToValibot(resolvedType)
426
- }
382
+ if (!valibotCode && resolvedValibot) {
383
+ valibotCode = resolvedValibot.get(name) ?? null
427
384
  }
428
385
 
429
386
  custom.push({
@@ -444,13 +401,12 @@ function extractMutationsFromModel(
444
401
  }
445
402
 
446
403
  function extractSchemaColumns(
447
- ts: typeof import('typescript'),
448
- sourceFile: ReturnType<typeof ts.createSourceFile>,
404
+ sourceFile: ts.SourceFile,
449
405
  columns: Record<string, SchemaColumn>,
450
406
  primaryKeys: string[]
451
407
  ) {
452
408
  // walk AST to find table(...).columns({...}).primaryKey(...)
453
- function visit(node: import('typescript').Node) {
409
+ function visit(node: ts.Node) {
454
410
  if (ts.isCallExpression(node)) {
455
411
  const text = node.expression.getText(sourceFile)
456
412
 
@@ -479,87 +435,71 @@ function extractSchemaColumns(
479
435
  }
480
436
  }
481
437
  }
482
- ts.forEachChild(node, visit)
438
+ node.forEachChild(visit)
483
439
  }
484
440
  visit(sourceFile)
485
441
  }
486
442
 
487
- function getInstantiatedPropertyType(
488
- checker: import('typescript').TypeChecker,
489
- parent: import('typescript').Type,
490
- name: string
491
- ) {
492
- const resolveProperty: unknown = Reflect.get(checker, 'getTypeOfPropertyOfType')
493
- if (typeof resolveProperty !== 'function') return undefined
494
- // typescript exposes this runtime method but omits it from TypeChecker.
495
- return Reflect.apply(resolveProperty, checker, [parent, name]) as
496
- | import('typescript').Type
497
- | undefined
498
- }
499
-
500
443
  // convert a ts.Type to valibot code by walking the type checker AST
501
- function tsTypeToValibot(
502
- ts: typeof import('typescript'),
503
- checker: import('typescript').TypeChecker,
504
- type: import('typescript').Type,
505
- seen?: Set<import('typescript').Type>
506
- ): string {
444
+ async function tsTypeToValibot(
445
+ project: Project,
446
+ type: Type,
447
+ seen = new Set<number>()
448
+ ): Promise<string> {
449
+ const checker = project.checker
507
450
  // prevent infinite recursion on circular types
508
451
  // only track structured types (objects, intersections) — not primitives/unions
509
- if (!seen) seen = new Set()
510
- const flags = type.getFlags()
511
- if (flags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection)) {
512
- if (seen.has(type)) return 'v.unknown()'
513
- seen.add(type)
452
+ const flags = type.flags
453
+ if (flags & (TypeFlags.Object | TypeFlags.Intersection)) {
454
+ if (seen.has(type.id)) return 'v.unknown()'
455
+ seen.add(type.id)
514
456
  }
515
457
 
516
- const recurse = (t: import('typescript').Type) => tsTypeToValibot(ts, checker, t, seen)
458
+ const recurse = (value: Type) => tsTypeToValibot(project, value, seen)
517
459
 
518
460
  // primitives
519
- if (flags & ts.TypeFlags.String) return 'v.string()'
520
- if (flags & ts.TypeFlags.Number) return 'v.number()'
521
- if (flags & ts.TypeFlags.Boolean) return 'v.boolean()'
522
- if (flags & ts.TypeFlags.Void || flags & ts.TypeFlags.Undefined) return 'v.void_()'
523
- if (flags & ts.TypeFlags.Null) return 'v.null_()'
524
- if (flags & ts.TypeFlags.Any || flags & ts.TypeFlags.Unknown) return 'v.unknown()'
525
- if (flags & ts.TypeFlags.Never) return 'v.never()'
526
- if (flags & ts.TypeFlags.TemplateLiteral) return 'v.string()'
527
- if (
528
- flags & ts.TypeFlags.Object &&
529
- (checker.isArrayType?.(type) || checker.isArrayLikeType?.(type))
530
- ) {
531
- const typeArgs =
532
- checker.getTypeArguments?.(type as import('typescript').TypeReference) ?? []
533
- const elementType = typeArgs.length === 1 ? typeArgs[0] : type.getNumberIndexType?.()
534
- return `v.array(${elementType ? recurse(elementType) : 'v.unknown()'})`
461
+ if (flags & TypeFlags.String) return 'v.string()'
462
+ if (flags & TypeFlags.Number) return 'v.number()'
463
+ if (flags & TypeFlags.Boolean) return 'v.boolean()'
464
+ if (flags & TypeFlags.Void || flags & TypeFlags.Undefined) return 'v.void_()'
465
+ if (flags & TypeFlags.Null) return 'v.null_()'
466
+ if (flags & TypeFlags.Any || flags & TypeFlags.Unknown) return 'v.unknown()'
467
+ if (flags & TypeFlags.Never) return 'v.never()'
468
+ if (flags & TypeFlags.TemplateLiteral) return 'v.string()'
469
+ if (flags & TypeFlags.Object && (await checker.isArrayLikeType(type))) {
470
+ const typeArgs = type.isTypeReference() ? await checker.getTypeArguments(type) : []
471
+ const indexInfo = (await checker.getIndexInfosOfType(type)).find((info) =>
472
+ Boolean(info.keyType.flags & TypeFlags.Number)
473
+ )
474
+ const elementType = typeArgs.length === 1 ? typeArgs[0] : indexInfo?.valueType
475
+ return `v.array(${elementType ? await recurse(elementType) : 'v.unknown()'})`
535
476
  }
536
477
 
537
478
  // string/number/boolean literals
538
- if (flags & ts.TypeFlags.StringLiteral) {
539
- return `v.literal(${JSON.stringify((type as import('typescript').StringLiteralType).value)})`
479
+ if (type.isStringLiteralType()) {
480
+ return `v.literal(${JSON.stringify(type.value)})`
540
481
  }
541
- if (flags & ts.TypeFlags.NumberLiteral) {
542
- return `v.literal(${(type as import('typescript').NumberLiteralType).value})`
482
+ if (type.isNumberLiteralType()) {
483
+ return `v.literal(${type.value})`
543
484
  }
544
- if (flags & ts.TypeFlags.BooleanLiteral) {
545
- const name = (type as any).intrinsicName
546
- return `v.literal(${name === 'true'})`
485
+ if (type.isBooleanLiteralType()) {
486
+ return `v.literal(${type.value})`
547
487
  }
548
488
 
549
489
  // union
550
- if (type.isUnion()) {
551
- const members = type.types
552
- const hasNull = members.some((t) => t.getFlags() & ts.TypeFlags.Null)
490
+ if (type.isUnionType()) {
491
+ const members = await type.getTypes()
492
+ const hasNull = members.some((member) => member.flags & TypeFlags.Null)
553
493
  const hasUndefined = members.some(
554
- (t) => t.getFlags() & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)
494
+ (member) => member.flags & (TypeFlags.Undefined | TypeFlags.Void)
555
495
  )
556
496
  const rest = members.filter(
557
- (t) =>
558
- !(t.getFlags() & (ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.Void))
497
+ (member) =>
498
+ !(member.flags & (TypeFlags.Null | TypeFlags.Undefined | TypeFlags.Void))
559
499
  )
560
500
  if (
561
501
  rest.length === 2 &&
562
- rest.every((t) => t.getFlags() & ts.TypeFlags.BooleanLiteral)
502
+ rest.every((member) => member.flags & TypeFlags.BooleanLiteral)
563
503
  ) {
564
504
  let inner = 'v.boolean()'
565
505
  if (hasNull) inner = `v.nullable(${inner})`
@@ -571,41 +511,32 @@ function tsTypeToValibot(
571
511
 
572
512
  let inner =
573
513
  rest.length === 1
574
- ? recurse(rest[0]!)
575
- : `v.union([${rest.map((t) => recurse(t)).join(', ')}])`
514
+ ? await recurse(rest[0]!)
515
+ : `v.union([${(await Promise.all(rest.map(recurse))).join(', ')}])`
576
516
 
577
517
  if (hasNull) inner = `v.nullable(${inner})`
578
518
  if (hasUndefined) inner = `v.optional(${inner})`
579
519
  return inner
580
520
  }
581
521
 
582
- // resolve symbol property type with fallbacks
583
- const resolveSymbolType = (
584
- parent: import('typescript').Type,
585
- prop: import('typescript').Symbol
586
- ) => {
587
- // mapped and conditional types can expose transient symbols with no
588
- // declaration, so resolve each property against its concrete parent.
589
- const instantiated = getInstantiatedPropertyType(checker, parent, prop.getName())
590
- if (instantiated) return instantiated
591
- if (prop.valueDeclaration)
592
- return checker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration)
593
- if ((prop as any).declarations?.[0])
594
- return checker.getTypeOfSymbolAtLocation(prop, (prop as any).declarations[0])
595
- return checker.getDeclaredTypeOfSymbol(prop)
522
+ const resolveSymbolType = async (property: Symbol) => {
523
+ const symbolType = await checker.getTypeOfSymbol(property)
524
+ if (symbolType && !symbolType.isErrorType()) return symbolType
525
+ const declaration = property.valueDeclaration ?? property.declarations[0]
526
+ const node = declaration ? await declaration.resolve(project) : undefined
527
+ if (node) return checker.getTypeOfSymbolAtLocation(property, node)
528
+ return checker.getDeclaredTypeOfSymbol(property)
596
529
  }
597
530
 
598
- // intersection - use checker's merged properties directly
599
- if (type.isIntersection()) {
600
- const props = type.getProperties()
531
+ const objectToValibot = async (props: readonly Symbol[]) => {
601
532
  if (props.length === 0) return 'v.object({})'
602
533
  const entries: string[] = []
603
534
  for (const prop of props) {
604
- const name = prop.getName()
535
+ const name = prop.name
605
536
  if (shouldSkipObjectKey(name)) continue
606
- const propType = resolveSymbolType(type, prop)
607
- const isOptional = !!(prop.getFlags() & ts.SymbolFlags.Optional)
608
- let val = recurse(propType)
537
+ const propType = await resolveSymbolType(prop)
538
+ const isOptional = Boolean(prop.flags & SymbolFlags.Optional)
539
+ let val = await recurse(propType)
609
540
  if (isOptional && !val.startsWith('v.optional(')) {
610
541
  val = `v.optional(${val})`
611
542
  }
@@ -615,59 +546,39 @@ function tsTypeToValibot(
615
546
  return `v.object({\n ${entries.join(',\n ')},\n })`
616
547
  }
617
548
 
618
- // object type with properties
619
- const props = type.getProperties()
620
- if (
621
- props.length > 0 &&
622
- (type.getFlags() & ts.TypeFlags.Object || (type as any).objectFlags)
623
- ) {
624
- const objectFlags = (type as import('typescript').ObjectType).objectFlags ?? 0
549
+ // intersection - use checker's merged properties directly
550
+ if (type.isIntersectionType()) {
551
+ return objectToValibot(await checker.getPropertiesOfType(type))
552
+ }
625
553
 
626
- // array
627
- if (objectFlags & ts.ObjectFlags.Reference) {
628
- const typeRef = type as import('typescript').TypeReference
629
- const symbol = type.getSymbol()
630
- const name = symbol?.getName()
554
+ if (type.isTupleType()) {
555
+ const typeArgs = await checker.getTypeArguments(type)
556
+ return `v.tuple([${(await Promise.all(typeArgs.map(recurse))).join(', ')}])`
557
+ }
558
+
559
+ if (type.isObjectType()) {
560
+ if (type.objectFlags & ObjectFlags.Reference && type.isTypeReference()) {
561
+ const symbol = await type.getSymbol()
562
+ const typeArgs = await checker.getTypeArguments(type)
631
563
  if (
632
- (name === 'Array' || name === 'ReadonlyArray') &&
633
- typeRef.typeArguments?.length === 1
564
+ (symbol?.name === 'Array' || symbol?.name === 'ReadonlyArray') &&
565
+ typeArgs.length === 1
634
566
  ) {
635
- return `v.array(${recurse(typeRef.typeArguments[0]!)})`
567
+ return `v.array(${await recurse(typeArgs[0]!)})`
636
568
  }
637
569
  }
638
-
639
- // tuple
640
- if (objectFlags & ts.ObjectFlags.Tuple) {
641
- const typeRef = type as import('typescript').TypeReference
642
- const typeArgs = typeRef.typeArguments || []
643
- return `v.tuple([${typeArgs.map((t) => recurse(t)).join(', ')}])`
644
- }
645
-
646
- // regular object
647
- const entries: string[] = []
648
- for (const prop of props) {
649
- const name = prop.getName()
650
- if (shouldSkipObjectKey(name)) continue
651
- const propType = resolveSymbolType(type, prop)
652
- const isOptional = !!(prop.getFlags() & ts.SymbolFlags.Optional)
653
- let val = recurse(propType)
654
- if (isOptional && !val.startsWith('v.optional(')) {
655
- val = `v.optional(${val})`
656
- }
657
- entries.push(`${formatObjectKey(name)}: ${val}`)
658
- }
659
- if (entries.length === 0) return 'v.object({})'
660
- return `v.object({\n ${entries.join(',\n ')},\n })`
570
+ const props = await checker.getPropertiesOfType(type)
571
+ if (props.length > 0) return objectToValibot(props)
661
572
  }
662
573
 
663
574
  // index signature / Record type
664
- const stringIndex = type.getStringIndexType()
665
- if (stringIndex) {
666
- return `v.record(v.string(), ${recurse(stringIndex)})`
667
- }
668
- const numberIndex = type.getNumberIndexType()
669
- if (numberIndex) {
670
- return `v.record(v.number(), ${recurse(numberIndex)})`
575
+ for (const indexInfo of await checker.getIndexInfosOfType(type)) {
576
+ if (indexInfo.keyType.flags & TypeFlags.String) {
577
+ return `v.record(v.string(), ${await recurse(indexInfo.valueType)})`
578
+ }
579
+ if (indexInfo.keyType.flags & TypeFlags.Number) {
580
+ return `v.record(v.number(), ${await recurse(indexInfo.valueType)})`
581
+ }
671
582
  }
672
583
 
673
584
  return 'v.unknown()'
@@ -888,17 +799,30 @@ function dataMembershipFromLayout(layout: DataLayout): DataMembership {
888
799
  }
889
800
  }
890
801
 
802
+ async function loadGeneratorProject(baseDir: string): Promise<NativeTypeScriptProject> {
803
+ const sourcePaths = collectTypeScriptSourcePaths([
804
+ baseDir,
805
+ resolve(dirname(baseDir), 'database'),
806
+ ])
807
+ return createNativeTypeScriptProject(baseDir, sourcePaths)
808
+ }
809
+
891
810
  export async function deriveDataMembership(options: {
892
811
  dir: string
893
812
  config?: string
894
813
  }): Promise<DataMembership> {
895
- const ts = await import('typescript')
896
- const layout = discoverDataLayout(
897
- ts,
898
- resolve(options.dir),
899
- options.config ? resolve(options.config) : undefined
900
- )
901
- return dataMembershipFromLayout(layout)
814
+ const baseDir = resolve(options.dir)
815
+ const project = await loadGeneratorProject(baseDir)
816
+ try {
817
+ const layout = discoverDataLayout(
818
+ project,
819
+ baseDir,
820
+ options.config ? resolve(options.config) : undefined
821
+ )
822
+ return dataMembershipFromLayout(layout)
823
+ } finally {
824
+ await project.close()
825
+ }
902
826
  }
903
827
 
904
828
  export async function generateDrizzleSchemaInputFile(options: {
@@ -906,10 +830,22 @@ export async function generateDrizzleSchemaInputFile(options: {
906
830
  schemaImportPath: string
907
831
  config?: string
908
832
  }): Promise<string> {
909
- const ts = await import('typescript')
833
+ const baseDir = resolve(options.dir)
834
+ const project = await loadGeneratorProject(baseDir)
835
+ try {
836
+ return generateDrizzleSchemaInputFileWithProject(options, project)
837
+ } finally {
838
+ await project.close()
839
+ }
840
+ }
841
+
842
+ async function generateDrizzleSchemaInputFileWithProject(
843
+ options: { dir: string; schemaImportPath: string; config?: string },
844
+ project: NativeTypeScriptProject
845
+ ): Promise<string> {
910
846
  const baseDir = resolve(options.dir)
911
847
  const layout = discoverDataLayout(
912
- ts,
848
+ project,
913
849
  baseDir,
914
850
  options.config ? resolve(options.config) : undefined
915
851
  )
@@ -923,19 +859,14 @@ export async function generateDrizzleSchemaInputFile(options: {
923
859
  const relationEntries: string[] = []
924
860
 
925
861
  if (relationsPath) {
926
- const source = ts.createSourceFile(
927
- relationsPath,
928
- readFileSync(relationsPath, 'utf8'),
929
- ts.ScriptTarget.Latest,
930
- true
931
- )
862
+ const source = project.sourceFile(relationsPath)
932
863
  const included = new Set(tableNames)
933
- const visit = (node: import('typescript').Node) => {
864
+ const visit = (node: ts.Node) => {
934
865
  if (
935
866
  !ts.isCallExpression(node) ||
936
867
  node.expression.getText(source) !== 'defineRelations'
937
868
  ) {
938
- ts.forEachChild(node, visit)
869
+ node.forEachChild(visit)
939
870
  return
940
871
  }
941
872
  const factory = node.arguments[1]
@@ -994,6 +925,18 @@ export async function generateDrizzleSchemaInputFile(options: {
994
925
  }
995
926
 
996
927
  export async function generate(options: GenerateOptions): Promise<GenerateResult> {
928
+ const project = await loadGeneratorProject(resolve(options.dir))
929
+ try {
930
+ return await generateWithProject(options, project)
931
+ } finally {
932
+ await project.close()
933
+ }
934
+ }
935
+
936
+ async function generateWithProject(
937
+ options: GenerateOptions,
938
+ project: NativeTypeScriptProject
939
+ ): Promise<GenerateResult> {
997
940
  const { dir, after, silent, force, config } = options
998
941
  const baseDir = resolve(dir)
999
942
  const generatedDir = resolve(baseDir, 'generated')
@@ -1006,8 +949,11 @@ export async function generate(options: GenerateOptions): Promise<GenerateResult
1006
949
 
1007
950
  // the layout pass is intentionally first: config, filenames, and related()
1008
951
  // calls determine schema membership before any type program exists.
1009
- const ts = await import('typescript')
1010
- const layout = discoverDataLayout(ts, baseDir, config ? resolve(config) : undefined)
952
+ const layout = discoverDataLayout(
953
+ project,
954
+ baseDir,
955
+ config ? resolve(config) : undefined
956
+ )
1011
957
  const metadataHash = hash(
1012
958
  layout.metadataPaths
1013
959
  .map((path) => `${path}\0${readFileSync(path, 'utf8')}`)
@@ -1019,6 +965,8 @@ export async function generate(options: GenerateOptions): Promise<GenerateResult
1019
965
  // saveCache), the outputs are already current — skip the typescript-program
1020
966
  // build entirely and return the cached counts. the configureServer watcher
1021
967
  // still re-runs generate on real model/query edits.
968
+ const databaseSchemaPath = resolve(dirname(baseDir), 'database/schema.ts')
969
+ const needsSqliteZeroSchema = existsSync(databaseSchemaPath)
1022
970
  const inputHash = hash(
1023
971
  `${hashInputTree(layout.sourceRoots, generatedDir)}\0${metadataHash}`
1024
972
  )
@@ -1026,7 +974,37 @@ export async function generate(options: GenerateOptions): Promise<GenerateResult
1026
974
  !force &&
1027
975
  generateCache.__generatorVersion === GENERATOR_CACHE_VERSION &&
1028
976
  generateCache.__inputHash === inputHash &&
1029
- existsSync(resolve(generatedDir, 'models.ts'))
977
+ // the input hash deliberately skips the generated directory, so it cannot
978
+ // notice that the outputs themselves moved. existence is not correctness
979
+ // either: a checkout, merge or revert leaves every file present and stale.
980
+ // confirm the bytes on disk still match what the cache says it wrote there
981
+ // before skipping the build, or the fast path serves someone else's output.
982
+ //
983
+ // a consumer that formats generated output after this runs never hits the
984
+ // fast path again, because the cache holds the bytes written here and disk
985
+ // holds the formatted ones. soot does exactly that, `on-zero generate &&
986
+ // generate-instance-tables && oxfmt src/data/generated/`, and the number is
987
+ // already taken: 2.50s median before, 3.69s after, four warm runs each in
988
+ // one checkout with only the pin swapped, so about 1.2 seconds or 47
989
+ // percent on this step. the full three-command script went 2.65s to 3.14s
990
+ // with a spread that swallows most of it.
991
+ //
992
+ // the shape matters more than the number. this is not a cache that hits
993
+ // less often, it is a cache that never hits, so the cost does not degrade
994
+ // gracefully and grows with the generated output rather than staying at
995
+ // 1.2s. correct and slower beats fast and stale, which is why it is written
996
+ // this way, but if generate time ever becomes the complaint, record the
997
+ // hash after the consumer's formatter runs rather than weakening this.
998
+ Object.entries(generateCache).every(([cachedPath, cachedHash]) => {
999
+ if (!cachedPath.startsWith(generatedDir)) return true
1000
+ try {
1001
+ return hash(readFileSync(cachedPath, 'utf-8')) === cachedHash
1002
+ } catch {
1003
+ return false
1004
+ }
1005
+ }) &&
1006
+ existsSync(resolve(generatedDir, 'models.ts')) &&
1007
+ (!needsSqliteZeroSchema || existsSync(resolve(generatedDir, 'schema.ts')))
1030
1008
  ) {
1031
1009
  let counts: Partial<GenerateResult> = {}
1032
1010
  try {
@@ -1080,6 +1058,27 @@ export async function generate(options: GenerateOptions): Promise<GenerateResult
1080
1058
  ]
1081
1059
 
1082
1060
  let filesChanged = writeResults.filter(Boolean).length
1061
+ if (needsSqliteZeroSchema) {
1062
+ const membership = dataMembershipFromLayout(layout)
1063
+ const drizzleSchema = await generateDrizzleSchemaInputFileWithProject(
1064
+ {
1065
+ dir: baseDir,
1066
+ schemaImportPath: '../../database/schema',
1067
+ config,
1068
+ },
1069
+ project
1070
+ )
1071
+ const sqliteSchema = renderDrizzleZeroSqliteSchemaModule({
1072
+ importPath: './drizzleSchema',
1073
+ tableNames: membership.allTables,
1074
+ })
1075
+ if (writeFileIfChanged(resolve(generatedDir, 'drizzleSchema.ts'), drizzleSchema)) {
1076
+ filesChanged++
1077
+ }
1078
+ if (writeFileIfChanged(resolve(generatedDir, 'schema.ts'), sqliteSchema)) {
1079
+ filesChanged++
1080
+ }
1081
+ }
1083
1082
  let queryCount = 0
1084
1083
  let mutationCount = 0
1085
1084
 
@@ -1102,17 +1101,6 @@ export async function generate(options: GenerateOptions): Promise<GenerateResult
1102
1101
  importPath: string
1103
1102
  }> = []
1104
1103
 
1105
- let queryResolver: ReturnType<typeof createTypeResolver> | null = null
1106
- const getQueryResolver = () => {
1107
- if (!queryResolver) {
1108
- const allFiles = [
1109
- ...new Set(layout.namespaces.flatMap((namespace) => namespace.sourcePaths)),
1110
- ].map((path) => ({ path, content: readFileSync(path, 'utf-8') }))
1111
- queryResolver = createTypeResolver(ts, allFiles, baseDir)
1112
- }
1113
- return queryResolver
1114
- }
1115
-
1116
1104
  for (const namespace of layout.namespaces.filter(
1117
1105
  (namespace): namespace is typeof namespace & { queryPath: string } =>
1118
1106
  namespace.queryPath !== null
@@ -1120,15 +1108,14 @@ export async function generate(options: GenerateOptions): Promise<GenerateResult
1120
1108
  const filePath = namespace.queryPath
1121
1109
 
1122
1110
  try {
1123
- const content = readFileSync(filePath, 'utf-8')
1124
- const sourceFile = ts.createSourceFile(
1125
- filePath,
1126
- content,
1127
- ts.ScriptTarget.Latest,
1128
- true
1129
- )
1130
-
1131
- ts.forEachChild(sourceFile, (node) => {
1111
+ const sourceFile = project.sourceFile(filePath)
1112
+ const candidates: Array<{
1113
+ name: string
1114
+ paramType: string
1115
+ typeNode: ts.TypeNode | null
1116
+ }> = []
1117
+
1118
+ sourceFile.forEachChild((node) => {
1132
1119
  if (ts.isVariableStatement(node)) {
1133
1120
  const exportModifier = node.modifiers?.find(
1134
1121
  (m) => m.kind === ts.SyntaxKind.ExportKeyword
@@ -1149,41 +1136,37 @@ export async function generate(options: GenerateOptions): Promise<GenerateResult
1149
1136
  const param = params[0]!
1150
1137
  paramType = param.type?.getText(sourceFile) || 'unknown'
1151
1138
  }
1152
-
1153
- let valibotCode = typeToValibot(paramType)
1154
-
1155
- // if direct parse failed (unresolved reference), use TypeChecker
1156
- if (!valibotCode && params.length > 0 && params[0]!.type) {
1157
- const resolver = getQueryResolver()
1158
- const resolverSourceFile = resolver.program.getSourceFile(filePath)
1159
- if (resolverSourceFile) {
1160
- const resolvedType = resolveParamType(
1161
- ts,
1162
- resolver,
1163
- resolverSourceFile,
1164
- name,
1165
- 0
1166
- )
1167
- if (resolvedType) {
1168
- valibotCode = resolver.typeToValibot(resolvedType)
1169
- }
1170
- }
1171
- }
1172
-
1173
- if (valibotCode) {
1174
- allQueries.push({
1175
- name,
1176
- params: paramType,
1177
- valibotCode,
1178
- sourceFile: namespace.name,
1179
- importPath: namespaceImportPath(baseDir, filePath),
1180
- })
1181
- } else if (!silent && paramType !== 'void') {
1182
- console.error(`✗ ${name}: could not resolve type "${paramType}"`)
1183
- }
1139
+ candidates.push({ name, paramType, typeNode: params[0]?.type ?? null })
1184
1140
  }
1185
1141
  }
1186
1142
  })
1143
+
1144
+ const resolver = createTypeResolver(project.projectForFile(filePath))
1145
+ for (const candidate of candidates) {
1146
+ let valibotCode = typeToValibot(candidate.paramType)
1147
+ if (!valibotCode && candidate.typeNode) {
1148
+ const resolvedType = await resolveParamType(
1149
+ resolver,
1150
+ sourceFile,
1151
+ candidate.name,
1152
+ 0
1153
+ )
1154
+ if (resolvedType) valibotCode = await resolver.typeToValibot(resolvedType)
1155
+ }
1156
+ if (valibotCode) {
1157
+ allQueries.push({
1158
+ name: candidate.name,
1159
+ params: candidate.paramType,
1160
+ valibotCode,
1161
+ sourceFile: namespace.name,
1162
+ importPath: namespaceImportPath(baseDir, filePath),
1163
+ })
1164
+ } else if (!silent && candidate.paramType !== 'void') {
1165
+ console.error(
1166
+ `✗ ${candidate.name}: could not resolve type "${candidate.paramType}"`
1167
+ )
1168
+ }
1169
+ }
1187
1170
  } catch (err) {
1188
1171
  if (!silent) console.error(`Error processing ${filePath}:`, err)
1189
1172
  }
@@ -1244,7 +1227,6 @@ export async function generate(options: GenerateOptions): Promise<GenerateResult
1244
1227
  const allModelMutations: ModelMutations[] = []
1245
1228
 
1246
1229
  // first pass: extract mutations, note which have unresolved types
1247
- const mutationFiles: Array<{ path: string; content: string; baseName: string }> = []
1248
1230
  const unresolvedModels: Array<{ baseName: string; filePath: string }> = []
1249
1231
 
1250
1232
  for (const namespace of modelNamespaces) {
@@ -1254,16 +1236,8 @@ export async function generate(options: GenerateOptions): Promise<GenerateResult
1254
1236
  try {
1255
1237
  const content = readFileSync(filePath, 'utf-8')
1256
1238
 
1257
- mutationFiles.push({ path: filePath, content, baseName: fileBaseName })
1258
-
1259
- const sourceFile = ts.createSourceFile(
1260
- filePath,
1261
- content,
1262
- ts.ScriptTarget.Latest,
1263
- true
1264
- )
1239
+ const sourceFile = project.sourceFile(filePath)
1265
1240
  const result = extractMutationsFromModel(
1266
- ts,
1267
1241
  sourceFile,
1268
1242
  content,
1269
1243
  filePath,
@@ -1290,57 +1264,25 @@ export async function generate(options: GenerateOptions): Promise<GenerateResult
1290
1264
 
1291
1265
  // second pass: resolve imported types using TypeChecker
1292
1266
  if (unresolvedModels.length > 0) {
1293
- // build resolver with all ts files under baseDir for full import resolution
1294
- // (model files may import types from generated files, shared types, etc)
1295
- const collectTsFiles = (dir: string): Array<{ path: string; content: string }> => {
1296
- const results: Array<{ path: string; content: string }> = []
1297
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
1298
- const fullPath = resolve(dir, entry.name)
1299
- if (entry.isDirectory() && entry.name !== 'node_modules') {
1300
- results.push(...collectTsFiles(fullPath))
1301
- } else if (entry.isFile() && isGeneratorSourceFile(entry.name)) {
1302
- results.push({ path: fullPath, content: readFileSync(fullPath, 'utf-8') })
1303
- }
1304
- }
1305
- return results
1306
- }
1307
- const allFiles = [
1308
- ...new Map(
1309
- layout.sourceRoots
1310
- .flatMap(collectTsFiles)
1311
- .map((file) => [file.path, file] as const)
1312
- ).values(),
1313
- ]
1314
- const modelResolver = createTypeResolver(ts, allFiles, baseDir)
1315
-
1316
1267
  for (const { baseName, filePath } of unresolvedModels) {
1317
- const resolverSourceFile = modelResolver.program.getSourceFile(filePath)
1318
- if (!resolverSourceFile) continue
1319
-
1320
- const resolvedTypes = resolveMutationParamTypes(
1321
- ts,
1322
- modelResolver,
1323
- resolverSourceFile
1324
- )
1268
+ const sourceFile = project.sourceFile(filePath)
1269
+ const modelResolver = createTypeResolver(project.projectForFile(filePath))
1270
+ const resolvedTypes = await resolveMutationParamTypes(modelResolver, sourceFile)
1325
1271
  if (resolvedTypes.size === 0) continue
1272
+ const resolvedValibot = new Map<string, string>()
1273
+ for (const [name, type] of resolvedTypes) {
1274
+ resolvedValibot.set(name, await modelResolver.typeToValibot(type))
1275
+ }
1326
1276
 
1327
1277
  // re-extract with resolved types
1328
1278
  const content = readFileSync(filePath, 'utf-8')
1329
- const sourceFile = ts.createSourceFile(
1330
- filePath,
1331
- content,
1332
- ts.ScriptTarget.Latest,
1333
- true
1334
- )
1335
1279
  const result = extractMutationsFromModel(
1336
- ts,
1337
1280
  sourceFile,
1338
1281
  content,
1339
1282
  filePath,
1340
1283
  !!silent,
1341
1284
  typeToValibot,
1342
- resolvedTypes,
1343
- modelResolver.typeToValibot
1285
+ resolvedValibot
1344
1286
  )
1345
1287
 
1346
1288
  if (result) {
@@ -1429,12 +1371,17 @@ export async function watch(options: WatchOptions) {
1429
1371
  }
1430
1372
 
1431
1373
  const databaseDir = resolve(dirname(baseDir), 'database')
1432
- const ts = await import('typescript')
1433
- const layout = discoverDataLayout(
1434
- ts,
1435
- baseDir,
1436
- options.config ? resolve(options.config) : undefined
1437
- )
1374
+ const project = await loadGeneratorProject(baseDir)
1375
+ let layout: DataLayout
1376
+ try {
1377
+ layout = discoverDataLayout(
1378
+ project,
1379
+ baseDir,
1380
+ options.config ? resolve(options.config) : undefined
1381
+ )
1382
+ } finally {
1383
+ await project.close()
1384
+ }
1438
1385
  const watcher = chokidar.watch(
1439
1386
  [
1440
1387
  ...new Set([