@kronor/dtv 5.0.0 → 5.1.0
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/assets/{index-BCuRRUo9.js → index-JkFoTAyM.js} +1 -1
- package/dist/cli/commands/init.js +106 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/typegen.js +26 -0
- package/dist/cli/commands/typegen.js.map +1 -0
- package/dist/cli/config/loadConfig.js +101 -0
- package/dist/cli/config/loadConfig.js.map +1 -0
- package/dist/cli/config/types.js +2 -0
- package/dist/cli/config/types.js.map +1 -0
- package/dist/cli/dtv.js +4 -504
- package/dist/cli/dtv.js.map +1 -1
- package/dist/cli/typegen/runTypegen.js +638 -0
- package/dist/cli/typegen/runTypegen.js.map +1 -0
- package/dist/cli/typegen/schemaToTs.js +5 -1
- package/dist/cli/typegen/schemaToTs.js.map +1 -1
- package/dist/index.es.js +5 -4
- package/dist/index.html +1 -1
- package/dist/types/cli/commands/init.d.ts +3 -0
- package/dist/types/cli/commands/init.d.ts.map +1 -0
- package/dist/types/cli/commands/typegen.d.ts +3 -0
- package/dist/types/cli/commands/typegen.d.ts.map +1 -0
- package/dist/types/cli/config/loadConfig.d.ts +3 -0
- package/dist/types/cli/config/loadConfig.d.ts.map +1 -0
- package/dist/types/cli/config/types.d.ts +34 -0
- package/dist/types/cli/config/types.d.ts.map +1 -0
- package/dist/types/cli/typegen/runTypegen.d.ts +9 -0
- package/dist/types/cli/typegen/runTypegen.d.ts.map +1 -0
- package/dist/types/cli/typegen/schemaToTs.d.ts.map +1 -1
- package/dist/types/dsl/filterExpr.d.ts +130 -46
- package/dist/types/dsl/filterExpr.d.ts.map +1 -1
- package/dist/types/dsl/filters.d.ts +72 -2
- package/dist/types/dsl/filters.d.ts.map +1 -1
- package/docs/typegen.md +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import * as fs from 'node:fs/promises';
|
|
3
|
+
import fg from 'fast-glob';
|
|
4
|
+
import ts from 'typescript';
|
|
5
|
+
import { getIntrospectionQuery, buildClientSchema } from 'graphql';
|
|
6
|
+
import { loadConfig } from '../config/loadConfig.js';
|
|
7
|
+
import { collectReachableTypes, renderTsFromSchema, unwrapCollectionElementType } from './schemaToTs.js';
|
|
8
|
+
function toPascalCase(input) {
|
|
9
|
+
return input
|
|
10
|
+
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
|
11
|
+
.trim()
|
|
12
|
+
.split(' ')
|
|
13
|
+
.filter(Boolean)
|
|
14
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
15
|
+
.join('');
|
|
16
|
+
}
|
|
17
|
+
function toIdentifier(pascal) {
|
|
18
|
+
if (/^[A-Za-z_]/.test(pascal))
|
|
19
|
+
return pascal;
|
|
20
|
+
return `_${pascal}`;
|
|
21
|
+
}
|
|
22
|
+
function singleQuoteStringLiteral(value) {
|
|
23
|
+
return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n').replace(/\r/g, '\\r')}'`;
|
|
24
|
+
}
|
|
25
|
+
async function writeFileEnsuringDir(filePath, content) {
|
|
26
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
27
|
+
await fs.writeFile(filePath, content, 'utf8');
|
|
28
|
+
}
|
|
29
|
+
async function fetchSchema(endpoint, headers) {
|
|
30
|
+
const query = getIntrospectionQuery({ descriptions: true });
|
|
31
|
+
const res = await fetch(endpoint, {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: {
|
|
34
|
+
'content-type': 'application/json',
|
|
35
|
+
...headers
|
|
36
|
+
},
|
|
37
|
+
body: JSON.stringify({ query })
|
|
38
|
+
});
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
const text = await res.text().catch(() => '');
|
|
41
|
+
throw new Error(`Introspection request failed (${res.status} ${res.statusText}): ${text}`);
|
|
42
|
+
}
|
|
43
|
+
const json = await res.json();
|
|
44
|
+
if (json.errors && Array.isArray(json.errors) && json.errors.length) {
|
|
45
|
+
throw new Error(`Introspection returned errors: ${JSON.stringify(json.errors, null, 2)}`);
|
|
46
|
+
}
|
|
47
|
+
if (!json.data) {
|
|
48
|
+
throw new Error('Introspection response missing data');
|
|
49
|
+
}
|
|
50
|
+
return buildClientSchema(json.data);
|
|
51
|
+
}
|
|
52
|
+
function applyFileNamePattern(pattern, view) {
|
|
53
|
+
return pattern
|
|
54
|
+
.replace(/\{viewId\}/g, view.viewId)
|
|
55
|
+
.replace(/\{collectionName\}/g, view.collectionName);
|
|
56
|
+
}
|
|
57
|
+
function getColumnDefinitionsArray(argObject) {
|
|
58
|
+
for (const p of argObject.properties) {
|
|
59
|
+
if (!ts.isPropertyAssignment(p))
|
|
60
|
+
continue;
|
|
61
|
+
const name = p.name;
|
|
62
|
+
const key = ts.isIdentifier(name)
|
|
63
|
+
? name.text
|
|
64
|
+
: ts.isStringLiteral(name)
|
|
65
|
+
? name.text
|
|
66
|
+
: null;
|
|
67
|
+
if (key !== 'columnDefinitions')
|
|
68
|
+
continue;
|
|
69
|
+
return ts.isArrayLiteralExpression(p.initializer) ? p.initializer : null;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
function hasRowTypeProp(obj) {
|
|
74
|
+
for (const p of obj.properties) {
|
|
75
|
+
if (!ts.isPropertyAssignment(p))
|
|
76
|
+
continue;
|
|
77
|
+
const name = p.name;
|
|
78
|
+
const key = ts.isIdentifier(name)
|
|
79
|
+
? name.text
|
|
80
|
+
: ts.isStringLiteral(name)
|
|
81
|
+
? name.text
|
|
82
|
+
: null;
|
|
83
|
+
if (key === 'rowType')
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
function applyTextEdits(original, edits) {
|
|
89
|
+
const sorted = [...edits].sort((a, b) => b.pos - a.pos);
|
|
90
|
+
let out = original;
|
|
91
|
+
for (const e of sorted) {
|
|
92
|
+
out = out.slice(0, e.pos) + e.insert + out.slice(e.pos);
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
function ensureRowTypeImport(sourceText, sourceFile, importName, importPathNoExt) {
|
|
97
|
+
for (const stmt of sourceFile.statements) {
|
|
98
|
+
if (!ts.isImportDeclaration(stmt))
|
|
99
|
+
continue;
|
|
100
|
+
if (!ts.isStringLiteral(stmt.moduleSpecifier))
|
|
101
|
+
continue;
|
|
102
|
+
if (stmt.moduleSpecifier.text !== importPathNoExt)
|
|
103
|
+
continue;
|
|
104
|
+
const nb = stmt.importClause?.namedBindings;
|
|
105
|
+
if (!nb || !ts.isNamedImports(nb)) {
|
|
106
|
+
return { updatedText: sourceText, changed: false };
|
|
107
|
+
}
|
|
108
|
+
if (nb.elements.some(e => e.name.text === importName)) {
|
|
109
|
+
return { updatedText: sourceText, changed: false };
|
|
110
|
+
}
|
|
111
|
+
const insertPos = nb.getEnd() - 1; // before `}`
|
|
112
|
+
const insert = `${nb.elements.length ? ', ' : ' '}${importName}`;
|
|
113
|
+
return { updatedText: applyTextEdits(sourceText, [{ pos: insertPos, insert }]), changed: true };
|
|
114
|
+
}
|
|
115
|
+
const importStmts = sourceFile.statements.filter(ts.isImportDeclaration);
|
|
116
|
+
const insertPos = importStmts.length
|
|
117
|
+
? importStmts[importStmts.length - 1].end
|
|
118
|
+
: 0;
|
|
119
|
+
const prefix = insertPos === 0 ? '' : '\n';
|
|
120
|
+
const importLine = `${prefix}import { ${importName} } from ${singleQuoteStringLiteral(importPathNoExt)};\n`;
|
|
121
|
+
return {
|
|
122
|
+
updatedText: applyTextEdits(sourceText, [{ pos: insertPos, insert: importLine }]),
|
|
123
|
+
changed: true
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function patchInlineColumnsWithRowType(args) {
|
|
127
|
+
const cols = getColumnDefinitionsArray(args.viewArgObject);
|
|
128
|
+
if (!cols)
|
|
129
|
+
return { updatedText: args.sourceText, changed: false, patchedCount: 0 };
|
|
130
|
+
const edits = [];
|
|
131
|
+
let patchedCount = 0;
|
|
132
|
+
for (const el of cols.elements) {
|
|
133
|
+
if (!ts.isCallExpression(el))
|
|
134
|
+
continue;
|
|
135
|
+
const expr = el.expression;
|
|
136
|
+
if (!ts.isPropertyAccessExpression(expr))
|
|
137
|
+
continue;
|
|
138
|
+
if (expr.name.text !== 'column')
|
|
139
|
+
continue;
|
|
140
|
+
const receiver = expr.expression;
|
|
141
|
+
const isDtvColumn = (() => {
|
|
142
|
+
if (ts.isIdentifier(receiver) && args.dslIdentifiers.has(receiver.text))
|
|
143
|
+
return true;
|
|
144
|
+
if (ts.isPropertyAccessExpression(receiver)) {
|
|
145
|
+
if (receiver.name.text !== 'DSL')
|
|
146
|
+
return false;
|
|
147
|
+
const maybeNs = receiver.expression;
|
|
148
|
+
return ts.isIdentifier(maybeNs) && args.dtvNamespaces.has(maybeNs.text);
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
})();
|
|
152
|
+
if (!isDtvColumn)
|
|
153
|
+
continue;
|
|
154
|
+
const firstArg = el.arguments[0];
|
|
155
|
+
if (!firstArg || !ts.isObjectLiteralExpression(firstArg))
|
|
156
|
+
continue;
|
|
157
|
+
if (hasRowTypeProp(firstArg))
|
|
158
|
+
continue;
|
|
159
|
+
const firstProp = firstArg.properties[0];
|
|
160
|
+
const insertPos = firstProp ? firstProp.getStart(args.sourceFile, false) : firstArg.getEnd() - 1;
|
|
161
|
+
const between = args.sourceText.slice(firstArg.getStart(args.sourceFile, false) + 1, insertPos);
|
|
162
|
+
const isMultiline = between.includes('\n');
|
|
163
|
+
if (!isMultiline) {
|
|
164
|
+
edits.push({ pos: insertPos, insert: `rowType: ${args.rowTypeIdentifier}, ` });
|
|
165
|
+
patchedCount += 1;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const lineStart = args.sourceText.lastIndexOf('\n', insertPos - 1) + 1;
|
|
169
|
+
const before = args.sourceText.slice(lineStart, insertPos);
|
|
170
|
+
const indentMatch = before.match(/^[ \t]*/);
|
|
171
|
+
const indent = indentMatch ? indentMatch[0] : '';
|
|
172
|
+
edits.push({ pos: insertPos, insert: `${indent}rowType: ${args.rowTypeIdentifier},\n` });
|
|
173
|
+
patchedCount += 1;
|
|
174
|
+
}
|
|
175
|
+
if (edits.length === 0) {
|
|
176
|
+
return { updatedText: args.sourceText, changed: false, patchedCount: 0 };
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
updatedText: applyTextEdits(args.sourceText, edits),
|
|
180
|
+
changed: true,
|
|
181
|
+
patchedCount
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function findViewsInFile(sourceText, fileName, dtvImport, debug) {
|
|
185
|
+
const sourceFile = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest, true);
|
|
186
|
+
const unwrapParens = (expr) => {
|
|
187
|
+
let cur = expr;
|
|
188
|
+
while (ts.isParenthesizedExpression(cur)) {
|
|
189
|
+
cur = cur.expression;
|
|
190
|
+
}
|
|
191
|
+
return cur;
|
|
192
|
+
};
|
|
193
|
+
const getDslReceivers = () => {
|
|
194
|
+
const dslIdentifiers = new Set();
|
|
195
|
+
const dtvNamespaces = new Set();
|
|
196
|
+
for (const stmt of sourceFile.statements) {
|
|
197
|
+
if (ts.isImportDeclaration(stmt)) {
|
|
198
|
+
if (!ts.isStringLiteral(stmt.moduleSpecifier))
|
|
199
|
+
continue;
|
|
200
|
+
if (stmt.moduleSpecifier.text !== dtvImport)
|
|
201
|
+
continue;
|
|
202
|
+
const clause = stmt.importClause;
|
|
203
|
+
if (!clause)
|
|
204
|
+
continue;
|
|
205
|
+
if (clause.name) {
|
|
206
|
+
dtvNamespaces.add(clause.name.text);
|
|
207
|
+
}
|
|
208
|
+
const nb = clause.namedBindings;
|
|
209
|
+
if (!nb)
|
|
210
|
+
continue;
|
|
211
|
+
if (ts.isNamespaceImport(nb)) {
|
|
212
|
+
dtvNamespaces.add(nb.name.text);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (ts.isNamedImports(nb)) {
|
|
216
|
+
for (const el of nb.elements) {
|
|
217
|
+
const imported = (el.propertyName ?? el.name).text;
|
|
218
|
+
const local = el.name.text;
|
|
219
|
+
if (imported === 'DSL') {
|
|
220
|
+
dslIdentifiers.add(local);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (ts.isImportEqualsDeclaration(stmt)) {
|
|
227
|
+
const mr = stmt.moduleReference;
|
|
228
|
+
if (!ts.isExternalModuleReference(mr))
|
|
229
|
+
continue;
|
|
230
|
+
const expr = mr.expression;
|
|
231
|
+
if (!expr || !ts.isStringLiteral(expr))
|
|
232
|
+
continue;
|
|
233
|
+
if (expr.text !== dtvImport)
|
|
234
|
+
continue;
|
|
235
|
+
dtvNamespaces.add(stmt.name.text);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (ts.isVariableStatement(stmt)) {
|
|
239
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
240
|
+
if (!decl.initializer)
|
|
241
|
+
continue;
|
|
242
|
+
const init = unwrapParens(decl.initializer);
|
|
243
|
+
if (!ts.isCallExpression(init))
|
|
244
|
+
continue;
|
|
245
|
+
if (!ts.isIdentifier(init.expression) || init.expression.text !== 'require')
|
|
246
|
+
continue;
|
|
247
|
+
const arg0 = init.arguments[0];
|
|
248
|
+
if (!arg0 || !ts.isStringLiteral(arg0) || arg0.text !== dtvImport)
|
|
249
|
+
continue;
|
|
250
|
+
if (ts.isIdentifier(decl.name)) {
|
|
251
|
+
dtvNamespaces.add(decl.name.text);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (ts.isObjectBindingPattern(decl.name)) {
|
|
255
|
+
for (const el of decl.name.elements) {
|
|
256
|
+
const imported = (el.propertyName ?? el.name);
|
|
257
|
+
if (ts.isIdentifier(imported) && imported.text === 'DSL') {
|
|
258
|
+
if (ts.isIdentifier(el.name)) {
|
|
259
|
+
dslIdentifiers.add(el.name.text);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return { dslIdentifiers, dtvNamespaces };
|
|
268
|
+
};
|
|
269
|
+
const receivers = getDslReceivers();
|
|
270
|
+
if (receivers.dslIdentifiers.size === 0 && receivers.dtvNamespaces.size === 0) {
|
|
271
|
+
debug?.log(`- no matching imports/requires from ${JSON.stringify(dtvImport)}`);
|
|
272
|
+
return [];
|
|
273
|
+
}
|
|
274
|
+
debug?.log(`- dslIdentifiers: ${[...receivers.dslIdentifiers].sort().join(', ') || '(none)'}`);
|
|
275
|
+
debug?.log(`- dtvNamespaces: ${[...receivers.dtvNamespaces].sort().join(', ') || '(none)'}`);
|
|
276
|
+
const views = [];
|
|
277
|
+
const tryGetStringProp = (obj, propName) => {
|
|
278
|
+
for (const p of obj.properties) {
|
|
279
|
+
if (!ts.isPropertyAssignment(p))
|
|
280
|
+
continue;
|
|
281
|
+
const name = p.name;
|
|
282
|
+
const key = ts.isIdentifier(name)
|
|
283
|
+
? name.text
|
|
284
|
+
: ts.isStringLiteral(name)
|
|
285
|
+
? name.text
|
|
286
|
+
: null;
|
|
287
|
+
if (key !== propName)
|
|
288
|
+
continue;
|
|
289
|
+
if (ts.isStringLiteral(p.initializer))
|
|
290
|
+
return p.initializer.text;
|
|
291
|
+
if (ts.isNoSubstitutionTemplateLiteral(p.initializer))
|
|
292
|
+
return p.initializer.text;
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
};
|
|
297
|
+
const visit = (node) => {
|
|
298
|
+
if (ts.isCallExpression(node)) {
|
|
299
|
+
const expr = node.expression;
|
|
300
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
301
|
+
const receiver = unwrapParens(expr.expression);
|
|
302
|
+
const method = expr.name.text;
|
|
303
|
+
const isDslReceiver = (() => {
|
|
304
|
+
if (ts.isIdentifier(receiver) && receivers.dslIdentifiers.has(receiver.text))
|
|
305
|
+
return true;
|
|
306
|
+
if (ts.isPropertyAccessExpression(receiver)) {
|
|
307
|
+
const maybeNs = unwrapParens(receiver.expression);
|
|
308
|
+
if (receiver.name.text !== 'DSL')
|
|
309
|
+
return false;
|
|
310
|
+
return ts.isIdentifier(maybeNs) && receivers.dtvNamespaces.has(maybeNs.text);
|
|
311
|
+
}
|
|
312
|
+
return false;
|
|
313
|
+
})();
|
|
314
|
+
if (method === 'view' && isDslReceiver) {
|
|
315
|
+
const firstArg = node.arguments[0];
|
|
316
|
+
if (!firstArg) {
|
|
317
|
+
debug?.log('- found DSL.view(...) with no args (skipping)');
|
|
318
|
+
}
|
|
319
|
+
else if (!ts.isObjectLiteralExpression(firstArg)) {
|
|
320
|
+
debug?.log('- found DSL.view(<non-object-literal>) (skipping)');
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
const viewId = tryGetStringProp(firstArg, 'id');
|
|
324
|
+
const collectionName = tryGetStringProp(firstArg, 'collectionName');
|
|
325
|
+
if (!viewId || !collectionName) {
|
|
326
|
+
debug?.log(`- found DSL.view({ ... }) but id/collectionName not string literals (id=${viewId ?? 'null'}, collectionName=${collectionName ?? 'null'})`);
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
debug?.log(`- found view id=${JSON.stringify(viewId)} collectionName=${JSON.stringify(collectionName)}`);
|
|
330
|
+
views.push({
|
|
331
|
+
viewId,
|
|
332
|
+
collectionName,
|
|
333
|
+
sourceFile: fileName
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
ts.forEachChild(node, visit);
|
|
341
|
+
};
|
|
342
|
+
visit(sourceFile);
|
|
343
|
+
return views;
|
|
344
|
+
}
|
|
345
|
+
async function scanViews(config, debug) {
|
|
346
|
+
const dtvImport = config.scan.dtvImport ?? '@kronor/dtv';
|
|
347
|
+
const files = await fg(config.scan.include, {
|
|
348
|
+
ignore: config.scan.exclude ?? [],
|
|
349
|
+
absolute: true,
|
|
350
|
+
onlyFiles: true
|
|
351
|
+
});
|
|
352
|
+
const focusAbs = debug?.focusFile
|
|
353
|
+
? (path.isAbsolute(debug.focusFile) ? debug.focusFile : path.resolve(process.cwd(), debug.focusFile))
|
|
354
|
+
: undefined;
|
|
355
|
+
if (debug?.enabled) {
|
|
356
|
+
console.log('[dtv typegen] scan debug');
|
|
357
|
+
console.log(`- dtvImport: ${dtvImport}`);
|
|
358
|
+
console.log(`- include: ${JSON.stringify(config.scan.include)}`);
|
|
359
|
+
console.log(`- exclude: ${JSON.stringify(config.scan.exclude ?? [])}`);
|
|
360
|
+
console.log(`- matchedFiles: ${files.length}`);
|
|
361
|
+
if (focusAbs) {
|
|
362
|
+
console.log(`- focusFile: ${focusAbs}`);
|
|
363
|
+
console.log(`- focusFileMatchedByGlob: ${files.map(f => path.resolve(f)).includes(path.resolve(focusAbs))}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const results = [];
|
|
367
|
+
const filesToScan = focusAbs ? files.filter(f => path.resolve(f) === path.resolve(focusAbs)) : files;
|
|
368
|
+
for (const f of filesToScan) {
|
|
369
|
+
if (!f.endsWith('.ts') && !f.endsWith('.tsx'))
|
|
370
|
+
continue;
|
|
371
|
+
const text = await fs.readFile(f, 'utf8');
|
|
372
|
+
const fileDebug = debug?.enabled
|
|
373
|
+
? { log: (line) => console.log(`[dtv typegen] ${path.resolve(f)} ${line}`) }
|
|
374
|
+
: undefined;
|
|
375
|
+
results.push(...findViewsInFile(text, f, dtvImport, fileDebug));
|
|
376
|
+
}
|
|
377
|
+
const byId = new Map();
|
|
378
|
+
for (const v of results) {
|
|
379
|
+
const list = byId.get(v.viewId);
|
|
380
|
+
if (list)
|
|
381
|
+
list.push(v);
|
|
382
|
+
else
|
|
383
|
+
byId.set(v.viewId, [v]);
|
|
384
|
+
}
|
|
385
|
+
const duplicates = [];
|
|
386
|
+
for (const [viewId, views] of byId.entries()) {
|
|
387
|
+
if (views.length > 1)
|
|
388
|
+
duplicates.push({ viewId, views });
|
|
389
|
+
}
|
|
390
|
+
if (duplicates.length) {
|
|
391
|
+
const lines = [];
|
|
392
|
+
lines.push('Duplicate DTV view ids found (each view `id` must be unique):');
|
|
393
|
+
for (const d of duplicates.sort((a, b) => a.viewId.localeCompare(b.viewId))) {
|
|
394
|
+
lines.push(`- ${d.viewId}`);
|
|
395
|
+
for (const v of d.views) {
|
|
396
|
+
lines.push(` - ${path.resolve(v.sourceFile)}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
throw new Error(lines.join('\n'));
|
|
400
|
+
}
|
|
401
|
+
return results;
|
|
402
|
+
}
|
|
403
|
+
export async function runTypegen(args) {
|
|
404
|
+
const config = await loadConfig(args.configPath);
|
|
405
|
+
const debug = (args.debugScan || args.debugScanFile)
|
|
406
|
+
? { enabled: true, focusFile: args.debugScanFile }
|
|
407
|
+
: undefined;
|
|
408
|
+
const views = await scanViews(config, debug);
|
|
409
|
+
if (views.length === 0) {
|
|
410
|
+
throw new Error('No views found. Ensure Config.scan.include matches files that import DSL from your configured DTV specifier and call DSL.view({ ... }).');
|
|
411
|
+
}
|
|
412
|
+
const selectedViews = args.onlyViewId
|
|
413
|
+
? views.filter(v => v.viewId === args.onlyViewId)
|
|
414
|
+
: views;
|
|
415
|
+
if (args.onlyViewId && selectedViews.length === 0) {
|
|
416
|
+
const sample = views.map(v => v.viewId).slice(0, 25);
|
|
417
|
+
throw new Error(`No view found with id ${JSON.stringify(args.onlyViewId)}. `
|
|
418
|
+
+ `Sample discovered view ids: ${sample.join(', ')}${sample.length === 25 ? ', ...' : ''}`);
|
|
419
|
+
}
|
|
420
|
+
const schema = await fetchSchema(config.schema.endpoint, config.schema.headers ?? {});
|
|
421
|
+
const queryType = schema.getQueryType();
|
|
422
|
+
if (!queryType)
|
|
423
|
+
throw new Error('Schema has no Query type');
|
|
424
|
+
const queryFields = queryType.getFields();
|
|
425
|
+
const dtvImport = config.scan.dtvImport ?? '@kronor/dtv';
|
|
426
|
+
// Resolve view -> row type
|
|
427
|
+
const viewRows = selectedViews.map(v => {
|
|
428
|
+
const f = queryFields[v.collectionName];
|
|
429
|
+
if (!f) {
|
|
430
|
+
const sample = Object.keys(queryFields).slice(0, 25);
|
|
431
|
+
throw new Error(`View "${v.viewId}" references collectionName "${v.collectionName}" but it was not found on Query. `
|
|
432
|
+
+ `Sample Query fields: ${sample.join(', ')}${sample.length === 25 ? ', ...' : ''}`);
|
|
433
|
+
}
|
|
434
|
+
const rowNamed = unwrapCollectionElementType(f.type);
|
|
435
|
+
return {
|
|
436
|
+
...v,
|
|
437
|
+
rowTypeName: rowNamed.name
|
|
438
|
+
};
|
|
439
|
+
});
|
|
440
|
+
const outputFiles = new Set();
|
|
441
|
+
for (const v of viewRows) {
|
|
442
|
+
const root = schema.getType(v.rowTypeName);
|
|
443
|
+
if (!root || Array.isArray(root)) {
|
|
444
|
+
throw new Error(`Could not resolve row type "${v.rowTypeName}" in schema for view "${v.viewId}"`);
|
|
445
|
+
}
|
|
446
|
+
const reachable = collectReachableTypes(schema, [root]);
|
|
447
|
+
const viewTypeName = `${toIdentifier(toPascalCase(v.viewId))}Row`;
|
|
448
|
+
const rowTypeConstName = `${toIdentifier(toPascalCase(v.viewId))}RowType`;
|
|
449
|
+
const fileName = applyFileNamePattern(config.output.fileNamePattern, v);
|
|
450
|
+
if (!fileName.endsWith('.ts')) {
|
|
451
|
+
throw new Error(`Config.output.fileNamePattern must produce a .ts file name. Got: ${fileName}`);
|
|
452
|
+
}
|
|
453
|
+
const outFile = path.join(path.dirname(v.sourceFile), fileName);
|
|
454
|
+
if (outputFiles.has(outFile)) {
|
|
455
|
+
throw new Error(`Multiple views would write the same output file: ${outFile}`);
|
|
456
|
+
}
|
|
457
|
+
outputFiles.add(outFile);
|
|
458
|
+
const content = [
|
|
459
|
+
`import { DSL as DTV } from ${singleQuoteStringLiteral(dtvImport)};`,
|
|
460
|
+
'',
|
|
461
|
+
renderTsFromSchema(reachable, {
|
|
462
|
+
scalars: config.scalars,
|
|
463
|
+
includeGraphqlTypeComments: config.debug?.includeGraphqlTypeComments === true,
|
|
464
|
+
exportTypes: false
|
|
465
|
+
}).trimEnd(),
|
|
466
|
+
'',
|
|
467
|
+
`export type ${viewTypeName} = ${v.rowTypeName};`,
|
|
468
|
+
`export const ${rowTypeConstName} = DTV.rowType<${viewTypeName}>();`,
|
|
469
|
+
''
|
|
470
|
+
].join('\n');
|
|
471
|
+
await writeFileEnsuringDir(outFile, content);
|
|
472
|
+
// Best-effort patch view file for inline columns.
|
|
473
|
+
try {
|
|
474
|
+
const viewText = await fs.readFile(v.sourceFile, 'utf8');
|
|
475
|
+
const sf = ts.createSourceFile(v.sourceFile, viewText, ts.ScriptTarget.Latest, true);
|
|
476
|
+
const dslIdentifiers = new Set();
|
|
477
|
+
const dtvNamespaces = new Set();
|
|
478
|
+
for (const stmt of sf.statements) {
|
|
479
|
+
if (ts.isImportDeclaration(stmt)) {
|
|
480
|
+
if (!ts.isStringLiteral(stmt.moduleSpecifier))
|
|
481
|
+
continue;
|
|
482
|
+
if (stmt.moduleSpecifier.text !== dtvImport)
|
|
483
|
+
continue;
|
|
484
|
+
const clause = stmt.importClause;
|
|
485
|
+
if (!clause)
|
|
486
|
+
continue;
|
|
487
|
+
if (clause.name)
|
|
488
|
+
dtvNamespaces.add(clause.name.text);
|
|
489
|
+
const nb = clause.namedBindings;
|
|
490
|
+
if (!nb)
|
|
491
|
+
continue;
|
|
492
|
+
if (ts.isNamespaceImport(nb)) {
|
|
493
|
+
dtvNamespaces.add(nb.name.text);
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (ts.isNamedImports(nb)) {
|
|
497
|
+
for (const el of nb.elements) {
|
|
498
|
+
const imported = (el.propertyName ?? el.name).text;
|
|
499
|
+
const local = el.name.text;
|
|
500
|
+
if (imported === 'DSL')
|
|
501
|
+
dslIdentifiers.add(local);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (ts.isImportEqualsDeclaration(stmt)) {
|
|
506
|
+
const mr = stmt.moduleReference;
|
|
507
|
+
if (!ts.isExternalModuleReference(mr))
|
|
508
|
+
continue;
|
|
509
|
+
const expr = mr.expression;
|
|
510
|
+
if (!expr || !ts.isStringLiteral(expr))
|
|
511
|
+
continue;
|
|
512
|
+
if (expr.text !== dtvImport)
|
|
513
|
+
continue;
|
|
514
|
+
dtvNamespaces.add(stmt.name.text);
|
|
515
|
+
}
|
|
516
|
+
if (ts.isVariableStatement(stmt)) {
|
|
517
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
518
|
+
if (!decl.initializer)
|
|
519
|
+
continue;
|
|
520
|
+
if (!ts.isCallExpression(decl.initializer))
|
|
521
|
+
continue;
|
|
522
|
+
if (!ts.isIdentifier(decl.initializer.expression) || decl.initializer.expression.text !== 'require')
|
|
523
|
+
continue;
|
|
524
|
+
const arg0 = decl.initializer.arguments[0];
|
|
525
|
+
if (!arg0 || !ts.isStringLiteral(arg0) || arg0.text !== dtvImport)
|
|
526
|
+
continue;
|
|
527
|
+
if (ts.isIdentifier(decl.name)) {
|
|
528
|
+
dtvNamespaces.add(decl.name.text);
|
|
529
|
+
}
|
|
530
|
+
else if (ts.isObjectBindingPattern(decl.name)) {
|
|
531
|
+
for (const el of decl.name.elements) {
|
|
532
|
+
const imported = (el.propertyName ?? el.name);
|
|
533
|
+
if (ts.isIdentifier(imported) && imported.text === 'DSL') {
|
|
534
|
+
if (ts.isIdentifier(el.name))
|
|
535
|
+
dslIdentifiers.add(el.name.text);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
// Find the first matching view call object for this viewId.
|
|
543
|
+
let viewArgObject = null;
|
|
544
|
+
const visit = (node) => {
|
|
545
|
+
if (viewArgObject)
|
|
546
|
+
return;
|
|
547
|
+
if (ts.isCallExpression(node)) {
|
|
548
|
+
const expr = node.expression;
|
|
549
|
+
if (ts.isPropertyAccessExpression(expr) && expr.name.text === 'view') {
|
|
550
|
+
const receiver = expr.expression;
|
|
551
|
+
const isDslReceiver = (() => {
|
|
552
|
+
if (ts.isIdentifier(receiver) && dslIdentifiers.has(receiver.text))
|
|
553
|
+
return true;
|
|
554
|
+
if (ts.isPropertyAccessExpression(receiver) && receiver.name.text === 'DSL') {
|
|
555
|
+
const maybeNs = receiver.expression;
|
|
556
|
+
return ts.isIdentifier(maybeNs) && dtvNamespaces.has(maybeNs.text);
|
|
557
|
+
}
|
|
558
|
+
return false;
|
|
559
|
+
})();
|
|
560
|
+
if (!isDslReceiver)
|
|
561
|
+
return;
|
|
562
|
+
const firstArg = node.arguments[0];
|
|
563
|
+
if (!firstArg || !ts.isObjectLiteralExpression(firstArg))
|
|
564
|
+
return;
|
|
565
|
+
const idProp = firstArg.properties.find(p => ts.isPropertyAssignment(p)
|
|
566
|
+
&& ((ts.isIdentifier(p.name) && p.name.text === 'id')
|
|
567
|
+
|| (ts.isStringLiteral(p.name) && p.name.text === 'id')));
|
|
568
|
+
const idVal = idProp?.initializer;
|
|
569
|
+
const id = idVal && (ts.isStringLiteral(idVal) || ts.isNoSubstitutionTemplateLiteral(idVal))
|
|
570
|
+
? idVal.text
|
|
571
|
+
: null;
|
|
572
|
+
if (id === v.viewId) {
|
|
573
|
+
viewArgObject = firstArg;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
ts.forEachChild(node, visit);
|
|
578
|
+
};
|
|
579
|
+
visit(sf);
|
|
580
|
+
if (!viewArgObject) {
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
const importPathNoExt = './' + fileName.replace(/\.ts$/i, '');
|
|
584
|
+
const withImport = ensureRowTypeImport(viewText, sf, rowTypeConstName, importPathNoExt);
|
|
585
|
+
const sf2 = ts.createSourceFile(v.sourceFile, withImport.updatedText, ts.ScriptTarget.Latest, true);
|
|
586
|
+
// Re-find view object in updated text.
|
|
587
|
+
let viewArgObject2 = null;
|
|
588
|
+
const visit2 = (node) => {
|
|
589
|
+
if (viewArgObject2)
|
|
590
|
+
return;
|
|
591
|
+
if (ts.isCallExpression(node)) {
|
|
592
|
+
const expr = node.expression;
|
|
593
|
+
if (ts.isPropertyAccessExpression(expr) && expr.name.text === 'view') {
|
|
594
|
+
const firstArg = node.arguments[0];
|
|
595
|
+
if (!firstArg || !ts.isObjectLiteralExpression(firstArg))
|
|
596
|
+
return;
|
|
597
|
+
const idProp = firstArg.properties.find(p => ts.isPropertyAssignment(p)
|
|
598
|
+
&& ((ts.isIdentifier(p.name) && p.name.text === 'id')
|
|
599
|
+
|| (ts.isStringLiteral(p.name) && p.name.text === 'id')));
|
|
600
|
+
const idVal = idProp?.initializer;
|
|
601
|
+
const id = idVal && (ts.isStringLiteral(idVal) || ts.isNoSubstitutionTemplateLiteral(idVal))
|
|
602
|
+
? idVal.text
|
|
603
|
+
: null;
|
|
604
|
+
if (id === v.viewId) {
|
|
605
|
+
viewArgObject2 = firstArg;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
ts.forEachChild(node, visit2);
|
|
610
|
+
};
|
|
611
|
+
visit2(sf2);
|
|
612
|
+
if (!viewArgObject2) {
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
const patched = patchInlineColumnsWithRowType({
|
|
616
|
+
sourceText: withImport.updatedText,
|
|
617
|
+
sourceFile: sf2,
|
|
618
|
+
viewArgObject: viewArgObject2,
|
|
619
|
+
rowTypeIdentifier: rowTypeConstName,
|
|
620
|
+
dslIdentifiers,
|
|
621
|
+
dtvNamespaces
|
|
622
|
+
});
|
|
623
|
+
if (withImport.changed || patched.changed) {
|
|
624
|
+
await fs.writeFile(v.sourceFile, patched.updatedText, 'utf8');
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
// Ignore patching errors; generation still succeeds.
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (args.onlyViewId) {
|
|
632
|
+
console.log(`Generated types for view ${JSON.stringify(args.onlyViewId)}.`);
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
console.log(`Generated types for ${viewRows.length} view(s).`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
//# sourceMappingURL=runTypegen.js.map
|