@dereekb/dbx-cli 13.11.0 → 13.11.2
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/firebase-api-manifest/main.js +711 -0
- package/firebase-api-manifest/package.json +10 -0
- package/index.cjs.js +242 -54
- package/index.esm.js +240 -55
- package/manifest-extract/LICENSE +21 -0
- package/manifest-extract/index.cjs.default.js +1 -0
- package/manifest-extract/index.cjs.js +459 -0
- package/manifest-extract/index.cjs.mjs +2 -0
- package/manifest-extract/index.d.ts +1 -0
- package/manifest-extract/index.esm.js +457 -0
- package/manifest-extract/package.json +20 -0
- package/manifest-extract/src/index.d.ts +2 -0
- package/manifest-extract/src/lib/extract-crud.d.ts +36 -0
- package/manifest-extract/src/lib/types.d.ts +86 -0
- package/package.json +22 -10
- package/src/lib/config/env.d.ts +1 -1
- package/src/lib/manifest/build-manifest-commands.d.ts +58 -3
- package/src/lib/manifest/types.d.ts +20 -0
- package/src/lib/runner/run.d.ts +3 -0
- package/firebase-api-manifest/src/generate-api-manifest/bind-validators.d.ts +0 -44
- package/firebase-api-manifest/src/generate-api-manifest/emit.d.ts +0 -29
- package/firebase-api-manifest/src/generate-api-manifest/extract-crud.d.ts +0 -25
- package/firebase-api-manifest/src/generate-api-manifest/find-api-files.d.ts +0 -16
- package/firebase-api-manifest/src/generate-api-manifest/main.d.ts +0 -46
- package/firebase-api-manifest/src/generate-api-manifest/parse-functions.d.ts +0 -18
- package/firebase-api-manifest/src/generate-api-manifest/resolve-package.d.ts +0 -59
- package/firebase-api-manifest/src/generate-api-manifest/types.d.ts +0 -44
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __createRequire } from 'node:module';
|
|
3
|
+
const require = __createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
// packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/main.ts
|
|
6
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync5, writeFileSync } from "node:fs";
|
|
7
|
+
import { dirname as dirname3, isAbsolute as isAbsolute3, relative as relative2, resolve as resolve3 } from "node:path";
|
|
8
|
+
|
|
9
|
+
// packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/parse-functions.ts
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { Node, Project } from "ts-morph";
|
|
12
|
+
function parseFunctionsConfig(functionsTsPath) {
|
|
13
|
+
const project = new Project({ useInMemoryFileSystem: true, skipAddingFilesFromTsConfig: true });
|
|
14
|
+
const text = readFileSync(functionsTsPath, "utf8");
|
|
15
|
+
const sourceFile = project.createSourceFile("functions.ts", text, { overwrite: true });
|
|
16
|
+
const importIdentToModule = collectImportSpecifierMap(sourceFile);
|
|
17
|
+
const result = [];
|
|
18
|
+
for (const variable of sourceFile.getVariableDeclarations()) {
|
|
19
|
+
const initializer = readFunctionsConfigInitializer(variable);
|
|
20
|
+
if (initializer) collectGroupsFromConfig(initializer, importIdentToModule, result);
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
function readFunctionsConfigInitializer(variable) {
|
|
25
|
+
let result;
|
|
26
|
+
if (isFunctionsConfigVariable(variable)) {
|
|
27
|
+
const initializer = variable.getInitializer();
|
|
28
|
+
if (initializer && Node.isObjectLiteralExpression(initializer)) result = initializer;
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
function collectGroupsFromConfig(initializer, importIdentToModule, sink) {
|
|
33
|
+
for (const property of initializer.getProperties()) {
|
|
34
|
+
const group = readGroupFromProperty(property, importIdentToModule);
|
|
35
|
+
if (group) sink.push(group);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function readGroupFromProperty(property, importIdentToModule) {
|
|
39
|
+
if (!Node.isPropertyAssignment(property)) return void 0;
|
|
40
|
+
const valueExpr = property.getInitializer();
|
|
41
|
+
if (!valueExpr || !Node.isArrayLiteralExpression(valueExpr)) return void 0;
|
|
42
|
+
const first = valueExpr.getElements()[0];
|
|
43
|
+
if (!first || !Node.isIdentifier(first)) return void 0;
|
|
44
|
+
const className = first.getText();
|
|
45
|
+
const importedFromModule = importIdentToModule.get(className);
|
|
46
|
+
if (!importedFromModule) return void 0;
|
|
47
|
+
return { groupKey: property.getName(), className, importedFromModule };
|
|
48
|
+
}
|
|
49
|
+
function isFunctionsConfigVariable(variable) {
|
|
50
|
+
const name = variable.getName();
|
|
51
|
+
return name.endsWith("FIREBASE_FUNCTIONS_CONFIG");
|
|
52
|
+
}
|
|
53
|
+
function collectImportSpecifierMap(sourceFile) {
|
|
54
|
+
const map = /* @__PURE__ */ new Map();
|
|
55
|
+
for (const decl of sourceFile.getImportDeclarations()) {
|
|
56
|
+
const moduleSpecifier = decl.getModuleSpecifierValue();
|
|
57
|
+
for (const named of decl.getNamedImports()) {
|
|
58
|
+
const localName = named.getAliasNode()?.getText() ?? named.getName();
|
|
59
|
+
map.set(localName, moduleSpecifier);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return map;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/resolve-package.ts
|
|
66
|
+
import { existsSync, readFileSync as readFileSync2 } from "node:fs";
|
|
67
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
68
|
+
var _pathsCache;
|
|
69
|
+
function loadTsconfigPaths(workspaceRoot) {
|
|
70
|
+
if (_pathsCache) return _pathsCache;
|
|
71
|
+
const tsconfigPath = join(workspaceRoot, "tsconfig.base.json");
|
|
72
|
+
const raw = readFileSync2(tsconfigPath, "utf8");
|
|
73
|
+
const cleaned = stripJsonComments(raw);
|
|
74
|
+
const tsconfig = JSON.parse(cleaned);
|
|
75
|
+
const paths = tsconfig.compilerOptions?.paths ?? {};
|
|
76
|
+
const map = /* @__PURE__ */ new Map();
|
|
77
|
+
for (const [key, value] of Object.entries(paths)) {
|
|
78
|
+
if (Array.isArray(value) && value[0]) {
|
|
79
|
+
map.set(key, resolve(workspaceRoot, value[0]));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
_pathsCache = map;
|
|
83
|
+
return map;
|
|
84
|
+
}
|
|
85
|
+
function resolveModuleToPackage(input) {
|
|
86
|
+
const { workspaceRoot, importingFile, moduleSpecifier } = input;
|
|
87
|
+
if (moduleSpecifier.startsWith(".")) {
|
|
88
|
+
const importingDir = dirname(importingFile);
|
|
89
|
+
const candidatePath = resolve(importingDir, moduleSpecifier);
|
|
90
|
+
return locatePackageForPath(workspaceRoot, candidatePath);
|
|
91
|
+
}
|
|
92
|
+
const paths = loadTsconfigPaths(workspaceRoot);
|
|
93
|
+
const indexFile = paths.get(moduleSpecifier);
|
|
94
|
+
if (indexFile) return locatePackageForPath(workspaceRoot, indexFile);
|
|
95
|
+
return locatePackageInNodeModules({ workspaceRoot, importingFile, moduleSpecifier });
|
|
96
|
+
}
|
|
97
|
+
function locatePackageInNodeModules(input) {
|
|
98
|
+
const { importingFile, moduleSpecifier } = input;
|
|
99
|
+
let current = dirname(importingFile);
|
|
100
|
+
while (current && current !== dirname(current)) {
|
|
101
|
+
const candidateRoot = join(current, "node_modules", moduleSpecifier);
|
|
102
|
+
const pkgJson = join(candidateRoot, "package.json");
|
|
103
|
+
if (existsSync(pkgJson)) {
|
|
104
|
+
try {
|
|
105
|
+
const pkg = JSON.parse(readFileSync2(pkgJson, "utf8"));
|
|
106
|
+
if (pkg.name) return { packageName: pkg.name, packageRoot: candidateRoot };
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
current = dirname(current);
|
|
111
|
+
}
|
|
112
|
+
return void 0;
|
|
113
|
+
}
|
|
114
|
+
function locatePackageForPath(workspaceRoot, startPath) {
|
|
115
|
+
let current = startPath;
|
|
116
|
+
while (current && current !== workspaceRoot && current !== dirname(current)) {
|
|
117
|
+
if (existsSync(join(current, "package.json"))) {
|
|
118
|
+
try {
|
|
119
|
+
const pkg = JSON.parse(readFileSync2(join(current, "package.json"), "utf8"));
|
|
120
|
+
if (pkg.name) return { packageName: pkg.name, packageRoot: current };
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
current = dirname(current);
|
|
125
|
+
}
|
|
126
|
+
return void 0;
|
|
127
|
+
}
|
|
128
|
+
function relPath(workspaceRoot, absolutePath) {
|
|
129
|
+
return relative(workspaceRoot, absolutePath).split("\\").join("/");
|
|
130
|
+
}
|
|
131
|
+
function stripJsonComments(text) {
|
|
132
|
+
return text.replaceAll(/\/\*[\s\S]*?\*\//g, "").replaceAll(/^\s*\/\/.*$/gm, "");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/find-api-files.ts
|
|
136
|
+
import { readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
|
|
137
|
+
import { join as join2 } from "node:path";
|
|
138
|
+
|
|
139
|
+
// packages/dbx-cli/manifest-extract/src/lib/extract-crud.ts
|
|
140
|
+
import { Node as Node2, Project as Project2 } from "ts-morph";
|
|
141
|
+
var SUPPORTED_VERBS = /* @__PURE__ */ new Set(["create", "read", "update", "delete", "query"]);
|
|
142
|
+
function extractCrudEntries(source) {
|
|
143
|
+
const project = new Project2({ useInMemoryFileSystem: true, skipAddingFilesFromTsConfig: true });
|
|
144
|
+
const sourceFile = project.createSourceFile(source.name, source.text, { overwrite: true });
|
|
145
|
+
const entries = [];
|
|
146
|
+
const modelKeys = [];
|
|
147
|
+
const crudConfigType = findTypeAliasByEnding(sourceFile, "ModelCrudFunctionsConfig");
|
|
148
|
+
const groupName = inferGroupName(sourceFile);
|
|
149
|
+
const functionsClassName = findFunctionsClassName(sourceFile);
|
|
150
|
+
const typeDocsCache = /* @__PURE__ */ new Map();
|
|
151
|
+
const resolveTypeDocs = (typeName) => {
|
|
152
|
+
if (!typeName) {
|
|
153
|
+
return void 0;
|
|
154
|
+
}
|
|
155
|
+
if (typeDocsCache.has(typeName)) {
|
|
156
|
+
return typeDocsCache.get(typeName);
|
|
157
|
+
}
|
|
158
|
+
const docs = readTypeDocs(sourceFile, typeName);
|
|
159
|
+
if (docs) {
|
|
160
|
+
typeDocsCache.set(typeName, docs);
|
|
161
|
+
}
|
|
162
|
+
return docs;
|
|
163
|
+
};
|
|
164
|
+
if (crudConfigType) {
|
|
165
|
+
const literal = crudConfigType.getTypeNode();
|
|
166
|
+
if (literal && Node2.isTypeLiteral(literal)) {
|
|
167
|
+
for (const member of literal.getMembers()) {
|
|
168
|
+
if (!Node2.isPropertySignature(member)) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const modelName = member.getName();
|
|
172
|
+
modelKeys.push(modelName);
|
|
173
|
+
const valueNode = member.getTypeNode();
|
|
174
|
+
if (!valueNode) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (isNullLiteralType(valueNode)) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (Node2.isTypeLiteral(valueNode)) {
|
|
181
|
+
for (const verbMember of valueNode.getMembers()) {
|
|
182
|
+
if (!Node2.isPropertySignature(verbMember)) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const verbName = verbMember.getName();
|
|
186
|
+
if (!SUPPORTED_VERBS.has(verbName)) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const verb = verbName;
|
|
190
|
+
const verbValueNode = verbMember.getTypeNode();
|
|
191
|
+
if (!verbValueNode) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
collectVerbEntries({ modelName, verb, valueNode: verbValueNode, entries, fallbackDescription: readJsDocSummary(verbMember), resolveTypeDocs });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const functionTypeMap = findTypeAliasByEnding(sourceFile, "FunctionTypeMap");
|
|
201
|
+
if (functionTypeMap) {
|
|
202
|
+
const literal = functionTypeMap.getTypeNode();
|
|
203
|
+
if (literal && Node2.isTypeLiteral(literal)) {
|
|
204
|
+
for (const member of literal.getMembers()) {
|
|
205
|
+
if (!Node2.isPropertySignature(member)) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const key = member.getName();
|
|
209
|
+
const valueNode = member.getTypeNode();
|
|
210
|
+
const tuple = valueNode ? readTupleParamsResult(valueNode) : void 0;
|
|
211
|
+
const paramsDocs = resolveTypeDocs(tuple?.params);
|
|
212
|
+
const resultDocs = resolveTypeDocs(tuple?.result);
|
|
213
|
+
entries.push({
|
|
214
|
+
model: key,
|
|
215
|
+
verb: "standalone",
|
|
216
|
+
specifier: void 0,
|
|
217
|
+
paramsTypeName: tuple?.params,
|
|
218
|
+
resultTypeName: tuple?.result,
|
|
219
|
+
line: member.getStartLineNumber(),
|
|
220
|
+
description: readJsDocSummary(member),
|
|
221
|
+
paramsTypeDescription: paramsDocs?.typeDescription,
|
|
222
|
+
paramsFields: paramsDocs?.fields,
|
|
223
|
+
resultTypeDescription: resultDocs?.typeDescription,
|
|
224
|
+
resultFields: resultDocs?.fields
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return { groupName, modelKeys, entries, functionsClassName };
|
|
230
|
+
}
|
|
231
|
+
function findTypeAliasByEnding(sourceFile, ending) {
|
|
232
|
+
for (const alias of sourceFile.getTypeAliases()) {
|
|
233
|
+
if (alias.getName().endsWith(ending) && alias.getTypeNode()) {
|
|
234
|
+
return alias;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return void 0;
|
|
238
|
+
}
|
|
239
|
+
function findFunctionsClassName(sourceFile) {
|
|
240
|
+
for (const cls of sourceFile.getClasses()) {
|
|
241
|
+
if (!cls.isAbstract()) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const name = cls.getName();
|
|
245
|
+
if (name?.endsWith("Functions")) {
|
|
246
|
+
return name;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return void 0;
|
|
250
|
+
}
|
|
251
|
+
function inferGroupName(sourceFile) {
|
|
252
|
+
for (const alias of sourceFile.getTypeAliases()) {
|
|
253
|
+
const name = alias.getName();
|
|
254
|
+
if (name.endsWith("ModelCrudFunctionsConfig")) {
|
|
255
|
+
const stem = name.slice(0, -"ModelCrudFunctionsConfig".length);
|
|
256
|
+
if (stem.length > 0) {
|
|
257
|
+
return stem;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
for (const alias of sourceFile.getTypeAliases()) {
|
|
262
|
+
const name = alias.getName();
|
|
263
|
+
if (name.endsWith("FunctionTypeMap")) {
|
|
264
|
+
const stem = name.slice(0, -"FunctionTypeMap".length);
|
|
265
|
+
if (stem.length > 0) {
|
|
266
|
+
return stem;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return void 0;
|
|
271
|
+
}
|
|
272
|
+
function isNullLiteralType(node) {
|
|
273
|
+
if (Node2.isLiteralTypeNode(node)) {
|
|
274
|
+
const literal = node.getLiteral();
|
|
275
|
+
if (Node2.isNullLiteral(literal)) {
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
function collectVerbEntries(input) {
|
|
282
|
+
const { modelName, verb, valueNode, entries, fallbackDescription, resolveTypeDocs } = input;
|
|
283
|
+
if (Node2.isTypeLiteral(valueNode)) {
|
|
284
|
+
for (const specMember of valueNode.getMembers()) {
|
|
285
|
+
if (!Node2.isPropertySignature(specMember)) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const specifier = specMember.getName();
|
|
289
|
+
const leafNode = specMember.getTypeNode();
|
|
290
|
+
const leaf2 = leafNode ? readTupleParamsResult(leafNode) ?? readBareParams(leafNode) : void 0;
|
|
291
|
+
const paramsDocs2 = resolveTypeDocs(leaf2?.params);
|
|
292
|
+
const resultDocs2 = resolveTypeDocs(leaf2?.result);
|
|
293
|
+
entries.push({
|
|
294
|
+
model: modelName,
|
|
295
|
+
verb,
|
|
296
|
+
specifier,
|
|
297
|
+
paramsTypeName: leaf2?.params,
|
|
298
|
+
resultTypeName: leaf2?.result,
|
|
299
|
+
line: specMember.getStartLineNumber(),
|
|
300
|
+
description: readJsDocSummary(specMember),
|
|
301
|
+
paramsTypeDescription: paramsDocs2?.typeDescription,
|
|
302
|
+
paramsFields: paramsDocs2?.fields,
|
|
303
|
+
resultTypeDescription: resultDocs2?.typeDescription,
|
|
304
|
+
resultFields: resultDocs2?.fields
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const leaf = readTupleParamsResult(valueNode) ?? readBareParams(valueNode);
|
|
310
|
+
const paramsDocs = resolveTypeDocs(leaf?.params);
|
|
311
|
+
const resultDocs = resolveTypeDocs(leaf?.result);
|
|
312
|
+
entries.push({
|
|
313
|
+
model: modelName,
|
|
314
|
+
verb,
|
|
315
|
+
specifier: void 0,
|
|
316
|
+
paramsTypeName: leaf?.params,
|
|
317
|
+
resultTypeName: leaf?.result,
|
|
318
|
+
line: valueNode.getStartLineNumber(),
|
|
319
|
+
description: fallbackDescription,
|
|
320
|
+
paramsTypeDescription: paramsDocs?.typeDescription,
|
|
321
|
+
paramsFields: paramsDocs?.fields,
|
|
322
|
+
resultTypeDescription: resultDocs?.typeDescription,
|
|
323
|
+
resultFields: resultDocs?.fields
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
function readTupleParamsResult(node) {
|
|
327
|
+
if (!Node2.isTupleTypeNode(node)) {
|
|
328
|
+
return void 0;
|
|
329
|
+
}
|
|
330
|
+
const elements = node.getElements();
|
|
331
|
+
if (elements.length === 0) {
|
|
332
|
+
return void 0;
|
|
333
|
+
}
|
|
334
|
+
const params = elements[0] ? typeNodeName(elements[0]) : void 0;
|
|
335
|
+
const result = elements[1] ? typeNodeName(elements[1]) : void 0;
|
|
336
|
+
return { params, result };
|
|
337
|
+
}
|
|
338
|
+
function readBareParams(node) {
|
|
339
|
+
const params = typeNodeName(node);
|
|
340
|
+
if (!params) {
|
|
341
|
+
return void 0;
|
|
342
|
+
}
|
|
343
|
+
return { params, result: void 0 };
|
|
344
|
+
}
|
|
345
|
+
function typeNodeName(node) {
|
|
346
|
+
if (Node2.isTypeReference(node)) {
|
|
347
|
+
return node.getTypeName().getText();
|
|
348
|
+
}
|
|
349
|
+
const text = node.getText().trim();
|
|
350
|
+
return text.length > 0 ? text : void 0;
|
|
351
|
+
}
|
|
352
|
+
function readTypeDocs(sourceFile, typeName) {
|
|
353
|
+
const interfaceDecl = sourceFile.getInterface(typeName);
|
|
354
|
+
if (interfaceDecl) {
|
|
355
|
+
const typeDescription = readJsDocSummary(interfaceDecl);
|
|
356
|
+
const fields = [];
|
|
357
|
+
for (const property of interfaceDecl.getProperties()) {
|
|
358
|
+
const fieldName = property.getName();
|
|
359
|
+
const description = readJsDocSummary(property);
|
|
360
|
+
const typeNode = property.getTypeNode();
|
|
361
|
+
const typeText = typeNode?.getText().trim() ?? "";
|
|
362
|
+
const field = description ? { name: fieldName, typeText, description } : { name: fieldName, typeText };
|
|
363
|
+
fields.push(field);
|
|
364
|
+
}
|
|
365
|
+
if (!typeDescription && fields.length === 0) {
|
|
366
|
+
return void 0;
|
|
367
|
+
}
|
|
368
|
+
return { typeDescription, fields: fields.length > 0 ? fields : void 0 };
|
|
369
|
+
}
|
|
370
|
+
const typeAlias = sourceFile.getTypeAlias(typeName);
|
|
371
|
+
if (typeAlias) {
|
|
372
|
+
const typeDescription = readJsDocSummary(typeAlias);
|
|
373
|
+
return typeDescription ? { typeDescription } : void 0;
|
|
374
|
+
}
|
|
375
|
+
return void 0;
|
|
376
|
+
}
|
|
377
|
+
function readJsDocSummary(node) {
|
|
378
|
+
const docs = node.getJsDocs();
|
|
379
|
+
if (docs.length === 0) {
|
|
380
|
+
return void 0;
|
|
381
|
+
}
|
|
382
|
+
const last = docs[docs.length - 1];
|
|
383
|
+
const description = last.getDescription().trim();
|
|
384
|
+
return description.length > 0 ? description : void 0;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/find-api-files.ts
|
|
388
|
+
function findApiFiles(packageRoot) {
|
|
389
|
+
const libRoot = join2(packageRoot, "src", "lib");
|
|
390
|
+
if (!safeIsDirectory(libRoot)) return [];
|
|
391
|
+
const out = [];
|
|
392
|
+
const seenClassNames = /* @__PURE__ */ new Set();
|
|
393
|
+
for (const file of walkApiFiles(libRoot)) {
|
|
394
|
+
const text = readFileSync3(file, "utf8");
|
|
395
|
+
const extraction = extractCrudEntries({ name: file, text });
|
|
396
|
+
if (!extraction.functionsClassName) continue;
|
|
397
|
+
if (seenClassNames.has(extraction.functionsClassName)) continue;
|
|
398
|
+
seenClassNames.add(extraction.functionsClassName);
|
|
399
|
+
out.push({ filePath: file, className: extraction.functionsClassName, extraction });
|
|
400
|
+
}
|
|
401
|
+
return out;
|
|
402
|
+
}
|
|
403
|
+
function* walkApiFiles(dir) {
|
|
404
|
+
for (const entry of readdirSync(dir).sort()) {
|
|
405
|
+
if (entry === "node_modules" || entry === "dist") continue;
|
|
406
|
+
const p = join2(dir, entry);
|
|
407
|
+
const stat = statSync(p);
|
|
408
|
+
if (stat.isDirectory()) {
|
|
409
|
+
yield* walkApiFiles(p);
|
|
410
|
+
} else if (isApiFile(entry)) {
|
|
411
|
+
yield p;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function isApiFile(name) {
|
|
416
|
+
if (name.endsWith(".spec.ts") || name.endsWith(".test.ts")) return false;
|
|
417
|
+
return name.endsWith(".api.ts") || name.endsWith(".api.d.ts");
|
|
418
|
+
}
|
|
419
|
+
function safeIsDirectory(p) {
|
|
420
|
+
try {
|
|
421
|
+
return statSync(p).isDirectory();
|
|
422
|
+
} catch {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/bind-validators.ts
|
|
428
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2 } from "node:fs";
|
|
429
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join3, resolve as resolve2 } from "node:path";
|
|
430
|
+
function deriveValidatorName(paramsTypeName) {
|
|
431
|
+
if (!paramsTypeName) return "";
|
|
432
|
+
return paramsTypeName.charAt(0).toLowerCase() + paramsTypeName.slice(1) + "Type";
|
|
433
|
+
}
|
|
434
|
+
function isExportedFromPackage(input) {
|
|
435
|
+
const { packageRoot, identifier } = input;
|
|
436
|
+
const indexPath = locateBarrelEntry(packageRoot);
|
|
437
|
+
if (!indexPath) return false;
|
|
438
|
+
return findIdentifierInBarrelChain(indexPath, identifier, /* @__PURE__ */ new Set());
|
|
439
|
+
}
|
|
440
|
+
function locateBarrelEntry(packageRoot) {
|
|
441
|
+
const candidates = [join3(packageRoot, "src", "index.ts"), join3(packageRoot, "src", "index.d.ts"), join3(packageRoot, "index.d.ts"), join3(packageRoot, "index.ts")];
|
|
442
|
+
return candidates.find((candidate) => existsSync2(candidate));
|
|
443
|
+
}
|
|
444
|
+
var EXPORT_DECL_PATTERNS = [/export\s+(?:declare\s+)?const\s+IDENT\b/, /export\s+(?:declare\s+)?function\s+IDENT\b/, /export\s*\{[^}]*\bIDENT\b[^}]*\}/];
|
|
445
|
+
function findIdentifierInBarrelChain(filePath, identifier, visited) {
|
|
446
|
+
if (visited.has(filePath)) return false;
|
|
447
|
+
visited.add(filePath);
|
|
448
|
+
let text;
|
|
449
|
+
try {
|
|
450
|
+
text = readFileSync4(filePath, "utf8");
|
|
451
|
+
} catch {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
for (const pattern of EXPORT_DECL_PATTERNS) {
|
|
455
|
+
const re = new RegExp(pattern.source.replace("IDENT", escapeRegExp(identifier)));
|
|
456
|
+
if (re.test(text)) return true;
|
|
457
|
+
}
|
|
458
|
+
const dir = dirname2(filePath);
|
|
459
|
+
for (const reExportTarget of collectReExportTargets(text)) {
|
|
460
|
+
const resolved = resolveReExport(dir, reExportTarget);
|
|
461
|
+
if (!resolved) continue;
|
|
462
|
+
if (findIdentifierInBarrelChain(resolved, identifier, visited)) return true;
|
|
463
|
+
}
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
function collectReExportTargets(text) {
|
|
467
|
+
const out = [];
|
|
468
|
+
const re = /export\s*(?:\*|\{[^}]*\})\s*from\s*['"]([^'"]+)['"]/g;
|
|
469
|
+
let match;
|
|
470
|
+
while ((match = re.exec(text)) !== null) {
|
|
471
|
+
out.push(match[1]);
|
|
472
|
+
}
|
|
473
|
+
return out;
|
|
474
|
+
}
|
|
475
|
+
function resolveReExport(fromDir, target) {
|
|
476
|
+
if (!target.startsWith(".")) return void 0;
|
|
477
|
+
const candidate = isAbsolute2(target) ? target : resolve2(fromDir, target);
|
|
478
|
+
let result;
|
|
479
|
+
for (const ext of [".ts", ".mts", ".d.ts", "/index.ts", "/index.mts", "/index.d.ts"]) {
|
|
480
|
+
const probe = hasTsModuleExtension(candidate) ? candidate : candidate + ext;
|
|
481
|
+
const resolved = resolveExistingTsPath(probe);
|
|
482
|
+
if (resolved) {
|
|
483
|
+
result = resolved;
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return result;
|
|
488
|
+
}
|
|
489
|
+
function hasTsModuleExtension(value) {
|
|
490
|
+
return value.endsWith(".ts") || value.endsWith(".mts");
|
|
491
|
+
}
|
|
492
|
+
function resolveExistingTsPath(probe) {
|
|
493
|
+
if (!existsSync2(probe)) return void 0;
|
|
494
|
+
const stat = statSync2(probe);
|
|
495
|
+
if (stat.isFile()) return probe;
|
|
496
|
+
if (!stat.isDirectory()) return void 0;
|
|
497
|
+
const sourceIndex = join3(probe, "index.ts");
|
|
498
|
+
if (existsSync2(sourceIndex)) return sourceIndex;
|
|
499
|
+
const declarationIndex = join3(probe, "index.d.ts");
|
|
500
|
+
return existsSync2(declarationIndex) ? declarationIndex : void 0;
|
|
501
|
+
}
|
|
502
|
+
function escapeRegExp(value) {
|
|
503
|
+
return value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/emit.ts
|
|
507
|
+
import { format, resolveConfig } from "prettier";
|
|
508
|
+
async function renderManifest(input) {
|
|
509
|
+
const { outputFile, entries, projectName, namespace } = input;
|
|
510
|
+
const importsByPackage = /* @__PURE__ */ new Map();
|
|
511
|
+
for (const entry of entries) {
|
|
512
|
+
if (!entry.packageName || !entry.validatorName) continue;
|
|
513
|
+
const set = importsByPackage.get(entry.packageName) ?? /* @__PURE__ */ new Set();
|
|
514
|
+
set.add(entry.validatorName);
|
|
515
|
+
importsByPackage.set(entry.packageName, set);
|
|
516
|
+
}
|
|
517
|
+
const importLines = [...importsByPackage.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([pkg, names]) => {
|
|
518
|
+
const sortedNames = [...names].sort((a, b) => a.localeCompare(b)).join(", ");
|
|
519
|
+
return `import { ${sortedNames} } from '${pkg}';`;
|
|
520
|
+
});
|
|
521
|
+
const entryLines = entries.map((e) => renderEntry(e));
|
|
522
|
+
const source = `/* eslint-disable @nx/enforce-module-boundaries */
|
|
523
|
+
// AUTO-GENERATED \u2014 DO NOT EDIT.
|
|
524
|
+
// Run \`pnpm nx run ${projectName}:generate-api-manifest\` to refresh.
|
|
525
|
+
|
|
526
|
+
${importLines.join("\n")}
|
|
527
|
+
import { type CliApiManifest } from '@dereekb/dbx-cli';
|
|
528
|
+
|
|
529
|
+
export const ${namespace}: CliApiManifest = [
|
|
530
|
+
${entryLines.join(",\n")}
|
|
531
|
+
];
|
|
532
|
+
`;
|
|
533
|
+
return formatWithPrettier(source, outputFile);
|
|
534
|
+
}
|
|
535
|
+
function renderEntry({ entry, validatorName }) {
|
|
536
|
+
const fields = [
|
|
537
|
+
`model: ${JSON.stringify(entry.model)}`,
|
|
538
|
+
`verb: ${JSON.stringify(entry.verb)}`,
|
|
539
|
+
entry.specifier ? `specifier: ${JSON.stringify(entry.specifier)}` : void 0,
|
|
540
|
+
entry.paramsTypeName ? `paramsTypeName: ${JSON.stringify(entry.paramsTypeName)}` : void 0,
|
|
541
|
+
validatorName ? `paramsValidator: ${validatorName}` : void 0,
|
|
542
|
+
entry.resultTypeName ? `resultTypeName: ${JSON.stringify(entry.resultTypeName)}` : void 0,
|
|
543
|
+
`groupName: ${JSON.stringify(entry.groupName)}`,
|
|
544
|
+
`sourceFile: ${JSON.stringify(entry.sourceFile)}`,
|
|
545
|
+
entry.description ? `description: ${JSON.stringify(entry.description)}` : void 0,
|
|
546
|
+
entry.paramsTypeDescription ? `paramsTypeDescription: ${JSON.stringify(entry.paramsTypeDescription)}` : void 0,
|
|
547
|
+
entry.paramsFields && entry.paramsFields.length > 0 ? `paramsFields: ${renderDocFields(entry.paramsFields)}` : void 0,
|
|
548
|
+
entry.resultTypeDescription ? `resultTypeDescription: ${JSON.stringify(entry.resultTypeDescription)}` : void 0,
|
|
549
|
+
entry.resultFields && entry.resultFields.length > 0 ? `resultFields: ${renderDocFields(entry.resultFields)}` : void 0
|
|
550
|
+
];
|
|
551
|
+
return ` { ${fields.filter((v) => Boolean(v)).join(", ")} }`;
|
|
552
|
+
}
|
|
553
|
+
function renderDocFields(fields) {
|
|
554
|
+
const items = fields.map((field) => {
|
|
555
|
+
const parts = [`name: ${JSON.stringify(field.name)}`, `typeText: ${JSON.stringify(field.typeText)}`];
|
|
556
|
+
if (field.description) parts.push(`description: ${JSON.stringify(field.description)}`);
|
|
557
|
+
return `{ ${parts.join(", ")} }`;
|
|
558
|
+
});
|
|
559
|
+
return `[${items.join(", ")}]`;
|
|
560
|
+
}
|
|
561
|
+
async function formatWithPrettier(source, outputFile) {
|
|
562
|
+
const config = await resolveConfig(outputFile);
|
|
563
|
+
return format(source, { ...config, filepath: outputFile });
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/main.ts
|
|
567
|
+
var WORKSPACE_ROOT = process.cwd();
|
|
568
|
+
async function main() {
|
|
569
|
+
const flags = parseFlags(process.argv.slice(2));
|
|
570
|
+
if (!flags.functionsConfig || !flags.output) {
|
|
571
|
+
printUsageAndExit();
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
const functionsConfigPath = resolveWorkspacePath(flags.functionsConfig);
|
|
575
|
+
const outputFile = resolveWorkspacePath(flags.output);
|
|
576
|
+
const outputDir = dirname3(outputFile);
|
|
577
|
+
const projectName = flags.project ?? "<cli>";
|
|
578
|
+
const namespace = deriveNamespace(flags.project);
|
|
579
|
+
if (!existsSync3(functionsConfigPath)) {
|
|
580
|
+
throw new Error(`functions-config file not found: ${functionsConfigPath}`);
|
|
581
|
+
}
|
|
582
|
+
const groups = parseFunctionsConfig(functionsConfigPath);
|
|
583
|
+
if (groups.length === 0) {
|
|
584
|
+
throw new Error(`No function groups discovered in ${relPath(WORKSPACE_ROOT, functionsConfigPath)}.`);
|
|
585
|
+
}
|
|
586
|
+
const packageCache = /* @__PURE__ */ new Map();
|
|
587
|
+
const apiFilesCache = /* @__PURE__ */ new Map();
|
|
588
|
+
const collected = [];
|
|
589
|
+
let missingValidators = 0;
|
|
590
|
+
let skippedGroups = 0;
|
|
591
|
+
for (const group of groups) {
|
|
592
|
+
const pkg = resolveModuleToPackage({ workspaceRoot: WORKSPACE_ROOT, importingFile: functionsConfigPath, moduleSpecifier: group.importedFromModule });
|
|
593
|
+
if (!pkg) {
|
|
594
|
+
console.warn(`[skip] ${group.groupKey}: cannot resolve module '${group.importedFromModule}' for class ${group.className}`);
|
|
595
|
+
skippedGroups++;
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
if (!packageCache.has(pkg.packageRoot)) packageCache.set(pkg.packageRoot, pkg);
|
|
599
|
+
if (!apiFilesCache.has(pkg.packageRoot)) apiFilesCache.set(pkg.packageRoot, findApiFiles(pkg.packageRoot));
|
|
600
|
+
const apiFiles = apiFilesCache.get(pkg.packageRoot) ?? [];
|
|
601
|
+
const match = apiFiles.find((f) => f.className === group.className);
|
|
602
|
+
if (!match) {
|
|
603
|
+
console.warn(`[skip] ${group.groupKey}: no .api.{ts,d.ts} under ${pkg.packageName} declares 'export abstract class ${group.className}'`);
|
|
604
|
+
skippedGroups++;
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
if (match.extraction.entries.length === 0) {
|
|
608
|
+
console.warn(`[skip] ${group.groupKey}: ${group.className} is not a CRUD model group (no *ModelCrudFunctionsConfig in ${relPath(WORKSPACE_ROOT, match.filePath)})`);
|
|
609
|
+
skippedGroups++;
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
const groupName = match.extraction.groupName ?? group.className.replace(/Functions$/, "");
|
|
613
|
+
const sourceFileRel = relPath(WORKSPACE_ROOT, match.filePath);
|
|
614
|
+
for (const entry of match.extraction.entries) {
|
|
615
|
+
if (flags.only && !flags.only.has(entry.model)) continue;
|
|
616
|
+
if (entry.verb === "standalone") continue;
|
|
617
|
+
const enriched = { entry: { ...entry, groupName, sourceFile: sourceFileRel } };
|
|
618
|
+
if (entry.paramsTypeName) {
|
|
619
|
+
const validatorName = deriveValidatorName(entry.paramsTypeName);
|
|
620
|
+
const found = isExportedFromPackage({ packageRoot: pkg.packageRoot, identifier: validatorName });
|
|
621
|
+
if (found) {
|
|
622
|
+
enriched.packageName = pkg.packageName;
|
|
623
|
+
enriched.validatorName = validatorName;
|
|
624
|
+
} else {
|
|
625
|
+
missingValidators++;
|
|
626
|
+
const specPart = entry.specifier ? "/" + entry.specifier : "";
|
|
627
|
+
console.warn(`[no-validator] ${pkg.packageName} \xB7 ${entry.model}/${entry.verb}${specPart} \u2192 ${validatorName} not exported`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
collected.push(enriched);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
collected.sort(compareEntries);
|
|
634
|
+
ensureOutputDir(outputDir);
|
|
635
|
+
const formatted = await renderManifest({ outputFile, entries: collected, projectName, namespace });
|
|
636
|
+
if (existsSync3(outputFile) && readFileSync5(outputFile, "utf8") === formatted) {
|
|
637
|
+
console.log(`[unchanged] ${relative2(WORKSPACE_ROOT, outputFile)}`);
|
|
638
|
+
} else {
|
|
639
|
+
writeFileSync(outputFile, formatted);
|
|
640
|
+
console.log(`[wrote] ${relative2(WORKSPACE_ROOT, outputFile)}`);
|
|
641
|
+
}
|
|
642
|
+
const groupCount = packageCache.size === 0 ? 0 : new Set(collected.map((c) => c.entry.groupName)).size;
|
|
643
|
+
console.log(`Summary: ${groupCount} groups \xB7 ${collected.length} entries \xB7 ${collected.length - missingValidators} validators bound \xB7 ${missingValidators} missing \xB7 ${skippedGroups} skipped`);
|
|
644
|
+
if (flags.strict && missingValidators > 0) {
|
|
645
|
+
console.error(`[strict] ${missingValidators} validator(s) missing \u2014 failing build.`);
|
|
646
|
+
process.exit(1);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
function compareEntries(a, b) {
|
|
650
|
+
if (a.entry.model !== b.entry.model) return a.entry.model.localeCompare(b.entry.model);
|
|
651
|
+
if (a.entry.verb !== b.entry.verb) return a.entry.verb.localeCompare(b.entry.verb);
|
|
652
|
+
return (a.entry.specifier ?? "").localeCompare(b.entry.specifier ?? "");
|
|
653
|
+
}
|
|
654
|
+
function ensureOutputDir(outputDir) {
|
|
655
|
+
if (!existsSync3(outputDir)) mkdirSync(outputDir, { recursive: true });
|
|
656
|
+
}
|
|
657
|
+
function resolveWorkspacePath(value) {
|
|
658
|
+
return isAbsolute3(value) ? value : resolve3(WORKSPACE_ROOT, value);
|
|
659
|
+
}
|
|
660
|
+
function deriveNamespace(projectName) {
|
|
661
|
+
const base = (projectName ?? "cli").replaceAll(/[^a-zA-Z0-9]+/g, "_");
|
|
662
|
+
return `${base.toUpperCase()}_API_MANIFEST`;
|
|
663
|
+
}
|
|
664
|
+
function parseFlags(argv) {
|
|
665
|
+
let only;
|
|
666
|
+
let strict = false;
|
|
667
|
+
let functionsConfig;
|
|
668
|
+
let output;
|
|
669
|
+
let project;
|
|
670
|
+
for (const arg of argv) {
|
|
671
|
+
if (arg === "--strict") {
|
|
672
|
+
strict = true;
|
|
673
|
+
} else if (arg.startsWith("--only=")) {
|
|
674
|
+
const list = arg.slice("--only=".length).split(",").map((s) => s.trim()).filter(Boolean);
|
|
675
|
+
if (list.length > 0) only = new Set(list);
|
|
676
|
+
} else if (arg.startsWith("--functions-config=")) {
|
|
677
|
+
functionsConfig = arg.slice("--functions-config=".length);
|
|
678
|
+
} else if (arg.startsWith("--output=")) {
|
|
679
|
+
output = arg.slice("--output=".length);
|
|
680
|
+
} else if (arg.startsWith("--project=")) {
|
|
681
|
+
project = arg.slice("--project=".length);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return { only, strict, functionsConfig, output, project };
|
|
685
|
+
}
|
|
686
|
+
function printUsageAndExit() {
|
|
687
|
+
console.error(String.raw`generate-api-manifest
|
|
688
|
+
|
|
689
|
+
Usage:
|
|
690
|
+
node dist/packages/dbx-cli/firebase-api-manifest/main.js \
|
|
691
|
+
--project=<name> \
|
|
692
|
+
--functions-config=<path-to-functions.ts> \
|
|
693
|
+
--output=<path-to-manifest.generated.ts> \
|
|
694
|
+
[--only=model[,model]] [--strict]
|
|
695
|
+
|
|
696
|
+
Required flags:
|
|
697
|
+
--functions-config=<path> Path to the app's functions.ts (workspace-relative ok).
|
|
698
|
+
--output=<path> Path to the manifest TS file to write (workspace-relative ok).
|
|
699
|
+
|
|
700
|
+
Optional:
|
|
701
|
+
--project=<name> Project name to show in the regenerate banner.
|
|
702
|
+
--only=<csv> Filter to listed model names.
|
|
703
|
+
--strict Fail when any validator binding is missing.`);
|
|
704
|
+
process.exit(1);
|
|
705
|
+
}
|
|
706
|
+
try {
|
|
707
|
+
await main();
|
|
708
|
+
} catch (e) {
|
|
709
|
+
console.error(e);
|
|
710
|
+
process.exit(1);
|
|
711
|
+
}
|