This is page 3 of 10. Use http://codebase.md/push-based/angular-toolkit-mcp?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .aiignore ├── .cursor │ ├── flows │ │ ├── component-refactoring │ │ │ ├── 01-review-component.mdc │ │ │ ├── 02-refactor-component.mdc │ │ │ ├── 03-validate-component.mdc │ │ │ └── angular-20.md │ │ ├── ds-refactoring-flow │ │ │ ├── 01-find-violations.mdc │ │ │ ├── 01b-find-all-violations.mdc │ │ │ ├── 02-plan-refactoring.mdc │ │ │ ├── 02b-plan-refactoring-for-all-violations.mdc │ │ │ ├── 03-fix-violations.mdc │ │ │ ├── 03-non-viable-cases.mdc │ │ │ ├── 04-validate-changes.mdc │ │ │ ├── 05-prepare-report.mdc │ │ │ └── clean-global-styles.mdc │ │ └── README.md │ └── mcp.json.example ├── .github │ └── workflows │ └── ci.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── assets │ ├── entain-logo.png │ └── entain.png ├── CONTRIBUTING.MD ├── docs │ ├── architecture-internal-design.md │ ├── component-refactoring-flow.md │ ├── contracts.md │ ├── ds-refactoring-flow.md │ ├── getting-started.md │ ├── README.md │ ├── tools.md │ └── writing-custom-tools.md ├── eslint.config.mjs ├── jest.config.ts ├── jest.preset.mjs ├── LICENSE ├── nx.json ├── package-lock.json ├── package.json ├── packages │ ├── .gitkeep │ ├── angular-mcp │ │ ├── eslint.config.mjs │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── assets │ │ │ │ └── .gitkeep │ │ │ └── main.ts │ │ ├── tsconfig.app.json │ │ ├── tsconfig.json │ │ ├── vitest.config.mts │ │ └── webpack.config.cjs │ ├── angular-mcp-server │ │ ├── eslint.config.mjs │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── index.ts │ │ │ └── lib │ │ │ ├── angular-mcp-server.ts │ │ │ ├── prompts │ │ │ │ └── prompt-registry.ts │ │ │ ├── tools │ │ │ │ ├── ds │ │ │ │ │ ├── component │ │ │ │ │ │ ├── get-deprecated-css-classes.tool.ts │ │ │ │ │ │ ├── get-ds-component-data.tool.ts │ │ │ │ │ │ ├── list-ds-components.tool.ts │ │ │ │ │ │ └── utils │ │ │ │ │ │ ├── deprecated-css-helpers.ts │ │ │ │ │ │ ├── doc-helpers.ts │ │ │ │ │ │ ├── metadata-helpers.ts │ │ │ │ │ │ └── paths-helpers.ts │ │ │ │ │ ├── component-contract │ │ │ │ │ │ ├── builder │ │ │ │ │ │ │ ├── build-component-contract.tool.ts │ │ │ │ │ │ │ ├── models │ │ │ │ │ │ │ │ ├── schema.ts │ │ │ │ │ │ │ │ └── types.ts │ │ │ │ │ │ │ ├── spec │ │ │ │ │ │ │ │ ├── css-match.spec.ts │ │ │ │ │ │ │ │ ├── dom-slots.extractor.spec.ts │ │ │ │ │ │ │ │ ├── element-helpers.spec.ts │ │ │ │ │ │ │ │ ├── inline-styles.collector.spec.ts │ │ │ │ │ │ │ │ ├── meta.generator.spec.ts │ │ │ │ │ │ │ │ ├── public-api.extractor.spec.ts │ │ │ │ │ │ │ │ ├── styles.collector.spec.ts │ │ │ │ │ │ │ │ └── typescript-analyzer.spec.ts │ │ │ │ │ │ │ └── utils │ │ │ │ │ │ │ ├── build-contract.ts │ │ │ │ │ │ │ ├── css-match.ts │ │ │ │ │ │ │ ├── dom-slots.extractor.ts │ │ │ │ │ │ │ ├── element-helpers.ts │ │ │ │ │ │ │ ├── inline-styles.collector.ts │ │ │ │ │ │ │ ├── meta.generator.ts │ │ │ │ │ │ │ ├── public-api.extractor.ts │ │ │ │ │ │ │ ├── styles.collector.ts │ │ │ │ │ │ │ └── typescript-analyzer.ts │ │ │ │ │ │ ├── diff │ │ │ │ │ │ │ ├── diff-component-contract.tool.ts │ │ │ │ │ │ │ ├── models │ │ │ │ │ │ │ │ └── schema.ts │ │ │ │ │ │ │ ├── spec │ │ │ │ │ │ │ │ ├── diff-utils.spec.ts │ │ │ │ │ │ │ │ └── dom-path-utils.spec.ts │ │ │ │ │ │ │ └── utils │ │ │ │ │ │ │ ├── diff-utils.ts │ │ │ │ │ │ │ └── dom-path-utils.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── list │ │ │ │ │ │ │ ├── list-component-contracts.tool.ts │ │ │ │ │ │ │ ├── models │ │ │ │ │ │ │ │ ├── schema.ts │ │ │ │ │ │ │ │ └── types.ts │ │ │ │ │ │ │ ├── spec │ │ │ │ │ │ │ │ └── contract-list-utils.spec.ts │ │ │ │ │ │ │ └── utils │ │ │ │ │ │ │ └── contract-list-utils.ts │ │ │ │ │ │ └── shared │ │ │ │ │ │ ├── models │ │ │ │ │ │ │ └── types.ts │ │ │ │ │ │ ├── spec │ │ │ │ │ │ │ └── contract-file-ops.spec.ts │ │ │ │ │ │ └── utils │ │ │ │ │ │ └── contract-file-ops.ts │ │ │ │ │ ├── component-usage-graph │ │ │ │ │ │ ├── build-component-usage-graph.tool.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── models │ │ │ │ │ │ │ ├── config.ts │ │ │ │ │ │ │ ├── schema.ts │ │ │ │ │ │ │ └── types.ts │ │ │ │ │ │ └── utils │ │ │ │ │ │ ├── angular-parser.ts │ │ │ │ │ │ ├── component-helpers.ts │ │ │ │ │ │ ├── component-usage-graph-builder.ts │ │ │ │ │ │ ├── path-resolver.ts │ │ │ │ │ │ └── unified-ast-analyzer.ts │ │ │ │ │ ├── ds.tools.ts │ │ │ │ │ ├── project │ │ │ │ │ │ ├── get-project-dependencies.tool.ts │ │ │ │ │ │ ├── report-deprecated-css.tool.ts │ │ │ │ │ │ └── utils │ │ │ │ │ │ ├── dependencies-helpers.ts │ │ │ │ │ │ └── styles-report-helpers.ts │ │ │ │ │ ├── report-violations │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── models │ │ │ │ │ │ │ ├── schema.ts │ │ │ │ │ │ │ └── types.ts │ │ │ │ │ │ ├── report-all-violations.tool.ts │ │ │ │ │ │ └── report-violations.tool.ts │ │ │ │ │ ├── shared │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── models │ │ │ │ │ │ │ ├── input-schemas.model.ts │ │ │ │ │ │ │ └── schema-helpers.ts │ │ │ │ │ │ ├── utils │ │ │ │ │ │ │ ├── component-validation.ts │ │ │ │ │ │ │ ├── cross-platform-path.ts │ │ │ │ │ │ │ ├── handler-helpers.ts │ │ │ │ │ │ │ ├── output.utils.ts │ │ │ │ │ │ │ └── regex-helpers.ts │ │ │ │ │ │ └── violation-analysis │ │ │ │ │ │ ├── base-analyzer.ts │ │ │ │ │ │ ├── coverage-analyzer.ts │ │ │ │ │ │ ├── formatters.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── types.ts │ │ │ │ │ └── tools.ts │ │ │ │ ├── schema.ts │ │ │ │ ├── tools.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils.ts │ │ │ └── validation │ │ │ ├── angular-mcp-server-options.schema.ts │ │ │ ├── ds-components-file-loader.validation.ts │ │ │ ├── ds-components-file.validation.ts │ │ │ ├── ds-components.schema.ts │ │ │ └── file-existence.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ ├── tsconfig.tsbuildinfo │ │ └── vitest.config.mts │ ├── minimal-repo │ │ └── packages │ │ ├── application │ │ │ ├── angular.json │ │ │ ├── code-pushup.config.ts │ │ │ ├── src │ │ │ │ ├── app │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.config.ts │ │ │ │ │ ├── app.routes.ts │ │ │ │ │ ├── components │ │ │ │ │ │ ├── refactoring-tests │ │ │ │ │ │ │ ├── bad-alert-tooltip-input.component.ts │ │ │ │ │ │ │ ├── bad-alert.component.ts │ │ │ │ │ │ │ ├── bad-button-dropdown.component.ts │ │ │ │ │ │ │ ├── bad-document.component.ts │ │ │ │ │ │ │ ├── bad-global-this.component.ts │ │ │ │ │ │ │ ├── bad-mixed-external-assets.component.css │ │ │ │ │ │ │ ├── bad-mixed-external-assets.component.html │ │ │ │ │ │ │ ├── bad-mixed-external-assets.component.ts │ │ │ │ │ │ │ ├── bad-mixed-not-standalone.component.ts │ │ │ │ │ │ │ ├── bad-mixed.component.ts │ │ │ │ │ │ │ ├── bad-mixed.module.ts │ │ │ │ │ │ │ ├── bad-modal-progress.component.ts │ │ │ │ │ │ │ ├── bad-this-window-document.component.ts │ │ │ │ │ │ │ ├── bad-window.component.ts │ │ │ │ │ │ │ ├── complex-components │ │ │ │ │ │ │ │ ├── first-case │ │ │ │ │ │ │ │ │ ├── dashboard-demo.component.html │ │ │ │ │ │ │ │ │ ├── dashboard-demo.component.scss │ │ │ │ │ │ │ │ │ ├── dashboard-demo.component.ts │ │ │ │ │ │ │ │ │ ├── dashboard-header.component.html │ │ │ │ │ │ │ │ │ ├── dashboard-header.component.scss │ │ │ │ │ │ │ │ │ └── dashboard-header.component.ts │ │ │ │ │ │ │ │ ├── second-case │ │ │ │ │ │ │ │ │ ├── complex-badge-widget.component.scss │ │ │ │ │ │ │ │ │ ├── complex-badge-widget.component.ts │ │ │ │ │ │ │ │ │ └── complex-widget-demo.component.ts │ │ │ │ │ │ │ │ └── third-case │ │ │ │ │ │ │ │ ├── product-card.component.scss │ │ │ │ │ │ │ │ ├── product-card.component.ts │ │ │ │ │ │ │ │ └── product-showcase.component.ts │ │ │ │ │ │ │ ├── group-1 │ │ │ │ │ │ │ │ ├── bad-mixed-1.component.ts │ │ │ │ │ │ │ │ ├── bad-mixed-1.module.ts │ │ │ │ │ │ │ │ ├── bad-mixed-external-assets-1.component.css │ │ │ │ │ │ │ │ ├── bad-mixed-external-assets-1.component.html │ │ │ │ │ │ │ │ ├── bad-mixed-external-assets-1.component.ts │ │ │ │ │ │ │ │ └── bad-mixed-not-standalone-1.component.ts │ │ │ │ │ │ │ ├── group-2 │ │ │ │ │ │ │ │ ├── bad-mixed-2.component.ts │ │ │ │ │ │ │ │ ├── bad-mixed-2.module.ts │ │ │ │ │ │ │ │ ├── bad-mixed-external-assets-2.component.css │ │ │ │ │ │ │ │ ├── bad-mixed-external-assets-2.component.html │ │ │ │ │ │ │ │ ├── bad-mixed-external-assets-2.component.ts │ │ │ │ │ │ │ │ └── bad-mixed-not-standalone-2.component.ts │ │ │ │ │ │ │ ├── group-3 │ │ │ │ │ │ │ │ ├── bad-mixed-3.component.spec.ts │ │ │ │ │ │ │ │ ├── bad-mixed-3.component.ts │ │ │ │ │ │ │ │ ├── bad-mixed-3.module.ts │ │ │ │ │ │ │ │ ├── bad-mixed-external-assets-3.component.css │ │ │ │ │ │ │ │ ├── bad-mixed-external-assets-3.component.html │ │ │ │ │ │ │ │ ├── bad-mixed-external-assets-3.component.ts │ │ │ │ │ │ │ │ ├── bad-mixed-not-standalone-3.component.ts │ │ │ │ │ │ │ │ └── lazy-loader-3.component.ts │ │ │ │ │ │ │ └── group-4 │ │ │ │ │ │ │ ├── multi-violation-test.component.html │ │ │ │ │ │ │ ├── multi-violation-test.component.scss │ │ │ │ │ │ │ └── multi-violation-test.component.ts │ │ │ │ │ │ └── validation-tests │ │ │ │ │ │ ├── circular-dependency.component.ts │ │ │ │ │ │ ├── external-files-missing.component.ts │ │ │ │ │ │ ├── invalid-lifecycle.component.ts │ │ │ │ │ │ ├── invalid-pipe-usage.component.ts │ │ │ │ │ │ ├── invalid-template-syntax.component.ts │ │ │ │ │ │ ├── missing-imports.component.ts │ │ │ │ │ │ ├── missing-method.component.ts │ │ │ │ │ │ ├── README.md │ │ │ │ │ │ ├── standalone-module-conflict.component.ts │ │ │ │ │ │ ├── standalone-module-conflict.module.ts │ │ │ │ │ │ ├── template-reference-error.component.ts │ │ │ │ │ │ ├── type-mismatch.component.ts │ │ │ │ │ │ ├── valid.component.ts │ │ │ │ │ │ ├── wrong-decorator-usage.component.ts │ │ │ │ │ │ └── wrong-property-binding.component.ts │ │ │ │ │ └── styles │ │ │ │ │ ├── bad-global-styles.scss │ │ │ │ │ ├── base │ │ │ │ │ │ ├── _reset.scss │ │ │ │ │ │ └── base.scss │ │ │ │ │ ├── components │ │ │ │ │ │ └── components.scss │ │ │ │ │ ├── extended-deprecated-styles.scss │ │ │ │ │ ├── layout │ │ │ │ │ │ └── layout.scss │ │ │ │ │ ├── new-styles-1.scss │ │ │ │ │ ├── new-styles-10.scss │ │ │ │ │ ├── new-styles-2.scss │ │ │ │ │ ├── new-styles-3.scss │ │ │ │ │ ├── new-styles-4.scss │ │ │ │ │ ├── new-styles-5.scss │ │ │ │ │ ├── new-styles-6.scss │ │ │ │ │ ├── new-styles-7.scss │ │ │ │ │ ├── new-styles-8.scss │ │ │ │ │ ├── new-styles-9.scss │ │ │ │ │ ├── themes │ │ │ │ │ │ └── themes.scss │ │ │ │ │ └── utilities │ │ │ │ │ └── utilities.scss │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ └── styles.css │ │ │ ├── tsconfig.app.json │ │ │ ├── tsconfig.json │ │ │ └── tsconfig.spec.json │ │ └── design-system │ │ ├── component-options.mjs │ │ ├── storybook │ │ │ └── card │ │ │ └── card-tabs │ │ │ └── overview.mdx │ │ ├── storybook-host-app │ │ │ └── src │ │ │ └── components │ │ │ ├── badge │ │ │ │ ├── badge-tabs │ │ │ │ │ ├── api.mdx │ │ │ │ │ ├── examples.mdx │ │ │ │ │ └── overview.mdx │ │ │ │ ├── badge.component.mdx │ │ │ │ └── badge.component.stories.ts │ │ │ ├── modal │ │ │ │ ├── demo-cdk-dialog-cmp.component.ts │ │ │ │ ├── demo-modal-cmp.component.ts │ │ │ │ ├── modal-tabs │ │ │ │ │ ├── api.mdx │ │ │ │ │ ├── examples.mdx │ │ │ │ │ └── overview.mdx │ │ │ │ ├── modal.component.mdx │ │ │ │ └── modal.component.stories.ts │ │ │ └── segmented-control │ │ │ ├── segmented-control-tabs │ │ │ │ ├── api.mdx │ │ │ │ ├── examples.mdx │ │ │ │ └── overview.mdx │ │ │ ├── segmented-control.component.mdx │ │ │ └── segmented-control.component.stories.ts │ │ └── ui │ │ ├── badge │ │ │ ├── package.json │ │ │ ├── project.json │ │ │ └── src │ │ │ └── badge.component.ts │ │ ├── modal │ │ │ ├── package.json │ │ │ ├── project.json │ │ │ └── src │ │ │ ├── modal-content.component.ts │ │ │ ├── modal-header │ │ │ │ └── modal-header.component.ts │ │ │ ├── modal-header-drag │ │ │ │ └── modal-header-drag.component.ts │ │ │ └── modal.component.ts │ │ ├── rx-host-listener │ │ │ ├── package.json │ │ │ ├── project.json │ │ │ └── src │ │ │ └── rx-host-listener.ts │ │ └── segmented-control │ │ ├── package.json │ │ ├── project.json │ │ └── src │ │ ├── segmented-control.component.html │ │ ├── segmented-control.component.ts │ │ ├── segmented-control.token.ts │ │ └── segmented-option.component.ts │ └── shared │ ├── angular-ast-utils │ │ ├── .spec.swcrc │ │ ├── ai │ │ │ ├── API.md │ │ │ ├── EXAMPLES.md │ │ │ └── FUNCTIONS.md │ │ ├── docs │ │ │ └── angular-component-tree.md │ │ ├── eslint.config.mjs │ │ ├── jest.config.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── index.ts │ │ │ └── lib │ │ │ ├── constants.ts │ │ │ ├── decorator-config.visitor.inline-styles.spec.ts │ │ │ ├── decorator-config.visitor.spec.ts │ │ │ ├── decorator-config.visitor.ts │ │ │ ├── parse-component.ts │ │ │ ├── schema.ts │ │ │ ├── styles │ │ │ │ └── utils.ts │ │ │ ├── template │ │ │ │ ├── noop-tmpl-visitor.ts │ │ │ │ ├── template.walk.ts │ │ │ │ ├── utils.spec.ts │ │ │ │ ├── utils.ts │ │ │ │ └── utils.unit.test.ts │ │ │ ├── ts.walk.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ ├── tsconfig.spec.json │ │ └── vitest.config.mts │ ├── DEPENDENCIES.md │ ├── ds-component-coverage │ │ ├── .spec.swcrc │ │ ├── ai │ │ │ ├── API.md │ │ │ ├── EXAMPLES.md │ │ │ └── FUNCTIONS.md │ │ ├── docs │ │ │ ├── examples │ │ │ │ ├── report.json │ │ │ │ └── report.md │ │ │ ├── images │ │ │ │ └── report-overview.png │ │ │ └── README.md │ │ ├── jest.config.ts │ │ ├── mocks │ │ │ └── fixtures │ │ │ └── e2e │ │ │ ├── asset-location │ │ │ │ ├── code-pushup.config.ts │ │ │ │ ├── inl-styl-inl-tmpl │ │ │ │ │ └── inl-styl-inl-tmpl.component.ts │ │ │ │ ├── inl-styl-url-tmpl │ │ │ │ │ ├── inl-styl-url-tmpl.component.html │ │ │ │ │ └── inl-styl-url-tmpl.component.ts │ │ │ │ ├── multi-url-styl-inl-tmpl │ │ │ │ │ ├── multi-url-styl-inl-tmpl-1.component.css │ │ │ │ │ ├── multi-url-styl-inl-tmpl-2.component.css │ │ │ │ │ └── multi-url-styl-inl-tmpl.component.ts │ │ │ │ ├── url-styl-inl-tmpl │ │ │ │ │ ├── url-styl-inl-tmpl.component.css │ │ │ │ │ └── url-styl-inl-tmpl.component.ts │ │ │ │ ├── url-styl-single-inl-tmpl │ │ │ │ │ ├── url-styl-inl-tmpl.component.ts │ │ │ │ │ └── url-styl-single-inl-tmpl.component.css │ │ │ │ └── url-styl-url-tmpl │ │ │ │ ├── inl-styl-url-tmpl.component.css │ │ │ │ ├── inl-styl-url-tmpl.component.html │ │ │ │ └── inl-styl-url-tmpl.component.ts │ │ │ ├── demo │ │ │ │ ├── code-pushup.config.ts │ │ │ │ ├── prompt.md │ │ │ │ └── src │ │ │ │ ├── bad-button-dropdown.component.ts │ │ │ │ ├── bad-modal-progress.component.ts │ │ │ │ ├── mixed-external-assets.component.css │ │ │ │ ├── mixed-external-assets.component.html │ │ │ │ ├── mixed-external-assets.component.ts │ │ │ │ └── sub-folder-1 │ │ │ │ ├── bad-alert.component.ts │ │ │ │ ├── button.component.ts │ │ │ │ └── sub-folder-2 │ │ │ │ ├── bad-alert-tooltip-input.component.ts │ │ │ │ └── bad-mixed.component.ts │ │ │ ├── line-number │ │ │ │ ├── code-pushup.config.ts │ │ │ │ ├── inl-styl-single.component.ts │ │ │ │ ├── inl-styl-span.component.ts │ │ │ │ ├── inl-tmpl-single.component.ts │ │ │ │ ├── inl-tmpl-span.component.ts │ │ │ │ ├── url-style │ │ │ │ │ ├── url-styl-single.component.css │ │ │ │ │ ├── url-styl-single.component.ts │ │ │ │ │ ├── url-styl-span.component.css │ │ │ │ │ └── url-styl-span.component.ts │ │ │ │ └── url-tmpl │ │ │ │ ├── url-tmpl-single.component.html │ │ │ │ ├── url-tmpl-single.component.ts │ │ │ │ ├── url-tmpl-span.component.html │ │ │ │ └── url-tmpl-span.component.ts │ │ │ ├── style-format │ │ │ │ ├── code-pushup.config.ts │ │ │ │ ├── inl-css.component.ts │ │ │ │ ├── inl-scss.component.ts │ │ │ │ ├── styles.css │ │ │ │ ├── styles.scss │ │ │ │ ├── url-css.component.ts │ │ │ │ └── url-scss.component.ts │ │ │ └── template-syntax │ │ │ ├── class-attribute.component.ts │ │ │ ├── class-binding.component.ts │ │ │ ├── code-pushup.config.ts │ │ │ └── ng-class-binding.component.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── core.config.ts │ │ │ ├── index.ts │ │ │ └── lib │ │ │ ├── constants.ts │ │ │ ├── ds-component-coverage.plugin.ts │ │ │ ├── runner │ │ │ │ ├── audits │ │ │ │ │ └── ds-coverage │ │ │ │ │ ├── class-definition.utils.ts │ │ │ │ │ ├── class-definition.visitor.ts │ │ │ │ │ ├── class-definition.visitor.unit.test.ts │ │ │ │ │ ├── class-usage.utils.ts │ │ │ │ │ ├── class-usage.visitor.spec.ts │ │ │ │ │ ├── class-usage.visitor.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── ds-coverage.audit.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── create-runner.ts │ │ │ │ └── schema.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ ├── tsconfig.spec.json │ │ └── vitest.config.mts │ ├── LLMS.md │ ├── models │ │ ├── ai │ │ │ ├── API.md │ │ │ ├── EXAMPLES.md │ │ │ └── FUNCTIONS.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── index.ts │ │ │ └── lib │ │ │ ├── cli.ts │ │ │ ├── diagnostics.ts │ │ │ └── mcp.ts │ │ ├── tsconfig.json │ │ └── tsconfig.lib.json │ ├── styles-ast-utils │ │ ├── .spec.swcrc │ │ ├── ai │ │ │ ├── API.md │ │ │ ├── EXAMPLES.md │ │ │ └── FUNCTIONS.md │ │ ├── jest.config.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── index.ts │ │ │ └── lib │ │ │ ├── postcss-safe-parser.d.ts │ │ │ ├── styles-ast-utils.spec.ts │ │ │ ├── styles-ast-utils.ts │ │ │ ├── stylesheet.parse.ts │ │ │ ├── stylesheet.parse.unit.test.ts │ │ │ ├── stylesheet.visitor.ts │ │ │ ├── stylesheet.walk.ts │ │ │ ├── types.ts │ │ │ ├── utils.ts │ │ │ └── utils.unit.test.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ ├── tsconfig.spec.json │ │ └── vitest.config.mts │ ├── typescript-ast-utils │ │ ├── .spec.swcrc │ │ ├── ai │ │ │ ├── API.md │ │ │ ├── EXAMPLES.md │ │ │ └── FUNCTIONS.md │ │ ├── jest.config.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── index.ts │ │ │ └── lib │ │ │ ├── constants.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ ├── tsconfig.spec.json │ │ └── vitest.config.mts │ └── utils │ ├── .spec.swcrc │ ├── ai │ │ ├── API.md │ │ ├── EXAMPLES.md │ │ └── FUNCTIONS.md │ ├── package.json │ ├── README.md │ ├── src │ │ ├── index.ts │ │ └── lib │ │ ├── execute-process.ts │ │ ├── execute-process.unit.test.ts │ │ ├── file │ │ │ ├── default-export-loader.spec.ts │ │ │ ├── default-export-loader.ts │ │ │ ├── file.resolver.ts │ │ │ └── find-in-file.ts │ │ ├── format-command-log.integration.test.ts │ │ ├── format-command-log.ts │ │ ├── logging.ts │ │ └── utils.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ ├── vite.config.ts │ └── vitest.config.mts ├── README.md ├── testing │ ├── setup │ │ ├── eslint.config.mjs │ │ ├── eslint.next.config.mjs │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── index.d.ts │ │ │ ├── index.mjs │ │ │ └── memfs.constants.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ ├── tsconfig.spec.json │ │ ├── vitest.config.mts │ │ └── vitest.integration.config.mts │ ├── utils │ │ ├── eslint.config.mjs │ │ ├── eslint.next.config.mjs │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── index.ts │ │ │ └── lib │ │ │ ├── constants.ts │ │ │ ├── e2e-setup.ts │ │ │ ├── execute-process-helper.mock.ts │ │ │ ├── execute-process.mock.mjs │ │ │ ├── os-agnostic-paths.ts │ │ │ ├── os-agnostic-paths.unit.test.ts │ │ │ ├── source-file-from.code.ts │ │ │ └── string.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ ├── tsconfig.spec.json │ │ ├── vite.config.ts │ │ ├── vitest.config.mts │ │ └── vitest.integration.config.mts │ └── vitest-setup │ ├── eslint.config.mjs │ ├── eslint.next.config.mjs │ ├── package.json │ ├── README.md │ ├── src │ │ ├── index.ts │ │ └── lib │ │ ├── configuration.ts │ │ └── fs-memfs.setup-file.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ ├── vite.config.ts │ ├── vitest.config.mts │ └── vitest.integration.config.mts ├── tools │ ├── nx-advanced-profile.bin.js │ ├── nx-advanced-profile.js │ ├── nx-advanced-profile.postinstall.js │ └── perf_hooks.patch.js ├── tsconfig.base.json ├── tsconfig.json └── vitest.workspace.ts ``` # Files -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/styles/new-styles-10.scss: -------------------------------------------------------------------------------- ```scss 1 | // Example SCSS file with deprecated and non-deprecated classes 2 | .example-class-19 { 3 | border-bottom: 1px solid #ddd; 4 | } 5 | 6 | .form-control-tabs-segmented-v3 { 7 | padding: 5px; 8 | } 9 | 10 | .example-class-20 { 11 | margin-left: 20px; 12 | } 13 | 14 | .form-control-tabs-segmented-v4 { 15 | border-radius: 3px; 16 | } 17 | 18 | .example-class-21 { 19 | color: red; 20 | } 21 | 22 | .example-class-22 { 23 | background-color: green; 24 | } 25 | 26 | .example-class-23 { 27 | font-size: 18px; 28 | } 29 | 30 | .example-class-24 { 31 | padding: 10px; 32 | } 33 | 34 | .example-class-25 { 35 | margin: 5px; 36 | } 37 | 38 | .example-class-26 { 39 | border: 1px solid black; 40 | } 41 | 42 | .example-class-27 { 43 | text-align: left; 44 | } 45 | 46 | .example-class-28 { 47 | line-height: 2; 48 | } 49 | 50 | .example-class-29 { 51 | font-weight: normal; 52 | } 53 | 54 | .example-class-30 { 55 | display: inline-block; 56 | } 57 | 58 | .example-class-31 { 59 | width: 50%; 60 | } 61 | 62 | .example-class-32 { 63 | height: 100px; 64 | } 65 | 66 | .example-class-33 { 67 | overflow: auto; 68 | } 69 | 70 | .example-class-34 { 71 | position: relative; 72 | } 73 | 74 | .example-class-35 { 75 | top: 10px; 76 | } 77 | 78 | .example-class-36 { 79 | left: 20px; 80 | } 81 | 82 | .example-class-37 { 83 | right: 30px; 84 | } 85 | 86 | .example-class-38 { 87 | bottom: 40px; 88 | } 89 | 90 | .example-class-39 { 91 | z-index: 1; 92 | } 93 | 94 | .example-class-40 { 95 | opacity: 0.5; 96 | } 97 | 98 | .example-class-41 { 99 | visibility: hidden; 100 | } 101 | 102 | .example-class-42 { 103 | cursor: pointer; 104 | } 105 | 106 | .example-class-43 { 107 | transition: all 0.3s ease; 108 | } 109 | 110 | .example-class-44 { 111 | transform: rotate(45deg); 112 | } 113 | 114 | .example-class-45 { 115 | animation: fadeIn 1s; 116 | } 117 | 118 | .example-class-46 { 119 | box-sizing: border-box; 120 | } 121 | 122 | .example-class-47 { 123 | content: ''; 124 | } 125 | 126 | .example-class-48 { 127 | clip: rect(0, 0, 0, 0); 128 | } 129 | 130 | .example-class-49 { 131 | float: left; 132 | } 133 | 134 | .example-class-50 { 135 | clear: both; 136 | } 137 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/styles/new-styles-9.scss: -------------------------------------------------------------------------------- ```scss 1 | // Example SCSS file with deprecated and non-deprecated classes 2 | .example-class-17 { 3 | padding-top: 10px; 4 | } 5 | 6 | .form-control-tabs-segmented-flex { 7 | flex-direction: row; 8 | } 9 | 10 | .example-class-18 { 11 | font-size: 16px; 12 | } 13 | 14 | .form-control-tabs-segmented-v2-dark { 15 | background-color: #333; 16 | } 17 | 18 | .example-class-21 { 19 | color: red; 20 | } 21 | 22 | .example-class-22 { 23 | background-color: green; 24 | } 25 | 26 | .example-class-23 { 27 | font-size: 18px; 28 | } 29 | 30 | .example-class-24 { 31 | padding: 10px; 32 | } 33 | 34 | .example-class-25 { 35 | margin: 5px; 36 | } 37 | 38 | .example-class-26 { 39 | border: 1px solid black; 40 | } 41 | 42 | .example-class-27 { 43 | text-align: left; 44 | } 45 | 46 | .example-class-28 { 47 | line-height: 2; 48 | } 49 | 50 | .example-class-29 { 51 | font-weight: normal; 52 | } 53 | 54 | .example-class-30 { 55 | display: inline-block; 56 | } 57 | 58 | .example-class-31 { 59 | width: 50%; 60 | } 61 | 62 | .example-class-32 { 63 | height: 100px; 64 | } 65 | 66 | .example-class-33 { 67 | overflow: auto; 68 | } 69 | 70 | .example-class-34 { 71 | position: relative; 72 | } 73 | 74 | .example-class-35 { 75 | top: 10px; 76 | } 77 | 78 | .example-class-36 { 79 | left: 20px; 80 | } 81 | 82 | .example-class-37 { 83 | right: 30px; 84 | } 85 | 86 | .example-class-38 { 87 | bottom: 40px; 88 | } 89 | 90 | .example-class-39 { 91 | z-index: 1; 92 | } 93 | 94 | .example-class-40 { 95 | opacity: 0.5; 96 | } 97 | 98 | .example-class-41 { 99 | visibility: hidden; 100 | } 101 | 102 | .example-class-42 { 103 | cursor: pointer; 104 | } 105 | 106 | .example-class-43 { 107 | transition: all 0.3s ease; 108 | } 109 | 110 | .example-class-44 { 111 | transform: rotate(45deg); 112 | } 113 | 114 | .example-class-45 { 115 | animation: fadeIn 1s; 116 | } 117 | 118 | .example-class-46 { 119 | box-sizing: border-box; 120 | } 121 | 122 | .example-class-47 { 123 | content: ''; 124 | } 125 | 126 | .example-class-48 { 127 | clip: rect(0, 0, 0, 0); 128 | } 129 | 130 | .example-class-49 { 131 | float: left; 132 | } 133 | 134 | .example-class-50 { 135 | clear: both; 136 | } 137 | ``` -------------------------------------------------------------------------------- /packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/utils.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Audit, AuditOutput, Issue } from '@code-pushup/models'; 2 | import { pluralize, slugify } from '@code-pushup/utils'; 3 | import { ComponentReplacement } from './schema.js'; 4 | 5 | /** 6 | * Creates a scored audit output. 7 | * @returns Audit output. 8 | * @param componentReplacements 9 | */ 10 | export function getCompUsageAudits( 11 | componentReplacements: ComponentReplacement[], 12 | ): Audit[] { 13 | return componentReplacements.map((comp) => ({ 14 | slug: getCompCoverageAuditSlug(comp), 15 | title: getCompCoverageAuditTitle(comp), 16 | description: getCompCoverageAuditDescription(comp), 17 | })); 18 | } 19 | 20 | /** 21 | * Creates a scored audit output. 22 | * @param componentName 23 | * @param issues 24 | * @returns Audit output. 25 | */ 26 | export function getCompCoverageAuditOutput( 27 | componentName: ComponentReplacement, 28 | issues: Issue[], 29 | ): AuditOutput { 30 | return { 31 | slug: getCompCoverageAuditSlug(componentName), 32 | displayValue: `${issues.length} ${pluralize('class', issues.length)} found`, 33 | score: issues.length === 0 ? 1 : 0, 34 | value: issues.length, 35 | details: { 36 | issues, 37 | }, 38 | }; 39 | } 40 | 41 | export function getCompCoverageAuditSlug({ 42 | componentName, 43 | }: ComponentReplacement): string { 44 | return slugify(`coverage-${componentName}`); 45 | } 46 | export function getCompCoverageAuditTitle({ 47 | componentName, 48 | }: ComponentReplacement): string { 49 | return `Usage coverage for ${componentName} component`; 50 | } 51 | export function getCompCoverageAuditDescription({ 52 | componentName, 53 | deprecatedCssClasses, 54 | }: ComponentReplacement): string { 55 | return `Coverage audit for ${componentName} component. Matching classes: ${deprecatedCssClasses.join( 56 | ', ', 57 | )}`; 58 | } 59 | ``` -------------------------------------------------------------------------------- /packages/shared/utils/src/lib/file/file.resolver.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { existsSync } from 'fs'; 2 | import { readFile } from 'fs/promises'; 3 | import * as path from 'path'; 4 | 5 | export const fileResolverCache = new Map<string, Promise<string>>(); 6 | 7 | /** 8 | * Resolves a file content from the file system, caching the result 9 | * to avoid reading the same file multiple times. 10 | * 11 | * This function returns a Promise that resolves to the file content. 12 | * This is important to avoid reading the same file multiple times. 13 | * @param filePath 14 | */ 15 | export async function resolveFileCached(filePath: string): Promise<string> { 16 | const normalizedPath = path.normalize(filePath); 17 | if (!existsSync(normalizedPath)) { 18 | throw new Error(`File not found: ${normalizedPath}`); 19 | } 20 | 21 | if (fileResolverCache.has(normalizedPath)) { 22 | const cachedPromise = fileResolverCache.get(normalizedPath); 23 | if (cachedPromise) { 24 | return cachedPromise; 25 | } 26 | } 27 | 28 | const fileReadOperationPromise = resolveFile(filePath) 29 | .then((content) => { 30 | fileResolverCache.set(normalizedPath, Promise.resolve(content)); 31 | return content; 32 | }) 33 | .catch((ctx) => { 34 | fileResolverCache.delete(normalizedPath); 35 | throw ctx; 36 | }); 37 | 38 | fileResolverCache.set(normalizedPath, fileReadOperationPromise); 39 | return fileReadOperationPromise; 40 | } 41 | 42 | /** 43 | * Resolves a file content from the file system directly, bypassing any cache. 44 | * 45 | * @param filePath 46 | */ 47 | export async function resolveFile(filePath: string): Promise<string> { 48 | const normalizedPath = path.normalize(filePath); 49 | if (!existsSync(normalizedPath)) { 50 | throw new Error(`File not found: ${normalizedPath}`); 51 | } 52 | return readFile(normalizedPath, 'utf-8'); 53 | } 54 | ``` -------------------------------------------------------------------------------- /tools/nx-advanced-profile.js: -------------------------------------------------------------------------------- ```javascript 1 | import { fork } from 'node:child_process'; 2 | import { fileURLToPath } from 'node:url'; 3 | 4 | export async function nxRunWithPerfLogging( 5 | args, 6 | { 7 | verbose = false, 8 | noPatch = false, 9 | onData = () => {}, 10 | onTraceEvent = () => {}, 11 | onMetadata = () => {}, 12 | beforeExit = () => {}, 13 | } = {}, 14 | ) { 15 | const patch = !noPatch; 16 | const nxUrl = await import.meta.resolve('nx'); 17 | const nxPath = fileURLToPath(nxUrl); 18 | 19 | const profile = { 20 | metadata: {}, 21 | traceEvents: [], 22 | }; 23 | 24 | const forkArgs = [ 25 | nxPath, 26 | args, 27 | { 28 | stdio: ['pipe', 'pipe', 'pipe', 'ipc'], 29 | env: { 30 | ...process.env, 31 | NX_DAEMON: 'false', 32 | NX_CACHE: 'false', 33 | NX_PERF_LOGGING: 'true', 34 | }, 35 | // Preload the patch file so that it applies before NX is loaded. 36 | execArgv: patch ? ['--require', './tools/perf_hooks.patch.js'] : [], 37 | }, 38 | ]; 39 | if (verbose) { 40 | console.log('Forking NX with args:', forkArgs); 41 | } 42 | 43 | const child = fork(...forkArgs); 44 | 45 | child.stdout?.on('data', (data) => { 46 | const lines = data.toString().split('\n'); 47 | for (const line of lines) { 48 | onData(line); 49 | const res = line.split(':JSON:'); 50 | 51 | if (res.length === 2) { 52 | const [prop, jsonString] = res; 53 | const perfProfileEvent = JSON.parse(jsonString?.trim() || '{}'); 54 | if (prop === 'traceEvent') { 55 | onTraceEvent(perfProfileEvent); 56 | profile.traceEvents.push(perfProfileEvent); 57 | } 58 | if (prop === 'metadata') { 59 | onMetadata(perfProfileEvent); 60 | profile.metadata = perfProfileEvent; 61 | } 62 | } 63 | } 64 | }); 65 | 66 | child.on('close', () => { 67 | beforeExit(profile); 68 | }); 69 | } 70 | ``` -------------------------------------------------------------------------------- /packages/shared/utils/src/lib/file/default-export-loader.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { pathToFileURL } from 'node:url'; 2 | 3 | /** 4 | * Dynamically imports an ES Module and extracts the default export. 5 | * 6 | * @param filePath - Absolute path to the ES module file to import 7 | * @returns The default export from the module 8 | * @throws Error if the module cannot be loaded or has no default export 9 | * 10 | * @example 11 | * ```typescript 12 | * const data = await loadDefaultExport('/path/to/config.js'); 13 | * ``` 14 | */ 15 | export async function loadDefaultExport<T = unknown>( 16 | filePath: string, 17 | ): Promise<T> { 18 | try { 19 | const fileUrl = pathToFileURL(filePath).toString(); 20 | 21 | // In test environments (Vitest), use native import to avoid transformation issues 22 | // In production (webpack/bundled), use Function constructor to preserve dynamic import 23 | const isTestEnv = 24 | typeof process !== 'undefined' && 25 | (process.env.NODE_ENV === 'test' || 26 | process.env.VITEST === 'true' || 27 | typeof (globalThis as Record<string, unknown>).vitest !== 'undefined'); 28 | 29 | const module = isTestEnv 30 | ? await import(fileUrl) 31 | : await new Function('url', 'return import(url)')(fileUrl); 32 | 33 | if (!('default' in module)) { 34 | throw new Error( 35 | `No default export found in module. Expected ES Module format:\n` + 36 | `export default [...]\n\n` + 37 | `Available exports: ${Object.keys(module).join(', ') || 'none'}`, 38 | ); 39 | } 40 | 41 | return module.default; 42 | } catch (ctx) { 43 | if ( 44 | ctx instanceof Error && 45 | ctx.message.includes('No default export found') 46 | ) { 47 | throw ctx; 48 | } 49 | throw new Error( 50 | `Failed to load module from ${filePath}: ${ctx instanceof Error ? ctx.message : String(ctx)}`, 51 | ); 52 | } 53 | } 54 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/base-analyzer.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as process from 'node:process'; 2 | import { getDeprecatedCssClasses } from '../../component/utils/deprecated-css-helpers.js'; 3 | import { validateComponentName } from '../utils/component-validation.js'; 4 | import { 5 | BaseViolationOptions, 6 | BaseViolationResult, 7 | ReportCoverageParams, 8 | } from './types.js'; 9 | import { analyzeProjectCoverage as collectFilesViolations } from './coverage-analyzer.js'; 10 | 11 | /** 12 | * Base analyzer for design system violations - shared logic between file and folder reporting 13 | */ 14 | export async function analyzeViolationsBase<T extends BaseViolationResult>( 15 | options: BaseViolationOptions, 16 | ): Promise<T> { 17 | const { 18 | cwd = process.cwd(), 19 | directory, 20 | componentName, 21 | deprecatedCssClassesPath, 22 | } = options; 23 | 24 | validateComponentName(componentName); 25 | 26 | if (!directory || typeof directory !== 'string') { 27 | throw new Error('Directory parameter is required and must be a string'); 28 | } 29 | 30 | process.chdir(cwd); 31 | 32 | if (!deprecatedCssClassesPath) { 33 | throw new Error( 34 | 'Missing ds.deprecatedCssClassesPath. Provide --ds.deprecatedCssClassesPath in mcp.json file.', 35 | ); 36 | } 37 | 38 | const deprecatedCssClasses = await getDeprecatedCssClasses( 39 | componentName, 40 | deprecatedCssClassesPath, 41 | cwd, 42 | ); 43 | 44 | const dsComponents = [ 45 | { 46 | componentName, 47 | deprecatedCssClasses, 48 | }, 49 | ]; 50 | 51 | const params: ReportCoverageParams = { 52 | cwd, 53 | returnRawData: true, 54 | directory, 55 | dsComponents, 56 | }; 57 | 58 | const result = await collectFilesViolations(params); 59 | 60 | if (!result.rawData?.rawPluginResult) { 61 | throw new Error('Failed to get raw plugin result for violation analysis'); 62 | } 63 | 64 | return result.rawData.rawPluginResult as T; 65 | } 66 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/design-system/component-options.mjs: -------------------------------------------------------------------------------- ``` 1 | const dsComponents = [ 2 | { 3 | componentName: 'DsPill', 4 | deprecatedCssClasses: [ 5 | 'pill-with-badge', 6 | 'pill-with-badge-v2', 7 | 'sports-pill', 8 | ], 9 | }, 10 | { 11 | componentName: 'DsBadge', 12 | deprecatedCssClasses: ['offer-badge'], 13 | }, 14 | { 15 | componentName: 'DsTabsModule', 16 | deprecatedCssClasses: ['tab-nav', 'nav-tabs', 'tab-nav-item'], 17 | }, 18 | { 19 | componentName: 'DsButton', 20 | deprecatedCssClasses: ['btn', 'btn-primary', 'legacy-button'], 21 | }, 22 | { 23 | componentName: 'DsModal', 24 | deprecatedCssClasses: ['modal'], 25 | }, 26 | { 27 | componentName: 'DsCard', 28 | deprecatedCssClasses: ['card'], 29 | }, 30 | { 31 | componentName: 'DsLoadingSpinner', 32 | deprecatedCssClasses: ['loading', 'loading-v2', 'loading-v3'], 33 | }, 34 | { 35 | componentName: 'DsCardExpandable', 36 | deprecatedCssClasses: ['collapsible-container'], 37 | }, 38 | { 39 | componentName: 'DsDivider', 40 | deprecatedCssClasses: ['divider'], 41 | }, 42 | { 43 | componentName: 'DsNotificationBubble', 44 | deprecatedCssClasses: ['count', 'badge-circle'], 45 | }, 46 | { 47 | componentName: 'DsCheckbox', 48 | deprecatedCssClasses: ['custom-control-checkbox'], 49 | }, 50 | { 51 | componentName: 'DsRadioModule', 52 | deprecatedCssClasses: ['custom-control-radio'], 53 | }, 54 | { 55 | componentName: 'DsSegmentedControlModule', 56 | deprecatedCssClasses: [ 57 | 'form-control-tabs-segmented-v2', 58 | 'form-control-tabs-segmented-flex', 59 | 'form-control-tabs-segmented-v2-dark', 60 | 'form-control-tabs-segmented-v3', 61 | 'form-control-tabs-segmented-v4', 62 | 'form-control-tabs-segmented', 63 | ], 64 | }, 65 | { 66 | componentName: 'DsSwitch', 67 | deprecatedCssClasses: ['custom-control-switcher'], 68 | }, 69 | ]; 70 | 71 | export default dsComponents; 72 | ``` -------------------------------------------------------------------------------- /testing/vitest-setup/src/lib/fs-memfs.setup-file.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | MockInstance, 3 | afterEach, 4 | beforeEach, 5 | vi, 6 | beforeAll, 7 | afterAll, 8 | } from 'vitest'; 9 | 10 | // Define the constant locally since cross-project imports cause build issues 11 | const MEMFS_VOLUME = '/memfs'; 12 | 13 | /** 14 | * Mocks the fs and fs/promises modules with memfs. 15 | */ 16 | 17 | type Memfs = typeof import('memfs'); 18 | 19 | vi.mock('fs', async () => { 20 | const memfs: Memfs = await vi.importActual('memfs'); 21 | return memfs.fs; 22 | }); 23 | 24 | vi.mock('fs/promises', async () => { 25 | const memfs: Memfs = await vi.importActual('memfs'); 26 | return memfs.fs.promises; 27 | }); 28 | 29 | /** 30 | * Mocks the current working directory to MEMFS_VOLUME. 31 | * This is useful when you use relative paths in your code 32 | * @type {MockInstance<[], string>} 33 | * 34 | * @example 35 | * - `readFile('./file.txt')` reads MEMFS_VOLUME/file.txt 36 | * - `readFile(join(process.cwd(), 'file.txt'))` reads MEMFS_VOLUME/file.txt 37 | * - `readFile('file.txt')` reads file.txt 38 | */ 39 | let cwdSpy: MockInstance; 40 | 41 | // This covers arrange blocks at the top of a "describe" block 42 | beforeAll(() => { 43 | cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(MEMFS_VOLUME); 44 | }); 45 | 46 | // Clear mock usage data in arrange blocks as well as usage of the API in each "it" block. 47 | // docs: https://vitest.dev/api/mock.html#mockclear 48 | beforeEach(() => { 49 | cwdSpy.mockClear(); 50 | }); 51 | 52 | // Restore mock implementation and usage data "it" block 53 | // Mock implementations remain if given. => vi.fn(impl).mockRestore() === vi.fn(impl) 54 | // docs: https://vitest.dev/api/mock.html#mockrestore 55 | afterEach(() => { 56 | cwdSpy.mockRestore(); 57 | }); 58 | 59 | // Restore the original implementation after all "describe" block in a file 60 | // docs: https://vitest.dev/api/mock.html#mockreset 61 | afterAll(() => { 62 | cwdSpy.mockReset(); 63 | }); 64 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/models/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ToolSchemaOptions } from '@push-based/models'; 2 | import { COMMON_ANNOTATIONS } from '../../../shared/index.js'; 3 | 4 | /** 5 | * Schema for building component contracts 6 | */ 7 | export const buildComponentContractSchema: ToolSchemaOptions = { 8 | name: 'build_component_contract', 9 | description: 10 | "Generate a static surface contract for a component's template and SCSS.", 11 | inputSchema: { 12 | type: 'object', 13 | properties: { 14 | saveLocation: { 15 | type: 'string', 16 | description: 17 | 'Path where to save the contract file. Supports both absolute and relative paths.', 18 | }, 19 | templateFile: { 20 | type: 'string', 21 | description: 22 | '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.', 23 | }, 24 | styleFile: { 25 | type: 'string', 26 | description: 27 | '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.', 28 | }, 29 | typescriptFile: { 30 | type: 'string', 31 | description: 32 | 'Path to the TypeScript component file (.ts). Supports both absolute and relative paths.', 33 | }, 34 | dsComponentName: { 35 | type: 'string', 36 | description: 'The name of the design system component being used', 37 | default: '', 38 | }, 39 | }, 40 | required: ['saveLocation', 'typescriptFile'], 41 | }, 42 | annotations: { 43 | title: 'Build Component Contract', 44 | ...COMMON_ANNOTATIONS.readOnly, 45 | }, 46 | }; 47 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component/utils/metadata-helpers.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { validateComponentName } from '../../shared/utils/component-validation.js'; 2 | import { getDeprecatedCssClasses } from './deprecated-css-helpers.js'; 3 | import { getComponentDocs } from './doc-helpers.js'; 4 | import { getComponentPathsInfo } from './paths-helpers.js'; 5 | 6 | export interface ComponentMetadataParams { 7 | componentName: string; 8 | storybookDocsRoot?: string; 9 | deprecatedCssClassesPath?: string; 10 | uiRoot?: string; 11 | cwd?: string; 12 | } 13 | 14 | export interface ComponentMetadataResult { 15 | sourcePath: string | null; 16 | importPath: string | null; 17 | tagName: string | null; 18 | api: string | null; 19 | overview: string | null; 20 | deprecatedCssClasses: string; 21 | } 22 | 23 | export async function analyzeComponentMetadata( 24 | params: ComponentMetadataParams, 25 | ): Promise<ComponentMetadataResult> { 26 | validateComponentName(params.componentName); 27 | 28 | const storybookDocsRoot = params.storybookDocsRoot || 'docs'; 29 | const deprecatedCssClassesPath = 30 | params.deprecatedCssClassesPath || 'deprecated-css-classes.js'; 31 | const uiRoot = params.uiRoot || 'libs/ui'; 32 | const cwd = params.cwd || process.cwd(); 33 | 34 | const documentation = getComponentDocs( 35 | params.componentName, 36 | storybookDocsRoot, 37 | ); 38 | const deprecatedCssClassesList = await getDeprecatedCssClasses( 39 | params.componentName, 40 | deprecatedCssClassesPath, 41 | cwd, 42 | ); 43 | const componentPaths = getComponentPathsInfo( 44 | params.componentName, 45 | uiRoot, 46 | cwd, 47 | ); 48 | 49 | return { 50 | sourcePath: componentPaths.srcPath, 51 | importPath: componentPaths.importPath, 52 | tagName: documentation.tagName, 53 | api: documentation.api, 54 | overview: documentation.overview, 55 | deprecatedCssClasses: JSON.stringify(deprecatedCssClassesList), 56 | }; 57 | } 58 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/project/utils/styles-report-helpers.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as fs from 'node:fs/promises'; 2 | import * as path from 'node:path'; 3 | import { parseStylesheet, visitEachChild } from '@push-based/styles-ast-utils'; 4 | import { findAllFiles } from '@push-based/utils'; 5 | import type { Rule } from 'postcss'; 6 | 7 | export interface StyleFileReport { 8 | filePath: string; 9 | foundClasses: { 10 | className: string; 11 | selector: string; 12 | lineNumber?: number; 13 | }[]; 14 | } 15 | 16 | const STYLE_EXT = new Set(['.css', '.scss', '.sass', '.less']); 17 | 18 | const isStyleFile = (f: string) => STYLE_EXT.has(path.extname(f).toLowerCase()); 19 | const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 20 | 21 | export async function findStyleFiles(dir: string): Promise<string[]> { 22 | const files: string[] = []; 23 | for await (const file of findAllFiles(dir, isStyleFile)) { 24 | files.push(file); 25 | } 26 | return files; 27 | } 28 | 29 | export async function analyzeStyleFile( 30 | filePath: string, 31 | deprecated: string[], 32 | ): Promise<StyleFileReport> { 33 | const css = await fs.readFile(filePath, 'utf8'); 34 | const { root } = await parseStylesheet(css, filePath); 35 | 36 | const found: StyleFileReport['foundClasses'] = []; 37 | const master = new RegExp( 38 | `\\.(${deprecated.map(escapeRegex).join('|')})(?![\\w-])`, 39 | 'g', 40 | ); 41 | 42 | // Handle both Document_ and Root_ types 43 | if (root.type !== 'root') { 44 | return { filePath, foundClasses: found }; 45 | } 46 | 47 | visitEachChild(root, { 48 | visitRule(rule: Rule) { 49 | let match; 50 | while ((match = master.exec(rule.selector)) !== null) { 51 | found.push({ 52 | className: match[1], 53 | selector: rule.selector, 54 | lineNumber: rule.source?.start?.line, 55 | }); 56 | } 57 | master.lastIndex = 0; 58 | }, 59 | }); 60 | 61 | return { filePath, foundClasses: found }; 62 | } 63 | ``` -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- ``` 1 | import nx from '@nx/eslint-plugin'; 2 | import unicorn from 'eslint-plugin-unicorn'; 3 | import tseslint from 'typescript-eslint'; 4 | 5 | export default tseslint.config( 6 | // Base Nx configurations 7 | ...nx.configs['flat/base'], 8 | ...nx.configs['flat/typescript'], 9 | ...nx.configs['flat/javascript'], 10 | 11 | // Global ignores 12 | { 13 | ignores: [ 14 | '**/dist', 15 | '**/vite.config.*.timestamp*', 16 | '**/vitest.config.*.timestamp*', 17 | '**/mocks/**', 18 | '**/fixtures/**', 19 | '**/__snapshot__/**', 20 | '**/__snapshots__/**', 21 | '**/__tests__/**', 22 | '**/__mocks__/**', 23 | '**/test-fixtures/**', 24 | '**/e2e/fixtures/**', 25 | ], 26 | }, 27 | 28 | // Nx module boundaries and TypeScript rules 29 | { 30 | files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], 31 | rules: { 32 | '@nx/enforce-module-boundaries': [ 33 | 'error', 34 | { 35 | enforceBuildableLibDependency: true, 36 | allow: ['^.*/eslint(\\.base)?\\.config\\.[cm]?js$'], 37 | depConstraints: [ 38 | { 39 | sourceTag: '*', 40 | onlyDependOnLibsWithTags: ['*'], 41 | }, 42 | ], 43 | }, 44 | ], 45 | '@typescript-eslint/no-unused-vars': [ 46 | 'error', 47 | { 48 | argsIgnorePattern: '^_', 49 | destructuredArrayIgnorePattern: '^_', 50 | ignoreRestSiblings: true, 51 | }, 52 | ], 53 | }, 54 | }, 55 | 56 | // Unicorn plugin rules 57 | { 58 | files: [ 59 | '**/*.ts', 60 | '**/*.tsx', 61 | '**/*.cts', 62 | '**/*.mts', 63 | '**/*.js', 64 | '**/*.jsx', 65 | '**/*.cjs', 66 | '**/*.mjs', 67 | ], 68 | plugins: { 69 | unicorn: unicorn, 70 | }, 71 | rules: { 72 | 'unicorn/prefer-top-level-await': 'error', 73 | 'unicorn/catch-error-name': ['error', { name: 'ctx' }], 74 | }, 75 | }, 76 | ); 77 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component/utils/deprecated-css-helpers.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as fs from 'fs'; 2 | import { resolveCrossPlatformPath } from '../../shared/utils/cross-platform-path.js'; 3 | import { loadDefaultExport } from '@push-based/utils'; 4 | 5 | export interface DeprecatedCssComponent { 6 | componentName: string; 7 | deprecatedCssClasses: string[]; 8 | } 9 | 10 | /** 11 | * Retrieves deprecated CSS classes for a specific component from a configuration file 12 | * @param componentName - The name of the component to get deprecated classes for 13 | * @param deprecatedCssClassesPath - Path to the file containing deprecated CSS classes configuration 14 | * @param cwd - Current working directory 15 | * @returns Array of deprecated CSS classes for the component 16 | * @throws Error if file not found, invalid format, or component not found 17 | */ 18 | export async function getDeprecatedCssClasses( 19 | componentName: string, 20 | deprecatedCssClassesPath: string, 21 | cwd: string, 22 | ): Promise<string[]> { 23 | if ( 24 | !deprecatedCssClassesPath || 25 | typeof deprecatedCssClassesPath !== 'string' 26 | ) { 27 | throw new Error('deprecatedCssClassesPath must be a string path'); 28 | } 29 | 30 | const absPath = resolveCrossPlatformPath(cwd, deprecatedCssClassesPath); 31 | if (!fs.existsSync(absPath)) { 32 | throw new Error(`File not found at deprecatedCssClassesPath: ${absPath}`); 33 | } 34 | 35 | const dsComponents = 36 | await loadDefaultExport<DeprecatedCssComponent[]>(absPath); 37 | 38 | if (!Array.isArray(dsComponents)) { 39 | throw new Error('Invalid export: expected dsComponents to be an array'); 40 | } 41 | 42 | const componentData = dsComponents.find( 43 | (item) => item.componentName === componentName, 44 | ); 45 | 46 | if (!componentData) { 47 | throw new Error( 48 | `No deprecated classes found for component: ${componentName}`, 49 | ); 50 | } 51 | 52 | return componentData.deprecatedCssClasses; 53 | } 54 | ``` -------------------------------------------------------------------------------- /testing/utils/src/lib/e2e-setup.ts: -------------------------------------------------------------------------------- ```typescript 1 | import path from 'node:path'; 2 | import fs from 'node:fs/promises'; 3 | 4 | export function getE2eAppProjectName(): string | undefined { 5 | const e2eProjectName = process.env['NX_TASK_TARGET_PROJECT'] ?? ''; 6 | if (e2eProjectName == null) { 7 | console.warn('NX_TASK_TARGET_PROJECT is not set.'); 8 | } 9 | return e2eProjectName ? `${e2eProjectName}-app` : undefined; 10 | } 11 | 12 | export async function setupE2eApp( 13 | fixtureProjectName: string, 14 | e2eFixtures?: string, 15 | ) { 16 | const targetProjectName = 17 | getE2eAppProjectName() ?? fixtureProjectName + '-e2e-app'; 18 | const fixture = path.join( 19 | __dirname, 20 | `../../../e2e/fixtures/${fixtureProjectName}`, 21 | ); 22 | const target = path.join( 23 | __dirname, 24 | `../../../e2e/__test__/${targetProjectName}`, 25 | ); 26 | try { 27 | await fs.stat(fixture); 28 | } catch (ctx) { 29 | console.warn( 30 | `Fixture folder not found. Did you change the file or move it? Error: ${ 31 | (ctx as Error).message 32 | }`, 33 | ); 34 | return; 35 | } 36 | 37 | // copy fixtures folder 38 | await fs.rm(target, { recursive: true, force: true }); 39 | await fs.cp(fixture, target, { 40 | recursive: true, 41 | force: true, 42 | filter(source: string, _: string): boolean | Promise<boolean> { 43 | return !source.includes('node_modules') && !source.includes('dist'); 44 | }, 45 | }); 46 | 47 | // adjust package.json#nx to new location and rename project 48 | const packageJson = ( 49 | await fs.readFile(path.join(target, 'package.json'), 'utf-8') 50 | ).toString(); 51 | await fs.writeFile( 52 | path.join(target, 'package.json'), 53 | packageJson 54 | .replaceAll('fixtures', '__test__') 55 | .replaceAll(fixtureProjectName, targetProjectName), 56 | ); 57 | // add e2e fixtures 58 | if (e2eFixtures) { 59 | await fs.cp(e2eFixtures, target, { 60 | recursive: true, 61 | force: true, 62 | }); 63 | } 64 | } 65 | ``` -------------------------------------------------------------------------------- /packages/shared/utils/src/lib/utils.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { CliArgsObject, ArgumentValue } from '@push-based/models'; 2 | 3 | /** 4 | * Converts an object with different types of values into an array of command-line arguments. 5 | * 6 | * @example 7 | * const args = objectToCliArgs({ 8 | * _: ['node', 'index.js'], // node index.js 9 | * name: 'Juanita', // --name=Juanita 10 | * formats: ['json', 'md'] // --format=json --format=md 11 | * }); 12 | */ 13 | export function objectToCliArgs< 14 | T extends object = Record<string, ArgumentValue>, 15 | >(params?: CliArgsObject<T>): string[] { 16 | if (!params) { 17 | return []; 18 | } 19 | 20 | return Object.entries(params).flatMap(([key, value]) => { 21 | // process/file/script 22 | if (key === '_') { 23 | return Array.isArray(value) ? value : [`${value}`]; 24 | } 25 | const prefix = key.length === 1 ? '-' : '--'; 26 | // "-*" arguments (shorthands) 27 | if (Array.isArray(value)) { 28 | return value.map((v) => `${prefix}${key}="${v}"`); 29 | } 30 | // "--*" arguments ========== 31 | 32 | if (Array.isArray(value)) { 33 | return value.map((v) => `${prefix}${key}="${v}"`); 34 | } 35 | 36 | if (typeof value === 'object') { 37 | return Object.entries(value as Record<string, ArgumentValue>).flatMap( 38 | // transform nested objects to the dot notation `key.subkey` 39 | ([k, v]) => objectToCliArgs({ [`${key}.${k}`]: v }), 40 | ); 41 | } 42 | 43 | if (typeof value === 'string') { 44 | return [`${prefix}${key}="${value}"`]; 45 | } 46 | 47 | if (typeof value === 'number') { 48 | return [`${prefix}${key}=${value}`]; 49 | } 50 | 51 | if (typeof value === 'boolean') { 52 | return [`${prefix}${value ? '' : 'no-'}${key}`]; 53 | } 54 | 55 | throw new Error(`Unsupported type ${typeof value} for key ${key}`); 56 | }); 57 | } 58 | 59 | export function calcDuration(start: number, stop?: number): number { 60 | return Math.round((stop ?? performance.now()) - start); 61 | } 62 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/report-violations/report-violations.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | createHandler, 3 | BaseHandlerOptions, 4 | RESULT_FORMATTERS, 5 | } from '../shared/utils/handler-helpers.js'; 6 | import { 7 | createViolationReportingSchema, 8 | COMMON_ANNOTATIONS, 9 | } from '../shared/models/schema-helpers.js'; 10 | import { analyzeViolationsBase } from '../shared/violation-analysis/base-analyzer.js'; 11 | import { formatViolations } from '../shared/violation-analysis/formatters.js'; 12 | import { ViolationResult } from './models/types.js'; 13 | 14 | interface ReportViolationsOptions extends BaseHandlerOptions { 15 | directory: string; 16 | componentName: string; 17 | groupBy?: 'file' | 'folder'; 18 | } 19 | 20 | export const reportViolationsSchema = { 21 | name: 'report-violations', 22 | description: `Report deprecated DS CSS usage in a directory with configurable grouping format.`, 23 | inputSchema: createViolationReportingSchema(), 24 | annotations: { 25 | title: 'Report Violations', 26 | ...COMMON_ANNOTATIONS.readOnly, 27 | }, 28 | }; 29 | 30 | export const reportViolationsHandler = createHandler< 31 | ReportViolationsOptions, 32 | string[] 33 | >( 34 | reportViolationsSchema.name, 35 | async (params) => { 36 | // Default to 'file' grouping if not specified 37 | const groupBy = params.groupBy || 'file'; 38 | 39 | const result = await analyzeViolationsBase<ViolationResult>(params); 40 | 41 | const formattedContent = formatViolations(result, params.directory, { 42 | groupBy, 43 | }); 44 | 45 | // Extract text content from the formatted violations 46 | const violationLines = formattedContent.map((item) => { 47 | if (item.type === 'text') { 48 | return item.text; 49 | } 50 | return String(item); 51 | }); 52 | 53 | return violationLines; 54 | }, 55 | (result) => RESULT_FORMATTERS.list(result, 'Design System Violations:'), 56 | ); 57 | 58 | export const reportViolationsTools = [ 59 | { 60 | schema: reportViolationsSchema, 61 | handler: reportViolationsHandler, 62 | }, 63 | ]; 64 | ``` -------------------------------------------------------------------------------- /packages/shared/typescript-ast-utils/ai/API.md: -------------------------------------------------------------------------------- ```markdown 1 | # TypeScript AST Utils 2 | 3 | Comprehensive **TypeScript AST manipulation utilities** for working with TypeScript compiler API decorators, nodes, and source file analysis. 4 | 5 | ## Minimal usage 6 | 7 | ```ts 8 | import { 9 | isComponentDecorator, 10 | getDecorators, 11 | removeQuotes, 12 | } from '@push-based/typescript-ast-utils'; 13 | import * as ts from 'typescript'; 14 | 15 | // Check if a decorator is a Component decorator 16 | const isComponent = isComponentDecorator(decorator); 17 | 18 | // Get all decorators from a node 19 | const decorators = getDecorators(classNode); 20 | 21 | // Remove quotes from a string literal node 22 | const cleanText = removeQuotes(stringNode, sourceFile); 23 | ``` 24 | 25 | ## Key Features 26 | 27 | - **Decorator Analysis**: Utilities for identifying and working with TypeScript decorators 28 | - **Node Inspection**: Functions to safely extract decorators from AST nodes 29 | - **String Processing**: Tools for cleaning quoted strings from AST nodes 30 | - **Type Guards**: Type-safe functions for checking node properties 31 | - **Cross-Version Compatibility**: Handles different TypeScript compiler API versions 32 | 33 | ## Use Cases 34 | 35 | - **Angular Component Analysis**: Identify `@Component` decorators in Angular applications 36 | - **AST Traversal**: Safely navigate TypeScript abstract syntax trees 37 | - **Code Analysis Tools**: Build static analysis tools for TypeScript codebases 38 | - **Decorator Processing**: Extract and process decorator metadata 39 | - **Source Code Transformation**: Clean and manipulate string literals from source files 40 | 41 | ## Documentation map 42 | 43 | | Doc | What you'll find | 44 | | ------------------------------ | ------------------------------------------- | 45 | | [FUNCTIONS.md](./FUNCTIONS.md) | A–Z quick reference for every public symbol | 46 | | [EXAMPLES.md](./EXAMPLES.md) | Runnable scenarios with expected output | 47 | 48 | ``` 49 | 50 | ``` 51 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/project/get-project-dependencies.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ToolSchemaOptions } from '@push-based/models'; 2 | import { 3 | createHandler, 4 | BaseHandlerOptions, 5 | } from '../shared/utils/handler-helpers.js'; 6 | import { 7 | createProjectAnalysisSchema, 8 | COMMON_ANNOTATIONS, 9 | } from '../shared/models/schema-helpers.js'; 10 | import { analyzeProjectDependencies } from './utils/dependencies-helpers.js'; 11 | import { validateComponentName } from '../shared/utils/component-validation.js'; 12 | 13 | interface ProjectDependenciesOptions extends BaseHandlerOptions { 14 | directory: string; 15 | componentName?: string; 16 | workspaceRoot?: string; 17 | uiRoot?: string; 18 | } 19 | 20 | export const getProjectDependenciesSchema: ToolSchemaOptions = { 21 | name: 'get-project-dependencies', 22 | description: ` 23 | Analyze project dependencies and detect if library is buildable/publishable. 24 | Checks for peer dependencies and validates import paths for DS components. 25 | `, 26 | inputSchema: createProjectAnalysisSchema({ 27 | componentName: { 28 | type: 'string', 29 | description: 30 | 'Optional component name to validate import path for (e.g., DsButton)', 31 | }, 32 | }), 33 | annotations: { 34 | title: 'Get Project Dependencies', 35 | ...COMMON_ANNOTATIONS.readOnly, 36 | }, 37 | }; 38 | 39 | export const getProjectDependenciesHandler = createHandler< 40 | ProjectDependenciesOptions, 41 | any 42 | >( 43 | getProjectDependenciesSchema.name, 44 | async (params, { cwd, workspaceRoot, uiRoot }) => { 45 | const { directory, componentName } = params; 46 | 47 | if (componentName) { 48 | validateComponentName(componentName); 49 | } 50 | 51 | return await analyzeProjectDependencies( 52 | cwd, 53 | directory, 54 | componentName, 55 | workspaceRoot, 56 | uiRoot, 57 | ); 58 | }, 59 | (result) => [JSON.stringify(result, null, 2)], 60 | ); 61 | 62 | export const getProjectDependenciesTools = [ 63 | { 64 | schema: getProjectDependenciesSchema, 65 | handler: getProjectDependenciesHandler, 66 | }, 67 | ]; 68 | ``` -------------------------------------------------------------------------------- /packages/shared/utils/src/lib/format-command-log.integration.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import path from 'node:path'; 2 | import { describe, expect, it } from 'vitest'; 3 | import { removeColorCodes } from '@push-based/testing-utils'; 4 | import { formatCommandLog } from './format-command-log.js'; 5 | 6 | describe('formatCommandLog', () => { 7 | it('should format simple command', () => { 8 | const result = removeColorCodes( 9 | formatCommandLog('npx', ['command', '--verbose']), 10 | ); 11 | 12 | expect(result).toBe('$ npx command --verbose'); 13 | }); 14 | 15 | it('should format simple command with explicit process.cwd()', () => { 16 | const result = removeColorCodes( 17 | formatCommandLog('npx', ['command', '--verbose'], process.cwd()), 18 | ); 19 | 20 | expect(result).toBe('$ npx command --verbose'); 21 | }); 22 | 23 | it('should format simple command with relative cwd', () => { 24 | const result = removeColorCodes( 25 | formatCommandLog('npx', ['command', '--verbose'], './wololo'), 26 | ); 27 | 28 | expect(result).toBe(`wololo $ npx command --verbose`); 29 | }); 30 | 31 | it('should format simple command with absolute non-current path converted to relative', () => { 32 | const result = removeColorCodes( 33 | formatCommandLog( 34 | 'npx', 35 | ['command', '--verbose'], 36 | path.join(process.cwd(), 'tmp'), 37 | ), 38 | ); 39 | expect(result).toBe('tmp $ npx command --verbose'); 40 | }); 41 | 42 | it('should format simple command with relative cwd in parent folder', () => { 43 | const result = removeColorCodes( 44 | formatCommandLog('npx', ['command', '--verbose'], '..'), 45 | ); 46 | 47 | expect(result).toBe(`.. $ npx command --verbose`); 48 | }); 49 | 50 | it('should format simple command using relative path to parent directory', () => { 51 | const result = removeColorCodes( 52 | formatCommandLog( 53 | 'npx', 54 | ['command', '--verbose'], 55 | path.dirname(process.cwd()), 56 | ), 57 | ); 58 | 59 | expect(result).toBe('.. $ npx command --verbose'); 60 | }); 61 | }); 62 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/group-3/lazy-loader-3.component.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | Component, 3 | ViewContainerRef, 4 | ComponentRef, 5 | OnInit, 6 | } from '@angular/core'; 7 | 8 | @Component({ 9 | selector: 'app-lazy-loader', 10 | template: ` 11 | <div> 12 | <h2>Lazy Component Loader</h2> 13 | <button (click)="loadComponent()" [disabled]="isLoading"> 14 | {{ isLoading ? 'Loading...' : 'Load External Assets Component' }} 15 | </button> 16 | <div #dynamicContainer></div> 17 | </div> 18 | `, 19 | styles: [ 20 | ` 21 | button { 22 | padding: 10px 20px; 23 | margin: 10px 0; 24 | background-color: #007bff; 25 | color: white; 26 | border: none; 27 | border-radius: 4px; 28 | cursor: pointer; 29 | } 30 | button:disabled { 31 | background-color: #6c757d; 32 | cursor: not-allowed; 33 | } 34 | `, 35 | ], 36 | }) 37 | export class LazyLoaderComponent3 implements OnInit { 38 | isLoading = false; 39 | private componentRef?: ComponentRef<any>; 40 | 41 | constructor(private viewContainerRef: ViewContainerRef) {} 42 | 43 | ngOnInit() { 44 | console.log('LazyLoaderComponent initialized'); 45 | } 46 | 47 | async loadComponent() { 48 | if (this.isLoading || this.componentRef) { 49 | return; 50 | } 51 | 52 | this.isLoading = true; 53 | 54 | try { 55 | // Dynamic import statement - this is the key part for lazy loading 56 | const { BadMixedExternalAssetsComponent3 } = await import( 57 | './bad-mixed-external-assets-3.component' 58 | ); 59 | 60 | // Clear the container 61 | this.viewContainerRef.clear(); 62 | 63 | // Create the component dynamically 64 | this.componentRef = this.viewContainerRef.createComponent( 65 | BadMixedExternalAssetsComponent3 66 | ); 67 | 68 | console.log('Component loaded successfully'); 69 | } catch (error) { 70 | console.error('Failed to load component:', error); 71 | } finally { 72 | this.isLoading = false; 73 | } 74 | } 75 | 76 | ngOnDestroy() { 77 | if (this.componentRef) { 78 | this.componentRef.destroy(); 79 | } 80 | } 81 | } 82 | ``` -------------------------------------------------------------------------------- /packages/shared/angular-ast-utils/src/lib/decorator-config.visitor.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { classDecoratorVisitor } from './decorator-config.visitor.js'; 2 | import * as ts from 'typescript'; 3 | 4 | describe('DecoratorConfigVisitor', () => { 5 | it.skip('should not find class when it is not a class-binding', async () => { 6 | const sourceCode = ` 7 | class Example { 8 | method() {} 9 | } 10 | 11 | function greet() { 12 | console.log('Hello'); 13 | } 14 | 15 | let x = 123; 16 | `; 17 | 18 | const sourceFile = ts.createSourceFile( 19 | 'example.ts', 20 | sourceCode, 21 | ts.ScriptTarget.Latest, 22 | true, 23 | ); 24 | 25 | const visitor = await classDecoratorVisitor({ sourceFile }); 26 | ts.visitEachChild(sourceFile, visitor, undefined); 27 | 28 | expect(visitor.components).toEqual([]); 29 | }); 30 | 31 | it('should not find class when it is not a class-binding', async () => { 32 | const sourceCode = ` 33 | import { Component } from '@angular/core'; 34 | import { RouterOutlet } from '@angular/router'; 35 | 36 | @Component({ 37 | selector: 'app-root', 38 | imports: [RouterOutlet], 39 | templateUrl: './app.component.html', 40 | styleUrls: ['./app.component.css'] 41 | }) 42 | export class AppComponent { 43 | title = 'minimal'; 44 | } 45 | `; 46 | 47 | const sourceFile = ts.createSourceFile( 48 | 'example.ts', 49 | sourceCode, 50 | ts.ScriptTarget.Latest, 51 | true, 52 | ); 53 | const visitor = await classDecoratorVisitor({ sourceFile }); 54 | 55 | ts.visitEachChild(sourceFile, visitor, undefined); 56 | 57 | expect(visitor.components).toStrictEqual([ 58 | expect.objectContaining({ 59 | className: 'AppComponent', 60 | fileName: 'example.ts', 61 | startLine: 4, 62 | selector: 'app-root', 63 | imports: '[RouterOutlet]', 64 | styleUrls: [ 65 | expect.objectContaining({ 66 | startLine: 8, 67 | filePath: 'example.ts', 68 | }), 69 | ], 70 | templateUrl: expect.objectContaining({ 71 | startLine: 7, 72 | filePath: 'example.ts', 73 | }), 74 | }), 75 | ]); 76 | }); 77 | }); 78 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/validation/ds-components-file.validation.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as path from 'path'; 2 | import { AngularMcpServerOptions } from './angular-mcp-server-options.schema.js'; 3 | import { DsComponentsArraySchema } from './ds-components.schema.js'; 4 | import { loadDefaultExport } from '@push-based/utils'; 5 | 6 | export async function validateDeprecatedCssClassesFile( 7 | config: AngularMcpServerOptions, 8 | ): Promise<void> { 9 | const relPath = config.ds.deprecatedCssClassesPath; 10 | if (!relPath) { 11 | // Optional parameter not provided; nothing to validate 12 | return; 13 | } 14 | const deprecatedCssClassesAbsPath = path.resolve( 15 | config.workspaceRoot, 16 | relPath, 17 | ); 18 | 19 | const dsComponents = await loadDefaultExport(deprecatedCssClassesAbsPath); 20 | 21 | const validation = DsComponentsArraySchema.safeParse(dsComponents); 22 | if (!validation.success) { 23 | const actualType = Array.isArray(dsComponents) 24 | ? 'array' 25 | : typeof dsComponents; 26 | const exportedValue = 27 | dsComponents === undefined 28 | ? 'undefined' 29 | : dsComponents === null 30 | ? 'null' 31 | : JSON.stringify(dsComponents, null, 2).substring(0, 100) + 32 | (JSON.stringify(dsComponents).length > 100 ? '...' : ''); 33 | 34 | throw new Error( 35 | `Invalid deprecated CSS classes configuration format in: ${deprecatedCssClassesAbsPath}\n\n` + 36 | `Expected: Array of component objects\n` + 37 | `Received: ${actualType}\n` + 38 | `Value: ${exportedValue}\n\n` + 39 | `Fix options:\n` + 40 | `1. ES Module format:\n` + 41 | ` export default [\n` + 42 | ` { componentName: 'DsButton', deprecatedCssClasses: ['btn'] }\n` + 43 | ` ];\n\n` + 44 | `2. CommonJS format:\n` + 45 | ` module.exports = {\n` + 46 | ` dsComponents: [{ componentName: 'DsButton', deprecatedCssClasses: ['btn'] }]\n` + 47 | ` };\n\n` + 48 | `Schema errors: ${JSON.stringify(validation.error.format(), null, 2)}`, 49 | ); 50 | } 51 | } 52 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/bad-mixed.component.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-mixed-styles', 5 | template: ` 6 | <!-- ✅ Good: Using DSButton --> 7 | <ds-button>Good Button</ds-button> 8 | 9 | <!-- ❌ Bad: Legacy button class --> 10 | <button class="btn btn-primary">Bad Button</button> 11 | 12 | <!-- ✅ Good: Using DSModal --> 13 | <ds-modal [open]="true"> 14 | <p>Good Modal Content</p> 15 | </ds-modal> 16 | 17 | <!-- ❌ Bad: Custom modal with legacy styles --> 18 | <div class="modal"> 19 | <div class="modal-content"> 20 | <h2>Bad Modal</h2> 21 | <p>This is a legacy modal.</p> 22 | </div> 23 | </div> 24 | 25 | <!-- ✅ Good: DSProgressBar --> 26 | <ds-progress-bar [value]="50"></ds-progress-bar> 27 | 28 | <!-- ❌ Bad: Manually styled progress bar --> 29 | <div class="progress-bar"> 30 | <div class="progress" style="width: 50%;"></div> 31 | </div> 32 | 33 | <!-- ✅ Good: DSDropdown --> 34 | <ds-dropdown [options]="['Option 1', 'Option 2']"></ds-dropdown> 35 | 36 | <!-- ❌ Bad: Legacy dropdown --> 37 | <select class="dropdown"> 38 | <option>Option 1</option> 39 | <option>Option 2</option> 40 | </select> 41 | 42 | <!-- ✅ Good: Using DSAlert --> 43 | <ds-alert type="error"> Good Alert </ds-alert> 44 | 45 | <!-- ❌ Bad: Manually styled alert --> 46 | <div class="alert alert-danger">Bad Alert</div> 47 | 48 | <!-- ✅ Good: Using DSTooltip --> 49 | <ds-tooltip content="Good tooltip">Hover me</ds-tooltip> 50 | 51 | <!-- ❌ Bad: Legacy tooltip --> 52 | <div class="tooltip">Bad tooltip</div> 53 | 54 | <!-- ✅ Good: Using DSBreadcrumb --> 55 | <ds-breadcrumb> 56 | <ds-breadcrumb-item>Home</ds-breadcrumb-item> 57 | <ds-breadcrumb-item>Products</ds-breadcrumb-item> 58 | <ds-breadcrumb-item>Details</ds-breadcrumb-item> 59 | </ds-breadcrumb> 60 | 61 | <!-- ❌ Bad: Manually created breadcrumb --> 62 | <nav class="breadcrumb"> 63 | <span>Home</span> / <span>Products</span> / <span>Details</span> 64 | </nav> 65 | `, 66 | }) 67 | export class MixedStylesComponent {} 68 | ``` -------------------------------------------------------------------------------- /packages/shared/ds-component-coverage/mocks/fixtures/e2e/demo/src/sub-folder-1/sub-folder-2/bad-mixed.component.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-mixed-styles', 5 | template: ` 6 | <!-- ✅ Good: Using DSButton --> 7 | <ds-button>Good Button</ds-button> 8 | 9 | <!-- ❌ Bad: Legacy button class --> 10 | <button class="btn btn-primary">Bad Button</button> 11 | 12 | <!-- ✅ Good: Using DSModal --> 13 | <ds-modal [open]="true"> 14 | <p>Good Modal Content</p> 15 | </ds-modal> 16 | 17 | <!-- ❌ Bad: Custom modal with legacy styles --> 18 | <div class="modal"> 19 | <div class="modal-content"> 20 | <h2>Bad Modal</h2> 21 | <p>This is a legacy modal.</p> 22 | </div> 23 | </div> 24 | 25 | <!-- ✅ Good: DSProgressBar --> 26 | <ds-progress-bar [value]="50"></ds-progress-bar> 27 | 28 | <!-- ❌ Bad: Manually styled progress bar --> 29 | <div class="progress-bar"> 30 | <div class="progress" style="width: 50%;"></div> 31 | </div> 32 | 33 | <!-- ✅ Good: DSDropdown --> 34 | <ds-dropdown [options]="['Option 1', 'Option 2']"></ds-dropdown> 35 | 36 | <!-- ❌ Bad: Legacy dropdown --> 37 | <select class="dropdown"> 38 | <option>Option 1</option> 39 | <option>Option 2</option> 40 | </select> 41 | 42 | <!-- ✅ Good: Using DSAlert --> 43 | <ds-alert type="error"> Good Alert </ds-alert> 44 | 45 | <!-- ❌ Bad: Manually styled alert --> 46 | <div class="alert alert-danger">Bad Alert</div> 47 | 48 | <!-- ✅ Good: Using DSTooltip --> 49 | <ds-tooltip content="Good tooltip">Hover me</ds-tooltip> 50 | 51 | <!-- ❌ Bad: Legacy tooltip --> 52 | <div class="tooltip">Bad tooltip</div> 53 | 54 | <!-- ✅ Good: Using DSBreadcrumb --> 55 | <ds-breadcrumb> 56 | <ds-breadcrumb-item>Home</ds-breadcrumb-item> 57 | <ds-breadcrumb-item>Products</ds-breadcrumb-item> 58 | <ds-breadcrumb-item>Details</ds-breadcrumb-item> 59 | </ds-breadcrumb> 60 | 61 | <!-- ❌ Bad: Manually created breadcrumb --> 62 | <nav class="breadcrumb"> 63 | <span>Home</span> / <span>Products</span> / <span>Details</span> 64 | </nav> 65 | `, 66 | }) 67 | export class MixedStylesComponent {} 68 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/group-1/bad-mixed-1.component.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-mixed-styles', 5 | template: ` 6 | <!-- ✅ Good: Using DSButton --> 7 | <ds-button>Good Button</ds-button> 8 | 9 | <!-- ❌ Bad: Legacy button class --> 10 | <button class="btn btn-primary">Bad Button</button> 11 | 12 | <!-- ✅ Good: Using DSModal --> 13 | <ds-modal [open]="true"> 14 | <p>Good Modal Content</p> 15 | </ds-modal> 16 | 17 | <!-- ❌ Bad: Custom modal with legacy styles --> 18 | <div class="modal"> 19 | <div class="modal-content"> 20 | <h2>Bad Modal</h2> 21 | <p>This is a legacy modal.</p> 22 | </div> 23 | </div> 24 | 25 | <!-- ✅ Good: DSProgressBar --> 26 | <ds-progress-bar [value]="50"></ds-progress-bar> 27 | 28 | <!-- ❌ Bad: Manually styled progress bar --> 29 | <div class="progress-bar"> 30 | <div class="progress" style="width: 50%;"></div> 31 | </div> 32 | 33 | <!-- ✅ Good: DSDropdown --> 34 | <ds-dropdown [options]="['Option 1', 'Option 2']"></ds-dropdown> 35 | 36 | <!-- ❌ Bad: Legacy dropdown --> 37 | <select class="dropdown"> 38 | <option>Option 1</option> 39 | <option>Option 2</option> 40 | </select> 41 | 42 | <!-- ✅ Good: Using DSAlert --> 43 | <ds-alert type="error"> Good Alert </ds-alert> 44 | 45 | <!-- ❌ Bad: Manually styled alert --> 46 | <div class="alert alert-danger">Bad Alert</div> 47 | 48 | <!-- ✅ Good: Using DSTooltip --> 49 | <ds-tooltip content="Good tooltip">Hover me</ds-tooltip> 50 | 51 | <!-- ❌ Bad: Legacy tooltip --> 52 | <div class="tooltip">Bad tooltip</div> 53 | 54 | <!-- ✅ Good: Using DSBreadcrumb --> 55 | <ds-breadcrumb> 56 | <ds-breadcrumb-item>Home</ds-breadcrumb-item> 57 | <ds-breadcrumb-item>Products</ds-breadcrumb-item> 58 | <ds-breadcrumb-item>Details</ds-breadcrumb-item> 59 | </ds-breadcrumb> 60 | 61 | <!-- ❌ Bad: Manually created breadcrumb --> 62 | <nav class="breadcrumb"> 63 | <span>Home</span> / <span>Products</span> / <span>Details</span> 64 | </nav> 65 | `, 66 | }) 67 | export class MixedStylesComponent1 {} 68 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/group-2/bad-mixed-2.component.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-mixed-styles', 5 | template: ` 6 | <!-- ✅ Good: Using DSButton --> 7 | <ds-button>Good Button</ds-button> 8 | 9 | <!-- ❌ Bad: Legacy button class --> 10 | <button class="btn btn-primary">Bad Button</button> 11 | 12 | <!-- ✅ Good: Using DSModal --> 13 | <ds-modal [open]="true"> 14 | <p>Good Modal Content</p> 15 | </ds-modal> 16 | 17 | <!-- ❌ Bad: Custom modal with legacy styles --> 18 | <div class="modal"> 19 | <div class="modal-content"> 20 | <h2>Bad Modal</h2> 21 | <p>This is a legacy modal.</p> 22 | </div> 23 | </div> 24 | 25 | <!-- ✅ Good: DSProgressBar --> 26 | <ds-progress-bar [value]="50"></ds-progress-bar> 27 | 28 | <!-- ❌ Bad: Manually styled progress bar --> 29 | <div class="progress-bar"> 30 | <div class="progress" style="width: 50%;"></div> 31 | </div> 32 | 33 | <!-- ✅ Good: DSDropdown --> 34 | <ds-dropdown [options]="['Option 1', 'Option 2']"></ds-dropdown> 35 | 36 | <!-- ❌ Bad: Legacy dropdown --> 37 | <select class="dropdown"> 38 | <option>Option 1</option> 39 | <option>Option 2</option> 40 | </select> 41 | 42 | <!-- ✅ Good: Using DSAlert --> 43 | <ds-alert type="error"> Good Alert </ds-alert> 44 | 45 | <!-- ❌ Bad: Manually styled alert --> 46 | <div class="alert alert-danger">Bad Alert</div> 47 | 48 | <!-- ✅ Good: Using DSTooltip --> 49 | <ds-tooltip content="Good tooltip">Hover me</ds-tooltip> 50 | 51 | <!-- ❌ Bad: Legacy tooltip --> 52 | <div class="tooltip">Bad tooltip</div> 53 | 54 | <!-- ✅ Good: Using DSBreadcrumb --> 55 | <ds-breadcrumb> 56 | <ds-breadcrumb-item>Home</ds-breadcrumb-item> 57 | <ds-breadcrumb-item>Products</ds-breadcrumb-item> 58 | <ds-breadcrumb-item>Details</ds-breadcrumb-item> 59 | </ds-breadcrumb> 60 | 61 | <!-- ❌ Bad: Manually created breadcrumb --> 62 | <nav class="breadcrumb"> 63 | <span>Home</span> / <span>Products</span> / <span>Details</span> 64 | </nav> 65 | `, 66 | }) 67 | export class MixedStylesComponent2 {} 68 | ``` -------------------------------------------------------------------------------- /packages/shared/styles-ast-utils/src/lib/stylesheet.walk.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Root, Rule } from 'postcss'; 2 | 3 | import { CssAstVisitor } from './stylesheet.visitor.js'; 4 | import { NodeType } from './types.js'; 5 | 6 | /** 7 | * Single function that traverses the AST, calling 8 | * specialized visitor methods as it encounters each node type. 9 | */ 10 | export function visitEachChild<T>(root: Root, visitor: CssAstVisitor<T>) { 11 | visitor.visitRoot?.(root); 12 | 13 | root.walk((node) => { 14 | const visitMethodName = `visit${ 15 | node.type[0].toUpperCase() + node.type.slice(1) 16 | }` as keyof CssAstVisitor<T>; 17 | const visitMethod = visitor[visitMethodName] as 18 | | ((node: NodeType<typeof visitMethodName>) => void) 19 | | undefined; 20 | visitMethod?.(node as NodeType<typeof visitMethodName>); 21 | }); 22 | } 23 | 24 | export function visitStyleSheet<T>(root: Root, visitor: CssAstVisitor<T>) { 25 | for (const node of root.nodes) { 26 | switch (node.type) { 27 | case 'rule': 28 | visitor?.visitRule?.(node); 29 | break; 30 | case 'atrule': 31 | visitor?.visitAtRule?.(node); 32 | break; 33 | case 'decl': 34 | throw new Error('visit declaration not implemented'); 35 | // visitor?.visitDeclaration?.(node); 36 | case 'comment': 37 | visitor?.visitComment?.(node); 38 | break; 39 | default: 40 | throw new Error(`Unknown node type: ${(node as Root).type}`); 41 | } 42 | } 43 | } 44 | 45 | export function visitEachStyleNode<T>( 46 | nodes: Root['nodes'], 47 | visitor: CssAstVisitor<T>, 48 | ) { 49 | for (const node of nodes) { 50 | switch (node.type) { 51 | case 'rule': 52 | visitor?.visitRule?.(node); 53 | visitEachStyleNode((node as Rule).nodes, visitor); 54 | break; 55 | case 'atrule': 56 | visitor?.visitAtRule?.(node); 57 | break; 58 | case 'decl': 59 | visitor?.visitDecl?.(node); 60 | break; 61 | case 'comment': 62 | visitor?.visitComment?.(node); 63 | break; 64 | default: 65 | throw new Error(`Unknown node type: ${(node as Root).type}`); 66 | } 67 | } 68 | } 69 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/utils/inline-styles.collector.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { parseStylesheet, visitEachChild } from '@push-based/styles-ast-utils'; 2 | import { selectorMatches } from './css-match.js'; 3 | import type { 4 | StyleDeclarations, 5 | DomStructure, 6 | } from '../../shared/models/types.js'; 7 | import type { Declaration, Rule } from 'postcss'; 8 | import type { ParsedComponent } from '@push-based/angular-ast-utils'; 9 | 10 | /** 11 | * Collect style rules declared inline via the `styles` property of an 12 | * `@Component` decorator and map them to the DOM snapshot that comes from the 13 | * template. 14 | */ 15 | export async function collectInlineStyles( 16 | component: ParsedComponent, 17 | dom: DomStructure, 18 | ): Promise<StyleDeclarations> { 19 | const styles: StyleDeclarations = { 20 | // Inline styles logically live in the component TS file 21 | sourceFile: component.fileName, 22 | rules: {}, 23 | }; 24 | 25 | if (!component.styles || component.styles.length === 0) { 26 | return styles; 27 | } 28 | 29 | // Combine all inline style strings into one CSS blob 30 | const cssText = ( 31 | await Promise.all(component.styles.map((asset) => asset.parse())) 32 | ) 33 | .map((root) => root.toString()) 34 | .join('\n'); 35 | 36 | if (!cssText.trim()) { 37 | return styles; 38 | } 39 | 40 | const parsed = parseStylesheet(cssText, component.fileName); 41 | if (parsed.root.type !== 'root') { 42 | return styles; 43 | } 44 | 45 | visitEachChild(parsed.root, { 46 | visitRule: (rule: Rule) => { 47 | const properties: Record<string, string> = {}; 48 | 49 | rule.walkDecls?.((decl: Declaration) => { 50 | properties[decl.prop] = decl.value; 51 | }); 52 | 53 | styles.rules[rule.selector] = { 54 | appliesTo: findMatchingDomElements(rule.selector, dom), 55 | properties, 56 | }; 57 | }, 58 | }); 59 | 60 | return styles; 61 | } 62 | 63 | function findMatchingDomElements( 64 | cssSelector: string, 65 | dom: DomStructure, 66 | ): string[] { 67 | return Object.entries(dom) 68 | .filter(([domKey, element]) => 69 | selectorMatches(cssSelector, domKey, element), 70 | ) 71 | .map(([domKey]) => domKey); 72 | } 73 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/bad-mixed-not-standalone.component.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-mixed-styles', 5 | standalone: false, 6 | template: ` 7 | <!-- ✅ Good: Using DSButton --> 8 | <ds-button>Good Button</ds-button> 9 | 10 | <!-- ❌ Bad: Legacy button class --> 11 | <button class="btn btn-primary">Bad Button</button> 12 | 13 | <!-- ✅ Good: Using DSModal --> 14 | <ds-modal [open]="true"> 15 | <p>Good Modal Content</p> 16 | </ds-modal> 17 | 18 | <!-- ❌ Bad: Custom modal with legacy styles --> 19 | <div class="modal"> 20 | <div class="modal-content"> 21 | <h2>Bad Modal</h2> 22 | <p>This is a legacy modal.</p> 23 | </div> 24 | </div> 25 | 26 | <!-- ✅ Good: DSProgressBar --> 27 | <ds-progress-bar [value]="50"></ds-progress-bar> 28 | 29 | <!-- ❌ Bad: Manually styled progress bar --> 30 | <div class="progress-bar"> 31 | <div class="progress" style="width: 50%;"></div> 32 | </div> 33 | 34 | <!-- ✅ Good: DSDropdown --> 35 | <ds-dropdown [options]="['Option 1', 'Option 2']"></ds-dropdown> 36 | 37 | <!-- ❌ Bad: Legacy dropdown --> 38 | <select class="dropdown"> 39 | <option>Option 1</option> 40 | <option>Option 2</option> 41 | </select> 42 | 43 | <!-- ✅ Good: Using DSAlert --> 44 | <ds-alert type="error"> Good Alert </ds-alert> 45 | 46 | <!-- ❌ Bad: Manually styled alert --> 47 | <div class="alert alert-danger">Bad Alert</div> 48 | 49 | <!-- ✅ Good: Using DSTooltip --> 50 | <ds-tooltip content="Good tooltip">Hover me</ds-tooltip> 51 | 52 | <!-- ❌ Bad: Legacy tooltip --> 53 | <div class="tooltip">Bad tooltip</div> 54 | 55 | <!-- ✅ Good: Using DSBreadcrumb --> 56 | <ds-breadcrumb> 57 | <ds-breadcrumb-item>Home</ds-breadcrumb-item> 58 | <ds-breadcrumb-item>Products</ds-breadcrumb-item> 59 | <ds-breadcrumb-item>Details</ds-breadcrumb-item> 60 | </ds-breadcrumb> 61 | 62 | <!-- ❌ Bad: Manually created breadcrumb --> 63 | <nav class="breadcrumb"> 64 | <span>Home</span> / <span>Products</span> / <span>Details</span> 65 | </nav> 66 | `, 67 | }) 68 | export class MixedStylesNotStandaloneComponent {} 69 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/group-1/bad-mixed-not-standalone-1.component.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-mixed-styles', 5 | standalone: false, 6 | template: ` 7 | <!-- ✅ Good: Using DSButton --> 8 | <ds-button>Good Button</ds-button> 9 | 10 | <!-- ❌ Bad: Legacy button class --> 11 | <button class="btn btn-primary">Bad Button</button> 12 | 13 | <!-- ✅ Good: Using DSModal --> 14 | <ds-modal [open]="true"> 15 | <p>Good Modal Content</p> 16 | </ds-modal> 17 | 18 | <!-- ❌ Bad: Custom modal with legacy styles --> 19 | <div class="modal"> 20 | <div class="modal-content"> 21 | <h2>Bad Modal</h2> 22 | <p>This is a legacy modal.</p> 23 | </div> 24 | </div> 25 | 26 | <!-- ✅ Good: DSProgressBar --> 27 | <ds-progress-bar [value]="50"></ds-progress-bar> 28 | 29 | <!-- ❌ Bad: Manually styled progress bar --> 30 | <div class="progress-bar"> 31 | <div class="progress" style="width: 50%;"></div> 32 | </div> 33 | 34 | <!-- ✅ Good: DSDropdown --> 35 | <ds-dropdown [options]="['Option 1', 'Option 2']"></ds-dropdown> 36 | 37 | <!-- ❌ Bad: Legacy dropdown --> 38 | <select class="dropdown"> 39 | <option>Option 1</option> 40 | <option>Option 2</option> 41 | </select> 42 | 43 | <!-- ✅ Good: Using DSAlert --> 44 | <ds-alert type="error"> Good Alert </ds-alert> 45 | 46 | <!-- ❌ Bad: Manually styled alert --> 47 | <div class="alert alert-danger">Bad Alert</div> 48 | 49 | <!-- ✅ Good: Using DSTooltip --> 50 | <ds-tooltip content="Good tooltip">Hover me</ds-tooltip> 51 | 52 | <!-- ❌ Bad: Legacy tooltip --> 53 | <div class="tooltip">Bad tooltip</div> 54 | 55 | <!-- ✅ Good: Using DSBreadcrumb --> 56 | <ds-breadcrumb> 57 | <ds-breadcrumb-item>Home</ds-breadcrumb-item> 58 | <ds-breadcrumb-item>Products</ds-breadcrumb-item> 59 | <ds-breadcrumb-item>Details</ds-breadcrumb-item> 60 | </ds-breadcrumb> 61 | 62 | <!-- ❌ Bad: Manually created breadcrumb --> 63 | <nav class="breadcrumb"> 64 | <span>Home</span> / <span>Products</span> / <span>Details</span> 65 | </nav> 66 | `, 67 | }) 68 | export class MixedStylesNotStandaloneComponent1 {} 69 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/group-2/bad-mixed-not-standalone-2.component.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-mixed-styles', 5 | standalone: false, 6 | template: ` 7 | <!-- ✅ Good: Using DSButton --> 8 | <ds-button>Good Button</ds-button> 9 | 10 | <!-- ❌ Bad: Legacy button class --> 11 | <button class="btn btn-primary">Bad Button</button> 12 | 13 | <!-- ✅ Good: Using DSModal --> 14 | <ds-modal [open]="true"> 15 | <p>Good Modal Content</p> 16 | </ds-modal> 17 | 18 | <!-- ❌ Bad: Custom modal with legacy styles --> 19 | <div class="modal"> 20 | <div class="modal-content"> 21 | <h2>Bad Modal</h2> 22 | <p>This is a legacy modal.</p> 23 | </div> 24 | </div> 25 | 26 | <!-- ✅ Good: DSProgressBar --> 27 | <ds-progress-bar [value]="50"></ds-progress-bar> 28 | 29 | <!-- ❌ Bad: Manually styled progress bar --> 30 | <div class="progress-bar"> 31 | <div class="progress" style="width: 50%;"></div> 32 | </div> 33 | 34 | <!-- ✅ Good: DSDropdown --> 35 | <ds-dropdown [options]="['Option 1', 'Option 2']"></ds-dropdown> 36 | 37 | <!-- ❌ Bad: Legacy dropdown --> 38 | <select class="dropdown"> 39 | <option>Option 1</option> 40 | <option>Option 2</option> 41 | </select> 42 | 43 | <!-- ✅ Good: Using DSAlert --> 44 | <ds-alert type="error"> Good Alert </ds-alert> 45 | 46 | <!-- ❌ Bad: Manually styled alert --> 47 | <div class="alert alert-danger">Bad Alert</div> 48 | 49 | <!-- ✅ Good: Using DSTooltip --> 50 | <ds-tooltip content="Good tooltip">Hover me</ds-tooltip> 51 | 52 | <!-- ❌ Bad: Legacy tooltip --> 53 | <div class="tooltip">Bad tooltip</div> 54 | 55 | <!-- ✅ Good: Using DSBreadcrumb --> 56 | <ds-breadcrumb> 57 | <ds-breadcrumb-item>Home</ds-breadcrumb-item> 58 | <ds-breadcrumb-item>Products</ds-breadcrumb-item> 59 | <ds-breadcrumb-item>Details</ds-breadcrumb-item> 60 | </ds-breadcrumb> 61 | 62 | <!-- ❌ Bad: Manually created breadcrumb --> 63 | <nav class="breadcrumb"> 64 | <span>Home</span> / <span>Products</span> / <span>Details</span> 65 | </nav> 66 | `, 67 | }) 68 | export class MixedStylesNotStandaloneComponent2 {} 69 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/group-3/bad-mixed-not-standalone-3.component.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-mixed-styles', 5 | standalone: false, 6 | template: ` 7 | <!-- ✅ Good: Using DSButton --> 8 | <ds-button>Good Button</ds-button> 9 | 10 | <!-- ❌ Bad: Legacy button class --> 11 | <button class="btn btn-primary">Bad Button</button> 12 | 13 | <!-- ✅ Good: Using DSModal --> 14 | <ds-modal [open]="true"> 15 | <p>Good Modal Content</p> 16 | </ds-modal> 17 | 18 | <!-- ❌ Bad: Custom modal with legacy styles --> 19 | <div class="modal"> 20 | <div class="modal-content"> 21 | <h2>Bad Modal</h2> 22 | <p>This is a legacy modal.</p> 23 | </div> 24 | </div> 25 | 26 | <!-- ✅ Good: DSProgressBar --> 27 | <ds-progress-bar [value]="50"></ds-progress-bar> 28 | 29 | <!-- ❌ Bad: Manually styled progress bar --> 30 | <div class="progress-bar"> 31 | <div class="progress" style="width: 50%;"></div> 32 | </div> 33 | 34 | <!-- ✅ Good: DSDropdown --> 35 | <ds-dropdown [options]="['Option 1', 'Option 2']"></ds-dropdown> 36 | 37 | <!-- ❌ Bad: Legacy dropdown --> 38 | <select class="dropdown"> 39 | <option>Option 1</option> 40 | <option>Option 2</option> 41 | </select> 42 | 43 | <!-- ✅ Good: Using DSAlert --> 44 | <ds-alert type="error"> Good Alert </ds-alert> 45 | 46 | <!-- ❌ Bad: Manually styled alert --> 47 | <div class="alert alert-danger">Bad Alert</div> 48 | 49 | <!-- ✅ Good: Using DSTooltip --> 50 | <ds-tooltip content="Good tooltip">Hover me</ds-tooltip> 51 | 52 | <!-- ❌ Bad: Legacy tooltip --> 53 | <div class="tooltip">Bad tooltip</div> 54 | 55 | <!-- ✅ Good: Using DSBreadcrumb --> 56 | <ds-breadcrumb> 57 | <ds-breadcrumb-item>Home</ds-breadcrumb-item> 58 | <ds-breadcrumb-item>Products</ds-breadcrumb-item> 59 | <ds-breadcrumb-item>Details</ds-breadcrumb-item> 60 | </ds-breadcrumb> 61 | 62 | <!-- ❌ Bad: Manually created breadcrumb --> 63 | <nav class="breadcrumb"> 64 | <span>Home</span> / <span>Products</span> / <span>Details</span> 65 | </nav> 66 | `, 67 | }) 68 | export class MixedStylesNotStandaloneComponent3 {} 69 | ``` -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 3 | "namedInputs": { 4 | "default": ["{projectRoot}/**/*", "sharedGlobals"], 5 | "production": [ 6 | "default", 7 | "!{projectRoot}/.eslintrc.json", 8 | "!{projectRoot}/eslint.config.mjs", 9 | "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", 10 | "!{projectRoot}/tsconfig.spec.json", 11 | "!{projectRoot}/jest.config.[jt]s", 12 | "!{projectRoot}/src/test-setup.[jt]s", 13 | "!{projectRoot}/test-setup.[jt]s" 14 | ], 15 | "sharedGlobals": ["{workspaceRoot}/.github/workflows/ci.yml"] 16 | }, 17 | "plugins": [ 18 | { 19 | "plugin": "@nx/js/typescript", 20 | "options": { 21 | "typecheck": { 22 | "targetName": "typecheck" 23 | }, 24 | "build": { 25 | "targetName": "build", 26 | "configName": "tsconfig.lib.json", 27 | "buildDepsName": "build-deps", 28 | "watchDepsName": "watch-deps" 29 | } 30 | } 31 | }, 32 | { 33 | "plugin": "@nx/webpack/plugin", 34 | "options": { 35 | "buildTargetName": "build", 36 | "serveTargetName": "serve", 37 | "previewTargetName": "preview", 38 | "buildDepsTargetName": "build-deps", 39 | "watchDepsTargetName": "watch-deps" 40 | } 41 | }, 42 | { 43 | "plugin": "@nx/eslint/plugin", 44 | "options": { 45 | "targetName": "lint" 46 | } 47 | }, 48 | { 49 | "plugin": "@nx/vite/plugin", 50 | "options": { 51 | "buildTargetName": "build", 52 | "testTargetName": "test", 53 | "serveTargetName": "serve", 54 | "devTargetName": "dev", 55 | "previewTargetName": "preview", 56 | "serveStaticTargetName": "serve-static", 57 | "typecheckTargetName": "typecheck", 58 | "buildDepsTargetName": "build-deps", 59 | "watchDepsTargetName": "watch-deps" 60 | } 61 | } 62 | ], 63 | "targetDefaults": { 64 | "@nx/js:swc": { 65 | "cache": true, 66 | "dependsOn": ["^build"], 67 | "inputs": ["production", "^production"] 68 | }, 69 | "test": { 70 | "dependsOn": ["^build"] 71 | } 72 | } 73 | } 74 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/design-system/storybook-host-app/src/components/modal/modal-tabs/api.mdx: -------------------------------------------------------------------------------- ```markdown 1 | ## Inputs 2 | 3 | ### `ds-modal-header` 4 | 5 | | Name | Type | Default | Description | 6 | | --------- | ------------------------------------------------------------------ | ----------- | ------------------------------------- | 7 | | `variant` | `'surface-lowest' \| 'surface-low' \| 'surface' \| 'surface-high'` | `'surface'` | Background style for the modal header | 8 | 9 | Other components (`ds-modal`, `ds-modal-content`, `ds-modal-header-drag`) do not define any `@Input()` bindings. 10 | 11 | --- 12 | 13 | ## Outputs 14 | 15 | None of the modal-related components emit Angular `@Output()` events. 16 | 17 | --- 18 | 19 | ## Content Projection 20 | 21 | ### `ds-modal` 22 | 23 | Supports default slot: 24 | 25 | ```html 26 | <ds-modal> 27 | <ds-modal-header>...</ds-modal-header> 28 | <ds-modal-content>...</ds-modal-content> 29 | </ds-modal> 30 | ``` 31 | 32 | ### `ds-modal-header` 33 | 34 | Defines multiple named slots: 35 | 36 | ```html 37 | <ds-modal-header> 38 | <span slot="start">Back</span> 39 | <div slot="center">Title</div> 40 | <button slot="end">Close</button> 41 | </ds-modal-header> 42 | ``` 43 | 44 | Content is rendered into: 45 | 46 | - `[slot=start]` → left section 47 | - `[slot=center]` → center section 48 | - `ds-modal-header-drag`, `[modal-header-image]` → center below title 49 | - `[slot=end]` → right section 50 | 51 | ### `ds-modal-content` 52 | 53 | Projects content as modal body: 54 | 55 | ```html 56 | <ds-modal-content> Modal text goes here. </ds-modal-content> 57 | ``` 58 | 59 | --- 60 | 61 | ## Host Element Behavior 62 | 63 | ### `ds-modal` 64 | 65 | - Host class: `ds-modal` 66 | - Attributes: 67 | - `role="dialog"` 68 | - `aria-label="Modal dialog"` 69 | 70 | ### `ds-modal-header` 71 | 72 | - Host class: `ds-modal-header` 73 | - Dynamic class based on `variant`: `ds-modal-header-surface`, `ds-modal-header-surface-low`, etc. 74 | - Attributes: 75 | - `role="dialog"` 76 | - `aria-label="Modal header dialog"` 77 | 78 | ### `ds-modal-header-drag` 79 | 80 | - Host class: `ds-modal-header-drag` 81 | - Attributes: 82 | - `role="dialog"` 83 | - `aria-label="Modal header drag dialog"` 84 | 85 | ### `ds-modal-content` 86 | 87 | - Host class: `ds-modal-content` 88 | - No interactive attributes applied 89 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/group-3/bad-mixed-3.component.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-mixed-styles', 5 | standalone: true, 6 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 7 | template: ` 8 | <!-- ✅ Good: Using DSButton --> 9 | <ds-button>Good Button</ds-button> 10 | 11 | <!-- ❌ Bad: Legacy button class --> 12 | <button class="btn btn-primary">Bad Button</button> 13 | 14 | <!-- ✅ Good: Using DSModal --> 15 | <ds-modal [open]="true"> 16 | <p>Good Modal Content</p> 17 | </ds-modal> 18 | 19 | <!-- ❌ Bad: Custom modal with legacy styles --> 20 | <div class="modal"> 21 | <div class="modal-content"> 22 | <h2>Bad Modal</h2> 23 | <p>This is a legacy modal.</p> 24 | </div> 25 | </div> 26 | 27 | <!-- ✅ Good: DSProgressBar --> 28 | <ds-progress-bar [value]="50"></ds-progress-bar> 29 | 30 | <!-- ❌ Bad: Manually styled progress bar --> 31 | <div class="progress-bar"> 32 | <div class="progress" style="width: 50%;"></div> 33 | </div> 34 | 35 | <!-- ✅ Good: DSDropdown --> 36 | <ds-dropdown [options]="['Option 1', 'Option 2']"></ds-dropdown> 37 | 38 | <!-- ❌ Bad: Legacy dropdown --> 39 | <select class="dropdown"> 40 | <option>Option 1</option> 41 | <option>Option 2</option> 42 | </select> 43 | 44 | <!-- ✅ Good: Using DSAlert --> 45 | <ds-alert type="error"> Good Alert </ds-alert> 46 | 47 | <!-- ❌ Bad: Manually styled alert --> 48 | <div class="alert alert-danger">Bad Alert</div> 49 | 50 | <!-- ✅ Good: Using DSTooltip --> 51 | <ds-tooltip content="Good tooltip">Hover me</ds-tooltip> 52 | 53 | <!-- ❌ Bad: Legacy tooltip --> 54 | <div class="tooltip">Bad tooltip</div> 55 | 56 | <!-- ✅ Good: Using DSBreadcrumb --> 57 | <ds-breadcrumb> 58 | <ds-breadcrumb-item>Home</ds-breadcrumb-item> 59 | <ds-breadcrumb-item>Products</ds-breadcrumb-item> 60 | <ds-breadcrumb-item>Details</ds-breadcrumb-item> 61 | </ds-breadcrumb> 62 | 63 | <!-- ❌ Bad: Manually created breadcrumb --> 64 | <nav class="breadcrumb"> 65 | <span>Home</span> / <span>Products</span> / <span>Details</span> 66 | </nav> 67 | `, 68 | }) 69 | export class MixedStylesComponent3 {} 70 | ``` -------------------------------------------------------------------------------- /packages/shared/angular-ast-utils/ai/EXAMPLES.md: -------------------------------------------------------------------------------- ```markdown 1 | # Examples 2 | 3 | ## 1 — Parsing components 4 | 5 | > Parse a single Angular component file and list component class names. 6 | 7 | ```ts 8 | import { parseComponents } from 'angular-ast-utils'; 9 | 10 | const comps = await parseComponents(['src/app/app.component.ts']); 11 | console.log(comps.map((c) => c.className)); 12 | ``` 13 | 14 | --- 15 | 16 | ## 2 — Checking for a CSS class 17 | 18 | > Detect whether a given class name appears in an Angular `[ngClass]` binding. 19 | 20 | ```ts 21 | import { ngClassesIncludeClassName } from 'angular-ast-utils'; 22 | 23 | const source = "{'btn' : isActive}"; 24 | const hasBtn = ngClassesIncludeClassName(source, 'btn'); 25 | console.log(hasBtn); // → true 26 | ``` 27 | 28 | --- 29 | 30 | ## 3 — Finding Angular units by type 31 | 32 | > Find all components, directives, pipes, or services in a directory. 33 | 34 | ```ts 35 | import { findAngularUnits } from 'angular-ast-utils'; 36 | 37 | const componentFiles = await findAngularUnits('./src/app', 'component'); 38 | const serviceFiles = await findAngularUnits('./src/app', 'service'); 39 | console.log(componentFiles); // → ['./src/app/app.component.ts', ...] 40 | ``` 41 | 42 | --- 43 | 44 | ## 4 — Parsing Angular units in a directory 45 | 46 | > Parse all Angular components in a directory and get their metadata. 47 | 48 | ```ts 49 | import { parseAngularUnit } from 'angular-ast-utils'; 50 | 51 | const components = await parseAngularUnit('./src/app', 'component'); 52 | console.log(components.map((c) => c.className)); // → ['AppComponent', ...] 53 | ``` 54 | 55 | --- 56 | 57 | ## 5 — Visiting component templates 58 | 59 | > Run a visitor function against a component's template AST. 60 | 61 | ```ts 62 | import { visitComponentTemplate } from 'angular-ast-utils'; 63 | 64 | await visitComponentTemplate(component, searchTerm, async (term, template) => { 65 | // Process template AST and return issues 66 | return []; 67 | }); 68 | ``` 69 | 70 | --- 71 | 72 | ## 6 — Visiting component styles 73 | 74 | > Run a visitor function against a component's styles. 75 | 76 | ```ts 77 | import { visitComponentStyles } from 'angular-ast-utils'; 78 | 79 | const issues = await visitComponentStyles( 80 | component, 81 | searchTerm, 82 | async (term, style) => { 83 | // Process style AST and return issues 84 | return []; 85 | } 86 | ); 87 | ``` 88 | ``` -------------------------------------------------------------------------------- /packages/shared/styles-ast-utils/src/lib/utils.unit.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect } from 'vitest'; 2 | import { parseStylesheet } from './stylesheet.parse'; 3 | import { Rule } from 'postcss'; 4 | import { styleAstRuleToSource } from './utils'; 5 | 6 | describe('styleAstRuleToSource', () => { 7 | it('should have line number starting from 1', () => { 8 | const result = parseStylesheet(`.btn{ color: red; }`, 'inline-styles').root; 9 | const source = styleAstRuleToSource(result?.nodes?.at(0) as Rule); 10 | expect(source).toStrictEqual({ 11 | file: expect.stringMatching(/inline-styles$/), 12 | position: { 13 | startLine: 1, 14 | startColumn: 1, 15 | endLine: 1, 16 | endColumn: 19, 17 | }, 18 | }); 19 | }); 20 | 21 | it('should have line number where startLine is respected', () => { 22 | const result = parseStylesheet(`.btn{ color: red; }`, 'styles.css').root; 23 | const source = styleAstRuleToSource(result?.nodes?.at(0) as Rule, 4); 24 | expect(source).toStrictEqual({ 25 | file: expect.stringMatching(/styles\.css$/), 26 | position: { 27 | startLine: 5, 28 | startColumn: 1, 29 | endLine: 5, 30 | endColumn: 19, 31 | }, 32 | }); 33 | }); 34 | 35 | it('should have correct line number for starting line breaks', () => { 36 | const result = parseStylesheet( 37 | ` 38 | 39 | .btn{ color: red; }`, 40 | 'styles.css', 41 | ).root; 42 | const source = styleAstRuleToSource(result?.nodes?.at(0) as Rule); 43 | expect(source).toStrictEqual({ 44 | file: expect.stringMatching(/styles\.css$/), 45 | position: { 46 | startLine: 3, 47 | startColumn: 1, 48 | endLine: 3, 49 | endColumn: 19, 50 | }, 51 | }); 52 | }); 53 | 54 | it('should have correct line number for spans', () => { 55 | const result = parseStylesheet( 56 | ` 57 | .btn{ 58 | color: red; 59 | }`, 60 | 'styles.css', 61 | ).root; 62 | 63 | const source = styleAstRuleToSource(result?.nodes?.at(0) as Rule); 64 | expect(source).toStrictEqual({ 65 | file: expect.stringMatching(/styles\.css$/), 66 | position: { 67 | startLine: 2, 68 | startColumn: 1, 69 | endLine: 4, 70 | endColumn: 1, 71 | }, 72 | }); 73 | }); 74 | }); 75 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp/package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "@push-based/angular-toolkit-mcp", 3 | "version": "0.2.0", 4 | "description": "A Model Context Protocol server for Angular project analysis and refactoring", 5 | "keywords": [ 6 | "mcp", 7 | "angular", 8 | "refactoring", 9 | "analysis", 10 | "model-context-protocol" 11 | ], 12 | "author": "Push-Based", 13 | "license": "MIT", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/push-based/angular-toolkit-mcp.git" 17 | }, 18 | "homepage": "https://github.com/push-based/angular-toolkit-mcp", 19 | "bugs": "https://github.com/push-based/angular-toolkit-mcp/issues", 20 | "engines": { 21 | "node": ">=18" 22 | }, 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "bin": { 27 | "angular-toolkit-mcp": "main.js" 28 | }, 29 | "files": [ 30 | "main.js", 31 | "*.js", 32 | "README.md" 33 | ], 34 | "nx": { 35 | "implicitDependencies": [ 36 | "angular-mcp-server" 37 | ], 38 | "targets": { 39 | "serve": { 40 | "executor": "@nx/js:node", 41 | "defaultConfiguration": "development", 42 | "dependsOn": [ 43 | "build" 44 | ], 45 | "options": { 46 | "buildTarget": "angular-mcp:build", 47 | "runBuildTargetDependencies": false 48 | }, 49 | "configurations": { 50 | "development": { 51 | "buildTarget": "angular-mcp:build:development" 52 | }, 53 | "production": { 54 | "buildTarget": "angular-mcp:build:production" 55 | } 56 | } 57 | }, 58 | "serve-static": { 59 | "dependsOn": [ 60 | "^build" 61 | ], 62 | "command": "node packages/angular-mcp/dist/main.js" 63 | }, 64 | "debug": { 65 | "dependsOn": [ 66 | "build" 67 | ], 68 | "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" 69 | } 70 | } 71 | } 72 | } 73 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/design-system/ui/segmented-control/src/segmented-control.component.html: -------------------------------------------------------------------------------- ```html 1 | <div class="ds-segmented-control-container" #scContainer> 2 | <div 3 | class="ds-segmented-controls" 4 | [attr.role]="roleType()" 5 | [class.ds-sc-ready]="isReady()" 6 | [class.ds-segment-full-width]="fullWidth()" 7 | [class.ds-segment-inverse]="inverse()" 8 | (keydown)="onKeydown($event)" 9 | > 10 | @for (option of segmentedOptions(); track option.name()) { 11 | <div 12 | #tabOption 13 | class="ds-segment-item" 14 | [class.ds-segment-selected]=" 15 | option.name() === this.selectedOption()?.name() 16 | " 17 | [id]="'ds-segment-item-' + option.name()" 18 | [attr.tabindex]=" 19 | option.name() === this.selectedOption()?.name() ? 0 : -1 20 | " 21 | [attr.role]="roleType() === 'tablist' ? 'tab' : 'radio'" 22 | [attr.aria-selected]=" 23 | roleType() === 'tablist' 24 | ? option.name() === this.selectedOption()?.name() 25 | ? 'true' 26 | : 'false' 27 | : null 28 | " 29 | [attr.aria-checked]=" 30 | roleType() === 'radiogroup' 31 | ? option.name() === this.selectedOption()?.name() 32 | ? 'true' 33 | : 'false' 34 | : null 35 | " 36 | [attr.aria-label]="option.title() || option.name()" 37 | (click)="selectOption(option.name(), $event)" 38 | > 39 | <input 40 | type="radio" 41 | class="ds-segmented-control-hidden-input" 42 | [value]="option.name()" 43 | [name]="option.name()" 44 | [id]="'ds-sc-option-' + option.name()" 45 | [checked]="option.selected()" 46 | [attr.aria-labelledby]="'ds-segment-item-' + option.name()" 47 | [title]="option.title()" 48 | /> 49 | <label 50 | class="ds-segment-item-label" 51 | [for]="'ds-sc-option-' + option.title()" 52 | [class.ds-segmented-item-two-line-text]="twoLineTruncation()" 53 | [class.ds-segment-item-custom-template]="option.customTemplate()" 54 | > 55 | @if (option.customTemplate()) { 56 | <ng-container [ngTemplateOutlet]="option.customTemplate()!" /> 57 | } @else { 58 | {{ option.title() }} 59 | } 60 | </label> 61 | </div> 62 | } 63 | </div> 64 | </div> 65 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/spec/dom-path-utils.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | /* eslint-disable prefer-const */ 2 | import { describe, it, expect } from 'vitest'; 3 | 4 | import { 5 | createDomPathDictionary, 6 | isDomPath, 7 | isValidDomPath, 8 | addDomPath, 9 | processDomPaths, 10 | } from '../utils/dom-path-utils.js'; 11 | 12 | const SAMPLE_PATH = 'div#root > span.foo > button.bar'; 13 | 14 | describe('dom-path-utils', () => { 15 | describe('isDomPath / isValidDomPath', () => { 16 | it('detects DOM-like selector strings correctly', () => { 17 | expect(isDomPath(SAMPLE_PATH)).toBe(true); 18 | expect(isValidDomPath(SAMPLE_PATH)).toBe(true); 19 | 20 | expect(isDomPath('div')).toBe(false); 21 | expect(isDomPath('div.foo')).toBe(false); 22 | const mediaPath = `${SAMPLE_PATH} @media`; 23 | expect(isDomPath(mediaPath)).toBe(true); 24 | expect(isValidDomPath(mediaPath)).toBe(false); 25 | }); 26 | }); 27 | 28 | describe('addDomPath & createDomPathDictionary', () => { 29 | it('adds new paths and deduplicates existing ones', () => { 30 | const dict = createDomPathDictionary(); 31 | 32 | const ref1 = addDomPath(dict, SAMPLE_PATH); 33 | expect(ref1).toEqual({ $domPath: 0 }); 34 | expect(dict.paths[0]).toBe(SAMPLE_PATH); 35 | expect(dict.stats.totalPaths).toBe(1); 36 | expect(dict.stats.uniquePaths).toBe(1); 37 | expect(dict.stats.duplicateReferences).toBe(0); 38 | 39 | const ref2 = addDomPath(dict, SAMPLE_PATH); 40 | expect(ref2).toEqual({ $domPath: 0 }); 41 | expect(dict.stats.totalPaths).toBe(2); 42 | expect(dict.stats.uniquePaths).toBe(1); 43 | expect(dict.stats.duplicateReferences).toBe(1); 44 | }); 45 | }); 46 | 47 | describe('processDomPaths', () => { 48 | it('recursively replaces DOM path strings with references', () => { 49 | const dict = createDomPathDictionary(); 50 | 51 | const input = { 52 | pathA: SAMPLE_PATH, 53 | nested: ['no-dom-path', SAMPLE_PATH, { deeper: SAMPLE_PATH }], 54 | }; 55 | 56 | const processed = processDomPaths(input, dict); 57 | 58 | expect(processed.pathA).toEqual({ $domPath: 0 }); 59 | expect(processed.nested[1]).toEqual({ $domPath: 0 }); 60 | expect(processed.nested[2].deeper).toEqual({ $domPath: 0 }); 61 | 62 | expect(dict.paths).toEqual([SAMPLE_PATH]); 63 | expect(dict.stats.uniquePaths).toBe(1); 64 | }); 65 | }); 66 | }); 67 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/app.component.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Component } from '@angular/core'; 2 | import { RouterOutlet } from '@angular/router'; 3 | import { BadAlertComponent } from './components/refactoring-tests/bad-alert.component'; 4 | import { BadAlertTooltipInputComponent } from './components/refactoring-tests/bad-alert-tooltip-input.component'; 5 | import { BadButtonDropdownComponent } from './components/refactoring-tests/bad-button-dropdown.component'; 6 | import { MixedStylesComponent } from './components/refactoring-tests/bad-mixed.component'; 7 | import { BadModalProgressComponent } from './components/refactoring-tests/bad-modal-progress.component'; 8 | import { BadMixedExternalAssetsComponent } from './components/refactoring-tests/bad-mixed-external-assets.component'; 9 | import { BadDocumentComponent } from './components/refactoring-tests/bad-document.component'; 10 | import { BadWindowComponent } from './components/refactoring-tests/bad-window.component'; 11 | import { BadThisWindowDocumentComponent } from './components/refactoring-tests/bad-this-window-document.component'; 12 | import { BadGlobalThisComponent } from './components/refactoring-tests/bad-global-this.component'; 13 | 14 | @Component({ 15 | selector: 'app-root', 16 | imports: [ 17 | RouterOutlet, 18 | BadAlertComponent, 19 | BadAlertTooltipInputComponent, 20 | BadModalProgressComponent, 21 | BadButtonDropdownComponent, 22 | MixedStylesComponent, 23 | BadMixedExternalAssetsComponent, 24 | BadDocumentComponent, 25 | BadGlobalThisComponent, 26 | BadWindowComponent, 27 | BadThisWindowDocumentComponent, 28 | ], 29 | template: ` 30 | <h1>{{ title }}</h1> 31 | <button class="btn">Sports</button> 32 | <app-bad-alert></app-bad-alert> 33 | <app-bad-alert-tooltip-input></app-bad-alert-tooltip-input> 34 | <app-bad-modal-progress></app-bad-modal-progress> 35 | <app-mixed-styles></app-mixed-styles> 36 | <app-bad-button-dropdown></app-bad-button-dropdown> 37 | <app-bad-mixed-external-assets></app-bad-mixed-external-assets> 38 | <app-bad-window></app-bad-window> 39 | <app-bad-this-window-document></app-bad-this-window-document> 40 | <app-bad-document></app-bad-document> 41 | <app-bad-global-this></app-bad-global-this> 42 | <router-outlet /> 43 | `, 44 | }) 45 | export class AppComponent { 46 | title = 'minimal'; 47 | } 48 | ``` -------------------------------------------------------------------------------- /packages/shared/angular-ast-utils/src/lib/template/noop-tmpl-visitor.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { 2 | TmplAstVisitor, 3 | TmplAstElement, 4 | TmplAstTemplate, 5 | TmplAstContent, 6 | TmplAstText, 7 | TmplAstBoundText, 8 | TmplAstIcu, 9 | TmplAstReference, 10 | TmplAstVariable, 11 | TmplAstBoundEvent, 12 | TmplAstBoundAttribute, 13 | TmplAstTextAttribute, 14 | TmplAstUnknownBlock, 15 | TmplAstDeferredBlock, 16 | TmplAstDeferredBlockError, 17 | TmplAstDeferredBlockLoading, 18 | TmplAstDeferredBlockPlaceholder, 19 | TmplAstDeferredTrigger, 20 | TmplAstIfBlock, 21 | TmplAstIfBlockBranch, 22 | TmplAstSwitchBlock, 23 | TmplAstSwitchBlockCase, 24 | TmplAstForLoopBlock, 25 | TmplAstForLoopBlockEmpty, 26 | TmplAstLetDeclaration, 27 | } from '@angular/compiler' with { 'resolution-mode': 'import' }; 28 | 29 | /** 30 | * Base visitor that does nothing. 31 | * Extend this in concrete visitors so you only override what you need. 32 | */ 33 | export abstract class NoopTmplVisitor implements TmplAstVisitor<void> { 34 | /* eslint-disable @typescript-eslint/no-empty-function */ 35 | visitElement(_: TmplAstElement): void {} 36 | visitTemplate(_: TmplAstTemplate): void {} 37 | visitContent(_: TmplAstContent): void {} 38 | visitText(_: TmplAstText): void {} 39 | visitBoundText(_: TmplAstBoundText): void {} 40 | visitIcu(_: TmplAstIcu): void {} 41 | visitReference(_: TmplAstReference): void {} 42 | visitVariable(_: TmplAstVariable): void {} 43 | visitBoundEvent(_: TmplAstBoundEvent): void {} 44 | visitBoundAttribute(_: TmplAstBoundAttribute): void {} 45 | visitTextAttribute(_: TmplAstTextAttribute): void {} 46 | visitUnknownBlock(_: TmplAstUnknownBlock): void {} 47 | visitDeferredBlock(_: TmplAstDeferredBlock): void {} 48 | visitDeferredBlockError(_: TmplAstDeferredBlockError): void {} 49 | visitDeferredBlockLoading(_: TmplAstDeferredBlockLoading): void {} 50 | visitDeferredBlockPlaceholder(_: TmplAstDeferredBlockPlaceholder): void {} 51 | visitDeferredTrigger(_: TmplAstDeferredTrigger): void {} 52 | visitIfBlock(_: TmplAstIfBlock): void {} 53 | visitIfBlockBranch(_: TmplAstIfBlockBranch): void {} 54 | visitSwitchBlock(_: TmplAstSwitchBlock): void {} 55 | visitSwitchBlockCase(_: TmplAstSwitchBlockCase): void {} 56 | visitForLoopBlock(_: TmplAstForLoopBlock): void {} 57 | visitForLoopBlockEmpty(_: TmplAstForLoopBlockEmpty): void {} 58 | visitLetDeclaration(_: TmplAstLetDeclaration): void {} 59 | /* eslint-enable */ 60 | } 61 | ``` -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- ```markdown 1 | # Getting Started with Angular MCP Toolkit 2 | 3 | A concise, hands-on guide to install, configure, and verify the Angular MCP server in under **5 minutes**. 4 | 5 | --- 6 | 7 | ## 1. Prerequisites 8 | 9 | | Tool | Minimum Version | Notes | 10 | | ---- | --------------- | ----- | 11 | | Node.js | **18.x** LTS | Tested with 18.18 ⬆︎ | 12 | | npm | **9.x** | Bundled with Node LTS | 13 | | Nx CLI | **≥ 21** | `npm i -g nx` | 14 | | Git | Any recent | For workspace cloning | 15 | 16 | > The server itself is framework-agnostic, but most built-in tools assume an **Nx workspace** with Angular projects. 17 | 18 | --- 19 | 20 | ## 2. Install the Server 21 | 22 | ### Clone the repository 23 | 24 | ```bash 25 | git clone https://github.com/push-based/angular-toolkit-mcp.git 26 | cd angular-toolkit-mcp 27 | npm install # install workspace dependencies 28 | ``` 29 | 30 | The MCP server source resides under `packages/angular-mcp/` and `packages/angular-mcp-server/`. No package needs to be fetched from the npm registry. 31 | 32 | --- 33 | 34 | ## 3. Register with Your Editor 35 | 36 | Instead of the palette-based flow, copy the manual configuration from your workspace’s `.cursor/mcp.json` (shown below) and adjust paths if necessary. 37 | 38 | ```json 39 | { 40 | "mcpServers": { 41 | "angular-mcp": { 42 | "command": "node", 43 | "args": [ 44 | "./packages/angular-mcp/dist/main.js", 45 | "--workspaceRoot=/absolute/path/to/angular-toolkit-mcp", 46 | "--ds.storybookDocsRoot=packages/minimal-repo/packages/design-system/storybook-host-app/src/components", 47 | "--ds.deprecatedCssClassesPath=packages/minimal-repo/packages/design-system/component-options.mjs", 48 | "--ds.uiRoot=packages/minimal-repo/packages/design-system/ui" 49 | ] 50 | } 51 | } 52 | } 53 | ``` 54 | 55 | Add or edit this JSON in **Cursor → Settings → MCP Servers** (or the equivalent dialog in your editor). 56 | 57 | --- 58 | 59 | ## 4. Next Steps 60 | 61 | 🔗 Continue with the [Architecture & Internal Design](./architecture-internal-design.md) document (work-in-progress). 62 | 63 | 🚀 Jump straight into [Writing Custom Tools](./writing-custom-tools.md) when ready. 64 | 65 | --- 66 | 67 | ## Troubleshooting 68 | 69 | | Symptom | Possible Cause | Fix | 70 | | ------- | -------------- | --- | 71 | | `command not found: nx` | Nx CLI missing | `npm i -g nx` | 72 | | Editor shows “tool not found” | Server not running or wrong path in `mcp.json` | Check configuration and restart editor | 73 | 74 | --- 75 | 76 | *Happy coding!* ✨ ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/utils/dom-path-utils.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { DomPathDictionary } from '../../shared/models/types.js'; 2 | 3 | /** 4 | * Creates a new DOM path dictionary 5 | */ 6 | export function createDomPathDictionary(): DomPathDictionary { 7 | return { 8 | paths: [], 9 | lookup: new Map<string, number>(), 10 | stats: { 11 | totalPaths: 0, 12 | uniquePaths: 0, 13 | duplicateReferences: 0, 14 | bytesBeforeDeduplication: 0, 15 | bytesAfterDeduplication: 0, 16 | }, 17 | }; 18 | } 19 | 20 | /** 21 | * Detects if a string is a DOM path based on Angular component patterns 22 | */ 23 | export function isDomPath(str: string): boolean { 24 | return ( 25 | typeof str === 'string' && 26 | str.length > 20 && 27 | str.includes(' > ') && 28 | (str.includes('.') || str.includes('#')) && 29 | /^[a-zA-Z]/.test(str) 30 | ); 31 | } 32 | 33 | /** 34 | * Validates that a string is specifically a DOM path and not just any CSS selector 35 | */ 36 | export function isValidDomPath(str: string): boolean { 37 | return ( 38 | isDomPath(str) && 39 | !str.includes('@media') && 40 | !str.includes('{') && 41 | !str.includes('}') && 42 | str.split(' > ').length > 2 43 | ); 44 | } 45 | 46 | /** 47 | * Adds a DOM path to the dictionary and returns its reference 48 | */ 49 | export function addDomPath( 50 | dict: DomPathDictionary, 51 | path: string, 52 | ): { $domPath: number } { 53 | dict.stats.totalPaths++; 54 | dict.stats.bytesBeforeDeduplication += path.length; 55 | 56 | if (dict.lookup.has(path)) { 57 | dict.stats.duplicateReferences++; 58 | dict.stats.bytesAfterDeduplication += 12; 59 | return { $domPath: dict.lookup.get(path)! }; 60 | } 61 | 62 | const index = dict.paths.length; 63 | dict.paths.push(path); 64 | dict.lookup.set(path, index); 65 | dict.stats.uniquePaths++; 66 | dict.stats.bytesAfterDeduplication += 12; 67 | 68 | return { $domPath: index }; 69 | } 70 | 71 | /** 72 | * Processes a value and replaces DOM paths with references 73 | */ 74 | export function processDomPaths(value: any, dict: DomPathDictionary): any { 75 | if (typeof value === 'string') { 76 | if (isValidDomPath(value)) { 77 | return addDomPath(dict, value); 78 | } 79 | return value; 80 | } 81 | 82 | if (Array.isArray(value)) { 83 | return value.map((item) => processDomPaths(item, dict)); 84 | } 85 | 86 | if (value && typeof value === 'object') { 87 | const processed: any = {}; 88 | for (const [key, val] of Object.entries(value)) { 89 | processed[key] = processDomPaths(val, dict); 90 | } 91 | return processed; 92 | } 93 | 94 | return value; 95 | } 96 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/design-system/ui/badge/src/badge.component.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | ElementRef, 5 | ViewEncapsulation, 6 | booleanAttribute, 7 | computed, 8 | input, 9 | } from '@angular/core'; 10 | 11 | export const DS_BADGE_VARIANT_ARRAY = [ 12 | 'primary', 13 | 'primary-strong', 14 | 'primary-subtle', 15 | 'secondary', 16 | 'secondary-strong', 17 | 'secondary-subtle', 18 | 'green', 19 | 'green-strong', 20 | 'green-subtle', 21 | 'blue', 22 | 'blue-strong', 23 | 'blue-subtle', 24 | 'red', 25 | 'red-strong', 26 | 'red-subtle', 27 | 'purple', 28 | 'purple-strong', 29 | 'purple-subtle', 30 | 'neutral', 31 | 'neutral-strong', 32 | 'neutral-subtle', 33 | 'yellow', 34 | 'yellow-strong', 35 | 'yellow-subtle', 36 | 'orange', 37 | 'orange-strong', 38 | 'orange-subtle', 39 | ] as const; 40 | 41 | export type DsBadgeVariant = (typeof DS_BADGE_VARIANT_ARRAY)[number]; 42 | 43 | export const DS_BADGE_SIZE_ARRAY = ['xsmall', 'medium'] as const; 44 | export type DsBadgeSize = (typeof DS_BADGE_SIZE_ARRAY)[number]; 45 | 46 | @Component({ 47 | selector: 'ds-badge', 48 | template: ` 49 | <div class="ds-badge-slot-container"> 50 | <ng-content select="[slot=start]" /> 51 | </div> 52 | <span class="ds-badge-text"> 53 | <ng-content /> 54 | </span> 55 | <div class="ds-badge-slot-container"> 56 | <ng-content select="[slot=end]" /> 57 | </div> 58 | `, 59 | host: { 60 | '[class]': 'hostClass()', 61 | '[class.ds-badge-disabled]': 'disabled()', 62 | '[class.ds-badge-inverse]': 'inverse()', 63 | '[attr.aria-label]': 'getAriaLabel()', 64 | role: 'img', // for now we are using role img till we find better solution to work with nvda 65 | }, 66 | standalone: true, 67 | encapsulation: ViewEncapsulation.None, 68 | changeDetection: ChangeDetectionStrategy.OnPush, 69 | }) 70 | export class DsBadge { 71 | size = input<DsBadgeSize>('medium'); 72 | variant = input<DsBadgeVariant>('primary'); 73 | disabled = input(false, { transform: booleanAttribute }); 74 | inverse = input(false, { transform: booleanAttribute }); 75 | 76 | hostClass = computed( 77 | () => `ds-badge ds-badge-${this.size()} ds-badge-${this.variant()}`, 78 | ); 79 | 80 | constructor(public elementRef: ElementRef<HTMLElement>) {} 81 | 82 | public getAriaLabel(): string { 83 | const mainContent = this.elementRef.nativeElement 84 | .querySelector('.ds-badge-text') 85 | ?.textContent?.trim(); 86 | 87 | const label = mainContent || ''; 88 | 89 | if (this.disabled()) { 90 | return `Disabled badge: ${label}`; 91 | } 92 | return `Badge: ${label}`; 93 | } 94 | } 95 | ``` -------------------------------------------------------------------------------- /packages/shared/ds-component-coverage/ai/FUNCTIONS.md: -------------------------------------------------------------------------------- ```markdown 1 | # Public API — Quick Reference 2 | 3 | | Symbol | Kind | Signature | Summary | 4 | | -------------------------------------- | -------- | --------------------------------------------------------------------------------------------- | ---------------------------------------------------- | 5 | | `ANGULAR_DS_USAGE_PLUGIN_SLUG` | constant | `const ANGULAR_DS_USAGE_PLUGIN_SLUG: string` | Plugin slug identifier for ds-component-coverage | 6 | | `ComponentCoverageRunnerOptionsSchema` | schema | `const ComponentCoverageRunnerOptionsSchema: ZodObject` | Zod schema for runner configuration validation | 7 | | `ComponentReplacement` | type | `type ComponentReplacement` | Type for component replacement configuration | 8 | | `ComponentReplacementSchema` | schema | `const ComponentReplacementSchema: ZodObject` | Zod schema for component replacement validation | 9 | | `CreateRunnerConfig` | type | `type CreateRunnerConfig` | Type alias for runner configuration | 10 | | `dsComponentCoveragePlugin` | function | `dsComponentCoveragePlugin(options: DsComponentUsagePluginConfig): PluginConfig` | Create DS component coverage plugin for Code Pushup | 11 | | `DsComponentUsagePluginConfig` | type | `type DsComponentUsagePluginConfig` | Configuration type for the DS component usage plugin | 12 | | `getAngularDsUsageCategoryRefs` | function | `getAngularDsUsageCategoryRefs(componentReplacements: ComponentReplacement[]): CategoryRef[]` | Generate category references for audit organization | 13 | | `runnerFunction` | function | `runnerFunction(config: CreateRunnerConfig): Promise<AuditOutputs>` | Execute DS component coverage analysis | 14 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/design-system/storybook-host-app/src/components/badge/badge-tabs/api.mdx: -------------------------------------------------------------------------------- ```markdown 1 | ## Inputs 2 | 3 | | Name | Type | Default | Description | 4 | | ---------- | ---------------------------------------------------------------------- | ----------- | ------------------------------------------------------ | 5 | | `size` | `'xsmall'` \| `'medium'` | `'medium'` | Controls the badge size. | 6 | | `variant` | A value from `DsBadgeVariant`<br/>(e.g. `'primary'`, `'green-strong'`) | `'primary'` | Visual style variant (color + intensity). | 7 | | `disabled` | `boolean` | `false` | Visually and semantically marks the badge as disabled. | 8 | | `inverse` | `boolean` | `false` | Applies inverse theme styling (for dark backgrounds). | 9 | 10 | --- 11 | 12 | ## Outputs / Events 13 | 14 | This component does not emit any custom events. 15 | 16 | --- 17 | 18 | ## Content Projection 19 | 20 | The badge supports slot-based content for flexible icon/text layouts. 21 | 22 | ### Named Slots 23 | 24 | - `[slot=start]` – content rendered before the main text. 25 | - `[slot=end]` – content rendered after the main text. 26 | 27 | ### Default Slot 28 | 29 | Text or elements directly inside the component will be rendered in the central label span. 30 | 31 | ```html 32 | <ds-badge variant="green-strong"> 33 | <span slot="start">✔</span> 34 | Confirmed 35 | <span slot="end">✓</span> 36 | </ds-badge> 37 | ``` 38 | 39 | --- 40 | 41 | ## Host Element Behavior 42 | 43 | The following CSS classes are dynamically applied to the host element to reflect component state: 44 | 45 | - `ds-badge` – base class applied to all badge instances 46 | - `ds-badge-xsmall` or `ds-badge-medium` – based on the `size` input 47 | - `ds-badge-[variant]` – where `[variant]` corresponds to the selected variant (e.g. `ds-badge-green-subtle`) 48 | - `ds-badge-inverse` – applied when `inverse` is `true` 49 | - `ds-badge-disabled` – applied when `disabled` is `true` 50 | 51 | These classes are computed and set via the `hostClass()` method using Angular’s `@computed()` signal. 52 | 53 | The host also defines the following attributes: 54 | 55 | - `role="img"` – applied for accessibility support 56 | - `aria-label` – dynamically generated from content, prepended with `"Disabled badge:"` if `disabled` is `true` 57 | ``` -------------------------------------------------------------------------------- /packages/shared/angular-ast-utils/src/lib/utils.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as ts from 'typescript'; 2 | import { removeQuotes } from '@push-based/typescript-ast-utils'; 3 | import { AngularUnit, Asset } from './types.js'; 4 | 5 | export function assetFromPropertyValueInitializer<T>({ 6 | prop, 7 | sourceFile, 8 | textParser, 9 | }: { 10 | prop: ts.PropertyAssignment; 11 | sourceFile: ts.SourceFile; 12 | textParser: (text: string) => Promise<T>; 13 | }): Asset<T> { 14 | const { line: startLine } = sourceFile.getLineAndCharacterOfPosition( 15 | prop.getStart(sourceFile), 16 | ); 17 | const value = removeQuotes(prop.initializer, sourceFile); 18 | return { 19 | filePath: sourceFile.fileName, 20 | startLine, 21 | parse: () => textParser(value), 22 | } satisfies Asset<T>; 23 | } 24 | 25 | export function assetFromPropertyArrayInitializer<T>( 26 | prop: ts.PropertyAssignment, 27 | sourceFile: ts.SourceFile, 28 | textParser: (text: string) => Promise<T>, 29 | ): Asset<T>[] { 30 | const elements: ts.NodeArray<ts.Expression> = ts.isArrayLiteralExpression( 31 | prop.initializer, 32 | ) 33 | ? prop.initializer.elements 34 | : ts.factory.createNodeArray(); 35 | 36 | return elements.map((element) => { 37 | const { line: startLine } = sourceFile.getLineAndCharacterOfPosition( 38 | element.getStart(sourceFile), 39 | ); 40 | const value = removeQuotes(element, sourceFile); 41 | return { 42 | filePath: sourceFile.fileName, 43 | startLine, 44 | parse: () => textParser(value), 45 | } satisfies Asset<T>; 46 | }); 47 | } 48 | 49 | import { findFilesWithPattern } from '@push-based/utils'; 50 | import { parseComponents } from './parse-component.js'; 51 | 52 | const unitToSearchPattern = { 53 | component: '@Component', 54 | directive: '@Directive', 55 | pipe: '@Pipe', 56 | service: '@Service', 57 | } as const satisfies Record<AngularUnit, string>; 58 | 59 | export async function findAngularUnits( 60 | directory: string, 61 | unit: AngularUnit, 62 | ): Promise<string[]> { 63 | const searchPattern = 64 | unitToSearchPattern[unit] ?? unitToSearchPattern.component; 65 | return await findFilesWithPattern(directory, searchPattern); 66 | } 67 | 68 | /** 69 | * Parse Angular units in a given directory. 70 | * 71 | * @param directory 72 | * @param unit 73 | */ 74 | export async function parseAngularUnit(directory: string, unit: AngularUnit) { 75 | const componentFiles = await findAngularUnits(directory, unit); 76 | 77 | switch (unit) { 78 | case 'component': 79 | return parseComponents(componentFiles); 80 | default: 81 | throw new Error(`Unit ${unit} is not supported for parsing.`); 82 | } 83 | } 84 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-usage-graph/build-component-usage-graph.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | createHandler, 3 | BaseHandlerOptions, 4 | } from '../shared/utils/handler-helpers.js'; 5 | import { 6 | buildComponentUsageGraph, 7 | clearAnalysisCache, 8 | } from './utils/component-usage-graph-builder.js'; 9 | import { filterGraph, printComponents } from './utils/component-helpers.js'; 10 | import { buildComponentUsageGraphSchema } from './models/schema.js'; 11 | import { resolveCrossPlatformPath } from '../shared/utils/cross-platform-path.js'; 12 | 13 | interface ComponentUsageGraphOptions extends BaseHandlerOptions { 14 | directory: string; 15 | violationFiles: string[]; 16 | } 17 | 18 | export const buildComponentUsageGraphHandler = createHandler< 19 | ComponentUsageGraphOptions, 20 | any 21 | >( 22 | buildComponentUsageGraphSchema.name, 23 | async (params, { cwd, workspaceRoot }) => { 24 | const startTime = performance.now(); 25 | 26 | try { 27 | const { directory, violationFiles } = params; 28 | 29 | if ( 30 | !violationFiles || 31 | !Array.isArray(violationFiles) || 32 | violationFiles.length === 0 33 | ) { 34 | throw new Error( 35 | 'violationFiles parameter is required and must be an array of strings', 36 | ); 37 | } 38 | 39 | const fullComponentUsageGraph = await buildComponentUsageGraph({ 40 | cwd, 41 | directory, 42 | workspaceRoot, 43 | }); 44 | 45 | const targetPath = resolveCrossPlatformPath(cwd, directory); 46 | 47 | const componentUsageGraph = 48 | violationFiles.length > 0 49 | ? filterGraph(fullComponentUsageGraph, violationFiles, targetPath) 50 | : fullComponentUsageGraph; 51 | 52 | const content = printComponents(componentUsageGraph, 'entity'); 53 | const totalTime = performance.now() - startTime; 54 | 55 | return { 56 | content, 57 | timing: `⚡ Analysis completed in ${totalTime.toFixed(2)}ms (${Object.keys(fullComponentUsageGraph).length} files processed)`, 58 | }; 59 | } finally { 60 | clearAnalysisCache(); 61 | } 62 | }, 63 | (result) => { 64 | // Format the result as text lines 65 | const lines: string[] = []; 66 | 67 | if (Array.isArray(result.content)) { 68 | result.content.forEach((item: any) => { 69 | if (item.type === 'text') { 70 | lines.push(item.text); 71 | } else { 72 | lines.push(JSON.stringify(item)); 73 | } 74 | }); 75 | } 76 | 77 | lines.push(result.timing); 78 | return lines; 79 | }, 80 | ); 81 | 82 | export const buildComponentUsageGraphTools = [ 83 | { 84 | schema: buildComponentUsageGraphSchema, 85 | handler: buildComponentUsageGraphHandler, 86 | }, 87 | ]; 88 | ``` -------------------------------------------------------------------------------- /packages/shared/angular-ast-utils/src/lib/template/utils.unit.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, expect } from 'vitest'; 2 | import { tmplAstElementToSource } from './utils'; 3 | import type { TmplAstElement } from '@angular/compiler' with { 'resolution-mode': 'import' }; 4 | 5 | describe('tmplAstElementToSource', () => { 6 | let parseTemplate: typeof import('@angular/compiler').parseTemplate; 7 | beforeAll(async () => { 8 | parseTemplate = (await import('@angular/compiler')).parseTemplate; 9 | }); 10 | it('should have line number starting from 1', () => { 11 | const result = parseTemplate( 12 | `<button class="btn">click</button>`, 13 | 'inline-template.component.ts', 14 | ); 15 | const attribute = result.nodes.at(0) as TmplAstElement; 16 | const source = tmplAstElementToSource(attribute); 17 | expect(source).toStrictEqual({ 18 | file: 'inline-template.component.ts', 19 | position: { 20 | startLine: 1, 21 | }, 22 | }); 23 | }); 24 | 25 | it('should have line number where startLine is respected', () => { 26 | const result = parseTemplate( 27 | `<button class="btn">click</button>`, 28 | 'template.html', 29 | ); 30 | const attribute = (result.nodes.at(0) as TmplAstElement)?.attributes.at(0); 31 | 32 | const source = tmplAstElementToSource(attribute); 33 | 34 | expect(source).toStrictEqual({ 35 | file: expect.stringMatching(/template\.html$/), 36 | position: { 37 | startLine: 5, 38 | startColumn: 1, 39 | endLine: 5, 40 | endColumn: 19, 41 | }, 42 | }); 43 | }); 44 | 45 | it('should have correct line number for starting line breaks', () => { 46 | const result = parseTemplate( 47 | ` 48 | 49 | <button class="btn">click</button>`, 50 | 'template.html', 51 | ); 52 | const attribute = (result.nodes.at(0) as TmplAstElement)?.attributes.at(0); 53 | const source = tmplAstElementToSource(attribute); 54 | 55 | expect(source).toStrictEqual({ 56 | file: expect.stringMatching(/template\.html/), 57 | position: { 58 | startLine: 3, 59 | startColumn: 1, 60 | endLine: 3, 61 | endColumn: 19, 62 | }, 63 | }); 64 | }); 65 | 66 | it('should have correct line number for spans', () => { 67 | const result = parseTemplate( 68 | `<button class="btn"> 69 | click 70 | </button>`, 71 | 'template.html', 72 | ); 73 | const attribute = result.nodes.at(0)?.attributes.at(0); 74 | const source = tmplAstElementToSource(attribute); 75 | 76 | expect(source).toStrictEqual({ 77 | file: expect.stringMatching(/template\.html$/), 78 | position: { 79 | startLine: 1, 80 | startColumn: 1, 81 | endLine: 4, 82 | endColumn: 1, 83 | }, 84 | }); 85 | }); 86 | }); 87 | ``` -------------------------------------------------------------------------------- /packages/shared/utils/ai/API.md: -------------------------------------------------------------------------------- ```markdown 1 | # Utils 2 | 3 | Comprehensive **utility library** providing process execution, file operations, string manipulation, and logging utilities for Node.js applications. 4 | 5 | ## Minimal usage 6 | 7 | ```ts 8 | import { 9 | executeProcess, 10 | findFilesWithPattern, 11 | resolveFileCached, 12 | loadDefaultExport, 13 | objectToCliArgs, 14 | } from '@push-based/utils'; 15 | 16 | import { slugify } from '@code-pushup/utils'; 17 | 18 | // Execute a process with observer 19 | const result = await executeProcess({ 20 | command: 'node', 21 | args: ['--version'], 22 | observer: { 23 | onStdout: (data) => console.log(data), 24 | }, 25 | }); 26 | 27 | // Find files containing a pattern 28 | const files = await findFilesWithPattern('./src', 'Component'); 29 | 30 | // Resolve file with caching 31 | const content = await resolveFileCached('./config.json'); 32 | 33 | // Load ES module default export 34 | const config = await loadDefaultExport('./config.mjs'); 35 | 36 | // String utilities 37 | const slug = slugify('Hello World!'); // → 'hello-world' 38 | const args = objectToCliArgs({ name: 'test', verbose: true }); // → ['--name="test"', '--verbose'] 39 | ``` 40 | 41 | ## Key Features 42 | 43 | - **Process Execution**: Robust child process management with observers and error handling 44 | - **File Operations**: Cached file resolution and pattern-based file searching 45 | - **ES Module Loading**: Dynamic import of ES modules with default export extraction 46 | - **String Utilities**: Text transformation, slugification, and pluralization 47 | - **CLI Utilities**: Object-to-arguments conversion and command formatting 48 | - **Logging**: Environment-based verbose logging control 49 | - **Type Safety**: Full TypeScript support with comprehensive type definitions 50 | 51 | ## Use Cases 52 | 53 | - **Build Tools**: Execute CLI commands with real-time output monitoring 54 | - **File Processing**: Search and resolve files efficiently with caching 55 | - **Module Loading**: Dynamic import of configuration files and plugins 56 | - **Code Generation**: Transform data into CLI arguments and formatted strings 57 | - **Development Tools**: Create development utilities with proper logging 58 | - **Static Analysis**: Find and process files based on content patterns 59 | - **Cross-Platform**: Handle path normalization and command execution 60 | 61 | ## Documentation map 62 | 63 | | Doc | What you'll find | 64 | | ------------------------------ | ------------------------------------------- | 65 | | [FUNCTIONS.md](./FUNCTIONS.md) | A–Z quick reference for every public symbol | 66 | | [EXAMPLES.md](./EXAMPLES.md) | Runnable scenarios with expected output | 67 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/design-system/storybook-host-app/src/components/badge/badge-tabs/overview.mdx: -------------------------------------------------------------------------------- ```markdown 1 | import { Canvas } from '@storybook/blocks'; 2 | 3 | import * as BadgeStories from '../badge.component.stories'; 4 | 5 | The `DsBadge` component provides a way to display badges with various sizes, variants and additional features such as icons or placeholders (like success icon in our case). 6 | You can make icon movable with properties slot for start and end position. 7 | 8 | <Canvas of={BadgeStories.Default} /> 9 | 10 | --- 11 | 12 | ## Usage 13 | 14 | Import `DsBadge` in your component and apply `ds-badge` selector in your template. 15 | 16 | ```ts 17 | import { DsBadge } from '@frontend/ui/badge'; // 👈 add to file imports 18 | 19 | @Component({ 20 | imports: [DsBadge], // 👈 add to component imports 21 | template: `...`, 22 | }) 23 | export class AppComponent {} 24 | ``` 25 | 26 | --- 27 | 28 | ## Badge variants 29 | 30 | - By default, badges don't have any icons attached and are set in medium size and in primary variant 31 | - Badges with icon are available only in xsmall and medium sizes 32 | - Badges with icon are available only in xsmall and medium sizes 33 | - Badges with icon are available only in medium sizes 34 | - Badges with success icon are available only in xsmall and medium sizes 35 | - Badges with both icon and success icon are available only in xsmall and medium sizes 36 | 37 | --- 38 | 39 | ## Accessibility 40 | 41 | - The host sets `role="img"` for screen reader compatibility (interim solution until NVDA-compatible alternative is found). 42 | - The `aria-label` is automatically generated from the badge text content. 43 | - If `disabled` is true, the label is prefixed with `"Disabled badge: "`. 44 | - Otherwise, it's `"Badge: {text}"`. 45 | 46 | --- 47 | 48 | ## Test Coverage 49 | 50 | The component is comprehensively tested using Angular CDK Testing and a custom `DsBadgeHarness`. 51 | 52 | ### Functional Behavior 53 | 54 | - Retrieves and verifies label text content 55 | - Filters badges by: 56 | - `size` (e.g., `'xsmall'`, `'medium'`) 57 | - `variant` (e.g., `'secondary'`) 58 | - label using a regex matcher 59 | - Validates dynamic input changes: 60 | - Updates to `size` correctly toggle size class 61 | - Changes to `variant` are reflected in DOM state 62 | 63 | ### Slot Content 64 | 65 | - Verifies rendering of text in the `[slot=start]` and `[slot=end]` containers 66 | - Confirms SVG elements are supported and rendered in the start slot 67 | 68 | ### State & Styling 69 | 70 | - Verifies toggling of the `inverse` class using `inverse` input 71 | - Confirms `ds-badge-disabled` class presence when `disabled` is set 72 | 73 | ### Accessibility 74 | 75 | - Checks that the computed `aria-label` accurately reflects the text and disabled state 76 | - Validates screen reader output using `@guidepup/virtual-screen-reader` 77 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/validation/ds-components-file-loader.validation.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as fs from 'node:fs'; 2 | import * as path from 'node:path'; 3 | import { 4 | DsComponentsArraySchema, 5 | DsComponentSchema, 6 | } from './ds-components.schema.js'; 7 | import { z } from 'zod'; 8 | import { loadDefaultExport } from '@push-based/utils'; 9 | 10 | export type DsComponent = z.infer<typeof DsComponentSchema>; 11 | export type DsComponentsArray = z.infer<typeof DsComponentsArraySchema>; 12 | 13 | export function validateDsComponent(rawComponent: unknown): DsComponent { 14 | const validation = DsComponentSchema.safeParse(rawComponent); 15 | if (!validation.success) { 16 | throw new Error( 17 | `Invalid component format: ${JSON.stringify(validation.error.format())}`, 18 | ); 19 | } 20 | return validation.data; 21 | } 22 | 23 | export function validateDsComponentsArray(rawData: unknown): DsComponentsArray { 24 | if (!Array.isArray(rawData)) { 25 | throw new Error(`Expected array of components, received ${typeof rawData}`); 26 | } 27 | 28 | const validatedComponents: DsComponent[] = []; 29 | for (let i = 0; i < rawData.length; i++) { 30 | try { 31 | const validComponent = validateDsComponent(rawData[i]); 32 | validatedComponents.push(validComponent); 33 | } catch (ctx) { 34 | throw new Error(`Component at index ${i}: ${(ctx as Error).message}`); 35 | } 36 | } 37 | 38 | const arrayValidation = 39 | DsComponentsArraySchema.safeParse(validatedComponents); 40 | if (!arrayValidation.success) { 41 | throw new Error( 42 | `Array validation failed: ${JSON.stringify( 43 | arrayValidation.error.format(), 44 | )}`, 45 | ); 46 | } 47 | 48 | return arrayValidation.data; 49 | } 50 | 51 | export async function loadAndValidateDsComponentsFile( 52 | cwd: string, 53 | deprecatedCssClassesPath: string, 54 | ): Promise<DsComponentsArray> { 55 | if ( 56 | !deprecatedCssClassesPath || 57 | typeof deprecatedCssClassesPath !== 'string' 58 | ) { 59 | throw new Error('deprecatedCssClassesPath must be a string path'); 60 | } 61 | 62 | const absPath = path.resolve(cwd, deprecatedCssClassesPath); 63 | if (!fs.existsSync(absPath)) { 64 | throw new Error(`File not found at deprecatedCssClassesPath: ${absPath}`); 65 | } 66 | 67 | try { 68 | const rawData = await loadDefaultExport(absPath); 69 | 70 | return validateDsComponentsArray(rawData); 71 | } catch (ctx) { 72 | if ( 73 | ctx instanceof Error && 74 | (ctx.message.includes('Invalid component format') || 75 | ctx.message.includes('Expected array of components') || 76 | ctx.message.includes('Component at index')) 77 | ) { 78 | throw ctx; 79 | } 80 | throw new Error( 81 | `Failed to load configuration file: ${(ctx as Error).message}`, 82 | ); 83 | } 84 | } 85 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/angular.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "minimal": { 7 | "projectType": "application", 8 | "schematics": {}, 9 | "root": "", 10 | "sourceRoot": "src", 11 | "prefix": "app", 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:application", 15 | "options": { 16 | "outputPath": "dist/minimal", 17 | "index": "src/index.html", 18 | "browser": "src/main.ts", 19 | "polyfills": ["zone.js"], 20 | "tsConfig": "tsconfig.app.json", 21 | "assets": [ 22 | { 23 | "glob": "**/*", 24 | "input": "public" 25 | } 26 | ], 27 | "styles": ["src/styles.css"], 28 | "scripts": [] 29 | }, 30 | "configurations": { 31 | "production": { 32 | "budgets": [ 33 | { 34 | "type": "initial", 35 | "maximumWarning": "500kB", 36 | "maximumError": "1MB" 37 | }, 38 | { 39 | "type": "anyComponentStyle", 40 | "maximumWarning": "4kB", 41 | "maximumError": "8kB" 42 | } 43 | ], 44 | "outputHashing": "all" 45 | }, 46 | "development": { 47 | "optimization": false, 48 | "extractLicenses": false, 49 | "sourceMap": true 50 | } 51 | }, 52 | "defaultConfiguration": "production" 53 | }, 54 | "serve": { 55 | "builder": "@angular-devkit/build-angular:dev-server", 56 | "configurations": { 57 | "production": { 58 | "buildTarget": "minimal:build:production" 59 | }, 60 | "development": { 61 | "buildTarget": "minimal:build:development" 62 | } 63 | }, 64 | "defaultConfiguration": "development" 65 | }, 66 | "extract-i18n": { 67 | "builder": "@angular-devkit/build-angular:extract-i18n" 68 | }, 69 | "test": { 70 | "builder": "@angular-devkit/build-angular:karma", 71 | "options": { 72 | "polyfills": ["zone.js", "zone.js/testing"], 73 | "tsConfig": "tsconfig.spec.json", 74 | "assets": [ 75 | { 76 | "glob": "**/*", 77 | "input": "public" 78 | } 79 | ], 80 | "styles": ["src/styles.css"], 81 | "scripts": [] 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "@push-based/source", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "license": "MIT", 6 | "scripts": { 7 | "publish:mcp": "nx build @push-based/angular-toolkit-mcp && cd packages/angular-mcp/dist && npm publish" 8 | }, 9 | "private": true, 10 | "devDependencies": { 11 | "@eslint/js": "^9.28.0", 12 | "@modelcontextprotocol/inspector": "^0.14.0", 13 | "@nx/angular": "21.0.4", 14 | "@nx/eslint": "21.0.4", 15 | "@nx/eslint-plugin": "21.0.4", 16 | "@nx/express": "21.0.4", 17 | "@nx/jest": "21.0.4", 18 | "@nx/js": "21.0.4", 19 | "@nx/node": "21.0.4", 20 | "@nx/vite": "21.0.4", 21 | "@nx/web": "21.0.4", 22 | "@nx/webpack": "21.0.4", 23 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.16", 24 | "@svgr/webpack": "^8.0.1", 25 | "@swc-node/register": "~1.10.10", 26 | "@swc/cli": "0.7.7", 27 | "@swc/core": "~1.11.31", 28 | "@swc/helpers": "~0.5.11", 29 | "@swc/jest": "~0.2.38", 30 | "@types/express": "^4.17.23", 31 | "@types/jest": "^29.5.12", 32 | "@types/node": "~18.16.20", 33 | "@vitest/coverage-v8": "^3.2.3", 34 | "@vitest/ui": "^3.2.3", 35 | "eslint": "^9.28.0", 36 | "eslint-config-prettier": "10.1.5", 37 | "eslint-plugin-functional": "^9.0.2", 38 | "eslint-plugin-unicorn": "^59.0.1", 39 | "ignore-loader": "^0.1.2", 40 | "jest": "^29.7.0", 41 | "jest-environment-node": "^29.7.0", 42 | "jiti": "2.4.2", 43 | "jsdom": "~22.1.0", 44 | "nx": "21.0.4", 45 | "prettier": "^3.5.3", 46 | "react-refresh": "^0.17.0", 47 | "simple-git": "^3.28.0", 48 | "ts-jest": "^29.3.4", 49 | "ts-node": "10.9.2", 50 | "tslib": "^2.3.0", 51 | "typescript": "~5.7.2", 52 | "typescript-eslint": "^8.34.0", 53 | "vite": "^6.3.5", 54 | "vitest": "^3.2.3", 55 | "webpack-cli": "^6.0.1" 56 | }, 57 | "workspaces": [ 58 | "packages/*", 59 | "packages/server/app/*", 60 | "packages/libs/*", 61 | "packages/shared/*", 62 | "shared" 63 | ], 64 | "dependencies": { 65 | "@angular-devkit/schematics": "~19.2.0", 66 | "@angular/cli": "~19.2.0", 67 | "@angular/compiler": "~19.2.0", 68 | "@code-pushup/core": "^0.75.0", 69 | "@code-pushup/eslint-plugin": "^0.75.0", 70 | "@code-pushup/models": "^0.75.0", 71 | "@code-pushup/utils": "^0.75.0", 72 | "@modelcontextprotocol/sdk": "^1.12.1", 73 | "@push-based/ds-component-coverage": "^0.0.1", 74 | "@push-based/models": "^0.0.1", 75 | "@push-based/utils": "^0.0.1", 76 | "@vue/language-core": "^2.2.10", 77 | "axios": "^1.9.0", 78 | "express": "^4.21.2", 79 | "memfs": "^4.17.0", 80 | "microdiff": "^1.5.0", 81 | "postcss": "^8.5.4", 82 | "postcss-safe-parser": "^7.0.1", 83 | "simplegit": "^1.0.2", 84 | "ts-morph": "^26.0.0", 85 | "vite-plugin-dts": "^4.5.4", 86 | "zod": "^3.25.57" 87 | } 88 | } 89 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component/utils/doc-helpers.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import { 4 | validateComponentName, 5 | componentNameToTagName, 6 | componentNameToKebabCase, 7 | } from '../../shared/utils/component-validation.js'; 8 | import { resolveCrossPlatformPath } from '../../shared/utils/cross-platform-path.js'; 9 | 10 | export interface ComponentDocPaths { 11 | componentName: string; 12 | folderSlug: string; 13 | tagName: string; 14 | paths: { api: string; overview: string }; 15 | } 16 | 17 | export interface ComponentDocContent { 18 | componentName: string; 19 | tagName: string; 20 | api: string | null; 21 | overview: string | null; 22 | } 23 | 24 | export function getComponentDocPathsForName( 25 | docsBasePath: string, 26 | componentName: string, 27 | ): ComponentDocPaths { 28 | const folderSlug = componentNameToKebabCase(componentName); 29 | const tagName = componentNameToTagName(componentName); 30 | const base = path.join(docsBasePath, folderSlug); 31 | return { 32 | componentName, 33 | folderSlug, 34 | tagName, 35 | paths: { 36 | api: path.join(base, `${folderSlug}-tabs/api.mdx`), 37 | overview: path.join(base, `${folderSlug}-tabs/overview.mdx`), 38 | }, 39 | }; 40 | } 41 | 42 | export function enrichSingleComponentDoc( 43 | doc: ComponentDocPaths, 44 | ): ComponentDocContent { 45 | let apiContent = null; 46 | let overviewContent = null; 47 | 48 | if (fs.existsSync(doc.paths.api)) { 49 | apiContent = fs.readFileSync(doc.paths.api, 'utf-8'); 50 | } 51 | if (fs.existsSync(doc.paths.overview)) { 52 | overviewContent = fs.readFileSync(doc.paths.overview, 'utf-8'); 53 | } 54 | 55 | return { 56 | componentName: doc.componentName, 57 | tagName: doc.tagName, 58 | api: apiContent, 59 | overview: overviewContent, 60 | }; 61 | } 62 | 63 | /** 64 | * Reusable helper function to get component documentation 65 | * @param componentName - The name of the component (e.g., DsButton) 66 | * @param storybookDocsRoot - The root path to the storybook docs 67 | * @param cwd - Current working directory (optional, defaults to process.cwd()) 68 | * @returns Component documentation with API and Overview content 69 | * @throws Error if component validation fails or documentation retrieval fails 70 | */ 71 | export function getComponentDocs( 72 | componentName: string, 73 | storybookDocsRoot: string, 74 | cwd: string = process.cwd(), 75 | ): ComponentDocContent { 76 | try { 77 | validateComponentName(componentName); 78 | 79 | const docsBasePath = resolveCrossPlatformPath(cwd, storybookDocsRoot); 80 | const docPaths = getComponentDocPathsForName(docsBasePath, componentName); 81 | const doc = enrichSingleComponentDoc(docPaths); 82 | 83 | if (!doc || (!doc.api && !doc.overview)) { 84 | throw new Error(`No documentation found for component: ${componentName}`); 85 | } 86 | 87 | return doc; 88 | } catch (ctx) { 89 | throw new Error( 90 | `Error retrieving component documentation: ${(ctx as Error).message}`, 91 | ); 92 | } 93 | } 94 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/design-system/storybook-host-app/src/components/segmented-control/segmented-control-tabs/overview.mdx: -------------------------------------------------------------------------------- ```markdown 1 | import { Canvas } from '@storybook/blocks'; 2 | 3 | import * as SegmentedControlStories from '../segmented-control.component.stories'; 4 | 5 | The `DsSegmentedControlModule` is used for toggling between multiple options within a group. It enhances user experience by providing a visually distinct and easily navigable selection mechanism. 6 | 7 | <Canvas of={SegmentedControlStories.Default} /> 8 | 9 | --- 10 | 11 | ## Usage 12 | 13 | Import `DsSegmentedControlModule` in your component, apply `ds-segmented-control` and `ds-segmented-options` selectors in your template. 14 | 15 | ```ts 16 | import { DsSegmentedControlModule } from '@frontend/ui/segmented-control'; // 👈 add to file imports 17 | 18 | @Component({ 19 | imports: [DsSegmentedControlModule], // 👈 add to component imports 20 | template: `...`, 21 | }) 22 | export class AppComponent {} 23 | ``` 24 | 25 | --- 26 | 27 | ## Additional Configuration 28 | 29 | ### Segmented item width (max-width) 30 | 31 | For text truncation we have token for min-width(44px) so it will apply the same for all the items but you need to specify the "max-width" for segmented item so that it will take the max width for the option and then it will truncate if it is more than that width. 32 | You can customize the width of the segment item in the `segmented-control` using CSS variables. Set these variables in your CSS to adjust the width: 33 | 34 | - `--ds-segment-item-text-max-width`: Sets the max-width of the segment item. Default is `auto`. 35 | 36 | To adjust the width, add the following CSS to your styles: 37 | 38 | ```html 39 | <ds-segmented-control class="ds-segmented-control component-class-name"> 40 | <ds-segment-item>...</ds-segment-item> 41 | </ds-segmented-control> 42 | ``` 43 | 44 | ```css 45 | .component-class-name { 46 | --ds-segment-item-text-max-width: 100px; 47 | } 48 | ``` 49 | 50 | --- 51 | 52 | ## Accessibility 53 | 54 | - The `role` attribute on the segmented control is dynamically set to `tablist` or `radiogroup`. 55 | - Each `ds-segmented-option`: 56 | - Uses `role="tab"` or `role="radio"` depending on control type 57 | - Has `aria-selected` or `aria-checked` to reflect selection state 58 | - Includes `aria-label` using the option's `title` or `name` 59 | - Uses `tabindex="0"` for selected, `-1` for others 60 | - Supports keyboard navigation: 61 | - `ArrowLeft` / `ArrowRight` for focus movement 62 | - `Enter` / `Space` to activate 63 | - Screen reader support confirmed via virtual-screen-reader tests 64 | 65 | --- 66 | 67 | ## Test Coverage 68 | 69 | - Loads and renders segmented control and its options 70 | - Selects options using: 71 | - `selectTabByText` 72 | - `selectTabByName` 73 | - Supports toggling `fullWidth` and `inverse` inputs 74 | - Switches between `tablist` and `radiogroup` roles 75 | - Emits `activeOptionChange` when selection changes 76 | - Keyboard navigation (arrow keys, enter, space) 77 | - All roles and ARIA states validated for accessibility 78 | - Screen reader flows tested using `@guidepup/virtual-screen-reader` 79 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/list/utils/contract-list-utils.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Utility functions for contract listing and file operations 3 | */ 4 | 5 | import type { ContractFileInfo } from '../models/types.js'; 6 | 7 | /** 8 | * Extracts component name from contract filename 9 | * Handles pattern: componentName-timestamp.contract.json 10 | */ 11 | export function extractComponentNameFromFile(fileName: string): string { 12 | const baseName = fileName.replace('.contract.json', ''); 13 | const parts = baseName.split('-'); 14 | 15 | if (parts.length > 2) { 16 | let timestampStartIndex = -1; 17 | for (let i = 1; i < parts.length; i++) { 18 | if (/^\d{4}$/.test(parts[i])) { 19 | timestampStartIndex = i; 20 | break; 21 | } 22 | } 23 | 24 | if (timestampStartIndex > 0) { 25 | return parts.slice(0, timestampStartIndex).join('-'); 26 | } 27 | } 28 | 29 | return parts[0] || 'unknown'; 30 | } 31 | 32 | /** 33 | * Formats byte size into human-readable format 34 | */ 35 | export function formatBytes(bytes: number): string { 36 | if (bytes === 0) return '0 B'; 37 | 38 | const k = 1024; 39 | const sizes = ['B', 'KB', 'MB']; 40 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 41 | 42 | return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; 43 | } 44 | 45 | /** 46 | * Converts timestamp to human-readable "time ago" format 47 | */ 48 | export function getTimeAgo(timestamp: string): string { 49 | const now = new Date(); 50 | const past = new Date(timestamp); 51 | const diffMs = now.getTime() - past.getTime(); 52 | 53 | const minutes = Math.floor(diffMs / (1000 * 60)); 54 | const hours = Math.floor(diffMs / (1000 * 60 * 60)); 55 | const days = Math.floor(diffMs / (1000 * 60 * 60 * 24)); 56 | 57 | if (minutes < 1) return 'just now'; 58 | if (minutes < 60) return `${minutes}m ago`; 59 | if (hours < 24) return `${hours}h ago`; 60 | if (days < 7) return `${days}d ago`; 61 | 62 | return past.toLocaleDateString(); 63 | } 64 | 65 | /** 66 | * Groups contracts by component name and formats them for display output 67 | */ 68 | export function formatContractsByComponent( 69 | contracts: ContractFileInfo[], 70 | ): string[] { 71 | const output: string[] = []; 72 | 73 | const contractsByComponent = new Map<string, ContractFileInfo[]>(); 74 | contracts.forEach((contract) => { 75 | const componentContracts = 76 | contractsByComponent.get(contract.componentName) || []; 77 | componentContracts.push(contract); 78 | contractsByComponent.set(contract.componentName, componentContracts); 79 | }); 80 | 81 | contractsByComponent.forEach((componentContracts, componentName) => { 82 | output.push(`🎯 ${componentName}:`); 83 | 84 | componentContracts.forEach((contract) => { 85 | const timeAgo = getTimeAgo(contract.timestamp); 86 | output.push(` 📄 ${contract.fileName}`); 87 | output.push(` 🔗 ${contract.filePath}`); 88 | output.push(` ⏱️ ${timeAgo}`); 89 | output.push(` 🔑 ${contract.hash.substring(0, 12)}...`); 90 | output.push(` 📊 ${contract.size}`); 91 | }); 92 | 93 | output.push(''); 94 | }); 95 | 96 | return output; 97 | } 98 | ```