@sun-asterisk/sungen 1.0.0
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/LICENSE +21 -0
- package/README.md +490 -0
- package/bin/sungen.js +12 -0
- package/dist/cli/commands/auto-tag-command.d.ts +8 -0
- package/dist/cli/commands/auto-tag-command.d.ts.map +1 -0
- package/dist/cli/commands/auto-tag-command.js +104 -0
- package/dist/cli/commands/auto-tag-command.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +196 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/config/ai-providers.yaml +56 -0
- package/dist/config/config-loader.d.ts +51 -0
- package/dist/config/config-loader.d.ts.map +1 -0
- package/dist/config/config-loader.js +216 -0
- package/dist/config/config-loader.js.map +1 -0
- package/dist/config/config-schema.d.ts +121 -0
- package/dist/config/config-schema.d.ts.map +1 -0
- package/dist/config/config-schema.js +7 -0
- package/dist/config/config-schema.js.map +1 -0
- package/dist/config/default.config.yaml +101 -0
- package/dist/config/framework.config.yaml +52 -0
- package/dist/config/routes.yaml +31 -0
- package/dist/core/selector-base/annotation-handler.d.ts +45 -0
- package/dist/core/selector-base/annotation-handler.d.ts.map +1 -0
- package/dist/core/selector-base/annotation-handler.js +102 -0
- package/dist/core/selector-base/annotation-handler.js.map +1 -0
- package/dist/core/selector-base/base-generator.d.ts +49 -0
- package/dist/core/selector-base/base-generator.d.ts.map +1 -0
- package/dist/core/selector-base/base-generator.js +214 -0
- package/dist/core/selector-base/base-generator.js.map +1 -0
- package/dist/core/selector-base/gherkin-parser.d.ts +24 -0
- package/dist/core/selector-base/gherkin-parser.d.ts.map +1 -0
- package/dist/core/selector-base/gherkin-parser.js +42 -0
- package/dist/core/selector-base/gherkin-parser.js.map +1 -0
- package/dist/core/selector-mapper/priority-mapper.d.ts +74 -0
- package/dist/core/selector-mapper/priority-mapper.d.ts.map +1 -0
- package/dist/core/selector-mapper/priority-mapper.js +477 -0
- package/dist/core/selector-mapper/priority-mapper.js.map +1 -0
- package/dist/core/ui-scanner/heuristics/base-heuristic.d.ts +91 -0
- package/dist/core/ui-scanner/heuristics/base-heuristic.d.ts.map +1 -0
- package/dist/core/ui-scanner/heuristics/base-heuristic.js +175 -0
- package/dist/core/ui-scanner/heuristics/base-heuristic.js.map +1 -0
- package/dist/core/ui-scanner/react-scanner.d.ts +32 -0
- package/dist/core/ui-scanner/react-scanner.d.ts.map +1 -0
- package/dist/core/ui-scanner/react-scanner.js +163 -0
- package/dist/core/ui-scanner/react-scanner.js.map +1 -0
- package/dist/core/ui-scanner/scanner-interface.d.ts +94 -0
- package/dist/core/ui-scanner/scanner-interface.d.ts.map +1 -0
- package/dist/core/ui-scanner/scanner-interface.js +33 -0
- package/dist/core/ui-scanner/scanner-interface.js.map +1 -0
- package/dist/core/ui-scanner/strict-scanner.d.ts +81 -0
- package/dist/core/ui-scanner/strict-scanner.d.ts.map +1 -0
- package/dist/core/ui-scanner/strict-scanner.js +511 -0
- package/dist/core/ui-scanner/strict-scanner.js.map +1 -0
- package/dist/executor/playwright/playwright-generator.d.ts +33 -0
- package/dist/executor/playwright/playwright-generator.d.ts.map +1 -0
- package/dist/executor/playwright/playwright-generator.js +136 -0
- package/dist/executor/playwright/playwright-generator.js.map +1 -0
- package/dist/executor/test-generator.d.ts +63 -0
- package/dist/executor/test-generator.d.ts.map +1 -0
- package/dist/executor/test-generator.js +30 -0
- package/dist/executor/test-generator.js.map +1 -0
- package/dist/external/ai-provider.d.ts +60 -0
- package/dist/external/ai-provider.d.ts.map +1 -0
- package/dist/external/ai-provider.js +30 -0
- package/dist/external/ai-provider.js.map +1 -0
- package/dist/external/anthropic-provider.d.ts +29 -0
- package/dist/external/anthropic-provider.d.ts.map +1 -0
- package/dist/external/anthropic-provider.js +85 -0
- package/dist/external/anthropic-provider.js.map +1 -0
- package/dist/generators/cache/cache-manager.d.ts +66 -0
- package/dist/generators/cache/cache-manager.d.ts.map +1 -0
- package/dist/generators/cache/cache-manager.js +286 -0
- package/dist/generators/cache/cache-manager.js.map +1 -0
- package/dist/generators/cli.d.ts +7 -0
- package/dist/generators/cli.d.ts.map +1 -0
- package/dist/generators/cli.js +570 -0
- package/dist/generators/cli.js.map +1 -0
- package/dist/generators/dsl-writer/index.d.ts +33 -0
- package/dist/generators/dsl-writer/index.d.ts.map +1 -0
- package/dist/generators/dsl-writer/index.js +226 -0
- package/dist/generators/dsl-writer/index.js.map +1 -0
- package/dist/generators/gherkin-parser/index.d.ts +47 -0
- package/dist/generators/gherkin-parser/index.d.ts.map +1 -0
- package/dist/generators/gherkin-parser/index.js +149 -0
- package/dist/generators/gherkin-parser/index.js.map +1 -0
- package/dist/generators/gherkin-parser/selector-extractor.d.ts +37 -0
- package/dist/generators/gherkin-parser/selector-extractor.d.ts.map +1 -0
- package/dist/generators/gherkin-parser/selector-extractor.js +108 -0
- package/dist/generators/gherkin-parser/selector-extractor.js.map +1 -0
- package/dist/generators/scaffold-generator/index.d.ts +111 -0
- package/dist/generators/scaffold-generator/index.d.ts.map +1 -0
- package/dist/generators/scaffold-generator/index.js +408 -0
- package/dist/generators/scaffold-generator/index.js.map +1 -0
- package/dist/generators/selector-mapper/ai-mapper.d.ts +56 -0
- package/dist/generators/selector-mapper/ai-mapper.d.ts.map +1 -0
- package/dist/generators/selector-mapper/ai-mapper.js +457 -0
- package/dist/generators/selector-mapper/ai-mapper.js.map +1 -0
- package/dist/generators/selector-mapper/hybrid-mapper.d.ts +67 -0
- package/dist/generators/selector-mapper/hybrid-mapper.d.ts.map +1 -0
- package/dist/generators/selector-mapper/hybrid-mapper.js +349 -0
- package/dist/generators/selector-mapper/hybrid-mapper.js.map +1 -0
- package/dist/generators/selector-mapper/index.d.ts +8 -0
- package/dist/generators/selector-mapper/index.d.ts.map +1 -0
- package/dist/generators/selector-mapper/index.js +12 -0
- package/dist/generators/selector-mapper/index.js.map +1 -0
- package/dist/generators/selector-mapper/intelligent-mapper.d.ts +125 -0
- package/dist/generators/selector-mapper/intelligent-mapper.d.ts.map +1 -0
- package/dist/generators/selector-mapper/intelligent-mapper.js +391 -0
- package/dist/generators/selector-mapper/intelligent-mapper.js.map +1 -0
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +49 -0
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -0
- package/dist/generators/test-generator/adapters/adapter-interface.js +7 -0
- package/dist/generators/test-generator/adapters/adapter-interface.js.map +1 -0
- package/dist/generators/test-generator/adapters/adapter-registry.d.ts +29 -0
- package/dist/generators/test-generator/adapters/adapter-registry.d.ts.map +1 -0
- package/dist/generators/test-generator/adapters/adapter-registry.js +50 -0
- package/dist/generators/test-generator/adapters/adapter-registry.js.map +1 -0
- package/dist/generators/test-generator/adapters/index.d.ts +4 -0
- package/dist/generators/test-generator/adapters/index.d.ts.map +1 -0
- package/dist/generators/test-generator/adapters/index.js +13 -0
- package/dist/generators/test-generator/adapters/index.js.map +1 -0
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +23 -0
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -0
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js +38 -0
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/before-each.hbs +8 -0
- package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +5 -0
- package/dist/generators/test-generator/adapters/playwright/templates/scenario.hbs +8 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/check-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/clear-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/click-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/double-click-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/fill-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/hover-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/press-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/uncheck-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/active-state-assertion.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/ai-response-assertion-selector.hbs +5 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/ai-response-assertion-simple.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/application-running.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/checked-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/contain-text-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/count-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-assertion.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/empty-assertion.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/enabled-assertion.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/focused-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/have-text-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/not-checked-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/not-visible-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/check-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/checkbox.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/checked-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/clear-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/clear-auth.hbs +6 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/clear-browser-state.hbs +6 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/clear-database.hbs +4 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/clear.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/click-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/click.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/contain-text-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/contains-text-assertion.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/count-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/count-greater-than.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/count-less-than.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/disabled-assertion.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/displayed-containing-text.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/displayed-with-text.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/double-click-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/empty-assertion-advanced.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/empty-assertion.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/enabled-assertion.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/error-message-assertion.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/fill-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/fill.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/focused-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/generic-message-assertion.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/has-attribute.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/has-class.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/has-count.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/has-image-src.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/has-link.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/has-placeholder.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/has-value.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/have-text-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/hover-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/html5-validation-check.hbs +4 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/is-checked.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/is-editable.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/is-focused.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/is-hidden.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/is-unchecked.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/locator.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/login.hbs +8 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/message-assertion-body.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/message-assertion-selector.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/message-count-assertion.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/navigation.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-timeout.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/not-checked-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/not-visible-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/not-visible.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/notification-assertion.hbs +4 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/press-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/press-enter.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/redirect-assertion.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/route-assertion.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/screen-navigation.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/scroll-bottom-assertion.hbs +5 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/select-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/select.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/setup/application-running.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/setup/clear-auth.hbs +6 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/setup/clear-browser-state.hbs +6 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/setup/clear-database.hbs +4 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/setup/user-login-todo.hbs +6 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/text-matches-pattern.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/uncheck-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/user-login-todo.hbs +6 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/visibility-assertion.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/visible-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/wait-for-element.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/wait-timeout.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/wait.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +19 -0
- package/dist/generators/test-generator/ai-step-mapper.d.ts +27 -0
- package/dist/generators/test-generator/ai-step-mapper.d.ts.map +1 -0
- package/dist/generators/test-generator/ai-step-mapper.js +204 -0
- package/dist/generators/test-generator/ai-step-mapper.js.map +1 -0
- package/dist/generators/test-generator/code-generator.d.ts +52 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -0
- package/dist/generators/test-generator/code-generator.js +191 -0
- package/dist/generators/test-generator/code-generator.js.map +1 -0
- package/dist/generators/test-generator/patterns/assertion-patterns.d.ts +7 -0
- package/dist/generators/test-generator/patterns/assertion-patterns.d.ts.map +1 -0
- package/dist/generators/test-generator/patterns/assertion-patterns.js +173 -0
- package/dist/generators/test-generator/patterns/assertion-patterns.js.map +1 -0
- package/dist/generators/test-generator/patterns/form-patterns.d.ts +8 -0
- package/dist/generators/test-generator/patterns/form-patterns.d.ts.map +1 -0
- package/dist/generators/test-generator/patterns/form-patterns.js +110 -0
- package/dist/generators/test-generator/patterns/form-patterns.js.map +1 -0
- package/dist/generators/test-generator/patterns/index.d.ts +45 -0
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -0
- package/dist/generators/test-generator/patterns/index.js +106 -0
- package/dist/generators/test-generator/patterns/index.js.map +1 -0
- package/dist/generators/test-generator/patterns/interaction-patterns.d.ts +7 -0
- package/dist/generators/test-generator/patterns/interaction-patterns.d.ts.map +1 -0
- package/dist/generators/test-generator/patterns/interaction-patterns.js +100 -0
- package/dist/generators/test-generator/patterns/interaction-patterns.js.map +1 -0
- package/dist/generators/test-generator/patterns/navigation-patterns.d.ts +8 -0
- package/dist/generators/test-generator/patterns/navigation-patterns.d.ts.map +1 -0
- package/dist/generators/test-generator/patterns/navigation-patterns.js +92 -0
- package/dist/generators/test-generator/patterns/navigation-patterns.js.map +1 -0
- package/dist/generators/test-generator/patterns/setup-patterns.d.ts +7 -0
- package/dist/generators/test-generator/patterns/setup-patterns.d.ts.map +1 -0
- package/dist/generators/test-generator/patterns/setup-patterns.js +84 -0
- package/dist/generators/test-generator/patterns/setup-patterns.js.map +1 -0
- package/dist/generators/test-generator/patterns/types.d.ts +38 -0
- package/dist/generators/test-generator/patterns/types.d.ts.map +1 -0
- package/dist/generators/test-generator/patterns/types.js +3 -0
- package/dist/generators/test-generator/patterns/types.js.map +1 -0
- package/dist/generators/test-generator/step-mapper-old.d.ts +180 -0
- package/dist/generators/test-generator/step-mapper-old.d.ts.map +1 -0
- package/dist/generators/test-generator/step-mapper-old.js +752 -0
- package/dist/generators/test-generator/step-mapper-old.js.map +1 -0
- package/dist/generators/test-generator/step-mapper-refactored.d.ts +47 -0
- package/dist/generators/test-generator/step-mapper-refactored.d.ts.map +1 -0
- package/dist/generators/test-generator/step-mapper-refactored.js +182 -0
- package/dist/generators/test-generator/step-mapper-refactored.js.map +1 -0
- package/dist/generators/test-generator/step-mapper.d.ts +66 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -0
- package/dist/generators/test-generator/step-mapper.js +248 -0
- package/dist/generators/test-generator/step-mapper.js.map +1 -0
- package/dist/generators/test-generator/template-engine.d.ts +33 -0
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -0
- package/dist/generators/test-generator/template-engine.js +129 -0
- package/dist/generators/test-generator/template-engine.js.map +1 -0
- package/dist/generators/test-generator/utils/data-resolver.d.ts +39 -0
- package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/data-resolver.js +162 -0
- package/dist/generators/test-generator/utils/data-resolver.js.map +1 -0
- package/dist/generators/test-generator/utils/path-inference.d.ts +49 -0
- package/dist/generators/test-generator/utils/path-inference.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/path-inference.js +286 -0
- package/dist/generators/test-generator/utils/path-inference.js.map +1 -0
- package/dist/generators/test-generator/utils/selector-resolver.d.ts +93 -0
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/selector-resolver.js +408 -0
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -0
- package/dist/generators/types.d.ts +118 -0
- package/dist/generators/types.d.ts.map +1 -0
- package/dist/generators/types.js +48 -0
- package/dist/generators/types.js.map +1 -0
- package/dist/generators/ui-model-builder/deep-scanner.d.ts +121 -0
- package/dist/generators/ui-model-builder/deep-scanner.d.ts.map +1 -0
- package/dist/generators/ui-model-builder/deep-scanner.js +1113 -0
- package/dist/generators/ui-model-builder/deep-scanner.js.map +1 -0
- package/dist/generators/ui-model-builder/enhanced-deep-scanner.d.ts +110 -0
- package/dist/generators/ui-model-builder/enhanced-deep-scanner.d.ts.map +1 -0
- package/dist/generators/ui-model-builder/enhanced-deep-scanner.js +608 -0
- package/dist/generators/ui-model-builder/enhanced-deep-scanner.js.map +1 -0
- package/dist/generators/ui-model-builder/react-scanner.d.ts +107 -0
- package/dist/generators/ui-model-builder/react-scanner.d.ts.map +1 -0
- package/dist/generators/ui-model-builder/react-scanner.js +797 -0
- package/dist/generators/ui-model-builder/react-scanner.js.map +1 -0
- package/dist/input/cli-adapter.d.ts +63 -0
- package/dist/input/cli-adapter.d.ts.map +1 -0
- package/dist/input/cli-adapter.js +173 -0
- package/dist/input/cli-adapter.js.map +1 -0
- package/dist/input/config-adapter.d.ts +25 -0
- package/dist/input/config-adapter.d.ts.map +1 -0
- package/dist/input/config-adapter.js +70 -0
- package/dist/input/config-adapter.js.map +1 -0
- package/dist/input/input-adapter.d.ts +28 -0
- package/dist/input/input-adapter.d.ts.map +1 -0
- package/dist/input/input-adapter.js +17 -0
- package/dist/input/input-adapter.js.map +1 -0
- package/dist/input/vscode-adapter.d.ts +62 -0
- package/dist/input/vscode-adapter.d.ts.map +1 -0
- package/dist/input/vscode-adapter.js +64 -0
- package/dist/input/vscode-adapter.js.map +1 -0
- package/dist/orchestrator/cache-manager.d.ts +37 -0
- package/dist/orchestrator/cache-manager.d.ts.map +1 -0
- package/dist/orchestrator/cache-manager.js +148 -0
- package/dist/orchestrator/cache-manager.js.map +1 -0
- package/dist/orchestrator/pipeline.d.ts +73 -0
- package/dist/orchestrator/pipeline.d.ts.map +1 -0
- package/dist/orchestrator/pipeline.js +607 -0
- package/dist/orchestrator/pipeline.js.map +1 -0
- package/dist/orchestrator/project-initializer.d.ts +51 -0
- package/dist/orchestrator/project-initializer.d.ts.map +1 -0
- package/dist/orchestrator/project-initializer.js +326 -0
- package/dist/orchestrator/project-initializer.js.map +1 -0
- package/dist/orchestrator/reporter.d.ts +15 -0
- package/dist/orchestrator/reporter.d.ts.map +1 -0
- package/dist/orchestrator/reporter.js +30 -0
- package/dist/orchestrator/reporter.js.map +1 -0
- package/dist/orchestrator/screen-manager.d.ts +47 -0
- package/dist/orchestrator/screen-manager.d.ts.map +1 -0
- package/dist/orchestrator/screen-manager.js +271 -0
- package/dist/orchestrator/screen-manager.js.map +1 -0
- package/dist/tools/auto-tagger.d.ts +107 -0
- package/dist/tools/auto-tagger.d.ts.map +1 -0
- package/dist/tools/auto-tagger.js +502 -0
- package/dist/tools/auto-tagger.js.map +1 -0
- package/package.json +73 -0
- package/src/cli/commands/auto-tag-command.ts +80 -0
- package/src/cli/index.ts +205 -0
- package/src/config/ai-providers.yaml +56 -0
- package/src/config/config-loader.ts +248 -0
- package/src/config/config-schema.ts +148 -0
- package/src/config/default.config.yaml +101 -0
- package/src/config/framework.config.yaml +52 -0
- package/src/config/routes.yaml +31 -0
- package/src/core/selector-base/annotation-handler.ts +127 -0
- package/src/core/selector-base/base-generator.ts +234 -0
- package/src/core/selector-base/gherkin-parser.ts +57 -0
- package/src/core/selector-mapper/priority-mapper.ts +607 -0
- package/src/core/ui-scanner/heuristics/base-heuristic.ts +216 -0
- package/src/core/ui-scanner/react-scanner.ts +156 -0
- package/src/core/ui-scanner/scanner-interface.ts +133 -0
- package/src/core/ui-scanner/strict-scanner.ts +629 -0
- package/src/executor/playwright/playwright-generator.ts +125 -0
- package/src/executor/test-generator.ts +90 -0
- package/src/external/ai-provider.ts +90 -0
- package/src/external/anthropic-provider.ts +114 -0
- package/src/generators/README.md +410 -0
- package/src/generators/cache/cache-manager.ts +322 -0
- package/src/generators/cli.ts +640 -0
- package/src/generators/dsl-writer/index.ts +253 -0
- package/src/generators/gherkin-parser/index.ts +155 -0
- package/src/generators/gherkin-parser/selector-extractor.ts +142 -0
- package/src/generators/scaffold-generator/index.ts +524 -0
- package/src/generators/selector-mapper/ai-mapper.ts +528 -0
- package/src/generators/selector-mapper/hybrid-mapper.ts +427 -0
- package/src/generators/selector-mapper/index.ts +10 -0
- package/src/generators/selector-mapper/intelligent-mapper.ts +530 -0
- package/src/generators/test-generator/adapters/adapter-interface.ts +49 -0
- package/src/generators/test-generator/adapters/adapter-registry.ts +56 -0
- package/src/generators/test-generator/adapters/index.ts +9 -0
- package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +40 -0
- package/src/generators/test-generator/adapters/playwright/templates/before-each.hbs +8 -0
- package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +5 -0
- package/src/generators/test-generator/adapters/playwright/templates/scenario.hbs +8 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/check-action.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/clear-action.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/click-action.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/double-click-action.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/fill-action.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/hover-action.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/press-action.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/uncheck-action.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/checked-assertion.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/contain-text-assertion.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/count-assertion.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-assertion.hbs +2 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/empty-assertion.hbs +2 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/enabled-assertion.hbs +2 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/focused-assertion.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/have-text-assertion.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/not-checked-assertion.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/not-visible-assertion.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-assertion.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/navigation.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +2 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-timeout.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/setup/application-running.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/setup/clear-auth.hbs +6 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/setup/clear-browser-state.hbs +6 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/setup/clear-database.hbs +4 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/setup/user-login-todo.hbs +6 -0
- package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +19 -0
- package/src/generators/test-generator/ai-step-mapper.ts +224 -0
- package/src/generators/test-generator/code-generator.ts +235 -0
- package/src/generators/test-generator/patterns/assertion-patterns.ts +183 -0
- package/src/generators/test-generator/patterns/form-patterns.ts +124 -0
- package/src/generators/test-generator/patterns/index.ts +97 -0
- package/src/generators/test-generator/patterns/interaction-patterns.ts +119 -0
- package/src/generators/test-generator/patterns/navigation-patterns.ts +110 -0
- package/src/generators/test-generator/patterns/setup-patterns.ts +94 -0
- package/src/generators/test-generator/patterns/types.ts +41 -0
- package/src/generators/test-generator/step-mapper.ts +254 -0
- package/src/generators/test-generator/template-engine.ts +160 -0
- package/src/generators/test-generator/utils/data-resolver.ts +147 -0
- package/src/generators/test-generator/utils/path-inference.ts +344 -0
- package/src/generators/test-generator/utils/selector-resolver.ts +480 -0
- package/src/generators/types.ts +226 -0
- package/src/generators/ui-model-builder/deep-scanner.ts +1244 -0
- package/src/generators/ui-model-builder/enhanced-deep-scanner.ts +731 -0
- package/src/generators/ui-model-builder/react-scanner.ts +959 -0
- package/src/input/cli-adapter.ts +185 -0
- package/src/input/config-adapter.ts +71 -0
- package/src/input/input-adapter.ts +32 -0
- package/src/input/vscode-adapter.ts +90 -0
- package/src/orchestrator/cache-manager.ts +138 -0
- package/src/orchestrator/pipeline.ts +713 -0
- package/src/orchestrator/project-initializer.ts +315 -0
- package/src/orchestrator/reporter.ts +36 -0
- package/src/orchestrator/screen-manager.ts +268 -0
- package/src/tools/auto-tagger.ts +572 -0
|
@@ -0,0 +1,959 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React/Next.js UI Model Builder
|
|
3
|
+
* Scans React/Next.js source code to extract UI elements using heuristic analysis
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as parser from '@babel/parser';
|
|
7
|
+
import traverse, { NodePath } from '@babel/traverse';
|
|
8
|
+
import * as t from '@babel/types';
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import { glob } from 'glob';
|
|
12
|
+
import { UIModel, UIElement, FrameworkType } from '../types';
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Configuration
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
interface ScanConfig {
|
|
19
|
+
sourceRoot: string;
|
|
20
|
+
framework: FrameworkType;
|
|
21
|
+
verbose?: boolean;
|
|
22
|
+
maxDepth?: number; // Maximum depth for component scanning (default: 999 = unlimited)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// React Scanner Class
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
interface ComponentInfo {
|
|
30
|
+
name: string; // Component name (e.g., "ChatInput")
|
|
31
|
+
importPath: string; // Import path (e.g., "@/components/ChatInput")
|
|
32
|
+
sourceFile: string; // File where component was used
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class ReactScanner {
|
|
36
|
+
private config: ScanConfig;
|
|
37
|
+
private elements: UIElement[] = [];
|
|
38
|
+
private elementCounter = 0;
|
|
39
|
+
private detectedComponents: ComponentInfo[] = [];
|
|
40
|
+
private scannedComponentPaths = new Set<string>(); // Avoid duplicate scans
|
|
41
|
+
|
|
42
|
+
constructor(config: ScanConfig) {
|
|
43
|
+
this.config = config;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Discover all routes/screens in Next.js app directory
|
|
48
|
+
*/
|
|
49
|
+
async discoverScreens(): Promise<string[]> {
|
|
50
|
+
const appDir = this.config.sourceRoot;
|
|
51
|
+
|
|
52
|
+
// Find all page.tsx/page.jsx files (Next.js 13+ app directory)
|
|
53
|
+
const pageFiles = await glob('**/page.{tsx,jsx}', {
|
|
54
|
+
cwd: appDir,
|
|
55
|
+
absolute: false,
|
|
56
|
+
ignore: ['**/node_modules/**', '**/api/**']
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Extract screen IDs from paths
|
|
60
|
+
// Example: "login/page.tsx" -> "login"
|
|
61
|
+
// Example: "auth/login/page.tsx" -> "auth-login"
|
|
62
|
+
// Example: "page.tsx" -> "home"
|
|
63
|
+
const screens = pageFiles.map(file => {
|
|
64
|
+
const dir = path.dirname(file);
|
|
65
|
+
return dir === '.' ? 'home' : dir.replace(/\//g, '-');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return screens;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build UI Model for a specific screen
|
|
73
|
+
*/
|
|
74
|
+
async buildUIModel(screenId: string): Promise<UIModel> {
|
|
75
|
+
// Reset state for new screen
|
|
76
|
+
this.elements = [];
|
|
77
|
+
this.elementCounter = 0;
|
|
78
|
+
this.detectedComponents = [];
|
|
79
|
+
this.scannedComponentPaths.clear();
|
|
80
|
+
|
|
81
|
+
// Determine route path from screen ID
|
|
82
|
+
// "home" -> "" (root)
|
|
83
|
+
// "auth-login" -> "auth/login"
|
|
84
|
+
const routePath = screenId === 'home' ? '' : screenId.replace(/-/g, '/');
|
|
85
|
+
const routeDir = path.join(this.config.sourceRoot, routePath);
|
|
86
|
+
|
|
87
|
+
// Collect source files to scan
|
|
88
|
+
const sourceFiles: string[] = [];
|
|
89
|
+
|
|
90
|
+
// 1. App root layout
|
|
91
|
+
const rootLayout = path.join(this.config.sourceRoot, 'layout.tsx');
|
|
92
|
+
if (fs.existsSync(rootLayout)) {
|
|
93
|
+
sourceFiles.push(rootLayout);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 2. Route-specific layout (if exists)
|
|
97
|
+
const routeLayout = path.join(routeDir, 'layout.tsx');
|
|
98
|
+
if (fs.existsSync(routeLayout)) {
|
|
99
|
+
sourceFiles.push(routeLayout);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 3. Page component
|
|
103
|
+
const pageTsx = path.join(routeDir, 'page.tsx');
|
|
104
|
+
const pageJsx = path.join(routeDir, 'page.jsx');
|
|
105
|
+
if (fs.existsSync(pageTsx)) {
|
|
106
|
+
sourceFiles.push(pageTsx);
|
|
107
|
+
} else if (fs.existsSync(pageJsx)) {
|
|
108
|
+
sourceFiles.push(pageJsx);
|
|
109
|
+
} else {
|
|
110
|
+
throw new Error(`Page component not found for screen: ${screenId} at ${routeDir}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Phase 1: Scan all source files (page components)
|
|
114
|
+
for (const filePath of sourceFiles) {
|
|
115
|
+
await this.scanFile(filePath);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Phase 2: Scan detected components (unlimited depth by default)
|
|
119
|
+
const maxDepth = this.config.maxDepth !== undefined ? this.config.maxDepth : 999; // Default unlimited
|
|
120
|
+
const depthLabel = maxDepth === 999 ? 'unlimited' : maxDepth.toString();
|
|
121
|
+
if (this.config.verbose && this.detectedComponents.length > 0) {
|
|
122
|
+
console.log(` Detected ${this.detectedComponents.length} component(s), scanning with depth=${depthLabel}...`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
await this.scanComponentsRecursive(this.detectedComponents, 1, maxDepth);
|
|
126
|
+
|
|
127
|
+
// Build and return UI Model
|
|
128
|
+
const uiModel: UIModel = {
|
|
129
|
+
screenId,
|
|
130
|
+
routeId: routePath || 'home',
|
|
131
|
+
framework: this.config.framework,
|
|
132
|
+
sourceFiles: sourceFiles.map(f => path.relative(this.config.sourceRoot, f)),
|
|
133
|
+
elements: this.elements,
|
|
134
|
+
metadata: {
|
|
135
|
+
scannedAt: new Date().toISOString(),
|
|
136
|
+
version: '1.0.0',
|
|
137
|
+
hash: this.generateHash(this.elements)
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
return uiModel;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Scan a single file and extract UI elements
|
|
146
|
+
*/
|
|
147
|
+
private async scanFile(filePath: string): Promise<void> {
|
|
148
|
+
const sourceFileName = path.basename(filePath);
|
|
149
|
+
const importMap = new Map<string, string>(); // componentName → importPath
|
|
150
|
+
|
|
151
|
+
if (this.config.verbose) {
|
|
152
|
+
console.log(` Scanning: ${filePath}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const code = fs.readFileSync(filePath, 'utf-8');
|
|
156
|
+
|
|
157
|
+
// Parse with Babel
|
|
158
|
+
const ast = parser.parse(code, {
|
|
159
|
+
sourceType: 'module',
|
|
160
|
+
plugins: ['typescript', 'jsx']
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Traverse AST to extract imports and JSX elements
|
|
164
|
+
traverse(ast, {
|
|
165
|
+
// Detect imports
|
|
166
|
+
ImportDeclaration: (path: NodePath<t.ImportDeclaration>) => {
|
|
167
|
+
const importPath = path.node.source.value;
|
|
168
|
+
|
|
169
|
+
// Extract default import: import ChatInput from '...'
|
|
170
|
+
const defaultSpecifier = path.node.specifiers.find(s => t.isImportDefaultSpecifier(s));
|
|
171
|
+
if (defaultSpecifier && t.isImportDefaultSpecifier(defaultSpecifier)) {
|
|
172
|
+
const componentName = defaultSpecifier.local.name;
|
|
173
|
+
importMap.set(componentName, importPath);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Extract named imports: import { Button } from '...'
|
|
177
|
+
path.node.specifiers
|
|
178
|
+
.filter(s => t.isImportSpecifier(s))
|
|
179
|
+
.forEach(specifier => {
|
|
180
|
+
if (t.isImportSpecifier(specifier) && t.isIdentifier(specifier.imported)) {
|
|
181
|
+
const componentName = specifier.imported.name;
|
|
182
|
+
importMap.set(componentName, importPath);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
// Extract JSX elements
|
|
188
|
+
JSXElement: (path: NodePath<t.JSXElement>) => {
|
|
189
|
+
this.extractElement(path, sourceFileName, filePath, importMap);
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
// PHASE 1: Detect conditional rendering patterns
|
|
193
|
+
JSXExpressionContainer: (path: NodePath<t.JSXExpressionContainer>) => {
|
|
194
|
+
this.detectConditionalElements(path, sourceFileName, filePath, importMap);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Smart element filter - determines if element should be scanned
|
|
201
|
+
*/
|
|
202
|
+
private shouldScanElement(
|
|
203
|
+
tagName: string,
|
|
204
|
+
attributes: Record<string, string>,
|
|
205
|
+
textContent: string | null,
|
|
206
|
+
path?: NodePath<t.JSXElement>
|
|
207
|
+
): boolean {
|
|
208
|
+
const lowercaseTag = tagName.toLowerCase();
|
|
209
|
+
|
|
210
|
+
// 1. Always scan form elements
|
|
211
|
+
const formElements = ['input', 'button', 'textarea', 'select', 'form'];
|
|
212
|
+
if (formElements.includes(lowercaseTag)) return true;
|
|
213
|
+
|
|
214
|
+
// 2. Always scan links (a, Link)
|
|
215
|
+
if (lowercaseTag === 'a' || tagName === 'Link') return true;
|
|
216
|
+
|
|
217
|
+
// 3. Scan headings with text content
|
|
218
|
+
if (/^h[1-6]$/.test(lowercaseTag) && textContent) return true;
|
|
219
|
+
|
|
220
|
+
// 4. Scan images with alt text
|
|
221
|
+
if (lowercaseTag === 'img' && attributes['alt']) return true;
|
|
222
|
+
|
|
223
|
+
// 5. Scan elements with identifying attributes
|
|
224
|
+
const hasIdentifier =
|
|
225
|
+
attributes['id'] ||
|
|
226
|
+
attributes['name'] ||
|
|
227
|
+
attributes['data-testid'] ||
|
|
228
|
+
attributes['data-test-id'] ||
|
|
229
|
+
attributes['aria-label'] ||
|
|
230
|
+
attributes['ariaLabel'];
|
|
231
|
+
if (hasIdentifier) return true;
|
|
232
|
+
|
|
233
|
+
// 6. Scan semantic components (ChatInput, SubmitButton, UserProfile, etc.)
|
|
234
|
+
// Pattern: *Input, *Field, *TextArea, *Select, *Button, *Link, *Form
|
|
235
|
+
const semanticPatterns = [
|
|
236
|
+
/Input$/, // ChatInput, MessageInput
|
|
237
|
+
/Field$/, // TextField, EmailField
|
|
238
|
+
/TextArea$/, // MessageTextArea
|
|
239
|
+
/Select$/, // CountrySelect
|
|
240
|
+
/Button$/, // SubmitButton, CancelButton
|
|
241
|
+
/Link$/, // NavLink
|
|
242
|
+
/Form$/, // LoginForm
|
|
243
|
+
/Avatar$/, // UserAvatar
|
|
244
|
+
/Icon$/, // SearchIcon
|
|
245
|
+
/Image$/, // ProfileImage
|
|
246
|
+
/Card$/, // UserCard
|
|
247
|
+
/List$/, // MessageList
|
|
248
|
+
/Item$/ // ListItem
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
if (semanticPatterns.some(pattern => pattern.test(tagName))) {
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 7. Scan text containers with content
|
|
256
|
+
const textContainers = ['p', 'span', 'div', 'section', 'article'];
|
|
257
|
+
if (textContainers.includes(lowercaseTag) && textContent && textContent.length > 0) {
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 7.5. PHASE 1.5: Scan error/success message elements by className pattern (even without text)
|
|
262
|
+
const className = attributes['className'] || '';
|
|
263
|
+
const isErrorOrSuccessMessage =
|
|
264
|
+
className.includes('text-red') ||
|
|
265
|
+
className.includes('text-green') ||
|
|
266
|
+
className.includes('bg-red') ||
|
|
267
|
+
className.includes('bg-green') ||
|
|
268
|
+
className.includes('error') ||
|
|
269
|
+
className.includes('success') ||
|
|
270
|
+
className.includes('alert');
|
|
271
|
+
|
|
272
|
+
if (textContainers.includes(lowercaseTag) && isErrorOrSuccessMessage) {
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// 7.6. PHASE 1.6: Scan timestamp/metadata elements by className pattern
|
|
277
|
+
// Fix for elements with dynamic expressions like {timestamp.toLocaleTimeString()}
|
|
278
|
+
const isTimestampOrMetadata =
|
|
279
|
+
className.includes('timestamp') ||
|
|
280
|
+
className.includes('time') ||
|
|
281
|
+
(className.includes('text-xs') && className.includes('text-gray')) ||
|
|
282
|
+
(className.includes('text-sm') && className.includes('text-gray') && className.includes('mt-'));
|
|
283
|
+
|
|
284
|
+
if (textContainers.includes(lowercaseTag) && isTimestampOrMetadata) {
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// 8. NEW: Smart container detection for structural elements
|
|
289
|
+
if (['div', 'section', 'article', 'main', 'aside', 'nav'].includes(lowercaseTag)) {
|
|
290
|
+
if (path && this.shouldScanContainer(tagName, attributes, path)) {
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return false; // Skip everything else
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Smart container detection - checks if container is structurally important
|
|
300
|
+
* NEW: Phase 1 - Task 1
|
|
301
|
+
*/
|
|
302
|
+
private shouldScanContainer(
|
|
303
|
+
tagName: string,
|
|
304
|
+
attributes: Record<string, string>,
|
|
305
|
+
path: NodePath<t.JSXElement>
|
|
306
|
+
): boolean {
|
|
307
|
+
|
|
308
|
+
// 1. Has important children (form elements, buttons, custom components)?
|
|
309
|
+
const children = path.node.children.filter(child => t.isJSXElement(child)) as t.JSXElement[];
|
|
310
|
+
|
|
311
|
+
for (const child of children) {
|
|
312
|
+
const childTag = this.getTagName(child.openingElement.name);
|
|
313
|
+
const childTagLower = childTag.toLowerCase();
|
|
314
|
+
|
|
315
|
+
// Has form elements
|
|
316
|
+
if (['input', 'button', 'textarea', 'select', 'form'].includes(childTagLower)) {
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Has custom components (PascalCase)
|
|
321
|
+
if (/^[A-Z]/.test(childTag)) {
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// 2. Semantic className keywords
|
|
327
|
+
const className = attributes['className'] || '';
|
|
328
|
+
const semanticKeywords = [
|
|
329
|
+
'message', 'messages', 'chat',
|
|
330
|
+
'list', 'items',
|
|
331
|
+
'container', 'wrapper', 'content',
|
|
332
|
+
'scroll', 'overflow',
|
|
333
|
+
'grid', 'flex'
|
|
334
|
+
];
|
|
335
|
+
|
|
336
|
+
const hasSemanticClass = semanticKeywords.some(keyword =>
|
|
337
|
+
className.toLowerCase().includes(keyword)
|
|
338
|
+
);
|
|
339
|
+
if (hasSemanticClass) {
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// 3. Has scroll behavior (overflow-auto, overflow-y-auto, etc.)
|
|
344
|
+
if (className.includes('overflow')) {
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// 4. Has ARIA role
|
|
349
|
+
if (attributes['role']) {
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// 5. Has ref (often used for important containers)
|
|
354
|
+
if (attributes['ref']) {
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Extract UI element from JSX node
|
|
363
|
+
*/
|
|
364
|
+
private extractElement(
|
|
365
|
+
path: NodePath<t.JSXElement>,
|
|
366
|
+
sourceFile: string,
|
|
367
|
+
fullFilePath: string,
|
|
368
|
+
importMap: Map<string, string>
|
|
369
|
+
): void {
|
|
370
|
+
const openingElement = path.node.openingElement;
|
|
371
|
+
const tagName = this.getTagName(openingElement.name);
|
|
372
|
+
|
|
373
|
+
// Extract attributes and text first (needed for filtering)
|
|
374
|
+
const attributes = this.extractAttributes(openingElement.attributes);
|
|
375
|
+
const textContent = this.extractTextContent(path.node);
|
|
376
|
+
|
|
377
|
+
// Check if this is a custom component (starts with uppercase)
|
|
378
|
+
const isCustomComponent = /^[A-Z]/.test(tagName);
|
|
379
|
+
|
|
380
|
+
// If it's a custom component and we have an import for it, track it for scanning
|
|
381
|
+
// UPDATED: Phase 1 - Task 2 - Track ALL PascalCase components, not just semantic patterns
|
|
382
|
+
if (isCustomComponent && importMap.has(tagName)) {
|
|
383
|
+
const importPath = importMap.get(tagName)!;
|
|
384
|
+
|
|
385
|
+
// Check if not already tracked
|
|
386
|
+
const alreadyTracked = this.detectedComponents.some(c => c.name === tagName && c.importPath === importPath);
|
|
387
|
+
if (!alreadyTracked) {
|
|
388
|
+
this.detectedComponents.push({
|
|
389
|
+
name: tagName,
|
|
390
|
+
importPath,
|
|
391
|
+
sourceFile: fullFilePath
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
if (this.config.verbose) {
|
|
395
|
+
console.log(` Found component: ${tagName} from ${importPath}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Use smart filter instead of static whitelist (pass path for container detection)
|
|
401
|
+
if (!this.shouldScanElement(tagName, attributes, textContent, path)) {
|
|
402
|
+
return; // Skip non-scannable elements
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Build UI Element
|
|
406
|
+
const element: UIElement = {
|
|
407
|
+
key: `e${++this.elementCounter}`,
|
|
408
|
+
tag: tagName.toLowerCase(),
|
|
409
|
+
role: attributes['role'] || this.inferRole(tagName),
|
|
410
|
+
name: attributes['name'],
|
|
411
|
+
id: attributes['id'],
|
|
412
|
+
placeholder: attributes['placeholder'],
|
|
413
|
+
ariaLabel: attributes['aria-label'] || attributes['ariaLabel'],
|
|
414
|
+
testId: attributes['data-testid'] || attributes['data-test-id'],
|
|
415
|
+
text: textContent || attributes['value'],
|
|
416
|
+
source: sourceFile,
|
|
417
|
+
props: attributes
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// Add label if associated
|
|
421
|
+
if (attributes['aria-labelledby']) {
|
|
422
|
+
element.label = `[ref:${attributes['aria-labelledby']}]`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
this.elements.push(element);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* PHASE 1: Detect and extract conditionally rendered elements
|
|
430
|
+
* Handles patterns: {condition && <Element />}, {condition ? <A /> : <B />}, {arr.map(...)}
|
|
431
|
+
* PHASE 1.5: Now also traverses nested children
|
|
432
|
+
*/
|
|
433
|
+
private detectConditionalElements(
|
|
434
|
+
path: NodePath<t.JSXExpressionContainer>,
|
|
435
|
+
sourceFile: string,
|
|
436
|
+
fullFilePath: string,
|
|
437
|
+
importMap: Map<string, string>
|
|
438
|
+
): void {
|
|
439
|
+
const expr = path.node.expression;
|
|
440
|
+
|
|
441
|
+
// Pattern 1: Logical AND (condition && <Element />)
|
|
442
|
+
if (t.isLogicalExpression(expr, { operator: '&&' })) {
|
|
443
|
+
const rightSide = expr.right;
|
|
444
|
+
|
|
445
|
+
if (t.isJSXElement(rightSide)) {
|
|
446
|
+
// Extract the direct element
|
|
447
|
+
this.extractConditionalElement(rightSide, sourceFile, fullFilePath, importMap, 'conditional-and');
|
|
448
|
+
|
|
449
|
+
// PHASE 1.5: Also traverse nested children
|
|
450
|
+
this.traverseConditionalChildren(rightSide, sourceFile, fullFilePath, importMap, 'conditional-nested');
|
|
451
|
+
} else if (t.isJSXFragment(rightSide)) {
|
|
452
|
+
// Handle {condition && (<><Element1 /><Element2 /></>)}
|
|
453
|
+
this.extractFragmentElements(rightSide, sourceFile, fullFilePath, importMap, 'conditional-and');
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Pattern 2: Ternary operator (condition ? <A /> : <B />)
|
|
458
|
+
if (t.isConditionalExpression(expr)) {
|
|
459
|
+
const consequent = expr.consequent;
|
|
460
|
+
const alternate = expr.alternate;
|
|
461
|
+
|
|
462
|
+
if (t.isJSXElement(consequent)) {
|
|
463
|
+
this.extractConditionalElement(consequent, sourceFile, fullFilePath, importMap, 'conditional-true');
|
|
464
|
+
// PHASE 1.5: Traverse nested children
|
|
465
|
+
this.traverseConditionalChildren(consequent, sourceFile, fullFilePath, importMap, 'conditional-nested');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (t.isJSXElement(alternate)) {
|
|
469
|
+
this.extractConditionalElement(alternate, sourceFile, fullFilePath, importMap, 'conditional-false');
|
|
470
|
+
// PHASE 1.5: Traverse nested children
|
|
471
|
+
this.traverseConditionalChildren(alternate, sourceFile, fullFilePath, importMap, 'conditional-nested');
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Pattern 3: Array.map() rendering (arr.map(item => <Element />))
|
|
476
|
+
if (t.isCallExpression(expr)) {
|
|
477
|
+
const callee = expr.callee;
|
|
478
|
+
|
|
479
|
+
// Check if it's a .map() call
|
|
480
|
+
if (t.isMemberExpression(callee) && t.isIdentifier(callee.property, { name: 'map' })) {
|
|
481
|
+
const mapCallback = expr.arguments[0];
|
|
482
|
+
|
|
483
|
+
if (t.isArrowFunctionExpression(mapCallback) || t.isFunctionExpression(mapCallback)) {
|
|
484
|
+
const body = mapCallback.body;
|
|
485
|
+
|
|
486
|
+
// Case 1: Direct return - arr.map(item => <Element />)
|
|
487
|
+
if (t.isJSXElement(body)) {
|
|
488
|
+
this.extractConditionalElement(body, sourceFile, fullFilePath, importMap, 'loop-item');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Case 2: Block with return - arr.map(item => { return <Element /> })
|
|
492
|
+
if (t.isBlockStatement(body)) {
|
|
493
|
+
for (const statement of body.body) {
|
|
494
|
+
if (t.isReturnStatement(statement) && statement.argument && t.isJSXElement(statement.argument)) {
|
|
495
|
+
this.extractConditionalElement(statement.argument, sourceFile, fullFilePath, importMap, 'loop-item');
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Extract elements from JSX Fragment
|
|
506
|
+
*/
|
|
507
|
+
private extractFragmentElements(
|
|
508
|
+
fragment: t.JSXFragment,
|
|
509
|
+
sourceFile: string,
|
|
510
|
+
fullFilePath: string,
|
|
511
|
+
importMap: Map<string, string>,
|
|
512
|
+
renderType: string
|
|
513
|
+
): void {
|
|
514
|
+
for (const child of fragment.children) {
|
|
515
|
+
if (t.isJSXElement(child)) {
|
|
516
|
+
this.extractConditionalElement(child, sourceFile, fullFilePath, importMap, renderType);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* PHASE 1.5: Traverse nested children of conditional elements
|
|
523
|
+
* Example: {error && <div><p>{error}</p></div>} - detect the nested <p> tag
|
|
524
|
+
*/
|
|
525
|
+
private traverseConditionalChildren(
|
|
526
|
+
element: t.JSXElement,
|
|
527
|
+
sourceFile: string,
|
|
528
|
+
fullFilePath: string,
|
|
529
|
+
importMap: Map<string, string>,
|
|
530
|
+
renderType: string
|
|
531
|
+
): void {
|
|
532
|
+
for (const child of element.children) {
|
|
533
|
+
// Traverse nested JSX elements
|
|
534
|
+
if (t.isJSXElement(child)) {
|
|
535
|
+
const openingElement = child.openingElement;
|
|
536
|
+
const tagName = this.getTagName(openingElement.name);
|
|
537
|
+
const attributes = this.extractAttributes(openingElement.attributes);
|
|
538
|
+
const textContent = this.extractTextContent(child);
|
|
539
|
+
|
|
540
|
+
// Check if this nested element should be scanned
|
|
541
|
+
if (this.shouldScanElement(tagName, attributes, textContent, undefined)) {
|
|
542
|
+
this.extractConditionalElement(child, sourceFile, fullFilePath, importMap, renderType);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Recursively traverse deeper children
|
|
546
|
+
this.traverseConditionalChildren(child, sourceFile, fullFilePath, importMap, renderType);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Traverse JSX expression containers (for nested dynamic content)
|
|
550
|
+
if (t.isJSXExpressionContainer(child)) {
|
|
551
|
+
const expr = child.expression;
|
|
552
|
+
|
|
553
|
+
// Handle nested JSX elements in expressions
|
|
554
|
+
if (t.isJSXElement(expr)) {
|
|
555
|
+
this.extractConditionalElement(expr, sourceFile, fullFilePath, importMap, renderType);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Extract a conditionally rendered JSX element
|
|
563
|
+
*/
|
|
564
|
+
private extractConditionalElement(
|
|
565
|
+
jsxElement: t.JSXElement,
|
|
566
|
+
sourceFile: string,
|
|
567
|
+
fullFilePath: string,
|
|
568
|
+
importMap: Map<string, string>,
|
|
569
|
+
renderType: string
|
|
570
|
+
): void {
|
|
571
|
+
const openingElement = jsxElement.openingElement;
|
|
572
|
+
const tagName = this.getTagName(openingElement.name);
|
|
573
|
+
|
|
574
|
+
// Extract attributes and text
|
|
575
|
+
const attributes = this.extractAttributes(openingElement.attributes);
|
|
576
|
+
const textContent = this.extractTextContent(jsxElement);
|
|
577
|
+
|
|
578
|
+
// Check if this is a custom component
|
|
579
|
+
const isCustomComponent = /^[A-Z]/.test(tagName);
|
|
580
|
+
if (isCustomComponent && importMap.has(tagName)) {
|
|
581
|
+
const importPath = importMap.get(tagName)!;
|
|
582
|
+
const alreadyTracked = this.detectedComponents.some(c => c.name === tagName && c.importPath === importPath);
|
|
583
|
+
if (!alreadyTracked) {
|
|
584
|
+
this.detectedComponents.push({
|
|
585
|
+
name: tagName,
|
|
586
|
+
importPath,
|
|
587
|
+
sourceFile: fullFilePath
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Use smart filter to decide if we should scan this element
|
|
593
|
+
if (!this.shouldScanElement(tagName, attributes, textContent, undefined)) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Build UI Element with conditional marker
|
|
598
|
+
const element: UIElement = {
|
|
599
|
+
key: `e${++this.elementCounter}`,
|
|
600
|
+
tag: tagName.toLowerCase(),
|
|
601
|
+
role: attributes['role'] || this.inferRole(tagName),
|
|
602
|
+
name: attributes['name'],
|
|
603
|
+
id: attributes['id'],
|
|
604
|
+
placeholder: attributes['placeholder'],
|
|
605
|
+
ariaLabel: attributes['aria-label'] || attributes['ariaLabel'],
|
|
606
|
+
testId: attributes['data-testid'] || attributes['data-test-id'],
|
|
607
|
+
text: textContent || attributes['value'],
|
|
608
|
+
source: sourceFile,
|
|
609
|
+
props: {
|
|
610
|
+
...attributes,
|
|
611
|
+
isConditional: renderType // Mark as conditional with type
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
if (attributes['aria-labelledby']) {
|
|
616
|
+
element.label = `[ref:${attributes['aria-labelledby']}]`;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
this.elements.push(element);
|
|
620
|
+
|
|
621
|
+
if (this.config.verbose) {
|
|
622
|
+
console.log(` Found conditional element: ${tagName} (${renderType})`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Get tag name from JSX element
|
|
628
|
+
*/
|
|
629
|
+
private getTagName(name: t.JSXElement['openingElement']['name']): string {
|
|
630
|
+
if (t.isJSXIdentifier(name)) {
|
|
631
|
+
return name.name;
|
|
632
|
+
}
|
|
633
|
+
if (t.isJSXMemberExpression(name)) {
|
|
634
|
+
// Handle cases like <Form.Input />
|
|
635
|
+
return this.getTagName(name.property);
|
|
636
|
+
}
|
|
637
|
+
return 'unknown';
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Extract attributes from JSX element
|
|
642
|
+
*/
|
|
643
|
+
private extractAttributes(attributes: (t.JSXAttribute | t.JSXSpreadAttribute)[]): Record<string, string> {
|
|
644
|
+
const result: Record<string, string> = {};
|
|
645
|
+
|
|
646
|
+
for (const attr of attributes) {
|
|
647
|
+
if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) {
|
|
648
|
+
const name = attr.name.name;
|
|
649
|
+
const value = this.getAttributeValue(attr.value);
|
|
650
|
+
if (value !== null) {
|
|
651
|
+
result[name] = value;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return result;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Get attribute value
|
|
661
|
+
*/
|
|
662
|
+
private getAttributeValue(value: t.JSXAttribute['value']): string | null {
|
|
663
|
+
if (!value) return null;
|
|
664
|
+
|
|
665
|
+
if (t.isStringLiteral(value)) {
|
|
666
|
+
return value.value;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (t.isJSXExpressionContainer(value)) {
|
|
670
|
+
const expr = value.expression;
|
|
671
|
+
|
|
672
|
+
// Handle string literals in expressions
|
|
673
|
+
if (t.isStringLiteral(expr)) {
|
|
674
|
+
return expr.value;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Handle template literals
|
|
678
|
+
if (t.isTemplateLiteral(expr) && expr.quasis.length === 1) {
|
|
679
|
+
return expr.quasis[0].value.cooked || '';
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// For complex expressions, return placeholder
|
|
683
|
+
return '[expression]';
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Extract text content from JSX element
|
|
691
|
+
* PHASE 1 ENHANCED: Extract text from dynamic expressions
|
|
692
|
+
*/
|
|
693
|
+
private extractTextContent(node: t.JSXElement): string | null {
|
|
694
|
+
const children = node.children;
|
|
695
|
+
const textParts: string[] = [];
|
|
696
|
+
|
|
697
|
+
for (const child of children) {
|
|
698
|
+
if (t.isJSXText(child)) {
|
|
699
|
+
const text = child.value.trim();
|
|
700
|
+
if (text) textParts.push(text);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (t.isJSXExpressionContainer(child)) {
|
|
704
|
+
const expr = child.expression;
|
|
705
|
+
|
|
706
|
+
// Static string literal
|
|
707
|
+
if (t.isStringLiteral(expr)) {
|
|
708
|
+
textParts.push(expr.value);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// PHASE 1: Extract text from ternary (condition ? 'Text A' : 'Text B')
|
|
712
|
+
if (t.isConditionalExpression(expr)) {
|
|
713
|
+
const texts: string[] = [];
|
|
714
|
+
|
|
715
|
+
if (t.isStringLiteral(expr.consequent)) {
|
|
716
|
+
texts.push(expr.consequent.value);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (t.isStringLiteral(expr.alternate)) {
|
|
720
|
+
texts.push(expr.alternate.value);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (texts.length > 0) {
|
|
724
|
+
textParts.push(texts.join(' / ')); // e.g., "Copy / Copied!"
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// PHASE 1: Extract text from logical OR (defaultValue || 'Fallback')
|
|
729
|
+
if (t.isLogicalExpression(expr, { operator: '||' })) {
|
|
730
|
+
if (t.isStringLiteral(expr.right)) {
|
|
731
|
+
textParts.push(expr.right.value);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return textParts.length > 0 ? textParts.join(' ') : null;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Infer ARIA role from tag name
|
|
742
|
+
*/
|
|
743
|
+
private inferRole(tagName: string): string | undefined {
|
|
744
|
+
const roleMap: Record<string, string> = {
|
|
745
|
+
'button': 'button',
|
|
746
|
+
'input': 'textbox',
|
|
747
|
+
'textarea': 'textbox',
|
|
748
|
+
'select': 'combobox',
|
|
749
|
+
'a': 'link',
|
|
750
|
+
'form': 'form'
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
return roleMap[tagName.toLowerCase()];
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Resolve component import path to actual file path
|
|
758
|
+
*/
|
|
759
|
+
private resolveComponentPath(importPath: string, sourceFile: string): string | null {
|
|
760
|
+
try {
|
|
761
|
+
// Handle different import path formats
|
|
762
|
+
let resolvedPath: string;
|
|
763
|
+
|
|
764
|
+
if (importPath.startsWith('@/')) {
|
|
765
|
+
// Alias import: @/components/ChatInput → PROJECT_ROOT/components/ChatInput.tsx
|
|
766
|
+
// Note: @ alias points to project root, not sourceRoot (app/)
|
|
767
|
+
const projectRoot = path.resolve(this.config.sourceRoot, '..');
|
|
768
|
+
const relativePath = importPath.replace('@/', '');
|
|
769
|
+
resolvedPath = path.join(projectRoot, relativePath);
|
|
770
|
+
} else if (importPath.startsWith('./') || importPath.startsWith('../')) {
|
|
771
|
+
// Relative import: ./components/ChatInput
|
|
772
|
+
const sourceDir = path.dirname(sourceFile);
|
|
773
|
+
resolvedPath = path.join(sourceDir, importPath);
|
|
774
|
+
} else {
|
|
775
|
+
// Absolute or node_modules import - skip
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Try different file extensions
|
|
780
|
+
const extensions = ['.tsx', '.ts', '.jsx', '.js'];
|
|
781
|
+
for (const ext of extensions) {
|
|
782
|
+
const fullPath = resolvedPath + ext;
|
|
783
|
+
if (fs.existsSync(fullPath)) {
|
|
784
|
+
return fullPath;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Try index files
|
|
789
|
+
for (const ext of extensions) {
|
|
790
|
+
const indexPath = path.join(resolvedPath, `index${ext}`);
|
|
791
|
+
if (fs.existsSync(indexPath)) {
|
|
792
|
+
return indexPath;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
return null;
|
|
797
|
+
} catch (error) {
|
|
798
|
+
if (this.config.verbose) {
|
|
799
|
+
console.log(` Could not resolve: ${importPath}`);
|
|
800
|
+
}
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* NEW: Recursively scan components at multiple depths
|
|
807
|
+
* @param components - List of components to scan
|
|
808
|
+
* @param currentDepth - Current depth level
|
|
809
|
+
* @param maxDepth - Maximum depth to scan
|
|
810
|
+
*/
|
|
811
|
+
private async scanComponentsRecursive(
|
|
812
|
+
components: ComponentInfo[],
|
|
813
|
+
currentDepth: number,
|
|
814
|
+
maxDepth: number
|
|
815
|
+
): Promise<void> {
|
|
816
|
+
if (currentDepth > maxDepth) {
|
|
817
|
+
return; // Stop recursion at max depth
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const startTime = Date.now();
|
|
821
|
+
const componentsScanned: ComponentInfo[] = [];
|
|
822
|
+
|
|
823
|
+
// Scan all components at current level
|
|
824
|
+
for (const component of components) {
|
|
825
|
+
const componentPath = this.resolveComponentPath(component.importPath, component.sourceFile);
|
|
826
|
+
|
|
827
|
+
if (!componentPath) {
|
|
828
|
+
continue; // Skip if path cannot be resolved
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Skip node_modules
|
|
832
|
+
if (componentPath.includes('node_modules')) {
|
|
833
|
+
if (this.config.verbose) {
|
|
834
|
+
console.log(` Skipping node_modules: ${componentPath}`);
|
|
835
|
+
}
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Skip if already scanned (avoid duplicates)
|
|
840
|
+
if (this.scannedComponentPaths.has(componentPath)) {
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Mark as scanned
|
|
845
|
+
this.scannedComponentPaths.add(componentPath);
|
|
846
|
+
|
|
847
|
+
// Store the count of detected components before scanning
|
|
848
|
+
const detectedCountBefore = this.detectedComponents.length;
|
|
849
|
+
|
|
850
|
+
// Scan the component file
|
|
851
|
+
await this.scanComponentFile(componentPath, component.name);
|
|
852
|
+
|
|
853
|
+
// Track newly detected components from this scan
|
|
854
|
+
const newlyDetected = this.detectedComponents.slice(detectedCountBefore);
|
|
855
|
+
componentsScanned.push(...newlyDetected);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const elapsed = Date.now() - startTime;
|
|
859
|
+
if (this.config.verbose && componentsScanned.length > 0) {
|
|
860
|
+
console.log(` [Depth ${currentDepth}] Scanned ${components.length} component(s), found ${componentsScanned.length} sub-component(s) (${elapsed}ms)`);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Recursively scan newly detected components at next depth
|
|
864
|
+
if (componentsScanned.length > 0 && currentDepth < maxDepth) {
|
|
865
|
+
await this.scanComponentsRecursive(componentsScanned, currentDepth + 1, maxDepth);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Scan component file and extract elements
|
|
871
|
+
*/
|
|
872
|
+
private async scanComponentFile(componentPath: string, componentName: string): Promise<void> {
|
|
873
|
+
if (this.config.verbose) {
|
|
874
|
+
console.log(` Scanning component: ${componentName} at ${componentPath}`);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const code = fs.readFileSync(componentPath, 'utf-8');
|
|
878
|
+
|
|
879
|
+
// Parse with Babel
|
|
880
|
+
const ast = parser.parse(code, {
|
|
881
|
+
sourceType: 'module',
|
|
882
|
+
plugins: ['typescript', 'jsx']
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
const importMap = new Map<string, string>(); // Empty import map for component scanning
|
|
886
|
+
|
|
887
|
+
// Traverse AST and extract JSX elements (no nested component scanning)
|
|
888
|
+
traverse(ast, {
|
|
889
|
+
JSXElement: (path: NodePath<t.JSXElement>) => {
|
|
890
|
+
const openingElement = path.node.openingElement;
|
|
891
|
+
const tagName = this.getTagName(openingElement.name);
|
|
892
|
+
|
|
893
|
+
// Extract attributes and text
|
|
894
|
+
const attributes = this.extractAttributes(openingElement.attributes);
|
|
895
|
+
const textContent = this.extractTextContent(path.node);
|
|
896
|
+
|
|
897
|
+
// Use smart filter (pass path for container detection)
|
|
898
|
+
if (!this.shouldScanElement(tagName, attributes, textContent, path)) {
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Build UI Element with component context
|
|
903
|
+
const element: UIElement = {
|
|
904
|
+
key: `e${++this.elementCounter}`,
|
|
905
|
+
tag: tagName.toLowerCase(),
|
|
906
|
+
role: attributes['role'] || this.inferRole(tagName),
|
|
907
|
+
name: attributes['name'],
|
|
908
|
+
id: attributes['id'],
|
|
909
|
+
placeholder: attributes['placeholder'],
|
|
910
|
+
ariaLabel: attributes['aria-label'] || attributes['ariaLabel'],
|
|
911
|
+
testId: attributes['data-testid'] || attributes['data-test-id'],
|
|
912
|
+
text: textContent || attributes['value'],
|
|
913
|
+
source: `${componentName}.tsx`, // Mark as from component
|
|
914
|
+
props: attributes
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
// Add label if associated
|
|
918
|
+
if (attributes['aria-labelledby']) {
|
|
919
|
+
element.label = `[ref:${attributes['aria-labelledby']}]`;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
this.elements.push(element);
|
|
923
|
+
},
|
|
924
|
+
|
|
925
|
+
// PHASE 1: Detect conditional rendering in components too
|
|
926
|
+
JSXExpressionContainer: (path: NodePath<t.JSXExpressionContainer>) => {
|
|
927
|
+
this.detectConditionalElements(path, `${componentName}.tsx`, componentPath, importMap);
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Generate hash for cache key
|
|
934
|
+
*/
|
|
935
|
+
private generateHash(elements: UIElement[]): string {
|
|
936
|
+
const crypto = require('crypto');
|
|
937
|
+
const content = JSON.stringify(elements);
|
|
938
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// ============================================================================
|
|
943
|
+
// Factory Function
|
|
944
|
+
// ============================================================================
|
|
945
|
+
|
|
946
|
+
export async function buildUIModel(
|
|
947
|
+
screenId: string,
|
|
948
|
+
config: ScanConfig
|
|
949
|
+
): Promise<UIModel> {
|
|
950
|
+
const scanner = new ReactScanner(config);
|
|
951
|
+
return scanner.buildUIModel(screenId);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
export async function discoverScreens(
|
|
955
|
+
config: ScanConfig
|
|
956
|
+
): Promise<string[]> {
|
|
957
|
+
const scanner = new ReactScanner(config);
|
|
958
|
+
return scanner.discoverScreens();
|
|
959
|
+
}
|