@dereekb/dbx-cli 13.11.4 → 13.11.6
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 +706 -16
- package/firebase-api-manifest/package.json +1 -1
- package/index.cjs.js +577 -48
- package/index.esm.js +570 -49
- package/manifest-extract/index.cjs.js +764 -0
- package/manifest-extract/index.esm.js +764 -1
- package/manifest-extract/package.json +1 -1
- package/manifest-extract/src/index.d.ts +1 -0
- package/manifest-extract/src/lib/extract-models.d.ts +33 -0
- package/manifest-extract/src/lib/types.d.ts +148 -0
- package/package.json +5 -5
- package/src/lib/api/expand-keys.d.ts +31 -0
- package/src/lib/api/index.d.ts +1 -0
- package/src/lib/manifest/build-manifest-commands.d.ts +12 -1
- package/src/lib/manifest/build-model-info-command.d.ts +37 -0
- package/src/lib/manifest/index.d.ts +2 -0
- package/src/lib/manifest/model-info-utils.d.ts +40 -0
- package/src/lib/manifest/types.d.ts +130 -0
- package/src/lib/runner/run.d.ts +29 -0
|
@@ -3,7 +3,7 @@ import { createRequire as __createRequire } from 'node:module';
|
|
|
3
3
|
const require = __createRequire(import.meta.url);
|
|
4
4
|
|
|
5
5
|
// packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/main.ts
|
|
6
|
-
import { existsSync as existsSync3, mkdirSync, readFileSync as
|
|
6
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync6, writeFileSync } from "node:fs";
|
|
7
7
|
import { dirname as dirname3, isAbsolute as isAbsolute3, relative as relative2, resolve as resolve3 } from "node:path";
|
|
8
8
|
|
|
9
9
|
// packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/parse-functions.ts
|
|
@@ -384,6 +384,368 @@ function readJsDocSummary(node) {
|
|
|
384
384
|
return description.length > 0 ? description : void 0;
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
+
// packages/dbx-cli/manifest-extract/src/lib/extract-models.ts
|
|
388
|
+
import { Node as Node3, Project as Project3 } from "ts-morph";
|
|
389
|
+
var PASSTHROUGH_TYPE_WRAPPERS = /* @__PURE__ */ new Set(["Partial", "Required", "Readonly", "NonNullable", "MaybeMap", "Pick", "Omit"]);
|
|
390
|
+
var IDENTITY_FN = "firestoreModelIdentity";
|
|
391
|
+
var CONVERTER_FN_NAMES = ["snapshotConverterFunctions", "firestoreSubObject", "firestoreObjectArray"];
|
|
392
|
+
var SUB_OBJECT_FN = "firestoreSubObject";
|
|
393
|
+
var OBJECT_ARRAY_FN = "firestoreObjectArray";
|
|
394
|
+
var SNAPSHOT_FN = "snapshotConverterFunctions";
|
|
395
|
+
var FIELDS_LITERAL_KEY = "fields";
|
|
396
|
+
var OBJECT_FIELD_KEY = "objectField";
|
|
397
|
+
function extractModelsFromSource(input) {
|
|
398
|
+
const project = new Project3({ useInMemoryFileSystem: true, skipAddingFilesFromTsConfig: true });
|
|
399
|
+
const sourceFile = project.createSourceFile(input.name, input.text, { overwrite: true });
|
|
400
|
+
const identities = readIdentities(sourceFile);
|
|
401
|
+
const interfaces = readInterfaces(sourceFile);
|
|
402
|
+
const converters = readConverters(sourceFile);
|
|
403
|
+
const enums = readEnums(sourceFile);
|
|
404
|
+
const modelGroups = readModelGroups(sourceFile);
|
|
405
|
+
return { identities, interfaces, converters, enums, modelGroups };
|
|
406
|
+
}
|
|
407
|
+
function readIdentities(sourceFile) {
|
|
408
|
+
const out = [];
|
|
409
|
+
for (const statement of sourceFile.getVariableStatements()) {
|
|
410
|
+
if (!statement.isExported()) continue;
|
|
411
|
+
for (const decl of statement.getDeclarations()) {
|
|
412
|
+
const initializer = decl.getInitializer();
|
|
413
|
+
if (!initializer || !Node3.isCallExpression(initializer)) continue;
|
|
414
|
+
if (initializer.getExpression().getText() !== IDENTITY_FN) continue;
|
|
415
|
+
const parsed = parseIdentityArgs(initializer);
|
|
416
|
+
if (parsed) {
|
|
417
|
+
out.push({ identityConst: decl.getName(), ...parsed });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return out;
|
|
422
|
+
}
|
|
423
|
+
function parseIdentityArgs(call) {
|
|
424
|
+
const args = call.getArguments();
|
|
425
|
+
let result;
|
|
426
|
+
if (args.length === 1) {
|
|
427
|
+
const modelType = stringLiteralValue(args[0]);
|
|
428
|
+
if (modelType !== void 0) {
|
|
429
|
+
result = { modelType, collectionPrefix: void 0, parentIdentityConst: void 0 };
|
|
430
|
+
}
|
|
431
|
+
} else if (args.length === 2) {
|
|
432
|
+
const first = stringLiteralValue(args[0]);
|
|
433
|
+
if (first === void 0) {
|
|
434
|
+
const modelType = stringLiteralValue(args[1]);
|
|
435
|
+
if (modelType !== void 0) {
|
|
436
|
+
result = { modelType, collectionPrefix: void 0, parentIdentityConst: identifierName(args[0]) };
|
|
437
|
+
}
|
|
438
|
+
} else {
|
|
439
|
+
result = { modelType: first, collectionPrefix: stringLiteralValue(args[1]), parentIdentityConst: void 0 };
|
|
440
|
+
}
|
|
441
|
+
} else if (args.length >= 3) {
|
|
442
|
+
const modelType = stringLiteralValue(args[1]);
|
|
443
|
+
if (modelType !== void 0) {
|
|
444
|
+
result = { modelType, collectionPrefix: stringLiteralValue(args[2]), parentIdentityConst: identifierName(args[0]) };
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return result;
|
|
448
|
+
}
|
|
449
|
+
function readInterfaces(sourceFile) {
|
|
450
|
+
const out = [];
|
|
451
|
+
for (const decl of sourceFile.getInterfaces()) {
|
|
452
|
+
if (!decl.isExported()) continue;
|
|
453
|
+
out.push(buildInterface(decl));
|
|
454
|
+
}
|
|
455
|
+
return out;
|
|
456
|
+
}
|
|
457
|
+
function buildInterface(decl) {
|
|
458
|
+
const jsDocs = decl.getJsDocs();
|
|
459
|
+
const hasDbxModelTag = jsDocsHaveTag(jsDocs, "dbxModel");
|
|
460
|
+
const extendsNames = decl.getExtends().map(resolveExtendsName);
|
|
461
|
+
const props = [];
|
|
462
|
+
for (const prop of decl.getProperties()) {
|
|
463
|
+
const propJsDocs = prop.getJsDocs();
|
|
464
|
+
const longName = readJsDocTagText(propJsDocs, "dbxModelVariable");
|
|
465
|
+
const syncFlag = readJsDocTagText(propJsDocs, "dbxModelVariableSyncFlag");
|
|
466
|
+
const tsType = (prop.getTypeNode()?.getText() ?? "").replaceAll(/\s+/g, " ").trim();
|
|
467
|
+
const optional = prop.hasQuestionToken() || tsType.startsWith("Maybe<");
|
|
468
|
+
props.push({
|
|
469
|
+
name: prop.getName(),
|
|
470
|
+
tsType,
|
|
471
|
+
optional,
|
|
472
|
+
description: readJsDocDescription(propJsDocs),
|
|
473
|
+
longName,
|
|
474
|
+
syncFlag
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
name: decl.getName(),
|
|
479
|
+
description: readJsDocDescription(jsDocs),
|
|
480
|
+
hasDbxModelTag,
|
|
481
|
+
extendsNames,
|
|
482
|
+
props
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
function resolveExtendsName(expr) {
|
|
486
|
+
const head = expr.getExpression().getText();
|
|
487
|
+
let result = head;
|
|
488
|
+
if (PASSTHROUGH_TYPE_WRAPPERS.has(head)) {
|
|
489
|
+
const typeArgs = expr.getTypeArguments();
|
|
490
|
+
if (typeArgs.length > 0) {
|
|
491
|
+
const peeled = peelTypeNode(typeArgs[0]);
|
|
492
|
+
if (peeled !== void 0) {
|
|
493
|
+
result = peeled;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return result;
|
|
498
|
+
}
|
|
499
|
+
function peelTypeNode(node) {
|
|
500
|
+
let current = node;
|
|
501
|
+
while (Node3.isParenthesizedTypeNode(current)) {
|
|
502
|
+
current = current.getTypeNode();
|
|
503
|
+
}
|
|
504
|
+
let result;
|
|
505
|
+
if (Node3.isTypeReference(current)) {
|
|
506
|
+
const name = current.getTypeName().getText();
|
|
507
|
+
if (PASSTHROUGH_TYPE_WRAPPERS.has(name)) {
|
|
508
|
+
const inner = current.getTypeArguments();
|
|
509
|
+
if (inner.length > 0) {
|
|
510
|
+
result = peelTypeNode(inner[0]);
|
|
511
|
+
}
|
|
512
|
+
} else {
|
|
513
|
+
result = name;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return result;
|
|
517
|
+
}
|
|
518
|
+
function readConverters(sourceFile) {
|
|
519
|
+
const out = [];
|
|
520
|
+
for (const statement of sourceFile.getVariableStatements()) {
|
|
521
|
+
if (!statement.isExported()) continue;
|
|
522
|
+
for (const decl of statement.getDeclarations()) {
|
|
523
|
+
const initializer = decl.getInitializer();
|
|
524
|
+
if (!initializer || !Node3.isCallExpression(initializer)) continue;
|
|
525
|
+
const factory = initializer.getExpression().getText();
|
|
526
|
+
if (!isConverterFactoryName(factory)) continue;
|
|
527
|
+
const interfaceName = readGenericInterfaceName(initializer);
|
|
528
|
+
const fields = readConverterFields(initializer);
|
|
529
|
+
if (!fields) continue;
|
|
530
|
+
out.push({
|
|
531
|
+
converterConst: decl.getName(),
|
|
532
|
+
factory,
|
|
533
|
+
interfaceName,
|
|
534
|
+
fields,
|
|
535
|
+
line: decl.getStartLineNumber()
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return out;
|
|
540
|
+
}
|
|
541
|
+
function isConverterFactoryName(name) {
|
|
542
|
+
return CONVERTER_FN_NAMES.includes(name);
|
|
543
|
+
}
|
|
544
|
+
function readGenericInterfaceName(call) {
|
|
545
|
+
const typeArgs = call.getTypeArguments();
|
|
546
|
+
let result;
|
|
547
|
+
if (typeArgs.length > 0) {
|
|
548
|
+
result = typeArgs[0].getText().replaceAll(/<[^>]*>/g, "").trim();
|
|
549
|
+
}
|
|
550
|
+
return result;
|
|
551
|
+
}
|
|
552
|
+
function readConverterFields(call) {
|
|
553
|
+
const fnName = call.getExpression().getText();
|
|
554
|
+
const args = call.getArguments();
|
|
555
|
+
if (args.length === 0) return void 0;
|
|
556
|
+
const config = args[0];
|
|
557
|
+
if (!Node3.isObjectLiteralExpression(config)) return void 0;
|
|
558
|
+
let fieldsLiteral;
|
|
559
|
+
if (fnName === SNAPSHOT_FN) {
|
|
560
|
+
fieldsLiteral = readObjectProperty(config, FIELDS_LITERAL_KEY);
|
|
561
|
+
} else {
|
|
562
|
+
const objectField = readPropertyValue(config, OBJECT_FIELD_KEY);
|
|
563
|
+
if (objectField && Node3.isObjectLiteralExpression(objectField)) {
|
|
564
|
+
fieldsLiteral = readObjectProperty(objectField, FIELDS_LITERAL_KEY);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (!fieldsLiteral) return void 0;
|
|
568
|
+
return readFieldEntries(fieldsLiteral);
|
|
569
|
+
}
|
|
570
|
+
function readFieldEntries(fields) {
|
|
571
|
+
const out = [];
|
|
572
|
+
for (const property of fields.getProperties()) {
|
|
573
|
+
if (Node3.isPropertyAssignment(property)) {
|
|
574
|
+
const initializer = property.getInitializer();
|
|
575
|
+
const converterText = initializer ? initializer.getText().replaceAll(/\s+/g, " ").trim() : "";
|
|
576
|
+
const nested = initializer ? readNestedFromExpression(initializer) : void 0;
|
|
577
|
+
out.push({
|
|
578
|
+
key: property.getName(),
|
|
579
|
+
converter: converterText,
|
|
580
|
+
nestedConverterRef: nested?.ref,
|
|
581
|
+
nestedConverterInline: nested?.inline,
|
|
582
|
+
nestedIsArray: nested?.isArray
|
|
583
|
+
});
|
|
584
|
+
} else if (Node3.isShorthandPropertyAssignment(property)) {
|
|
585
|
+
const name = property.getName();
|
|
586
|
+
out.push({ key: name, converter: name });
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return out;
|
|
590
|
+
}
|
|
591
|
+
function readNestedFromExpression(expr) {
|
|
592
|
+
if (!Node3.isCallExpression(expr)) return void 0;
|
|
593
|
+
const fnName = expr.getExpression().getText();
|
|
594
|
+
if (fnName !== SUB_OBJECT_FN && fnName !== OBJECT_ARRAY_FN) return void 0;
|
|
595
|
+
const args = expr.getArguments();
|
|
596
|
+
if (args.length === 0) return void 0;
|
|
597
|
+
const config = args[0];
|
|
598
|
+
if (!Node3.isObjectLiteralExpression(config)) return void 0;
|
|
599
|
+
const objectField = readPropertyValue(config, OBJECT_FIELD_KEY);
|
|
600
|
+
if (!objectField) return void 0;
|
|
601
|
+
const isArray = fnName === OBJECT_ARRAY_FN;
|
|
602
|
+
let result;
|
|
603
|
+
if (Node3.isIdentifier(objectField)) {
|
|
604
|
+
result = { ref: objectField.getText(), isArray };
|
|
605
|
+
} else if (Node3.isObjectLiteralExpression(objectField)) {
|
|
606
|
+
const fieldsLiteral = readObjectProperty(objectField, FIELDS_LITERAL_KEY);
|
|
607
|
+
if (fieldsLiteral) {
|
|
608
|
+
const inlineFields = readFieldEntries(fieldsLiteral);
|
|
609
|
+
result = {
|
|
610
|
+
inline: {
|
|
611
|
+
converterConst: void 0,
|
|
612
|
+
factory: fnName,
|
|
613
|
+
interfaceName: readGenericInterfaceName(expr),
|
|
614
|
+
fields: inlineFields,
|
|
615
|
+
line: expr.getStartLineNumber()
|
|
616
|
+
},
|
|
617
|
+
isArray
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return result;
|
|
622
|
+
}
|
|
623
|
+
function readPropertyValue(literal, key) {
|
|
624
|
+
const property = literal.getProperty(key);
|
|
625
|
+
let result;
|
|
626
|
+
if (property && Node3.isPropertyAssignment(property)) {
|
|
627
|
+
result = property.getInitializer();
|
|
628
|
+
} else if (property && Node3.isShorthandPropertyAssignment(property)) {
|
|
629
|
+
result = property.getNameNode();
|
|
630
|
+
}
|
|
631
|
+
return result;
|
|
632
|
+
}
|
|
633
|
+
function readObjectProperty(literal, key) {
|
|
634
|
+
const value = readPropertyValue(literal, key);
|
|
635
|
+
return value && Node3.isObjectLiteralExpression(value) ? value : void 0;
|
|
636
|
+
}
|
|
637
|
+
function readEnums(sourceFile) {
|
|
638
|
+
const out = [];
|
|
639
|
+
for (const decl of sourceFile.getEnums()) {
|
|
640
|
+
if (!decl.isExported()) continue;
|
|
641
|
+
const values = [];
|
|
642
|
+
for (const member of decl.getMembers()) {
|
|
643
|
+
const value = member.getValue();
|
|
644
|
+
const description = readJsDocDescription(member.getJsDocs());
|
|
645
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
646
|
+
values.push({ name: member.getName(), value, description });
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
out.push({
|
|
650
|
+
name: decl.getName(),
|
|
651
|
+
values,
|
|
652
|
+
description: readJsDocDescription(decl.getJsDocs())
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
return out;
|
|
656
|
+
}
|
|
657
|
+
function readModelGroups(sourceFile) {
|
|
658
|
+
const out = [];
|
|
659
|
+
for (const iface of sourceFile.getInterfaces()) {
|
|
660
|
+
if (!iface.isExported()) continue;
|
|
661
|
+
const groupTag = readJsDocTagText(iface.getJsDocs(), "dbxModelGroup");
|
|
662
|
+
if (!groupTag) continue;
|
|
663
|
+
const containerName = iface.getName();
|
|
664
|
+
if (!containerName.endsWith("FirestoreCollections")) continue;
|
|
665
|
+
const modelNames = [];
|
|
666
|
+
for (const prop of iface.getProperties()) {
|
|
667
|
+
const tsType = prop.getTypeNode()?.getText() ?? "";
|
|
668
|
+
const match = /([A-Z]\w*)FirestoreCollection(?:Factory)?(?:\b|<)/.exec(tsType);
|
|
669
|
+
if (match) modelNames.push(match[1]);
|
|
670
|
+
}
|
|
671
|
+
out.push({
|
|
672
|
+
name: groupTag,
|
|
673
|
+
containerName,
|
|
674
|
+
description: readJsDocDescription(iface.getJsDocs()),
|
|
675
|
+
modelNames
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
return out;
|
|
679
|
+
}
|
|
680
|
+
function jsDocsHaveTag(jsDocs, tagName) {
|
|
681
|
+
let found = false;
|
|
682
|
+
for (const jsDoc of jsDocs) {
|
|
683
|
+
for (const tag of jsDoc.getTags()) {
|
|
684
|
+
if (tag.getTagName() === tagName) {
|
|
685
|
+
found = true;
|
|
686
|
+
break;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (found) break;
|
|
690
|
+
}
|
|
691
|
+
return found;
|
|
692
|
+
}
|
|
693
|
+
function readJsDocTagText(jsDocs, tagName) {
|
|
694
|
+
let result;
|
|
695
|
+
for (const jsDoc of jsDocs) {
|
|
696
|
+
for (const tag of jsDoc.getTags()) {
|
|
697
|
+
if (tag.getTagName() !== tagName) continue;
|
|
698
|
+
const text = tag.getCommentText()?.trim();
|
|
699
|
+
if (text !== void 0 && text.length > 0) {
|
|
700
|
+
result = text;
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (result !== void 0) break;
|
|
705
|
+
}
|
|
706
|
+
return result;
|
|
707
|
+
}
|
|
708
|
+
function readJsDocDescription(jsDocs) {
|
|
709
|
+
let result;
|
|
710
|
+
for (const jsDoc of jsDocs) {
|
|
711
|
+
const description = jsDoc.getDescription().trim();
|
|
712
|
+
if (description.length === 0) continue;
|
|
713
|
+
const paragraph = firstParagraph(description);
|
|
714
|
+
if (paragraph.length > 0) {
|
|
715
|
+
result = paragraph;
|
|
716
|
+
break;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return result;
|
|
720
|
+
}
|
|
721
|
+
function firstParagraph(text) {
|
|
722
|
+
const lines = text.split("\n").map((line) => line.trim());
|
|
723
|
+
const collected = [];
|
|
724
|
+
for (const line of lines) {
|
|
725
|
+
if (line.startsWith("@")) break;
|
|
726
|
+
if (line.length === 0) {
|
|
727
|
+
if (collected.length > 0) break;
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
collected.push(line);
|
|
731
|
+
}
|
|
732
|
+
return collected.join(" ").trim();
|
|
733
|
+
}
|
|
734
|
+
function stringLiteralValue(node) {
|
|
735
|
+
let result;
|
|
736
|
+
if (Node3.isStringLiteral(node) || Node3.isNoSubstitutionTemplateLiteral(node)) {
|
|
737
|
+
result = node.getLiteralText();
|
|
738
|
+
}
|
|
739
|
+
return result;
|
|
740
|
+
}
|
|
741
|
+
function identifierName(node) {
|
|
742
|
+
let result;
|
|
743
|
+
if (Node3.isIdentifier(node)) {
|
|
744
|
+
result = node.getText();
|
|
745
|
+
}
|
|
746
|
+
return result;
|
|
747
|
+
}
|
|
748
|
+
|
|
387
749
|
// packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/find-api-files.ts
|
|
388
750
|
function findApiFiles(packageRoot) {
|
|
389
751
|
const libRoot = join2(packageRoot, "src", "lib");
|
|
@@ -424,9 +786,263 @@ function safeIsDirectory(p) {
|
|
|
424
786
|
}
|
|
425
787
|
}
|
|
426
788
|
|
|
789
|
+
// packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/find-model-files.ts
|
|
790
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2 } from "node:fs";
|
|
791
|
+
import { join as join3 } from "node:path";
|
|
792
|
+
function findModelFiles(packageRoot) {
|
|
793
|
+
const libRoot = join3(packageRoot, "src", "lib");
|
|
794
|
+
if (!safeIsDirectory2(libRoot)) return [];
|
|
795
|
+
const out = [];
|
|
796
|
+
for (const filePath of walkSourceFiles(libRoot)) {
|
|
797
|
+
const text = readFileSync4(filePath, "utf8");
|
|
798
|
+
if (!textHasModelMarker(text)) continue;
|
|
799
|
+
const extraction = extractModelsFromSource({ name: filePath, text });
|
|
800
|
+
if (extraction.identities.length === 0 && extraction.modelGroups.length === 0 && extraction.converters.length === 0) continue;
|
|
801
|
+
out.push({ filePath, extraction });
|
|
802
|
+
}
|
|
803
|
+
return out;
|
|
804
|
+
}
|
|
805
|
+
function textHasModelMarker(text) {
|
|
806
|
+
if (text.includes("firestoreModelIdentity(")) return true;
|
|
807
|
+
if (text.includes("@dbxModelGroup")) return true;
|
|
808
|
+
if (text.includes("snapshotConverterFunctions")) return true;
|
|
809
|
+
if (text.includes("firestoreSubObject")) return true;
|
|
810
|
+
if (text.includes("firestoreObjectArray")) return true;
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
function* walkSourceFiles(dir) {
|
|
814
|
+
for (const entry of readdirSync2(dir).sort()) {
|
|
815
|
+
if (entry === "node_modules" || entry === "dist") continue;
|
|
816
|
+
const p = join3(dir, entry);
|
|
817
|
+
const stat = statSync2(p);
|
|
818
|
+
if (stat.isDirectory()) {
|
|
819
|
+
yield* walkSourceFiles(p);
|
|
820
|
+
} else if (isCandidateSourceFile(entry)) {
|
|
821
|
+
yield p;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
function isCandidateSourceFile(name) {
|
|
826
|
+
if (!name.endsWith(".ts")) return false;
|
|
827
|
+
if (name.endsWith(".api.ts")) return false;
|
|
828
|
+
if (name.endsWith(".spec.ts")) return false;
|
|
829
|
+
if (name.endsWith(".test.ts")) return false;
|
|
830
|
+
if (name.endsWith(".id.ts")) return false;
|
|
831
|
+
if (name.endsWith(".d.ts")) return false;
|
|
832
|
+
return true;
|
|
833
|
+
}
|
|
834
|
+
function safeIsDirectory2(p) {
|
|
835
|
+
try {
|
|
836
|
+
return statSync2(p).isDirectory();
|
|
837
|
+
} catch {
|
|
838
|
+
return false;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/assemble-models.ts
|
|
843
|
+
var MAX_NESTED_DEPTH = 8;
|
|
844
|
+
var LONG_NAME_RE = /^[a-z][a-zA-Z0-9]*$/;
|
|
845
|
+
var ENUM_GENERIC_RE = /firestoreEnum<(\w+)>|optionalFirestoreEnum<(\w+)>/;
|
|
846
|
+
function assembleModels(input) {
|
|
847
|
+
const registries = buildGlobalRegistries(input.extractions);
|
|
848
|
+
const accumulator = { seen: /* @__PURE__ */ new Set(), entries: [] };
|
|
849
|
+
for (const source of input.extractions) {
|
|
850
|
+
appendEntriesFromSource(source, registries, accumulator);
|
|
851
|
+
}
|
|
852
|
+
accumulator.entries.sort((a, b) => a.modelType.localeCompare(b.modelType));
|
|
853
|
+
return accumulator.entries;
|
|
854
|
+
}
|
|
855
|
+
function buildGlobalRegistries(extractions) {
|
|
856
|
+
const converterRegistry = /* @__PURE__ */ new Map();
|
|
857
|
+
const interfaceRegistry = /* @__PURE__ */ new Map();
|
|
858
|
+
const groupByModelName = /* @__PURE__ */ new Map();
|
|
859
|
+
for (const { extraction } of extractions) {
|
|
860
|
+
for (const converter of extraction.converters) {
|
|
861
|
+
if (converter.converterConst && !converterRegistry.has(converter.converterConst)) {
|
|
862
|
+
converterRegistry.set(converter.converterConst, converter);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
for (const iface of extraction.interfaces) {
|
|
866
|
+
if (!interfaceRegistry.has(iface.name)) interfaceRegistry.set(iface.name, iface);
|
|
867
|
+
}
|
|
868
|
+
for (const group of extraction.modelGroups) {
|
|
869
|
+
for (const modelName of group.modelNames) {
|
|
870
|
+
if (!groupByModelName.has(modelName)) groupByModelName.set(modelName, group.name);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return { converterRegistry, interfaceRegistry, groupByModelName };
|
|
875
|
+
}
|
|
876
|
+
function appendEntriesFromSource(source, registries, accumulator) {
|
|
877
|
+
const enumNames = new Set(source.extraction.enums.map((e) => e.name));
|
|
878
|
+
for (const identity of source.extraction.identities) {
|
|
879
|
+
if (accumulator.seen.has(identity.identityConst)) continue;
|
|
880
|
+
const entry = buildEntryForIdentity({ identity, source, registries, enumNames });
|
|
881
|
+
if (entry) {
|
|
882
|
+
accumulator.seen.add(identity.identityConst);
|
|
883
|
+
accumulator.entries.push(entry);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
function buildEntryForIdentity(input) {
|
|
888
|
+
const { identity, source, registries, enumNames } = input;
|
|
889
|
+
if (identity.collectionPrefix === void 0) return void 0;
|
|
890
|
+
const modelName = capitalize(identity.modelType);
|
|
891
|
+
const iface = registries.interfaceRegistry.get(modelName);
|
|
892
|
+
if (!iface?.hasDbxModelTag) return void 0;
|
|
893
|
+
const converter = findConverterForInterface(source.extraction, modelName) ?? findConverterFromRegistry(registries.converterRegistry, modelName);
|
|
894
|
+
if (!converter) return void 0;
|
|
895
|
+
const fields = buildFields({
|
|
896
|
+
converter,
|
|
897
|
+
iface,
|
|
898
|
+
interfaceRegistry: registries.interfaceRegistry,
|
|
899
|
+
converterRegistry: registries.converterRegistry,
|
|
900
|
+
enumNames,
|
|
901
|
+
depth: 0,
|
|
902
|
+
visitedConverters: /* @__PURE__ */ new Set()
|
|
903
|
+
});
|
|
904
|
+
const modelGroup = registries.groupByModelName.get(modelName);
|
|
905
|
+
return {
|
|
906
|
+
modelType: identity.modelType,
|
|
907
|
+
modelName,
|
|
908
|
+
...modelGroup ? { modelGroup } : {},
|
|
909
|
+
identityConst: identity.identityConst,
|
|
910
|
+
collectionPrefix: identity.collectionPrefix,
|
|
911
|
+
...identity.parentIdentityConst ? { parentIdentityConst: identity.parentIdentityConst } : {},
|
|
912
|
+
...iface.description ? { description: iface.description } : {},
|
|
913
|
+
sourcePackage: source.sourcePackage,
|
|
914
|
+
sourceFile: source.sourceFile,
|
|
915
|
+
fields
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
function findConverterForInterface(extraction, interfaceName) {
|
|
919
|
+
return extraction.converters.find((c) => c.interfaceName === interfaceName);
|
|
920
|
+
}
|
|
921
|
+
function findConverterFromRegistry(registry, interfaceName) {
|
|
922
|
+
let result;
|
|
923
|
+
for (const converter of registry.values()) {
|
|
924
|
+
if (converter.interfaceName === interfaceName) {
|
|
925
|
+
result = converter;
|
|
926
|
+
break;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
return result;
|
|
930
|
+
}
|
|
931
|
+
function buildFields(input) {
|
|
932
|
+
const out = [];
|
|
933
|
+
const propByName = /* @__PURE__ */ new Map();
|
|
934
|
+
if (input.iface) {
|
|
935
|
+
for (const prop of input.iface.props) propByName.set(prop.name, prop);
|
|
936
|
+
for (const ancestor of collectAncestors(input.iface, input.interfaceRegistry)) {
|
|
937
|
+
for (const prop of ancestor.props) {
|
|
938
|
+
if (!propByName.has(prop.name)) propByName.set(prop.name, prop);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
for (const field of input.converter.fields) {
|
|
943
|
+
out.push(buildField({ ...input, field, propByName }));
|
|
944
|
+
}
|
|
945
|
+
return out;
|
|
946
|
+
}
|
|
947
|
+
function buildField(input) {
|
|
948
|
+
const { field, propByName } = input;
|
|
949
|
+
const prop = propByName.get(field.key);
|
|
950
|
+
const enumRef = resolveEnumRef(field.converter, prop?.tsType, input.enumNames);
|
|
951
|
+
const optional = prop?.optional ?? field.converter.startsWith("optionalFirestore");
|
|
952
|
+
const longName = resolveLongName(field.key, prop?.longName);
|
|
953
|
+
const nested = resolveNestedFields(input);
|
|
954
|
+
const out = {
|
|
955
|
+
name: field.key,
|
|
956
|
+
longName,
|
|
957
|
+
converter: field.converter,
|
|
958
|
+
...prop?.tsType ? { tsType: prop.tsType } : {},
|
|
959
|
+
optional,
|
|
960
|
+
...prop?.description ? { description: prop.description } : {},
|
|
961
|
+
...enumRef ? { enumRef } : {},
|
|
962
|
+
...prop?.syncFlag ? { syncFlag: prop.syncFlag } : {},
|
|
963
|
+
...nested ? { nestedFields: nested.fields, nestedIsArray: nested.isArray } : {}
|
|
964
|
+
};
|
|
965
|
+
return out;
|
|
966
|
+
}
|
|
967
|
+
function resolveNestedFields(input) {
|
|
968
|
+
const { field } = input;
|
|
969
|
+
if (input.depth >= MAX_NESTED_DEPTH) return void 0;
|
|
970
|
+
let nestedConverter;
|
|
971
|
+
if (field.nestedConverterInline) {
|
|
972
|
+
nestedConverter = field.nestedConverterInline;
|
|
973
|
+
} else if (field.nestedConverterRef) {
|
|
974
|
+
if (input.visitedConverters.has(field.nestedConverterRef)) return void 0;
|
|
975
|
+
nestedConverter = input.converterRegistry.get(field.nestedConverterRef);
|
|
976
|
+
}
|
|
977
|
+
if (!nestedConverter) return void 0;
|
|
978
|
+
const nextVisited = new Set(input.visitedConverters);
|
|
979
|
+
if (nestedConverter.converterConst) nextVisited.add(nestedConverter.converterConst);
|
|
980
|
+
const nestedIface = nestedConverter.interfaceName ? input.interfaceRegistry.get(nestedConverter.interfaceName) : void 0;
|
|
981
|
+
const fields = buildFields({
|
|
982
|
+
converter: nestedConverter,
|
|
983
|
+
iface: nestedIface,
|
|
984
|
+
interfaceRegistry: input.interfaceRegistry,
|
|
985
|
+
converterRegistry: input.converterRegistry,
|
|
986
|
+
enumNames: input.enumNames,
|
|
987
|
+
depth: input.depth + 1,
|
|
988
|
+
visitedConverters: nextVisited
|
|
989
|
+
});
|
|
990
|
+
return { fields, isArray: field.nestedIsArray ?? false };
|
|
991
|
+
}
|
|
992
|
+
function collectAncestors(iface, registry) {
|
|
993
|
+
const out = [];
|
|
994
|
+
const visited = /* @__PURE__ */ new Set([iface.name]);
|
|
995
|
+
const stack = [iface];
|
|
996
|
+
while (stack.length > 0) {
|
|
997
|
+
const current = stack.pop();
|
|
998
|
+
for (const parentName of current.extendsNames) {
|
|
999
|
+
if (visited.has(parentName)) continue;
|
|
1000
|
+
visited.add(parentName);
|
|
1001
|
+
const parent = registry.get(parentName);
|
|
1002
|
+
if (parent) {
|
|
1003
|
+
out.push(parent);
|
|
1004
|
+
stack.push(parent);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
return out;
|
|
1009
|
+
}
|
|
1010
|
+
function resolveLongName(fieldName, propLongName) {
|
|
1011
|
+
let result;
|
|
1012
|
+
if (propLongName && LONG_NAME_RE.test(propLongName)) {
|
|
1013
|
+
result = propLongName;
|
|
1014
|
+
} else {
|
|
1015
|
+
result = fieldName;
|
|
1016
|
+
}
|
|
1017
|
+
return result;
|
|
1018
|
+
}
|
|
1019
|
+
function resolveEnumRef(converter, tsType, enumNames) {
|
|
1020
|
+
let result;
|
|
1021
|
+
if (tsType) {
|
|
1022
|
+
for (const name of enumNames) {
|
|
1023
|
+
const re = new RegExp(String.raw`\b${name}\b`);
|
|
1024
|
+
if (re.test(tsType)) {
|
|
1025
|
+
result = name;
|
|
1026
|
+
break;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
if (!result) {
|
|
1031
|
+
const m = ENUM_GENERIC_RE.exec(converter);
|
|
1032
|
+
if (m) {
|
|
1033
|
+
const name = m[1] ?? m[2];
|
|
1034
|
+
if (enumNames.has(name)) result = name;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
return result;
|
|
1038
|
+
}
|
|
1039
|
+
function capitalize(s) {
|
|
1040
|
+
return s.length > 0 ? s[0].toUpperCase() + s.slice(1) : s;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
427
1043
|
// packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/bind-validators.ts
|
|
428
|
-
import { existsSync as existsSync2, readdirSync as
|
|
429
|
-
import { dirname as dirname2, isAbsolute as isAbsolute2, join as
|
|
1044
|
+
import { existsSync as existsSync2, readdirSync as readdirSync3, readFileSync as readFileSync5, statSync as statSync3 } from "node:fs";
|
|
1045
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join4, resolve as resolve2 } from "node:path";
|
|
430
1046
|
function deriveValidatorName(paramsTypeName) {
|
|
431
1047
|
if (!paramsTypeName) return "";
|
|
432
1048
|
return paramsTypeName.charAt(0).toLowerCase() + paramsTypeName.slice(1) + "Type";
|
|
@@ -438,7 +1054,7 @@ function isExportedFromPackage(input) {
|
|
|
438
1054
|
return findIdentifierInBarrelChain(indexPath, identifier, /* @__PURE__ */ new Set());
|
|
439
1055
|
}
|
|
440
1056
|
function locateBarrelEntry(packageRoot) {
|
|
441
|
-
const candidates = [
|
|
1057
|
+
const candidates = [join4(packageRoot, "src", "index.ts"), join4(packageRoot, "src", "index.d.ts"), join4(packageRoot, "index.d.ts"), join4(packageRoot, "index.ts")];
|
|
442
1058
|
return candidates.find((candidate) => existsSync2(candidate));
|
|
443
1059
|
}
|
|
444
1060
|
var EXPORT_DECL_PATTERNS = [/export\s+(?:declare\s+)?const\s+IDENT\b/, /export\s+(?:declare\s+)?function\s+IDENT\b/, /export\s*\{[^}]*\bIDENT\b[^}]*\}/];
|
|
@@ -447,7 +1063,7 @@ function findIdentifierInBarrelChain(filePath, identifier, visited) {
|
|
|
447
1063
|
visited.add(filePath);
|
|
448
1064
|
let text;
|
|
449
1065
|
try {
|
|
450
|
-
text =
|
|
1066
|
+
text = readFileSync5(filePath, "utf8");
|
|
451
1067
|
} catch {
|
|
452
1068
|
return false;
|
|
453
1069
|
}
|
|
@@ -491,12 +1107,12 @@ function hasTsModuleExtension(value) {
|
|
|
491
1107
|
}
|
|
492
1108
|
function resolveExistingTsPath(probe) {
|
|
493
1109
|
if (!existsSync2(probe)) return void 0;
|
|
494
|
-
const stat =
|
|
1110
|
+
const stat = statSync3(probe);
|
|
495
1111
|
if (stat.isFile()) return probe;
|
|
496
1112
|
if (!stat.isDirectory()) return void 0;
|
|
497
|
-
const sourceIndex =
|
|
1113
|
+
const sourceIndex = join4(probe, "index.ts");
|
|
498
1114
|
if (existsSync2(sourceIndex)) return sourceIndex;
|
|
499
|
-
const declarationIndex =
|
|
1115
|
+
const declarationIndex = join4(probe, "index.d.ts");
|
|
500
1116
|
return existsSync2(declarationIndex) ? declarationIndex : void 0;
|
|
501
1117
|
}
|
|
502
1118
|
function escapeRegExp(value) {
|
|
@@ -506,7 +1122,7 @@ function escapeRegExp(value) {
|
|
|
506
1122
|
// packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/emit.ts
|
|
507
1123
|
import { format, resolveConfig } from "prettier";
|
|
508
1124
|
async function renderManifest(input) {
|
|
509
|
-
const { outputFile, entries, projectName, namespace } = input;
|
|
1125
|
+
const { outputFile, entries, projectName, namespace, modelEntries, modelNamespace, emitConverters = false } = input;
|
|
510
1126
|
const importsByPackage = /* @__PURE__ */ new Map();
|
|
511
1127
|
for (const entry of entries) {
|
|
512
1128
|
if (!entry.packageName || !entry.validatorName) continue;
|
|
@@ -519,16 +1135,24 @@ async function renderManifest(input) {
|
|
|
519
1135
|
return `import { ${sortedNames} } from '${pkg}';`;
|
|
520
1136
|
});
|
|
521
1137
|
const entryLines = entries.map((e) => renderEntry(e));
|
|
1138
|
+
const emitModels = Boolean(modelEntries && modelEntries.length > 0 && modelNamespace);
|
|
1139
|
+
const dbxCliTypeImports = emitModels ? `import { type CliApiManifest, type CliModelManifest } from '@dereekb/dbx-cli';` : `import { type CliApiManifest } from '@dereekb/dbx-cli';`;
|
|
1140
|
+
const modelSection = emitModels ? `
|
|
1141
|
+
|
|
1142
|
+
export const ${modelNamespace}: CliModelManifest = [
|
|
1143
|
+
${(modelEntries ?? []).map((m) => renderModelEntry(m, emitConverters)).join(",\n")}
|
|
1144
|
+
];
|
|
1145
|
+
` : "";
|
|
522
1146
|
const source = `/* eslint-disable @nx/enforce-module-boundaries */
|
|
523
1147
|
// AUTO-GENERATED \u2014 DO NOT EDIT.
|
|
524
1148
|
// Run \`pnpm nx run ${projectName}:generate-api-manifest\` to refresh.
|
|
525
1149
|
|
|
526
1150
|
${importLines.join("\n")}
|
|
527
|
-
|
|
1151
|
+
${dbxCliTypeImports}
|
|
528
1152
|
|
|
529
1153
|
export const ${namespace}: CliApiManifest = [
|
|
530
1154
|
${entryLines.join(",\n")}
|
|
531
|
-
]
|
|
1155
|
+
];${modelSection}
|
|
532
1156
|
`;
|
|
533
1157
|
return formatWithPrettier(source, outputFile);
|
|
534
1158
|
}
|
|
@@ -562,6 +1186,41 @@ async function formatWithPrettier(source, outputFile) {
|
|
|
562
1186
|
const config = await resolveConfig(outputFile);
|
|
563
1187
|
return format(source, { ...config, filepath: outputFile });
|
|
564
1188
|
}
|
|
1189
|
+
function renderModelEntry(entry, emitConverters) {
|
|
1190
|
+
const fields = [
|
|
1191
|
+
`modelType: ${JSON.stringify(entry.modelType)}`,
|
|
1192
|
+
`modelName: ${JSON.stringify(entry.modelName)}`,
|
|
1193
|
+
entry.modelGroup ? `modelGroup: ${JSON.stringify(entry.modelGroup)}` : void 0,
|
|
1194
|
+
`identityConst: ${JSON.stringify(entry.identityConst)}`,
|
|
1195
|
+
`collectionPrefix: ${JSON.stringify(entry.collectionPrefix)}`,
|
|
1196
|
+
entry.parentIdentityConst ? `parentIdentityConst: ${JSON.stringify(entry.parentIdentityConst)}` : void 0,
|
|
1197
|
+
entry.description ? `description: ${JSON.stringify(entry.description)}` : void 0,
|
|
1198
|
+
`sourcePackage: ${JSON.stringify(entry.sourcePackage)}`,
|
|
1199
|
+
`sourceFile: ${JSON.stringify(entry.sourceFile)}`,
|
|
1200
|
+
`fields: ${renderModelFields(entry.fields, emitConverters)}`
|
|
1201
|
+
];
|
|
1202
|
+
return ` { ${fields.filter((v) => Boolean(v)).join(", ")} }`;
|
|
1203
|
+
}
|
|
1204
|
+
function renderModelFields(fields, emitConverters) {
|
|
1205
|
+
if (fields.length === 0) return "[]";
|
|
1206
|
+
const items = fields.map((field) => renderModelField(field, emitConverters));
|
|
1207
|
+
return `[${items.join(", ")}]`;
|
|
1208
|
+
}
|
|
1209
|
+
function renderModelField(field, emitConverters) {
|
|
1210
|
+
const parts = [
|
|
1211
|
+
`name: ${JSON.stringify(field.name)}`,
|
|
1212
|
+
`longName: ${JSON.stringify(field.longName)}`,
|
|
1213
|
+
emitConverters && field.converter !== void 0 ? `converter: ${JSON.stringify(field.converter)}` : void 0,
|
|
1214
|
+
field.tsType ? `tsType: ${JSON.stringify(field.tsType)}` : void 0,
|
|
1215
|
+
`optional: ${field.optional ? "true" : "false"}`,
|
|
1216
|
+
field.description ? `description: ${JSON.stringify(field.description)}` : void 0,
|
|
1217
|
+
field.enumRef ? `enumRef: ${JSON.stringify(field.enumRef)}` : void 0,
|
|
1218
|
+
field.syncFlag ? `syncFlag: ${JSON.stringify(field.syncFlag)}` : void 0,
|
|
1219
|
+
field.nestedFields ? `nestedFields: ${renderModelFields(field.nestedFields, emitConverters)}` : void 0,
|
|
1220
|
+
field.nestedFields ? `nestedIsArray: ${field.nestedIsArray ? "true" : "false"}` : void 0
|
|
1221
|
+
];
|
|
1222
|
+
return `{ ${parts.filter((v) => Boolean(v)).join(", ")} }`;
|
|
1223
|
+
}
|
|
565
1224
|
|
|
566
1225
|
// packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/main.ts
|
|
567
1226
|
var WORKSPACE_ROOT = process.cwd();
|
|
@@ -586,6 +1245,8 @@ async function main() {
|
|
|
586
1245
|
const packageCache = /* @__PURE__ */ new Map();
|
|
587
1246
|
const apiFilesCache = /* @__PURE__ */ new Map();
|
|
588
1247
|
const collected = [];
|
|
1248
|
+
const modelSources = [];
|
|
1249
|
+
const modelPackagesScanned = /* @__PURE__ */ new Set();
|
|
589
1250
|
let missingValidators = 0;
|
|
590
1251
|
let skippedGroups = 0;
|
|
591
1252
|
for (const group of groups) {
|
|
@@ -597,6 +1258,16 @@ async function main() {
|
|
|
597
1258
|
}
|
|
598
1259
|
if (!packageCache.has(pkg.packageRoot)) packageCache.set(pkg.packageRoot, pkg);
|
|
599
1260
|
if (!apiFilesCache.has(pkg.packageRoot)) apiFilesCache.set(pkg.packageRoot, findApiFiles(pkg.packageRoot));
|
|
1261
|
+
if (flags.emitModels && !modelPackagesScanned.has(pkg.packageRoot)) {
|
|
1262
|
+
modelPackagesScanned.add(pkg.packageRoot);
|
|
1263
|
+
for (const match2 of findModelFiles(pkg.packageRoot)) {
|
|
1264
|
+
modelSources.push({
|
|
1265
|
+
sourcePackage: pkg.packageName,
|
|
1266
|
+
sourceFile: relPath(WORKSPACE_ROOT, match2.filePath),
|
|
1267
|
+
extraction: match2.extraction
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
600
1271
|
const apiFiles = apiFilesCache.get(pkg.packageRoot) ?? [];
|
|
601
1272
|
const match = apiFiles.find((f) => f.className === group.className);
|
|
602
1273
|
if (!match) {
|
|
@@ -631,16 +1302,19 @@ async function main() {
|
|
|
631
1302
|
}
|
|
632
1303
|
}
|
|
633
1304
|
collected.sort(compareEntries);
|
|
1305
|
+
const modelEntries = flags.emitModels ? assembleModels({ extractions: modelSources }) : [];
|
|
1306
|
+
const filteredModelEntries = flags.only ? modelEntries.filter((m) => flags.only?.has(m.modelType)) : modelEntries;
|
|
634
1307
|
ensureOutputDir(outputDir);
|
|
635
|
-
const formatted = await renderManifest({ outputFile, entries: collected, projectName, namespace });
|
|
636
|
-
if (existsSync3(outputFile) &&
|
|
1308
|
+
const formatted = await renderManifest({ outputFile, entries: collected, projectName, namespace, modelEntries: filteredModelEntries, modelNamespace: deriveModelNamespace(flags.project), emitConverters: flags.emitModelConverters });
|
|
1309
|
+
if (existsSync3(outputFile) && readFileSync6(outputFile, "utf8") === formatted) {
|
|
637
1310
|
console.log(`[unchanged] ${relative2(WORKSPACE_ROOT, outputFile)}`);
|
|
638
1311
|
} else {
|
|
639
1312
|
writeFileSync(outputFile, formatted);
|
|
640
1313
|
console.log(`[wrote] ${relative2(WORKSPACE_ROOT, outputFile)}`);
|
|
641
1314
|
}
|
|
642
1315
|
const groupCount = packageCache.size === 0 ? 0 : new Set(collected.map((c) => c.entry.groupName)).size;
|
|
643
|
-
|
|
1316
|
+
const modelSummary = flags.emitModels ? ` \xB7 ${filteredModelEntries.length} models` : "";
|
|
1317
|
+
console.log(`Summary: ${groupCount} groups \xB7 ${collected.length} entries \xB7 ${collected.length - missingValidators} validators bound \xB7 ${missingValidators} missing \xB7 ${skippedGroups} skipped${modelSummary}`);
|
|
644
1318
|
if (flags.strict && missingValidators > 0) {
|
|
645
1319
|
console.error(`[strict] ${missingValidators} validator(s) missing \u2014 failing build.`);
|
|
646
1320
|
process.exit(1);
|
|
@@ -661,15 +1335,25 @@ function deriveNamespace(projectName) {
|
|
|
661
1335
|
const base = (projectName ?? "cli").replaceAll(/[^a-zA-Z0-9]+/g, "_");
|
|
662
1336
|
return `${base.toUpperCase()}_API_MANIFEST`;
|
|
663
1337
|
}
|
|
1338
|
+
function deriveModelNamespace(projectName) {
|
|
1339
|
+
const base = (projectName ?? "cli").replaceAll(/[^a-zA-Z0-9]+/g, "_");
|
|
1340
|
+
return `${base.toUpperCase()}_MODEL_MANIFEST`;
|
|
1341
|
+
}
|
|
664
1342
|
function parseFlags(argv) {
|
|
665
1343
|
let only;
|
|
666
1344
|
let strict = false;
|
|
667
1345
|
let functionsConfig;
|
|
668
1346
|
let output;
|
|
669
1347
|
let project;
|
|
1348
|
+
let emitModels = false;
|
|
1349
|
+
let emitModelConverters = false;
|
|
670
1350
|
for (const arg of argv) {
|
|
671
1351
|
if (arg === "--strict") {
|
|
672
1352
|
strict = true;
|
|
1353
|
+
} else if (arg === "--emit-models") {
|
|
1354
|
+
emitModels = true;
|
|
1355
|
+
} else if (arg === "--emit-model-converters") {
|
|
1356
|
+
emitModelConverters = true;
|
|
673
1357
|
} else if (arg.startsWith("--only=")) {
|
|
674
1358
|
const list = arg.slice("--only=".length).split(",").map((s) => s.trim()).filter(Boolean);
|
|
675
1359
|
if (list.length > 0) only = new Set(list);
|
|
@@ -681,7 +1365,7 @@ function parseFlags(argv) {
|
|
|
681
1365
|
project = arg.slice("--project=".length);
|
|
682
1366
|
}
|
|
683
1367
|
}
|
|
684
|
-
return { only, strict, functionsConfig, output, project };
|
|
1368
|
+
return { only, strict, functionsConfig, output, project, emitModels, emitModelConverters };
|
|
685
1369
|
}
|
|
686
1370
|
function printUsageAndExit() {
|
|
687
1371
|
console.error(String.raw`generate-api-manifest
|
|
@@ -700,7 +1384,13 @@ Required flags:
|
|
|
700
1384
|
Optional:
|
|
701
1385
|
--project=<name> Project name to show in the regenerate banner.
|
|
702
1386
|
--only=<csv> Filter to listed model names.
|
|
703
|
-
--strict Fail when any validator binding is missing
|
|
1387
|
+
--strict Fail when any validator binding is missing.
|
|
1388
|
+
--emit-models Opt in to emitting <NAMESPACE>_MODEL_MANIFEST. Off by default —
|
|
1389
|
+
pair with the runtime \`modelManifest\` option on \`runCli\` to
|
|
1390
|
+
enable the built-in \`model-info\` command.
|
|
1391
|
+
--emit-model-converters Opt in to including each field's converter expression text inside
|
|
1392
|
+
the model manifest. Off by default — useful for downstream tooling
|
|
1393
|
+
(e.g. dbx-components MCP) but unused by the CLI.`);
|
|
704
1394
|
process.exit(1);
|
|
705
1395
|
}
|
|
706
1396
|
try {
|