on-zero 0.1.21 → 0.1.23

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 (68) hide show
  1. package/dist/cjs/cli.cjs +17 -420
  2. package/dist/cjs/cli.js +7 -398
  3. package/dist/cjs/cli.js.map +2 -2
  4. package/dist/cjs/cli.native.js +15 -514
  5. package/dist/cjs/cli.native.js.map +1 -1
  6. package/dist/cjs/generate.cjs +370 -0
  7. package/dist/cjs/generate.js +339 -0
  8. package/dist/cjs/generate.js.map +6 -0
  9. package/dist/cjs/generate.native.js +464 -0
  10. package/dist/cjs/generate.native.js.map +1 -0
  11. package/dist/cjs/helpers/createMutators.cjs +4 -3
  12. package/dist/cjs/helpers/createMutators.js +12 -9
  13. package/dist/cjs/helpers/createMutators.js.map +1 -1
  14. package/dist/cjs/helpers/createMutators.native.js +25 -21
  15. package/dist/cjs/helpers/createMutators.native.js.map +1 -1
  16. package/dist/cjs/mutations.cjs +34 -4
  17. package/dist/cjs/mutations.js +29 -4
  18. package/dist/cjs/mutations.js.map +1 -1
  19. package/dist/cjs/mutations.native.js +36 -4
  20. package/dist/cjs/mutations.native.js.map +1 -1
  21. package/dist/cjs/vite-plugin.cjs +84 -0
  22. package/dist/cjs/vite-plugin.js +86 -0
  23. package/dist/cjs/vite-plugin.js.map +6 -0
  24. package/dist/cjs/vite-plugin.native.js +99 -0
  25. package/dist/cjs/vite-plugin.native.js.map +1 -0
  26. package/dist/esm/cli.js +8 -384
  27. package/dist/esm/cli.js.map +2 -2
  28. package/dist/esm/cli.mjs +17 -398
  29. package/dist/esm/cli.mjs.map +1 -1
  30. package/dist/esm/cli.native.js +15 -492
  31. package/dist/esm/cli.native.js.map +1 -1
  32. package/dist/esm/generate.js +317 -0
  33. package/dist/esm/generate.js.map +6 -0
  34. package/dist/esm/generate.mjs +335 -0
  35. package/dist/esm/generate.mjs.map +1 -0
  36. package/dist/esm/generate.native.js +426 -0
  37. package/dist/esm/generate.native.js.map +1 -0
  38. package/dist/esm/helpers/createMutators.js +12 -9
  39. package/dist/esm/helpers/createMutators.js.map +1 -1
  40. package/dist/esm/helpers/createMutators.mjs +4 -3
  41. package/dist/esm/helpers/createMutators.mjs.map +1 -1
  42. package/dist/esm/helpers/createMutators.native.js +25 -21
  43. package/dist/esm/helpers/createMutators.native.js.map +1 -1
  44. package/dist/esm/mutations.js +29 -4
  45. package/dist/esm/mutations.js.map +1 -1
  46. package/dist/esm/mutations.mjs +34 -4
  47. package/dist/esm/mutations.mjs.map +1 -1
  48. package/dist/esm/mutations.native.js +35 -3
  49. package/dist/esm/mutations.native.js.map +1 -1
  50. package/dist/esm/vite-plugin.js +71 -0
  51. package/dist/esm/vite-plugin.js.map +6 -0
  52. package/dist/esm/vite-plugin.mjs +59 -0
  53. package/dist/esm/vite-plugin.mjs.map +1 -0
  54. package/dist/esm/vite-plugin.native.js +71 -0
  55. package/dist/esm/vite-plugin.native.js.map +1 -0
  56. package/package.json +7 -2
  57. package/readme.md +42 -32
  58. package/src/cli.ts +9 -638
  59. package/src/generate.ts +490 -0
  60. package/src/helpers/createMutators.ts +14 -8
  61. package/src/mutations.ts +57 -4
  62. package/src/vite-plugin.ts +110 -0
  63. package/types/generate.d.ts +21 -0
  64. package/types/generate.d.ts.map +1 -0
  65. package/types/helpers/createMutators.d.ts.map +1 -1
  66. package/types/mutations.d.ts.map +1 -1
  67. package/types/vite-plugin.d.ts +16 -0
  68. package/types/vite-plugin.d.ts.map +1 -0
package/src/cli.ts CHANGED
@@ -1,164 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { createHash } from 'node:crypto'
3
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
4
- import { basename, resolve } from 'node:path'
2
+ import { resolve } from 'node:path'
5
3
 
6
- import { ModelToValibot } from '@sinclair/typebox-codegen/model'
7
- import { TypeScriptToModel } from '@sinclair/typebox-codegen/typescript'
8
4
  import { defineCommand, runMain } from 'citty'
9
- import * as ts from 'typescript'
10
5
 
11
- const hash = (s: string) => createHash('sha256').update(s).digest('hex')
6
+ import { generate, watch } from './generate'
12
7
 
13
- /**
14
- * cache of raw generated content hashes so we can skip writes when
15
- * a formatter (--after) rewrites files to a different style.
16
- * without this, the raw output never matches the formatted file on disk
17
- * and every regeneration triggers unnecessary file watcher events.
18
- */
19
- let generateCache: Record<string, string> = {}
20
- let generateCachePath = ''
21
-
22
- function getCacheDir() {
23
- // walk up from cwd to find nearest node_modules
24
- let dir = process.cwd()
25
- while (dir !== '/') {
26
- const nm = resolve(dir, 'node_modules')
27
- if (existsSync(nm)) {
28
- const cacheDir = resolve(nm, '.on-zero')
29
- if (!existsSync(cacheDir)) {
30
- mkdirSync(cacheDir, { recursive: true })
31
- }
32
- return cacheDir
33
- }
34
- dir = resolve(dir, '..')
35
- }
36
- return null
37
- }
38
-
39
- function loadCache() {
40
- const cacheDir = getCacheDir()
41
- if (!cacheDir) return
42
- generateCachePath = resolve(cacheDir, 'generate-cache.json')
43
- try {
44
- generateCache = JSON.parse(readFileSync(generateCachePath, 'utf-8'))
45
- } catch {
46
- generateCache = {}
47
- }
48
- }
49
-
50
- function saveCache() {
51
- if (generateCachePath) {
52
- writeFileSync(generateCachePath, JSON.stringify(generateCache) + '\n', 'utf-8')
53
- }
54
- }
55
-
56
- /**
57
- * Write file only if the content has changed.
58
- * Uses a hash cache of raw generated content to avoid false positives
59
- * when a formatter rewrites files to a different style.
60
- */
61
- function writeFileIfChanged(filePath: string, content: string): boolean {
62
- const contentHash = hash(content)
63
- const cachedHash = generateCache[filePath]
64
-
65
- if (cachedHash === contentHash && existsSync(filePath)) {
66
- return false // raw content unchanged, formatted file on disk is fine
67
- }
68
-
69
- writeFileSync(filePath, content, 'utf-8')
70
- generateCache[filePath] = contentHash
71
- return true // file was written
72
- }
73
-
74
- const generateQueries = defineCommand({
75
- meta: {
76
- name: 'generate-queries',
77
- description: 'Generate server-side query validators from TypeScript query functions',
78
- },
79
- args: {
80
- dir: {
81
- type: 'positional',
82
- description: 'Directory containing query files',
83
- required: false,
84
- default: '.',
85
- },
86
- },
87
- async run({ args }) {
88
- const dir = resolve(args.dir)
89
-
90
- const { readdirSync } = await import('node:fs')
91
-
92
- const files = readdirSync(dir).filter((f) => f.endsWith('.ts'))
93
-
94
- const allQueries: Array<{ name: string; params: string; valibotCode: string }> = []
95
-
96
- // process files in parallel
97
- const results = await Promise.all(
98
- files.map(async (file) => {
99
- const filePath = resolve(dir, file)
100
- const queries: typeof allQueries = []
101
-
102
- try {
103
- const content = readFileSync(filePath, 'utf-8')
104
-
105
- const sourceFile = ts.createSourceFile(
106
- filePath,
107
- content,
108
- ts.ScriptTarget.Latest,
109
- true
110
- )
111
-
112
- ts.forEachChild(sourceFile, (node) => {
113
- if (ts.isVariableStatement(node)) {
114
- const exportModifier = node.modifiers?.find(
115
- (m) => m.kind === ts.SyntaxKind.ExportKeyword
116
- )
117
- if (!exportModifier) return
118
-
119
- const declaration = node.declarationList.declarations[0]
120
- if (!declaration || !ts.isVariableDeclaration(declaration)) return
121
-
122
- const name = declaration.name.getText(sourceFile)
123
-
124
- if (
125
- declaration.initializer &&
126
- ts.isArrowFunction(declaration.initializer)
127
- ) {
128
- const params = declaration.initializer.parameters
129
- let paramType = 'void'
130
-
131
- if (params.length > 0) {
132
- const param = params[0]!
133
- paramType = param.type?.getText(sourceFile) || 'unknown'
134
- }
135
-
136
- try {
137
- const typeString = `type QueryParams = ${paramType}`
138
- const model = TypeScriptToModel.Generate(typeString)
139
- const valibotCode = ModelToValibot.Generate(model)
140
-
141
- queries.push({ name, params: paramType, valibotCode })
142
- } catch (err) {
143
- console.error(`✗ ${name}: ${err}`)
144
- }
145
- }
146
- }
147
- })
148
- } catch (err) {
149
- console.error(`Error processing ${file}:`, err)
150
- }
151
-
152
- return queries
153
- })
154
- )
155
-
156
- allQueries.push(...results.flat())
157
- console.info(`✓ ${allQueries.length} query validators`)
158
- },
159
- })
160
-
161
- const generate = defineCommand({
8
+ const generateCommand = defineCommand({
162
9
  meta: {
163
10
  name: 'generate',
164
11
  description: 'Generate models, types, tables, and query validators',
@@ -182,502 +29,26 @@ const generate = defineCommand({
182
29
  required: false,
183
30
  },
184
31
  },
185
- async run({ args }) {
186
- const baseDir = resolve(args.dir)
187
- const modelsDir = resolve(baseDir, 'models')
188
- const generatedDir = resolve(baseDir, 'generated')
189
- const queriesDir = resolve(baseDir, 'queries')
190
-
191
- const runGenerate = async (options?: { silent?: boolean; runAfter?: boolean }) => {
192
- const silent = options?.silent ?? false
193
- const runAfter = options?.runAfter ?? !silent
194
- // ensure generated dir exists
195
- if (!existsSync(generatedDir)) {
196
- mkdirSync(generatedDir, { recursive: true })
197
- }
198
-
199
- loadCache()
200
-
201
- // read all model files and check for schemas in parallel
202
- const allModelFiles = readdirSync(modelsDir)
203
- .filter((f) => f.endsWith('.ts'))
204
- .sort()
205
-
206
- const schemaChecks = await Promise.all(
207
- allModelFiles.map(async (f) => ({
208
- file: f,
209
- hasSchema: readFileSync(resolve(modelsDir, f), 'utf-8').includes(
210
- 'export const schema = table('
211
- ),
212
- }))
213
- )
214
-
215
- const filesWithSchema = schemaChecks.filter((c) => c.hasSchema).map((c) => c.file)
216
-
217
- // generate all files in parallel
218
- const [modelsOutput, typesOutput, tablesOutput, readmeOutput] = await Promise.all([
219
- Promise.resolve(generateModelsFile(allModelFiles)),
220
- Promise.resolve(generateTypesFile(filesWithSchema)),
221
- Promise.resolve(generateTablesFile(filesWithSchema)),
222
- Promise.resolve(generateReadmeFile()),
223
- ])
224
-
225
- // write all generated files in parallel
226
- const writeResults = await Promise.all([
227
- Promise.resolve(
228
- writeFileIfChanged(resolve(generatedDir, 'models.ts'), modelsOutput)
229
- ),
230
- Promise.resolve(
231
- writeFileIfChanged(resolve(generatedDir, 'types.ts'), typesOutput)
232
- ),
233
- Promise.resolve(
234
- writeFileIfChanged(resolve(generatedDir, 'tables.ts'), tablesOutput)
235
- ),
236
- Promise.resolve(
237
- writeFileIfChanged(resolve(generatedDir, 'README.md'), readmeOutput)
238
- ),
239
- ])
240
-
241
- const filesChanged = writeResults.filter(Boolean).length
242
- if (filesChanged > 0 && !silent) {
243
- console.info(` 📝 Updated ${filesChanged} file(s)`)
244
- }
245
-
246
- // generate synced queries
247
- if (existsSync(queriesDir)) {
248
- const queryFiles = readdirSync(queriesDir).filter((f) => f.endsWith('.ts'))
249
-
250
- // process query files in parallel
251
- const queryResults = await Promise.all(
252
- queryFiles.map(async (file) => {
253
- const filePath = resolve(queriesDir, file)
254
- const fileBaseName = basename(file, '.ts')
255
- const queries: Array<{
256
- name: string
257
- params: string
258
- valibotCode: string
259
- sourceFile: string
260
- }> = []
261
-
262
- try {
263
- const content = readFileSync(filePath, 'utf-8')
264
-
265
- const sourceFile = ts.createSourceFile(
266
- filePath,
267
- content,
268
- ts.ScriptTarget.Latest,
269
- true
270
- )
271
-
272
- ts.forEachChild(sourceFile, (node) => {
273
- if (ts.isVariableStatement(node)) {
274
- const exportModifier = node.modifiers?.find(
275
- (m) => m.kind === ts.SyntaxKind.ExportKeyword
276
- )
277
- if (!exportModifier) return
278
32
 
279
- const declaration = node.declarationList.declarations[0]
280
- if (!declaration || !ts.isVariableDeclaration(declaration)) return
281
-
282
- const name = declaration.name.getText(sourceFile)
283
-
284
- // skip 'permission' exports
285
- if (name === 'permission') return
286
-
287
- if (
288
- declaration.initializer &&
289
- ts.isArrowFunction(declaration.initializer)
290
- ) {
291
- const params = declaration.initializer.parameters
292
- let paramType = 'void'
293
-
294
- if (params.length > 0) {
295
- const param = params[0]!
296
- paramType = param.type?.getText(sourceFile) || 'unknown'
297
- }
298
-
299
- try {
300
- const typeString = `type QueryParams = ${paramType}`
301
- const model = TypeScriptToModel.Generate(typeString)
302
- const valibotCode = ModelToValibot.Generate(model)
303
-
304
- queries.push({
305
- name,
306
- params: paramType,
307
- valibotCode,
308
- sourceFile: fileBaseName,
309
- })
310
- } catch (err) {
311
- console.error(`✗ ${name}: ${err}`)
312
- }
313
- }
314
- }
315
- })
316
- } catch (err) {
317
- console.error(`Error processing ${file}:`, err)
318
- }
319
-
320
- return queries
321
- })
322
- )
323
-
324
- const allQueries = queryResults.flat()
325
- const groupedQueriesOutput = generateGroupedQueriesFile(allQueries)
326
- const syncedQueriesOutput = generateSyncedQueriesFile(allQueries)
327
-
328
- const groupedChanged = writeFileIfChanged(
329
- resolve(generatedDir, 'groupedQueries.ts'),
330
- groupedQueriesOutput
331
- )
332
- const syncedChanged = writeFileIfChanged(
333
- resolve(generatedDir, 'syncedQueries.ts'),
334
- syncedQueriesOutput
335
- )
336
-
337
- const queryFilesChanged = (groupedChanged ? 1 : 0) + (syncedChanged ? 1 : 0)
338
- const totalFilesChanged = filesChanged + queryFilesChanged
339
-
340
- if (totalFilesChanged > 0 && !silent) {
341
- if (groupedChanged) {
342
- console.info(` 📝 Updated groupedQueries.ts`)
343
- }
344
- if (syncedChanged) {
345
- console.info(` 📝 Updated syncedQueries.ts`)
346
- }
347
- console.info(
348
- `✓ ${allModelFiles.length} models (${filesWithSchema.length} schemas), ${allQueries.length} queries`
349
- )
350
- }
351
-
352
- // run after command only if files changed
353
- if (totalFilesChanged > 0 && runAfter && args.after) {
354
- try {
355
- const { execSync } = await import('node:child_process')
356
- execSync(args.after, {
357
- stdio: 'inherit',
358
- env: { ...process.env, ON_ZERO_GENERATED_DIR: generatedDir },
359
- })
360
- } catch (err) {
361
- console.error(`Error running after command: ${err}`)
362
- }
363
- }
364
-
365
- saveCache()
366
- } else {
367
- if (filesChanged > 0 && !silent) {
368
- console.info(
369
- `✓ ${allModelFiles.length} models (${filesWithSchema.length} schemas)`
370
- )
371
- }
372
-
373
- // run after command only if files changed
374
- if (filesChanged > 0 && runAfter && args.after) {
375
- try {
376
- const { execSync } = await import('node:child_process')
377
- execSync(args.after, {
378
- stdio: 'inherit',
379
- env: { ...process.env, ON_ZERO_GENERATED_DIR: generatedDir },
380
- })
381
- } catch (err) {
382
- console.error(`Error running after command: ${err}`)
383
- }
384
- }
385
-
386
- saveCache()
387
- }
388
- }
389
-
390
- // run once (silent in watch mode for clean startup)
391
- await runGenerate({ silent: args.watch, runAfter: true })
33
+ async run({ args }) {
34
+ const opts = { dir: resolve(args.dir), after: args.after }
392
35
 
393
- // watch mode
394
36
  if (args.watch) {
395
- console.info('👀 watching...\n')
396
- const chokidar = await import('chokidar')
397
-
398
- let debounceTimer: ReturnType<typeof setTimeout> | null = null
399
-
400
- const debouncedRegenerate = (path: string, event: string) => {
401
- if (debounceTimer) {
402
- clearTimeout(debounceTimer)
403
- }
404
-
405
- console.info(`\n${event} ${path}`)
406
-
407
- debounceTimer = setTimeout(() => {
408
- runGenerate()
409
- }, 1000)
410
- }
411
-
412
- const watcher = chokidar.watch([modelsDir, queriesDir], {
413
- persistent: true,
414
- ignoreInitial: true,
415
- ignored: [generatedDir],
416
- })
417
-
418
- watcher.on('change', (path) => debouncedRegenerate(path, '📝'))
419
- watcher.on('add', (path) => debouncedRegenerate(path, '➕'))
420
- watcher.on('unlink', (path) => debouncedRegenerate(path, '🗑️ '))
421
-
422
- // keep process alive
37
+ await watch(opts)
423
38
  await new Promise(() => {})
39
+ } else {
40
+ await generate(opts)
424
41
  }
425
42
  },
426
43
  })
427
44
 
428
- function generateModelsFile(modelFiles: string[]) {
429
- const modelNames = modelFiles.map((f) => basename(f, '.ts')).sort()
430
-
431
- // special case: user.ts should be imported as userPublic
432
- const getImportName = (name: string) => (name === 'user' ? 'userPublic' : name)
433
-
434
- // generate imports (sorted)
435
- const imports = modelNames
436
- .map((name) => {
437
- const importName = getImportName(name)
438
- return `import * as ${importName} from '../models/${name}'`
439
- })
440
- .join('\n')
441
-
442
- // generate models object (sorted by import name)
443
- const sortedByImportName = [...modelNames].sort((a, b) =>
444
- getImportName(a).localeCompare(getImportName(b))
445
- )
446
- const modelsObj = `export const models = {\n${sortedByImportName.map((name) => ` ${getImportName(name)},`).join('\n')}\n}`
447
-
448
- return `// auto-generated by: on-zero generate\n${imports}\n\n${modelsObj}\n`
449
- }
450
-
451
- function generateTypesFile(modelFiles: string[]) {
452
- const modelNames = modelFiles.map((f) => basename(f, '.ts')).sort()
453
-
454
- // special case: user.ts should reference userPublic in schema
455
- const getSchemaName = (name: string) => (name === 'user' ? 'userPublic' : name)
456
-
457
- // generate type exports using TableInsertRow and TableUpdateRow (sorted)
458
- const typeExports = modelNames
459
- .map((name) => {
460
- const pascalName = name.charAt(0).toUpperCase() + name.slice(1)
461
- const schemaName = getSchemaName(name)
462
- return `export type ${pascalName} = TableInsertRow<typeof schema.${schemaName}>\nexport type ${pascalName}Update = TableUpdateRow<typeof schema.${schemaName}>`
463
- })
464
- .join('\n\n')
465
-
466
- return `import type { TableInsertRow, TableUpdateRow } from 'on-zero'\nimport type * as schema from './tables'\n\n${typeExports}\n`
467
- }
468
-
469
- function generateTablesFile(modelFiles: string[]) {
470
- const modelNames = modelFiles.map((f) => basename(f, '.ts')).sort()
471
-
472
- // special case: user.ts should be exported as userPublic
473
- const getExportName = (name: string) => (name === 'user' ? 'userPublic' : name)
474
-
475
- // generate schema exports (sorted)
476
- const exports = modelNames
477
- .map((name) => `export { schema as ${getExportName(name)} } from '../models/${name}'`)
478
- .join('\n')
479
-
480
- return `// auto-generated by: on-zero generate\n// this is separate from models as otherwise you end up with circular types :/\n\n${exports}\n`
481
- }
482
-
483
- function generateGroupedQueriesFile(
484
- queries: Array<{
485
- name: string
486
- params: string
487
- valibotCode: string
488
- sourceFile: string
489
- }>
490
- ) {
491
- // get unique source files sorted
492
- const sortedFiles = [...new Set(queries.map((q) => q.sourceFile))].sort()
493
-
494
- // generate re-exports
495
- const exports = sortedFiles
496
- .map((file) => `export * as ${file} from '../queries/${file}'`)
497
- .join('\n')
498
-
499
- return `/**
500
- * auto-generated by: on-zero generate
501
- *
502
- * grouped query re-exports for minification-safe query identity.
503
- * this file re-exports all query modules - while this breaks tree-shaking,
504
- * queries are typically small and few in number even in larger apps.
505
- */
506
- ${exports}
507
- `
508
- }
509
-
510
- function generateSyncedQueriesFile(
511
- queries: Array<{
512
- name: string
513
- params: string
514
- valibotCode: string
515
- sourceFile: string
516
- }>
517
- ) {
518
- // group queries by source file
519
- const queryByFile = new Map<string, typeof queries>()
520
- for (const q of queries) {
521
- if (!queryByFile.has(q.sourceFile)) {
522
- queryByFile.set(q.sourceFile, [])
523
- }
524
- queryByFile.get(q.sourceFile)!.push(q)
525
- }
526
-
527
- // sort file names for consistent output
528
- const sortedFiles = Array.from(queryByFile.keys()).sort()
529
-
530
- const imports = `// auto-generated by: on-zero generate
531
- // server-side query definitions with validators
532
- import { defineQuery, defineQueries } from '@rocicorp/zero'
533
- import * as v from 'valibot'
534
- import * as Queries from './groupedQueries'
535
- `
536
-
537
- // generate grouped definitions by namespace
538
- const namespaceDefs = sortedFiles
539
- .map((file) => {
540
- const fileQueries = queryByFile
541
- .get(file)!
542
- .sort((a, b) => a.name.localeCompare(b.name))
543
-
544
- const queryDefs = fileQueries
545
- .map((q) => {
546
- // extract validator schema
547
- const lines = q.valibotCode.split('\n').filter((l) => l.trim())
548
- const schemaLineIndex = lines.findIndex((l) =>
549
- l.startsWith('export const QueryParams')
550
- )
551
-
552
- let validatorDef = ''
553
- if (schemaLineIndex !== -1) {
554
- const schemaLines: string[] = []
555
- let openBraces = 0
556
- let started = false
557
-
558
- for (let i = schemaLineIndex; i < lines.length; i++) {
559
- const line = lines[i]!
560
- const cleaned = started
561
- ? line
562
- : line.replace('export const QueryParams = ', '')
563
- schemaLines.push(cleaned)
564
- started = true
565
-
566
- openBraces += (cleaned.match(/\{/g) || []).length
567
- openBraces -= (cleaned.match(/\}/g) || []).length
568
- openBraces += (cleaned.match(/\(/g) || []).length
569
- openBraces -= (cleaned.match(/\)/g) || []).length
570
-
571
- if (openBraces === 0 && schemaLines.length > 0) {
572
- break
573
- }
574
- }
575
- validatorDef = schemaLines.join('\n')
576
- }
577
-
578
- // for void queries, use the no-validator overload
579
- if (q.params === 'void' || !validatorDef) {
580
- return ` ${q.name}: defineQuery(() => Queries.${file}.${q.name}()),`
581
- }
582
-
583
- // indent the validator for proper formatting
584
- const indentedValidator = validatorDef
585
- .split('\n')
586
- .map((line, i) => (i === 0 ? line : ` ${line}`))
587
- .join('\n')
588
-
589
- // defineQuery with validator and args
590
- return ` ${q.name}: defineQuery(
591
- ${indentedValidator},
592
- ({ args }) => Queries.${file}.${q.name}(args)
593
- ),`
594
- })
595
- .join('\n')
596
-
597
- return `const ${file} = {\n${queryDefs}\n}`
598
- })
599
- .join('\n\n')
600
-
601
- // build the defineQueries call with all namespaces
602
- const queriesObject = sortedFiles.map((file) => ` ${file},`).join('\n')
603
-
604
- return `${imports}
605
- ${namespaceDefs}
606
-
607
- export const queries = defineQueries({
608
- ${queriesObject}
609
- })
610
- `
611
- }
612
-
613
- function generateReadmeFile() {
614
- return `# generated
615
-
616
- this folder is auto-generated by on-zero. do not edit files here directly.
617
-
618
- ## what's generated
619
-
620
- - \`models.ts\` - exports all models from ../models
621
- - \`types.ts\` - typescript types derived from table schemas
622
- - \`tables.ts\` - exports table schemas for type inference
623
- - \`groupedQueries.ts\` - namespaced query re-exports for client setup
624
- - \`syncedQueries.ts\` - namespaced syncedQuery wrappers for server setup
625
-
626
- ## usage guidelines
627
-
628
- **do not import generated files outside of the data folder.**
629
-
630
- ### queries
631
-
632
- write your queries as plain functions in \`../queries/\` and import them directly:
633
-
634
- \`\`\`ts
635
- // ✅ good - import from queries
636
- import { channelMessages } from '~/data/queries/message'
637
- \`\`\`
638
-
639
- the generated query files are only used internally by zero client/server setup.
640
-
641
- ### types
642
-
643
- you can import types from this folder, but prefer re-exporting from \`../types.ts\`:
644
-
645
- \`\`\`ts
646
- // ❌ okay but not preferred
647
- import type { Message } from '~/data/generated/types'
648
-
649
- // ✅ better - re-export from types.ts
650
- import type { Message } from '~/data/types'
651
- \`\`\`
652
-
653
- ## regeneration
654
-
655
- files are regenerated when you run:
656
-
657
- \`\`\`bash
658
- bun on-zero generate
659
- \`\`\`
660
-
661
- or in watch mode:
662
-
663
- \`\`\`bash
664
- bun on-zero generate --watch
665
- \`\`\`
666
-
667
- ## more info
668
-
669
- see the [on-zero readme](./node_modules/on-zero/README.md) for full documentation.
670
- `
671
- }
672
-
673
45
  const main = defineCommand({
674
46
  meta: {
675
47
  name: 'on-zero',
676
48
  description: 'on-zero CLI tools',
677
49
  },
678
50
  subCommands: {
679
- generate: generate,
680
- 'generate-queries': generateQueries,
51
+ generate: generateCommand,
681
52
  },
682
53
  })
683
54