@dereekb/dbx-cli 13.11.3 → 13.11.5

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,334 @@ 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 IDENTITY_FN = "firestoreModelIdentity";
390
+ var CONVERTER_FN_NAMES = ["snapshotConverterFunctions", "firestoreSubObject", "firestoreObjectArray"];
391
+ var SUB_OBJECT_FN = "firestoreSubObject";
392
+ var OBJECT_ARRAY_FN = "firestoreObjectArray";
393
+ var SNAPSHOT_FN = "snapshotConverterFunctions";
394
+ var FIELDS_LITERAL_KEY = "fields";
395
+ var OBJECT_FIELD_KEY = "objectField";
396
+ function extractModelsFromSource(input) {
397
+ const project = new Project3({ useInMemoryFileSystem: true, skipAddingFilesFromTsConfig: true });
398
+ const sourceFile = project.createSourceFile(input.name, input.text, { overwrite: true });
399
+ const identities = readIdentities(sourceFile);
400
+ const interfaces = readInterfaces(sourceFile);
401
+ const converters = readConverters(sourceFile);
402
+ const enums = readEnums(sourceFile);
403
+ const modelGroups = readModelGroups(sourceFile);
404
+ return { identities, interfaces, converters, enums, modelGroups };
405
+ }
406
+ function readIdentities(sourceFile) {
407
+ const out = [];
408
+ for (const statement of sourceFile.getVariableStatements()) {
409
+ if (!statement.isExported()) continue;
410
+ for (const decl of statement.getDeclarations()) {
411
+ const initializer = decl.getInitializer();
412
+ if (!initializer || !Node3.isCallExpression(initializer)) continue;
413
+ if (initializer.getExpression().getText() !== IDENTITY_FN) continue;
414
+ const parsed = parseIdentityArgs(initializer);
415
+ if (parsed) {
416
+ out.push({ identityConst: decl.getName(), ...parsed });
417
+ }
418
+ }
419
+ }
420
+ return out;
421
+ }
422
+ function parseIdentityArgs(call) {
423
+ const args = call.getArguments();
424
+ let result;
425
+ if (args.length === 1) {
426
+ const modelType = stringLiteralValue(args[0]);
427
+ if (modelType !== void 0) {
428
+ result = { modelType, collectionPrefix: void 0, parentIdentityConst: void 0 };
429
+ }
430
+ } else if (args.length === 2) {
431
+ const first = stringLiteralValue(args[0]);
432
+ if (first === void 0) {
433
+ const modelType = stringLiteralValue(args[1]);
434
+ if (modelType !== void 0) {
435
+ result = { modelType, collectionPrefix: void 0, parentIdentityConst: identifierName(args[0]) };
436
+ }
437
+ } else {
438
+ result = { modelType: first, collectionPrefix: stringLiteralValue(args[1]), parentIdentityConst: void 0 };
439
+ }
440
+ } else if (args.length >= 3) {
441
+ const modelType = stringLiteralValue(args[1]);
442
+ if (modelType !== void 0) {
443
+ result = { modelType, collectionPrefix: stringLiteralValue(args[2]), parentIdentityConst: identifierName(args[0]) };
444
+ }
445
+ }
446
+ return result;
447
+ }
448
+ function readInterfaces(sourceFile) {
449
+ const out = [];
450
+ for (const decl of sourceFile.getInterfaces()) {
451
+ if (!decl.isExported()) continue;
452
+ out.push(buildInterface(decl));
453
+ }
454
+ return out;
455
+ }
456
+ function buildInterface(decl) {
457
+ const jsDocs = decl.getJsDocs();
458
+ const hasDbxModelTag = jsDocsHaveTag(jsDocs, "dbxModel");
459
+ const extendsNames = decl.getExtends().map((e) => e.getExpression().getText());
460
+ const props = [];
461
+ for (const prop of decl.getProperties()) {
462
+ const propJsDocs = prop.getJsDocs();
463
+ const longName = readJsDocTagText(propJsDocs, "dbxModelVariable");
464
+ const syncFlag = readJsDocTagText(propJsDocs, "dbxModelVariableSyncFlag");
465
+ const tsType = (prop.getTypeNode()?.getText() ?? "").replaceAll(/\s+/g, " ").trim();
466
+ const optional = prop.hasQuestionToken() || tsType.startsWith("Maybe<");
467
+ props.push({
468
+ name: prop.getName(),
469
+ tsType,
470
+ optional,
471
+ description: readJsDocDescription(propJsDocs),
472
+ longName,
473
+ syncFlag
474
+ });
475
+ }
476
+ return {
477
+ name: decl.getName(),
478
+ description: readJsDocDescription(jsDocs),
479
+ hasDbxModelTag,
480
+ extendsNames,
481
+ props
482
+ };
483
+ }
484
+ function readConverters(sourceFile) {
485
+ const out = [];
486
+ for (const statement of sourceFile.getVariableStatements()) {
487
+ if (!statement.isExported()) continue;
488
+ for (const decl of statement.getDeclarations()) {
489
+ const initializer = decl.getInitializer();
490
+ if (!initializer || !Node3.isCallExpression(initializer)) continue;
491
+ const factory = initializer.getExpression().getText();
492
+ if (!isConverterFactoryName(factory)) continue;
493
+ const interfaceName = readGenericInterfaceName(initializer);
494
+ const fields = readConverterFields(initializer);
495
+ if (!fields) continue;
496
+ out.push({
497
+ converterConst: decl.getName(),
498
+ factory,
499
+ interfaceName,
500
+ fields,
501
+ line: decl.getStartLineNumber()
502
+ });
503
+ }
504
+ }
505
+ return out;
506
+ }
507
+ function isConverterFactoryName(name) {
508
+ return CONVERTER_FN_NAMES.includes(name);
509
+ }
510
+ function readGenericInterfaceName(call) {
511
+ const typeArgs = call.getTypeArguments();
512
+ let result;
513
+ if (typeArgs.length > 0) {
514
+ result = typeArgs[0].getText().replaceAll(/<[^>]*>/g, "").trim();
515
+ }
516
+ return result;
517
+ }
518
+ function readConverterFields(call) {
519
+ const fnName = call.getExpression().getText();
520
+ const args = call.getArguments();
521
+ if (args.length === 0) return void 0;
522
+ const config = args[0];
523
+ if (!Node3.isObjectLiteralExpression(config)) return void 0;
524
+ let fieldsLiteral;
525
+ if (fnName === SNAPSHOT_FN) {
526
+ fieldsLiteral = readObjectProperty(config, FIELDS_LITERAL_KEY);
527
+ } else {
528
+ const objectField = readPropertyValue(config, OBJECT_FIELD_KEY);
529
+ if (objectField && Node3.isObjectLiteralExpression(objectField)) {
530
+ fieldsLiteral = readObjectProperty(objectField, FIELDS_LITERAL_KEY);
531
+ }
532
+ }
533
+ if (!fieldsLiteral) return void 0;
534
+ return readFieldEntries(fieldsLiteral);
535
+ }
536
+ function readFieldEntries(fields) {
537
+ const out = [];
538
+ for (const property of fields.getProperties()) {
539
+ if (Node3.isPropertyAssignment(property)) {
540
+ const initializer = property.getInitializer();
541
+ const converterText = initializer ? initializer.getText().replaceAll(/\s+/g, " ").trim() : "";
542
+ const nested = initializer ? readNestedFromExpression(initializer) : void 0;
543
+ out.push({
544
+ key: property.getName(),
545
+ converter: converterText,
546
+ nestedConverterRef: nested?.ref,
547
+ nestedConverterInline: nested?.inline,
548
+ nestedIsArray: nested?.isArray
549
+ });
550
+ } else if (Node3.isShorthandPropertyAssignment(property)) {
551
+ const name = property.getName();
552
+ out.push({ key: name, converter: name });
553
+ }
554
+ }
555
+ return out;
556
+ }
557
+ function readNestedFromExpression(expr) {
558
+ if (!Node3.isCallExpression(expr)) return void 0;
559
+ const fnName = expr.getExpression().getText();
560
+ if (fnName !== SUB_OBJECT_FN && fnName !== OBJECT_ARRAY_FN) return void 0;
561
+ const args = expr.getArguments();
562
+ if (args.length === 0) return void 0;
563
+ const config = args[0];
564
+ if (!Node3.isObjectLiteralExpression(config)) return void 0;
565
+ const objectField = readPropertyValue(config, OBJECT_FIELD_KEY);
566
+ if (!objectField) return void 0;
567
+ const isArray = fnName === OBJECT_ARRAY_FN;
568
+ let result;
569
+ if (Node3.isIdentifier(objectField)) {
570
+ result = { ref: objectField.getText(), isArray };
571
+ } else if (Node3.isObjectLiteralExpression(objectField)) {
572
+ const fieldsLiteral = readObjectProperty(objectField, FIELDS_LITERAL_KEY);
573
+ if (fieldsLiteral) {
574
+ const inlineFields = readFieldEntries(fieldsLiteral);
575
+ result = {
576
+ inline: {
577
+ converterConst: void 0,
578
+ factory: fnName,
579
+ interfaceName: readGenericInterfaceName(expr),
580
+ fields: inlineFields,
581
+ line: expr.getStartLineNumber()
582
+ },
583
+ isArray
584
+ };
585
+ }
586
+ }
587
+ return result;
588
+ }
589
+ function readPropertyValue(literal, key) {
590
+ const property = literal.getProperty(key);
591
+ let result;
592
+ if (property && Node3.isPropertyAssignment(property)) {
593
+ result = property.getInitializer();
594
+ } else if (property && Node3.isShorthandPropertyAssignment(property)) {
595
+ result = property.getNameNode();
596
+ }
597
+ return result;
598
+ }
599
+ function readObjectProperty(literal, key) {
600
+ const value = readPropertyValue(literal, key);
601
+ return value && Node3.isObjectLiteralExpression(value) ? value : void 0;
602
+ }
603
+ function readEnums(sourceFile) {
604
+ const out = [];
605
+ for (const decl of sourceFile.getEnums()) {
606
+ if (!decl.isExported()) continue;
607
+ const values = [];
608
+ for (const member of decl.getMembers()) {
609
+ const value = member.getValue();
610
+ const description = readJsDocDescription(member.getJsDocs());
611
+ if (typeof value === "string" || typeof value === "number") {
612
+ values.push({ name: member.getName(), value, description });
613
+ }
614
+ }
615
+ out.push({
616
+ name: decl.getName(),
617
+ values,
618
+ description: readJsDocDescription(decl.getJsDocs())
619
+ });
620
+ }
621
+ return out;
622
+ }
623
+ function readModelGroups(sourceFile) {
624
+ const out = [];
625
+ for (const iface of sourceFile.getInterfaces()) {
626
+ if (!iface.isExported()) continue;
627
+ const groupTag = readJsDocTagText(iface.getJsDocs(), "dbxModelGroup");
628
+ if (!groupTag) continue;
629
+ const containerName = iface.getName();
630
+ if (!containerName.endsWith("FirestoreCollections")) continue;
631
+ const modelNames = [];
632
+ for (const prop of iface.getProperties()) {
633
+ const tsType = prop.getTypeNode()?.getText() ?? "";
634
+ const match = /([A-Z]\w*)FirestoreCollection(?:Factory)?(?:\b|<)/.exec(tsType);
635
+ if (match) modelNames.push(match[1]);
636
+ }
637
+ out.push({
638
+ name: groupTag,
639
+ containerName,
640
+ description: readJsDocDescription(iface.getJsDocs()),
641
+ modelNames
642
+ });
643
+ }
644
+ return out;
645
+ }
646
+ function jsDocsHaveTag(jsDocs, tagName) {
647
+ let found = false;
648
+ for (const jsDoc of jsDocs) {
649
+ for (const tag of jsDoc.getTags()) {
650
+ if (tag.getTagName() === tagName) {
651
+ found = true;
652
+ break;
653
+ }
654
+ }
655
+ if (found) break;
656
+ }
657
+ return found;
658
+ }
659
+ function readJsDocTagText(jsDocs, tagName) {
660
+ let result;
661
+ for (const jsDoc of jsDocs) {
662
+ for (const tag of jsDoc.getTags()) {
663
+ if (tag.getTagName() !== tagName) continue;
664
+ const text = tag.getCommentText()?.trim();
665
+ if (text !== void 0 && text.length > 0) {
666
+ result = text;
667
+ break;
668
+ }
669
+ }
670
+ if (result !== void 0) break;
671
+ }
672
+ return result;
673
+ }
674
+ function readJsDocDescription(jsDocs) {
675
+ let result;
676
+ for (const jsDoc of jsDocs) {
677
+ const description = jsDoc.getDescription().trim();
678
+ if (description.length === 0) continue;
679
+ const paragraph = firstParagraph(description);
680
+ if (paragraph.length > 0) {
681
+ result = paragraph;
682
+ break;
683
+ }
684
+ }
685
+ return result;
686
+ }
687
+ function firstParagraph(text) {
688
+ const lines = text.split("\n").map((line) => line.trim());
689
+ const collected = [];
690
+ for (const line of lines) {
691
+ if (line.startsWith("@")) break;
692
+ if (line.length === 0) {
693
+ if (collected.length > 0) break;
694
+ continue;
695
+ }
696
+ collected.push(line);
697
+ }
698
+ return collected.join(" ").trim();
699
+ }
700
+ function stringLiteralValue(node) {
701
+ let result;
702
+ if (Node3.isStringLiteral(node) || Node3.isNoSubstitutionTemplateLiteral(node)) {
703
+ result = node.getLiteralText();
704
+ }
705
+ return result;
706
+ }
707
+ function identifierName(node) {
708
+ let result;
709
+ if (Node3.isIdentifier(node)) {
710
+ result = node.getText();
711
+ }
712
+ return result;
713
+ }
714
+
387
715
  // packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/find-api-files.ts
388
716
  function findApiFiles(packageRoot) {
389
717
  const libRoot = join2(packageRoot, "src", "lib");
@@ -424,9 +752,263 @@ function safeIsDirectory(p) {
424
752
  }
425
753
  }
426
754
 
755
+ // packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/find-model-files.ts
756
+ import { readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2 } from "node:fs";
757
+ import { join as join3 } from "node:path";
758
+ function findModelFiles(packageRoot) {
759
+ const libRoot = join3(packageRoot, "src", "lib");
760
+ if (!safeIsDirectory2(libRoot)) return [];
761
+ const out = [];
762
+ for (const filePath of walkSourceFiles(libRoot)) {
763
+ const text = readFileSync4(filePath, "utf8");
764
+ if (!textHasModelMarker(text)) continue;
765
+ const extraction = extractModelsFromSource({ name: filePath, text });
766
+ if (extraction.identities.length === 0 && extraction.modelGroups.length === 0 && extraction.converters.length === 0) continue;
767
+ out.push({ filePath, extraction });
768
+ }
769
+ return out;
770
+ }
771
+ function textHasModelMarker(text) {
772
+ if (text.includes("firestoreModelIdentity(")) return true;
773
+ if (text.includes("@dbxModelGroup")) return true;
774
+ if (text.includes("snapshotConverterFunctions")) return true;
775
+ if (text.includes("firestoreSubObject")) return true;
776
+ if (text.includes("firestoreObjectArray")) return true;
777
+ return false;
778
+ }
779
+ function* walkSourceFiles(dir) {
780
+ for (const entry of readdirSync2(dir).sort()) {
781
+ if (entry === "node_modules" || entry === "dist") continue;
782
+ const p = join3(dir, entry);
783
+ const stat = statSync2(p);
784
+ if (stat.isDirectory()) {
785
+ yield* walkSourceFiles(p);
786
+ } else if (isCandidateSourceFile(entry)) {
787
+ yield p;
788
+ }
789
+ }
790
+ }
791
+ function isCandidateSourceFile(name) {
792
+ if (!name.endsWith(".ts")) return false;
793
+ if (name.endsWith(".api.ts")) return false;
794
+ if (name.endsWith(".spec.ts")) return false;
795
+ if (name.endsWith(".test.ts")) return false;
796
+ if (name.endsWith(".id.ts")) return false;
797
+ if (name.endsWith(".d.ts")) return false;
798
+ return true;
799
+ }
800
+ function safeIsDirectory2(p) {
801
+ try {
802
+ return statSync2(p).isDirectory();
803
+ } catch {
804
+ return false;
805
+ }
806
+ }
807
+
808
+ // packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/assemble-models.ts
809
+ var MAX_NESTED_DEPTH = 8;
810
+ var LONG_NAME_RE = /^[a-z][a-zA-Z0-9]*$/;
811
+ var ENUM_GENERIC_RE = /firestoreEnum<(\w+)>|optionalFirestoreEnum<(\w+)>/;
812
+ function assembleModels(input) {
813
+ const registries = buildGlobalRegistries(input.extractions);
814
+ const accumulator = { seen: /* @__PURE__ */ new Set(), entries: [] };
815
+ for (const source of input.extractions) {
816
+ appendEntriesFromSource(source, registries, accumulator);
817
+ }
818
+ accumulator.entries.sort((a, b) => a.modelType.localeCompare(b.modelType));
819
+ return accumulator.entries;
820
+ }
821
+ function buildGlobalRegistries(extractions) {
822
+ const converterRegistry = /* @__PURE__ */ new Map();
823
+ const interfaceRegistry = /* @__PURE__ */ new Map();
824
+ const groupByModelName = /* @__PURE__ */ new Map();
825
+ for (const { extraction } of extractions) {
826
+ for (const converter of extraction.converters) {
827
+ if (converter.converterConst && !converterRegistry.has(converter.converterConst)) {
828
+ converterRegistry.set(converter.converterConst, converter);
829
+ }
830
+ }
831
+ for (const iface of extraction.interfaces) {
832
+ if (!interfaceRegistry.has(iface.name)) interfaceRegistry.set(iface.name, iface);
833
+ }
834
+ for (const group of extraction.modelGroups) {
835
+ for (const modelName of group.modelNames) {
836
+ if (!groupByModelName.has(modelName)) groupByModelName.set(modelName, group.name);
837
+ }
838
+ }
839
+ }
840
+ return { converterRegistry, interfaceRegistry, groupByModelName };
841
+ }
842
+ function appendEntriesFromSource(source, registries, accumulator) {
843
+ const enumNames = new Set(source.extraction.enums.map((e) => e.name));
844
+ for (const identity of source.extraction.identities) {
845
+ if (accumulator.seen.has(identity.identityConst)) continue;
846
+ const entry = buildEntryForIdentity({ identity, source, registries, enumNames });
847
+ if (entry) {
848
+ accumulator.seen.add(identity.identityConst);
849
+ accumulator.entries.push(entry);
850
+ }
851
+ }
852
+ }
853
+ function buildEntryForIdentity(input) {
854
+ const { identity, source, registries, enumNames } = input;
855
+ if (identity.collectionPrefix === void 0) return void 0;
856
+ const modelName = capitalize(identity.modelType);
857
+ const iface = registries.interfaceRegistry.get(modelName);
858
+ if (!iface?.hasDbxModelTag) return void 0;
859
+ const converter = findConverterForInterface(source.extraction, modelName) ?? findConverterFromRegistry(registries.converterRegistry, modelName);
860
+ if (!converter) return void 0;
861
+ const fields = buildFields({
862
+ converter,
863
+ iface,
864
+ interfaceRegistry: registries.interfaceRegistry,
865
+ converterRegistry: registries.converterRegistry,
866
+ enumNames,
867
+ depth: 0,
868
+ visitedConverters: /* @__PURE__ */ new Set()
869
+ });
870
+ const modelGroup = registries.groupByModelName.get(modelName);
871
+ return {
872
+ modelType: identity.modelType,
873
+ modelName,
874
+ ...modelGroup ? { modelGroup } : {},
875
+ identityConst: identity.identityConst,
876
+ collectionPrefix: identity.collectionPrefix,
877
+ ...identity.parentIdentityConst ? { parentIdentityConst: identity.parentIdentityConst } : {},
878
+ ...iface.description ? { description: iface.description } : {},
879
+ sourcePackage: source.sourcePackage,
880
+ sourceFile: source.sourceFile,
881
+ fields
882
+ };
883
+ }
884
+ function findConverterForInterface(extraction, interfaceName) {
885
+ return extraction.converters.find((c) => c.interfaceName === interfaceName);
886
+ }
887
+ function findConverterFromRegistry(registry, interfaceName) {
888
+ let result;
889
+ for (const converter of registry.values()) {
890
+ if (converter.interfaceName === interfaceName) {
891
+ result = converter;
892
+ break;
893
+ }
894
+ }
895
+ return result;
896
+ }
897
+ function buildFields(input) {
898
+ const out = [];
899
+ const propByName = /* @__PURE__ */ new Map();
900
+ if (input.iface) {
901
+ for (const prop of input.iface.props) propByName.set(prop.name, prop);
902
+ for (const ancestor of collectAncestors(input.iface, input.interfaceRegistry)) {
903
+ for (const prop of ancestor.props) {
904
+ if (!propByName.has(prop.name)) propByName.set(prop.name, prop);
905
+ }
906
+ }
907
+ }
908
+ for (const field of input.converter.fields) {
909
+ out.push(buildField({ ...input, field, propByName }));
910
+ }
911
+ return out;
912
+ }
913
+ function buildField(input) {
914
+ const { field, propByName } = input;
915
+ const prop = propByName.get(field.key);
916
+ const enumRef = resolveEnumRef(field.converter, prop?.tsType, input.enumNames);
917
+ const optional = prop?.optional ?? field.converter.startsWith("optionalFirestore");
918
+ const longName = resolveLongName(field.key, prop?.longName);
919
+ const nested = resolveNestedFields(input);
920
+ const out = {
921
+ name: field.key,
922
+ longName,
923
+ converter: field.converter,
924
+ ...prop?.tsType ? { tsType: prop.tsType } : {},
925
+ optional,
926
+ ...prop?.description ? { description: prop.description } : {},
927
+ ...enumRef ? { enumRef } : {},
928
+ ...prop?.syncFlag ? { syncFlag: prop.syncFlag } : {},
929
+ ...nested ? { nestedFields: nested.fields, nestedIsArray: nested.isArray } : {}
930
+ };
931
+ return out;
932
+ }
933
+ function resolveNestedFields(input) {
934
+ const { field } = input;
935
+ if (input.depth >= MAX_NESTED_DEPTH) return void 0;
936
+ let nestedConverter;
937
+ if (field.nestedConverterInline) {
938
+ nestedConverter = field.nestedConverterInline;
939
+ } else if (field.nestedConverterRef) {
940
+ if (input.visitedConverters.has(field.nestedConverterRef)) return void 0;
941
+ nestedConverter = input.converterRegistry.get(field.nestedConverterRef);
942
+ }
943
+ if (!nestedConverter) return void 0;
944
+ const nextVisited = new Set(input.visitedConverters);
945
+ if (nestedConverter.converterConst) nextVisited.add(nestedConverter.converterConst);
946
+ const nestedIface = nestedConverter.interfaceName ? input.interfaceRegistry.get(nestedConverter.interfaceName) : void 0;
947
+ const fields = buildFields({
948
+ converter: nestedConverter,
949
+ iface: nestedIface,
950
+ interfaceRegistry: input.interfaceRegistry,
951
+ converterRegistry: input.converterRegistry,
952
+ enumNames: input.enumNames,
953
+ depth: input.depth + 1,
954
+ visitedConverters: nextVisited
955
+ });
956
+ return { fields, isArray: field.nestedIsArray ?? false };
957
+ }
958
+ function collectAncestors(iface, registry) {
959
+ const out = [];
960
+ const visited = /* @__PURE__ */ new Set([iface.name]);
961
+ const stack = [iface];
962
+ while (stack.length > 0) {
963
+ const current = stack.pop();
964
+ for (const parentName of current.extendsNames) {
965
+ if (visited.has(parentName)) continue;
966
+ visited.add(parentName);
967
+ const parent = registry.get(parentName);
968
+ if (parent) {
969
+ out.push(parent);
970
+ stack.push(parent);
971
+ }
972
+ }
973
+ }
974
+ return out;
975
+ }
976
+ function resolveLongName(fieldName, propLongName) {
977
+ let result;
978
+ if (propLongName && LONG_NAME_RE.test(propLongName)) {
979
+ result = propLongName;
980
+ } else {
981
+ result = fieldName;
982
+ }
983
+ return result;
984
+ }
985
+ function resolveEnumRef(converter, tsType, enumNames) {
986
+ let result;
987
+ if (tsType) {
988
+ for (const name of enumNames) {
989
+ const re = new RegExp(String.raw`\b${name}\b`);
990
+ if (re.test(tsType)) {
991
+ result = name;
992
+ break;
993
+ }
994
+ }
995
+ }
996
+ if (!result) {
997
+ const m = ENUM_GENERIC_RE.exec(converter);
998
+ if (m) {
999
+ const name = m[1] ?? m[2];
1000
+ if (enumNames.has(name)) result = name;
1001
+ }
1002
+ }
1003
+ return result;
1004
+ }
1005
+ function capitalize(s) {
1006
+ return s.length > 0 ? s[0].toUpperCase() + s.slice(1) : s;
1007
+ }
1008
+
427
1009
  // 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";
1010
+ import { existsSync as existsSync2, readdirSync as readdirSync3, readFileSync as readFileSync5, statSync as statSync3 } from "node:fs";
1011
+ import { dirname as dirname2, isAbsolute as isAbsolute2, join as join4, resolve as resolve2 } from "node:path";
430
1012
  function deriveValidatorName(paramsTypeName) {
431
1013
  if (!paramsTypeName) return "";
432
1014
  return paramsTypeName.charAt(0).toLowerCase() + paramsTypeName.slice(1) + "Type";
@@ -438,7 +1020,7 @@ function isExportedFromPackage(input) {
438
1020
  return findIdentifierInBarrelChain(indexPath, identifier, /* @__PURE__ */ new Set());
439
1021
  }
440
1022
  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")];
1023
+ const candidates = [join4(packageRoot, "src", "index.ts"), join4(packageRoot, "src", "index.d.ts"), join4(packageRoot, "index.d.ts"), join4(packageRoot, "index.ts")];
442
1024
  return candidates.find((candidate) => existsSync2(candidate));
443
1025
  }
444
1026
  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 +1029,7 @@ function findIdentifierInBarrelChain(filePath, identifier, visited) {
447
1029
  visited.add(filePath);
448
1030
  let text;
449
1031
  try {
450
- text = readFileSync4(filePath, "utf8");
1032
+ text = readFileSync5(filePath, "utf8");
451
1033
  } catch {
452
1034
  return false;
453
1035
  }
@@ -491,12 +1073,12 @@ function hasTsModuleExtension(value) {
491
1073
  }
492
1074
  function resolveExistingTsPath(probe) {
493
1075
  if (!existsSync2(probe)) return void 0;
494
- const stat = statSync2(probe);
1076
+ const stat = statSync3(probe);
495
1077
  if (stat.isFile()) return probe;
496
1078
  if (!stat.isDirectory()) return void 0;
497
- const sourceIndex = join3(probe, "index.ts");
1079
+ const sourceIndex = join4(probe, "index.ts");
498
1080
  if (existsSync2(sourceIndex)) return sourceIndex;
499
- const declarationIndex = join3(probe, "index.d.ts");
1081
+ const declarationIndex = join4(probe, "index.d.ts");
500
1082
  return existsSync2(declarationIndex) ? declarationIndex : void 0;
501
1083
  }
502
1084
  function escapeRegExp(value) {
@@ -506,7 +1088,7 @@ function escapeRegExp(value) {
506
1088
  // packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/emit.ts
507
1089
  import { format, resolveConfig } from "prettier";
508
1090
  async function renderManifest(input) {
509
- const { outputFile, entries, projectName, namespace } = input;
1091
+ const { outputFile, entries, projectName, namespace, modelEntries, modelNamespace } = input;
510
1092
  const importsByPackage = /* @__PURE__ */ new Map();
511
1093
  for (const entry of entries) {
512
1094
  if (!entry.packageName || !entry.validatorName) continue;
@@ -519,16 +1101,24 @@ async function renderManifest(input) {
519
1101
  return `import { ${sortedNames} } from '${pkg}';`;
520
1102
  });
521
1103
  const entryLines = entries.map((e) => renderEntry(e));
1104
+ const emitModels = Boolean(modelEntries && modelEntries.length > 0 && modelNamespace);
1105
+ const dbxCliTypeImports = emitModels ? `import { type CliApiManifest, type CliModelManifest } from '@dereekb/dbx-cli';` : `import { type CliApiManifest } from '@dereekb/dbx-cli';`;
1106
+ const modelSection = emitModels ? `
1107
+
1108
+ export const ${modelNamespace}: CliModelManifest = [
1109
+ ${(modelEntries ?? []).map((m) => renderModelEntry(m)).join(",\n")}
1110
+ ];
1111
+ ` : "";
522
1112
  const source = `/* eslint-disable @nx/enforce-module-boundaries */
523
1113
  // AUTO-GENERATED \u2014 DO NOT EDIT.
524
1114
  // Run \`pnpm nx run ${projectName}:generate-api-manifest\` to refresh.
525
1115
 
526
1116
  ${importLines.join("\n")}
527
- import { type CliApiManifest } from '@dereekb/dbx-cli';
1117
+ ${dbxCliTypeImports}
528
1118
 
529
1119
  export const ${namespace}: CliApiManifest = [
530
1120
  ${entryLines.join(",\n")}
531
- ];
1121
+ ];${modelSection}
532
1122
  `;
533
1123
  return formatWithPrettier(source, outputFile);
534
1124
  }
@@ -562,6 +1152,41 @@ async function formatWithPrettier(source, outputFile) {
562
1152
  const config = await resolveConfig(outputFile);
563
1153
  return format(source, { ...config, filepath: outputFile });
564
1154
  }
1155
+ function renderModelEntry(entry) {
1156
+ const fields = [
1157
+ `modelType: ${JSON.stringify(entry.modelType)}`,
1158
+ `modelName: ${JSON.stringify(entry.modelName)}`,
1159
+ entry.modelGroup ? `modelGroup: ${JSON.stringify(entry.modelGroup)}` : void 0,
1160
+ `identityConst: ${JSON.stringify(entry.identityConst)}`,
1161
+ `collectionPrefix: ${JSON.stringify(entry.collectionPrefix)}`,
1162
+ entry.parentIdentityConst ? `parentIdentityConst: ${JSON.stringify(entry.parentIdentityConst)}` : void 0,
1163
+ entry.description ? `description: ${JSON.stringify(entry.description)}` : void 0,
1164
+ `sourcePackage: ${JSON.stringify(entry.sourcePackage)}`,
1165
+ `sourceFile: ${JSON.stringify(entry.sourceFile)}`,
1166
+ `fields: ${renderModelFields(entry.fields)}`
1167
+ ];
1168
+ return ` { ${fields.filter((v) => Boolean(v)).join(", ")} }`;
1169
+ }
1170
+ function renderModelFields(fields) {
1171
+ if (fields.length === 0) return "[]";
1172
+ const items = fields.map((field) => renderModelField(field));
1173
+ return `[${items.join(", ")}]`;
1174
+ }
1175
+ function renderModelField(field) {
1176
+ const parts = [
1177
+ `name: ${JSON.stringify(field.name)}`,
1178
+ `longName: ${JSON.stringify(field.longName)}`,
1179
+ `converter: ${JSON.stringify(field.converter)}`,
1180
+ field.tsType ? `tsType: ${JSON.stringify(field.tsType)}` : void 0,
1181
+ `optional: ${field.optional ? "true" : "false"}`,
1182
+ field.description ? `description: ${JSON.stringify(field.description)}` : void 0,
1183
+ field.enumRef ? `enumRef: ${JSON.stringify(field.enumRef)}` : void 0,
1184
+ field.syncFlag ? `syncFlag: ${JSON.stringify(field.syncFlag)}` : void 0,
1185
+ field.nestedFields ? `nestedFields: ${renderModelFields(field.nestedFields)}` : void 0,
1186
+ field.nestedFields ? `nestedIsArray: ${field.nestedIsArray ? "true" : "false"}` : void 0
1187
+ ];
1188
+ return `{ ${parts.filter((v) => Boolean(v)).join(", ")} }`;
1189
+ }
565
1190
 
566
1191
  // packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/main.ts
567
1192
  var WORKSPACE_ROOT = process.cwd();
@@ -586,6 +1211,8 @@ async function main() {
586
1211
  const packageCache = /* @__PURE__ */ new Map();
587
1212
  const apiFilesCache = /* @__PURE__ */ new Map();
588
1213
  const collected = [];
1214
+ const modelSources = [];
1215
+ const modelPackagesScanned = /* @__PURE__ */ new Set();
589
1216
  let missingValidators = 0;
590
1217
  let skippedGroups = 0;
591
1218
  for (const group of groups) {
@@ -597,6 +1224,16 @@ async function main() {
597
1224
  }
598
1225
  if (!packageCache.has(pkg.packageRoot)) packageCache.set(pkg.packageRoot, pkg);
599
1226
  if (!apiFilesCache.has(pkg.packageRoot)) apiFilesCache.set(pkg.packageRoot, findApiFiles(pkg.packageRoot));
1227
+ if (flags.emitModels && !modelPackagesScanned.has(pkg.packageRoot)) {
1228
+ modelPackagesScanned.add(pkg.packageRoot);
1229
+ for (const match2 of findModelFiles(pkg.packageRoot)) {
1230
+ modelSources.push({
1231
+ sourcePackage: pkg.packageName,
1232
+ sourceFile: relPath(WORKSPACE_ROOT, match2.filePath),
1233
+ extraction: match2.extraction
1234
+ });
1235
+ }
1236
+ }
600
1237
  const apiFiles = apiFilesCache.get(pkg.packageRoot) ?? [];
601
1238
  const match = apiFiles.find((f) => f.className === group.className);
602
1239
  if (!match) {
@@ -631,16 +1268,19 @@ async function main() {
631
1268
  }
632
1269
  }
633
1270
  collected.sort(compareEntries);
1271
+ const modelEntries = flags.emitModels ? assembleModels({ extractions: modelSources }) : [];
1272
+ const filteredModelEntries = flags.only ? modelEntries.filter((m) => flags.only?.has(m.modelType)) : modelEntries;
634
1273
  ensureOutputDir(outputDir);
635
- const formatted = await renderManifest({ outputFile, entries: collected, projectName, namespace });
636
- if (existsSync3(outputFile) && readFileSync5(outputFile, "utf8") === formatted) {
1274
+ const formatted = await renderManifest({ outputFile, entries: collected, projectName, namespace, modelEntries: filteredModelEntries, modelNamespace: deriveModelNamespace(flags.project) });
1275
+ if (existsSync3(outputFile) && readFileSync6(outputFile, "utf8") === formatted) {
637
1276
  console.log(`[unchanged] ${relative2(WORKSPACE_ROOT, outputFile)}`);
638
1277
  } else {
639
1278
  writeFileSync(outputFile, formatted);
640
1279
  console.log(`[wrote] ${relative2(WORKSPACE_ROOT, outputFile)}`);
641
1280
  }
642
1281
  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`);
1282
+ const modelSummary = flags.emitModels ? ` \xB7 ${filteredModelEntries.length} models` : "";
1283
+ console.log(`Summary: ${groupCount} groups \xB7 ${collected.length} entries \xB7 ${collected.length - missingValidators} validators bound \xB7 ${missingValidators} missing \xB7 ${skippedGroups} skipped${modelSummary}`);
644
1284
  if (flags.strict && missingValidators > 0) {
645
1285
  console.error(`[strict] ${missingValidators} validator(s) missing \u2014 failing build.`);
646
1286
  process.exit(1);
@@ -661,15 +1301,22 @@ function deriveNamespace(projectName) {
661
1301
  const base = (projectName ?? "cli").replaceAll(/[^a-zA-Z0-9]+/g, "_");
662
1302
  return `${base.toUpperCase()}_API_MANIFEST`;
663
1303
  }
1304
+ function deriveModelNamespace(projectName) {
1305
+ const base = (projectName ?? "cli").replaceAll(/[^a-zA-Z0-9]+/g, "_");
1306
+ return `${base.toUpperCase()}_MODEL_MANIFEST`;
1307
+ }
664
1308
  function parseFlags(argv) {
665
1309
  let only;
666
1310
  let strict = false;
667
1311
  let functionsConfig;
668
1312
  let output;
669
1313
  let project;
1314
+ let emitModels = false;
670
1315
  for (const arg of argv) {
671
1316
  if (arg === "--strict") {
672
1317
  strict = true;
1318
+ } else if (arg === "--emit-models") {
1319
+ emitModels = true;
673
1320
  } else if (arg.startsWith("--only=")) {
674
1321
  const list = arg.slice("--only=".length).split(",").map((s) => s.trim()).filter(Boolean);
675
1322
  if (list.length > 0) only = new Set(list);
@@ -681,7 +1328,7 @@ function parseFlags(argv) {
681
1328
  project = arg.slice("--project=".length);
682
1329
  }
683
1330
  }
684
- return { only, strict, functionsConfig, output, project };
1331
+ return { only, strict, functionsConfig, output, project, emitModels };
685
1332
  }
686
1333
  function printUsageAndExit() {
687
1334
  console.error(String.raw`generate-api-manifest
@@ -700,7 +1347,10 @@ Required flags:
700
1347
  Optional:
701
1348
  --project=<name> Project name to show in the regenerate banner.
702
1349
  --only=<csv> Filter to listed model names.
703
- --strict Fail when any validator binding is missing.`);
1350
+ --strict Fail when any validator binding is missing.
1351
+ --emit-models Opt in to emitting <NAMESPACE>_MODEL_MANIFEST. Off by default —
1352
+ pair with the runtime \`modelManifest\` option on \`runCli\` to
1353
+ enable the built-in \`model-info\` command.`);
704
1354
  process.exit(1);
705
1355
  }
706
1356
  try {