From 9b539a18ba8d16b82fa358431f1eecff174cc306 Mon Sep 17 00:00:00 2001 From: Matthieu Riegler Date: Fri, 15 Aug 2025 15:07:02 +0200 Subject: [PATCH] docs(docs-infra): lift circular imports (#63186) This commit also adds adev on the cyclic import check. PR Close #63186 --- .../components/viewers/BUILD.bazel | 1 + .../docs-viewer/docs-viewer.component.ts | 8 +++-- .../example-viewer.component.html | 10 +++---- .../example-viewer.component.spec.ts | 30 +++++++++---------- .../example-viewer.component.ts | 6 ++-- adev/shared-docs/interfaces/BUILD.bazel | 1 + adev/shared-docs/interfaces/code-example.ts | 3 +- .../code-editor/extensions/autocomplete.ts | 2 +- .../code-editor/extensions/diagnostics.ts | 2 +- .../editor/code-editor/extensions/tooltip.ts | 2 +- adev/src/app/editor/node-runtime-errors.ts | 10 +++++++ .../node-runtime-sandbox.service.spec.ts | 8 ++--- .../editor/node-runtime-sandbox.service.ts | 4 +-- .../editor/node-runtime-state.service.spec.ts | 2 +- .../app/editor/node-runtime-state.service.ts | 4 +-- .../features/home/animation/plugins/types.ts | 2 +- .../errors/cyclic-imports/child.component.ts | 10 ------- .../errors/cyclic-imports/parent.component.ts | 9 ------ adev/src/content/reference/errors/NG3003.md | 25 ++++++++++++++-- packages/circular-deps-test.conf.js | 2 +- 20 files changed, 75 insertions(+), 66 deletions(-) create mode 100644 adev/src/app/editor/node-runtime-errors.ts delete mode 100644 adev/src/content/examples/errors/cyclic-imports/child.component.ts delete mode 100644 adev/src/content/examples/errors/cyclic-imports/parent.component.ts diff --git a/adev/shared-docs/components/viewers/BUILD.bazel b/adev/shared-docs/components/viewers/BUILD.bazel index c5ae291c02d..5e6352693b3 100644 --- a/adev/shared-docs/components/viewers/BUILD.bazel +++ b/adev/shared-docs/components/viewers/BUILD.bazel @@ -21,6 +21,7 @@ ng_project( "//adev:node_modules/@angular/common", "//adev:node_modules/@angular/core", "//adev:node_modules/@angular/material", + "//adev:node_modules/@angular/platform-browser", "//adev:node_modules/@angular/router", "//adev:node_modules/rxjs", "//adev/shared-docs/components/breadcrumb", diff --git a/adev/shared-docs/components/viewers/docs-viewer/docs-viewer.component.ts b/adev/shared-docs/components/viewers/docs-viewer/docs-viewer.component.ts index c520660bf03..28f6c00e5a6 100644 --- a/adev/shared-docs/components/viewers/docs-viewer/docs-viewer.component.ts +++ b/adev/shared-docs/components/viewers/docs-viewer/docs-viewer.component.ts @@ -39,6 +39,7 @@ import {fromEvent} from 'rxjs'; import {Breadcrumb} from '../../breadcrumb/breadcrumb.component'; import {CopySourceCodeButton} from '../../copy-source-code-button/copy-source-code-button.component'; import {ExampleViewer} from '../example-viewer/example-viewer.component'; +import {DomSanitizer} from '@angular/platform-browser'; const TOC_HOST_ELEMENT_NAME = 'docs-table-of-contents'; export const ASSETS_EXAMPLES_PATH = 'assets/content/examples'; @@ -74,6 +75,7 @@ export class DocViewer { private readonly environmentInjector = inject(EnvironmentInjector); private readonly injector = inject(Injector); private readonly appRef = inject(ApplicationRef); + private readonly sanitizer = inject(DomSanitizer); protected animateContent = false; private readonly pendingTasks = inject(PendingTasks); @@ -215,7 +217,7 @@ export class DocViewer { return tabs.map((tab) => ({ name: tab.getAttribute('path') ?? tab.getAttribute('header') ?? '', - content: tab.innerHTML, + sanitizedContent: this.sanitizer.bypassSecurityTrustHtml(tab.innerHTML), visibleLinesRange: tab.getAttribute('visibleLines') ?? undefined, })); } @@ -235,7 +237,9 @@ export class DocViewer { return { title, name: path, - content: content?.outerHTML, + sanitizedContent: content?.outerHTML + ? this.sanitizer.bypassSecurityTrustHtml(content.outerHTML) + : '', visibleLinesRange: visibleLines, }; } diff --git a/adev/shared-docs/components/viewers/example-viewer/example-viewer.component.html b/adev/shared-docs/components/viewers/example-viewer/example-viewer.component.html index 28d55bc9629..73997c25abb 100644 --- a/adev/shared-docs/components/viewers/example-viewer/example-viewer.component.html +++ b/adev/shared-docs/components/viewers/example-viewer/example-viewer.component.html @@ -5,11 +5,7 @@ } @if (view() === CodeExampleViewMode.MULTI_FILE) { - + @for (tab of tabs(); track tab) { } @@ -87,7 +83,9 @@ [class.docs-example-viewer-multi-file]="view() === CodeExampleViewMode.MULTI_FILE" > - + @if (snippetCode()?.sanitizedContent; as content) { +
+ } @if (exampleComponent) { diff --git a/adev/shared-docs/components/viewers/example-viewer/example-viewer.component.spec.ts b/adev/shared-docs/components/viewers/example-viewer/example-viewer.component.spec.ts index 68461b068d9..2cc255d0bc4 100644 --- a/adev/shared-docs/components/viewers/example-viewer/example-viewer.component.spec.ts +++ b/adev/shared-docs/components/viewers/example-viewer/example-viewer.component.spec.ts @@ -53,9 +53,9 @@ describe('ExampleViewer', () => { 'metadata', getMetadata({ files: [ - {name: 'file.ts', content: ''}, - {name: 'file.html', content: ''}, - {name: 'file.css', content: ''}, + {name: 'file.ts', sanitizedContent: ''}, + {name: 'file.html', sanitizedContent: ''}, + {name: 'file.css', sanitizedContent: ''}, ], }), ); @@ -73,9 +73,9 @@ describe('ExampleViewer', () => { 'metadata', getMetadata({ files: [ - {name: 'file.ts', content: 'typescript file'}, - {name: 'file.html', content: 'html file'}, - {name: 'file.css', content: 'css file'}, + {name: 'file.ts', sanitizedContent: 'typescript file'}, + {name: 'file.html', sanitizedContent: 'html file'}, + {name: 'file.css', sanitizedContent: 'css file'}, ], }), ); @@ -93,9 +93,9 @@ describe('ExampleViewer', () => { 'metadata', getMetadata({ files: [ - {name: 'example.ts', content: 'typescript file'}, - {name: 'example.html', content: 'html file'}, - {name: 'another-example.ts', content: 'css file'}, + {name: 'example.ts', sanitizedContent: 'typescript file'}, + {name: 'example.html', sanitizedContent: 'html file'}, + {name: 'another-example.ts', sanitizedContent: 'css file'}, ], }), ); @@ -124,7 +124,7 @@ describe('ExampleViewer', () => { files: [ { name: 'example.ts', - content: `
${expectedCodeSnippetContent}
`, + sanitizedContent: `
${expectedCodeSnippetContent}
`, visibleLinesRange: '[1]', }, ], @@ -148,7 +148,7 @@ describe('ExampleViewer', () => { files: [ { name: 'example.ts', - content: `
${expectedCodeSnippetContent}
`, + sanitizedContent: `
${expectedCodeSnippetContent}
`, visibleLinesRange: '[1]', }, ], @@ -243,9 +243,9 @@ describe('ExampleViewer', () => { files: [ { name: 'example.ts', - content: `
${expectedCodeSnippetContent}
`, + sanitizedContent: `
${expectedCodeSnippetContent}
`, }, - {name: 'example.css', content: ''}, + {name: 'example.css', sanitizedContent: ''}, ], }), ); @@ -279,8 +279,8 @@ const getMetadata = (value: Partial = {}): ExampleMetadata => { return { id: 1, files: [ - {name: 'example.ts', content: ''}, - {name: 'example.css', content: ''}, + {name: 'example.ts', sanitizedContent: ''}, + {name: 'example.css', sanitizedContent: ''}, ], preview: false, ...value, diff --git a/adev/shared-docs/components/viewers/example-viewer/example-viewer.component.ts b/adev/shared-docs/components/viewers/example-viewer/example-viewer.component.ts index a0dcc220554..e1033e7ff2d 100644 --- a/adev/shared-docs/components/viewers/example-viewer/example-viewer.component.ts +++ b/adev/shared-docs/components/viewers/example-viewer/example-viewer.component.ts @@ -14,7 +14,6 @@ import { computed, DestroyRef, ElementRef, - forwardRef, inject, Injector, input, @@ -29,7 +28,6 @@ import {CopySourceCodeButton} from '../../copy-source-code-button/copy-source-co import {ExampleMetadata, Snippet} from '../../../interfaces/index'; import {EXAMPLE_VIEWER_CONTENT_LOADER} from '../../../providers/index'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; -import {DocViewer} from '../docs-viewer/docs-viewer.component'; export enum CodeExampleViewMode { SNIPPET = 'snippet', @@ -43,7 +41,7 @@ export const HIDDEN_CLASS_NAME = 'hidden'; @Component({ selector: 'docs-example-viewer', - imports: [CommonModule, forwardRef(() => DocViewer), CopySourceCodeButton, MatTabsModule], + imports: [CommonModule, CopySourceCodeButton, MatTabsModule], templateUrl: './example-viewer.component.html', styleUrls: ['./example-viewer.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, @@ -80,7 +78,7 @@ export class ExampleViewer { this.exampleMetadata()?.files.map((file) => ({ name: file.title ?? (this.shouldDisplayFullName() ? file.name : this.getFileExtension(file.name)), - code: file.content, + code: file.sanitizedContent, })), ); view = computed(() => diff --git a/adev/shared-docs/interfaces/BUILD.bazel b/adev/shared-docs/interfaces/BUILD.bazel index 13919806305..52b3272630f 100644 --- a/adev/shared-docs/interfaces/BUILD.bazel +++ b/adev/shared-docs/interfaces/BUILD.bazel @@ -26,6 +26,7 @@ ts_project( ), deps = [ "//adev:node_modules/@angular/core", + "//adev:node_modules/@angular/platform-browser", "//adev:node_modules/@angular/router", "//adev:node_modules/@types/node", "//adev:node_modules/@webcontainer/api", diff --git a/adev/shared-docs/interfaces/code-example.ts b/adev/shared-docs/interfaces/code-example.ts index 5efe4435190..4c521a8267a 100644 --- a/adev/shared-docs/interfaces/code-example.ts +++ b/adev/shared-docs/interfaces/code-example.ts @@ -7,6 +7,7 @@ */ import {Type} from '@angular/core'; +import {SafeHtml} from '@angular/platform-browser'; /** * Map of the examples, values are functions which returns the promise of the component type, which will be displayed as preview in the ExampleViewer component @@ -21,7 +22,7 @@ export interface Snippet { /** Name of the file. */ name: string; /** Content of code snippet */ - content: string; + sanitizedContent: SafeHtml; /** Text in following format `start-end`. Start and end are numbers, based on them provided range of lines will be displayed in collapsed mode */ visibleLinesRange?: string; } diff --git a/adev/src/app/editor/code-editor/extensions/autocomplete.ts b/adev/src/app/editor/code-editor/extensions/autocomplete.ts index 3edf0778375..080d616f911 100644 --- a/adev/src/app/editor/code-editor/extensions/autocomplete.ts +++ b/adev/src/app/editor/code-editor/extensions/autocomplete.ts @@ -19,7 +19,7 @@ import { import {EditorView} from '@codemirror/view'; import {Subject, filter, take} from 'rxjs'; -import {EditorFile} from '../code-mirror-editor.service'; +import type {EditorFile} from '../code-mirror-editor.service'; import {TsVfsWorkerActions} from '../workers/enums/actions'; import {AutocompleteRequest} from '../workers/interfaces/autocomplete-request'; import {AutocompleteItem, AutocompleteResponse} from '../workers/interfaces/autocomplete-response'; diff --git a/adev/src/app/editor/code-editor/extensions/diagnostics.ts b/adev/src/app/editor/code-editor/extensions/diagnostics.ts index 4fd04d59c1f..505559783d2 100644 --- a/adev/src/app/editor/code-editor/extensions/diagnostics.ts +++ b/adev/src/app/editor/code-editor/extensions/diagnostics.ts @@ -9,7 +9,7 @@ import {Diagnostic, linter} from '@codemirror/lint'; import {TsVfsWorkerActions} from '../workers/enums/actions'; import {Signal} from '@angular/core'; -import {EditorFile} from '../code-mirror-editor.service'; +import type {EditorFile} from '../code-mirror-editor.service'; import {ActionMessage} from '../workers/interfaces/message'; import {DiagnosticsRequest} from '../workers/interfaces/diagnostics-request'; import {Subject, filter, take} from 'rxjs'; diff --git a/adev/src/app/editor/code-editor/extensions/tooltip.ts b/adev/src/app/editor/code-editor/extensions/tooltip.ts index 434d3d2ab17..2d2fc49ff2d 100644 --- a/adev/src/app/editor/code-editor/extensions/tooltip.ts +++ b/adev/src/app/editor/code-editor/extensions/tooltip.ts @@ -13,7 +13,7 @@ import {Subject, filter, take} from 'rxjs'; import ts from 'typescript'; -import {EditorFile} from '../code-mirror-editor.service'; +import type {EditorFile} from '../code-mirror-editor.service'; import {TsVfsWorkerActions} from '../workers/enums/actions'; import {DisplayTooltipRequest} from '../workers/interfaces/display-tooltip-request'; import {DisplayTooltipResponse} from '../workers/interfaces/display-tooltip-response'; diff --git a/adev/src/app/editor/node-runtime-errors.ts b/adev/src/app/editor/node-runtime-errors.ts new file mode 100644 index 00000000000..28d5b270648 --- /dev/null +++ b/adev/src/app/editor/node-runtime-errors.ts @@ -0,0 +1,10 @@ +/*! + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +export const DEV_SERVER_READY_MSG = 'Watch mode enabled. Watching for file changes...'; +export const OUT_OF_MEMORY_MSG = 'Out of memory'; diff --git a/adev/src/app/editor/node-runtime-sandbox.service.spec.ts b/adev/src/app/editor/node-runtime-sandbox.service.spec.ts index be0b3442284..4e92347e644 100644 --- a/adev/src/app/editor/node-runtime-sandbox.service.spec.ts +++ b/adev/src/app/editor/node-runtime-sandbox.service.spec.ts @@ -15,15 +15,11 @@ import {FakeWebContainer, FakeWebContainerProcess, TutorialType} from '@angular/ import {AlertManager} from './alert-manager.service'; import {EmbeddedTutorialManager} from './embedded-tutorial-manager.service'; import {LoadingStep} from './enums/loading-steps'; -import { - DEV_SERVER_READY_MSG, - NodeRuntimeSandbox, - OUT_OF_MEMORY_MSG, - PACKAGE_MANAGER, -} from './node-runtime-sandbox.service'; +import {NodeRuntimeSandbox, PACKAGE_MANAGER} from './node-runtime-sandbox.service'; import {NodeRuntimeState} from './node-runtime-state.service'; import {TerminalHandler} from './terminal/terminal-handler.service'; import {TypingsLoader} from './typings-loader.service'; +import {DEV_SERVER_READY_MSG, OUT_OF_MEMORY_MSG} from './node-runtime-errors'; describe('NodeRuntimeSandbox', () => { let testBed: TestBed; diff --git a/adev/src/app/editor/node-runtime-sandbox.service.ts b/adev/src/app/editor/node-runtime-sandbox.service.ts index 2a98d781371..187d15fb6d8 100644 --- a/adev/src/app/editor/node-runtime-sandbox.service.ts +++ b/adev/src/app/editor/node-runtime-sandbox.service.ts @@ -19,9 +19,7 @@ import {LoadingStep} from './enums/loading-steps'; import {ErrorType, NodeRuntimeState} from './node-runtime-state.service'; import {TerminalHandler} from './terminal/terminal-handler.service'; import {TypingsLoader} from './typings-loader.service'; - -export const DEV_SERVER_READY_MSG = 'Watch mode enabled. Watching for file changes...'; -export const OUT_OF_MEMORY_MSG = 'Out of memory'; +import {DEV_SERVER_READY_MSG, OUT_OF_MEMORY_MSG} from './node-runtime-errors'; const enum PROCESS_EXIT_CODE { SUCCESS = 0, // process exited successfully diff --git a/adev/src/app/editor/node-runtime-state.service.spec.ts b/adev/src/app/editor/node-runtime-state.service.spec.ts index aeb247cb130..3f7305ead8a 100644 --- a/adev/src/app/editor/node-runtime-state.service.spec.ts +++ b/adev/src/app/editor/node-runtime-state.service.spec.ts @@ -9,7 +9,7 @@ import {TestBed} from '@angular/core/testing'; import {ErrorType, NodeRuntimeState} from './node-runtime-state.service'; -import {OUT_OF_MEMORY_MSG} from './node-runtime-sandbox.service'; +import {OUT_OF_MEMORY_MSG} from './node-runtime-errors'; describe('NodeRuntimeState', () => { let service: NodeRuntimeState; diff --git a/adev/src/app/editor/node-runtime-state.service.ts b/adev/src/app/editor/node-runtime-state.service.ts index 54b99977338..2378a3779bb 100644 --- a/adev/src/app/editor/node-runtime-state.service.ts +++ b/adev/src/app/editor/node-runtime-state.service.ts @@ -7,10 +7,10 @@ */ import {Injectable, signal} from '@angular/core'; -import {isFirefox, isIos} from '@angular/docs'; +import {isIos} from '@angular/docs'; import {LoadingStep} from './enums/loading-steps'; -import {OUT_OF_MEMORY_MSG} from './node-runtime-sandbox.service'; +import {OUT_OF_MEMORY_MSG} from './node-runtime-errors'; export const MAX_RECOMMENDED_WEBCONTAINERS_INSTANCES = 3; export const WEBCONTAINERS_COUNTER_KEY = 'numberOfWebcontainers'; diff --git a/adev/src/app/features/home/animation/plugins/types.ts b/adev/src/app/features/home/animation/plugins/types.ts index dcf7e0353ba..cb9fd291a10 100644 --- a/adev/src/app/features/home/animation/plugins/types.ts +++ b/adev/src/app/features/home/animation/plugins/types.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {Animation} from '../animation'; +import type {Animation} from '../animation'; /** * Animation plugin interface. diff --git a/adev/src/content/examples/errors/cyclic-imports/child.component.ts b/adev/src/content/examples/errors/cyclic-imports/child.component.ts deleted file mode 100644 index 936fc1422b7..00000000000 --- a/adev/src/content/examples/errors/cyclic-imports/child.component.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {Component, inject} from '@angular/core'; -import {ParentComponent} from './parent.component'; - -@Component({ - selector: 'app-child', - template: 'The child!', -}) -export class ChildComponent { - private parent = inject(ParentComponent); -} diff --git a/adev/src/content/examples/errors/cyclic-imports/parent.component.ts b/adev/src/content/examples/errors/cyclic-imports/parent.component.ts deleted file mode 100644 index 790909e46c5..00000000000 --- a/adev/src/content/examples/errors/cyclic-imports/parent.component.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {Component} from '@angular/core'; -import {ChildComponent} from './child.component'; - -@Component({ - selector: 'app-parent', - imports: [ChildComponent], - template: '', -}) -export class ParentComponent {} diff --git a/adev/src/content/reference/errors/NG3003.md b/adev/src/content/reference/errors/NG3003.md index cbbde347500..5914c57c8b1 100644 --- a/adev/src/content/reference/errors/NG3003.md +++ b/adev/src/content/reference/errors/NG3003.md @@ -3,9 +3,30 @@ A component, directive, or pipe that is referenced by this component would require the compiler to add an import that would lead to a cycle of imports. For example, consider a scenario where a `ParentComponent` references a `ChildComponent` in its template: - + +import {Component} from '@angular/core'; +import {ChildComponent} from './child.component'; - +@Component({ + selector: 'app-parent', + imports: [ChildComponent], + template: '', +}) +export class ParentComponent {} + + + +import {Component, inject} from '@angular/core'; +import {ParentComponent} from './parent.component'; + +@Component({ + selector: 'app-child', + template: 'The child!', +}) +export class ChildComponent { + private parent = inject(ParentComponent); +} + There is already an import from `child.component.ts` to `parent.component.ts` since the `ChildComponent` references the `ParentComponent` in its constructor. diff --git a/packages/circular-deps-test.conf.js b/packages/circular-deps-test.conf.js index c4dd5d798f2..f26e4fc7e1d 100644 --- a/packages/circular-deps-test.conf.js +++ b/packages/circular-deps-test.conf.js @@ -10,7 +10,7 @@ const path = require('path'); module.exports = { baseDir: '../', - glob: `./**/*.ts`, + glob: `../{packages,adev}/**/*.ts`, resolveModule: resolveModule, ignoreTypeOnlyChecks: true, };