This is page 8 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 -------------------------------------------------------------------------------- /docs/contracts.md: -------------------------------------------------------------------------------- ```markdown 1 | # The component contracts system serves two primary purposes: 2 | - **Breaking change detection** during refactoring and validation 3 | - **Documentation** of successful refactoring patterns for future reference 4 | 5 | ## Table of Contents 6 | 1. [File Organization & Storage Structure](#file-organization--storage-structure) — where contracts and diffs live 7 | 2. [What a Contract Includes](#what-a-contract-includes) — data captured in a snapshot 8 | 3. [When to Build a Contract](#when-to-build-a-contract) — baseline vs validation 9 | 4. [When to Generate Diffs](#when-to-generate-diffs) — automated and manual diffing 10 | 5. [Integration with Refactoring Rules Workflow](#integration-with-refactoring-rules-workflow) — contracts in 5-step automation 11 | 6. [Post-Review & Hot-Fix Workflow](#post-review--hot-fix-workflow) — fixing bugs after validation 12 | 7. [Building a Refactoring Knowledge Base](#building-a-refactoring-knowledge-base) — storing proven patterns 13 | 14 | ## File Organization & Storage Structure 15 | 16 | Contracts are organised in a predictable, component-scoped directory tree: 17 | 18 | ``` 19 | .cursor/tmp/contracts/ 20 | ├── badge/ # Component-specific directory 21 | │ ├── ui-header-20250703T185531Z.contract.json 22 | │ ├── ui-header-20250703T192519Z.contract.json 23 | │ └── diffs/ # Diff files subdirectory 24 | │ └── diff-header-20250703T194046Z.json 25 | ├── button/ 26 | │ ├── feature-modal-20250704T120000Z.contract.json 27 | │ └── diffs/ 28 | └── input/ 29 | ├── login-form-20250705T090000Z.contract.json 30 | └── diffs/ 31 | ``` 32 | 33 | **Key Highlights:** 34 | - **Component-scoped directories** 35 | - **Timestamped contract files** 36 | - **Component-specific diff folders** 37 | - **Predictable naming** (`diff-<component>-<timestamp>.json`) 38 | 39 | ## What a Contract Includes 40 | 41 | Each contract captures a comprehensive snapshot of a component: 42 | 43 | ### Public API 44 | - **Properties**: Input/output properties with types and default values 45 | - **Events**: EventEmitter declarations and their types 46 | - **Methods**: Public methods with parameters and return types 47 | - **Lifecycle hooks**: Implemented Angular lifecycle interfaces 48 | - **Imports**: All imported dependencies and their sources 49 | 50 | ### DOM Structure 51 | - **Element hierarchy**: Complete DOM tree with parent-child relationships 52 | - **Attributes**: Static and dynamic attributes on elements 53 | - **Bindings**: Property bindings, event handlers, and structural directives 54 | - **Content projection**: ng-content slots and their selectors 55 | 56 | ### Styles 57 | - **CSS rules**: All styles applied to DOM elements 58 | - **Element mapping**: Which styles apply to which DOM elements 59 | - **Source tracking**: Whether styles come from component files or external stylesheets 60 | 61 | Because contracts track every public-facing facet of a component, any refactor that breaks its API or behaviour is flagged immediately. 62 | 63 | ## How do I build a contract? 64 | 65 | Rules taking care about the contract building during the workflow, but if you need to build it "manually" say in the chat: 66 | 67 | ``` 68 | build_component_contract(saveLocation, typescriptFile, templateFile?, styleFile?, dsComponentName?) 69 | ``` 70 | 71 | > Replace the parameters with: 72 | > - `saveLocation`: Path where to save the contract file (supports absolute and relative paths) 73 | > - `typescriptFile`: Path to the TypeScript component file (.ts) — **Required** 74 | > - `templateFile`: *(Optional)* Path to the component template file (.html). Omit for inline templates 75 | > - `styleFile`: *(Optional)* Path to the component style file (.scss, .css, etc.). Omit for inline styles or components without styles 76 | > - `dsComponentName`: *(Optional)* Design system component name (e.g., `DsBadge`) 77 | > 78 | > The tool analyses the template, TypeScript, and styles, then saves the contract to your specified location. 79 | > 80 | > **Note**: Angular components can have inline templates and styles. If `templateFile` or `styleFile` are not provided, the tool will extract inline template/styles from the TypeScript file. 81 | 82 | ## When to Build a Contract 83 | 84 | ### Core Principle 85 | 86 | **Build a new contract whenever a component changes.** This ensures you have an accurate snapshot of the component's state at each critical point in its evolution. 87 | 88 | ### Pre-Refactoring Contract (Baseline) 89 | 90 | Always build an initial contract **before** starting any refactoring work. This creates your baseline for comparison. 91 | 92 | - **Automated workflow**: Handled automatically by the refactoring rules (see `03-fix-violations.mdc`) 93 | - **Manual workflow**: Build the contract manually before making any changes 94 | 95 | ### Post-Refactoring Contract (Validation) 96 | 97 | Build a new contract **after** completing your refactoring work to capture the final state. 98 | 99 | - **Automated workflow**: Generated during the validation phase (see `04-validate-changes.mdc`) 100 | - **Manual workflow**: Build the contract after completing all changes 101 | 102 | ### Multiple Contract States 103 | 104 | For workflows involving QA, E2E testing, or UAT phases, build additional contracts if the component changes during or after testing. 105 | 106 | **Best practice**: Create a new contract for each significant milestone where the component is modified. 107 | 108 | ## When to Generate Diffs 109 | 110 | ### Automated Workflow Diffs 111 | 112 | Contract diffs are automatically generated during the validation phase (see `04-validate-changes.mdc`). These diffs enable AI-powered validation of refactoring changes. 113 | 114 | ### Manual Workflow Diffs 115 | 116 | When refactoring manually or outside the automated rules workflow, generate diffs to: 117 | - Identify breaking or risky changes 118 | - Get AI analysis of the refactoring impact 119 | - Validate that changes meet expectations 120 | 121 | ## Integration with Refactoring Rules Workflow 122 | 123 | The contracts system is fully integrated into the 5-step refactoring workflow: 124 | 125 | ### Step 1: Find Violations (`01-find-violations.mdc`) 126 | - **Purpose**: Identify legacy component usage across the codebase 127 | - **Contract role**: No contracts generated at this stage 128 | - **Output**: Ranked list of folders and files with violations 129 | 130 | ### Step 2: Plan Refactoring (`02-plan-refactoring.mdc`) 131 | - **Purpose**: Create detailed migration plan for each affected component 132 | - **Contract role**: Analysis of component structure informs refactoring strategy 133 | - **Output**: Comprehensive migration plan with complexity scoring 134 | 135 | **Note on Non-Viable Cases**: When components are identified as non-viable during step 2 and developer approval is obtained, the non-viable cases handling (`03-non-viable-cases.mdc`) is used instead of proceeding to steps 3-5. This handling does not use contracts as it maintains existing component structure while marking components for exclusion from future reports. 136 | 137 | ### Step 3: Fix Violations (`03-fix-violations.mdc`) 138 | - **Purpose**: Execute the refactoring plan systematically 139 | - **Contract role**: **Pre-refactoring contracts are automatically generated** for each component before changes begin 140 | - **Key integration**: `build_component_contract(component_files, dsComponentName)` creates baseline contracts 141 | - **Output**: Refactored components with baseline contracts stored 142 | 143 | ### Step 4: Validate Changes (`04-validate-changes.mdc`) 144 | - **Purpose**: Verify refactoring safety and detect breaking changes 145 | - **Contract role**: **Post-refactoring contracts are generated and compared** against baselines 146 | - **Key integration**: 147 | - `build_component_contract()` captures refactored state 148 | - `diff_component_contract()` compares before/after contracts 149 | - AI analyzes diffs for breaking changes and risks 150 | - **Output**: Validation report highlighting risky changes 151 | 152 | ### Step 5: Prepare Report (`05-prepare-report.mdc`) 153 | - **Purpose**: Generate testing checklists and documentation 154 | - **Contract role**: Contract diffs inform testing requirements and risk assessment 155 | - **Output**: Role-specific testing checklists and commit documentation 156 | 157 | ## Post-Review & Hot-Fix Workflow 158 | 159 | What happens when QA finds a bug or a reviewer requests changes **after** the initial refactor has already been validated? 160 | 161 | 1. **Apply the Code Fix** – attach the changed component file(s). 162 | 2. **Re-build the "latest" contract** 163 | ``` 164 | User: build_component_contract(<changed-file.ts>, dsComponentName) 165 | ``` 166 | The new snapshot will be stored alongside the previous ones in 167 | `.cursor/tmp/contracts/<ds-component-kebab>/`. 168 | 3. **Locate the original baseline contract** – this is the contract that was captured for the initial state (usually the very first timestamp in the folder). 169 | 4. **Generate a diff** between the baseline and the latest contract: 170 | ``` 171 | User: diff_component_contract(saveLocation, contractBeforePath, contractAfterPath, dsComponentName) 172 | ``` 173 | > Replace the parameters with: 174 | > - `saveLocation`: Path where to save the diff result file (supports absolute and relative paths) 175 | > - `contractBeforePath`: Path to the baseline contract file 176 | > - `contractAfterPath`: Path to the latest contract file 177 | > - `dsComponentName`: Optional design system component name 178 | 5. **Review the diff output using AI** – attach the diff and ask it to analyze it. 179 | * If only intentional changes appear, proceed to merge / re-test. 180 | * If unexpected API, DOM, or style changes surface, iterate on the fix and repeat steps 1-4. 181 | 182 | ### Why keep the original baseline? 183 | 184 | Diffing against the **first** snapshot ensures you do not 185 | inadvertently mask breaking changes introduced during multiple fix cycles. 186 | 187 | ### Tip: Cleaning up old snapshots 188 | 189 | Once a hot-fix is approved, you may delete intermediate contract and diff files to 190 | reduce noise and keep the folder tidy – leave the original baseline, final state and the 191 | latest approved diff. You can manually move it to `.cursor/tmp/patterns/ds-component-name`. 192 | 193 | ## Building a Refactoring Knowledge Base 194 | 195 | Consider storing successful diffs (that passed all checks and testing) to build a knowledge base of proven refactoring patterns. This is particularly valuable when: 196 | 197 | - Refactoring hundreds or thousands of similar components 198 | - Establishing team standards for common refactoring scenarios 199 | - Training new team members on safe refactoring practices 200 | - Automating similar refactoring tasks in the future 201 | ``` -------------------------------------------------------------------------------- /docs/tools.md: -------------------------------------------------------------------------------- ```markdown 1 | # Design System Tools for AI Agents 2 | 3 | This document provides comprehensive guidance for AI agents working with Angular Design System (DS) migration and analysis tools. Each tool is designed to support automated refactoring, validation, and analysis workflows. 4 | 5 | ## Tool Categories 6 | 7 | ### 🔍 Project Analysis Tools 8 | 9 | #### `report-violations` 10 | **Purpose**: Identifies deprecated DS CSS usage patterns in Angular projects 11 | **AI Usage**: Use as the first step in migration workflows to identify all violations before planning refactoring 12 | **Key Parameters**: 13 | - `directory`: Target analysis directory (use relative paths like `./src/app`) 14 | - `componentName`: DS component class name (e.g., `DsButton`) 15 | - `groupBy`: `"file"` or `"folder"` for result organization 16 | **Output**: Structured violation reports grouped by file or folder 17 | **Best Practice**: Always run this before other migration tools to establish baseline 18 | 19 | #### `report-all-violations` 20 | **Purpose**: Reports all deprecated DS CSS usage for every DS component within a directory 21 | **AI Usage**: Use for a fast, global inventory of violations across the codebase before narrowing to specific components 22 | **Key Parameters**: 23 | - `directory`: Target analysis directory (use relative paths like `./src/app`) 24 | - `groupBy`: `"file"` or `"folder"` for result organization (default: `"file"`) 25 | **Output**: Structured violation reports grouped by file or folder covering all DS components 26 | **Best Practice**: Use to discover all violations and establish the baseline for subsequent refactoring. 27 | 28 | #### `get-project-dependencies` 29 | **Purpose**: Analyzes project structure, dependencies, and buildability 30 | **AI Usage**: Validate project architecture before suggesting refactoring strategies 31 | **Key Parameters**: 32 | - `directory`: Project directory to analyze 33 | - `componentName`: Optional DS component for import path validation 34 | **Output**: Dependency analysis, buildable/publishable status, peer dependencies 35 | **Best Practice**: Use to understand project constraints before recommending changes 36 | 37 | #### `report-deprecated-css` 38 | **Purpose**: Scans styling files for deprecated CSS classes 39 | **AI Usage**: Complement violation reports with style-specific analysis 40 | **Key Parameters**: 41 | - `directory`: Directory containing style files 42 | - `componentName`: Target DS component 43 | **Output**: List of deprecated CSS classes found in stylesheets 44 | **Best Practice**: Run after `report-violations` for comprehensive CSS analysis 45 | 46 | ### 📚 Component Information Tools 47 | 48 | #### `list-ds-components` 49 | **Purpose**: Lists all available Design System components in the project with their file paths and metadata 50 | **AI Usage**: Discover available DS components before starting migration or analysis workflows 51 | **Key Parameters**: 52 | - `sections`: Array of sections to include - `"implementation"`, `"documentation"`, `"stories"`, or `"all"` (default: `["all"]`) 53 | **Output**: Complete inventory of DS components with their implementation files, documentation files, stories files, and import paths 54 | **Best Practice**: Use as the first step to understand the DS component landscape before targeted analysis 55 | 56 | #### `get-ds-component-data` 57 | **Purpose**: Returns comprehensive data for a specific DS component including implementation files, documentation files, stories files, and import path 58 | **AI Usage**: Get detailed information about a specific component for analysis or migration planning 59 | **Key Parameters**: 60 | - `componentName`: DS component class name (e.g., `DsBadge`) 61 | - `sections`: Array of sections to include - `"implementation"`, `"documentation"`, `"stories"`, or `"all"` (default: `["all"]`) 62 | **Output**: Structured data with file paths for implementation, documentation, stories, and import information 63 | **Best Practice**: Use selective sections to optimize performance when you only need specific types of files 64 | 65 | #### `get-component-docs` 66 | **Purpose**: Retrieves MDX documentation for DS components 67 | **AI Usage**: Access official component documentation to understand proper usage patterns 68 | **Key Parameters**: 69 | - `componentName`: DS component class name (e.g., `DsButton`) 70 | **Output**: API documentation and usage examples in MDX format 71 | **Best Practice**: Always consult docs before suggesting component usage changes 72 | 73 | #### `get-component-paths` 74 | **Purpose**: Provides filesystem and NPM import paths for DS components 75 | **AI Usage**: Verify correct import paths when suggesting code changes 76 | **Key Parameters**: 77 | - `componentName`: DS component class name 78 | **Output**: Source directory path and NPM import path 79 | **Best Practice**: Use to ensure accurate import statements in generated code 80 | 81 | #### `get-deprecated-css-classes` 82 | **Purpose**: Lists deprecated CSS classes for specific DS components 83 | **AI Usage**: Understand what CSS classes to avoid or replace during migration 84 | **Key Parameters**: 85 | - `componentName`: DS component class name 86 | **Output**: Array of deprecated CSS class names 87 | **Best Practice**: Cross-reference with violation reports to prioritize fixes 88 | 89 | ### 🔗 Analysis & Mapping Tools 90 | 91 | #### `build-component-usage-graph` 92 | **Purpose**: Maps component usage across modules, specs, templates, and styles 93 | **AI Usage**: Understand component dependencies before refactoring to avoid breaking changes 94 | **Key Parameters**: 95 | - `directory`: Root directory for analysis 96 | - `violationFiles`: Array of files with violations (from `report-violations`) 97 | **Output**: Component usage graph showing all import relationships 98 | **Best Practice**: Essential for large refactoring projects to map impact scope 99 | 100 | #### `lint-changes` 101 | **Purpose**: Runs ESLint validation on changed Angular files 102 | **AI Usage**: Validate code quality after making automated changes 103 | **Key Parameters**: 104 | - `directory`: Root directory containing components 105 | - `files`: Optional list of changed files 106 | - `configPath`: Optional ESLint config path 107 | **Output**: ESLint results and violations 108 | **Best Practice**: Always run after code modifications to ensure quality 109 | 110 | ### 📋 Component Contract Tools 111 | 112 | #### `build_component_contract` 113 | **Purpose**: Creates static surface contracts for component templates and styles 114 | **AI Usage**: Generate contracts before refactoring to track breaking changes 115 | **Key Parameters**: 116 | - `saveLocation`: Path where to save the contract file (supports absolute and relative paths) 117 | - `typescriptFile`: **Required** TypeScript component file (.ts) 118 | - `templateFile`: *Optional* Template file name (.html). Omit for inline templates 119 | - `styleFile`: *Optional* Style file name (.scss, .css, etc.). Omit for inline styles or no styles 120 | - `dsComponentName`: *Optional* design system component name 121 | **Output**: Component contract file with API surface 122 | **Best Practice**: Create contracts before major refactoring for comparison. Template and style files are optional—the tool will extract inline templates/styles from the TypeScript file when not provided 123 | 124 | #### `diff_component_contract` 125 | **Purpose**: Compares before/after contracts to identify breaking changes 126 | **AI Usage**: Validate that refactoring doesn't introduce breaking changes 127 | **Key Parameters**: 128 | - `saveLocation`: Path where to save the diff result file (supports absolute and relative paths) 129 | - `contractBeforePath`: Path to pre-refactoring contract 130 | - `contractAfterPath`: Path to post-refactoring contract 131 | - `dsComponentName`: Optional design system component name 132 | **Output**: Diff analysis showing breaking changes 133 | **Best Practice**: Essential validation step after component modifications 134 | 135 | #### `list_component_contracts` 136 | **Purpose**: Lists all available component contracts 137 | **AI Usage**: Discover existing contracts for comparison operations 138 | **Key Parameters**: 139 | - `directory`: Directory to search for contracts 140 | **Output**: List of available contract files 141 | **Best Practice**: Use to manage contract lifecycle during refactoring 142 | 143 | ### ⚙️ Configuration Tools 144 | 145 | ## AI Agent Workflow Patterns 146 | 147 | ### 1. Discovery & Analysis Workflow 148 | ``` 149 | 1. list-ds-components → Discover available DS components 150 | 2. report-violations → Identify all violations 151 | 3. get-project-dependencies → Analyze project structure 152 | ``` 153 | 154 | ### 2. Planning & Preparation Workflow 155 | ``` 156 | 1. get-ds-component-data → Get comprehensive component information 157 | 2. build-component-usage-graph → Map component relationships 158 | 3. get-component-docs → Review proper usage patterns 159 | 4. get-component-paths → Verify import paths 160 | 5. build_component_contract → Create baseline contracts 161 | ``` 162 | 163 | ### 3. Refactoring & Validation Workflow 164 | ``` 165 | 1. [Apply code changes] 166 | 2. build_component_contract → Create post-change contracts 167 | 3. diff_component_contract → Validate no breaking changes 168 | 4. lint-changes → Ensure code quality 169 | ``` 170 | 171 | ### 4. Non-Viable Cases Handling (Alternative to Steps 3-5) 172 | ``` 173 | 1. report-deprecated-css → Identify CSS usage in global styles 174 | 2. report-deprecated-css → Identify CSS usage in component overrides 175 | 3. [Replace HTML classes with after-migration- prefix] 176 | 4. [Duplicate CSS selectors with prefixed versions] 177 | 5. report-deprecated-css → Validate CSS count consistency 178 | 6. report-violations → Validate violation reduction 179 | ``` 180 | **Purpose**: Used during the main DS refactoring workflow when components are identified as non-viable during planning step 181 | **Trigger**: Requires developer review and approval after AI identifies non-viable cases in step 2 182 | **Key Pattern**: Use `after-migration-[ORIGINAL_CLASS]` prefix to exclude components from future analysis 183 | **Replaces**: Normal fix violations → validate changes → prepare report sequence 184 | 185 | ## Error Handling for AI Agents 186 | 187 | - **Path Resolution**: Always use relative paths starting with `./` 188 | - **Component Names**: Use PascalCase with `Ds` prefix (e.g., `DsButton`) 189 | - **File Arrays**: Ensure violation file arrays are properly formatted 190 | - **Directory Validation**: Verify directories exist before analysis 191 | - **Contract Management**: Clean up temporary contracts after analysis 192 | 193 | ## Performance Considerations 194 | 195 | - Use `groupBy: "folder"` for large codebases to reduce output size 196 | - Limit `violationFiles` arrays to relevant files only 197 | - Use selective `sections` parameter in `get-ds-component-data` and `list-ds-components` to retrieve only needed data types 198 | - Cache component documentation between related operations 199 | - Run validation tools in parallel when possible 200 | 201 | ## Integration Points 202 | 203 | These tools integrate with: 204 | - **Storybook**: For component documentation 205 | - **PostCSS**: For style analysis 206 | 207 | ## Output Formats 208 | 209 | All tools return structured data suitable for: 210 | - JSON parsing for programmatic analysis 211 | - Markdown formatting for human-readable reports 212 | - File path arrays for batch operations 213 | - Contract objects for API comparison ``` -------------------------------------------------------------------------------- /packages/shared/typescript-ast-utils/ai/EXAMPLES.md: -------------------------------------------------------------------------------- ```markdown 1 | # Examples 2 | 3 | ## 1 — Identifying Angular Component decorators 4 | 5 | > Find and validate `@Component` decorators in Angular class declarations. 6 | 7 | ```ts 8 | import { 9 | isComponentDecorator, 10 | getDecorators, 11 | } from '@push-based/typescript-ast-utils'; 12 | import * as ts from 'typescript'; 13 | 14 | // Sample Angular component source code 15 | const sourceCode = ` 16 | @Component({ 17 | selector: 'app-example', 18 | template: '<div>Hello World</div>' 19 | }) 20 | export class ExampleComponent {} 21 | `; 22 | 23 | // Create source file and program 24 | const sourceFile = ts.createSourceFile( 25 | 'example.ts', 26 | sourceCode, 27 | ts.ScriptTarget.Latest 28 | ); 29 | 30 | // Visit class declarations 31 | function visitClassDeclaration(node: ts.ClassDeclaration) { 32 | const decorators = getDecorators(node); 33 | 34 | for (const decorator of decorators) { 35 | if (isComponentDecorator(decorator)) { 36 | console.log(`Found @Component decorator on class: ${node.name?.text}`); 37 | // → 'Found @Component decorator on class: ExampleComponent' 38 | } 39 | } 40 | } 41 | 42 | // Traverse the AST 43 | ts.forEachChild(sourceFile, function visit(node) { 44 | if (ts.isClassDeclaration(node)) { 45 | visitClassDeclaration(node); 46 | } 47 | ts.forEachChild(node, visit); 48 | }); 49 | ``` 50 | 51 | --- 52 | 53 | ## 2 — Generic decorator detection 54 | 55 | > Detect any decorator by name or check for any decorators on a node. 56 | 57 | ```ts 58 | import { isDecorator, getDecorators } from '@push-based/typescript-ast-utils'; 59 | import * as ts from 'typescript'; 60 | 61 | const sourceCode = ` 62 | @Injectable() 63 | @Component({ 64 | selector: 'app-service' 65 | }) 66 | @CustomDecorator('config') 67 | export class ServiceComponent {} 68 | `; 69 | 70 | const sourceFile = ts.createSourceFile( 71 | 'service.ts', 72 | sourceCode, 73 | ts.ScriptTarget.Latest 74 | ); 75 | 76 | function analyzeDecorators(node: ts.ClassDeclaration) { 77 | const decorators = getDecorators(node); 78 | 79 | console.log(`Found ${decorators.length} decorators`); // → 'Found 3 decorators' 80 | 81 | for (const decorator of decorators) { 82 | // Check for specific decorators 83 | if (isDecorator(decorator, 'Injectable')) { 84 | console.log('Has @Injectable decorator'); 85 | } 86 | 87 | if (isDecorator(decorator, 'Component')) { 88 | console.log('Has @Component decorator'); 89 | } 90 | 91 | if (isDecorator(decorator, 'CustomDecorator')) { 92 | console.log('Has @CustomDecorator decorator'); 93 | } 94 | 95 | // Check if it's any valid decorator 96 | if (isDecorator(decorator)) { 97 | console.log('Found a valid decorator'); 98 | } 99 | } 100 | } 101 | 102 | // Output: 103 | // → 'Found 3 decorators' 104 | // → 'Has @Injectable decorator' 105 | // → 'Found a valid decorator' 106 | // → 'Has @Component decorator' 107 | // → 'Found a valid decorator' 108 | // → 'Has @CustomDecorator decorator' 109 | // → 'Found a valid decorator' 110 | ``` 111 | 112 | --- 113 | 114 | ## 3 — Removing quotes from string literals 115 | 116 | > Clean quoted strings from AST nodes for processing. 117 | 118 | ```ts 119 | import { removeQuotes } from '@push-based/typescript-ast-utils'; 120 | import * as ts from 'typescript'; 121 | 122 | const sourceCode = ` 123 | const singleQuoted = 'hello world'; 124 | const doubleQuoted = "typescript utils"; 125 | const backtickQuoted = \`template string\`; 126 | const multipleQuotes = """heavily quoted"""; 127 | `; 128 | 129 | const sourceFile = ts.createSourceFile( 130 | 'strings.ts', 131 | sourceCode, 132 | ts.ScriptTarget.Latest 133 | ); 134 | 135 | function processStringLiterals(node: ts.Node) { 136 | if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) { 137 | const originalText = node.getText(sourceFile); 138 | const cleanedText = removeQuotes(node, sourceFile); 139 | 140 | console.log(`Original: ${originalText} → Cleaned: "${cleanedText}"`); 141 | } 142 | } 143 | 144 | ts.forEachChild(sourceFile, function visit(node) { 145 | processStringLiterals(node); 146 | ts.forEachChild(node, visit); 147 | }); 148 | 149 | // Output: 150 | // → Original: 'hello world' → Cleaned: "hello world" 151 | // → Original: "typescript utils" → Cleaned: "typescript utils" 152 | // → Original: `template string` → Cleaned: "template string" 153 | // → Original: """heavily quoted""" → Cleaned: "heavily quoted" 154 | ``` 155 | 156 | --- 157 | 158 | ## 4 — Safe decorator extraction across TypeScript versions 159 | 160 | > Handle different TypeScript compiler API versions when extracting decorators. 161 | 162 | ```ts 163 | import { getDecorators, hasDecorators } from '@push-based/typescript-ast-utils'; 164 | import * as ts from 'typescript'; 165 | 166 | const sourceCode = ` 167 | @Deprecated() 168 | @Component({ 169 | selector: 'legacy-component' 170 | }) 171 | export class LegacyComponent { 172 | @Input() data: string; 173 | 174 | @Output() change = new EventEmitter(); 175 | } 176 | `; 177 | 178 | const sourceFile = ts.createSourceFile( 179 | 'legacy.ts', 180 | sourceCode, 181 | ts.ScriptTarget.Latest 182 | ); 183 | 184 | function safelyExtractDecorators(node: ts.Node) { 185 | // Safe extraction that works across TypeScript versions 186 | const decorators = getDecorators(node); 187 | 188 | if (decorators.length > 0) { 189 | console.log(`Node has ${decorators.length} decorators`); 190 | 191 | // Type-safe check 192 | if (hasDecorators(node)) { 193 | console.log('Confirmed: node has decorators property'); 194 | } 195 | } 196 | 197 | return decorators; 198 | } 199 | 200 | // Process class and its members 201 | ts.forEachChild(sourceFile, function visit(node) { 202 | if (ts.isClassDeclaration(node)) { 203 | console.log(`Class decorators: ${safelyExtractDecorators(node).length}`); 204 | // → 'Class decorators: 2' 205 | 206 | // Check property decorators 207 | for (const member of node.members) { 208 | if (ts.isPropertyDeclaration(member)) { 209 | const memberDecorators = safelyExtractDecorators(member); 210 | if (memberDecorators.length > 0) { 211 | console.log( 212 | `Property "${member.name?.getText(sourceFile)}" has ${ 213 | memberDecorators.length 214 | } decorators` 215 | ); 216 | // → 'Property "data" has 1 decorators' 217 | // → 'Property "change" has 1 decorators' 218 | } 219 | } 220 | } 221 | } 222 | 223 | ts.forEachChild(node, visit); 224 | }); 225 | ``` 226 | 227 | --- 228 | 229 | ## 5 — Building a decorator analyzer tool 230 | 231 | > Create a comprehensive tool to analyze all decorators in a TypeScript file. 232 | 233 | ```ts 234 | import { 235 | getDecorators, 236 | isDecorator, 237 | isComponentDecorator, 238 | removeQuotes, 239 | } from '@push-based/typescript-ast-utils'; 240 | import * as ts from 'typescript'; 241 | 242 | interface DecoratorInfo { 243 | name: string; 244 | target: string; 245 | arguments: string[]; 246 | line: number; 247 | } 248 | 249 | class DecoratorAnalyzer { 250 | private decorators: DecoratorInfo[] = []; 251 | 252 | analyze( 253 | sourceCode: string, 254 | fileName: string = 'analysis.ts' 255 | ): DecoratorInfo[] { 256 | const sourceFile = ts.createSourceFile( 257 | fileName, 258 | sourceCode, 259 | ts.ScriptTarget.Latest 260 | ); 261 | this.decorators = []; 262 | 263 | this.visitNode(sourceFile, sourceFile); 264 | return this.decorators; 265 | } 266 | 267 | private visitNode(node: ts.Node, sourceFile: ts.SourceFile) { 268 | const decorators = getDecorators(node); 269 | 270 | if (decorators.length > 0) { 271 | const targetName = this.getTargetName(node, sourceFile); 272 | 273 | for (const decorator of decorators) { 274 | const decoratorInfo = this.extractDecoratorInfo( 275 | decorator, 276 | targetName, 277 | sourceFile 278 | ); 279 | if (decoratorInfo) { 280 | this.decorators.push(decoratorInfo); 281 | } 282 | } 283 | } 284 | 285 | ts.forEachChild(node, (child) => this.visitNode(child, sourceFile)); 286 | } 287 | 288 | private extractDecoratorInfo( 289 | decorator: ts.Decorator, 290 | targetName: string, 291 | sourceFile: ts.SourceFile 292 | ): DecoratorInfo | null { 293 | if (!isDecorator(decorator)) return null; 294 | 295 | const expression = decorator.expression; 296 | let name = ''; 297 | const args: string[] = []; 298 | 299 | if (ts.isIdentifier(expression)) { 300 | name = expression.text; 301 | } else if ( 302 | ts.isCallExpression(expression) && 303 | ts.isIdentifier(expression.expression) 304 | ) { 305 | name = expression.expression.text; 306 | 307 | // Extract arguments 308 | for (const arg of expression.arguments) { 309 | if (ts.isStringLiteral(arg)) { 310 | args.push(removeQuotes(arg, sourceFile)); 311 | } else { 312 | args.push(arg.getText(sourceFile)); 313 | } 314 | } 315 | } 316 | 317 | const line = 318 | sourceFile.getLineAndCharacterOfPosition(decorator.getStart()).line + 1; 319 | 320 | return { 321 | name, 322 | target: targetName, 323 | arguments: args, 324 | line, 325 | }; 326 | } 327 | 328 | private getTargetName(node: ts.Node, sourceFile: ts.SourceFile): string { 329 | if (ts.isClassDeclaration(node) && node.name) { 330 | return `class ${node.name.text}`; 331 | } 332 | if (ts.isPropertyDeclaration(node) && node.name) { 333 | return `property ${node.name.getText(sourceFile)}`; 334 | } 335 | if (ts.isMethodDeclaration(node) && node.name) { 336 | return `method ${node.name.getText(sourceFile)}`; 337 | } 338 | return 'unknown'; 339 | } 340 | } 341 | 342 | // Usage example 343 | const analyzer = new DecoratorAnalyzer(); 344 | const sourceCode = ` 345 | @Component({ 346 | selector: 'app-example', 347 | template: '<div>Example</div>' 348 | }) 349 | export class ExampleComponent { 350 | @Input('inputAlias') data: string; 351 | 352 | @Output() change = new EventEmitter(); 353 | 354 | @HostListener('click', ['$event']) 355 | onClick(event: Event) {} 356 | } 357 | `; 358 | 359 | const results = analyzer.analyze(sourceCode); 360 | results.forEach((info) => { 361 | console.log(`Line ${info.line}: @${info.name} on ${info.target}`); 362 | if (info.arguments.length > 0) { 363 | console.log(` Arguments: ${info.arguments.join(', ')}`); 364 | } 365 | }); 366 | 367 | // Output: 368 | // → Line 1: @Component on class ExampleComponent 369 | // → Arguments: { selector: 'app-example', template: '<div>Example</div>' } 370 | // → Line 6: @Input on property data 371 | // → Arguments: inputAlias 372 | // → Line 8: @Output on property change 373 | // → Line 10: @HostListener on method onClick 374 | // → Arguments: click, ['$event'] 375 | ``` 376 | 377 | --- 378 | 379 | ## 6 — Error handling and edge cases 380 | 381 | > Handle malformed decorators and edge cases gracefully. 382 | 383 | ```ts 384 | import { getDecorators, isDecorator } from '@push-based/typescript-ast-utils'; 385 | import * as ts from 'typescript'; 386 | 387 | const problematicCode = ` 388 | // Valid decorator 389 | @Component() 390 | export class ValidComponent {} 391 | 392 | // Malformed decorator (will be handled gracefully) 393 | @ 394 | export class MalformedDecorator {} 395 | 396 | // Complex decorator expression 397 | @NgModule({ 398 | imports: [CommonModule], 399 | declarations: [SomeComponent] 400 | }) 401 | export class ComplexModule {} 402 | `; 403 | 404 | function safeDecoratorAnalysis(sourceCode: string) { 405 | try { 406 | const sourceFile = ts.createSourceFile( 407 | 'test.ts', 408 | sourceCode, 409 | ts.ScriptTarget.Latest 410 | ); 411 | 412 | ts.forEachChild(sourceFile, function visit(node) { 413 | if (ts.isClassDeclaration(node)) { 414 | const decorators = getDecorators(node); 415 | 416 | console.log(`Analyzing class: ${node.name?.text || 'anonymous'}`); 417 | console.log(`Found ${decorators.length} decorators`); 418 | 419 | for (const decorator of decorators) { 420 | try { 421 | if (isDecorator(decorator)) { 422 | console.log('✓ Valid decorator found'); 423 | } else { 424 | console.log('✗ Invalid decorator structure'); 425 | } 426 | } catch (error) { 427 | console.log(`⚠ Error processing decorator: ${error}`); 428 | } 429 | } 430 | 431 | console.log('---'); 432 | } 433 | 434 | ts.forEachChild(node, visit); 435 | }); 436 | } catch (error) { 437 | console.error('Failed to parse source code:', error); 438 | } 439 | } 440 | 441 | safeDecoratorAnalysis(problematicCode); 442 | 443 | // Output: 444 | // → Analyzing class: ValidComponent 445 | // → Found 1 decorators 446 | // → ✓ Valid decorator found 447 | // → --- 448 | // → Analyzing class: MalformedDecorator 449 | // → Found 0 decorators 450 | // → --- 451 | // → Analyzing class: ComplexModule 452 | // → Found 1 decorators 453 | // → ✓ Valid decorator found 454 | // → --- 455 | ``` 456 | 457 | These examples demonstrate the comprehensive capabilities and practical usage patterns of the `@push-based/typescript-ast-utils` library for TypeScript AST analysis, decorator processing, and source code manipulation. 458 | ``` -------------------------------------------------------------------------------- /packages/shared/angular-ast-utils/docs/angular-component-tree.md: -------------------------------------------------------------------------------- ```markdown 1 | # Angular Component Tree 2 | 3 | This document describes the structure of an Angular component tree created by using the utils helper under `angular.` 4 | The tree is a representation of the Angular component structure in a project. It is used to visualize the component hierarchy and the relationships between components. 5 | 6 | ## File Content 7 | 8 | - JS Content: `*{}` - shorthand for CSS text content 9 | - HTML Content: `</>` - shorthand for HTML text content 10 | - CSS Content: `{;}` - shorthand for JS text content 11 | - JSON Content: `[:]` - shorthand for JSON text content 12 | 13 | ## File-tree 14 | 15 | - Sold Style: `━` - bold 16 | - Tree: `┃` - Tree connection 17 | - Branch: `┣━━` / `┗━━` - Content connection e.g. `Object property` 18 | - Folder Connector: `📂` - the property name `children` 19 | - File Name: `:` - any value `string`, `number` 20 | - Important File: `:` - entry points e.g. `📦` repo; `🅰️`, `⚛️` framework; `🟦` stack, `📜` organisation 21 | 22 | ### Example 23 | 24 | ```bash 25 | root 26 | ┣━━ 📂src 27 | ┃ ┗━━ 📂button 28 | ┃ ┣━━ button.component.ts 29 | ┃ ┣━━ 📂other-compoennt 30 | ┃ ┗━━ 📂other-folder 31 | ┃ ┗━━ button.component.ts 32 | ┣━━ 📦package.json 33 | ┣━━ 🟦tsconfig.json 34 | ┣━━ 🅰️angular.json 35 | ┗━━ 📜README.md 36 | ``` 37 | 38 | ## Code-tree 39 | 40 | - Line Type: `│` - Tree connection 41 | - Tree Style: `─` - light 42 | - Branch: `├` / `└` - Content connection e.g. `Object property` 43 | - Branch Connector: `╼` - the property name `children` 44 | - Array: `[ ]` - Information list e.g. `Array`, `Set`, `Iterable` 45 | - Array Index: `[0]` - list item e.g. `Object property` 46 | - Object: `{ }` - Information pairs e.g. `Class`, `Object`, `Map` 47 | - Prop Name: `prop` - any value `string` 48 | - Value Connector: `:` - separates property or index from value 49 | - Prop Value: `42`, `"test"` - any value any value `string`, `number` 50 | 51 | ### Example 52 | 53 | ```bash 54 | { } # component object 55 | ├╼ className: 'ButtonComponent' - # property: string 56 | └╼ styleUrls: [ ] # array 57 | ├╼ [0]: { } # index 0 - first style object 58 | │ ├╼ startLine: 4 59 | │ └╼ value: './styles-1.css' 60 | │ └╼ value: './styles-1.css' 61 | └╼ [1]: { } # index 1 - second style object 62 | ├╼ startLine: 2 - # property: number 63 | └╼ value: './styles-2.css' - # property: string 64 | ``` 65 | 66 | ## AST-tree 67 | 68 | - Line Type: `│` - Tree connection 69 | - Tree Style: `─` - light 70 | - Branch: `├` / `└` - Content connection e.g. `Object property` 71 | - Branch Connector: `↘` - the property name `children` 72 | - Array: `[ ]` - Information list e.g. `Array`, `Set`, `Iterable` 73 | - Array Index: `[0]` - list item e.g. `Object property` 74 | - Object: `{ }` - Information pairs e.g. `Class`, `Object`, `Map` 75 | - Prop Name: `prop` - any value `string` 76 | - Value Connector: `:` - separates property or index from value 77 | - Prop Value: `42`, `"test"` - any value any value `string`, `number` 78 | 79 | ### Example 80 | 81 | _`component.ts` - Text representation of the file content_ 82 | 83 | ```bash 84 | export class BaseComponent { 85 | styles: String; 86 | } 87 | 88 | export class Component { 89 | styles = ['styles.css']; 90 | } 91 | ``` 92 | 93 | _AbstractSyntaxTree representation of the file content of `component.ts`_ 94 | 95 | ```bash 96 | ( ) # `sourceFile` - A plain JavaScript function returning a object 97 | └╼ sourceFile: TsAstNode # Start of AST tree 98 | ├↘ [0]: ClassDeclaration # shorter form us array syntax is used 99 | └↘ [1]: ClassDeclaration 100 | ├↘ ClassDeclaration 101 | ├↘ FunctionDeclaration 102 | └↘ PropertyDeclaration 103 | └↘ ObjectLiteralExpression 104 | └↘ PropertyAssignment 105 | └↘ Identifier 106 | └↘ getText(): './button.css' # Function returning `./button.css` 107 | ⤷ `.btn { color: red; }` # './button.css' content 108 | ``` 109 | 110 | ## Tree Links 111 | 112 | - File System Reference: `⤷` - links a file reference to it's content 113 | - In Memory Reference: `↳` - links a reference to values, pairs or lists 114 | - In Memory Reference: `↳` - links a reference to values, pairs or lists 115 | 116 | ### File System Reference 117 | 118 | ```bash 119 | root 120 | ┗━━ 📂constants 121 | ┣━━ button.ts # const buttonStylePath = './button.css' 122 | ┃ ⤷ `.btn { color: red; }` # './button.css' content 123 | ┗━━ select.ts # const selectTemplatePath = './select.css' 124 | ⤷ `<button class="btn btn-primary">Click me</button>` # './select.css' content 125 | ``` 126 | 127 | ### In Memory Reference to File System 128 | 129 | ```bash 130 | { } # The default export of 'file.ts' file. (a JavaScript object) 131 | ├╼ className: 'ButtonComponent' - # property: string 132 | └╼ styleUrls: [ ] # array 133 | └╼ [0] './button.css' # index 0 134 | ⤷ `.btn { color: red; }` # './button.css' content 135 | ``` 136 | 137 | ### In Memory Reference - AST to FileSystem 138 | 139 | ```bash 140 | ( ) # `sourceFile` - A plain JavaScript function returning a object 141 | └╼ sourceFile: 142 | └╼ sourceFile: ObjectLiteralExpression 143 | └↘ PropertyAssignment 144 | └↘ Identifier 145 | └↘ getText(): './button.css' # Function returning `./button.css` 146 | ⤷ `.btn { color: red; }` # './button.css' content 147 | ``` 148 | 149 | ## Angular Projects 150 | 151 | ### Minimal Angular Project - File Tree 152 | 153 | ```bash 154 | root 155 | ┣━━ 📂public 156 | ┣━━ 📂src 157 | ┃ ┣━━ 📂app 158 | ┃ ┃ ┣━━ 📂until 159 | ┃ ┃ ┃ ┣━━ 📂button 160 | ┃ ┃ ┃ ┃ ┗━━ button.component.ts 161 | ┃ ┃ ┃ ┃ ↳ inline-css 162 | ┃ ┃ ┃ ┃ ↳ inline-html 163 | ┃ ┃ ┃ ┣━━ 📂select 164 | ┃ ┃ ┃ ┃ ┣━━ select.component.ts 165 | ┃ ┃ ┃ ┃ ┣━━ ⤷ select.component.css 166 | ┃ ┃ ┃ ┃ ┗━━ ⤷ select.component.html 167 | ┃ ┃ ┃ ┗━━... 168 | ┃ ┃ ┣━━ app.routes.ts 169 | ┃ ┃ ┣━━ app.component.ts 170 | ┃ ┃ ┗━━ app.config.ts 171 | ┃ ┣━━ index.html 172 | ┃ ┣━━ main.ts 173 | ┃ ┗━━ styles.css 174 | ┣━━ 📦package.json 175 | ┣━━ 🟦tsconfig.json 176 | ┣━━ 🅰️angular.json 177 | ┗━━ README.md 178 | ``` 179 | 180 | ### Inline Assets (Styles and Templates) 181 | 182 | ```bash 183 | root 184 | ┗━━ 📂any 185 | ┗━━ any.component.ts 186 | ├╼ styles: [ ] 187 | │ ├╼ [0]: `.btn { size: 13px; }` 188 | │ └╼ [1]: `.red { color: red; }` 189 | └╼ template: `<button class="btn red">Click me</button>` 190 | ``` 191 | 192 | ### External Assets (Styles and Templates) 193 | 194 | ```bash 195 | root 196 | ┣━━ 📂any 197 | ┣━━ any.component.ts 198 | ┃ ├╼ styleUrls: [ ] 199 | ┃ │ ├╼ [0]: `any.styles.css` 200 | ┃ │ │ ⤷ `.btn { size: 13px; }` 201 | ┃ │ └╼ [1]: `other.styles.css` 202 | ┃ │ ⤷ `.red { color: red; }` 203 | ┃ └╼ templateUrl: 'any.component.html' 204 | ┃ ⤷ `<button class="btn red">Click me</button>` 205 | ┣━━ any.style.css 206 | ┃ └╼ `.btn { color: red; }` 207 | ┣━━ other.style-1.css 208 | ┃ └╼ `.btn-primary { color: blue; }` 209 | ┗━━ any.component.html 210 | └╼ `<button class="btn btn-primary">Click me</button>` 211 | ``` 212 | 213 | ## Creating Component AST's 214 | 215 | ### 1. Find Component Files - Regex matching 216 | 217 | This part of the process should get optimized for scale as it is one of the costly steps. 218 | We use simple regex matching against a string pattern to detect interesting files. 219 | It is accepted that some of the matching files don't contain components (false positives), as they are later on excluded anyway. 220 | 221 | ```bash 222 | [ ] 223 | ├╼ [0]: 'src/app/app.component.ts' 224 | ├╼ [1]: 'src/app/until/button/button.component.ts' 225 | └╼ [2]: 'src/app/until/select.component.ts' 226 | ``` 227 | 228 | ### 2. Create the TS Program - Read Files and Parse AST 229 | 230 | In this step we need to feed the TS program our entry to the frameworks TS code. 231 | 232 | The entry is located in Angular's RC file `angular.json`. 233 | 234 | ```bash 235 | root 236 | ┗━━ angular.json 237 | ⤷ index.html 238 | ⤷ styles.css 239 | ⤷ tsconfig.json 240 | ⤷ └↳main.ts # TSProgram entry 241 | └↘ app.config.ts 242 | └↘ app.routes.ts 243 | └↘ app/app.component.ts 244 | ├↘ button.component.ts 245 | │ ↳ styles[0]: `*{}` 246 | │ ↳ styles[1]: `*{}` 247 | │ ↳ template: `</>` 248 | └↘ select.component.ts 249 | ├↘ styleUrls[0]: 'select.styles.css' 250 | │ ↳ `*{}` 251 | ├↘ styleUrls[1]: 'select.styles.css' 252 | │ ↳ `*{}` 253 | └↘ templateUrl: 'select.component.html'' 254 | ↳ `</>` 255 | ``` 256 | 257 | This can potentially be used to filter or add more file to our result file list. 258 | 259 | ```ts 260 | const tsProgramm = createProgram([ 261 | 'src/app/app.component.ts', 262 | // ... 263 | ]); 264 | 265 | const components = tsProgramm.getSourceFiles(); 266 | ``` 267 | 268 | _components content_ 269 | 270 | ```bash 271 | [ ] 272 | ├↘ FunctionDeclaration # node_modules/lib/file.js 273 | ├↘ ClassDeclaration # node_modules/other-lib/other-file.js 274 | ├↘ VariableDeclaration # node_modules/any-lib/any-file.js 275 | ├↘ ClassDeclaration # app/app.component.ts 276 | ├↘ ClassDeclaration # app/ui/button.component.ts 277 | └↘ ClassDeclaration # app/ui/select.component.ts 278 | ``` 279 | 280 | ### 3. Filtered Components - exclude other files that got added due to existing imports 281 | 282 | ```ts 283 | components.filter((source) => matchingFiles.includes(source)); 284 | ``` 285 | 286 | ```bash 287 | [ ] 288 | ├↘ ClassDeclaration # app/app.component.ts#AppComponent AST 289 | ├↘ ClassDeclaration # app/ui/button.component.ts#ButtonComponent AST 290 | └↘ ClassDeclaration # app/ui/select.component.ts#SelectComponent AST 291 | ``` 292 | 293 | ### 4. Parsed Component Class 294 | 295 | To go on with creating the Angular component tree we need to get the essential information of every component. 296 | 297 | We need: 298 | 299 | - basic component data (file, name, contextual infos) 300 | - the component's AST 301 | - all references to assets (internal as well as external) 302 | - in a step that should be executable lazily we need to resolve the assets 303 | 304 | As we have to nest different trees here let's quickly define the types that guide us in the future. 305 | 306 | ```ts 307 | type Unit = { 308 | type: 'class' | 'html' | 'styles'; 309 | }; 310 | type Code = { 311 | filePath: string; 312 | value: string; 313 | }; 314 | type CodeAware = { 315 | source: <T extends AST>() => T; 316 | }; 317 | 318 | type LinkedCode<T> = Code & 319 | CodeAware<T> & { 320 | startLine: string; 321 | }; 322 | 323 | type Style = LinkedCode<CssAst> & { 324 | type: 'class'; 325 | }; 326 | 327 | type Template = LinkedCode<HtmlAst> & { 328 | type: 'class'; 329 | }; 330 | 331 | type ParsedComponentClass<T> = Code & 332 | CodeAware<T> & { 333 | type: 'class'; 334 | className: string; 335 | styles?: Style[]; 336 | styleUrls?: Style[]; 337 | template?: Template; 338 | templateUrl?: Template; 339 | }; 340 | ``` 341 | 342 | ```bash 343 | ParsedComponent 344 | ├╼ className: 'AppComponent`' 345 | ├╼ filePath: './app.component.ts' 346 | ├╼ value: `{;}` 347 | ├╼ source(): TsAst 348 | │ └↘ ClassDeclaration 349 | ├╼ styles: [ ] 350 | │ └╼ [0]: { } 351 | │ ├╼ filePath: './app.component.ts' 352 | │ ├╼ startLine: 7 353 | │ ├╼ value: `*{}` 354 | │ └╼ source(): CssAst 355 | │ └↘ RuleContainer 356 | ├╼ styleUrls: [ ] 357 | │ └╼ [0]: { } 358 | │ ├╼ filePath: './any-styles.css' 359 | │ ├╼ startLine: 13 360 | │ ├╼ value: `*{}` 361 | │ └╼ source(): CssAst 362 | │ └↘ RuleContainer 363 | ├╼ template: { } 364 | │ ├╼ filePath: './app.component.ts' 365 | │ ├╼ startLine: 21 366 | │ ├╼ value: `</>` 367 | │ └╼ source(): HtmlAst 368 | │ └↘ Element 369 | └╼ templateUrl: { } 370 | ├╼ filePath: './app.component.html' 371 | ├╼ startLine: 42 372 | ├╼ value: `*{}` 373 | └╼ source(): HtmlAst 374 | └↘ Element 375 | ``` 376 | 377 | ### 5. Glue Traversal Logic 378 | 379 | // walk component tree 380 | 381 | ```ts 382 | const comps: ParsedCompoent[] = getParsedComponents(); 383 | 384 | function walkComponents(node: CodeAware, visitor: T) { 385 | for (let comp of comps) { 386 | if ('source' in comp) { 387 | const unit = comp.source(); 388 | switch (unit.type) { 389 | case 'class': 390 | ts.visitAllChildren(unit, visitor as TsVisitor); 391 | case 'styles': 392 | walkRules(unit, visitor as CssVisitor); 393 | case 'html': 394 | forEachChild(unit, visitor as HtmlVisitor); 395 | } 396 | } 397 | } 398 | } 399 | ``` 400 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/complex-components/second-case/complex-badge-widget.component.scss: -------------------------------------------------------------------------------- ```scss 1 | .widget-container { 2 | padding: 2rem; 3 | background: #f8fafc; 4 | border-radius: 0.75rem; 5 | box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); 6 | } 7 | 8 | .widget-container h3 { 9 | margin: 0 0 1.5rem 0; 10 | color: #1f2937; 11 | font-size: 1.5rem; 12 | font-weight: 600; 13 | } 14 | 15 | .badge-grid { 16 | display: grid; 17 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 18 | gap: 1.5rem; 19 | margin-bottom: 2rem; 20 | } 21 | 22 | .badge-item { 23 | position: relative; 24 | background: white; 25 | border-radius: 0.5rem; 26 | padding: 1rem; 27 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 28 | transition: all 0.3s ease; 29 | } 30 | 31 | .badge-item:hover { 32 | transform: translateY(-2px); 33 | box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15); 34 | } 35 | 36 | // Complex custom badge styles that will be difficult to refactor 37 | .offer-badge { 38 | display: flex; 39 | flex-direction: column; 40 | background: var(--badge-color, #6b7280); 41 | color: white; 42 | border-radius: 0.75rem; 43 | padding: 0; 44 | overflow: hidden; 45 | position: relative; 46 | cursor: pointer; 47 | transition: all 0.3s ease; 48 | min-height: 120px; 49 | 50 | // Complex pseudo-elements that DsBadge won't support 51 | &::before { 52 | content: ''; 53 | position: absolute; 54 | top: 0; 55 | left: 0; 56 | right: 0; 57 | height: 3px; 58 | background: linear-gradient(90deg, rgba(255,255,255,0.3) 0%, rgba(255,255,255,0.8) 50%, rgba(255,255,255,0.3) 100%); 59 | animation: shimmer 2s infinite; 60 | } 61 | 62 | &::after { 63 | content: attr(data-custom-prop); 64 | position: absolute; 65 | top: 0.5rem; 66 | right: 0.5rem; 67 | background: rgba(0, 0, 0, 0.2); 68 | padding: 0.25rem 0.5rem; 69 | border-radius: 0.25rem; 70 | font-size: 0.625rem; 71 | font-weight: 600; 72 | text-transform: uppercase; 73 | } 74 | 75 | // Level-specific complex styling 76 | &.offer-badge-low { 77 | background: linear-gradient(135deg, #10b981 0%, #059669 100%); 78 | 79 | .offer-badge-header { 80 | background: rgba(5, 150, 105, 0.2); 81 | border-bottom: 2px solid rgba(5, 150, 105, 0.3); 82 | } 83 | 84 | .level-dot.active { 85 | background: #34d399; 86 | box-shadow: 0 0 8px #34d399; 87 | } 88 | } 89 | 90 | &.offer-badge-medium { 91 | background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); 92 | 93 | .offer-badge-header { 94 | background: rgba(217, 119, 6, 0.2); 95 | border-bottom: 2px solid rgba(217, 119, 6, 0.3); 96 | } 97 | 98 | .level-dot.active { 99 | background: #fbbf24; 100 | box-shadow: 0 0 8px #fbbf24; 101 | } 102 | } 103 | 104 | &.offer-badge-high { 105 | background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); 106 | 107 | .offer-badge-header { 108 | background: rgba(37, 99, 235, 0.2); 109 | border-bottom: 2px solid rgba(37, 99, 235, 0.3); 110 | } 111 | 112 | .level-dot.active { 113 | background: #60a5fa; 114 | box-shadow: 0 0 8px #60a5fa; 115 | } 116 | } 117 | 118 | &.offer-badge-critical { 119 | background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); 120 | animation: pulse-critical 2s infinite; 121 | 122 | .offer-badge-header { 123 | background: rgba(220, 38, 38, 0.2); 124 | border-bottom: 2px solid rgba(220, 38, 38, 0.3); 125 | } 126 | 127 | .level-dot.active { 128 | background: #f87171; 129 | box-shadow: 0 0 8px #f87171; 130 | animation: blink 1s infinite; 131 | } 132 | } 133 | 134 | // Type-specific complex styling 135 | &.offer-badge-offer-badge { 136 | .offer-badge-type-indicator { 137 | animation: bounce 2s infinite; 138 | } 139 | 140 | .offer-badge-content { 141 | background: rgba(255, 255, 255, 0.1); 142 | backdrop-filter: blur(10px); 143 | } 144 | } 145 | 146 | &.offer-badge-status { 147 | .offer-badge-header { 148 | background: linear-gradient(45deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.2) 100%); 149 | } 150 | 151 | .badge-status-indicator { 152 | width: 12px; 153 | height: 12px; 154 | border-radius: 50%; 155 | background: #10b981; 156 | animation: pulse-status 1.5s infinite; 157 | } 158 | } 159 | 160 | &.offer-badge-priority { 161 | border: 2px solid rgba(255, 255, 255, 0.3); 162 | 163 | .offer-badge-type-indicator { 164 | color: #fbbf24; 165 | text-shadow: 0 0 8px #fbbf24; 166 | } 167 | } 168 | 169 | // Interactive states 170 | &.offer-badge-interactive { 171 | &:hover { 172 | transform: scale(1.02); 173 | box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); 174 | 175 | .offer-badge-header { 176 | background: rgba(255, 255, 255, 0.2); 177 | } 178 | 179 | .offer-badge-footer { 180 | background: rgba(255, 255, 255, 0.1); 181 | } 182 | } 183 | 184 | &:active { 185 | transform: scale(0.98); 186 | } 187 | } 188 | 189 | &.offer-badge-selected { 190 | border: 3px solid #fbbf24; 191 | box-shadow: 0 0 0 3px rgba(251, 191, 36, 0.3); 192 | 193 | &::before { 194 | background: linear-gradient(90deg, #fbbf24 0%, #f59e0b 50%, #fbbf24 100%); 195 | } 196 | } 197 | 198 | // Advanced mode modifications 199 | &.modified-badge { 200 | border: 2px dashed rgba(255, 255, 255, 0.5); 201 | 202 | .offer-badge-content { 203 | filter: hue-rotate(30deg); 204 | } 205 | } 206 | } 207 | 208 | // Complex nested structure that DsBadge cannot replicate 209 | .offer-badge-header { 210 | display: flex; 211 | justify-content: space-between; 212 | align-items: center; 213 | padding: 0.75rem 1rem; 214 | background: rgba(255, 255, 255, 0.1); 215 | border-bottom: 1px solid rgba(255, 255, 255, 0.2); 216 | transition: all 0.3s ease; 217 | 218 | &.hover-active { 219 | background: rgba(255, 255, 255, 0.2); 220 | transform: translateY(-1px); 221 | } 222 | } 223 | 224 | .offer-badge-type-indicator { 225 | font-size: 1.25rem; 226 | font-weight: 600; 227 | display: flex; 228 | align-items: center; 229 | gap: 0.5rem; 230 | 231 | &::after { 232 | content: attr(data-type); 233 | font-size: 0.75rem; 234 | opacity: 0.8; 235 | } 236 | } 237 | 238 | .offer-badge-level-dots { 239 | display: flex; 240 | gap: 0.25rem; 241 | align-items: center; 242 | } 243 | 244 | .level-dot { 245 | width: 8px; 246 | height: 8px; 247 | border-radius: 50%; 248 | background: rgba(255, 255, 255, 0.3); 249 | transition: all 0.3s ease; 250 | 251 | &.active { 252 | background: white; 253 | box-shadow: 0 0 4px rgba(255, 255, 255, 0.8); 254 | } 255 | } 256 | 257 | .offer-badge-content { 258 | flex: 1; 259 | padding: 1rem; 260 | display: flex; 261 | flex-direction: column; 262 | justify-content: center; 263 | position: relative; 264 | 265 | &::before { 266 | content: ''; 267 | position: absolute; 268 | top: 0; 269 | left: 0; 270 | right: 0; 271 | bottom: 0; 272 | background: linear-gradient(45deg, transparent 0%, rgba(255,255,255,0.05) 50%, transparent 100%); 273 | pointer-events: none; 274 | } 275 | } 276 | 277 | .offer-badge-primary-text { 278 | font-size: 1.125rem; 279 | font-weight: 600; 280 | margin-bottom: 0.5rem; 281 | text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); 282 | } 283 | 284 | .offer-badge-metadata { 285 | display: flex; 286 | flex-direction: column; 287 | gap: 0.25rem; 288 | opacity: 0.9; 289 | } 290 | 291 | .badge-id, 292 | .badge-timestamp { 293 | font-size: 0.75rem; 294 | font-weight: 500; 295 | opacity: 0.8; 296 | } 297 | 298 | .offer-badge-footer { 299 | display: flex; 300 | justify-content: space-between; 301 | align-items: center; 302 | padding: 0.75rem 1rem; 303 | background: rgba(0, 0, 0, 0.1); 304 | border-top: 1px solid rgba(255, 255, 255, 0.1); 305 | transition: all 0.3s ease; 306 | 307 | &.hover-active { 308 | background: rgba(0, 0, 0, 0.2); 309 | transform: translateY(1px); 310 | } 311 | } 312 | 313 | .badge-action { 314 | background: rgba(255, 255, 255, 0.2); 315 | border: 1px solid rgba(255, 255, 255, 0.3); 316 | color: white; 317 | padding: 0.25rem 0.75rem; 318 | border-radius: 0.25rem; 319 | font-size: 0.75rem; 320 | font-weight: 500; 321 | cursor: pointer; 322 | transition: all 0.2s ease; 323 | 324 | &:hover { 325 | background: rgba(255, 255, 255, 0.3); 326 | transform: translateY(-1px); 327 | } 328 | 329 | &.danger { 330 | background: rgba(239, 68, 68, 0.3); 331 | border-color: rgba(239, 68, 68, 0.5); 332 | 333 | &:hover { 334 | background: rgba(239, 68, 68, 0.5); 335 | } 336 | } 337 | } 338 | 339 | .badge-status-indicator { 340 | width: 10px; 341 | height: 10px; 342 | border-radius: 50%; 343 | background: #10b981; 344 | 345 | // Dynamic status classes that depend on complex logic 346 | &.status-low-offer-badge { 347 | background: #10b981; 348 | animation: pulse-green 2s infinite; 349 | } 350 | 351 | &.status-medium-status { 352 | background: #f59e0b; 353 | animation: pulse-yellow 2s infinite; 354 | } 355 | 356 | &.status-high-priority { 357 | background: #3b82f6; 358 | animation: pulse-blue 2s infinite; 359 | } 360 | 361 | &.status-critical-priority { 362 | background: #ef4444; 363 | animation: pulse-red 1s infinite; 364 | } 365 | } 366 | 367 | // Tooltip system that depends on custom badge structure 368 | .badge-tooltip { 369 | position: absolute; 370 | top: 100%; 371 | left: 50%; 372 | transform: translateX(-50%); 373 | background: #1f2937; 374 | color: white; 375 | padding: 1rem; 376 | border-radius: 0.5rem; 377 | box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); 378 | opacity: 0; 379 | pointer-events: none; 380 | transition: all 0.3s ease; 381 | z-index: 1000; 382 | min-width: 200px; 383 | 384 | &.visible { 385 | opacity: 1; 386 | pointer-events: auto; 387 | transform: translateX(-50%) translateY(0.5rem); 388 | } 389 | 390 | &::before { 391 | content: ''; 392 | position: absolute; 393 | top: -8px; 394 | left: 50%; 395 | transform: translateX(-50%); 396 | border-left: 8px solid transparent; 397 | border-right: 8px solid transparent; 398 | border-bottom: 8px solid #1f2937; 399 | } 400 | } 401 | 402 | .tooltip-content { 403 | h4 { 404 | margin: 0 0 0.5rem 0; 405 | font-size: 1rem; 406 | font-weight: 600; 407 | } 408 | 409 | p { 410 | margin: 0 0 0.25rem 0; 411 | font-size: 0.875rem; 412 | opacity: 0.9; 413 | } 414 | } 415 | 416 | .custom-data { 417 | margin-top: 0.5rem; 418 | padding-top: 0.5rem; 419 | border-top: 1px solid rgba(255, 255, 255, 0.2); 420 | } 421 | 422 | .data-item { 423 | font-size: 0.75rem; 424 | margin-bottom: 0.25rem; 425 | 426 | strong { 427 | color: #fbbf24; 428 | } 429 | } 430 | 431 | // Widget controls 432 | .widget-controls { 433 | display: flex; 434 | gap: 1rem; 435 | margin-bottom: 2rem; 436 | flex-wrap: wrap; 437 | align-items: center; 438 | 439 | button { 440 | background: #3b82f6; 441 | color: white; 442 | border: none; 443 | padding: 0.5rem 1rem; 444 | border-radius: 0.375rem; 445 | font-size: 0.875rem; 446 | font-weight: 500; 447 | cursor: pointer; 448 | transition: all 0.2s ease; 449 | 450 | &:hover { 451 | background: #2563eb; 452 | transform: translateY(-1px); 453 | } 454 | } 455 | 456 | label { 457 | display: flex; 458 | align-items: center; 459 | gap: 0.5rem; 460 | font-size: 0.875rem; 461 | color: #374151; 462 | cursor: pointer; 463 | 464 | input[type="checkbox"] { 465 | width: 1rem; 466 | height: 1rem; 467 | } 468 | } 469 | } 470 | 471 | // Status display 472 | .status-display { 473 | background: white; 474 | padding: 1.5rem; 475 | border-radius: 0.5rem; 476 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 477 | 478 | h4 { 479 | margin: 0 0 1rem 0; 480 | color: #1f2937; 481 | font-size: 1.125rem; 482 | font-weight: 600; 483 | } 484 | } 485 | 486 | .status-grid { 487 | display: grid; 488 | grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); 489 | gap: 1rem; 490 | } 491 | 492 | .status-item { 493 | padding: 0.75rem; 494 | background: #f9fafb; 495 | border-radius: 0.375rem; 496 | text-align: center; 497 | 498 | strong { 499 | display: block; 500 | color: #374151; 501 | font-size: 0.875rem; 502 | margin-bottom: 0.25rem; 503 | } 504 | 505 | // Dynamic content that depends on badge counting 506 | &:nth-child(1) strong::after { 507 | content: ' 📊'; 508 | } 509 | 510 | &:nth-child(2) strong::after { 511 | content: ' 🎯'; 512 | } 513 | 514 | &:nth-child(3) strong::after { 515 | content: ' ✅'; 516 | } 517 | 518 | &:nth-child(4) strong::after { 519 | content: ' ⚠️'; 520 | } 521 | } 522 | 523 | // Complex animations that DsBadge won't support 524 | @keyframes shimmer { 525 | 0% { transform: translateX(-100%); } 526 | 100% { transform: translateX(100%); } 527 | } 528 | 529 | @keyframes pulse-critical { 530 | 0%, 100% { 531 | box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); 532 | } 533 | 50% { 534 | box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); 535 | } 536 | } 537 | 538 | @keyframes blink { 539 | 0%, 50% { opacity: 1; } 540 | 51%, 100% { opacity: 0.3; } 541 | } 542 | 543 | @keyframes bounce { 544 | 0%, 20%, 50%, 80%, 100% { transform: translateY(0); } 545 | 40% { transform: translateY(-4px); } 546 | 60% { transform: translateY(-2px); } 547 | } 548 | 549 | @keyframes pulse-status { 550 | 0%, 100% { transform: scale(1); opacity: 1; } 551 | 50% { transform: scale(1.2); opacity: 0.7; } 552 | } 553 | 554 | @keyframes pulse-green { 555 | 0%, 100% { background: #10b981; } 556 | 50% { background: #34d399; } 557 | } 558 | 559 | @keyframes pulse-yellow { 560 | 0%, 100% { background: #f59e0b; } 561 | 50% { background: #fbbf24; } 562 | } 563 | 564 | @keyframes pulse-blue { 565 | 0%, 100% { background: #3b82f6; } 566 | 50% { background: #60a5fa; } 567 | } 568 | 569 | @keyframes pulse-red { 570 | 0%, 100% { background: #ef4444; } 571 | 50% { background: #f87171; } 572 | } 573 | 574 | // Responsive design with complex badge adaptations 575 | @media (max-width: 768px) { 576 | .badge-grid { 577 | grid-template-columns: 1fr; 578 | } 579 | 580 | .offer-badge { 581 | min-height: 100px; 582 | 583 | .offer-badge-header { 584 | padding: 0.5rem; 585 | } 586 | 587 | .offer-badge-content { 588 | padding: 0.75rem; 589 | } 590 | 591 | .offer-badge-footer { 592 | padding: 0.5rem; 593 | } 594 | 595 | .offer-badge-primary-text { 596 | font-size: 1rem; 597 | } 598 | 599 | // Complex responsive behavior that DsBadge won't handle 600 | &.offer-badge-critical { 601 | .offer-badge-type-indicator { 602 | font-size: 1rem; 603 | } 604 | 605 | .level-dot { 606 | width: 6px; 607 | height: 6px; 608 | } 609 | } 610 | } 611 | 612 | .widget-controls { 613 | flex-direction: column; 614 | align-items: stretch; 615 | 616 | button { 617 | width: 100%; 618 | } 619 | } 620 | } ``` -------------------------------------------------------------------------------- /packages/shared/utils/ai/EXAMPLES.md: -------------------------------------------------------------------------------- ```markdown 1 | # Examples 2 | 3 | ## 1 — Process execution with real-time monitoring 4 | 5 | > Execute commands with live output streaming and error handling. 6 | 7 | ```ts 8 | import { executeProcess, ProcessObserver } from '@push-based/utils'; 9 | 10 | // Create an observer to handle process events 11 | const observer: ProcessObserver = { 12 | onStdout: (data) => { 13 | console.log(`📤 ${data.trim()}`); 14 | }, 15 | onStderr: (data) => { 16 | console.error(`❌ ${data.trim()}`); 17 | }, 18 | onError: (error) => { 19 | console.error(`Process failed with code ${error.code}`); 20 | }, 21 | onComplete: () => { 22 | console.log('✅ Process completed successfully'); 23 | }, 24 | }; 25 | 26 | // Execute a Node.js command 27 | const result = await executeProcess({ 28 | command: 'node', 29 | args: ['--version'], 30 | observer, 31 | }); 32 | 33 | console.log(`Exit code: ${result.code}`); 34 | console.log(`Duration: ${result.duration}ms`); 35 | console.log(`Output: ${result.stdout.trim()}`); 36 | 37 | // Output: 38 | // → 📤 v18.17.0 39 | // → ✅ Process completed successfully 40 | // → Exit code: 0 41 | // → Duration: 45ms 42 | // → Output: v18.17.0 43 | ``` 44 | 45 | --- 46 | 47 | ## 2 — File pattern searching and processing 48 | 49 | > Search for files containing specific patterns and process the results. 50 | 51 | ```ts 52 | import { 53 | findFilesWithPattern, 54 | findInFile, 55 | resolveFileCached, 56 | } from '@push-based/utils'; 57 | 58 | // Find all TypeScript files containing 'Component' 59 | const componentFiles = await findFilesWithPattern('./src', 'Component'); 60 | 61 | console.log(`Found ${componentFiles.length} files with 'Component':`); 62 | componentFiles.forEach((file) => console.log(` - ${file}`)); 63 | 64 | // Get detailed information about matches in a specific file 65 | if (componentFiles.length > 0) { 66 | const firstFile = componentFiles[0]; 67 | const matches = await findInFile(firstFile, 'Component'); 68 | 69 | console.log(`\nDetailed matches in ${firstFile}:`); 70 | matches.forEach((match) => { 71 | console.log( 72 | ` Line ${match.position.startLine}, Column ${match.position.startColumn}` 73 | ); 74 | }); 75 | 76 | // Load and cache the file content 77 | const content = await resolveFileCached(firstFile); 78 | console.log(`File size: ${content.length} characters`); 79 | 80 | // Subsequent calls will use cached version 81 | const cachedContent = await resolveFileCached(firstFile); // ⚡ Fast cached access 82 | } 83 | 84 | // Output: 85 | // → Found 3 files with 'Component': 86 | // → - ./src/app/user.component.ts 87 | // → - ./src/app/admin.component.ts 88 | // → - ./src/shared/base.component.ts 89 | // → 90 | // → Detailed matches in ./src/app/user.component.ts: 91 | // → Line 5, Column 14 92 | // → Line 12, Column 25 93 | // → File size: 1247 characters 94 | ``` 95 | 96 | --- 97 | 98 | ## 3 — Command formatting and logging 99 | 100 | > Format commands with colors and context for better development experience. 101 | 102 | ```ts 103 | import { formatCommandLog, isVerbose, calcDuration } from '@push-based/utils'; 104 | 105 | // Set verbose mode for demonstration 106 | process.env['NG_MCP_VERBOSE'] = 'true'; 107 | 108 | // Format commands with different contexts 109 | const commands = [ 110 | { cmd: 'npm', args: ['install'], cwd: undefined }, 111 | { cmd: 'npx', args: ['eslint', '--fix', 'src/'], cwd: './packages/app' }, 112 | { cmd: 'node', args: ['build.js', '--prod'], cwd: '../tools' }, 113 | { 114 | cmd: 'git', 115 | args: ['commit', '-m', 'feat: add new feature'], 116 | cwd: process.cwd(), 117 | }, 118 | ]; 119 | 120 | console.log('Formatted commands:'); 121 | commands.forEach(({ cmd, args, cwd }) => { 122 | const formatted = formatCommandLog(cmd, args, cwd); 123 | console.log(formatted); 124 | }); 125 | 126 | // Performance timing example 127 | async function timedOperation() { 128 | const start = performance.now(); 129 | 130 | // Simulate some work 131 | await new Promise((resolve) => setTimeout(resolve, 150)); 132 | 133 | const duration = calcDuration(start); 134 | console.log(`Operation completed in ${duration}ms`); 135 | } 136 | 137 | // Verbose logging check 138 | if (isVerbose()) { 139 | console.log('🔍 Verbose logging is enabled'); 140 | await timedOperation(); 141 | } else { 142 | console.log('🔇 Verbose logging is disabled'); 143 | } 144 | 145 | // Output (with ANSI colors in terminal): 146 | // → Formatted commands: 147 | // → $ npm install 148 | // → packages/app $ npx eslint --fix src/ 149 | // → .. $ node build.js --prod 150 | // → $ git commit -m feat: add new feature 151 | // → 🔍 Verbose logging is enabled 152 | // → Operation completed in 152ms 153 | ``` 154 | 155 | --- 156 | 157 | ## 4 — CLI argument generation 158 | 159 | > Convert objects to command-line arguments for process execution. 160 | 161 | ```ts 162 | import { objectToCliArgs, executeProcess } from '@push-based/utils'; 163 | 164 | // Simple configuration object 165 | const config = { 166 | _: ['npx', 'eslint'], // Command and base args 167 | fix: true, // Boolean flag 168 | format: 'json', // String value 169 | ext: ['.ts', '.js'], // Array values 170 | 'max-warnings': 0, // Numeric value 171 | quiet: false, // Negative boolean 172 | }; 173 | 174 | const args = objectToCliArgs(config); 175 | console.log('Generated CLI args:'); 176 | args.forEach((arg) => console.log(` ${arg}`)); 177 | 178 | // Use the generated arguments in process execution 179 | const result = await executeProcess({ 180 | command: args[0], // 'npx' 181 | args: args.slice(1), // Everything after the command 182 | }); 183 | 184 | // Output: 185 | // → Generated CLI args: 186 | // → npx 187 | // → eslint 188 | // → --fix 189 | // → --format="json" 190 | // → --ext=".ts" 191 | // → --ext=".js" 192 | // → --max-warnings=0 193 | // → --no-quiet 194 | 195 | // Complex nested configuration 196 | const complexConfig = { 197 | _: ['node', 'build.js'], 198 | output: { 199 | path: './dist', 200 | format: 'esm', 201 | }, 202 | optimization: { 203 | minify: true, 204 | 'tree-shake': true, 205 | }, 206 | }; 207 | 208 | const complexArgs = objectToCliArgs(complexConfig); 209 | console.log('\nComplex nested args:'); 210 | complexArgs.forEach((arg) => console.log(` ${arg}`)); 211 | 212 | // Output: 213 | // → Complex nested args: 214 | // → node 215 | // → build.js 216 | // → --output.path="./dist" 217 | // → --output.format="esm" 218 | // → --optimization.minify 219 | // → --optimization.tree-shake 220 | ``` 221 | 222 | --- 223 | 224 | ## 5 — Error handling and process management 225 | 226 | > Handle process errors gracefully with comprehensive error information. 227 | 228 | ```ts 229 | import { executeProcess, ProcessError } from '@push-based/utils'; 230 | 231 | async function robustProcessExecution() { 232 | const commands = [ 233 | { command: 'node', args: ['--version'] }, // ✅ Should succeed 234 | { command: 'nonexistent-command', args: [] }, // ❌ Should fail 235 | { command: 'node', args: ['-e', 'process.exit(1)'] }, // ❌ Should fail with exit code 1 236 | ]; 237 | 238 | for (const config of commands) { 239 | try { 240 | console.log( 241 | `\n🚀 Executing: ${config.command} ${config.args?.join(' ') || ''}` 242 | ); 243 | 244 | const result = await executeProcess({ 245 | ...config, 246 | observer: { 247 | onStdout: (data) => console.log(` 📤 ${data.trim()}`), 248 | onStderr: (data) => console.error(` ❌ ${data.trim()}`), 249 | onComplete: () => console.log(' ✅ Process completed'), 250 | }, 251 | }); 252 | 253 | console.log( 254 | ` ✅ Success! Exit code: ${result.code}, Duration: ${result.duration}ms` 255 | ); 256 | } catch (error) { 257 | if (error instanceof ProcessError) { 258 | console.error(` ❌ Process failed:`); 259 | console.error(` Exit code: ${error.code}`); 260 | console.error( 261 | ` Error output: ${error.stderr.trim() || 'No stderr'}` 262 | ); 263 | console.error( 264 | ` Standard output: ${error.stdout.trim() || 'No stdout'}` 265 | ); 266 | } else { 267 | console.error(` ❌ Unexpected error: ${error}`); 268 | } 269 | } 270 | } 271 | 272 | // Example with ignoreExitCode option 273 | console.log('\n🔄 Executing command with ignoreExitCode=true:'); 274 | try { 275 | const result = await executeProcess({ 276 | command: 'node', 277 | args: ['-e', 'console.log("Hello"); process.exit(1)'], 278 | ignoreExitCode: true, 279 | observer: { 280 | onStdout: (data) => console.log(` 📤 ${data.trim()}`), 281 | onComplete: () => 282 | console.log(' ✅ Process completed (exit code ignored)'), 283 | }, 284 | }); 285 | 286 | console.log(` ✅ Completed with exit code ${result.code} (ignored)`); 287 | console.log(` 📝 Output: ${result.stdout.trim()}`); 288 | } catch (error) { 289 | console.error(` ❌ This shouldn't happen with ignoreExitCode=true`); 290 | } 291 | } 292 | 293 | await robustProcessExecution(); 294 | 295 | // Output: 296 | // → 🚀 Executing: node --version 297 | // → 📤 v18.17.0 298 | // → ✅ Process completed 299 | // → ✅ Success! Exit code: 0, Duration: 42ms 300 | // → 301 | // → 🚀 Executing: nonexistent-command 302 | // → ❌ Process failed: 303 | // → Exit code: null 304 | // → Error output: spawn nonexistent-command ENOENT 305 | // → Standard output: No stdout 306 | // → 307 | // → 🚀 Executing: node -e process.exit(1) 308 | // → ❌ Process failed: 309 | // → Exit code: 1 310 | // → Error output: No stderr 311 | // → Standard output: No stdout 312 | // → 313 | // → 🔄 Executing command with ignoreExitCode=true: 314 | // → 📤 Hello 315 | // → ✅ Process completed (exit code ignored) 316 | // → ✅ Completed with exit code 1 (ignored) 317 | // → 📝 Output: Hello 318 | ``` 319 | 320 | --- 321 | 322 | ## 6 — Advanced file operations with generators 323 | 324 | > Use async generators for efficient file processing. 325 | 326 | ```ts 327 | import { 328 | findAllFiles, 329 | accessContent, 330 | getLineHits, 331 | isExcludedDirectory, 332 | } from '@push-based/utils'; 333 | 334 | // Custom file finder with filtering 335 | async function findLargeTypeScriptFiles( 336 | baseDir: string, 337 | minSize: number = 1000 338 | ) { 339 | const largeFiles: string[] = []; 340 | 341 | // Use async generator to process files one by one 342 | for await (const file of findAllFiles(baseDir, (path) => 343 | path.endsWith('.ts') 344 | )) { 345 | try { 346 | const stats = await fs.stat(file); 347 | if (stats.size > minSize) { 348 | largeFiles.push(file); 349 | console.log(`📁 Large file: ${file} (${stats.size} bytes)`); 350 | } 351 | } catch (error) { 352 | console.warn(`⚠️ Could not stat file: ${file}`); 353 | } 354 | } 355 | 356 | return largeFiles; 357 | } 358 | 359 | // Process file content line by line 360 | async function analyzeFileContent(filePath: string, searchTerm: string) { 361 | const content = await fs.readFile(filePath, 'utf-8'); 362 | const results = { 363 | totalLines: 0, 364 | matchingLines: 0, 365 | matches: [] as Array<{ line: number; hits: number; content: string }>, 366 | }; 367 | 368 | // Use generator to process content efficiently 369 | let lineNumber = 0; 370 | for (const line of accessContent(content)) { 371 | lineNumber++; 372 | results.totalLines++; 373 | 374 | const hits = getLineHits(line, searchTerm); 375 | if (hits.length > 0) { 376 | results.matchingLines++; 377 | results.matches.push({ 378 | line: lineNumber, 379 | hits: hits.length, 380 | content: line.trim(), 381 | }); 382 | } 383 | } 384 | 385 | return results; 386 | } 387 | 388 | // Directory filtering 389 | const directories = [ 390 | 'src', 391 | '.git', 392 | 'node_modules', 393 | 'dist', 394 | 'coverage', 395 | '.vscode', 396 | ]; 397 | directories.forEach((dir) => { 398 | const excluded = isExcludedDirectory(dir); 399 | console.log(`${dir}: ${excluded ? '❌ excluded' : '✅ included'}`); 400 | }); 401 | 402 | // Usage example 403 | const largeFiles = await findLargeTypeScriptFiles('./src', 2000); 404 | if (largeFiles.length > 0) { 405 | const analysis = await analyzeFileContent(largeFiles[0], 'export'); 406 | console.log(`\nAnalysis of ${largeFiles[0]}:`); 407 | console.log(`Total lines: ${analysis.totalLines}`); 408 | console.log(`Lines with 'export': ${analysis.matchingLines}`); 409 | console.log(`First few matches:`); 410 | analysis.matches.slice(0, 3).forEach((match) => { 411 | console.log(` Line ${match.line} (${match.hits} hits): ${match.content}`); 412 | }); 413 | } 414 | 415 | // Output: 416 | // → src: ✅ included 417 | // → .git: ❌ excluded 418 | // → node_modules: ❌ excluded 419 | // → dist: ❌ excluded 420 | // → coverage: ❌ excluded 421 | // → .vscode: ✅ included 422 | // → 📁 Large file: ./src/lib/utils.ts (2247 bytes) 423 | // → 📁 Large file: ./src/lib/execute-process.ts (5043 bytes) 424 | // → 425 | // → Analysis of ./src/lib/utils.ts: 426 | // → Total lines: 88 427 | // → Lines with 'export': 5 428 | // → First few matches: 429 | // → Line 2 (1 hits): export function calcDuration(start: number, stop?: number): number { 430 | // → Line 6 (1 hits): export function isVerbose(): boolean { 431 | // → Line 14 (1 hits): export function formatCommandLog(command: string, args?: string[], cwd?: string): string { 432 | ``` 433 | 434 | --- 435 | 436 | ## 7 — ES Module loading and dynamic imports 437 | 438 | > Load ES modules dynamically and extract default exports safely. 439 | 440 | ```ts 441 | import { loadDefaultExport } from '@push-based/utils'; 442 | 443 | // Load configuration from ES module 444 | const config = await loadDefaultExport('./config/app.config.mjs'); 445 | console.log(`API Port: ${config.port}`); 446 | 447 | // Load with type safety 448 | interface AppData { 449 | version: string; 450 | features: string[]; 451 | } 452 | 453 | const appData = await loadDefaultExport<AppData>('./data/app.mjs'); 454 | console.log(`App version: ${appData.version}`); 455 | console.log(`Features: ${appData.features.join(', ')}`); 456 | 457 | // Handle loading errors gracefully 458 | try { 459 | const plugin = await loadDefaultExport('./plugins/optional.mjs'); 460 | console.log('✅ Plugin loaded'); 461 | } catch (error) { 462 | if (error.message.includes('No default export found')) { 463 | console.warn('⚠️ Module missing default export'); 464 | } else { 465 | console.warn('⚠️ Plugin not found, continuing without it'); 466 | } 467 | } 468 | 469 | // Output: 470 | // → API Port: 3000 471 | // → App version: 1.2.0 472 | // → Features: auth, logging, metrics 473 | // → ⚠️ Plugin not found, continuing without it 474 | ``` 475 | 476 | --- 477 | 478 | 479 | 480 | These examples demonstrate the comprehensive capabilities of the `@push-based/utils` library for process execution, file operations, string manipulation, and development tooling in Node.js applications. 481 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/complex-components/second-case/complex-badge-widget.component.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | ElementRef, 5 | ViewEncapsulation, 6 | computed, 7 | input, 8 | output, 9 | signal, 10 | booleanAttribute, 11 | OnInit, 12 | OnDestroy, 13 | inject, 14 | Renderer2, 15 | } from '@angular/core'; 16 | import { CommonModule } from '@angular/common'; 17 | import { FormsModule } from '@angular/forms'; 18 | 19 | export interface BadgeConfig { 20 | id: string; 21 | text: string; 22 | type: 'offer-badge' | 'status' | 'priority'; 23 | level: 'low' | 'medium' | 'high' | 'critical'; 24 | interactive: boolean; 25 | customData: Record<string, any>; 26 | } 27 | 28 | @Component({ 29 | selector: 'app-complex-badge-widget', 30 | standalone: true, 31 | imports: [CommonModule, FormsModule], 32 | template: ` 33 | <div class="widget-container"> 34 | <h3>Complex Badge Widget</h3> 35 | 36 | <!-- This component relies heavily on custom badge structure that will break --> 37 | <div class="badge-grid"> 38 | @for (badge of badges(); track badge.id) { 39 | <div class="badge-item" [attr.data-badge-id]="badge.id"> 40 | <!-- Complex custom badge with nested structure --> 41 | <div 42 | class="offer-badge offer-badge-{{ badge.level }} offer-badge-{{ badge.type }}" 43 | [class.offer-badge-interactive]="badge.interactive" 44 | [class.offer-badge-selected]="isSelected(badge.id)" 45 | (click)="toggleBadge(badge.id)" 46 | [attr.data-custom-prop]="badge.customData.prop" 47 | [style.--badge-color]="getBadgeColor(badge.level)"> 48 | 49 | <!-- This structure is incompatible with DsBadge slots --> 50 | <div class="offer-badge-header"> 51 | <span class="offer-badge-type-indicator">{{ getTypeIcon(badge.type) }}</span> 52 | <div class="offer-badge-level-dots"> 53 | @for (dot of getLevelDots(badge.level); track $index) { 54 | <span class="level-dot" [class.active]="dot"></span> 55 | } 56 | </div> 57 | </div> 58 | 59 | <!-- Main content with complex nested structure --> 60 | <div class="offer-badge-content"> 61 | <div class="offer-badge-primary-text">{{ badge.text }}</div> 62 | <div class="offer-badge-metadata"> 63 | <span class="badge-id">ID: {{ badge.id }}</span> 64 | <span class="badge-timestamp">{{ getTimestamp() }}</span> 65 | </div> 66 | </div> 67 | 68 | <!-- Footer with actions - incompatible with DsBadge --> 69 | <div class="offer-badge-footer"> 70 | @if (badge.interactive) { 71 | <button class="badge-action" (click)="editBadge($event, badge.id)">Edit</button> 72 | <button class="badge-action danger" (click)="deleteBadge($event, badge.id)">×</button> 73 | } 74 | <div class="badge-status-indicator" [class]="getStatusClass(badge)"></div> 75 | </div> 76 | </div> 77 | 78 | <!-- Additional complex elements that depend on custom structure --> 79 | <div class="badge-tooltip" [class.visible]="showTooltip() === badge.id"> 80 | <div class="tooltip-content"> 81 | <h4>{{ badge.text }}</h4> 82 | <p>Type: {{ badge.type }}</p> 83 | <p>Level: {{ badge.level }}</p> 84 | <div class="custom-data"> 85 | @for (item of getCustomDataEntries(badge.customData); track item.key) { 86 | <div class="data-item"> 87 | <strong>{{ item.key }}:</strong> {{ item.value }} 88 | </div> 89 | } 90 | </div> 91 | </div> 92 | </div> 93 | </div> 94 | } 95 | </div> 96 | 97 | <!-- Controls that manipulate badge structure directly --> 98 | <div class="widget-controls"> 99 | <button (click)="addRandomBadge()">Add Random Badge</button> 100 | <button (click)="modifyAllBadges()">Modify All Badges</button> 101 | <button (click)="resetBadges()">Reset</button> 102 | <label> 103 | <input type="checkbox" [(ngModel)]="enableAdvancedMode" (change)="onAdvancedModeChange()"> 104 | Advanced Mode (manipulates DOM directly) 105 | </label> 106 | </div> 107 | 108 | <!-- Status display that reads from custom badge elements --> 109 | <div class="status-display"> 110 | <h4>Status Summary:</h4> 111 | <div class="status-grid"> 112 | <div class="status-item"> 113 | <strong>Total Badges:</strong> {{ badges().length }} 114 | </div> 115 | <div class="status-item"> 116 | <strong>Interactive:</strong> {{ getInteractiveBadgesCount() }} 117 | </div> 118 | <div class="status-item"> 119 | <strong>Selected:</strong> {{ selectedBadges().length }} 120 | </div> 121 | <div class="status-item"> 122 | <strong>Critical Level:</strong> {{ getCriticalBadgesCount() }} 123 | </div> 124 | </div> 125 | </div> 126 | </div> 127 | `, 128 | styleUrls: ['./complex-badge-widget.component.scss'], 129 | encapsulation: ViewEncapsulation.None, 130 | changeDetection: ChangeDetectionStrategy.OnPush, 131 | }) 132 | export class ComplexBadgeWidgetComponent implements OnInit, OnDestroy { 133 | // Inputs 134 | initialBadges = input<BadgeConfig[]>([]); 135 | enableAdvancedMode = signal(false); 136 | showTooltip = signal<string | null>(null); 137 | 138 | // Outputs 139 | badgeSelected = output<string>(); 140 | badgeModified = output<BadgeConfig>(); 141 | badgeDeleted = output<string>(); 142 | 143 | // Internal state 144 | badges = signal<BadgeConfig[]>([]); 145 | selectedBadges = signal<string[]>([]); 146 | 147 | private elementRef = inject(ElementRef); 148 | private renderer = inject(Renderer2); 149 | private badgeCounter = 0; 150 | 151 | ngOnInit() { 152 | this.initializeBadges(); 153 | this.setupAdvancedModeObserver(); 154 | } 155 | 156 | ngOnDestroy() { 157 | // Cleanup 158 | } 159 | 160 | private initializeBadges() { 161 | const defaultBadges: BadgeConfig[] = [ 162 | { 163 | id: 'badge-1', 164 | text: 'Premium Offer', 165 | type: 'offer-badge', 166 | level: 'high', 167 | interactive: true, 168 | customData: { prop: 'premium', priority: 1, category: 'sales' } 169 | }, 170 | { 171 | id: 'badge-2', 172 | text: 'System Status', 173 | type: 'status', 174 | level: 'medium', 175 | interactive: false, 176 | customData: { prop: 'status', health: 'good', uptime: '99.9%' } 177 | }, 178 | { 179 | id: 'badge-3', 180 | text: 'Critical Alert', 181 | type: 'priority', 182 | level: 'critical', 183 | interactive: true, 184 | customData: { prop: 'alert', severity: 'high', source: 'monitoring' } 185 | } 186 | ]; 187 | 188 | this.badges.set(this.initialBadges().length > 0 ? this.initialBadges() : defaultBadges); 189 | } 190 | 191 | private setupAdvancedModeObserver() { 192 | // This method directly manipulates DOM elements in a way that will break with DsBadge 193 | // because it expects specific custom badge structure 194 | } 195 | 196 | // Methods that rely on custom badge structure 197 | getBadgeColor(level: string): string { 198 | const colorMap: Record<string, string> = { 199 | 'low': '#10b981', 200 | 'medium': '#f59e0b', 201 | 'high': '#3b82f6', 202 | 'critical': '#ef4444' 203 | }; 204 | return colorMap[level] || '#6b7280'; 205 | } 206 | 207 | getTypeIcon(type: string): string { 208 | const iconMap: Record<string, string> = { 209 | 'offer-badge': '🎯', 210 | 'status': '📊', 211 | 'priority': '⚠️' 212 | }; 213 | return iconMap[type] || '📌'; 214 | } 215 | 216 | getLevelDots(level: string): boolean[] { 217 | const dotMap: Record<string, boolean[]> = { 218 | 'low': [true, false, false], 219 | 'medium': [true, true, false], 220 | 'high': [true, true, true], 221 | 'critical': [true, true, true] 222 | }; 223 | return dotMap[level] || [false, false, false]; 224 | } 225 | 226 | getStatusClass(badge: BadgeConfig): string { 227 | return `status-${badge.level}-${badge.type}`; 228 | } 229 | 230 | getCustomDataEntries(customData: Record<string, any>): Array<{key: string, value: any}> { 231 | return Object.entries(customData).map(([key, value]) => ({ key, value })); 232 | } 233 | 234 | getTimestamp(): string { 235 | return new Date().toLocaleTimeString(); 236 | } 237 | 238 | // Interactive methods that depend on custom structure 239 | isSelected(badgeId: string): boolean { 240 | return this.selectedBadges().includes(badgeId); 241 | } 242 | 243 | toggleBadge(badgeId: string) { 244 | const selected = this.selectedBadges(); 245 | if (selected.includes(badgeId)) { 246 | this.selectedBadges.set(selected.filter(id => id !== badgeId)); 247 | } else { 248 | this.selectedBadges.set([...selected, badgeId]); 249 | } 250 | this.badgeSelected.emit(badgeId); 251 | } 252 | 253 | editBadge(event: Event, badgeId: string) { 254 | event.stopPropagation(); 255 | const badge = this.badges().find(b => b.id === badgeId); 256 | if (badge) { 257 | // Simulate editing 258 | const updatedBadge = { ...badge, text: badge.text + ' (edited)' }; 259 | this.updateBadge(updatedBadge); 260 | this.badgeModified.emit(updatedBadge); 261 | } 262 | } 263 | 264 | deleteBadge(event: Event, badgeId: string) { 265 | event.stopPropagation(); 266 | this.badges.set(this.badges().filter(b => b.id !== badgeId)); 267 | this.selectedBadges.set(this.selectedBadges().filter(id => id !== badgeId)); 268 | this.badgeDeleted.emit(badgeId); 269 | } 270 | 271 | addRandomBadge() { 272 | this.badgeCounter++; 273 | const types: BadgeConfig['type'][] = ['offer-badge', 'status', 'priority']; 274 | const levels: BadgeConfig['level'][] = ['low', 'medium', 'high', 'critical']; 275 | 276 | const newBadge: BadgeConfig = { 277 | id: `badge-${Date.now()}-${this.badgeCounter}`, 278 | text: `Dynamic Badge ${this.badgeCounter}`, 279 | type: types[Math.floor(Math.random() * types.length)], 280 | level: levels[Math.floor(Math.random() * levels.length)], 281 | interactive: Math.random() > 0.5, 282 | customData: { 283 | prop: `dynamic-${this.badgeCounter}`, 284 | generated: true, 285 | timestamp: Date.now() 286 | } 287 | }; 288 | 289 | this.badges.set([...this.badges(), newBadge]); 290 | } 291 | 292 | modifyAllBadges() { 293 | // This method directly manipulates DOM to demonstrate breaking changes 294 | if (this.enableAdvancedMode()) { 295 | const badgeElements = this.elementRef.nativeElement.querySelectorAll('.offer-badge'); 296 | badgeElements.forEach((element: HTMLElement, index: number) => { 297 | // Direct DOM manipulation that will break with DsBadge 298 | const contentDiv = element.querySelector('.offer-badge-content'); 299 | if (contentDiv) { 300 | this.renderer.setStyle(contentDiv, 'transform', `rotate(${index * 2}deg)`); 301 | this.renderer.addClass(element, 'modified-badge'); 302 | } 303 | 304 | // Add custom attributes that DsBadge won't support 305 | this.renderer.setAttribute(element, 'data-modification-time', Date.now().toString()); 306 | this.renderer.setAttribute(element, 'data-custom-behavior', 'advanced'); 307 | }); 308 | } 309 | } 310 | 311 | resetBadges() { 312 | this.initializeBadges(); 313 | this.selectedBadges.set([]); 314 | this.showTooltip.set(null); 315 | 316 | // Reset DOM modifications 317 | const badgeElements = this.elementRef.nativeElement.querySelectorAll('.offer-badge'); 318 | badgeElements.forEach((element: HTMLElement) => { 319 | this.renderer.removeStyle(element, 'transform'); 320 | this.renderer.removeClass(element, 'modified-badge'); 321 | this.renderer.removeAttribute(element, 'data-modification-time'); 322 | this.renderer.removeAttribute(element, 'data-custom-behavior'); 323 | }); 324 | } 325 | 326 | onAdvancedModeChange() { 327 | if (this.enableAdvancedMode()) { 328 | // Enable advanced DOM manipulation features 329 | this.setupAdvancedBehaviors(); 330 | } else { 331 | this.resetBadges(); 332 | } 333 | } 334 | 335 | private setupAdvancedBehaviors() { 336 | // This method sets up behaviors that depend on custom badge DOM structure 337 | // and will break when using DsBadge 338 | setTimeout(() => { 339 | const badgeElements = this.elementRef.nativeElement.querySelectorAll('.offer-badge'); 340 | badgeElements.forEach((element: HTMLElement) => { 341 | // Add hover effects that depend on custom structure 342 | element.addEventListener('mouseenter', () => { 343 | const header = element.querySelector('.offer-badge-header'); 344 | const footer = element.querySelector('.offer-badge-footer'); 345 | if (header && footer) { 346 | this.renderer.addClass(header, 'hover-active'); 347 | this.renderer.addClass(footer, 'hover-active'); 348 | } 349 | }); 350 | 351 | element.addEventListener('mouseleave', () => { 352 | const header = element.querySelector('.offer-badge-header'); 353 | const footer = element.querySelector('.offer-badge-footer'); 354 | if (header && footer) { 355 | this.renderer.removeClass(header, 'hover-active'); 356 | this.renderer.removeClass(footer, 'hover-active'); 357 | } 358 | }); 359 | }); 360 | }, 100); 361 | } 362 | 363 | private updateBadge(updatedBadge: BadgeConfig) { 364 | const badges = this.badges(); 365 | const index = badges.findIndex(b => b.id === updatedBadge.id); 366 | if (index !== -1) { 367 | badges[index] = updatedBadge; 368 | this.badges.set([...badges]); 369 | } 370 | } 371 | 372 | // Computed values that depend on custom badge structure 373 | getInteractiveBadgesCount(): number { 374 | return this.badges().filter(b => b.interactive).length; 375 | } 376 | 377 | getCriticalBadgesCount(): number { 378 | return this.badges().filter(b => b.level === 'critical').length; 379 | } 380 | } ``` -------------------------------------------------------------------------------- /docs/component-refactoring-flow.md: -------------------------------------------------------------------------------- ```markdown 1 | # Component Refactoring Flow 2 | 3 | ## Overview 4 | 5 | This document describes a 3-step AI-assisted component refactoring process for improving individual Angular components according to modern best practices. Each step uses a specific rule file (.mdc) that guides the Cursor agent through systematic analysis, code improvements, and validation. 6 | 7 | **Process Summary:** 8 | 1. **Review Component** → Analyze component against best practices and create improvement plan 9 | 2. **Refactor Component** → Execute approved checklist items and implement changes 10 | 3. **Validate Component** → Verify improvements through contract comparison and scoring 11 | 12 | The process includes two quality gates where human review and approval are required. When refactoring involves Design System components, the process can leverage selective data retrieval to access only the specific component information needed (implementation, documentation, or stories). 13 | 14 | ## Prerequisites 15 | 16 | Before starting the component refactoring flow, ensure you have: 17 | - Cursor IDE with this MCP (Model Context Protocol) server connected. This flow was tested with Cursor but should also work with Windsurf or Copilot. 18 | - The three rule files (.mdc) available in your workspace 19 | - A git branch for the refactoring work 20 | - The component files (TypeScript, template, and styles) accessible in your workspace 21 | 22 | ## 01-review-component.mdc 23 | 24 | ### Goal 25 | 26 | Analyze an Angular component against modern best practices and design system guidelines to create a comprehensive improvement plan. This rule evaluates component quality across five key dimensions and generates an actionable refactoring checklist. 27 | 28 | ### Process 29 | 30 | To start this process, drag file `01-review-component.mdc` to the cursor chat and provide the required parameters: 31 | 32 | ``` 33 | component_path=path/to/component.ts 34 | styleguide="Angular 20 best practices with signals, standalone components, and modern control flow" 35 | component_files=[provide the TypeScript, template, and style file contents] 36 | 37 | @01-review-component.mdc 38 | ``` 39 | 40 | This rule follows a structured analysis process: 41 | 42 | **Step 1: File Validation** 43 | - Verifies all essential component files are provided (TypeScript, template, styles) 44 | - Ensures component structure is complete for analysis 45 | - Stops process if critical files are missing 46 | 47 | **Step 2: Multi-Dimensional Analysis** 48 | - Evaluates component against five key categories: 49 | - **Accessibility**: ARIA attributes, semantic HTML, keyboard navigation 50 | - **Performance**: Change detection strategy, lazy loading, bundle size impact 51 | - **Scalability**: Code organization, reusability, maintainability patterns 52 | - **Maintainability**: Code clarity, documentation, testing considerations 53 | - **Best Practices**: Angular conventions, TypeScript usage, modern patterns 54 | 55 | **Step 3: Scoring and Assessment** 56 | - Assigns numerical scores (1-10) for each category 57 | - Identifies 3-5 concrete observations per category 58 | - Provides narrative analysis of overall component state 59 | 60 | **Step 4: Checklist Generation** 61 | - Creates actionable improvement items based on analysis 62 | - Prioritizes changes by impact and complexity 63 | - Formats as markdown checklist for systematic execution 64 | 65 | --- 66 | **🚦 Quality Gate 1** 67 | 68 | Before proceeding to implementation, you must review and approve the refactoring plan. 69 | 70 | **Required Actions:** 71 | - Review the component analysis and scores 72 | - Examine the proposed refactoring checklist 73 | - Approve the plan or request modifications 74 | 75 | **Next Step:** When satisfied with the checklist, attach the next rule file. 76 | --- 77 | 78 | ### Tools used 79 | 80 | None - This rule performs static analysis based on provided component files and styleguide requirements. 81 | 82 | ### Flow 83 | 84 | > You don't need to manually perform any of the listed actions except providing the initial parameters. 85 | 86 | 1. **Input Validation**: Verify component files and styleguide are provided 87 | 2. **File Structure Check**: Ensure TypeScript, template, and style files are available 88 | 3. **Multi-Category Analysis**: Evaluate component against five key dimensions 89 | 4. **Scoring Assignment**: Assign numerical scores (1-10) for each category 90 | 5. **Observation Collection**: Identify 3-5 concrete issues per category 91 | 6. **Narrative Generation**: Create overall component state summary 92 | 7. **Checklist Creation**: Generate actionable improvement items 93 | 8. **Approval Request**: Present checklist for user review and approval 94 | 95 | The rule enforces structured output with `<component_analysis>`, `<scoring>`, and `<refactoring_checklist>` tags, ensuring comprehensive coverage and clear next steps. 96 | 97 | ### Preferred model 98 | 99 | Claude-4-Sonnet 100 | 101 | ## 02-refactor-component.mdc 102 | 103 | ### Goal 104 | 105 | Execute the approved refactoring checklist by implementing code changes, tracking progress, and maintaining component contracts for validation. This rule systematically processes each checklist item and documents all modifications made to the component. 106 | 107 | ### Process 108 | 109 | To start this process, drag file `02-refactor-component.mdc` to the cursor chat and provide: 110 | 111 | ``` 112 | component_path=path/to/component.ts 113 | checklist_content=[the approved checklist from step 1] 114 | 115 | @02-refactor-component.mdc 116 | ``` 117 | 118 | The rule implements a systematic refactoring execution process: 119 | 120 | **Step 1: Pre-Refactor Contract Generation** 121 | - Creates baseline component contract capturing current state 122 | - Documents component's public API, DOM structure, and styles 123 | - Establishes reference point for later validation 124 | - Stops process if contract generation fails 125 | 126 | **Step 2: Checklist Processing** 127 | - Iterates through each unchecked item in the approved checklist 128 | - Implements necessary code changes using standard editing tools 129 | - Marks completed items with explanatory notes 130 | - Handles ambiguous items by requesting user clarification 131 | 132 | **Step 3: Progress Documentation** 133 | - Updates checklist with completion status and change descriptions 134 | - Saves updated checklist to `.cursor/tmp/component-refactor-checklist-{{COMPONENT_PATH}}.md` 135 | - Maintains audit trail of all modifications 136 | 137 | **Step 4: Summary Generation** 138 | - Creates comprehensive summary of completed changes 139 | - Documents what was modified and why 140 | - Provides updated checklist in markdown format 141 | 142 | --- 143 | **🚦 Quality Gate 2** 144 | 145 | At this point, all checklist items have been processed. You must review the refactoring results. 146 | 147 | **Required Actions:** 148 | - Review the refactor summary and completed changes 149 | - Verify all checklist items were addressed appropriately 150 | - Resolve any ambiguities or questions 151 | 152 | **Next Step:** After approving the refactoring results, attach the validation rule file. 153 | --- 154 | 155 | ### Tools used 156 | 157 | - `build_component_contract` - Creates component contracts for safe refactoring 158 | - Parameters: `saveLocation`, `typescriptFile` (required), `templateFile` (optional), `styleFile` (optional), `dsComponentName` (optional, set to "AUTO") 159 | - Returns: contract path with component's public API, DOM structure, and styles 160 | - Purpose: Establish baseline for validation comparison 161 | - Note: Template and style files are optional for components with inline templates/styles 162 | 163 | - `get-ds-component-data` - Retrieves Design System component information when needed 164 | - Parameters: `componentName`, `sections` (optional) - Array of sections to include: "implementation", "documentation", "stories", "all" 165 | - Returns: Selective component data based on refactoring needs 166 | - Purpose: Access DS component documentation and examples for proper implementation patterns 167 | 168 | ### Flow 169 | 170 | > You don't need to manually perform any of the listed actions except providing the initial parameters. 171 | 172 | 1. **Contract Generation**: Create pre-refactor component contract 173 | 2. **Error Handling**: Verify contract creation succeeded 174 | 3. **Checklist Iteration**: Process each unchecked item systematically 175 | 4. **Code Implementation**: Execute necessary changes for each item 176 | 5. **Progress Tracking**: Mark items complete with explanatory notes 177 | 6. **Ambiguity Resolution**: Request clarification for unclear items 178 | 7. **Checklist Persistence**: Save updated checklist to temporary file 179 | 8. **Summary Creation**: Generate comprehensive refactoring summary 180 | 9. **Completion Confirmation**: Request user approval to proceed to validation 181 | 182 | The rule enforces structured output with `<refactor_summary>` and `<checklist_updated>` tags, ensuring complete documentation of all changes and clear transition to validation. 183 | 184 | ### Preferred model 185 | 186 | Claude-4-Sonnet 187 | 188 | ## 03-validate-component.mdc 189 | 190 | ### Goal 191 | 192 | Analyze the refactored component by comparing before and after contracts, re-evaluating quality scores, and providing comprehensive validation assessment. This rule ensures refactoring improvements are measurable and identifies any remaining issues. 193 | 194 | ### Process 195 | 196 | To start this process, drag file `03-validate-component.mdc` to the cursor chat and provide: 197 | 198 | ``` 199 | component_path=path/to/component.ts 200 | baseline_contract_path=path/to/baseline/contract.json 201 | 202 | @03-validate-component.mdc 203 | ``` 204 | 205 | The rule implements a comprehensive validation process: 206 | 207 | **Step 1: Post-Refactor Contract Generation** 208 | - Creates new component contract capturing refactored state 209 | - Documents updated component API, DOM structure, and styles 210 | - Establishes comparison point against baseline contract 211 | 212 | **Step 2: Contract Comparison** 213 | - Performs detailed diff analysis between baseline and updated contracts 214 | - Identifies specific changes in component structure and behavior 215 | - Analyzes impact of modifications on component functionality 216 | 217 | **Step 3: Quality Re-Assessment** 218 | - Re-evaluates component against the same five categories from initial review: 219 | - **Accessibility**: Impact of changes on accessibility features 220 | - **Performance**: Improvements or regressions in performance metrics 221 | - **Scalability**: Changes affecting component scalability 222 | - **Maintainability**: Impact on code maintainability and clarity 223 | - **Best Practices**: Adherence to modern Angular best practices 224 | 225 | **Step 4: Score Calculation** 226 | - Assigns new scores (1-10) for each category 227 | - Calculates deltas showing improvement or regression 228 | - Provides objective measurement of refactoring success 229 | 230 | **Step 5: Validation Assessment** 231 | - Determines overall refactoring success or identifies remaining issues 232 | - Highlights any risks or necessary follow-ups 233 | - Provides final judgment on component quality 234 | 235 | ### Tools used 236 | 237 | - `build_component_contract` - Creates post-refactor component contract 238 | - Parameters: `saveLocation`, `typescriptFile` (required), `templateFile` (optional), `styleFile` (optional), `dsComponentName` (optional) 239 | - Returns: updated contract path with refactored component state 240 | - Purpose: Capture final component state for comparison 241 | - Note: Template and style files are optional for components with inline templates/styles 242 | 243 | - `diff_component_contract` - Compares baseline and updated contracts 244 | - Parameters: `saveLocation`, `contractBeforePath`, `contractAfterPath`, `dsComponentName` 245 | - Returns: detailed diff analysis showing specific changes 246 | - Purpose: Identify and analyze all modifications made during refactoring 247 | 248 | ### Flow 249 | 250 | > You don't need to manually perform any of the listed actions except providing the initial parameters. 251 | 252 | 1. **Post-Contract Generation**: Create contract for refactored component 253 | 2. **Error Handling**: Verify contract creation succeeded 254 | 3. **Contract Comparison**: Generate diff analysis between baseline and updated contracts 255 | 4. **Change Analysis**: Analyze diff results for impact assessment 256 | 5. **Quality Re-Evaluation**: Re-score component across five categories 257 | 6. **Delta Calculation**: Compute improvement or regression metrics 258 | 7. **Impact Assessment**: Evaluate overall refactoring effectiveness 259 | 8. **Validation Judgment**: Determine success status and identify remaining issues 260 | 9. **Final Report**: Generate comprehensive validation assessment 261 | 262 | The rule enforces structured output with `<diff_summary>`, `<new_scoring>`, and `<validation_assessment>` tags, ensuring objective measurement of refactoring success and clear identification of any remaining concerns. 263 | 264 | ### Preferred model 265 | 266 | Claude-4-Sonnet 267 | 268 | ## Integration with Angular 20 Best Practices 269 | 270 | The component refactoring flow specifically targets modern Angular development patterns as outlined in the `angular-20.md` reference document. Key focus areas include: 271 | 272 | ### TypeScript Modernization 273 | - **Strict Type Checking**: Ensures proper type safety throughout component 274 | - **Type Inference**: Reduces verbosity while maintaining type safety 275 | - **Avoiding `any`**: Promotes use of `unknown` and proper type definitions 276 | 277 | ### Angular Modern Patterns 278 | - **Standalone Components**: Migrates from NgModule-based to standalone architecture 279 | - **Signals for State Management**: Adopts reactive state management with Angular Signals 280 | - **New Input/Output Syntax**: Replaces decorators with `input()` and `output()` functions 281 | - **Computed Properties**: Implements `computed()` for derived state 282 | 283 | ### Template Modernization 284 | - **Control Flow Syntax**: Migrates from structural directives to `@if`, `@for`, `@switch` 285 | - **Native Class/Style Bindings**: Replaces `ngClass`/`ngStyle` with native bindings 286 | - **OnPush Change Detection**: Implements performance optimization strategies 287 | 288 | ### Service and Dependency Injection 289 | - **Inject Function**: Adopts modern `inject()` function over constructor injection 290 | - **Providable Services**: Ensures proper service registration and tree-shaking 291 | 292 | This comprehensive approach ensures components are not only improved but also aligned with the latest Angular best practices and performance recommendations. ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/utils/typescript-analyzer.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as ts from 'typescript'; 2 | import { 3 | MethodSignature, 4 | ParameterInfo, 5 | ImportInfo, 6 | } from '../../shared/models/types.js'; 7 | import { 8 | DecoratorInputMeta, 9 | DecoratorOutputMeta, 10 | SignalInputMeta, 11 | SignalOutputMeta, 12 | ExtractedInputsOutputs, 13 | } from '../models/types.js'; 14 | import { ParsedComponent } from '@push-based/angular-ast-utils'; 15 | 16 | /** 17 | * Angular lifecycle hooks that we can detect 18 | */ 19 | const LIFECYCLE_HOOKS = new Set<string>([ 20 | 'OnInit', 21 | 'OnDestroy', 22 | 'OnChanges', 23 | 'DoCheck', 24 | 'AfterContentInit', 25 | 'AfterContentChecked', 26 | 'AfterViewInit', 27 | 'AfterViewChecked', 28 | ]); 29 | 30 | /** 31 | * Utility: check whether a node has a given modifier 32 | */ 33 | const hasModifier = ( 34 | node: { modifiers?: ts.NodeArray<ts.ModifierLike> }, 35 | kind: ts.SyntaxKind, 36 | ): boolean => node.modifiers?.some((m) => m.kind === kind) ?? false; 37 | 38 | /** 39 | * Extract public methods from a TypeScript class declaration 40 | */ 41 | export function extractPublicMethods( 42 | classNode: ts.ClassDeclaration, 43 | sourceFile: ts.SourceFile, 44 | ): Record<string, MethodSignature> { 45 | const methods: Record<string, MethodSignature> = {}; 46 | 47 | for (const member of classNode.members) { 48 | if (!ts.isMethodDeclaration(member) || !ts.isIdentifier(member.name)) { 49 | continue; 50 | } 51 | 52 | const methodName = member.name.text; 53 | 54 | if ( 55 | methodName.startsWith('ng') && 56 | LIFECYCLE_HOOKS.has(methodName.slice(2)) 57 | ) { 58 | continue; 59 | } 60 | 61 | if ( 62 | hasModifier(member, ts.SyntaxKind.PrivateKeyword) || 63 | hasModifier(member, ts.SyntaxKind.ProtectedKeyword) 64 | ) { 65 | continue; 66 | } 67 | 68 | methods[methodName] = { 69 | name: methodName, 70 | parameters: extractParameters(member.parameters, sourceFile), 71 | returnType: extractReturnType(member, sourceFile), 72 | isPublic: true, 73 | isStatic: hasModifier(member, ts.SyntaxKind.StaticKeyword), 74 | isAsync: hasModifier(member, ts.SyntaxKind.AsyncKeyword), 75 | }; 76 | } 77 | 78 | return methods; 79 | } 80 | 81 | /** 82 | * Extract parameter information from method parameters 83 | */ 84 | function extractParameters( 85 | parameters: ts.NodeArray<ts.ParameterDeclaration>, 86 | sourceFile: ts.SourceFile, 87 | ): ParameterInfo[] { 88 | return parameters.map((param) => ({ 89 | name: ts.isIdentifier(param.name) ? param.name.text : 'unknown', 90 | type: param.type?.getText(sourceFile) ?? 'any', 91 | optional: !!param.questionToken, 92 | defaultValue: param.initializer?.getText(sourceFile), 93 | })); 94 | } 95 | 96 | /** 97 | * Extract return type from method declaration 98 | */ 99 | function extractReturnType( 100 | method: ts.MethodDeclaration, 101 | sourceFile: ts.SourceFile, 102 | ): string { 103 | return ( 104 | method.type?.getText(sourceFile) ?? 105 | (hasModifier(method, ts.SyntaxKind.AsyncKeyword) ? 'Promise<any>' : 'any') 106 | ); 107 | } 108 | 109 | /** 110 | * Extract implemented Angular lifecycle hooks 111 | */ 112 | export function extractLifecycleHooks( 113 | classNode: ts.ClassDeclaration, 114 | ): string[] { 115 | const implementedHooks = new Set<string>(); 116 | 117 | if (classNode.heritageClauses) { 118 | for (const heritage of classNode.heritageClauses) { 119 | if (heritage.token === ts.SyntaxKind.ImplementsKeyword) { 120 | for (const type of heritage.types) { 121 | if (ts.isIdentifier(type.expression)) { 122 | const interfaceName = type.expression.text; 123 | if (LIFECYCLE_HOOKS.has(interfaceName)) { 124 | implementedHooks.add(interfaceName); 125 | } 126 | } 127 | } 128 | } 129 | } 130 | } 131 | 132 | for (const member of classNode.members) { 133 | if (ts.isMethodDeclaration(member) && ts.isIdentifier(member.name)) { 134 | const methodName = member.name.text; 135 | if ( 136 | methodName.startsWith('ng') && 137 | LIFECYCLE_HOOKS.has(methodName.slice(2)) 138 | ) { 139 | implementedHooks.add(methodName.slice(2)); 140 | } 141 | } 142 | } 143 | 144 | return Array.from(implementedHooks); 145 | } 146 | 147 | /** 148 | * Extract TypeScript class declaration from parsed component 149 | * This function finds the class node from the component's source file 150 | */ 151 | export function extractClassDeclaration( 152 | parsedComponent: ParsedComponent, 153 | ): ts.ClassDeclaration | null { 154 | if (!parsedComponent.fileName) { 155 | return null; 156 | } 157 | 158 | try { 159 | const program = ts.createProgram([parsedComponent.fileName], { 160 | target: ts.ScriptTarget.Latest, 161 | module: ts.ModuleKind.ESNext, 162 | experimentalDecorators: true, 163 | }); 164 | 165 | const sourceFile = program.getSourceFile(parsedComponent.fileName); 166 | if (!sourceFile) { 167 | return null; 168 | } 169 | 170 | const classNode = findClassDeclaration( 171 | sourceFile, 172 | parsedComponent.className, 173 | ); 174 | return classNode; 175 | } catch (ctx) { 176 | console.warn( 177 | `Failed to extract class declaration for ${parsedComponent.className}:`, 178 | ctx, 179 | ); 180 | return null; 181 | } 182 | } 183 | 184 | /** 185 | * Find class declaration by name in source file 186 | */ 187 | function findClassDeclaration( 188 | sourceFile: ts.SourceFile, 189 | className: string, 190 | ): ts.ClassDeclaration | null { 191 | let foundClass: ts.ClassDeclaration | null = null; 192 | 193 | function visit(node: ts.Node) { 194 | if ( 195 | ts.isClassDeclaration(node) && 196 | node.name && 197 | node.name.text === className 198 | ) { 199 | foundClass = node; 200 | return; 201 | } 202 | ts.forEachChild(node, visit); 203 | } 204 | 205 | visit(sourceFile); 206 | return foundClass; 207 | } 208 | 209 | /** 210 | * Extract import statements from source file 211 | */ 212 | export function extractImports(sourceFile: ts.SourceFile): ImportInfo[] { 213 | const imports: ImportInfo[] = []; 214 | 215 | function visit(node: ts.Node) { 216 | if (ts.isImportDeclaration(node) && node.moduleSpecifier) { 217 | const moduleSpecifier = node.moduleSpecifier; 218 | if (ts.isStringLiteral(moduleSpecifier)) { 219 | if (node.importClause) { 220 | if (node.importClause.name) { 221 | imports.push({ 222 | name: node.importClause.name.text, 223 | path: moduleSpecifier.text, 224 | }); 225 | } 226 | 227 | if (node.importClause.namedBindings) { 228 | if (ts.isNamespaceImport(node.importClause.namedBindings)) { 229 | imports.push({ 230 | name: node.importClause.namedBindings.name.text, 231 | path: moduleSpecifier.text, 232 | }); 233 | } else if (ts.isNamedImports(node.importClause.namedBindings)) { 234 | for (const element of node.importClause.namedBindings.elements) { 235 | imports.push({ 236 | name: element.name.text, 237 | path: moduleSpecifier.text, 238 | }); 239 | } 240 | } 241 | } 242 | } 243 | } 244 | } 245 | ts.forEachChild(node, visit); 246 | } 247 | 248 | visit(sourceFile); 249 | return imports; 250 | } 251 | 252 | // Helper: determine if class member is a public property with identifier name 253 | function isPublicProp( 254 | member: ts.ClassElement, 255 | ): member is ts.PropertyDeclaration & { name: ts.Identifier } { 256 | return ( 257 | ts.isPropertyDeclaration(member) && 258 | ts.isIdentifier(member.name) && 259 | !hasModifier(member, ts.SyntaxKind.PrivateKeyword) && 260 | !hasModifier(member, ts.SyntaxKind.ProtectedKeyword) 261 | ); 262 | } 263 | 264 | export function extractInputsAndOutputs( 265 | classNode: ts.ClassDeclaration, 266 | sourceFile: ts.SourceFile, 267 | ): ExtractedInputsOutputs { 268 | const inputs: Record<string, DecoratorInputMeta | SignalInputMeta> = {}; 269 | const outputs: Record<string, DecoratorOutputMeta | SignalOutputMeta> = {}; 270 | 271 | for (const member of classNode.members) { 272 | if (!isPublicProp(member)) continue; 273 | 274 | const name = member.name.text; 275 | const { isInput, isOutput, type, required, alias } = 276 | extractDecoratorInputsOutputs(member, sourceFile); 277 | 278 | if (isInput) inputs[name] = { name, type, required, alias } as any; 279 | if (isOutput) outputs[name] = { name, type, alias } as any; 280 | 281 | const init = member.initializer; 282 | if ( 283 | !init || 284 | !ts.isCallExpression(init) || 285 | !ts.isIdentifier(init.expression) 286 | ) { 287 | continue; 288 | } 289 | 290 | switch (init.expression.text) { 291 | case 'input': 292 | inputs[name] = { 293 | name, 294 | ...(extractSignalInputMetadata(init, member, sourceFile) as any), 295 | } as any; 296 | break; 297 | case 'output': 298 | outputs[name] = { 299 | name, 300 | ...(extractSignalOutputMetadata(init, member, sourceFile) as any), 301 | } as any; 302 | break; 303 | } 304 | } 305 | 306 | return { inputs, outputs }; 307 | } 308 | 309 | function extractDecoratorInputsOutputs( 310 | member: ts.PropertyDeclaration, 311 | sourceFile: ts.SourceFile, 312 | ): { 313 | isInput: boolean; 314 | isOutput: boolean; 315 | type?: string; 316 | required?: boolean; 317 | alias?: string; 318 | } { 319 | let isInput = false, 320 | isOutput = false, 321 | alias: string | undefined, 322 | required = false; 323 | 324 | member.modifiers?.forEach((mod) => { 325 | if (!ts.isDecorator(mod)) return; 326 | 327 | const kind = getDecoratorName(mod); // 'Input' | 'Output' | null 328 | if (!kind) return; 329 | 330 | const args = getDecoratorArguments(mod); 331 | const first = args[0]; 332 | 333 | if (first && ts.isStringLiteral(first)) { 334 | alias = first.text; 335 | } 336 | 337 | if (kind === 'Input') { 338 | isInput = true; 339 | if (first && ts.isObjectLiteralExpression(first)) { 340 | first.properties.forEach((p) => { 341 | if (!ts.isPropertyAssignment(p) || !ts.isIdentifier(p.name)) return; 342 | if (p.name.text === 'alias' && ts.isStringLiteral(p.initializer)) { 343 | alias = p.initializer.text; 344 | } 345 | if (p.name.text === 'required') { 346 | required = p.initializer.kind === ts.SyntaxKind.TrueKeyword; 347 | } 348 | }); 349 | } 350 | } else if (kind === 'Output') { 351 | isOutput = true; 352 | } 353 | }); 354 | 355 | const type = extractPropertyType(member, sourceFile, isOutput); 356 | 357 | return { isInput, isOutput, type, required, alias }; 358 | } 359 | 360 | function getDecoratorName(decorator: ts.Decorator): string | null { 361 | if (ts.isCallExpression(decorator.expression)) { 362 | if (ts.isIdentifier(decorator.expression.expression)) { 363 | return decorator.expression.expression.text; 364 | } 365 | } else if (ts.isIdentifier(decorator.expression)) { 366 | return decorator.expression.text; 367 | } 368 | return null; 369 | } 370 | 371 | function getDecoratorArguments(decorator: ts.Decorator): ts.Expression[] { 372 | if (ts.isCallExpression(decorator.expression)) { 373 | return Array.from(decorator.expression.arguments); 374 | } 375 | return []; 376 | } 377 | 378 | function extractPropertyType( 379 | member: ts.PropertyDeclaration, 380 | sourceFile: ts.SourceFile, 381 | isOutput = false, 382 | ): string { 383 | if (member.type) { 384 | const typeText = member.type.getText(sourceFile); 385 | return typeText; 386 | } 387 | 388 | if (member.initializer) { 389 | if (ts.isNewExpression(member.initializer)) { 390 | const expression = member.initializer.expression; 391 | if (ts.isIdentifier(expression)) { 392 | if (expression.text === 'EventEmitter') { 393 | if ( 394 | member.initializer.typeArguments && 395 | member.initializer.typeArguments.length > 0 396 | ) { 397 | return `EventEmitter<${member.initializer.typeArguments[0].getText(sourceFile)}>`; 398 | } 399 | return 'EventEmitter<any>'; 400 | } 401 | } 402 | } 403 | } 404 | 405 | if (isOutput) { 406 | return 'EventEmitter<any>'; 407 | } 408 | return 'any'; 409 | } 410 | 411 | function extractSignalInputMetadata( 412 | callExpression: ts.CallExpression, 413 | propertyDeclaration: ts.PropertyDeclaration, 414 | sourceFile: ts.SourceFile, 415 | ): Omit<SignalInputMeta, 'name'> { 416 | const meta: Omit<SignalInputMeta, 'name'> = {}; 417 | 418 | meta.type = extractInputType(callExpression, propertyDeclaration, sourceFile); 419 | 420 | if (callExpression.arguments.length > 0) { 421 | const firstArg = callExpression.arguments[0]; 422 | meta.defaultValue = firstArg.getText(sourceFile); 423 | meta.required = false; 424 | } else { 425 | meta.required = true; 426 | } 427 | 428 | if (callExpression.arguments.length > 1) { 429 | const optionsArg = callExpression.arguments[1]; 430 | if (ts.isObjectLiteralExpression(optionsArg)) { 431 | for (const property of optionsArg.properties) { 432 | if ( 433 | ts.isPropertyAssignment(property) && 434 | ts.isIdentifier(property.name) 435 | ) { 436 | const propName = property.name.text; 437 | if (propName === 'transform') { 438 | meta.transform = property.initializer.getText(sourceFile); 439 | } 440 | } 441 | } 442 | } 443 | } 444 | 445 | return meta; 446 | } 447 | 448 | function extractSignalOutputMetadata( 449 | callExpression: ts.CallExpression, 450 | propertyDeclaration: ts.PropertyDeclaration, 451 | sourceFile: ts.SourceFile, 452 | ): Omit<SignalOutputMeta, 'name'> { 453 | const meta: Omit<SignalOutputMeta, 'name'> = {}; 454 | 455 | meta.type = extractOutputType( 456 | callExpression, 457 | propertyDeclaration, 458 | sourceFile, 459 | ); 460 | 461 | return meta; 462 | } 463 | 464 | function extractInputType( 465 | callExpression: ts.CallExpression, 466 | propertyDeclaration: ts.PropertyDeclaration, 467 | sourceFile: ts.SourceFile, 468 | ): string { 469 | if (callExpression.typeArguments && callExpression.typeArguments.length > 0) { 470 | return callExpression.typeArguments[0].getText(sourceFile); 471 | } 472 | 473 | if (propertyDeclaration.type) { 474 | return extractTypeFromInputSignal(propertyDeclaration.type, sourceFile); 475 | } 476 | 477 | return 'any'; 478 | } 479 | 480 | function extractOutputType( 481 | callExpression: ts.CallExpression, 482 | propertyDeclaration: ts.PropertyDeclaration, 483 | sourceFile: ts.SourceFile, 484 | ): string { 485 | if (callExpression.typeArguments && callExpression.typeArguments.length > 0) { 486 | return `EventEmitter<${callExpression.typeArguments[0].getText(sourceFile)}>`; 487 | } 488 | 489 | if (propertyDeclaration.type) { 490 | return extractTypeFromOutputEmitter(propertyDeclaration.type, sourceFile); 491 | } 492 | 493 | return 'EventEmitter<any>'; 494 | } 495 | 496 | function extractTypeFromInputSignal( 497 | typeNode: ts.TypeNode, 498 | sourceFile: ts.SourceFile, 499 | ): string { 500 | if ( 501 | ts.isTypeReferenceNode(typeNode) && 502 | typeNode.typeArguments && 503 | typeNode.typeArguments.length > 0 504 | ) { 505 | return typeNode.typeArguments[0].getText(sourceFile); 506 | } 507 | return typeNode.getText(sourceFile); 508 | } 509 | 510 | function extractTypeFromOutputEmitter( 511 | typeNode: ts.TypeNode, 512 | sourceFile: ts.SourceFile, 513 | ): string { 514 | if ( 515 | ts.isTypeReferenceNode(typeNode) && 516 | typeNode.typeArguments && 517 | typeNode.typeArguments.length > 0 518 | ) { 519 | return `EventEmitter<${typeNode.typeArguments[0].getText(sourceFile)}>`; 520 | } 521 | return `EventEmitter<${typeNode.getText(sourceFile)}>`; 522 | } 523 | 524 | export const extractSignalInputsAndOutputs = extractInputsAndOutputs; 525 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/design-system/storybook-host-app/src/components/modal/modal.component.stories.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { generateStatusBadges } from '@design-system/shared-storybook-utils'; 2 | import { 3 | DemoChevronComponent, 4 | DemoCloseIconComponent, 5 | DemoIconComponent, 6 | } from '@design-system/storybook-demo-cmp-lib'; 7 | import { DsBadge } from '@frontend/ui/badge'; 8 | import { DsButton } from '@frontend/ui/button'; 9 | import { DsButtonIcon } from '@frontend/ui/button-icon'; 10 | import { 11 | DsModal, 12 | DsModalContent, 13 | DsModalHeader, 14 | DsModalHeaderDrag, 15 | DsModalHeaderVariant, 16 | } from '@frontend/ui/modal'; 17 | import { type Meta, type StoryObj, moduleMetadata } from '@storybook/angular'; 18 | 19 | import { DemoCdkModalContainer } from './demo-cdk-dialog-cmp.component'; 20 | import { DemoModalContainer } from './demo-modal-cmp.component'; 21 | 22 | type DsModalStoryType = DsModal & { headerVariant: DsModalHeaderVariant }; 23 | 24 | const meta: Meta<DsModalStoryType> = { 25 | title: 'Components/Modal', 26 | component: DsModal, 27 | parameters: { 28 | status: generateStatusBadges('UX-2414', ['integration ready']), 29 | }, 30 | excludeStories: /.*Data$/, 31 | argTypes: { 32 | headerVariant: { 33 | options: [ 34 | 'surface-lowest', 35 | 'surface-low', 36 | 'surface', 37 | 'surface-high', 38 | 'nav-bg', 39 | ], 40 | table: { defaultValue: { summary: 'surface' }, category: 'Styling' }, 41 | control: { type: 'select' }, 42 | description: 'Surface type', 43 | }, 44 | variant: { 45 | options: ['surface-lowest', 'surface-low', 'surface'], 46 | table: { defaultValue: { summary: 'surface' }, category: 'Styling' }, 47 | control: { type: 'select' }, 48 | description: 'Surface type', 49 | }, 50 | inverse: { 51 | type: 'boolean', 52 | table: { defaultValue: { summary: 'false' }, category: 'Styling' }, 53 | control: { type: 'boolean' }, 54 | description: 'The inverse state of the Modal', 55 | }, 56 | bottomSheet: { 57 | type: 'boolean', 58 | table: { defaultValue: { summary: 'false' }, category: 'Styling' }, 59 | control: { type: 'boolean' }, 60 | description: 'The dialog should open from bottom', 61 | }, 62 | }, 63 | args: { 64 | headerVariant: 'surface', 65 | inverse: false, 66 | bottomSheet: true, 67 | variant: 'surface', 68 | }, 69 | decorators: [ 70 | moduleMetadata({ 71 | imports: [ 72 | DsModal, 73 | DemoIconComponent, 74 | DemoCloseIconComponent, 75 | DsButton, 76 | DsButtonIcon, 77 | DemoChevronComponent, 78 | DsBadge, 79 | DsModalHeader, 80 | DsModalContent, 81 | DsModalHeaderDrag, 82 | DemoModalContainer, 83 | DemoCdkModalContainer, 84 | ], 85 | }), 86 | ], 87 | }; 88 | 89 | export default meta; 90 | type Story = StoryObj<DsModalStoryType>; 91 | 92 | export const Default: Story = { 93 | render: () => ({ 94 | template: `<ds-modal style="width: 250px; min-height: 120px"></ds-modal>`, 95 | }), 96 | }; 97 | 98 | export const WithMatDialog: Story = { 99 | render: (modal) => ({ 100 | template: ` 101 | <div> 102 | <ds-demo-dialog-container headerVariant="${modal.headerVariant}" inverse="${modal.inverse}" variant="${modal.variant}" bottomSheetInput="${modal.bottomSheet}"/> 103 | </div> 104 | `, 105 | }), 106 | }; 107 | 108 | export const WithCdkDialog: Story = { 109 | render: (modal) => ({ 110 | template: ` 111 | <div> 112 | <ds-demo-cdk-dialog-container headerVariant="${modal.headerVariant}" variant="${modal.variant}" inverse="${modal.inverse}" bottomSheetInput="${modal.bottomSheet}" /> 113 | </div> 114 | `, 115 | }), 116 | }; 117 | 118 | export const ModalWithContentOnly: Story = { 119 | argTypes: { 120 | bottomSheet: { 121 | table: { disable: true }, 122 | }, 123 | }, 124 | render: (args) => ({ 125 | template: ` 126 | <ds-modal variant="${args.variant}" inverse="${args.inverse}" style="width: 250px;"> 127 | <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> 128 | </ds-modal> 129 | `, 130 | }), 131 | }; 132 | 133 | export const WithTitleAndClose: Story = { 134 | argTypes: { 135 | bottomSheet: { 136 | table: { disable: true }, 137 | }, 138 | }, 139 | render: (args) => ({ 140 | template: ` 141 | <ds-modal inverse="${args.inverse}" variant="${args.variant}" > 142 | <ds-modal-header variant="${args.headerVariant}"> 143 | <div slot="start"> 144 | <div slot="title">Hello world</div> 145 | </div> 146 | <button slot="end" ds-button-icon size="small" variant="flat" kind="utility"> 147 | <ds-demo-close-icon /> 148 | </button> 149 | </ds-modal-header> 150 | <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> 151 | </ds-modal> 152 | `, 153 | }), 154 | }; 155 | 156 | export const WithDragger: Story = { 157 | argTypes: { 158 | bottomSheet: { 159 | table: { disable: true }, 160 | }, 161 | }, 162 | render: (args) => ({ 163 | template: ` 164 | <ds-modal variant="${args.variant}" inverse="${args.inverse}" > 165 | <ds-modal-header variant="${args.headerVariant}"> 166 | <ds-modal-header-drag /> 167 | </ds-modal-header> 168 | <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> 169 | </ds-modal> 170 | `, 171 | }), 172 | }; 173 | 174 | export const BolderSubtitleThanTitle: Story = { 175 | argTypes: { 176 | bottomSheet: { 177 | table: { disable: true }, 178 | }, 179 | }, 180 | render: (args) => ({ 181 | template: ` 182 | <ds-modal variant="${args.variant}" inverse="${args.inverse}" > 183 | <ds-modal-header variant="${args.headerVariant}"> 184 | <button slot="start" ds-button variant="outline" size="medium">Cancel</button> 185 | <div slot="center"> 186 | <div slot="title">Title</div> 187 | <div slot="subtitle">Subtitle</div> 188 | </div> 189 | <button slot="end" ds-button variant="filled" size="medium"> 190 | <ds-demo-icon slot="start" /> 191 | Agree 192 | </button> 193 | </ds-modal-header> 194 | <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> 195 | </ds-modal> 196 | `, 197 | }), 198 | }; 199 | 200 | export const ActionsAndCenteredTitle: Story = { 201 | argTypes: { 202 | bottomSheet: { 203 | table: { disable: true }, 204 | }, 205 | }, 206 | render: (args) => ({ 207 | template: ` 208 | <ds-modal variant="${args.variant}" inverse="${args.inverse}" > 209 | <ds-modal-header variant="${args.headerVariant}"> 210 | <button slot="start" ds-button variant="outline" size="medium">Cancel</button> 211 | <div slot="center"> 212 | <div slot="title">Hello world</div> 213 | <div slot="subtitle">From DS team</div> 214 | </div> 215 | <button slot="end" ds-button variant="filled" size="medium"> 216 | <ds-demo-icon slot="start" /> 217 | Agree 218 | </button> 219 | </ds-modal-header> 220 | <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> 221 | </ds-modal> 222 | `, 223 | }), 224 | }; 225 | 226 | export const CloseTitleLabelAction: Story = { 227 | argTypes: { 228 | bottomSheet: { 229 | table: { disable: true }, 230 | }, 231 | }, 232 | render: (args) => ({ 233 | template: ` 234 | <ds-modal variant="${args.variant}" inverse="${args.inverse}" > 235 | <ds-modal-header variant="${args.headerVariant}"> 236 | <ng-container slot="start" > 237 | <button ds-button-icon variant="outline" size="medium" kind="secondary"> 238 | <ds-demo-close-icon /> 239 | </button> 240 | <div slot="title"> 241 | Hello world 242 | <ds-badge variant="blue">Label</ds-badge> 243 | </div> 244 | </ng-container> 245 | <ng-container slot="end"> 246 | <ds-demo-icon slot="start" /> 247 | <button slot="end" ds-button variant="outline" size="medium"> 248 | Action 249 | </button> 250 | </ng-container> 251 | </ds-modal-header> 252 | <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> 253 | </ds-modal> 254 | `, 255 | }), 256 | }; 257 | 258 | export const BackBtnTitleLabelAction: Story = { 259 | argTypes: { 260 | bottomSheet: { 261 | table: { disable: true }, 262 | }, 263 | }, 264 | render: (args) => ({ 265 | template: ` 266 | <ds-modal variant="${args.variant}" inverse="${args.inverse}" > 267 | <ds-modal-header variant="${args.headerVariant}"> 268 | <ng-container slot="start"> 269 | <button ds-button-icon variant="outline" size="medium" kind="secondary"> 270 | <ds-demo-chevron rotation="90" /> 271 | </button> 272 | <div style="display: flex; flex-direction: column;"> 273 | <div slot="title"> 274 | Hello world 275 | <ds-badge variant="blue">Label</ds-badge> 276 | </div> 277 | <div slot="subtitle">From DS team</div> 278 | </div> 279 | </ng-container> 280 | <ng-container slot="end"> 281 | <ds-demo-icon slot="start" /> 282 | <button slot="end" ds-button variant="outline" size="medium"> 283 | Action 284 | </button> 285 | </ng-container> 286 | </ds-modal-header> 287 | <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> 288 | </ds-modal> 289 | `, 290 | }), 291 | }; 292 | 293 | export const ModalHeaderTypes: Story = { 294 | argTypes: { 295 | headerVariant: { 296 | table: { disable: true }, 297 | }, 298 | bottomSheet: { 299 | table: { disable: true }, 300 | }, 301 | variant: { 302 | table: { disable: true }, 303 | }, 304 | }, 305 | render: (args) => ({ 306 | template: ` 307 | <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 30px"> 308 | <div style="display: grid; gap: 10px"> 309 | <ds-modal inverse="${args.inverse}" > 310 | <ds-modal-header variant="surface"> 311 | <div slot="start"> 312 | <div slot="title">Surface</div> 313 | </div> 314 | <button slot="end" ds-button-icon size="small" variant="flat" kind="utility"> 315 | <ds-demo-close-icon /> 316 | </button> 317 | </ds-modal-header> 318 | <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> 319 | </ds-modal> 320 | <ds-modal inverse="${args.inverse}" > 321 | <ds-modal-header variant="surface-lowest"> 322 | <div slot="start"> 323 | <div slot="title">Surface lowest</div> 324 | </div> 325 | <button slot="end" ds-button-icon size="small" variant="outline" kind="utility"> 326 | <ds-demo-close-icon /> 327 | </button> 328 | </ds-modal-header> 329 | <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> 330 | </ds-modal> 331 | <ds-modal inverse="${args.inverse}" > 332 | <ds-modal-header variant="surface-low"> 333 | <div slot="start"> 334 | <div slot="title">Surface low</div> 335 | </div> 336 | <button slot="end" ds-button-icon size="small" variant="filled" kind="utility"> 337 | <ds-demo-close-icon /> 338 | </button> 339 | </ds-modal-header> 340 | <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> 341 | </ds-modal> 342 | <ds-modal inverse="${args.inverse}" > 343 | <ds-modal-header variant="surface-high"> 344 | <div slot="start"> 345 | <div slot="title">Surface High</div> 346 | </div> 347 | <button slot="end" ds-button-icon size="small" kind="secondary"> 348 | <ds-demo-close-icon /> 349 | </button> 350 | </ds-modal-header> 351 | <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> 352 | </ds-modal> 353 | </div> 354 | <div style="display: grid; gap: 10px"> 355 | <ds-modal inverse="${args.inverse}" > 356 | <ds-modal-header variant="surface"> 357 | <div slot="start"> 358 | <div slot="title">Surface</div> 359 | <div slot="subtitle">Header subtitle</div> 360 | </div> 361 | <div slot="end"> 362 | <button ds-button-icon size="small" variant="flat" kind="utility"> 363 | <ds-demo-close-icon /> 364 | </button> 365 | </div> 366 | </ds-modal-header> 367 | <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> 368 | </ds-modal> 369 | <ds-modal inverse="${args.inverse}" > 370 | <ds-modal-header variant="surface-lowest"> 371 | <div slot="start"> 372 | <div slot="title">Surface lowest</div> 373 | <div slot="subtitle">Header subtitle</div> 374 | </div> 375 | <button slot="end" ds-button-icon size="small" variant="outline" kind="utility"> 376 | <ds-demo-close-icon /> 377 | </button> 378 | </ds-modal-header> 379 | <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> 380 | </ds-modal> 381 | <ds-modal inverse="${args.inverse}" > 382 | <ds-modal-header variant="surface-low"> 383 | <div slot="start"> 384 | <div slot="title">Surface low</div> 385 | <div slot="subtitle">Header subtitle</div> 386 | </div> 387 | <button slot="end" ds-button-icon size="small" kind="utility"> 388 | <ds-demo-close-icon /> 389 | </button> 390 | </ds-modal-header> 391 | <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> 392 | </ds-modal> 393 | <ds-modal inverse="${args.inverse}" > 394 | <ds-modal-header variant="surface-high"> 395 | <div slot="start"> 396 | <div slot="title">Surface High</div> 397 | <div slot="subtitle">Header subtitle</div> 398 | </div> 399 | <button slot="end" ds-button-icon size="small" kind="secondary"> 400 | <ds-demo-close-icon /> 401 | </button> 402 | </ds-modal-header> 403 | <ds-modal-content> Lorem ipsum dolor sit amet. </ds-modal-content> 404 | </ds-modal> 405 | </div> 406 | </div> 407 | `, 408 | }), 409 | }; 410 | ```