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.
- package/dist/cjs/cli.cjs +17 -420
- package/dist/cjs/cli.js +7 -398
- package/dist/cjs/cli.js.map +2 -2
- package/dist/cjs/cli.native.js +15 -514
- package/dist/cjs/cli.native.js.map +1 -1
- package/dist/cjs/generate.cjs +370 -0
- package/dist/cjs/generate.js +339 -0
- package/dist/cjs/generate.js.map +6 -0
- package/dist/cjs/generate.native.js +464 -0
- package/dist/cjs/generate.native.js.map +1 -0
- package/dist/cjs/helpers/createMutators.cjs +4 -3
- package/dist/cjs/helpers/createMutators.js +12 -9
- package/dist/cjs/helpers/createMutators.js.map +1 -1
- package/dist/cjs/helpers/createMutators.native.js +25 -21
- package/dist/cjs/helpers/createMutators.native.js.map +1 -1
- package/dist/cjs/mutations.cjs +34 -4
- package/dist/cjs/mutations.js +29 -4
- package/dist/cjs/mutations.js.map +1 -1
- package/dist/cjs/mutations.native.js +36 -4
- package/dist/cjs/mutations.native.js.map +1 -1
- package/dist/cjs/vite-plugin.cjs +84 -0
- package/dist/cjs/vite-plugin.js +86 -0
- package/dist/cjs/vite-plugin.js.map +6 -0
- package/dist/cjs/vite-plugin.native.js +99 -0
- package/dist/cjs/vite-plugin.native.js.map +1 -0
- package/dist/esm/cli.js +8 -384
- package/dist/esm/cli.js.map +2 -2
- package/dist/esm/cli.mjs +17 -398
- package/dist/esm/cli.mjs.map +1 -1
- package/dist/esm/cli.native.js +15 -492
- package/dist/esm/cli.native.js.map +1 -1
- package/dist/esm/generate.js +317 -0
- package/dist/esm/generate.js.map +6 -0
- package/dist/esm/generate.mjs +335 -0
- package/dist/esm/generate.mjs.map +1 -0
- package/dist/esm/generate.native.js +426 -0
- package/dist/esm/generate.native.js.map +1 -0
- package/dist/esm/helpers/createMutators.js +12 -9
- package/dist/esm/helpers/createMutators.js.map +1 -1
- package/dist/esm/helpers/createMutators.mjs +4 -3
- package/dist/esm/helpers/createMutators.mjs.map +1 -1
- package/dist/esm/helpers/createMutators.native.js +25 -21
- package/dist/esm/helpers/createMutators.native.js.map +1 -1
- package/dist/esm/mutations.js +29 -4
- package/dist/esm/mutations.js.map +1 -1
- package/dist/esm/mutations.mjs +34 -4
- package/dist/esm/mutations.mjs.map +1 -1
- package/dist/esm/mutations.native.js +35 -3
- package/dist/esm/mutations.native.js.map +1 -1
- package/dist/esm/vite-plugin.js +71 -0
- package/dist/esm/vite-plugin.js.map +6 -0
- package/dist/esm/vite-plugin.mjs +59 -0
- package/dist/esm/vite-plugin.mjs.map +1 -0
- package/dist/esm/vite-plugin.native.js +71 -0
- package/dist/esm/vite-plugin.native.js.map +1 -0
- package/package.json +7 -2
- package/readme.md +42 -32
- package/src/cli.ts +9 -638
- package/src/generate.ts +490 -0
- package/src/helpers/createMutators.ts +14 -8
- package/src/mutations.ts +57 -4
- package/src/vite-plugin.ts +110 -0
- package/types/generate.d.ts +21 -0
- package/types/generate.d.ts.map +1 -0
- package/types/helpers/createMutators.d.ts.map +1 -1
- package/types/mutations.d.ts.map +1 -1
- package/types/vite-plugin.d.ts +16 -0
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
280
|
-
|
|
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
|
-
|
|
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:
|
|
680
|
-
'generate-queries': generateQueries,
|
|
51
|
+
generate: generateCommand,
|
|
681
52
|
},
|
|
682
53
|
})
|
|
683
54
|
|