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
@@ -1,7 +1,9 @@
1
- import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
1
+ import { existsSync, readdirSync, statSync } from 'node:fs'
2
2
  import { basename, dirname, relative, resolve, sep } from 'node:path'
3
3
 
4
- import type ts from 'typescript'
4
+ import * as ts from 'typescript/unstable/ast'
5
+
6
+ import type { NativeTypeScriptProject } from './native-typescript'
5
7
 
6
8
  export type DataNamespace = {
7
9
  name: string
@@ -53,10 +55,7 @@ type ParsedInstanceConfig = {
53
55
  supportTables: string[]
54
56
  }
55
57
 
56
- function propertyName(
57
- ts: typeof import('typescript'),
58
- name: ts.PropertyName
59
- ): string | null {
58
+ function propertyName(name: ts.PropertyName): string | null {
60
59
  if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
61
60
  return name.text
62
61
  }
@@ -64,7 +63,7 @@ function propertyName(
64
63
  }
65
64
 
66
65
  function readDataConfig(
67
- ts: typeof import('typescript'),
66
+ project: NativeTypeScriptProject,
68
67
  baseDir: string,
69
68
  configPath: string | undefined
70
69
  ): { path: string; instances: ParsedInstanceConfig[] } | null {
@@ -76,13 +75,8 @@ function readDataConfig(
76
75
  if (dirname(path) !== baseDir) {
77
76
  throw new Error(`[on-zero] ${path} must be at the data root ${baseDir}`)
78
77
  }
79
- const source = ts.createSourceFile(
80
- path,
81
- readFileSync(path, 'utf8'),
82
- ts.ScriptTarget.Latest,
83
- true
84
- )
85
- if (hasParseErrors(source)) throw new Error(`[on-zero] unable to parse ${path}`)
78
+ const source = project.sourceFile(path)
79
+ if (project.hasParseErrors(path)) throw new Error(`[on-zero] unable to parse ${path}`)
86
80
 
87
81
  let config: ts.ObjectLiteralExpression | null = null
88
82
  for (const statement of source.statements) {
@@ -108,7 +102,7 @@ function readDataConfig(
108
102
  if (!ts.isPropertyAssignment(option)) {
109
103
  throw new Error(`[on-zero] ${path} options must use explicit property assignments`)
110
104
  }
111
- const name = propertyName(ts, option.name)
105
+ const name = propertyName(option.name)
112
106
  if (!name) throw new Error(`[on-zero] ${path} has an unsupported option name`)
113
107
  if (rootOptions.has(name))
114
108
  throw new Error(`[on-zero] ${path} repeats option '${name}'`)
@@ -130,7 +124,7 @@ function readDataConfig(
130
124
  `[on-zero] ${path} instances must use explicit property assignments`
131
125
  )
132
126
  }
133
- const name = propertyName(ts, property.name)
127
+ const name = propertyName(property.name)
134
128
  if (!name) throw new Error(`[on-zero] ${path} has an unsupported instance name`)
135
129
  if (!ts.isObjectLiteralExpression(property.initializer)) {
136
130
  throw new Error(`[on-zero] instance '${name}' must be an object`)
@@ -146,7 +140,7 @@ function readDataConfig(
146
140
  if (!ts.isPropertyAssignment(option)) {
147
141
  throw new Error(`[on-zero] instance '${name}' options must be assignments`)
148
142
  }
149
- const optionName = propertyName(ts, option.name)
143
+ const optionName = propertyName(option.name)
150
144
  if (!optionName)
151
145
  throw new Error(`[on-zero] instance '${name}' has an invalid option`)
152
146
  if (seen.has(optionName)) {
@@ -215,18 +209,6 @@ function readDataConfig(
215
209
  return { path, instances }
216
210
  }
217
211
 
218
- function hasParseErrors(source: ts.SourceFile): boolean {
219
- // typescript keeps syntax diagnostics on SourceFile but does not expose them
220
- // in its public type.
221
- return Boolean(
222
- (
223
- source as unknown as {
224
- parseDiagnostics?: readonly unknown[]
225
- }
226
- ).parseDiagnostics?.length
227
- )
228
- }
229
-
230
212
  /**
231
213
  * Which kinds of data exports a single-file namespace declares.
232
214
  *
@@ -241,18 +223,13 @@ function hasParseErrors(source: ts.SourceFile): boolean {
241
223
  type NamespaceExportKinds = { model: boolean; query: boolean }
242
224
 
243
225
  function namespaceExportKinds(
244
- ts: typeof import('typescript'),
226
+ project: NativeTypeScriptProject,
245
227
  baseDir: string,
246
228
  path: string
247
229
  ): NamespaceExportKinds {
248
230
  const none: NamespaceExportKinds = { model: false, query: false }
249
- const source = ts.createSourceFile(
250
- path,
251
- readFileSync(path, 'utf8'),
252
- ts.ScriptTarget.Latest,
253
- true
254
- )
255
- if (hasParseErrors(source)) {
231
+ const source = project.sourceFile(path)
232
+ if (project.hasParseErrors(path)) {
256
233
  const displayPath = relative(dirname(baseDir), path).split(sep).join('/')
257
234
  console.warn(`[on-zero] ignoring ${displayPath}: no recognized data exports`)
258
235
  return none
@@ -319,7 +296,7 @@ function namespaceExportKinds(
319
296
  }
320
297
  }
321
298
  let found = false
322
- ts.forEachChild(node, (child) => {
299
+ node.forEachChild((child) => {
323
300
  if (!found && reachesQuery(child)) found = true
324
301
  })
325
302
  return found
@@ -330,16 +307,11 @@ function namespaceExportKinds(
330
307
  }
331
308
 
332
309
  function mutationTable(
333
- ts: typeof import('typescript'),
310
+ project: NativeTypeScriptProject,
334
311
  path: string,
335
312
  namespace: string
336
313
  ): string {
337
- const source = ts.createSourceFile(
338
- path,
339
- readFileSync(path, 'utf8'),
340
- ts.ScriptTarget.Latest,
341
- true
342
- )
314
+ const source = project.sourceFile(path)
343
315
  let schemaTable: string | null = null
344
316
 
345
317
  for (const statement of source.statements) {
@@ -358,7 +330,7 @@ function mutationTable(
358
330
  ts.isCallExpression(declaration.initializer)
359
331
  ) {
360
332
  const firstArgument = declaration.initializer.arguments[0]
361
- if (firstArgument && ts.isStringLiteralLike(firstArgument)) {
333
+ if (firstArgument && ts.isStringLiteral(firstArgument)) {
362
334
  return firstArgument.text
363
335
  }
364
336
  }
@@ -368,12 +340,12 @@ function mutationTable(
368
340
  ts.isCallExpression(node) &&
369
341
  node.expression.getText(source) === 'table' &&
370
342
  node.arguments[0] &&
371
- ts.isStringLiteralLike(node.arguments[0])
343
+ ts.isStringLiteral(node.arguments[0])
372
344
  ) {
373
345
  schemaTable = node.arguments[0].text
374
346
  return
375
347
  }
376
- ts.forEachChild(node, visit)
348
+ node.forEachChild(visit)
377
349
  }
378
350
  visit(declaration.initializer)
379
351
  }
@@ -383,7 +355,7 @@ function mutationTable(
383
355
  }
384
356
 
385
357
  function discoverNamespaces(
386
- ts: typeof import('typescript'),
358
+ project: NativeTypeScriptProject,
387
359
  baseDir: string,
388
360
  instance: DataInstance,
389
361
  instanceDirs: Set<string>
@@ -395,7 +367,7 @@ function discoverNamespaces(
395
367
  if (entry.isFile()) {
396
368
  if (!isSourceFile(entry.name) || entry.name === 'on-zero.config.ts') continue
397
369
  const path = resolve(instance.dir, entry.name)
398
- const kinds = namespaceExportKinds(ts, baseDir, path)
370
+ const kinds = namespaceExportKinds(project, baseDir, path)
399
371
  if (!kinds.model && !kinds.query) continue
400
372
  const name = basename(entry.name, '.ts')
401
373
  namespaces.push({
@@ -406,7 +378,7 @@ function discoverNamespaces(
406
378
  // missing mutations.ts leaves modelPath null
407
379
  modelPath: kinds.model ? path : null,
408
380
  aggregatePath: null,
409
- table: kinds.model ? mutationTable(ts, path, name) : null,
381
+ table: kinds.model ? mutationTable(project, path, name) : null,
410
382
  sourcePaths: [path],
411
383
  })
412
384
  continue
@@ -439,7 +411,7 @@ function discoverNamespaces(
439
411
  queryPath: hasQueries ? queryPath : null,
440
412
  modelPath: hasMutations ? modelPath : null,
441
413
  aggregatePath: hasAggregates ? aggregatePath : null,
442
- table: hasMutations ? mutationTable(ts, modelPath, entry.name) : null,
414
+ table: hasMutations ? mutationTable(project, modelPath, entry.name) : null,
443
415
  sourcePaths: [
444
416
  hasQueries && queryPath,
445
417
  hasMutations && modelPath,
@@ -471,23 +443,18 @@ function metadataPaths(baseDir: string): string[] {
471
443
  }
472
444
 
473
445
  function relationTargets(
474
- ts: typeof import('typescript'),
446
+ project: NativeTypeScriptProject,
475
447
  paths: string[]
476
448
  ): Map<string, Map<string, string>> {
477
449
  const relations = new Map<string, Map<string, string>>()
478
450
  for (const path of paths.filter((path) => basename(path) === 'relations.ts')) {
479
- const source = ts.createSourceFile(
480
- path,
481
- readFileSync(path, 'utf8'),
482
- ts.ScriptTarget.Latest,
483
- true
484
- )
451
+ const source = project.sourceFile(path)
485
452
  const visit = (node: ts.Node) => {
486
453
  if (
487
454
  !ts.isCallExpression(node) ||
488
455
  node.expression.getText(source) !== 'defineRelations'
489
456
  ) {
490
- ts.forEachChild(node, visit)
457
+ node.forEachChild(visit)
491
458
  return
492
459
  }
493
460
  const factory = node.arguments[1]
@@ -527,7 +494,7 @@ function relationTargets(
527
494
  }
528
495
 
529
496
  function tableColumns(
530
- ts: typeof import('typescript'),
497
+ project: NativeTypeScriptProject,
531
498
  paths: string[],
532
499
  namespaces: DataNamespace[]
533
500
  ): Map<string, Set<string>> {
@@ -537,26 +504,21 @@ function tableColumns(
537
504
  ...namespaces.flatMap((namespace) => namespace.sourcePaths),
538
505
  ])
539
506
  for (const path of sources) {
540
- const source = ts.createSourceFile(
541
- path,
542
- readFileSync(path, 'utf8'),
543
- ts.ScriptTarget.Latest,
544
- true
545
- )
507
+ const source = project.sourceFile(path)
546
508
  const visit = (node: ts.Node) => {
547
509
  if (
548
510
  !ts.isVariableDeclaration(node) ||
549
511
  !ts.isIdentifier(node.name) ||
550
512
  !node.initializer
551
513
  ) {
552
- ts.forEachChild(node, visit)
514
+ node.forEachChild(visit)
553
515
  return
554
516
  }
555
517
  const tableNames = new Set([node.name.text])
556
518
  let foundColumns: ts.ObjectLiteralExpression | null = null
557
519
  const inspect = (candidate: ts.Node) => {
558
520
  if (!ts.isCallExpression(candidate)) {
559
- ts.forEachChild(candidate, inspect)
521
+ candidate.forEachChild(inspect)
560
522
  return
561
523
  }
562
524
  for (const argument of candidate.arguments) {
@@ -574,12 +536,16 @@ function tableColumns(
574
536
  if (foundColumns) {
575
537
  const names = new Set(
576
538
  (foundColumns as ts.ObjectLiteralExpression).properties
577
- .map((property) => property.name?.getText(source).replace(/^['"]|['"]$/g, ''))
539
+ .map((property) =>
540
+ ts.isSpreadAssignment(property)
541
+ ? null
542
+ : property.name?.getText(source).replace(/^['"]|['"]$/g, '')
543
+ )
578
544
  .filter((name): name is string => Boolean(name))
579
545
  )
580
546
  for (const tableName of tableNames) columns.set(tableName, names)
581
547
  }
582
- ts.forEachChild(node, visit)
548
+ node.forEachChild(visit)
583
549
  }
584
550
  visit(source)
585
551
  }
@@ -587,17 +553,12 @@ function tableColumns(
587
553
  }
588
554
 
589
555
  function queriedTables(
590
- ts: typeof import('typescript'),
556
+ project: NativeTypeScriptProject,
591
557
  namespace: DataNamespace,
592
558
  relations: Map<string, Map<string, string>>
593
559
  ): Array<{ table: string; query: string; root: boolean }> {
594
560
  if (!namespace.queryPath) return []
595
- const source = ts.createSourceFile(
596
- namespace.queryPath,
597
- readFileSync(namespace.queryPath, 'utf8'),
598
- ts.ScriptTarget.Latest,
599
- true
600
- )
561
+ const source = project.sourceFile(namespace.queryPath)
601
562
  const reached: Array<{ table: string; query: string; root: boolean }> = []
602
563
  const functions = new Map<string, ts.ConciseBody>()
603
564
  const exported = new Set<string>()
@@ -679,7 +640,7 @@ function queriedTables(
679
640
  visiting.delete(key)
680
641
  }
681
642
  }
682
- ts.forEachChild(node, (child) => visit(child, currentTable, query))
643
+ node.forEachChild((child) => visit(child, currentTable, query))
683
644
  }
684
645
 
685
646
  for (const name of exported) {
@@ -693,16 +654,11 @@ function queriedTables(
693
654
  }
694
655
 
695
656
  function aggregateTables(
696
- ts: typeof import('typescript'),
657
+ project: NativeTypeScriptProject,
697
658
  namespace: DataNamespace
698
659
  ): string[] {
699
660
  if (!namespace.aggregatePath) return []
700
- const source = ts.createSourceFile(
701
- namespace.aggregatePath,
702
- readFileSync(namespace.aggregatePath, 'utf8'),
703
- ts.ScriptTarget.Latest,
704
- true
705
- )
661
+ const source = project.sourceFile(namespace.aggregatePath)
706
662
  const unwrap = (node: ts.Expression): ts.Expression => {
707
663
  let current = node
708
664
  while (
@@ -754,14 +710,14 @@ function aggregateTables(
754
710
  const field = definition.properties.find(
755
711
  (candidate): candidate is ts.PropertyAssignment =>
756
712
  ts.isPropertyAssignment(candidate) &&
757
- propertyName(ts, candidate.name) === fieldName
713
+ propertyName(candidate.name) === fieldName
758
714
  )
759
- if (!field || !ts.isStringLiteralLike(unwrap(field.initializer))) {
715
+ if (!field || !ts.isStringLiteral(unwrap(field.initializer))) {
760
716
  throw new Error(
761
717
  `[on-zero] ${namespace.aggregatePath} aggregate ${property.name.getText(source)} ${fieldName} must be a string literal`
762
718
  )
763
719
  }
764
- tables.add((unwrap(field.initializer) as ts.StringLiteralLike).text)
720
+ tables.add((unwrap(field.initializer) as ts.StringLiteral).text)
765
721
  }
766
722
  }
767
723
  }
@@ -770,7 +726,7 @@ function aggregateTables(
770
726
  }
771
727
 
772
728
  function mutationSupportTables(
773
- ts: typeof import('typescript'),
729
+ project: NativeTypeScriptProject,
774
730
  baseDir: string,
775
731
  sourceRoots: string[],
776
732
  namespace: DataNamespace
@@ -782,12 +738,7 @@ function mutationSupportTables(
782
738
  const scan = (path: string) => {
783
739
  if (visited.has(path)) return
784
740
  visited.add(path)
785
- const source = ts.createSourceFile(
786
- path,
787
- readFileSync(path, 'utf8'),
788
- ts.ScriptTarget.Latest,
789
- true
790
- )
741
+ const source = project.sourceFile(path)
791
742
 
792
743
  const visit = (node: ts.Node) => {
793
744
  if (
@@ -831,7 +782,7 @@ function mutationSupportTables(
831
782
  }
832
783
  }
833
784
 
834
- ts.forEachChild(node, visit)
785
+ node.forEachChild(visit)
835
786
  }
836
787
  visit(source)
837
788
  }
@@ -861,7 +812,7 @@ function assertNoInstanceFiles(roots: string[]) {
861
812
  }
862
813
 
863
814
  function assertNoUnclaimedNamespaces(
864
- ts: typeof import('typescript'),
815
+ project: NativeTypeScriptProject,
865
816
  baseDir: string,
866
817
  configPath: string,
867
818
  instanceDirs: string[]
@@ -883,7 +834,7 @@ function assertNoUnclaimedNamespaces(
883
834
  ) {
884
835
  continue
885
836
  }
886
- const kinds = namespaceExportKinds(ts, baseDir, path)
837
+ const kinds = namespaceExportKinds(project, baseDir, path)
887
838
  if (kinds.model || kinds.query) {
888
839
  throw new Error(
889
840
  `[on-zero] data namespace ${path} is outside every instance directory declared in ${configPath}`
@@ -895,11 +846,11 @@ function assertNoUnclaimedNamespaces(
895
846
  }
896
847
 
897
848
  export function discoverDataLayout(
898
- ts: typeof import('typescript'),
849
+ project: NativeTypeScriptProject,
899
850
  baseDir: string,
900
851
  configPath?: string
901
852
  ): DataLayout {
902
- const config = readDataConfig(ts, baseDir, configPath)
853
+ const config = readDataConfig(project, baseDir, configPath)
903
854
  const configured =
904
855
  config?.instances ??
905
856
  ([
@@ -911,7 +862,7 @@ export function discoverDataLayout(
911
862
  assertNoInstanceFiles(sourceRoots)
912
863
  if (config) {
913
864
  assertNoUnclaimedNamespaces(
914
- ts,
865
+ project,
915
866
  baseDir,
916
867
  config.path,
917
868
  configured.map((instance) => instance.dir)
@@ -930,7 +881,7 @@ export function discoverDataLayout(
930
881
  const instanceDirs = new Set(instances.map((instance) => instance.dir))
931
882
 
932
883
  for (const instance of instances) {
933
- instance.namespaces = discoverNamespaces(ts, baseDir, instance, instanceDirs)
884
+ instance.namespaces = discoverNamespaces(project, baseDir, instance, instanceDirs)
934
885
  }
935
886
  const namespaces = instances.flatMap((instance) => instance.namespaces)
936
887
  const namespaceOwners = new Map<string, string>()
@@ -956,8 +907,8 @@ export function discoverDataLayout(
956
907
  }
957
908
 
958
909
  const metadata = metadataPaths(baseDir)
959
- const relations = relationTargets(ts, metadata)
960
- const columns = tableColumns(ts, metadata, namespaces)
910
+ const relations = relationTargets(project, metadata)
911
+ const columns = tableColumns(project, metadata, namespaces)
961
912
  const relatedOwners = new Map<string, string>()
962
913
  for (const instance of instances) {
963
914
  const tables = new Set(
@@ -967,7 +918,7 @@ export function discoverDataLayout(
967
918
  )
968
919
  const syncTables = new Set(tables)
969
920
  for (const namespace of instance.namespaces) {
970
- for (const table of aggregateTables(ts, namespace)) {
921
+ for (const table of aggregateTables(project, namespace)) {
971
922
  const owner = tableOwners.get(table) ?? relatedOwners.get(table)
972
923
  if (owner && owner !== instance.name) {
973
924
  throw new Error(
@@ -979,7 +930,7 @@ export function discoverDataLayout(
979
930
  tables.add(table)
980
931
  syncTables.add(table)
981
932
  }
982
- for (const reached of queriedTables(ts, namespace, relations)) {
933
+ for (const reached of queriedTables(project, namespace, relations)) {
983
934
  const owner = tableOwners.get(reached.table) ?? relatedOwners.get(reached.table)
984
935
  if (owner && owner !== instance.name) {
985
936
  throw new Error(
@@ -1012,7 +963,12 @@ export function discoverDataLayout(
1012
963
  // instance's change log or every later pull throws on it.
1013
964
  const supportTables = new Set<string>(instance.declaredSupportTables)
1014
965
  for (const namespace of instance.namespaces) {
1015
- for (const table of mutationSupportTables(ts, baseDir, sourceRoots, namespace)) {
966
+ for (const table of mutationSupportTables(
967
+ project,
968
+ baseDir,
969
+ sourceRoots,
970
+ namespace
971
+ )) {
1016
972
  if (tableOwners.has(table) || relatedOwners.has(table)) continue
1017
973
  if (!instance.syncTables.includes(table)) {
1018
974
  supportTables.add(table)
@@ -510,4 +510,53 @@ describe('generateLite', () => {
510
510
  // an explicit `{ crud: true }` is the same as omitting the options object
511
511
  expect(run(true).files['syncedMutations.ts']).toBe(validators)
512
512
  })
513
+
514
+ test('writes drizzle-zero sqlite schema from database/schema.ts', () => {
515
+ const files = {
516
+ [`${DIR}/post.ts`]: `export const posts = () => zql.post`,
517
+ ['/proj/src/database/schema.ts']: `export const post = sqliteTable('post', { id: text('id') })`,
518
+ ['/proj/src/database/relations.ts']: `export const relations = defineRelations(schema, (r) => ({ post: { comments: r.many.comment({}) } }))`,
519
+ }
520
+ const empty = { mutations: [], queries: [] }
521
+ const result = generateLite({
522
+ files,
523
+ dir: DIR,
524
+ parse: makeParse({
525
+ [`${DIR}/post.ts`]: {
526
+ mutations: [],
527
+ queries: [
528
+ {
529
+ name: 'posts',
530
+ rootTable: 'post',
531
+ paramTypeText: null,
532
+ relatedPaths: [],
533
+ },
534
+ ],
535
+ },
536
+ ['/proj/src/database/schema.ts']: {
537
+ ...empty,
538
+ tables: [{ name: 'post', columns: ['id'] }],
539
+ },
540
+ ['/proj/src/database/relations.ts']: {
541
+ ...empty,
542
+ relations: [
543
+ {
544
+ sourceTable: 'post',
545
+ name: 'comments',
546
+ targetTable: 'comment',
547
+ sourceText: 'comments: r.many.comment({})',
548
+ },
549
+ ],
550
+ },
551
+ }),
552
+ })
553
+ expect(result.files['drizzleSchema.ts']).toContain(
554
+ 'export { post } from "../../database/schema"'
555
+ )
556
+ expect(result.files['schema.ts']).toContain(
557
+ 'import * as drizzleSchema from "./drizzleSchema"'
558
+ )
559
+ expect(result.files['schema.ts']).toContain('export type Post =')
560
+ expect(result.files['drizzleSchema.ts']).not.toContain('comments:')
561
+ })
513
562
  })
@@ -27,6 +27,8 @@ import {
27
27
  generateTypesFile,
28
28
  parseColumnType,
29
29
  parseTypeString,
30
+ renderDrizzleSchemaInputModule,
31
+ renderDrizzleZeroSqliteSchemaModule,
30
32
  } from './generate-helpers'
31
33
 
32
34
  import type { ModelMutations, SchemaColumn } from './generate-helpers'
@@ -91,6 +93,8 @@ export type LiteRelationInfo = {
91
93
  sourceTable: string
92
94
  name: string
93
95
  targetTable: string
96
+ // original property text from defineRelations, used to emit drizzleSchema.ts
97
+ sourceText?: string
94
98
  }
95
99
 
96
100
  export type LiteTableInfo = {
@@ -468,6 +472,7 @@ export function generateLite(opts: LiteGenerateOptions): LiteGenerateResult {
468
472
  aggregateTables.set(namespace.name, [...new Set(parsed.aggregateTables ?? [])])
469
473
  }
470
474
  const relations = new Map<string, Map<string, string>>()
475
+ const drizzleRelations: LiteRelationInfo[] = []
471
476
  const tableColumns = new Map<string, Set<string>>()
472
477
  for (const path of Object.keys(files).filter(
473
478
  (path) =>
@@ -479,6 +484,7 @@ export function generateLite(opts: LiteGenerateOptions): LiteGenerateResult {
479
484
  const tableRelations = relations.get(relation.sourceTable) ?? new Map()
480
485
  tableRelations.set(relation.name, relation.targetTable)
481
486
  relations.set(relation.sourceTable, tableRelations)
487
+ drizzleRelations.push(relation)
482
488
  }
483
489
  for (const table of parsed.tables ?? []) {
484
490
  tableColumns.set(table.name, new Set(table.columns))
@@ -828,6 +834,44 @@ export function generateLite(opts: LiteGenerateOptions): LiteGenerateResult {
828
834
  )
829
835
  }
830
836
 
837
+ const allTables = [
838
+ ...new Set(
839
+ instances.flatMap((instance) => [
840
+ ...(syncTables.get(instance.name) ?? []),
841
+ ...(supportTables.get(instance.name) ?? []),
842
+ ])
843
+ ),
844
+ ].sort()
845
+ const hasDatabaseSchema = Object.keys(files).some((path) =>
846
+ /\/database\/schema\.ts$/.test(path)
847
+ )
848
+ if (hasDatabaseSchema && allTables.length > 0) {
849
+ const included = new Set(allTables)
850
+ const relationTables = new Map<string, string[]>()
851
+ for (const relation of drizzleRelations) {
852
+ if (!included.has(relation.sourceTable) || !included.has(relation.targetTable)) {
853
+ continue
854
+ }
855
+ const properties = relationTables.get(relation.sourceTable) ?? []
856
+ properties.push(
857
+ relation.sourceText ?? `${relation.name}: r.many.${relation.targetTable}({})`
858
+ )
859
+ relationTables.set(relation.sourceTable, properties)
860
+ }
861
+ out['drizzleSchema.ts'] = renderDrizzleSchemaInputModule({
862
+ schemaImportPath: '../../database/schema',
863
+ tableNames: allTables,
864
+ relationTables: [...relationTables.entries()].map(([key, properties]) => ({
865
+ key,
866
+ properties,
867
+ })),
868
+ })
869
+ out['schema.ts'] = renderDrizzleZeroSqliteSchemaModule({
870
+ importPath: './drizzleSchema',
871
+ tableNames: allTables,
872
+ })
873
+ }
874
+
831
875
  // count mutations the same way `generate()` does: one per generated crud slot
832
876
  // plus non-crud custom mutations (the crud-ops set is excluded when hasCRUD).
833
877
  let mutationCount = 0
@@ -87,6 +87,58 @@ describe('generate', () => {
87
87
  expect(existsSync(syncedQueriesPath)).toBe(true)
88
88
  })
89
89
 
90
+ test('rewrites a generated file whose contents changed underneath it', async () => {
91
+ writeFileSync(
92
+ join(testDir, 'post/mutations.ts'),
93
+ `export const schema = table('post', { id: string() })`
94
+ )
95
+ await generate({ dir: testDir, silent: true })
96
+
97
+ // a checkout, merge or revert replaces generated output without telling the
98
+ // cache, which still records the hash this generator last wrote there. the
99
+ // file exists and the recomputed content is unchanged, so every signal
100
+ // except the bytes themselves says there is nothing to do.
101
+ const modelsPath = join(testDir, 'generated/models.ts')
102
+ const generated = readFileSync(modelsPath, 'utf-8')
103
+ writeFileSync(modelsPath, '// replaced by a checkout\n')
104
+
105
+ await generate({ dir: testDir, silent: true })
106
+
107
+ expect(readFileSync(modelsPath, 'utf-8')).toBe(generated)
108
+ })
109
+
110
+ test('writes drizzle-zero sqlite schema from database/schema.ts', async () => {
111
+ const dataDir = join(testDir, 'src/data')
112
+ writeFileSync(join(dataDir, 'post.ts'), `export const posts = () => zql.post`)
113
+ writeFileSync(
114
+ join(testDir, 'src/database/schema.ts'),
115
+ `
116
+ import { sqliteTable, text } from 'drizzle-orm/sqlite-core'
117
+ export const post = sqliteTable('post', { id: text('id').primaryKey() })
118
+ `
119
+ )
120
+ writeFileSync(
121
+ join(testDir, 'src/database/relations.ts'),
122
+ `
123
+ export const relations = defineRelations(schema, (r) => ({
124
+ post: { comments: r.many.comment({}) },
125
+ }))
126
+ `
127
+ )
128
+
129
+ await generate({ dir: dataDir, silent: true })
130
+
131
+ expect(readFileSync(join(dataDir, 'generated/drizzleSchema.ts'), 'utf8')).toContain(
132
+ 'export { post } from "../../database/schema"'
133
+ )
134
+ expect(readFileSync(join(dataDir, 'generated/schema.ts'), 'utf8')).toContain(
135
+ 'import * as drizzleSchema from "./drizzleSchema"'
136
+ )
137
+ expect(readFileSync(join(dataDir, 'generated/schema.ts'), 'utf8')).toContain(
138
+ 'export type Post ='
139
+ )
140
+ })
141
+
90
142
  test('generates every crud slot for a default table registration', async () => {
91
143
  writeFileSync(
92
144
  join(testDir, 'post/mutations.ts'),