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.
- package/dist/cjs/createZeroClient.cjs +44 -11
- package/dist/cjs/createZeroClient.native.js +54 -17
- package/dist/cjs/createZeroClient.native.js.map +1 -1
- package/dist/cjs/createZeroClient.recovery.test.cjs +49 -0
- package/dist/cjs/createZeroClient.recovery.test.native.js +53 -0
- package/dist/cjs/createZeroClient.recovery.test.native.js.map +1 -1
- package/dist/cjs/generate-helpers.cjs +28 -0
- package/dist/cjs/generate-helpers.native.js +36 -0
- package/dist/cjs/generate-helpers.native.js.map +1 -1
- package/dist/cjs/generate-layout.cjs +62 -53
- package/dist/cjs/generate-layout.native.js +62 -54
- package/dist/cjs/generate-layout.native.js.map +1 -1
- package/dist/cjs/generate-lite.cjs +28 -0
- package/dist/cjs/generate-lite.native.js +89 -33
- package/dist/cjs/generate-lite.native.js.map +1 -1
- package/dist/cjs/generate-lite.test.cjs +46 -0
- package/dist/cjs/generate-lite.test.native.js +46 -0
- package/dist/cjs/generate-lite.test.native.js.map +1 -1
- package/dist/cjs/generate.cjs +237 -243
- package/dist/cjs/generate.native.js +352 -353
- package/dist/cjs/generate.native.js.map +1 -1
- package/dist/cjs/generate.test.cjs +35 -0
- package/dist/cjs/generate.test.native.js +35 -0
- package/dist/cjs/generate.test.native.js.map +1 -1
- package/dist/cjs/helpers/createMutators.cjs +2 -1
- package/dist/cjs/helpers/createMutators.native.js +3 -1
- package/dist/cjs/helpers/createMutators.native.js.map +1 -1
- package/dist/cjs/helpers/recoverZeroClient.cjs +8 -2
- package/dist/cjs/helpers/recoverZeroClient.native.js +10 -2
- package/dist/cjs/helpers/recoverZeroClient.native.js.map +1 -1
- package/dist/cjs/helpers/recoverZeroClient.test.cjs +53 -0
- package/dist/cjs/helpers/recoverZeroClient.test.native.js +55 -0
- package/dist/cjs/helpers/recoverZeroClient.test.native.js.map +1 -1
- package/dist/cjs/native-typescript.cjs +190 -0
- package/dist/cjs/native-typescript.native.js +255 -0
- package/dist/cjs/native-typescript.native.js.map +1 -0
- package/dist/esm/createZeroClient.mjs +44 -11
- package/dist/esm/createZeroClient.mjs.map +1 -1
- package/dist/esm/createZeroClient.native.js +54 -17
- package/dist/esm/createZeroClient.native.js.map +1 -1
- package/dist/esm/createZeroClient.recovery.test.mjs +50 -1
- package/dist/esm/createZeroClient.recovery.test.mjs.map +1 -1
- package/dist/esm/createZeroClient.recovery.test.native.js +54 -1
- package/dist/esm/createZeroClient.recovery.test.native.js.map +1 -1
- package/dist/esm/generate-helpers.mjs +27 -1
- package/dist/esm/generate-helpers.mjs.map +1 -1
- package/dist/esm/generate-helpers.native.js +35 -1
- package/dist/esm/generate-helpers.native.js.map +1 -1
- package/dist/esm/generate-layout.mjs +52 -54
- package/dist/esm/generate-layout.mjs.map +1 -1
- package/dist/esm/generate-layout.native.js +52 -55
- package/dist/esm/generate-layout.native.js.map +1 -1
- package/dist/esm/generate-lite.mjs +29 -1
- package/dist/esm/generate-lite.mjs.map +1 -1
- package/dist/esm/generate-lite.native.js +90 -34
- package/dist/esm/generate-lite.native.js.map +1 -1
- package/dist/esm/generate-lite.test.mjs +46 -0
- package/dist/esm/generate-lite.test.mjs.map +1 -1
- package/dist/esm/generate-lite.test.native.js +46 -0
- package/dist/esm/generate-lite.test.native.js.map +1 -1
- package/dist/esm/generate.mjs +238 -244
- package/dist/esm/generate.mjs.map +1 -1
- package/dist/esm/generate.native.js +353 -354
- package/dist/esm/generate.native.js.map +1 -1
- package/dist/esm/generate.test.mjs +35 -0
- package/dist/esm/generate.test.mjs.map +1 -1
- package/dist/esm/generate.test.native.js +35 -0
- package/dist/esm/generate.test.native.js.map +1 -1
- package/dist/esm/helpers/createMutators.mjs +2 -1
- package/dist/esm/helpers/createMutators.mjs.map +1 -1
- package/dist/esm/helpers/createMutators.native.js +3 -1
- package/dist/esm/helpers/createMutators.native.js.map +1 -1
- package/dist/esm/helpers/recoverZeroClient.mjs +8 -2
- package/dist/esm/helpers/recoverZeroClient.mjs.map +1 -1
- package/dist/esm/helpers/recoverZeroClient.native.js +10 -2
- package/dist/esm/helpers/recoverZeroClient.native.js.map +1 -1
- package/dist/esm/helpers/recoverZeroClient.test.mjs +53 -0
- package/dist/esm/helpers/recoverZeroClient.test.mjs.map +1 -1
- package/dist/esm/helpers/recoverZeroClient.test.native.js +55 -0
- package/dist/esm/helpers/recoverZeroClient.test.native.js.map +1 -1
- package/dist/esm/index.native.js.map +1 -1
- package/dist/esm/native-typescript.mjs +164 -0
- package/dist/esm/native-typescript.mjs.map +1 -0
- package/dist/esm/native-typescript.native.js +226 -0
- package/dist/esm/native-typescript.native.js.map +1 -0
- package/package.json +4 -3
- package/src/createZeroClient.recovery.test.tsx +56 -1
- package/src/createZeroClient.tsx +54 -15
- package/src/generate-helpers.ts +79 -0
- package/src/generate-layout.ts +64 -108
- package/src/generate-lite.test.ts +49 -0
- package/src/generate-lite.ts +44 -0
- package/src/generate.test.ts +52 -0
- package/src/generate.ts +313 -366
- package/src/helpers/createMutators.ts +14 -3
- package/src/helpers/recoverZeroClient.test.ts +50 -0
- package/src/helpers/recoverZeroClient.ts +12 -2
- package/src/native-typescript.ts +176 -0
- package/types/createZeroClient.d.ts.map +1 -1
- package/types/generate-helpers.d.ts +13 -0
- package/types/generate-helpers.d.ts.map +1 -1
- package/types/generate-layout.d.ts +2 -1
- package/types/generate-layout.d.ts.map +1 -1
- package/types/generate-lite.d.ts +1 -0
- package/types/generate-lite.d.ts.map +1 -1
- package/types/generate.d.ts.map +1 -1
- package/types/helpers/createMutators.d.ts.map +1 -1
- package/types/helpers/recoverZeroClient.d.ts.map +1 -1
- package/types/native-typescript.d.ts +11 -0
- 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 = '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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:
|
|
167
|
+
sourceFile: ts.SourceFile,
|
|
200
168
|
exportName: string,
|
|
201
169
|
paramIndex: number
|
|
202
|
-
):
|
|
203
|
-
let
|
|
170
|
+
): Promise<Type | null> {
|
|
171
|
+
let typeNode: ts.TypeNode | null = null
|
|
204
172
|
|
|
205
|
-
|
|
206
|
-
if (
|
|
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
|
-
|
|
185
|
+
typeNode = param.type
|
|
218
186
|
}
|
|
219
187
|
}
|
|
220
188
|
})
|
|
221
189
|
|
|
222
|
-
return
|
|
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:
|
|
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:
|
|
282
|
-
): Map<string,
|
|
283
|
-
const
|
|
245
|
+
sourceFile: ts.SourceFile
|
|
246
|
+
): Promise<Map<string, Type>> {
|
|
247
|
+
const nodes = new Map<string, ts.TypeNode>()
|
|
284
248
|
|
|
285
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
337
|
-
resolvedTypeToValibot?: (type: import('typescript').Type) => string
|
|
299
|
+
resolvedValibot?: Map<string, string>
|
|
338
300
|
): ModelMutations | null {
|
|
339
|
-
let mutateNode:
|
|
301
|
+
let mutateNode: ts.CallExpression | null = null
|
|
340
302
|
|
|
341
303
|
// find `export const mutate = mutations(...)`
|
|
342
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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 &&
|
|
423
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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 = (
|
|
458
|
+
const recurse = (value: Type) => tsTypeToValibot(project, value, seen)
|
|
517
459
|
|
|
518
460
|
// primitives
|
|
519
|
-
if (flags &
|
|
520
|
-
if (flags &
|
|
521
|
-
if (flags &
|
|
522
|
-
if (flags &
|
|
523
|
-
if (flags &
|
|
524
|
-
if (flags &
|
|
525
|
-
if (flags &
|
|
526
|
-
if (flags &
|
|
527
|
-
if (
|
|
528
|
-
|
|
529
|
-
(checker.
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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 (
|
|
539
|
-
return `v.literal(${JSON.stringify(
|
|
479
|
+
if (type.isStringLiteralType()) {
|
|
480
|
+
return `v.literal(${JSON.stringify(type.value)})`
|
|
540
481
|
}
|
|
541
|
-
if (
|
|
542
|
-
return `v.literal(${
|
|
482
|
+
if (type.isNumberLiteralType()) {
|
|
483
|
+
return `v.literal(${type.value})`
|
|
543
484
|
}
|
|
544
|
-
if (
|
|
545
|
-
|
|
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.
|
|
551
|
-
const members = type.
|
|
552
|
-
const hasNull = members.some((
|
|
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
|
-
(
|
|
494
|
+
(member) => member.flags & (TypeFlags.Undefined | TypeFlags.Void)
|
|
555
495
|
)
|
|
556
496
|
const rest = members.filter(
|
|
557
|
-
(
|
|
558
|
-
!(
|
|
497
|
+
(member) =>
|
|
498
|
+
!(member.flags & (TypeFlags.Null | TypeFlags.Undefined | TypeFlags.Void))
|
|
559
499
|
)
|
|
560
500
|
if (
|
|
561
501
|
rest.length === 2 &&
|
|
562
|
-
rest.every((
|
|
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(
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
|
|
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.
|
|
535
|
+
const name = prop.name
|
|
605
536
|
if (shouldSkipObjectKey(name)) continue
|
|
606
|
-
const propType = resolveSymbolType(
|
|
607
|
-
const isOptional =
|
|
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
|
-
//
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
|
|
564
|
+
(symbol?.name === 'Array' || symbol?.name === 'ReadonlyArray') &&
|
|
565
|
+
typeArgs.length === 1
|
|
634
566
|
) {
|
|
635
|
-
return `v.array(${recurse(
|
|
567
|
+
return `v.array(${await recurse(typeArgs[0]!)})`
|
|
636
568
|
}
|
|
637
569
|
}
|
|
638
|
-
|
|
639
|
-
|
|
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
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
|
896
|
-
const
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
864
|
+
const visit = (node: ts.Node) => {
|
|
934
865
|
if (
|
|
935
866
|
!ts.isCallExpression(node) ||
|
|
936
867
|
node.expression.getText(source) !== 'defineRelations'
|
|
937
868
|
) {
|
|
938
|
-
|
|
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
|
|
1010
|
-
|
|
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
|
-
|
|
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
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
ts.
|
|
1128
|
-
|
|
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
|
-
|
|
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
|
|
1318
|
-
|
|
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
|
-
|
|
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
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
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([
|