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