@supatype/cli 0.1.0-alpha.7 → 0.1.0-alpha.9
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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +66 -61
- package/.turbo/turbo-typecheck.log +1 -1
- package/dist/app/proxy-dev-app.d.ts +13 -0
- package/dist/app/proxy-dev-app.d.ts.map +1 -0
- package/dist/app/proxy-dev-app.js +53 -0
- package/dist/app/proxy-dev-app.js.map +1 -0
- package/dist/binary-cache.d.ts +5 -0
- package/dist/binary-cache.d.ts.map +1 -1
- package/dist/binary-cache.js +13 -0
- package/dist/binary-cache.js.map +1 -1
- package/dist/commands/cloud.d.ts +11 -3
- package/dist/commands/cloud.d.ts.map +1 -1
- package/dist/commands/cloud.js +33 -25
- package/dist/commands/cloud.js.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +3 -17
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts +3 -3
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +66 -59
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/diff.d.ts.map +1 -1
- package/dist/commands/diff.js +11 -1
- package/dist/commands/diff.js.map +1 -1
- package/dist/commands/init.js +16 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +42 -12
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +16 -0
- package/dist/commands/update.js.map +1 -1
- package/dist/dev-compose.d.ts +17 -0
- package/dist/dev-compose.d.ts.map +1 -0
- package/dist/dev-compose.js +374 -0
- package/dist/dev-compose.js.map +1 -0
- package/dist/diff-output.d.ts +4 -0
- package/dist/diff-output.d.ts.map +1 -0
- package/dist/diff-output.js +12 -0
- package/dist/diff-output.js.map +1 -0
- package/dist/docker-postgres.d.ts +21 -3
- package/dist/docker-postgres.d.ts.map +1 -1
- package/dist/docker-postgres.js +130 -18
- package/dist/docker-postgres.js.map +1 -1
- package/dist/engine-client.d.ts +5 -3
- package/dist/engine-client.d.ts.map +1 -1
- package/dist/engine-client.js +2 -1
- package/dist/engine-client.js.map +1 -1
- package/dist/kong-config.d.ts +4 -0
- package/dist/kong-config.d.ts.map +1 -1
- package/dist/kong-config.js +12 -1
- package/dist/kong-config.js.map +1 -1
- package/dist/process-manager.d.ts +2 -0
- package/dist/process-manager.d.ts.map +1 -1
- package/dist/process-manager.js +16 -1
- package/dist/process-manager.js.map +1 -1
- package/dist/project-config.d.ts +21 -1
- package/dist/project-config.d.ts.map +1 -1
- package/dist/project-config.js +15 -0
- package/dist/project-config.js.map +1 -1
- package/dist/runtime-routes.d.ts +9 -0
- package/dist/runtime-routes.d.ts.map +1 -1
- package/dist/runtime-routes.js +75 -12
- package/dist/runtime-routes.js.map +1 -1
- package/dist/schema-ast-v2.d.ts +127 -0
- package/dist/schema-ast-v2.d.ts.map +1 -0
- package/dist/schema-ast-v2.js +226 -0
- package/dist/schema-ast-v2.js.map +1 -0
- package/dist/seed.d.ts +8 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +32 -0
- package/dist/seed.js.map +1 -0
- package/dist/self-host-compose.d.ts +12 -4
- package/dist/self-host-compose.d.ts.map +1 -1
- package/dist/self-host-compose.js +146 -35
- package/dist/self-host-compose.js.map +1 -1
- package/dist/studio-admin-roles.d.ts +7 -0
- package/dist/studio-admin-roles.d.ts.map +1 -0
- package/dist/studio-admin-roles.js +14 -0
- package/dist/studio-admin-roles.js.map +1 -0
- package/dist/studio-dev-server.d.ts +22 -0
- package/dist/studio-dev-server.d.ts.map +1 -0
- package/dist/studio-dev-server.js +28 -0
- package/dist/studio-dev-server.js.map +1 -0
- package/dist/type-extractor.d.ts +3 -30
- package/dist/type-extractor.d.ts.map +1 -1
- package/dist/type-extractor.js +485 -148
- package/dist/type-extractor.js.map +1 -1
- package/dist/type-resolver.d.ts +33 -0
- package/dist/type-resolver.d.ts.map +1 -0
- package/dist/type-resolver.js +338 -0
- package/dist/type-resolver.js.map +1 -0
- package/package.json +7 -3
- package/src/TYPE-RESOLUTION.md +294 -0
- package/src/app/proxy-dev-app.ts +67 -0
- package/src/binary-cache.ts +20 -0
- package/src/commands/cloud.ts +40 -30
- package/src/commands/deploy.ts +3 -18
- package/src/commands/dev.ts +72 -69
- package/src/commands/diff.ts +11 -1
- package/src/commands/init.ts +16 -3
- package/src/commands/push.ts +49 -13
- package/src/commands/update.ts +17 -0
- package/src/dev-compose.ts +455 -0
- package/src/diff-output.ts +12 -0
- package/src/docker-postgres.ts +184 -27
- package/src/engine-client.ts +9 -4
- package/src/kong-config.ts +16 -1
- package/src/process-manager.ts +18 -1
- package/src/project-config.ts +34 -1
- package/src/runtime-routes.ts +87 -12
- package/src/schema-ast-v2.ts +324 -0
- package/src/seed.ts +43 -0
- package/src/self-host-compose.ts +168 -36
- package/src/studio-admin-roles.ts +16 -0
- package/src/studio-dev-server.ts +53 -0
- package/src/type-extractor.ts +649 -186
- package/src/type-resolver.ts +457 -0
- package/tests/config.test.ts +34 -3
- package/tests/docker-postgres.test.ts +39 -0
- package/tests/normalize-admin-config.test.ts +48 -0
- package/tests/proxy-dev-app.test.ts +33 -0
- package/tests/runtime-contract.test.ts +119 -4
- package/tests/studio-admin-roles.test.ts +27 -0
- package/tests/type-extractor.test.ts +607 -23
- package/tests/type-resolver.test.ts +59 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import { resolve } from "node:path"
|
|
2
|
+
import ts from "typescript"
|
|
3
|
+
|
|
4
|
+
export type AliasEntry = {
|
|
5
|
+
typeParams: string[]
|
|
6
|
+
body: ts.TypeNode
|
|
7
|
+
sourceFile: ts.SourceFile
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** fileName → (localName → canonicalName) for explicit `import { X as Y }` renames. */
|
|
11
|
+
export type ImportRenameMap = Map<string, Map<string, string>>
|
|
12
|
+
|
|
13
|
+
export type CheckerContext = {
|
|
14
|
+
program: ts.Program
|
|
15
|
+
checker: ts.TypeChecker
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type ResolveContext = {
|
|
19
|
+
aliasRegistry: Map<string, AliasEntry>
|
|
20
|
+
renameMap: ImportRenameMap
|
|
21
|
+
sourceFiles: ts.SourceFile[]
|
|
22
|
+
getChecker: () => CheckerContext
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const MODEL_ALIAS_NAMES = new Set(["Model", "LocalizedModel"])
|
|
26
|
+
|
|
27
|
+
export function createResolveContext(sourceFiles: ts.SourceFile[]): ResolveContext {
|
|
28
|
+
let checkerCtx: CheckerContext | undefined
|
|
29
|
+
return {
|
|
30
|
+
aliasRegistry: buildAliasRegistry(sourceFiles),
|
|
31
|
+
renameMap: buildImportRenameMap(sourceFiles),
|
|
32
|
+
sourceFiles,
|
|
33
|
+
getChecker: () => {
|
|
34
|
+
if (!checkerCtx) {
|
|
35
|
+
checkerCtx = createCheckerContext(sourceFiles)
|
|
36
|
+
}
|
|
37
|
+
return checkerCtx
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function buildAliasRegistry(sourceFiles: ts.SourceFile[]): Map<string, AliasEntry> {
|
|
43
|
+
const registry = new Map<string, AliasEntry>()
|
|
44
|
+
for (const sourceFile of sourceFiles) {
|
|
45
|
+
for (const stmt of sourceFile.statements) {
|
|
46
|
+
if (!ts.isTypeAliasDeclaration(stmt)) continue
|
|
47
|
+
if (ts.isTypeReferenceNode(stmt.type) && ts.isIdentifier(stmt.type.typeName)) {
|
|
48
|
+
if (MODEL_ALIAS_NAMES.has(stmt.type.typeName.text)) continue
|
|
49
|
+
}
|
|
50
|
+
const typeParams = stmt.typeParameters?.map((p) => p.name.text) ?? []
|
|
51
|
+
registry.set(stmt.name.text, {
|
|
52
|
+
typeParams,
|
|
53
|
+
body: stmt.type,
|
|
54
|
+
sourceFile,
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return registry
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function buildImportRenameMap(sourceFiles: ts.SourceFile[]): ImportRenameMap {
|
|
62
|
+
const renameMap: ImportRenameMap = new Map()
|
|
63
|
+
for (const sourceFile of sourceFiles) {
|
|
64
|
+
const fileRenames = new Map<string, string>()
|
|
65
|
+
for (const stmt of sourceFile.statements) {
|
|
66
|
+
if (!ts.isImportDeclaration(stmt) || !stmt.importClause) continue
|
|
67
|
+
const bindings = stmt.importClause.namedBindings
|
|
68
|
+
if (!bindings || !ts.isNamedImports(bindings)) continue
|
|
69
|
+
for (const el of bindings.elements) {
|
|
70
|
+
const localName = el.name.text
|
|
71
|
+
const canonicalName = el.propertyName?.text ?? localName
|
|
72
|
+
if (localName !== canonicalName) {
|
|
73
|
+
fileRenames.set(localName, canonicalName)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (fileRenames.size > 0) {
|
|
78
|
+
renameMap.set(sourceFile.fileName, fileRenames)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return renameMap
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function applyImportRename(
|
|
85
|
+
name: string,
|
|
86
|
+
sourceFile: ts.SourceFile,
|
|
87
|
+
renameMap: ImportRenameMap,
|
|
88
|
+
): string {
|
|
89
|
+
return renameMap.get(sourceFile.fileName)?.get(name) ?? name
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function needsChecker(node: ts.TypeNode): boolean {
|
|
93
|
+
return containsConditionalOrMapped(node)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function resolveTypeNode(
|
|
97
|
+
typeNode: ts.TypeNode,
|
|
98
|
+
sourceFile: ts.SourceFile,
|
|
99
|
+
ctx: ResolveContext,
|
|
100
|
+
options: { fieldName?: string; resolving?: Set<string> } = {},
|
|
101
|
+
): ts.TypeNode {
|
|
102
|
+
if (needsChecker(typeNode)) {
|
|
103
|
+
return resolveViaChecker(typeNode, sourceFile, ctx, options.fieldName)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (ts.isTypeReferenceNode(typeNode) && ts.isIdentifier(typeNode.typeName)) {
|
|
107
|
+
const renamed = applyImportRename(typeNode.typeName.text, sourceFile, ctx.renameMap)
|
|
108
|
+
if (renamed !== typeNode.typeName.text) {
|
|
109
|
+
return resolveTypeNode(
|
|
110
|
+
ts.factory.createTypeReferenceNode(renamed, typeNode.typeArguments),
|
|
111
|
+
sourceFile,
|
|
112
|
+
ctx,
|
|
113
|
+
options,
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
const expanded = tryExpandAlias(typeNode, sourceFile, ctx, options)
|
|
117
|
+
if (expanded) {
|
|
118
|
+
return resolveTypeNode(expanded, sourceFile, ctx, options)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return typeNode
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function tryResolveTypeReference(
|
|
126
|
+
typeNode: ts.TypeReferenceNode,
|
|
127
|
+
sourceFile: ts.SourceFile,
|
|
128
|
+
ctx: ResolveContext,
|
|
129
|
+
options: { fieldName?: string; resolving?: Set<string> } = {},
|
|
130
|
+
): ts.TypeNode | null {
|
|
131
|
+
if (!ts.isIdentifier(typeNode.typeName)) return null
|
|
132
|
+
|
|
133
|
+
const renamed = applyImportRename(typeNode.typeName.text, sourceFile, ctx.renameMap)
|
|
134
|
+
if (renamed !== typeNode.typeName.text) {
|
|
135
|
+
return ts.factory.createTypeReferenceNode(renamed, typeNode.typeArguments)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return tryExpandAlias(typeNode, sourceFile, ctx, options)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function tryExpandAlias(
|
|
142
|
+
typeNode: ts.TypeReferenceNode,
|
|
143
|
+
sourceFile: ts.SourceFile,
|
|
144
|
+
ctx: ResolveContext,
|
|
145
|
+
options: { fieldName?: string; resolving?: Set<string> },
|
|
146
|
+
): ts.TypeNode | null {
|
|
147
|
+
if (!ts.isIdentifier(typeNode.typeName)) return null
|
|
148
|
+
const aliasName = applyImportRename(typeNode.typeName.text, sourceFile, ctx.renameMap)
|
|
149
|
+
const entry = ctx.aliasRegistry.get(aliasName)
|
|
150
|
+
if (!entry) return null
|
|
151
|
+
|
|
152
|
+
const resolving = options.resolving ?? new Set<string>()
|
|
153
|
+
if (resolving.has(aliasName)) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`Field "${options.fieldName ?? "?"}": circular alias chain detected resolving "${aliasName}".`,
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
resolving.add(aliasName)
|
|
160
|
+
|
|
161
|
+
const typeArgs = typeNode.typeArguments ?? []
|
|
162
|
+
const synthetic =
|
|
163
|
+
tryEvaluateConditionalAlias(entry, typeArgs) ?? tryEvaluateMappedOptionalFields(entry, typeArgs)
|
|
164
|
+
if (synthetic) {
|
|
165
|
+
return expandAliasChain(synthetic, sourceFile, ctx, options, resolving)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let expanded: ts.TypeNode
|
|
169
|
+
if (needsChecker(entry.body)) {
|
|
170
|
+
expanded = resolveViaChecker(typeNode, sourceFile, ctx, options.fieldName)
|
|
171
|
+
} else {
|
|
172
|
+
expanded = substituteAndParse(entry, typeArgs)
|
|
173
|
+
if (needsChecker(expanded)) {
|
|
174
|
+
expanded = resolveViaChecker(typeNode, sourceFile, ctx, options.fieldName)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return expandAliasChain(expanded, sourceFile, ctx, options, resolving)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function expandAliasChain(
|
|
182
|
+
typeNode: ts.TypeNode,
|
|
183
|
+
sourceFile: ts.SourceFile,
|
|
184
|
+
ctx: ResolveContext,
|
|
185
|
+
options: { fieldName?: string; resolving?: Set<string> },
|
|
186
|
+
resolving: Set<string>,
|
|
187
|
+
): ts.TypeNode {
|
|
188
|
+
if (!ts.isTypeReferenceNode(typeNode) || !ts.isIdentifier(typeNode.typeName)) {
|
|
189
|
+
return typeNode
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const chainName = applyImportRename(typeNode.typeName.text, sourceFile, ctx.renameMap)
|
|
193
|
+
if (!ctx.aliasRegistry.has(chainName) || resolving.has(chainName)) {
|
|
194
|
+
return typeNode
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const entry = ctx.aliasRegistry.get(chainName)!
|
|
198
|
+
resolving.add(chainName)
|
|
199
|
+
|
|
200
|
+
const typeArgs = typeNode.typeArguments ?? []
|
|
201
|
+
const synthetic =
|
|
202
|
+
tryEvaluateConditionalAlias(entry, typeArgs) ?? tryEvaluateMappedOptionalFields(entry, typeArgs)
|
|
203
|
+
if (synthetic) {
|
|
204
|
+
return expandAliasChain(synthetic, sourceFile, ctx, options, resolving)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let expanded: ts.TypeNode
|
|
208
|
+
if (needsChecker(entry.body)) {
|
|
209
|
+
expanded = resolveViaChecker(typeNode, sourceFile, ctx, options.fieldName)
|
|
210
|
+
} else {
|
|
211
|
+
expanded = substituteAndParse(entry, typeArgs)
|
|
212
|
+
if (needsChecker(expanded)) {
|
|
213
|
+
expanded = resolveViaChecker(typeNode, sourceFile, ctx, options.fieldName)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return expandAliasChain(expanded, sourceFile, ctx, options, resolving)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function substituteAndParse(
|
|
221
|
+
entry: AliasEntry,
|
|
222
|
+
typeArgs: readonly ts.TypeNode[],
|
|
223
|
+
): ts.TypeNode {
|
|
224
|
+
let text = entry.body.getText(entry.sourceFile)
|
|
225
|
+
for (let i = 0; i < entry.typeParams.length; i++) {
|
|
226
|
+
const param = entry.typeParams[i]
|
|
227
|
+
const arg = typeArgs[i]
|
|
228
|
+
if (!param || !arg) continue
|
|
229
|
+
const argSource = arg.getSourceFile()
|
|
230
|
+
const argText = arg.getText(argSource)
|
|
231
|
+
text = text.replace(new RegExp(`\\b${escapeRegExp(param)}\\b`, "g"), argText)
|
|
232
|
+
}
|
|
233
|
+
const parsed = ts.createSourceFile(
|
|
234
|
+
"__alias__.ts",
|
|
235
|
+
`type __A__ = ${text}`,
|
|
236
|
+
ts.ScriptTarget.Latest,
|
|
237
|
+
true,
|
|
238
|
+
ts.ScriptKind.TS,
|
|
239
|
+
)
|
|
240
|
+
const decl = parsed.statements[0]
|
|
241
|
+
if (!decl || !ts.isTypeAliasDeclaration(decl)) {
|
|
242
|
+
throw new Error("Internal error: failed to parse substituted type alias body.")
|
|
243
|
+
}
|
|
244
|
+
return decl.type
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function resolveViaChecker(
|
|
248
|
+
node: ts.TypeNode,
|
|
249
|
+
sourceFile: ts.SourceFile,
|
|
250
|
+
ctx: ResolveContext,
|
|
251
|
+
fieldName?: string,
|
|
252
|
+
): ts.TypeNode {
|
|
253
|
+
const { program, checker } = ctx.getChecker()
|
|
254
|
+
const nodeFileName = resolve(node.getSourceFile().fileName)
|
|
255
|
+
const programSf = program.getSourceFile(nodeFileName) ?? program.getSourceFile(resolve(sourceFile.fileName))
|
|
256
|
+
if (!programSf) {
|
|
257
|
+
throw checkerResolutionError(fieldName)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const programNode = findNodeAtPosition(programSf, node.pos, node.end) ?? node
|
|
261
|
+
|
|
262
|
+
const type = checker.getTypeAtLocation(programNode)
|
|
263
|
+
let typeText = checker.typeToString(
|
|
264
|
+
type,
|
|
265
|
+
programNode,
|
|
266
|
+
ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope,
|
|
267
|
+
)
|
|
268
|
+
if (!typeText.trim()) {
|
|
269
|
+
throw checkerResolutionError(fieldName)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let parsed = parseResolvedTypeText(typeText)
|
|
273
|
+
if (needsChecker(parsed)) {
|
|
274
|
+
typeText = checker.typeToString(type, programNode)
|
|
275
|
+
parsed = parseResolvedTypeText(typeText)
|
|
276
|
+
}
|
|
277
|
+
if (needsChecker(parsed)) {
|
|
278
|
+
throw checkerResolutionError(fieldName)
|
|
279
|
+
}
|
|
280
|
+
return parsed
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function parseResolvedTypeText(typeText: string): ts.TypeNode {
|
|
284
|
+
const parsed = ts.createSourceFile(
|
|
285
|
+
"__resolved__.ts",
|
|
286
|
+
`type __R__ = ${typeText}`,
|
|
287
|
+
ts.ScriptTarget.Latest,
|
|
288
|
+
true,
|
|
289
|
+
ts.ScriptKind.TS,
|
|
290
|
+
)
|
|
291
|
+
const decl = parsed.statements[0]
|
|
292
|
+
if (!decl || !ts.isTypeAliasDeclaration(decl)) {
|
|
293
|
+
throw new Error("Internal error: failed to parse checker output.")
|
|
294
|
+
}
|
|
295
|
+
return decl.type
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** `T extends string ? Optional<T> : T` and similar — pick the Optional branch for scalar args. */
|
|
299
|
+
function tryEvaluateConditionalAlias(entry: AliasEntry, typeArgs: readonly ts.TypeNode[]): ts.TypeNode | null {
|
|
300
|
+
const substituted = substituteAndParse(entry, typeArgs)
|
|
301
|
+
if (!ts.isConditionalTypeNode(substituted)) return null
|
|
302
|
+
if (substituted.extendsType.getText() !== "string") return null
|
|
303
|
+
const trueType = substituted.trueType
|
|
304
|
+
if (
|
|
305
|
+
ts.isTypeReferenceNode(trueType) &&
|
|
306
|
+
ts.isIdentifier(trueType.typeName) &&
|
|
307
|
+
trueType.typeName.text === "Optional"
|
|
308
|
+
) {
|
|
309
|
+
return trueType
|
|
310
|
+
}
|
|
311
|
+
return null
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** `{ [K in keyof T]: Optional<T[K]> }` with a concrete `T` type literal. */
|
|
315
|
+
function tryEvaluateMappedOptionalFields(entry: AliasEntry, typeArgs: readonly ts.TypeNode[]): ts.TypeNode | null {
|
|
316
|
+
if (!ts.isMappedTypeNode(entry.body)) return null
|
|
317
|
+
const template = entry.body.type
|
|
318
|
+
if (
|
|
319
|
+
template === undefined ||
|
|
320
|
+
!ts.isTypeReferenceNode(template) ||
|
|
321
|
+
template.typeName.getText() !== "Optional"
|
|
322
|
+
) {
|
|
323
|
+
return null
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const arg = typeArgs[0]
|
|
327
|
+
if (!arg || !ts.isTypeLiteralNode(arg)) return null
|
|
328
|
+
|
|
329
|
+
const argSource = arg.getSourceFile()
|
|
330
|
+
const parts: string[] = []
|
|
331
|
+
for (const member of arg.members) {
|
|
332
|
+
if (!ts.isPropertySignature(member) || !member.type) continue
|
|
333
|
+
const name = member.name.getText(argSource)
|
|
334
|
+
const typeText = member.type.getText(argSource)
|
|
335
|
+
parts.push(`${name}: Optional<${typeText}>`)
|
|
336
|
+
}
|
|
337
|
+
if (parts.length === 0) return null
|
|
338
|
+
|
|
339
|
+
const parsed = ts.createSourceFile(
|
|
340
|
+
"__mapped__.ts",
|
|
341
|
+
`type __M__ = { ${parts.join("; ")} }`,
|
|
342
|
+
ts.ScriptTarget.Latest,
|
|
343
|
+
true,
|
|
344
|
+
ts.ScriptKind.TS,
|
|
345
|
+
)
|
|
346
|
+
const decl = parsed.statements[0]
|
|
347
|
+
if (!decl || !ts.isTypeAliasDeclaration(decl) || !ts.isTypeLiteralNode(decl.type)) return null
|
|
348
|
+
return decl.type
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function checkerResolutionError(fieldName?: string): Error {
|
|
352
|
+
const prefix = fieldName ? `Field "${fieldName}": ` : ""
|
|
353
|
+
return new Error(`${prefix}could not resolve conditional/mapped type via type checker.`)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const SUPATYPE_CHECKER_STUB_PATH = resolve(process.cwd(), "__supatype_checker_stubs__.ts")
|
|
357
|
+
const SUPATYPE_CHECKER_STUB_SOURCE = `
|
|
358
|
+
export type Optional<T> = T;
|
|
359
|
+
export type Unique<T> = T;
|
|
360
|
+
export type PrimaryKey<T> = T;
|
|
361
|
+
export type Localized<T> = T;
|
|
362
|
+
export type NotLocalized<T> = T;
|
|
363
|
+
export type Default<T, V> = T;
|
|
364
|
+
export type Email = string & { readonly __supatypeEmailBrand: unique symbol };
|
|
365
|
+
export type UUID = string & { readonly __supatypeUuidBrand: unique symbol };
|
|
366
|
+
export type RichText = unknown;
|
|
367
|
+
export type Int = number;
|
|
368
|
+
export type SmallInt = number;
|
|
369
|
+
export type BigInt = number;
|
|
370
|
+
export type Timestamp = string;
|
|
371
|
+
export type Date = string;
|
|
372
|
+
export type DateTime = string;
|
|
373
|
+
export type DateOnly = string;
|
|
374
|
+
export type Slug<S extends string = string> = string;
|
|
375
|
+
export type RelatedTo<T> = string;
|
|
376
|
+
export type Text = string;
|
|
377
|
+
`
|
|
378
|
+
|
|
379
|
+
function createCheckerStubFile(): ts.SourceFile {
|
|
380
|
+
return ts.createSourceFile(
|
|
381
|
+
SUPATYPE_CHECKER_STUB_PATH,
|
|
382
|
+
SUPATYPE_CHECKER_STUB_SOURCE,
|
|
383
|
+
ts.ScriptTarget.Latest,
|
|
384
|
+
true,
|
|
385
|
+
ts.ScriptKind.TS,
|
|
386
|
+
)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function createCheckerContext(sourceFiles: ts.SourceFile[]): CheckerContext {
|
|
390
|
+
const stubFile = createCheckerStubFile()
|
|
391
|
+
const fileMap = new Map<string, ts.SourceFile>([
|
|
392
|
+
[resolve(stubFile.fileName), stubFile],
|
|
393
|
+
...sourceFiles.map((sf) => [resolve(sf.fileName), sf] as const),
|
|
394
|
+
])
|
|
395
|
+
const options: ts.CompilerOptions = {
|
|
396
|
+
target: ts.ScriptTarget.Latest,
|
|
397
|
+
skipLibCheck: true,
|
|
398
|
+
baseUrl: process.cwd(),
|
|
399
|
+
paths: {
|
|
400
|
+
"@supatype/types": ["__supatype_checker_stubs__.ts"],
|
|
401
|
+
},
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const host: ts.CompilerHost = {
|
|
405
|
+
getSourceFile: (fileName, languageVersion) => {
|
|
406
|
+
const existing = fileMap.get(resolve(fileName))
|
|
407
|
+
if (existing) return existing
|
|
408
|
+
const libContent = ts.sys.readFile(fileName)
|
|
409
|
+
if (libContent) {
|
|
410
|
+
return ts.createSourceFile(fileName, libContent, languageVersion, true)
|
|
411
|
+
}
|
|
412
|
+
return undefined
|
|
413
|
+
},
|
|
414
|
+
getDefaultLibFileName: (opts) => ts.getDefaultLibFileName(opts ?? options),
|
|
415
|
+
writeFile: () => {},
|
|
416
|
+
getCurrentDirectory: () => process.cwd(),
|
|
417
|
+
getCanonicalFileName: (f) => f,
|
|
418
|
+
useCaseSensitiveFileNames: () => true,
|
|
419
|
+
getNewLine: () => ts.sys.newLine,
|
|
420
|
+
fileExists: (fileName) => fileMap.has(resolve(fileName)) || ts.sys.fileExists(fileName),
|
|
421
|
+
readFile: (fileName) => fileMap.get(resolve(fileName))?.getFullText() ?? ts.sys.readFile(fileName),
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const program = ts.createProgram([...fileMap.keys()], options, host)
|
|
425
|
+
return { program, checker: program.getTypeChecker() }
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function findNodeAtPosition(root: ts.Node, pos: number, end: number): ts.Node | undefined {
|
|
429
|
+
const queue: ts.Node[] = [root]
|
|
430
|
+
while (queue.length > 0) {
|
|
431
|
+
const current = queue.shift()
|
|
432
|
+
if (!current) continue
|
|
433
|
+
if (current.pos === pos && current.end === end) return current
|
|
434
|
+
ts.forEachChild(current, (child) => queue.push(child))
|
|
435
|
+
}
|
|
436
|
+
return undefined
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function containsConditionalOrMapped(node: ts.Node): boolean {
|
|
440
|
+
if (ts.isConditionalTypeNode(node) || ts.isMappedTypeNode(node)) return true
|
|
441
|
+
let found = false
|
|
442
|
+
ts.forEachChild(node, (child) => {
|
|
443
|
+
if (!found && containsConditionalOrMapped(child)) found = true
|
|
444
|
+
})
|
|
445
|
+
return found
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function escapeRegExp(value: string): string {
|
|
449
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export function unknownTypeError(typeName: string, fieldName: string): Error {
|
|
453
|
+
return new Error(
|
|
454
|
+
`Unknown Supatype type "${typeName}" in field "${fieldName}". ` +
|
|
455
|
+
"If this is a type alias, confirm the file defining it is reachable from your schema entry point.",
|
|
456
|
+
)
|
|
457
|
+
}
|
package/tests/config.test.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { mkdirSync, rmSync, writeFileSync } from "node:fs"
|
|
|
3
3
|
import { join } from "node:path"
|
|
4
4
|
import { tmpdir } from "node:os"
|
|
5
5
|
import { defineConfig, loadConfig } from "../src/config.js"
|
|
6
|
-
import { mergeProjectConfig, type SupatypeProjectConfig } from "../src/project-config.js"
|
|
6
|
+
import { mergeProjectConfig, resolveRuntimeProvider, type SupatypeProjectConfig } from "../src/project-config.js"
|
|
7
7
|
import { DENO_RELEASE_PIN } from "../src/release-pins.js"
|
|
8
8
|
|
|
9
9
|
let counter = 0
|
|
@@ -180,6 +180,26 @@ describe("loadConfig()", () => {
|
|
|
180
180
|
})
|
|
181
181
|
})
|
|
182
182
|
|
|
183
|
+
describe("resolveRuntimeProvider()", () => {
|
|
184
|
+
it("prefers top-level provider over database.provider", () => {
|
|
185
|
+
const cfg = defineConfig({
|
|
186
|
+
...minimalProject("p"),
|
|
187
|
+
provider: "docker",
|
|
188
|
+
database: { provider: "native" },
|
|
189
|
+
})
|
|
190
|
+
expect(resolveRuntimeProvider(cfg)).toBe("docker")
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it("falls back to database.provider then native", () => {
|
|
194
|
+
expect(resolveRuntimeProvider(defineConfig({ ...minimalProject("p"), database: { provider: "docker" } }))).toBe(
|
|
195
|
+
"docker",
|
|
196
|
+
)
|
|
197
|
+
expect(resolveRuntimeProvider(defineConfig({ ...minimalProject("p"), database: { provider: "native" } }))).toBe(
|
|
198
|
+
"native",
|
|
199
|
+
)
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
183
203
|
describe("mergeProjectConfig()", () => {
|
|
184
204
|
it("deep-merges email.smtp between base and local", () => {
|
|
185
205
|
const base = defineConfig({
|
|
@@ -213,8 +233,19 @@ describe("mergeProjectConfig()", () => {
|
|
|
213
233
|
app: { mode: "static", static_dir: "./dist", vite_dev_url: "http://127.0.0.1:1111" },
|
|
214
234
|
})
|
|
215
235
|
const merged = mergeProjectConfig(base, { app: { vite_dev_url: "http://127.0.0.1:5173" } })
|
|
216
|
-
expect(merged.app
|
|
217
|
-
|
|
236
|
+
expect(merged.app?.vite_dev_url).toBe("http://127.0.0.1:5173")
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it("merges app.start for proxy dev script override", () => {
|
|
240
|
+
const base = defineConfig({
|
|
241
|
+
...minimalProject("p"),
|
|
242
|
+
app: { mode: "static", static_dir: "./dist" },
|
|
243
|
+
})
|
|
244
|
+
const merged = mergeProjectConfig(base, {
|
|
245
|
+
app: { mode: "proxy", upstream: "http://127.0.0.1:4321", start: "dev:site" },
|
|
246
|
+
})
|
|
247
|
+
expect(merged.app.mode).toBe("proxy")
|
|
248
|
+
expect(merged.app.start).toBe("dev:site")
|
|
218
249
|
expect(merged.app.static_dir).toBe("./dist")
|
|
219
250
|
})
|
|
220
251
|
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
containerName,
|
|
4
|
+
dockerPgLoopbackDbUrl,
|
|
5
|
+
dockerPgPostInitServing,
|
|
6
|
+
} from "../src/docker-postgres.js"
|
|
7
|
+
|
|
8
|
+
describe("docker-postgres", () => {
|
|
9
|
+
it("derives container name from project name", () => {
|
|
10
|
+
expect(containerName("supatype-integration")).toBe("supatype-supatype-integration")
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it("detects post-init serving after first-boot init", () => {
|
|
14
|
+
const logs = [
|
|
15
|
+
"PostgreSQL init process complete; ready for start up.",
|
|
16
|
+
"database system is ready to accept connections",
|
|
17
|
+
].join("\n")
|
|
18
|
+
expect(dockerPgPostInitServing(logs)).toBe(true)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it("detects post-init serving on reused volume (no init banner this run)", () => {
|
|
22
|
+
const logs = "database system is ready to accept connections\n"
|
|
23
|
+
expect(dockerPgPostInitServing(logs)).toBe(true)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("builds loopback DB URL for in-container migrate", () => {
|
|
27
|
+
expect(dockerPgLoopbackDbUrl("my-project")).toBe(
|
|
28
|
+
"postgres://supatype_admin:postgres@127.0.0.1:5432/my-project?sslmode=disable",
|
|
29
|
+
)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it("rejects ready line before init complete on first boot", () => {
|
|
33
|
+
const logs = [
|
|
34
|
+
"database system is ready to accept connections",
|
|
35
|
+
"PostgreSQL init process complete; ready for start up.",
|
|
36
|
+
].join("\n")
|
|
37
|
+
expect(dockerPgPostInitServing(logs)).toBe(false)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { normalizeAdminConfig } from "../../studio/src/lib/normalize-admin-config.js"
|
|
3
|
+
|
|
4
|
+
describe("normalizeAdminConfig", () => {
|
|
5
|
+
it("maps global fields and Settings navigation items", () => {
|
|
6
|
+
const config = normalizeAdminConfig({
|
|
7
|
+
localization: { locales: ["en"], defaultLocale: "en" },
|
|
8
|
+
navigation: [
|
|
9
|
+
{
|
|
10
|
+
group: "Content",
|
|
11
|
+
items: [{ label: "Post", model: "posts" }],
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
group: "Settings",
|
|
15
|
+
items: [{ label: "Site Settings", global: "_global_site_settings" }],
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
models: [
|
|
19
|
+
{
|
|
20
|
+
name: "Post",
|
|
21
|
+
tableName: "posts",
|
|
22
|
+
fields: [{ name: "title", widget: "text", required: true }],
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
globals: [
|
|
26
|
+
{
|
|
27
|
+
name: "SiteSettings",
|
|
28
|
+
tableName: "_global_site_settings",
|
|
29
|
+
singleton: true,
|
|
30
|
+
fields: [{ name: "site_name", widget: "text", required: true }],
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
expect(config.globals).toHaveLength(1)
|
|
36
|
+
expect(config.globals[0]?.tableName).toBe("_global_site_settings")
|
|
37
|
+
expect(config.globals[0]?.apiPath).toBe("/rest/v1/_global_site_settings")
|
|
38
|
+
expect(config.globals[0]?.fields).toHaveLength(1)
|
|
39
|
+
expect(config.globals[0]?.fields[0]?.name).toBe("site_name")
|
|
40
|
+
|
|
41
|
+
const settingsNav = config.navigation.find((g) => g.label === "Settings")
|
|
42
|
+
expect(settingsNav?.items[0]).toMatchObject({
|
|
43
|
+
type: "global",
|
|
44
|
+
href: "/models/globals/SiteSettings",
|
|
45
|
+
label: "Site Settings",
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { resolveProxyDevScript } from "../src/app/proxy-dev-app.js"
|
|
3
|
+
import type { SupatypeProjectConfig } from "../src/project-config.js"
|
|
4
|
+
|
|
5
|
+
const base = {
|
|
6
|
+
project: { name: "test" },
|
|
7
|
+
database: { provider: "native" as const },
|
|
8
|
+
server: { mode: "dev" as const },
|
|
9
|
+
versions: { engine: "latest", server: "latest", postgres: "latest", deno: "latest" },
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe("resolveProxyDevScript()", () => {
|
|
13
|
+
it("returns null when app.mode is not proxy", () => {
|
|
14
|
+
const config = { ...base, app: { mode: "static" as const } } satisfies SupatypeProjectConfig
|
|
15
|
+
expect(resolveProxyDevScript(config)).toBeNull()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it("defaults to start when proxy mode and app.start omitted", () => {
|
|
19
|
+
const config = {
|
|
20
|
+
...base,
|
|
21
|
+
app: { mode: "proxy" as const, upstream: "http://127.0.0.1:4321" },
|
|
22
|
+
} satisfies SupatypeProjectConfig
|
|
23
|
+
expect(resolveProxyDevScript(config)).toBe("start")
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("uses app.start when set", () => {
|
|
27
|
+
const config = {
|
|
28
|
+
...base,
|
|
29
|
+
app: { mode: "proxy" as const, upstream: "http://127.0.0.1:4321", start: "dev:site" },
|
|
30
|
+
} satisfies SupatypeProjectConfig
|
|
31
|
+
expect(resolveProxyDevScript(config)).toBe("dev:site")
|
|
32
|
+
})
|
|
33
|
+
})
|