This is page 4 of 7. Use http://codebase.md/push-based/angular-toolkit-mcp?page={x} to view the full context. # Directory Structure ``` ├── .aiignore ├── .cursor │ ├── flows │ │ ├── component-refactoring │ │ │ ├── 01-review-component.mdc │ │ │ ├── 02-refactor-component.mdc │ │ │ ├── 03-validate-component.mdc │ │ │ └── angular-20.md │ │ ├── ds-refactoring-flow │ │ │ ├── 01-find-violations.mdc │ │ │ ├── 01b-find-all-violations.mdc │ │ │ ├── 02-plan-refactoring.mdc │ │ │ ├── 02b-plan-refactoring-for-all-violations.mdc │ │ │ ├── 03-fix-violations.mdc │ │ │ ├── 03-non-viable-cases.mdc │ │ │ ├── 04-validate-changes.mdc │ │ │ ├── 05-prepare-report.mdc │ │ │ └── clean-global-styles.mdc │ │ └── README.md │ └── mcp.json.example ├── .github │ └── workflows │ └── ci.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── assets │ ├── entain-logo.png │ └── entain.png ├── CONTRIBUTING.MD ├── docs │ ├── architecture-internal-design.md │ ├── component-refactoring-flow.md │ ├── contracts.md │ ├── ds-refactoring-flow.md │ ├── getting-started.md │ ├── README.md │ ├── tools.md │ └── writing-custom-tools.md ├── eslint.config.mjs ├── jest.config.ts ├── jest.preset.mjs ├── LICENSE ├── nx.json ├── package-lock.json ├── package.json ├── packages │ ├── .gitkeep │ ├── angular-mcp │ │ ├── eslint.config.mjs │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── assets │ │ │ │ └── .gitkeep │ │ │ └── main.ts │ │ ├── tsconfig.app.json │ │ ├── tsconfig.json │ │ ├── vitest.config.mts │ │ └── webpack.config.cjs │ ├── angular-mcp-server │ │ ├── eslint.config.mjs │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── index.ts │ │ │ └── lib │ │ │ ├── angular-mcp-server.ts │ │ │ ├── prompts │ │ │ │ └── prompt-registry.ts │ │ │ ├── tools │ │ │ │ ├── ds │ │ │ │ │ ├── component │ │ │ │ │ │ ├── get-deprecated-css-classes.tool.ts │ │ │ │ │ │ ├── get-ds-component-data.tool.ts │ │ │ │ │ │ ├── list-ds-components.tool.ts │ │ │ │ │ │ └── utils │ │ │ │ │ │ ├── deprecated-css-helpers.ts │ │ │ │ │ │ ├── doc-helpers.ts │ │ │ │ │ │ ├── metadata-helpers.ts │ │ │ │ │ │ └── paths-helpers.ts │ │ │ │ │ ├── component-contract │ │ │ │ │ │ ├── builder │ │ │ │ │ │ │ ├── build-component-contract.tool.ts │ │ │ │ │ │ │ ├── models │ │ │ │ │ │ │ │ ├── schema.ts │ │ │ │ │ │ │ │ └── types.ts │ │ │ │ │ │ │ ├── spec │ │ │ │ │ │ │ │ ├── css-match.spec.ts │ │ │ │ │ │ │ │ ├── dom-slots.extractor.spec.ts │ │ │ │ │ │ │ │ ├── element-helpers.spec.ts │ │ │ │ │ │ │ │ ├── inline-styles.collector.spec.ts │ │ │ │ │ │ │ │ ├── meta.generator.spec.ts │ │ │ │ │ │ │ │ ├── public-api.extractor.spec.ts │ │ │ │ │ │ │ │ ├── styles.collector.spec.ts │ │ │ │ │ │ │ │ └── typescript-analyzer.spec.ts │ │ │ │ │ │ │ └── utils │ │ │ │ │ │ │ ├── build-contract.ts │ │ │ │ │ │ │ ├── css-match.ts │ │ │ │ │ │ │ ├── dom-slots.extractor.ts │ │ │ │ │ │ │ ├── element-helpers.ts │ │ │ │ │ │ │ ├── inline-styles.collector.ts │ │ │ │ │ │ │ ├── meta.generator.ts │ │ │ │ │ │ │ ├── public-api.extractor.ts │ │ │ │ │ │ │ ├── styles.collector.ts │ │ │ │ │ │ │ └── typescript-analyzer.ts │ │ │ │ │ │ ├── diff │ │ │ │ │ │ │ ├── diff-component-contract.tool.ts │ │ │ │ │ │ │ ├── models │ │ │ │ │ │ │ │ └── schema.ts │ │ │ │ │ │ │ ├── spec │ │ │ │ │ │ │ │ ├── diff-utils.spec.ts │ │ │ │ │ │ │ │ └── dom-path-utils.spec.ts │ │ │ │ │ │ │ └── utils │ │ │ │ │ │ │ ├── diff-utils.ts │ │ │ │ │ │ │ └── dom-path-utils.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── list │ │ │ │ │ │ │ ├── list-component-contracts.tool.ts │ │ │ │ │ │ │ ├── models │ │ │ │ │ │ │ │ ├── schema.ts │ │ │ │ │ │ │ │ └── types.ts │ │ │ │ │ │ │ ├── spec │ │ │ │ │ │ │ │ └── contract-list-utils.spec.ts │ │ │ │ │ │ │ └── utils │ │ │ │ │ │ │ └── contract-list-utils.ts │ │ │ │ │ │ └── shared │ │ │ │ │ │ ├── models │ │ │ │ │ │ │ └── types.ts │ │ │ │ │ │ ├── spec │ │ │ │ │ │ │ └── contract-file-ops.spec.ts │ │ │ │ │ │ └── utils │ │ │ │ │ │ └── contract-file-ops.ts │ │ │ │ │ ├── component-usage-graph │ │ │ │ │ │ ├── build-component-usage-graph.tool.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── models │ │ │ │ │ │ │ ├── config.ts │ │ │ │ │ │ │ ├── schema.ts │ │ │ │ │ │ │ └── types.ts │ │ │ │ │ │ └── utils │ │ │ │ │ │ ├── angular-parser.ts │ │ │ │ │ │ ├── component-helpers.ts │ │ │ │ │ │ ├── component-usage-graph-builder.ts │ │ │ │ │ │ ├── path-resolver.ts │ │ │ │ │ │ └── unified-ast-analyzer.ts │ │ │ │ │ ├── ds.tools.ts │ │ │ │ │ ├── project │ │ │ │ │ │ ├── get-project-dependencies.tool.ts │ │ │ │ │ │ ├── report-deprecated-css.tool.ts │ │ │ │ │ │ └── utils │ │ │ │ │ │ ├── dependencies-helpers.ts │ │ │ │ │ │ └── styles-report-helpers.ts │ │ │ │ │ ├── report-violations │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── models │ │ │ │ │ │ │ ├── schema.ts │ │ │ │ │ │ │ └── types.ts │ │ │ │ │ │ ├── report-all-violations.tool.ts │ │ │ │ │ │ └── report-violations.tool.ts │ │ │ │ │ ├── shared │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── models │ │ │ │ │ │ │ ├── input-schemas.model.ts │ │ │ │ │ │ │ └── schema-helpers.ts │ │ │ │ │ │ ├── utils │ │ │ │ │ │ │ ├── component-validation.ts │ │ │ │ │ │ │ ├── cross-platform-path.ts │ │ │ │ │ │ │ ├── handler-helpers.ts │ │ │ │ │ │ │ ├── output.utils.ts │ │ │ │ │ │ │ └── regex-helpers.ts │ │ │ │ │ │ └── violation-analysis │ │ │ │ │ │ ├── base-analyzer.ts │ │ │ │ │ │ ├── coverage-analyzer.ts │ │ │ │ │ │ ├── formatters.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── types.ts │ │ │ │ │ └── tools.ts │ │ │ │ ├── schema.ts │ │ │ │ ├── tools.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils.ts │ │ │ └── validation │ │ │ ├── angular-mcp-server-options.schema.ts │ │ │ ├── ds-components-file-loader.validation.ts │ │ │ ├── ds-components-file.validation.ts │ │ │ ├── ds-components.schema.ts │ │ │ └── file-existence.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ ├── tsconfig.tsbuildinfo │ │ └── vitest.config.mts │ ├── minimal-repo │ │ └── packages │ │ ├── application │ │ │ ├── angular.json │ │ │ ├── code-pushup.config.ts │ │ │ ├── src │ │ │ │ ├── app │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.config.ts │ │ │ │ │ ├── app.routes.ts │ │ │ │ │ ├── components │ │ │ │ │ │ ├── refactoring-tests │ │ │ │ │ │ │ ├── bad-alert-tooltip-input.component.ts │ │ │ │ │ │ │ ├── bad-alert.component.ts │ │ │ │ │ │ │ ├── bad-button-dropdown.component.ts │ │ │ │ │ │ │ ├── bad-document.component.ts │ │ │ │ │ │ │ ├── bad-global-this.component.ts │ │ │ │ │ │ │ ├── bad-mixed-external-assets.component.css │ │ │ │ │ │ │ ├── bad-mixed-external-assets.component.html │ │ │ │ │ │ │ ├── bad-mixed-external-assets.component.ts │ │ │ │ │ │ │ ├── bad-mixed-not-standalone.component.ts │ │ │ │ │ │ │ ├── bad-mixed.component.ts │ │ │ │ │ │ │ ├── bad-mixed.module.ts │ │ │ │ │ │ │ ├── bad-modal-progress.component.ts │ │ │ │ │ │ │ ├── bad-this-window-document.component.ts │ │ │ │ │ │ │ ├── bad-window.component.ts │ │ │ │ │ │ │ ├── complex-components │ │ │ │ │ │ │ │ ├── first-case │ │ │ │ │ │ │ │ │ ├── dashboard-demo.component.html │ │ │ │ │ │ │ │ │ ├── dashboard-demo.component.scss │ │ │ │ │ │ │ │ │ ├── dashboard-demo.component.ts │ │ │ │ │ │ │ │ │ ├── dashboard-header.component.html │ │ │ │ │ │ │ │ │ ├── dashboard-header.component.scss │ │ │ │ │ │ │ │ │ └── dashboard-header.component.ts │ │ │ │ │ │ │ │ ├── second-case │ │ │ │ │ │ │ │ │ ├── complex-badge-widget.component.scss │ │ │ │ │ │ │ │ │ ├── complex-badge-widget.component.ts │ │ │ │ │ │ │ │ │ └── complex-widget-demo.component.ts │ │ │ │ │ │ │ │ └── third-case │ │ │ │ │ │ │ │ ├── product-card.component.scss │ │ │ │ │ │ │ │ ├── product-card.component.ts │ │ │ │ │ │ │ │ └── product-showcase.component.ts │ │ │ │ │ │ │ ├── group-1 │ │ │ │ │ │ │ │ ├── bad-mixed-1.component.ts │ │ │ │ │ │ │ │ ├── bad-mixed-1.module.ts │ │ │ │ │ │ │ │ ├── bad-mixed-external-assets-1.component.css │ │ │ │ │ │ │ │ ├── bad-mixed-external-assets-1.component.html │ │ │ │ │ │ │ │ ├── bad-mixed-external-assets-1.component.ts │ │ │ │ │ │ │ │ └── bad-mixed-not-standalone-1.component.ts │ │ │ │ │ │ │ ├── group-2 │ │ │ │ │ │ │ │ ├── bad-mixed-2.component.ts │ │ │ │ │ │ │ │ ├── bad-mixed-2.module.ts │ │ │ │ │ │ │ │ ├── bad-mixed-external-assets-2.component.css │ │ │ │ │ │ │ │ ├── bad-mixed-external-assets-2.component.html │ │ │ │ │ │ │ │ ├── bad-mixed-external-assets-2.component.ts │ │ │ │ │ │ │ │ └── bad-mixed-not-standalone-2.component.ts │ │ │ │ │ │ │ ├── group-3 │ │ │ │ │ │ │ │ ├── bad-mixed-3.component.spec.ts │ │ │ │ │ │ │ │ ├── bad-mixed-3.component.ts │ │ │ │ │ │ │ │ ├── bad-mixed-3.module.ts │ │ │ │ │ │ │ │ ├── bad-mixed-external-assets-3.component.css │ │ │ │ │ │ │ │ ├── bad-mixed-external-assets-3.component.html │ │ │ │ │ │ │ │ ├── bad-mixed-external-assets-3.component.ts │ │ │ │ │ │ │ │ ├── bad-mixed-not-standalone-3.component.ts │ │ │ │ │ │ │ │ └── lazy-loader-3.component.ts │ │ │ │ │ │ │ └── group-4 │ │ │ │ │ │ │ ├── multi-violation-test.component.html │ │ │ │ │ │ │ ├── multi-violation-test.component.scss │ │ │ │ │ │ │ └── multi-violation-test.component.ts │ │ │ │ │ │ └── validation-tests │ │ │ │ │ │ ├── circular-dependency.component.ts │ │ │ │ │ │ ├── external-files-missing.component.ts │ │ │ │ │ │ ├── invalid-lifecycle.component.ts │ │ │ │ │ │ ├── invalid-pipe-usage.component.ts │ │ │ │ │ │ ├── invalid-template-syntax.component.ts │ │ │ │ │ │ ├── missing-imports.component.ts │ │ │ │ │ │ ├── missing-method.component.ts │ │ │ │ │ │ ├── README.md │ │ │ │ │ │ ├── standalone-module-conflict.component.ts │ │ │ │ │ │ ├── standalone-module-conflict.module.ts │ │ │ │ │ │ ├── template-reference-error.component.ts │ │ │ │ │ │ ├── type-mismatch.component.ts │ │ │ │ │ │ ├── valid.component.ts │ │ │ │ │ │ ├── wrong-decorator-usage.component.ts │ │ │ │ │ │ └── wrong-property-binding.component.ts │ │ │ │ │ └── styles │ │ │ │ │ ├── bad-global-styles.scss │ │ │ │ │ ├── base │ │ │ │ │ │ ├── _reset.scss │ │ │ │ │ │ └── base.scss │ │ │ │ │ ├── components │ │ │ │ │ │ └── components.scss │ │ │ │ │ ├── extended-deprecated-styles.scss │ │ │ │ │ ├── layout │ │ │ │ │ │ └── layout.scss │ │ │ │ │ ├── new-styles-1.scss │ │ │ │ │ ├── new-styles-10.scss │ │ │ │ │ ├── new-styles-2.scss │ │ │ │ │ ├── new-styles-3.scss │ │ │ │ │ ├── new-styles-4.scss │ │ │ │ │ ├── new-styles-5.scss │ │ │ │ │ ├── new-styles-6.scss │ │ │ │ │ ├── new-styles-7.scss │ │ │ │ │ ├── new-styles-8.scss │ │ │ │ │ ├── new-styles-9.scss │ │ │ │ │ ├── themes │ │ │ │ │ │ └── themes.scss │ │ │ │ │ └── utilities │ │ │ │ │ └── utilities.scss │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ └── styles.css │ │ │ ├── tsconfig.app.json │ │ │ ├── tsconfig.json │ │ │ └── tsconfig.spec.json │ │ └── design-system │ │ ├── component-options.mjs │ │ ├── storybook │ │ │ └── card │ │ │ └── card-tabs │ │ │ └── overview.mdx │ │ ├── storybook-host-app │ │ │ └── src │ │ │ └── components │ │ │ ├── badge │ │ │ │ ├── badge-tabs │ │ │ │ │ ├── api.mdx │ │ │ │ │ ├── examples.mdx │ │ │ │ │ └── overview.mdx │ │ │ │ ├── badge.component.mdx │ │ │ │ └── badge.component.stories.ts │ │ │ ├── modal │ │ │ │ ├── demo-cdk-dialog-cmp.component.ts │ │ │ │ ├── demo-modal-cmp.component.ts │ │ │ │ ├── modal-tabs │ │ │ │ │ ├── api.mdx │ │ │ │ │ ├── examples.mdx │ │ │ │ │ └── overview.mdx │ │ │ │ ├── modal.component.mdx │ │ │ │ └── modal.component.stories.ts │ │ │ └── segmented-control │ │ │ ├── segmented-control-tabs │ │ │ │ ├── api.mdx │ │ │ │ ├── examples.mdx │ │ │ │ └── overview.mdx │ │ │ ├── segmented-control.component.mdx │ │ │ └── segmented-control.component.stories.ts │ │ └── ui │ │ ├── badge │ │ │ ├── package.json │ │ │ ├── project.json │ │ │ └── src │ │ │ └── badge.component.ts │ │ ├── modal │ │ │ ├── package.json │ │ │ ├── project.json │ │ │ └── src │ │ │ ├── modal-content.component.ts │ │ │ ├── modal-header │ │ │ │ └── modal-header.component.ts │ │ │ ├── modal-header-drag │ │ │ │ └── modal-header-drag.component.ts │ │ │ └── modal.component.ts │ │ ├── rx-host-listener │ │ │ ├── package.json │ │ │ ├── project.json │ │ │ └── src │ │ │ └── rx-host-listener.ts │ │ └── segmented-control │ │ ├── package.json │ │ ├── project.json │ │ └── src │ │ ├── segmented-control.component.html │ │ ├── segmented-control.component.ts │ │ ├── segmented-control.token.ts │ │ └── segmented-option.component.ts │ └── shared │ ├── angular-ast-utils │ │ ├── .spec.swcrc │ │ ├── ai │ │ │ ├── API.md │ │ │ ├── EXAMPLES.md │ │ │ └── FUNCTIONS.md │ │ ├── docs │ │ │ └── angular-component-tree.md │ │ ├── eslint.config.mjs │ │ ├── jest.config.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── index.ts │ │ │ └── lib │ │ │ ├── constants.ts │ │ │ ├── decorator-config.visitor.inline-styles.spec.ts │ │ │ ├── decorator-config.visitor.spec.ts │ │ │ ├── decorator-config.visitor.ts │ │ │ ├── parse-component.ts │ │ │ ├── schema.ts │ │ │ ├── styles │ │ │ │ └── utils.ts │ │ │ ├── template │ │ │ │ ├── noop-tmpl-visitor.ts │ │ │ │ ├── template.walk.ts │ │ │ │ ├── utils.spec.ts │ │ │ │ ├── utils.ts │ │ │ │ └── utils.unit.test.ts │ │ │ ├── ts.walk.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ ├── tsconfig.spec.json │ │ └── vitest.config.mts │ ├── DEPENDENCIES.md │ ├── ds-component-coverage │ │ ├── .spec.swcrc │ │ ├── ai │ │ │ ├── API.md │ │ │ ├── EXAMPLES.md │ │ │ └── FUNCTIONS.md │ │ ├── docs │ │ │ ├── examples │ │ │ │ ├── report.json │ │ │ │ └── report.md │ │ │ ├── images │ │ │ │ └── report-overview.png │ │ │ └── README.md │ │ ├── jest.config.ts │ │ ├── mocks │ │ │ └── fixtures │ │ │ └── e2e │ │ │ ├── asset-location │ │ │ │ ├── code-pushup.config.ts │ │ │ │ ├── inl-styl-inl-tmpl │ │ │ │ │ └── inl-styl-inl-tmpl.component.ts │ │ │ │ ├── inl-styl-url-tmpl │ │ │ │ │ ├── inl-styl-url-tmpl.component.html │ │ │ │ │ └── inl-styl-url-tmpl.component.ts │ │ │ │ ├── multi-url-styl-inl-tmpl │ │ │ │ │ ├── multi-url-styl-inl-tmpl-1.component.css │ │ │ │ │ ├── multi-url-styl-inl-tmpl-2.component.css │ │ │ │ │ └── multi-url-styl-inl-tmpl.component.ts │ │ │ │ ├── url-styl-inl-tmpl │ │ │ │ │ ├── url-styl-inl-tmpl.component.css │ │ │ │ │ └── url-styl-inl-tmpl.component.ts │ │ │ │ ├── url-styl-single-inl-tmpl │ │ │ │ │ ├── url-styl-inl-tmpl.component.ts │ │ │ │ │ └── url-styl-single-inl-tmpl.component.css │ │ │ │ └── url-styl-url-tmpl │ │ │ │ ├── inl-styl-url-tmpl.component.css │ │ │ │ ├── inl-styl-url-tmpl.component.html │ │ │ │ └── inl-styl-url-tmpl.component.ts │ │ │ ├── demo │ │ │ │ ├── code-pushup.config.ts │ │ │ │ ├── prompt.md │ │ │ │ └── src │ │ │ │ ├── bad-button-dropdown.component.ts │ │ │ │ ├── bad-modal-progress.component.ts │ │ │ │ ├── mixed-external-assets.component.css │ │ │ │ ├── mixed-external-assets.component.html │ │ │ │ ├── mixed-external-assets.component.ts │ │ │ │ └── sub-folder-1 │ │ │ │ ├── bad-alert.component.ts │ │ │ │ ├── button.component.ts │ │ │ │ └── sub-folder-2 │ │ │ │ ├── bad-alert-tooltip-input.component.ts │ │ │ │ └── bad-mixed.component.ts │ │ │ ├── line-number │ │ │ │ ├── code-pushup.config.ts │ │ │ │ ├── inl-styl-single.component.ts │ │ │ │ ├── inl-styl-span.component.ts │ │ │ │ ├── inl-tmpl-single.component.ts │ │ │ │ ├── inl-tmpl-span.component.ts │ │ │ │ ├── url-style │ │ │ │ │ ├── url-styl-single.component.css │ │ │ │ │ ├── url-styl-single.component.ts │ │ │ │ │ ├── url-styl-span.component.css │ │ │ │ │ └── url-styl-span.component.ts │ │ │ │ └── url-tmpl │ │ │ │ ├── url-tmpl-single.component.html │ │ │ │ ├── url-tmpl-single.component.ts │ │ │ │ ├── url-tmpl-span.component.html │ │ │ │ └── url-tmpl-span.component.ts │ │ │ ├── style-format │ │ │ │ ├── code-pushup.config.ts │ │ │ │ ├── inl-css.component.ts │ │ │ │ ├── inl-scss.component.ts │ │ │ │ ├── styles.css │ │ │ │ ├── styles.scss │ │ │ │ ├── url-css.component.ts │ │ │ │ └── url-scss.component.ts │ │ │ └── template-syntax │ │ │ ├── class-attribute.component.ts │ │ │ ├── class-binding.component.ts │ │ │ ├── code-pushup.config.ts │ │ │ └── ng-class-binding.component.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── core.config.ts │ │ │ ├── index.ts │ │ │ └── lib │ │ │ ├── constants.ts │ │ │ ├── ds-component-coverage.plugin.ts │ │ │ ├── runner │ │ │ │ ├── audits │ │ │ │ │ └── ds-coverage │ │ │ │ │ ├── class-definition.utils.ts │ │ │ │ │ ├── class-definition.visitor.ts │ │ │ │ │ ├── class-definition.visitor.unit.test.ts │ │ │ │ │ ├── class-usage.utils.ts │ │ │ │ │ ├── class-usage.visitor.spec.ts │ │ │ │ │ ├── class-usage.visitor.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── ds-coverage.audit.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── create-runner.ts │ │ │ │ └── schema.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ ├── tsconfig.spec.json │ │ └── vitest.config.mts │ ├── LLMS.md │ ├── models │ │ ├── ai │ │ │ ├── API.md │ │ │ ├── EXAMPLES.md │ │ │ └── FUNCTIONS.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── index.ts │ │ │ └── lib │ │ │ ├── cli.ts │ │ │ ├── diagnostics.ts │ │ │ └── mcp.ts │ │ ├── tsconfig.json │ │ └── tsconfig.lib.json │ ├── styles-ast-utils │ │ ├── .spec.swcrc │ │ ├── ai │ │ │ ├── API.md │ │ │ ├── EXAMPLES.md │ │ │ └── FUNCTIONS.md │ │ ├── jest.config.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── index.ts │ │ │ └── lib │ │ │ ├── postcss-safe-parser.d.ts │ │ │ ├── styles-ast-utils.spec.ts │ │ │ ├── styles-ast-utils.ts │ │ │ ├── stylesheet.parse.ts │ │ │ ├── stylesheet.parse.unit.test.ts │ │ │ ├── stylesheet.visitor.ts │ │ │ ├── stylesheet.walk.ts │ │ │ ├── types.ts │ │ │ ├── utils.ts │ │ │ └── utils.unit.test.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ ├── tsconfig.spec.json │ │ └── vitest.config.mts │ ├── typescript-ast-utils │ │ ├── .spec.swcrc │ │ ├── ai │ │ │ ├── API.md │ │ │ ├── EXAMPLES.md │ │ │ └── FUNCTIONS.md │ │ ├── jest.config.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── index.ts │ │ │ └── lib │ │ │ ├── constants.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ ├── tsconfig.spec.json │ │ └── vitest.config.mts │ └── utils │ ├── .spec.swcrc │ ├── ai │ │ ├── API.md │ │ ├── EXAMPLES.md │ │ └── FUNCTIONS.md │ ├── package.json │ ├── README.md │ ├── src │ │ ├── index.ts │ │ └── lib │ │ ├── execute-process.ts │ │ ├── execute-process.unit.test.ts │ │ ├── file │ │ │ ├── default-export-loader.spec.ts │ │ │ ├── default-export-loader.ts │ │ │ ├── file.resolver.ts │ │ │ └── find-in-file.ts │ │ ├── format-command-log.integration.test.ts │ │ ├── format-command-log.ts │ │ ├── logging.ts │ │ └── utils.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ ├── vite.config.ts │ └── vitest.config.mts ├── README.md ├── testing │ ├── setup │ │ ├── eslint.config.mjs │ │ ├── eslint.next.config.mjs │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── index.d.ts │ │ │ ├── index.mjs │ │ │ └── memfs.constants.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ ├── tsconfig.spec.json │ │ ├── vitest.config.mts │ │ └── vitest.integration.config.mts │ ├── utils │ │ ├── eslint.config.mjs │ │ ├── eslint.next.config.mjs │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── index.ts │ │ │ └── lib │ │ │ ├── constants.ts │ │ │ ├── e2e-setup.ts │ │ │ ├── execute-process-helper.mock.ts │ │ │ ├── execute-process.mock.mjs │ │ │ ├── os-agnostic-paths.ts │ │ │ ├── os-agnostic-paths.unit.test.ts │ │ │ ├── source-file-from.code.ts │ │ │ └── string.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ ├── tsconfig.spec.json │ │ ├── vite.config.ts │ │ ├── vitest.config.mts │ │ └── vitest.integration.config.mts │ └── vitest-setup │ ├── eslint.config.mjs │ ├── eslint.next.config.mjs │ ├── package.json │ ├── README.md │ ├── src │ │ ├── index.ts │ │ └── lib │ │ ├── configuration.ts │ │ └── fs-memfs.setup-file.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ ├── vite.config.ts │ ├── vitest.config.mts │ └── vitest.integration.config.mts ├── tools │ ├── nx-advanced-profile.bin.js │ ├── nx-advanced-profile.js │ ├── nx-advanced-profile.postinstall.js │ └── perf_hooks.patch.js ├── tsconfig.base.json ├── tsconfig.json └── vitest.workspace.ts ``` # Files -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/complex-components/first-case/dashboard-demo.component.ts: -------------------------------------------------------------------------------- ```typescript import { Component, signal } from '@angular/core'; import { DashboardHeaderComponent, UserProfile, NotificationItem } from '../dashboard-header.component'; @Component({ selector: 'app-dashboard-demo', standalone: true, imports: [DashboardHeaderComponent], templateUrl: './dashboard-demo.component.html', styleUrls: ['./dashboard-demo.component.scss'] }) export class DashboardDemoComponent { darkMode = signal(false); eventLog = signal<string[]>([]); userProfile = signal<UserProfile>({ id: '1', name: 'John Doe', email: '[email protected]', role: 'Administrator', lastLogin: new Date(), avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face' }); private users: UserProfile[] = [ { id: '1', name: 'John Doe', email: '[email protected]', role: 'Administrator', lastLogin: new Date(), avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face' }, { id: '2', name: 'Jane Smith', email: '[email protected]', role: 'Manager', lastLogin: new Date(Date.now() - 1000 * 60 * 30), // 30 minutes ago }, { id: '3', name: 'Mike Johnson', email: '[email protected]', role: 'Developer', lastLogin: new Date(Date.now() - 1000 * 60 * 60 * 2), // 2 hours ago avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face' } ]; private currentUserIndex = 0; onSearchPerformed(query: string) { this.addLogEntry(`Search performed: "${query}"`); } onBadgeDismissed() { this.addLogEntry('Offer badge dismissed'); } onNotificationClicked(notification: NotificationItem) { this.addLogEntry(`Notification clicked: ${notification.title}`); } onUserActionClicked(action: string) { this.addLogEntry(`User action: ${action}`); if (action === 'logout') { this.addLogEntry('User logged out'); // In a real app, you would handle logout logic here } } onThemeToggled(isDark: boolean) { this.darkMode.set(isDark); this.addLogEntry(`Theme toggled to: ${isDark ? 'dark' : 'light'}`); // Apply theme to demo container const container = document.querySelector('.demo-container'); if (container) { container.classList.toggle('dark', isDark); } } toggleTheme() { const newTheme = !this.darkMode(); this.onThemeToggled(newTheme); } changeUser() { this.currentUserIndex = (this.currentUserIndex + 1) % this.users.length; const newUser = this.users[this.currentUserIndex]; this.userProfile.set(newUser); this.addLogEntry(`User changed to: ${newUser.name}`); } addNotification() { const notifications = [ { id: Date.now().toString(), title: 'New Message', message: 'You have received a new message from a colleague', timestamp: new Date(), read: false, type: 'info' as const }, { id: Date.now().toString(), title: 'Task Completed', message: 'Your background task has finished successfully', timestamp: new Date(), read: false, type: 'success' as const }, { id: Date.now().toString(), title: 'Warning', message: 'Your session will expire in 10 minutes', timestamp: new Date(), read: false, type: 'warning' as const } ]; const randomNotification = notifications[Math.floor(Math.random() * notifications.length)]; this.addLogEntry(`Added notification: ${randomNotification.title}`); } private addLogEntry(message: string) { const timestamp = new Date().toLocaleTimeString(); const entry = `[${timestamp}] ${message}`; this.eventLog.update(log => [entry, ...log.slice(0, 49)]); // Keep last 50 entries } } ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/spec/css-match.spec.ts: -------------------------------------------------------------------------------- ```typescript import { describe, it, expect, vi } from 'vitest'; vi.mock('@push-based/angular-ast-utils', () => { return { parseClassNames: (classStr: string) => classStr.trim().split(/\s+/), ngClassesIncludeClassName: (expr: string, className: string) => expr.includes(className), }; }); import { selectorMatches } from '../utils/css-match.js'; import type { DomElement, Attribute, Binding, } from '../../shared/models/types.js'; function createElement(overrides: Partial<DomElement> = {}): DomElement { return { tag: 'div', parent: null, children: [], attributes: [], bindings: [], events: [], ...overrides, } as DomElement; } function attr(name: string, source: string): Attribute { return { type: 'attribute', name, source }; } function classBinding(name: string, source = ''): Binding { return { type: 'class', name, source } as Binding; } describe('selectorMatches', () => { const domKey = ''; describe('class selectors', () => { it('matches static class attribute', () => { const el = createElement({ attributes: [attr('class', 'foo bar')], }); expect(selectorMatches('.foo', domKey, el)).toBe(true); expect(selectorMatches('.bar', domKey, el)).toBe(true); expect(selectorMatches('.baz', domKey, el)).toBe(false); }); it('matches Angular [class.foo] binding', () => { const el = createElement({ bindings: [classBinding('class.foo')], }); expect(selectorMatches('.foo', domKey, el)).toBe(true); }); it('matches ngClass expression', () => { const el = createElement({ bindings: [classBinding('ngClass', "{ 'foo': cond }")], }); expect(selectorMatches('.foo', domKey, el)).toBe(true); }); }); describe('id selectors', () => { it('matches id attribute', () => { const el = createElement({ attributes: [attr('id', 'my-id')] }); expect(selectorMatches('#my-id', domKey, el)).toBe(true); expect(selectorMatches('#other', domKey, el)).toBe(false); }); }); describe('tag selectors', () => { it('matches element tag', () => { const spanEl = createElement({ tag: 'span' }); expect(selectorMatches('span', domKey, spanEl)).toBe(true); expect(selectorMatches('div', domKey, spanEl)).toBe(false); }); }); describe('attribute selectors', () => { it('matches existence selector', () => { const el = createElement({ attributes: [attr('disabled', '')] }); expect(selectorMatches('[disabled]', domKey, el)).toBe(true); }); it('matches equality selector', () => { const el = createElement({ attributes: [attr('type', 'button')] }); expect(selectorMatches('[type="button"]', domKey, el)).toBe(true); expect(selectorMatches('[type="text"]', domKey, el)).toBe(false); }); it('matches *= selector', () => { const el = createElement({ attributes: [attr('data-role', 'dialog-box')], }); expect(selectorMatches('[data-role*="dialog"]', domKey, el)).toBe(true); expect(selectorMatches('[data-role*="modal"]', domKey, el)).toBe(false); }); it('matches ^= and $= selectors', () => { const el = createElement({ attributes: [attr('data-role', 'dialog-box')], }); expect(selectorMatches('[data-role^="dialog"]', domKey, el)).toBe(true); expect(selectorMatches('[data-role$="box"]', domKey, el)).toBe(true); }); }); describe('composite selectors', () => { it('matches when any comma-separated selector matches', () => { const el = createElement({ attributes: [attr('class', 'foo')] }); expect(selectorMatches('.foo, #bar', domKey, el)).toBe(true); expect(selectorMatches('.baz, #bar', domKey, el)).toBe(false); }); it('matches last part of descendant selector', () => { const el = createElement({ attributes: [attr('class', 'foo')] }); expect(selectorMatches('div .foo', domKey, el)).toBe(true); }); }); }); ``` -------------------------------------------------------------------------------- /packages/shared/utils/src/lib/file/default-export-loader.spec.ts: -------------------------------------------------------------------------------- ```typescript import { describe, expect, it, beforeEach, afterEach } from 'vitest'; import { writeFileSync, rmSync, mkdirSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { loadDefaultExport } from './default-export-loader.js'; describe('loadDefaultExport', () => { let testDir: string; beforeEach(() => { testDir = join( tmpdir(), `test-${Date.now()}-${Math.random().toString(36).slice(2)}`, ); mkdirSync(testDir, { recursive: true }); }); afterEach(() => { rmSync(testDir, { recursive: true, force: true }); }); const createFile = (name: string, content: string) => { const path = join(testDir, name); writeFileSync(path, content, 'utf-8'); return path; }; describe('Success Cases', () => { it.each([ { type: 'array', content: '[{name: "test"}]', expected: [{ name: 'test' }], }, { type: 'object', content: '{version: "1.0"}', expected: { version: '1.0' }, }, { type: 'string', content: '"test"', expected: 'test' }, { type: 'null', content: 'null', expected: null }, { type: 'boolean', content: 'false', expected: false }, { type: 'undefined', content: 'undefined', expected: undefined }, ])('should load $type default export', async ({ content, expected }) => { const path = createFile('test.mjs', `export default ${content};`); expect(await loadDefaultExport(path)).toEqual(expected); }); }); describe('Error Cases - No Default Export', () => { it.each([ { desc: 'named exports only', content: 'export const a = 1; export const b = 2;', exports: 'a, b', }, { desc: 'empty module', content: '', exports: 'none' }, { desc: 'comments only', content: '// comment', exports: 'none' }, { desc: 'function exports', content: 'export function fn() {}', exports: 'fn', }, ])('should throw error for $desc', async ({ content, exports }) => { const path = createFile('test.mjs', content); await expect(loadDefaultExport(path)).rejects.toThrow( `No default export found in module. Expected ES Module format:\nexport default [...]\n\nAvailable exports: ${exports}`, ); }); }); describe('Error Cases - File System', () => { it('should throw error when file does not exist', async () => { const path = join(testDir, 'missing.mjs'); await expect(loadDefaultExport(path)).rejects.toThrow( `Failed to load module from ${path}`, ); }); it('should throw error when file has syntax errors', async () => { const path = createFile( 'syntax.mjs', 'export default { invalid: syntax }', ); await expect(loadDefaultExport(path)).rejects.toThrow( `Failed to load module from ${path}`, ); }); }); describe('Edge Cases', () => { it('should work with TypeScript generics', async () => { interface Config { name: string; } const path = createFile('typed.mjs', 'export default [{name: "test"}];'); const result = await loadDefaultExport<Config[]>(path); expect(result).toEqual([{ name: 'test' }]); }); it('should handle mixed exports (prefers default)', async () => { const path = createFile( 'mixed.mjs', 'export const named = "n"; export default "d";', ); expect(await loadDefaultExport<string>(path)).toBe('d'); }); it('should handle complex nested structures', async () => { const path = createFile( 'complex.mjs', ` export default { data: [{ name: 'test', meta: { date: new Date('2024-01-01') } }], version: '1.0' }; `, ); const result = await loadDefaultExport(path); expect(result).toMatchObject({ data: [{ name: 'test', meta: { date: expect.any(Date) } }], version: '1.0', }); }); }); }); ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/ds.tools.ts: -------------------------------------------------------------------------------- ```typescript import { createHandler, BaseHandlerOptions, } from './shared/utils/handler-helpers.js'; import { ToolSchemaOptions } from '@push-based/models'; import { dsComponentCoveragePlugin } from '@push-based/ds-component-coverage'; import { baseToolsSchema } from '../schema.js'; import { join } from 'node:path'; import { reportViolationsTools, reportAllViolationsTools, } from './report-violations/index.js'; export const componentCoverageToolsSchema: ToolSchemaOptions = { name: 'ds_component-coverage', description: 'Migration report. Search for deprecated CSS classes in a component. List available options with the tool `ds_list-options', inputSchema: { type: 'object', properties: { ...baseToolsSchema.inputSchema.properties, directory: { type: 'string', description: 'The relative path the directory (starting with "./path/to/dir" avoid big folders.) to run the task in starting from CWD. Respect the OS specifics.', }, dsComponents: { type: 'array', items: { type: 'object', required: ['componentName', 'deprecatedCssClasses'], properties: { componentName: { type: 'string', description: 'The class name of the component to search for', }, deprecatedCssClasses: { type: 'array', items: { type: 'string', }, description: 'List of deprecated CSS classes for this component', }, docsUrl: { type: 'string', description: 'URL to the component documentation', }, }, }, description: 'Array of components and their deprecated CSS classes', }, }, required: [ ...(baseToolsSchema.inputSchema.required as string[]), 'directory', 'dsComponents', ], }, annotations: { title: 'Design System Component Coverage', readOnlyHint: true, openWorldHint: true, idempotentHint: false, }, }; interface ComponentCoverageOptions extends BaseHandlerOptions { directory: string; dsComponents: Array<{ componentName: string; deprecatedCssClasses: string[]; docsUrl?: string; }>; } export const componentCoverageHandler = createHandler< ComponentCoverageOptions, any >( componentCoverageToolsSchema.name, async (params, { cwd }) => { const { directory, dsComponents, ...pluginOptions } = params; const pluginConfig = await dsComponentCoveragePlugin({ ...pluginOptions, directory: join(cwd, directory), dsComponents, }); const { executePlugin } = await import('@code-pushup/core'); const result = await executePlugin(pluginConfig as any, { cache: { read: false, write: false }, persist: { outputDir: '' }, }); const reducedResult = { ...result, audits: result.audits.filter(({ score }) => score < 1), }; return { directory, reducedResult, }; }, (result) => { const output = [`List of missing DS components:`]; output.push(`Result:\n\nBase directory: ${result.directory}`); result.reducedResult.audits.forEach(({ details, title }: any) => { const auditOutput = [`\n${title}`]; (details?.issues ?? []).forEach( ({ message, source }: any, index: number) => { if (index === 0) { auditOutput.push(message.replace(result.directory + '/', '')); } auditOutput.push( ` - ${source?.file.replace(result.directory + '/', '')}#L${source?.position?.startLine}`, ); }, ); output.push(auditOutput.join('\n')); }); return [output.join('\n')]; }, ); export const componentCoverageTools = [ { schema: componentCoverageToolsSchema, handler: componentCoverageHandler, }, ]; export const dsTools = [ ...componentCoverageTools, ...reportViolationsTools, ...reportAllViolationsTools, ]; ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/complex-components/third-case/product-card.component.scss: -------------------------------------------------------------------------------- ```scss // Product Card Component Styles .product-card { background: white; border-radius: 0.75rem; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); overflow: hidden; transition: all 0.3s ease; position: relative; &:hover { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); } &.product-card-selected { border: 2px solid #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); } } // Product Image Section .product-image-container { position: relative; width: 100%; height: 200px; overflow: hidden; } .product-image { width: 100%; height: 100%; object-fit: cover; transition: transform 0.3s ease; .product-card:hover & { transform: scale(1.05); } } // Badge Overlay - Moderate complexity offer-badge .badge-overlay { position: absolute; top: 0.75rem; left: 0.75rem; z-index: 2; } // DsBadge replaces the custom offer-badge implementation // The badge styling is now handled by the design system component // Stock Overlay .stock-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.6); display: flex; align-items: center; justify-content: center; z-index: 3; } .stock-badge { background: #ef4444; color: white; padding: 0.5rem 1rem; border-radius: 0.375rem; font-weight: 600; text-transform: uppercase; font-size: 0.875rem; } // Product Content .product-content { padding: 1rem; } .product-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem; } .product-name { margin: 0; font-size: 1.125rem; font-weight: 600; color: #1f2937; line-height: 1.4; flex: 1; margin-right: 0.5rem; } .favorite-button { background: none; border: none; cursor: pointer; padding: 0.25rem; border-radius: 0.25rem; transition: all 0.2s ease; color: #6b7280; &:hover { background: #f3f4f6; color: #ef4444; } &.favorite-active { color: #ef4444; } } .product-category { font-size: 0.875rem; color: #6b7280; margin-bottom: 0.75rem; text-transform: capitalize; } .product-pricing { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem; } .original-price { font-size: 0.875rem; color: #6b7280; text-decoration: line-through; } .current-price { font-size: 1.125rem; font-weight: 700; color: #ef4444; } .product-rating { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem; } .rating-stars { display: flex; gap: 0.125rem; } .star { color: #d1d5db; font-size: 1rem; &.star-filled { color: #f59e0b; } } .rating-text { font-size: 0.875rem; color: #6b7280; } .product-tags { display: flex; flex-wrap: wrap; gap: 0.375rem; margin-bottom: 1rem; } .product-tag { background: #f3f4f6; color: #374151; padding: 0.25rem 0.5rem; border-radius: 0.25rem; font-size: 0.75rem; font-weight: 500; } .tag-more { color: #6b7280; font-size: 0.75rem; font-style: italic; } // Product Actions .product-actions { padding: 0 1rem 1rem; display: flex; gap: 0.5rem; } .action-button { flex: 1; padding: 0.75rem; border: none; border-radius: 0.375rem; font-weight: 600; font-size: 0.875rem; cursor: pointer; transition: all 0.2s ease; &.add-to-cart { background: #3b82f6; color: white; &:hover:not(:disabled) { background: #2563eb; } &:disabled { background: #9ca3af; cursor: not-allowed; } } &.quick-view { background: #f3f4f6; color: #374151; border: 1px solid #d1d5db; &:hover { background: #e5e7eb; border-color: #9ca3af; } } } // Custom badge animations removed - DsBadge handles its own styling // Responsive design @media (max-width: 768px) { .product-card { margin: 0.5rem; } // DsBadge responsive styles are handled by the design system .product-name { font-size: 1rem; } .product-actions { flex-direction: column; .action-button { flex: none; } } } ``` -------------------------------------------------------------------------------- /packages/shared/angular-ast-utils/ai/FUNCTIONS.md: -------------------------------------------------------------------------------- ```markdown # Public API — Quick Reference | Symbol | Kind | Signature | Summary | | ----------------------------------- | -------- | ----------------------------------------------------------------------------- | ------------------------------------------------------ | | `ANGULAR_COMPONENT_DECORATOR` | function | `const ANGULAR_COMP...` | Constant for Angular component decorator string | | `AngularUnit` | function | `type AngularUnit = z.infer<…>` | Union type for Angular unit types | | `AngularUnitSchema` | function | `const AngularUnitSchema = z.enum…` | Zod schema for Angular unit types | | `Asset<T>` | function | `interface Asset<T>` | Typed asset wrapper | | `assetFromPropertyArrayInitializer` | function | `assetFromPropertyArrayInitializer(prop, sourceFile, textParser): Asset<T>[]` | Create assets from array property initializers | | `assetFromPropertyValueInitializer` | function | `assetFromPropertyValueInitializer(opts): Asset<T>` | Create asset from property value initializer | | `classDecoratorVisitor` | function | `classDecoratorVisitor(opts): Visitor` | Iterate class decorators in a SourceFile | | `findAngularUnits` | function | `findAngularUnits(directory, unit): Promise<string[]>` | Find Angular unit files by type in directory | | `ngClassesIncludeClassName` | function | `ngClassesIncludeClassName(src, name): boolean` | Check if class name exists inside `[ngClass]` bindings | | `parseAngularUnit` | function | `parseAngularUnit(directory, unit): Promise<ParsedComponent[]>` | Parse Angular units in a directory | | `parseClassNames` | function | `parseClassNames(str): string[]` | Split string into individual CSS class names | | `parseComponents` | function | `parseComponents(files): Promise<ParsedComponent[]>` | Parse TS/TSX components into AST descriptors | | `ParsedComponent` | function | `type ParsedComponent` | Type for parsed Angular component data | | `SourceLink` | function | `type SourceLink` | Type for source file location reference | | `tmplAstElementToSource` | function | `tmplAstElementToSource(el): Source` | Convert template AST elements to source map location | | `visitAngularDecoratorProperties` | function | `visitAngularDecoratorProperties(opts)` | Visit properties of Angular decorators | | `visitAngularDecorators` | function | `visitAngularDecorators(opts)` | Traverse decorators & return matches | | `visitComponentStyles` | function | `visitComponentStyles(comp, arg, cb): Promise<Issue[]>` | Visit component styles with callback | | `visitComponentTemplate` | function | `visitComponentTemplate(comp, arg, cb)` | Run visitor against a component's template | | `visitEachTmplChild` | function | `visitEachTmplChild(nodes, visitor)` | Visit each template AST child node | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-usage-graph/utils/component-helpers.ts: -------------------------------------------------------------------------------- ```typescript import * as path from 'path'; import { toUnixPath } from '@code-pushup/utils'; import { buildText } from '../../shared/utils/output.utils.js'; import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import { DependencyGraphResult, DependencyInfo, ComponentGroup, FileExtension, } from '../models/types.js'; import { DEPENDENCY_ANALYSIS_CONFIG } from '../models/config.js'; // Consolidated utilities const NAME_RE = /^(.*?)(?:\.(?:component|directive|pipe|service|module|spec))?\.(?:ts|js|html?|s?css|less)$/i; const baseName = (filePath: string): string => { const fileName = path.basename(filePath); const match = fileName.match(NAME_RE); return match ? match[1] : fileName; }; const rel = (root: string, p: string) => toUnixPath(path.isAbsolute(p) ? path.relative(root, p) : p); type Index = Map<string, string[]>; // baseName → related files const buildIndex = (graph: DependencyGraphResult): Index => { const index: Index = new Map(); for (const fp of Object.keys(graph)) { const bn = baseName(fp); (index.get(bn) ?? index.set(bn, []).get(bn)!).push(fp); } return index; }; const expandViolations = (seeds: string[], index: Index): string[] => [ ...new Set(seeds.flatMap((s) => index.get(baseName(s)) ?? [])), ]; export const assetToComponentTs = (p: string): string => path.join(path.dirname(p), baseName(p) + '.component.ts'); export const filterGraph = ( graph: DependencyGraphResult, violationFiles: string[], root: string, index: Index = buildIndex(graph), ): DependencyGraphResult => { const seeds = violationFiles.flatMap((f) => /\.(html?|s?css|sass|less)$/i.test(f) ? [f, assetToComponentTs(f)] : [f], ); const bad = new Set(expandViolations(seeds, index).map((f) => rel(root, f))); const badNames = new Set([...bad].map(baseName)); return Object.fromEntries( Object.entries(graph).filter( ([fp, info]) => bad.has(fp) || badNames.has(baseName(fp)) || info.dependencies.some( (d) => d.type === 'reverse-dependency' && bad.has(d.path), ), ), ); }; const buildGroups = ( result: DependencyGraphResult, ): Map<string, ComponentGroup> => { const componentGroups = new Map<string, ComponentGroup>(); for (const [filePath, fileInfo] of Object.entries(result)) { const bn = baseName(filePath); if (!componentGroups.has(bn)) { componentGroups.set(bn, { relatedFiles: [], hasReverseDeps: false, }); } const group = componentGroups.get(bn); if (group) { if (fileInfo.isAngularComponent) { group.componentFile = [filePath, fileInfo]; group.hasReverseDeps = fileInfo.dependencies.some( (dep: DependencyInfo) => dep.type === 'reverse-dependency', ); } else { group.relatedFiles.push([filePath, fileInfo]); } } } return componentGroups; }; const getFileType = (filePath: string): string => { const extension = path.extname(filePath).toLowerCase() as FileExtension; return DEPENDENCY_ANALYSIS_CONFIG.fileTypeMap[extension] || 'file'; }; type Mode = 'inline' | 'entity'; export const printComponents = ( graph: DependencyGraphResult, mode: Mode = 'inline', ): string | CallToolResult['content'] => { const groups = buildGroups(graph); const comps = [...groups.values()].filter((g) => g.componentFile); if (!comps.length) return mode === 'inline' ? 'No Angular components found with violations.' : [buildText('No Angular components found with violations.')]; const toLines = (g: ComponentGroup) => { if (!g.componentFile) return ''; const [cp, ci] = g.componentFile; return [ `Component: ${ci.componentName ?? 'Unknown'}`, '', `- ${ci.type}: ${cp}`, ...g.relatedFiles.map(([p, i]) => `- ${i.type}: ${p}`), ...ci.dependencies .filter((d) => d.type === 'reverse-dependency') .map( (d) => `- ${getFileType(d.resolvedPath ?? d.path)}: ${d.resolvedPath ?? d.path}`, ), ].join('\n'); }; if (mode === 'inline') { return comps.map(toLines).join('\n\n'); } return comps.map((g) => buildText(toLines(g))); }; ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/design-system/storybook-host-app/src/components/modal/demo-modal-cmp.component.ts: -------------------------------------------------------------------------------- ```typescript import { ChangeDetectionStrategy, Component, booleanAttribute, inject, input, } from '@angular/core'; import { MAT_BOTTOM_SHEET_DATA, MatBottomSheet, MatBottomSheetModule, MatBottomSheetRef, } from '@angular/material/bottom-sheet'; import { MAT_DIALOG_DATA, MatDialog, MatDialogModule, MatDialogRef, } from '@angular/material/dialog'; import { DemoCloseIconComponent } from '@design-system/storybook-demo-cmp-lib'; import { DsButton } from '@frontend/ui/button'; import { DsButtonIcon } from '@frontend/ui/button-icon'; import { DsModal, DsModalContent, DsModalHeader, DsModalHeaderVariant, DsModalVariant, } from '@frontend/ui/modal'; @Component({ selector: 'ds-demo-dialog-cmp', imports: [ MatDialogModule, DsButton, DsModalHeader, DsButtonIcon, DemoCloseIconComponent, DsModal, DsModalContent, ], standalone: true, template: ` <ds-modal [inverse]="data.inverse" [bottomSheet]="data.bottomSheet" [variant]="data.variant" > <ds-modal-header [variant]="data.headerVariant"> <div slot="start"> <div slot="title">Hello start</div> <div slot="subtitle">Header subtitle</div> </div> <button slot="end" ds-button-icon size="small" (click)="close()"> Close </button> </ds-modal-header> <ds-modal-content> Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquam, ducimus, sequi! Ab consequatur earum expedita fugit illo illum in maiores nihil nostrum officiis ratione repellendus temporibus, vel! <br /> <br /> <b>Lorem ipsum dolor sit amet</b>, consectetur adipisicing elit. Aliquam, ducimus, sequi! Ab consequatur earum expedita fugit illo illum in maiores nihil nostrum officiis ratione repellendus temporibus, vel! <br /> <br /> <div class="footer-buttons"> <button ds-button [inverse]="data.inverse" kind="secondary" variant="outline" mat-dialog-close > Outline Button </button> <button ds-button [inverse]="data.inverse" kind="primary" variant="filled" mat-dialog-close > Filled Button </button> </div> </ds-modal-content> </ds-modal> `, styles: ` ds-modal { width: 400px; min-height: 300px; margin-left: auto; margin-right: auto; } .footer-buttons { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } `, changeDetection: ChangeDetectionStrategy.OnPush, }) export class DemoModalCmp { dialogRef = inject(MatDialogRef<DemoModalCmp>, { optional: true }); bottomSheetRef = inject(MatBottomSheetRef<DemoModalCmp>, { optional: true }); dialogData = inject(MAT_DIALOG_DATA, { optional: true }); bottomSheetData = inject(MAT_BOTTOM_SHEET_DATA, { optional: true }); data = this.dialogData ?? this.bottomSheetData ?? {}; // fallback to empty {} close() { this.dialogRef?.close(); this.bottomSheetRef?.dismiss(); } } @Component({ selector: 'ds-demo-dialog-container', standalone: true, imports: [MatDialogModule, MatBottomSheetModule, DsButton], template: ` <button ds-button (click)="openDialog()">Open with Material Dialog</button> `, changeDetection: ChangeDetectionStrategy.OnPush, }) export class DemoModalContainer { dialog = inject(MatDialog); bottomSheet = inject(MatBottomSheet); headerVariant = input<DsModalHeaderVariant>(); variant = input<DsModalVariant>(); inverse = input(false, { transform: booleanAttribute }); bottomSheetInput = input(false, { transform: booleanAttribute }); openDialog() { const data = { headerVariant: this.headerVariant(), inverse: this.inverse(), variant: this.variant(), bottomSheet: this.bottomSheetInput(), }; if (data.bottomSheet) { this.bottomSheet.open(DemoModalCmp, { data, panelClass: 'ds-bottom-sheet-panel', }); } else { this.dialog.open(DemoModalCmp, { data, panelClass: 'ds-dialog-panel', }); } } } ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/coverage-analyzer.ts: -------------------------------------------------------------------------------- ```typescript import { dsComponentCoveragePlugin } from '@push-based/ds-component-coverage'; import * as process from 'node:process'; import { validateDsComponentsArray } from '../../../../validation/ds-components-file-loader.validation.js'; import { ReportCoverageParams, BaseViolationResult, FormattedCoverageResult, BaseViolationAudit, } from './types.js'; import { groupIssuesByFile } from './formatters.js'; import { resolveCrossPlatformPath } from '../utils/cross-platform-path.js'; /** * Validates the input parameters for the report coverage tool */ export function validateReportInput(params: ReportCoverageParams): void { if (!params.directory || typeof params.directory !== 'string') { throw new Error('Directory parameter is required and must be a string'); } try { validateDsComponentsArray(params.dsComponents); } catch (ctx) { throw new Error( `Invalid dsComponents parameter: ${(ctx as Error).message}`, ); } } /** * Executes the coverage plugin and returns the result */ export async function executeCoveragePlugin( params: ReportCoverageParams, ): Promise<BaseViolationResult> { const pluginConfig = await dsComponentCoveragePlugin({ dsComponents: params.dsComponents, directory: resolveCrossPlatformPath( params.cwd || process.cwd(), params.directory, ), }); const { executePlugin } = await import('@code-pushup/core'); return (await executePlugin(pluginConfig as any, { cache: { read: false, write: false }, persist: { outputDir: '' }, })) as BaseViolationResult; } /** * Extracts component name from audit title - performance optimized with caching */ const componentNameCache = new Map<string, string>(); export function extractComponentName(title: string): string { if (componentNameCache.has(title)) { return componentNameCache.get(title)!; } const componentMatch = title.match(/Usage coverage for (\w+) component/); const componentName = componentMatch ? componentMatch[1] : 'Unknown'; componentNameCache.set(title, componentName); return componentName; } /** * Formats the coverage result as text output - performance optimized */ export function formatCoverageResult( result: BaseViolationResult, params: ReportCoverageParams, ): string { // Pre-filter failed audits to avoid repeated filtering const failedAudits = result.audits.filter( ({ score }: BaseViolationAudit) => score < 1, ); if (failedAudits.length === 0) { return ''; } const output: string[] = []; output.push(''); // Pre-allocate with expected size for better performance for (const { details, title } of failedAudits) { const componentName = extractComponentName(title); output.push(`- design system component: ${componentName}`); output.push(`- base directory: ${params.directory}`); output.push(''); // Use shared, optimized groupIssuesByFile function const fileGroups = groupIssuesByFile( details?.issues ?? [], params.directory, ); // Add first message const firstFile = Object.keys(fileGroups)[0]; if (firstFile && fileGroups[firstFile]) { output.push(fileGroups[firstFile].message); output.push(''); } // Add files and lines - optimize sorting for (const [fileName, { lines }] of Object.entries(fileGroups)) { output.push(`- ${fileName}`); // Sort once and cache the result const sortedLines = lines.length > 1 ? lines.sort((a, b) => a - b) : lines; output.push(` - lines: ${sortedLines.join(',')}`); } output.push(''); } return output.join('\n'); } /** * Main implementation function for reporting project coverage */ export async function analyzeProjectCoverage( params: ReportCoverageParams, ): Promise<FormattedCoverageResult> { validateReportInput(params); if (params.cwd) { process.chdir(params.cwd); } const result = await executeCoveragePlugin(params); const textOutput = formatCoverageResult(result, params); const formattedResult: FormattedCoverageResult = { textOutput, }; if (params.returnRawData) { formattedResult.rawData = { rawPluginResult: result, pluginOptions: { directory: params.directory, dsComponents: params.dsComponents, }, }; } return formattedResult; } ``` -------------------------------------------------------------------------------- /tools/perf_hooks.patch.js: -------------------------------------------------------------------------------- ```javascript import { Performance, performance } from 'node:perf_hooks'; import { basename } from 'node:path'; // Global array to store complete events. const trace = []; // Metadata events. const processMetadata = { name: 'process_name', // Used to label the main process ph: 'M', pid: 0, tid: process.pid, ts: 0, args: { name: 'Measure Process' }, }; const threadMetadata = { name: 'thread_name', // Used to label the child processes ph: 'M', pid: 0, tid: process.pid, ts: 0, args: { name: `Child Process: ${basename(process.argv.at(0))} ${basename( process.argv.at(1), )} ${process.argv.slice(2).join(' ')}`, }, }; const originalMark = Performance.prototype.mark; const originalMeasure = Performance.prototype.measure; let correlationIdCounter = 0; function generateCorrelationId() { return ++correlationIdCounter; } /** * Parse an error stack into an array of frames. */ function parseStack(stack) { const frames = []; const lines = stack.split('\n').slice(2); // Skip error message & current function. for (const line of lines) { const trimmed = line.trim(); const regex1 = /^at\s+(.*?)\s+\((.*):(\d+):(\d+)\)$/; const regex2 = /^at\s+(.*):(\d+):(\d+)$/; let match = trimmed.match(regex1); if (match) { frames.push({ functionName: match[1], file: match[2].replace(process.cwd(), ''), line: Number(match[3]), column: Number(match[4]), }); } else { match = trimmed.match(regex2); if (match) { frames.push({ functionName: null, file: match[1], line: Number(match[2]), column: Number(match[3]), }); } else { frames.push({ raw: trimmed }); } } } return frames; } Performance.prototype.mark = function (name, options) { const err = new Error(); const callStack = parseStack(err.stack); const opt = Object.assign({}, options, { detail: Object.assign({}, (options && options.detail) || {}, { callStack }), }); return originalMark.call(this, name, opt); }; // Override measure to create complete events. Performance.prototype.measure = function (name, start, end, options) { const startEntry = performance.getEntriesByName(start, 'mark')[0]; const endEntry = performance.getEntriesByName(end, 'mark')[0]; let event = null; if (startEntry && endEntry) { const ts = startEntry.startTime * 1000; // Convert ms to microseconds. const dur = (endEntry.startTime - startEntry.startTime) * 1000; // Enrich event further if needed (here keeping it minimal to match your profile). event = { name, cat: 'measure', // Keeping the same category as in your uploaded trace. ph: 'X', ts, dur, pid: 0, tid: process.pid, args: { startDetail: startEntry.detail || {}, endDetail: endEntry.detail || {}, // Optionally: add correlation and extra labels. uiLabel: name, }, }; // Push metadata events once. if (trace.length < 1) { trace.push(threadMetadata); console.log(`traceEvent:JSON:${JSON.stringify(threadMetadata)}`); trace.push(processMetadata); console.log(`traceEvent:JSON:${JSON.stringify(processMetadata)}`); } trace.push(event); console.log(`traceEvent:JSON:${JSON.stringify(event)}`); // console.log('Measure Event:', JSON.stringify(event)); } else { console.warn('Missing start or end mark for measure', name); } return originalMeasure.call(this, name, start, end, options); }; // Return the complete Chrome Trace profile object. performance.profile = function () { return { metadata: { source: 'Nx Advanced Profiling', startTime: Date.now() / 1000, hardwareConcurrency: 12, dataOrigin: 'TraceEvents', modifications: { entriesModifications: { hiddenEntries: [], expandableEntries: [], }, initialBreadcrumb: { window: { min: 269106047711, max: 269107913714, range: 1866003, }, child: null, }, annotations: { entryLabels: [], labelledTimeRanges: [], linksBetweenEntries: [], }, }, }, traceEvents: trace, }; }; performance.trace = trace; ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/utils/css-match.ts: -------------------------------------------------------------------------------- ```typescript import { parseClassNames, ngClassesIncludeClassName, } from '@push-based/angular-ast-utils'; import type { DomElement, Attribute, Binding, } from '../../shared/models/types.js'; /** * Check if a CSS selector matches a DOM element with improved accuracy */ export function selectorMatches( cssSelector: string, domKey: string, element: DomElement, ): boolean { const normalizedSelector = cssSelector.trim(); // Handle multiple selectors separated by commas if (normalizedSelector.includes(',')) { return normalizedSelector .split(',') .some((selector) => selectorMatches(selector, domKey, element)); } // Handle descendant selectors (space-separated) if (normalizedSelector.includes(' ')) { const parts = normalizedSelector.split(/\s+/); const lastPart = parts[parts.length - 1]; // Check if the element matches the last part - simplified check as full implementation // would need to traverse the DOM tree to check ancestors return matchSelector(lastPart, element); } // Handle single selector return matchSelector(normalizedSelector, element); } /** * Match a single CSS selector (class, id, tag, or attribute) */ function matchSelector(selector: string, element: DomElement): boolean { // Class selector (.class-name) if (selector.startsWith('.')) { return matchAttribute('class', selector.substring(1), '=', element); } // ID selector (#id-name) if (selector.startsWith('#')) { return matchAttribute('id', selector.substring(1), '=', element); } // Attribute selector ([attr], [attr=value], [attr*=value], etc.) if (selector.startsWith('[') && selector.endsWith(']')) { const content = selector.slice(1, -1); // Simple attribute existence check [attr] if (!content.includes('=')) { return ( element.attributes?.some((attr: Attribute) => attr.name === content) || false ); } // Parse attribute selector with value using non-greedy split before the operator // This correctly captures operators like *=, ^=, $= const match = content.match(/^(.+?)([*^$]?=)(.+)$/); if (!match) return false; const [, attrNameRaw, operator, value] = match; const attrName = attrNameRaw.trim(); return matchAttribute(attrName, stripQuotes(value), operator, element); } // Tag selector (div, span, etc.) return element.tag === selector; } /** * Unified attribute matching with support for classes, IDs, and general attributes */ function matchAttribute( attrName: string, expectedValue: string, operator: string, element: DomElement, ): boolean { // Special handling for class attributes if (attrName === 'class') { // Check static class attribute const classAttr = element.attributes?.find( (attr: Attribute) => attr.name === 'class', ); if (classAttr) { const classes = parseClassNames(classAttr.source); if (classes.includes(expectedValue)) { return true; } } // Check class bindings [class.foo] const classBindings = element.bindings?.filter( (binding: Binding) => binding.type === 'class' && binding.name === `class.${expectedValue}`, ); // Check ngClass bindings const ngClassBindings = element.bindings?.filter( (binding: Binding) => binding.name === 'ngClass', ); for (const binding of ngClassBindings || []) { if (ngClassesIncludeClassName(binding.source, expectedValue)) { return true; } } return classBindings && classBindings.length > 0; } // General attribute matching const attr = element.attributes?.find( (attr: Attribute) => attr.name === attrName, ); if (!attr) return false; const attrValue = attr.source; return OPERATORS[operator]?.(attrValue, expectedValue) || false; } // Operator lookup table const OPERATORS: Record< string, (attrValue: string, expectedValue: string) => boolean > = { '=': (attrValue, expectedValue) => attrValue === expectedValue, '*=': (attrValue, expectedValue) => attrValue.includes(expectedValue), '^=': (attrValue, expectedValue) => attrValue.startsWith(expectedValue), '$=': (attrValue, expectedValue) => attrValue.endsWith(expectedValue), }; /** * Remove surrounding quotes from a string */ function stripQuotes(str: string): string { return str.startsWith('"') || str.startsWith("'") ? str.slice(1, -1) : str; } ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/shared/spec/contract-file-ops.spec.ts: -------------------------------------------------------------------------------- ```typescript /* eslint-disable prefer-const */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; let readFileMock: any; let mkdirMock: any; let writeFileMock: any; let existsSyncMock: any; let resolveCrossPlatformPathMock: any; vi.mock('node:fs/promises', () => ({ get readFile() { return readFileMock; }, get mkdir() { return mkdirMock; }, get writeFile() { return writeFileMock; }, })); vi.mock('node:fs', () => ({ get existsSync() { return existsSyncMock; }, })); vi.mock('node:crypto', () => ({ createHash: () => ({ update: () => ({ digest: () => 'deadbeefdeadbeef', }), }), })); vi.mock('../../../shared/utils/cross-platform-path.js', () => ({ get resolveCrossPlatformPath() { return resolveCrossPlatformPathMock; }, })); import { loadContract, saveContract, generateContractSummary, generateDiffFileName, } from '../utils/contract-file-ops.js'; import type { ComponentContract } from '../models/types.js'; function fixedDate(dateStr = '2024-02-10T12:00:00Z') { vi.useFakeTimers(); vi.setSystemTime(new Date(dateStr)); } function restoreTime() { vi.useRealTimers(); } const minimalContract: ComponentContract = { meta: { name: 'FooComponent', selector: 'app-foo', sourceFile: '/src/app/foo.component.ts', templateType: 'external', generatedAt: new Date().toISOString(), hash: 'hash', }, publicApi: { properties: { foo: { type: 'string', isInput: true, required: true } }, events: { done: { type: 'void' } }, methods: { do: { name: 'do', parameters: [], returnType: 'void', isPublic: true, isStatic: false, isAsync: false, }, }, lifecycle: ['ngOnInit'], imports: [], }, slots: { default: { selector: 'ng-content' } }, dom: { div: { tag: 'div', parent: null, children: [], bindings: [], attributes: [], events: [], }, }, styles: { sourceFile: '/src/app/foo.component.scss', rules: { div: { appliesTo: ['div'], properties: { color: 'red' } } }, }, }; describe('contract-file-ops', () => { beforeEach(() => { readFileMock = vi.fn(); mkdirMock = vi.fn(); writeFileMock = vi.fn(); existsSyncMock = vi.fn(); resolveCrossPlatformPathMock = vi.fn( (_root: string, p: string) => `${_root}/${p}`, ); }); afterEach(() => { restoreTime(); }); describe('loadContract', () => { it('loads wrapped contract files', async () => { const filePath = '/tmp/contract.json'; existsSyncMock.mockReturnValue(true); readFileMock.mockResolvedValue( JSON.stringify({ contract: minimalContract }), ); const contract = await loadContract(filePath); expect(readFileMock).toHaveBeenCalledWith(filePath, 'utf-8'); expect(contract).toEqual(minimalContract); }); it('throws when file is missing', async () => { existsSyncMock.mockReturnValue(false); await expect(loadContract('/missing.json')).rejects.toThrow( 'Contract file not found', ); }); }); describe('saveContract', () => { it('writes contract with metadata and returns path & hash', async () => { fixedDate(); const workspaceRoot = '/workspace'; const templatePath = 'src/app/foo.component.html'; const scssPath = 'src/app/foo.component.scss'; const cwd = '/cwd'; writeFileMock.mockResolvedValue(undefined); mkdirMock.mockResolvedValue(undefined); const { contractFilePath, hash } = await saveContract( minimalContract, workspaceRoot, templatePath, scssPath, cwd, ); // mkdir called for .cursor/tmp directory expect(mkdirMock).toHaveBeenCalled(); expect(contractFilePath).toMatch(/foo\.component.*\.contract\.json$/i); expect(hash.startsWith('sha256-')).toBe(true); expect(writeFileMock).toHaveBeenCalled(); }); }); describe('generateContractSummary', () => { it('generates human-readable summary lines', () => { const lines = generateContractSummary(minimalContract); expect(lines).toEqual( expect.arrayContaining([ expect.stringMatching(/^🎯 DOM Elements: 1/), expect.stringMatching(/^🎨 Style Rules: 1/), expect.stringMatching(/^📥 Properties: 1/), expect.stringMatching(/^📤 Events: 1/), ]), ); }); }); describe('generateDiffFileName', () => { it('creates timestamped diff filename', () => { fixedDate(); const before = '/contracts/foo.contract.json'; const after = '/contracts/bar.contract.json'; const fname = generateDiffFileName(before, after); expect(fname).toMatch(/^diff-foo-vs-bar-.*\.json$/); }); }); }); ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/spec/public-api.extractor.spec.ts: -------------------------------------------------------------------------------- ```typescript /* eslint-disable prefer-const */ import { describe, it, expect, beforeEach, vi } from 'vitest'; let createProgramMock: any; vi.mock('typescript', () => { return { get createProgram() { return createProgramMock; }, ScriptTarget: { Latest: 99 }, ModuleKind: { ESNext: 99 }, }; }); createProgramMock = vi.fn(); let extractClassDeclaration: any; let extractPublicMethods: any; let extractLifecycleHooks: any; let extractImports: any; let extractInputsAndOutputs: any; extractClassDeclaration = vi.fn(); extractPublicMethods = vi.fn(); extractLifecycleHooks = vi.fn(); extractImports = vi.fn(); extractInputsAndOutputs = vi.fn(); vi.mock('../utils/typescript-analyzer.js', () => ({ get extractClassDeclaration() { return extractClassDeclaration; }, get extractPublicMethods() { return extractPublicMethods; }, get extractLifecycleHooks() { return extractLifecycleHooks; }, get extractImports() { return extractImports; }, get extractInputsAndOutputs() { return extractInputsAndOutputs; }, })); import { extractPublicApi } from '../utils/public-api.extractor.js'; type ParsedComponentStub = { fileName: string; inputs?: Record<string, any>; outputs?: Record<string, any>; }; function makeParsedComponent( partial: Partial<ParsedComponentStub> = {}, ): ParsedComponentStub { return { fileName: '/comp.ts', ...partial, } as ParsedComponentStub; } function resetHelperMocks() { extractClassDeclaration.mockReset(); extractPublicMethods.mockReset(); extractLifecycleHooks.mockReset(); extractImports.mockReset(); extractInputsAndOutputs.mockReset(); createProgramMock.mockReset(); } describe('extractPublicApi', () => { beforeEach(() => { resetHelperMocks(); }); it('maps basic inputs and outputs when no class declaration', () => { const parsed = makeParsedComponent({ inputs: { foo: { type: 'string', required: true } }, outputs: { done: { type: 'void' } }, }); extractClassDeclaration.mockReturnValue(undefined); const api = extractPublicApi(parsed as any); expect(api.properties.foo).toEqual({ type: 'string', isInput: true, required: true, }); expect(api.events.done).toEqual({ type: 'void' }); expect(api.methods).toEqual({}); expect(api.lifecycle).toEqual([]); expect(api.imports).toEqual([]); }); it('merges TypeScript analysis results', () => { const parsed = makeParsedComponent(); const classDeclStub = {}; extractClassDeclaration.mockReturnValue(classDeclStub); createProgramMock.mockReturnValue({ getSourceFile: () => ({}), }); extractInputsAndOutputs.mockReturnValue({ inputs: { bar: { type: 'number', required: false, alias: 'baz' }, }, outputs: { submitted: { type: 'CustomEvt', alias: 'submittedAlias' }, }, }); extractPublicMethods.mockReturnValue({ doStuff: { parameters: [], returnType: 'void' }, }); extractLifecycleHooks.mockReturnValue(['ngOnInit']); extractImports.mockReturnValue([ { name: 'HttpClient', path: '@angular/common/http' }, ]); const api = extractPublicApi(parsed as any); expect(api.properties.bar).toEqual( expect.objectContaining({ type: 'number', isInput: true, alias: 'baz', }), ); expect(api.events.submitted).toEqual( expect.objectContaining({ type: 'CustomEvt', alias: 'submittedAlias' }), ); expect(api.methods).toHaveProperty('doStuff'); expect(api.lifecycle).toEqual(['ngOnInit']); expect(api.imports[0]).toEqual({ name: 'HttpClient', path: '@angular/common/http', }); }); it('coerces booleanAttribute transform to boolean type', () => { const parsed = makeParsedComponent(); extractClassDeclaration.mockReturnValue({}); createProgramMock.mockReturnValue({ getSourceFile: () => ({}) }); extractInputsAndOutputs.mockReturnValue({ inputs: { flag: { type: 'any', transform: 'booleanAttribute' }, }, outputs: {}, }); const api = extractPublicApi(parsed as any); expect(api.properties.flag.type).toBe('boolean'); }); it('handles signal input with defaultValue and transform', () => { const parsed = makeParsedComponent(); extractClassDeclaration.mockReturnValue({}); createProgramMock.mockReturnValue({ getSourceFile: () => ({}) }); extractInputsAndOutputs.mockReturnValue({ inputs: { count: { type: 'number', defaultValue: 0, transform: 'identity' }, }, outputs: {}, }); const api = extractPublicApi(parsed as any); const countProp = api.properties.count as any; expect(countProp.isInput).toBe(true); expect(countProp.defaultValue).toBe(0); expect(countProp.transform).toBe('identity'); expect(countProp).not.toHaveProperty('alias'); }); }); ``` -------------------------------------------------------------------------------- /docs/architecture-internal-design.md: -------------------------------------------------------------------------------- ```markdown # Angular MCP Server – Architecture & Internal Design > **Audience:** Backend & tool authors who need to understand how the MCP server is wired together so they can extend it with new tools, prompts, or transports. --- ## 1. High-level Overview The Angular MCP Server is a **Node.js** process that wraps the generic **[Model Context Protocol SDK](https://github.com/modelcontextprotocol/sdk)** and exposes Angular-specific analysis & refactoring tools. It follows a clean, layered design: 1. **Transport & Core (MCP SDK)** – HTTP/SSE transport, request routing, JSON-schema validation. 2. **Server Wrapper (`AngularMcpServerWrapper`)** – registers prompts, tools, and resources; injects workspace-specific paths. 3. **Tools Layer (`src/lib/tools/**`)** – thin adapters delegating to shared analysis libraries. 4. **Shared Libraries (`packages/shared/**`)** – AST utilities, DS coverage, generic helpers. --- ## 2. Runtime Flow ```mermaid graph TD subgraph Editor / LLM Client A[CallTool / ListTools] -->|HTTP + SSE| B(McpServer Core) end B --> C{Request Router} C -->|tools| D[Tools Registry] C -->|prompts| E[Prompts Registry] C -->|resources| F[Resources Provider] D --> G[Individual Tool Handler] G --> H[Shared Libs & Workspace FS] ``` 1. The client sends a **`CallTool`** request. 2. `McpServer` validates the request against JSON Schema. 3. `AngularMcpServerWrapper` routes it to the correct handler inside **Tools Registry**. 4. The handler performs analysis (often via shared libs) and returns structured content. 5. The response streams back. --- ## 3. Bootstrap Sequence 1. **CLI Invocation** (see `.cursor/mcp.json`): ```bash node packages/angular-mcp/dist/main.js --workspaceRoot=/abs/path ... ``` 2. `main.ts` → `AngularMcpServerWrapper.create()` 3. **Config Validation** (`AngularMcpServerOptionsSchema`) – checks absolute/relative paths. 4. **File Existence Validation** – ensures Storybook docs & DS mapping files are present. 5. **Server Setup** – registers capabilities & calls: - `registerPrompts()` - `registerTools()` - `registerResources()` 6. Server starts listening. --- ## 4. Directory Layout (server package) ``` packages/angular-mcp-server/ src/ lib/ angular-mcp-server.ts # Wrapper class (core of the server) tools/ ds/ # DS-specific tool categories shared/ # Internal helpers prompts/ # Prompt schemas & impls validation/ # Zod schemas & file checks index.ts # Re-export of wrapper ``` --- ## 5. Tools & Extension Points | Extension | Where to Add | Boilerplate | |-----------|-------------|-------------| | **Tool** | `src/lib/tools/**` | 1. Create `my-awesome.tool.ts` exporting `ToolsConfig[]`. <br>2. Add it to the export list in `tools/ds/tools.ts` (or another category). | | **Prompt** | `prompts/prompt-registry.ts` | 1. Append schema to `PROMPTS`. <br>2. Provide implementation in `PROMPTS_IMPL`. | | **Resource Provider** | `registerResources()` | Extend logic to aggregate custom docs or design-system assets. | All tools share the `ToolsConfig` interface (`@push-based/models`) that bundles: - `schema` (name, description, arguments, return type) - `handler(request)` async function. The MCP SDK auto-validates every call against the schema – no manual parsing required. --- ## 6. Configuration Options | Option | Type | Description | |--------|------|-------------| | `workspaceRoot` | absolute path | Root of the Nx/Angular workspace. | | `ds.storybookDocsRoot` | relative path | Path (from root) to Storybook MDX/Docs for DS components. | | `ds.deprecatedCssClassesPath` | relative path | JS file mapping components → deprecated CSS classes. | | `ds.uiRoot` | relative path | Folder containing raw design-system component source. | Validation is handled via **Zod** in `angular-mcp-server-options.schema.ts`. --- ## 7. Shared Libraries in Play ``` models (types & schemas) ├─ utils ├─ styles-ast-utils └─ angular-ast-utils └─ ds-component-coverage (top-level plugin) ``` These libraries provide AST parsing, file operations, and DS analysis. Tools import them directly; they are **framework-agnostic** and can be unit-tested in isolation. --- ## 8. Testing & Examples `packages/minimal-repo/**` contains miniature Angular apps used by unit/integration tests. They are **not** part of production code but useful when debugging a new tool. --- ## 9. Adding a New Tool – Checklist 1. Identify functionality and pick/create an appropriate shared library function. 2. Generate a JSON-schema with arguments & result shape (can use Zod helper). 3. Implement handler logic (avoid heavy FS operations in main thread; prefer async). 4. Export via `ToolsConfig[]` and append to category list. 5. Write unit tests. 6. Update `docs/tools.md` once published. --- ``` -------------------------------------------------------------------------------- /packages/shared/DEPENDENCIES.md: -------------------------------------------------------------------------------- ```markdown # Shared Libraries Dependencies This document provides an AI-friendly overview of the shared libraries in the `/packages/shared` directory, their purposes, and cross-dependencies. ## Library Overview ### Foundation Layer (No Internal Dependencies) #### `@push-based/models` - **Purpose**: Core types and interfaces for CLI and MCP tooling - **Key Exports**: CliArgsObject, ArgumentValue, ToolSchemaOptions, ToolsConfig, ToolHandlerContentResult, DiagnosticsAware - **Dependencies**: None (foundation library) - **Used By**: All other shared libraries #### `@push-based/typescript-ast-utils` - **Purpose**: TypeScript AST parsing and manipulation utilities - **Key Exports**: isComponentDecorator, removeQuotes, getDecorators, isDecorator - **Dependencies**: None (foundation library) - **Used By**: angular-ast-utils ### Intermediate Layer (Single Foundation Dependency) #### `@push-based/utils` - **Purpose**: General utility functions and file system operations - **Key Exports**: findFilesWithPattern, resolveFile - **Dependencies**: models - **Used By**: angular-ast-utils, ds-component-coverage #### `@push-based/styles-ast-utils` - **Purpose**: CSS/SCSS AST parsing and manipulation utilities - **Key Exports**: parseStylesheet, CssAstVisitor, visitStyleSheet, styleAstRuleToSource - **Dependencies**: models - **Used By**: angular-ast-utils, ds-component-coverage ### Advanced Layer (Multiple Dependencies) #### `@push-based/angular-ast-utils` - **Purpose**: Angular component parsing and template/style analysis - **Key Exports**: parseComponents, visitComponentTemplate, visitComponentStyles, findAngularUnits - **Dependencies**: - models (types and schemas) - utils (file operations) - typescript-ast-utils (TS AST utilities) - styles-ast-utils (CSS AST utilities) - **Used By**: ds-component-coverage #### `@push-based/ds-component-coverage` - **Purpose**: Design System component usage analysis and coverage reporting - **Key Exports**: dsComponentCoveragePlugin, runnerFunction, getAngularDsUsageCategoryRefs - **Dependencies**: - models (audit types) - utils (utilities) - styles-ast-utils (CSS analysis) - angular-ast-utils (component parsing) - **Used By**: None (top-level plugin) ## Dependency Graph ``` @code-pushup/models (foundation) ├── @code-pushup/utils ├── styles-ast-utils └── angular-ast-utils ├── @code-pushup/models ├── @code-pushup/utils ├── typescript-ast-utils └── styles-ast-utils ds-component-coverage (most complex) ├── @code-pushup/models ├── @code-pushup/utils ├── styles-ast-utils └── angular-ast-utils ``` ## Build Order Based on dependencies, the correct build order is: 1. **Foundation**: `models`, `typescript-ast-utils` 2. **Intermediate**: `utils`, `styles-ast-utils` 3. **Advanced**: `angular-ast-utils` 4. **Top-level**: `ds-component-coverage` ## Key Patterns ### Dependency Injection Pattern - Libraries accept dependencies through imports rather than direct instantiation - Enables testing and modularity ### Layered Architecture - Clear separation between foundation, intermediate, and advanced layers - Each layer builds upon the previous one ### Single Responsibility - Each library has a focused purpose - Cross-cutting concerns are handled by foundation libraries ### No Circular Dependencies - Clean acyclic dependency graph - Ensures predictable build order and runtime behavior ## Usage Guidelines for AI ### When to Use Each Library - **models**: When you need CLI argument types or MCP tooling interfaces - **utils**: For file operations, string manipulation, or general utilities - **typescript-ast-utils**: For TypeScript code analysis and manipulation - **styles-ast-utils**: For CSS/SCSS parsing and analysis - **angular-ast-utils**: For Angular component analysis and template/style processing - **ds-component-coverage**: For Design System migration analysis and reporting ### Common Import Patterns ```typescript // Foundation types import { CliArgsObject, ToolSchemaOptions, DiagnosticsAware } from '@push-based/models'; // File operations import { resolveFile, findFilesWithPattern } from '@code-pushup/utils'; // Angular component parsing import { parseComponents, visitComponentTemplate, } from '@push-based/angular-ast-utils'; // CSS analysis import { parseStylesheet, visitStyleSheet } from '@push-based/styles-ast-utils'; // TypeScript utilities import { isComponentDecorator, removeQuotes, } from '@push-based/typescript-ast-utils'; ``` ### Integration Points - All libraries use `models` for CLI and MCP tooling type definitions - File operations flow through `utils` - AST operations are specialized by language (TS, CSS, Angular) - Complex analysis combines multiple AST utilities through `angular-ast-utils` ## Maintenance Notes - Changes to `models` affect all other libraries - `angular-ast-utils` is the most integration-heavy library - `ds-component-coverage` represents the full stack integration - Foundation libraries should remain stable and focused - New features should follow the established layering pattern ``` -------------------------------------------------------------------------------- /packages/shared/utils/src/lib/execute-process.ts: -------------------------------------------------------------------------------- ```typescript import { type ChildProcess, type ChildProcessByStdio, type SpawnOptionsWithStdioTuple, type StdioPipe, spawn, } from 'node:child_process'; import type { Readable, Writable } from 'node:stream'; import { formatCommandLog } from './format-command-log.js'; import { isVerbose } from './logging.js'; import { calcDuration } from './utils.js'; /** * Represents the process result. * @category Types * @public * @property {string} stdout - The stdout of the process. * @property {string} stderr - The stderr of the process. * @property {number | null} code - The exit code of the process. */ export type ProcessResult = { stdout: string; stderr: string; code: number | null; date: string; duration: number; }; /** * Error class for process errors. * Contains additional information about the process result. * @category Error * @public * @class * @extends Error * @example * const result = await executeProcess({}) * .catch((error) => { * if (error instanceof ProcessError) { * console.error(error.code); * console.error(error.stderr); * console.error(error.stdout); * } * }); * */ export class ProcessError extends Error { code: number | null; stderr: string; stdout: string; constructor(result: ProcessResult) { super(result.stderr); this.code = result.code; this.stderr = result.stderr; this.stdout = result.stdout; } } /** * Process config object. Contains the command, args and observer. * @param cfg - process config object with command, args and observer (optional) * @category Types * @public * @property {string} command - The command to execute. * @property {string[]} args - The arguments for the command. * @property {ProcessObserver} observer - The observer for the process. * * @example * * // bash command * const cfg = { * command: 'bash', * args: ['-c', 'echo "hello world"'] * }; * * // node command * const cfg = { * command: 'node', * args: ['--version'] * }; * * // npx command * const cfg = { * command: 'npx', * args: ['--version'] * */ export type ProcessConfig = Omit< SpawnOptionsWithStdioTuple<StdioPipe, StdioPipe, StdioPipe>, 'stdio' > & { command: string; args?: string[]; observer?: ProcessObserver; ignoreExitCode?: boolean; }; /** * Process observer object. Contains the onStdout, error and complete function. * @category Types * @public * @property {function} onStdout - The onStdout function of the observer (optional). * @property {function} onError - The error function of the observer (optional). * @property {function} onComplete - The complete function of the observer (optional). * * @example * const observer = { * onStdout: (stdout) => console.info(stdout) * } */ export type ProcessObserver = { onStdout?: (stdout: string, sourceProcess?: ChildProcess) => void; onStderr?: (stderr: string, sourceProcess?: ChildProcess) => void; onError?: (error: ProcessError) => void; onComplete?: () => void; }; /** * Executes a process and returns a promise with the result as `ProcessResult`. * * @example * * // sync process execution * const result = await executeProcess({ * command: 'node', * args: ['--version'] * }); * * console.info(result); * * // async process execution * const result = await executeProcess({ * command: 'node', * args: ['download-data.js'], * observer: { * onStdout: updateProgress, * error: handleError, * complete: cleanLogs, * } * }); * * console.info(result); * * @param cfg - see {@link ProcessConfig} */ export function executeProcess(cfg: ProcessConfig): Promise<ProcessResult> { const { command, args, observer, ignoreExitCode = false, ...options } = cfg; const { onStdout, onStderr, onError, onComplete } = observer ?? {}; const date = new Date().toISOString(); const start = performance.now(); if (isVerbose()) { console.log(formatCommandLog(command, args, `${cfg.cwd ?? process.cwd()}`)); } return new Promise((resolve, reject) => { // shell:true tells Windows to use shell command for spawning a child process const spawnedProcess = spawn(command, args ?? [], { shell: true, windowsHide: true, ...options, }) as ChildProcessByStdio<Writable, Readable, Readable>; let stdout = ''; let stderr = ''; spawnedProcess.stdout.on('data', (data) => { stdout += String(data); onStdout?.(String(data), spawnedProcess); }); spawnedProcess.stderr.on('data', (data) => { stderr += String(data); onStderr?.(String(data), spawnedProcess); }); spawnedProcess.on('error', (err) => { stderr += err.toString(); }); spawnedProcess.on('close', (code) => { const timings = { date, duration: calcDuration(start) }; if (code === 0 || ignoreExitCode) { onComplete?.(); resolve({ code, stdout, stderr, ...timings }); } else { const errorMsg = new ProcessError({ code, stdout, stderr, ...timings }); onError?.(errorMsg); reject(errorMsg); } }); }); } ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/spec/dom-slots.extractor.spec.ts: -------------------------------------------------------------------------------- ```typescript import { describe, it, expect, vi } from 'vitest'; // ----------------------------------------------------------------------------- // Mocking angular-ast-utils functions relied upon by extractSlotsAndDom // ----------------------------------------------------------------------------- // Helper used by extractSlotsAndDom to traverse template function visitEachTmplChild(nodes: any[], visitor: any): void { nodes.forEach((node) => { if (typeof node.visit === 'function') { node.visit(visitor); } }); } // Mock implementation of visitComponentTemplate – supplies template nodes async function visitComponentTemplate( parsedComponent: any, _options: any, cb: any, ) { const templateAsset = { parse: async () => ({ nodes: parsedComponent.templateNodes }), }; return cb(parsedComponent, templateAsset); } /* eslint-disable prefer-const */ vi.mock('@push-based/angular-ast-utils', () => { // Define a lightweight NoopTmplVisitor inside the mock factory to avoid // reference-before-initialization issues caused by hoisting. class NoopTmplVisitor {} return { visitComponentTemplate, visitEachTmplChild, NoopTmplVisitor, parseClassNames: (classStr: string) => classStr.trim().split(/\s+/), }; }); // ----------------------------------------------------------------------------- // Imports (after mocks) // ----------------------------------------------------------------------------- import { extractSlotsAndDom } from '../utils/dom-slots.extractor.js'; // ----------------------------------------------------------------------------- // Minimal AST node builders // ----------------------------------------------------------------------------- type AttributeNode = { name: string; value: string }; type InputNode = { name: string; value?: any }; type OutputNode = { name: string; handler: { source: string } }; type ElementNode = { name: string; attributes: AttributeNode[]; inputs: InputNode[]; outputs: OutputNode[]; children: any[]; visit: (v: any) => void; }; function createElement( params: Partial<ElementNode> & { name: string }, ): ElementNode { const node: ElementNode = { name: params.name, attributes: params.attributes ?? [], inputs: params.inputs ?? [], outputs: params.outputs ?? [], children: params.children ?? [], visit(visitor: any) { // Call the appropriate visitor method if (typeof visitor.visitElement === 'function') { visitor.visitElement(this as any); } }, } as ElementNode; return node; } function createContent(selector: string | undefined): any { return { selector, visit(visitor: any) { if (typeof visitor.visitContent === 'function') { visitor.visitContent(this); } }, }; } function createForLoopBlock( children: any[], expressionSrc = 'item of items', alias = 'item', ): any { return { children, expression: { source: expressionSrc }, item: { name: alias }, visit(visitor: any) { if (typeof visitor.visitForLoopBlock === 'function') { visitor.visitForLoopBlock(this); } }, }; } // ----------------------------------------------------------------------------- // Test suites // ----------------------------------------------------------------------------- describe('extractSlotsAndDom', () => { it('extracts default and named slots', async () => { const defaultSlot = createContent(undefined); const namedSlot = createContent('[slot=header]'); const parsedComponent = { templateNodes: [defaultSlot, namedSlot], }; const { slots } = await extractSlotsAndDom(parsedComponent as any); expect(slots).toHaveProperty('default'); expect(slots.default.selector).toBe('ng-content'); expect(slots).toHaveProperty('header'); expect(slots.header.selector).toBe('ng-content[select="[slot=header]"]'); }); it('builds DOM structure with parent-child links', async () => { const span = createElement({ name: 'span', attributes: [{ name: 'class', value: 'foo' }], }); const div = createElement({ name: 'div', attributes: [{ name: 'id', value: 'root' }], children: [span], }); const parsedComponent = { templateNodes: [div] }; const { dom } = await extractSlotsAndDom(parsedComponent as any); const parentKey = 'div#root'; const childKey = 'div#root > span.foo'; expect(Object.keys(dom)).toEqual([parentKey, childKey]); const parent = dom[parentKey] as any; const child = dom[childKey] as any; expect(parent.children).toEqual([childKey]); expect(child.parent).toBe(parentKey); }); it('captures structural directive context (for loop)', async () => { const li = createElement({ name: 'li' }); const forBlock = createForLoopBlock([li]); const parsedComponent = { templateNodes: [forBlock] }; const { dom } = await extractSlotsAndDom(parsedComponent as any); const liNode = dom['li'] as any; expect(liNode).toBeTruthy(); expect(liNode.structural?.[0]).toEqual( expect.objectContaining({ kind: 'for', alias: 'item' }), ); }); }); ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/diff/utils/diff-utils.ts: -------------------------------------------------------------------------------- ```typescript import type { Difference } from 'microdiff'; import type { DomPathDictionary } from '../../shared/models/types.js'; import { createDomPathDictionary, processDomPaths } from './dom-path-utils.js'; /** * Enhanced version of consolidateAndPruneRemoveOperations with DOM path deduplication */ export function consolidateAndPruneRemoveOperationsWithDeduplication( diffResult: Difference[], ): { processedResult: Difference[]; domPathDict: DomPathDictionary; } { const consolidatedResult = consolidateAndPruneRemoveOperations(diffResult); const domPathDict = createDomPathDictionary(), processedResult = consolidatedResult.map((c) => processDomPaths(c, domPathDict), ); return { processedResult, domPathDict, }; } /** * Consolidates REMOVE operations for CSS rules in styles section and * prunes redundant child REMOVE operations from a microdiff result. */ export function consolidateAndPruneRemoveOperations( diffResult: Difference[], ): Difference[] { const { removeOperations, nonRemoveOperations } = diffResult.reduce( (acc, change) => { (change.type === 'REMOVE' ? acc.removeOperations : acc.nonRemoveOperations ).push(change); return acc; }, { removeOperations: [] as Difference[], nonRemoveOperations: [] as Difference[], }, ); if (removeOperations.length === 0) { return diffResult; } const cssRuleRemoves = new Map<string, Difference[]>(); const otherRemoves: Difference[] = []; const isCssRuleRemove = (d: Difference) => d.type === 'REMOVE' && d.path.length === 3 && d.path[0] === 'styles' && d.path[1] === 'rules' && typeof d.path[2] === 'string'; for (const remove of removeOperations) { if (isCssRuleRemove(remove)) { const key = 'styles.rules'; const group = cssRuleRemoves.get(key) ?? []; group.push(remove); cssRuleRemoves.set(key, group); } else { otherRemoves.push(remove); } } const consolidatedCssRuleChanges: Difference[] = [ ...[...cssRuleRemoves.values()].flatMap((removes) => removes.length > 1 ? [ { type: 'REMOVE', path: ['styles', 'rules'], oldValue: removes.map((r) => r.path[2]), } as Difference, ] : removes, ), ]; otherRemoves.sort((a, b) => a.path.length - b.path.length); const prunedOtherRemoves: Difference[] = []; for (const currentRemove of otherRemoves) { let isRedundant = false; for (const existingRemove of prunedOtherRemoves) { if (isChildPath(currentRemove.path, existingRemove.path)) { isRedundant = true; break; } } if (!isRedundant) { prunedOtherRemoves.push(currentRemove); } } return [ ...nonRemoveOperations, ...consolidatedCssRuleChanges, ...prunedOtherRemoves, ]; } /** * Checks if childPath is a descendant of parentPath. * For example, ['a', 'b', 'c'] is a child of ['a', 'b'] */ export function isChildPath( childPath: (string | number)[], parentPath: (string | number)[], ): boolean { return ( childPath.length > parentPath.length && parentPath.every((seg, i) => childPath[i] === seg) ); } /** * Groups diff changes by domain first, then by type within each domain. * Removes the domain from the path since it's captured in the grouping structure. */ export function groupChangesByDomainAndType( changes: Difference[], ): Record<string, Record<string, any[]>> { return changes.reduce( (acc: Record<string, Record<string, any[]>>, change: Difference) => { const { type, path, ...changeWithoutTypeAndPath } = change; const domain = path[0] as string; if (!acc[domain]) acc[domain] = {}; if (!acc[domain][type]) acc[domain][type] = []; acc[domain][type].push({ ...changeWithoutTypeAndPath, path: path.slice(1), }); return acc; }, {} as Record<string, Record<string, any[]>>, ); } /** * Generates a comprehensive summary of diff changes including totals and breakdowns by type and domain. */ export function generateDiffSummary( processedResult: Difference[], groupedChanges: Record<string, Record<string, any[]>>, ): { totalChanges: number; changeTypes: Record<string, number>; changesByDomain: Record<string, Record<string, number>>; } { return { totalChanges: processedResult.length, changeTypes: processedResult.reduce( (acc: Record<string, number>, change: Difference) => { acc[change.type] = (acc[change.type] ?? 0) + 1; return acc; }, {} as Record<string, number>, ), changesByDomain: Object.entries(groupedChanges).reduce( (acc: Record<string, Record<string, number>>, [domain, types]) => { acc[domain] = Object.entries(types).reduce( (domainAcc: Record<string, number>, [type, changes]) => { domainAcc[type] = changes.length; return domainAcc; }, {} as Record<string, number>, ); return acc; }, {} as Record<string, Record<string, number>>, ), }; } ``` -------------------------------------------------------------------------------- /packages/shared/LLMS.md: -------------------------------------------------------------------------------- ```markdown # LLM Documentation Index <a id="top"></a> **Contents:** - [Structure](#documentation-structure) - [Foundation](#foundation-layer) - [Intermediate](#intermediate-layer) - [Advanced](#advanced-layer) - [Navigation](#quick-navigation) - [Related](#related-docs) - [Tips](#tips) - [Status](#doc-status) This document provides quick access to all AI-friendly documentation across the shared libraries. Each library includes comprehensive API documentation, practical examples, and function references. ## 📚 Documentation Structure <a id="documentation-structure"></a> Each library provides three types of AI documentation: - **FUNCTIONS.md**: A-Z quick reference for every public symbol - **API.md**: Overview, key features, and minimal usage examples - **EXAMPLES.md**: Practical, runnable code scenarios with expected outputs ## 🏗️ Foundation Layer <a id="foundation-layer"></a> ### @code-pushup/models <a id="models"></a> Core types, interfaces, and Zod schemas for the entire ecosystem. - [🔍 Functions Reference](./models/ai/FUNCTIONS.md) - [📖 API Overview](./models/ai/API.md) - [💡 Examples](./models/ai/EXAMPLES.md) ### @push-based/typescript-ast-utils <a id="typescript-ast-utils"></a> TypeScript AST parsing and manipulation utilities. - [🔍 Functions Reference](./typescript-ast-utils/ai/FUNCTIONS.md) - [📖 API Overview](./typescript-ast-utils/ai/API.md) - [💡 Examples](./typescript-ast-utils/ai/EXAMPLES.md) ## 🔧 Intermediate Layer <a id="intermediate-layer"></a> ### @code-pushup/utils <a id="utils"></a> General utility functions and file system operations. - [🔍 Functions Reference](./utils/ai/FUNCTIONS.md) - [📖 API Overview](./utils/ai/API.md) - [💡 Examples](./utils/ai/EXAMPLES.md) ### @push-based/styles-ast-utils <a id="styles-ast-utils"></a> CSS/SCSS AST parsing and manipulation utilities. - [🔍 Functions Reference](./styles-ast-utils/ai/FUNCTIONS.md) - [📖 API Overview](./styles-ast-utils/ai/API.md) - [💡 Examples](./styles-ast-utils/ai/EXAMPLES.md) ## 🚀 Advanced Layer <a id="advanced-layer"></a> ### @push-based/angular-ast-utils <a id="angular-ast-utils"></a> Angular component parsing and template/style analysis. - [🔍 Functions Reference](./angular-ast-utils/ai/FUNCTIONS.md) - [📖 API Overview](./angular-ast-utils/ai/API.md) - [💡 Examples](./angular-ast-utils/ai/EXAMPLES.md) ### @push-based/ds-component-coverage <a id="ds-component-coverage"></a> Design System component usage analysis and coverage reporting. - [🔍 Functions Reference](./ds-component-coverage/ai/FUNCTIONS.md) - [📖 API Overview](./ds-component-coverage/ai/API.md) - [💡 Examples](./ds-component-coverage/ai/EXAMPLES.md) ## 🎯 Quick Navigation by Use Case <a id="quick-navigation"></a> ### Type Definitions & Schemas - [models/FUNCTIONS.md](./models/ai/FUNCTIONS.md) - All available types and interfaces - [models/API.md](./models/ai/API.md) - Core types and Zod schemas ### File & String Operations - [utils/FUNCTIONS.md](./utils/ai/FUNCTIONS.md) - Complete function reference - [utils/API.md](./utils/ai/API.md) - File system and utility functions - [utils/EXAMPLES.md](./utils/ai/EXAMPLES.md) - File operations and string manipulation ### AST Analysis & Manipulation - [typescript-ast-utils/FUNCTIONS.md](./typescript-ast-utils/ai/FUNCTIONS.md) - TS AST function reference - [styles-ast-utils/FUNCTIONS.md](./styles-ast-utils/ai/FUNCTIONS.md) - CSS AST function reference - [angular-ast-utils/FUNCTIONS.md](./angular-ast-utils/ai/FUNCTIONS.md) - Angular AST function reference ### Angular Development - [angular-ast-utils/EXAMPLES.md](./angular-ast-utils/ai/EXAMPLES.md) - Component parsing and analysis ### Design System Analysis - [ds-component-coverage/FUNCTIONS.md](./ds-component-coverage/ai/FUNCTIONS.md) - DS analysis functions - [ds-component-coverage/API.md](./ds-component-coverage/ai/API.md) - DS migration and coverage analysis - [ds-component-coverage/EXAMPLES.md](./ds-component-coverage/ai/EXAMPLES.md) - Real-world DS analysis scenarios ## 🔗 Related Documentation <a id="related-docs"></a> - [DEPENDENCIES.md](./DEPENDENCIES.md) - Cross-dependencies and architecture overview - [Individual README files](./*/README.md) - Library-specific setup and build instructions ## 💡 Tips for LLMs <a id="tips"></a> 1. **Start with FUNCTIONS.md** for quick function lookup and signatures 2. **Use API.md** for understanding library capabilities and minimal usage 3. **Reference EXAMPLES.md** for practical implementation patterns 4. **Check DEPENDENCIES.md** to understand library relationships 5. **Follow the layered architecture** when combining multiple libraries ## 📋 Documentation Status <a id="doc-status"></a> All shared libraries have complete AI documentation: | Library | Functions | API | Examples | Status | | --------------------- | --------- | --- | -------- | -------- | | models | ✅ | ✅ | ✅ | Complete | | typescript-ast-utils | ✅ | ✅ | ✅ | Complete | | utils | ✅ | ✅ | ✅ | Complete | | styles-ast-utils | ✅ | ✅ | ✅ | Complete | | angular-ast-utils | ✅ | ✅ | ✅ | Complete | | ds-component-coverage | ✅ | ✅ | ✅ | Complete | _Last updated: 2025-06-13_ ``` -------------------------------------------------------------------------------- /packages/shared/models/ai/EXAMPLES.md: -------------------------------------------------------------------------------- ```markdown # Examples ## 1 — Working with CLI arguments > Type-safe handling of command line arguments. ```ts import { type CliArgsObject, type ArgumentValue } from '@push-based/models'; // Basic CLI arguments const args: CliArgsObject = { directory: './src/components', componentName: 'DsButton', groupBy: 'file', _: ['report-violations'], }; console.log(args.directory); // → './src/components' console.log(args._); // → ['report-violations'] // Typed CLI arguments with specific structure interface MyToolArgs { directory: string; componentName: string; groupBy?: 'file' | 'folder'; verbose?: boolean; } const typedArgs: CliArgsObject<MyToolArgs> = { directory: './packages/shared/models', componentName: 'DsCard', groupBy: 'folder', verbose: true, _: ['analyze'], }; console.log(`Analyzing ${typedArgs.componentName} in ${typedArgs.directory}`); // → 'Analyzing DsCard in ./packages/shared/models' ``` --- ## 2 — Creating MCP tools > Build Model Context Protocol tools with proper typing. ```ts import { type ToolsConfig, type ToolSchemaOptions } from '@push-based/models'; // Define a simple MCP tool const reportViolationsTool: ToolsConfig = { schema: { name: 'report-violations', description: 'Report deprecated DS CSS usage in a directory', inputSchema: { type: 'object', properties: { directory: { type: 'string', description: 'The relative path to the directory to scan', }, componentName: { type: 'string', description: 'The class name of the component (e.g., DsButton)', }, groupBy: { type: 'string', enum: ['file', 'folder'], default: 'file', description: 'How to group the results', }, }, required: ['directory', 'componentName'], }, }, handler: async (request) => { const { directory, componentName, groupBy = 'file' } = request.params.arguments as { directory: string; componentName: string; groupBy?: 'file' | 'folder'; }; // Tool implementation logic here const violations = await analyzeViolations(directory, componentName, groupBy); return { content: [ { type: 'text', text: `Found ${violations.length} violations in ${directory}`, }, ], }; }, }; // Use the tool configuration console.log(`Tool: ${reportViolationsTool.schema.name}`); // → 'Tool: report-violations' ``` --- ## 3 — Implementing diagnostics > Create objects that can report issues and diagnostics. ```ts import { type DiagnosticsAware } from '@push-based/models'; class ComponentAnalyzer implements DiagnosticsAware { private issues: Array<{ code?: number; message: string; severity: string }> = []; analyze(componentPath: string): void { // Simulate analysis if (!componentPath.endsWith('.ts')) { this.issues.push({ code: 1001, message: 'Component file should have .ts extension', severity: 'error', }); } if (componentPath.includes('deprecated')) { this.issues.push({ code: 2001, message: 'Component uses deprecated patterns', severity: 'warning', }); } } getIssues() { return this.issues; } clear(): void { this.issues = []; } } // Usage const analyzer = new ComponentAnalyzer(); analyzer.analyze('src/components/deprecated-button.js'); const issues = analyzer.getIssues(); console.log(`Found ${issues.length} issues:`); issues.forEach((issue) => { console.log(` ${issue.severity}: ${issue.message} (code: ${issue.code})`); }); // → Found 2 issues: // → error: Component file should have .ts extension (code: 1001) // → warning: Component uses deprecated patterns (code: 2001) analyzer.clear(); console.log(`Issues after clear: ${analyzer.getIssues().length}`); // → 0 ``` --- ## 4 — Advanced MCP tool with content results > Create sophisticated MCP tools that return structured content. ```ts import { type ToolsConfig, type ToolHandlerContentResult, } from '@push-based/models'; const buildComponentContractTool: ToolsConfig = { schema: { name: 'build-component-contract', description: 'Generate a static surface contract for a component', inputSchema: { type: 'object', properties: { directory: { type: 'string' }, templateFile: { type: 'string' }, styleFile: { type: 'string' }, typescriptFile: { type: 'string' }, dsComponentName: { type: 'string' }, }, required: ['directory', 'templateFile', 'styleFile', 'typescriptFile', 'dsComponentName'], }, }, handler: async (request) => { const params = request.params.arguments as { directory: string; templateFile: string; styleFile: string; typescriptFile: string; dsComponentName: string; }; // Generate contract const contract = await generateContract(params); const content: ToolHandlerContentResult[] = [ { type: 'text', text: `Generated contract for ${params.dsComponentName}`, }, { type: 'text', text: `Template inputs: ${contract.templateInputs.length}`, }, { type: 'text', text: `Style classes: ${contract.styleClasses.length}`, }, ]; return { content }; }, }; // Mock contract generation function async function generateContract(params: any) { return { templateInputs: ['@Input() label: string', '@Input() disabled: boolean'], styleClasses: ['.ds-button', '.ds-button--primary'], }; } ``` These examples demonstrate the practical usage patterns of the `@push-based/models` library for building type-safe CLI tools, MCP integrations, and diagnostic utilities in the Angular MCP toolkit. ``` -------------------------------------------------------------------------------- /.cursor/flows/component-refactoring/angular-20.md: -------------------------------------------------------------------------------- ```markdown # Angular Best Practices This project adheres to modern Angular best practices, emphasizing maintainability, performance, accessibility, and scalability. ## TypeScript Best Practices - **Strict Type Checking:** Always enable and adhere to strict type checking. This helps catch errors early and improves code quality. - **Prefer Type Inference:** Allow TypeScript to infer types when they are obvious from the context. This reduces verbosity while maintaining type safety. - **Bad:** ```typescript let name: string = 'Angular'; ``` - **Good:** ```typescript let name = 'Angular'; ``` - **Avoid `any`:** Do not use the `any` type unless absolutely necessary as it bypasses type checking. Prefer `unknown` when a type is uncertain and you need to handle it safely. ## Angular Best Practices - **Standalone Components:** Always use standalone components, directives, and pipes. Avoid using `NgModules` for new features or refactoring existing ones. - **Implicit Standalone:** When creating standalone components, you do not need to explicitly set `standalone: true` as it is implied by default when generating a standalone component. - **Bad:** ```typescript @Component({ standalone: true, // ... }) export class MyComponent {} ``` - **Good:** ```typescript @Component({ // `standalone: true` is implied // ... }) export class MyComponent {} ``` - **Signals for State Management:** Utilize Angular Signals for reactive state management within components and services. - **Lazy Loading:** Implement lazy loading for feature routes to improve initial load times of your application. - **NgOptimizedImage:** Use `NgOptimizedImage` for all static images to automatically optimize image loading and performance. ## Components - **Single Responsibility:** Keep components small, focused, and responsible for a single piece of functionality. - **`input()` and `output()` Functions:** Prefer `input()` and `output()` functions over the `@Input()` and `@Output()` decorators for defining component inputs and outputs. - **Old Decorator Syntax:** ```typescript @Input() userId!: string; @Output() userSelected = new EventEmitter<string>(); ``` - **New Function Syntax:** ```typescript import { input, output } from '@angular/core'; // ... userId = input<string>(''); userSelected = output<string>(); ``` - **`computed()` for Derived State:** Use the `computed()` function from `@angular/core` for derived state based on signals. - **`ChangeDetectionStrategy.OnPush`:** Always set `changeDetection: ChangeDetectionStrategy.OnPush` in the `@Component` decorator for performance benefits by reducing unnecessary change detection cycles. - **Inline Templates:** Prefer inline templates (template: `...`) for small components to keep related code together. For larger templates, use external HTML files. - **Reactive Forms:** Prefer Reactive forms over Template-driven forms for complex forms, validation, and dynamic controls due to their explicit, immutable, and synchronous nature. - **No `ngClass` / `NgClass`:** Do not use the `ngClass` directive. Instead, use native `class` bindings for conditional styling. - **Bad:** ```html <section [ngClass]="{'active': isActive}"></section> ``` - **Good:** ```html <section [class.active]="isActive"></section> <section [class]="{'active': isActive}"></section> <section [class]="myClasses"></section> ``` - **No `ngStyle` / `NgStyle`:** Do not use the `ngStyle` directive. Instead, use native `style` bindings for conditional inline styles. - **Bad:** ```html <section [ngStyle]="{'font-size': fontSize + 'px'}"></section> ``` - **Good:** ```html <section [style.font-size.px]="fontSize"></section> <section [style]="myStyles"></section> ``` ## State Management - **Signals for Local State:** Use signals for managing local component state. - **`computed()` for Derived State:** Leverage `computed()` for any state that can be derived from other signals. - **Pure and Predictable Transformations:** Ensure state transformations are pure functions (no side effects) and predictable. ## Templates - **Simple Templates:** Keep templates as simple as possible, avoiding complex logic directly in the template. Delegate complex logic to the component's TypeScript code. - **Native Control Flow:** Use the new built-in control flow syntax (`@if`, `@for`, `@switch`) instead of the older structural directives (`*ngIf`, `*ngFor`, `*ngSwitch`). - **Old Syntax:** ```html <section *ngIf="isVisible">Content</section> <section *ngFor="let item of items">{{ item }}</section> ``` - **New Syntax:** ```html @if (isVisible) { <section>Content</section> } @for (item of items; track item.id) { <section>{{ item }}</section> } ``` - **Async Pipe:** Use the `async` pipe to handle observables in templates. This automatically subscribes and unsubscribes, preventing memory leaks. ## Services - **Single Responsibility:** Design services around a single, well-defined responsibility. - **`providedIn: 'root'`:** Use the `providedIn: 'root'` option when declaring injectable services to ensure they are singletons and tree-shakable. - **`inject()` Function:** Prefer the `inject()` function over constructor injection when injecting dependencies, especially within `provide` functions, `computed` properties, or outside of constructor context. - **Old Constructor Injection:** ```typescript constructor(private myService: MyService) {} ``` - **New `inject()` Function:** ```typescript import { inject } from '@angular/core'; export class MyComponent { private myService = inject(MyService); // ... } ``` ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/complex-components/third-case/product-showcase.component.ts: -------------------------------------------------------------------------------- ```typescript import { Component, signal } from '@angular/core'; import { ProductCardComponent, Product, ProductBadgeType } from './product-card.component'; @Component({ selector: 'app-product-showcase', standalone: true, imports: [ProductCardComponent], template: ` <div class="showcase-container"> <h2>Product Card Showcase - Moderate Badge Complexity</h2> <p>This demonstrates a moderate level of badge complexity that should be more manageable to refactor to DsBadge.</p> <div class="showcase-grid"> @for (product of products(); track product.id) { <app-product-card [product]="product" [badgeType]="getBadgeType(product)" [showBadge]="shouldShowBadge(product)" [animated]="true" [compact]="false" (productSelected)="onProductSelected($event)" (favoriteToggled)="onFavoriteToggled($event)" (addToCartClicked)="onAddToCart($event)" (quickViewClicked)="onQuickView($event)"> </app-product-card> } </div> <div class="showcase-log"> <h3>Event Log:</h3> <div class="log-entries"> @for (entry of eventLog(); track $index) { <div class="log-entry">{{ entry }}</div> } </div> </div> </div> `, styles: [` .showcase-container { padding: 2rem; max-width: 1200px; margin: 0 auto; } .showcase-container h2 { color: #1f2937; margin-bottom: 1rem; } .showcase-container p { color: #6b7280; margin-bottom: 2rem; } .showcase-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; margin-bottom: 2rem; } .showcase-log { background: white; border-radius: 0.5rem; padding: 1.5rem; box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); } .showcase-log h3 { margin: 0 0 1rem 0; color: #1f2937; font-size: 1.125rem; } .log-entries { max-height: 200px; overflow-y: auto; } .log-entry { padding: 0.5rem; border-bottom: 1px solid #f3f4f6; font-size: 0.875rem; color: #374151; font-family: monospace; } .log-entry:last-child { border-bottom: none; } `] }) export class ProductShowcaseComponent { eventLog = signal<string[]>([]); products = signal<Product[]>([ { id: 'prod-1', name: 'Premium Wireless Headphones', price: 199.99, originalPrice: 249.99, category: 'Electronics', rating: 4.5, reviewCount: 128, inStock: true, imageUrl: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=300&h=200&fit=crop', tags: ['wireless', 'premium', 'noise-canceling'] }, { id: 'prod-2', name: 'Smart Fitness Watch', price: 299.99, category: 'Wearables', rating: 4.8, reviewCount: 256, inStock: true, imageUrl: 'https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=300&h=200&fit=crop', tags: ['fitness', 'smart', 'waterproof', 'gps'] }, { id: 'prod-3', name: 'Professional Camera Lens', price: 899.99, category: 'Photography', rating: 4.9, reviewCount: 89, inStock: true, imageUrl: 'https://images.unsplash.com/photo-1606983340126-99ab4feaa64a?w=300&h=200&fit=crop', tags: ['professional', 'telephoto', 'canon'] }, { id: 'prod-4', name: 'Gaming Mechanical Keyboard', price: 149.99, originalPrice: 179.99, category: 'Gaming', rating: 4.6, reviewCount: 342, inStock: false, imageUrl: 'https://images.unsplash.com/photo-1541140532154-b024d705b90a?w=300&h=200&fit=crop', tags: ['mechanical', 'rgb', 'gaming', 'cherry-mx'] }, { id: 'prod-5', name: 'Eco-Friendly Water Bottle', price: 24.99, category: 'Lifestyle', rating: 4.3, reviewCount: 67, inStock: true, imageUrl: 'https://images.unsplash.com/photo-1602143407151-7111542de6e8?w=300&h=200&fit=crop', tags: ['eco-friendly', 'stainless-steel', 'insulated'] }, { id: 'prod-6', name: 'Designer Laptop Backpack', price: 79.99, originalPrice: 99.99, category: 'Accessories', rating: 4.4, reviewCount: 156, inStock: true, imageUrl: 'https://images.unsplash.com/photo-1553062407-98eeb64c6a62?w=300&h=200&fit=crop', tags: ['designer', 'laptop', 'travel', 'waterproof'] } ]); getBadgeType(product: Product): ProductBadgeType { // Logic to determine badge type based on product characteristics if (product.originalPrice && product.originalPrice > product.price) { return 'sale'; } if (product.rating >= 4.8) { return 'bestseller'; } if (!product.inStock) { return 'limited'; } if (product.tags.includes('new') || Date.now() % 2 === 0) { // Simulate new products return 'new'; } return 'sale'; } shouldShowBadge(product: Product): boolean { // Show badge for sale items, high-rated items, or out of stock return !!(product.originalPrice && product.originalPrice > product.price) || product.rating >= 4.7 || !product.inStock; } onProductSelected(product: Product) { this.addLogEntry(`Product selected: ${product.name}`); } onFavoriteToggled(event: {product: Product, favorited: boolean}) { this.addLogEntry(`${event.product.name} ${event.favorited ? 'added to' : 'removed from'} favorites`); } onAddToCart(product: Product) { this.addLogEntry(`Added to cart: ${product.name} - $${product.price.toFixed(2)}`); } onQuickView(product: Product) { this.addLogEntry(`Quick view opened: ${product.name}`); } private addLogEntry(message: string) { const timestamp = new Date().toLocaleTimeString(); const entry = `[${timestamp}] ${message}`; this.eventLog.update(log => [entry, ...log.slice(0, 49)]); } } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/styles/bad-global-styles.scss: -------------------------------------------------------------------------------- ```scss // Extended and more meaningful classes .pill-with-badge { color: red; border: 1px solid #ccc; padding: 5px 10px; border-radius: 15px; display: inline-block; } .pill-with-badge-v2 { color: blue; border: 2px solid #aaa; padding: 6px 12px; border-radius: 20px; display: inline-block; } .sports-pill { color: green; background-color: #f0f0f0; padding: 8px 16px; border-radius: 25px; display: inline-block; } .offer-badge { color: yellow; background-color: #333; padding: 4px 8px; border-radius: 10px; display: inline-block; } .tab-nav { color: orange; background-color: #fff; padding: 10px; border-bottom: 2px solid #ddd; } .nav-tabs { color: purple; background-color: #eee; padding: 10px; border-bottom: 2px solid #ccc; } .tab-nav-item { color: pink; padding: 10px 15px; border-radius: 5px; display: inline-block; cursor: pointer; } .btn { color: brown; background-color: #f5f5f5; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; } .btn-primary { color: cyan; background-color: #007bff; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; } .legacy-button { color: magenta; background-color: #f8f9fa; padding: 10px 20px; border: 1px solid #ccc; border-radius: 5px; cursor: pointer; } .modal { color: lime; background-color: #fff; padding: 20px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } .card { color: olive; background-color: #f8f9fa; padding: 15px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .loading { color: teal; font-size: 16px; display: flex; align-items: center; justify-content: center; } .loading-v2 { color: navy; font-size: 18px; display: flex; align-items: center; justify-content: center; } .loading-v3 { color: maroon; font-size: 20px; display: flex; align-items: center; justify-content: center; } .collapsible-container { color: silver; background-color: #f0f0f0; padding: 10px; border-radius: 5px; overflow: hidden; } .divider { color: gray; border-top: 1px solid #ccc; margin: 10px 0; } .count { color: gold; background-color: #333; padding: 5px 10px; border-radius: 50%; display: inline-block; } .badge-circle { color: coral; background-color: #f0f0f0; padding: 5px 10px; border-radius: 50%; display: inline-block; } .custom-control-checkbox { color: khaki; display: flex; align-items: center; } .custom-control-radio { color: lavender; display: flex; align-items: center; } .form-control-tabs-segmented-v2 { color: salmon; background-color: #fff; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented-flex { color: sienna; background-color: #f8f9fa; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented-v2-dark { color: tan; background-color: #333; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented-v3 { color: turquoise; background-color: #fff; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented-v4 { color: violet; background-color: #f8f9fa; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented { color: wheat; background-color: #fff; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .custom-control-switcher { color: azure; display: flex; align-items: center; } // 50 more random classes .random-class-1 { background-color: #f0f0f0; } .random-class-2 { background-color: #e0e0e0; } .random-class-3 { background-color: #d0d0d0; } .random-class-4 { background-color: #c0c0c0; } .random-class-5 { background-color: #b0b0b0; } .random-class-6 { background-color: #a0a0a0; } .random-class-7 { background-color: #909090; } .random-class-8 { background-color: #808080; } .random-class-9 { background-color: #707070; } .random-class-10 { background-color: #606060; } .random-class-11 { background-color: #505050; } .random-class-12 { background-color: #404040; } .random-class-13 { background-color: #303030; } .random-class-14 { background-color: #202020; } .random-class-15 { background-color: #101010; } .random-class-16 { background-color: #f8f8f8; } .random-class-17 { background-color: #e8e8e8; } .random-class-18 { background-color: #d8d8d8; } .random-class-19 { background-color: #c8c8c8; } .random-class-20 { background-color: #b8b8b8; } .random-class-21 { background-color: #a8a8a8; } .random-class-22 { background-color: #989898; } .random-class-23 { background-color: #888888; } .random-class-24 { background-color: #787878; } .random-class-25 { background-color: #686868; } .random-class-26 { background-color: #585858; } .random-class-27 { background-color: #484848; } .random-class-28 { background-color: #383838; } .random-class-29 { background-color: #282828; } .random-class-30 { background-color: #181818; } .random-class-31 { background-color: #080808; } .random-class-32 { background-color: #fefefe; } .random-class-33 { background-color: #ededed; } .random-class-34 { background-color: #dcdcdc; } .random-class-35 { background-color: #cbcbcb; } .random-class-36 { background-color: #bababa; } .random-class-37 { background-color: #a9a9a9; } .random-class-38 { background-color: #989898; } .random-class-39 { background-color: #878787; } .random-class-40 { background-color: #767676; } .random-class-41 { background-color: #656565; } .random-class-42 { background-color: #545454; } .random-class-43 { background-color: #434343; } .random-class-44 { background-color: #323232; } .random-class-45 { background-color: #212121; } .random-class-46 { background-color: #101010; } .random-class-47 { background-color: #f7f7f7; } .random-class-48 { background-color: #e6e6e6; } .random-class-49 { background-color: #d5d5d5; } .random-class-50 { background-color: #c4c4c4; } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/styles/base/base.scss: -------------------------------------------------------------------------------- ```scss // Extended and more meaningful classes .pill-with-badge { color: red; border: 1px solid #ccc; padding: 5px 10px; border-radius: 15px; display: inline-block; } .pill-with-badge-v2 { color: blue; border: 2px solid #aaa; padding: 6px 12px; border-radius: 20px; display: inline-block; } .sports-pill { color: green; background-color: #f0f0f0; padding: 8px 16px; border-radius: 25px; display: inline-block; } .offer-badge { color: yellow; background-color: #333; padding: 4px 8px; border-radius: 10px; display: inline-block; } .tab-nav { color: orange; background-color: #fff; padding: 10px; border-bottom: 2px solid #ddd; } .nav-tabs { color: purple; background-color: #eee; padding: 10px; border-bottom: 2px solid #ccc; } .tab-nav-item { color: pink; padding: 10px 15px; border-radius: 5px; display: inline-block; cursor: pointer; } .btn { color: brown; background-color: #f5f5f5; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; } .btn-primary { color: cyan; background-color: #007bff; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; } .legacy-button { color: magenta; background-color: #f8f9fa; padding: 10px 20px; border: 1px solid #ccc; border-radius: 5px; cursor: pointer; } .modal { color: lime; background-color: #fff; padding: 20px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } .card { color: olive; background-color: #f8f9fa; padding: 15px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .loading { color: teal; font-size: 16px; display: flex; align-items: center; justify-content: center; } .loading-v2 { color: navy; font-size: 18px; display: flex; align-items: center; justify-content: center; } .loading-v3 { color: maroon; font-size: 20px; display: flex; align-items: center; justify-content: center; } .collapsible-container { color: silver; background-color: #f0f0f0; padding: 10px; border-radius: 5px; overflow: hidden; } .divider { color: gray; border-top: 1px solid #ccc; margin: 10px 0; } .count { color: gold; background-color: #333; padding: 5px 10px; border-radius: 50%; display: inline-block; } .badge-circle { color: coral; background-color: #f0f0f0; padding: 5px 10px; border-radius: 50%; display: inline-block; } .custom-control-checkbox { color: khaki; display: flex; align-items: center; } .custom-control-radio { color: lavender; display: flex; align-items: center; } .form-control-tabs-segmented-v2 { color: salmon; background-color: #fff; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented-flex { color: sienna; background-color: #f8f9fa; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented-v2-dark { color: tan; background-color: #333; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented-v3 { color: turquoise; background-color: #fff; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented-v4 { color: violet; background-color: #f8f9fa; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented { color: wheat; background-color: #fff; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .custom-control-switcher { color: azure; display: flex; align-items: center; } // 50 more random classes .random-class-1 { background-color: #f0f0f0; } .random-class-2 { background-color: #e0e0e0; } .random-class-3 { background-color: #d0d0d0; } .random-class-4 { background-color: #c0c0c0; } .random-class-5 { background-color: #b0b0b0; } .random-class-6 { background-color: #a0a0a0; } .random-class-7 { background-color: #909090; } .random-class-8 { background-color: #808080; } .random-class-9 { background-color: #707070; } .random-class-10 { background-color: #606060; } .random-class-11 { background-color: #505050; } .random-class-12 { background-color: #404040; } .random-class-13 { background-color: #303030; } .random-class-14 { background-color: #202020; } .random-class-15 { background-color: #101010; } .random-class-16 { background-color: #f8f8f8; } .random-class-17 { background-color: #e8e8e8; } .random-class-18 { background-color: #d8d8d8; } .random-class-19 { background-color: #c8c8c8; } .random-class-20 { background-color: #b8b8b8; } .random-class-21 { background-color: #a8a8a8; } .random-class-22 { background-color: #989898; } .random-class-23 { background-color: #888888; } .random-class-24 { background-color: #787878; } .random-class-25 { background-color: #686868; } .random-class-26 { background-color: #585858; } .random-class-27 { background-color: #484848; } .random-class-28 { background-color: #383838; } .random-class-29 { background-color: #282828; } .random-class-30 { background-color: #181818; } .random-class-31 { background-color: #080808; } .random-class-32 { background-color: #fefefe; } .random-class-33 { background-color: #ededed; } .random-class-34 { background-color: #dcdcdc; } .random-class-35 { background-color: #cbcbcb; } .random-class-36 { background-color: #bababa; } .random-class-37 { background-color: #a9a9a9; } .random-class-38 { background-color: #989898; } .random-class-39 { background-color: #878787; } .random-class-40 { background-color: #767676; } .random-class-41 { background-color: #656565; } .random-class-42 { background-color: #545454; } .random-class-43 { background-color: #434343; } .random-class-44 { background-color: #323232; } .random-class-45 { background-color: #212121; } .random-class-46 { background-color: #101010; } .random-class-47 { background-color: #f7f7f7; } .random-class-48 { background-color: #e6e6e6; } .random-class-49 { background-color: #d5d5d5; } .random-class-50 { background-color: #c4c4c4; } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/styles/components/components.scss: -------------------------------------------------------------------------------- ```scss // Extended and more meaningful classes .pill-with-badge { color: red; border: 1px solid #ccc; padding: 5px 10px; border-radius: 15px; display: inline-block; } .pill-with-badge-v2 { color: blue; border: 2px solid #aaa; padding: 6px 12px; border-radius: 20px; display: inline-block; } .sports-pill { color: green; background-color: #f0f0f0; padding: 8px 16px; border-radius: 25px; display: inline-block; } .offer-badge { color: yellow; background-color: #333; padding: 4px 8px; border-radius: 10px; display: inline-block; } .tab-nav { color: orange; background-color: #fff; padding: 10px; border-bottom: 2px solid #ddd; } .nav-tabs { color: purple; background-color: #eee; padding: 10px; border-bottom: 2px solid #ccc; } .tab-nav-item { color: pink; padding: 10px 15px; border-radius: 5px; display: inline-block; cursor: pointer; } .btn { color: brown; background-color: #f5f5f5; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; } .btn-primary { color: cyan; background-color: #007bff; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; } .legacy-button { color: magenta; background-color: #f8f9fa; padding: 10px 20px; border: 1px solid #ccc; border-radius: 5px; cursor: pointer; } .modal { color: lime; background-color: #fff; padding: 20px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } .card { color: olive; background-color: #f8f9fa; padding: 15px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .loading { color: teal; font-size: 16px; display: flex; align-items: center; justify-content: center; } .loading-v2 { color: navy; font-size: 18px; display: flex; align-items: center; justify-content: center; } .loading-v3 { color: maroon; font-size: 20px; display: flex; align-items: center; justify-content: center; } .collapsible-container { color: silver; background-color: #f0f0f0; padding: 10px; border-radius: 5px; overflow: hidden; } .divider { color: gray; border-top: 1px solid #ccc; margin: 10px 0; } .count { color: gold; background-color: #333; padding: 5px 10px; border-radius: 50%; display: inline-block; } .badge-circle { color: coral; background-color: #f0f0f0; padding: 5px 10px; border-radius: 50%; display: inline-block; } .custom-control-checkbox { color: khaki; display: flex; align-items: center; } .custom-control-radio { color: lavender; display: flex; align-items: center; } .form-control-tabs-segmented-v2 { color: salmon; background-color: #fff; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented-flex { color: sienna; background-color: #f8f9fa; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented-v2-dark { color: tan; background-color: #333; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented-v3 { color: turquoise; background-color: #fff; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented-v4 { color: violet; background-color: #f8f9fa; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented { color: wheat; background-color: #fff; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .custom-control-switcher { color: azure; display: flex; align-items: center; } // 50 more random classes .random-class-1 { background-color: #f0f0f0; } .random-class-2 { background-color: #e0e0e0; } .random-class-3 { background-color: #d0d0d0; } .random-class-4 { background-color: #c0c0c0; } .random-class-5 { background-color: #b0b0b0; } .random-class-6 { background-color: #a0a0a0; } .random-class-7 { background-color: #909090; } .random-class-8 { background-color: #808080; } .random-class-9 { background-color: #707070; } .random-class-10 { background-color: #606060; } .random-class-11 { background-color: #505050; } .random-class-12 { background-color: #404040; } .random-class-13 { background-color: #303030; } .random-class-14 { background-color: #202020; } .random-class-15 { background-color: #101010; } .random-class-16 { background-color: #f8f8f8; } .random-class-17 { background-color: #e8e8e8; } .random-class-18 { background-color: #d8d8d8; } .random-class-19 { background-color: #c8c8c8; } .random-class-20 { background-color: #b8b8b8; } .random-class-21 { background-color: #a8a8a8; } .random-class-22 { background-color: #989898; } .random-class-23 { background-color: #888888; } .random-class-24 { background-color: #787878; } .random-class-25 { background-color: #686868; } .random-class-26 { background-color: #585858; } .random-class-27 { background-color: #484848; } .random-class-28 { background-color: #383838; } .random-class-29 { background-color: #282828; } .random-class-30 { background-color: #181818; } .random-class-31 { background-color: #080808; } .random-class-32 { background-color: #fefefe; } .random-class-33 { background-color: #ededed; } .random-class-34 { background-color: #dcdcdc; } .random-class-35 { background-color: #cbcbcb; } .random-class-36 { background-color: #bababa; } .random-class-37 { background-color: #a9a9a9; } .random-class-38 { background-color: #989898; } .random-class-39 { background-color: #878787; } .random-class-40 { background-color: #767676; } .random-class-41 { background-color: #656565; } .random-class-42 { background-color: #545454; } .random-class-43 { background-color: #434343; } .random-class-44 { background-color: #323232; } .random-class-45 { background-color: #212121; } .random-class-46 { background-color: #101010; } .random-class-47 { background-color: #f7f7f7; } .random-class-48 { background-color: #e6e6e6; } .random-class-49 { background-color: #d5d5d5; } .random-class-50 { background-color: #c4c4c4; } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/styles/layout/layout.scss: -------------------------------------------------------------------------------- ```scss // Extended and more meaningful classes .pill-with-badge { color: red; border: 1px solid #ccc; padding: 5px 10px; border-radius: 15px; display: inline-block; } .pill-with-badge-v2 { color: blue; border: 2px solid #aaa; padding: 6px 12px; border-radius: 20px; display: inline-block; } .sports-pill { color: green; background-color: #f0f0f0; padding: 8px 16px; border-radius: 25px; display: inline-block; } .offer-badge { color: yellow; background-color: #333; padding: 4px 8px; border-radius: 10px; display: inline-block; } .tab-nav { color: orange; background-color: #fff; padding: 10px; border-bottom: 2px solid #ddd; } .nav-tabs { color: purple; background-color: #eee; padding: 10px; border-bottom: 2px solid #ccc; } .tab-nav-item { color: pink; padding: 10px 15px; border-radius: 5px; display: inline-block; cursor: pointer; } .btn { color: brown; background-color: #f5f5f5; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; } .btn-primary { color: cyan; background-color: #007bff; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; } .legacy-button { color: magenta; background-color: #f8f9fa; padding: 10px 20px; border: 1px solid #ccc; border-radius: 5px; cursor: pointer; } .modal { color: lime; background-color: #fff; padding: 20px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } .card { color: olive; background-color: #f8f9fa; padding: 15px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .loading { color: teal; font-size: 16px; display: flex; align-items: center; justify-content: center; } .loading-v2 { color: navy; font-size: 18px; display: flex; align-items: center; justify-content: center; } .loading-v3 { color: maroon; font-size: 20px; display: flex; align-items: center; justify-content: center; } .collapsible-container { color: silver; background-color: #f0f0f0; padding: 10px; border-radius: 5px; overflow: hidden; } .divider { color: gray; border-top: 1px solid #ccc; margin: 10px 0; } .count { color: gold; background-color: #333; padding: 5px 10px; border-radius: 50%; display: inline-block; } .badge-circle { color: coral; background-color: #f0f0f0; padding: 5px 10px; border-radius: 50%; display: inline-block; } .custom-control-checkbox { color: khaki; display: flex; align-items: center; } .custom-control-radio { color: lavender; display: flex; align-items: center; } .form-control-tabs-segmented-v2 { color: salmon; background-color: #fff; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented-flex { color: sienna; background-color: #f8f9fa; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented-v2-dark { color: tan; background-color: #333; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented-v3 { color: turquoise; background-color: #fff; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented-v4 { color: violet; background-color: #f8f9fa; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented { color: wheat; background-color: #fff; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .custom-control-switcher { color: azure; display: flex; align-items: center; } // 50 more random classes .random-class-1 { background-color: #f0f0f0; } .random-class-2 { background-color: #e0e0e0; } .random-class-3 { background-color: #d0d0d0; } .random-class-4 { background-color: #c0c0c0; } .random-class-5 { background-color: #b0b0b0; } .random-class-6 { background-color: #a0a0a0; } .random-class-7 { background-color: #909090; } .random-class-8 { background-color: #808080; } .random-class-9 { background-color: #707070; } .random-class-10 { background-color: #606060; } .random-class-11 { background-color: #505050; } .random-class-12 { background-color: #404040; } .random-class-13 { background-color: #303030; } .random-class-14 { background-color: #202020; } .random-class-15 { background-color: #101010; } .random-class-16 { background-color: #f8f8f8; } .random-class-17 { background-color: #e8e8e8; } .random-class-18 { background-color: #d8d8d8; } .random-class-19 { background-color: #c8c8c8; } .random-class-20 { background-color: #b8b8b8; } .random-class-21 { background-color: #a8a8a8; } .random-class-22 { background-color: #989898; } .random-class-23 { background-color: #888888; } .random-class-24 { background-color: #787878; } .random-class-25 { background-color: #686868; } .random-class-26 { background-color: #585858; } .random-class-27 { background-color: #484848; } .random-class-28 { background-color: #383838; } .random-class-29 { background-color: #282828; } .random-class-30 { background-color: #181818; } .random-class-31 { background-color: #080808; } .random-class-32 { background-color: #fefefe; } .random-class-33 { background-color: #ededed; } .random-class-34 { background-color: #dcdcdc; } .random-class-35 { background-color: #cbcbcb; } .random-class-36 { background-color: #bababa; } .random-class-37 { background-color: #a9a9a9; } .random-class-38 { background-color: #989898; } .random-class-39 { background-color: #878787; } .random-class-40 { background-color: #767676; } .random-class-41 { background-color: #656565; } .random-class-42 { background-color: #545454; } .random-class-43 { background-color: #434343; } .random-class-44 { background-color: #323232; } .random-class-45 { background-color: #212121; } .random-class-46 { background-color: #101010; } .random-class-47 { background-color: #f7f7f7; } .random-class-48 { background-color: #e6e6e6; } .random-class-49 { background-color: #d5d5d5; } .random-class-50 { background-color: #c4c4c4; } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/styles/themes/themes.scss: -------------------------------------------------------------------------------- ```scss // Extended and more meaningful classes .pill-with-badge { color: red; border: 1px solid #ccc; padding: 5px 10px; border-radius: 15px; display: inline-block; } .pill-with-badge-v2 { color: blue; border: 2px solid #aaa; padding: 6px 12px; border-radius: 20px; display: inline-block; } .sports-pill { color: green; background-color: #f0f0f0; padding: 8px 16px; border-radius: 25px; display: inline-block; } .offer-badge { color: yellow; background-color: #333; padding: 4px 8px; border-radius: 10px; display: inline-block; } .tab-nav { color: orange; background-color: #fff; padding: 10px; border-bottom: 2px solid #ddd; } .nav-tabs { color: purple; background-color: #eee; padding: 10px; border-bottom: 2px solid #ccc; } .tab-nav-item { color: pink; padding: 10px 15px; border-radius: 5px; display: inline-block; cursor: pointer; } .btn { color: brown; background-color: #f5f5f5; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; } .btn-primary { color: cyan; background-color: #007bff; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; } .legacy-button { color: magenta; background-color: #f8f9fa; padding: 10px 20px; border: 1px solid #ccc; border-radius: 5px; cursor: pointer; } .modal { color: lime; background-color: #fff; padding: 20px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } .card { color: olive; background-color: #f8f9fa; padding: 15px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .loading { color: teal; font-size: 16px; display: flex; align-items: center; justify-content: center; } .loading-v2 { color: navy; font-size: 18px; display: flex; align-items: center; justify-content: center; } .loading-v3 { color: maroon; font-size: 20px; display: flex; align-items: center; justify-content: center; } .collapsible-container { color: silver; background-color: #f0f0f0; padding: 10px; border-radius: 5px; overflow: hidden; } .divider { color: gray; border-top: 1px solid #ccc; margin: 10px 0; } .count { color: gold; background-color: #333; padding: 5px 10px; border-radius: 50%; display: inline-block; } .badge-circle { color: coral; background-color: #f0f0f0; padding: 5px 10px; border-radius: 50%; display: inline-block; } .custom-control-checkbox { color: khaki; display: flex; align-items: center; } .custom-control-radio { color: lavender; display: flex; align-items: center; } .form-control-tabs-segmented-v2 { color: salmon; background-color: #fff; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented-flex { color: sienna; background-color: #f8f9fa; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented-v2-dark { color: tan; background-color: #333; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented-v3 { color: turquoise; background-color: #fff; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented-v4 { color: violet; background-color: #f8f9fa; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented { color: wheat; background-color: #fff; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .custom-control-switcher { color: azure; display: flex; align-items: center; } // 50 more random classes .random-class-1 { background-color: #f0f0f0; } .random-class-2 { background-color: #e0e0e0; } .random-class-3 { background-color: #d0d0d0; } .random-class-4 { background-color: #c0c0c0; } .random-class-5 { background-color: #b0b0b0; } .random-class-6 { background-color: #a0a0a0; } .random-class-7 { background-color: #909090; } .random-class-8 { background-color: #808080; } .random-class-9 { background-color: #707070; } .random-class-10 { background-color: #606060; } .random-class-11 { background-color: #505050; } .random-class-12 { background-color: #404040; } .random-class-13 { background-color: #303030; } .random-class-14 { background-color: #202020; } .random-class-15 { background-color: #101010; } .random-class-16 { background-color: #f8f8f8; } .random-class-17 { background-color: #e8e8e8; } .random-class-18 { background-color: #d8d8d8; } .random-class-19 { background-color: #c8c8c8; } .random-class-20 { background-color: #b8b8b8; } .random-class-21 { background-color: #a8a8a8; } .random-class-22 { background-color: #989898; } .random-class-23 { background-color: #888888; } .random-class-24 { background-color: #787878; } .random-class-25 { background-color: #686868; } .random-class-26 { background-color: #585858; } .random-class-27 { background-color: #484848; } .random-class-28 { background-color: #383838; } .random-class-29 { background-color: #282828; } .random-class-30 { background-color: #181818; } .random-class-31 { background-color: #080808; } .random-class-32 { background-color: #fefefe; } .random-class-33 { background-color: #ededed; } .random-class-34 { background-color: #dcdcdc; } .random-class-35 { background-color: #cbcbcb; } .random-class-36 { background-color: #bababa; } .random-class-37 { background-color: #a9a9a9; } .random-class-38 { background-color: #989898; } .random-class-39 { background-color: #878787; } .random-class-40 { background-color: #767676; } .random-class-41 { background-color: #656565; } .random-class-42 { background-color: #545454; } .random-class-43 { background-color: #434343; } .random-class-44 { background-color: #323232; } .random-class-45 { background-color: #212121; } .random-class-46 { background-color: #101010; } .random-class-47 { background-color: #f7f7f7; } .random-class-48 { background-color: #e6e6e6; } .random-class-49 { background-color: #d5d5d5; } .random-class-50 { background-color: #c4c4c4; } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/styles/utilities/utilities.scss: -------------------------------------------------------------------------------- ```scss // Extended and more meaningful classes .pill-with-badge { color: red; border: 1px solid #ccc; padding: 5px 10px; border-radius: 15px; display: inline-block; } .pill-with-badge-v2 { color: blue; border: 2px solid #aaa; padding: 6px 12px; border-radius: 20px; display: inline-block; } .sports-pill { color: green; background-color: #f0f0f0; padding: 8px 16px; border-radius: 25px; display: inline-block; } .offer-badge { color: yellow; background-color: #333; padding: 4px 8px; border-radius: 10px; display: inline-block; } .tab-nav { color: orange; background-color: #fff; padding: 10px; border-bottom: 2px solid #ddd; } .nav-tabs { color: purple; background-color: #eee; padding: 10px; border-bottom: 2px solid #ccc; } .tab-nav-item { color: pink; padding: 10px 15px; border-radius: 5px; display: inline-block; cursor: pointer; } .btn { color: brown; background-color: #f5f5f5; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; } .btn-primary { color: cyan; background-color: #007bff; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; } .legacy-button { color: magenta; background-color: #f8f9fa; padding: 10px 20px; border: 1px solid #ccc; border-radius: 5px; cursor: pointer; } .modal { color: lime; background-color: #fff; padding: 20px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } .card { color: olive; background-color: #f8f9fa; padding: 15px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .loading { color: teal; font-size: 16px; display: flex; align-items: center; justify-content: center; } .loading-v2 { color: navy; font-size: 18px; display: flex; align-items: center; justify-content: center; } .loading-v3 { color: maroon; font-size: 20px; display: flex; align-items: center; justify-content: center; } .collapsible-container { color: silver; background-color: #f0f0f0; padding: 10px; border-radius: 5px; overflow: hidden; } .divider { color: gray; border-top: 1px solid #ccc; margin: 10px 0; } .count { color: gold; background-color: #333; padding: 5px 10px; border-radius: 50%; display: inline-block; } .badge-circle { color: coral; background-color: #f0f0f0; padding: 5px 10px; border-radius: 50%; display: inline-block; } .custom-control-checkbox { color: khaki; display: flex; align-items: center; } .custom-control-radio { color: lavender; display: flex; align-items: center; } .form-control-tabs-segmented-v2 { color: salmon; background-color: #fff; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented-flex { color: sienna; background-color: #f8f9fa; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented-v2-dark { color: tan; background-color: #333; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented-v3 { color: turquoise; background-color: #fff; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented-v4 { color: violet; background-color: #f8f9fa; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .form-control-tabs-segmented { color: wheat; background-color: #fff; padding: 10px; border-radius: 5px; display: flex; justify-content: space-between; } .custom-control-switcher { color: azure; display: flex; align-items: center; } // 50 more random classes .random-class-1 { background-color: #f0f0f0; } .random-class-2 { background-color: #e0e0e0; } .random-class-3 { background-color: #d0d0d0; } .random-class-4 { background-color: #c0c0c0; } .random-class-5 { background-color: #b0b0b0; } .random-class-6 { background-color: #a0a0a0; } .random-class-7 { background-color: #909090; } .random-class-8 { background-color: #808080; } .random-class-9 { background-color: #707070; } .random-class-10 { background-color: #606060; } .random-class-11 { background-color: #505050; } .random-class-12 { background-color: #404040; } .random-class-13 { background-color: #303030; } .random-class-14 { background-color: #202020; } .random-class-15 { background-color: #101010; } .random-class-16 { background-color: #f8f8f8; } .random-class-17 { background-color: #e8e8e8; } .random-class-18 { background-color: #d8d8d8; } .random-class-19 { background-color: #c8c8c8; } .random-class-20 { background-color: #b8b8b8; } .random-class-21 { background-color: #a8a8a8; } .random-class-22 { background-color: #989898; } .random-class-23 { background-color: #888888; } .random-class-24 { background-color: #787878; } .random-class-25 { background-color: #686868; } .random-class-26 { background-color: #585858; } .random-class-27 { background-color: #484848; } .random-class-28 { background-color: #383838; } .random-class-29 { background-color: #282828; } .random-class-30 { background-color: #181818; } .random-class-31 { background-color: #080808; } .random-class-32 { background-color: #fefefe; } .random-class-33 { background-color: #ededed; } .random-class-34 { background-color: #dcdcdc; } .random-class-35 { background-color: #cbcbcb; } .random-class-36 { background-color: #bababa; } .random-class-37 { background-color: #a9a9a9; } .random-class-38 { background-color: #989898; } .random-class-39 { background-color: #878787; } .random-class-40 { background-color: #767676; } .random-class-41 { background-color: #656565; } .random-class-42 { background-color: #545454; } .random-class-43 { background-color: #434343; } .random-class-44 { background-color: #323232; } .random-class-45 { background-color: #212121; } .random-class-46 { background-color: #101010; } .random-class-47 { background-color: #f7f7f7; } .random-class-48 { background-color: #e6e6e6; } .random-class-49 { background-color: #d5d5d5; } .random-class-50 { background-color: #c4c4c4; } ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component/get-ds-component-data.tool.ts: -------------------------------------------------------------------------------- ```typescript import { ToolSchemaOptions } from '@push-based/models'; import { createHandler, BaseHandlerOptions, } from '../shared/utils/handler-helpers.js'; import { COMMON_ANNOTATIONS } from '../shared/models/schema-helpers.js'; import { getComponentPathsInfo } from './utils/paths-helpers.js'; import { getComponentDocPathsForName } from './utils/doc-helpers.js'; import { validateComponentName, componentNameToKebabCase, } from '../shared/utils/component-validation.js'; import { resolveCrossPlatformPath } from '../shared/utils/cross-platform-path.js'; import * as fs from 'fs'; import * as path from 'path'; interface DsComponentDataOptions extends BaseHandlerOptions { componentName: string; sections?: string[]; } interface DsComponentData { componentName: string; implementation: string[]; documentation: string[]; stories: string[]; importPath: string; } export const getDsComponentDataToolSchema: ToolSchemaOptions = { name: 'get-ds-component-data', description: `Return comprehensive data for a DS component including implementation files, documentation files, stories files, and import path.`, inputSchema: { type: 'object', properties: { componentName: { type: 'string', description: 'The class name of the component to get data for (e.g., DsBadge)', }, sections: { type: 'array', items: { type: 'string', enum: ['implementation', 'documentation', 'stories', 'all'], }, description: 'Sections to include in the response. Options: "implementation", "documentation", "stories", "all". Defaults to ["all"] if not specified.', default: ['all'], }, }, required: ['componentName'], }, annotations: { title: 'Get Design System Component Data', ...COMMON_ANNOTATIONS.readOnly, }, }; function getAllFilesInDirectory(dirPath: string): string[] { const files: string[] = []; function walkDirectory(currentPath: string) { try { const items = fs.readdirSync(currentPath); for (const item of items) { const fullPath = path.join(currentPath, item); const stat = fs.statSync(fullPath); if (stat.isDirectory()) { walkDirectory(fullPath); } else { files.push(fullPath); } } } catch { return; } } if (fs.existsSync(dirPath)) { walkDirectory(dirPath); } return files; } function findStoriesFiles(componentPath: string): string[] { const storiesFiles: string[] = []; try { if (fs.existsSync(componentPath)) { const items = fs.readdirSync(componentPath); for (const item of items) { const fullPath = path.join(componentPath, item); const stat = fs.statSync(fullPath); if (stat.isFile() && item.endsWith('.stories.ts')) { storiesFiles.push(fullPath); } } } } catch { return storiesFiles; } return storiesFiles; } export const getDsComponentDataHandler = createHandler< DsComponentDataOptions, DsComponentData >( getDsComponentDataToolSchema.name, async ( { componentName, sections = ['all'] }, { cwd, uiRoot, storybookDocsRoot }, ) => { try { validateComponentName(componentName); const includeAll = sections.includes('all'); const includeImplementation = includeAll || sections.includes('implementation'); const includeDocumentation = includeAll || sections.includes('documentation'); const includeStories = includeAll || sections.includes('stories'); const pathsInfo = getComponentPathsInfo(componentName, uiRoot, cwd); let implementationFiles: string[] = []; if (includeImplementation) { const srcFiles = getAllFilesInDirectory(pathsInfo.srcPath); implementationFiles = srcFiles.map((file) => `file://${file}`); } const documentationFiles: string[] = []; let storiesFilePaths: string[] = []; if (storybookDocsRoot) { const docsBasePath = resolveCrossPlatformPath(cwd, storybookDocsRoot); if (includeDocumentation) { const docPaths = getComponentDocPathsForName( docsBasePath, componentName, ); if (fs.existsSync(docPaths.paths.api)) { documentationFiles.push(`file://${docPaths.paths.api}`); } if (fs.existsSync(docPaths.paths.overview)) { documentationFiles.push(`file://${docPaths.paths.overview}`); } } if (includeStories) { const componentFolderName = componentNameToKebabCase(componentName); const storiesComponentFolderPath = path.join( docsBasePath, componentFolderName, ); const storiesFiles = findStoriesFiles(storiesComponentFolderPath); storiesFilePaths = storiesFiles.map((file) => `file://${file}`); } } return { componentName, implementation: implementationFiles, documentation: documentationFiles, stories: storiesFilePaths, importPath: pathsInfo.importPath, }; } catch (ctx) { throw new Error( `Error retrieving component data: ${(ctx as Error).message}`, ); } }, (result) => { const messages: string[] = []; if (result.implementation && result.implementation.length > 0) { messages.push('Implementation'); messages.push(''); result.implementation.forEach((file: string) => { messages.push(file); }); messages.push(''); } if (result.documentation && result.documentation.length > 0) { messages.push('Documentation'); messages.push(''); result.documentation.forEach((file: string) => { messages.push(file); }); messages.push(''); } if (result.stories && result.stories.length > 0) { messages.push('Stories'); messages.push(''); result.stories.forEach((file: string) => { messages.push(file); }); messages.push(''); } if (result.importPath) { messages.push('Import path'); messages.push(''); messages.push(result.importPath); } return messages; }, ); export const getDsComponentDataTools = [ { schema: getDsComponentDataToolSchema, handler: getDsComponentDataHandler, }, ]; ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/project/utils/dependencies-helpers.ts: -------------------------------------------------------------------------------- ```typescript import * as fs from 'fs'; import * as path from 'path'; import process from 'node:process'; import { getComponentPathsInfo } from '../../component/utils/paths-helpers.js'; import { resolveCrossPlatformPathAndValidateWithContext } from '../../shared/utils/cross-platform-path.js'; // Type definitions export interface PackageJsonPeerDependencies { [packageName: string]: string; } export interface PackageJson { name?: string; version?: string; peerDependencies?: PackageJsonPeerDependencies; dependencies?: Record<string, string>; devDependencies?: Record<string, string>; [key: string]: unknown; } export interface ProjectAnalysisResult { packageJsonFound: true; packageJsonPath: string; projectJsonPath: string | null; isPublishable: boolean; peerDependencies: PackageJsonPeerDependencies; peerDependencyMissing: boolean; importPath?: string; message?: string; suggestedChange?: { importPath: string; message: string; }; } export interface ProjectAnalysisNotFoundResult { packageJsonFound: false; searchedPath: string; message: string; } export type ProjectAnalysisResponse = | ProjectAnalysisResult | ProjectAnalysisNotFoundResult; export interface ComponentMetadata { importPath?: string; [key: string]: unknown; } /** * Finds package.json by traversing up the directory tree */ export function findPackageJson(startPath: string): string | null { let currentPath = path.resolve(startPath); const rootPath = path.parse(currentPath).root; while (currentPath !== rootPath) { const packageJsonPath = path.join(currentPath, 'package.json'); if (fs.existsSync(packageJsonPath)) { return packageJsonPath; } currentPath = path.dirname(currentPath); } return null; } /** * Finds project.json in the same directory as package.json or nearby */ export function findProjectJson(packageJsonDir: string): string | null { const projectJsonPath = path.join(packageJsonDir, 'project.json'); if (fs.existsSync(projectJsonPath)) { return projectJsonPath; } return null; } /** * Reads and parses package.json file */ export function readPackageJson(packageJsonPath: string): PackageJson { try { const content = fs.readFileSync(packageJsonPath, 'utf-8'); return JSON.parse(content) as PackageJson; } catch (ctx) { throw new Error( `Failed to read or parse package.json at ${packageJsonPath}: ${ (ctx as Error).message }`, ); } } /** * Checks if the library has peer dependencies (indicating it's buildable/publishable) */ export function hasPeerDependencies(packageJson: PackageJson): boolean { return ( packageJson.peerDependencies !== undefined && typeof packageJson.peerDependencies === 'object' && Object.keys(packageJson.peerDependencies).length > 0 ); } /** * Gets import path from component metadata */ export async function getComponentImportPath( componentName: string, cwd: string, uiRoot: string, ): Promise<string | null> { try { const pathsInfo = getComponentPathsInfo(componentName, uiRoot, cwd); return pathsInfo.importPath || null; } catch { return null; } } /** * Builds the base project analysis result from package.json and project.json */ export function buildProjectAnalysisResult( cwd: string, packageJsonPath: string, packageJson: PackageJson, ): ProjectAnalysisResult { const packageJsonDir = path.dirname(packageJsonPath); const hasPackagePeerDeps = hasPeerDependencies(packageJson); const projectJsonPath = findProjectJson(packageJsonDir); return { packageJsonFound: true, packageJsonPath: path.relative(cwd, packageJsonPath), projectJsonPath: projectJsonPath ? path.relative(cwd, projectJsonPath) : null, isPublishable: hasPackagePeerDeps, peerDependencies: packageJson.peerDependencies || {}, peerDependencyMissing: false, }; } /** * Handles peer dependencies analysis and component import path validation */ export async function handlePeerDependenciesAnalysis( result: ProjectAnalysisResult, componentName?: string, cwd?: string, uiRoot?: string, ): Promise<void> { if (!result.isPublishable) { result.message = 'Library has no peer dependencies - appears to be a normal library'; return; } if (!componentName) { result.message = 'Library has peer dependencies (publishable/buildable). Provide componentName to validate import path.'; return; } if (!cwd) { result.message = 'CWD is required for component import path validation.'; return; } if (!uiRoot) { result.message = 'UI root is required for component import path validation.'; return; } // Try to get import path for the component const importPath = await getComponentImportPath(componentName, cwd, uiRoot); if (!importPath || importPath.trim() === '') { result.peerDependencyMissing = true; result.suggestedChange = { importPath: '*', message: 'Component import path is missing or empty. This is required for publishable libraries.', }; } else { result.importPath = importPath; result.message = 'Component import path found - library appears properly configured'; } } /** * Analyzes project dependencies and determines if library is buildable/publishable */ export async function analyzeProjectDependencies( cwd: string, directory: string, componentName?: string, workspaceRoot?: string, uiRoot?: string, ): Promise<ProjectAnalysisResponse> { // Parameter validation if (!directory || typeof directory !== 'string') { throw new Error('Directory parameter is required and must be a string'); } // Set working directory process.chdir(cwd); // Validate target path exists const targetPath = resolveCrossPlatformPathAndValidateWithContext( cwd, directory, workspaceRoot, ); // Find package.json const packageJsonPath = findPackageJson(targetPath); if (!packageJsonPath) { return { packageJsonFound: false, searchedPath: targetPath, message: 'No package.json found in the directory tree', }; } // Read and parse package.json const packageJson = readPackageJson(packageJsonPath); // Build base result const result = buildProjectAnalysisResult(cwd, packageJsonPath, packageJson); // Handle peer dependencies analysis await handlePeerDependenciesAnalysis(result, componentName, cwd, uiRoot); return result; } ```