This is page 6 of 7. Use http://codebase.md/push-based/angular-toolkit-mcp?page={x} to view the full context. # Directory Structure ``` ├── .aiignore ├── .cursor │ ├── flows │ │ ├── component-refactoring │ │ │ ├── 01-review-component.mdc │ │ │ ├── 02-refactor-component.mdc │ │ │ ├── 03-validate-component.mdc │ │ │ └── angular-20.md │ │ ├── ds-refactoring-flow │ │ │ ├── 01-find-violations.mdc │ │ │ ├── 01b-find-all-violations.mdc │ │ │ ├── 02-plan-refactoring.mdc │ │ │ ├── 02b-plan-refactoring-for-all-violations.mdc │ │ │ ├── 03-fix-violations.mdc │ │ │ ├── 03-non-viable-cases.mdc │ │ │ ├── 04-validate-changes.mdc │ │ │ ├── 05-prepare-report.mdc │ │ │ └── clean-global-styles.mdc │ │ └── README.md │ └── mcp.json.example ├── .github │ └── workflows │ └── ci.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── assets │ ├── entain-logo.png │ └── entain.png ├── CONTRIBUTING.MD ├── docs │ ├── architecture-internal-design.md │ ├── component-refactoring-flow.md │ ├── contracts.md │ ├── ds-refactoring-flow.md │ ├── getting-started.md │ ├── README.md │ ├── tools.md │ └── writing-custom-tools.md ├── eslint.config.mjs ├── jest.config.ts ├── jest.preset.mjs ├── LICENSE ├── nx.json ├── package-lock.json ├── package.json ├── packages │ ├── .gitkeep │ ├── angular-mcp │ │ ├── eslint.config.mjs │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── assets │ │ │ │ └── .gitkeep │ │ │ └── main.ts │ │ ├── tsconfig.app.json │ │ ├── tsconfig.json │ │ ├── vitest.config.mts │ │ └── webpack.config.cjs │ ├── angular-mcp-server │ │ ├── eslint.config.mjs │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── index.ts │ │ │ └── lib │ │ │ ├── angular-mcp-server.ts │ │ │ ├── prompts │ │ │ │ └── prompt-registry.ts │ │ │ ├── tools │ │ │ │ ├── ds │ │ │ │ │ ├── component │ │ │ │ │ │ ├── get-deprecated-css-classes.tool.ts │ │ │ │ │ │ ├── get-ds-component-data.tool.ts │ │ │ │ │ │ ├── list-ds-components.tool.ts │ │ │ │ │ │ └── utils │ │ │ │ │ │ ├── deprecated-css-helpers.ts │ │ │ │ │ │ ├── doc-helpers.ts │ │ │ │ │ │ ├── metadata-helpers.ts │ │ │ │ │ │ └── paths-helpers.ts │ │ │ │ │ ├── component-contract │ │ │ │ │ │ ├── builder │ │ │ │ │ │ │ ├── build-component-contract.tool.ts │ │ │ │ │ │ │ ├── models │ │ │ │ │ │ │ │ ├── schema.ts │ │ │ │ │ │ │ │ └── types.ts │ │ │ │ │ │ │ ├── spec │ │ │ │ │ │ │ │ ├── css-match.spec.ts │ │ │ │ │ │ │ │ ├── dom-slots.extractor.spec.ts │ │ │ │ │ │ │ │ ├── element-helpers.spec.ts │ │ │ │ │ │ │ │ ├── inline-styles.collector.spec.ts │ │ │ │ │ │ │ │ ├── meta.generator.spec.ts │ │ │ │ │ │ │ │ ├── public-api.extractor.spec.ts │ │ │ │ │ │ │ │ ├── styles.collector.spec.ts │ │ │ │ │ │ │ │ └── typescript-analyzer.spec.ts │ │ │ │ │ │ │ └── utils │ │ │ │ │ │ │ ├── build-contract.ts │ │ │ │ │ │ │ ├── css-match.ts │ │ │ │ │ │ │ ├── dom-slots.extractor.ts │ │ │ │ │ │ │ ├── element-helpers.ts │ │ │ │ │ │ │ ├── inline-styles.collector.ts │ │ │ │ │ │ │ ├── meta.generator.ts │ │ │ │ │ │ │ ├── public-api.extractor.ts │ │ │ │ │ │ │ ├── styles.collector.ts │ │ │ │ │ │ │ └── typescript-analyzer.ts │ │ │ │ │ │ ├── diff │ │ │ │ │ │ │ ├── diff-component-contract.tool.ts │ │ │ │ │ │ │ ├── models │ │ │ │ │ │ │ │ └── schema.ts │ │ │ │ │ │ │ ├── spec │ │ │ │ │ │ │ │ ├── diff-utils.spec.ts │ │ │ │ │ │ │ │ └── dom-path-utils.spec.ts │ │ │ │ │ │ │ └── utils │ │ │ │ │ │ │ ├── diff-utils.ts │ │ │ │ │ │ │ └── dom-path-utils.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── list │ │ │ │ │ │ │ ├── list-component-contracts.tool.ts │ │ │ │ │ │ │ ├── models │ │ │ │ │ │ │ │ ├── schema.ts │ │ │ │ │ │ │ │ └── types.ts │ │ │ │ │ │ │ ├── spec │ │ │ │ │ │ │ │ └── contract-list-utils.spec.ts │ │ │ │ │ │ │ └── utils │ │ │ │ │ │ │ └── contract-list-utils.ts │ │ │ │ │ │ └── shared │ │ │ │ │ │ ├── models │ │ │ │ │ │ │ └── types.ts │ │ │ │ │ │ ├── spec │ │ │ │ │ │ │ └── contract-file-ops.spec.ts │ │ │ │ │ │ └── utils │ │ │ │ │ │ └── contract-file-ops.ts │ │ │ │ │ ├── component-usage-graph │ │ │ │ │ │ ├── build-component-usage-graph.tool.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── models │ │ │ │ │ │ │ ├── config.ts │ │ │ │ │ │ │ ├── schema.ts │ │ │ │ │ │ │ └── types.ts │ │ │ │ │ │ └── utils │ │ │ │ │ │ ├── angular-parser.ts │ │ │ │ │ │ ├── component-helpers.ts │ │ │ │ │ │ ├── component-usage-graph-builder.ts │ │ │ │ │ │ ├── path-resolver.ts │ │ │ │ │ │ └── unified-ast-analyzer.ts │ │ │ │ │ ├── ds.tools.ts │ │ │ │ │ ├── project │ │ │ │ │ │ ├── get-project-dependencies.tool.ts │ │ │ │ │ │ ├── report-deprecated-css.tool.ts │ │ │ │ │ │ └── utils │ │ │ │ │ │ ├── dependencies-helpers.ts │ │ │ │ │ │ └── styles-report-helpers.ts │ │ │ │ │ ├── report-violations │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── models │ │ │ │ │ │ │ ├── schema.ts │ │ │ │ │ │ │ └── types.ts │ │ │ │ │ │ ├── report-all-violations.tool.ts │ │ │ │ │ │ └── report-violations.tool.ts │ │ │ │ │ ├── shared │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── models │ │ │ │ │ │ │ ├── input-schemas.model.ts │ │ │ │ │ │ │ └── schema-helpers.ts │ │ │ │ │ │ ├── utils │ │ │ │ │ │ │ ├── component-validation.ts │ │ │ │ │ │ │ ├── cross-platform-path.ts │ │ │ │ │ │ │ ├── handler-helpers.ts │ │ │ │ │ │ │ ├── output.utils.ts │ │ │ │ │ │ │ └── regex-helpers.ts │ │ │ │ │ │ └── violation-analysis │ │ │ │ │ │ ├── base-analyzer.ts │ │ │ │ │ │ ├── coverage-analyzer.ts │ │ │ │ │ │ ├── formatters.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── types.ts │ │ │ │ │ └── tools.ts │ │ │ │ ├── schema.ts │ │ │ │ ├── tools.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils.ts │ │ │ └── validation │ │ │ ├── angular-mcp-server-options.schema.ts │ │ │ ├── ds-components-file-loader.validation.ts │ │ │ ├── ds-components-file.validation.ts │ │ │ ├── ds-components.schema.ts │ │ │ └── file-existence.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ ├── tsconfig.tsbuildinfo │ │ └── vitest.config.mts │ ├── minimal-repo │ │ └── packages │ │ ├── application │ │ │ ├── angular.json │ │ │ ├── code-pushup.config.ts │ │ │ ├── src │ │ │ │ ├── app │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.config.ts │ │ │ │ │ ├── app.routes.ts │ │ │ │ │ ├── components │ │ │ │ │ │ ├── refactoring-tests │ │ │ │ │ │ │ ├── bad-alert-tooltip-input.component.ts │ │ │ │ │ │ │ ├── bad-alert.component.ts │ │ │ │ │ │ │ ├── bad-button-dropdown.component.ts │ │ │ │ │ │ │ ├── bad-document.component.ts │ │ │ │ │ │ │ ├── bad-global-this.component.ts │ │ │ │ │ │ │ ├── bad-mixed-external-assets.component.css │ │ │ │ │ │ │ ├── bad-mixed-external-assets.component.html │ │ │ │ │ │ │ ├── bad-mixed-external-assets.component.ts │ │ │ │ │ │ │ ├── bad-mixed-not-standalone.component.ts │ │ │ │ │ │ │ ├── bad-mixed.component.ts │ │ │ │ │ │ │ ├── bad-mixed.module.ts │ │ │ │ │ │ │ ├── bad-modal-progress.component.ts │ │ │ │ │ │ │ ├── bad-this-window-document.component.ts │ │ │ │ │ │ │ ├── bad-window.component.ts │ │ │ │ │ │ │ ├── complex-components │ │ │ │ │ │ │ │ ├── first-case │ │ │ │ │ │ │ │ │ ├── dashboard-demo.component.html │ │ │ │ │ │ │ │ │ ├── dashboard-demo.component.scss │ │ │ │ │ │ │ │ │ ├── dashboard-demo.component.ts │ │ │ │ │ │ │ │ │ ├── dashboard-header.component.html │ │ │ │ │ │ │ │ │ ├── dashboard-header.component.scss │ │ │ │ │ │ │ │ │ └── dashboard-header.component.ts │ │ │ │ │ │ │ │ ├── second-case │ │ │ │ │ │ │ │ │ ├── complex-badge-widget.component.scss │ │ │ │ │ │ │ │ │ ├── complex-badge-widget.component.ts │ │ │ │ │ │ │ │ │ └── complex-widget-demo.component.ts │ │ │ │ │ │ │ │ └── third-case │ │ │ │ │ │ │ │ ├── product-card.component.scss │ │ │ │ │ │ │ │ ├── product-card.component.ts │ │ │ │ │ │ │ │ └── product-showcase.component.ts │ │ │ │ │ │ │ ├── group-1 │ │ │ │ │ │ │ │ ├── bad-mixed-1.component.ts │ │ │ │ │ │ │ │ ├── bad-mixed-1.module.ts │ │ │ │ │ │ │ │ ├── bad-mixed-external-assets-1.component.css │ │ │ │ │ │ │ │ ├── bad-mixed-external-assets-1.component.html │ │ │ │ │ │ │ │ ├── bad-mixed-external-assets-1.component.ts │ │ │ │ │ │ │ │ └── bad-mixed-not-standalone-1.component.ts │ │ │ │ │ │ │ ├── group-2 │ │ │ │ │ │ │ │ ├── bad-mixed-2.component.ts │ │ │ │ │ │ │ │ ├── bad-mixed-2.module.ts │ │ │ │ │ │ │ │ ├── bad-mixed-external-assets-2.component.css │ │ │ │ │ │ │ │ ├── bad-mixed-external-assets-2.component.html │ │ │ │ │ │ │ │ ├── bad-mixed-external-assets-2.component.ts │ │ │ │ │ │ │ │ └── bad-mixed-not-standalone-2.component.ts │ │ │ │ │ │ │ ├── group-3 │ │ │ │ │ │ │ │ ├── bad-mixed-3.component.spec.ts │ │ │ │ │ │ │ │ ├── bad-mixed-3.component.ts │ │ │ │ │ │ │ │ ├── bad-mixed-3.module.ts │ │ │ │ │ │ │ │ ├── bad-mixed-external-assets-3.component.css │ │ │ │ │ │ │ │ ├── bad-mixed-external-assets-3.component.html │ │ │ │ │ │ │ │ ├── bad-mixed-external-assets-3.component.ts │ │ │ │ │ │ │ │ ├── bad-mixed-not-standalone-3.component.ts │ │ │ │ │ │ │ │ └── lazy-loader-3.component.ts │ │ │ │ │ │ │ └── group-4 │ │ │ │ │ │ │ ├── multi-violation-test.component.html │ │ │ │ │ │ │ ├── multi-violation-test.component.scss │ │ │ │ │ │ │ └── multi-violation-test.component.ts │ │ │ │ │ │ └── validation-tests │ │ │ │ │ │ ├── circular-dependency.component.ts │ │ │ │ │ │ ├── external-files-missing.component.ts │ │ │ │ │ │ ├── invalid-lifecycle.component.ts │ │ │ │ │ │ ├── invalid-pipe-usage.component.ts │ │ │ │ │ │ ├── invalid-template-syntax.component.ts │ │ │ │ │ │ ├── missing-imports.component.ts │ │ │ │ │ │ ├── missing-method.component.ts │ │ │ │ │ │ ├── README.md │ │ │ │ │ │ ├── standalone-module-conflict.component.ts │ │ │ │ │ │ ├── standalone-module-conflict.module.ts │ │ │ │ │ │ ├── template-reference-error.component.ts │ │ │ │ │ │ ├── type-mismatch.component.ts │ │ │ │ │ │ ├── valid.component.ts │ │ │ │ │ │ ├── wrong-decorator-usage.component.ts │ │ │ │ │ │ └── wrong-property-binding.component.ts │ │ │ │ │ └── styles │ │ │ │ │ ├── bad-global-styles.scss │ │ │ │ │ ├── base │ │ │ │ │ │ ├── _reset.scss │ │ │ │ │ │ └── base.scss │ │ │ │ │ ├── components │ │ │ │ │ │ └── components.scss │ │ │ │ │ ├── extended-deprecated-styles.scss │ │ │ │ │ ├── layout │ │ │ │ │ │ └── layout.scss │ │ │ │ │ ├── new-styles-1.scss │ │ │ │ │ ├── new-styles-10.scss │ │ │ │ │ ├── new-styles-2.scss │ │ │ │ │ ├── new-styles-3.scss │ │ │ │ │ ├── new-styles-4.scss │ │ │ │ │ ├── new-styles-5.scss │ │ │ │ │ ├── new-styles-6.scss │ │ │ │ │ ├── new-styles-7.scss │ │ │ │ │ ├── new-styles-8.scss │ │ │ │ │ ├── new-styles-9.scss │ │ │ │ │ ├── themes │ │ │ │ │ │ └── themes.scss │ │ │ │ │ └── utilities │ │ │ │ │ └── utilities.scss │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ └── styles.css │ │ │ ├── tsconfig.app.json │ │ │ ├── tsconfig.json │ │ │ └── tsconfig.spec.json │ │ └── design-system │ │ ├── component-options.mjs │ │ ├── storybook │ │ │ └── card │ │ │ └── card-tabs │ │ │ └── overview.mdx │ │ ├── storybook-host-app │ │ │ └── src │ │ │ └── components │ │ │ ├── badge │ │ │ │ ├── badge-tabs │ │ │ │ │ ├── api.mdx │ │ │ │ │ ├── examples.mdx │ │ │ │ │ └── overview.mdx │ │ │ │ ├── badge.component.mdx │ │ │ │ └── badge.component.stories.ts │ │ │ ├── modal │ │ │ │ ├── demo-cdk-dialog-cmp.component.ts │ │ │ │ ├── demo-modal-cmp.component.ts │ │ │ │ ├── modal-tabs │ │ │ │ │ ├── api.mdx │ │ │ │ │ ├── examples.mdx │ │ │ │ │ └── overview.mdx │ │ │ │ ├── modal.component.mdx │ │ │ │ └── modal.component.stories.ts │ │ │ └── segmented-control │ │ │ ├── segmented-control-tabs │ │ │ │ ├── api.mdx │ │ │ │ ├── examples.mdx │ │ │ │ └── overview.mdx │ │ │ ├── segmented-control.component.mdx │ │ │ └── segmented-control.component.stories.ts │ │ └── ui │ │ ├── badge │ │ │ ├── package.json │ │ │ ├── project.json │ │ │ └── src │ │ │ └── badge.component.ts │ │ ├── modal │ │ │ ├── package.json │ │ │ ├── project.json │ │ │ └── src │ │ │ ├── modal-content.component.ts │ │ │ ├── modal-header │ │ │ │ └── modal-header.component.ts │ │ │ ├── modal-header-drag │ │ │ │ └── modal-header-drag.component.ts │ │ │ └── modal.component.ts │ │ ├── rx-host-listener │ │ │ ├── package.json │ │ │ ├── project.json │ │ │ └── src │ │ │ └── rx-host-listener.ts │ │ └── segmented-control │ │ ├── package.json │ │ ├── project.json │ │ └── src │ │ ├── segmented-control.component.html │ │ ├── segmented-control.component.ts │ │ ├── segmented-control.token.ts │ │ └── segmented-option.component.ts │ └── shared │ ├── angular-ast-utils │ │ ├── .spec.swcrc │ │ ├── ai │ │ │ ├── API.md │ │ │ ├── EXAMPLES.md │ │ │ └── FUNCTIONS.md │ │ ├── docs │ │ │ └── angular-component-tree.md │ │ ├── eslint.config.mjs │ │ ├── jest.config.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── index.ts │ │ │ └── lib │ │ │ ├── constants.ts │ │ │ ├── decorator-config.visitor.inline-styles.spec.ts │ │ │ ├── decorator-config.visitor.spec.ts │ │ │ ├── decorator-config.visitor.ts │ │ │ ├── parse-component.ts │ │ │ ├── schema.ts │ │ │ ├── styles │ │ │ │ └── utils.ts │ │ │ ├── template │ │ │ │ ├── noop-tmpl-visitor.ts │ │ │ │ ├── template.walk.ts │ │ │ │ ├── utils.spec.ts │ │ │ │ ├── utils.ts │ │ │ │ └── utils.unit.test.ts │ │ │ ├── ts.walk.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ ├── tsconfig.spec.json │ │ └── vitest.config.mts │ ├── DEPENDENCIES.md │ ├── ds-component-coverage │ │ ├── .spec.swcrc │ │ ├── ai │ │ │ ├── API.md │ │ │ ├── EXAMPLES.md │ │ │ └── FUNCTIONS.md │ │ ├── docs │ │ │ ├── examples │ │ │ │ ├── report.json │ │ │ │ └── report.md │ │ │ ├── images │ │ │ │ └── report-overview.png │ │ │ └── README.md │ │ ├── jest.config.ts │ │ ├── mocks │ │ │ └── fixtures │ │ │ └── e2e │ │ │ ├── asset-location │ │ │ │ ├── code-pushup.config.ts │ │ │ │ ├── inl-styl-inl-tmpl │ │ │ │ │ └── inl-styl-inl-tmpl.component.ts │ │ │ │ ├── inl-styl-url-tmpl │ │ │ │ │ ├── inl-styl-url-tmpl.component.html │ │ │ │ │ └── inl-styl-url-tmpl.component.ts │ │ │ │ ├── multi-url-styl-inl-tmpl │ │ │ │ │ ├── multi-url-styl-inl-tmpl-1.component.css │ │ │ │ │ ├── multi-url-styl-inl-tmpl-2.component.css │ │ │ │ │ └── multi-url-styl-inl-tmpl.component.ts │ │ │ │ ├── url-styl-inl-tmpl │ │ │ │ │ ├── url-styl-inl-tmpl.component.css │ │ │ │ │ └── url-styl-inl-tmpl.component.ts │ │ │ │ ├── url-styl-single-inl-tmpl │ │ │ │ │ ├── url-styl-inl-tmpl.component.ts │ │ │ │ │ └── url-styl-single-inl-tmpl.component.css │ │ │ │ └── url-styl-url-tmpl │ │ │ │ ├── inl-styl-url-tmpl.component.css │ │ │ │ ├── inl-styl-url-tmpl.component.html │ │ │ │ └── inl-styl-url-tmpl.component.ts │ │ │ ├── demo │ │ │ │ ├── code-pushup.config.ts │ │ │ │ ├── prompt.md │ │ │ │ └── src │ │ │ │ ├── bad-button-dropdown.component.ts │ │ │ │ ├── bad-modal-progress.component.ts │ │ │ │ ├── mixed-external-assets.component.css │ │ │ │ ├── mixed-external-assets.component.html │ │ │ │ ├── mixed-external-assets.component.ts │ │ │ │ └── sub-folder-1 │ │ │ │ ├── bad-alert.component.ts │ │ │ │ ├── button.component.ts │ │ │ │ └── sub-folder-2 │ │ │ │ ├── bad-alert-tooltip-input.component.ts │ │ │ │ └── bad-mixed.component.ts │ │ │ ├── line-number │ │ │ │ ├── code-pushup.config.ts │ │ │ │ ├── inl-styl-single.component.ts │ │ │ │ ├── inl-styl-span.component.ts │ │ │ │ ├── inl-tmpl-single.component.ts │ │ │ │ ├── inl-tmpl-span.component.ts │ │ │ │ ├── url-style │ │ │ │ │ ├── url-styl-single.component.css │ │ │ │ │ ├── url-styl-single.component.ts │ │ │ │ │ ├── url-styl-span.component.css │ │ │ │ │ └── url-styl-span.component.ts │ │ │ │ └── url-tmpl │ │ │ │ ├── url-tmpl-single.component.html │ │ │ │ ├── url-tmpl-single.component.ts │ │ │ │ ├── url-tmpl-span.component.html │ │ │ │ └── url-tmpl-span.component.ts │ │ │ ├── style-format │ │ │ │ ├── code-pushup.config.ts │ │ │ │ ├── inl-css.component.ts │ │ │ │ ├── inl-scss.component.ts │ │ │ │ ├── styles.css │ │ │ │ ├── styles.scss │ │ │ │ ├── url-css.component.ts │ │ │ │ └── url-scss.component.ts │ │ │ └── template-syntax │ │ │ ├── class-attribute.component.ts │ │ │ ├── class-binding.component.ts │ │ │ ├── code-pushup.config.ts │ │ │ └── ng-class-binding.component.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── core.config.ts │ │ │ ├── index.ts │ │ │ └── lib │ │ │ ├── constants.ts │ │ │ ├── ds-component-coverage.plugin.ts │ │ │ ├── runner │ │ │ │ ├── audits │ │ │ │ │ └── ds-coverage │ │ │ │ │ ├── class-definition.utils.ts │ │ │ │ │ ├── class-definition.visitor.ts │ │ │ │ │ ├── class-definition.visitor.unit.test.ts │ │ │ │ │ ├── class-usage.utils.ts │ │ │ │ │ ├── class-usage.visitor.spec.ts │ │ │ │ │ ├── class-usage.visitor.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── ds-coverage.audit.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── create-runner.ts │ │ │ │ └── schema.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ ├── tsconfig.spec.json │ │ └── vitest.config.mts │ ├── LLMS.md │ ├── models │ │ ├── ai │ │ │ ├── API.md │ │ │ ├── EXAMPLES.md │ │ │ └── FUNCTIONS.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── index.ts │ │ │ └── lib │ │ │ ├── cli.ts │ │ │ ├── diagnostics.ts │ │ │ └── mcp.ts │ │ ├── tsconfig.json │ │ └── tsconfig.lib.json │ ├── styles-ast-utils │ │ ├── .spec.swcrc │ │ ├── ai │ │ │ ├── API.md │ │ │ ├── EXAMPLES.md │ │ │ └── FUNCTIONS.md │ │ ├── jest.config.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── index.ts │ │ │ └── lib │ │ │ ├── postcss-safe-parser.d.ts │ │ │ ├── styles-ast-utils.spec.ts │ │ │ ├── styles-ast-utils.ts │ │ │ ├── stylesheet.parse.ts │ │ │ ├── stylesheet.parse.unit.test.ts │ │ │ ├── stylesheet.visitor.ts │ │ │ ├── stylesheet.walk.ts │ │ │ ├── types.ts │ │ │ ├── utils.ts │ │ │ └── utils.unit.test.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ ├── tsconfig.spec.json │ │ └── vitest.config.mts │ ├── typescript-ast-utils │ │ ├── .spec.swcrc │ │ ├── ai │ │ │ ├── API.md │ │ │ ├── EXAMPLES.md │ │ │ └── FUNCTIONS.md │ │ ├── jest.config.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── index.ts │ │ │ └── lib │ │ │ ├── constants.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ ├── tsconfig.spec.json │ │ └── vitest.config.mts │ └── utils │ ├── .spec.swcrc │ ├── ai │ │ ├── API.md │ │ ├── EXAMPLES.md │ │ └── FUNCTIONS.md │ ├── package.json │ ├── README.md │ ├── src │ │ ├── index.ts │ │ └── lib │ │ ├── execute-process.ts │ │ ├── execute-process.unit.test.ts │ │ ├── file │ │ │ ├── default-export-loader.spec.ts │ │ │ ├── default-export-loader.ts │ │ │ ├── file.resolver.ts │ │ │ └── find-in-file.ts │ │ ├── format-command-log.integration.test.ts │ │ ├── format-command-log.ts │ │ ├── logging.ts │ │ └── utils.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ ├── vite.config.ts │ └── vitest.config.mts ├── README.md ├── testing │ ├── setup │ │ ├── eslint.config.mjs │ │ ├── eslint.next.config.mjs │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── index.d.ts │ │ │ ├── index.mjs │ │ │ └── memfs.constants.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ ├── tsconfig.spec.json │ │ ├── vitest.config.mts │ │ └── vitest.integration.config.mts │ ├── utils │ │ ├── eslint.config.mjs │ │ ├── eslint.next.config.mjs │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── index.ts │ │ │ └── lib │ │ │ ├── constants.ts │ │ │ ├── e2e-setup.ts │ │ │ ├── execute-process-helper.mock.ts │ │ │ ├── execute-process.mock.mjs │ │ │ ├── os-agnostic-paths.ts │ │ │ ├── os-agnostic-paths.unit.test.ts │ │ │ ├── source-file-from.code.ts │ │ │ └── string.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ ├── tsconfig.spec.json │ │ ├── vite.config.ts │ │ ├── vitest.config.mts │ │ └── vitest.integration.config.mts │ └── vitest-setup │ ├── eslint.config.mjs │ ├── eslint.next.config.mjs │ ├── package.json │ ├── README.md │ ├── src │ │ ├── index.ts │ │ └── lib │ │ ├── configuration.ts │ │ └── fs-memfs.setup-file.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ ├── vite.config.ts │ ├── vitest.config.mts │ └── vitest.integration.config.mts ├── tools │ ├── nx-advanced-profile.bin.js │ ├── nx-advanced-profile.js │ ├── nx-advanced-profile.postinstall.js │ └── perf_hooks.patch.js ├── tsconfig.base.json ├── tsconfig.json └── vitest.workspace.ts ``` # Files -------------------------------------------------------------------------------- /packages/minimal-repo/packages/design-system/storybook-host-app/src/components/segmented-control/segmented-control.component.stories.ts: -------------------------------------------------------------------------------- ```typescript import { generateStatusBadges } from '@design-system/shared-storybook-utils'; import { DsSegmentedControl, DsSegmentedOption, } from '@frontend/ui/segmented-control'; import { Meta, StoryObj, moduleMetadata } from '@storybook/angular'; import { expect, fireEvent, within } from '@storybook/test'; type StoryType = DsSegmentedControl & { roleType: string; itemMaxWidth: string; customLabel: string; twoLineTruncation: boolean; }; export default { title: 'Components/Segmented Control', parameters: { status: generateStatusBadges('UX-2309', ['a11y', 'integration ready']), }, component: DsSegmentedControl, args: { fullWidth: false, inverse: false, activeOption: '2', roleType: 'tablist', itemMaxWidth: 'auto', customLabel: 'customLabel', twoLineTruncation: false, }, argTypes: { fullWidth: { type: 'boolean', table: { defaultValue: { summary: 'false' }, }, control: { type: 'boolean' }, description: 'Whether the segment should take up the full width of the container', }, inverse: { type: 'boolean', table: { defaultValue: { summary: 'false' } }, control: { type: 'boolean' }, description: 'The inverse state of the Segmented Control', }, twoLineTruncation: { type: 'boolean', table: { defaultValue: { summary: 'false' } }, control: { type: 'boolean' }, description: 'Defining if two lines of text should be visible', }, activeOption: { type: 'string', control: 'text', description: 'The text/value of name to be active', }, roleType: { control: { type: 'select' }, table: { defaultValue: { summary: 'tablist' } }, options: ['radiogroup', 'tablist'], description: 'Determines the ARIA role applied to the segmented control', }, itemMaxWidth: { type: 'string', table: { defaultValue: { summary: 'auto' } }, control: 'text', description: 'Max width of the item', }, customLabel: { type: 'string', table: { defaultValue: { summary: 'customLabel' } }, control: 'text', description: 'Custom text you can use to check the behavior', }, }, decorators: [ moduleMetadata({ imports: [DsSegmentedControl, DsSegmentedOption], }), ], } as Meta<StoryType>; export const Default: StoryObj<StoryType> = { parameters: { name: 'Default', design: { name: 'Whitelabel', type: 'figma', url: 'https://www.figma.com/file/NgrOt8MGJhe0obKFBQgqdT/Component-Tokens-(POC)?type=design&node-id=12596-148225&mode=design&t=fS1qO73SS8lGciLj-4', }, }, render: (args) => ({ props: args, template: ` <div style='width: 650px; text-align: center; display: block;'> <ds-segmented-control [twoLineTruncation]="${args.twoLineTruncation}" [roleType]="'${args.roleType}'" [fullWidth]="${args.fullWidth}" [inverse]="${args.inverse}" [activeOption]="'${args.activeOption}'" style="--ds-segment-item-text-max-width: ${args.itemMaxWidth};"> <ds-segmented-option name='0' title="Label1 long text support, label long text support label long text support label long text support" /> <ds-segmented-option name="1" title="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s" /> <ds-segmented-option name='2' title="Label3" /> <ds-segmented-option name="3" title="${args.customLabel}" /> </ds-segmented-control> </div> `, }), play: async ({ canvasElement, step }) => { await step('check Click event is being called', async () => { const canvas = within(canvasElement); const segments = canvas.getAllByRole('tab'); await fireEvent.click(segments[0]); await expect(segments[0]).toHaveClass('ds-segment-selected'); }); }, }; export const WithImage: StoryObj<StoryType> = { parameters: { name: 'Default', design: { name: 'Whitelabel', type: 'figma', url: 'https://www.figma.com/file/NgrOt8MGJhe0obKFBQgqdT/Component-Tokens-(POC)?type=design&node-id=12596-148323&mode=design&t=fS1qO73SS8lGciLj-4', }, }, argTypes: { customLabel: { table: { disable: true }, }, itemMaxWidth: { table: { disable: true }, }, twoLineTruncation: { table: { disable: true }, }, }, render: (args) => ({ props: args, template: ` <div style='width: 450px; text-align: center; display: block;' > <ds-segmented-control [roleType]="'${args.roleType}'" [fullWidth]="${args.fullWidth}" [inverse]="${args.inverse}" [activeOption]="'${args.activeOption}'"> <ds-segmented-option name="1" title="image1"> <ng-template #dsTemplate> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <g clip-path="url(#clip0_12185_5035)"> <path d="M1.22907 11.123L0.397165 10.2663C0.140073 10.0015 0.140072 9.5722 0.397165 9.30742L9.2418 0.198579C9.49889 -0.0661931 9.91572 -0.0661931 10.1728 0.198579L10.927 0.975275L1.22907 11.123Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M4.02262 14L1.68316 11.5907L11.3811 1.44293L15.3023 5.4813C15.5594 5.74607 15.5594 6.17535 15.3023 6.44013L7.96171 14H4.02262ZM10.6476 6.44004C10.3905 6.17527 10.3905 5.74599 10.6476 5.48122L11.5786 4.52239C11.8357 4.25762 12.2525 4.25762 12.5096 4.52239L13.4406 5.48122C13.6977 5.74599 13.6977 6.17527 13.4406 6.44004L12.5096 7.39887C12.2525 7.66364 11.8357 7.66364 11.5786 7.39887L10.6476 6.44004ZM3.66431 11.7136L7.38836 7.87826L7.85387 8.35767L4.12981 12.193L3.66431 11.7136Z" fill="currentColor"/> <path d="M9.76108 14.644H13.4364C13.6182 14.644 13.7655 14.7958 13.7655 14.983V15.661C13.7655 15.8483 13.6182 16 13.4364 16H2.24482C2.06303 16 1.91566 15.8483 1.91566 15.661V14.983C1.91566 14.8388 2.00309 14.7157 2.12634 14.6667H9.76108V14.644Z" fill="currentColor"/> </g> <defs> <clipPath id="clip0_12185_5035"> <rect width="16" height="16" fill="currentColor"/> </clipPath> </defs> </svg> </ng-template> </ds-segmented-option> <ds-segmented-option name="2" title="image2"> <ng-template #dsTemplate> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <g clip-path="url(#clip0_12185_5035)"> <path d="M1.22907 11.123L0.397165 10.2663C0.140073 10.0015 0.140072 9.5722 0.397165 9.30742L9.2418 0.198579C9.49889 -0.0661931 9.91572 -0.0661931 10.1728 0.198579L10.927 0.975275L1.22907 11.123Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M4.02262 14L1.68316 11.5907L11.3811 1.44293L15.3023 5.4813C15.5594 5.74607 15.5594 6.17535 15.3023 6.44013L7.96171 14H4.02262ZM10.6476 6.44004C10.3905 6.17527 10.3905 5.74599 10.6476 5.48122L11.5786 4.52239C11.8357 4.25762 12.2525 4.25762 12.5096 4.52239L13.4406 5.48122C13.6977 5.74599 13.6977 6.17527 13.4406 6.44004L12.5096 7.39887C12.2525 7.66364 11.8357 7.66364 11.5786 7.39887L10.6476 6.44004ZM3.66431 11.7136L7.38836 7.87826L7.85387 8.35767L4.12981 12.193L3.66431 11.7136Z" fill="currentColor"/> <path d="M9.76108 14.644H13.4364C13.6182 14.644 13.7655 14.7958 13.7655 14.983V15.661C13.7655 15.8483 13.6182 16 13.4364 16H2.24482C2.06303 16 1.91566 15.8483 1.91566 15.661V14.983C1.91566 14.8388 2.00309 14.7157 2.12634 14.6667H9.76108V14.644Z" fill="currentColor"/> </g> <defs> <clipPath id="clip0_12185_5035"> <rect width="16" height="16" fill="currentColor"/> </clipPath> </defs> </svg> </ng-template> </ds-segmented-option> <ds-segmented-option name="3" title="image3"> <ng-template #dsTemplate> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <g clip-path="url(#clip0_12185_5035)"> <path d="M1.22907 11.123L0.397165 10.2663C0.140073 10.0015 0.140072 9.5722 0.397165 9.30742L9.2418 0.198579C9.49889 -0.0661931 9.91572 -0.0661931 10.1728 0.198579L10.927 0.975275L1.22907 11.123Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M4.02262 14L1.68316 11.5907L11.3811 1.44293L15.3023 5.4813C15.5594 5.74607 15.5594 6.17535 15.3023 6.44013L7.96171 14H4.02262ZM10.6476 6.44004C10.3905 6.17527 10.3905 5.74599 10.6476 5.48122L11.5786 4.52239C11.8357 4.25762 12.2525 4.25762 12.5096 4.52239L13.4406 5.48122C13.6977 5.74599 13.6977 6.17527 13.4406 6.44004L12.5096 7.39887C12.2525 7.66364 11.8357 7.66364 11.5786 7.39887L10.6476 6.44004ZM3.66431 11.7136L7.38836 7.87826L7.85387 8.35767L4.12981 12.193L3.66431 11.7136Z" fill="currentColor"/> <path d="M9.76108 14.644H13.4364C13.6182 14.644 13.7655 14.7958 13.7655 14.983V15.661C13.7655 15.8483 13.6182 16 13.4364 16H2.24482C2.06303 16 1.91566 15.8483 1.91566 15.661V14.983C1.91566 14.8388 2.00309 14.7157 2.12634 14.6667H9.76108V14.644Z" fill="currentColor"/> </g> <defs> <clipPath id="clip0_12185_5035"> <rect width="16" height="16" fill="currentColor"/> </clipPath> </defs> </svg> </ng-template> </ds-segmented-option> </ds-segmented-control> </div> `, }), play: async ({ canvasElement, step }) => { await step('check Click event is being called', async () => { const canvas = within(canvasElement); const segments = canvas.getAllByRole('tab'); await fireEvent.click(segments[0]); await expect(segments[0]).toHaveClass('ds-segment-selected'); }); }, }; ``` -------------------------------------------------------------------------------- /docs/contracts.md: -------------------------------------------------------------------------------- ```markdown # The component contracts system serves two primary purposes: - **Breaking change detection** during refactoring and validation - **Documentation** of successful refactoring patterns for future reference ## Table of Contents 1. [File Organization & Storage Structure](#file-organization--storage-structure) — where contracts and diffs live 2. [What a Contract Includes](#what-a-contract-includes) — data captured in a snapshot 3. [When to Build a Contract](#when-to-build-a-contract) — baseline vs validation 4. [When to Generate Diffs](#when-to-generate-diffs) — automated and manual diffing 5. [Integration with Refactoring Rules Workflow](#integration-with-refactoring-rules-workflow) — contracts in 5-step automation 6. [Post-Review & Hot-Fix Workflow](#post-review--hot-fix-workflow) — fixing bugs after validation 7. [Building a Refactoring Knowledge Base](#building-a-refactoring-knowledge-base) — storing proven patterns ## File Organization & Storage Structure Contracts are organised in a predictable, component-scoped directory tree: ``` .cursor/tmp/contracts/ ├── badge/ # Component-specific directory │ ├── ui-header-20250703T185531Z.contract.json │ ├── ui-header-20250703T192519Z.contract.json │ └── diffs/ # Diff files subdirectory │ └── diff-header-20250703T194046Z.json ├── button/ │ ├── feature-modal-20250704T120000Z.contract.json │ └── diffs/ └── input/ ├── login-form-20250705T090000Z.contract.json └── diffs/ ``` **Key Highlights:** - **Component-scoped directories** - **Timestamped contract files** - **Component-specific diff folders** - **Predictable naming** (`diff-<component>-<timestamp>.json`) ## What a Contract Includes Each contract captures a comprehensive snapshot of a component: ### Public API - **Properties**: Input/output properties with types and default values - **Events**: EventEmitter declarations and their types - **Methods**: Public methods with parameters and return types - **Lifecycle hooks**: Implemented Angular lifecycle interfaces - **Imports**: All imported dependencies and their sources ### DOM Structure - **Element hierarchy**: Complete DOM tree with parent-child relationships - **Attributes**: Static and dynamic attributes on elements - **Bindings**: Property bindings, event handlers, and structural directives - **Content projection**: ng-content slots and their selectors ### Styles - **CSS rules**: All styles applied to DOM elements - **Element mapping**: Which styles apply to which DOM elements - **Source tracking**: Whether styles come from component files or external stylesheets Because contracts track every public-facing facet of a component, any refactor that breaks its API or behaviour is flagged immediately. ## How do I build a contract? Rules taking care about the contract building during the workflow, but if you need to build it "manually" say in the chat: ``` build_component_contract(saveLocation, typescriptFile, templateFile?, styleFile?, dsComponentName?) ``` > Replace the parameters with: > - `saveLocation`: Path where to save the contract file (supports absolute and relative paths) > - `typescriptFile`: Path to the TypeScript component file (.ts) — **Required** > - `templateFile`: *(Optional)* Path to the component template file (.html). Omit for inline templates > - `styleFile`: *(Optional)* Path to the component style file (.scss, .css, etc.). Omit for inline styles or components without styles > - `dsComponentName`: *(Optional)* Design system component name (e.g., `DsBadge`) > > The tool analyses the template, TypeScript, and styles, then saves the contract to your specified location. > > **Note**: Angular components can have inline templates and styles. If `templateFile` or `styleFile` are not provided, the tool will extract inline template/styles from the TypeScript file. ## When to Build a Contract ### Core Principle **Build a new contract whenever a component changes.** This ensures you have an accurate snapshot of the component's state at each critical point in its evolution. ### Pre-Refactoring Contract (Baseline) Always build an initial contract **before** starting any refactoring work. This creates your baseline for comparison. - **Automated workflow**: Handled automatically by the refactoring rules (see `03-fix-violations.mdc`) - **Manual workflow**: Build the contract manually before making any changes ### Post-Refactoring Contract (Validation) Build a new contract **after** completing your refactoring work to capture the final state. - **Automated workflow**: Generated during the validation phase (see `04-validate-changes.mdc`) - **Manual workflow**: Build the contract after completing all changes ### Multiple Contract States For workflows involving QA, E2E testing, or UAT phases, build additional contracts if the component changes during or after testing. **Best practice**: Create a new contract for each significant milestone where the component is modified. ## When to Generate Diffs ### Automated Workflow Diffs Contract diffs are automatically generated during the validation phase (see `04-validate-changes.mdc`). These diffs enable AI-powered validation of refactoring changes. ### Manual Workflow Diffs When refactoring manually or outside the automated rules workflow, generate diffs to: - Identify breaking or risky changes - Get AI analysis of the refactoring impact - Validate that changes meet expectations ## Integration with Refactoring Rules Workflow The contracts system is fully integrated into the 5-step refactoring workflow: ### Step 1: Find Violations (`01-find-violations.mdc`) - **Purpose**: Identify legacy component usage across the codebase - **Contract role**: No contracts generated at this stage - **Output**: Ranked list of folders and files with violations ### Step 2: Plan Refactoring (`02-plan-refactoring.mdc`) - **Purpose**: Create detailed migration plan for each affected component - **Contract role**: Analysis of component structure informs refactoring strategy - **Output**: Comprehensive migration plan with complexity scoring **Note on Non-Viable Cases**: When components are identified as non-viable during step 2 and developer approval is obtained, the non-viable cases handling (`03-non-viable-cases.mdc`) is used instead of proceeding to steps 3-5. This handling does not use contracts as it maintains existing component structure while marking components for exclusion from future reports. ### Step 3: Fix Violations (`03-fix-violations.mdc`) - **Purpose**: Execute the refactoring plan systematically - **Contract role**: **Pre-refactoring contracts are automatically generated** for each component before changes begin - **Key integration**: `build_component_contract(component_files, dsComponentName)` creates baseline contracts - **Output**: Refactored components with baseline contracts stored ### Step 4: Validate Changes (`04-validate-changes.mdc`) - **Purpose**: Verify refactoring safety and detect breaking changes - **Contract role**: **Post-refactoring contracts are generated and compared** against baselines - **Key integration**: - `build_component_contract()` captures refactored state - `diff_component_contract()` compares before/after contracts - AI analyzes diffs for breaking changes and risks - **Output**: Validation report highlighting risky changes ### Step 5: Prepare Report (`05-prepare-report.mdc`) - **Purpose**: Generate testing checklists and documentation - **Contract role**: Contract diffs inform testing requirements and risk assessment - **Output**: Role-specific testing checklists and commit documentation ## Post-Review & Hot-Fix Workflow What happens when QA finds a bug or a reviewer requests changes **after** the initial refactor has already been validated? 1. **Apply the Code Fix** – attach the changed component file(s). 2. **Re-build the "latest" contract** ``` User: build_component_contract(<changed-file.ts>, dsComponentName) ``` The new snapshot will be stored alongside the previous ones in `.cursor/tmp/contracts/<ds-component-kebab>/`. 3. **Locate the original baseline contract** – this is the contract that was captured for the initial state (usually the very first timestamp in the folder). 4. **Generate a diff** between the baseline and the latest contract: ``` User: diff_component_contract(saveLocation, contractBeforePath, contractAfterPath, dsComponentName) ``` > Replace the parameters with: > - `saveLocation`: Path where to save the diff result file (supports absolute and relative paths) > - `contractBeforePath`: Path to the baseline contract file > - `contractAfterPath`: Path to the latest contract file > - `dsComponentName`: Optional design system component name 5. **Review the diff output using AI** – attach the diff and ask it to analyze it. * If only intentional changes appear, proceed to merge / re-test. * If unexpected API, DOM, or style changes surface, iterate on the fix and repeat steps 1-4. ### Why keep the original baseline? Diffing against the **first** snapshot ensures you do not inadvertently mask breaking changes introduced during multiple fix cycles. ### Tip: Cleaning up old snapshots Once a hot-fix is approved, you may delete intermediate contract and diff files to reduce noise and keep the folder tidy – leave the original baseline, final state and the latest approved diff. You can manually move it to `.cursor/tmp/patterns/ds-component-name`. ## Building a Refactoring Knowledge Base Consider storing successful diffs (that passed all checks and testing) to build a knowledge base of proven refactoring patterns. This is particularly valuable when: - Refactoring hundreds or thousands of similar components - Establishing team standards for common refactoring scenarios - Training new team members on safe refactoring practices - Automating similar refactoring tasks in the future ``` -------------------------------------------------------------------------------- /docs/tools.md: -------------------------------------------------------------------------------- ```markdown # Design System Tools for AI Agents This document provides comprehensive guidance for AI agents working with Angular Design System (DS) migration and analysis tools. Each tool is designed to support automated refactoring, validation, and analysis workflows. ## Tool Categories ### 🔍 Project Analysis Tools #### `report-violations` **Purpose**: Identifies deprecated DS CSS usage patterns in Angular projects **AI Usage**: Use as the first step in migration workflows to identify all violations before planning refactoring **Key Parameters**: - `directory`: Target analysis directory (use relative paths like `./src/app`) - `componentName`: DS component class name (e.g., `DsButton`) - `groupBy`: `"file"` or `"folder"` for result organization **Output**: Structured violation reports grouped by file or folder **Best Practice**: Always run this before other migration tools to establish baseline #### `report-all-violations` **Purpose**: Reports all deprecated DS CSS usage for every DS component within a directory **AI Usage**: Use for a fast, global inventory of violations across the codebase before narrowing to specific components **Key Parameters**: - `directory`: Target analysis directory (use relative paths like `./src/app`) - `groupBy`: `"file"` or `"folder"` for result organization (default: `"file"`) **Output**: Structured violation reports grouped by file or folder covering all DS components **Best Practice**: Use to discover all violations and establish the baseline for subsequent refactoring. #### `get-project-dependencies` **Purpose**: Analyzes project structure, dependencies, and buildability **AI Usage**: Validate project architecture before suggesting refactoring strategies **Key Parameters**: - `directory`: Project directory to analyze - `componentName`: Optional DS component for import path validation **Output**: Dependency analysis, buildable/publishable status, peer dependencies **Best Practice**: Use to understand project constraints before recommending changes #### `report-deprecated-css` **Purpose**: Scans styling files for deprecated CSS classes **AI Usage**: Complement violation reports with style-specific analysis **Key Parameters**: - `directory`: Directory containing style files - `componentName`: Target DS component **Output**: List of deprecated CSS classes found in stylesheets **Best Practice**: Run after `report-violations` for comprehensive CSS analysis ### 📚 Component Information Tools #### `list-ds-components` **Purpose**: Lists all available Design System components in the project with their file paths and metadata **AI Usage**: Discover available DS components before starting migration or analysis workflows **Key Parameters**: - `sections`: Array of sections to include - `"implementation"`, `"documentation"`, `"stories"`, or `"all"` (default: `["all"]`) **Output**: Complete inventory of DS components with their implementation files, documentation files, stories files, and import paths **Best Practice**: Use as the first step to understand the DS component landscape before targeted analysis #### `get-ds-component-data` **Purpose**: Returns comprehensive data for a specific DS component including implementation files, documentation files, stories files, and import path **AI Usage**: Get detailed information about a specific component for analysis or migration planning **Key Parameters**: - `componentName`: DS component class name (e.g., `DsBadge`) - `sections`: Array of sections to include - `"implementation"`, `"documentation"`, `"stories"`, or `"all"` (default: `["all"]`) **Output**: Structured data with file paths for implementation, documentation, stories, and import information **Best Practice**: Use selective sections to optimize performance when you only need specific types of files #### `get-component-docs` **Purpose**: Retrieves MDX documentation for DS components **AI Usage**: Access official component documentation to understand proper usage patterns **Key Parameters**: - `componentName`: DS component class name (e.g., `DsButton`) **Output**: API documentation and usage examples in MDX format **Best Practice**: Always consult docs before suggesting component usage changes #### `get-component-paths` **Purpose**: Provides filesystem and NPM import paths for DS components **AI Usage**: Verify correct import paths when suggesting code changes **Key Parameters**: - `componentName`: DS component class name **Output**: Source directory path and NPM import path **Best Practice**: Use to ensure accurate import statements in generated code #### `get-deprecated-css-classes` **Purpose**: Lists deprecated CSS classes for specific DS components **AI Usage**: Understand what CSS classes to avoid or replace during migration **Key Parameters**: - `componentName`: DS component class name **Output**: Array of deprecated CSS class names **Best Practice**: Cross-reference with violation reports to prioritize fixes ### 🔗 Analysis & Mapping Tools #### `build-component-usage-graph` **Purpose**: Maps component usage across modules, specs, templates, and styles **AI Usage**: Understand component dependencies before refactoring to avoid breaking changes **Key Parameters**: - `directory`: Root directory for analysis - `violationFiles`: Array of files with violations (from `report-violations`) **Output**: Component usage graph showing all import relationships **Best Practice**: Essential for large refactoring projects to map impact scope #### `lint-changes` **Purpose**: Runs ESLint validation on changed Angular files **AI Usage**: Validate code quality after making automated changes **Key Parameters**: - `directory`: Root directory containing components - `files`: Optional list of changed files - `configPath`: Optional ESLint config path **Output**: ESLint results and violations **Best Practice**: Always run after code modifications to ensure quality ### 📋 Component Contract Tools #### `build_component_contract` **Purpose**: Creates static surface contracts for component templates and styles **AI Usage**: Generate contracts before refactoring to track breaking changes **Key Parameters**: - `saveLocation`: Path where to save the contract file (supports absolute and relative paths) - `typescriptFile`: **Required** TypeScript component file (.ts) - `templateFile`: *Optional* Template file name (.html). Omit for inline templates - `styleFile`: *Optional* Style file name (.scss, .css, etc.). Omit for inline styles or no styles - `dsComponentName`: *Optional* design system component name **Output**: Component contract file with API surface **Best Practice**: Create contracts before major refactoring for comparison. Template and style files are optional—the tool will extract inline templates/styles from the TypeScript file when not provided #### `diff_component_contract` **Purpose**: Compares before/after contracts to identify breaking changes **AI Usage**: Validate that refactoring doesn't introduce breaking changes **Key Parameters**: - `saveLocation`: Path where to save the diff result file (supports absolute and relative paths) - `contractBeforePath`: Path to pre-refactoring contract - `contractAfterPath`: Path to post-refactoring contract - `dsComponentName`: Optional design system component name **Output**: Diff analysis showing breaking changes **Best Practice**: Essential validation step after component modifications #### `list_component_contracts` **Purpose**: Lists all available component contracts **AI Usage**: Discover existing contracts for comparison operations **Key Parameters**: - `directory`: Directory to search for contracts **Output**: List of available contract files **Best Practice**: Use to manage contract lifecycle during refactoring ### ⚙️ Configuration Tools ## AI Agent Workflow Patterns ### 1. Discovery & Analysis Workflow ``` 1. list-ds-components → Discover available DS components 2. report-violations → Identify all violations 3. get-project-dependencies → Analyze project structure ``` ### 2. Planning & Preparation Workflow ``` 1. get-ds-component-data → Get comprehensive component information 2. build-component-usage-graph → Map component relationships 3. get-component-docs → Review proper usage patterns 4. get-component-paths → Verify import paths 5. build_component_contract → Create baseline contracts ``` ### 3. Refactoring & Validation Workflow ``` 1. [Apply code changes] 2. build_component_contract → Create post-change contracts 3. diff_component_contract → Validate no breaking changes 4. lint-changes → Ensure code quality ``` ### 4. Non-Viable Cases Handling (Alternative to Steps 3-5) ``` 1. report-deprecated-css → Identify CSS usage in global styles 2. report-deprecated-css → Identify CSS usage in component overrides 3. [Replace HTML classes with after-migration- prefix] 4. [Duplicate CSS selectors with prefixed versions] 5. report-deprecated-css → Validate CSS count consistency 6. report-violations → Validate violation reduction ``` **Purpose**: Used during the main DS refactoring workflow when components are identified as non-viable during planning step **Trigger**: Requires developer review and approval after AI identifies non-viable cases in step 2 **Key Pattern**: Use `after-migration-[ORIGINAL_CLASS]` prefix to exclude components from future analysis **Replaces**: Normal fix violations → validate changes → prepare report sequence ## Error Handling for AI Agents - **Path Resolution**: Always use relative paths starting with `./` - **Component Names**: Use PascalCase with `Ds` prefix (e.g., `DsButton`) - **File Arrays**: Ensure violation file arrays are properly formatted - **Directory Validation**: Verify directories exist before analysis - **Contract Management**: Clean up temporary contracts after analysis ## Performance Considerations - Use `groupBy: "folder"` for large codebases to reduce output size - Limit `violationFiles` arrays to relevant files only - Use selective `sections` parameter in `get-ds-component-data` and `list-ds-components` to retrieve only needed data types - Cache component documentation between related operations - Run validation tools in parallel when possible ## Integration Points These tools integrate with: - **Storybook**: For component documentation - **PostCSS**: For style analysis ## Output Formats All tools return structured data suitable for: - JSON parsing for programmatic analysis - Markdown formatting for human-readable reports - File path arrays for batch operations - Contract objects for API comparison ``` -------------------------------------------------------------------------------- /packages/shared/typescript-ast-utils/ai/EXAMPLES.md: -------------------------------------------------------------------------------- ```markdown # Examples ## 1 — Identifying Angular Component decorators > Find and validate `@Component` decorators in Angular class declarations. ```ts import { isComponentDecorator, getDecorators, } from '@push-based/typescript-ast-utils'; import * as ts from 'typescript'; // Sample Angular component source code const sourceCode = ` @Component({ selector: 'app-example', template: '<div>Hello World</div>' }) export class ExampleComponent {} `; // Create source file and program const sourceFile = ts.createSourceFile( 'example.ts', sourceCode, ts.ScriptTarget.Latest ); // Visit class declarations function visitClassDeclaration(node: ts.ClassDeclaration) { const decorators = getDecorators(node); for (const decorator of decorators) { if (isComponentDecorator(decorator)) { console.log(`Found @Component decorator on class: ${node.name?.text}`); // → 'Found @Component decorator on class: ExampleComponent' } } } // Traverse the AST ts.forEachChild(sourceFile, function visit(node) { if (ts.isClassDeclaration(node)) { visitClassDeclaration(node); } ts.forEachChild(node, visit); }); ``` --- ## 2 — Generic decorator detection > Detect any decorator by name or check for any decorators on a node. ```ts import { isDecorator, getDecorators } from '@push-based/typescript-ast-utils'; import * as ts from 'typescript'; const sourceCode = ` @Injectable() @Component({ selector: 'app-service' }) @CustomDecorator('config') export class ServiceComponent {} `; const sourceFile = ts.createSourceFile( 'service.ts', sourceCode, ts.ScriptTarget.Latest ); function analyzeDecorators(node: ts.ClassDeclaration) { const decorators = getDecorators(node); console.log(`Found ${decorators.length} decorators`); // → 'Found 3 decorators' for (const decorator of decorators) { // Check for specific decorators if (isDecorator(decorator, 'Injectable')) { console.log('Has @Injectable decorator'); } if (isDecorator(decorator, 'Component')) { console.log('Has @Component decorator'); } if (isDecorator(decorator, 'CustomDecorator')) { console.log('Has @CustomDecorator decorator'); } // Check if it's any valid decorator if (isDecorator(decorator)) { console.log('Found a valid decorator'); } } } // Output: // → 'Found 3 decorators' // → 'Has @Injectable decorator' // → 'Found a valid decorator' // → 'Has @Component decorator' // → 'Found a valid decorator' // → 'Has @CustomDecorator decorator' // → 'Found a valid decorator' ``` --- ## 3 — Removing quotes from string literals > Clean quoted strings from AST nodes for processing. ```ts import { removeQuotes } from '@push-based/typescript-ast-utils'; import * as ts from 'typescript'; const sourceCode = ` const singleQuoted = 'hello world'; const doubleQuoted = "typescript utils"; const backtickQuoted = \`template string\`; const multipleQuotes = """heavily quoted"""; `; const sourceFile = ts.createSourceFile( 'strings.ts', sourceCode, ts.ScriptTarget.Latest ); function processStringLiterals(node: ts.Node) { if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) { const originalText = node.getText(sourceFile); const cleanedText = removeQuotes(node, sourceFile); console.log(`Original: ${originalText} → Cleaned: "${cleanedText}"`); } } ts.forEachChild(sourceFile, function visit(node) { processStringLiterals(node); ts.forEachChild(node, visit); }); // Output: // → Original: 'hello world' → Cleaned: "hello world" // → Original: "typescript utils" → Cleaned: "typescript utils" // → Original: `template string` → Cleaned: "template string" // → Original: """heavily quoted""" → Cleaned: "heavily quoted" ``` --- ## 4 — Safe decorator extraction across TypeScript versions > Handle different TypeScript compiler API versions when extracting decorators. ```ts import { getDecorators, hasDecorators } from '@push-based/typescript-ast-utils'; import * as ts from 'typescript'; const sourceCode = ` @Deprecated() @Component({ selector: 'legacy-component' }) export class LegacyComponent { @Input() data: string; @Output() change = new EventEmitter(); } `; const sourceFile = ts.createSourceFile( 'legacy.ts', sourceCode, ts.ScriptTarget.Latest ); function safelyExtractDecorators(node: ts.Node) { // Safe extraction that works across TypeScript versions const decorators = getDecorators(node); if (decorators.length > 0) { console.log(`Node has ${decorators.length} decorators`); // Type-safe check if (hasDecorators(node)) { console.log('Confirmed: node has decorators property'); } } return decorators; } // Process class and its members ts.forEachChild(sourceFile, function visit(node) { if (ts.isClassDeclaration(node)) { console.log(`Class decorators: ${safelyExtractDecorators(node).length}`); // → 'Class decorators: 2' // Check property decorators for (const member of node.members) { if (ts.isPropertyDeclaration(member)) { const memberDecorators = safelyExtractDecorators(member); if (memberDecorators.length > 0) { console.log( `Property "${member.name?.getText(sourceFile)}" has ${ memberDecorators.length } decorators` ); // → 'Property "data" has 1 decorators' // → 'Property "change" has 1 decorators' } } } } ts.forEachChild(node, visit); }); ``` --- ## 5 — Building a decorator analyzer tool > Create a comprehensive tool to analyze all decorators in a TypeScript file. ```ts import { getDecorators, isDecorator, isComponentDecorator, removeQuotes, } from '@push-based/typescript-ast-utils'; import * as ts from 'typescript'; interface DecoratorInfo { name: string; target: string; arguments: string[]; line: number; } class DecoratorAnalyzer { private decorators: DecoratorInfo[] = []; analyze( sourceCode: string, fileName: string = 'analysis.ts' ): DecoratorInfo[] { const sourceFile = ts.createSourceFile( fileName, sourceCode, ts.ScriptTarget.Latest ); this.decorators = []; this.visitNode(sourceFile, sourceFile); return this.decorators; } private visitNode(node: ts.Node, sourceFile: ts.SourceFile) { const decorators = getDecorators(node); if (decorators.length > 0) { const targetName = this.getTargetName(node, sourceFile); for (const decorator of decorators) { const decoratorInfo = this.extractDecoratorInfo( decorator, targetName, sourceFile ); if (decoratorInfo) { this.decorators.push(decoratorInfo); } } } ts.forEachChild(node, (child) => this.visitNode(child, sourceFile)); } private extractDecoratorInfo( decorator: ts.Decorator, targetName: string, sourceFile: ts.SourceFile ): DecoratorInfo | null { if (!isDecorator(decorator)) return null; const expression = decorator.expression; let name = ''; const args: string[] = []; if (ts.isIdentifier(expression)) { name = expression.text; } else if ( ts.isCallExpression(expression) && ts.isIdentifier(expression.expression) ) { name = expression.expression.text; // Extract arguments for (const arg of expression.arguments) { if (ts.isStringLiteral(arg)) { args.push(removeQuotes(arg, sourceFile)); } else { args.push(arg.getText(sourceFile)); } } } const line = sourceFile.getLineAndCharacterOfPosition(decorator.getStart()).line + 1; return { name, target: targetName, arguments: args, line, }; } private getTargetName(node: ts.Node, sourceFile: ts.SourceFile): string { if (ts.isClassDeclaration(node) && node.name) { return `class ${node.name.text}`; } if (ts.isPropertyDeclaration(node) && node.name) { return `property ${node.name.getText(sourceFile)}`; } if (ts.isMethodDeclaration(node) && node.name) { return `method ${node.name.getText(sourceFile)}`; } return 'unknown'; } } // Usage example const analyzer = new DecoratorAnalyzer(); const sourceCode = ` @Component({ selector: 'app-example', template: '<div>Example</div>' }) export class ExampleComponent { @Input('inputAlias') data: string; @Output() change = new EventEmitter(); @HostListener('click', ['$event']) onClick(event: Event) {} } `; const results = analyzer.analyze(sourceCode); results.forEach((info) => { console.log(`Line ${info.line}: @${info.name} on ${info.target}`); if (info.arguments.length > 0) { console.log(` Arguments: ${info.arguments.join(', ')}`); } }); // Output: // → Line 1: @Component on class ExampleComponent // → Arguments: { selector: 'app-example', template: '<div>Example</div>' } // → Line 6: @Input on property data // → Arguments: inputAlias // → Line 8: @Output on property change // → Line 10: @HostListener on method onClick // → Arguments: click, ['$event'] ``` --- ## 6 — Error handling and edge cases > Handle malformed decorators and edge cases gracefully. ```ts import { getDecorators, isDecorator } from '@push-based/typescript-ast-utils'; import * as ts from 'typescript'; const problematicCode = ` // Valid decorator @Component() export class ValidComponent {} // Malformed decorator (will be handled gracefully) @ export class MalformedDecorator {} // Complex decorator expression @NgModule({ imports: [CommonModule], declarations: [SomeComponent] }) export class ComplexModule {} `; function safeDecoratorAnalysis(sourceCode: string) { try { const sourceFile = ts.createSourceFile( 'test.ts', sourceCode, ts.ScriptTarget.Latest ); ts.forEachChild(sourceFile, function visit(node) { if (ts.isClassDeclaration(node)) { const decorators = getDecorators(node); console.log(`Analyzing class: ${node.name?.text || 'anonymous'}`); console.log(`Found ${decorators.length} decorators`); for (const decorator of decorators) { try { if (isDecorator(decorator)) { console.log('✓ Valid decorator found'); } else { console.log('✗ Invalid decorator structure'); } } catch (error) { console.log(`⚠ Error processing decorator: ${error}`); } } console.log('---'); } ts.forEachChild(node, visit); }); } catch (error) { console.error('Failed to parse source code:', error); } } safeDecoratorAnalysis(problematicCode); // Output: // → Analyzing class: ValidComponent // → Found 1 decorators // → ✓ Valid decorator found // → --- // → Analyzing class: MalformedDecorator // → Found 0 decorators // → --- // → Analyzing class: ComplexModule // → Found 1 decorators // → ✓ Valid decorator found // → --- ``` These examples demonstrate the comprehensive capabilities and practical usage patterns of the `@push-based/typescript-ast-utils` library for TypeScript AST analysis, decorator processing, and source code manipulation. ``` -------------------------------------------------------------------------------- /packages/shared/angular-ast-utils/docs/angular-component-tree.md: -------------------------------------------------------------------------------- ```markdown # Angular Component Tree This document describes the structure of an Angular component tree created by using the utils helper under `angular.` The tree is a representation of the Angular component structure in a project. It is used to visualize the component hierarchy and the relationships between components. ## File Content - JS Content: `*{}` - shorthand for CSS text content - HTML Content: `</>` - shorthand for HTML text content - CSS Content: `{;}` - shorthand for JS text content - JSON Content: `[:]` - shorthand for JSON text content ## File-tree - Sold Style: `━` - bold - Tree: `┃` - Tree connection - Branch: `┣━━` / `┗━━` - Content connection e.g. `Object property` - Folder Connector: `📂` - the property name `children` - File Name: `:` - any value `string`, `number` - Important File: `:` - entry points e.g. `📦` repo; `🅰️`, `⚛️` framework; `🟦` stack, `📜` organisation ### Example ```bash root ┣━━ 📂src ┃ ┗━━ 📂button ┃ ┣━━ button.component.ts ┃ ┣━━ 📂other-compoennt ┃ ┗━━ 📂other-folder ┃ ┗━━ button.component.ts ┣━━ 📦package.json ┣━━ 🟦tsconfig.json ┣━━ 🅰️angular.json ┗━━ 📜README.md ``` ## Code-tree - Line Type: `│` - Tree connection - Tree Style: `─` - light - Branch: `├` / `└` - Content connection e.g. `Object property` - Branch Connector: `╼` - the property name `children` - Array: `[ ]` - Information list e.g. `Array`, `Set`, `Iterable` - Array Index: `[0]` - list item e.g. `Object property` - Object: `{ }` - Information pairs e.g. `Class`, `Object`, `Map` - Prop Name: `prop` - any value `string` - Value Connector: `:` - separates property or index from value - Prop Value: `42`, `"test"` - any value any value `string`, `number` ### Example ```bash { } # component object ├╼ className: 'ButtonComponent' - # property: string └╼ styleUrls: [ ] # array ├╼ [0]: { } # index 0 - first style object │ ├╼ startLine: 4 │ └╼ value: './styles-1.css' │ └╼ value: './styles-1.css' └╼ [1]: { } # index 1 - second style object ├╼ startLine: 2 - # property: number └╼ value: './styles-2.css' - # property: string ``` ## AST-tree - Line Type: `│` - Tree connection - Tree Style: `─` - light - Branch: `├` / `└` - Content connection e.g. `Object property` - Branch Connector: `↘` - the property name `children` - Array: `[ ]` - Information list e.g. `Array`, `Set`, `Iterable` - Array Index: `[0]` - list item e.g. `Object property` - Object: `{ }` - Information pairs e.g. `Class`, `Object`, `Map` - Prop Name: `prop` - any value `string` - Value Connector: `:` - separates property or index from value - Prop Value: `42`, `"test"` - any value any value `string`, `number` ### Example _`component.ts` - Text representation of the file content_ ```bash export class BaseComponent { styles: String; } export class Component { styles = ['styles.css']; } ``` _AbstractSyntaxTree representation of the file content of `component.ts`_ ```bash ( ) # `sourceFile` - A plain JavaScript function returning a object └╼ sourceFile: TsAstNode # Start of AST tree ├↘ [0]: ClassDeclaration # shorter form us array syntax is used └↘ [1]: ClassDeclaration ├↘ ClassDeclaration ├↘ FunctionDeclaration └↘ PropertyDeclaration └↘ ObjectLiteralExpression └↘ PropertyAssignment └↘ Identifier └↘ getText(): './button.css' # Function returning `./button.css` ⤷ `.btn { color: red; }` # './button.css' content ``` ## Tree Links - File System Reference: `⤷` - links a file reference to it's content - In Memory Reference: `↳` - links a reference to values, pairs or lists - In Memory Reference: `↳` - links a reference to values, pairs or lists ### File System Reference ```bash root ┗━━ 📂constants ┣━━ button.ts # const buttonStylePath = './button.css' ┃ ⤷ `.btn { color: red; }` # './button.css' content ┗━━ select.ts # const selectTemplatePath = './select.css' ⤷ `<button class="btn btn-primary">Click me</button>` # './select.css' content ``` ### In Memory Reference to File System ```bash { } # The default export of 'file.ts' file. (a JavaScript object) ├╼ className: 'ButtonComponent' - # property: string └╼ styleUrls: [ ] # array └╼ [0] './button.css' # index 0 ⤷ `.btn { color: red; }` # './button.css' content ``` ### In Memory Reference - AST to FileSystem ```bash ( ) # `sourceFile` - A plain JavaScript function returning a object └╼ sourceFile: └╼ sourceFile: ObjectLiteralExpression └↘ PropertyAssignment └↘ Identifier └↘ getText(): './button.css' # Function returning `./button.css` ⤷ `.btn { color: red; }` # './button.css' content ``` ## Angular Projects ### Minimal Angular Project - File Tree ```bash root ┣━━ 📂public ┣━━ 📂src ┃ ┣━━ 📂app ┃ ┃ ┣━━ 📂until ┃ ┃ ┃ ┣━━ 📂button ┃ ┃ ┃ ┃ ┗━━ button.component.ts ┃ ┃ ┃ ┃ ↳ inline-css ┃ ┃ ┃ ┃ ↳ inline-html ┃ ┃ ┃ ┣━━ 📂select ┃ ┃ ┃ ┃ ┣━━ select.component.ts ┃ ┃ ┃ ┃ ┣━━ ⤷ select.component.css ┃ ┃ ┃ ┃ ┗━━ ⤷ select.component.html ┃ ┃ ┃ ┗━━... ┃ ┃ ┣━━ app.routes.ts ┃ ┃ ┣━━ app.component.ts ┃ ┃ ┗━━ app.config.ts ┃ ┣━━ index.html ┃ ┣━━ main.ts ┃ ┗━━ styles.css ┣━━ 📦package.json ┣━━ 🟦tsconfig.json ┣━━ 🅰️angular.json ┗━━ README.md ``` ### Inline Assets (Styles and Templates) ```bash root ┗━━ 📂any ┗━━ any.component.ts ├╼ styles: [ ] │ ├╼ [0]: `.btn { size: 13px; }` │ └╼ [1]: `.red { color: red; }` └╼ template: `<button class="btn red">Click me</button>` ``` ### External Assets (Styles and Templates) ```bash root ┣━━ 📂any ┣━━ any.component.ts ┃ ├╼ styleUrls: [ ] ┃ │ ├╼ [0]: `any.styles.css` ┃ │ │ ⤷ `.btn { size: 13px; }` ┃ │ └╼ [1]: `other.styles.css` ┃ │ ⤷ `.red { color: red; }` ┃ └╼ templateUrl: 'any.component.html' ┃ ⤷ `<button class="btn red">Click me</button>` ┣━━ any.style.css ┃ └╼ `.btn { color: red; }` ┣━━ other.style-1.css ┃ └╼ `.btn-primary { color: blue; }` ┗━━ any.component.html └╼ `<button class="btn btn-primary">Click me</button>` ``` ## Creating Component AST's ### 1. Find Component Files - Regex matching This part of the process should get optimized for scale as it is one of the costly steps. We use simple regex matching against a string pattern to detect interesting files. It is accepted that some of the matching files don't contain components (false positives), as they are later on excluded anyway. ```bash [ ] ├╼ [0]: 'src/app/app.component.ts' ├╼ [1]: 'src/app/until/button/button.component.ts' └╼ [2]: 'src/app/until/select.component.ts' ``` ### 2. Create the TS Program - Read Files and Parse AST In this step we need to feed the TS program our entry to the frameworks TS code. The entry is located in Angular's RC file `angular.json`. ```bash root ┗━━ angular.json ⤷ index.html ⤷ styles.css ⤷ tsconfig.json ⤷ └↳main.ts # TSProgram entry └↘ app.config.ts └↘ app.routes.ts └↘ app/app.component.ts ├↘ button.component.ts │ ↳ styles[0]: `*{}` │ ↳ styles[1]: `*{}` │ ↳ template: `</>` └↘ select.component.ts ├↘ styleUrls[0]: 'select.styles.css' │ ↳ `*{}` ├↘ styleUrls[1]: 'select.styles.css' │ ↳ `*{}` └↘ templateUrl: 'select.component.html'' ↳ `</>` ``` This can potentially be used to filter or add more file to our result file list. ```ts const tsProgramm = createProgram([ 'src/app/app.component.ts', // ... ]); const components = tsProgramm.getSourceFiles(); ``` _components content_ ```bash [ ] ├↘ FunctionDeclaration # node_modules/lib/file.js ├↘ ClassDeclaration # node_modules/other-lib/other-file.js ├↘ VariableDeclaration # node_modules/any-lib/any-file.js ├↘ ClassDeclaration # app/app.component.ts ├↘ ClassDeclaration # app/ui/button.component.ts └↘ ClassDeclaration # app/ui/select.component.ts ``` ### 3. Filtered Components - exclude other files that got added due to existing imports ```ts components.filter((source) => matchingFiles.includes(source)); ``` ```bash [ ] ├↘ ClassDeclaration # app/app.component.ts#AppComponent AST ├↘ ClassDeclaration # app/ui/button.component.ts#ButtonComponent AST └↘ ClassDeclaration # app/ui/select.component.ts#SelectComponent AST ``` ### 4. Parsed Component Class To go on with creating the Angular component tree we need to get the essential information of every component. We need: - basic component data (file, name, contextual infos) - the component's AST - all references to assets (internal as well as external) - in a step that should be executable lazily we need to resolve the assets As we have to nest different trees here let's quickly define the types that guide us in the future. ```ts type Unit = { type: 'class' | 'html' | 'styles'; }; type Code = { filePath: string; value: string; }; type CodeAware = { source: <T extends AST>() => T; }; type LinkedCode<T> = Code & CodeAware<T> & { startLine: string; }; type Style = LinkedCode<CssAst> & { type: 'class'; }; type Template = LinkedCode<HtmlAst> & { type: 'class'; }; type ParsedComponentClass<T> = Code & CodeAware<T> & { type: 'class'; className: string; styles?: Style[]; styleUrls?: Style[]; template?: Template; templateUrl?: Template; }; ``` ```bash ParsedComponent ├╼ className: 'AppComponent`' ├╼ filePath: './app.component.ts' ├╼ value: `{;}` ├╼ source(): TsAst │ └↘ ClassDeclaration ├╼ styles: [ ] │ └╼ [0]: { } │ ├╼ filePath: './app.component.ts' │ ├╼ startLine: 7 │ ├╼ value: `*{}` │ └╼ source(): CssAst │ └↘ RuleContainer ├╼ styleUrls: [ ] │ └╼ [0]: { } │ ├╼ filePath: './any-styles.css' │ ├╼ startLine: 13 │ ├╼ value: `*{}` │ └╼ source(): CssAst │ └↘ RuleContainer ├╼ template: { } │ ├╼ filePath: './app.component.ts' │ ├╼ startLine: 21 │ ├╼ value: `</>` │ └╼ source(): HtmlAst │ └↘ Element └╼ templateUrl: { } ├╼ filePath: './app.component.html' ├╼ startLine: 42 ├╼ value: `*{}` └╼ source(): HtmlAst └↘ Element ``` ### 5. Glue Traversal Logic // walk component tree ```ts const comps: ParsedCompoent[] = getParsedComponents(); function walkComponents(node: CodeAware, visitor: T) { for (let comp of comps) { if ('source' in comp) { const unit = comp.source(); switch (unit.type) { case 'class': ts.visitAllChildren(unit, visitor as TsVisitor); case 'styles': walkRules(unit, visitor as CssVisitor); case 'html': forEachChild(unit, visitor as HtmlVisitor); } } } } ``` ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/complex-components/second-case/complex-badge-widget.component.scss: -------------------------------------------------------------------------------- ```scss .widget-container { padding: 2rem; background: #f8fafc; border-radius: 0.75rem; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); } .widget-container h3 { margin: 0 0 1.5rem 0; color: #1f2937; font-size: 1.5rem; font-weight: 600; } .badge-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; margin-bottom: 2rem; } .badge-item { position: relative; background: white; border-radius: 0.5rem; padding: 1rem; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); transition: all 0.3s ease; } .badge-item:hover { transform: translateY(-2px); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15); } // Complex custom badge styles that will be difficult to refactor .offer-badge { display: flex; flex-direction: column; background: var(--badge-color, #6b7280); color: white; border-radius: 0.75rem; padding: 0; overflow: hidden; position: relative; cursor: pointer; transition: all 0.3s ease; min-height: 120px; // Complex pseudo-elements that DsBadge won't support &::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: linear-gradient(90deg, rgba(255,255,255,0.3) 0%, rgba(255,255,255,0.8) 50%, rgba(255,255,255,0.3) 100%); animation: shimmer 2s infinite; } &::after { content: attr(data-custom-prop); position: absolute; top: 0.5rem; right: 0.5rem; background: rgba(0, 0, 0, 0.2); padding: 0.25rem 0.5rem; border-radius: 0.25rem; font-size: 0.625rem; font-weight: 600; text-transform: uppercase; } // Level-specific complex styling &.offer-badge-low { background: linear-gradient(135deg, #10b981 0%, #059669 100%); .offer-badge-header { background: rgba(5, 150, 105, 0.2); border-bottom: 2px solid rgba(5, 150, 105, 0.3); } .level-dot.active { background: #34d399; box-shadow: 0 0 8px #34d399; } } &.offer-badge-medium { background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); .offer-badge-header { background: rgba(217, 119, 6, 0.2); border-bottom: 2px solid rgba(217, 119, 6, 0.3); } .level-dot.active { background: #fbbf24; box-shadow: 0 0 8px #fbbf24; } } &.offer-badge-high { background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); .offer-badge-header { background: rgba(37, 99, 235, 0.2); border-bottom: 2px solid rgba(37, 99, 235, 0.3); } .level-dot.active { background: #60a5fa; box-shadow: 0 0 8px #60a5fa; } } &.offer-badge-critical { background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); animation: pulse-critical 2s infinite; .offer-badge-header { background: rgba(220, 38, 38, 0.2); border-bottom: 2px solid rgba(220, 38, 38, 0.3); } .level-dot.active { background: #f87171; box-shadow: 0 0 8px #f87171; animation: blink 1s infinite; } } // Type-specific complex styling &.offer-badge-offer-badge { .offer-badge-type-indicator { animation: bounce 2s infinite; } .offer-badge-content { background: rgba(255, 255, 255, 0.1); backdrop-filter: blur(10px); } } &.offer-badge-status { .offer-badge-header { background: linear-gradient(45deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.2) 100%); } .badge-status-indicator { width: 12px; height: 12px; border-radius: 50%; background: #10b981; animation: pulse-status 1.5s infinite; } } &.offer-badge-priority { border: 2px solid rgba(255, 255, 255, 0.3); .offer-badge-type-indicator { color: #fbbf24; text-shadow: 0 0 8px #fbbf24; } } // Interactive states &.offer-badge-interactive { &:hover { transform: scale(1.02); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); .offer-badge-header { background: rgba(255, 255, 255, 0.2); } .offer-badge-footer { background: rgba(255, 255, 255, 0.1); } } &:active { transform: scale(0.98); } } &.offer-badge-selected { border: 3px solid #fbbf24; box-shadow: 0 0 0 3px rgba(251, 191, 36, 0.3); &::before { background: linear-gradient(90deg, #fbbf24 0%, #f59e0b 50%, #fbbf24 100%); } } // Advanced mode modifications &.modified-badge { border: 2px dashed rgba(255, 255, 255, 0.5); .offer-badge-content { filter: hue-rotate(30deg); } } } // Complex nested structure that DsBadge cannot replicate .offer-badge-header { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem 1rem; background: rgba(255, 255, 255, 0.1); border-bottom: 1px solid rgba(255, 255, 255, 0.2); transition: all 0.3s ease; &.hover-active { background: rgba(255, 255, 255, 0.2); transform: translateY(-1px); } } .offer-badge-type-indicator { font-size: 1.25rem; font-weight: 600; display: flex; align-items: center; gap: 0.5rem; &::after { content: attr(data-type); font-size: 0.75rem; opacity: 0.8; } } .offer-badge-level-dots { display: flex; gap: 0.25rem; align-items: center; } .level-dot { width: 8px; height: 8px; border-radius: 50%; background: rgba(255, 255, 255, 0.3); transition: all 0.3s ease; &.active { background: white; box-shadow: 0 0 4px rgba(255, 255, 255, 0.8); } } .offer-badge-content { flex: 1; padding: 1rem; display: flex; flex-direction: column; justify-content: center; position: relative; &::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(45deg, transparent 0%, rgba(255,255,255,0.05) 50%, transparent 100%); pointer-events: none; } } .offer-badge-primary-text { font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } .offer-badge-metadata { display: flex; flex-direction: column; gap: 0.25rem; opacity: 0.9; } .badge-id, .badge-timestamp { font-size: 0.75rem; font-weight: 500; opacity: 0.8; } .offer-badge-footer { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem 1rem; background: rgba(0, 0, 0, 0.1); border-top: 1px solid rgba(255, 255, 255, 0.1); transition: all 0.3s ease; &.hover-active { background: rgba(0, 0, 0, 0.2); transform: translateY(1px); } } .badge-action { background: rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.3); color: white; padding: 0.25rem 0.75rem; border-radius: 0.25rem; font-size: 0.75rem; font-weight: 500; cursor: pointer; transition: all 0.2s ease; &:hover { background: rgba(255, 255, 255, 0.3); transform: translateY(-1px); } &.danger { background: rgba(239, 68, 68, 0.3); border-color: rgba(239, 68, 68, 0.5); &:hover { background: rgba(239, 68, 68, 0.5); } } } .badge-status-indicator { width: 10px; height: 10px; border-radius: 50%; background: #10b981; // Dynamic status classes that depend on complex logic &.status-low-offer-badge { background: #10b981; animation: pulse-green 2s infinite; } &.status-medium-status { background: #f59e0b; animation: pulse-yellow 2s infinite; } &.status-high-priority { background: #3b82f6; animation: pulse-blue 2s infinite; } &.status-critical-priority { background: #ef4444; animation: pulse-red 1s infinite; } } // Tooltip system that depends on custom badge structure .badge-tooltip { position: absolute; top: 100%; left: 50%; transform: translateX(-50%); background: #1f2937; color: white; padding: 1rem; border-radius: 0.5rem; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); opacity: 0; pointer-events: none; transition: all 0.3s ease; z-index: 1000; min-width: 200px; &.visible { opacity: 1; pointer-events: auto; transform: translateX(-50%) translateY(0.5rem); } &::before { content: ''; position: absolute; top: -8px; left: 50%; transform: translateX(-50%); border-left: 8px solid transparent; border-right: 8px solid transparent; border-bottom: 8px solid #1f2937; } } .tooltip-content { h4 { margin: 0 0 0.5rem 0; font-size: 1rem; font-weight: 600; } p { margin: 0 0 0.25rem 0; font-size: 0.875rem; opacity: 0.9; } } .custom-data { margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid rgba(255, 255, 255, 0.2); } .data-item { font-size: 0.75rem; margin-bottom: 0.25rem; strong { color: #fbbf24; } } // Widget controls .widget-controls { display: flex; gap: 1rem; margin-bottom: 2rem; flex-wrap: wrap; align-items: center; button { background: #3b82f6; color: white; border: none; padding: 0.5rem 1rem; border-radius: 0.375rem; font-size: 0.875rem; font-weight: 500; cursor: pointer; transition: all 0.2s ease; &:hover { background: #2563eb; transform: translateY(-1px); } } label { display: flex; align-items: center; gap: 0.5rem; font-size: 0.875rem; color: #374151; cursor: pointer; input[type="checkbox"] { width: 1rem; height: 1rem; } } } // Status display .status-display { background: white; padding: 1.5rem; border-radius: 0.5rem; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); h4 { margin: 0 0 1rem 0; color: #1f2937; font-size: 1.125rem; font-weight: 600; } } .status-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; } .status-item { padding: 0.75rem; background: #f9fafb; border-radius: 0.375rem; text-align: center; strong { display: block; color: #374151; font-size: 0.875rem; margin-bottom: 0.25rem; } // Dynamic content that depends on badge counting &:nth-child(1) strong::after { content: ' 📊'; } &:nth-child(2) strong::after { content: ' 🎯'; } &:nth-child(3) strong::after { content: ' ✅'; } &:nth-child(4) strong::after { content: ' ⚠️'; } } // Complex animations that DsBadge won't support @keyframes shimmer { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } @keyframes pulse-critical { 0%, 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); } 50% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); } } @keyframes blink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0.3; } } @keyframes bounce { 0%, 20%, 50%, 80%, 100% { transform: translateY(0); } 40% { transform: translateY(-4px); } 60% { transform: translateY(-2px); } } @keyframes pulse-status { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.2); opacity: 0.7; } } @keyframes pulse-green { 0%, 100% { background: #10b981; } 50% { background: #34d399; } } @keyframes pulse-yellow { 0%, 100% { background: #f59e0b; } 50% { background: #fbbf24; } } @keyframes pulse-blue { 0%, 100% { background: #3b82f6; } 50% { background: #60a5fa; } } @keyframes pulse-red { 0%, 100% { background: #ef4444; } 50% { background: #f87171; } } // Responsive design with complex badge adaptations @media (max-width: 768px) { .badge-grid { grid-template-columns: 1fr; } .offer-badge { min-height: 100px; .offer-badge-header { padding: 0.5rem; } .offer-badge-content { padding: 0.75rem; } .offer-badge-footer { padding: 0.5rem; } .offer-badge-primary-text { font-size: 1rem; } // Complex responsive behavior that DsBadge won't handle &.offer-badge-critical { .offer-badge-type-indicator { font-size: 1rem; } .level-dot { width: 6px; height: 6px; } } } .widget-controls { flex-direction: column; align-items: stretch; button { width: 100%; } } } ``` -------------------------------------------------------------------------------- /packages/shared/utils/ai/EXAMPLES.md: -------------------------------------------------------------------------------- ```markdown # Examples ## 1 — Process execution with real-time monitoring > Execute commands with live output streaming and error handling. ```ts import { executeProcess, ProcessObserver } from '@push-based/utils'; // Create an observer to handle process events const observer: ProcessObserver = { onStdout: (data) => { console.log(`📤 ${data.trim()}`); }, onStderr: (data) => { console.error(`❌ ${data.trim()}`); }, onError: (error) => { console.error(`Process failed with code ${error.code}`); }, onComplete: () => { console.log('✅ Process completed successfully'); }, }; // Execute a Node.js command const result = await executeProcess({ command: 'node', args: ['--version'], observer, }); console.log(`Exit code: ${result.code}`); console.log(`Duration: ${result.duration}ms`); console.log(`Output: ${result.stdout.trim()}`); // Output: // → 📤 v18.17.0 // → ✅ Process completed successfully // → Exit code: 0 // → Duration: 45ms // → Output: v18.17.0 ``` --- ## 2 — File pattern searching and processing > Search for files containing specific patterns and process the results. ```ts import { findFilesWithPattern, findInFile, resolveFileCached, } from '@push-based/utils'; // Find all TypeScript files containing 'Component' const componentFiles = await findFilesWithPattern('./src', 'Component'); console.log(`Found ${componentFiles.length} files with 'Component':`); componentFiles.forEach((file) => console.log(` - ${file}`)); // Get detailed information about matches in a specific file if (componentFiles.length > 0) { const firstFile = componentFiles[0]; const matches = await findInFile(firstFile, 'Component'); console.log(`\nDetailed matches in ${firstFile}:`); matches.forEach((match) => { console.log( ` Line ${match.position.startLine}, Column ${match.position.startColumn}` ); }); // Load and cache the file content const content = await resolveFileCached(firstFile); console.log(`File size: ${content.length} characters`); // Subsequent calls will use cached version const cachedContent = await resolveFileCached(firstFile); // ⚡ Fast cached access } // Output: // → Found 3 files with 'Component': // → - ./src/app/user.component.ts // → - ./src/app/admin.component.ts // → - ./src/shared/base.component.ts // → // → Detailed matches in ./src/app/user.component.ts: // → Line 5, Column 14 // → Line 12, Column 25 // → File size: 1247 characters ``` --- ## 3 — Command formatting and logging > Format commands with colors and context for better development experience. ```ts import { formatCommandLog, isVerbose, calcDuration } from '@push-based/utils'; // Set verbose mode for demonstration process.env['NG_MCP_VERBOSE'] = 'true'; // Format commands with different contexts const commands = [ { cmd: 'npm', args: ['install'], cwd: undefined }, { cmd: 'npx', args: ['eslint', '--fix', 'src/'], cwd: './packages/app' }, { cmd: 'node', args: ['build.js', '--prod'], cwd: '../tools' }, { cmd: 'git', args: ['commit', '-m', 'feat: add new feature'], cwd: process.cwd(), }, ]; console.log('Formatted commands:'); commands.forEach(({ cmd, args, cwd }) => { const formatted = formatCommandLog(cmd, args, cwd); console.log(formatted); }); // Performance timing example async function timedOperation() { const start = performance.now(); // Simulate some work await new Promise((resolve) => setTimeout(resolve, 150)); const duration = calcDuration(start); console.log(`Operation completed in ${duration}ms`); } // Verbose logging check if (isVerbose()) { console.log('🔍 Verbose logging is enabled'); await timedOperation(); } else { console.log('🔇 Verbose logging is disabled'); } // Output (with ANSI colors in terminal): // → Formatted commands: // → $ npm install // → packages/app $ npx eslint --fix src/ // → .. $ node build.js --prod // → $ git commit -m feat: add new feature // → 🔍 Verbose logging is enabled // → Operation completed in 152ms ``` --- ## 4 — CLI argument generation > Convert objects to command-line arguments for process execution. ```ts import { objectToCliArgs, executeProcess } from '@push-based/utils'; // Simple configuration object const config = { _: ['npx', 'eslint'], // Command and base args fix: true, // Boolean flag format: 'json', // String value ext: ['.ts', '.js'], // Array values 'max-warnings': 0, // Numeric value quiet: false, // Negative boolean }; const args = objectToCliArgs(config); console.log('Generated CLI args:'); args.forEach((arg) => console.log(` ${arg}`)); // Use the generated arguments in process execution const result = await executeProcess({ command: args[0], // 'npx' args: args.slice(1), // Everything after the command }); // Output: // → Generated CLI args: // → npx // → eslint // → --fix // → --format="json" // → --ext=".ts" // → --ext=".js" // → --max-warnings=0 // → --no-quiet // Complex nested configuration const complexConfig = { _: ['node', 'build.js'], output: { path: './dist', format: 'esm', }, optimization: { minify: true, 'tree-shake': true, }, }; const complexArgs = objectToCliArgs(complexConfig); console.log('\nComplex nested args:'); complexArgs.forEach((arg) => console.log(` ${arg}`)); // Output: // → Complex nested args: // → node // → build.js // → --output.path="./dist" // → --output.format="esm" // → --optimization.minify // → --optimization.tree-shake ``` --- ## 5 — Error handling and process management > Handle process errors gracefully with comprehensive error information. ```ts import { executeProcess, ProcessError } from '@push-based/utils'; async function robustProcessExecution() { const commands = [ { command: 'node', args: ['--version'] }, // ✅ Should succeed { command: 'nonexistent-command', args: [] }, // ❌ Should fail { command: 'node', args: ['-e', 'process.exit(1)'] }, // ❌ Should fail with exit code 1 ]; for (const config of commands) { try { console.log( `\n🚀 Executing: ${config.command} ${config.args?.join(' ') || ''}` ); const result = await executeProcess({ ...config, observer: { onStdout: (data) => console.log(` 📤 ${data.trim()}`), onStderr: (data) => console.error(` ❌ ${data.trim()}`), onComplete: () => console.log(' ✅ Process completed'), }, }); console.log( ` ✅ Success! Exit code: ${result.code}, Duration: ${result.duration}ms` ); } catch (error) { if (error instanceof ProcessError) { console.error(` ❌ Process failed:`); console.error(` Exit code: ${error.code}`); console.error( ` Error output: ${error.stderr.trim() || 'No stderr'}` ); console.error( ` Standard output: ${error.stdout.trim() || 'No stdout'}` ); } else { console.error(` ❌ Unexpected error: ${error}`); } } } // Example with ignoreExitCode option console.log('\n🔄 Executing command with ignoreExitCode=true:'); try { const result = await executeProcess({ command: 'node', args: ['-e', 'console.log("Hello"); process.exit(1)'], ignoreExitCode: true, observer: { onStdout: (data) => console.log(` 📤 ${data.trim()}`), onComplete: () => console.log(' ✅ Process completed (exit code ignored)'), }, }); console.log(` ✅ Completed with exit code ${result.code} (ignored)`); console.log(` 📝 Output: ${result.stdout.trim()}`); } catch (error) { console.error(` ❌ This shouldn't happen with ignoreExitCode=true`); } } await robustProcessExecution(); // Output: // → 🚀 Executing: node --version // → 📤 v18.17.0 // → ✅ Process completed // → ✅ Success! Exit code: 0, Duration: 42ms // → // → 🚀 Executing: nonexistent-command // → ❌ Process failed: // → Exit code: null // → Error output: spawn nonexistent-command ENOENT // → Standard output: No stdout // → // → 🚀 Executing: node -e process.exit(1) // → ❌ Process failed: // → Exit code: 1 // → Error output: No stderr // → Standard output: No stdout // → // → 🔄 Executing command with ignoreExitCode=true: // → 📤 Hello // → ✅ Process completed (exit code ignored) // → ✅ Completed with exit code 1 (ignored) // → 📝 Output: Hello ``` --- ## 6 — Advanced file operations with generators > Use async generators for efficient file processing. ```ts import { findAllFiles, accessContent, getLineHits, isExcludedDirectory, } from '@push-based/utils'; // Custom file finder with filtering async function findLargeTypeScriptFiles( baseDir: string, minSize: number = 1000 ) { const largeFiles: string[] = []; // Use async generator to process files one by one for await (const file of findAllFiles(baseDir, (path) => path.endsWith('.ts') )) { try { const stats = await fs.stat(file); if (stats.size > minSize) { largeFiles.push(file); console.log(`📁 Large file: ${file} (${stats.size} bytes)`); } } catch (error) { console.warn(`⚠️ Could not stat file: ${file}`); } } return largeFiles; } // Process file content line by line async function analyzeFileContent(filePath: string, searchTerm: string) { const content = await fs.readFile(filePath, 'utf-8'); const results = { totalLines: 0, matchingLines: 0, matches: [] as Array<{ line: number; hits: number; content: string }>, }; // Use generator to process content efficiently let lineNumber = 0; for (const line of accessContent(content)) { lineNumber++; results.totalLines++; const hits = getLineHits(line, searchTerm); if (hits.length > 0) { results.matchingLines++; results.matches.push({ line: lineNumber, hits: hits.length, content: line.trim(), }); } } return results; } // Directory filtering const directories = [ 'src', '.git', 'node_modules', 'dist', 'coverage', '.vscode', ]; directories.forEach((dir) => { const excluded = isExcludedDirectory(dir); console.log(`${dir}: ${excluded ? '❌ excluded' : '✅ included'}`); }); // Usage example const largeFiles = await findLargeTypeScriptFiles('./src', 2000); if (largeFiles.length > 0) { const analysis = await analyzeFileContent(largeFiles[0], 'export'); console.log(`\nAnalysis of ${largeFiles[0]}:`); console.log(`Total lines: ${analysis.totalLines}`); console.log(`Lines with 'export': ${analysis.matchingLines}`); console.log(`First few matches:`); analysis.matches.slice(0, 3).forEach((match) => { console.log(` Line ${match.line} (${match.hits} hits): ${match.content}`); }); } // Output: // → src: ✅ included // → .git: ❌ excluded // → node_modules: ❌ excluded // → dist: ❌ excluded // → coverage: ❌ excluded // → .vscode: ✅ included // → 📁 Large file: ./src/lib/utils.ts (2247 bytes) // → 📁 Large file: ./src/lib/execute-process.ts (5043 bytes) // → // → Analysis of ./src/lib/utils.ts: // → Total lines: 88 // → Lines with 'export': 5 // → First few matches: // → Line 2 (1 hits): export function calcDuration(start: number, stop?: number): number { // → Line 6 (1 hits): export function isVerbose(): boolean { // → Line 14 (1 hits): export function formatCommandLog(command: string, args?: string[], cwd?: string): string { ``` --- ## 7 — ES Module loading and dynamic imports > Load ES modules dynamically and extract default exports safely. ```ts import { loadDefaultExport } from '@push-based/utils'; // Load configuration from ES module const config = await loadDefaultExport('./config/app.config.mjs'); console.log(`API Port: ${config.port}`); // Load with type safety interface AppData { version: string; features: string[]; } const appData = await loadDefaultExport<AppData>('./data/app.mjs'); console.log(`App version: ${appData.version}`); console.log(`Features: ${appData.features.join(', ')}`); // Handle loading errors gracefully try { const plugin = await loadDefaultExport('./plugins/optional.mjs'); console.log('✅ Plugin loaded'); } catch (error) { if (error.message.includes('No default export found')) { console.warn('⚠️ Module missing default export'); } else { console.warn('⚠️ Plugin not found, continuing without it'); } } // Output: // → API Port: 3000 // → App version: 1.2.0 // → Features: auth, logging, metrics // → ⚠️ Plugin not found, continuing without it ``` --- These examples demonstrate the comprehensive capabilities of the `@push-based/utils` library for process execution, file operations, string manipulation, and development tooling in Node.js applications. ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/complex-components/second-case/complex-badge-widget.component.ts: -------------------------------------------------------------------------------- ```typescript import { ChangeDetectionStrategy, Component, ElementRef, ViewEncapsulation, computed, input, output, signal, booleanAttribute, OnInit, OnDestroy, inject, Renderer2, } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; export interface BadgeConfig { id: string; text: string; type: 'offer-badge' | 'status' | 'priority'; level: 'low' | 'medium' | 'high' | 'critical'; interactive: boolean; customData: Record<string, any>; } @Component({ selector: 'app-complex-badge-widget', standalone: true, imports: [CommonModule, FormsModule], template: ` <div class="widget-container"> <h3>Complex Badge Widget</h3> <!-- This component relies heavily on custom badge structure that will break --> <div class="badge-grid"> @for (badge of badges(); track badge.id) { <div class="badge-item" [attr.data-badge-id]="badge.id"> <!-- Complex custom badge with nested structure --> <div class="offer-badge offer-badge-{{ badge.level }} offer-badge-{{ badge.type }}" [class.offer-badge-interactive]="badge.interactive" [class.offer-badge-selected]="isSelected(badge.id)" (click)="toggleBadge(badge.id)" [attr.data-custom-prop]="badge.customData.prop" [style.--badge-color]="getBadgeColor(badge.level)"> <!-- This structure is incompatible with DsBadge slots --> <div class="offer-badge-header"> <span class="offer-badge-type-indicator">{{ getTypeIcon(badge.type) }}</span> <div class="offer-badge-level-dots"> @for (dot of getLevelDots(badge.level); track $index) { <span class="level-dot" [class.active]="dot"></span> } </div> </div> <!-- Main content with complex nested structure --> <div class="offer-badge-content"> <div class="offer-badge-primary-text">{{ badge.text }}</div> <div class="offer-badge-metadata"> <span class="badge-id">ID: {{ badge.id }}</span> <span class="badge-timestamp">{{ getTimestamp() }}</span> </div> </div> <!-- Footer with actions - incompatible with DsBadge --> <div class="offer-badge-footer"> @if (badge.interactive) { <button class="badge-action" (click)="editBadge($event, badge.id)">Edit</button> <button class="badge-action danger" (click)="deleteBadge($event, badge.id)">×</button> } <div class="badge-status-indicator" [class]="getStatusClass(badge)"></div> </div> </div> <!-- Additional complex elements that depend on custom structure --> <div class="badge-tooltip" [class.visible]="showTooltip() === badge.id"> <div class="tooltip-content"> <h4>{{ badge.text }}</h4> <p>Type: {{ badge.type }}</p> <p>Level: {{ badge.level }}</p> <div class="custom-data"> @for (item of getCustomDataEntries(badge.customData); track item.key) { <div class="data-item"> <strong>{{ item.key }}:</strong> {{ item.value }} </div> } </div> </div> </div> </div> } </div> <!-- Controls that manipulate badge structure directly --> <div class="widget-controls"> <button (click)="addRandomBadge()">Add Random Badge</button> <button (click)="modifyAllBadges()">Modify All Badges</button> <button (click)="resetBadges()">Reset</button> <label> <input type="checkbox" [(ngModel)]="enableAdvancedMode" (change)="onAdvancedModeChange()"> Advanced Mode (manipulates DOM directly) </label> </div> <!-- Status display that reads from custom badge elements --> <div class="status-display"> <h4>Status Summary:</h4> <div class="status-grid"> <div class="status-item"> <strong>Total Badges:</strong> {{ badges().length }} </div> <div class="status-item"> <strong>Interactive:</strong> {{ getInteractiveBadgesCount() }} </div> <div class="status-item"> <strong>Selected:</strong> {{ selectedBadges().length }} </div> <div class="status-item"> <strong>Critical Level:</strong> {{ getCriticalBadgesCount() }} </div> </div> </div> </div> `, styleUrls: ['./complex-badge-widget.component.scss'], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, }) export class ComplexBadgeWidgetComponent implements OnInit, OnDestroy { // Inputs initialBadges = input<BadgeConfig[]>([]); enableAdvancedMode = signal(false); showTooltip = signal<string | null>(null); // Outputs badgeSelected = output<string>(); badgeModified = output<BadgeConfig>(); badgeDeleted = output<string>(); // Internal state badges = signal<BadgeConfig[]>([]); selectedBadges = signal<string[]>([]); private elementRef = inject(ElementRef); private renderer = inject(Renderer2); private badgeCounter = 0; ngOnInit() { this.initializeBadges(); this.setupAdvancedModeObserver(); } ngOnDestroy() { // Cleanup } private initializeBadges() { const defaultBadges: BadgeConfig[] = [ { id: 'badge-1', text: 'Premium Offer', type: 'offer-badge', level: 'high', interactive: true, customData: { prop: 'premium', priority: 1, category: 'sales' } }, { id: 'badge-2', text: 'System Status', type: 'status', level: 'medium', interactive: false, customData: { prop: 'status', health: 'good', uptime: '99.9%' } }, { id: 'badge-3', text: 'Critical Alert', type: 'priority', level: 'critical', interactive: true, customData: { prop: 'alert', severity: 'high', source: 'monitoring' } } ]; this.badges.set(this.initialBadges().length > 0 ? this.initialBadges() : defaultBadges); } private setupAdvancedModeObserver() { // This method directly manipulates DOM elements in a way that will break with DsBadge // because it expects specific custom badge structure } // Methods that rely on custom badge structure getBadgeColor(level: string): string { const colorMap: Record<string, string> = { 'low': '#10b981', 'medium': '#f59e0b', 'high': '#3b82f6', 'critical': '#ef4444' }; return colorMap[level] || '#6b7280'; } getTypeIcon(type: string): string { const iconMap: Record<string, string> = { 'offer-badge': '🎯', 'status': '📊', 'priority': '⚠️' }; return iconMap[type] || '📌'; } getLevelDots(level: string): boolean[] { const dotMap: Record<string, boolean[]> = { 'low': [true, false, false], 'medium': [true, true, false], 'high': [true, true, true], 'critical': [true, true, true] }; return dotMap[level] || [false, false, false]; } getStatusClass(badge: BadgeConfig): string { return `status-${badge.level}-${badge.type}`; } getCustomDataEntries(customData: Record<string, any>): Array<{key: string, value: any}> { return Object.entries(customData).map(([key, value]) => ({ key, value })); } getTimestamp(): string { return new Date().toLocaleTimeString(); } // Interactive methods that depend on custom structure isSelected(badgeId: string): boolean { return this.selectedBadges().includes(badgeId); } toggleBadge(badgeId: string) { const selected = this.selectedBadges(); if (selected.includes(badgeId)) { this.selectedBadges.set(selected.filter(id => id !== badgeId)); } else { this.selectedBadges.set([...selected, badgeId]); } this.badgeSelected.emit(badgeId); } editBadge(event: Event, badgeId: string) { event.stopPropagation(); const badge = this.badges().find(b => b.id === badgeId); if (badge) { // Simulate editing const updatedBadge = { ...badge, text: badge.text + ' (edited)' }; this.updateBadge(updatedBadge); this.badgeModified.emit(updatedBadge); } } deleteBadge(event: Event, badgeId: string) { event.stopPropagation(); this.badges.set(this.badges().filter(b => b.id !== badgeId)); this.selectedBadges.set(this.selectedBadges().filter(id => id !== badgeId)); this.badgeDeleted.emit(badgeId); } addRandomBadge() { this.badgeCounter++; const types: BadgeConfig['type'][] = ['offer-badge', 'status', 'priority']; const levels: BadgeConfig['level'][] = ['low', 'medium', 'high', 'critical']; const newBadge: BadgeConfig = { id: `badge-${Date.now()}-${this.badgeCounter}`, text: `Dynamic Badge ${this.badgeCounter}`, type: types[Math.floor(Math.random() * types.length)], level: levels[Math.floor(Math.random() * levels.length)], interactive: Math.random() > 0.5, customData: { prop: `dynamic-${this.badgeCounter}`, generated: true, timestamp: Date.now() } }; this.badges.set([...this.badges(), newBadge]); } modifyAllBadges() { // This method directly manipulates DOM to demonstrate breaking changes if (this.enableAdvancedMode()) { const badgeElements = this.elementRef.nativeElement.querySelectorAll('.offer-badge'); badgeElements.forEach((element: HTMLElement, index: number) => { // Direct DOM manipulation that will break with DsBadge const contentDiv = element.querySelector('.offer-badge-content'); if (contentDiv) { this.renderer.setStyle(contentDiv, 'transform', `rotate(${index * 2}deg)`); this.renderer.addClass(element, 'modified-badge'); } // Add custom attributes that DsBadge won't support this.renderer.setAttribute(element, 'data-modification-time', Date.now().toString()); this.renderer.setAttribute(element, 'data-custom-behavior', 'advanced'); }); } } resetBadges() { this.initializeBadges(); this.selectedBadges.set([]); this.showTooltip.set(null); // Reset DOM modifications const badgeElements = this.elementRef.nativeElement.querySelectorAll('.offer-badge'); badgeElements.forEach((element: HTMLElement) => { this.renderer.removeStyle(element, 'transform'); this.renderer.removeClass(element, 'modified-badge'); this.renderer.removeAttribute(element, 'data-modification-time'); this.renderer.removeAttribute(element, 'data-custom-behavior'); }); } onAdvancedModeChange() { if (this.enableAdvancedMode()) { // Enable advanced DOM manipulation features this.setupAdvancedBehaviors(); } else { this.resetBadges(); } } private setupAdvancedBehaviors() { // This method sets up behaviors that depend on custom badge DOM structure // and will break when using DsBadge setTimeout(() => { const badgeElements = this.elementRef.nativeElement.querySelectorAll('.offer-badge'); badgeElements.forEach((element: HTMLElement) => { // Add hover effects that depend on custom structure element.addEventListener('mouseenter', () => { const header = element.querySelector('.offer-badge-header'); const footer = element.querySelector('.offer-badge-footer'); if (header && footer) { this.renderer.addClass(header, 'hover-active'); this.renderer.addClass(footer, 'hover-active'); } }); element.addEventListener('mouseleave', () => { const header = element.querySelector('.offer-badge-header'); const footer = element.querySelector('.offer-badge-footer'); if (header && footer) { this.renderer.removeClass(header, 'hover-active'); this.renderer.removeClass(footer, 'hover-active'); } }); }); }, 100); } private updateBadge(updatedBadge: BadgeConfig) { const badges = this.badges(); const index = badges.findIndex(b => b.id === updatedBadge.id); if (index !== -1) { badges[index] = updatedBadge; this.badges.set([...badges]); } } // Computed values that depend on custom badge structure getInteractiveBadgesCount(): number { return this.badges().filter(b => b.interactive).length; } getCriticalBadgesCount(): number { return this.badges().filter(b => b.level === 'critical').length; } } ``` -------------------------------------------------------------------------------- /docs/component-refactoring-flow.md: -------------------------------------------------------------------------------- ```markdown # Component Refactoring Flow ## Overview This document describes a 3-step AI-assisted component refactoring process for improving individual Angular components according to modern best practices. Each step uses a specific rule file (.mdc) that guides the Cursor agent through systematic analysis, code improvements, and validation. **Process Summary:** 1. **Review Component** → Analyze component against best practices and create improvement plan 2. **Refactor Component** → Execute approved checklist items and implement changes 3. **Validate Component** → Verify improvements through contract comparison and scoring The process includes two quality gates where human review and approval are required. When refactoring involves Design System components, the process can leverage selective data retrieval to access only the specific component information needed (implementation, documentation, or stories). ## Prerequisites Before starting the component refactoring flow, ensure you have: - Cursor IDE with this MCP (Model Context Protocol) server connected. This flow was tested with Cursor but should also work with Windsurf or Copilot. - The three rule files (.mdc) available in your workspace - A git branch for the refactoring work - The component files (TypeScript, template, and styles) accessible in your workspace ## 01-review-component.mdc ### Goal Analyze an Angular component against modern best practices and design system guidelines to create a comprehensive improvement plan. This rule evaluates component quality across five key dimensions and generates an actionable refactoring checklist. ### Process To start this process, drag file `01-review-component.mdc` to the cursor chat and provide the required parameters: ``` component_path=path/to/component.ts styleguide="Angular 20 best practices with signals, standalone components, and modern control flow" component_files=[provide the TypeScript, template, and style file contents] @01-review-component.mdc ``` This rule follows a structured analysis process: **Step 1: File Validation** - Verifies all essential component files are provided (TypeScript, template, styles) - Ensures component structure is complete for analysis - Stops process if critical files are missing **Step 2: Multi-Dimensional Analysis** - Evaluates component against five key categories: - **Accessibility**: ARIA attributes, semantic HTML, keyboard navigation - **Performance**: Change detection strategy, lazy loading, bundle size impact - **Scalability**: Code organization, reusability, maintainability patterns - **Maintainability**: Code clarity, documentation, testing considerations - **Best Practices**: Angular conventions, TypeScript usage, modern patterns **Step 3: Scoring and Assessment** - Assigns numerical scores (1-10) for each category - Identifies 3-5 concrete observations per category - Provides narrative analysis of overall component state **Step 4: Checklist Generation** - Creates actionable improvement items based on analysis - Prioritizes changes by impact and complexity - Formats as markdown checklist for systematic execution --- **🚦 Quality Gate 1** Before proceeding to implementation, you must review and approve the refactoring plan. **Required Actions:** - Review the component analysis and scores - Examine the proposed refactoring checklist - Approve the plan or request modifications **Next Step:** When satisfied with the checklist, attach the next rule file. --- ### Tools used None - This rule performs static analysis based on provided component files and styleguide requirements. ### Flow > You don't need to manually perform any of the listed actions except providing the initial parameters. 1. **Input Validation**: Verify component files and styleguide are provided 2. **File Structure Check**: Ensure TypeScript, template, and style files are available 3. **Multi-Category Analysis**: Evaluate component against five key dimensions 4. **Scoring Assignment**: Assign numerical scores (1-10) for each category 5. **Observation Collection**: Identify 3-5 concrete issues per category 6. **Narrative Generation**: Create overall component state summary 7. **Checklist Creation**: Generate actionable improvement items 8. **Approval Request**: Present checklist for user review and approval The rule enforces structured output with `<component_analysis>`, `<scoring>`, and `<refactoring_checklist>` tags, ensuring comprehensive coverage and clear next steps. ### Preferred model Claude-4-Sonnet ## 02-refactor-component.mdc ### Goal Execute the approved refactoring checklist by implementing code changes, tracking progress, and maintaining component contracts for validation. This rule systematically processes each checklist item and documents all modifications made to the component. ### Process To start this process, drag file `02-refactor-component.mdc` to the cursor chat and provide: ``` component_path=path/to/component.ts checklist_content=[the approved checklist from step 1] @02-refactor-component.mdc ``` The rule implements a systematic refactoring execution process: **Step 1: Pre-Refactor Contract Generation** - Creates baseline component contract capturing current state - Documents component's public API, DOM structure, and styles - Establishes reference point for later validation - Stops process if contract generation fails **Step 2: Checklist Processing** - Iterates through each unchecked item in the approved checklist - Implements necessary code changes using standard editing tools - Marks completed items with explanatory notes - Handles ambiguous items by requesting user clarification **Step 3: Progress Documentation** - Updates checklist with completion status and change descriptions - Saves updated checklist to `.cursor/tmp/component-refactor-checklist-{{COMPONENT_PATH}}.md` - Maintains audit trail of all modifications **Step 4: Summary Generation** - Creates comprehensive summary of completed changes - Documents what was modified and why - Provides updated checklist in markdown format --- **🚦 Quality Gate 2** At this point, all checklist items have been processed. You must review the refactoring results. **Required Actions:** - Review the refactor summary and completed changes - Verify all checklist items were addressed appropriately - Resolve any ambiguities or questions **Next Step:** After approving the refactoring results, attach the validation rule file. --- ### Tools used - `build_component_contract` - Creates component contracts for safe refactoring - Parameters: `saveLocation`, `typescriptFile` (required), `templateFile` (optional), `styleFile` (optional), `dsComponentName` (optional, set to "AUTO") - Returns: contract path with component's public API, DOM structure, and styles - Purpose: Establish baseline for validation comparison - Note: Template and style files are optional for components with inline templates/styles - `get-ds-component-data` - Retrieves Design System component information when needed - Parameters: `componentName`, `sections` (optional) - Array of sections to include: "implementation", "documentation", "stories", "all" - Returns: Selective component data based on refactoring needs - Purpose: Access DS component documentation and examples for proper implementation patterns ### Flow > You don't need to manually perform any of the listed actions except providing the initial parameters. 1. **Contract Generation**: Create pre-refactor component contract 2. **Error Handling**: Verify contract creation succeeded 3. **Checklist Iteration**: Process each unchecked item systematically 4. **Code Implementation**: Execute necessary changes for each item 5. **Progress Tracking**: Mark items complete with explanatory notes 6. **Ambiguity Resolution**: Request clarification for unclear items 7. **Checklist Persistence**: Save updated checklist to temporary file 8. **Summary Creation**: Generate comprehensive refactoring summary 9. **Completion Confirmation**: Request user approval to proceed to validation The rule enforces structured output with `<refactor_summary>` and `<checklist_updated>` tags, ensuring complete documentation of all changes and clear transition to validation. ### Preferred model Claude-4-Sonnet ## 03-validate-component.mdc ### Goal Analyze the refactored component by comparing before and after contracts, re-evaluating quality scores, and providing comprehensive validation assessment. This rule ensures refactoring improvements are measurable and identifies any remaining issues. ### Process To start this process, drag file `03-validate-component.mdc` to the cursor chat and provide: ``` component_path=path/to/component.ts baseline_contract_path=path/to/baseline/contract.json @03-validate-component.mdc ``` The rule implements a comprehensive validation process: **Step 1: Post-Refactor Contract Generation** - Creates new component contract capturing refactored state - Documents updated component API, DOM structure, and styles - Establishes comparison point against baseline contract **Step 2: Contract Comparison** - Performs detailed diff analysis between baseline and updated contracts - Identifies specific changes in component structure and behavior - Analyzes impact of modifications on component functionality **Step 3: Quality Re-Assessment** - Re-evaluates component against the same five categories from initial review: - **Accessibility**: Impact of changes on accessibility features - **Performance**: Improvements or regressions in performance metrics - **Scalability**: Changes affecting component scalability - **Maintainability**: Impact on code maintainability and clarity - **Best Practices**: Adherence to modern Angular best practices **Step 4: Score Calculation** - Assigns new scores (1-10) for each category - Calculates deltas showing improvement or regression - Provides objective measurement of refactoring success **Step 5: Validation Assessment** - Determines overall refactoring success or identifies remaining issues - Highlights any risks or necessary follow-ups - Provides final judgment on component quality ### Tools used - `build_component_contract` - Creates post-refactor component contract - Parameters: `saveLocation`, `typescriptFile` (required), `templateFile` (optional), `styleFile` (optional), `dsComponentName` (optional) - Returns: updated contract path with refactored component state - Purpose: Capture final component state for comparison - Note: Template and style files are optional for components with inline templates/styles - `diff_component_contract` - Compares baseline and updated contracts - Parameters: `saveLocation`, `contractBeforePath`, `contractAfterPath`, `dsComponentName` - Returns: detailed diff analysis showing specific changes - Purpose: Identify and analyze all modifications made during refactoring ### Flow > You don't need to manually perform any of the listed actions except providing the initial parameters. 1. **Post-Contract Generation**: Create contract for refactored component 2. **Error Handling**: Verify contract creation succeeded 3. **Contract Comparison**: Generate diff analysis between baseline and updated contracts 4. **Change Analysis**: Analyze diff results for impact assessment 5. **Quality Re-Evaluation**: Re-score component across five categories 6. **Delta Calculation**: Compute improvement or regression metrics 7. **Impact Assessment**: Evaluate overall refactoring effectiveness 8. **Validation Judgment**: Determine success status and identify remaining issues 9. **Final Report**: Generate comprehensive validation assessment The rule enforces structured output with `<diff_summary>`, `<new_scoring>`, and `<validation_assessment>` tags, ensuring objective measurement of refactoring success and clear identification of any remaining concerns. ### Preferred model Claude-4-Sonnet ## Integration with Angular 20 Best Practices The component refactoring flow specifically targets modern Angular development patterns as outlined in the `angular-20.md` reference document. Key focus areas include: ### TypeScript Modernization - **Strict Type Checking**: Ensures proper type safety throughout component - **Type Inference**: Reduces verbosity while maintaining type safety - **Avoiding `any`**: Promotes use of `unknown` and proper type definitions ### Angular Modern Patterns - **Standalone Components**: Migrates from NgModule-based to standalone architecture - **Signals for State Management**: Adopts reactive state management with Angular Signals - **New Input/Output Syntax**: Replaces decorators with `input()` and `output()` functions - **Computed Properties**: Implements `computed()` for derived state ### Template Modernization - **Control Flow Syntax**: Migrates from structural directives to `@if`, `@for`, `@switch` - **Native Class/Style Bindings**: Replaces `ngClass`/`ngStyle` with native bindings - **OnPush Change Detection**: Implements performance optimization strategies ### Service and Dependency Injection - **Inject Function**: Adopts modern `inject()` function over constructor injection - **Providable Services**: Ensures proper service registration and tree-shaking This comprehensive approach ensures components are not only improved but also aligned with the latest Angular best practices and performance recommendations. ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/utils/typescript-analyzer.ts: -------------------------------------------------------------------------------- ```typescript import * as ts from 'typescript'; import { MethodSignature, ParameterInfo, ImportInfo, } from '../../shared/models/types.js'; import { DecoratorInputMeta, DecoratorOutputMeta, SignalInputMeta, SignalOutputMeta, ExtractedInputsOutputs, } from '../models/types.js'; import { ParsedComponent } from '@push-based/angular-ast-utils'; /** * Angular lifecycle hooks that we can detect */ const LIFECYCLE_HOOKS = new Set<string>([ 'OnInit', 'OnDestroy', 'OnChanges', 'DoCheck', 'AfterContentInit', 'AfterContentChecked', 'AfterViewInit', 'AfterViewChecked', ]); /** * Utility: check whether a node has a given modifier */ const hasModifier = ( node: { modifiers?: ts.NodeArray<ts.ModifierLike> }, kind: ts.SyntaxKind, ): boolean => node.modifiers?.some((m) => m.kind === kind) ?? false; /** * Extract public methods from a TypeScript class declaration */ export function extractPublicMethods( classNode: ts.ClassDeclaration, sourceFile: ts.SourceFile, ): Record<string, MethodSignature> { const methods: Record<string, MethodSignature> = {}; for (const member of classNode.members) { if (!ts.isMethodDeclaration(member) || !ts.isIdentifier(member.name)) { continue; } const methodName = member.name.text; if ( methodName.startsWith('ng') && LIFECYCLE_HOOKS.has(methodName.slice(2)) ) { continue; } if ( hasModifier(member, ts.SyntaxKind.PrivateKeyword) || hasModifier(member, ts.SyntaxKind.ProtectedKeyword) ) { continue; } methods[methodName] = { name: methodName, parameters: extractParameters(member.parameters, sourceFile), returnType: extractReturnType(member, sourceFile), isPublic: true, isStatic: hasModifier(member, ts.SyntaxKind.StaticKeyword), isAsync: hasModifier(member, ts.SyntaxKind.AsyncKeyword), }; } return methods; } /** * Extract parameter information from method parameters */ function extractParameters( parameters: ts.NodeArray<ts.ParameterDeclaration>, sourceFile: ts.SourceFile, ): ParameterInfo[] { return parameters.map((param) => ({ name: ts.isIdentifier(param.name) ? param.name.text : 'unknown', type: param.type?.getText(sourceFile) ?? 'any', optional: !!param.questionToken, defaultValue: param.initializer?.getText(sourceFile), })); } /** * Extract return type from method declaration */ function extractReturnType( method: ts.MethodDeclaration, sourceFile: ts.SourceFile, ): string { return ( method.type?.getText(sourceFile) ?? (hasModifier(method, ts.SyntaxKind.AsyncKeyword) ? 'Promise<any>' : 'any') ); } /** * Extract implemented Angular lifecycle hooks */ export function extractLifecycleHooks( classNode: ts.ClassDeclaration, ): string[] { const implementedHooks = new Set<string>(); if (classNode.heritageClauses) { for (const heritage of classNode.heritageClauses) { if (heritage.token === ts.SyntaxKind.ImplementsKeyword) { for (const type of heritage.types) { if (ts.isIdentifier(type.expression)) { const interfaceName = type.expression.text; if (LIFECYCLE_HOOKS.has(interfaceName)) { implementedHooks.add(interfaceName); } } } } } } for (const member of classNode.members) { if (ts.isMethodDeclaration(member) && ts.isIdentifier(member.name)) { const methodName = member.name.text; if ( methodName.startsWith('ng') && LIFECYCLE_HOOKS.has(methodName.slice(2)) ) { implementedHooks.add(methodName.slice(2)); } } } return Array.from(implementedHooks); } /** * Extract TypeScript class declaration from parsed component * This function finds the class node from the component's source file */ export function extractClassDeclaration( parsedComponent: ParsedComponent, ): ts.ClassDeclaration | null { if (!parsedComponent.fileName) { return null; } try { const program = ts.createProgram([parsedComponent.fileName], { target: ts.ScriptTarget.Latest, module: ts.ModuleKind.ESNext, experimentalDecorators: true, }); const sourceFile = program.getSourceFile(parsedComponent.fileName); if (!sourceFile) { return null; } const classNode = findClassDeclaration( sourceFile, parsedComponent.className, ); return classNode; } catch (ctx) { console.warn( `Failed to extract class declaration for ${parsedComponent.className}:`, ctx, ); return null; } } /** * Find class declaration by name in source file */ function findClassDeclaration( sourceFile: ts.SourceFile, className: string, ): ts.ClassDeclaration | null { let foundClass: ts.ClassDeclaration | null = null; function visit(node: ts.Node) { if ( ts.isClassDeclaration(node) && node.name && node.name.text === className ) { foundClass = node; return; } ts.forEachChild(node, visit); } visit(sourceFile); return foundClass; } /** * Extract import statements from source file */ export function extractImports(sourceFile: ts.SourceFile): ImportInfo[] { const imports: ImportInfo[] = []; function visit(node: ts.Node) { if (ts.isImportDeclaration(node) && node.moduleSpecifier) { const moduleSpecifier = node.moduleSpecifier; if (ts.isStringLiteral(moduleSpecifier)) { if (node.importClause) { if (node.importClause.name) { imports.push({ name: node.importClause.name.text, path: moduleSpecifier.text, }); } if (node.importClause.namedBindings) { if (ts.isNamespaceImport(node.importClause.namedBindings)) { imports.push({ name: node.importClause.namedBindings.name.text, path: moduleSpecifier.text, }); } else if (ts.isNamedImports(node.importClause.namedBindings)) { for (const element of node.importClause.namedBindings.elements) { imports.push({ name: element.name.text, path: moduleSpecifier.text, }); } } } } } } ts.forEachChild(node, visit); } visit(sourceFile); return imports; } // Helper: determine if class member is a public property with identifier name function isPublicProp( member: ts.ClassElement, ): member is ts.PropertyDeclaration & { name: ts.Identifier } { return ( ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name) && !hasModifier(member, ts.SyntaxKind.PrivateKeyword) && !hasModifier(member, ts.SyntaxKind.ProtectedKeyword) ); } export function extractInputsAndOutputs( classNode: ts.ClassDeclaration, sourceFile: ts.SourceFile, ): ExtractedInputsOutputs { const inputs: Record<string, DecoratorInputMeta | SignalInputMeta> = {}; const outputs: Record<string, DecoratorOutputMeta | SignalOutputMeta> = {}; for (const member of classNode.members) { if (!isPublicProp(member)) continue; const name = member.name.text; const { isInput, isOutput, type, required, alias } = extractDecoratorInputsOutputs(member, sourceFile); if (isInput) inputs[name] = { name, type, required, alias } as any; if (isOutput) outputs[name] = { name, type, alias } as any; const init = member.initializer; if ( !init || !ts.isCallExpression(init) || !ts.isIdentifier(init.expression) ) { continue; } switch (init.expression.text) { case 'input': inputs[name] = { name, ...(extractSignalInputMetadata(init, member, sourceFile) as any), } as any; break; case 'output': outputs[name] = { name, ...(extractSignalOutputMetadata(init, member, sourceFile) as any), } as any; break; } } return { inputs, outputs }; } function extractDecoratorInputsOutputs( member: ts.PropertyDeclaration, sourceFile: ts.SourceFile, ): { isInput: boolean; isOutput: boolean; type?: string; required?: boolean; alias?: string; } { let isInput = false, isOutput = false, alias: string | undefined, required = false; member.modifiers?.forEach((mod) => { if (!ts.isDecorator(mod)) return; const kind = getDecoratorName(mod); // 'Input' | 'Output' | null if (!kind) return; const args = getDecoratorArguments(mod); const first = args[0]; if (first && ts.isStringLiteral(first)) { alias = first.text; } if (kind === 'Input') { isInput = true; if (first && ts.isObjectLiteralExpression(first)) { first.properties.forEach((p) => { if (!ts.isPropertyAssignment(p) || !ts.isIdentifier(p.name)) return; if (p.name.text === 'alias' && ts.isStringLiteral(p.initializer)) { alias = p.initializer.text; } if (p.name.text === 'required') { required = p.initializer.kind === ts.SyntaxKind.TrueKeyword; } }); } } else if (kind === 'Output') { isOutput = true; } }); const type = extractPropertyType(member, sourceFile, isOutput); return { isInput, isOutput, type, required, alias }; } function getDecoratorName(decorator: ts.Decorator): string | null { if (ts.isCallExpression(decorator.expression)) { if (ts.isIdentifier(decorator.expression.expression)) { return decorator.expression.expression.text; } } else if (ts.isIdentifier(decorator.expression)) { return decorator.expression.text; } return null; } function getDecoratorArguments(decorator: ts.Decorator): ts.Expression[] { if (ts.isCallExpression(decorator.expression)) { return Array.from(decorator.expression.arguments); } return []; } function extractPropertyType( member: ts.PropertyDeclaration, sourceFile: ts.SourceFile, isOutput = false, ): string { if (member.type) { const typeText = member.type.getText(sourceFile); return typeText; } if (member.initializer) { if (ts.isNewExpression(member.initializer)) { const expression = member.initializer.expression; if (ts.isIdentifier(expression)) { if (expression.text === 'EventEmitter') { if ( member.initializer.typeArguments && member.initializer.typeArguments.length > 0 ) { return `EventEmitter<${member.initializer.typeArguments[0].getText(sourceFile)}>`; } return 'EventEmitter<any>'; } } } } if (isOutput) { return 'EventEmitter<any>'; } return 'any'; } function extractSignalInputMetadata( callExpression: ts.CallExpression, propertyDeclaration: ts.PropertyDeclaration, sourceFile: ts.SourceFile, ): Omit<SignalInputMeta, 'name'> { const meta: Omit<SignalInputMeta, 'name'> = {}; meta.type = extractInputType(callExpression, propertyDeclaration, sourceFile); if (callExpression.arguments.length > 0) { const firstArg = callExpression.arguments[0]; meta.defaultValue = firstArg.getText(sourceFile); meta.required = false; } else { meta.required = true; } if (callExpression.arguments.length > 1) { const optionsArg = callExpression.arguments[1]; if (ts.isObjectLiteralExpression(optionsArg)) { for (const property of optionsArg.properties) { if ( ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) ) { const propName = property.name.text; if (propName === 'transform') { meta.transform = property.initializer.getText(sourceFile); } } } } } return meta; } function extractSignalOutputMetadata( callExpression: ts.CallExpression, propertyDeclaration: ts.PropertyDeclaration, sourceFile: ts.SourceFile, ): Omit<SignalOutputMeta, 'name'> { const meta: Omit<SignalOutputMeta, 'name'> = {}; meta.type = extractOutputType( callExpression, propertyDeclaration, sourceFile, ); return meta; } function extractInputType( callExpression: ts.CallExpression, propertyDeclaration: ts.PropertyDeclaration, sourceFile: ts.SourceFile, ): string { if (callExpression.typeArguments && callExpression.typeArguments.length > 0) { return callExpression.typeArguments[0].getText(sourceFile); } if (propertyDeclaration.type) { return extractTypeFromInputSignal(propertyDeclaration.type, sourceFile); } return 'any'; } function extractOutputType( callExpression: ts.CallExpression, propertyDeclaration: ts.PropertyDeclaration, sourceFile: ts.SourceFile, ): string { if (callExpression.typeArguments && callExpression.typeArguments.length > 0) { return `EventEmitter<${callExpression.typeArguments[0].getText(sourceFile)}>`; } if (propertyDeclaration.type) { return extractTypeFromOutputEmitter(propertyDeclaration.type, sourceFile); } return 'EventEmitter<any>'; } function extractTypeFromInputSignal( typeNode: ts.TypeNode, sourceFile: ts.SourceFile, ): string { if ( ts.isTypeReferenceNode(typeNode) && typeNode.typeArguments && typeNode.typeArguments.length > 0 ) { return typeNode.typeArguments[0].getText(sourceFile); } return typeNode.getText(sourceFile); } function extractTypeFromOutputEmitter( typeNode: ts.TypeNode, sourceFile: ts.SourceFile, ): string { if ( ts.isTypeReferenceNode(typeNode) && typeNode.typeArguments && typeNode.typeArguments.length > 0 ) { return `EventEmitter<${typeNode.typeArguments[0].getText(sourceFile)}>`; } return `EventEmitter<${typeNode.getText(sourceFile)}>`; } export const extractSignalInputsAndOutputs = extractInputsAndOutputs; ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/design-system/storybook-host-app/src/components/modal/modal.component.stories.ts: -------------------------------------------------------------------------------- ```typescript import { generateStatusBadges } from '@design-system/shared-storybook-utils'; import { DemoChevronComponent, DemoCloseIconComponent, DemoIconComponent, } from '@design-system/storybook-demo-cmp-lib'; import { DsBadge } from '@frontend/ui/badge'; import { DsButton } from '@frontend/ui/button'; import { DsButtonIcon } from '@frontend/ui/button-icon'; import { DsModal, DsModalContent, DsModalHeader, DsModalHeaderDrag, DsModalHeaderVariant, } from '@frontend/ui/modal'; import { type Meta, type StoryObj, moduleMetadata } from '@storybook/angular'; import { DemoCdkModalContainer } from './demo-cdk-dialog-cmp.component'; import { DemoModalContainer } from './demo-modal-cmp.component'; type DsModalStoryType = DsModal & { headerVariant: DsModalHeaderVariant }; const meta: Meta<DsModalStoryType> = { title: 'Components/Modal', component: DsModal, parameters: { status: generateStatusBadges('UX-2414', ['integration ready']), }, excludeStories: /.*Data$/, argTypes: { headerVariant: { options: [ 'surface-lowest', 'surface-low', 'surface', 'surface-high', 'nav-bg', ], table: { defaultValue: { summary: 'surface' }, category: 'Styling' }, control: { type: 'select' }, description: 'Surface type', }, variant: { options: ['surface-lowest', 'surface-low', 'surface'], table: { defaultValue: { summary: 'surface' }, category: 'Styling' }, control: { type: 'select' }, description: 'Surface type', }, inverse: { type: 'boolean', table: { defaultValue: { summary: 'false' }, category: 'Styling' }, control: { type: 'boolean' }, description: 'The inverse state of the Modal', }, bottomSheet: { type: 'boolean', table: { defaultValue: { summary: 'false' }, category: 'Styling' }, control: { type: 'boolean' }, description: 'The dialog should open from bottom', }, }, args: { headerVariant: 'surface', inverse: false, bottomSheet: true, variant: 'surface', }, decorators: [ moduleMetadata({ imports: [ DsModal, DemoIconComponent, DemoCloseIconComponent, DsButton, DsButtonIcon, DemoChevronComponent, DsBadge, DsModalHeader, DsModalContent, DsModalHeaderDrag, DemoModalContainer, DemoCdkModalContainer, ], }), ], }; export default meta; type Story = StoryObj<DsModalStoryType>; export const Default: Story = { render: () => ({ template: `<ds-modal style="width: 250px; min-height: 120px"></ds-modal>`, }), }; export const WithMatDialog: Story = { render: (modal) => ({ template: ` <div> <ds-demo-dialog-container headerVariant="${modal.headerVariant}" inverse="${modal.inverse}" variant="${modal.variant}" bottomSheetInput="${modal.bottomSheet}"/> </div> `, }), }; export const WithCdkDialog: Story = { render: (modal) => ({ template: ` <div> <ds-demo-cdk-dialog-container headerVariant="${modal.headerVariant}" variant="${modal.variant}" inverse="${modal.inverse}" bottomSheetInput="${modal.bottomSheet}" /> </div> `, }), }; export const ModalWithContentOnly: Story = { argTypes: { bottomSheet: { table: { disable: true }, }, }, render: (args) => ({ template: ` <ds-modal variant="${args.variant}" inverse="${args.inverse}" style="width: 250px;"> <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> </ds-modal> `, }), }; export const WithTitleAndClose: Story = { argTypes: { bottomSheet: { table: { disable: true }, }, }, render: (args) => ({ template: ` <ds-modal inverse="${args.inverse}" variant="${args.variant}" > <ds-modal-header variant="${args.headerVariant}"> <div slot="start"> <div slot="title">Hello world</div> </div> <button slot="end" ds-button-icon size="small" variant="flat" kind="utility"> <ds-demo-close-icon /> </button> </ds-modal-header> <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> </ds-modal> `, }), }; export const WithDragger: Story = { argTypes: { bottomSheet: { table: { disable: true }, }, }, render: (args) => ({ template: ` <ds-modal variant="${args.variant}" inverse="${args.inverse}" > <ds-modal-header variant="${args.headerVariant}"> <ds-modal-header-drag /> </ds-modal-header> <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> </ds-modal> `, }), }; export const BolderSubtitleThanTitle: Story = { argTypes: { bottomSheet: { table: { disable: true }, }, }, render: (args) => ({ template: ` <ds-modal variant="${args.variant}" inverse="${args.inverse}" > <ds-modal-header variant="${args.headerVariant}"> <button slot="start" ds-button variant="outline" size="medium">Cancel</button> <div slot="center"> <div slot="title">Title</div> <div slot="subtitle">Subtitle</div> </div> <button slot="end" ds-button variant="filled" size="medium"> <ds-demo-icon slot="start" /> Agree </button> </ds-modal-header> <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> </ds-modal> `, }), }; export const ActionsAndCenteredTitle: Story = { argTypes: { bottomSheet: { table: { disable: true }, }, }, render: (args) => ({ template: ` <ds-modal variant="${args.variant}" inverse="${args.inverse}" > <ds-modal-header variant="${args.headerVariant}"> <button slot="start" ds-button variant="outline" size="medium">Cancel</button> <div slot="center"> <div slot="title">Hello world</div> <div slot="subtitle">From DS team</div> </div> <button slot="end" ds-button variant="filled" size="medium"> <ds-demo-icon slot="start" /> Agree </button> </ds-modal-header> <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> </ds-modal> `, }), }; export const CloseTitleLabelAction: Story = { argTypes: { bottomSheet: { table: { disable: true }, }, }, render: (args) => ({ template: ` <ds-modal variant="${args.variant}" inverse="${args.inverse}" > <ds-modal-header variant="${args.headerVariant}"> <ng-container slot="start" > <button ds-button-icon variant="outline" size="medium" kind="secondary"> <ds-demo-close-icon /> </button> <div slot="title"> Hello world <ds-badge variant="blue">Label</ds-badge> </div> </ng-container> <ng-container slot="end"> <ds-demo-icon slot="start" /> <button slot="end" ds-button variant="outline" size="medium"> Action </button> </ng-container> </ds-modal-header> <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> </ds-modal> `, }), }; export const BackBtnTitleLabelAction: Story = { argTypes: { bottomSheet: { table: { disable: true }, }, }, render: (args) => ({ template: ` <ds-modal variant="${args.variant}" inverse="${args.inverse}" > <ds-modal-header variant="${args.headerVariant}"> <ng-container slot="start"> <button ds-button-icon variant="outline" size="medium" kind="secondary"> <ds-demo-chevron rotation="90" /> </button> <div style="display: flex; flex-direction: column;"> <div slot="title"> Hello world <ds-badge variant="blue">Label</ds-badge> </div> <div slot="subtitle">From DS team</div> </div> </ng-container> <ng-container slot="end"> <ds-demo-icon slot="start" /> <button slot="end" ds-button variant="outline" size="medium"> Action </button> </ng-container> </ds-modal-header> <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> </ds-modal> `, }), }; export const ModalHeaderTypes: Story = { argTypes: { headerVariant: { table: { disable: true }, }, bottomSheet: { table: { disable: true }, }, variant: { table: { disable: true }, }, }, render: (args) => ({ template: ` <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 30px"> <div style="display: grid; gap: 10px"> <ds-modal inverse="${args.inverse}" > <ds-modal-header variant="surface"> <div slot="start"> <div slot="title">Surface</div> </div> <button slot="end" ds-button-icon size="small" variant="flat" kind="utility"> <ds-demo-close-icon /> </button> </ds-modal-header> <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> </ds-modal> <ds-modal inverse="${args.inverse}" > <ds-modal-header variant="surface-lowest"> <div slot="start"> <div slot="title">Surface lowest</div> </div> <button slot="end" ds-button-icon size="small" variant="outline" kind="utility"> <ds-demo-close-icon /> </button> </ds-modal-header> <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> </ds-modal> <ds-modal inverse="${args.inverse}" > <ds-modal-header variant="surface-low"> <div slot="start"> <div slot="title">Surface low</div> </div> <button slot="end" ds-button-icon size="small" variant="filled" kind="utility"> <ds-demo-close-icon /> </button> </ds-modal-header> <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> </ds-modal> <ds-modal inverse="${args.inverse}" > <ds-modal-header variant="surface-high"> <div slot="start"> <div slot="title">Surface High</div> </div> <button slot="end" ds-button-icon size="small" kind="secondary"> <ds-demo-close-icon /> </button> </ds-modal-header> <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> </ds-modal> </div> <div style="display: grid; gap: 10px"> <ds-modal inverse="${args.inverse}" > <ds-modal-header variant="surface"> <div slot="start"> <div slot="title">Surface</div> <div slot="subtitle">Header subtitle</div> </div> <div slot="end"> <button ds-button-icon size="small" variant="flat" kind="utility"> <ds-demo-close-icon /> </button> </div> </ds-modal-header> <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> </ds-modal> <ds-modal inverse="${args.inverse}" > <ds-modal-header variant="surface-lowest"> <div slot="start"> <div slot="title">Surface lowest</div> <div slot="subtitle">Header subtitle</div> </div> <button slot="end" ds-button-icon size="small" variant="outline" kind="utility"> <ds-demo-close-icon /> </button> </ds-modal-header> <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> </ds-modal> <ds-modal inverse="${args.inverse}" > <ds-modal-header variant="surface-low"> <div slot="start"> <div slot="title">Surface low</div> <div slot="subtitle">Header subtitle</div> </div> <button slot="end" ds-button-icon size="small" kind="utility"> <ds-demo-close-icon /> </button> </ds-modal-header> <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> </ds-modal> <ds-modal inverse="${args.inverse}" > <ds-modal-header variant="surface-high"> <div slot="start"> <div slot="title">Surface High</div> <div slot="subtitle">Header subtitle</div> </div> <button slot="end" ds-button-icon size="small" kind="secondary"> <ds-demo-close-icon /> </button> </ds-modal-header> <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> </ds-modal> </div> </div> `, }), }; ``` -------------------------------------------------------------------------------- /packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/class-usage.visitor.spec.ts: -------------------------------------------------------------------------------- ```typescript import { ClassUsageVisitor } from './class-usage.visitor'; import type { ParsedTemplate, ParseTemplateOptions, } from '@angular/compiler' with { 'resolution-mode': 'import' }; describe('ClassCollectorVisitor', () => { let visitor: ClassUsageVisitor; let parseTemplate: ( template: string, templateUrl: string, options?: ParseTemplateOptions, ) => ParsedTemplate; beforeAll(async () => { parseTemplate = (await import('@angular/compiler')).parseTemplate; }); beforeEach(() => { visitor = new ClassUsageVisitor({ componentName: 'CounterComponent', deprecatedCssClasses: ['count', 'count-badge', 'count-item'], docsUrl: 'my.doc#CounterComponent', }); }); it('should not find class when it is not a class-binding', () => { const template = ` <ms-list-item [count]="link.count" > </ms-list-item> `; const ast = parseTemplate(template, 'template.html'); ast.nodes.forEach((node) => node.visit(visitor)); expect(visitor.getIssues()).toHaveLength(0); }); it('<div class="count">1</div> should find node with css class', () => { const template = `<div class="count">1</div>`; const ast = parseTemplate(template, 'template.html'); ast.nodes.forEach((node) => node.visit(visitor)); expect(visitor.getIssues()).toStrictEqual([ expect.objectContaining({ message: expect.stringContaining('CounterComponent'), }), ]); }); it('<div class="count">1</div> should find node within other css classes', () => { const template = `<div class="a count b">1</div>`; const ast = parseTemplate(template, 'template.html'); ast.nodes.forEach((node) => node.visit(visitor)); expect(visitor.getIssues()).toStrictEqual([ expect.objectContaining({ message: expect.stringContaining('CounterComponent'), severity: 'error', source: expect.objectContaining({ file: 'template.html', position: expect.any(Object), }), }), ]); }); it('<div [class.count]="true">2</div>', () => { const template = `<div [class.count]="true">2</div>`; const ast = parseTemplate(template, 'template.html'); ast.nodes.forEach((node) => node.visit(visitor)); expect(visitor.getIssues()).toStrictEqual([ expect.objectContaining({ message: expect.stringContaining('CounterComponent'), severity: 'error', source: expect.objectContaining({ file: 'template.html', position: expect.any(Object), }), }), ]); }); it('<div [class.count]="false">2</div> should find node with class-binding', () => { const template = `<div [class.count]="false">2</div>`; const ast = parseTemplate(template, 'template.html'); ast.nodes.forEach((node) => node.visit(visitor)); expect(visitor.getIssues()).toStrictEqual([ expect.objectContaining({ message: expect.stringContaining('CounterComponent'), severity: 'error', source: expect.objectContaining({ file: 'template.html', position: expect.any(Object), }), }), ]); }); it('<div [class.a]="true">3</div> should not find not when other class is used in class-binding', () => { const template = `<div [class.a]="true">3</div>`; const ast = parseTemplate(template, 'template.html'); ast.nodes.forEach((node) => node.visit(visitor)); expect(visitor.getIssues()).toHaveLength(0); }); it('<div [class.a]="false">3</div> should not find node when other class is used in class-binding', () => { const template = `<div [class.a]="false">3</div>`; const ast = parseTemplate(template, 'template.html'); ast.nodes.forEach((node) => node.visit(visitor)); expect(visitor.getIssues()).toHaveLength(0); }); it("<div [ngClass]=\"['count', 'second']\">5</div> should find node when class is used in ngClass-binding", () => { const template = `<div [ngClass]="['count', 'second']">5</div>`; const ast = parseTemplate(template, 'template.html'); ast.nodes.forEach((node) => node.visit(visitor)); expect(visitor.getIssues()).toStrictEqual([ expect.objectContaining({ message: expect.stringContaining('CounterComponent'), severity: 'error', source: expect.objectContaining({ file: 'template.html', position: expect.any(Object), }), }), ]); }); it('<div [ngClass]="{ count: true, second: true, third: true }">6</div> should find node when class is used in ngClass-binding with object-binding', () => { const template = `<div [ngClass]="{ count: true, second: true, third: true }">6</div>`; const ast = parseTemplate(template, 'template.html'); ast.nodes.forEach((node) => node.visit(visitor)); expect(visitor.getIssues()).toStrictEqual([ expect.objectContaining({ message: expect.stringContaining('CounterComponent'), severity: 'error', source: expect.objectContaining({ file: 'template.html', position: expect.any(Object), }), }), ]); }); it('<div [ngClass]="{ count: false, second: true, third: true }">6</div> should find node when class is used in ngClass-binding with object-binding and other classes', () => { const template = `<div [ngClass]="{ count: false, second: true, third: true }">6</div>`; const ast = parseTemplate(template, 'template.html'); ast.nodes.forEach((node) => node.visit(visitor)); expect(visitor.getIssues()).toStrictEqual([ expect.objectContaining({ message: expect.stringContaining('CounterComponent'), severity: 'error', source: expect.objectContaining({ file: 'template.html', position: expect.any(Object), }), }), ]); }); it('<div [ngClass]="{ \'count second\': true }">7</div> should find node when class is used in ngClass-binding with object-binding and condensed signature', () => { const template = `<div [ngClass]="{ 'count second': true }">7</div>`; const ast = parseTemplate(template, 'template.html'); ast.nodes.forEach((node) => node.visit(visitor)); expect(visitor.getIssues()).toStrictEqual([ expect.objectContaining({ message: expect.stringContaining('CounterComponent'), severity: 'error', source: expect.objectContaining({ file: 'template.html', position: expect.any(Object), }), }), ]); }); it('<div [ngClass]="{ \'count second\': false }">7</div>', () => { const template = `<div [ngClass]="{ 'count second': false }">7</div>`; const ast = parseTemplate(template, 'template.html'); ast.nodes.forEach((node) => node.visit(visitor)); expect(visitor.getIssues()).toStrictEqual([ expect.objectContaining({ message: expect.stringContaining('CounterComponent'), severity: 'error', source: expect.objectContaining({ file: 'template.html', position: expect.any(Object), }), }), ]); }); it('should find all nodes in @if-blocks', () => { const template = ` @if (true){ <div id="1" class="count"></div> } <div> <span> @if (true){ <div id="2" class="count"></div> } </span> </div> `; const ast = parseTemplate(template, 'template.html'); ast.nodes.forEach((node) => node.visit(visitor)); expect(visitor.getIssues()).toHaveLength(2); expect(visitor.getIssues()).toEqual([ expect.objectContaining({ message: expect.stringContaining('CounterComponent'), severity: 'error', source: expect.objectContaining({ file: 'template.html', position: expect.any(Object), }), }), expect.objectContaining({ message: expect.stringContaining('CounterComponent'), severity: 'error', source: expect.objectContaining({ file: 'template.html', position: expect.any(Object), }), }), ]); }); it('should find all nodes with *ngIf', () => { const template = ` <ng-container *ngIf="true"> <div id="1" class="count"></div> </ng-container> <div> <span> <div *ngIf="true" id="2" class="count"></div> </span> </div> `; const ast = parseTemplate(template, 'template.html'); ast.nodes.forEach((node) => node.visit(visitor)); expect(visitor.getIssues()).toHaveLength(2); expect(visitor.getIssues()).toEqual([ expect.objectContaining({ message: expect.stringContaining('CounterComponent'), severity: 'error', source: expect.objectContaining({ file: 'template.html', position: expect.any(Object), }), }), expect.objectContaining({ message: expect.stringContaining('CounterComponent'), severity: 'error', source: expect.objectContaining({ file: 'template.html', position: expect.any(Object), }), }), ]); }); it('should find all nodes in @for-block', () => { const template = ` @for (item of items; track item.name) { <div id="1" class="count"></div> } <div> <span> @for (item of items; track item.name) { <div id="2" class="count"></div> } </span> </div> `; const ast = parseTemplate(template, 'template.html'); ast.nodes.forEach((node) => node.visit(visitor)); expect(visitor.getIssues()).toHaveLength(2); expect(visitor.getIssues()).toEqual([ expect.objectContaining({ message: expect.stringContaining('CounterComponent'), severity: 'error', source: expect.objectContaining({ file: 'template.html', position: expect.any(Object), }), }), expect.objectContaining({ message: expect.stringContaining('CounterComponent'), severity: 'error', source: expect.objectContaining({ file: 'template.html', position: expect.any(Object), }), }), ]); }); it('should find all nodes with *ngFor', () => { const template = ` <div id="1" *ngFor="let item of [1,2,3]" class="count"></div> `; const ast = parseTemplate(template, 'template.html'); ast.nodes.forEach((node) => node.visit(visitor)); expect(visitor.getIssues()).toStrictEqual([ expect.objectContaining({ message: expect.stringContaining('CounterComponent'), severity: 'error', source: expect.objectContaining({ file: 'template.html', position: expect.any(Object), }), }), ]); }); it('should find all nodes with *switch', () => { const template = ` <ng-container *ngSwitchCase="userPermissions"> <ng-container *ngSwitchCase="'admin'"> <div id="1" class="count"></div> </ng-container> <ng-container *ngSwitchCase="'reviewer'"> </ng-container> <ng-container *ngSwitchDefault> </ng-container> </ng-container> `; const ast = parseTemplate(template, 'template.html'); ast.nodes.forEach((node) => node.visit(visitor)); expect(visitor.getIssues()).toStrictEqual([ expect.objectContaining({ message: expect.stringContaining('CounterComponent'), severity: 'error', source: expect.objectContaining({ file: 'template.html', position: expect.any(Object), }), }), ]); }); it('should find all nodes inside @switch', () => { const template = ` @switch (userPermissions) { @case ('admin') { <div id="1" class="count"></div> } @case ('reviewer') { } @case ('editor') { } @default { } } `; const ast = parseTemplate(template, 'template.html'); ast.nodes.forEach((node) => node.visit(visitor)); expect(visitor.getIssues()).toStrictEqual([ expect.objectContaining({ message: expect.stringContaining('CounterComponent'), severity: 'error', source: expect.objectContaining({ file: 'template.html', position: expect.any(Object), }), }), ]); }); it('should find all nodes inside @defer', () => { const template = ` @defer { <div id="1" class="count"></div> } `; const ast = parseTemplate(template, 'template.html'); ast.nodes.forEach((node) => node.visit(visitor)); expect(visitor.getIssues()).toStrictEqual([ expect.objectContaining({ message: expect.stringContaining('CounterComponent'), severity: 'error', source: expect.objectContaining({ file: 'template.html', position: expect.any(Object), }), }), ]); }); it('should find deprecated classes in interpolated class attributes', () => { const template = `<div class="count count-{{ size() }} other-class">Content</div>`; const ast = parseTemplate(template, 'template.html'); ast.nodes.forEach((node) => node.visit(visitor)); expect(visitor.getIssues()).toStrictEqual([ expect.objectContaining({ message: expect.stringContaining('CounterComponent'), severity: 'error', source: expect.objectContaining({ file: 'template.html', position: expect.any(Object), }), }), ]); }); // Deduplication tests describe('deduplication', () => { it('should deduplicate multiple deprecated classes in same class attribute', () => { const template = `<div class="count count-badge other-class">Content</div>`; const ast = parseTemplate(template, 'template.html'); ast.nodes.forEach((node) => node.visit(visitor)); expect(visitor.getIssues()).toHaveLength(1); const message = visitor.getIssues()[0].message; expect(message).toContain('count, count-badge'); expect(message).toContain('CounterComponent'); expect(visitor.getIssues()[0]).toEqual( expect.objectContaining({ severity: 'error', source: expect.objectContaining({ file: 'template.html', position: expect.any(Object), }), }), ); }); it('should deduplicate multiple deprecated classes in ngClass array', () => { const template = `<div [ngClass]="['count', 'count-badge', 'other-class']">Content</div>`; const ast = parseTemplate(template, 'template.html'); ast.nodes.forEach((node) => node.visit(visitor)); expect(visitor.getIssues()).toHaveLength(1); const message = visitor.getIssues()[0].message; expect(message).toContain('count, count-badge'); expect(message).toContain('CounterComponent'); expect(visitor.getIssues()[0]).toEqual( expect.objectContaining({ severity: 'error', source: expect.objectContaining({ file: 'template.html', position: expect.any(Object), }), }), ); }); it('should deduplicate multiple deprecated classes in ngClass object', () => { const template = `<div [ngClass]="{ count: true, 'count-badge': true, other: false }">Content</div>`; const ast = parseTemplate(template, 'template.html'); ast.nodes.forEach((node) => node.visit(visitor)); expect(visitor.getIssues()).toHaveLength(1); const message = visitor.getIssues()[0].message; expect(message).toContain('count, count-badge'); expect(message).toContain('CounterComponent'); expect(visitor.getIssues()[0]).toEqual( expect.objectContaining({ severity: 'error', source: expect.objectContaining({ file: 'template.html', position: expect.any(Object), }), }), ); }); it('should still create single issue for single deprecated class', () => { const template = `<div class="count other-class">Content</div>`; const ast = parseTemplate(template, 'template.html'); ast.nodes.forEach((node) => node.visit(visitor)); expect(visitor.getIssues()).toHaveLength(1); const message = visitor.getIssues()[0].message; expect(message).toContain('count'); expect(message).not.toContain(','); expect(message).toContain('CounterComponent'); expect(visitor.getIssues()[0]).toEqual( expect.objectContaining({ severity: 'error', source: expect.objectContaining({ file: 'template.html', position: expect.any(Object), }), }), ); }); it('should deduplicate three deprecated classes in same class attribute', () => { const template = `<div class="count count-badge count-item other-class">Content</div>`; const ast = parseTemplate(template, 'template.html'); ast.nodes.forEach((node) => node.visit(visitor)); expect(visitor.getIssues()).toHaveLength(1); const message = visitor.getIssues()[0].message; expect(message).toContain('count, count-badge, count-item'); expect(message).toContain('CounterComponent'); expect(visitor.getIssues()[0]).toEqual( expect.objectContaining({ severity: 'error', source: expect.objectContaining({ file: 'template.html', position: expect.any(Object), }), }), ); }); }); }); ```