deepline 0.1.156 → 0.1.158

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.
@@ -12,7 +12,9 @@ import {
12
12
  resolve,
13
13
  } from 'node:path';
14
14
  import { builtinModules } from 'node:module';
15
- import { build, type Message, type Plugin } from 'esbuild';
15
+ import { Parser } from 'acorn';
16
+ import { tsPlugin } from 'acorn-typescript';
17
+ import { build, transformSync, type Message, type Plugin } from 'esbuild';
16
18
  import {
17
19
  PLAY_ARTIFACT_KINDS,
18
20
  type PlayArtifactKind,
@@ -438,30 +440,6 @@ function findSourceImportReferences(
438
440
  );
439
441
  }
440
442
 
441
- function unquoteStringLiteral(literal: string): string | null {
442
- const trimmed = literal.trim();
443
- const quote = trimmed[0];
444
- if (
445
- (quote !== '"' && quote !== "'" && quote !== '`') ||
446
- trimmed[trimmed.length - 1] !== quote
447
- ) {
448
- return null;
449
- }
450
- if (quote === '`') {
451
- const value = trimmed.slice(1, -1);
452
- return value.includes('${') ? null : value;
453
- }
454
- try {
455
- return JSON.parse(
456
- quote === '"'
457
- ? trimmed
458
- : `"${trimmed.slice(1, -1).replace(/"/g, '\\"')}"`,
459
- );
460
- } catch {
461
- return trimmed.slice(1, -1);
462
- }
463
- }
464
-
465
443
  function findMatchingBrace(source: string, openIndex: number): number {
466
444
  const structuralSource = maskSourceForStructure(source);
467
445
  let depth = 0;
@@ -476,262 +454,542 @@ function findMatchingBrace(source: string, openIndex: number): number {
476
454
  return -1;
477
455
  }
478
456
 
479
- function findMatchingParen(source: string, openIndex: number): number {
480
- const structuralSource = maskSourceForStructure(source);
481
- let depth = 0;
482
- for (let index = openIndex; index < structuralSource.length; index += 1) {
483
- const char = structuralSource[index]!;
484
- if (char === '(') depth += 1;
485
- if (char === ')') {
486
- depth -= 1;
487
- if (depth === 0) return index;
488
- }
489
- }
490
- return -1;
491
- }
492
-
493
- function splitTopLevelArguments(source: string): string[] {
494
- const args: string[] = [];
495
- const structuralSource = maskSourceForStructure(source);
496
- let start = 0;
497
- let parenDepth = 0;
498
- let braceDepth = 0;
499
- let bracketDepth = 0;
500
- for (let index = 0; index < structuralSource.length; index += 1) {
501
- const char = structuralSource[index]!;
502
- if (char === '(') parenDepth += 1;
503
- if (char === ')') parenDepth -= 1;
504
- if (char === '{') braceDepth += 1;
505
- if (char === '}') braceDepth -= 1;
506
- if (char === '[') bracketDepth += 1;
507
- if (char === ']') bracketDepth -= 1;
508
- if (
509
- char === ',' &&
510
- parenDepth === 0 &&
511
- braceDepth === 0 &&
512
- bracketDepth === 0
513
- ) {
514
- args.push(source.slice(start, index).trim());
515
- start = index + 1;
516
- }
517
- }
518
- const finalArg = source.slice(start).trim();
519
- if (finalArg) args.push(finalArg);
520
- return args;
521
- }
522
-
523
- function extractStringProperty(
524
- objectSource: string,
525
- propertyName: string,
526
- ): string | null {
527
- for (const entry of splitTopLevelArguments(objectSource)) {
528
- const structuralEntry = stripCommentsToSpaces(entry).trim();
529
- const match = structuralEntry.match(
530
- new RegExp(
531
- `^(?:${propertyName}|['"]${propertyName}['"])\\s*:\\s*((['"\`])(?:\\\\.|(?!\\2)[\\s\\S])*\\2)\\s*$`,
532
- ),
533
- );
534
- const value = match?.[1] ? unquoteStringLiteral(match[1]) : null;
535
- if (value?.trim()) return value.trim();
536
- }
537
- return null;
538
- }
539
-
540
457
  export function extractDefinedPlayName(sourceCode: string): string | null {
541
- for (const call of findDefinePlayCalls(sourceCode)) {
542
- const name = nameFromDefinePlayArguments(sourceCode, call.arguments);
543
- if (name) return name;
544
- }
545
- return null;
458
+ return extractDefinedPlayMetadata(sourceCode, null)?.name ?? null;
546
459
  }
547
460
 
548
461
  export function extractDefinedPlayNameForExport(
549
462
  sourceCode: string,
550
463
  exportName: string,
551
464
  ): string | null {
552
- return findDefinedPlayMetadata(
553
- sourceCode,
554
- exportName,
555
- nameFromDefinePlayArguments,
556
- );
465
+ return extractDefinedPlayMetadata(sourceCode, exportName)?.name ?? null;
557
466
  }
558
467
 
559
468
  export function extractDefinedPlayDescription(
560
469
  sourceCode: string,
561
470
  ): string | null {
562
- return findDefinedPlayMetadata(
563
- sourceCode,
564
- null,
565
- descriptionFromDefinePlayArguments,
566
- );
471
+ return extractDefinedPlayMetadata(sourceCode, null)?.description ?? null;
567
472
  }
568
473
 
569
474
  export function extractDefinedPlayDescriptionForExport(
570
475
  sourceCode: string,
571
476
  exportName: string,
572
477
  ): string | null {
573
- return findDefinedPlayMetadata(
574
- sourceCode,
575
- exportName,
576
- descriptionFromDefinePlayArguments,
577
- );
478
+ return extractDefinedPlayMetadata(sourceCode, exportName)?.description ?? null;
578
479
  }
579
480
 
580
- function findDefinedPlayMetadata(
581
- sourceCode: string,
582
- exportName: string | null,
583
- getMetadata: (sourceCode: string, args: string[]) => string | null,
584
- ): string | null {
585
- const resolvedExportName =
586
- exportName === 'default'
587
- ? (findDefaultExportIdentifier(sourceCode) ?? exportName)
588
- : exportName;
589
- for (const call of findDefinePlayCalls(sourceCode)) {
590
- if (resolvedExportName && call.exportName !== resolvedExportName) {
591
- continue;
481
+ type AstNode = {
482
+ type: string;
483
+ [key: string]: unknown;
484
+ };
485
+
486
+ type PlayMetadataExtractionContext = {
487
+ declarations: Map<string, AstNode | null>;
488
+ namedExports: Map<string, string>;
489
+ commonJsExports: Map<string, AstNode>;
490
+ defaultExport: AstNode | null;
491
+ };
492
+
493
+ type ExtractedPlayMetadata = {
494
+ name: string | null;
495
+ description: string | null;
496
+ };
497
+
498
+ const TypeScriptParser = Parser.extend(
499
+ tsPlugin({
500
+ allowSatisfies: true,
501
+ jsx: {
502
+ allowNamespaces: true,
503
+ allowNamespacedObjects: true,
504
+ },
505
+ }) as unknown as (BaseParser: typeof Parser) => typeof Parser,
506
+ );
507
+
508
+ function parsePlaySourceAst(sourceCode: string): AstNode | null {
509
+ try {
510
+ return TypeScriptParser.parse(sourceCode, {
511
+ ecmaVersion: 'latest',
512
+ sourceType: 'module',
513
+ allowHashBang: true,
514
+ }) as unknown as AstNode;
515
+ } catch {
516
+ try {
517
+ const transformed = transformSync(sourceCode, {
518
+ loader: 'ts',
519
+ format: 'esm',
520
+ target: 'esnext',
521
+ legalComments: 'none',
522
+ sourcemap: false,
523
+ }).code;
524
+ return Parser.parse(transformed, {
525
+ ecmaVersion: 'latest',
526
+ sourceType: 'module',
527
+ allowHashBang: true,
528
+ }) as unknown as AstNode;
529
+ } catch {
530
+ return null;
592
531
  }
593
- const value = getMetadata(sourceCode, call.arguments);
594
- if (value) return value;
595
532
  }
596
- return null;
597
533
  }
598
534
 
599
- type DefinePlayCallMetadata = {
600
- arguments: string[];
601
- exportName: string | null;
602
- };
535
+ function getIdentifierName(node: unknown): string | null {
536
+ return isAstNode(node) && node.type === 'Identifier'
537
+ ? typeof node.name === 'string'
538
+ ? node.name
539
+ : null
540
+ : null;
541
+ }
603
542
 
604
- function findDefinePlayCalls(sourceCode: string): DefinePlayCallMetadata[] {
605
- const source = maskSourceForStructure(sourceCode);
606
- const callPattern =
607
- /(?:\b[A-Za-z_$][\w$]*\s*\.\s*)?\b(?:definePlay|defineWorkflow)\s*\(/g;
608
- const calls: DefinePlayCallMetadata[] = [];
609
- for (const match of source.matchAll(callPattern)) {
610
- const callStart = match.index!;
611
- const openParen = callStart + match[0].length - 1;
612
- const closeParen = findMatchingParen(sourceCode, openParen);
613
- if (closeParen < 0) continue;
614
- calls.push({
615
- arguments: splitTopLevelArguments(
616
- sourceCode.slice(openParen + 1, closeParen),
617
- ),
618
- exportName: exportNameForDefinePlayCall(sourceCode, callStart),
619
- });
620
- }
621
- return calls;
543
+ function isAstNode(value: unknown): value is AstNode {
544
+ return value !== null && typeof value === 'object' && 'type' in value;
622
545
  }
623
546
 
624
- function findDefaultExportIdentifier(sourceCode: string): string | null {
625
- const source = maskSourceForStructure(sourceCode);
626
- const pattern = /\bexport\s+default\s+([A-Za-z_$][\w$]*)/g;
627
- for (const match of source.matchAll(pattern)) {
628
- const afterIdentifier = match.index! + match[0].length;
629
- const nextNonSpace = source.slice(afterIdentifier).match(/\S/)?.[0] ?? '';
630
- if (nextNonSpace !== '(') return match[1] ?? null;
547
+ function astArray(value: unknown): AstNode[] {
548
+ return Array.isArray(value) ? value.filter(isAstNode) : [];
549
+ }
550
+
551
+ function memberExpressionPath(node: AstNode | null | undefined): string[] | null {
552
+ const expression = unwrapStaticExpression(node);
553
+ if (!expression) return null;
554
+ if (expression.type === 'Identifier' && typeof expression.name === 'string') {
555
+ return [expression.name];
556
+ }
557
+ if (expression.type !== 'MemberExpression') return null;
558
+ const objectPath = memberExpressionPath(
559
+ isAstNode(expression.object) ? expression.object : null,
560
+ );
561
+ if (!objectPath) return null;
562
+ const property = isAstNode(expression.property) ? expression.property : null;
563
+ if (!property) return null;
564
+ if (!expression.computed && property.type === 'Identifier') {
565
+ return typeof property.name === 'string'
566
+ ? [...objectPath, property.name]
567
+ : null;
568
+ }
569
+ if (expression.computed && property.type === 'Literal') {
570
+ return typeof property.value === 'string'
571
+ ? [...objectPath, property.value]
572
+ : null;
631
573
  }
632
574
  return null;
633
575
  }
634
576
 
635
- function nameFromDefinePlayArguments(
636
- sourceCode: string,
637
- args: string[],
638
- ): string | null {
639
- const firstArg = args[0]?.trim();
640
- if (!firstArg) return null;
641
- if (/^(['"`])/.test(firstArg))
642
- return unquoteStringLiteral(firstArg)?.trim() ?? null;
643
- const objectSource = objectLiteralBodyFromArgument(sourceCode, firstArg);
644
- if (objectSource) {
645
- return extractStringProperty(objectSource, 'id');
577
+ function commonJsExportName(left: AstNode | null | undefined): string | null {
578
+ const path = memberExpressionPath(left);
579
+ if (!path) return null;
580
+ if (path.length === 2 && path[0] === 'module' && path[1] === 'exports') {
581
+ return 'default';
582
+ }
583
+ if (path.length === 3 && path[0] === 'module' && path[1] === 'exports') {
584
+ return path[2] || null;
585
+ }
586
+ if (path.length === 2 && path[0] === 'exports') {
587
+ return path[1] || null;
646
588
  }
647
589
  return null;
648
590
  }
649
591
 
650
- function descriptionFromDefinePlayArguments(
651
- sourceCode: string,
652
- args: string[],
592
+ function isDefinePlayCallExpression(node: AstNode | null | undefined): boolean {
593
+ if (!node || node.type !== 'CallExpression') return false;
594
+ const callee = isAstNode(node.callee) ? node.callee : null;
595
+ if (!callee) return false;
596
+ if (callee.type === 'Identifier') {
597
+ return callee.name === 'definePlay' || callee.name === 'defineWorkflow';
598
+ }
599
+ if (callee.type === 'MemberExpression' && isAstNode(callee.property)) {
600
+ const property = callee.property;
601
+ return (
602
+ !callee.computed &&
603
+ property.type === 'Identifier' &&
604
+ (property.name === 'definePlay' || property.name === 'defineWorkflow')
605
+ );
606
+ }
607
+ return false;
608
+ }
609
+
610
+ function canResolveDeclarationInitializer(
611
+ declarationKind: unknown,
612
+ initializer: AstNode | null,
613
+ ): boolean {
614
+ return declarationKind === 'const' || isDefinePlayCallExpression(initializer);
615
+ }
616
+
617
+ function buildPlayMetadataContext(ast: AstNode): PlayMetadataExtractionContext {
618
+ const declarations = new Map<string, AstNode | null>();
619
+ const namedExports = new Map<string, string>();
620
+ const commonJsExports = new Map<string, AstNode>();
621
+ let defaultExport: AstNode | null = null;
622
+
623
+ for (const statement of astArray(ast.body)) {
624
+ if (statement.type === 'VariableDeclaration') {
625
+ for (const declaration of astArray(statement.declarations)) {
626
+ const name = getIdentifierName(declaration.id);
627
+ if (!name) continue;
628
+ const initializer = isAstNode(declaration.init)
629
+ ? declaration.init
630
+ : null;
631
+ declarations.set(
632
+ name,
633
+ canResolveDeclarationInitializer(statement.kind, initializer)
634
+ ? initializer
635
+ : null,
636
+ );
637
+ }
638
+ continue;
639
+ }
640
+
641
+ if (statement.type === 'ExportDefaultDeclaration') {
642
+ defaultExport = isAstNode(statement.declaration)
643
+ ? statement.declaration
644
+ : null;
645
+ continue;
646
+ }
647
+
648
+ if (statement.type === 'TSExportAssignment') {
649
+ defaultExport = isAstNode(statement.expression)
650
+ ? statement.expression
651
+ : null;
652
+ continue;
653
+ }
654
+
655
+ if (statement.type === 'ExportNamedDeclaration') {
656
+ if (
657
+ isAstNode(statement.declaration) &&
658
+ statement.declaration.type === 'VariableDeclaration'
659
+ ) {
660
+ for (const declaration of astArray(statement.declaration.declarations)) {
661
+ const name = getIdentifierName(declaration.id);
662
+ if (!name) continue;
663
+ const initializer = isAstNode(declaration.init)
664
+ ? declaration.init
665
+ : null;
666
+ declarations.set(
667
+ name,
668
+ canResolveDeclarationInitializer(
669
+ statement.declaration.kind,
670
+ initializer,
671
+ )
672
+ ? initializer
673
+ : null,
674
+ );
675
+ namedExports.set(name, name);
676
+ }
677
+ }
678
+
679
+ for (const specifier of astArray(statement.specifiers)) {
680
+ const localName = getIdentifierName(specifier.local);
681
+ const exportedName =
682
+ getIdentifierName(specifier.exported) ??
683
+ (isAstNode(specifier.exported) &&
684
+ specifier.exported.type === 'Literal' &&
685
+ typeof specifier.exported.value === 'string'
686
+ ? specifier.exported.value
687
+ : null);
688
+ if (localName && exportedName) {
689
+ namedExports.set(exportedName, localName);
690
+ }
691
+ }
692
+ }
693
+
694
+ if (
695
+ statement.type === 'ExpressionStatement' &&
696
+ isAstNode(statement.expression) &&
697
+ statement.expression.type === 'AssignmentExpression'
698
+ ) {
699
+ const exportName = commonJsExportName(
700
+ isAstNode(statement.expression.left) ? statement.expression.left : null,
701
+ );
702
+ if (exportName && isAstNode(statement.expression.right)) {
703
+ commonJsExports.set(exportName, statement.expression.right);
704
+ }
705
+ }
706
+ }
707
+
708
+ return { declarations, namedExports, commonJsExports, defaultExport };
709
+ }
710
+
711
+ function unwrapStaticExpression(node: AstNode | null | undefined): AstNode | null {
712
+ let current = node ?? null;
713
+ while (
714
+ current &&
715
+ (current.type === 'TSAsExpression' ||
716
+ current.type === 'TSSatisfiesExpression' ||
717
+ current.type === 'TSTypeAssertion' ||
718
+ current.type === 'TSNonNullExpression' ||
719
+ current.type === 'ParenthesizedExpression')
720
+ ) {
721
+ current = isAstNode(current.expression) ? current.expression : null;
722
+ }
723
+ return current;
724
+ }
725
+
726
+ function staticStringFromExpression(
727
+ node: AstNode | null | undefined,
728
+ context: PlayMetadataExtractionContext,
729
+ seen = new Set<string>(),
730
+ trimResult = true,
653
731
  ): string | null {
654
- const firstArgDescription = descriptionFromDefinePlayArgument(
655
- sourceCode,
656
- args[0],
657
- );
658
- if (firstArgDescription) return firstArgDescription;
659
- for (let index = args.length - 1; index >= 2; index -= 1) {
660
- const description = descriptionFromDefinePlayArgument(
661
- sourceCode,
662
- args[index],
732
+ const expression = unwrapStaticExpression(node);
733
+ if (!expression) return null;
734
+
735
+ if (expression.type === 'Literal') {
736
+ const value = expression.value;
737
+ if (typeof value !== 'string' || !value.trim()) return null;
738
+ return trimResult ? value.trim() : value;
739
+ }
740
+
741
+ if (
742
+ expression.type === 'TemplateLiteral' &&
743
+ astArray(expression.expressions).length === 0
744
+ ) {
745
+ const firstQuasi = astArray(expression.quasis)[0];
746
+ const cooked =
747
+ firstQuasi && typeof firstQuasi.value === 'object'
748
+ ? (firstQuasi.value as { cooked?: unknown }).cooked
749
+ : null;
750
+ if (typeof cooked !== 'string' || !cooked.trim()) return null;
751
+ return trimResult ? cooked.trim() : cooked;
752
+ }
753
+
754
+ if (expression.type === 'BinaryExpression' && expression.operator === '+') {
755
+ const left = staticStringFromExpression(
756
+ isAstNode(expression.left) ? expression.left : null,
757
+ context,
758
+ new Set(seen),
759
+ false,
663
760
  );
664
- if (description) return description;
761
+ const right = staticStringFromExpression(
762
+ isAstNode(expression.right) ? expression.right : null,
763
+ context,
764
+ new Set(seen),
765
+ false,
766
+ );
767
+ const value = left !== null && right !== null ? `${left}${right}` : null;
768
+ if (!value?.trim()) return null;
769
+ return trimResult ? value.trim() : value;
770
+ }
771
+
772
+ const identifier = getIdentifierName(expression);
773
+ if (!identifier || seen.has(identifier)) return null;
774
+ seen.add(identifier);
775
+ return staticStringFromExpression(
776
+ context.declarations.get(identifier),
777
+ context,
778
+ seen,
779
+ trimResult,
780
+ );
781
+ }
782
+
783
+ function propertyNameFromKey(property: AstNode): string | null {
784
+ if (property.computed) return null;
785
+ const key = isAstNode(property.key) ? property.key : null;
786
+ if (!key) return null;
787
+ if (key.type === 'Identifier' && typeof key.name === 'string') {
788
+ return key.name;
789
+ }
790
+ if (key.type === 'Literal' && typeof key.value === 'string') {
791
+ return key.value;
665
792
  }
666
793
  return null;
667
794
  }
668
795
 
669
- function descriptionFromDefinePlayArgument(
670
- sourceCode: string,
671
- arg: string | undefined,
672
- ): string | null {
673
- const trimmed = arg?.trim();
674
- if (!trimmed) return null;
675
- const objectSource = objectLiteralBodyFromArgument(sourceCode, trimmed);
676
- if (!objectSource) return null;
677
- return extractStringProperty(objectSource, 'description');
796
+ function objectExpressionFromNode(
797
+ node: AstNode | null | undefined,
798
+ context: PlayMetadataExtractionContext,
799
+ seen = new Set<string>(),
800
+ ): AstNode | null {
801
+ const expression = unwrapStaticExpression(node);
802
+ if (!expression) return null;
803
+ if (expression.type === 'ObjectExpression') return expression;
804
+
805
+ const identifier = getIdentifierName(expression);
806
+ if (!identifier || seen.has(identifier)) return null;
807
+ seen.add(identifier);
808
+ return objectExpressionFromNode(
809
+ context.declarations.get(identifier),
810
+ context,
811
+ seen,
812
+ );
678
813
  }
679
814
 
680
- function objectLiteralBodyFromArgument(
681
- sourceCode: string,
682
- arg: string,
815
+ function stringPropertyFromObjectExpression(
816
+ node: AstNode | null | undefined,
817
+ propertyName: string,
818
+ context: PlayMetadataExtractionContext,
819
+ seenObjects = new Set<AstNode>(),
683
820
  ): string | null {
684
- const trimmed = arg.trim();
685
- if (trimmed.startsWith('{')) {
686
- const closeBrace = findMatchingBrace(trimmed, 0);
687
- return closeBrace >= 0 ? trimmed.slice(1, closeBrace) : trimmed.slice(1);
821
+ const object = objectExpressionFromNode(node, context);
822
+ if (!object || seenObjects.has(object)) return null;
823
+ seenObjects.add(object);
824
+
825
+ let value: string | null = null;
826
+ for (const property of astArray(object.properties)) {
827
+ if (property.type === 'SpreadElement') {
828
+ const spreadValue = stringPropertyFromObjectExpression(
829
+ isAstNode(property.argument) ? property.argument : null,
830
+ propertyName,
831
+ context,
832
+ seenObjects,
833
+ );
834
+ if (spreadValue) value = spreadValue;
835
+ continue;
836
+ }
837
+
838
+ if (property.type !== 'Property') continue;
839
+ if (propertyNameFromKey(property) !== propertyName) continue;
840
+ const propertyValue = property.shorthand
841
+ ? property.key
842
+ : property.value;
843
+ value = staticStringFromExpression(
844
+ isAstNode(propertyValue) ? propertyValue : null,
845
+ context,
846
+ );
688
847
  }
689
- if (!/^[A-Za-z_$][\w$]*$/.test(trimmed)) return null;
690
- return findTopLevelObjectLiteralBody(sourceCode, trimmed);
848
+ return value;
691
849
  }
692
850
 
693
- function escapeRegExp(value: string): string {
694
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
851
+ function isDefinePlayCallee(node: AstNode | null | undefined): boolean {
852
+ const callee = unwrapStaticExpression(node);
853
+ if (!callee) return false;
854
+ if (
855
+ callee.type === 'Identifier' &&
856
+ (callee.name === 'definePlay' || callee.name === 'defineWorkflow')
857
+ ) {
858
+ return true;
859
+ }
860
+ if (callee.type === 'MemberExpression' && isAstNode(callee.property)) {
861
+ const property = callee.property;
862
+ return (
863
+ !callee.computed &&
864
+ property.type === 'Identifier' &&
865
+ (property.name === 'definePlay' || property.name === 'defineWorkflow')
866
+ );
867
+ }
868
+ return false;
695
869
  }
696
870
 
697
- function findTopLevelObjectLiteralBody(
698
- sourceCode: string,
699
- identifier: string,
700
- ): string | null {
701
- const source = maskSourceForStructure(sourceCode);
702
- const pattern = new RegExp(
703
- `(?:^|[;\\n])\\s*(?:export\\s+)?(?:const|let|var)\\s+${escapeRegExp(
704
- identifier,
705
- )}(?:\\s*:[^=;]+)?\\s*=\\s*{`,
706
- 'g',
707
- );
708
- for (const match of source.matchAll(pattern)) {
709
- const openBrace = source.indexOf('{', match.index);
710
- if (openBrace < 0) continue;
711
- const closeBrace = findMatchingBrace(sourceCode, openBrace);
712
- if (closeBrace < 0) continue;
713
- return sourceCode.slice(openBrace + 1, closeBrace);
871
+ function playMetadataFromDefinePlayCall(
872
+ node: AstNode | null | undefined,
873
+ context: PlayMetadataExtractionContext,
874
+ ): ExtractedPlayMetadata | null {
875
+ const expression = unwrapStaticExpression(node);
876
+ if (
877
+ !expression ||
878
+ expression.type !== 'CallExpression' ||
879
+ !isDefinePlayCallee(isAstNode(expression.callee) ? expression.callee : null)
880
+ ) {
881
+ return null;
882
+ }
883
+
884
+ const args = astArray(expression.arguments);
885
+ const firstArg = args[0] ?? null;
886
+ const objectName = stringPropertyFromObjectExpression(firstArg, 'id', context);
887
+ const name = objectName ?? staticStringFromExpression(firstArg, context);
888
+
889
+ let description =
890
+ stringPropertyFromObjectExpression(firstArg, 'description', context) ?? null;
891
+ for (let index = args.length - 1; !description && index >= 2; index -= 1) {
892
+ description = stringPropertyFromObjectExpression(
893
+ args[index],
894
+ 'description',
895
+ context,
896
+ );
714
897
  }
898
+
899
+ if (!name && !description) return null;
900
+ return { name, description };
901
+ }
902
+
903
+ function resolveExportExpression(
904
+ exportName: string | null,
905
+ context: PlayMetadataExtractionContext,
906
+ ): AstNode | null {
907
+ if (exportName === 'default') {
908
+ const commonJsDefault = context.commonJsExports.get('default');
909
+ if (commonJsDefault) {
910
+ const commonJsDefaultIdentifier = getIdentifierName(
911
+ unwrapStaticExpression(commonJsDefault),
912
+ );
913
+ return commonJsDefaultIdentifier
914
+ ? (context.declarations.get(commonJsDefaultIdentifier) ??
915
+ commonJsDefault)
916
+ : commonJsDefault;
917
+ }
918
+ const defaultExpression = unwrapStaticExpression(context.defaultExport);
919
+ const defaultIdentifier = getIdentifierName(defaultExpression);
920
+ if (!defaultIdentifier) {
921
+ const defaultExportName = context.namedExports.get('default');
922
+ return defaultExportName
923
+ ? (context.declarations.get(defaultExportName) ?? null)
924
+ : defaultExpression;
925
+ }
926
+ return defaultIdentifier
927
+ ? (context.declarations.get(defaultIdentifier) ?? context.defaultExport)
928
+ : context.defaultExport;
929
+ }
930
+
931
+ if (exportName) {
932
+ const commonJsExport = context.commonJsExports.get(exportName);
933
+ if (commonJsExport) {
934
+ const commonJsExportIdentifier = getIdentifierName(
935
+ unwrapStaticExpression(commonJsExport),
936
+ );
937
+ return commonJsExportIdentifier
938
+ ? (context.declarations.get(commonJsExportIdentifier) ?? commonJsExport)
939
+ : commonJsExport;
940
+ }
941
+ const localName = context.namedExports.get(exportName) ?? exportName;
942
+ return context.declarations.get(localName) ?? null;
943
+ }
944
+
715
945
  return null;
716
946
  }
717
947
 
718
- function exportNameForDefinePlayCall(
948
+ function extractDefinedPlayMetadata(
719
949
  sourceCode: string,
720
- callStart: number,
721
- ): string | null {
722
- const source = maskSourceForStructure(sourceCode);
723
- const structuralPrefix = source
724
- .slice(Math.max(0, callStart - 500), callStart)
725
- .trimEnd();
726
- if (/\bexport\s+default\s*$/.test(structuralPrefix)) return 'default';
727
- const variableMatch = structuralPrefix.match(
728
- /\bexport\s+(?:declare\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)(?:\s*:[^=;]+)?\s*=\s*$/,
729
- );
730
- if (variableMatch?.[1]) return variableMatch[1];
731
- const localVariableMatch = structuralPrefix.match(
732
- /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)(?:\s*:[^=;]+)?\s*=\s*$/,
950
+ exportName: string | null,
951
+ ): ExtractedPlayMetadata | null {
952
+ const ast = parsePlaySourceAst(sourceCode);
953
+ if (!ast) return null;
954
+ const context = buildPlayMetadataContext(ast);
955
+
956
+ const exportedExpression = resolveExportExpression(exportName, context);
957
+ const exportedMetadata = playMetadataFromDefinePlayCall(
958
+ exportedExpression,
959
+ context,
733
960
  );
734
- return localVariableMatch?.[1] ?? null;
961
+ if (exportedMetadata) return exportedMetadata;
962
+
963
+ if (exportName) return null;
964
+
965
+ if (context.defaultExport) {
966
+ const directDefaultMetadata = playMetadataFromDefinePlayCall(
967
+ context.defaultExport,
968
+ context,
969
+ );
970
+ if (directDefaultMetadata) return directDefaultMetadata;
971
+
972
+ const defaultIdentifier = getIdentifierName(context.defaultExport);
973
+ if (defaultIdentifier) {
974
+ const defaultMetadata = playMetadataFromDefinePlayCall(
975
+ context.declarations.get(defaultIdentifier),
976
+ context,
977
+ );
978
+ if (defaultMetadata) return defaultMetadata;
979
+ }
980
+ }
981
+
982
+ for (const expression of context.declarations.values()) {
983
+ const metadata = playMetadataFromDefinePlayCall(expression, context);
984
+ if (metadata) return metadata;
985
+ }
986
+
987
+ for (const expression of context.commonJsExports.values()) {
988
+ const metadata = playMetadataFromDefinePlayCall(expression, context);
989
+ if (metadata) return metadata;
990
+ }
991
+
992
+ return null;
735
993
  }
736
994
 
737
995
  function canonicalizeRootPlayNameForWorkersRuntimeHash(