forked from angular/components
-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathstackblitz-writer.ts
More file actions
235 lines (207 loc) · 8.09 KB
/
stackblitz-writer.ts
File metadata and controls
235 lines (207 loc) · 8.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
/**
* @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;
}
}