@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.
@@ -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 readFileSync5, writeFileSync } from "node:fs";
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 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";
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 = [join3(packageRoot, "src", "index.ts"), join3(packageRoot, "src", "index.d.ts"), join3(packageRoot, "index.d.ts"), join3(packageRoot, "index.ts")];
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 = readFileSync4(filePath, "utf8");
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 = statSync2(probe);
1110
+ const stat = statSync3(probe);
495
1111
  if (stat.isFile()) return probe;
496
1112
  if (!stat.isDirectory()) return void 0;
497
- const sourceIndex = join3(probe, "index.ts");
1113
+ const sourceIndex = join4(probe, "index.ts");
498
1114
  if (existsSync2(sourceIndex)) return sourceIndex;
499
- const declarationIndex = join3(probe, "index.d.ts");
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
- import { type CliApiManifest } from '@dereekb/dbx-cli';
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) && readFileSync5(outputFile, "utf8") === formatted) {
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
- console.log(`Summary: ${groupCount} groups \xB7 ${collected.length} entries \xB7 ${collected.length - missingValidators} validators bound \xB7 ${missingValidators} missing \xB7 ${skippedGroups} skipped`);
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 {