This is page 2 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/shared/angular-ast-utils/src/lib/ts.walk.ts: -------------------------------------------------------------------------------- ```typescript import * as ts from 'typescript'; import { getDecorators, isDecorator } from 'typescript'; /** * Visits all decorators in a given class. */ export function visitAngularDecorators( node: ts.Node, visitor: (decorator: ts.Decorator) => void, ) { const decorators = getDecorators(node as ts.HasDecorators); if (!decorators?.length) return; decorators.forEach((decorator: ts.Decorator) => { if (!isDecorator(decorator)) return; visitor(decorator); }); } // 👀 visit every decorator // ✔ in visitor use `isDecorator`or `isComponentDecorator` /** * Visits all properties inside a Angular decorator. e.g. @Component() */ export function visitAngularDecoratorProperties( decorator: ts.Decorator, visitor: (node: ts.PropertyAssignment) => void, ) { if (!ts.isCallExpression(decorator.expression)) { return; } const args = decorator.expression.arguments; if (!args?.length || !ts.isObjectLiteralExpression(args[0])) { return; } args[0].properties.forEach((prop: ts.ObjectLiteralElementLike) => { if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) return; visitor(prop); }); } ``` -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- ```yaml name: CI on: push: branches: - main pull_request: permissions: actions: read contents: read jobs: main: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # This enables task distribution via Nx Cloud # Run this command as early as possible, before dependencies are installed # Learn more at https://nx.dev/ci/reference/nx-cloud-cli#npx-nxcloud-startcirun # Uncomment this line to enable task distribution # - run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium-js" --stop-agents-after="build" # Cache node_modules - uses: actions/setup-node@v4 with: node-version: 20 cache: 'npm' - run: npm ci - uses: nrwl/nx-set-shas@v4 - run: npx nx format:check # Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud # - run: npx nx-cloud record -- echo Hello World # Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected - run: npx nx affected -t lint test build ``` -------------------------------------------------------------------------------- /packages/shared/angular-ast-utils/ai/API.md: -------------------------------------------------------------------------------- ```markdown # Angular AST Utils Small, zero‑dependency helpers for **parsing and analysing Angular code** inside Model Context Protocol (MCP) tools. ## Minimal usage ```ts import { parseComponents } from 'angular-ast-utils'; const components = await parseComponents(['src/app/app.component.ts']); console.log(components[0].className); // → 'AppComponent' ``` ## Key Features - **Component Parsing**: Parse Angular components from TypeScript files - **Template Analysis**: Visit and analyze Angular template AST nodes - **Style Processing**: Process component styles and stylesheets - **Angular Unit Discovery**: Find components, directives, pipes, and services - **Decorator Traversal**: Walk through Angular decorators and their properties - **CSS Class Detection**: Check for CSS classes in `[ngClass]` bindings ## Documentation map | Doc | What you'll find | | ------------------------------ | ------------------------------------------- | | [FUNCTIONS.md](./FUNCTIONS.md) | A–Z quick reference for every public symbol | | [EXAMPLES.md](./EXAMPLES.md) | Runnable scenarios with expected output | ``` -------------------------------------------------------------------------------- /packages/shared/angular-ast-utils/src/lib/decorator-config.visitor.inline-styles.spec.ts: -------------------------------------------------------------------------------- ```typescript /* eslint-disable prefer-const */ import { describe, it, expect } from 'vitest'; import * as ts from 'typescript'; import { classDecoratorVisitor } from './decorator-config.visitor.js'; describe('DecoratorConfigVisitor - inline styles', () => { it('extracts inline styles when styles is provided as single string', async () => { const source = [ "import { Component } from '@angular/core';", '@Component({', " selector: 'x-comp',", ' styles: `', ' .btn { color: red; }', ' `', '})', 'export class XComponent {}', ].join('\n'); const sourceFile = ts.createSourceFile( 'cmp.ts', source, ts.ScriptTarget.Latest, true, ); const visitor = await classDecoratorVisitor({ sourceFile }); ts.visitEachChild(sourceFile, visitor, undefined); expect(visitor.components).toHaveLength(1); const cmp = visitor.components[0] as any; expect(cmp.styles).toBeDefined(); expect(Array.isArray(cmp.styles)).toBe(true); expect(cmp.styles.length).toBe(1); expect(cmp.styles[0]).toEqual( expect.objectContaining({ filePath: 'cmp.ts' }), ); }); }); ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-usage-graph/utils/path-resolver.ts: -------------------------------------------------------------------------------- ```typescript import * as fs from 'fs'; import * as path from 'path'; import { toUnixPath } from '@code-pushup/utils'; import { DEPENDENCY_ANALYSIS_CONFIG } from '../models/config.js'; const { resolveExtensions, indexFiles } = DEPENDENCY_ANALYSIS_CONFIG; const SUFFIXES = ['', ...resolveExtensions, ...indexFiles.map((ext) => ext)]; // Memoized fs.existsSync (tiny utility) const existsCache = new Map<string, boolean>(); const exists = (p: string): boolean => { const cached = existsCache.get(p); if (cached !== undefined) return cached; const ok = fs.existsSync(p); existsCache.set(p, ok); return ok; }; export const isExternal = (p: string): boolean => !p.startsWith('./') && !p.startsWith('../') && !path.isAbsolute(p); export const resolveDependencyPath = ( importPath: string, fromFile: string, basePath: string, ): string | null => { if (isExternal(importPath)) return null; const base = path.resolve( path.dirname(fromFile), importPath.replace(/\/$/, ''), ); for (const s of SUFFIXES) { const candidate = base + s; if (exists(candidate)) { return toUnixPath(path.relative(basePath, candidate)); } } return null; }; ``` -------------------------------------------------------------------------------- /packages/angular-mcp/webpack.config.cjs: -------------------------------------------------------------------------------- ``` const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin'); const { join } = require('path'); const webpack = require('webpack'); module.exports = { output: { path: join(__dirname, 'dist'), }, resolve: { extensions: ['.ts', '.js', '.mjs', '.cjs', '.json'], }, externals: [ // Keep Node.js built-ins external function ({ request }, callback) { if (/^node:/.test(request)) { return callback(null, 'commonjs ' + request); } callback(); }, ], module: { rules: [ { test: /\.d\.ts$/, loader: 'ignore-loader', }, ], }, plugins: [ new NxAppWebpackPlugin({ target: 'node', compiler: 'tsc', main: './src/main.ts', tsConfig: './tsconfig.app.json', assets: [ './src/assets', { input: '.', glob: 'README.md', output: '.', }, ], optimization: false, outputHashing: 'none', generatePackageJson: true, externalDependencies: 'none', }), new webpack.BannerPlugin({ banner: '#!/usr/bin/env node', raw: true, entryOnly: true, }), ], }; ``` -------------------------------------------------------------------------------- /testing/vitest-setup/vite.config.ts: -------------------------------------------------------------------------------- ```typescript /// <reference types='vitest' /> import { defineConfig } from 'vite'; import dts from 'vite-plugin-dts'; import * as path from 'path'; export default defineConfig({ root: __dirname, cacheDir: '../../node_modules/.vite/testing/testing-vitest-setup', plugins: [ dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'), }), ], // Uncomment this if you are using workers. // worker: { // plugins: [ nxViteTsPaths() ], // }, // Configuration for building your library. // See: https://vitejs.dev/guide/build.html#library-mode build: { outDir: './dist', emptyOutDir: true, reportCompressedSize: true, commonjsOptions: { transformMixedEsModules: true, }, lib: { // Could also be a dictionary or array of multiple entry points. entry: 'src/index.ts', name: 'testing-vitest-setup', fileName: 'index', // Change this to the formats you want to support. // Don't forget to update your package.json as well. formats: ['es'], }, rollupOptions: { // External packages that should not be bundled into your library. external: [], }, }, }); ``` -------------------------------------------------------------------------------- /packages/shared/typescript-ast-utils/src/lib/utils.ts: -------------------------------------------------------------------------------- ```typescript import * as ts from 'typescript'; import { QUOTE_REGEX } from './constants.js'; export function isComponentDecorator(decorator: ts.Decorator): boolean { return isDecorator(decorator, 'Component'); } export function getDecorators(node: ts.Node): readonly ts.Decorator[] { if (ts.getDecorators) { return ts.getDecorators(node as ts.HasDecorators) ?? []; } if (hasDecorators(node)) { return node.decorators; } return []; } export function isDecorator( decorator: ts.Decorator, decoratorName?: string, ): boolean { const nodeObject = decorator?.expression as unknown as { expression: ts.Expression; }; const identifierObject = nodeObject?.expression; if (identifierObject == null || !ts.isIdentifier(identifierObject)) return false; if (decoratorName == null) { return true; } return identifierObject.text === decoratorName; } export function removeQuotes(node: ts.Node, sourceFile: ts.SourceFile): string { return node.getText(sourceFile).replace(QUOTE_REGEX, ''); } export function hasDecorators( node: ts.Node, ): node is ts.Node & { decorators: readonly ts.Decorator[] } { return 'decorators' in node && Array.isArray(node.decorators); } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/bad-mixed-external-assets.component.css: -------------------------------------------------------------------------------- ```css /* ❌ Bad: Legacy button styles */ .btn { padding: 10px 15px; background-color: blue; color: white; border: none; border-radius: 5px; cursor: pointer; } /* ❌ Bad: Custom modal styles */ .modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2); } .modal-content { text-align: center; } /* ❌ Bad: Custom progress bar */ .progress-bar { width: 100%; background-color: #ddd; } .progress { height: 10px; background-color: green; } /* ❌ Bad: Legacy alert */ .alert { padding: 10px; border: 1px solid red; background-color: pink; } /* ❌ Bad: Hardcoded dropdown */ .dropdown { padding: 5px; border: 1px solid #ccc; background-color: white; } /* ❌ Bad: Custom tooltip */ .tooltip { position: relative; display: inline-block; cursor: pointer; } .tooltip:hover::after { content: 'This is a tooltip'; position: absolute; background: black; color: white; padding: 5px; border-radius: 5px; top: 100%; left: 50%; transform: translateX(-50%); } /* ❌ Bad: Breadcrumb styling */ .breadcrumb { display: flex; gap: 5px; } .breadcrumb span { color: blue; cursor: pointer; } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/group-1/bad-mixed-external-assets-1.component.css: -------------------------------------------------------------------------------- ```css /* ❌ Bad: Legacy button styles */ .btn { padding: 10px 15px; background-color: blue; color: white; border: none; border-radius: 5px; cursor: pointer; } /* ❌ Bad: Custom modal styles */ .modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2); } .modal-content { text-align: center; } /* ❌ Bad: Custom progress bar */ .progress-bar { width: 100%; background-color: #ddd; } .progress { height: 10px; background-color: green; } /* ❌ Bad: Legacy alert */ .alert { padding: 10px; border: 1px solid red; background-color: pink; } /* ❌ Bad: Hardcoded dropdown */ .dropdown { padding: 5px; border: 1px solid #ccc; background-color: white; } /* ❌ Bad: Custom tooltip */ .tooltip { position: relative; display: inline-block; cursor: pointer; } .tooltip:hover::after { content: 'This is a tooltip'; position: absolute; background: black; color: white; padding: 5px; border-radius: 5px; top: 100%; left: 50%; transform: translateX(-50%); } /* ❌ Bad: Breadcrumb styling */ .breadcrumb { display: flex; gap: 5px; } .breadcrumb span { color: blue; cursor: pointer; } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/group-2/bad-mixed-external-assets-2.component.css: -------------------------------------------------------------------------------- ```css /* ❌ Bad: Legacy button styles */ .btn { padding: 10px 15px; background-color: blue; color: white; border: none; border-radius: 5px; cursor: pointer; } /* ❌ Bad: Custom modal styles */ .modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2); } .modal-content { text-align: center; } /* ❌ Bad: Custom progress bar */ .progress-bar { width: 100%; background-color: #ddd; } .progress { height: 10px; background-color: green; } /* ❌ Bad: Legacy alert */ .alert { padding: 10px; border: 1px solid red; background-color: pink; } /* ❌ Bad: Hardcoded dropdown */ .dropdown { padding: 5px; border: 1px solid #ccc; background-color: white; } /* ❌ Bad: Custom tooltip */ .tooltip { position: relative; display: inline-block; cursor: pointer; } .tooltip:hover::after { content: 'This is a tooltip'; position: absolute; background: black; color: white; padding: 5px; border-radius: 5px; top: 100%; left: 50%; transform: translateX(-50%); } /* ❌ Bad: Breadcrumb styling */ .breadcrumb { display: flex; gap: 5px; } .breadcrumb span { color: blue; cursor: pointer; } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/group-3/bad-mixed-external-assets-3.component.css: -------------------------------------------------------------------------------- ```css /* ❌ Bad: Legacy button styles */ .btn { padding: 10px 15px; background-color: blue; color: white; border: none; border-radius: 5px; cursor: pointer; } /* ❌ Bad: Custom modal styles */ .modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2); } .modal-content { text-align: center; } /* ❌ Bad: Custom progress bar */ .progress-bar { width: 100%; background-color: #ddd; } .progress { height: 10px; background-color: green; } /* ❌ Bad: Legacy alert */ .alert { padding: 10px; border: 1px solid red; background-color: pink; } /* ❌ Bad: Hardcoded dropdown */ .dropdown { padding: 5px; border: 1px solid #ccc; background-color: white; } /* ❌ Bad: Custom tooltip */ .tooltip { position: relative; display: inline-block; cursor: pointer; } .tooltip:hover::after { content: 'This is a tooltip'; position: absolute; background: black; color: white; padding: 5px; border-radius: 5px; top: 100%; left: 50%; transform: translateX(-50%); } /* ❌ Bad: Breadcrumb styling */ .breadcrumb { display: flex; gap: 5px; } .breadcrumb span { color: blue; cursor: pointer; } ``` -------------------------------------------------------------------------------- /packages/shared/ds-component-coverage/mocks/fixtures/e2e/demo/src/mixed-external-assets.component.css: -------------------------------------------------------------------------------- ```css /* ❌ Bad: Legacy button styles */ .btn { padding: 10px 15px; background-color: blue; color: white; border: none; border-radius: 5px; cursor: pointer; } /* ❌ Bad: Custom modal styles */ .modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2); } .modal-content { text-align: center; } /* ❌ Bad: Custom progress bar */ .progress-bar { width: 100%; background-color: #ddd; } .progress { height: 10px; background-color: green; } /* ❌ Bad: Legacy alert */ .alert { padding: 10px; border: 1px solid red; background-color: pink; } /* ❌ Bad: Hardcoded dropdown */ .dropdown { padding: 5px; border: 1px solid #ccc; background-color: white; } /* ❌ Bad: Custom tooltip */ .tooltip { position: relative; display: inline-block; cursor: pointer; } .tooltip:hover::after { content: 'This is a tooltip'; position: absolute; background: black; color: white; padding: 5px; border-radius: 5px; top: 100%; left: 50%; transform: translateX(-50%); } /* ❌ Bad: Breadcrumb styling */ .breadcrumb { display: flex; gap: 5px; } .breadcrumb span { color: blue; cursor: pointer; } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/design-system/ui/modal/src/modal.component.ts: -------------------------------------------------------------------------------- ```typescript import { ChangeDetectionStrategy, Component, Signal, ViewEncapsulation, booleanAttribute, computed, inject, input, signal, } from '@angular/core'; export const DS_MODAL_VARIANT_ARRAY = [ 'surface-lowest', 'surface-low', 'surface', ] as const; export type DsModalVariant = (typeof DS_MODAL_VARIANT_ARRAY)[number]; export class DsModalContext { inverse: Signal<boolean> = signal(false); // Explicit type } @Component({ selector: 'ds-modal', template: `<ng-content />`, host: { class: 'ds-modal', role: 'dialog', 'aria-label': 'Modal dialog', '[class.ds-modal-inverse]': 'inverse()', '[class.ds-modal-bottom-sheet]': 'bottomSheet()', '[class]': 'hostClass()', }, providers: [DsModalContext], standalone: true, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, }) export class DsModal { inverse = input(false, { transform: booleanAttribute }); bottomSheet = input(false, { transform: booleanAttribute }); variant = input<DsModalVariant>('surface'); private context = inject(DsModalContext); protected hostClass = computed(() => `ds-modal-${this.variant()}`); constructor() { this.context.inverse = computed(() => this.inverse()); } } ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/tools.ts: -------------------------------------------------------------------------------- ```typescript import { ToolsConfig } from '@push-based/models'; import { reportViolationsTools, reportAllViolationsTools, } from './report-violations/index.js'; import { getProjectDependenciesTools } from './project/get-project-dependencies.tool.js'; import { reportDeprecatedCssTools } from './project/report-deprecated-css.tool.js'; import { buildComponentUsageGraphTools } from './component-usage-graph/index.js'; import { getDsComponentDataTools } from './component/get-ds-component-data.tool.js'; import { listDsComponentsTools } from './component/list-ds-components.tool.js'; import { getDeprecatedCssClassesTools } from './component/get-deprecated-css-classes.tool.js'; import { buildComponentContractTools, diffComponentContractTools, listComponentContractsTools, } from './component-contract/index.js'; export const dsTools: ToolsConfig[] = [ // Project tools ...reportViolationsTools, ...reportAllViolationsTools, ...getProjectDependenciesTools, ...reportDeprecatedCssTools, ...buildComponentUsageGraphTools, // Component contract tools ...buildComponentContractTools, ...diffComponentContractTools, ...listComponentContractsTools, // Component tools ...getDsComponentDataTools, ...listDsComponentsTools, ...getDeprecatedCssClassesTools, ]; ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/prompts/prompt-registry.ts: -------------------------------------------------------------------------------- ```typescript import { PromptSchema } from '@modelcontextprotocol/sdk/types.js'; import { z } from 'zod'; /** * Registry of available prompts for the Angular MCP server. * * This registry is currently empty but provides the infrastructure * for future prompt implementations. Prompts allow LLMs to request * structured text generation with specific parameters. * * When adding prompts: * 1. Add the prompt schema to PROMPTS with name, description, and arguments * 2. Add the corresponding implementation to PROMPTS_IMPL * * Example implementation: * ```typescript * PROMPTS['component-docs'] = { * name: 'component-docs', * description: 'Generate documentation for Angular components', * arguments: [ * { * name: 'componentName', * description: 'Name of the component to document', * required: true, * }, * ], * }; * * PROMPTS_IMPL['component-docs'] = { * text: (args) => `Generate docs for ${args.componentName}...`, * }; * ``` */ export const PROMPTS: Record<string, z.infer<typeof PromptSchema>> = { // Future prompts will be added here } as const; export const PROMPTS_IMPL: Record< string, { text: (args: Record<string, string>) => string } > = { // Future prompt implementations will be added here } as const; ``` -------------------------------------------------------------------------------- /tools/nx-advanced-profile.bin.js: -------------------------------------------------------------------------------- ```javascript import { mkdirSync, writeFileSync } from 'node:fs'; import { parseArgs } from 'node:util'; import { nxRunWithPerfLogging } from './nx-advanced-profile.js'; const { values } = parseArgs({ options: { args: { type: 'string', }, verbose: { type: 'boolean', short: 'v', }, noPatch: { type: 'boolean', short: 'p', }, outDir: { type: 'string', short: 'd', }, outFile: { type: 'string', short: 'f', }, }, }); const { args = ['show', 'projects'].join(','), verbose, noPatch, outDir = '.nx-profiling', outFile = `nx-${args.split(',').join('-')}.${Date.now()}.profile.json`, } = values; // Run the function with arguments and write the collected timings to a JSON file. nxRunWithPerfLogging(args.split(','), { verbose, noPatch, onData: (perfProfileEvent) => { // console.log(perfProfileEvent); }, beforeExit: (profile) => { // @TODO figure out why profile directly does not show the flames but profile.traceEvents does const profileStdout = JSON.stringify(profile.traceEvents, null, 2); mkdirSync(outDir, { recursive: true }); writeFileSync(`${outDir}/${outFile}`, profileStdout); if (verbose) { console.log(profileStdout); } }, }); ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/types.ts: -------------------------------------------------------------------------------- ```typescript export type BaseToolSchema = { cwd: string; }; export type ToolInput< TSchema extends Record< string, { inputSchema: { properties: Record<string, unknown> } } >, T extends keyof TSchema, > = { [K in keyof TSchema[T]['inputSchema']['properties']]: TSchema[T]['inputSchema']['properties'][K] extends { type: 'string'; } ? string : TSchema[T]['inputSchema']['properties'][K] extends { type: 'array'; items: { type: 'string' }; } ? string[] : TSchema[T]['inputSchema']['properties'][K] extends { type: 'array'; items: { type: 'object'; properties: Record<string, unknown> }; } ? Array<{ [P in keyof TSchema[T]['inputSchema']['properties'][K]['items']['properties']]: TSchema[T]['inputSchema']['properties'][K]['items']['properties'][P] extends { type: 'string'; } ? string : TSchema[T]['inputSchema']['properties'][K]['items']['properties'][P] extends { type: 'array'; items: { type: 'string' }; } ? string[] : never; }> : never; }; export type NamedRecord<T extends string> = Record<T, { name: T }>; ``` -------------------------------------------------------------------------------- /packages/shared/models/ai/API.md: -------------------------------------------------------------------------------- ```markdown # Models Simple **TypeScript types** for Angular MCP toolkit shared interfaces and utilities. ## Minimal usage ```ts import { type CliArgsObject, type ToolsConfig } from '@push-based/models'; // CLI argument types const args: CliArgsObject = { directory: './src', componentName: 'DsButton', _: ['command'], }; // MCP tool configuration const toolConfig: ToolsConfig = { schema: { name: 'my-tool', description: 'A custom tool', inputSchema: { type: 'object', properties: {}, }, }, handler: async (request) => { return { content: [{ type: 'text', text: 'Result' }] }; }, }; ``` ## Key Features - **CLI Types**: Type-safe command line argument handling - **MCP Integration**: Model Context Protocol tool schema definitions and handlers - **Diagnostics**: Interface for objects that can report issues and diagnostics - **Lightweight**: Minimal dependencies, focused on essential shared types ## Documentation map | Doc | What you'll find | | ------------------------------ | ------------------------------------------- | | [FUNCTIONS.md](./FUNCTIONS.md) | A–Z quick reference for every public symbol | | [EXAMPLES.md](./EXAMPLES.md) | Runnable scenarios with expected output | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/design-system/ui/segmented-control/src/segmented-option.component.ts: -------------------------------------------------------------------------------- ```typescript import { ChangeDetectionStrategy, Component, TemplateRef, ViewEncapsulation, computed, contentChild, inject, input, output, } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { rxHostPressedListener } from '@frontend/ui/rx-host-listener'; import { DsSegmentedControl } from './segmented-control.component'; @Component({ selector: 'ds-segmented-option', template: `<ng-content />`, standalone: true, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, }) export class DsSegmentedOption { private segmentedControl = inject(DsSegmentedControl); readonly title = input(''); readonly name = input.required<string>(); readonly selectOption = output<string>(); readonly customTemplate = contentChild<TemplateRef<any>>('dsTemplate'); readonly selected = computed( () => this.segmentedControl.selectedOption() === this, ); readonly focusVisible = computed( () => this.segmentedControl.focusVisibleOption() === this, ); readonly focused = computed( () => this.segmentedControl.focusedOption() === this, ); constructor() { rxHostPressedListener() .pipe(takeUntilDestroyed()) .subscribe(() => this.selectOption.emit(this.name())); } } ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/models/schema.ts: -------------------------------------------------------------------------------- ```typescript import { ToolSchemaOptions } from '@push-based/models'; import { COMMON_ANNOTATIONS } from '../../../shared/index.js'; /** * Schema for diffing component contracts */ export const diffComponentContractSchema: ToolSchemaOptions = { name: 'diff_component_contract', description: 'Compare before/after contracts for parity and surface breaking changes.', inputSchema: { type: 'object', properties: { saveLocation: { type: 'string', description: 'Path where to save the diff result file. Supports both absolute and relative paths.', }, contractBeforePath: { type: 'string', description: 'Path to the contract file before refactoring. Supports both absolute and relative paths.', }, contractAfterPath: { type: 'string', description: 'Path to the contract file after refactoring. Supports both absolute and relative paths.', }, dsComponentName: { type: 'string', description: 'The name of the design system component being used', default: '', }, }, required: ['saveLocation', 'contractBeforePath', 'contractAfterPath'], }, annotations: { title: 'Diff Component Contract', ...COMMON_ANNOTATIONS.readOnly, }, }; ``` -------------------------------------------------------------------------------- /packages/shared/angular-ast-utils/src/lib/parse-component.ts: -------------------------------------------------------------------------------- ```typescript import { toUnixPath } from '@code-pushup/utils'; import * as ts from 'typescript'; import { classDecoratorVisitor } from './decorator-config.visitor.js'; import { ParsedComponent } from './types.js'; /** * Parses Angular components from a `FastFindInFiles` result. * It uses `typescript` to parse the components source files and extract the decorators. * From the decorators, it extracts the `@Component` decorator and its properties. * The used properties are `templateUrl`, `template`, `styles`, and `styleUrls`. * * @param files */ export async function parseComponents( files: string[], ): Promise<ParsedComponent[]> { const filePaths = new Set(files.map((filePath) => toUnixPath(filePath))); const program = ts.createProgram([...filePaths], { target: ts.ScriptTarget.Latest, module: ts.ModuleKind.ESNext, experimentalDecorators: true, }); const sourceFiles: ts.SourceFile[] = program .getSourceFiles() .filter((file: ts.SourceFile) => filePaths.has(file.fileName)); const results: ParsedComponent[] = []; //sourceFiles for (const sourceFile of sourceFiles) { const visitor = await classDecoratorVisitor({ sourceFile }); ts.visitEachChild(sourceFile, visitor, undefined); results.push(...visitor.components); } return results; } ``` -------------------------------------------------------------------------------- /packages/shared/styles-ast-utils/ai/API.md: -------------------------------------------------------------------------------- ```markdown # Styles AST Utils Small, zero‑dependency helpers for **parsing and analyzing CSS/SCSS stylesheets** using PostCSS inside Model Context Protocol (MCP) tools. ## Minimal usage ```ts import { parseStylesheet, visitEachChild } from '@push-based/styles-ast-utils'; const result = parseStylesheet('.btn { color: red; }', 'styles.css'); const visitor = { visitRule: (rule) => console.log(rule.selector), // → '.btn' }; visitEachChild(result.root, visitor); ``` ## Key Features - **Stylesheet Parsing**: Parse CSS/SCSS content using PostCSS with safe parsing - **AST Traversal**: Multiple traversal strategies for visiting CSS nodes - **Visitor Pattern**: Flexible visitor interface for processing different node types - **Source Location Mapping**: Convert AST nodes to linkable source locations - **Line Number Preservation**: Maintain accurate line numbers for error reporting - **Safe Parsing**: Graceful handling of malformed CSS using postcss-safe-parser ## Documentation map | Doc | What you'll find | | ------------------------------ | ------------------------------------------- | | [FUNCTIONS.md](./FUNCTIONS.md) | A–Z quick reference for every public symbol | | [EXAMPLES.md](./EXAMPLES.md) | Runnable scenarios with expected output | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/validation/file-existence.ts: -------------------------------------------------------------------------------- ```typescript import * as fs from 'node:fs'; import * as path from 'node:path'; import { AngularMcpServerOptions } from './angular-mcp-server-options.schema.js'; export function validateAngularMcpServerFilesExist( config: AngularMcpServerOptions, ) { const root = config.workspaceRoot; if (!fs.existsSync(root)) { throw new Error(`workspaceRoot directory does not exist: ${root}`); } const missingFiles: string[] = []; // Always require uiRoot, optional: storybookDocsRoot, deprecatedCssClassesPath const dsPaths = [ config.ds.storybookDocsRoot ? { label: 'ds.storybookDocsRoot', relPath: config.ds.storybookDocsRoot } : null, config.ds.deprecatedCssClassesPath ? { label: 'ds.deprecatedCssClassesPath', relPath: config.ds.deprecatedCssClassesPath, } : null, { label: 'ds.uiRoot', relPath: config.ds.uiRoot }, ].filter(Boolean) as { label: string; relPath: string }[]; for (const { label, relPath } of dsPaths) { const absPath = path.resolve(root, relPath); if (!fs.existsSync(absPath)) { missingFiles.push(`${label} (resolved to: ${absPath})`); } } if (missingFiles.length > 0) { throw new Error( `The following required files or directories do not exist:\n` + missingFiles.join('\n'), ); } } ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/spec/meta.generator.spec.ts: -------------------------------------------------------------------------------- ```typescript import { describe, it, expect } from 'vitest'; import { generateMeta } from '../utils/meta.generator.js'; interface ParsedComponentStub { className?: string; selector?: string; } describe('generateMeta', () => { const templatePath = 'src/app/foo.component.html'; it('uses parsed component className and selector when provided', () => { const parsedComponent: ParsedComponentStub = { className: 'FooComponent', selector: 'app-foo', }; const meta = generateMeta(templatePath, parsedComponent as any, false); expect(meta.name).toBe('FooComponent'); expect(meta.selector).toBe('app-foo'); expect(meta.sourceFile).toBe(templatePath); expect(meta.templateType).toBe('external'); expect(meta.hash).toBe(''); expect(typeof meta.generatedAt).toBe('string'); }); it('falls back to filename when className or selector missing', () => { const parsedComponent: ParsedComponentStub = {}; const meta = generateMeta(templatePath, parsedComponent as any); expect(meta.name).toBe('foo.component'); expect(meta.selector).toBe('.foo.component'); }); it('sets templateType to inline when flag is true', () => { const parsedComponent: ParsedComponentStub = {}; const meta = generateMeta(templatePath, parsedComponent as any, true); expect(meta.templateType).toBe('inline'); }); }); ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/validation/angular-mcp-server-options.schema.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; import * as path from 'path'; const isAbsolutePath = (val: string) => path.isAbsolute(val); const isRelativePath = (val: string) => !path.isAbsolute(val); export const AngularMcpServerOptionsSchema = z.object({ workspaceRoot: z.string().refine(isAbsolutePath, { message: 'workspaceRoot must be an absolute path to the repository root where MCP server is working (e.g., /path/to/workspace-root)', }), ds: z.object({ storybookDocsRoot: z .string() .optional() .refine((val) => val === undefined || isRelativePath(val), { message: 'ds.storybookDocsRoot must be a relative path from workspace root to the storybook project root (e.g., path/to/storybook/components)', }), deprecatedCssClassesPath: z .string() .optional() .refine((val) => val === undefined || isRelativePath(val), { message: 'ds.deprecatedCssClassesPath must be a relative path from workspace root to the file component to deprecated css classes mapping (e.g., path/to/components-config.js)', }), uiRoot: z.string().refine(isRelativePath, { message: 'ds.uiRoot must be a relative path from workspace root to the components folder (e.g., path/to/components)', }), }), }); export type AngularMcpServerOptions = z.infer< typeof AngularMcpServerOptionsSchema >; ``` -------------------------------------------------------------------------------- /packages/shared/styles-ast-utils/src/lib/utils.ts: -------------------------------------------------------------------------------- ```typescript import { Issue } from '@code-pushup/models'; import { Rule } from 'postcss'; /** * Convert a Root to an Issue source object and adjust its position based on startLine. * It creates a "linkable" source object for the issue. * By default, the source location is 0 indexed, so we add 1 to the startLine to make it work in file links. * * @param rule The AST rule to convert into a linkable source object. * @param startLine The positions of the asset contain the style rules. */ export function styleAstRuleToSource( { source }: Pick<Rule, 'source'>, startLine = 0, // 0 indexed ): Issue['source'] { if (source?.input.file == null) { throw new Error( 'style parsing was not initialized with a file path. Check the postcss options.', ); } const offset = startLine - 1; // -1 because PostCss is 1 indexed so we have to substract 1 to make is work in 0 based index return { file: source.input.file, position: { startLine: (source?.start?.line ?? 1) + offset + 1, // +1 because the code works 0 indexed and file links work 1 indexed. ...(source?.start?.column && { startColumn: source?.start?.column }), ...(source?.end?.line && { endLine: source?.end?.line + offset + 1 }), // +1 because the code works 0 indexed and file links work 1 indexed. ...(source?.end?.column && { endColumn: source?.end?.column }), }, }; } ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/types.ts: -------------------------------------------------------------------------------- ```typescript /** * Shared types for violation analysis across file and folder reporting modules */ export interface BaseViolationOptions { cwd?: string; directory: string; componentName: string; deprecatedCssClassesPath?: string; } export interface BaseViolationIssue { message: string; source?: { file: string; position?: { startLine: number; }; }; } export interface BaseViolationAudit { details?: { issues?: BaseViolationIssue[]; }; title: string; score: number; } export interface BaseViolationResult { audits: BaseViolationAudit[]; [key: string]: unknown; } // Coverage analysis types (moved from project/utils/coverage-helpers.ts) export interface ReportCoverageParams { cwd?: string; returnRawData?: boolean; outputFormat?: 'text'; directory: string; dsComponents: DsComponent[]; } export interface DsComponent { componentName: string; deprecatedCssClasses: string[]; } export interface FormattedCoverageResult { textOutput: string; rawData?: { rawPluginResult: BaseViolationResult; pluginOptions: any; }; } // Shared file grouping types (consolidated from different modules) export interface FileGroup { message: string; lines: number[]; } export interface FileGroups { [fileName: string]: FileGroup; } // Performance-optimized path cache export interface PathCache { [key: string]: string; } ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/validation/ds-components.schema.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; /** * Expected format example: * [ * { * componentName: 'DsButton', * deprecatedCssClasses: ['btn', 'btn-primary', 'legacy-button'] * }, * { * componentName: 'DsModal', * deprecatedCssClasses: ['modal'] * } * ] */ const EXAMPLE_FORMAT = `Expected: [{ componentName: 'DsButton', deprecatedCssClasses: ['btn'] }]`; export const DsComponentSchema = z.object({ componentName: z.string({ required_error: 'Missing required "componentName" field. Must be a string like "DsButton".', invalid_type_error: 'Invalid "componentName" type. Must be a string like "DsButton".', }), deprecatedCssClasses: z.array( z.string({ required_error: 'CSS class name must be a string. Example: "btn" or "legacy-button".', invalid_type_error: 'CSS class name must be a string.', }), { required_error: 'Missing required "deprecatedCssClasses" field. Must be an array like ["btn", "legacy-button"].', invalid_type_error: 'Invalid "deprecatedCssClasses" type. Must be an array of strings.', }, ), }); export const DsComponentsArraySchema = z.array(DsComponentSchema, { required_error: `Configuration must be an array of component objects. ${EXAMPLE_FORMAT}`, invalid_type_error: `Invalid configuration format. Must export an array. ${EXAMPLE_FORMAT}`, }); ``` -------------------------------------------------------------------------------- /packages/shared/ds-component-coverage/ai/API.md: -------------------------------------------------------------------------------- ```markdown # DS Component Coverage Small, zero‑dependency helpers for **measuring and asserting usage of Design System components** in Angular projects by detecting deprecated CSS classes. ## Minimal usage ```ts import { dsComponentCoveragePlugin } from '@push-based/ds-component-coverage'; const plugin = dsComponentCoveragePlugin({ directory: './src/app', dsComponents: [ { componentName: 'DsButton', deprecatedCssClasses: ['btn', 'button'], docsUrl: 'https://design-system.com/button', }, ], }); ``` ## Key Features - **CSS Class Detection**: Find deprecated CSS classes in templates and stylesheets - **Design System Migration**: Track migration progress from legacy styles to DS components - **Template Analysis**: Scan Angular templates for class usage in various binding types - **Style Analysis**: Detect deprecated class definitions in component styles - **Code Pushup Integration**: Built-in plugin for Code Pushup auditing framework - **Comprehensive Reporting**: Generate detailed reports with file locations and suggestions ## Documentation map | Doc | What you'll find | | ------------------------------ | ------------------------------------------- | | [FUNCTIONS.md](./FUNCTIONS.md) | A–Z quick reference for every public symbol | | [EXAMPLES.md](./EXAMPLES.md) | Runnable scenarios with expected output | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/design-system/ui/modal/src/modal-header/modal-header.component.ts: -------------------------------------------------------------------------------- ```typescript import { ChangeDetectionStrategy, Component, ViewEncapsulation, computed, inject, input, } from '@angular/core'; import { DsModalContext } from '../modal.component'; export const DS_MODAL_HEADER_VARIANT_ARRAY = [ 'surface-lowest', 'surface-low', 'surface', 'surface-high', 'nav-bg', ] as const; export type DsModalHeaderVariant = (typeof DS_MODAL_HEADER_VARIANT_ARRAY)[number]; @Component({ selector: 'ds-modal-header', standalone: true, template: ` <div class="ds-modal-header-container"> <div class="ds-modal-header-start"> <ng-content select="[slot=start]" /> </div> <div class="ds-modal-header-center"> <ng-content select="[slot=center]" /> <ng-content select="ds-modal-header-drag, [modal-header-image]" /> </div> <div class="ds-modal-header-end"> <ng-content select="[slot=end]" /> </div> </div> `, host: { class: 'ds-modal-header', '[class]': 'hostClass()', '[class.ds-modal-header-inverse]': 'context.inverse()', role: 'dialog', 'aria-label': 'Modal header dialog', }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, }) export class DsModalHeader { variant = input<DsModalHeaderVariant>('surface'); protected context = inject(DsModalContext); protected hostClass = computed(() => `ds-modal-header-${this.variant()}`); } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/complex-components/first-case/dashboard-demo.component.scss: -------------------------------------------------------------------------------- ```scss .demo-container { min-height: 100vh; background: #f9fafb; transition: background-color 0.2s ease; } .demo-container.dark { background: #111827; } .demo-content { padding: 2rem; max-width: 1200px; margin: 0 auto; } .demo-content h2 { color: #1f2937; margin-bottom: 1rem; } .demo-content p { color: #6b7280; margin-bottom: 2rem; } .demo-actions { display: flex; gap: 1rem; margin-bottom: 2rem; flex-wrap: wrap; } .demo-button { padding: 0.5rem 1rem; background: #3b82f6; color: white; border: none; border-radius: 0.375rem; cursor: pointer; font-size: 0.875rem; transition: background-color 0.2s ease; } .demo-button:hover { background: #2563eb; } .demo-log { background: white; border-radius: 0.5rem; padding: 1.5rem; box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); } .demo-log h3 { margin: 0 0 1rem 0; color: #1f2937; font-size: 1.125rem; } .log-entries { max-height: 300px; overflow-y: auto; } .log-entry { padding: 0.5rem; border-bottom: 1px solid #f3f4f6; font-size: 0.875rem; color: #374151; font-family: monospace; } .log-entry:last-child { border-bottom: none; } /* Dark mode styles */ .dark .demo-content h2 { color: #f9fafb; } .dark .demo-content p { color: #d1d5db; } .dark .demo-log { background: #374151; } .dark .demo-log h3 { color: #f9fafb; } .dark .log-entry { color: #d1d5db; border-bottom-color: #4b5563; } ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/utils/styles.collector.ts: -------------------------------------------------------------------------------- ```typescript import { readFileSync } from 'node:fs'; import { parseStylesheet, visitEachChild } from '@push-based/styles-ast-utils'; import { selectorMatches } from './css-match.js'; import type { StyleDeclarations, DomStructure, } from '../../shared/models/types.js'; import type { Declaration, Rule } from 'postcss'; /** * Collect styles with explicit DOM relationships */ export async function collectStylesV2( scssPath: string, dom: DomStructure, ): Promise<StyleDeclarations> { const styles: StyleDeclarations = { sourceFile: scssPath, rules: {} }; const scssContent = readFileSync(scssPath, 'utf-8'); const parsedStyles = parseStylesheet(scssContent, scssPath); if (parsedStyles.root.type === 'root') { visitEachChild(parsedStyles.root, { visitRule: (rule: Rule) => { const properties: Record<string, string> = {}; rule.walkDecls?.((decl: Declaration) => { properties[decl.prop] = decl.value; }); styles.rules[rule.selector] = { appliesTo: findMatchingDomElements(rule.selector, dom), properties, }; }, }); } return styles; } /** * Find DOM elements that match a CSS selector */ function findMatchingDomElements( cssSelector: string, dom: DomStructure, ): string[] { return Object.entries(dom) .filter(([domKey, element]) => selectorMatches(cssSelector, domKey, element), ) .map(([domKey]) => domKey); } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/complex-components/first-case/dashboard-demo.component.html: -------------------------------------------------------------------------------- ```html <div class="demo-container"> <app-dashboard-header [badgeType]="'premium'" [badgeSize]="'medium'" [animated]="true" [pulsing]="false" [dismissible]="true" [userProfile]="userProfile()" [darkMode]="darkMode()" (searchPerformed)="onSearchPerformed($event)" (badgeDismissed)="onBadgeDismissed()" (notificationClicked)="onNotificationClicked($event)" (userActionClicked)="onUserActionClicked($event)" (themeToggled)="onThemeToggled($event)"> <!-- Custom icon for the offer badge --> <span slot="start">⭐</span> <!-- Custom badge content --> Premium Features Available! </app-dashboard-header> <div class="demo-content"> <h2>Dashboard Content</h2> <p>This is a demo of the complex dashboard header component.</p> <div class="demo-actions"> <button (click)="toggleTheme()" class="demo-button"> Toggle Theme (Current: {{ darkMode() ? 'Dark' : 'Light' }}) </button> <button (click)="changeUser()" class="demo-button"> Change User </button> <button (click)="addNotification()" class="demo-button"> Add Notification </button> </div> <div class="demo-log"> <h3>Event Log:</h3> <div class="log-entries"> @for (entry of eventLog(); track $index) { <div class="log-entry">{{ entry }}</div> } </div> </div> </div> </div> ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/utils/element-helpers.ts: -------------------------------------------------------------------------------- ```typescript import type { TmplAstElement, ASTWithSource, } from '@angular/compiler' with { 'resolution-mode': 'import' }; /** * Extract bindings from a template element */ export function extractBindings(element: TmplAstElement) { return element.inputs.map((input) => ({ type: getBindingType(input.name), name: input.name, source: (input.value as ASTWithSource).source || '', sourceSpan: input.sourceSpan ? { start: input.sourceSpan.start.offset, end: input.sourceSpan.end.offset, file: input.sourceSpan.start.file.url, } : undefined, })); } /** * Extract attributes from a template element */ export function extractAttributes(element: TmplAstElement) { return element.attributes.map((attr) => ({ type: 'attribute' as const, name: attr.name, source: attr.value, })); } /** * Extract events from a template element */ export function extractEvents(element: TmplAstElement) { return element.outputs.map((output) => ({ name: output.name, handler: (output.handler as ASTWithSource).source || '', })); } /** * Determine the binding type based on the binding name */ function getBindingType( bindingName: string, ): 'class' | 'style' | 'property' | 'attribute' { if (bindingName.startsWith('class.')) return 'class'; if (bindingName.startsWith('style.')) return 'style'; if (bindingName.startsWith('attr.')) return 'attribute'; return 'property'; } ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component/get-deprecated-css-classes.tool.ts: -------------------------------------------------------------------------------- ```typescript import { ToolSchemaOptions } from '@push-based/models'; import { createHandler } from '../shared/utils/handler-helpers.js'; import { createComponentInputSchema, COMMON_ANNOTATIONS, } from '../shared/models/schema-helpers.js'; import { getDeprecatedCssClasses } from './utils/deprecated-css-helpers.js'; interface DeprecatedCssClassesOptions { componentName: string; } export const getDeprecatedCssClassesSchema: ToolSchemaOptions = { name: 'get-deprecated-css-classes', description: `List deprecated CSS classes for a DS component.`, inputSchema: createComponentInputSchema( 'The class name of the component to get deprecated classes for (e.g., DsButton)', ), annotations: { title: 'Get Deprecated CSS Classes', ...COMMON_ANNOTATIONS.readOnly, }, }; export const getDeprecatedCssClassesHandler = createHandler< DeprecatedCssClassesOptions, string[] >( getDeprecatedCssClassesSchema.name, async ({ componentName }, { cwd, deprecatedCssClassesPath }) => { if (!deprecatedCssClassesPath) { throw new Error( 'Missing ds.deprecatedCssClassesPath. Provide --ds.deprecatedCssClassesPath in mcp.json file.', ); } return await getDeprecatedCssClasses( componentName, deprecatedCssClassesPath, cwd, ); }, (result) => result, ); export const getDeprecatedCssClassesTools = [ { schema: getDeprecatedCssClassesSchema, handler: getDeprecatedCssClassesHandler, }, ]; ``` -------------------------------------------------------------------------------- /packages/shared/styles-ast-utils/src/lib/stylesheet.parse.unit.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, expect } from 'vitest'; import { parseStylesheet } from './stylesheet.parse.js'; import { Rule } from 'postcss'; describe('parseStylesheet', () => { it('should have line numbers starting from 1', () => { const result = parseStylesheet(`.btn{ color: red; }`, 'styles.css').root; const { source = {} as Exclude<Rule['source'], undefined> } = result.nodes.at(0) as Rule; const { start, end } = source; expect(start?.line).toBe(1); expect(start?.column).toBe(1); expect(end?.line).toBe(1); expect(end?.column).toBe(19); }); it('should have correct line number for starting line breaks', () => { const result = parseStylesheet( ` .btn{ color: red; }`, 'styles.css', ).root; const { source = {} as Exclude<Rule['source'], undefined> } = result?.nodes?.at(0) as Rule; const { start, end } = source; expect(start?.line).toBe(3); expect(start?.column).toBe(1); expect(end?.line).toBe(3); expect(end?.column).toBe(19); }); it('should have correct line number for spans', () => { const result = parseStylesheet( ` .btn{ color: red; }`, 'styles.css', ).root; const { source = {} as Exclude<Rule['source'], undefined> } = result?.nodes?.at(0) as Rule; const { start, end } = source; expect(start?.line).toBe(2); expect(start?.column).toBe(1); expect(end?.line).toBe(4); expect(end?.column).toBe(1); }); }); ``` -------------------------------------------------------------------------------- /packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/ds-coverage.audit.ts: -------------------------------------------------------------------------------- ```typescript import { getCompCoverageAuditOutput } from './utils.js'; import { AuditOutputs, Issue } from '@code-pushup/models'; import { ParsedComponent, visitComponentStyles, visitComponentTemplate, Asset, } from '@push-based/angular-ast-utils'; import type { ParsedTemplate } from '@angular/compiler' with { 'resolution-mode': 'import' }; import { ComponentReplacement } from './schema.js'; import { getClassUsageIssues } from './class-usage.utils.js'; import { getClassDefinitionIssues } from './class-definition.utils.js'; export function dsCompCoverageAuditOutputs( dsComponents: ComponentReplacement[], parsedComponents: ParsedComponent[], ): Promise<AuditOutputs> { return Promise.all( dsComponents.map(async (dsComponent) => { const allIssues = ( await Promise.all( parsedComponents.flatMap(async (component) => { return [ ...(await visitComponentTemplate( component, dsComponent, getClassUsageIssues as ( tokenReplacement: ComponentReplacement, asset: Asset<ParsedTemplate>, ) => Promise<Issue[]>, )), ...(await visitComponentStyles( component, dsComponent, getClassDefinitionIssues, )), ]; }), ) ).flat(); return getCompCoverageAuditOutput(dsComponent, allIssues); }), ); } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/styles/new-styles-4.scss: -------------------------------------------------------------------------------- ```scss // Example SCSS file with deprecated and non-deprecated classes .example-class-7 { text-align: center; } .modal { width: 100%; } .example-class-8 { height: 50px; } .card { box-shadow: 0 0 10px gray; } .example-class-21 { color: red; } .example-class-22 { background-color: green; } .example-class-23 { font-size: 18px; } .example-class-24 { padding: 10px; } .example-class-25 { margin: 5px; } .example-class-26 { border: 1px solid black; } .example-class-27 { text-align: left; } .example-class-28 { line-height: 2; } .example-class-29 { font-weight: normal; } .example-class-30 { display: inline-block; } .example-class-31 { width: 50%; } .example-class-32 { height: 100px; } .example-class-33 { overflow: auto; } .example-class-34 { position: relative; } .example-class-35 { top: 10px; } .example-class-36 { left: 20px; } .example-class-37 { right: 30px; } .example-class-38 { bottom: 40px; } .example-class-39 { z-index: 1; } .example-class-40 { opacity: 0.5; } .example-class-41 { visibility: hidden; } .example-class-42 { cursor: pointer; } .example-class-43 { transition: all 0.3s ease; } .example-class-44 { transform: rotate(45deg); } .example-class-45 { animation: fadeIn 1s; } .example-class-46 { box-sizing: border-box; } .example-class-47 { content: ''; } .example-class-48 { clip: rect(0, 0, 0, 0); } .example-class-49 { float: left; } .example-class-50 { clear: both; } ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/shared/utils/component-validation.ts: -------------------------------------------------------------------------------- ```typescript import { COMPONENT_REGEXES } from './regex-helpers.js'; /** * Validates that a component name is a valid Design System component name * Accepts both formats: "Button" and "DsButton" * @param componentName The component name to validate * @throws Error if the component name is invalid */ export function validateComponentName( componentName: unknown, ): asserts componentName is string { if ( !componentName || typeof componentName !== 'string' || !COMPONENT_REGEXES.isValidDsComponent(componentName) ) { throw new Error( 'Invalid component name. Must be a valid PascalCase string (e.g., "Button" or "DsButton").', ); } } /** * Converts a Design System component name to kebab case * @param componentName The component name (e.g., "DsButton" or "Button") * @returns The kebab case name (e.g., "button") */ export function componentNameToKebabCase(componentName: string): string { const kebabCase = COMPONENT_REGEXES.toKebabCase(componentName); if (!kebabCase?.trim()?.length) { throw new Error( 'Invalid component name. Must be a valid PascalCase string (e.g., "Button" or "DsButton").', ); } return kebabCase; } /** * Creates a tag name from a component name * @param componentName The component name (e.g., "DsButton" or "Button") * @returns The tag name (e.g., "ds-button") */ export function componentNameToTagName(componentName: string): string { return `ds-${componentNameToKebabCase(componentName)}`; } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/styles/new-styles-1.scss: -------------------------------------------------------------------------------- ```scss // Example SCSS file with deprecated and non-deprecated classes .example-class-1 { color: blue; } .pill-with-badge { background-color: red; } .example-class-2 { margin: 10px; } .btn-primary { padding: 5px; } .example-class-21 { color: red; } .example-class-22 { background-color: green; } .example-class-23 { font-size: 18px; } .example-class-24 { padding: 10px; } .example-class-25 { margin: 5px; } .example-class-26 { border: 1px solid black; } .example-class-27 { text-align: left; } .example-class-28 { line-height: 2; } .example-class-29 { font-weight: normal; } .example-class-30 { display: inline-block; } .example-class-31 { width: 50%; } .example-class-32 { height: 100px; } .example-class-33 { overflow: auto; } .example-class-34 { position: relative; } .example-class-35 { top: 10px; } .example-class-36 { left: 20px; } .example-class-37 { right: 30px; } .example-class-38 { bottom: 40px; } .example-class-39 { z-index: 1; } .example-class-40 { opacity: 0.5; } .example-class-41 { visibility: hidden; } .example-class-42 { cursor: pointer; } .example-class-43 { transition: all 0.3s ease; } .example-class-44 { transform: rotate(45deg); } .example-class-45 { animation: fadeIn 1s; } .example-class-46 { box-sizing: border-box; } .example-class-47 { content: ''; } .example-class-48 { clip: rect(0, 0, 0, 0); } .example-class-49 { float: left; } .example-class-50 { clear: both; } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/styles/new-styles-2.scss: -------------------------------------------------------------------------------- ```scss // Example SCSS file with deprecated and non-deprecated classes .example-class-3 { font-size: 14px; } .offer-badge { border: 1px solid black; } .example-class-4 { padding: 20px; } .nav-tabs { display: flex; } .example-class-21 { color: red; } .example-class-22 { background-color: green; } .example-class-23 { font-size: 18px; } .example-class-24 { padding: 10px; } .example-class-25 { margin: 5px; } .example-class-26 { border: 1px solid black; } .example-class-27 { text-align: left; } .example-class-28 { line-height: 2; } .example-class-29 { font-weight: normal; } .example-class-30 { display: inline-block; } .example-class-31 { width: 50%; } .example-class-32 { height: 100px; } .example-class-33 { overflow: auto; } .example-class-34 { position: relative; } .example-class-35 { top: 10px; } .example-class-36 { left: 20px; } .example-class-37 { right: 30px; } .example-class-38 { bottom: 40px; } .example-class-39 { z-index: 1; } .example-class-40 { opacity: 0.5; } .example-class-41 { visibility: hidden; } .example-class-42 { cursor: pointer; } .example-class-43 { transition: all 0.3s ease; } .example-class-44 { transform: rotate(45deg); } .example-class-45 { animation: fadeIn 1s; } .example-class-46 { box-sizing: border-box; } .example-class-47 { content: ''; } .example-class-48 { clip: rect(0, 0, 0, 0); } .example-class-49 { float: left; } .example-class-50 { clear: both; } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/styles/new-styles-3.scss: -------------------------------------------------------------------------------- ```scss // Example SCSS file with deprecated and non-deprecated classes .example-class-5 { background-color: yellow; } .tab-nav-item { color: green; } .example-class-6 { border-radius: 5px; } .legacy-button { font-weight: bold; } .example-class-21 { color: red; } .example-class-22 { background-color: green; } .example-class-23 { font-size: 18px; } .example-class-24 { padding: 10px; } .example-class-25 { margin: 5px; } .example-class-26 { border: 1px solid black; } .example-class-27 { text-align: left; } .example-class-28 { line-height: 2; } .example-class-29 { font-weight: normal; } .example-class-30 { display: inline-block; } .example-class-31 { width: 50%; } .example-class-32 { height: 100px; } .example-class-33 { overflow: auto; } .example-class-34 { position: relative; } .example-class-35 { top: 10px; } .example-class-36 { left: 20px; } .example-class-37 { right: 30px; } .example-class-38 { bottom: 40px; } .example-class-39 { z-index: 1; } .example-class-40 { opacity: 0.5; } .example-class-41 { visibility: hidden; } .example-class-42 { cursor: pointer; } .example-class-43 { transition: all 0.3s ease; } .example-class-44 { transform: rotate(45deg); } .example-class-45 { animation: fadeIn 1s; } .example-class-46 { box-sizing: border-box; } .example-class-47 { content: ''; } .example-class-48 { clip: rect(0, 0, 0, 0); } .example-class-49 { float: left; } .example-class-50 { clear: both; } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/styles/new-styles-6.scss: -------------------------------------------------------------------------------- ```scss // Example SCSS file with deprecated and non-deprecated classes .example-class-11 { font-family: Arial, sans-serif; } .divider { border-top: 1px solid #ccc; } .example-class-12 { padding-left: 15px; } .count { font-size: 12px; } .example-class-21 { color: red; } .example-class-22 { background-color: green; } .example-class-23 { font-size: 18px; } .example-class-24 { padding: 10px; } .example-class-25 { margin: 5px; } .example-class-26 { border: 1px solid black; } .example-class-27 { text-align: left; } .example-class-28 { line-height: 2; } .example-class-29 { font-weight: normal; } .example-class-30 { display: inline-block; } .example-class-31 { width: 50%; } .example-class-32 { height: 100px; } .example-class-33 { overflow: auto; } .example-class-34 { position: relative; } .example-class-35 { top: 10px; } .example-class-36 { left: 20px; } .example-class-37 { right: 30px; } .example-class-38 { bottom: 40px; } .example-class-39 { z-index: 1; } .example-class-40 { opacity: 0.5; } .example-class-41 { visibility: hidden; } .example-class-42 { cursor: pointer; } .example-class-43 { transition: all 0.3s ease; } .example-class-44 { transform: rotate(45deg); } .example-class-45 { animation: fadeIn 1s; } .example-class-46 { box-sizing: border-box; } .example-class-47 { content: ''; } .example-class-48 { clip: rect(0, 0, 0, 0); } .example-class-49 { float: left; } .example-class-50 { clear: both; } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/styles/new-styles-7.scss: -------------------------------------------------------------------------------- ```scss // Example SCSS file with deprecated and non-deprecated classes .example-class-13 { margin-right: 10px; } .badge-circle { border-radius: 50%; } .example-class-14 { color: #333; } .custom-control-checkbox { display: inline-block; } .example-class-21 { color: red; } .example-class-22 { background-color: green; } .example-class-23 { font-size: 18px; } .example-class-24 { padding: 10px; } .example-class-25 { margin: 5px; } .example-class-26 { border: 1px solid black; } .example-class-27 { text-align: left; } .example-class-28 { line-height: 2; } .example-class-29 { font-weight: normal; } .example-class-30 { display: inline-block; } .example-class-31 { width: 50%; } .example-class-32 { height: 100px; } .example-class-33 { overflow: auto; } .example-class-34 { position: relative; } .example-class-35 { top: 10px; } .example-class-36 { left: 20px; } .example-class-37 { right: 30px; } .example-class-38 { bottom: 40px; } .example-class-39 { z-index: 1; } .example-class-40 { opacity: 0.5; } .example-class-41 { visibility: hidden; } .example-class-42 { cursor: pointer; } .example-class-43 { transition: all 0.3s ease; } .example-class-44 { transform: rotate(45deg); } .example-class-45 { animation: fadeIn 1s; } .example-class-46 { box-sizing: border-box; } .example-class-47 { content: ''; } .example-class-48 { clip: rect(0, 0, 0, 0); } .example-class-49 { float: left; } .example-class-50 { clear: both; } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/styles/new-styles-5.scss: -------------------------------------------------------------------------------- ```scss // Example SCSS file with deprecated and non-deprecated classes .example-class-9 { line-height: 1.5; } .loading { animation: spin 2s linear infinite; } .example-class-10 { max-width: 200px; } .collapsible-container { overflow: hidden; } .example-class-21 { color: red; } .example-class-22 { background-color: green; } .example-class-23 { font-size: 18px; } .example-class-24 { padding: 10px; } .example-class-25 { margin: 5px; } .example-class-26 { border: 1px solid black; } .example-class-27 { text-align: left; } .example-class-28 { line-height: 2; } .example-class-29 { font-weight: normal; } .example-class-30 { display: inline-block; } .example-class-31 { width: 50%; } .example-class-32 { height: 100px; } .example-class-33 { overflow: auto; } .example-class-34 { position: relative; } .example-class-35 { top: 10px; } .example-class-36 { left: 20px; } .example-class-37 { right: 30px; } .example-class-38 { bottom: 40px; } .example-class-39 { z-index: 1; } .example-class-40 { opacity: 0.5; } .example-class-41 { visibility: hidden; } .example-class-42 { cursor: pointer; } .example-class-43 { transition: all 0.3s ease; } .example-class-44 { transform: rotate(45deg); } .example-class-45 { animation: fadeIn 1s; } .example-class-46 { box-sizing: border-box; } .example-class-47 { content: ''; } .example-class-48 { clip: rect(0, 0, 0, 0); } .example-class-49 { float: left; } .example-class-50 { clear: both; } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/styles/new-styles-8.scss: -------------------------------------------------------------------------------- ```scss // Example SCSS file with deprecated and non-deprecated classes .example-class-15 { background: #f0f0f0; } .custom-control-radio { margin-bottom: 5px; } .example-class-16 { border: 2px solid #000; } .form-control-tabs-segmented-v2 { display: block; } .example-class-21 { color: red; } .example-class-22 { background-color: green; } .example-class-23 { font-size: 18px; } .example-class-24 { padding: 10px; } .example-class-25 { margin: 5px; } .example-class-26 { border: 1px solid black; } .example-class-27 { text-align: left; } .example-class-28 { line-height: 2; } .example-class-29 { font-weight: normal; } .example-class-30 { display: inline-block; } .example-class-31 { width: 50%; } .example-class-32 { height: 100px; } .example-class-33 { overflow: auto; } .example-class-34 { position: relative; } .example-class-35 { top: 10px; } .example-class-36 { left: 20px; } .example-class-37 { right: 30px; } .example-class-38 { bottom: 40px; } .example-class-39 { z-index: 1; } .example-class-40 { opacity: 0.5; } .example-class-41 { visibility: hidden; } .example-class-42 { cursor: pointer; } .example-class-43 { transition: all 0.3s ease; } .example-class-44 { transform: rotate(45deg); } .example-class-45 { animation: fadeIn 1s; } .example-class-46 { box-sizing: border-box; } .example-class-47 { content: ''; } .example-class-48 { clip: rect(0, 0, 0, 0); } .example-class-49 { float: left; } .example-class-50 { clear: both; } ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-usage-graph/models/types.ts: -------------------------------------------------------------------------------- ```typescript import { SourceFileLocation } from '@code-pushup/models'; export interface DependencyInfo { path: string; type: DependencyInfoType; resolved: boolean; resolvedPath?: string; componentName?: string; sourceFile?: string; source?: SourceFileLocation; } export interface FileInfo { type: string; size: number; dependencies: DependencyInfo[]; lastModified: number; componentName?: string; isAngularComponent?: boolean; source?: string; // Optional - not stored in optimized version to save memory } export type ComponentUsageGraphResult = Record<string, FileInfo>; export interface BuildComponentUsageGraphOptions { cwd: string; directory: string; workspaceRoot?: string; } export interface ComponentMetadata { className: string; } export interface ComponentGroup { componentFile?: [string, FileInfo]; relatedFiles: [string, FileInfo][]; hasReverseDeps: boolean; } export type DependencyInfoType = | 'import' | 'require' | 'dynamic-import' | 'css-import' | 'asset' | 'external' | 'reverse-dependency'; export type FileExtension = | '.ts' | '.js' | '.jsx' | '.tsx' | '.css' | '.scss' | '.sass' | '.less' | '.html'; export type FileType = | 'typescript' | 'typescript-react' | 'javascript' | 'javascript-react' | 'css' | 'scss' | 'sass' | 'less' | 'template'; // Legacy aliases for backward compatibility export type DependencyGraphResult = ComponentUsageGraphResult; export type BuildDependencyGraphOptions = BuildComponentUsageGraphOptions; ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/styles/new-styles-10.scss: -------------------------------------------------------------------------------- ```scss // Example SCSS file with deprecated and non-deprecated classes .example-class-19 { border-bottom: 1px solid #ddd; } .form-control-tabs-segmented-v3 { padding: 5px; } .example-class-20 { margin-left: 20px; } .form-control-tabs-segmented-v4 { border-radius: 3px; } .example-class-21 { color: red; } .example-class-22 { background-color: green; } .example-class-23 { font-size: 18px; } .example-class-24 { padding: 10px; } .example-class-25 { margin: 5px; } .example-class-26 { border: 1px solid black; } .example-class-27 { text-align: left; } .example-class-28 { line-height: 2; } .example-class-29 { font-weight: normal; } .example-class-30 { display: inline-block; } .example-class-31 { width: 50%; } .example-class-32 { height: 100px; } .example-class-33 { overflow: auto; } .example-class-34 { position: relative; } .example-class-35 { top: 10px; } .example-class-36 { left: 20px; } .example-class-37 { right: 30px; } .example-class-38 { bottom: 40px; } .example-class-39 { z-index: 1; } .example-class-40 { opacity: 0.5; } .example-class-41 { visibility: hidden; } .example-class-42 { cursor: pointer; } .example-class-43 { transition: all 0.3s ease; } .example-class-44 { transform: rotate(45deg); } .example-class-45 { animation: fadeIn 1s; } .example-class-46 { box-sizing: border-box; } .example-class-47 { content: ''; } .example-class-48 { clip: rect(0, 0, 0, 0); } .example-class-49 { float: left; } .example-class-50 { clear: both; } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/styles/new-styles-9.scss: -------------------------------------------------------------------------------- ```scss // Example SCSS file with deprecated and non-deprecated classes .example-class-17 { padding-top: 10px; } .form-control-tabs-segmented-flex { flex-direction: row; } .example-class-18 { font-size: 16px; } .form-control-tabs-segmented-v2-dark { background-color: #333; } .example-class-21 { color: red; } .example-class-22 { background-color: green; } .example-class-23 { font-size: 18px; } .example-class-24 { padding: 10px; } .example-class-25 { margin: 5px; } .example-class-26 { border: 1px solid black; } .example-class-27 { text-align: left; } .example-class-28 { line-height: 2; } .example-class-29 { font-weight: normal; } .example-class-30 { display: inline-block; } .example-class-31 { width: 50%; } .example-class-32 { height: 100px; } .example-class-33 { overflow: auto; } .example-class-34 { position: relative; } .example-class-35 { top: 10px; } .example-class-36 { left: 20px; } .example-class-37 { right: 30px; } .example-class-38 { bottom: 40px; } .example-class-39 { z-index: 1; } .example-class-40 { opacity: 0.5; } .example-class-41 { visibility: hidden; } .example-class-42 { cursor: pointer; } .example-class-43 { transition: all 0.3s ease; } .example-class-44 { transform: rotate(45deg); } .example-class-45 { animation: fadeIn 1s; } .example-class-46 { box-sizing: border-box; } .example-class-47 { content: ''; } .example-class-48 { clip: rect(0, 0, 0, 0); } .example-class-49 { float: left; } .example-class-50 { clear: both; } ``` -------------------------------------------------------------------------------- /packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/utils.ts: -------------------------------------------------------------------------------- ```typescript import { Audit, AuditOutput, Issue } from '@code-pushup/models'; import { pluralize, slugify } from '@code-pushup/utils'; import { ComponentReplacement } from './schema.js'; /** * Creates a scored audit output. * @returns Audit output. * @param componentReplacements */ export function getCompUsageAudits( componentReplacements: ComponentReplacement[], ): Audit[] { return componentReplacements.map((comp) => ({ slug: getCompCoverageAuditSlug(comp), title: getCompCoverageAuditTitle(comp), description: getCompCoverageAuditDescription(comp), })); } /** * Creates a scored audit output. * @param componentName * @param issues * @returns Audit output. */ export function getCompCoverageAuditOutput( componentName: ComponentReplacement, issues: Issue[], ): AuditOutput { return { slug: getCompCoverageAuditSlug(componentName), displayValue: `${issues.length} ${pluralize('class', issues.length)} found`, score: issues.length === 0 ? 1 : 0, value: issues.length, details: { issues, }, }; } export function getCompCoverageAuditSlug({ componentName, }: ComponentReplacement): string { return slugify(`coverage-${componentName}`); } export function getCompCoverageAuditTitle({ componentName, }: ComponentReplacement): string { return `Usage coverage for ${componentName} component`; } export function getCompCoverageAuditDescription({ componentName, deprecatedCssClasses, }: ComponentReplacement): string { return `Coverage audit for ${componentName} component. Matching classes: ${deprecatedCssClasses.join( ', ', )}`; } ``` -------------------------------------------------------------------------------- /packages/shared/utils/src/lib/file/file.resolver.ts: -------------------------------------------------------------------------------- ```typescript import { existsSync } from 'fs'; import { readFile } from 'fs/promises'; import * as path from 'path'; export const fileResolverCache = new Map<string, Promise<string>>(); /** * Resolves a file content from the file system, caching the result * to avoid reading the same file multiple times. * * This function returns a Promise that resolves to the file content. * This is important to avoid reading the same file multiple times. * @param filePath */ export async function resolveFileCached(filePath: string): Promise<string> { const normalizedPath = path.normalize(filePath); if (!existsSync(normalizedPath)) { throw new Error(`File not found: ${normalizedPath}`); } if (fileResolverCache.has(normalizedPath)) { const cachedPromise = fileResolverCache.get(normalizedPath); if (cachedPromise) { return cachedPromise; } } const fileReadOperationPromise = resolveFile(filePath) .then((content) => { fileResolverCache.set(normalizedPath, Promise.resolve(content)); return content; }) .catch((ctx) => { fileResolverCache.delete(normalizedPath); throw ctx; }); fileResolverCache.set(normalizedPath, fileReadOperationPromise); return fileReadOperationPromise; } /** * Resolves a file content from the file system directly, bypassing any cache. * * @param filePath */ export async function resolveFile(filePath: string): Promise<string> { const normalizedPath = path.normalize(filePath); if (!existsSync(normalizedPath)) { throw new Error(`File not found: ${normalizedPath}`); } return readFile(normalizedPath, 'utf-8'); } ``` -------------------------------------------------------------------------------- /tools/nx-advanced-profile.js: -------------------------------------------------------------------------------- ```javascript import { fork } from 'node:child_process'; import { fileURLToPath } from 'node:url'; export async function nxRunWithPerfLogging( args, { verbose = false, noPatch = false, onData = () => {}, onTraceEvent = () => {}, onMetadata = () => {}, beforeExit = () => {}, } = {}, ) { const patch = !noPatch; const nxUrl = await import.meta.resolve('nx'); const nxPath = fileURLToPath(nxUrl); const profile = { metadata: {}, traceEvents: [], }; const forkArgs = [ nxPath, args, { stdio: ['pipe', 'pipe', 'pipe', 'ipc'], env: { ...process.env, NX_DAEMON: 'false', NX_CACHE: 'false', NX_PERF_LOGGING: 'true', }, // Preload the patch file so that it applies before NX is loaded. execArgv: patch ? ['--require', './tools/perf_hooks.patch.js'] : [], }, ]; if (verbose) { console.log('Forking NX with args:', forkArgs); } const child = fork(...forkArgs); child.stdout?.on('data', (data) => { const lines = data.toString().split('\n'); for (const line of lines) { onData(line); const res = line.split(':JSON:'); if (res.length === 2) { const [prop, jsonString] = res; const perfProfileEvent = JSON.parse(jsonString?.trim() || '{}'); if (prop === 'traceEvent') { onTraceEvent(perfProfileEvent); profile.traceEvents.push(perfProfileEvent); } if (prop === 'metadata') { onMetadata(perfProfileEvent); profile.metadata = perfProfileEvent; } } } }); child.on('close', () => { beforeExit(profile); }); } ``` -------------------------------------------------------------------------------- /packages/shared/utils/src/lib/file/default-export-loader.ts: -------------------------------------------------------------------------------- ```typescript import { pathToFileURL } from 'node:url'; /** * Dynamically imports an ES Module and extracts the default export. * * @param filePath - Absolute path to the ES module file to import * @returns The default export from the module * @throws Error if the module cannot be loaded or has no default export * * @example * ```typescript * const data = await loadDefaultExport('/path/to/config.js'); * ``` */ export async function loadDefaultExport<T = unknown>( filePath: string, ): Promise<T> { try { const fileUrl = pathToFileURL(filePath).toString(); // In test environments (Vitest), use native import to avoid transformation issues // In production (webpack/bundled), use Function constructor to preserve dynamic import const isTestEnv = typeof process !== 'undefined' && (process.env.NODE_ENV === 'test' || process.env.VITEST === 'true' || typeof (globalThis as Record<string, unknown>).vitest !== 'undefined'); const module = isTestEnv ? await import(fileUrl) : await new Function('url', 'return import(url)')(fileUrl); if (!('default' in module)) { throw new Error( `No default export found in module. Expected ES Module format:\n` + `export default [...]\n\n` + `Available exports: ${Object.keys(module).join(', ') || 'none'}`, ); } return module.default; } catch (ctx) { if ( ctx instanceof Error && ctx.message.includes('No default export found') ) { throw ctx; } throw new Error( `Failed to load module from ${filePath}: ${ctx instanceof Error ? ctx.message : String(ctx)}`, ); } } ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/base-analyzer.ts: -------------------------------------------------------------------------------- ```typescript import * as process from 'node:process'; import { getDeprecatedCssClasses } from '../../component/utils/deprecated-css-helpers.js'; import { validateComponentName } from '../utils/component-validation.js'; import { BaseViolationOptions, BaseViolationResult, ReportCoverageParams, } from './types.js'; import { analyzeProjectCoverage as collectFilesViolations } from './coverage-analyzer.js'; /** * Base analyzer for design system violations - shared logic between file and folder reporting */ export async function analyzeViolationsBase<T extends BaseViolationResult>( options: BaseViolationOptions, ): Promise<T> { const { cwd = process.cwd(), directory, componentName, deprecatedCssClassesPath, } = options; validateComponentName(componentName); if (!directory || typeof directory !== 'string') { throw new Error('Directory parameter is required and must be a string'); } process.chdir(cwd); if (!deprecatedCssClassesPath) { throw new Error( 'Missing ds.deprecatedCssClassesPath. Provide --ds.deprecatedCssClassesPath in mcp.json file.', ); } const deprecatedCssClasses = await getDeprecatedCssClasses( componentName, deprecatedCssClassesPath, cwd, ); const dsComponents = [ { componentName, deprecatedCssClasses, }, ]; const params: ReportCoverageParams = { cwd, returnRawData: true, directory, dsComponents, }; const result = await collectFilesViolations(params); if (!result.rawData?.rawPluginResult) { throw new Error('Failed to get raw plugin result for violation analysis'); } return result.rawData.rawPluginResult as T; } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/design-system/component-options.mjs: -------------------------------------------------------------------------------- ``` const dsComponents = [ { componentName: 'DsPill', deprecatedCssClasses: [ 'pill-with-badge', 'pill-with-badge-v2', 'sports-pill', ], }, { componentName: 'DsBadge', deprecatedCssClasses: ['offer-badge'], }, { componentName: 'DsTabsModule', deprecatedCssClasses: ['tab-nav', 'nav-tabs', 'tab-nav-item'], }, { componentName: 'DsButton', deprecatedCssClasses: ['btn', 'btn-primary', 'legacy-button'], }, { componentName: 'DsModal', deprecatedCssClasses: ['modal'], }, { componentName: 'DsCard', deprecatedCssClasses: ['card'], }, { componentName: 'DsLoadingSpinner', deprecatedCssClasses: ['loading', 'loading-v2', 'loading-v3'], }, { componentName: 'DsCardExpandable', deprecatedCssClasses: ['collapsible-container'], }, { componentName: 'DsDivider', deprecatedCssClasses: ['divider'], }, { componentName: 'DsNotificationBubble', deprecatedCssClasses: ['count', 'badge-circle'], }, { componentName: 'DsCheckbox', deprecatedCssClasses: ['custom-control-checkbox'], }, { componentName: 'DsRadioModule', deprecatedCssClasses: ['custom-control-radio'], }, { componentName: 'DsSegmentedControlModule', deprecatedCssClasses: [ 'form-control-tabs-segmented-v2', 'form-control-tabs-segmented-flex', 'form-control-tabs-segmented-v2-dark', 'form-control-tabs-segmented-v3', 'form-control-tabs-segmented-v4', 'form-control-tabs-segmented', ], }, { componentName: 'DsSwitch', deprecatedCssClasses: ['custom-control-switcher'], }, ]; export default dsComponents; ``` -------------------------------------------------------------------------------- /testing/vitest-setup/src/lib/fs-memfs.setup-file.ts: -------------------------------------------------------------------------------- ```typescript import { MockInstance, afterEach, beforeEach, vi, beforeAll, afterAll, } from 'vitest'; // Define the constant locally since cross-project imports cause build issues const MEMFS_VOLUME = '/memfs'; /** * Mocks the fs and fs/promises modules with memfs. */ type Memfs = typeof import('memfs'); vi.mock('fs', async () => { const memfs: Memfs = await vi.importActual('memfs'); return memfs.fs; }); vi.mock('fs/promises', async () => { const memfs: Memfs = await vi.importActual('memfs'); return memfs.fs.promises; }); /** * Mocks the current working directory to MEMFS_VOLUME. * This is useful when you use relative paths in your code * @type {MockInstance<[], string>} * * @example * - `readFile('./file.txt')` reads MEMFS_VOLUME/file.txt * - `readFile(join(process.cwd(), 'file.txt'))` reads MEMFS_VOLUME/file.txt * - `readFile('file.txt')` reads file.txt */ let cwdSpy: MockInstance; // This covers arrange blocks at the top of a "describe" block beforeAll(() => { cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(MEMFS_VOLUME); }); // Clear mock usage data in arrange blocks as well as usage of the API in each "it" block. // docs: https://vitest.dev/api/mock.html#mockclear beforeEach(() => { cwdSpy.mockClear(); }); // Restore mock implementation and usage data "it" block // Mock implementations remain if given. => vi.fn(impl).mockRestore() === vi.fn(impl) // docs: https://vitest.dev/api/mock.html#mockrestore afterEach(() => { cwdSpy.mockRestore(); }); // Restore the original implementation after all "describe" block in a file // docs: https://vitest.dev/api/mock.html#mockreset afterAll(() => { cwdSpy.mockReset(); }); ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/models/schema.ts: -------------------------------------------------------------------------------- ```typescript import { ToolSchemaOptions } from '@push-based/models'; import { COMMON_ANNOTATIONS } from '../../../shared/index.js'; /** * Schema for building component contracts */ export const buildComponentContractSchema: ToolSchemaOptions = { name: 'build_component_contract', description: "Generate a static surface contract for a component's template and SCSS.", inputSchema: { type: 'object', properties: { saveLocation: { type: 'string', description: 'Path where to save the contract file. Supports both absolute and relative paths.', }, templateFile: { type: 'string', description: 'Path to the component template file (.html). Optional - if not provided or if the path matches typescriptFile, will extract inline template from the component. Supports both absolute and relative paths.', }, styleFile: { type: 'string', description: 'Path to the component style file (.scss, .sass, .less, .css). Optional - if not provided or if the path matches typescriptFile, will extract inline styles from the component. Supports both absolute and relative paths.', }, typescriptFile: { type: 'string', description: 'Path to the TypeScript component file (.ts). Supports both absolute and relative paths.', }, dsComponentName: { type: 'string', description: 'The name of the design system component being used', default: '', }, }, required: ['saveLocation', 'typescriptFile'], }, annotations: { title: 'Build Component Contract', ...COMMON_ANNOTATIONS.readOnly, }, }; ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component/utils/metadata-helpers.ts: -------------------------------------------------------------------------------- ```typescript import { validateComponentName } from '../../shared/utils/component-validation.js'; import { getDeprecatedCssClasses } from './deprecated-css-helpers.js'; import { getComponentDocs } from './doc-helpers.js'; import { getComponentPathsInfo } from './paths-helpers.js'; export interface ComponentMetadataParams { componentName: string; storybookDocsRoot?: string; deprecatedCssClassesPath?: string; uiRoot?: string; cwd?: string; } export interface ComponentMetadataResult { sourcePath: string | null; importPath: string | null; tagName: string | null; api: string | null; overview: string | null; deprecatedCssClasses: string; } export async function analyzeComponentMetadata( params: ComponentMetadataParams, ): Promise<ComponentMetadataResult> { validateComponentName(params.componentName); const storybookDocsRoot = params.storybookDocsRoot || 'docs'; const deprecatedCssClassesPath = params.deprecatedCssClassesPath || 'deprecated-css-classes.js'; const uiRoot = params.uiRoot || 'libs/ui'; const cwd = params.cwd || process.cwd(); const documentation = getComponentDocs( params.componentName, storybookDocsRoot, ); const deprecatedCssClassesList = await getDeprecatedCssClasses( params.componentName, deprecatedCssClassesPath, cwd, ); const componentPaths = getComponentPathsInfo( params.componentName, uiRoot, cwd, ); return { sourcePath: componentPaths.srcPath, importPath: componentPaths.importPath, tagName: documentation.tagName, api: documentation.api, overview: documentation.overview, deprecatedCssClasses: JSON.stringify(deprecatedCssClassesList), }; } ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/project/utils/styles-report-helpers.ts: -------------------------------------------------------------------------------- ```typescript import * as fs from 'node:fs/promises'; import * as path from 'node:path'; import { parseStylesheet, visitEachChild } from '@push-based/styles-ast-utils'; import { findAllFiles } from '@push-based/utils'; import type { Rule } from 'postcss'; export interface StyleFileReport { filePath: string; foundClasses: { className: string; selector: string; lineNumber?: number; }[]; } const STYLE_EXT = new Set(['.css', '.scss', '.sass', '.less']); const isStyleFile = (f: string) => STYLE_EXT.has(path.extname(f).toLowerCase()); const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); export async function findStyleFiles(dir: string): Promise<string[]> { const files: string[] = []; for await (const file of findAllFiles(dir, isStyleFile)) { files.push(file); } return files; } export async function analyzeStyleFile( filePath: string, deprecated: string[], ): Promise<StyleFileReport> { const css = await fs.readFile(filePath, 'utf8'); const { root } = await parseStylesheet(css, filePath); const found: StyleFileReport['foundClasses'] = []; const master = new RegExp( `\\.(${deprecated.map(escapeRegex).join('|')})(?![\\w-])`, 'g', ); // Handle both Document_ and Root_ types if (root.type !== 'root') { return { filePath, foundClasses: found }; } visitEachChild(root, { visitRule(rule: Rule) { let match; while ((match = master.exec(rule.selector)) !== null) { found.push({ className: match[1], selector: rule.selector, lineNumber: rule.source?.start?.line, }); } master.lastIndex = 0; }, }); return { filePath, foundClasses: found }; } ``` -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- ``` import nx from '@nx/eslint-plugin'; import unicorn from 'eslint-plugin-unicorn'; import tseslint from 'typescript-eslint'; export default tseslint.config( // Base Nx configurations ...nx.configs['flat/base'], ...nx.configs['flat/typescript'], ...nx.configs['flat/javascript'], // Global ignores { ignores: [ '**/dist', '**/vite.config.*.timestamp*', '**/vitest.config.*.timestamp*', '**/mocks/**', '**/fixtures/**', '**/__snapshot__/**', '**/__snapshots__/**', '**/__tests__/**', '**/__mocks__/**', '**/test-fixtures/**', '**/e2e/fixtures/**', ], }, // Nx module boundaries and TypeScript rules { files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], rules: { '@nx/enforce-module-boundaries': [ 'error', { enforceBuildableLibDependency: true, allow: ['^.*/eslint(\\.base)?\\.config\\.[cm]?js$'], depConstraints: [ { sourceTag: '*', onlyDependOnLibsWithTags: ['*'], }, ], }, ], '@typescript-eslint/no-unused-vars': [ 'error', { argsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_', ignoreRestSiblings: true, }, ], }, }, // Unicorn plugin rules { files: [ '**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts', '**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs', ], plugins: { unicorn: unicorn, }, rules: { 'unicorn/prefer-top-level-await': 'error', 'unicorn/catch-error-name': ['error', { name: 'ctx' }], }, }, ); ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component/utils/deprecated-css-helpers.ts: -------------------------------------------------------------------------------- ```typescript import * as fs from 'fs'; import { resolveCrossPlatformPath } from '../../shared/utils/cross-platform-path.js'; import { loadDefaultExport } from '@push-based/utils'; export interface DeprecatedCssComponent { componentName: string; deprecatedCssClasses: string[]; } /** * Retrieves deprecated CSS classes for a specific component from a configuration file * @param componentName - The name of the component to get deprecated classes for * @param deprecatedCssClassesPath - Path to the file containing deprecated CSS classes configuration * @param cwd - Current working directory * @returns Array of deprecated CSS classes for the component * @throws Error if file not found, invalid format, or component not found */ export async function getDeprecatedCssClasses( componentName: string, deprecatedCssClassesPath: string, cwd: string, ): Promise<string[]> { if ( !deprecatedCssClassesPath || typeof deprecatedCssClassesPath !== 'string' ) { throw new Error('deprecatedCssClassesPath must be a string path'); } const absPath = resolveCrossPlatformPath(cwd, deprecatedCssClassesPath); if (!fs.existsSync(absPath)) { throw new Error(`File not found at deprecatedCssClassesPath: ${absPath}`); } const dsComponents = await loadDefaultExport<DeprecatedCssComponent[]>(absPath); if (!Array.isArray(dsComponents)) { throw new Error('Invalid export: expected dsComponents to be an array'); } const componentData = dsComponents.find( (item) => item.componentName === componentName, ); if (!componentData) { throw new Error( `No deprecated classes found for component: ${componentName}`, ); } return componentData.deprecatedCssClasses; } ``` -------------------------------------------------------------------------------- /testing/utils/src/lib/e2e-setup.ts: -------------------------------------------------------------------------------- ```typescript import path from 'node:path'; import fs from 'node:fs/promises'; export function getE2eAppProjectName(): string | undefined { const e2eProjectName = process.env['NX_TASK_TARGET_PROJECT'] ?? ''; if (e2eProjectName == null) { console.warn('NX_TASK_TARGET_PROJECT is not set.'); } return e2eProjectName ? `${e2eProjectName}-app` : undefined; } export async function setupE2eApp( fixtureProjectName: string, e2eFixtures?: string, ) { const targetProjectName = getE2eAppProjectName() ?? fixtureProjectName + '-e2e-app'; const fixture = path.join( __dirname, `../../../e2e/fixtures/${fixtureProjectName}`, ); const target = path.join( __dirname, `../../../e2e/__test__/${targetProjectName}`, ); try { await fs.stat(fixture); } catch (ctx) { console.warn( `Fixture folder not found. Did you change the file or move it? Error: ${ (ctx as Error).message }`, ); return; } // copy fixtures folder await fs.rm(target, { recursive: true, force: true }); await fs.cp(fixture, target, { recursive: true, force: true, filter(source: string, _: string): boolean | Promise<boolean> { return !source.includes('node_modules') && !source.includes('dist'); }, }); // adjust package.json#nx to new location and rename project const packageJson = ( await fs.readFile(path.join(target, 'package.json'), 'utf-8') ).toString(); await fs.writeFile( path.join(target, 'package.json'), packageJson .replaceAll('fixtures', '__test__') .replaceAll(fixtureProjectName, targetProjectName), ); // add e2e fixtures if (e2eFixtures) { await fs.cp(e2eFixtures, target, { recursive: true, force: true, }); } } ``` -------------------------------------------------------------------------------- /packages/shared/utils/src/lib/utils.ts: -------------------------------------------------------------------------------- ```typescript import { CliArgsObject, ArgumentValue } from '@push-based/models'; /** * Converts an object with different types of values into an array of command-line arguments. * * @example * const args = objectToCliArgs({ * _: ['node', 'index.js'], // node index.js * name: 'Juanita', // --name=Juanita * formats: ['json', 'md'] // --format=json --format=md * }); */ export function objectToCliArgs< T extends object = Record<string, ArgumentValue>, >(params?: CliArgsObject<T>): string[] { if (!params) { return []; } return Object.entries(params).flatMap(([key, value]) => { // process/file/script if (key === '_') { return Array.isArray(value) ? value : [`${value}`]; } const prefix = key.length === 1 ? '-' : '--'; // "-*" arguments (shorthands) if (Array.isArray(value)) { return value.map((v) => `${prefix}${key}="${v}"`); } // "--*" arguments ========== if (Array.isArray(value)) { return value.map((v) => `${prefix}${key}="${v}"`); } if (typeof value === 'object') { return Object.entries(value as Record<string, ArgumentValue>).flatMap( // transform nested objects to the dot notation `key.subkey` ([k, v]) => objectToCliArgs({ [`${key}.${k}`]: v }), ); } if (typeof value === 'string') { return [`${prefix}${key}="${value}"`]; } if (typeof value === 'number') { return [`${prefix}${key}=${value}`]; } if (typeof value === 'boolean') { return [`${prefix}${value ? '' : 'no-'}${key}`]; } throw new Error(`Unsupported type ${typeof value} for key ${key}`); }); } export function calcDuration(start: number, stop?: number): number { return Math.round((stop ?? performance.now()) - start); } ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/report-violations/report-violations.tool.ts: -------------------------------------------------------------------------------- ```typescript import { createHandler, BaseHandlerOptions, RESULT_FORMATTERS, } from '../shared/utils/handler-helpers.js'; import { createViolationReportingSchema, COMMON_ANNOTATIONS, } from '../shared/models/schema-helpers.js'; import { analyzeViolationsBase } from '../shared/violation-analysis/base-analyzer.js'; import { formatViolations } from '../shared/violation-analysis/formatters.js'; import { ViolationResult } from './models/types.js'; interface ReportViolationsOptions extends BaseHandlerOptions { directory: string; componentName: string; groupBy?: 'file' | 'folder'; } export const reportViolationsSchema = { name: 'report-violations', description: `Report deprecated DS CSS usage in a directory with configurable grouping format.`, inputSchema: createViolationReportingSchema(), annotations: { title: 'Report Violations', ...COMMON_ANNOTATIONS.readOnly, }, }; export const reportViolationsHandler = createHandler< ReportViolationsOptions, string[] >( reportViolationsSchema.name, async (params) => { // Default to 'file' grouping if not specified const groupBy = params.groupBy || 'file'; const result = await analyzeViolationsBase<ViolationResult>(params); const formattedContent = formatViolations(result, params.directory, { groupBy, }); // Extract text content from the formatted violations const violationLines = formattedContent.map((item) => { if (item.type === 'text') { return item.text; } return String(item); }); return violationLines; }, (result) => RESULT_FORMATTERS.list(result, 'Design System Violations:'), ); export const reportViolationsTools = [ { schema: reportViolationsSchema, handler: reportViolationsHandler, }, ]; ``` -------------------------------------------------------------------------------- /packages/shared/typescript-ast-utils/ai/API.md: -------------------------------------------------------------------------------- ```markdown # TypeScript AST Utils Comprehensive **TypeScript AST manipulation utilities** for working with TypeScript compiler API decorators, nodes, and source file analysis. ## Minimal usage ```ts import { isComponentDecorator, getDecorators, removeQuotes, } from '@push-based/typescript-ast-utils'; import * as ts from 'typescript'; // Check if a decorator is a Component decorator const isComponent = isComponentDecorator(decorator); // Get all decorators from a node const decorators = getDecorators(classNode); // Remove quotes from a string literal node const cleanText = removeQuotes(stringNode, sourceFile); ``` ## Key Features - **Decorator Analysis**: Utilities for identifying and working with TypeScript decorators - **Node Inspection**: Functions to safely extract decorators from AST nodes - **String Processing**: Tools for cleaning quoted strings from AST nodes - **Type Guards**: Type-safe functions for checking node properties - **Cross-Version Compatibility**: Handles different TypeScript compiler API versions ## Use Cases - **Angular Component Analysis**: Identify `@Component` decorators in Angular applications - **AST Traversal**: Safely navigate TypeScript abstract syntax trees - **Code Analysis Tools**: Build static analysis tools for TypeScript codebases - **Decorator Processing**: Extract and process decorator metadata - **Source Code Transformation**: Clean and manipulate string literals from source files ## Documentation map | Doc | What you'll find | | ------------------------------ | ------------------------------------------- | | [FUNCTIONS.md](./FUNCTIONS.md) | A–Z quick reference for every public symbol | | [EXAMPLES.md](./EXAMPLES.md) | Runnable scenarios with expected output | ``` ``` ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/project/get-project-dependencies.tool.ts: -------------------------------------------------------------------------------- ```typescript import { ToolSchemaOptions } from '@push-based/models'; import { createHandler, BaseHandlerOptions, } from '../shared/utils/handler-helpers.js'; import { createProjectAnalysisSchema, COMMON_ANNOTATIONS, } from '../shared/models/schema-helpers.js'; import { analyzeProjectDependencies } from './utils/dependencies-helpers.js'; import { validateComponentName } from '../shared/utils/component-validation.js'; interface ProjectDependenciesOptions extends BaseHandlerOptions { directory: string; componentName?: string; workspaceRoot?: string; uiRoot?: string; } export const getProjectDependenciesSchema: ToolSchemaOptions = { name: 'get-project-dependencies', description: ` Analyze project dependencies and detect if library is buildable/publishable. Checks for peer dependencies and validates import paths for DS components. `, inputSchema: createProjectAnalysisSchema({ componentName: { type: 'string', description: 'Optional component name to validate import path for (e.g., DsButton)', }, }), annotations: { title: 'Get Project Dependencies', ...COMMON_ANNOTATIONS.readOnly, }, }; export const getProjectDependenciesHandler = createHandler< ProjectDependenciesOptions, any >( getProjectDependenciesSchema.name, async (params, { cwd, workspaceRoot, uiRoot }) => { const { directory, componentName } = params; if (componentName) { validateComponentName(componentName); } return await analyzeProjectDependencies( cwd, directory, componentName, workspaceRoot, uiRoot, ); }, (result) => [JSON.stringify(result, null, 2)], ); export const getProjectDependenciesTools = [ { schema: getProjectDependenciesSchema, handler: getProjectDependenciesHandler, }, ]; ``` -------------------------------------------------------------------------------- /packages/shared/utils/src/lib/format-command-log.integration.test.ts: -------------------------------------------------------------------------------- ```typescript import path from 'node:path'; import { describe, expect, it } from 'vitest'; import { removeColorCodes } from '@push-based/testing-utils'; import { formatCommandLog } from './format-command-log.js'; describe('formatCommandLog', () => { it('should format simple command', () => { const result = removeColorCodes( formatCommandLog('npx', ['command', '--verbose']), ); expect(result).toBe('$ npx command --verbose'); }); it('should format simple command with explicit process.cwd()', () => { const result = removeColorCodes( formatCommandLog('npx', ['command', '--verbose'], process.cwd()), ); expect(result).toBe('$ npx command --verbose'); }); it('should format simple command with relative cwd', () => { const result = removeColorCodes( formatCommandLog('npx', ['command', '--verbose'], './wololo'), ); expect(result).toBe(`wololo $ npx command --verbose`); }); it('should format simple command with absolute non-current path converted to relative', () => { const result = removeColorCodes( formatCommandLog( 'npx', ['command', '--verbose'], path.join(process.cwd(), 'tmp'), ), ); expect(result).toBe('tmp $ npx command --verbose'); }); it('should format simple command with relative cwd in parent folder', () => { const result = removeColorCodes( formatCommandLog('npx', ['command', '--verbose'], '..'), ); expect(result).toBe(`.. $ npx command --verbose`); }); it('should format simple command using relative path to parent directory', () => { const result = removeColorCodes( formatCommandLog( 'npx', ['command', '--verbose'], path.dirname(process.cwd()), ), ); expect(result).toBe('.. $ npx command --verbose'); }); }); ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/group-3/lazy-loader-3.component.ts: -------------------------------------------------------------------------------- ```typescript import { Component, ViewContainerRef, ComponentRef, OnInit, } from '@angular/core'; @Component({ selector: 'app-lazy-loader', template: ` <div> <h2>Lazy Component Loader</h2> <button (click)="loadComponent()" [disabled]="isLoading"> {{ isLoading ? 'Loading...' : 'Load External Assets Component' }} </button> <div #dynamicContainer></div> </div> `, styles: [ ` button { padding: 10px 20px; margin: 10px 0; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; } button:disabled { background-color: #6c757d; cursor: not-allowed; } `, ], }) export class LazyLoaderComponent3 implements OnInit { isLoading = false; private componentRef?: ComponentRef<any>; constructor(private viewContainerRef: ViewContainerRef) {} ngOnInit() { console.log('LazyLoaderComponent initialized'); } async loadComponent() { if (this.isLoading || this.componentRef) { return; } this.isLoading = true; try { // Dynamic import statement - this is the key part for lazy loading const { BadMixedExternalAssetsComponent3 } = await import( './bad-mixed-external-assets-3.component' ); // Clear the container this.viewContainerRef.clear(); // Create the component dynamically this.componentRef = this.viewContainerRef.createComponent( BadMixedExternalAssetsComponent3 ); console.log('Component loaded successfully'); } catch (error) { console.error('Failed to load component:', error); } finally { this.isLoading = false; } } ngOnDestroy() { if (this.componentRef) { this.componentRef.destroy(); } } } ``` -------------------------------------------------------------------------------- /packages/shared/angular-ast-utils/src/lib/decorator-config.visitor.spec.ts: -------------------------------------------------------------------------------- ```typescript import { classDecoratorVisitor } from './decorator-config.visitor.js'; import * as ts from 'typescript'; describe('DecoratorConfigVisitor', () => { it.skip('should not find class when it is not a class-binding', async () => { const sourceCode = ` class Example { method() {} } function greet() { console.log('Hello'); } let x = 123; `; const sourceFile = ts.createSourceFile( 'example.ts', sourceCode, ts.ScriptTarget.Latest, true, ); const visitor = await classDecoratorVisitor({ sourceFile }); ts.visitEachChild(sourceFile, visitor, undefined); expect(visitor.components).toEqual([]); }); it('should not find class when it is not a class-binding', async () => { const sourceCode = ` import { Component } from '@angular/core'; import { RouterOutlet } from '@angular/router'; @Component({ selector: 'app-root', imports: [RouterOutlet], templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'minimal'; } `; const sourceFile = ts.createSourceFile( 'example.ts', sourceCode, ts.ScriptTarget.Latest, true, ); const visitor = await classDecoratorVisitor({ sourceFile }); ts.visitEachChild(sourceFile, visitor, undefined); expect(visitor.components).toStrictEqual([ expect.objectContaining({ className: 'AppComponent', fileName: 'example.ts', startLine: 4, selector: 'app-root', imports: '[RouterOutlet]', styleUrls: [ expect.objectContaining({ startLine: 8, filePath: 'example.ts', }), ], templateUrl: expect.objectContaining({ startLine: 7, filePath: 'example.ts', }), }), ]); }); }); ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/validation/ds-components-file.validation.ts: -------------------------------------------------------------------------------- ```typescript import * as path from 'path'; import { AngularMcpServerOptions } from './angular-mcp-server-options.schema.js'; import { DsComponentsArraySchema } from './ds-components.schema.js'; import { loadDefaultExport } from '@push-based/utils'; export async function validateDeprecatedCssClassesFile( config: AngularMcpServerOptions, ): Promise<void> { const relPath = config.ds.deprecatedCssClassesPath; if (!relPath) { // Optional parameter not provided; nothing to validate return; } const deprecatedCssClassesAbsPath = path.resolve( config.workspaceRoot, relPath, ); const dsComponents = await loadDefaultExport(deprecatedCssClassesAbsPath); const validation = DsComponentsArraySchema.safeParse(dsComponents); if (!validation.success) { const actualType = Array.isArray(dsComponents) ? 'array' : typeof dsComponents; const exportedValue = dsComponents === undefined ? 'undefined' : dsComponents === null ? 'null' : JSON.stringify(dsComponents, null, 2).substring(0, 100) + (JSON.stringify(dsComponents).length > 100 ? '...' : ''); throw new Error( `Invalid deprecated CSS classes configuration format in: ${deprecatedCssClassesAbsPath}\n\n` + `Expected: Array of component objects\n` + `Received: ${actualType}\n` + `Value: ${exportedValue}\n\n` + `Fix options:\n` + `1. ES Module format:\n` + ` export default [\n` + ` { componentName: 'DsButton', deprecatedCssClasses: ['btn'] }\n` + ` ];\n\n` + `2. CommonJS format:\n` + ` module.exports = {\n` + ` dsComponents: [{ componentName: 'DsButton', deprecatedCssClasses: ['btn'] }]\n` + ` };\n\n` + `Schema errors: ${JSON.stringify(validation.error.format(), null, 2)}`, ); } } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/bad-mixed.component.ts: -------------------------------------------------------------------------------- ```typescript import { Component } from '@angular/core'; @Component({ selector: 'app-mixed-styles', template: ` <!-- ✅ Good: Using DSButton --> <ds-button>Good Button</ds-button> <!-- ❌ Bad: Legacy button class --> <button class="btn btn-primary">Bad Button</button> <!-- ✅ Good: Using DSModal --> <ds-modal [open]="true"> <p>Good Modal Content</p> </ds-modal> <!-- ❌ Bad: Custom modal with legacy styles --> <div class="modal"> <div class="modal-content"> <h2>Bad Modal</h2> <p>This is a legacy modal.</p> </div> </div> <!-- ✅ Good: DSProgressBar --> <ds-progress-bar [value]="50"></ds-progress-bar> <!-- ❌ Bad: Manually styled progress bar --> <div class="progress-bar"> <div class="progress" style="width: 50%;"></div> </div> <!-- ✅ Good: DSDropdown --> <ds-dropdown [options]="['Option 1', 'Option 2']"></ds-dropdown> <!-- ❌ Bad: Legacy dropdown --> <select class="dropdown"> <option>Option 1</option> <option>Option 2</option> </select> <!-- ✅ Good: Using DSAlert --> <ds-alert type="error"> Good Alert </ds-alert> <!-- ❌ Bad: Manually styled alert --> <div class="alert alert-danger">Bad Alert</div> <!-- ✅ Good: Using DSTooltip --> <ds-tooltip content="Good tooltip">Hover me</ds-tooltip> <!-- ❌ Bad: Legacy tooltip --> <div class="tooltip">Bad tooltip</div> <!-- ✅ Good: Using DSBreadcrumb --> <ds-breadcrumb> <ds-breadcrumb-item>Home</ds-breadcrumb-item> <ds-breadcrumb-item>Products</ds-breadcrumb-item> <ds-breadcrumb-item>Details</ds-breadcrumb-item> </ds-breadcrumb> <!-- ❌ Bad: Manually created breadcrumb --> <nav class="breadcrumb"> <span>Home</span> / <span>Products</span> / <span>Details</span> </nav> `, }) export class MixedStylesComponent {} ``` -------------------------------------------------------------------------------- /packages/shared/ds-component-coverage/mocks/fixtures/e2e/demo/src/sub-folder-1/sub-folder-2/bad-mixed.component.ts: -------------------------------------------------------------------------------- ```typescript import { Component } from '@angular/core'; @Component({ selector: 'app-mixed-styles', template: ` <!-- ✅ Good: Using DSButton --> <ds-button>Good Button</ds-button> <!-- ❌ Bad: Legacy button class --> <button class="btn btn-primary">Bad Button</button> <!-- ✅ Good: Using DSModal --> <ds-modal [open]="true"> <p>Good Modal Content</p> </ds-modal> <!-- ❌ Bad: Custom modal with legacy styles --> <div class="modal"> <div class="modal-content"> <h2>Bad Modal</h2> <p>This is a legacy modal.</p> </div> </div> <!-- ✅ Good: DSProgressBar --> <ds-progress-bar [value]="50"></ds-progress-bar> <!-- ❌ Bad: Manually styled progress bar --> <div class="progress-bar"> <div class="progress" style="width: 50%;"></div> </div> <!-- ✅ Good: DSDropdown --> <ds-dropdown [options]="['Option 1', 'Option 2']"></ds-dropdown> <!-- ❌ Bad: Legacy dropdown --> <select class="dropdown"> <option>Option 1</option> <option>Option 2</option> </select> <!-- ✅ Good: Using DSAlert --> <ds-alert type="error"> Good Alert </ds-alert> <!-- ❌ Bad: Manually styled alert --> <div class="alert alert-danger">Bad Alert</div> <!-- ✅ Good: Using DSTooltip --> <ds-tooltip content="Good tooltip">Hover me</ds-tooltip> <!-- ❌ Bad: Legacy tooltip --> <div class="tooltip">Bad tooltip</div> <!-- ✅ Good: Using DSBreadcrumb --> <ds-breadcrumb> <ds-breadcrumb-item>Home</ds-breadcrumb-item> <ds-breadcrumb-item>Products</ds-breadcrumb-item> <ds-breadcrumb-item>Details</ds-breadcrumb-item> </ds-breadcrumb> <!-- ❌ Bad: Manually created breadcrumb --> <nav class="breadcrumb"> <span>Home</span> / <span>Products</span> / <span>Details</span> </nav> `, }) export class MixedStylesComponent {} ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/group-1/bad-mixed-1.component.ts: -------------------------------------------------------------------------------- ```typescript import { Component } from '@angular/core'; @Component({ selector: 'app-mixed-styles', template: ` <!-- ✅ Good: Using DSButton --> <ds-button>Good Button</ds-button> <!-- ❌ Bad: Legacy button class --> <button class="btn btn-primary">Bad Button</button> <!-- ✅ Good: Using DSModal --> <ds-modal [open]="true"> <p>Good Modal Content</p> </ds-modal> <!-- ❌ Bad: Custom modal with legacy styles --> <div class="modal"> <div class="modal-content"> <h2>Bad Modal</h2> <p>This is a legacy modal.</p> </div> </div> <!-- ✅ Good: DSProgressBar --> <ds-progress-bar [value]="50"></ds-progress-bar> <!-- ❌ Bad: Manually styled progress bar --> <div class="progress-bar"> <div class="progress" style="width: 50%;"></div> </div> <!-- ✅ Good: DSDropdown --> <ds-dropdown [options]="['Option 1', 'Option 2']"></ds-dropdown> <!-- ❌ Bad: Legacy dropdown --> <select class="dropdown"> <option>Option 1</option> <option>Option 2</option> </select> <!-- ✅ Good: Using DSAlert --> <ds-alert type="error"> Good Alert </ds-alert> <!-- ❌ Bad: Manually styled alert --> <div class="alert alert-danger">Bad Alert</div> <!-- ✅ Good: Using DSTooltip --> <ds-tooltip content="Good tooltip">Hover me</ds-tooltip> <!-- ❌ Bad: Legacy tooltip --> <div class="tooltip">Bad tooltip</div> <!-- ✅ Good: Using DSBreadcrumb --> <ds-breadcrumb> <ds-breadcrumb-item>Home</ds-breadcrumb-item> <ds-breadcrumb-item>Products</ds-breadcrumb-item> <ds-breadcrumb-item>Details</ds-breadcrumb-item> </ds-breadcrumb> <!-- ❌ Bad: Manually created breadcrumb --> <nav class="breadcrumb"> <span>Home</span> / <span>Products</span> / <span>Details</span> </nav> `, }) export class MixedStylesComponent1 {} ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/group-2/bad-mixed-2.component.ts: -------------------------------------------------------------------------------- ```typescript import { Component } from '@angular/core'; @Component({ selector: 'app-mixed-styles', template: ` <!-- ✅ Good: Using DSButton --> <ds-button>Good Button</ds-button> <!-- ❌ Bad: Legacy button class --> <button class="btn btn-primary">Bad Button</button> <!-- ✅ Good: Using DSModal --> <ds-modal [open]="true"> <p>Good Modal Content</p> </ds-modal> <!-- ❌ Bad: Custom modal with legacy styles --> <div class="modal"> <div class="modal-content"> <h2>Bad Modal</h2> <p>This is a legacy modal.</p> </div> </div> <!-- ✅ Good: DSProgressBar --> <ds-progress-bar [value]="50"></ds-progress-bar> <!-- ❌ Bad: Manually styled progress bar --> <div class="progress-bar"> <div class="progress" style="width: 50%;"></div> </div> <!-- ✅ Good: DSDropdown --> <ds-dropdown [options]="['Option 1', 'Option 2']"></ds-dropdown> <!-- ❌ Bad: Legacy dropdown --> <select class="dropdown"> <option>Option 1</option> <option>Option 2</option> </select> <!-- ✅ Good: Using DSAlert --> <ds-alert type="error"> Good Alert </ds-alert> <!-- ❌ Bad: Manually styled alert --> <div class="alert alert-danger">Bad Alert</div> <!-- ✅ Good: Using DSTooltip --> <ds-tooltip content="Good tooltip">Hover me</ds-tooltip> <!-- ❌ Bad: Legacy tooltip --> <div class="tooltip">Bad tooltip</div> <!-- ✅ Good: Using DSBreadcrumb --> <ds-breadcrumb> <ds-breadcrumb-item>Home</ds-breadcrumb-item> <ds-breadcrumb-item>Products</ds-breadcrumb-item> <ds-breadcrumb-item>Details</ds-breadcrumb-item> </ds-breadcrumb> <!-- ❌ Bad: Manually created breadcrumb --> <nav class="breadcrumb"> <span>Home</span> / <span>Products</span> / <span>Details</span> </nav> `, }) export class MixedStylesComponent2 {} ``` -------------------------------------------------------------------------------- /packages/shared/styles-ast-utils/src/lib/stylesheet.walk.ts: -------------------------------------------------------------------------------- ```typescript import { Root, Rule } from 'postcss'; import { CssAstVisitor } from './stylesheet.visitor.js'; import { NodeType } from './types.js'; /** * Single function that traverses the AST, calling * specialized visitor methods as it encounters each node type. */ export function visitEachChild<T>(root: Root, visitor: CssAstVisitor<T>) { visitor.visitRoot?.(root); root.walk((node) => { const visitMethodName = `visit${ node.type[0].toUpperCase() + node.type.slice(1) }` as keyof CssAstVisitor<T>; const visitMethod = visitor[visitMethodName] as | ((node: NodeType<typeof visitMethodName>) => void) | undefined; visitMethod?.(node as NodeType<typeof visitMethodName>); }); } export function visitStyleSheet<T>(root: Root, visitor: CssAstVisitor<T>) { for (const node of root.nodes) { switch (node.type) { case 'rule': visitor?.visitRule?.(node); break; case 'atrule': visitor?.visitAtRule?.(node); break; case 'decl': throw new Error('visit declaration not implemented'); // visitor?.visitDeclaration?.(node); case 'comment': visitor?.visitComment?.(node); break; default: throw new Error(`Unknown node type: ${(node as Root).type}`); } } } export function visitEachStyleNode<T>( nodes: Root['nodes'], visitor: CssAstVisitor<T>, ) { for (const node of nodes) { switch (node.type) { case 'rule': visitor?.visitRule?.(node); visitEachStyleNode((node as Rule).nodes, visitor); break; case 'atrule': visitor?.visitAtRule?.(node); break; case 'decl': visitor?.visitDecl?.(node); break; case 'comment': visitor?.visitComment?.(node); break; default: throw new Error(`Unknown node type: ${(node as Root).type}`); } } } ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/utils/inline-styles.collector.ts: -------------------------------------------------------------------------------- ```typescript import { parseStylesheet, visitEachChild } from '@push-based/styles-ast-utils'; import { selectorMatches } from './css-match.js'; import type { StyleDeclarations, DomStructure, } from '../../shared/models/types.js'; import type { Declaration, Rule } from 'postcss'; import type { ParsedComponent } from '@push-based/angular-ast-utils'; /** * Collect style rules declared inline via the `styles` property of an * `@Component` decorator and map them to the DOM snapshot that comes from the * template. */ export async function collectInlineStyles( component: ParsedComponent, dom: DomStructure, ): Promise<StyleDeclarations> { const styles: StyleDeclarations = { // Inline styles logically live in the component TS file sourceFile: component.fileName, rules: {}, }; if (!component.styles || component.styles.length === 0) { return styles; } // Combine all inline style strings into one CSS blob const cssText = ( await Promise.all(component.styles.map((asset) => asset.parse())) ) .map((root) => root.toString()) .join('\n'); if (!cssText.trim()) { return styles; } const parsed = parseStylesheet(cssText, component.fileName); if (parsed.root.type !== 'root') { return styles; } visitEachChild(parsed.root, { visitRule: (rule: Rule) => { const properties: Record<string, string> = {}; rule.walkDecls?.((decl: Declaration) => { properties[decl.prop] = decl.value; }); styles.rules[rule.selector] = { appliesTo: findMatchingDomElements(rule.selector, dom), properties, }; }, }); return styles; } function findMatchingDomElements( cssSelector: string, dom: DomStructure, ): string[] { return Object.entries(dom) .filter(([domKey, element]) => selectorMatches(cssSelector, domKey, element), ) .map(([domKey]) => domKey); } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/bad-mixed-not-standalone.component.ts: -------------------------------------------------------------------------------- ```typescript import { Component } from '@angular/core'; @Component({ selector: 'app-mixed-styles', standalone: false, template: ` <!-- ✅ Good: Using DSButton --> <ds-button>Good Button</ds-button> <!-- ❌ Bad: Legacy button class --> <button class="btn btn-primary">Bad Button</button> <!-- ✅ Good: Using DSModal --> <ds-modal [open]="true"> <p>Good Modal Content</p> </ds-modal> <!-- ❌ Bad: Custom modal with legacy styles --> <div class="modal"> <div class="modal-content"> <h2>Bad Modal</h2> <p>This is a legacy modal.</p> </div> </div> <!-- ✅ Good: DSProgressBar --> <ds-progress-bar [value]="50"></ds-progress-bar> <!-- ❌ Bad: Manually styled progress bar --> <div class="progress-bar"> <div class="progress" style="width: 50%;"></div> </div> <!-- ✅ Good: DSDropdown --> <ds-dropdown [options]="['Option 1', 'Option 2']"></ds-dropdown> <!-- ❌ Bad: Legacy dropdown --> <select class="dropdown"> <option>Option 1</option> <option>Option 2</option> </select> <!-- ✅ Good: Using DSAlert --> <ds-alert type="error"> Good Alert </ds-alert> <!-- ❌ Bad: Manually styled alert --> <div class="alert alert-danger">Bad Alert</div> <!-- ✅ Good: Using DSTooltip --> <ds-tooltip content="Good tooltip">Hover me</ds-tooltip> <!-- ❌ Bad: Legacy tooltip --> <div class="tooltip">Bad tooltip</div> <!-- ✅ Good: Using DSBreadcrumb --> <ds-breadcrumb> <ds-breadcrumb-item>Home</ds-breadcrumb-item> <ds-breadcrumb-item>Products</ds-breadcrumb-item> <ds-breadcrumb-item>Details</ds-breadcrumb-item> </ds-breadcrumb> <!-- ❌ Bad: Manually created breadcrumb --> <nav class="breadcrumb"> <span>Home</span> / <span>Products</span> / <span>Details</span> </nav> `, }) export class MixedStylesNotStandaloneComponent {} ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/group-1/bad-mixed-not-standalone-1.component.ts: -------------------------------------------------------------------------------- ```typescript import { Component } from '@angular/core'; @Component({ selector: 'app-mixed-styles', standalone: false, template: ` <!-- ✅ Good: Using DSButton --> <ds-button>Good Button</ds-button> <!-- ❌ Bad: Legacy button class --> <button class="btn btn-primary">Bad Button</button> <!-- ✅ Good: Using DSModal --> <ds-modal [open]="true"> <p>Good Modal Content</p> </ds-modal> <!-- ❌ Bad: Custom modal with legacy styles --> <div class="modal"> <div class="modal-content"> <h2>Bad Modal</h2> <p>This is a legacy modal.</p> </div> </div> <!-- ✅ Good: DSProgressBar --> <ds-progress-bar [value]="50"></ds-progress-bar> <!-- ❌ Bad: Manually styled progress bar --> <div class="progress-bar"> <div class="progress" style="width: 50%;"></div> </div> <!-- ✅ Good: DSDropdown --> <ds-dropdown [options]="['Option 1', 'Option 2']"></ds-dropdown> <!-- ❌ Bad: Legacy dropdown --> <select class="dropdown"> <option>Option 1</option> <option>Option 2</option> </select> <!-- ✅ Good: Using DSAlert --> <ds-alert type="error"> Good Alert </ds-alert> <!-- ❌ Bad: Manually styled alert --> <div class="alert alert-danger">Bad Alert</div> <!-- ✅ Good: Using DSTooltip --> <ds-tooltip content="Good tooltip">Hover me</ds-tooltip> <!-- ❌ Bad: Legacy tooltip --> <div class="tooltip">Bad tooltip</div> <!-- ✅ Good: Using DSBreadcrumb --> <ds-breadcrumb> <ds-breadcrumb-item>Home</ds-breadcrumb-item> <ds-breadcrumb-item>Products</ds-breadcrumb-item> <ds-breadcrumb-item>Details</ds-breadcrumb-item> </ds-breadcrumb> <!-- ❌ Bad: Manually created breadcrumb --> <nav class="breadcrumb"> <span>Home</span> / <span>Products</span> / <span>Details</span> </nav> `, }) export class MixedStylesNotStandaloneComponent1 {} ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/group-2/bad-mixed-not-standalone-2.component.ts: -------------------------------------------------------------------------------- ```typescript import { Component } from '@angular/core'; @Component({ selector: 'app-mixed-styles', standalone: false, template: ` <!-- ✅ Good: Using DSButton --> <ds-button>Good Button</ds-button> <!-- ❌ Bad: Legacy button class --> <button class="btn btn-primary">Bad Button</button> <!-- ✅ Good: Using DSModal --> <ds-modal [open]="true"> <p>Good Modal Content</p> </ds-modal> <!-- ❌ Bad: Custom modal with legacy styles --> <div class="modal"> <div class="modal-content"> <h2>Bad Modal</h2> <p>This is a legacy modal.</p> </div> </div> <!-- ✅ Good: DSProgressBar --> <ds-progress-bar [value]="50"></ds-progress-bar> <!-- ❌ Bad: Manually styled progress bar --> <div class="progress-bar"> <div class="progress" style="width: 50%;"></div> </div> <!-- ✅ Good: DSDropdown --> <ds-dropdown [options]="['Option 1', 'Option 2']"></ds-dropdown> <!-- ❌ Bad: Legacy dropdown --> <select class="dropdown"> <option>Option 1</option> <option>Option 2</option> </select> <!-- ✅ Good: Using DSAlert --> <ds-alert type="error"> Good Alert </ds-alert> <!-- ❌ Bad: Manually styled alert --> <div class="alert alert-danger">Bad Alert</div> <!-- ✅ Good: Using DSTooltip --> <ds-tooltip content="Good tooltip">Hover me</ds-tooltip> <!-- ❌ Bad: Legacy tooltip --> <div class="tooltip">Bad tooltip</div> <!-- ✅ Good: Using DSBreadcrumb --> <ds-breadcrumb> <ds-breadcrumb-item>Home</ds-breadcrumb-item> <ds-breadcrumb-item>Products</ds-breadcrumb-item> <ds-breadcrumb-item>Details</ds-breadcrumb-item> </ds-breadcrumb> <!-- ❌ Bad: Manually created breadcrumb --> <nav class="breadcrumb"> <span>Home</span> / <span>Products</span> / <span>Details</span> </nav> `, }) export class MixedStylesNotStandaloneComponent2 {} ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/group-3/bad-mixed-not-standalone-3.component.ts: -------------------------------------------------------------------------------- ```typescript import { Component } from '@angular/core'; @Component({ selector: 'app-mixed-styles', standalone: false, template: ` <!-- ✅ Good: Using DSButton --> <ds-button>Good Button</ds-button> <!-- ❌ Bad: Legacy button class --> <button class="btn btn-primary">Bad Button</button> <!-- ✅ Good: Using DSModal --> <ds-modal [open]="true"> <p>Good Modal Content</p> </ds-modal> <!-- ❌ Bad: Custom modal with legacy styles --> <div class="modal"> <div class="modal-content"> <h2>Bad Modal</h2> <p>This is a legacy modal.</p> </div> </div> <!-- ✅ Good: DSProgressBar --> <ds-progress-bar [value]="50"></ds-progress-bar> <!-- ❌ Bad: Manually styled progress bar --> <div class="progress-bar"> <div class="progress" style="width: 50%;"></div> </div> <!-- ✅ Good: DSDropdown --> <ds-dropdown [options]="['Option 1', 'Option 2']"></ds-dropdown> <!-- ❌ Bad: Legacy dropdown --> <select class="dropdown"> <option>Option 1</option> <option>Option 2</option> </select> <!-- ✅ Good: Using DSAlert --> <ds-alert type="error"> Good Alert </ds-alert> <!-- ❌ Bad: Manually styled alert --> <div class="alert alert-danger">Bad Alert</div> <!-- ✅ Good: Using DSTooltip --> <ds-tooltip content="Good tooltip">Hover me</ds-tooltip> <!-- ❌ Bad: Legacy tooltip --> <div class="tooltip">Bad tooltip</div> <!-- ✅ Good: Using DSBreadcrumb --> <ds-breadcrumb> <ds-breadcrumb-item>Home</ds-breadcrumb-item> <ds-breadcrumb-item>Products</ds-breadcrumb-item> <ds-breadcrumb-item>Details</ds-breadcrumb-item> </ds-breadcrumb> <!-- ❌ Bad: Manually created breadcrumb --> <nav class="breadcrumb"> <span>Home</span> / <span>Products</span> / <span>Details</span> </nav> `, }) export class MixedStylesNotStandaloneComponent3 {} ``` -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- ```json { "$schema": "./node_modules/nx/schemas/nx-schema.json", "namedInputs": { "default": ["{projectRoot}/**/*", "sharedGlobals"], "production": [ "default", "!{projectRoot}/.eslintrc.json", "!{projectRoot}/eslint.config.mjs", "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", "!{projectRoot}/tsconfig.spec.json", "!{projectRoot}/jest.config.[jt]s", "!{projectRoot}/src/test-setup.[jt]s", "!{projectRoot}/test-setup.[jt]s" ], "sharedGlobals": ["{workspaceRoot}/.github/workflows/ci.yml"] }, "plugins": [ { "plugin": "@nx/js/typescript", "options": { "typecheck": { "targetName": "typecheck" }, "build": { "targetName": "build", "configName": "tsconfig.lib.json", "buildDepsName": "build-deps", "watchDepsName": "watch-deps" } } }, { "plugin": "@nx/webpack/plugin", "options": { "buildTargetName": "build", "serveTargetName": "serve", "previewTargetName": "preview", "buildDepsTargetName": "build-deps", "watchDepsTargetName": "watch-deps" } }, { "plugin": "@nx/eslint/plugin", "options": { "targetName": "lint" } }, { "plugin": "@nx/vite/plugin", "options": { "buildTargetName": "build", "testTargetName": "test", "serveTargetName": "serve", "devTargetName": "dev", "previewTargetName": "preview", "serveStaticTargetName": "serve-static", "typecheckTargetName": "typecheck", "buildDepsTargetName": "build-deps", "watchDepsTargetName": "watch-deps" } } ], "targetDefaults": { "@nx/js:swc": { "cache": true, "dependsOn": ["^build"], "inputs": ["production", "^production"] }, "test": { "dependsOn": ["^build"] } } } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/design-system/storybook-host-app/src/components/modal/modal-tabs/api.mdx: -------------------------------------------------------------------------------- ```markdown ## Inputs ### `ds-modal-header` | Name | Type | Default | Description | | --------- | ------------------------------------------------------------------ | ----------- | ------------------------------------- | | `variant` | `'surface-lowest' \| 'surface-low' \| 'surface' \| 'surface-high'` | `'surface'` | Background style for the modal header | Other components (`ds-modal`, `ds-modal-content`, `ds-modal-header-drag`) do not define any `@Input()` bindings. --- ## Outputs None of the modal-related components emit Angular `@Output()` events. --- ## Content Projection ### `ds-modal` Supports default slot: ```html <ds-modal> <ds-modal-header>...</ds-modal-header> <ds-modal-content>...</ds-modal-content> </ds-modal> ``` ### `ds-modal-header` Defines multiple named slots: ```html <ds-modal-header> <span slot="start">Back</span> <div slot="center">Title</div> <button slot="end">Close</button> </ds-modal-header> ``` Content is rendered into: - `[slot=start]` → left section - `[slot=center]` → center section - `ds-modal-header-drag`, `[modal-header-image]` → center below title - `[slot=end]` → right section ### `ds-modal-content` Projects content as modal body: ```html <ds-modal-content> Modal text goes here. </ds-modal-content> ``` --- ## Host Element Behavior ### `ds-modal` - Host class: `ds-modal` - Attributes: - `role="dialog"` - `aria-label="Modal dialog"` ### `ds-modal-header` - Host class: `ds-modal-header` - Dynamic class based on `variant`: `ds-modal-header-surface`, `ds-modal-header-surface-low`, etc. - Attributes: - `role="dialog"` - `aria-label="Modal header dialog"` ### `ds-modal-header-drag` - Host class: `ds-modal-header-drag` - Attributes: - `role="dialog"` - `aria-label="Modal header drag dialog"` ### `ds-modal-content` - Host class: `ds-modal-content` - No interactive attributes applied ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/group-3/bad-mixed-3.component.ts: -------------------------------------------------------------------------------- ```typescript import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; @Component({ selector: 'app-mixed-styles', standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <!-- ✅ Good: Using DSButton --> <ds-button>Good Button</ds-button> <!-- ❌ Bad: Legacy button class --> <button class="btn btn-primary">Bad Button</button> <!-- ✅ Good: Using DSModal --> <ds-modal [open]="true"> <p>Good Modal Content</p> </ds-modal> <!-- ❌ Bad: Custom modal with legacy styles --> <div class="modal"> <div class="modal-content"> <h2>Bad Modal</h2> <p>This is a legacy modal.</p> </div> </div> <!-- ✅ Good: DSProgressBar --> <ds-progress-bar [value]="50"></ds-progress-bar> <!-- ❌ Bad: Manually styled progress bar --> <div class="progress-bar"> <div class="progress" style="width: 50%;"></div> </div> <!-- ✅ Good: DSDropdown --> <ds-dropdown [options]="['Option 1', 'Option 2']"></ds-dropdown> <!-- ❌ Bad: Legacy dropdown --> <select class="dropdown"> <option>Option 1</option> <option>Option 2</option> </select> <!-- ✅ Good: Using DSAlert --> <ds-alert type="error"> Good Alert </ds-alert> <!-- ❌ Bad: Manually styled alert --> <div class="alert alert-danger">Bad Alert</div> <!-- ✅ Good: Using DSTooltip --> <ds-tooltip content="Good tooltip">Hover me</ds-tooltip> <!-- ❌ Bad: Legacy tooltip --> <div class="tooltip">Bad tooltip</div> <!-- ✅ Good: Using DSBreadcrumb --> <ds-breadcrumb> <ds-breadcrumb-item>Home</ds-breadcrumb-item> <ds-breadcrumb-item>Products</ds-breadcrumb-item> <ds-breadcrumb-item>Details</ds-breadcrumb-item> </ds-breadcrumb> <!-- ❌ Bad: Manually created breadcrumb --> <nav class="breadcrumb"> <span>Home</span> / <span>Products</span> / <span>Details</span> </nav> `, }) export class MixedStylesComponent3 {} ``` -------------------------------------------------------------------------------- /packages/shared/angular-ast-utils/ai/EXAMPLES.md: -------------------------------------------------------------------------------- ```markdown # Examples ## 1 — Parsing components > Parse a single Angular component file and list component class names. ```ts import { parseComponents } from 'angular-ast-utils'; const comps = await parseComponents(['src/app/app.component.ts']); console.log(comps.map((c) => c.className)); ``` --- ## 2 — Checking for a CSS class > Detect whether a given class name appears in an Angular `[ngClass]` binding. ```ts import { ngClassesIncludeClassName } from 'angular-ast-utils'; const source = "{'btn' : isActive}"; const hasBtn = ngClassesIncludeClassName(source, 'btn'); console.log(hasBtn); // → true ``` --- ## 3 — Finding Angular units by type > Find all components, directives, pipes, or services in a directory. ```ts import { findAngularUnits } from 'angular-ast-utils'; const componentFiles = await findAngularUnits('./src/app', 'component'); const serviceFiles = await findAngularUnits('./src/app', 'service'); console.log(componentFiles); // → ['./src/app/app.component.ts', ...] ``` --- ## 4 — Parsing Angular units in a directory > Parse all Angular components in a directory and get their metadata. ```ts import { parseAngularUnit } from 'angular-ast-utils'; const components = await parseAngularUnit('./src/app', 'component'); console.log(components.map((c) => c.className)); // → ['AppComponent', ...] ``` --- ## 5 — Visiting component templates > Run a visitor function against a component's template AST. ```ts import { visitComponentTemplate } from 'angular-ast-utils'; await visitComponentTemplate(component, searchTerm, async (term, template) => { // Process template AST and return issues return []; }); ``` --- ## 6 — Visiting component styles > Run a visitor function against a component's styles. ```ts import { visitComponentStyles } from 'angular-ast-utils'; const issues = await visitComponentStyles( component, searchTerm, async (term, style) => { // Process style AST and return issues return []; } ); ``` ``` -------------------------------------------------------------------------------- /packages/shared/styles-ast-utils/src/lib/utils.unit.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, expect } from 'vitest'; import { parseStylesheet } from './stylesheet.parse'; import { Rule } from 'postcss'; import { styleAstRuleToSource } from './utils'; describe('styleAstRuleToSource', () => { it('should have line number starting from 1', () => { const result = parseStylesheet(`.btn{ color: red; }`, 'inline-styles').root; const source = styleAstRuleToSource(result?.nodes?.at(0) as Rule); expect(source).toStrictEqual({ file: expect.stringMatching(/inline-styles$/), position: { startLine: 1, startColumn: 1, endLine: 1, endColumn: 19, }, }); }); it('should have line number where startLine is respected', () => { const result = parseStylesheet(`.btn{ color: red; }`, 'styles.css').root; const source = styleAstRuleToSource(result?.nodes?.at(0) as Rule, 4); expect(source).toStrictEqual({ file: expect.stringMatching(/styles\.css$/), position: { startLine: 5, startColumn: 1, endLine: 5, endColumn: 19, }, }); }); it('should have correct line number for starting line breaks', () => { const result = parseStylesheet( ` .btn{ color: red; }`, 'styles.css', ).root; const source = styleAstRuleToSource(result?.nodes?.at(0) as Rule); expect(source).toStrictEqual({ file: expect.stringMatching(/styles\.css$/), position: { startLine: 3, startColumn: 1, endLine: 3, endColumn: 19, }, }); }); it('should have correct line number for spans', () => { const result = parseStylesheet( ` .btn{ color: red; }`, 'styles.css', ).root; const source = styleAstRuleToSource(result?.nodes?.at(0) as Rule); expect(source).toStrictEqual({ file: expect.stringMatching(/styles\.css$/), position: { startLine: 2, startColumn: 1, endLine: 4, endColumn: 1, }, }); }); }); ``` -------------------------------------------------------------------------------- /packages/angular-mcp/package.json: -------------------------------------------------------------------------------- ```json { "name": "@push-based/angular-toolkit-mcp", "version": "0.2.0", "description": "A Model Context Protocol server for Angular project analysis and refactoring", "keywords": [ "mcp", "angular", "refactoring", "analysis", "model-context-protocol" ], "author": "Push-Based", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/push-based/angular-toolkit-mcp.git" }, "homepage": "https://github.com/push-based/angular-toolkit-mcp", "bugs": "https://github.com/push-based/angular-toolkit-mcp/issues", "engines": { "node": ">=18" }, "publishConfig": { "access": "public" }, "bin": { "angular-toolkit-mcp": "main.js" }, "files": [ "main.js", "*.js", "README.md" ], "nx": { "implicitDependencies": [ "angular-mcp-server" ], "targets": { "serve": { "executor": "@nx/js:node", "defaultConfiguration": "development", "dependsOn": [ "build" ], "options": { "buildTarget": "angular-mcp:build", "runBuildTargetDependencies": false }, "configurations": { "development": { "buildTarget": "angular-mcp:build:development" }, "production": { "buildTarget": "angular-mcp:build:production" } } }, "serve-static": { "dependsOn": [ "^build" ], "command": "node packages/angular-mcp/dist/main.js" }, "debug": { "dependsOn": [ "build" ], "command": "npx @modelcontextprotocol/inspector node packages/angular-mcp/dist/main.js --workspaceRoot=/root/path/to/workspace --ds.uiRoot=packages/minimal-repo/packages/design-system/ui --ds.storybookDocsRoot=packages/minimal-repo/packages/design-system/storybook-host-app/src/components --ds.deprecatedCssClassesPath=packages/minimal-repo/packages/design-system/component-options.mjs" } } } } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/design-system/ui/segmented-control/src/segmented-control.component.html: -------------------------------------------------------------------------------- ```html <div class="ds-segmented-control-container" #scContainer> <div class="ds-segmented-controls" [attr.role]="roleType()" [class.ds-sc-ready]="isReady()" [class.ds-segment-full-width]="fullWidth()" [class.ds-segment-inverse]="inverse()" (keydown)="onKeydown($event)" > @for (option of segmentedOptions(); track option.name()) { <div #tabOption class="ds-segment-item" [class.ds-segment-selected]=" option.name() === this.selectedOption()?.name() " [id]="'ds-segment-item-' + option.name()" [attr.tabindex]=" option.name() === this.selectedOption()?.name() ? 0 : -1 " [attr.role]="roleType() === 'tablist' ? 'tab' : 'radio'" [attr.aria-selected]=" roleType() === 'tablist' ? option.name() === this.selectedOption()?.name() ? 'true' : 'false' : null " [attr.aria-checked]=" roleType() === 'radiogroup' ? option.name() === this.selectedOption()?.name() ? 'true' : 'false' : null " [attr.aria-label]="option.title() || option.name()" (click)="selectOption(option.name(), $event)" > <input type="radio" class="ds-segmented-control-hidden-input" [value]="option.name()" [name]="option.name()" [id]="'ds-sc-option-' + option.name()" [checked]="option.selected()" [attr.aria-labelledby]="'ds-segment-item-' + option.name()" [title]="option.title()" /> <label class="ds-segment-item-label" [for]="'ds-sc-option-' + option.title()" [class.ds-segmented-item-two-line-text]="twoLineTruncation()" [class.ds-segment-item-custom-template]="option.customTemplate()" > @if (option.customTemplate()) { <ng-container [ngTemplateOutlet]="option.customTemplate()!" /> } @else { {{ option.title() }} } </label> </div> } </div> </div> ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/spec/dom-path-utils.spec.ts: -------------------------------------------------------------------------------- ```typescript /* eslint-disable prefer-const */ import { describe, it, expect } from 'vitest'; import { createDomPathDictionary, isDomPath, isValidDomPath, addDomPath, processDomPaths, } from '../utils/dom-path-utils.js'; const SAMPLE_PATH = 'div#root > span.foo > button.bar'; describe('dom-path-utils', () => { describe('isDomPath / isValidDomPath', () => { it('detects DOM-like selector strings correctly', () => { expect(isDomPath(SAMPLE_PATH)).toBe(true); expect(isValidDomPath(SAMPLE_PATH)).toBe(true); expect(isDomPath('div')).toBe(false); expect(isDomPath('div.foo')).toBe(false); const mediaPath = `${SAMPLE_PATH} @media`; expect(isDomPath(mediaPath)).toBe(true); expect(isValidDomPath(mediaPath)).toBe(false); }); }); describe('addDomPath & createDomPathDictionary', () => { it('adds new paths and deduplicates existing ones', () => { const dict = createDomPathDictionary(); const ref1 = addDomPath(dict, SAMPLE_PATH); expect(ref1).toEqual({ $domPath: 0 }); expect(dict.paths[0]).toBe(SAMPLE_PATH); expect(dict.stats.totalPaths).toBe(1); expect(dict.stats.uniquePaths).toBe(1); expect(dict.stats.duplicateReferences).toBe(0); const ref2 = addDomPath(dict, SAMPLE_PATH); expect(ref2).toEqual({ $domPath: 0 }); expect(dict.stats.totalPaths).toBe(2); expect(dict.stats.uniquePaths).toBe(1); expect(dict.stats.duplicateReferences).toBe(1); }); }); describe('processDomPaths', () => { it('recursively replaces DOM path strings with references', () => { const dict = createDomPathDictionary(); const input = { pathA: SAMPLE_PATH, nested: ['no-dom-path', SAMPLE_PATH, { deeper: SAMPLE_PATH }], }; const processed = processDomPaths(input, dict); expect(processed.pathA).toEqual({ $domPath: 0 }); expect(processed.nested[1]).toEqual({ $domPath: 0 }); expect(processed.nested[2].deeper).toEqual({ $domPath: 0 }); expect(dict.paths).toEqual([SAMPLE_PATH]); expect(dict.stats.uniquePaths).toBe(1); }); }); }); ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/app.component.ts: -------------------------------------------------------------------------------- ```typescript import { Component } from '@angular/core'; import { RouterOutlet } from '@angular/router'; import { BadAlertComponent } from './components/refactoring-tests/bad-alert.component'; import { BadAlertTooltipInputComponent } from './components/refactoring-tests/bad-alert-tooltip-input.component'; import { BadButtonDropdownComponent } from './components/refactoring-tests/bad-button-dropdown.component'; import { MixedStylesComponent } from './components/refactoring-tests/bad-mixed.component'; import { BadModalProgressComponent } from './components/refactoring-tests/bad-modal-progress.component'; import { BadMixedExternalAssetsComponent } from './components/refactoring-tests/bad-mixed-external-assets.component'; import { BadDocumentComponent } from './components/refactoring-tests/bad-document.component'; import { BadWindowComponent } from './components/refactoring-tests/bad-window.component'; import { BadThisWindowDocumentComponent } from './components/refactoring-tests/bad-this-window-document.component'; import { BadGlobalThisComponent } from './components/refactoring-tests/bad-global-this.component'; @Component({ selector: 'app-root', imports: [ RouterOutlet, BadAlertComponent, BadAlertTooltipInputComponent, BadModalProgressComponent, BadButtonDropdownComponent, MixedStylesComponent, BadMixedExternalAssetsComponent, BadDocumentComponent, BadGlobalThisComponent, BadWindowComponent, BadThisWindowDocumentComponent, ], template: ` <h1>{{ title }}</h1> <button class="btn">Sports</button> <app-bad-alert></app-bad-alert> <app-bad-alert-tooltip-input></app-bad-alert-tooltip-input> <app-bad-modal-progress></app-bad-modal-progress> <app-mixed-styles></app-mixed-styles> <app-bad-button-dropdown></app-bad-button-dropdown> <app-bad-mixed-external-assets></app-bad-mixed-external-assets> <app-bad-window></app-bad-window> <app-bad-this-window-document></app-bad-this-window-document> <app-bad-document></app-bad-document> <app-bad-global-this></app-bad-global-this> <router-outlet /> `, }) export class AppComponent { title = 'minimal'; } ``` -------------------------------------------------------------------------------- /packages/shared/angular-ast-utils/src/lib/template/noop-tmpl-visitor.ts: -------------------------------------------------------------------------------- ```typescript import type { TmplAstVisitor, TmplAstElement, TmplAstTemplate, TmplAstContent, TmplAstText, TmplAstBoundText, TmplAstIcu, TmplAstReference, TmplAstVariable, TmplAstBoundEvent, TmplAstBoundAttribute, TmplAstTextAttribute, TmplAstUnknownBlock, TmplAstDeferredBlock, TmplAstDeferredBlockError, TmplAstDeferredBlockLoading, TmplAstDeferredBlockPlaceholder, TmplAstDeferredTrigger, TmplAstIfBlock, TmplAstIfBlockBranch, TmplAstSwitchBlock, TmplAstSwitchBlockCase, TmplAstForLoopBlock, TmplAstForLoopBlockEmpty, TmplAstLetDeclaration, } from '@angular/compiler' with { 'resolution-mode': 'import' }; /** * Base visitor that does nothing. * Extend this in concrete visitors so you only override what you need. */ export abstract class NoopTmplVisitor implements TmplAstVisitor<void> { /* eslint-disable @typescript-eslint/no-empty-function */ visitElement(_: TmplAstElement): void {} visitTemplate(_: TmplAstTemplate): void {} visitContent(_: TmplAstContent): void {} visitText(_: TmplAstText): void {} visitBoundText(_: TmplAstBoundText): void {} visitIcu(_: TmplAstIcu): void {} visitReference(_: TmplAstReference): void {} visitVariable(_: TmplAstVariable): void {} visitBoundEvent(_: TmplAstBoundEvent): void {} visitBoundAttribute(_: TmplAstBoundAttribute): void {} visitTextAttribute(_: TmplAstTextAttribute): void {} visitUnknownBlock(_: TmplAstUnknownBlock): void {} visitDeferredBlock(_: TmplAstDeferredBlock): void {} visitDeferredBlockError(_: TmplAstDeferredBlockError): void {} visitDeferredBlockLoading(_: TmplAstDeferredBlockLoading): void {} visitDeferredBlockPlaceholder(_: TmplAstDeferredBlockPlaceholder): void {} visitDeferredTrigger(_: TmplAstDeferredTrigger): void {} visitIfBlock(_: TmplAstIfBlock): void {} visitIfBlockBranch(_: TmplAstIfBlockBranch): void {} visitSwitchBlock(_: TmplAstSwitchBlock): void {} visitSwitchBlockCase(_: TmplAstSwitchBlockCase): void {} visitForLoopBlock(_: TmplAstForLoopBlock): void {} visitForLoopBlockEmpty(_: TmplAstForLoopBlockEmpty): void {} visitLetDeclaration(_: TmplAstLetDeclaration): void {} /* eslint-enable */ } ``` -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- ```markdown # Getting Started with Angular MCP Toolkit A concise, hands-on guide to install, configure, and verify the Angular MCP server in under **5 minutes**. --- ## 1. Prerequisites | Tool | Minimum Version | Notes | | ---- | --------------- | ----- | | Node.js | **18.x** LTS | Tested with 18.18 ⬆︎ | | npm | **9.x** | Bundled with Node LTS | | Nx CLI | **≥ 21** | `npm i -g nx` | | Git | Any recent | For workspace cloning | > The server itself is framework-agnostic, but most built-in tools assume an **Nx workspace** with Angular projects. --- ## 2. Install the Server ### Clone the repository ```bash git clone https://github.com/push-based/angular-toolkit-mcp.git cd angular-toolkit-mcp npm install # install workspace dependencies ``` The MCP server source resides under `packages/angular-mcp/` and `packages/angular-mcp-server/`. No package needs to be fetched from the npm registry. --- ## 3. Register with Your Editor Instead of the palette-based flow, copy the manual configuration from your workspace’s `.cursor/mcp.json` (shown below) and adjust paths if necessary. ```json { "mcpServers": { "angular-mcp": { "command": "node", "args": [ "./packages/angular-mcp/dist/main.js", "--workspaceRoot=/absolute/path/to/angular-toolkit-mcp", "--ds.storybookDocsRoot=packages/minimal-repo/packages/design-system/storybook-host-app/src/components", "--ds.deprecatedCssClassesPath=packages/minimal-repo/packages/design-system/component-options.mjs", "--ds.uiRoot=packages/minimal-repo/packages/design-system/ui" ] } } } ``` Add or edit this JSON in **Cursor → Settings → MCP Servers** (or the equivalent dialog in your editor). --- ## 4. Next Steps 🔗 Continue with the [Architecture & Internal Design](./architecture-internal-design.md) document (work-in-progress). 🚀 Jump straight into [Writing Custom Tools](./writing-custom-tools.md) when ready. --- ## Troubleshooting | Symptom | Possible Cause | Fix | | ------- | -------------- | --- | | `command not found: nx` | Nx CLI missing | `npm i -g nx` | | Editor shows “tool not found” | Server not running or wrong path in `mcp.json` | Check configuration and restart editor | --- *Happy coding!* ✨ ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/utils/dom-path-utils.ts: -------------------------------------------------------------------------------- ```typescript import type { DomPathDictionary } from '../../shared/models/types.js'; /** * Creates a new DOM path dictionary */ export function createDomPathDictionary(): DomPathDictionary { return { paths: [], lookup: new Map<string, number>(), stats: { totalPaths: 0, uniquePaths: 0, duplicateReferences: 0, bytesBeforeDeduplication: 0, bytesAfterDeduplication: 0, }, }; } /** * Detects if a string is a DOM path based on Angular component patterns */ export function isDomPath(str: string): boolean { return ( typeof str === 'string' && str.length > 20 && str.includes(' > ') && (str.includes('.') || str.includes('#')) && /^[a-zA-Z]/.test(str) ); } /** * Validates that a string is specifically a DOM path and not just any CSS selector */ export function isValidDomPath(str: string): boolean { return ( isDomPath(str) && !str.includes('@media') && !str.includes('{') && !str.includes('}') && str.split(' > ').length > 2 ); } /** * Adds a DOM path to the dictionary and returns its reference */ export function addDomPath( dict: DomPathDictionary, path: string, ): { $domPath: number } { dict.stats.totalPaths++; dict.stats.bytesBeforeDeduplication += path.length; if (dict.lookup.has(path)) { dict.stats.duplicateReferences++; dict.stats.bytesAfterDeduplication += 12; return { $domPath: dict.lookup.get(path)! }; } const index = dict.paths.length; dict.paths.push(path); dict.lookup.set(path, index); dict.stats.uniquePaths++; dict.stats.bytesAfterDeduplication += 12; return { $domPath: index }; } /** * Processes a value and replaces DOM paths with references */ export function processDomPaths(value: any, dict: DomPathDictionary): any { if (typeof value === 'string') { if (isValidDomPath(value)) { return addDomPath(dict, value); } return value; } if (Array.isArray(value)) { return value.map((item) => processDomPaths(item, dict)); } if (value && typeof value === 'object') { const processed: any = {}; for (const [key, val] of Object.entries(value)) { processed[key] = processDomPaths(val, dict); } return processed; } return value; } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/design-system/ui/badge/src/badge.component.ts: -------------------------------------------------------------------------------- ```typescript import { ChangeDetectionStrategy, Component, ElementRef, ViewEncapsulation, booleanAttribute, computed, input, } from '@angular/core'; export const DS_BADGE_VARIANT_ARRAY = [ 'primary', 'primary-strong', 'primary-subtle', 'secondary', 'secondary-strong', 'secondary-subtle', 'green', 'green-strong', 'green-subtle', 'blue', 'blue-strong', 'blue-subtle', 'red', 'red-strong', 'red-subtle', 'purple', 'purple-strong', 'purple-subtle', 'neutral', 'neutral-strong', 'neutral-subtle', 'yellow', 'yellow-strong', 'yellow-subtle', 'orange', 'orange-strong', 'orange-subtle', ] as const; export type DsBadgeVariant = (typeof DS_BADGE_VARIANT_ARRAY)[number]; export const DS_BADGE_SIZE_ARRAY = ['xsmall', 'medium'] as const; export type DsBadgeSize = (typeof DS_BADGE_SIZE_ARRAY)[number]; @Component({ selector: 'ds-badge', template: ` <div class="ds-badge-slot-container"> <ng-content select="[slot=start]" /> </div> <span class="ds-badge-text"> <ng-content /> </span> <div class="ds-badge-slot-container"> <ng-content select="[slot=end]" /> </div> `, host: { '[class]': 'hostClass()', '[class.ds-badge-disabled]': 'disabled()', '[class.ds-badge-inverse]': 'inverse()', '[attr.aria-label]': 'getAriaLabel()', role: 'img', // for now we are using role img till we find better solution to work with nvda }, standalone: true, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, }) export class DsBadge { size = input<DsBadgeSize>('medium'); variant = input<DsBadgeVariant>('primary'); disabled = input(false, { transform: booleanAttribute }); inverse = input(false, { transform: booleanAttribute }); hostClass = computed( () => `ds-badge ds-badge-${this.size()} ds-badge-${this.variant()}`, ); constructor(public elementRef: ElementRef<HTMLElement>) {} public getAriaLabel(): string { const mainContent = this.elementRef.nativeElement .querySelector('.ds-badge-text') ?.textContent?.trim(); const label = mainContent || ''; if (this.disabled()) { return `Disabled badge: ${label}`; } return `Badge: ${label}`; } } ```