@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,1244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRODUCTION-READY Deep Scanner
|
|
3
|
+
* Implements all 4 iterations:
|
|
4
|
+
* 1. Deep Scanning (4+ levels)
|
|
5
|
+
* 2. UI Library Handling
|
|
6
|
+
* 3. Props Merging
|
|
7
|
+
* 4. Smart Filtering
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as parser from '@babel/parser';
|
|
11
|
+
import traverse from '@babel/traverse';
|
|
12
|
+
import * as t from '@babel/types';
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
import { UIElement } from '../types';
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Configuration
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
interface DeepScanConfig {
|
|
22
|
+
sourceRoot: string;
|
|
23
|
+
projectRoot: string;
|
|
24
|
+
maxDepth: number;
|
|
25
|
+
verbose?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Framework Components Registry
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
interface FrameworkComponentDef {
|
|
33
|
+
convertTo: string; // Target DOM element (e.g., 'a', 'img')
|
|
34
|
+
role?: string; // ARIA role
|
|
35
|
+
propsMap: Record<string, string | ((attrs: any) => any)>; // Props mapping
|
|
36
|
+
defaultProps?: Record<string, any>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const FRAMEWORK_COMPONENTS: Record<string, Record<string, FrameworkComponentDef>> = {
|
|
40
|
+
// Next.js Framework Components
|
|
41
|
+
'next/link': {
|
|
42
|
+
Link: {
|
|
43
|
+
convertTo: 'a',
|
|
44
|
+
role: 'link',
|
|
45
|
+
propsMap: {
|
|
46
|
+
href: 'href',
|
|
47
|
+
// Next.js Link wraps children in <a>, so we preserve all props
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
'next/image': {
|
|
52
|
+
Image: {
|
|
53
|
+
convertTo: 'img',
|
|
54
|
+
role: 'img',
|
|
55
|
+
propsMap: {
|
|
56
|
+
src: 'src',
|
|
57
|
+
alt: 'alt',
|
|
58
|
+
width: 'width',
|
|
59
|
+
height: 'height',
|
|
60
|
+
loading: 'loading',
|
|
61
|
+
priority: (attrs: any) => attrs.priority ? 'eager' : 'lazy'
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
'next/script': {
|
|
66
|
+
Script: {
|
|
67
|
+
convertTo: 'script',
|
|
68
|
+
propsMap: {
|
|
69
|
+
src: 'src',
|
|
70
|
+
strategy: 'strategy',
|
|
71
|
+
onLoad: 'onload'
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
// React Router Components (for future support)
|
|
77
|
+
'react-router-dom': {
|
|
78
|
+
Link: {
|
|
79
|
+
convertTo: 'a',
|
|
80
|
+
role: 'link',
|
|
81
|
+
propsMap: {
|
|
82
|
+
to: 'href' // React Router uses 'to' instead of 'href'
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
NavLink: {
|
|
86
|
+
convertTo: 'a',
|
|
87
|
+
role: 'link',
|
|
88
|
+
propsMap: {
|
|
89
|
+
to: 'href'
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
// Remix Framework Components (for future support)
|
|
95
|
+
'remix': {
|
|
96
|
+
Link: {
|
|
97
|
+
convertTo: 'a',
|
|
98
|
+
role: 'link',
|
|
99
|
+
propsMap: {
|
|
100
|
+
to: 'href'
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
Form: {
|
|
104
|
+
convertTo: 'form',
|
|
105
|
+
role: 'form',
|
|
106
|
+
propsMap: {
|
|
107
|
+
action: 'action',
|
|
108
|
+
method: 'method'
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// UI libraries to scan (don't skip these)
|
|
115
|
+
const UI_LIBRARIES = [
|
|
116
|
+
'flowbite-react',
|
|
117
|
+
'@mui/material',
|
|
118
|
+
'@mui/icons-material',
|
|
119
|
+
'antd',
|
|
120
|
+
'react-bootstrap',
|
|
121
|
+
'@headlessui/react',
|
|
122
|
+
'@radix-ui',
|
|
123
|
+
'chakra-ui',
|
|
124
|
+
'@chakra-ui/react',
|
|
125
|
+
'semantic-ui-react',
|
|
126
|
+
'@fluentui/react'
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
// Skip these patterns (REFINED - don't skip framework UI components)
|
|
130
|
+
const SKIP_PATTERNS = [
|
|
131
|
+
/^react$/,
|
|
132
|
+
/^react-dom$/,
|
|
133
|
+
/^next$/, // Skip 'next' core package
|
|
134
|
+
/^next\/navigation$/, // Skip navigation hooks (not UI)
|
|
135
|
+
/^next\/headers$/, // Skip headers (not UI)
|
|
136
|
+
/^next\/server$/, // Skip server utilities (not UI)
|
|
137
|
+
/^next\/font/, // Skip font utilities (not UI)
|
|
138
|
+
// NOTE: We DON'T skip 'next/link', 'next/image', 'next/script' anymore!
|
|
139
|
+
/Icon$/,
|
|
140
|
+
/^Logo$/,
|
|
141
|
+
/^Copyright$/,
|
|
142
|
+
/^RenderIf$/,
|
|
143
|
+
/^Fragment$/
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
// Prioritize scanning these patterns
|
|
147
|
+
const PRIORITY_PATTERNS = [
|
|
148
|
+
/Input$/,
|
|
149
|
+
/Field$/,
|
|
150
|
+
/Select$/,
|
|
151
|
+
/Checkbox$/,
|
|
152
|
+
/Radio$/,
|
|
153
|
+
/Button$/,
|
|
154
|
+
/TextArea$/,
|
|
155
|
+
/Form$/,
|
|
156
|
+
/TextField$/,
|
|
157
|
+
/Switch$/,
|
|
158
|
+
/Slider$/
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
// ============================================================================
|
|
162
|
+
// Deep Scanner Class
|
|
163
|
+
// ============================================================================
|
|
164
|
+
|
|
165
|
+
export class DeepScanner {
|
|
166
|
+
private config: DeepScanConfig;
|
|
167
|
+
private elements: UIElement[] = [];
|
|
168
|
+
private elementCounter = 0;
|
|
169
|
+
private scannedFiles = new Set<string>(); // Track file+props combinations
|
|
170
|
+
private componentCache = new Map<string, any>(); // Cache parsed components
|
|
171
|
+
|
|
172
|
+
constructor(config: DeepScanConfig) {
|
|
173
|
+
this.config = config;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Main entry point - scan files deeply
|
|
178
|
+
*/
|
|
179
|
+
async scanFiles(filePaths: string[]): Promise<UIElement[]> {
|
|
180
|
+
this.elements = [];
|
|
181
|
+
this.elementCounter = 0;
|
|
182
|
+
this.scannedFiles.clear();
|
|
183
|
+
|
|
184
|
+
for (const filePath of filePaths) {
|
|
185
|
+
await this.scanFile(filePath, 0, {});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return this.elements;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Check if component should be scanned
|
|
193
|
+
*/
|
|
194
|
+
private shouldScanComponent(componentName: string, importPath: string): boolean {
|
|
195
|
+
// Skip patterns
|
|
196
|
+
if (SKIP_PATTERNS.some(pattern => pattern.test(componentName))) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (SKIP_PATTERNS.some(pattern => pattern.test(importPath))) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Always scan priority components
|
|
205
|
+
if (PRIORITY_PATTERNS.some(pattern => pattern.test(componentName))) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Scan UI library components
|
|
210
|
+
if (UI_LIBRARIES.some(lib => importPath.includes(lib))) {
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Scan custom components (starts with uppercase)
|
|
215
|
+
if (/^[A-Z]/.test(componentName)) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Check if this is an actual DOM element OR a UI library component
|
|
224
|
+
* We treat UI library components as terminal elements (don't scan deeper)
|
|
225
|
+
*/
|
|
226
|
+
private isActualDOMElement(tagName: string): boolean {
|
|
227
|
+
const domElements = [
|
|
228
|
+
'input', 'button', 'textarea', 'select', 'form',
|
|
229
|
+
'a', 'img', 'video', 'audio', 'canvas',
|
|
230
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
231
|
+
'p', 'span', 'div', 'section', 'article',
|
|
232
|
+
'table', 'tr', 'td', 'th', 'thead', 'tbody',
|
|
233
|
+
'ul', 'ol', 'li', 'dl', 'dt', 'dd',
|
|
234
|
+
'label', 'fieldset', 'legend'
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
return domElements.includes(tagName.toLowerCase());
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Check if this is a UI library component (flowbite, MUI, etc.)
|
|
242
|
+
* These are treated as "actual elements" for our purposes
|
|
243
|
+
*/
|
|
244
|
+
private isUILibraryComponent(tagName: string, importPath: string): boolean {
|
|
245
|
+
// Check if from UI library
|
|
246
|
+
const isFromUILibrary = UI_LIBRARIES.some(lib => importPath.includes(lib));
|
|
247
|
+
|
|
248
|
+
if (!isFromUILibrary) return false;
|
|
249
|
+
|
|
250
|
+
// For UI libraries, check if it's a form/interactive component
|
|
251
|
+
// (not layout/utility components like Flowbite, ThemeProvider, etc.)
|
|
252
|
+
const uiComponentPatterns = [
|
|
253
|
+
/Input/i, // TextInput, InputText, Input, EmailInput
|
|
254
|
+
/TextField/i,
|
|
255
|
+
/Button/i,
|
|
256
|
+
/Select/i,
|
|
257
|
+
/Checkbox/i,
|
|
258
|
+
/Radio/i,
|
|
259
|
+
/Switch/i,
|
|
260
|
+
/Slider/i,
|
|
261
|
+
/TextArea/i,
|
|
262
|
+
/Dropdown/i,
|
|
263
|
+
/Modal/i,
|
|
264
|
+
/Dialog/i,
|
|
265
|
+
/Picker/i, // DatePicker, TimePicker
|
|
266
|
+
/Toggle/i
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
const matchesPattern = uiComponentPatterns.some(pattern => pattern.test(tagName));
|
|
270
|
+
|
|
271
|
+
// Skip utility components
|
|
272
|
+
const utilityComponents = ['Flowbite', 'ThemeProvider', 'Label', 'HelperText'];
|
|
273
|
+
const isUtility = utilityComponents.some(util => tagName.includes(util));
|
|
274
|
+
|
|
275
|
+
return matchesPattern && !isUtility;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Check if component is a custom wrapper of UI library component
|
|
280
|
+
* E.g., Button wraps ButtonFlowbite, TextInput wraps InputText
|
|
281
|
+
* These should be treated as terminal elements (buttons/inputs)
|
|
282
|
+
*/
|
|
283
|
+
private isCustomUIWrapper(componentName: string, importPath: string): boolean {
|
|
284
|
+
// Not from UI library (it's custom)
|
|
285
|
+
const isFromUILib = UI_LIBRARIES.some(lib => importPath.includes(lib));
|
|
286
|
+
if (isFromUILib) return false;
|
|
287
|
+
|
|
288
|
+
// Must be from project's components
|
|
289
|
+
if (!importPath.includes('@/components')) return false;
|
|
290
|
+
|
|
291
|
+
// Match patterns that suggest it's a UI wrapper
|
|
292
|
+
const wrapperPatterns = [
|
|
293
|
+
/Button$/i, // Button, SubmitButton, etc.
|
|
294
|
+
/Input$/i, // TextInput, EmailInput, etc.
|
|
295
|
+
/Select$/i,
|
|
296
|
+
/Checkbox$/i,
|
|
297
|
+
/Radio$/i,
|
|
298
|
+
/TextArea$/i,
|
|
299
|
+
/Switch$/i,
|
|
300
|
+
/Slider$/i,
|
|
301
|
+
/Link$/i // Custom Link components
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
return wrapperPatterns.some(pattern => pattern.test(componentName));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Check if component is a framework component (Next.js, React Router, etc.)
|
|
309
|
+
* Returns component definition if found, null otherwise
|
|
310
|
+
*/
|
|
311
|
+
private getFrameworkComponent(componentName: string, importPath: string): FrameworkComponentDef | null {
|
|
312
|
+
// Check if import path is in FRAMEWORK_COMPONENTS registry
|
|
313
|
+
if (!FRAMEWORK_COMPONENTS[importPath]) {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Check if component name exists for this import path
|
|
318
|
+
const componentDef = FRAMEWORK_COMPONENTS[importPath][componentName];
|
|
319
|
+
return componentDef || null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Map framework component props to target DOM element props
|
|
324
|
+
*/
|
|
325
|
+
private mapFrameworkProps(
|
|
326
|
+
attrs: Record<string, any>,
|
|
327
|
+
propsMap: Record<string, string | ((attrs: any) => any)>
|
|
328
|
+
): Record<string, any> {
|
|
329
|
+
const mapped: Record<string, any> = {};
|
|
330
|
+
|
|
331
|
+
// Map each prop according to propsMap
|
|
332
|
+
for (const [sourceKey, targetKey] of Object.entries(propsMap)) {
|
|
333
|
+
if (attrs[sourceKey] !== undefined) {
|
|
334
|
+
if (typeof targetKey === 'function') {
|
|
335
|
+
// Custom mapping function
|
|
336
|
+
mapped[sourceKey] = targetKey(attrs);
|
|
337
|
+
} else {
|
|
338
|
+
// Direct prop name mapping
|
|
339
|
+
mapped[targetKey] = attrs[sourceKey];
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Preserve unmapped props that might be useful
|
|
345
|
+
const mappedKeys = new Set(Object.keys(propsMap));
|
|
346
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
347
|
+
if (!mappedKeys.has(key) && !mapped[key]) {
|
|
348
|
+
// Keep useful attributes like className, id, data-testid, etc.
|
|
349
|
+
if (['className', 'id', 'data-testid', 'aria-label', 'aria-labelledby', 'role'].includes(key)) {
|
|
350
|
+
mapped[key] = value;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return mapped;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Convert framework component to synthetic DOM element
|
|
360
|
+
*/
|
|
361
|
+
private convertFrameworkComponent(
|
|
362
|
+
componentName: string,
|
|
363
|
+
componentDef: FrameworkComponentDef,
|
|
364
|
+
attrs: Record<string, any>,
|
|
365
|
+
text: string | null,
|
|
366
|
+
sourceFileName: string
|
|
367
|
+
): UIElement {
|
|
368
|
+
// Map props according to component definition
|
|
369
|
+
const mappedProps = this.mapFrameworkProps(attrs, componentDef.propsMap);
|
|
370
|
+
|
|
371
|
+
// Apply default props if any
|
|
372
|
+
const finalProps = {
|
|
373
|
+
...componentDef.defaultProps,
|
|
374
|
+
...mappedProps
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
// Create synthetic element
|
|
378
|
+
const element: UIElement = {
|
|
379
|
+
key: `e${++this.elementCounter}`,
|
|
380
|
+
tag: componentDef.convertTo,
|
|
381
|
+
role: componentDef.role || this.inferRole(componentDef.convertTo),
|
|
382
|
+
id: finalProps.id,
|
|
383
|
+
name: finalProps.name,
|
|
384
|
+
placeholder: finalProps.placeholder,
|
|
385
|
+
ariaLabel: finalProps['aria-label'] || finalProps.ariaLabel,
|
|
386
|
+
testId: finalProps['data-testid'],
|
|
387
|
+
text: text,
|
|
388
|
+
source: sourceFileName,
|
|
389
|
+
props: finalProps
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// Add metadata to track this was converted from framework component
|
|
393
|
+
(element as any).metadata = {
|
|
394
|
+
frameworkComponent: componentName,
|
|
395
|
+
originalImport: 'framework' // Will be set by caller
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
return element;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Try to resolve a path with different file extensions
|
|
403
|
+
*/
|
|
404
|
+
private tryResolveWithExtensions(basePath: string): string | null {
|
|
405
|
+
const extensions = ['.tsx', '.ts', '.jsx', '.js'];
|
|
406
|
+
|
|
407
|
+
// Try direct file with extensions
|
|
408
|
+
for (const ext of extensions) {
|
|
409
|
+
const fullPath = basePath + ext;
|
|
410
|
+
if (fs.existsSync(fullPath)) {
|
|
411
|
+
return fullPath;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Try index files
|
|
416
|
+
for (const ext of extensions) {
|
|
417
|
+
const indexPath = path.join(basePath, `index${ext}`);
|
|
418
|
+
if (fs.existsSync(indexPath)) {
|
|
419
|
+
return indexPath;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Resolve import path to actual file path
|
|
428
|
+
* Enhanced to try multiple locations for @/ alias
|
|
429
|
+
*/
|
|
430
|
+
private resolveComponentPath(importPath: string, sourceFile: string): string | null {
|
|
431
|
+
try {
|
|
432
|
+
// Handle @ alias - Try multiple locations (Next.js, CRA, custom)
|
|
433
|
+
if (importPath.startsWith('@/')) {
|
|
434
|
+
const relativePath = importPath.replace('@/', '');
|
|
435
|
+
|
|
436
|
+
// Try multiple candidate directories in priority order
|
|
437
|
+
const candidates = [
|
|
438
|
+
path.join(this.config.projectRoot, 'src', relativePath), // CRA, Vite: src/
|
|
439
|
+
path.join(this.config.projectRoot, 'app', relativePath), // Next.js 13+: app/
|
|
440
|
+
path.join(this.config.projectRoot, relativePath), // Root level
|
|
441
|
+
path.join(this.config.sourceRoot, '..', relativePath), // Relative to sourceRoot
|
|
442
|
+
];
|
|
443
|
+
|
|
444
|
+
for (const candidate of candidates) {
|
|
445
|
+
const resolved = this.tryResolveWithExtensions(candidate);
|
|
446
|
+
if (resolved) {
|
|
447
|
+
if (this.config.verbose) {
|
|
448
|
+
console.log(` ✓ Resolved @/${relativePath} → ${path.relative(this.config.projectRoot, resolved)}`);
|
|
449
|
+
}
|
|
450
|
+
return resolved;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// If not found, log for debugging
|
|
455
|
+
if (this.config.verbose) {
|
|
456
|
+
console.log(` ⚠️ Could not resolve @/${relativePath} (tried: src/, app/, root/)`);
|
|
457
|
+
}
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Handle relative imports
|
|
462
|
+
else if (importPath.startsWith('./') || importPath.startsWith('../')) {
|
|
463
|
+
const sourceDir = path.dirname(sourceFile);
|
|
464
|
+
const resolvedPath = path.resolve(sourceDir, importPath);
|
|
465
|
+
return this.tryResolveWithExtensions(resolvedPath);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Handle UI library imports (node_modules)
|
|
469
|
+
else if (UI_LIBRARIES.some(lib => importPath.includes(lib))) {
|
|
470
|
+
const nodeModulesPath = path.join(this.config.projectRoot, 'node_modules', importPath);
|
|
471
|
+
|
|
472
|
+
// Try with extensions first
|
|
473
|
+
const resolved = this.tryResolveWithExtensions(nodeModulesPath);
|
|
474
|
+
if (resolved) return resolved;
|
|
475
|
+
|
|
476
|
+
// Try package.json main/module field
|
|
477
|
+
const pkgPath = path.join(nodeModulesPath, 'package.json');
|
|
478
|
+
if (fs.existsSync(pkgPath)) {
|
|
479
|
+
try {
|
|
480
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
481
|
+
const mainFile = pkg.module || pkg.main || 'index.js';
|
|
482
|
+
const mainPath = path.join(nodeModulesPath, mainFile);
|
|
483
|
+
if (fs.existsSync(mainPath)) {
|
|
484
|
+
return mainPath;
|
|
485
|
+
}
|
|
486
|
+
} catch (e) {
|
|
487
|
+
// Ignore package.json parse errors
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Skip other node_modules
|
|
495
|
+
else {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
} catch (error) {
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Extract JSX element from conditional expression (ignoring the condition)
|
|
505
|
+
* This allows us to discover elements in modals, conditional blocks, etc.
|
|
506
|
+
*/
|
|
507
|
+
private extractConditionalJSXElement(
|
|
508
|
+
jsxElement: any,
|
|
509
|
+
sourceFileName: string,
|
|
510
|
+
imports: Map<string, string>,
|
|
511
|
+
componentProps: Record<string, any>,
|
|
512
|
+
depth: number,
|
|
513
|
+
detectedComponents: Array<{ name: string; importPath: string; props: Record<string, any>; isProjectComponent?: boolean }>
|
|
514
|
+
): void {
|
|
515
|
+
const openingElement = jsxElement.openingElement;
|
|
516
|
+
let tagName = '';
|
|
517
|
+
|
|
518
|
+
if (openingElement.name.type === 'JSXIdentifier') {
|
|
519
|
+
tagName = openingElement.name.name;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (this.config.verbose && depth === 2) {
|
|
523
|
+
console.log(` ${' '.repeat(depth)} 🔄 Conditional JSX: <${tagName}>`);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Extract attributes and merge with component props
|
|
527
|
+
const attrs = this.extractAttributes(openingElement.attributes, componentProps);
|
|
528
|
+
const text = this.extractTextContent(jsxElement);
|
|
529
|
+
const mergedAttrs = this.mergeProps(componentProps, attrs);
|
|
530
|
+
|
|
531
|
+
// Check if custom component (Modal, Button, Input, etc.)
|
|
532
|
+
if (/^[A-Z]/.test(tagName) && imports.has(tagName)) {
|
|
533
|
+
const importPath = imports.get(tagName)!;
|
|
534
|
+
|
|
535
|
+
// Check if it's a project component that should be deep scanned
|
|
536
|
+
if (importPath.startsWith('@/') || importPath.startsWith('./') || importPath.startsWith('../')) {
|
|
537
|
+
if (this.config.verbose) {
|
|
538
|
+
console.log(` ${' '.repeat(depth)} 🔄 Conditional component: <${tagName}> - adding to scan queue`);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Add to detectedComponents queue for deep scanning
|
|
542
|
+
// This ensures <Input>, <Button>, etc. get fully scanned
|
|
543
|
+
detectedComponents.push({
|
|
544
|
+
name: tagName,
|
|
545
|
+
importPath,
|
|
546
|
+
props: mergedAttrs, // Use merged props for prop resolution
|
|
547
|
+
isProjectComponent: true
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// Also recursively scan immediate JSX children (for containers like <Modal>)
|
|
551
|
+
// This will discover HTML elements and additional custom components
|
|
552
|
+
if (jsxElement.children && jsxElement.children.length > 0) {
|
|
553
|
+
for (const child of jsxElement.children) {
|
|
554
|
+
if (child.type === 'JSXElement') {
|
|
555
|
+
this.extractConditionalJSXElement(child, sourceFileName, imports, componentProps, depth, detectedComponents);
|
|
556
|
+
} else if (child.type === 'JSXExpressionContainer') {
|
|
557
|
+
const expr = child.expression;
|
|
558
|
+
if (expr.type === 'LogicalExpression' && expr.operator === '&&' && expr.right.type === 'JSXElement') {
|
|
559
|
+
this.extractConditionalJSXElement(expr.right, sourceFileName, imports, componentProps, depth, detectedComponents);
|
|
560
|
+
} else if (expr.type === 'ConditionalExpression') {
|
|
561
|
+
if (expr.consequent.type === 'JSXElement') {
|
|
562
|
+
this.extractConditionalJSXElement(expr.consequent, sourceFileName, imports, componentProps, depth, detectedComponents);
|
|
563
|
+
}
|
|
564
|
+
if (expr.alternate && expr.alternate.type === 'JSXElement') {
|
|
565
|
+
this.extractConditionalJSXElement(expr.alternate, sourceFileName, imports, componentProps, depth, detectedComponents);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return; // Don't add component itself as element
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// HTML element - add directly
|
|
577
|
+
if (this.shouldIncludeElement(tagName, mergedAttrs, text)) {
|
|
578
|
+
this.elements.push({
|
|
579
|
+
key: `e${++this.elementCounter}`,
|
|
580
|
+
tag: tagName,
|
|
581
|
+
role: this.inferRole(tagName),
|
|
582
|
+
id: mergedAttrs.id,
|
|
583
|
+
name: mergedAttrs.name,
|
|
584
|
+
placeholder: mergedAttrs.placeholder,
|
|
585
|
+
ariaLabel: mergedAttrs['aria-label'] || mergedAttrs.ariaLabel,
|
|
586
|
+
testId: mergedAttrs['data-testid'],
|
|
587
|
+
text: text,
|
|
588
|
+
source: sourceFileName,
|
|
589
|
+
props: mergedAttrs
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
if (this.config.verbose) {
|
|
593
|
+
console.log(` ${' '.repeat(depth)} ✅ Found conditional element: <${tagName}> ${mergedAttrs['aria-label'] || mergedAttrs.ariaLabel ? `aria-label="${mergedAttrs['aria-label'] || mergedAttrs.ariaLabel}"` : ''}${text ? `text="${text.substring(0, 30)}"` : ''}`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Recursively process children
|
|
598
|
+
if (jsxElement.children && jsxElement.children.length > 0) {
|
|
599
|
+
for (const child of jsxElement.children) {
|
|
600
|
+
if (child.type === 'JSXElement') {
|
|
601
|
+
this.extractConditionalJSXElement(child, sourceFileName, imports, componentProps, depth, detectedComponents);
|
|
602
|
+
} else if (child.type === 'JSXExpressionContainer') {
|
|
603
|
+
// Handle nested conditionals
|
|
604
|
+
const expr = child.expression;
|
|
605
|
+
|
|
606
|
+
if (expr.type === 'LogicalExpression' && expr.operator === '&&' && expr.right.type === 'JSXElement') {
|
|
607
|
+
this.extractConditionalJSXElement(expr.right, sourceFileName, imports, componentProps, depth, detectedComponents);
|
|
608
|
+
} else if (expr.type === 'ConditionalExpression') {
|
|
609
|
+
if (expr.consequent.type === 'JSXElement') {
|
|
610
|
+
this.extractConditionalJSXElement(expr.consequent, sourceFileName, imports, componentProps, depth, detectedComponents);
|
|
611
|
+
}
|
|
612
|
+
if (expr.alternate && expr.alternate.type === 'JSXElement') {
|
|
613
|
+
this.extractConditionalJSXElement(expr.alternate, sourceFileName, imports, componentProps, depth, detectedComponents);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Extract attributes from JSX element
|
|
623
|
+
* Now supports resolving expressions from componentProps!
|
|
624
|
+
*/
|
|
625
|
+
private extractAttributes(attributes: any[], componentProps: Record<string, any> = {}): Record<string, any> {
|
|
626
|
+
const attrs: Record<string, any> = {};
|
|
627
|
+
|
|
628
|
+
attributes.forEach(attr => {
|
|
629
|
+
if (attr.type !== 'JSXAttribute' || attr.name.type !== 'JSXIdentifier') {
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const attrName = attr.name.name;
|
|
634
|
+
let value: any = null;
|
|
635
|
+
|
|
636
|
+
if (!attr.value) {
|
|
637
|
+
value = true; // Boolean attribute like <input required />
|
|
638
|
+
} else if (attr.value.type === 'StringLiteral') {
|
|
639
|
+
value = attr.value.value;
|
|
640
|
+
} else if (attr.value.type === 'JSXExpressionContainer') {
|
|
641
|
+
const expr = attr.value.expression;
|
|
642
|
+
|
|
643
|
+
if (expr.type === 'BooleanLiteral') {
|
|
644
|
+
value = expr.value;
|
|
645
|
+
} else if (expr.type === 'StringLiteral') {
|
|
646
|
+
value = expr.value;
|
|
647
|
+
} else if (expr.type === 'NumericLiteral') {
|
|
648
|
+
value = expr.value;
|
|
649
|
+
} else if (expr.type === 'MemberExpression') {
|
|
650
|
+
// props.name → try to resolve from componentProps
|
|
651
|
+
if (expr.object.type === 'Identifier' && expr.object.name === 'props' &&
|
|
652
|
+
expr.property.type === 'Identifier') {
|
|
653
|
+
const propName = expr.property.name;
|
|
654
|
+
|
|
655
|
+
// Try to resolve from componentProps
|
|
656
|
+
if (componentProps[propName] !== undefined) {
|
|
657
|
+
value = componentProps[propName]; // ✅ RESOLVED!
|
|
658
|
+
} else {
|
|
659
|
+
// Keep as expression if can't resolve
|
|
660
|
+
value = `{props.${propName}}`;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
} else if (expr.type === 'Identifier') {
|
|
664
|
+
// Handle: className={inputClassName}
|
|
665
|
+
// Try to resolve from componentProps
|
|
666
|
+
const identifierName = expr.name;
|
|
667
|
+
if (componentProps[identifierName] !== undefined) {
|
|
668
|
+
value = componentProps[identifierName]; // ✅ RESOLVED!
|
|
669
|
+
} else {
|
|
670
|
+
// Keep as expression
|
|
671
|
+
value = `{${identifierName}}`;
|
|
672
|
+
}
|
|
673
|
+
} else if (expr.type === 'CallExpression') {
|
|
674
|
+
// Handle: register('email', ...) → extract first string argument
|
|
675
|
+
if (expr.arguments && expr.arguments.length > 0) {
|
|
676
|
+
const firstArg = expr.arguments[0];
|
|
677
|
+
if (firstArg.type === 'StringLiteral') {
|
|
678
|
+
// register('email') → use 'email' as value
|
|
679
|
+
value = firstArg.value; // ✅ EXTRACTED!
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (value !== null) {
|
|
686
|
+
attrs[attrName] = value;
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
return attrs;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Extract text content from JSX element
|
|
695
|
+
*/
|
|
696
|
+
private extractTextContent(node: any): string | null {
|
|
697
|
+
let text = '';
|
|
698
|
+
|
|
699
|
+
if (!node.children) return null;
|
|
700
|
+
|
|
701
|
+
node.children.forEach((child: any) => {
|
|
702
|
+
if (child.type === 'JSXText') {
|
|
703
|
+
const trimmed = child.value.trim();
|
|
704
|
+
if (trimmed) {
|
|
705
|
+
text += trimmed + ' ';
|
|
706
|
+
}
|
|
707
|
+
} else if (child.type === 'JSXExpressionContainer') {
|
|
708
|
+
if (child.expression.type === 'StringLiteral') {
|
|
709
|
+
text += child.expression.value + ' ';
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
return text.trim() || null;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Check if element should be included in UI model
|
|
719
|
+
*/
|
|
720
|
+
private shouldIncludeElement(tagName: string, attrs: Record<string, any>, text: string | null): boolean {
|
|
721
|
+
const lowercaseTag = tagName.toLowerCase();
|
|
722
|
+
|
|
723
|
+
// Priority 1: UI library components (ALWAYS include)
|
|
724
|
+
if (lowercaseTag.includes('input') || lowercaseTag.includes('button') || lowercaseTag.includes('select')) {
|
|
725
|
+
return true;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Priority 2: Form elements (ALWAYS include)
|
|
729
|
+
const formElements = ['input', 'button', 'textarea', 'select', 'form'];
|
|
730
|
+
if (formElements.includes(lowercaseTag)) return true;
|
|
731
|
+
|
|
732
|
+
// Priority 2: Links
|
|
733
|
+
if (lowercaseTag === 'a' || tagName === 'Link') return true;
|
|
734
|
+
|
|
735
|
+
// Priority 3: Elements with identifiers
|
|
736
|
+
if (attrs.id || attrs.name || attrs['data-testid'] || attrs['aria-label']) {
|
|
737
|
+
return true;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Priority 4: Headings with text
|
|
741
|
+
if (/^h[1-6]$/.test(lowercaseTag) && text) return true;
|
|
742
|
+
|
|
743
|
+
// Priority 5: Images with alt
|
|
744
|
+
if (lowercaseTag === 'img' && attrs.alt) return true;
|
|
745
|
+
|
|
746
|
+
// Priority 6: Interactive elements
|
|
747
|
+
if (attrs.onClick || attrs.onSubmit || attrs.href) return true;
|
|
748
|
+
|
|
749
|
+
// Skip generic containers unless meaningful
|
|
750
|
+
if (['div', 'span', 'p'].includes(lowercaseTag)) {
|
|
751
|
+
return !!(text && text.length > 5); // Only if meaningful text
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Include semantic elements
|
|
755
|
+
if (['section', 'article', 'nav', 'header', 'footer', 'main'].includes(lowercaseTag)) {
|
|
756
|
+
return true;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Merge props from component chain
|
|
764
|
+
*/
|
|
765
|
+
private mergeProps(componentProps: Record<string, any>, elementAttrs: Record<string, any>): Record<string, any> {
|
|
766
|
+
const merged = { ...componentProps };
|
|
767
|
+
|
|
768
|
+
// Element attributes override component props
|
|
769
|
+
Object.entries(elementAttrs).forEach(([key, value]) => {
|
|
770
|
+
// Handle prop expressions like {props.name}
|
|
771
|
+
if (typeof value === 'string' && value.startsWith('{props.')) {
|
|
772
|
+
const propName = value.match(/\{props\.(\w+)\}/)?.[1];
|
|
773
|
+
if (propName && componentProps[propName]) {
|
|
774
|
+
merged[key] = componentProps[propName];
|
|
775
|
+
} else {
|
|
776
|
+
merged[key] = value; // Keep expression as-is
|
|
777
|
+
}
|
|
778
|
+
} else {
|
|
779
|
+
merged[key] = value;
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
return merged;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Generate cache key for file+props combination
|
|
788
|
+
* This allows same component to be scanned multiple times with different props
|
|
789
|
+
*/
|
|
790
|
+
private getCacheKey(filePath: string, componentProps: Record<string, any>): string {
|
|
791
|
+
// Create a stable key from significant props (id, name, ariaLabel, etc.)
|
|
792
|
+
const significantProps = ['id', 'name', 'ariaLabel', 'aria-label', 'placeholder', 'type', 'testId', 'data-testid'];
|
|
793
|
+
const propsSignature = significantProps
|
|
794
|
+
.map(key => componentProps[key] || componentProps[key.replace(/-/g, '')])
|
|
795
|
+
.filter(Boolean)
|
|
796
|
+
.join('|');
|
|
797
|
+
|
|
798
|
+
return `${filePath}::${propsSignature}`;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Recursively scan a file for UI elements
|
|
803
|
+
*/
|
|
804
|
+
private async scanFile(
|
|
805
|
+
filePath: string,
|
|
806
|
+
depth: number,
|
|
807
|
+
componentProps: Record<string, any>
|
|
808
|
+
): Promise<void> {
|
|
809
|
+
// Safety checks
|
|
810
|
+
if (depth >= this.config.maxDepth) {
|
|
811
|
+
if (this.config.verbose) {
|
|
812
|
+
console.log(` ${' '.repeat(depth)}⚠️ Max depth ${this.config.maxDepth} reached`);
|
|
813
|
+
}
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Use cache key based on file+props to allow same component with different props
|
|
818
|
+
const cacheKey = this.getCacheKey(filePath, componentProps);
|
|
819
|
+
if (this.scannedFiles.has(cacheKey)) {
|
|
820
|
+
if (this.config.verbose) {
|
|
821
|
+
console.log(` ${' '.repeat(depth)}⚠️ Already scanned: ${path.basename(filePath)} with these props`);
|
|
822
|
+
}
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (!fs.existsSync(filePath)) {
|
|
827
|
+
if (this.config.verbose) {
|
|
828
|
+
console.log(` ${' '.repeat(depth)}⚠️ File not found: ${filePath}`);
|
|
829
|
+
}
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
this.scannedFiles.add(cacheKey);
|
|
834
|
+
|
|
835
|
+
if (this.config.verbose) {
|
|
836
|
+
console.log(` ${' '.repeat(depth)}📄 Scanning (L${depth}): ${path.basename(filePath)}`);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Read and parse file
|
|
840
|
+
const code = fs.readFileSync(filePath, 'utf-8');
|
|
841
|
+
let ast;
|
|
842
|
+
|
|
843
|
+
try {
|
|
844
|
+
ast = parser.parse(code, {
|
|
845
|
+
sourceType: 'module',
|
|
846
|
+
plugins: ['typescript', 'jsx']
|
|
847
|
+
});
|
|
848
|
+
} catch (error) {
|
|
849
|
+
if (this.config.verbose) {
|
|
850
|
+
console.log(` ${' '.repeat(depth)}⚠️ Parse error: ${(error as Error).message}`);
|
|
851
|
+
}
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Extract imports and JSX elements
|
|
856
|
+
const self = this; // Capture 'this' and avoid naming conflict
|
|
857
|
+
const sourceFileName = path.basename(filePath);
|
|
858
|
+
const imports = new Map<string, string>();
|
|
859
|
+
const detectedComponents: Array<{ name: string; importPath: string; props: Record<string, any>; isProjectComponent?: boolean }> = [];
|
|
860
|
+
let jsxElementCount = 0;
|
|
861
|
+
|
|
862
|
+
traverse(ast, {
|
|
863
|
+
ImportDeclaration: (path: any) => {
|
|
864
|
+
const importPath = path.node.source.value;
|
|
865
|
+
|
|
866
|
+
// Default import
|
|
867
|
+
const defaultSpec = path.node.specifiers.find((s: any) => s.type === 'ImportDefaultSpecifier');
|
|
868
|
+
if (defaultSpec) {
|
|
869
|
+
imports.set(defaultSpec.local.name, importPath);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Named imports
|
|
873
|
+
path.node.specifiers
|
|
874
|
+
.filter((s: any) => s.type === 'ImportSpecifier')
|
|
875
|
+
.forEach((spec: any) => {
|
|
876
|
+
if (spec.imported.type === 'Identifier') {
|
|
877
|
+
// Use LOCAL name (after 'as'), not imported name
|
|
878
|
+
imports.set(spec.local.name, importPath);
|
|
879
|
+
|
|
880
|
+
if (self.config.verbose && depth === 2) {
|
|
881
|
+
console.log(` ${' '.repeat(depth)} Import: ${spec.imported.name} as ${spec.local.name} from ${importPath}`);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
// Extract JSX elements
|
|
889
|
+
|
|
890
|
+
traverse(ast, {
|
|
891
|
+
JSXElement: (nodePath: any) => {
|
|
892
|
+
jsxElementCount++;
|
|
893
|
+
const openingElement = nodePath.node.openingElement;
|
|
894
|
+
let tagName = '';
|
|
895
|
+
|
|
896
|
+
if (openingElement.name.type === 'JSXIdentifier') {
|
|
897
|
+
tagName = openingElement.name.name;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (self.config.verbose && depth === 2) {
|
|
901
|
+
console.log(` ${' '.repeat(depth)} JSX #${jsxElementCount}: <${tagName}>`);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Extract attributes WITH componentProps for expression resolution
|
|
905
|
+
const attrs = self.extractAttributes(openingElement.attributes, componentProps);
|
|
906
|
+
const text = self.extractTextContent(nodePath.node);
|
|
907
|
+
|
|
908
|
+
// Merge with component props (for additional props not in attrs)
|
|
909
|
+
const mergedAttrs = self.mergeProps(componentProps, attrs);
|
|
910
|
+
|
|
911
|
+
// Check if custom component
|
|
912
|
+
if (/^[A-Z]/.test(tagName) && imports.has(tagName)) {
|
|
913
|
+
const importPath = imports.get(tagName)!;
|
|
914
|
+
|
|
915
|
+
if (self.config.verbose) {
|
|
916
|
+
console.log(` ${' '.repeat(depth)} 🔎 Component: <${tagName}> from ${importPath}`);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// NEW: Check if framework component (Next.js Link, Image, etc.)
|
|
920
|
+
const frameworkComp = self.getFrameworkComponent(tagName, importPath);
|
|
921
|
+
if (frameworkComp) {
|
|
922
|
+
// Convert to synthetic DOM element
|
|
923
|
+
const syntheticElement = self.convertFrameworkComponent(
|
|
924
|
+
tagName,
|
|
925
|
+
frameworkComp,
|
|
926
|
+
mergedAttrs,
|
|
927
|
+
text,
|
|
928
|
+
sourceFileName
|
|
929
|
+
);
|
|
930
|
+
|
|
931
|
+
// Update metadata with actual import path
|
|
932
|
+
(syntheticElement as any).metadata.originalImport = importPath;
|
|
933
|
+
|
|
934
|
+
// Add to elements
|
|
935
|
+
self.elements.push(syntheticElement);
|
|
936
|
+
|
|
937
|
+
if (self.config.verbose) {
|
|
938
|
+
console.log(` ${' '.repeat(depth)} ✅ Found Framework Component: <${tagName}> → <${frameworkComp.convertTo}> ${mergedAttrs.href ? `href="${mergedAttrs.href}"` : ''}${text ? `text="${text.substring(0, 30)}"` : ''}`);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Don't scan deeper - framework components are terminal
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// Check if UI library component - treat as terminal element
|
|
946
|
+
const isUILib = self.isUILibraryComponent(tagName, importPath);
|
|
947
|
+
|
|
948
|
+
if (self.config.verbose && depth === 2) {
|
|
949
|
+
console.log(` ${' '.repeat(depth)} isUILibraryComponent(${tagName}, ${importPath}) = ${isUILib}`);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (isUILib) {
|
|
953
|
+
// Treat as actual element (don't scan deeper)
|
|
954
|
+
if (self.shouldIncludeElement(tagName, mergedAttrs, text)) {
|
|
955
|
+
self.elements.push({
|
|
956
|
+
key: `e${++self.elementCounter}`,
|
|
957
|
+
tag: tagName.toLowerCase(), // e.g. "inputtext"
|
|
958
|
+
role: self.inferRole('input'), // Infer as input
|
|
959
|
+
id: mergedAttrs.id,
|
|
960
|
+
name: mergedAttrs.name,
|
|
961
|
+
placeholder: mergedAttrs.placeholder,
|
|
962
|
+
ariaLabel: mergedAttrs['aria-label'] || mergedAttrs.ariaLabel,
|
|
963
|
+
testId: mergedAttrs['data-testid'],
|
|
964
|
+
text: text,
|
|
965
|
+
source: sourceFileName,
|
|
966
|
+
props: mergedAttrs
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
if (self.config.verbose) {
|
|
970
|
+
console.log(` ${' '.repeat(depth)} ✅ Found UI Library: <${tagName}> ${mergedAttrs.id ? `id="${mergedAttrs.id}"` : ''}${mergedAttrs['data-testid'] ? `data-testid="${mergedAttrs['data-testid']}"` : ''}`);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
// NEW: Project components - ALWAYS try to scan deeper
|
|
975
|
+
else if (importPath.startsWith('@/') || importPath.startsWith('./') || importPath.startsWith('../')) {
|
|
976
|
+
// This is a project component - add to scan queue
|
|
977
|
+
// We'll scan it after traverse completes (cannot await in traverse callback)
|
|
978
|
+
if (self.config.verbose) {
|
|
979
|
+
console.log(` ${' '.repeat(depth)} → Will scan project component: <${tagName}>`);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
detectedComponents.push({
|
|
983
|
+
name: tagName,
|
|
984
|
+
importPath,
|
|
985
|
+
props: mergedAttrs, // Use merged props (includes component props)
|
|
986
|
+
isProjectComponent: true // Mark as project component
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
// For other components (3rd party libs not in UI_LIBRARIES), decide whether to scan
|
|
990
|
+
else if (self.shouldScanComponent(tagName, importPath)) {
|
|
991
|
+
detectedComponents.push({
|
|
992
|
+
name: tagName,
|
|
993
|
+
importPath,
|
|
994
|
+
props: attrs,
|
|
995
|
+
isProjectComponent: false
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
// Check if actual DOM element
|
|
1000
|
+
else if (self.isActualDOMElement(tagName)) {
|
|
1001
|
+
// Found actual element - include it!
|
|
1002
|
+
if (self.shouldIncludeElement(tagName, mergedAttrs, text)) {
|
|
1003
|
+
self.elements.push({
|
|
1004
|
+
key: `e${++self.elementCounter}`,
|
|
1005
|
+
tag: tagName.toLowerCase(),
|
|
1006
|
+
role: self.inferRole(tagName),
|
|
1007
|
+
id: mergedAttrs.id,
|
|
1008
|
+
name: mergedAttrs.name,
|
|
1009
|
+
placeholder: mergedAttrs.placeholder,
|
|
1010
|
+
ariaLabel: mergedAttrs['aria-label'] || mergedAttrs.ariaLabel,
|
|
1011
|
+
testId: mergedAttrs['data-testid'],
|
|
1012
|
+
text: text,
|
|
1013
|
+
source: sourceFileName,
|
|
1014
|
+
props: mergedAttrs
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
if (self.config.verbose) {
|
|
1018
|
+
console.log(` ${' '.repeat(depth)} ✅ Found: <${tagName}> ${mergedAttrs.id ? `id="${mergedAttrs.id}"` : ''}${mergedAttrs['data-testid'] ? `data-testid="${mergedAttrs['data-testid']}"` : ''}`);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
},
|
|
1023
|
+
|
|
1024
|
+
// NEW: Handle conditional rendering - ignore conditions, scan all JSX
|
|
1025
|
+
JSXExpressionContainer: (nodePath: any) => {
|
|
1026
|
+
const expr = nodePath.node.expression;
|
|
1027
|
+
|
|
1028
|
+
// Pattern 1: Logical AND (condition && <Element />)
|
|
1029
|
+
if (expr.type === 'LogicalExpression' && expr.operator === '&&') {
|
|
1030
|
+
const rightSide = expr.right;
|
|
1031
|
+
|
|
1032
|
+
if (rightSide.type === 'JSXElement') {
|
|
1033
|
+
// Extract element from condition, ignoring the condition itself
|
|
1034
|
+
self.extractConditionalJSXElement(rightSide, sourceFileName, imports, componentProps, depth, detectedComponents);
|
|
1035
|
+
} else if (rightSide.type === 'JSXFragment') {
|
|
1036
|
+
// Handle {condition && (<><Element1 /><Element2 /></>)}
|
|
1037
|
+
for (const child of rightSide.children) {
|
|
1038
|
+
if (child.type === 'JSXElement') {
|
|
1039
|
+
self.extractConditionalJSXElement(child, sourceFileName, imports, componentProps, depth, detectedComponents);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Pattern 2: Ternary operator (condition ? <A /> : <B />)
|
|
1046
|
+
if (expr.type === 'ConditionalExpression') {
|
|
1047
|
+
const consequent = expr.consequent;
|
|
1048
|
+
const alternate = expr.alternate;
|
|
1049
|
+
|
|
1050
|
+
// Extract BOTH branches (we want all possible UI)
|
|
1051
|
+
if (consequent.type === 'JSXElement') {
|
|
1052
|
+
self.extractConditionalJSXElement(consequent, sourceFileName, imports, componentProps, depth, detectedComponents);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
if (alternate && alternate.type === 'JSXElement') {
|
|
1056
|
+
self.extractConditionalJSXElement(alternate, sourceFileName, imports, componentProps, depth, detectedComponents);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Pattern 3: Array.map() rendering
|
|
1061
|
+
if (expr.type === 'CallExpression') {
|
|
1062
|
+
const callee = expr.callee;
|
|
1063
|
+
|
|
1064
|
+
if (callee.type === 'MemberExpression' &&
|
|
1065
|
+
callee.property.type === 'Identifier' &&
|
|
1066
|
+
callee.property.name === 'map') {
|
|
1067
|
+
const mapCallback = expr.arguments[0];
|
|
1068
|
+
|
|
1069
|
+
if (mapCallback &&
|
|
1070
|
+
(mapCallback.type === 'ArrowFunctionExpression' ||
|
|
1071
|
+
mapCallback.type === 'FunctionExpression')) {
|
|
1072
|
+
const body = mapCallback.body;
|
|
1073
|
+
|
|
1074
|
+
// Direct return: arr.map(item => <Element />)
|
|
1075
|
+
if (body.type === 'JSXElement') {
|
|
1076
|
+
self.extractConditionalJSXElement(body, sourceFileName, imports, componentProps, depth, detectedComponents);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// Block with return: arr.map(item => { return <Element /> })
|
|
1080
|
+
if (body.type === 'BlockStatement') {
|
|
1081
|
+
for (const statement of body.body) {
|
|
1082
|
+
if (statement.type === 'ReturnStatement' &&
|
|
1083
|
+
statement.argument &&
|
|
1084
|
+
statement.argument.type === 'JSXElement') {
|
|
1085
|
+
self.extractConditionalJSXElement(statement.argument, sourceFileName, imports, componentProps, depth, detectedComponents);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
if (this.config.verbose && detectedComponents.length > 0) {
|
|
1096
|
+
console.log(` ${' '.repeat(depth)} → Found ${detectedComponents.length} component(s) to scan deeper`);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// Recursively scan detected components
|
|
1100
|
+
for (const comp of detectedComponents) {
|
|
1101
|
+
const componentPath = this.resolveComponentPath(comp.importPath, filePath);
|
|
1102
|
+
|
|
1103
|
+
if (componentPath) {
|
|
1104
|
+
const elementsBefore = this.elements.length;
|
|
1105
|
+
|
|
1106
|
+
if (this.config.verbose) {
|
|
1107
|
+
console.log(` ${' '.repeat(depth)} → Diving into: ${comp.name}`);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
await this.scanFile(componentPath, depth + 1, comp.props);
|
|
1111
|
+
|
|
1112
|
+
const elementsAfter = this.elements.length;
|
|
1113
|
+
const elementsFound = elementsAfter - elementsBefore;
|
|
1114
|
+
|
|
1115
|
+
// Project components: Check if found elements, if not, try fallback
|
|
1116
|
+
if ((comp as any).isProjectComponent && elementsFound === 0) {
|
|
1117
|
+
// No elements found inside project component - try fallback as wrapper
|
|
1118
|
+
if (this.config.verbose) {
|
|
1119
|
+
console.log(` ${' '.repeat(depth)} ⚠️ <${comp.name}> scanned but empty, trying wrapper fallback`);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
if (this.isCustomUIWrapper(comp.name, comp.importPath)) {
|
|
1123
|
+
// Extract as synthetic element
|
|
1124
|
+
const { tag: elementType, role } = this.inferWrapperType(comp.name);
|
|
1125
|
+
|
|
1126
|
+
this.elements.push({
|
|
1127
|
+
key: `e${++this.elementCounter}`,
|
|
1128
|
+
tag: elementType,
|
|
1129
|
+
role: role,
|
|
1130
|
+
id: comp.props.id,
|
|
1131
|
+
name: comp.props.name,
|
|
1132
|
+
placeholder: comp.props.placeholder,
|
|
1133
|
+
ariaLabel: comp.props['aria-label'] || comp.props.ariaLabel,
|
|
1134
|
+
testId: comp.props['data-testid'],
|
|
1135
|
+
text: null,
|
|
1136
|
+
source: sourceFileName,
|
|
1137
|
+
props: comp.props
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
if (this.config.verbose) {
|
|
1141
|
+
console.log(` ${' '.repeat(depth)} ✅ Fallback: Extracted <${comp.name}> as ${elementType}`);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
} else if (this.config.verbose && elementsFound > 0) {
|
|
1145
|
+
console.log(` ${' '.repeat(depth)} ✅ Scanned <${comp.name}> → found ${elementsFound} element(s)`);
|
|
1146
|
+
}
|
|
1147
|
+
} else {
|
|
1148
|
+
// Cannot resolve path
|
|
1149
|
+
if (this.config.verbose) {
|
|
1150
|
+
console.log(` ${' '.repeat(depth)} ⚠️ Could not resolve: ${comp.name} from ${comp.importPath}`);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// For project components that cannot be resolved, try wrapper fallback
|
|
1154
|
+
if ((comp as any).isProjectComponent && this.isCustomUIWrapper(comp.name, comp.importPath)) {
|
|
1155
|
+
const { tag: elementType, role } = this.inferWrapperType(comp.name);
|
|
1156
|
+
|
|
1157
|
+
this.elements.push({
|
|
1158
|
+
key: `e${++this.elementCounter}`,
|
|
1159
|
+
tag: elementType,
|
|
1160
|
+
role: role,
|
|
1161
|
+
id: comp.props.id,
|
|
1162
|
+
name: comp.props.name,
|
|
1163
|
+
placeholder: comp.props.placeholder,
|
|
1164
|
+
ariaLabel: comp.props['aria-label'] || comp.props.ariaLabel,
|
|
1165
|
+
testId: comp.props['data-testid'],
|
|
1166
|
+
text: null,
|
|
1167
|
+
source: sourceFileName,
|
|
1168
|
+
props: comp.props
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
if (this.config.verbose) {
|
|
1172
|
+
console.log(` ${' '.repeat(depth)} ✅ Fallback (unresolved): Extracted <${comp.name}> as ${elementType}`);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* Infer wrapper type from component name
|
|
1181
|
+
* Used for fallback when component is empty
|
|
1182
|
+
*/
|
|
1183
|
+
private inferWrapperType(componentName: string): { tag: string; role: string } {
|
|
1184
|
+
if (/Input$/i.test(componentName)) {
|
|
1185
|
+
return { tag: 'input', role: 'textbox' };
|
|
1186
|
+
} else if (/TextArea$/i.test(componentName)) {
|
|
1187
|
+
return { tag: 'textarea', role: 'textbox' };
|
|
1188
|
+
} else if (/Select$/i.test(componentName)) {
|
|
1189
|
+
return { tag: 'select', role: 'combobox' };
|
|
1190
|
+
} else if (/Link$/i.test(componentName)) {
|
|
1191
|
+
return { tag: 'a', role: 'link' };
|
|
1192
|
+
} else if (/Button$/i.test(componentName)) {
|
|
1193
|
+
return { tag: 'button', role: 'button' };
|
|
1194
|
+
} else {
|
|
1195
|
+
// Default to div
|
|
1196
|
+
return { tag: 'div', role: undefined as any };
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/**
|
|
1201
|
+
* Infer semantic role from tag name
|
|
1202
|
+
*/
|
|
1203
|
+
private inferRole(tagName: string): string | undefined {
|
|
1204
|
+
const roleMap: Record<string, string> = {
|
|
1205
|
+
'button': 'button',
|
|
1206
|
+
'a': 'link',
|
|
1207
|
+
'input': 'textbox',
|
|
1208
|
+
'textarea': 'textbox',
|
|
1209
|
+
'select': 'combobox',
|
|
1210
|
+
'img': 'img',
|
|
1211
|
+
'nav': 'navigation',
|
|
1212
|
+
'main': 'main',
|
|
1213
|
+
'header': 'banner',
|
|
1214
|
+
'footer': 'contentinfo'
|
|
1215
|
+
};
|
|
1216
|
+
|
|
1217
|
+
return roleMap[tagName.toLowerCase()];
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/**
|
|
1221
|
+
* Get scan results with statistics
|
|
1222
|
+
*/
|
|
1223
|
+
getResults() {
|
|
1224
|
+
const byTag: Record<string, number> = {};
|
|
1225
|
+
const formElements = [];
|
|
1226
|
+
|
|
1227
|
+
this.elements.forEach(el => {
|
|
1228
|
+
byTag[el.tag] = (byTag[el.tag] || 0) + 1;
|
|
1229
|
+
|
|
1230
|
+
const formTags = ['input', 'button', 'textarea', 'select', 'form'];
|
|
1231
|
+
if (formTags.includes(el.tag)) {
|
|
1232
|
+
formElements.push(el);
|
|
1233
|
+
}
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
return {
|
|
1237
|
+
totalElements: this.elements.length,
|
|
1238
|
+
filesScanned: this.scannedFiles.size,
|
|
1239
|
+
byTag,
|
|
1240
|
+
formElements,
|
|
1241
|
+
elements: this.elements
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
}
|