This is page 4 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/shared/typescript-ast-utils/ai/FUNCTIONS.md: -------------------------------------------------------------------------------- ```markdown 1 | # Public API — Quick Reference 2 | 3 | | Symbol | Kind | Summary | 4 | | ---------------------- | -------- | --------------------------------------------------------------- | 5 | | `QUOTE_REGEX` | constant | Regular expression for matching quotes at start/end of strings | 6 | | `getDecorators` | function | Extract decorators from a TypeScript AST node safely | 7 | | `hasDecorators` | function | Type guard to check if a node has decorators property | 8 | | `isComponentDecorator` | function | Check if a decorator is specifically a `@Component` decorator | 9 | | `isDecorator` | function | Check if a decorator matches a specific name (or any decorator) | 10 | | `removeQuotes` | function | Remove surrounding quotes from a string literal AST node | 11 | 12 | ## Function Signatures 13 | 14 | ### `getDecorators(node: ts.Node): readonly ts.Decorator[]` 15 | 16 | Safely extracts decorators from a TypeScript AST node, handling different TypeScript compiler API versions. 17 | 18 | **Parameters:** 19 | 20 | - `node` - The TypeScript AST node to extract decorators from 21 | 22 | **Returns:** Array of decorators (empty array if none found) 23 | 24 | ### `hasDecorators(node: ts.Node): node is ts.Node & { decorators: readonly ts.Decorator[] }` 25 | 26 | Type guard function that checks if a node has a decorators property. 27 | 28 | **Parameters:** 29 | 30 | - `node` - The TypeScript AST node to check 31 | 32 | **Returns:** Type predicate indicating if the node has decorators 33 | 34 | ### `isComponentDecorator(decorator: ts.Decorator): boolean` 35 | 36 | Convenience function to check if a decorator is specifically a `@Component` decorator. 37 | 38 | **Parameters:** 39 | 40 | - `decorator` - The decorator to check 41 | 42 | **Returns:** `true` if the decorator is `@Component`, `false` otherwise 43 | 44 | ### `isDecorator(decorator: ts.Decorator, decoratorName?: string): boolean` 45 | 46 | Generic function to check if a decorator matches a specific name or is any valid decorator. 47 | 48 | **Parameters:** 49 | 50 | - `decorator` - The decorator to check 51 | - `decoratorName` - Optional name to match against (if omitted, checks if it's any valid decorator) 52 | 53 | **Returns:** `true` if the decorator matches the criteria, `false` otherwise 54 | 55 | ### `removeQuotes(node: ts.Node, sourceFile: ts.SourceFile): string` 56 | 57 | Removes surrounding quotes from a string literal AST node's text content. 58 | 59 | **Parameters:** 60 | 61 | - `node` - The TypeScript AST node containing the quoted string 62 | - `sourceFile` - The source file context for getting node text 63 | 64 | **Returns:** The string content without surrounding quotes 65 | 66 | ## Constants 67 | 68 | ### `QUOTE_REGEX: RegExp` 69 | 70 | Regular expression pattern `/^['"`]+|['"`]+$/g` used to match and remove quotes from the beginning and end of strings. Supports single quotes (`'`), double quotes (`"` ), and backticks (`` ` ``). 71 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/shared/models/schema-helpers.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ToolSchemaOptions } from '@push-based/models'; 2 | 3 | /** 4 | * Common schema property definitions used across DS tools 5 | * Note: cwd and workspaceRoot are injected by MCP server configuration, not user inputs 6 | */ 7 | export const COMMON_SCHEMA_PROPERTIES = { 8 | directory: { 9 | type: 'string' as const, 10 | description: 11 | 'The relative path to the directory (starting with "./path/to/dir") to scan. Respect the OS specifics.', 12 | }, 13 | 14 | componentName: { 15 | type: 'string' as const, 16 | description: 'The class name of the component (e.g., DsButton)', 17 | }, 18 | 19 | groupBy: { 20 | type: 'string' as const, 21 | enum: ['file', 'folder'] as const, 22 | description: 'How to group the results', 23 | default: 'file' as const, 24 | }, 25 | } as const; 26 | 27 | /** 28 | * Creates a component input schema with a custom description 29 | */ 30 | export const createComponentInputSchema = ( 31 | description: string, 32 | ): ToolSchemaOptions['inputSchema'] => ({ 33 | type: 'object', 34 | properties: { 35 | componentName: { 36 | ...COMMON_SCHEMA_PROPERTIES.componentName, 37 | description, 38 | }, 39 | }, 40 | required: ['componentName'], 41 | }); 42 | 43 | /** 44 | * Creates a directory + component schema for tools that analyze both 45 | */ 46 | export const createDirectoryComponentSchema = ( 47 | componentDescription: string, 48 | additionalProperties?: Record<string, any>, 49 | ): ToolSchemaOptions['inputSchema'] => ({ 50 | type: 'object', 51 | properties: { 52 | directory: COMMON_SCHEMA_PROPERTIES.directory, 53 | componentName: { 54 | ...COMMON_SCHEMA_PROPERTIES.componentName, 55 | description: componentDescription, 56 | }, 57 | ...additionalProperties, 58 | }, 59 | required: ['directory', 'componentName'], 60 | }); 61 | 62 | /** 63 | * Creates a project analysis schema with common project properties 64 | * Note: cwd and workspaceRoot are handled by MCP server configuration, not user inputs 65 | */ 66 | export const createProjectAnalysisSchema = ( 67 | additionalProperties?: Record<string, any>, 68 | ): ToolSchemaOptions['inputSchema'] => ({ 69 | type: 'object', 70 | properties: { 71 | directory: COMMON_SCHEMA_PROPERTIES.directory, 72 | ...additionalProperties, 73 | }, 74 | required: ['directory'], 75 | }); 76 | 77 | /** 78 | * Creates a violation reporting schema with grouping options 79 | */ 80 | export const createViolationReportingSchema = ( 81 | additionalProperties?: Record<string, any>, 82 | ): ToolSchemaOptions['inputSchema'] => ({ 83 | type: 'object', 84 | properties: { 85 | directory: COMMON_SCHEMA_PROPERTIES.directory, 86 | componentName: COMMON_SCHEMA_PROPERTIES.componentName, 87 | groupBy: COMMON_SCHEMA_PROPERTIES.groupBy, 88 | ...additionalProperties, 89 | }, 90 | required: ['directory', 'componentName'], 91 | }); 92 | 93 | /** 94 | * Common annotation patterns for DS tools 95 | */ 96 | export const COMMON_ANNOTATIONS = { 97 | readOnly: { 98 | readOnlyHint: true, 99 | openWorldHint: true, 100 | idempotentHint: true, 101 | }, 102 | project: { 103 | readOnlyHint: true, 104 | openWorldHint: true, 105 | idempotentHint: true, 106 | }, 107 | } as const; 108 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-usage-graph/models/config.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { FileExtension, FileType } from './types.js'; 2 | import { 3 | IMPORT_REGEXES, 4 | REGEX_CACHE_UTILS, 5 | } from '../../shared/utils/regex-helpers.js'; 6 | 7 | const STYLES_EXTENSIONS = ['.css', '.scss', '.sass', '.less'] as const; 8 | const SCRIPT_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'] as const; 9 | const TEMPLATE_EXTENSIONS = ['.html'] as const; 10 | const FILE_EXTENSIONS = [ 11 | ...STYLES_EXTENSIONS, 12 | ...SCRIPT_EXTENSIONS, 13 | ...TEMPLATE_EXTENSIONS, 14 | ] as const; 15 | const RESOLVE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'] as const; 16 | const INDEX_FILES = [ 17 | '/index.ts', 18 | '/index.tsx', 19 | '/index.js', 20 | '/index.jsx', 21 | ] as const; 22 | const EXCLUDE_PATTERNS = [ 23 | 'node_modules', 24 | 'dist', 25 | 'build', 26 | '.git', 27 | '.vscode', 28 | '.idea', 29 | ] as const; 30 | 31 | const FILE_TYPE_MAP = { 32 | '.ts': 'typescript', 33 | '.tsx': 'typescript-react', 34 | '.js': 'javascript', 35 | '.jsx': 'javascript-react', 36 | '.css': 'css', 37 | '.scss': 'scss', 38 | '.sass': 'sass', 39 | '.less': 'less', 40 | '.html': 'template', 41 | } as const satisfies Record<FileExtension, FileType>; 42 | 43 | export const DEPENDENCY_ANALYSIS_CONFIG = { 44 | stylesExtensions: STYLES_EXTENSIONS, 45 | scriptExtensions: SCRIPT_EXTENSIONS, 46 | fileExtensions: FILE_EXTENSIONS, 47 | resolveExtensions: RESOLVE_EXTENSIONS, 48 | indexFiles: INDEX_FILES, 49 | excludePatterns: EXCLUDE_PATTERNS, 50 | fileTypeMap: FILE_TYPE_MAP, 51 | } as const; 52 | 53 | // Use shared regex patterns instead of duplicating them 54 | export const REGEX_PATTERNS = { 55 | ES6_IMPORT: IMPORT_REGEXES.ES6_IMPORT, 56 | COMMONJS_REQUIRE: IMPORT_REGEXES.COMMONJS_REQUIRE, 57 | DYNAMIC_IMPORT: IMPORT_REGEXES.DYNAMIC_IMPORT, 58 | CSS_IMPORT: IMPORT_REGEXES.CSS_IMPORT, 59 | CSS_URL: IMPORT_REGEXES.CSS_URL, 60 | ANGULAR_COMPONENT_DECORATOR: IMPORT_REGEXES.ANGULAR_COMPONENT_DECORATOR, 61 | GLOB_WILDCARD_REPLACEMENT: /\*/g, 62 | } as const; 63 | 64 | // Use shared regex cache utilities instead of duplicating cache management 65 | export const componentImportRegex = (componentName: string): RegExp => 66 | REGEX_CACHE_UTILS.getOrCreate(`component-import-${componentName}`, () => 67 | IMPORT_REGEXES.createComponentImportRegex(componentName), 68 | ); 69 | 70 | export const getComponentImportRegex = (componentName: string): RegExp => 71 | componentImportRegex(componentName); 72 | 73 | export const getCombinedComponentImportRegex = ( 74 | componentNames: string[], 75 | ): RegExp => { 76 | const cacheKey = componentNames.sort().join('|'); 77 | return REGEX_CACHE_UTILS.getOrCreate( 78 | `combined-component-import-${cacheKey}`, 79 | () => IMPORT_REGEXES.createCombinedComponentImportRegex(componentNames), 80 | ); 81 | }; 82 | 83 | export const clearComponentImportRegexCache = (): void => { 84 | REGEX_CACHE_UTILS.clear(); 85 | }; 86 | 87 | export const getComponentImportRegexCacheStats = () => { 88 | const stats = REGEX_CACHE_UTILS.getStats(); 89 | return { 90 | singleComponentCacheSize: stats.size, // Combined cache now 91 | combinedComponentCacheSize: 0, // No longer separate 92 | totalCacheSize: stats.size, 93 | }; 94 | }; 95 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/group-4/multi-violation-test.component.html: -------------------------------------------------------------------------------- ```html 1 | <!-- Multi-violation test component using deprecated CSS classes from 4 DS components --> 2 | 3 | <div class="test-container"> 4 | <h2>Multi-Violation Test Component</h2> 5 | 6 | <!-- ❌ BAD: DsButton violations - using deprecated 'btn', 'btn-primary', 'legacy-button' --> 7 | <div class="button-section"> 8 | <h3>Button Violations (DsButton)</h3> 9 | <button class="btn btn-primary" (click)="handleButtonClick()"> 10 | Primary Legacy Button 11 | </button> 12 | <button class="btn"> 13 | Basic Legacy Button 14 | </button> 15 | <button class="legacy-button"> 16 | Legacy Button Style 17 | </button> 18 | </div> 19 | 20 | <!-- ❌ BAD: DsBadge violations - using deprecated 'offer-badge' --> 21 | <div class="badge-section"> 22 | <h3>Badge Violations (DsBadge)</h3> 23 | <span class="offer-badge">50% OFF</span> 24 | <div class="product-item"> 25 | <span>Special Product</span> 26 | <span class="offer-badge">NEW</span> 27 | </div> 28 | </div> 29 | 30 | <!-- ❌ BAD: DsTabsModule violations - using deprecated 'tab-nav', 'nav-tabs', 'tab-nav-item' --> 31 | <div class="tabs-section"> 32 | <h3>Tabs Violations (DsTabsModule)</h3> 33 | <ul class="nav-tabs tab-nav"> 34 | <li class="tab-nav-item" 35 | [class.active]="activeTab === 0" 36 | (click)="switchTab(0)"> 37 | Tab 1 38 | </li> 39 | <li class="tab-nav-item" 40 | [class.active]="activeTab === 1" 41 | (click)="switchTab(1)"> 42 | Tab 2 43 | </li> 44 | <li class="tab-nav-item" 45 | [class.active]="activeTab === 2" 46 | (click)="switchTab(2)"> 47 | Tab 3 48 | </li> 49 | </ul> 50 | <div class="tab-content"> 51 | <div *ngIf="activeTab === 0">Content for Tab 1</div> 52 | <div *ngIf="activeTab === 1">Content for Tab 2</div> 53 | <div *ngIf="activeTab === 2">Content for Tab 3</div> 54 | </div> 55 | </div> 56 | 57 | <!-- ❌ BAD: DsCard violations - using deprecated 'card' --> 58 | <div class="card-section"> 59 | <h3>Card Violations (DsCard)</h3> 60 | <div class="card" *ngIf="showCard"> 61 | <div class="card-header"> 62 | <h4>Legacy Card Header</h4> 63 | <button class="btn" (click)="toggleCard()">×</button> 64 | </div> 65 | <div class="card-body"> 66 | <p>This is a legacy card implementation using deprecated CSS classes.</p> 67 | <span class="offer-badge">FEATURED</span> 68 | </div> 69 | <div class="card-footer"> 70 | <button class="legacy-button">Action</button> 71 | </div> 72 | </div> 73 | </div> 74 | 75 | <!-- Mixed violations in a single section --> 76 | <div class="mixed-section"> 77 | <h3>Mixed Violations</h3> 78 | <div class="card"> 79 | <nav class="tab-nav"> 80 | <span class="tab-nav-item">Settings</span> 81 | <span class="offer-badge">{{notifications}}</span> 82 | </nav> 83 | <div class="card-body"> 84 | <button class="btn btn-primary">Save Settings</button> 85 | <button class="legacy-button">Cancel</button> 86 | </div> 87 | </div> 88 | </div> 89 | </div> 90 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/spec/inline-styles.collector.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | /* eslint-disable prefer-const */ 2 | import { describe, it, expect, vi, beforeEach } from 'vitest'; 3 | 4 | // ----------------------------------------------------------------------------- 5 | // Mocks 6 | // ----------------------------------------------------------------------------- 7 | 8 | // @push-based/styles-ast-utils 9 | let parseStylesheetMock: any; 10 | let visitEachChildMock: any; 11 | vi.mock('@push-based/styles-ast-utils', () => ({ 12 | get parseStylesheet() { 13 | return parseStylesheetMock; 14 | }, 15 | get visitEachChild() { 16 | return visitEachChildMock; 17 | }, 18 | })); 19 | parseStylesheetMock = vi.fn(); 20 | visitEachChildMock = vi.fn(); 21 | 22 | // css-match 23 | let selectorMatchesMock: any; 24 | vi.mock('../utils/css-match.js', () => ({ 25 | get selectorMatches() { 26 | return selectorMatchesMock; 27 | }, 28 | })); 29 | selectorMatchesMock = vi.fn(); 30 | 31 | // SUT 32 | import { collectInlineStyles } from '../utils/inline-styles.collector.js'; 33 | 34 | // ----------------------------------------------------------------------------- 35 | // Helpers 36 | // ----------------------------------------------------------------------------- 37 | function createRule(selector: string, decls: Record<string, string>) { 38 | return { 39 | selector, 40 | walkDecls(cb: (decl: { prop: string; value: string }) => void) { 41 | Object.entries(decls).forEach(([prop, value]) => cb({ prop, value })); 42 | }, 43 | } as any; 44 | } 45 | 46 | function resetMocks() { 47 | parseStylesheetMock.mockReset(); 48 | visitEachChildMock.mockReset(); 49 | selectorMatchesMock.mockReset(); 50 | } 51 | 52 | // ----------------------------------------------------------------------------- 53 | // Tests 54 | // ----------------------------------------------------------------------------- 55 | 56 | describe('collectInlineStyles', () => { 57 | beforeEach(() => { 58 | resetMocks(); 59 | // Provide a simple PostCSS root mock 60 | parseStylesheetMock.mockReturnValue({ root: { type: 'root' } }); 61 | }); 62 | 63 | it('returns style declarations from inline styles', async () => { 64 | // Fake ParsedComponent with inline styles array 65 | const parsedComponent = { 66 | fileName: '/cmp.ts', 67 | styles: [ 68 | { 69 | parse: async () => ({ 70 | toString: () => '.btn{color:red}', 71 | }), 72 | }, 73 | ], 74 | } as any; 75 | 76 | // DOM snapshot with one matching element 77 | const dom = { 78 | button: {} as any, 79 | }; 80 | 81 | visitEachChildMock.mockImplementation((_root: any, visitor: any) => { 82 | visitor.visitRule(createRule('.btn', { color: 'red' })); 83 | }); 84 | 85 | selectorMatchesMock.mockImplementation( 86 | (cssSel: string, domKey: string) => 87 | cssSel === '.btn' && domKey === 'button', 88 | ); 89 | 90 | const styles = await collectInlineStyles(parsedComponent, dom as any); 91 | 92 | expect(styles.sourceFile).toBe('/cmp.ts'); 93 | expect(styles.rules['.btn']).toBeDefined(); 94 | expect(styles.rules['.btn'].properties).toEqual({ color: 'red' }); 95 | expect(styles.rules['.btn'].appliesTo).toEqual(['button']); 96 | }); 97 | }); 98 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/diff-component-contract.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | createHandler, 3 | BaseHandlerOptions, 4 | } from '../../shared/utils/handler-helpers.js'; 5 | import { 6 | resolveCrossPlatformPath, 7 | normalizePathsInObject, 8 | } from '../../shared/utils/cross-platform-path.js'; 9 | import { diffComponentContractSchema } from './models/schema.js'; 10 | import type { DomPathDictionary } from '../shared/models/types.js'; 11 | import { loadContract } from '../shared/utils/contract-file-ops.js'; 12 | import { 13 | consolidateAndPruneRemoveOperationsWithDeduplication, 14 | groupChangesByDomainAndType, 15 | generateDiffSummary, 16 | } from './utils/diff-utils.js'; 17 | import { writeFile, mkdir } from 'node:fs/promises'; 18 | import diff from 'microdiff'; 19 | 20 | interface DiffComponentContractOptions extends BaseHandlerOptions { 21 | saveLocation: string; 22 | contractBeforePath: string; 23 | contractAfterPath: string; 24 | dsComponentName?: string; 25 | } 26 | 27 | export const diffComponentContractHandler = createHandler< 28 | DiffComponentContractOptions, 29 | { 30 | fileUrl: string; 31 | domPathStats?: DomPathDictionary['stats']; 32 | } 33 | >( 34 | diffComponentContractSchema.name, 35 | async (params, { cwd, workspaceRoot }) => { 36 | const { 37 | saveLocation, 38 | contractBeforePath, 39 | contractAfterPath, 40 | dsComponentName = '', 41 | } = params; 42 | 43 | const effectiveBeforePath = resolveCrossPlatformPath( 44 | cwd, 45 | contractBeforePath, 46 | ); 47 | const effectiveAfterPath = resolveCrossPlatformPath(cwd, contractAfterPath); 48 | 49 | const contractBefore = await loadContract(effectiveBeforePath); 50 | const contractAfter = await loadContract(effectiveAfterPath); 51 | 52 | const rawDiffResult = diff(contractBefore, contractAfter); 53 | 54 | const { processedResult, domPathDict } = 55 | consolidateAndPruneRemoveOperationsWithDeduplication(rawDiffResult); 56 | 57 | const groupedChanges = groupChangesByDomainAndType(processedResult); 58 | 59 | const diffData = { 60 | before: effectiveBeforePath, 61 | after: effectiveAfterPath, 62 | dsComponentName, 63 | timestamp: new Date().toISOString(), 64 | domPathDictionary: domPathDict.paths, 65 | changes: groupedChanges, 66 | summary: generateDiffSummary(processedResult, groupedChanges), 67 | }; 68 | 69 | const normalizedDiffData = normalizePathsInObject(diffData, workspaceRoot); 70 | 71 | const effectiveSaveLocation = resolveCrossPlatformPath(cwd, saveLocation); 72 | 73 | const { dirname } = await import('node:path'); 74 | await mkdir(dirname(effectiveSaveLocation), { recursive: true }); 75 | 76 | const diffFilePath = effectiveSaveLocation; 77 | 78 | const formattedJson = JSON.stringify(normalizedDiffData, null, 2); 79 | await writeFile(diffFilePath, formattedJson, 'utf-8'); 80 | 81 | return { 82 | fileUrl: `file://${diffFilePath}`, 83 | domPathStats: domPathDict.stats, 84 | }; 85 | }, 86 | (result) => { 87 | return [result.fileUrl]; 88 | }, 89 | ); 90 | 91 | export const diffComponentContractTools = [ 92 | { 93 | schema: diffComponentContractSchema, 94 | handler: diffComponentContractHandler, 95 | }, 96 | ]; 97 | ``` -------------------------------------------------------------------------------- /packages/shared/ds-component-coverage/package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "@push-based/ds-component-coverage", 3 | "version": "0.0.1", 4 | "private": true, 5 | "type": "module", 6 | "main": "./dist/index.js", 7 | "module": "./dist/index.js", 8 | "types": "./dist/index.d.ts", 9 | "exports": { 10 | "./package.json": "./package.json", 11 | ".": { 12 | "development": "./src/index.ts", 13 | "types": "./dist/index.d.ts", 14 | "import": "./dist/index.js", 15 | "default": "./dist/index.js" 16 | } 17 | }, 18 | "nx": { 19 | "name": "ds-component-coverage", 20 | "targets": { 21 | "ds-component-coverage:demo": { 22 | "command": "npx @code-pushup/cli collect --config=packages/shared/ds-component-coverage/mocks/fixtures/e2e/demo/code-pushup.config.ts", 23 | "options": { 24 | "onlyPlugins": "ds-component-coverage", 25 | "progress": false 26 | } 27 | }, 28 | "ds-component-coverage:asset-location": { 29 | "command": "npx @code-pushup/cli collect --config=packages/ds-component-coverage/mocks/fixtures/e2e/asset-location/code-pushup.config.ts", 30 | "options": { 31 | "onlyPlugins": "ds-component-coverage", 32 | "progress": false 33 | } 34 | }, 35 | "ds-component-coverage:line-number": { 36 | "command": "npx @code-pushup/cli collect --config=packages/ds-component-coverage/mocks/fixtures/e2e/line-number/code-pushup.config.ts", 37 | "options": { 38 | "onlyPlugins": "ds-component-coverage", 39 | "progress": false 40 | } 41 | }, 42 | "ds-component-coverage:style-format": { 43 | "command": "npx @code-pushup/cli collect --config=packages/ds-component-coverage/mocks/fixtures/e2e/style-format/code-pushup.config.ts", 44 | "options": { 45 | "onlyPlugins": "ds-component-coverage", 46 | "progress": false 47 | } 48 | }, 49 | "ds-component-coverage:template-syntax": { 50 | "command": "npx @code-pushup/cli collect --config=packages/ds-component-coverage/mocks/fixtures/e2e/template-syntax/code-pushup.config.ts", 51 | "options": { 52 | "onlyPlugins": "ds-component-coverage", 53 | "progress": false 54 | } 55 | }, 56 | "ds-quality:demo": { 57 | "command": "npx @code-pushup/cli collect --config=packages/ds-quality/mocks/fixtures/minimal-design-system/code-pushup.config.ts", 58 | "options": { 59 | "onlyPlugins": "ds-quality", 60 | "progress": false 61 | } 62 | }, 63 | "ds-quality:variable-usage": { 64 | "command": "npx @code-pushup/cli collect --config=packages/ds-quality/mocks/fixtures/variable-usage/code-pushup.config.ts", 65 | "options": { 66 | "onlyPlugins": "ds-quality", 67 | "progress": false 68 | } 69 | }, 70 | "ds-quality:mixin-usage": { 71 | "command": "npx @code-pushup/cli collect --config=packages/ds-quality/mocks/fixtures/mixin-usage/code-pushup.config.ts", 72 | "options": { 73 | "onlyPlugins": "ds-quality", 74 | "progress": false 75 | } 76 | } 77 | } 78 | } 79 | } 80 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/project/report-deprecated-css.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ToolSchemaOptions } from '@push-based/models'; 2 | import * as path from 'node:path'; 3 | import { 4 | createHandler, 5 | BaseHandlerOptions, 6 | RESULT_FORMATTERS, 7 | } from '../shared/utils/handler-helpers.js'; 8 | import { 9 | createDirectoryComponentSchema, 10 | COMMON_ANNOTATIONS, 11 | } from '../shared/models/schema-helpers.js'; 12 | import { getDeprecatedCssClasses } from '../component/utils/deprecated-css-helpers.js'; 13 | import { 14 | findStyleFiles, 15 | analyzeStyleFile, 16 | } from './utils/styles-report-helpers.js'; 17 | import { resolveCrossPlatformPath } from '../shared/utils/cross-platform-path.js'; 18 | 19 | interface ReportDeprecatedCssOptions extends BaseHandlerOptions { 20 | directory: string; 21 | componentName: string; 22 | deprecatedCssClassesPath?: string; 23 | } 24 | 25 | export const reportDeprecatedCssSchema: ToolSchemaOptions = { 26 | name: 'report-deprecated-css', 27 | description: `Report deprecated CSS classes found in styling files in a directory.`, 28 | inputSchema: createDirectoryComponentSchema( 29 | 'The class name of the component to get deprecated classes for (e.g., DsButton)', 30 | ), 31 | annotations: { 32 | title: 'Report Deprecated CSS', 33 | ...COMMON_ANNOTATIONS.readOnly, 34 | }, 35 | }; 36 | 37 | export const reportDeprecatedCssHandler = createHandler< 38 | ReportDeprecatedCssOptions, 39 | string[] 40 | >( 41 | reportDeprecatedCssSchema.name, 42 | async (params, { cwd, deprecatedCssClassesPath }) => { 43 | const { directory, componentName } = params; 44 | 45 | if (!deprecatedCssClassesPath) { 46 | throw new Error( 47 | 'Missing ds.deprecatedCssClassesPath. Provide --ds.deprecatedCssClassesPath in mcp.json file.', 48 | ); 49 | } 50 | 51 | const deprecated = await getDeprecatedCssClasses( 52 | componentName, 53 | deprecatedCssClassesPath, 54 | cwd, 55 | ); 56 | 57 | if (!deprecated.length) { 58 | return [`No deprecated CSS classes defined for ${componentName}`]; 59 | } 60 | 61 | const styleFiles = await findStyleFiles( 62 | resolveCrossPlatformPath(cwd, directory), 63 | ); 64 | 65 | if (!styleFiles.length) { 66 | return [`No styling files found in ${directory}`]; 67 | } 68 | 69 | const results = await Promise.all( 70 | styleFiles.map((f) => analyzeStyleFile(f, deprecated)), 71 | ); 72 | 73 | const violations: string[] = []; 74 | 75 | for (const { filePath, foundClasses } of results) { 76 | if (!foundClasses.length) continue; 77 | 78 | const relativePath = path.relative(cwd, filePath); 79 | 80 | for (const { className, lineNumber } of foundClasses) { 81 | const lineInfo = lineNumber ? ` (line ${lineNumber})` : ''; 82 | violations.push( 83 | `${relativePath}${lineInfo}: The selector's class \`${className}\` is deprecated.`, 84 | ); 85 | } 86 | } 87 | 88 | return violations.length ? violations : ['No deprecated CSS classes found']; 89 | }, 90 | (result) => RESULT_FORMATTERS.list(result, 'Design System CSS Violations:'), 91 | ); 92 | 93 | export const reportDeprecatedCssTools = [ 94 | { 95 | schema: reportDeprecatedCssSchema, 96 | handler: reportDeprecatedCssHandler, 97 | }, 98 | ]; 99 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component/utils/paths-helpers.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import { 4 | validateComponentName, 5 | componentNameToKebabCase, 6 | } from '../../shared/utils/component-validation.js'; 7 | import { resolveCrossPlatformPath } from '../../shared/utils/cross-platform-path.js'; 8 | 9 | export interface ComponentPaths { 10 | componentName: string; 11 | folderSlug: string; 12 | srcPath: string; 13 | packageJsonPath: string; 14 | } 15 | 16 | export interface ComponentPathsInfo { 17 | srcPath: string; 18 | importPath: string; 19 | relativeSrcPath: string; 20 | } 21 | 22 | export function getComponentPaths( 23 | uiRoot: string, 24 | componentName: string, 25 | ): ComponentPaths { 26 | const folderSlug = componentNameToKebabCase(componentName); 27 | const componentFolder = path.join(uiRoot, folderSlug); 28 | const srcPath = path.join(componentFolder, 'src'); 29 | const packageJsonPath = path.join(componentFolder, 'package.json'); 30 | 31 | return { 32 | componentName, 33 | folderSlug, 34 | srcPath, 35 | packageJsonPath, 36 | }; 37 | } 38 | 39 | export function getImportPathFromPackageJson( 40 | packageJsonPath: string, 41 | ): string | null { 42 | try { 43 | if (!fs.existsSync(packageJsonPath)) { 44 | return null; 45 | } 46 | 47 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); 48 | return packageJson.name || null; 49 | } catch (ctx) { 50 | throw new Error( 51 | `Error reading import path from package.json: ${(ctx as Error).message}`, 52 | ); 53 | } 54 | } 55 | 56 | /** 57 | * Reusable helper function to get component paths information 58 | * @param componentName - The class name of the component (e.g., DsBadge) 59 | * @param uiRoot - The UI root directory path 60 | * @param cwd - Current working directory (optional, defaults to process.cwd()) 61 | * @returns Object containing source path and import path information 62 | */ 63 | export function getComponentPathsInfo( 64 | componentName: string, 65 | uiRoot: string, 66 | cwd: string = process.cwd(), 67 | ): ComponentPathsInfo { 68 | try { 69 | validateComponentName(componentName); 70 | 71 | if (!uiRoot || typeof uiRoot !== 'string') { 72 | throw new Error('uiRoot must be provided and be a string path.'); 73 | } 74 | 75 | const componentsBasePath = resolveCrossPlatformPath(cwd, uiRoot); 76 | const componentPaths = getComponentPaths(componentsBasePath, componentName); 77 | 78 | const relativeComponentPaths = getComponentPaths(uiRoot, componentName); 79 | 80 | if (!fs.existsSync(componentPaths.srcPath)) { 81 | throw new Error( 82 | `Component source directory not found: ${relativeComponentPaths.srcPath}`, 83 | ); 84 | } 85 | 86 | const importPath = getImportPathFromPackageJson( 87 | componentPaths.packageJsonPath, 88 | ); 89 | 90 | if (!importPath) { 91 | throw new Error( 92 | `Could not read import path from package.json for component: ${componentName}`, 93 | ); 94 | } 95 | 96 | return { 97 | srcPath: componentPaths.srcPath, 98 | importPath, 99 | relativeSrcPath: relativeComponentPaths.srcPath, 100 | }; 101 | } catch (ctx) { 102 | throw new Error( 103 | `Error retrieving component information: ${(ctx as Error).message}`, 104 | ); 105 | } 106 | } 107 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/design-system/storybook-host-app/src/components/modal/modal-tabs/overview.mdx: -------------------------------------------------------------------------------- ```markdown 1 | You are tasked with creating a comprehensive refactoring plan for migrating legacy markup to a single design-system component. This plan should cover templates, class TypeScript files, styles, NgModules, and specs surfaced by the usage graph. Follow these instructions carefully to generate the plan: 2 | 3 | 1. Review the following input variables: 4 | <component_name> 5 | {{COMPONENT_NAME}} 6 | </component_name> 7 | 8 | <subfolder> 9 | {{SUBFOLDER}} 10 | </subfolder> 11 | 12 | <violations> 13 | {{VIOLATIONS}} 14 | </violations> 15 | 16 | <scan_result> 17 | {{SCAN_RESULT}} 18 | </scan_result> 19 | 20 | <file_scan> 21 | {{FILE_SCAN}} 22 | </file_scan> 23 | 24 | 2. Acquire reference material: 25 | a. Call the function get-component-docs with the component name as the argument. 26 | b. If there's an error or no docs are found, output a commentary message and stop. 27 | c. Parse the docs to create a dsExemplar object containing markup, required parts, optional parts, and API information. 28 | 29 | 3. Map dependencies and impact: 30 | a. Call the function build-component-usage-graph with the subfolder as the argument. 31 | b. If there's an error or any violation file is missing from the graph, output a commentary message and stop. 32 | c. Derive working sets for host templates, classes, styles, specs, and modules. 33 | 34 | 4. Generate baseline contracts: 35 | a. For each host template, call the build_component_contract function. 36 | b. If there's a contract build error, output a commentary message and stop. 37 | 38 | 5. Analyze refactorability: 39 | a. Compare each violation instance with the dsExemplar markup. 40 | b. Classify as "non-viable", "requires-restructure", or "simple-swap". 41 | c. Record template edits, CSS clean-up, and ancillary edits. 42 | d. Calculate complexity scores. 43 | e. Aggregate results into filePlans. 44 | 45 | 6. Synthesize and output the plan: 46 | a. Sort filePlans by complexity score, violation count, and file path. 47 | b. Generate the plan in the following format: 48 | 49 | <plan> 50 | [For each file in filePlans, include: 51 | - File path 52 | - File type (template|class|style|spec|module) 53 | - Refactor class (non-viable|requires-restructure|simple-swap) 54 | - Actions to take (bullet points for template edits, TS updates, style removal, NgModule changes, spec tweaks) 55 | - Complexity score] 56 | </plan> 57 | 58 | 7. After the plan, ask the following question: 59 | 🛠️ Approve this plan or specify adjustments? 60 | 61 | Important reminders: 62 | - Maintain NgModule-based structure (no stand-alone conversion). 63 | - Generate contracts only for host components (template + class). 64 | - Output must be minimal: a single <plan> block followed by one question. 65 | - All other messages or errors should be enclosed in <commentary> tags. 66 | - Ensure every host module needing a new import appears in filePlans. 67 | - Ensure every host spec appears in filePlans (even if action="none"). 68 | - Verify that dsExemplar was referenced at least once. 69 | 70 | Your final output should consist of only the <plan> block and the follow-up question. Any additional comments or error messages should be enclosed in <commentary> tags. ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/build-component-contract.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | createHandler, 3 | BaseHandlerOptions, 4 | } from '../../shared/utils/handler-helpers.js'; 5 | import { buildComponentContractSchema } from './models/schema.js'; 6 | import { buildComponentContract } from './utils/build-contract.js'; 7 | import { generateContractSummary } from '../shared/utils/contract-file-ops.js'; 8 | import { ContractResult } from './models/types.js'; 9 | import { resolveCrossPlatformPath } from '../../shared/utils/cross-platform-path.js'; 10 | import { createHash } from 'node:crypto'; 11 | 12 | interface BuildComponentContractOptions extends BaseHandlerOptions { 13 | saveLocation: string; 14 | templateFile?: string; 15 | styleFile?: string; 16 | typescriptFile: string; 17 | dsComponentName?: string; 18 | } 19 | 20 | export const buildComponentContractHandler = createHandler< 21 | BuildComponentContractOptions, 22 | ContractResult 23 | >( 24 | buildComponentContractSchema.name, 25 | async (params, { cwd, workspaceRoot: _workspaceRoot }) => { 26 | const { 27 | saveLocation, 28 | templateFile, 29 | styleFile, 30 | typescriptFile, 31 | dsComponentName = '', 32 | } = params; 33 | 34 | const effectiveTypescriptPath = resolveCrossPlatformPath( 35 | cwd, 36 | typescriptFile, 37 | ); 38 | 39 | // If templateFile or styleFile are not provided, use the TypeScript file path 40 | // This indicates inline template/styles 41 | const effectiveTemplatePath = templateFile 42 | ? resolveCrossPlatformPath(cwd, templateFile) 43 | : effectiveTypescriptPath; 44 | const effectiveScssPath = styleFile 45 | ? resolveCrossPlatformPath(cwd, styleFile) 46 | : effectiveTypescriptPath; 47 | 48 | const contract = await buildComponentContract( 49 | effectiveTemplatePath, 50 | effectiveScssPath, 51 | cwd, 52 | effectiveTypescriptPath, 53 | ); 54 | 55 | const contractString = JSON.stringify(contract, null, 2); 56 | const hash = createHash('sha256').update(contractString).digest('hex'); 57 | 58 | const effectiveSaveLocation = resolveCrossPlatformPath(cwd, saveLocation); 59 | 60 | const { mkdir, writeFile } = await import('node:fs/promises'); 61 | const { dirname } = await import('node:path'); 62 | await mkdir(dirname(effectiveSaveLocation), { recursive: true }); 63 | 64 | const contractData = { 65 | contract, 66 | hash: `sha256-${hash}`, 67 | metadata: { 68 | templatePath: effectiveTemplatePath, 69 | scssPath: effectiveScssPath, 70 | typescriptPath: effectiveTypescriptPath, 71 | timestamp: new Date().toISOString(), 72 | dsComponentName, 73 | }, 74 | }; 75 | 76 | await writeFile( 77 | effectiveSaveLocation, 78 | JSON.stringify(contractData, null, 2), 79 | 'utf-8', 80 | ); 81 | 82 | const contractFilePath = effectiveSaveLocation; 83 | 84 | return { 85 | contract, 86 | hash: `sha256-${hash}`, 87 | contractFilePath, 88 | }; 89 | }, 90 | (result) => { 91 | const summary = generateContractSummary(result.contract); 92 | return [ 93 | `✅ Contract Hash: ${result.hash}`, 94 | `📁 Saved to: ${result.contractFilePath}`, 95 | ...summary, 96 | ]; 97 | }, 98 | ); 99 | 100 | export const buildComponentContractTools = [ 101 | { 102 | schema: buildComponentContractSchema, 103 | handler: buildComponentContractHandler, 104 | }, 105 | ]; 106 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp/src/main.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | import express from 'express'; 3 | import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; 4 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 5 | import yargs from 'yargs'; 6 | import { hideBin } from 'yargs/helpers'; 7 | import { AngularMcpServerWrapper } from '@push-based/angular-mcp-server'; 8 | 9 | interface ArgvType { 10 | sse: boolean; 11 | port?: number; 12 | _: (string | number)[]; 13 | $0: string; 14 | 15 | [x: string]: unknown; 16 | } 17 | 18 | const argv = yargs(hideBin(process.argv)) 19 | .command('$0', 'Start the angular-mcp server') 20 | .option('workspaceRoot', { 21 | describe: 'The root directory of the workspace as absolute path', 22 | type: 'string', 23 | required: true, 24 | }) 25 | .option('ds.storybookDocsRoot', { 26 | describe: 27 | 'The root directory of the storybook docs relative from workspace root', 28 | type: 'string', 29 | }) 30 | .option('ds.deprecatedCssClassesPath', { 31 | describe: 32 | 'The path to the deprecated classes file relative from workspace root', 33 | type: 'string', 34 | }) 35 | .option('ds.uiRoot', { 36 | describe: 37 | 'The root directory of the actual Angular components relative from workspace root', 38 | type: 'string', 39 | }) 40 | .option('sse', { 41 | describe: 'Configure the server to use SSE (Server-Sent Events)', 42 | type: 'boolean', 43 | default: false, 44 | }) 45 | .option('port', { 46 | alias: 'p', 47 | describe: 'Port to use for the SSE server (default: 9921)', 48 | type: 'number', 49 | }) 50 | .check((argv) => { 51 | if (argv.port !== undefined && !argv.sse) { 52 | throw new Error( 53 | 'The --port option can only be used when --sse is enabled', 54 | ); 55 | } 56 | return true; 57 | }) 58 | .help() 59 | .parseSync() as ArgvType; 60 | 61 | const { workspaceRoot, ds } = argv as unknown as { 62 | workspaceRoot: string; 63 | ds: { 64 | storybookDocsRoot?: string; 65 | deprecatedCssClassesPath?: string; 66 | uiRoot: string; 67 | }; 68 | }; 69 | const { storybookDocsRoot, deprecatedCssClassesPath, uiRoot } = ds; 70 | 71 | async function startServer() { 72 | const server = await AngularMcpServerWrapper.create({ 73 | workspaceRoot: workspaceRoot as string, 74 | ds: { 75 | storybookDocsRoot, 76 | deprecatedCssClassesPath, 77 | uiRoot, 78 | }, 79 | }); 80 | 81 | if (argv.sse) { 82 | const port = argv.port ?? 9921; 83 | 84 | const app = express(); 85 | let transport: SSEServerTransport; 86 | app.get('/sse', async (_, res) => { 87 | transport = new SSEServerTransport('/messages', res); 88 | await server.getMcpServer().connect(transport); 89 | }); 90 | 91 | app.post('/messages', async (req, res) => { 92 | if (!transport) { 93 | res.status(400).send('No transport found'); 94 | return; 95 | } 96 | await transport.handlePostMessage(req, res); 97 | }); 98 | 99 | const server_instance = app.listen(port); 100 | 101 | process.on('exit', () => { 102 | server_instance.close(); 103 | }); 104 | } else { 105 | const transport = new StdioServerTransport(); 106 | server.getMcpServer().connect(transport); 107 | } 108 | } 109 | 110 | // eslint-disable-next-line unicorn/prefer-top-level-await 111 | startServer().catch((ctx) => { 112 | console.error('Failed to start server:', ctx); 113 | process.exit(1); 114 | }); 115 | ``` -------------------------------------------------------------------------------- /testing/utils/src/lib/os-agnostic-paths.ts: -------------------------------------------------------------------------------- ```typescript 1 | const AGNOSTIC_PATH_SEP_REGEX = /[/\\]/g; 2 | const OS_AGNOSTIC_PATH_SEP = '/'; 3 | const OS_AGNOSTIC_CWD = `<CWD>`; 4 | 5 | /** 6 | * Converts a given file path to an OS-agnostic path by replacing the current working directory with '<CWD>' 7 | * and normalizing path separators to '/'. 8 | * 9 | * @param filePath - The file path to be converted. 10 | * @param separator - The path separator to use for normalization. Defaults to the OS-specific separator. 11 | * @returns The OS-agnostic path. 12 | * 13 | * @example 14 | * 15 | * At CWD on Ubuntu (Linux) 16 | * Input: /home/projects/my-folder/my-file.ts 17 | * Output: <CWD>/my-folder/my-file.ts 18 | * 19 | * At CWD on Windows 20 | * Input: D:\projects\my-folder\my-file.ts 21 | * Output: <CWD>/my-folder/my-file.ts 22 | * 23 | * At CWD on macOS 24 | * Input: /Users/projects/my-folder/my-file.ts 25 | * Output: <CWD>/my-folder/my-file.ts 26 | * 27 | * Out of CWD on all OS 28 | * Input: /Users/projects/../my-folder/my-file.ts 29 | * Output: ../my-folder/my-file.ts 30 | * 31 | * Absolute paths (all OS) 32 | * Input: \\my-folder\\my-file.ts 33 | * Output: /my-folder/my-file.ts 34 | * 35 | * Relative paths (all OS) 36 | * Input: ..\\my-folder\\my-file.ts 37 | * Output: ../my-folder/my-file.ts 38 | * 39 | */ 40 | 41 | export function osAgnosticPath(filePath: undefined): undefined; 42 | export function osAgnosticPath(filePath: string): string; 43 | export function osAgnosticPath(filePath?: string): string | undefined { 44 | if (filePath == null) { 45 | return filePath; 46 | } 47 | // prepare the path for comparison 48 | // normalize path separators od cwd: "Users\\repo" => "Users/repo" 49 | const osAgnosticCwd = process 50 | .cwd() 51 | .split(AGNOSTIC_PATH_SEP_REGEX) 52 | .join(OS_AGNOSTIC_PATH_SEP); 53 | // normalize path separators => "..\\folder\\repo.ts" => => "../folder/repo.ts" 54 | const osAgnosticFilePath = filePath 55 | .split(AGNOSTIC_PATH_SEP_REGEX) 56 | .join(OS_AGNOSTIC_PATH_SEP); 57 | // remove the current working directory for easier comparison 58 | const osAgnosticPathWithoutCwd = osAgnosticFilePath 59 | .replace(osAgnosticCwd, '') 60 | // consider already agnostic paths 61 | .replace(OS_AGNOSTIC_CWD, ''); 62 | 63 | // path is outside cwd (Users/repo/../my-folder/my-file.ts) 64 | if ( 65 | osAgnosticPathWithoutCwd.startsWith( 66 | `${OS_AGNOSTIC_PATH_SEP}..${OS_AGNOSTIC_PATH_SEP}`, 67 | ) 68 | ) { 69 | return osAgnosticPathWithoutCwd.slice(1); // remove the leading '/' 70 | } 71 | 72 | // path is at cwd (Users/repo/my-folder/my-file.ts) 73 | if ( 74 | osAgnosticFilePath.startsWith(osAgnosticCwd) || 75 | osAgnosticFilePath.startsWith(OS_AGNOSTIC_CWD) 76 | ) { 77 | // Add a substitute for the current working directory 78 | return `${OS_AGNOSTIC_CWD}${osAgnosticPathWithoutCwd}`; 79 | } 80 | 81 | // Notice: I kept the following conditions for documentation purposes 82 | 83 | // path is absolute (/my-folder/my-file.ts) 84 | if (osAgnosticPathWithoutCwd.startsWith(OS_AGNOSTIC_PATH_SEP)) { 85 | return osAgnosticPathWithoutCwd; 86 | } 87 | 88 | // path is relative (./my-folder/my-file.ts) 89 | if (osAgnosticPathWithoutCwd.startsWith(`.${OS_AGNOSTIC_PATH_SEP}`)) { 90 | return osAgnosticPathWithoutCwd; 91 | } 92 | 93 | // path is segment (my-folder/my-file.ts or my-folder/sub-folder) 94 | return osAgnosticPathWithoutCwd; 95 | } 96 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/design-system/storybook-host-app/src/components/segmented-control/segmented-control-tabs/api.mdx: -------------------------------------------------------------------------------- ```markdown 1 | ## Inputs 2 | 3 | ### DsSegmentedControl 4 | 5 | | Name | Type | Default | Description | 6 | | ------------------- | ----------------------------- | ----------- | ------------------------------------------------------------- | 7 | | `activeOption` | `string` | `''` | Name of the currently selected option. | 8 | | `fullWidth` | `boolean` | `false` | Whether the segmented control takes the full container width. | 9 | | `inverse` | `boolean` | `false` | Applies inverse color scheme to the component. | 10 | | `roleType` | `'radiogroup'` \| `'tablist'` | `'tablist'` | Sets the ARIA role of the segmented control group. | 11 | | `twoLineTruncation` | `boolean` | `false` | Enables two-line truncation for long labels. | 12 | 13 | ### DsSegmentedOption 14 | 15 | | Name | Type | Required | Default | Description | 16 | | ------- | -------- | -------- | ------- | ------------------------------- | 17 | | `name` | `string` | Yes | — | Unique name for the option. | 18 | | `title` | `string` | No | `''` | Displayed label for the option. | 19 | 20 | <sup>**1**</sup> **activeOption note:** **name** needs to be added to segmented 21 | option to have possibility to define selected item 22 | 23 | --- 24 | 25 | ## Outputs 26 | 27 | ### DsSegmentedControl 28 | 29 | | Name | Type | Description | 30 | | -------------------- | ---------------------- | --------------------------------------------- | 31 | | `activeOptionChange` | `EventEmitter<string>` | Emits the new selected option name on change. | 32 | 33 | ```json 34 | { 35 | "index": 2, 36 | "event": { 37 | "isTrusted": true, 38 | "altKey": false, 39 | "altitudeAngle": 1.5707963267948966, 40 | "azimuthAngle": 0, 41 | "bubbles": true, 42 | "button": 0, 43 | "buttons": 0, 44 | "cancelBubble": false, 45 | "cancelable": true, 46 | "clientX": 433, 47 | "clientY": 85, 48 | "composed": true, 49 | "ctrlKey": false 50 | } 51 | } 52 | ``` 53 | 54 | ### DsSegmentedOption 55 | 56 | | Name | Type | Description | 57 | | -------------- | ---------------------- | ---------------------------------- | 58 | | `selectOption` | `EventEmitter<string>` | Emits when the option is selected. | 59 | 60 | --- 61 | 62 | ## Content Projection 63 | 64 | | Selector | Description | 65 | | ------------- | ------------------------------------------ | 66 | | `#dsTemplate` | A `TemplateRef` for custom option content. | 67 | 68 | --- 69 | 70 | ## Host Bindings 71 | 72 | ### DsSegmentedControl 73 | 74 | - `class="ds-segmented-control"` — base class 75 | - `class.ds-segment-full-width` — applied when `fullWidth` is true 76 | - `class.ds-segment-inverse` — applied when `inverse` is true 77 | - `class.ds-sc-ready` — applied after view initialization 78 | - `role` — set to `tablist` or `radiogroup` depending on `roleType` 79 | 80 | ### DsSegmentedOption 81 | 82 | - Focus state and tabindex are controlled dynamically. 83 | - Host uses `rxHostPressedListener` for click-based selection. 84 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/group-4/multi-violation-test.component.scss: -------------------------------------------------------------------------------- ```scss 1 | /* Multi-violation test component styles using deprecated CSS classes */ 2 | 3 | .test-container { 4 | padding: 20px; 5 | max-width: 800px; 6 | margin: 0 auto; 7 | } 8 | 9 | /* ❌ BAD: DsButton deprecated styles - 'btn', 'btn-primary', 'legacy-button' */ 10 | .btn { 11 | display: inline-block; 12 | padding: 8px 16px; 13 | margin: 4px; 14 | border: 1px solid #ccc; 15 | border-radius: 4px; 16 | background-color: #f8f9fa; 17 | color: #333; 18 | cursor: pointer; 19 | font-size: 14px; 20 | text-decoration: none; 21 | 22 | &:hover { 23 | background-color: #e9ecef; 24 | } 25 | } 26 | 27 | .btn-primary { 28 | background-color: #007bff; 29 | border-color: #007bff; 30 | color: white; 31 | 32 | &:hover { 33 | background-color: #0056b3; 34 | } 35 | } 36 | 37 | .legacy-button { 38 | background: linear-gradient(45deg, #ff6b6b, #feca57); 39 | border: none; 40 | padding: 10px 20px; 41 | color: white; 42 | border-radius: 8px; 43 | cursor: pointer; 44 | font-weight: bold; 45 | margin: 4px; 46 | 47 | &:hover { 48 | transform: translateY(-2px); 49 | box-shadow: 0 4px 8px rgba(0,0,0,0.2); 50 | } 51 | } 52 | 53 | /* ❌ BAD: DsBadge deprecated styles - 'offer-badge' */ 54 | .offer-badge { 55 | background-color: #ff4757; 56 | color: white; 57 | padding: 4px 8px; 58 | border-radius: 12px; 59 | font-size: 12px; 60 | font-weight: bold; 61 | text-transform: uppercase; 62 | margin-left: 8px; 63 | display: inline-block; 64 | 65 | &:before { 66 | content: "🔥 "; 67 | } 68 | } 69 | 70 | /* ❌ BAD: DsTabsModule deprecated styles - 'tab-nav', 'nav-tabs', 'tab-nav-item' */ 71 | .nav-tabs { 72 | display: flex; 73 | list-style: none; 74 | padding: 0; 75 | margin: 0; 76 | border-bottom: 2px solid #dee2e6; 77 | } 78 | 79 | .tab-nav { 80 | background-color: #f8f9fa; 81 | border-radius: 4px 4px 0 0; 82 | } 83 | 84 | .tab-nav-item { 85 | padding: 12px 16px; 86 | cursor: pointer; 87 | border: 1px solid transparent; 88 | border-bottom: none; 89 | background-color: #f8f9fa; 90 | color: #495057; 91 | 92 | &:hover { 93 | background-color: #e9ecef; 94 | } 95 | 96 | &.active { 97 | background-color: white; 98 | border-color: #dee2e6; 99 | color: #007bff; 100 | border-bottom: 2px solid white; 101 | margin-bottom: -2px; 102 | } 103 | } 104 | 105 | /* ❌ BAD: DsCard deprecated styles - 'card' */ 106 | .card { 107 | border: 1px solid #dee2e6; 108 | border-radius: 8px; 109 | background-color: white; 110 | box-shadow: 0 2px 4px rgba(0,0,0,0.1); 111 | margin: 16px 0; 112 | overflow: hidden; 113 | } 114 | 115 | .card-header { 116 | padding: 16px; 117 | background-color: #f8f9fa; 118 | border-bottom: 1px solid #dee2e6; 119 | display: flex; 120 | justify-content: space-between; 121 | align-items: center; 122 | 123 | h4 { 124 | margin: 0; 125 | color: #495057; 126 | } 127 | } 128 | 129 | .card-body { 130 | padding: 16px; 131 | 132 | p { 133 | margin: 0 0 12px 0; 134 | color: #6c757d; 135 | } 136 | } 137 | 138 | .card-footer { 139 | padding: 16px; 140 | background-color: #f8f9fa; 141 | border-top: 1px solid #dee2e6; 142 | text-align: right; 143 | } 144 | 145 | /* Section styling */ 146 | .button-section, 147 | .badge-section, 148 | .tabs-section, 149 | .card-section, 150 | .mixed-section { 151 | margin-bottom: 32px; 152 | 153 | h3 { 154 | color: #495057; 155 | border-bottom: 1px solid #dee2e6; 156 | padding-bottom: 8px; 157 | margin-bottom: 16px; 158 | } 159 | } 160 | 161 | .product-item { 162 | display: flex; 163 | align-items: center; 164 | justify-content: space-between; 165 | padding: 8px; 166 | border: 1px solid #dee2e6; 167 | border-radius: 4px; 168 | margin: 8px 0; 169 | background-color: #f8f9fa; 170 | } 171 | 172 | .tab-content { 173 | padding: 20px; 174 | border: 1px solid #dee2e6; 175 | border-top: none; 176 | background-color: white; 177 | min-height: 100px; 178 | } 179 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/spec/typescript-analyzer.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | import ts from 'typescript'; 4 | 5 | import { 6 | extractPublicMethods, 7 | extractLifecycleHooks, 8 | extractImports, 9 | } from '../utils/typescript-analyzer.js'; 10 | 11 | // ----------------------------------------------------------------------------- 12 | // Helpers 13 | // ----------------------------------------------------------------------------- 14 | 15 | function parseSource(code: string, fileName = 'comp.ts') { 16 | return ts.createSourceFile( 17 | fileName, 18 | code, 19 | ts.ScriptTarget.Latest, 20 | true, 21 | ts.ScriptKind.TS, 22 | ); 23 | } 24 | 25 | function getFirstClass(sourceFile: ts.SourceFile): ts.ClassDeclaration { 26 | const cls = sourceFile.statements.find(ts.isClassDeclaration); 27 | if (!cls) throw new Error('Class not found'); 28 | return cls; 29 | } 30 | 31 | // ----------------------------------------------------------------------------- 32 | // Tests 33 | // ----------------------------------------------------------------------------- 34 | 35 | describe('typescript-analyzer utilities', () => { 36 | it('extractPublicMethods returns only public non-lifecycle methods', () => { 37 | const code = ` 38 | class MyComponent { 39 | foo(a: number): void {} 40 | private hidden() {} 41 | protected prot() {} 42 | ngOnInit() {} 43 | static util() {} 44 | async fetchData() {} 45 | } 46 | `; 47 | 48 | const sf = parseSource(code); 49 | const cls = getFirstClass(sf); 50 | 51 | const methods = extractPublicMethods(cls, sf); 52 | 53 | expect(methods).toHaveProperty('foo'); 54 | expect(methods).toHaveProperty('util'); 55 | expect(methods).toHaveProperty('fetchData'); 56 | expect(methods).not.toHaveProperty('hidden'); 57 | expect(methods).not.toHaveProperty('prot'); 58 | expect(methods).not.toHaveProperty('ngOnInit'); 59 | 60 | const foo = methods.foo; 61 | expect(foo.parameters[0]).toEqual( 62 | expect.objectContaining({ name: 'a', type: 'number' }), 63 | ); 64 | expect(foo.isStatic).toBe(false); 65 | expect(foo.isAsync).toBe(false); 66 | 67 | expect(methods.util.isStatic).toBe(true); 68 | expect(methods.fetchData.isAsync).toBe(true); 69 | }); 70 | 71 | it('extractLifecycleHooks detects implemented and method-based hooks', () => { 72 | const code = ` 73 | interface OnInit { ngOnInit(): void; } 74 | class MyComponent implements OnInit { 75 | ngOnInit() {} 76 | ngAfterViewInit() {} 77 | foo() {} 78 | } 79 | `; 80 | 81 | const sf = parseSource(code); 82 | const cls = getFirstClass(sf); 83 | 84 | const hooks = extractLifecycleHooks(cls); 85 | expect(hooks).toEqual(expect.arrayContaining(['OnInit', 'AfterViewInit'])); 86 | }); 87 | 88 | it('extractImports lists imported symbols with their paths', () => { 89 | const code = ` 90 | import { HttpClient } from '@angular/common/http'; 91 | import * as _ from 'lodash'; 92 | import defaultExport from 'lib'; 93 | import { MatButtonModule as MB } from '@angular/material/button'; 94 | `; 95 | 96 | const sf = parseSource(code); 97 | const imports = extractImports(sf); 98 | 99 | expect(imports).toEqual( 100 | expect.arrayContaining([ 101 | { name: 'HttpClient', path: '@angular/common/http' }, 102 | { name: '_', path: 'lodash' }, 103 | { name: 'defaultExport', path: 'lib' }, 104 | { name: 'MB', path: '@angular/material/button' }, 105 | ]), 106 | ); 107 | }); 108 | }); 109 | ``` -------------------------------------------------------------------------------- /packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/class-definition.visitor.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Rule } from 'postcss'; 2 | import { Issue } from '@code-pushup/models'; 3 | import { DiagnosticsAware } from '@push-based/models'; 4 | import { 5 | CssAstVisitor, 6 | styleAstRuleToSource, 7 | } from '@push-based/styles-ast-utils'; 8 | 9 | import { 10 | EXTERNAL_ASSET_ICON, 11 | INLINE_ASSET_ICON, 12 | STYLES_ASSET_ICON, 13 | } from './constants.js'; 14 | import { ComponentReplacement } from './schema.js'; 15 | 16 | export type ClassDefinitionVisitor = CssAstVisitor & DiagnosticsAware; 17 | 18 | /** 19 | * Visits a `CssAstVisitor` that is `DiagnosticsAware`and collects the definition of deprecated class names. 20 | * 21 | * @example 22 | * const ast: Root = postcss.parse(` 23 | * .btn { 24 | * color: red; 25 | * } 26 | * `); 27 | * const visitor = createClassDefinitionVisitor(ast, { deprecatedCssClasses: ['btn'] }); 28 | * // The visitor will check each `Rule` definition for matching deprecateCssClasses 29 | * visitEachStyleNode(ast.nodes, visitor); 30 | * 31 | * // The visitor is `DiagnosticsAware` and xou can get the issues over a public API. 32 | * const issues: Issue & { coed?: number } = visitor.getIssues(); 33 | * 34 | * // Subsequent usags will add to the issues. 35 | * // You can also clear the issues 36 | * visitor.clear(); 37 | * 38 | * @param componentReplacement 39 | * @param startLine 40 | */ 41 | export const createClassDefinitionVisitor = ( 42 | componentReplacement: ComponentReplacement, 43 | startLine = 0, 44 | ): ClassDefinitionVisitor => { 45 | const { deprecatedCssClasses = [] } = componentReplacement; 46 | let diagnostics: Issue[] = []; 47 | 48 | return { 49 | getIssues(): Issue[] { 50 | return diagnostics; 51 | }, 52 | 53 | clear(): void { 54 | diagnostics = []; 55 | }, 56 | 57 | visitRule(rule: Rule) { 58 | const matchingClassNames = getMatchingClassNames( 59 | { selector: rule.selector }, 60 | deprecatedCssClasses, 61 | ); 62 | 63 | if (matchingClassNames.length > 0) { 64 | const message = classUsageMessage({ 65 | className: matchingClassNames.join(', '), 66 | rule, 67 | componentName: componentReplacement.componentName, 68 | docsUrl: componentReplacement.docsUrl, 69 | }); 70 | const isInline = rule.source?.input.file?.match(/\.ts$/) != null; 71 | diagnostics.push({ 72 | message, 73 | severity: 'error', 74 | source: styleAstRuleToSource(rule, isInline ? startLine : 0), 75 | }); 76 | } 77 | }, 78 | }; 79 | }; 80 | 81 | function classUsageMessage({ 82 | className, 83 | rule, 84 | componentName, 85 | docsUrl, 86 | }: Pick<ComponentReplacement, 'componentName' | 'docsUrl'> & { 87 | className: string; 88 | rule: Rule; 89 | }): string { 90 | const isInline = rule.source?.input.file?.match(/\.ts$/) != null; 91 | const iconString = `${ 92 | isInline ? INLINE_ASSET_ICON : EXTERNAL_ASSET_ICON 93 | }${STYLES_ASSET_ICON}`; 94 | const docsLink = docsUrl 95 | ? ` <a href="${docsUrl}" target="_blank">Learn more</a>.` 96 | : ''; 97 | return `${iconString}️ The selector's class <code>${className}</code> is deprecated. Use <code>${componentName}</code> and delete the styles.${docsLink}`; 98 | } 99 | 100 | export function getMatchingClassNames( 101 | { selector }: Pick<Rule, 'selector'>, 102 | targetClassNames: string[], 103 | ): string[] { 104 | const classNames = selector.match(/\.[\w-]+/g) || []; 105 | return classNames 106 | .map((className) => className.slice(1)) // Strip the leading "." 107 | .filter((className) => targetClassNames.includes(className)); 108 | } 109 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/shared/models/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Component Contract Types 3 | */ 4 | 5 | export type TemplateType = 'inline' | 'external'; 6 | 7 | export interface ComponentContract { 8 | meta: Meta; 9 | publicApi: PublicApi; 10 | slots: Slots; 11 | dom: DomStructure; 12 | styles: StyleDeclarations; 13 | } 14 | 15 | export interface Meta { 16 | name: string; 17 | selector: string; 18 | sourceFile: string; 19 | templateType: TemplateType; 20 | generatedAt: string; 21 | hash: string; 22 | } 23 | 24 | export interface PublicApi { 25 | properties: Record<string, PropertyBinding>; 26 | events: Record<string, EventBinding>; 27 | methods: Record<string, MethodSignature>; 28 | lifecycle: string[]; 29 | imports: ImportInfo[]; 30 | } 31 | 32 | export interface PropertyBinding { 33 | type: string; 34 | isInput: boolean; 35 | required: boolean; 36 | transform?: string; 37 | } 38 | 39 | export interface EventBinding { 40 | type: string; 41 | } 42 | 43 | export interface MethodSignature { 44 | name: string; 45 | parameters: ParameterInfo[]; 46 | returnType: string; 47 | isPublic: boolean; 48 | isStatic: boolean; 49 | isAsync: boolean; 50 | } 51 | 52 | export interface ParameterInfo { 53 | name: string; 54 | type: string; 55 | optional: boolean; 56 | defaultValue?: string; 57 | } 58 | 59 | export interface ImportInfo { 60 | name: string; 61 | path: string; 62 | } 63 | 64 | export interface Slots { 65 | [slotName: string]: { 66 | selector: string; 67 | }; 68 | } 69 | 70 | /** 71 | * Flat DOM structure with CSS selector keys for efficient lookups and minimal diff noise 72 | */ 73 | export interface DomStructure { 74 | [selectorKey: string]: DomElement; 75 | } 76 | 77 | export interface DomElement { 78 | tag: string; 79 | parent: string | null; 80 | children: string[]; 81 | bindings: Binding[]; 82 | attributes: Attribute[]; 83 | events: Event[]; 84 | /** 85 | * Optional stack of active structural directives (Angular control-flow / template constructs) 86 | * that wrap this DOM element, preserving the order of nesting from outermost → innermost. 87 | * Kept optional so that existing contracts without this field remain valid. 88 | */ 89 | structural?: StructuralDirectiveContext[]; 90 | } 91 | 92 | export interface Binding { 93 | type: 'class' | 'style' | 'property' | 'attribute'; 94 | name: string; 95 | source: string; 96 | } 97 | 98 | export interface Attribute { 99 | type: 'attribute'; 100 | name: string; 101 | source: string; 102 | } 103 | 104 | export interface Event { 105 | name: string; 106 | handler: string; 107 | } 108 | 109 | /** 110 | * Style declarations with explicit DOM relationships for property-level change detection 111 | */ 112 | export interface StyleDeclarations { 113 | sourceFile: string; 114 | rules: Record<string, StyleRule>; 115 | } 116 | 117 | export interface StyleRule { 118 | appliesTo: string[]; 119 | properties: Record<string, string>; 120 | } 121 | 122 | /** 123 | * DOM path deduplication utility for diff operations 124 | */ 125 | export interface DomPathDictionary { 126 | paths: string[]; 127 | lookup: Map<string, number>; 128 | stats: { 129 | totalPaths: number; 130 | uniquePaths: number; 131 | duplicateReferences: number; 132 | bytesBeforeDeduplication: number; 133 | bytesAfterDeduplication: number; 134 | }; 135 | } 136 | 137 | /** 138 | * Captures context of Angular structural directives (v17 control-flow blocks and classic *ngX) 139 | */ 140 | export interface StructuralDirectiveContext { 141 | kind: 'for' | 'if' | 'switch' | 'switchCase' | 'switchDefault' | 'defer'; 142 | /** Raw expression text (loop expression, condition…) when available */ 143 | expression?: string; 144 | /** Optional alias / let-var (e.g., let-item) or trackBy identifier */ 145 | alias?: string; 146 | /** Optional branch inside control-flow blocks (then/else, empty, case…). */ 147 | branch?: 'then' | 'else' | 'empty' | 'case' | 'default'; 148 | } 149 | ``` -------------------------------------------------------------------------------- /packages/shared/styles-ast-utils/ai/FUNCTIONS.md: -------------------------------------------------------------------------------- ```markdown 1 | # Public API — Quick Reference 2 | 3 | | Symbol | Kind | Summary | 4 | | ---------------------- | --------- | -------------------------------------------------- | 5 | | `CssAstVisitor` | interface | Visitor interface for traversing CSS AST nodes | 6 | | `NodeType` | type | Type mapping for visitor method parameters | 7 | | `parseStylesheet` | function | Parse CSS content and return PostCSS AST | 8 | | `styleAstRuleToSource` | function | Convert CSS rule to linkable source location | 9 | | `stylesAstUtils` | function | Utility function (returns package identifier) | 10 | | `visitEachChild` | function | Traverse AST calling visitor methods for each node | 11 | | `visitEachStyleNode` | function | Recursively visit CSS nodes with visitor pattern | 12 | | `visitStyleSheet` | function | Visit top-level stylesheet nodes | 13 | 14 | ## Interface Details 15 | 16 | ### `CssAstVisitor<T = void>` 17 | 18 | Visitor interface for processing different types of CSS AST nodes: 19 | 20 | - `visitRoot?: (root: Container) => T` - Called once for the root node 21 | - `visitAtRule?: (atRule: AtRule) => T` - Called for @rule nodes (@media, @charset, etc.) 22 | - `visitRule?: (rule: Rule) => T` - Called for CSS rule nodes (.btn, .box, etc.) 23 | - `visitDecl?: (decl: Declaration) => T` - Called for property declarations (color: red, etc.) 24 | - `visitComment?: (comment: Comment) => T` - Called for comment nodes (/_ comment _/) 25 | 26 | ### `NodeType<K extends keyof CssAstVisitor>` 27 | 28 | Type utility that maps visitor method names to their corresponding PostCSS node types: 29 | 30 | - `visitRoot` → `Container` 31 | - `visitAtRule` → `AtRule` 32 | - `visitRule` → `Rule` 33 | - `visitDecl` → `Declaration` 34 | - `visitComment` → `Comment` 35 | 36 | ## Function Details 37 | 38 | ### `parseStylesheet(content: string, filePath: string)` 39 | 40 | Parse CSS content using PostCSS with safe parsing. Returns a PostCSS `LazyResult` object. 41 | 42 | **Parameters:** 43 | 44 | - `content` - The CSS content to parse 45 | - `filePath` - File path for source mapping and error reporting 46 | 47 | **Returns:** PostCSS `LazyResult` with parsed AST 48 | 49 | ### `styleAstRuleToSource(rule: Pick<Rule, 'source'>, startLine?: number)` 50 | 51 | Convert a PostCSS rule to a linkable source location for issue reporting. 52 | 53 | **Parameters:** 54 | 55 | - `rule` - PostCSS rule with source information 56 | - `startLine` - Optional offset for line numbers (0-indexed, default: 0) 57 | 58 | **Returns:** `Issue['source']` object with file path and position information 59 | 60 | ### `visitEachChild<T>(root: Root, visitor: CssAstVisitor<T>)` 61 | 62 | Single function that traverses the entire AST, calling specialized visitor methods for each node type. 63 | 64 | **Parameters:** 65 | 66 | - `root` - PostCSS Root node to traverse 67 | - `visitor` - Visitor object with optional methods for different node types 68 | 69 | ### `visitEachStyleNode<T>(nodes: Root['nodes'], visitor: CssAstVisitor<T>)` 70 | 71 | Recursively visit CSS nodes using the visitor pattern, processing nested structures. 72 | 73 | **Parameters:** 74 | 75 | - `nodes` - Array of PostCSS nodes to visit 76 | - `visitor` - Visitor object with optional methods for different node types 77 | 78 | ### `visitStyleSheet<T>(root: Root, visitor: CssAstVisitor<T>)` 79 | 80 | Visit only the top-level nodes of a stylesheet (non-recursive). 81 | 82 | **Parameters:** 83 | 84 | - `root` - PostCSS Root node 85 | - `visitor` - Visitor object with optional methods for different node types 86 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/shared/utils/handler-helpers.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | CallToolRequest, 3 | CallToolResult, 4 | } from '@modelcontextprotocol/sdk/types.js'; 5 | import { validateComponentName } from './component-validation.js'; 6 | import { buildTextResponse, throwError } from './output.utils.js'; 7 | import * as process from 'node:process'; 8 | 9 | /** 10 | * Common handler options interface - includes both user inputs and MCP server injected config 11 | */ 12 | export interface BaseHandlerOptions { 13 | cwd?: string; 14 | directory?: string; 15 | componentName?: string; 16 | workspaceRoot?: string; 17 | // MCP server injected configuration 18 | storybookDocsRoot?: string; 19 | deprecatedCssClassesPath?: string; 20 | uiRoot?: string; 21 | } 22 | 23 | /** 24 | * Handler context with all available configuration 25 | */ 26 | export interface HandlerContext { 27 | cwd: string; 28 | workspaceRoot: string; 29 | storybookDocsRoot?: string; 30 | deprecatedCssClassesPath?: string; 31 | uiRoot: string; 32 | } 33 | 34 | /** 35 | * Validates common input parameters 36 | */ 37 | export function validateCommonInputs(params: BaseHandlerOptions): void { 38 | if (params.componentName) { 39 | validateComponentName(params.componentName); 40 | } 41 | 42 | if (params.directory && typeof params.directory !== 'string') { 43 | throw new Error('Directory parameter is required and must be a string'); 44 | } 45 | } 46 | 47 | /** 48 | * Sets up common environment for handlers 49 | */ 50 | export function setupHandlerEnvironment( 51 | params: BaseHandlerOptions, 52 | ): HandlerContext { 53 | const originalCwd = process.cwd(); 54 | const cwd = params.cwd || originalCwd; 55 | 56 | if (cwd !== originalCwd) { 57 | process.chdir(cwd); 58 | } 59 | 60 | return { 61 | cwd, 62 | workspaceRoot: params.workspaceRoot || cwd, 63 | storybookDocsRoot: params.storybookDocsRoot, 64 | deprecatedCssClassesPath: params.deprecatedCssClassesPath, 65 | uiRoot: params.uiRoot || '', 66 | }; 67 | } 68 | 69 | /** 70 | * Generic handler wrapper that provides common functionality 71 | */ 72 | export function createHandler<TParams extends BaseHandlerOptions, TResult>( 73 | toolName: string, 74 | handlerFn: (params: TParams, context: HandlerContext) => Promise<TResult>, 75 | formatResult: (result: TResult) => string[], 76 | ) { 77 | return async (options: CallToolRequest): Promise<CallToolResult> => { 78 | try { 79 | const params = options.params.arguments as TParams; 80 | 81 | validateCommonInputs(params); 82 | const context = setupHandlerEnvironment(params); 83 | 84 | const result = await handlerFn(params, context); 85 | const formattedLines = formatResult(result); 86 | 87 | return buildTextResponse(formattedLines); 88 | } catch (ctx) { 89 | return throwError(`${toolName}: ${(ctx as Error).message || ctx}`); 90 | } 91 | }; 92 | } 93 | 94 | /** 95 | * Common result formatters 96 | */ 97 | export const RESULT_FORMATTERS = { 98 | /** 99 | * Formats a simple success message 100 | */ 101 | success: (message: string): string[] => [message], 102 | 103 | /** 104 | * Formats a list of items 105 | */ 106 | list: (items: string[], title?: string): string[] => 107 | title ? [title, ...items.map((item) => ` - ${item}`)] : items, 108 | 109 | /** 110 | * Formats key-value pairs 111 | */ 112 | keyValue: (pairs: Record<string, any>): string[] => 113 | Object.entries(pairs).map( 114 | ([key, value]) => 115 | `${key}: ${typeof value === 'object' ? JSON.stringify(value, null, 2) : value}`, 116 | ), 117 | 118 | /** 119 | * Formats errors with context 120 | */ 121 | error: (error: string, context?: string): string[] => 122 | context ? [`Error in ${context}:`, ` ${error}`] : [`Error: ${error}`], 123 | 124 | /** 125 | * Formats empty results 126 | */ 127 | empty: (entityType: string): string[] => [`No ${entityType} found`], 128 | } as const; 129 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/utils/public-api.extractor.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as ts from 'typescript'; 2 | import type { PublicApi } from '../../shared/models/types.js'; 3 | import { 4 | extractClassDeclaration, 5 | extractPublicMethods, 6 | extractLifecycleHooks, 7 | extractImports, 8 | extractInputsAndOutputs, 9 | } from './typescript-analyzer.js'; 10 | import { ParsedComponent } from '@push-based/angular-ast-utils'; 11 | 12 | type InputMeta = { 13 | type?: string; 14 | required?: boolean; 15 | transform?: string; 16 | }; 17 | 18 | type OutputMeta = { 19 | type?: string; 20 | }; 21 | 22 | /** 23 | * `ParsedComponent` provided by `angular-ast-utils` does not yet expose 24 | * `inputs` / `outputs`. We extend it locally in a non-breaking fashion 25 | * (both properties remain optional). 26 | */ 27 | type ParsedComponentWithIO = ParsedComponent & { 28 | inputs?: Record<string, InputMeta>; 29 | outputs?: Record<string, OutputMeta>; 30 | }; 31 | 32 | /** 33 | * Extract Public API from TypeScript class analysis 34 | */ 35 | export function extractPublicApi( 36 | parsedComponent: ParsedComponentWithIO, 37 | ): PublicApi { 38 | const publicApi: PublicApi = { 39 | properties: {}, 40 | events: {}, 41 | methods: {}, 42 | lifecycle: [], 43 | imports: [], 44 | }; 45 | 46 | if (parsedComponent.inputs) { 47 | for (const [name, config] of Object.entries( 48 | parsedComponent.inputs as Record<string, InputMeta>, 49 | )) { 50 | publicApi.properties[name] = { 51 | type: config?.type ?? 'any', 52 | isInput: true, 53 | required: config?.required ?? false, 54 | transform: config?.transform, 55 | }; 56 | } 57 | } 58 | 59 | if (parsedComponent.outputs) { 60 | for (const [name, config] of Object.entries( 61 | parsedComponent.outputs as Record<string, OutputMeta>, 62 | )) { 63 | publicApi.events[name] = { 64 | type: config?.type ?? 'EventEmitter<any>', 65 | }; 66 | } 67 | } 68 | 69 | const classDeclaration = extractClassDeclaration(parsedComponent); 70 | if (classDeclaration) { 71 | const program = ts.createProgram([parsedComponent.fileName], { 72 | target: ts.ScriptTarget.Latest, 73 | module: ts.ModuleKind.ESNext, 74 | experimentalDecorators: true, 75 | }); 76 | const sourceFile = program.getSourceFile(parsedComponent.fileName); 77 | 78 | if (sourceFile) { 79 | const { inputs: allInputs, outputs: allOutputs } = 80 | extractInputsAndOutputs(classDeclaration, sourceFile); 81 | 82 | for (const [name, config] of Object.entries(allInputs)) { 83 | const isSignalInput = 'defaultValue' in config || 'transform' in config; 84 | 85 | publicApi.properties[name] = { 86 | type: config.type ?? 'any', 87 | isInput: true, 88 | required: config.required ?? false, 89 | ...(isSignalInput && { 90 | transform: (config as any).transform, 91 | defaultValue: (config as any).defaultValue, 92 | }), 93 | ...(!isSignalInput && { 94 | alias: (config as any).alias, 95 | }), 96 | }; 97 | 98 | const propRef = publicApi.properties[name] as any; 99 | if ( 100 | propRef.transform === 'booleanAttribute' && 101 | propRef.type === 'any' 102 | ) { 103 | propRef.type = 'boolean'; 104 | } 105 | } 106 | 107 | for (const [name, config] of Object.entries(allOutputs)) { 108 | publicApi.events[name] = { 109 | type: config.type ?? 'EventEmitter<any>', 110 | ...('alias' in config && { alias: config.alias }), 111 | }; 112 | } 113 | 114 | publicApi.methods = extractPublicMethods(classDeclaration, sourceFile); 115 | publicApi.lifecycle = extractLifecycleHooks(classDeclaration); 116 | publicApi.imports = extractImports(sourceFile); 117 | } 118 | } 119 | 120 | return publicApi; 121 | } 122 | ``` -------------------------------------------------------------------------------- /packages/shared/ds-component-coverage/mocks/fixtures/e2e/demo/code-pushup.config.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ComponentReplacement } from '../../../../src/lib/runner/audits/ds-coverage/schema'; 2 | import { dsComponentUsagePluginCoreConfig } from '../../../../src/core.config.js'; 3 | import * as path from 'path'; 4 | import { fileURLToPath } from 'url'; 5 | 6 | const currentDir = path.dirname(fileURLToPath(import.meta.url)); 7 | const packageRoot = path.resolve(currentDir, '../../../..'); 8 | 9 | const dsComponents: ComponentReplacement[] = [ 10 | { 11 | componentName: 'DSButton', 12 | deprecatedCssClasses: ['btn', 'btn-primary', 'legacy-button'], 13 | docsUrl: 14 | 'https://storybook.company.com/latest/?path=/docs/components-button--overview', 15 | }, 16 | { 17 | componentName: 'DSTabsModule', 18 | deprecatedCssClasses: ['ms-tab-bar', 'legacy-tabs', 'custom-tabs'], 19 | docsUrl: 20 | 'https://storybook.company.com/latest/?path=/docs/components-tabsgroup--overview', 21 | }, 22 | { 23 | componentName: 'DSCard', 24 | deprecatedCssClasses: ['card', 'legacy-card', 'custom-card'], 25 | docsUrl: 26 | 'https://storybook.company.com/latest/?path=/docs/components-card--overview', 27 | }, 28 | { 29 | componentName: 'DSModal', 30 | deprecatedCssClasses: ['modal', 'popup', 'legacy-dialog'], 31 | docsUrl: 32 | 'https://storybook.company.com/latest/?path=/docs/components-modal--overview', 33 | }, 34 | { 35 | componentName: 'DSInput', 36 | deprecatedCssClasses: ['input', 'form-control', 'legacy-input'], 37 | docsUrl: 38 | 'https://storybook.company.com/latest/?path=/docs/components-input--overview', 39 | }, 40 | { 41 | componentName: 'DSDropdown', 42 | deprecatedCssClasses: ['dropdown', 'legacy-dropdown', 'custom-dropdown'], 43 | docsUrl: 44 | 'https://storybook.company.com/latest/?path=/docs/components-dropdown--overview', 45 | }, 46 | { 47 | componentName: 'DSAccordion', 48 | deprecatedCssClasses: ['accordion', 'collapse-panel', 'legacy-accordion'], 49 | docsUrl: 50 | 'https://storybook.company.com/latest/?path=/docs/components-accordion--overview', 51 | }, 52 | { 53 | componentName: 'DSAlert', 54 | deprecatedCssClasses: ['alert', 'notification', 'legacy-alert'], 55 | docsUrl: 56 | 'https://storybook.company.com/latest/?path=/docs/components-alert--overview', 57 | }, 58 | { 59 | componentName: 'DSTooltip', 60 | deprecatedCssClasses: ['tooltip', 'legacy-tooltip', 'info-bubble'], 61 | docsUrl: 62 | 'https://storybook.company.com/latest/?path=/docs/components-tooltip--overview', 63 | }, 64 | { 65 | componentName: 'DSBreadcrumb', 66 | deprecatedCssClasses: ['breadcrumb', 'legacy-breadcrumb', 'nav-breadcrumb'], 67 | docsUrl: 68 | 'https://storybook.company.com/latest/?path=/docs/components-breadcrumb--overview', 69 | }, 70 | { 71 | componentName: 'DSProgressBar', 72 | deprecatedCssClasses: ['progress-bar', 'loading-bar', 'legacy-progress'], 73 | docsUrl: 74 | 'https://storybook.company.com/latest/?path=/docs/components-progressbar--overview', 75 | }, 76 | { 77 | componentName: 'DSSlider', 78 | deprecatedCssClasses: ['slider', 'range-slider', 'legacy-slider'], 79 | docsUrl: 80 | 'https://storybook.company.com/latest/?path=/docs/components-slider--overview', 81 | }, 82 | { 83 | componentName: 'DSNavbar', 84 | deprecatedCssClasses: ['navbar', 'navigation', 'legacy-navbar'], 85 | docsUrl: 'https://storybook.company.com/latest/?p', 86 | }, 87 | ]; 88 | 89 | export default { 90 | persist: { 91 | outputDir: path.join( 92 | packageRoot, 93 | '.code-pushup/ds-component-coverage/demo', 94 | ), 95 | format: ['json', 'md'], 96 | }, 97 | ...(await dsComponentUsagePluginCoreConfig({ 98 | directory: currentDir, 99 | dsComponents, 100 | })), 101 | }; 102 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/spec/diff-utils.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | /* eslint-disable prefer-const */ 2 | import { describe, it, expect } from 'vitest'; 3 | 4 | import type { Difference } from 'microdiff'; 5 | import { 6 | consolidateAndPruneRemoveOperations, 7 | consolidateAndPruneRemoveOperationsWithDeduplication, 8 | isChildPath, 9 | groupChangesByDomainAndType, 10 | generateDiffSummary, 11 | } from '../utils/diff-utils.js'; 12 | 13 | function makeRemove(path: (string | number)[]): Difference { 14 | return { type: 'REMOVE', path, oldValue: 'dummy' } as any; 15 | } 16 | 17 | function makeAdd(path: (string | number)[], value: any): Difference { 18 | return { type: 'ADD', path, value } as any; 19 | } 20 | 21 | describe('diff-utils', () => { 22 | describe('isChildPath', () => { 23 | it('identifies descendant paths correctly', () => { 24 | expect(isChildPath(['a', 'b', 'c'], ['a', 'b'])).toBe(true); 25 | expect(isChildPath(['a', 'b'], ['a', 'b', 'c'])).toBe(false); 26 | expect(isChildPath(['x'], ['x'])).toBe(false); 27 | }); 28 | }); 29 | 30 | describe('consolidateAndPruneRemoveOperations', () => { 31 | it('consolidates CSS rule removals and prunes redundant child removals', () => { 32 | const diff: Difference[] = [ 33 | makeRemove(['styles', 'rules', 'div']), 34 | makeRemove(['styles', 'rules', 'span']), 35 | makeRemove(['dom', 'elements', 0, 'attributes']), 36 | makeRemove(['dom', 'elements']), 37 | makeAdd(['meta', 'name'], 'Foo'), 38 | ]; 39 | 40 | const processed = consolidateAndPruneRemoveOperations(diff); 41 | 42 | expect(processed).toEqual( 43 | expect.arrayContaining([expect.objectContaining({ type: 'ADD' })]), 44 | ); 45 | 46 | expect(processed).toContainEqual({ 47 | type: 'REMOVE', 48 | path: ['styles', 'rules'], 49 | oldValue: ['div', 'span'], 50 | } as any); 51 | 52 | expect( 53 | processed.filter((c) => JSON.stringify(c.path).includes('dom')), 54 | ).toHaveLength(1); 55 | expect(processed).toContainEqual(makeRemove(['dom', 'elements'])); 56 | }); 57 | }); 58 | 59 | describe('consolidateAndPruneRemoveOperationsWithDeduplication', () => { 60 | it('deduplicates DOM paths into dictionary', () => { 61 | const LONG_PATH = 'div#root > span.foo > button.bar'; 62 | 63 | const diff: Difference[] = [ 64 | { 65 | type: 'CHANGE', 66 | path: ['dom', 'elementPath'], 67 | oldValue: LONG_PATH, 68 | value: `${LONG_PATH} > svg.icon`, 69 | } as any, 70 | ]; 71 | 72 | const { processedResult, domPathDict } = 73 | consolidateAndPruneRemoveOperationsWithDeduplication(diff); 74 | 75 | const refObj = { $domPath: 0 }; 76 | expect((processedResult[0] as any).oldValue).toEqual(refObj); 77 | 78 | expect(domPathDict.paths).toEqual([LONG_PATH, `${LONG_PATH} > svg.icon`]); 79 | expect(domPathDict.stats.uniquePaths).toBe(2); 80 | }); 81 | }); 82 | 83 | describe('groupChangesByDomainAndType / generateDiffSummary', () => { 84 | it('groups changes and summarizes stats', () => { 85 | const diff: Difference[] = [ 86 | makeAdd(['meta', 'name'], 'Foo'), 87 | makeRemove(['styles', 'rules', 'div']), 88 | ]; 89 | 90 | const grouped = groupChangesByDomainAndType(diff); 91 | 92 | expect(grouped).toHaveProperty('meta'); 93 | expect(grouped.meta).toHaveProperty('ADD'); 94 | expect(grouped.meta.ADD).toHaveLength(1); 95 | expect(grouped).toHaveProperty('styles'); 96 | 97 | const summary = generateDiffSummary(diff, grouped); 98 | 99 | expect(summary.totalChanges).toBe(2); 100 | expect(summary.changeTypes.ADD).toBe(1); 101 | expect(summary.changeTypes.REMOVE).toBe(1); 102 | expect(summary.changesByDomain.meta.ADD).toBe(1); 103 | }); 104 | }); 105 | }); 106 | ``` -------------------------------------------------------------------------------- /packages/shared/utils/src/lib/file/find-in-file.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Dirent } from 'node:fs'; 2 | import * as fs from 'node:fs/promises'; 3 | import * as path from 'node:path'; 4 | 5 | /** 6 | * Searches for `.ts` files containing the search pattern. 7 | * @param {string} baseDir - The directory to search. Should be absolute or resolved by the caller. 8 | * @param {RegExp | string} searchPattern - The pattern to match. 9 | */ 10 | export async function findFilesWithPattern( 11 | baseDir: string, 12 | searchPattern: string, 13 | ) { 14 | const resolvedBaseDir = path.resolve(baseDir); 15 | 16 | const tsFiles: string[] = []; 17 | for await (const file of findAllFiles( 18 | resolvedBaseDir, 19 | (file) => file.endsWith('.ts') && !file.endsWith('.spec.ts'), 20 | )) { 21 | tsFiles.push(file); 22 | } 23 | 24 | const results: SourceLocation[] = []; 25 | for (const file of tsFiles) { 26 | try { 27 | const hits = await findInFile(file, searchPattern); 28 | if (hits.length > 0) { 29 | results.push(...hits); 30 | } 31 | } catch (ctx) { 32 | console.error(`Error searching file ${file}:`, ctx); 33 | } 34 | } 35 | 36 | return results.map((r: SourceLocation) => r.file); 37 | } 38 | 39 | /** 40 | * Finds all files in a directory and its subdirectories that match a predicate 41 | */ 42 | export async function* findAllFiles( 43 | baseDir: string, 44 | predicate: (file: string) => boolean = (fullPath) => fullPath.endsWith('.ts'), 45 | ): AsyncGenerator<string> { 46 | const entries = await getDirectoryEntries(baseDir); 47 | 48 | for (const entry of entries) { 49 | const fullPath = path.join(baseDir, entry.name); 50 | if (entry.isDirectory()) { 51 | // Skip node_modules and other common exclude directories 52 | if (!isExcludedDirectory(entry.name)) { 53 | yield* findAllFiles(fullPath, predicate); 54 | } 55 | } else if (entry.isFile() && predicate(fullPath)) { 56 | yield fullPath; 57 | } 58 | } 59 | } 60 | 61 | export function isExcludedDirectory(fileName: string) { 62 | return ( 63 | fileName.startsWith('.') || 64 | fileName === 'node_modules' || 65 | fileName === 'dist' || 66 | fileName === 'coverage' 67 | ); 68 | } 69 | 70 | async function getDirectoryEntries(dir: string): Promise<Dirent[]> { 71 | try { 72 | return await fs.readdir(dir, { withFileTypes: true }); 73 | } catch (ctx) { 74 | console.error(`Error reading directory ${dir}:`, ctx); 75 | return []; 76 | } 77 | } 78 | 79 | export function* accessContent(content: string): Generator<string> { 80 | for (const line of content.split('\n')) { 81 | yield line; 82 | } 83 | } 84 | 85 | export function getLineHits( 86 | content: string, 87 | pattern: string, 88 | bail = false, 89 | ): LinePosition[] { 90 | const hits: LinePosition[] = []; 91 | let index = content.indexOf(pattern); 92 | 93 | while (index !== -1) { 94 | hits.push({ startColumn: index, endColumn: index + pattern.length }); 95 | if (bail) { 96 | return hits; 97 | } 98 | index = content.indexOf(pattern, index + 1); 99 | } 100 | return hits; 101 | } 102 | 103 | export type LinePosition = { 104 | startColumn: number; 105 | endColumn?: number; 106 | }; 107 | 108 | export type SourcePosition = { 109 | startLine: number; 110 | endLine?: number; 111 | } & LinePosition; 112 | 113 | export type SourceLocation = { 114 | file: string; 115 | position: SourcePosition; 116 | }; 117 | 118 | export async function findInFile( 119 | file: string, 120 | searchPattern: string, 121 | bail = false, 122 | ): Promise<SourceLocation[]> { 123 | const hits: SourceLocation[] = []; 124 | const content = await fs.readFile(file, 'utf8'); 125 | let startLine = 0; 126 | for (const line of accessContent(content)) { 127 | startLine++; 128 | getLineHits(line, searchPattern, bail).forEach((position) => { 129 | hits.push({ 130 | file, 131 | position: { 132 | startLine, 133 | ...position, 134 | }, 135 | }); 136 | }); 137 | } 138 | return hits; 139 | } 140 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/list/list-component-contracts.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { readdir, stat, readFile } from 'node:fs/promises'; 2 | import { join } from 'node:path'; 3 | import { 4 | createHandler, 5 | BaseHandlerOptions, 6 | } from '../../shared/utils/handler-helpers.js'; 7 | import { resolveCrossPlatformPath } from '../../shared/utils/cross-platform-path.js'; 8 | import { listComponentContractsSchema } from './models/schema.js'; 9 | import type { ContractFileInfo } from './models/types.js'; 10 | import { 11 | extractComponentNameFromFile, 12 | formatBytes, 13 | formatContractsByComponent, 14 | } from './utils/contract-list-utils.js'; 15 | 16 | interface ListComponentContractsOptions extends BaseHandlerOptions { 17 | directory: string; 18 | } 19 | 20 | /** 21 | * Recursively scan directory for contract files 22 | */ 23 | async function scanContractsRecursively( 24 | dirPath: string, 25 | contracts: ContractFileInfo[], 26 | ): Promise<void> { 27 | try { 28 | const entries = await readdir(dirPath, { withFileTypes: true }); 29 | 30 | for (const entry of entries) { 31 | const fullPath = join(dirPath, entry.name); 32 | 33 | if (entry.isDirectory()) { 34 | // Recursively scan subdirectories 35 | await scanContractsRecursively(fullPath, contracts); 36 | } else if (entry.isFile() && entry.name.endsWith('.contract.json')) { 37 | // Process contract file 38 | const stats = await stat(fullPath); 39 | 40 | try { 41 | const contractData = JSON.parse(await readFile(fullPath, 'utf-8')); 42 | const componentName = 43 | contractData.metadata?.componentName || 44 | extractComponentNameFromFile(entry.name); 45 | 46 | contracts.push({ 47 | fileName: entry.name, 48 | filePath: fullPath, 49 | componentName, 50 | timestamp: 51 | contractData.metadata?.timestamp || stats.mtime.toISOString(), 52 | hash: contractData.hash || 'unknown', 53 | size: formatBytes(stats.size), 54 | }); 55 | } catch { 56 | console.warn(`Skipping invalid contract file: ${fullPath}`); 57 | continue; 58 | } 59 | } 60 | } 61 | } catch (ctx) { 62 | // Silently skip directories that can't be read 63 | if ((ctx as NodeJS.ErrnoException).code !== 'ENOENT') { 64 | console.warn(`Error scanning directory ${dirPath}:`, ctx); 65 | } 66 | } 67 | } 68 | 69 | export const listComponentContractsHandler = createHandler< 70 | ListComponentContractsOptions, 71 | ContractFileInfo[] 72 | >( 73 | listComponentContractsSchema.name, 74 | async (_, { cwd: _cwd, workspaceRoot }) => { 75 | const contractDir = resolveCrossPlatformPath( 76 | workspaceRoot, 77 | '.cursor/tmp/contracts', 78 | ); 79 | const contracts: ContractFileInfo[] = []; 80 | 81 | await scanContractsRecursively(contractDir, contracts); 82 | 83 | // Sort by timestamp (newest first) 84 | contracts.sort( 85 | (a, b) => 86 | new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(), 87 | ); 88 | 89 | return contracts; 90 | }, 91 | (contracts) => { 92 | if (contracts.length === 0) { 93 | return [ 94 | '📁 No component contracts found', 95 | '💡 Use the build_component_contract tool to generate contracts', 96 | '🎯 Contracts are stored in .cursor/tmp/contracts/*.contract.json', 97 | ]; 98 | } 99 | 100 | const output: string[] = []; 101 | 102 | output.push(...formatContractsByComponent(contracts)); 103 | 104 | output.push('💡 Use diff_component_contract to compare contracts'); 105 | output.push('🔄 Newer contracts appear first within each component'); 106 | 107 | return output; 108 | }, 109 | ); 110 | 111 | export const listComponentContractsTools = [ 112 | { 113 | schema: listComponentContractsSchema, 114 | handler: listComponentContractsHandler, 115 | }, 116 | ]; 117 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/report-violations/report-all-violations.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | BaseHandlerOptions, 3 | createHandler, 4 | } from '../shared/utils/handler-helpers.js'; 5 | import { 6 | COMMON_ANNOTATIONS, 7 | createProjectAnalysisSchema, 8 | } from '../shared/models/schema-helpers.js'; 9 | import { 10 | analyzeProjectCoverage, 11 | extractComponentName, 12 | } from '../shared/violation-analysis/coverage-analyzer.js'; 13 | import { 14 | formatViolations, 15 | filterFailedAudits, 16 | groupIssuesByFile, 17 | } from '../shared/violation-analysis/formatters.js'; 18 | import { loadAndValidateDsComponentsFile } from '../../../validation/ds-components-file-loader.validation.js'; 19 | import { RESULT_FORMATTERS } from '../shared/utils/handler-helpers.js'; 20 | 21 | interface ReportAllViolationsOptions extends BaseHandlerOptions { 22 | directory: string; 23 | groupBy?: 'file' | 'folder'; 24 | } 25 | 26 | export const reportAllViolationsSchema = { 27 | name: 'report-all-violations', 28 | description: 29 | 'Scan a directory for deprecated design system CSS classes defined in the config at `deprecatedCssClassesPath`, and output a usage report', 30 | inputSchema: createProjectAnalysisSchema({ 31 | groupBy: { 32 | type: 'string', 33 | enum: ['file', 'folder'], 34 | description: 'How to group the results', 35 | default: 'file', 36 | }, 37 | }), 38 | annotations: { 39 | title: 'Report All Violations', 40 | ...COMMON_ANNOTATIONS.readOnly, 41 | }, 42 | }; 43 | 44 | export const reportAllViolationsHandler = createHandler< 45 | ReportAllViolationsOptions, 46 | string[] 47 | >( 48 | reportAllViolationsSchema.name, 49 | async (params, { cwd, deprecatedCssClassesPath }) => { 50 | if (!deprecatedCssClassesPath) { 51 | throw new Error( 52 | 'Missing ds.deprecatedCssClassesPath. Provide --ds.deprecatedCssClassesPath in mcp.json file.', 53 | ); 54 | } 55 | const groupBy = params.groupBy || 'file'; 56 | const dsComponents = await loadAndValidateDsComponentsFile( 57 | cwd, 58 | deprecatedCssClassesPath || '', 59 | ); 60 | 61 | const coverageResult = await analyzeProjectCoverage({ 62 | cwd, 63 | returnRawData: true, 64 | directory: params.directory, 65 | dsComponents, 66 | }); 67 | 68 | const raw = coverageResult.rawData?.rawPluginResult; 69 | if (!raw) return []; 70 | 71 | const failedAudits = filterFailedAudits(raw); 72 | if (failedAudits.length === 0) return ['No violations found.']; 73 | 74 | if (groupBy === 'file') { 75 | const lines: string[] = []; 76 | for (const audit of failedAudits) { 77 | extractComponentName(audit.title); 78 | const fileGroups = groupIssuesByFile( 79 | audit.details?.issues ?? [], 80 | params.directory, 81 | ); 82 | for (const [fileName, { lines: fileLines, message }] of Object.entries( 83 | fileGroups, 84 | )) { 85 | const sorted = 86 | fileLines.length > 1 87 | ? [...fileLines].sort((a, b) => a - b) 88 | : fileLines; 89 | const lineInfo = 90 | sorted.length > 1 91 | ? `lines ${sorted.join(', ')}` 92 | : `line ${sorted[0]}`; 93 | lines.push(`${fileName} (${lineInfo}): ${message}`); 94 | } 95 | } 96 | return lines; 97 | } 98 | 99 | const formattedContent = formatViolations(raw, params.directory, { 100 | groupBy: 'folder', 101 | }); 102 | return formattedContent.map( 103 | (item: { type?: string; text?: string } | string) => 104 | typeof item === 'string' ? item : (item?.text ?? String(item)), 105 | ); 106 | }, 107 | (result) => RESULT_FORMATTERS.list(result, 'Design System Violations:'), 108 | ); 109 | 110 | export const reportAllViolationsTools = [ 111 | { 112 | schema: reportAllViolationsSchema, 113 | handler: reportAllViolationsHandler, 114 | }, 115 | ]; 116 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/spec/element-helpers.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | import { 4 | extractBindings, 5 | extractAttributes, 6 | extractEvents, 7 | } from '../utils/element-helpers.js'; 8 | 9 | type MockASTWithSource = { source: string }; 10 | 11 | type MockPosition = { 12 | offset: number; 13 | file: { url: string }; 14 | }; 15 | 16 | type MockSourceSpan = { 17 | start: MockPosition; 18 | end: MockPosition; 19 | }; 20 | 21 | function makeSourceSpan( 22 | start: number, 23 | end: number, 24 | fileUrl = '/comp.html', 25 | ): MockSourceSpan { 26 | return { 27 | start: { offset: start, file: { url: fileUrl } }, 28 | end: { offset: end, file: { url: fileUrl } }, 29 | }; 30 | } 31 | 32 | interface MockInput { 33 | name: string; 34 | value: MockASTWithSource; 35 | sourceSpan?: MockSourceSpan; 36 | } 37 | 38 | interface MockAttribute { 39 | name: string; 40 | value: string; 41 | } 42 | 43 | interface MockOutput { 44 | name: string; 45 | handler: MockASTWithSource; 46 | } 47 | 48 | interface MockElement { 49 | inputs: MockInput[]; 50 | attributes: MockAttribute[]; 51 | outputs: MockOutput[]; 52 | } 53 | 54 | function createElement(partial: Partial<MockElement>): MockElement { 55 | return { 56 | inputs: [], 57 | attributes: [], 58 | outputs: [], 59 | ...partial, 60 | } as MockElement; 61 | } 62 | 63 | describe('element-helpers', () => { 64 | describe('extractBindings', () => { 65 | it('class, style, attribute, and property binding types are detected', () => { 66 | const element = createElement({ 67 | inputs: [ 68 | { name: 'class.foo', value: { source: 'cond' } }, 69 | { name: 'style.color', value: { source: 'expr' } }, 70 | { name: 'attr.data-id', value: { source: 'id' } }, 71 | { name: 'value', value: { source: 'val' } }, 72 | ], 73 | }); 74 | 75 | const bindings = extractBindings(element as any); 76 | 77 | expect(bindings).toEqual([ 78 | { 79 | type: 'class', 80 | name: 'class.foo', 81 | source: 'cond', 82 | sourceSpan: undefined, 83 | }, 84 | { 85 | type: 'style', 86 | name: 'style.color', 87 | source: 'expr', 88 | sourceSpan: undefined, 89 | }, 90 | { 91 | type: 'attribute', 92 | name: 'attr.data-id', 93 | source: 'id', 94 | sourceSpan: undefined, 95 | }, 96 | { 97 | type: 'property', 98 | name: 'value', 99 | source: 'val', 100 | sourceSpan: undefined, 101 | }, 102 | ]); 103 | }); 104 | 105 | it('maps sourceSpan information', () => { 106 | const span = makeSourceSpan(5, 15); 107 | const element = createElement({ 108 | inputs: [ 109 | { name: 'value', value: { source: 'expr' }, sourceSpan: span }, 110 | ], 111 | }); 112 | 113 | const [binding] = extractBindings(element as any); 114 | expect(binding.sourceSpan).toEqual({ 115 | start: 5, 116 | end: 15, 117 | file: '/comp.html', 118 | }); 119 | }); 120 | }); 121 | 122 | describe('extractAttributes', () => { 123 | it('returns attribute objects with type="attribute"', () => { 124 | const element = createElement({ 125 | attributes: [ 126 | { name: 'id', value: 'root' }, 127 | { name: 'role', value: 'banner' }, 128 | ], 129 | }); 130 | 131 | const attrs = extractAttributes(element as any); 132 | expect(attrs).toEqual([ 133 | { type: 'attribute', name: 'id', source: 'root' }, 134 | { type: 'attribute', name: 'role', source: 'banner' }, 135 | ]); 136 | }); 137 | }); 138 | 139 | describe('extractEvents', () => { 140 | it('returns event objects with handler source', () => { 141 | const element = createElement({ 142 | outputs: [{ name: 'click', handler: { source: 'onClick()' } }], 143 | }); 144 | 145 | const events = extractEvents(element as any); 146 | expect(events).toEqual([{ name: 'click', handler: 'onClick()' }]); 147 | }); 148 | }); 149 | }); 150 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/complex-components/second-case/complex-widget-demo.component.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Component, signal } from '@angular/core'; 2 | import { ComplexBadgeWidgetComponent, BadgeConfig } from './complex-badge-widget.component'; 3 | 4 | @Component({ 5 | selector: 'app-complex-widget-demo', 6 | standalone: true, 7 | imports: [ComplexBadgeWidgetComponent], 8 | template: ` 9 | <div class="demo-container"> 10 | <h2>Complex Badge Widget Demo</h2> 11 | <p>This component demonstrates a complex badge implementation that will fail when refactored to DsBadge.</p> 12 | 13 | <app-complex-badge-widget 14 | [initialBadges]="customBadges()" 15 | (badgeSelected)="onBadgeSelected($event)" 16 | (badgeModified)="onBadgeModified($event)" 17 | (badgeDeleted)="onBadgeDeleted($event)"> 18 | </app-complex-badge-widget> 19 | 20 | <div class="demo-log"> 21 | <h3>Event Log:</h3> 22 | <div class="log-entries"> 23 | @for (entry of eventLog(); track $index) { 24 | <div class="log-entry">{{ entry }}</div> 25 | } 26 | </div> 27 | </div> 28 | </div> 29 | `, 30 | styles: [` 31 | .demo-container { 32 | padding: 2rem; 33 | max-width: 1200px; 34 | margin: 0 auto; 35 | } 36 | 37 | .demo-container h2 { 38 | color: #1f2937; 39 | margin-bottom: 1rem; 40 | } 41 | 42 | .demo-container p { 43 | color: #6b7280; 44 | margin-bottom: 2rem; 45 | } 46 | 47 | .demo-log { 48 | background: white; 49 | border-radius: 0.5rem; 50 | padding: 1.5rem; 51 | box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); 52 | margin-top: 2rem; 53 | } 54 | 55 | .demo-log h3 { 56 | margin: 0 0 1rem 0; 57 | color: #1f2937; 58 | font-size: 1.125rem; 59 | } 60 | 61 | .log-entries { 62 | max-height: 200px; 63 | overflow-y: auto; 64 | } 65 | 66 | .log-entry { 67 | padding: 0.5rem; 68 | border-bottom: 1px solid #f3f4f6; 69 | font-size: 0.875rem; 70 | color: #374151; 71 | font-family: monospace; 72 | } 73 | 74 | .log-entry:last-child { 75 | border-bottom: none; 76 | } 77 | `] 78 | }) 79 | export class ComplexWidgetDemoComponent { 80 | eventLog = signal<string[]>([]); 81 | 82 | customBadges = signal<BadgeConfig[]>([ 83 | { 84 | id: 'complex-1', 85 | text: 'Ultra Premium', 86 | type: 'offer-badge', 87 | level: 'critical', 88 | interactive: true, 89 | customData: { 90 | prop: 'ultra-premium', 91 | complexity: 'high', 92 | features: ['animations', 'tooltips', 'dom-manipulation'], 93 | breakingPoints: ['nested-structure', 'pseudo-elements', 'direct-dom-access'] 94 | } 95 | }, 96 | { 97 | id: 'complex-2', 98 | text: 'System Health', 99 | type: 'status', 100 | level: 'high', 101 | interactive: false, 102 | customData: { 103 | prop: 'health-monitor', 104 | realTime: true, 105 | dependencies: ['custom-animations', 'complex-selectors'], 106 | refactorRisk: 'high' 107 | } 108 | }, 109 | { 110 | id: 'complex-3', 111 | text: 'Critical Priority', 112 | type: 'priority', 113 | level: 'critical', 114 | interactive: true, 115 | customData: { 116 | prop: 'priority-alert', 117 | escalated: true, 118 | customBehaviors: ['hover-effects', 'click-handlers', 'tooltip-system'], 119 | migrationComplexity: 'very-high' 120 | } 121 | } 122 | ]); 123 | 124 | onBadgeSelected(badgeId: string) { 125 | this.addLogEntry(`Badge selected: ${badgeId}`); 126 | } 127 | 128 | onBadgeModified(badge: BadgeConfig) { 129 | this.addLogEntry(`Badge modified: ${badge.id} - ${badge.text}`); 130 | } 131 | 132 | onBadgeDeleted(badgeId: string) { 133 | this.addLogEntry(`Badge deleted: ${badgeId}`); 134 | } 135 | 136 | private addLogEntry(message: string) { 137 | const timestamp = new Date().toLocaleTimeString(); 138 | const entry = `[${timestamp}] ${message}`; 139 | this.eventLog.update(log => [entry, ...log.slice(0, 49)]); 140 | } 141 | } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/validation-tests/valid.component.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Component } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | 5 | @Component({ 6 | selector: 'app-valid', 7 | standalone: true, 8 | imports: [CommonModule, FormsModule], 9 | template: ` 10 | <div class="valid-component"> 11 | <h2>{{ title }}</h2> 12 | <div class="todo-section"> 13 | <input 14 | [(ngModel)]="newTodo" 15 | placeholder="Enter a new todo" 16 | (keyup.enter)="addTodo()" 17 | class="todo-input" 18 | /> 19 | <button (click)="addTodo()" [disabled]="!newTodo.trim()"> 20 | Add Todo 21 | </button> 22 | </div> 23 | 24 | <ul class="todo-list" *ngIf="todos.length > 0"> 25 | <li *ngFor="let todo of todos; let i = index" class="todo-item"> 26 | <span [class.completed]="todo.completed">{{ todo.text }}</span> 27 | <div class="todo-actions"> 28 | <button (click)="toggleTodo(i)" class="toggle-btn"> 29 | {{ todo.completed ? 'Undo' : 'Complete' }} 30 | </button> 31 | <button (click)="removeTodo(i)" class="remove-btn">Remove</button> 32 | </div> 33 | </li> 34 | </ul> 35 | 36 | <div class="stats" *ngIf="todos.length > 0"> 37 | <p>Total: {{ todos.length }} | 38 | Completed: {{ completedCount }} | 39 | Remaining: {{ remainingCount }} 40 | </p> 41 | </div> 42 | </div> 43 | `, 44 | styles: [` 45 | .valid-component { 46 | max-width: 500px; 47 | margin: 20px auto; 48 | padding: 20px; 49 | border: 2px solid #4CAF50; 50 | border-radius: 8px; 51 | background: #f9fff9; 52 | } 53 | 54 | .todo-section { 55 | display: flex; 56 | gap: 10px; 57 | margin-bottom: 20px; 58 | } 59 | 60 | .todo-input { 61 | flex: 1; 62 | padding: 8px; 63 | border: 1px solid #ddd; 64 | border-radius: 4px; 65 | } 66 | 67 | .todo-list { 68 | list-style: none; 69 | padding: 0; 70 | } 71 | 72 | .todo-item { 73 | display: flex; 74 | justify-content: space-between; 75 | align-items: center; 76 | padding: 10px; 77 | margin-bottom: 5px; 78 | background: white; 79 | border-radius: 4px; 80 | border: 1px solid #eee; 81 | } 82 | 83 | .completed { 84 | text-decoration: line-through; 85 | color: #888; 86 | } 87 | 88 | .todo-actions { 89 | display: flex; 90 | gap: 5px; 91 | } 92 | 93 | .toggle-btn, .remove-btn { 94 | padding: 4px 8px; 95 | border: none; 96 | border-radius: 3px; 97 | cursor: pointer; 98 | font-size: 12px; 99 | } 100 | 101 | .toggle-btn { 102 | background: #2196F3; 103 | color: white; 104 | } 105 | 106 | .remove-btn { 107 | background: #f44336; 108 | color: white; 109 | } 110 | 111 | .stats { 112 | margin-top: 20px; 113 | padding: 10px; 114 | background: #e8f5e8; 115 | border-radius: 4px; 116 | text-align: center; 117 | } 118 | `] 119 | }) 120 | export class ValidComponent { 121 | title = 'Valid Todo Component'; 122 | newTodo = ''; 123 | todos: { text: string; completed: boolean }[] = [ 124 | { text: 'Learn Angular', completed: true }, 125 | { text: 'Build awesome apps', completed: false } 126 | ]; 127 | 128 | addTodo(): void { 129 | if (this.newTodo.trim()) { 130 | this.todos.push({ 131 | text: this.newTodo.trim(), 132 | completed: false 133 | }); 134 | this.newTodo = ''; 135 | } 136 | } 137 | 138 | toggleTodo(index: number): void { 139 | if (index >= 0 && index < this.todos.length) { 140 | this.todos[index].completed = !this.todos[index].completed; 141 | } 142 | } 143 | 144 | removeTodo(index: number): void { 145 | if (index >= 0 && index < this.todos.length) { 146 | this.todos.splice(index, 1); 147 | } 148 | } 149 | 150 | get completedCount(): number { 151 | return this.todos.filter(todo => todo.completed).length; 152 | } 153 | 154 | get remainingCount(): number { 155 | return this.todos.filter(todo => !todo.completed).length; 156 | } 157 | } ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/shared/utils/contract-file-ops.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { readFile, mkdir, writeFile } from 'node:fs/promises'; 2 | import { existsSync } from 'node:fs'; 3 | import { resolve, basename, extname } from 'node:path'; 4 | import { createHash } from 'node:crypto'; 5 | import { ComponentContract } from '../models/types.js'; 6 | import { resolveCrossPlatformPath } from '../../../shared/utils/cross-platform-path.js'; 7 | import { componentNameToKebabCase } from '../../../shared/utils/component-validation.js'; 8 | 9 | /** 10 | * Load a contract from a JSON file, handling both wrapped and direct formats 11 | */ 12 | export async function loadContract(path: string): Promise<ComponentContract> { 13 | if (!existsSync(path)) { 14 | throw new Error(`Contract file not found: ${path}`); 15 | } 16 | 17 | const content = await readFile(path, 'utf-8'); 18 | const data = JSON.parse(content); 19 | 20 | return data.contract || data; 21 | } 22 | 23 | /** 24 | * Save a contract to the standard location with metadata 25 | */ 26 | export async function saveContract( 27 | contract: ComponentContract, 28 | workspaceRoot: string, 29 | templatePath: string, 30 | scssPath: string, 31 | cwd: string, 32 | dsComponentName?: string, 33 | ): Promise<{ contractFilePath: string; hash: string }> { 34 | const componentName = basename(templatePath, extname(templatePath)); 35 | 36 | // Stringify early so we can compute a deterministic hash before naming the file 37 | const contractString = JSON.stringify(contract, null, 2); 38 | 39 | const hash = createHash('sha256').update(contractString).digest('hex'); 40 | 41 | const timestamp = new Date() 42 | .toISOString() 43 | .replace(/[-:]/g, '') 44 | .replace(/\.\d+Z$/, 'Z'); 45 | 46 | const contractFileName = `${componentName}-${timestamp}.contract.json`; 47 | 48 | // Determine final directory: .cursor/tmp/contracts/<kebab-scope> 49 | let contractDir = resolveCrossPlatformPath( 50 | workspaceRoot, 51 | '.cursor/tmp/contracts', 52 | ); 53 | 54 | if (dsComponentName) { 55 | const folderSlug = componentNameToKebabCase(dsComponentName); 56 | contractDir = resolveCrossPlatformPath( 57 | workspaceRoot, 58 | `.cursor/tmp/contracts/${folderSlug}`, 59 | ); 60 | } 61 | 62 | await mkdir(contractDir, { recursive: true }); 63 | 64 | const contractFilePath = resolve(contractDir, contractFileName); 65 | 66 | const contractData = { 67 | contract, 68 | hash: `sha256-${hash}`, 69 | metadata: { 70 | templatePath: resolve(cwd, templatePath), 71 | scssPath: resolve(cwd, scssPath), 72 | timestamp: new Date().toISOString(), 73 | componentName, 74 | }, 75 | }; 76 | 77 | await writeFile( 78 | contractFilePath, 79 | JSON.stringify(contractData, null, 2), 80 | 'utf-8', 81 | ); 82 | 83 | return { 84 | contractFilePath, 85 | hash: `sha256-${hash}`, 86 | }; 87 | } 88 | 89 | /** 90 | * Generate a standardized contract summary for display 91 | */ 92 | export function generateContractSummary(contract: ComponentContract): string[] { 93 | return [ 94 | `🎯 DOM Elements: ${Object.keys(contract.dom).length}`, 95 | `🎨 Style Rules: ${Object.keys(contract.styles.rules).length}`, 96 | `📥 Properties: ${Object.keys(contract.publicApi.properties).length}`, 97 | `📤 Events: ${Object.keys(contract.publicApi.events).length}`, 98 | `⚙️ Methods: ${Object.keys(contract.publicApi.methods).length}`, 99 | `🔄 Lifecycle Hooks: ${contract.publicApi.lifecycle.length}`, 100 | `📦 Imports: ${contract.publicApi.imports.length}`, 101 | `🎪 Slots: ${Object.keys(contract.slots).length}`, 102 | `📁 Source: ${contract.meta.sourceFile}`, 103 | ]; 104 | } 105 | 106 | /** 107 | * Generate a timestamped filename for diff results 108 | */ 109 | export function generateDiffFileName( 110 | beforePath: string, 111 | afterPath: string, 112 | ): string { 113 | const beforeName = basename(beforePath, '.contract.json'); 114 | const afterName = basename(afterPath, '.contract.json'); 115 | const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); 116 | return `diff-${beforeName}-vs-${afterName}-${timestamp}.json`; 117 | } 118 | ``` -------------------------------------------------------------------------------- /docs/writing-custom-tools.md: -------------------------------------------------------------------------------- ```markdown 1 | # Writing Custom Tools for Angular MCP Server 2 | 3 | > **Goal:** Enable developers to add new, type-safe MCP tools that can be called by LLMs or scripts. 4 | 5 | --- 6 | 7 | ## 1. Anatomy of a Tool 8 | 9 | Every tool consists of **three parts**: 10 | 11 | 1. **Schema** – JSON-schema (via `zod`) that describes the tool name, description and arguments. 12 | 2. **Handler** – an async function that receives a typed `CallToolRequest`, executes logic, and returns a `CallToolResult`. 13 | 3. **Registration** – export a `ToolsConfig` object and add it to the category list (`dsTools`, etc.). 14 | 15 | Directory convention 16 | ``` 17 | packages/angular-mcp-server/src/lib/tools/<category>/my-feature/hello-world.tool.ts 18 | ``` 19 | File name **must** end with `.tool.ts` so tests & registries can auto-discover it. 20 | 21 | --- 22 | 23 | ## 2. Boilerplate Template 24 | 25 | ```ts 26 | import { z } from 'zod'; 27 | import { ToolSchemaOptions, ToolsConfig } from '@push-based/models'; 28 | import { createHandler, RESULT_FORMATTERS } from '../shared/utils/handler-helpers.js'; 29 | 30 | // 1️⃣ Schema 31 | const helloWorldSchema: ToolSchemaOptions = { 32 | name: 'hello-world', 33 | description: 'Echo a friendly greeting', 34 | inputSchema: z.object({ 35 | name: z.string().describe('Name to greet'), 36 | }), 37 | annotations: { 38 | title: 'Hello World', 39 | }, 40 | }; 41 | 42 | // 2️⃣ Handler (business logic) 43 | const helloWorldHandler = createHandler<{ name: string }, string>( 44 | helloWorldSchema.name, 45 | async (params) => { 46 | return `Hello, ${params.name}! 👋`; 47 | }, 48 | (result) => RESULT_FORMATTERS.success(result), 49 | ); 50 | 51 | // 3️⃣ Registration 52 | export const helloWorldTools: ToolsConfig[] = [ 53 | { schema: helloWorldSchema, handler: helloWorldHandler }, 54 | ]; 55 | ``` 56 | 57 | Key points: 58 | * `createHandler` automatically validates common arguments, injects workspace paths, and formats output. 59 | * Generic parameters `<{ name: string }, string>` indicate input shape and raw result type. 60 | * Use `RESULT_FORMATTERS` helpers to produce consistent textual arrays. 61 | 62 | --- 63 | 64 | ## 3. Adding to the Registry 65 | 66 | Open the category file (e.g. `tools/ds/tools.ts`) and spread your array: 67 | 68 | ```ts 69 | import { helloWorldTools } from './my-feature/hello-world.tool'; 70 | 71 | export const dsTools: ToolsConfig[] = [ 72 | // …existing 73 | ...helloWorldTools, 74 | ]; 75 | ``` 76 | 77 | The server will now expose `hello-world` via `list_tools`. 78 | 79 | --- 80 | 81 | ## 4. Parameter Injection 82 | 83 | The server adds workspace-specific paths to every call so you don’t need to pass them manually: 84 | 85 | | Field | Injected Value | 86 | |-------|----------------| 87 | | `cwd` | Current working dir (may be overridden) | 88 | | `workspaceRoot` | `--workspaceRoot` CLI flag | 89 | | `storybookDocsRoot` | Relative path from CLI flags | 90 | | `deprecatedCssClassesPath` | Path to deprecated CSS map | 91 | | `uiRoot` | Path to DS component source | 92 | 93 | Access them inside the handler via the second argument of `createHandler`: 94 | 95 | ```ts 96 | async (params, ctx) => { 97 | console.log('Workspace root:', ctx.workspaceRoot); 98 | } 99 | ``` 100 | 101 | --- 102 | 103 | ## 5. Validation Helpers 104 | 105 | * `validateCommonInputs` – ensures `directory` is string, `componentName` matches `Ds[A-Z]…` pattern. 106 | * Custom validation: extend the Zod `inputSchema` with additional constraints. 107 | 108 | --- 109 | 110 | ## 6. Testing Your Tool 111 | 112 | 1. **Unit tests** (recommended): import the handler function directly and assert on the returned `CallToolResult`. 113 | 114 | --- 115 | 116 | ## 7. Documentation Checklist 117 | 118 | After publishing a tool: 119 | 120 | - [ ] Add an entry to `docs/tools.md` with purpose, parameters, output. 121 | 122 | --- 123 | 124 | ## 8. Common Pitfalls 125 | 126 | | Pitfall | Fix | 127 | |---------|-----| 128 | | Tool not listed via `list_tools` | Forgot to spread into `dsTools` array. | 129 | | “Unknown argument” error | Ensure `inputSchema` matches argument names exactly. | 130 | | Long-running sync FS operations | Use async `fs/promises` or worker threads to keep server responsive. | 131 | 132 | --- 133 | 134 | Happy tooling! 🎉 ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/shared/utils/regex-helpers.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Shared regex utilities for DS tools 3 | * Consolidates regex patterns used across multiple tools to avoid duplication 4 | */ 5 | 6 | // CSS Processing Regexes 7 | export const CSS_REGEXES = { 8 | /** 9 | * Escapes special regex characters in a string for safe use in regex patterns 10 | */ 11 | escape: (str: string): string => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 12 | 13 | /** 14 | * Creates a regex to match CSS classes from a list of class names 15 | */ 16 | createClassMatcher: (classNames: string[]): RegExp => 17 | new RegExp( 18 | `\\.(${classNames.map(CSS_REGEXES.escape).join('|')})(?![\\w-])`, 19 | 'g', 20 | ), 21 | 22 | /** 23 | * Style file extensions 24 | */ 25 | STYLE_EXTENSIONS: /\.(css|scss|sass|less)$/i, 26 | } as const; 27 | 28 | // Path Processing Regexes 29 | export const PATH_REGEXES = { 30 | /** 31 | * Normalizes file paths to use forward slashes 32 | */ 33 | normalizeToUnix: (path: string): string => path.replace(/\\/g, '/'), 34 | 35 | /** 36 | * Removes directory prefix from file paths 37 | */ 38 | removeDirectoryPrefix: (filePath: string, directory: string): string => { 39 | const normalizedFilePath = PATH_REGEXES.normalizeToUnix(filePath); 40 | const normalizedDirectory = PATH_REGEXES.normalizeToUnix(directory); 41 | const directoryPrefix = normalizedDirectory.endsWith('/') 42 | ? normalizedDirectory 43 | : normalizedDirectory + '/'; 44 | 45 | return normalizedFilePath.startsWith(directoryPrefix) 46 | ? normalizedFilePath.replace(directoryPrefix, '') 47 | : normalizedFilePath; 48 | }, 49 | } as const; 50 | 51 | // Component Name Processing Regexes 52 | export const COMPONENT_REGEXES = { 53 | /** 54 | * Converts DS component name to kebab-case 55 | */ 56 | toKebabCase: (componentName: string): string => 57 | componentName 58 | .replace(/^Ds/, '') 59 | .replace(/([a-z0-9])([A-Z])/g, '$1-$2') 60 | .toLowerCase(), 61 | 62 | /** 63 | * Validates DS component name format (accepts both "DsButton" and "Button" formats) 64 | */ 65 | isValidDsComponent: (name: string): boolean => 66 | /^(Ds)?[A-Z][a-zA-Z0-9]*$/.test(name), 67 | 68 | /** 69 | * Extracts component name from coverage titles 70 | */ 71 | extractFromCoverageTitle: (title: string): string | null => { 72 | const match = title.match(/Usage coverage for (\w+) component/); 73 | return match ? match[1] : null; 74 | }, 75 | } as const; 76 | 77 | // Import/Dependency Processing Regexes (from component-usage-graph) 78 | export const IMPORT_REGEXES = { 79 | ES6_IMPORT: 80 | /import\s+(?:(?:[\w\s{},*]+\s+from\s+)?['"`]([^'"`]+)['"`]|['"`]([^'"`]+)['"`])/g, 81 | COMMONJS_REQUIRE: /require\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g, 82 | DYNAMIC_IMPORT: /import\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g, 83 | CSS_IMPORT: /@import\s+['"`]([^'"`]+)['"`]/g, 84 | CSS_URL: /url\s*\(\s*['"`]?([^'"`)]+)['"`]?\s*\)/g, 85 | ANGULAR_COMPONENT_DECORATOR: /@Component/, 86 | 87 | /** 88 | * Creates cached regex for component imports 89 | */ 90 | createComponentImportRegex: (componentName: string): RegExp => 91 | new RegExp(`import[\\s\\S]*?\\b${componentName}\\b[\\s\\S]*?from`, 'gm'), 92 | 93 | /** 94 | * Creates combined regex for multiple component imports 95 | */ 96 | createCombinedComponentImportRegex: (componentNames: string[]): RegExp => 97 | new RegExp( 98 | `import[\\s\\S]*?\\b(${componentNames.join('|')})\\b[\\s\\S]*?from`, 99 | 'gm', 100 | ), 101 | } as const; 102 | 103 | // Regex Cache Management 104 | const REGEX_CACHE = new Map<string, RegExp>(); 105 | 106 | export const REGEX_CACHE_UTILS = { 107 | /** 108 | * Gets or creates a cached regex 109 | */ 110 | getOrCreate: (key: string, factory: () => RegExp): RegExp => { 111 | let regex = REGEX_CACHE.get(key); 112 | if (!regex) { 113 | regex = factory(); 114 | REGEX_CACHE.set(key, regex); 115 | } 116 | regex.lastIndex = 0; // Reset for consistent behavior 117 | return regex; 118 | }, 119 | 120 | /** 121 | * Clears the regex cache 122 | */ 123 | clear: (): void => REGEX_CACHE.clear(), 124 | 125 | /** 126 | * Gets cache statistics 127 | */ 128 | getStats: () => ({ size: REGEX_CACHE.size }), 129 | } as const; 130 | ``` -------------------------------------------------------------------------------- /packages/shared/ds-component-coverage/ai/EXAMPLES.md: -------------------------------------------------------------------------------- ```markdown 1 | # Examples 2 | 3 | ## 1 — Basic plugin setup 4 | 5 | > Create a DS component coverage plugin to detect deprecated CSS classes. 6 | 7 | ```ts 8 | import { dsComponentCoveragePlugin } from '@push-based/ds-component-coverage'; 9 | 10 | const plugin = dsComponentCoveragePlugin({ 11 | directory: './src/app', 12 | dsComponents: [ 13 | { 14 | componentName: 'DsButton', 15 | deprecatedCssClasses: ['btn', 'button-primary'], 16 | docsUrl: 'https://design-system.com/button', 17 | }, 18 | ], 19 | }); 20 | 21 | console.log(plugin.slug); // → 'ds-component-coverage' 22 | ``` 23 | 24 | --- 25 | 26 | ## 2 — Running coverage analysis 27 | 28 | > Execute the runner function to analyze Angular components for deprecated CSS usage. 29 | 30 | ```ts 31 | import { runnerFunction } from '@push-based/ds-component-coverage'; 32 | 33 | const results = await runnerFunction({ 34 | directory: './src/app', 35 | dsComponents: [ 36 | { 37 | componentName: 'DsCard', 38 | deprecatedCssClasses: ['card', 'card-header', 'card-body'], 39 | }, 40 | ], 41 | }); 42 | 43 | console.log(results.length); // → Number of audit outputs 44 | ``` 45 | 46 | --- 47 | 48 | ## 3 — Multiple component tracking 49 | 50 | > Track multiple design system components and their deprecated classes. 51 | 52 | ```ts 53 | import { dsComponentCoveragePlugin } from '@push-based/ds-component-coverage'; 54 | 55 | const plugin = dsComponentCoveragePlugin({ 56 | directory: './src', 57 | dsComponents: [ 58 | { 59 | componentName: 'DsButton', 60 | deprecatedCssClasses: ['btn', 'button'], 61 | docsUrl: 'https://design-system.com/button', 62 | }, 63 | { 64 | componentName: 'DsModal', 65 | deprecatedCssClasses: ['modal', 'dialog'], 66 | docsUrl: 'https://design-system.com/modal', 67 | }, 68 | { 69 | componentName: 'DsInput', 70 | deprecatedCssClasses: ['form-control', 'input-field'], 71 | }, 72 | ], 73 | }); 74 | 75 | console.log(plugin.audits.length); // → 3 audits (one per component) 76 | ``` 77 | 78 | --- 79 | 80 | ## 4 — Code Pushup integration 81 | 82 | > Integrate with Code Pushup for automated design system migration tracking. 83 | 84 | ```ts 85 | import { 86 | dsComponentCoveragePlugin, 87 | getAngularDsUsageCategoryRefs, 88 | } from '@push-based/ds-component-coverage'; 89 | 90 | const dsComponents = [ 91 | { 92 | componentName: 'DsBadge', 93 | deprecatedCssClasses: ['badge', 'label'], 94 | docsUrl: 'https://design-system.com/badge', 95 | }, 96 | ]; 97 | 98 | // Use in code-pushup.config.ts 99 | export default { 100 | plugins: [ 101 | dsComponentCoveragePlugin({ 102 | directory: './src/app', 103 | dsComponents, 104 | }), 105 | ], 106 | categories: [ 107 | { 108 | slug: 'design-system-usage', 109 | title: 'Design System Usage', 110 | description: 'Usage of design system components', 111 | refs: getAngularDsUsageCategoryRefs(dsComponents), 112 | }, 113 | ], 114 | }; 115 | ``` 116 | 117 | --- 118 | 119 | ## 5 — Category references for reporting 120 | 121 | > Generate category references for organizing audit results. 122 | 123 | ```ts 124 | import { getAngularDsUsageCategoryRefs } from '@push-based/ds-component-coverage'; 125 | 126 | const dsComponents = [ 127 | { 128 | componentName: 'DsButton', 129 | deprecatedCssClasses: ['btn'], 130 | }, 131 | { 132 | componentName: 'DsCard', 133 | deprecatedCssClasses: ['card'], 134 | }, 135 | ]; 136 | 137 | const categoryRefs = getAngularDsUsageCategoryRefs(dsComponents); 138 | console.log(categoryRefs); // → Array of category references for each component 139 | ``` 140 | 141 | --- 142 | 143 | ## 6 — Custom configuration with schema validation 144 | 145 | > Use schema validation to ensure proper configuration structure. 146 | 147 | ```ts 148 | import { 149 | ComponentCoverageRunnerOptionsSchema, 150 | ComponentReplacementSchema, 151 | } from '@push-based/ds-component-coverage'; 152 | 153 | // Validate individual component replacement 154 | const componentConfig = ComponentReplacementSchema.parse({ 155 | componentName: 'DsAlert', 156 | deprecatedCssClasses: ['alert', 'notification'], 157 | docsUrl: 'https://design-system.com/alert', 158 | }); 159 | 160 | // Validate full runner options 161 | const runnerConfig = ComponentCoverageRunnerOptionsSchema.parse({ 162 | directory: './src/app', 163 | dsComponents: [componentConfig], 164 | }); 165 | 166 | console.log(runnerConfig.directory); // → './src/app' 167 | ``` 168 | ``` -------------------------------------------------------------------------------- /packages/shared/utils/src/lib/execute-process.unit.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ChildProcess } from 'node:child_process'; 2 | import { describe, expect, it, vi } from 'vitest'; 3 | import { getAsyncProcessRunnerConfig } from '@push-based/testing-utils'; 4 | import { type ProcessObserver, executeProcess } from './execute-process.js'; 5 | 6 | describe('executeProcess', () => { 7 | const spyObserver: ProcessObserver = { 8 | onStdout: vi.fn(), 9 | onStderr: vi.fn(), 10 | onError: vi.fn(), 11 | onComplete: vi.fn(), 12 | }; 13 | const errorSpy = vi.fn(); 14 | 15 | beforeEach(() => { 16 | vi.clearAllMocks(); 17 | }); 18 | 19 | it('should work with node command `node -v`', async () => { 20 | const processResult = await executeProcess({ 21 | command: `node`, 22 | args: ['-v'], 23 | observer: spyObserver, 24 | }); 25 | 26 | // Note: called once or twice depending on environment (2nd time for a new line) 27 | expect(spyObserver.onStdout).toHaveBeenCalled(); 28 | expect(spyObserver.onComplete).toHaveBeenCalledOnce(); 29 | expect(spyObserver.onError).not.toHaveBeenCalled(); 30 | expect(processResult.stdout).toMatch(/v\d{1,2}(\.\d{1,2}){0,2}/); 31 | }); 32 | 33 | it('should work with npx command `npx --help`', async () => { 34 | const processResult = await executeProcess({ 35 | command: `npx`, 36 | args: ['--help'], 37 | observer: spyObserver, 38 | }); 39 | expect(spyObserver.onStdout).toHaveBeenCalledOnce(); 40 | expect(spyObserver.onComplete).toHaveBeenCalledOnce(); 41 | expect(spyObserver.onError).not.toHaveBeenCalled(); 42 | expect(processResult.stdout).toContain('npm exec'); 43 | }); 44 | 45 | it('should work with script `node custom-script.js`', async () => { 46 | const processResult = await executeProcess({ 47 | ...getAsyncProcessRunnerConfig({ interval: 10, runs: 4 }), 48 | observer: spyObserver, 49 | }).catch(errorSpy); 50 | 51 | expect(errorSpy).not.toHaveBeenCalled(); 52 | expect(processResult.stdout).toContain('process:complete'); 53 | expect(spyObserver.onStdout).toHaveBeenCalledTimes(6); // intro + 4 runs + complete 54 | expect(spyObserver.onError).not.toHaveBeenCalled(); 55 | expect(spyObserver.onComplete).toHaveBeenCalledOnce(); 56 | }); 57 | 58 | it('should work with async script `node custom-script.js` that throws an error', async () => { 59 | const processResult = await executeProcess({ 60 | ...getAsyncProcessRunnerConfig({ 61 | interval: 10, 62 | runs: 1, 63 | throwError: true, 64 | }), 65 | observer: spyObserver, 66 | }).catch(errorSpy); 67 | 68 | expect(errorSpy).toHaveBeenCalledOnce(); 69 | expect(processResult).toBeUndefined(); 70 | expect(spyObserver.onStdout).toHaveBeenCalledTimes(2); // intro + 1 run before error 71 | expect(spyObserver.onStdout).toHaveBeenLastCalledWith( 72 | 'process:update\n', 73 | expect.any(ChildProcess), 74 | ); 75 | expect(spyObserver.onStderr).toHaveBeenCalled(); 76 | expect(spyObserver.onStderr).toHaveBeenCalledWith( 77 | expect.stringContaining('dummy-error'), 78 | expect.any(ChildProcess), 79 | ); 80 | expect(spyObserver.onError).toHaveBeenCalledOnce(); 81 | expect(spyObserver.onComplete).not.toHaveBeenCalled(); 82 | }); 83 | 84 | it('should successfully exit process after an error is thrown when ignoreExitCode is set', async () => { 85 | const processResult = await executeProcess({ 86 | ...getAsyncProcessRunnerConfig({ 87 | interval: 10, 88 | runs: 1, 89 | throwError: true, 90 | }), 91 | observer: spyObserver, 92 | ignoreExitCode: true, 93 | }).catch(errorSpy); 94 | 95 | expect(errorSpy).not.toHaveBeenCalled(); 96 | expect(processResult.code).toBe(1); 97 | expect(processResult.stdout).toContain('process:update'); 98 | expect(processResult.stderr).toContain('dummy-error'); 99 | expect(spyObserver.onStdout).toHaveBeenCalledTimes(2); // intro + 1 run before error 100 | expect(spyObserver.onStderr).toHaveBeenCalled(); 101 | expect(spyObserver.onError).not.toHaveBeenCalled(); 102 | expect(spyObserver.onComplete).toHaveBeenCalledOnce(); 103 | }); 104 | }); 105 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/list/spec/contract-list-utils.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | /* eslint-disable prefer-const */ 2 | import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; 3 | 4 | import { 5 | extractComponentNameFromFile, 6 | formatBytes, 7 | getTimeAgo, 8 | formatContractsByComponent, 9 | } from '../utils/contract-list-utils.js'; 10 | 11 | import type { ContractFileInfo } from '../models/types.js'; 12 | 13 | function advanceSystemTimeTo(fixed: Date) { 14 | vi.useFakeTimers(); 15 | vi.setSystemTime(fixed); 16 | } 17 | 18 | function restoreSystemTime() { 19 | vi.useRealTimers(); 20 | } 21 | 22 | describe('contract-list-utils', () => { 23 | afterEach(() => { 24 | restoreSystemTime(); 25 | }); 26 | 27 | describe('extractComponentNameFromFile', () => { 28 | it('extracts simple component names', () => { 29 | expect( 30 | extractComponentNameFromFile('foo-20240208T123456.contract.json'), 31 | ).toBe('foo'); 32 | expect(extractComponentNameFromFile('bar.contract.json')).toBe('bar'); 33 | }); 34 | 35 | it('handles multi-part component names', () => { 36 | const file = 'my-super-button-20240208T123456.contract.json'; 37 | expect(extractComponentNameFromFile(file)).toBe('my'); 38 | }); 39 | }); 40 | 41 | describe('formatBytes', () => { 42 | it('formats bytes into readable units', () => { 43 | expect(formatBytes(0)).toBe('0 B'); 44 | expect(formatBytes(512)).toBe('512 B'); 45 | expect(formatBytes(2048)).toBe('2 KB'); 46 | expect(formatBytes(1024 * 1024)).toBe('1 MB'); 47 | }); 48 | }); 49 | 50 | describe('getTimeAgo', () => { 51 | beforeEach(() => { 52 | advanceSystemTimeTo(new Date('2024-02-10T12:00:00Z')); 53 | }); 54 | 55 | it('returns minutes ago for <1h', () => { 56 | const ts = new Date('2024-02-10T11:45:00Z').toISOString(); 57 | expect(getTimeAgo(ts)).toBe('15m ago'); 58 | }); 59 | 60 | it('returns hours ago for <24h', () => { 61 | const ts = new Date('2024-02-10T08:00:00Z').toISOString(); 62 | expect(getTimeAgo(ts)).toBe('4h ago'); 63 | }); 64 | 65 | it('returns days ago for <7d', () => { 66 | const ts = new Date('2024-02-07T12:00:00Z').toISOString(); 67 | expect(getTimeAgo(ts)).toBe('3d ago'); 68 | }); 69 | 70 | it('returns locale date for older timestamps', () => { 71 | const ts = new Date('2023-12-25T00:00:00Z').toISOString(); 72 | expect(getTimeAgo(ts)).toContain('2023'); 73 | }); 74 | }); 75 | 76 | describe('formatContractsByComponent', () => { 77 | beforeEach(() => { 78 | advanceSystemTimeTo(new Date('2024-02-10T12:00:00Z')); 79 | }); 80 | 81 | it('groups contracts by component and formats output', () => { 82 | const contracts: ContractFileInfo[] = [ 83 | { 84 | fileName: 'foo-20240210T090000.contract.json', 85 | filePath: '/contracts/foo-20240210T090000.contract.json', 86 | componentName: 'foo', 87 | timestamp: new Date('2024-02-10T09:00:00Z').toISOString(), 88 | hash: 'abcdef1234567890', 89 | size: '5 KB', 90 | }, 91 | { 92 | fileName: 'foo-20240209T090000.contract.json', 93 | filePath: '/contracts/foo-20240209T090000.contract.json', 94 | componentName: 'foo', 95 | timestamp: new Date('2024-02-09T09:00:00Z').toISOString(), 96 | hash: '123456abcdef7890', 97 | size: '4 KB', 98 | }, 99 | { 100 | fileName: 'bar-20240210T090000.contract.json', 101 | filePath: '/contracts/bar-20240210T090000.contract.json', 102 | componentName: 'bar', 103 | timestamp: new Date('2024-02-10T09:00:00Z').toISOString(), 104 | hash: 'fedcba9876543210', 105 | size: '6 KB', 106 | }, 107 | ]; 108 | 109 | const output = formatContractsByComponent(contracts); 110 | 111 | expect(output).toEqual( 112 | expect.arrayContaining([ 113 | expect.stringMatching(/^🎯 foo:/), 114 | expect.stringMatching(/^🎯 bar:/), 115 | ]), 116 | ); 117 | 118 | expect(output).toEqual( 119 | expect.arrayContaining([ 120 | expect.stringContaining('foo-20240210T090000.contract.json'), 121 | expect.stringContaining('bar-20240210T090000.contract.json'), 122 | ]), 123 | ); 124 | }); 125 | }); 126 | }); 127 | ```