/
githubmirror
/
components
Обзор
Документация
Войти
/
githubmirror
/
components
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
docs/src/app/shared/stackblitz/stackblitz-writer.ts
235 строк
8 KB
Kristiyan Kostadinov
build: switch docs to service decorator
25 апр 2026, 23:10
25 апр 2026, 23:10
7ac0542
Код
Авторство
О чём код?
/** * @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 */ import {HttpClient} from '@angular/common/http'; import {Service, NgZone, inject} from '@angular/core'; import type {ExampleData} from '@angular/components-examples'; import {Observable} from 'rxjs'; import {shareReplay} from 'rxjs/operators'; import stackblitz from '@stackblitz/sdk'; import {normalizedMaterialVersion} from '../normalized-version'; import {normalizePath} from '../normalize-path'; const COPYRIGHT = `Copyright ${new Date().getFullYear()} 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.io/license`; /** * Path that refers to the docs-content from the "@angular/components-examples" package. The * structure is defined in the Material repository, but we include the docs-content as assets in * in the CLI configuration. */ const DOCS_CONTENT_PATH = '/docs-content/examples-source'; const TEMPLATE_PATH = '/assets/stackblitz/'; /** * List of boilerplate files for an example StackBlitz. * This currently matches files needed for a basic Angular CLI project. * * Note: The template files match up with a basic app generated through `ng new`. * StackBlitz does not support binary files like `favicon.ico`, so we removed that * file from the boilerplate. */ export const TEMPLATE_FILES = [ 'angular.json', 'package.json', 'package-lock.json', 'tsconfig.app.json', 'tsconfig.json', 'tsconfig.spec.json', 'src/index.html', 'src/main.ts', 'src/styles.css', ]; /** * Type describing an in-memory file dictionary, representing a * directory and its contents. */ type FileDictionary = {[path: string]: string}; /** * StackBlitz writer, write example files to StackBlitz. */ @Service() export class StackBlitzWriter { private _http = inject(HttpClient); private _ngZone = inject(NgZone); private _fileCache = new Map<string, Observable<string>>(); /** Opens a StackBlitz for the specified example. */ createStackBlitzForExample( exampleId: string, data: ExampleData, isTest: boolean, ): Promise<() => void> { // Run outside the zone since the creation doesn't interact with Angular // and the file requests can cause excessive change detections. return this._ngZone.runOutsideAngular(async () => { const files = await this._buildInMemoryFileDictionary(data, exampleId, isTest); const exampleMainFile = `src/example/${data.indexFilename}`; return () => { this._openStackBlitz({ files, title: `Angular Components - ${data.description}`, description: `${data.description}\n\nAuto-generated from: https://material.angular.dev`, openFile: exampleMainFile, startScript: isTest ? 'test' : 'start', }); }; }); } /** Opens a new WebContainer-based StackBlitz for the given files. */ private _openStackBlitz({ title, description, openFile, files, startScript, }: { title: string; description: string; openFile: string; files: FileDictionary; startScript: string; }): void { stackblitz.openProject( { title, files, description, template: 'node', tags: ['angular', 'material', 'cdk', 'web', 'example'], }, { openFile, startScript, }, ); } /** * Builds an in-memory file dictionary representing an CLI project serving * the example. The dictionary can then be passed to StackBlitz as project files. */ private async _buildInMemoryFileDictionary( data: ExampleData, exampleId: string, isTest: boolean, ): Promise<FileDictionary> { const examples = await import('@angular/components-examples'); const result: FileDictionary = {}; const tasks: Promise<unknown>[] = []; const liveExample = examples.EXAMPLE_COMPONENTS[exampleId]; const exampleBaseContentPath = `${DOCS_CONTENT_PATH}/${liveExample.importPath}/${exampleId}/`; for (const relativeFilePath of TEMPLATE_FILES) { tasks.push( this._loadFile(TEMPLATE_PATH + relativeFilePath) // Replace example placeholders in the template files. .then(content => this._replaceExamplePlaceholders(data, relativeFilePath, content, isTest), ) .then(content => (result[relativeFilePath] = content)), ); } for (const relativeFilePath of data.exampleFiles) { // Note: Since we join with paths from the example data, we normalize // the final target path. This is necessary because StackBlitz does // not and paths like `./bla.ts` would result in a directory called `.`. const targetPath = normalizePath(`src/example/${relativeFilePath}`); tasks.push( this._loadFile(exampleBaseContentPath + relativeFilePath) // Insert a copyright footer for all example files inserted into the project. .then(content => this._appendCopyright(relativeFilePath, content)) .then(content => (result[targetPath] = content)), ); } // Wait for the file dictionary to be populated. All file requests are // triggered concurrently to speed up the example StackBlitz generation. await Promise.all(tasks); return result; } /** * Loads the specified file and returns a promise resolving to its contents. */ private _loadFile(fileUrl: string): Promise<string> { let stream = this._fileCache.get(fileUrl); if (!stream) { stream = this._http.get(fileUrl, {responseType: 'text'}).pipe(shareReplay(1)); this._fileCache.set(fileUrl, stream); } return stream.toPromise(); } private _replaceExamplePlaceholders( data: ExampleData, fileName: string, fileContent: string, isTest: boolean, ): string { // Replaces the version placeholder in the `index.html` and `package.json` file. // Technically we invalidate the `package-lock.json` file for the StackBlitz boilerplate // by dynamically changing the version in the `package.json`, but the Turbo package manager // seems to be able to partially re-use the lock file to speed up the module tree computation, // so providing a lock file is still reasonable while modifying the `package.json`. if (fileName === 'src/index.html' || fileName === 'package.json') { fileContent = fileContent.replace(/\${version}/g, normalizedMaterialVersion); } if (fileName === 'src/index.html') { // Replace the component selector in `index,html`. // For example, <material-docs-example></material-docs-example> will be replaced as // <button-demo></button-demo> fileContent = fileContent .replace(/material-docs-example/g, data.selectorName) .replace(/\${title}/g, data.description); } else if (fileName === 'src/main.ts') { const mainComponentName = data.componentNames[0]; // Replace the component name in `main.ts`. // Replace `import {MaterialDocsExample} from 'material-docs-example'` // will be replaced as `import {ButtonDemo} from './button-demo'` fileContent = fileContent.replace(/{MaterialDocsExample}/g, `{${mainComponentName}}`); // Replace `bootstrapApplication(MaterialDocsExample,` // will be replaced as `bootstrapApplication(ButtonDemo,` fileContent = fileContent.replace( /bootstrapApplication\(MaterialDocsExample/g, `bootstrapApplication(${mainComponentName}`, ); const dotIndex = data.indexFilename.lastIndexOf('.'); const importFileName = data.indexFilename.slice(0, dotIndex === -1 ? undefined : dotIndex); fileContent = fileContent.replace(/material-docs-example/g, importFileName); } return fileContent; } _appendCopyright(filename: string, content: string) { if (filename.indexOf('.ts') > -1 || filename.indexOf('.scss') > -1) { content = `${content}\n\n/** ${COPYRIGHT} */`; } else if (filename.indexOf('.html') > -1) { content = `${content}\n\n<!-- ${COPYRIGHT} -->`; } return content; } }