@agent-scope/manifest 1.19.0 → 1.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -30,6 +30,32 @@ interface PropDescriptor {
30
30
  required: boolean;
31
31
  /** Raw TypeScript type string as written in source. */
32
32
  rawType: string;
33
+ /**
34
+ * Where this prop is defined.
35
+ *
36
+ * `"own"` — declared in the project's source files (component-specific prop).
37
+ * `"inherited"` — inherited from React/HTML/third-party types in node_modules
38
+ * (e.g. `onClick`, `className` from `React.ComponentProps<'button'>`).
39
+ *
40
+ * Omitted when the source cannot be determined (legacy extraction paths).
41
+ */
42
+ source?: "own" | "inherited";
43
+ /**
44
+ * For inherited props, which category the prop belongs to — determined by
45
+ * the declaring interface in the type definitions.
46
+ *
47
+ * `"react"` — React-specific (Attributes, RefAttributes, DOMAttributes — includes
48
+ * ref, key, children, synthetic event handlers like onClick).
49
+ * `"html"` — HTML element attributes (HTMLAttributes, ButtonHTMLAttributes, etc. —
50
+ * includes className, style, id, disabled, title).
51
+ * `"aria"` — Accessibility attributes (AriaAttributes — aria-label, aria-hidden, etc.).
52
+ *
53
+ * When the prop comes from a third-party package (not React), this is the
54
+ * package name (e.g. `"class-variance-authority"`).
55
+ *
56
+ * Omitted for own props or when the group cannot be determined.
57
+ */
58
+ sourceGroup?: string;
33
59
  }
34
60
  /**
35
61
  * How the component is exported from its module.
@@ -132,6 +158,32 @@ interface ComponentDescriptor {
132
158
  * `null` when no scope file is found next to the component source file.
133
159
  */
134
160
  scopeFile: ScopeFileMeta | null;
161
+ /**
162
+ * The collection this component belongs to (e.g. "Forms", "Navigation").
163
+ * `undefined` means ungrouped.
164
+ *
165
+ * Resolution precedence (highest → lowest):
166
+ * 1. `@collection <name>` TSDoc tag on the component declaration
167
+ * 2. `export const collection = "name"` in a co-located `.scope.ts` file
168
+ * 3. First matching pattern in `config.collections[].patterns`
169
+ */
170
+ collection?: string;
171
+ /**
172
+ * Whether this component is internal (not intended for external consumers).
173
+ * Defaults to `false`.
174
+ *
175
+ * Set to `true` when:
176
+ * - `@internal` TSDoc tag is present on the component declaration, OR
177
+ * - The component's `filePath` or `displayName` matches any pattern in
178
+ * `config.internalPatterns`.
179
+ */
180
+ internal: boolean;
181
+ /**
182
+ * Search keywords extracted from `@keywords` TSDoc tag.
183
+ * Comma-separated values are split into individual strings.
184
+ * Empty when no `@keywords` tag is present.
185
+ */
186
+ keywords: string[];
135
187
  }
136
188
  /**
137
189
  * Metadata extracted from a `.scope.ts(x)` / `.scope.js(x)` file that lives
@@ -151,6 +203,17 @@ interface ScopeFileMeta {
151
203
  */
152
204
  hasWrapper: boolean;
153
205
  }
206
+ /**
207
+ * Configuration for a named component collection.
208
+ */
209
+ interface CollectionConfig {
210
+ /** Human-readable collection name (e.g. "Forms", "Navigation"). */
211
+ name: string;
212
+ /** Optional description of the collection. */
213
+ description?: string;
214
+ /** Glob patterns matching component file paths that belong to this collection. */
215
+ patterns: string[];
216
+ }
154
217
  /**
155
218
  * A node in the composition tree.
156
219
  */
@@ -172,6 +235,11 @@ interface Manifest {
172
235
  components: Record<string, ComponentDescriptor>;
173
236
  /** Composition tree (child/parent relationships). */
174
237
  tree: Record<string, TreeNode>;
238
+ /**
239
+ * All defined collections, echoing `config.collections`.
240
+ * Empty array when no collections are configured.
241
+ */
242
+ collections: CollectionConfig[];
175
243
  }
176
244
  /**
177
245
  * Configuration for `generateManifest`.
@@ -199,6 +267,24 @@ interface ManifestConfig {
199
267
  * Defaults to `<rootDir>/tsconfig.json`.
200
268
  */
201
269
  tsConfigFilePath?: string;
270
+ /**
271
+ * Named component collections. Each entry maps a set of glob patterns to a
272
+ * collection name. Components whose `filePath` matches a pattern are assigned
273
+ * to that collection (config-level precedence — lower than TSDoc / .scope.ts).
274
+ */
275
+ collections?: CollectionConfig[];
276
+ /**
277
+ * Glob patterns for components that should be flagged as internal.
278
+ * Matched against both `filePath` and `displayName`.
279
+ * Config-level precedence — lower than an explicit `@internal` TSDoc tag.
280
+ */
281
+ internalPatterns?: string[];
282
+ /**
283
+ * Glob patterns identifying icon components. Matched against both
284
+ * `filePath` and `displayName`. Icon-matching components are
285
+ * automatically flagged as internal (hidden from sidebar/index).
286
+ */
287
+ iconPatterns?: string[];
202
288
  }
203
289
 
204
290
  /**
@@ -206,4 +292,4 @@ interface ManifestConfig {
206
292
  */
207
293
  declare function generateManifest(config: ManifestConfig): Manifest;
208
294
 
209
- export { type ComplexityClass, type ComponentDescriptor, type ExportType, type Manifest, type ManifestConfig, type PropDescriptor, type PropKind, type ScopeFileMeta, type SideEffects, type TreeNode, generateManifest };
295
+ export { type CollectionConfig, type ComplexityClass, type ComponentDescriptor, type ExportType, type Manifest, type ManifestConfig, type PropDescriptor, type PropKind, type ScopeFileMeta, type SideEffects, type TreeNode, generateManifest };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync } from 'fs';
1
+ import { realpathSync, existsSync, readFileSync } from 'fs';
2
2
  import { join, relative } from 'path';
3
3
  import { Project, Node } from 'ts-morph';
4
4
 
@@ -333,7 +333,7 @@ function expandUnionValues(type) {
333
333
  }
334
334
  return values.length > 0 ? values : void 0;
335
335
  }
336
- function buildPropDescriptor(type, required, defaultValue) {
336
+ function buildPropDescriptor(type, required, defaultValue, source, sourceGroup) {
337
337
  const kind = resolvePropKind(type);
338
338
  const desc = {
339
339
  type: kind,
@@ -348,6 +348,12 @@ function buildPropDescriptor(type, required, defaultValue) {
348
348
  desc.default = defaultValue;
349
349
  desc.required = false;
350
350
  }
351
+ if (source !== void 0) {
352
+ desc.source = source;
353
+ }
354
+ if (sourceGroup !== void 0) {
355
+ desc.sourceGroup = sourceGroup;
356
+ }
351
357
  return desc;
352
358
  }
353
359
  function extractPropsFromType(typeName, sourceFile, defaultValues = {}) {
@@ -359,25 +365,47 @@ function extractPropsFromType(typeName, sourceFile, defaultValues = {}) {
359
365
  if (name.startsWith("[")) continue;
360
366
  const type = prop.getType();
361
367
  const required = !prop.hasQuestionToken();
362
- props[name] = buildPropDescriptor(type, required, defaultValues[name]);
368
+ props[name] = buildPropDescriptor(type, required, defaultValues[name], "own");
363
369
  }
364
370
  return props;
365
371
  }
366
372
  const typeAlias = sourceFile.getTypeAlias(typeName);
367
373
  if (typeAlias) {
368
374
  const aliasType = typeAlias.getType();
369
- if (aliasType.isObject()) {
375
+ if (aliasType.isObject() || aliasType.isIntersection()) {
370
376
  for (const prop of aliasType.getProperties()) {
371
377
  const name = prop.getName();
372
378
  if (name.startsWith("[")) continue;
373
379
  const decls = prop.getDeclarations();
374
- const required = decls.length === 0 || !prop.getDeclarations().some((d) => Node.isPropertySignature(d) && d.hasQuestionToken());
380
+ let required = decls.length === 0 || !prop.getDeclarations().some((d) => Node.isPropertySignature(d) && d.hasQuestionToken());
375
381
  const valType = prop.getTypeAtLocation(sourceFile);
376
- props[name] = buildPropDescriptor(valType, required, defaultValues[name]);
382
+ if (required && typeIncludesUndefined(valType)) {
383
+ required = false;
384
+ }
385
+ const { source, sourceGroup } = classifyPropSource(prop);
386
+ props[name] = buildPropDescriptor(
387
+ valType,
388
+ required,
389
+ defaultValues[name],
390
+ source,
391
+ sourceGroup
392
+ );
377
393
  }
378
394
  }
379
395
  return props;
380
396
  }
397
+ for (const importDecl of sourceFile.getImportDeclarations()) {
398
+ const match = importDecl.getNamedImports().find((ni) => {
399
+ const localName = ni.getAliasNode()?.getText() ?? ni.getName();
400
+ return localName === typeName;
401
+ });
402
+ if (!match) continue;
403
+ const importedFile = importDecl.getModuleSpecifierSourceFile();
404
+ if (importedFile) {
405
+ const originalName = match.getName();
406
+ return extractPropsFromType(originalName, importedFile, defaultValues);
407
+ }
408
+ }
381
409
  return props;
382
410
  }
383
411
  function inferPropsTypeName(params) {
@@ -474,6 +502,78 @@ function extractNameFromWrappedCall(node) {
474
502
  }
475
503
  return void 0;
476
504
  }
505
+ function extractForwardRefPropsTypeNode(node) {
506
+ if (!Node.isCallExpression(node)) return void 0;
507
+ const expr = node.getExpression();
508
+ const name = expr.getText();
509
+ if (name === "React.forwardRef" || name === "forwardRef") {
510
+ const typeArgs = node.getTypeArguments();
511
+ if (typeArgs.length >= 2 && typeArgs[1]) {
512
+ return typeArgs[1];
513
+ }
514
+ return void 0;
515
+ }
516
+ const args = node.getArguments();
517
+ if (args[0] && Node.isCallExpression(args[0])) {
518
+ return extractForwardRefPropsTypeNode(args[0]);
519
+ }
520
+ return void 0;
521
+ }
522
+ function typeIncludesUndefined(type) {
523
+ if (type.isUndefined()) return true;
524
+ if (type.isUnion()) {
525
+ return type.getUnionTypes().some((t) => t.isUndefined());
526
+ }
527
+ return false;
528
+ }
529
+ var REACT_INTERFACES = /* @__PURE__ */ new Set([
530
+ "Attributes",
531
+ "RefAttributes",
532
+ "ClassAttributes",
533
+ "DOMAttributes"
534
+ ]);
535
+ function classifyPropSource(prop) {
536
+ const decls = prop.getDeclarations();
537
+ if (decls.length === 0) return { source: "inherited" };
538
+ const allInNodeModules = decls.every(
539
+ (d) => d.getSourceFile().getFilePath().includes("/node_modules/")
540
+ );
541
+ if (!allInNodeModules) return { source: "own" };
542
+ for (const d of decls) {
543
+ const parent = d.getParent();
544
+ if (!Node.isInterfaceDeclaration(parent)) continue;
545
+ const ifaceName = parent.getName();
546
+ if (ifaceName === "AriaAttributes") return { source: "inherited", sourceGroup: "aria" };
547
+ if (ifaceName.endsWith("HTMLAttributes")) return { source: "inherited", sourceGroup: "html" };
548
+ if (REACT_INTERFACES.has(ifaceName)) return { source: "inherited", sourceGroup: "react" };
549
+ }
550
+ const filePath = decls[0]?.getSourceFile().getFilePath() ?? "";
551
+ const pkgMatch = filePath.match(/node_modules\/((?:@[^/]+\/)?[^/]+)/);
552
+ if (pkgMatch?.[1]) {
553
+ const pkg = pkgMatch[1];
554
+ if (pkg === "@types/react" || pkg === "react")
555
+ return { source: "inherited", sourceGroup: "react" };
556
+ return { source: "inherited", sourceGroup: pkg };
557
+ }
558
+ return { source: "inherited" };
559
+ }
560
+ function extractPropsFromResolvedType(resolvedType, sourceFile, defaultValues = {}) {
561
+ const props = {};
562
+ if (!resolvedType.isObject() && !resolvedType.isIntersection()) return props;
563
+ for (const prop of resolvedType.getProperties()) {
564
+ const name = prop.getName();
565
+ if (name.startsWith("[")) continue;
566
+ const decls = prop.getDeclarations();
567
+ let required = decls.length === 0 || !decls.some((d) => Node.isPropertySignature(d) && d.hasQuestionToken());
568
+ const valType = prop.getTypeAtLocation(sourceFile);
569
+ if (required && typeIncludesUndefined(valType)) {
570
+ required = false;
571
+ }
572
+ const { source, sourceGroup } = classifyPropSource(prop);
573
+ props[name] = buildPropDescriptor(valType, required, defaultValues[name], source, sourceGroup);
574
+ }
575
+ return props;
576
+ }
477
577
  function nodeReturnsJsx(node) {
478
578
  let found = false;
479
579
  function visit(n) {
@@ -487,6 +587,62 @@ function nodeReturnsJsx(node) {
487
587
  visit(node);
488
588
  return found;
489
589
  }
590
+ function matchGlob(pattern, value) {
591
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
592
+ const regexStr = escaped.replace(/\*\*/g, "\xA7GLOBSTAR\xA7").replace(/\*/g, "[^/]*").replace(/§GLOBSTAR§/g, ".*");
593
+ const regex = new RegExp(`^${regexStr}$`, "i");
594
+ return regex.test(value);
595
+ }
596
+ function extractTsDocTags(declNode) {
597
+ let collection;
598
+ let internal = false;
599
+ const keywords = [];
600
+ const nodeWithDocs = declNode;
601
+ if (typeof nodeWithDocs.getJsDocs !== "function") {
602
+ return { collection, internal, keywords };
603
+ }
604
+ const jsDocs = nodeWithDocs.getJsDocs();
605
+ for (const jsDoc of jsDocs) {
606
+ for (const tag of jsDoc.getTags()) {
607
+ const tagName = tag.getTagName();
608
+ if (tagName === "collection") {
609
+ const comment = tag.getComment();
610
+ if (comment && typeof comment === "string") {
611
+ collection = comment.trim();
612
+ }
613
+ } else if (tagName === "internal") {
614
+ internal = true;
615
+ } else if (tagName === "keywords") {
616
+ const comment = tag.getComment();
617
+ if (comment && typeof comment === "string") {
618
+ for (const kw of comment.split(",")) {
619
+ const trimmed = kw.trim();
620
+ if (trimmed) keywords.push(trimmed);
621
+ }
622
+ }
623
+ }
624
+ }
625
+ }
626
+ return { collection, internal, keywords };
627
+ }
628
+ function readCollectionFromScopeFile(scopeFilePath, project) {
629
+ let sf = project.getSourceFile(scopeFilePath);
630
+ if (!sf) {
631
+ sf = project.addSourceFileAtPath(scopeFilePath);
632
+ }
633
+ for (const varStmt of sf.getVariableStatements()) {
634
+ if (!varStmt.isExported()) continue;
635
+ for (const varDecl of varStmt.getDeclarations()) {
636
+ if (varDecl.getName() !== "collection") continue;
637
+ const initializer = varDecl.getInitializer();
638
+ if (!initializer) continue;
639
+ if (Node.isStringLiteral(initializer)) {
640
+ return initializer.getLiteralValue();
641
+ }
642
+ }
643
+ }
644
+ return void 0;
645
+ }
490
646
  function processSourceFile(sourceFile, rootDir, project) {
491
647
  const results = [];
492
648
  const filePath = relative(rootDir, sourceFile.getFilePath());
@@ -547,7 +703,10 @@ function processSourceFile(sourceFile, rootDir, project) {
547
703
  detectedHooks: detectHooks(fn),
548
704
  requiredContexts: detectRequiredContexts(fn, sourceFile, project),
549
705
  sideEffects: detectSideEffects(fn),
550
- scopeFile: null
706
+ scopeFile: null,
707
+ // collection, internal, and keywords will be filled in after all components are collected
708
+ internal: false,
709
+ keywords: []
551
710
  }
552
711
  });
553
712
  }
@@ -585,7 +744,16 @@ function processSourceFile(sourceFile, rootDir, project) {
585
744
  }
586
745
  const propsTypeName = inferPropsTypeName(params);
587
746
  const defaults = extractDefaultsFromDestructuring(params);
588
- const props = propsTypeName ? extractPropsFromType(propsTypeName, sourceFile, defaults) : {};
747
+ let props = {};
748
+ if (propsTypeName) {
749
+ props = extractPropsFromType(propsTypeName, sourceFile, defaults);
750
+ }
751
+ if (Object.keys(props).length === 0 && wrappers.forwardedRef) {
752
+ const propsTypeNode = extractForwardRefPropsTypeNode(initializer);
753
+ if (propsTypeNode) {
754
+ props = extractPropsFromResolvedType(propsTypeNode.getType(), sourceFile, defaults);
755
+ }
756
+ }
589
757
  const composes = collectJsxCompositions(bodyNode);
590
758
  const startLine = varDecl.getStartLineNumber();
591
759
  const endLine = varDecl.getEndLineNumber();
@@ -612,7 +780,9 @@ function processSourceFile(sourceFile, rootDir, project) {
612
780
  detectedHooks: detectHooks(bodyNode),
613
781
  requiredContexts: detectRequiredContexts(bodyNode, sourceFile, project),
614
782
  sideEffects: detectSideEffects(bodyNode),
615
- scopeFile: null
783
+ scopeFile: null,
784
+ internal: false,
785
+ keywords: []
616
786
  }
617
787
  });
618
788
  }
@@ -661,19 +831,22 @@ function processSourceFile(sourceFile, rootDir, project) {
661
831
  detectedHooks: [],
662
832
  requiredContexts: [],
663
833
  sideEffects: detectSideEffects(cls),
664
- scopeFile: null
834
+ scopeFile: null,
835
+ internal: false,
836
+ keywords: []
665
837
  }
666
838
  });
667
839
  }
668
840
  return results;
669
841
  }
670
842
  function generateManifest(config) {
843
+ const normalizedRootDir = realpathSync(config.rootDir);
671
844
  const {
672
- rootDir,
673
845
  include = ["src/**/*.tsx", "src/**/*.ts"],
674
846
  exclude = ["**/node_modules/**", "**/*.test.*", "**/*.spec.*", "**/dist/**", "**/*.d.ts"],
675
- tsConfigFilePath = join(rootDir, "tsconfig.json")
847
+ tsConfigFilePath = join(normalizedRootDir, "tsconfig.json")
676
848
  } = config;
849
+ const rootDir = normalizedRootDir;
677
850
  const project = new Project({
678
851
  tsConfigFilePath,
679
852
  skipAddingFilesFromTsConfig: true
@@ -729,11 +902,78 @@ function generateManifest(config) {
729
902
  desc.scopeFile = scopeMeta;
730
903
  }
731
904
  }
905
+ const configCollections = config.collections ?? [];
906
+ const internalPatterns = config.internalPatterns ?? [];
907
+ for (const [compName, desc] of Object.entries(allComponents)) {
908
+ const absFilePath = desc.filePath.startsWith("/") ? desc.filePath : join(rootDir, desc.filePath);
909
+ const sf = project.getSourceFile(absFilePath);
910
+ let tsdocCollection;
911
+ let tsdocInternal = false;
912
+ let tsdocKeywords = [];
913
+ if (sf) {
914
+ const fn = sf.getFunction(compName);
915
+ if (fn) {
916
+ const tags = extractTsDocTags(fn);
917
+ tsdocCollection = tags.collection;
918
+ tsdocInternal = tags.internal;
919
+ tsdocKeywords = tags.keywords;
920
+ }
921
+ if (tsdocCollection === void 0 && !tsdocInternal && tsdocKeywords.length === 0) {
922
+ const varDecl = sf.getVariableDeclaration(compName);
923
+ if (varDecl) {
924
+ const varStmt = varDecl.getVariableStatement();
925
+ if (varStmt) {
926
+ const tags = extractTsDocTags(varStmt);
927
+ tsdocCollection = tags.collection;
928
+ tsdocInternal = tags.internal;
929
+ tsdocKeywords = tags.keywords;
930
+ }
931
+ }
932
+ }
933
+ if (tsdocCollection === void 0 && !tsdocInternal && tsdocKeywords.length === 0) {
934
+ const cls = sf.getClass(compName);
935
+ if (cls) {
936
+ const tags = extractTsDocTags(cls);
937
+ tsdocCollection = tags.collection;
938
+ tsdocInternal = tags.internal;
939
+ tsdocKeywords = tags.keywords;
940
+ }
941
+ }
942
+ }
943
+ let scopeFileCollection;
944
+ if (desc.scopeFile) {
945
+ scopeFileCollection = readCollectionFromScopeFile(desc.scopeFile.filePath, project);
946
+ }
947
+ let configCollection;
948
+ for (const colConfig of configCollections) {
949
+ if (colConfig.patterns.some((p) => matchGlob(p, desc.filePath))) {
950
+ configCollection = colConfig.name;
951
+ break;
952
+ }
953
+ }
954
+ let configInternal = false;
955
+ if (internalPatterns.length > 0) {
956
+ configInternal = internalPatterns.some(
957
+ (p) => matchGlob(p, desc.filePath) || matchGlob(p, desc.displayName)
958
+ );
959
+ }
960
+ const iconPats = config.iconPatterns ?? [];
961
+ const isIcon = iconPats.length > 0 && iconPats.some((p) => matchGlob(p, desc.filePath) || matchGlob(p, desc.displayName));
962
+ const resolvedCollection = tsdocCollection ?? scopeFileCollection ?? configCollection;
963
+ if (resolvedCollection !== void 0) {
964
+ desc.collection = resolvedCollection;
965
+ }
966
+ desc.internal = tsdocInternal || configInternal || isIcon;
967
+ if (tsdocKeywords.length > 0) {
968
+ desc.keywords = tsdocKeywords;
969
+ }
970
+ }
732
971
  return {
733
972
  version: "0.1",
734
973
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
735
974
  components: allComponents,
736
- tree
975
+ tree,
976
+ collections: configCollections
737
977
  };
738
978
  }
739
979
  var SCOPE_EXTENSIONS = [".scope.tsx", ".scope.ts", ".scope.jsx", ".scope.js"];
@@ -743,7 +983,103 @@ function detectScopeFile(componentFilePath, rootDir) {
743
983
  for (const ext of SCOPE_EXTENSIONS) {
744
984
  const candidate = `${stem}${ext}`;
745
985
  if (existsSync(candidate)) {
746
- return { filePath: candidate, scenarioNames: [], hasWrapper: false };
986
+ return readScopeFileMeta(candidate);
987
+ }
988
+ }
989
+ return null;
990
+ }
991
+ function readScopeFileMeta(filePath) {
992
+ const source = readFileSync(filePath, "utf-8");
993
+ const scenarioNames = extractScenarioNames(source);
994
+ const hasWrapper = /\bexport\s+(?:const|function)\s+wrapper\b/.test(source) || /\bwrapper\s*:/.test(source);
995
+ return { filePath, scenarioNames, hasWrapper };
996
+ }
997
+ function extractScenarioNames(source) {
998
+ const objectSource = extractScenarioObjectLiteral(source, /\bexport\s+const\s+scenarios\s*=\s*\{/) ?? extractScenarioObjectLiteral(source, /\bscenarios\s*:\s*\{/);
999
+ if (objectSource === null) return [];
1000
+ const names = /* @__PURE__ */ new Set();
1001
+ let depth = 0;
1002
+ let quote = null;
1003
+ let escaped = false;
1004
+ let token = "";
1005
+ const flushTokenAsKey = () => {
1006
+ const name = token.trim().replace(/^['"]|['"]$/g, "");
1007
+ if (depth === 0 && name && name !== "scenarios" && name !== "wrapper") {
1008
+ names.add(name);
1009
+ }
1010
+ token = "";
1011
+ };
1012
+ for (let i = 0; i < objectSource.length; i += 1) {
1013
+ const char = objectSource[i];
1014
+ if (quote !== null) {
1015
+ token += char;
1016
+ if (escaped) {
1017
+ escaped = false;
1018
+ } else if (char === "\\") {
1019
+ escaped = true;
1020
+ } else if (char === quote) {
1021
+ quote = null;
1022
+ }
1023
+ continue;
1024
+ }
1025
+ if (char === '"' || char === "'" || char === "`") {
1026
+ quote = char;
1027
+ token += char;
1028
+ continue;
1029
+ }
1030
+ if (char === "{") {
1031
+ depth += 1;
1032
+ token = "";
1033
+ continue;
1034
+ }
1035
+ if (char === "}") {
1036
+ depth = Math.max(0, depth - 1);
1037
+ token = "";
1038
+ continue;
1039
+ }
1040
+ if (depth === 0 && char === ":") {
1041
+ flushTokenAsKey();
1042
+ continue;
1043
+ }
1044
+ if (depth === 0 && char === ",") {
1045
+ token = "";
1046
+ continue;
1047
+ }
1048
+ token += char;
1049
+ }
1050
+ return [...names];
1051
+ }
1052
+ function extractScenarioObjectLiteral(source, pattern) {
1053
+ const match = pattern.exec(source);
1054
+ if (!match) return null;
1055
+ const openBraceIndex = source.indexOf("{", match.index);
1056
+ if (openBraceIndex < 0) return null;
1057
+ let depth = 0;
1058
+ let quote = null;
1059
+ let escaped = false;
1060
+ for (let i = openBraceIndex; i < source.length; i += 1) {
1061
+ const char = source[i];
1062
+ if (quote !== null) {
1063
+ if (escaped) {
1064
+ escaped = false;
1065
+ } else if (char === "\\") {
1066
+ escaped = true;
1067
+ } else if (char === quote) {
1068
+ quote = null;
1069
+ }
1070
+ continue;
1071
+ }
1072
+ if (char === '"' || char === "'" || char === "`") {
1073
+ quote = char;
1074
+ continue;
1075
+ }
1076
+ if (char === "{") {
1077
+ depth += 1;
1078
+ continue;
1079
+ }
1080
+ if (char === "}") {
1081
+ depth -= 1;
1082
+ if (depth === 0) return source.slice(openBraceIndex + 1, i);
747
1083
  }
748
1084
  }
749
1085
  return null;