Skip to content
This repository was archived by the owner on Dec 18, 2024. It is now read-only.

Commit 8c78f28

Browse files
committed
refactor: update stackblitz examples write to use webcontainers for v13 compatibility
Updates to the Stackblitz example writer: * Simplifies opening mechanism by relying on the small/tree-shakable `@stackblitz/sdk` package instead of manually constructing the form. * Updates the template files to reflect the files generated by a new CLI project generated with CLI v13.rc. * Updates the StackBlitz base template from Angular to `node` which is bringing in the new WebContainer feature from StackBlitz. We need to use this as v13 packages are currently not supported by StackBlitz due to the APF v13 changes. Also WebContainer is recommended to be used in our case due to the complexities involed in running/replicating the CLI, while WebContainers could simply run them. -> with the benfit of users being able to experience Angular directly in the browser, as they would locally. * One downside is that the Stackblitz examples will not work in Safari and Firefox yet. Stackblitz prints a good message explaining why. For testing reproductions, the team needs to just download the zipped project and run it locally. This is another benefit of using the CLI directly in a WebContainer. It's a small trade-off for being able to actually rely on the CLI fully (mitigating potential cache issues like we have faced before). * Removes the second boilerplate/template for running test harness examples. These will run as part of the same CLI boilerplate (allowing us to reduce the burden of maintaining a second boilerplate). * This also required some logic for selectively populating a `${startCommand}` placeholder so that either `yarn test` or `yarn start` is executed in the StackBlitz VM.
1 parent 0c5c09f commit 8c78f28

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+21555
-831
lines changed

.firebaserc

Lines changed: 0 additions & 1 deletion
This file was deleted.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
"build:sm": "ng build --configuration production --source-map",
2020
"prod-build": "ng build --configuration production",
2121
"preinstall": "node ./tools/npm/check-npm.js",
22-
"postinstall": "ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points",
2322
"publish-prod": "bash ./tools/deploy.sh stable prod",
2423
"publish-dev": "bash ./tools/deploy.sh",
2524
"publish-beta": "bash ./tools/deploy.sh stable beta",
@@ -52,6 +51,7 @@
5251
"@angular/platform-browser-dynamic": "13.0.0-next.15",
5352
"@angular/router": "13.0.0-next.15",
5453
"@angular/youtube-player": "^13.0.0-rc.2",
54+
"@stackblitz/sdk": "^1.5.2",
5555
"material-components-web": "13.0.0-canary.0a9069300.0",
5656
"moment": "^2.29.1",
5757
"rxjs": "^6.6.7",
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
<button mat-icon-button type="button" (click)="openStackBlitz()" [disabled]="isDisabled"
2-
[attr.aria-label]="isDisabled ? 'Building StackBlitz example...' : 'Edit this example in StackBlitz'"
3-
[matTooltip]="isDisabled ? 'Building ' + exampleData?.description + ' StackBlitz example...' :
4-
'Edit ' + exampleData?.description + ' example in StackBlitz'">
1+
<button mat-icon-button type="button" (click)="openStackBlitz()"
2+
aria-label="Edit this example in StackBlitz"
3+
[matTooltip]="'Edit ' + exampleData?.description + ' example in StackBlitz'">
54
<mat-icon>open_in_new</mat-icon>
65
</button>

src/app/shared/stack-blitz/stack-blitz-button.ts

Lines changed: 33 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,64 @@
1-
import {Component, HostListener, Input, NgModule} from '@angular/core';
1+
import {Component, Input, NgModule, NgZone} from '@angular/core';
22
import {ExampleData} from '@angular/components-examples';
33
import {MatButtonModule} from '@angular/material/button';
44
import {MatIconModule} from '@angular/material/icon';
55
import {MatTooltipModule} from '@angular/material/tooltip';
66
import {StackBlitzWriter} from './stack-blitz-writer';
7+
import {MatSnackBar, MatSnackBarModule} from '@angular/material/snack-bar';
78

89
@Component({
910
selector: 'stack-blitz-button',
1011
templateUrl: './stack-blitz-button.html',
1112
})
1213
export class StackBlitzButton {
13-
/**
14-
* The button becomes disabled if the user hovers over the button before the StackBlitz form
15-
* is created. After the form is created, the button becomes enabled again.
16-
* The form creation usually happens extremely quickly, but we handle the case of the
17-
* StackBlitz not yet being ready for people with poor network connections or slow devices.
18-
*/
19-
isDisabled = false;
2014
exampleData: ExampleData | undefined;
2115

2216
/**
23-
* Form used to submit the data to Stackblitz.
24-
* Important! it needs to be constructed ahead-of-time, because doing so on-demand
25-
* will cause Firefox to block the submit as a popup, because it didn't happen within
26-
* the same tick as the user interaction.
17+
* Function that can be invoked to open the StackBlitz window synchronously.
18+
*
19+
* **Note**: All files for the StackBlitz need to be loaded and prepared ahead-of-time,
20+
* because doing so on-demand will cause Firefox to block the submit as a popup as the
21+
* form submission (used internally to create the StackBlitz) didn't happen within the
22+
* same tick as the user interaction.
2723
*/
28-
private _stackBlitzForm: HTMLFormElement | undefined;
29-
30-
@HostListener('mouseover')
31-
onMouseOver() {
32-
this.isDisabled = !this._stackBlitzForm;
33-
}
24+
private _openStackBlitzFn: (() => void) | null = null;
3425

3526
@Input()
36-
set example(example: string | undefined) {
37-
if (example) {
38-
const isTest = example.includes('harness');
39-
this.exampleData = new ExampleData(example);
40-
this.stackBlitzWriter.constructStackBlitzForm(example, this.exampleData, isTest)
41-
.then(form => {
42-
this._stackBlitzForm = form;
43-
this.isDisabled = false;
44-
});
27+
set example(exampleId: string | undefined) {
28+
if (exampleId) {
29+
this.exampleData = new ExampleData(exampleId);
30+
this._prepareStackBlitzForExample(exampleId, this.exampleData);
4531
} else {
46-
this.isDisabled = true;
32+
this.exampleData = undefined;
33+
this._openStackBlitzFn = null;
4734
}
4835
}
4936

50-
constructor(private stackBlitzWriter: StackBlitzWriter) {}
37+
constructor(
38+
private stackBlitzWriter: StackBlitzWriter,
39+
private ngZone: NgZone,
40+
private snackBar: MatSnackBar) {}
5141

5242
openStackBlitz(): void {
53-
// When the form is submitted, it must be in the document body. The standard of forms is not
54-
// to submit if it is detached from the document. See the following chromium commit for
55-
// more details:
56-
// https://chromium.googlesource.com/chromium/src/+/962c2a22ddc474255c776aefc7abeba00edc7470%5E!
57-
if (this._stackBlitzForm) {
58-
document.body.appendChild(this._stackBlitzForm);
59-
this._stackBlitzForm.submit();
60-
document.body.removeChild(this._stackBlitzForm);
43+
if (this._openStackBlitzFn) {
44+
this._openStackBlitzFn();
45+
} else {
46+
this.snackBar.open('StackBlitz is not ready yet. Please try again in a few seconds.',
47+
undefined, {duration: 5000});
6148
}
6249
}
50+
51+
private _prepareStackBlitzForExample(exampleId: string, data: ExampleData): void {
52+
this.ngZone.runOutsideAngular(async () => {
53+
const isTest = exampleId.includes('harness');
54+
this._openStackBlitzFn = await this.stackBlitzWriter
55+
.createStackBlitzForExample(exampleId, data, isTest);
56+
});
57+
}
6358
}
6459

6560
@NgModule({
66-
imports: [MatTooltipModule, MatButtonModule, MatIconModule],
61+
imports: [MatTooltipModule, MatButtonModule, MatIconModule, MatSnackBarModule],
6762
exports: [StackBlitzButton],
6863
declarations: [StackBlitzButton],
6964
providers: [StackBlitzWriter],
Lines changed: 66 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,26 @@
11
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
22
import {waitForAsync, fakeAsync, flushMicrotasks, inject, TestBed} from '@angular/core/testing';
33
import {EXAMPLE_COMPONENTS, ExampleData, LiveExample} from '@angular/components-examples';
4-
import {StackBlitzWriter} from './stack-blitz-writer';
4+
import {StackBlitzWriter, TEMPLATE_FILES} from './stack-blitz-writer';
5+
import stackblitz from '@stackblitz/sdk';
56

67
const testExampleId = 'my-test-example-id';
8+
const testExampleBasePath = `/docs-content/examples-source/cdk/my-comp/${testExampleId}`;
9+
10+
const FAKE_DOCS: {[key: string]: string} = {
11+
'/assets/stack-blitz/src/index.html': '<material-docs-example></material-docs-example>',
12+
'/assets/stack-blitz/src/app/app.module.ts':
13+
`import {MaterialDocsExample} from './material-docs-example';`,
14+
[`${testExampleBasePath}/test.ts`]: 'ExampleComponent',
15+
[`${testExampleBasePath}/test.html`]: `<example></example>`,
16+
[`${testExampleBasePath}/src/detail.ts`]: 'DetailComponent',
17+
};
18+
19+
const TEST_URLS = TEMPLATE_FILES.map(filePath => `/assets/stack-blitz/${filePath}`).concat([
20+
`${testExampleBasePath}/test.ts`,
21+
`${testExampleBasePath}/test.html`,
22+
`${testExampleBasePath}/src/detail.ts`,
23+
]);
724

825
describe('StackBlitzWriter', () => {
926
let stackBlitzWriter: StackBlitzWriter;
@@ -27,7 +44,8 @@ describe('StackBlitzWriter', () => {
2744
beforeEach(() => {
2845
stackBlitzWriter = TestBed.inject(StackBlitzWriter);
2946
data = new ExampleData('');
30-
data.componentNames = [];
47+
data.selectorName = 'this-is-the-comp-name';
48+
data.componentNames = ['ExampleComponent', 'AdditionalComp'];
3149
data.exampleFiles = ['test.ts', 'test.html', 'src/detail.ts'];
3250
data.indexFilename = data.exampleFiles[0];
3351

@@ -40,94 +58,70 @@ describe('StackBlitzWriter', () => {
4058
delete EXAMPLE_COMPONENTS[testExampleId];
4159
});
4260

61+
function fakeExternalFileRequests() {
62+
for (const url of TEST_URLS) {
63+
http.expectOne(url).flush(FAKE_DOCS[url] || 'fake');
64+
}
65+
}
66+
4367
it('should append correct copyright', () => {
4468
const year = new Date().getFullYear();
4569
expect(stackBlitzWriter._appendCopyright('test.ts', 'NoContent')).toBe(`NoContent
4670
4771
/** Copyright ${year} Google LLC. All Rights Reserved.
4872
Use of this source code is governed by an MIT-style license that
49-
can be found in the LICENSE file at http://angular.io/license */`);
73+
can be found in the LICENSE file at https://angular.io/license */`);
5074

5175
expect(stackBlitzWriter._appendCopyright('test.html', 'NoContent')).toBe(`NoContent
5276
5377
<!-- Copyright ${year} Google LLC. All Rights Reserved.
5478
Use of this source code is governed by an MIT-style license that
55-
can be found in the LICENSE file at http://angular.io/license -->`);
79+
can be found in the LICENSE file at https://angular.io/license -->`);
5680

5781
});
5882

59-
it('should create form element', () => {
60-
expect(stackBlitzWriter._createFormElement('index.ts').outerHTML).toBe(
61-
`<form action="https://run.stackblitz.com/api/angular/v1?file=index.ts" ` +
62-
`method="post" target="_blank"></form>`);
63-
});
83+
it('should set tags for example stackblitz', fakeAsync(() => {
84+
const openProjectSpy = spyOn(stackblitz, 'openProject');
6485

65-
it('should add files to form input', () => {
66-
const form = stackBlitzWriter._createFormElement('index.ts');
86+
stackBlitzWriter
87+
.createStackBlitzForExample(testExampleId, data, false)
88+
.then(openBlitzFn => openBlitzFn());
6789

68-
stackBlitzWriter._addFileToForm(form, data, 'NoContent', 'test.ts', 'path/to/file', false);
69-
stackBlitzWriter._addFileToForm(form, data, 'Test', 'test.html', 'path/to/file', false);
70-
stackBlitzWriter._addFileToForm(form, data, 'Detail', 'src/detail.ts', 'path/to/file', false);
90+
flushMicrotasks();
91+
fakeExternalFileRequests();
92+
flushMicrotasks();
7193

72-
expect(form.elements.length).toBe(3);
73-
expect(form.elements[0].getAttribute('name')).toBe('files[src/app/test.ts]');
74-
expect(form.elements[1].getAttribute('name')).toBe('files[src/app/test.html]');
75-
expect(form.elements[2].getAttribute('name')).toBe('files[src/app/src/detail.ts]');
76-
});
94+
expect(openProjectSpy).toHaveBeenCalledTimes(1);
95+
expect(openProjectSpy).toHaveBeenCalledWith(jasmine.objectContaining(
96+
{tags: ['angular', 'material', 'cdk', 'web', 'example']}), jasmine.anything());
97+
}));
98+
99+
it('should read and transform template files properly', fakeAsync(() => {
100+
const openProjectSpy = spyOn(stackblitz, 'openProject');
77101

78-
it('should open a new window with stackblitz url', fakeAsync(() => {
79-
let form: HTMLFormElement;
80-
stackBlitzWriter.constructStackBlitzForm(testExampleId, data, false).then(result => {
81-
form = result;
82-
flushMicrotasks();
83-
84-
for (const url of TEST_URLS) {
85-
http.expectOne(url).flush(FAKE_DOCS[url] || '');
86-
}
87-
flushMicrotasks();
88-
89-
expect(form.elements.length).toBe(14);
90-
91-
// Should have correct tags
92-
expect(form.elements[0].getAttribute('name')).toBe('tags[0]');
93-
expect(form.elements[0].getAttribute('value')).toBe('angular');
94-
expect(form.elements[1].getAttribute('name')).toBe('tags[1]');
95-
expect(form.elements[1].getAttribute('value')).toBe('material');
96-
expect(form.elements[2].getAttribute('name')).toBe('tags[2]');
97-
expect(form.elements[2].getAttribute('value')).toBe('example');
98-
99-
// Should bet set as private and have description and dependencies.
100-
expect(form.elements[3].getAttribute('name')).toBe('private');
101-
expect(form.elements[3].getAttribute('value')).toBe('true');
102-
expect(form.elements[4].getAttribute('name')).toBe('description');
103-
expect(form.elements[5].getAttribute('name')).toBe('dependencies');
104-
105-
// Should have files needed for example.
106-
expect(form.elements[6].getAttribute('name')).toBe('files[src/index.html]');
107-
expect(form.elements[7].getAttribute('name')).toBe('files[src/styles.scss]');
108-
expect(form.elements[8].getAttribute('name')).toBe('files[src/polyfills.ts]');
109-
expect(form.elements[9].getAttribute('name')).toBe('files[src/main.ts]');
110-
expect(form.elements[10].getAttribute('name')).toBe('files[src/app/material-module.ts]');
111-
expect(form.elements[11].getAttribute('name')).toBe('files[src/app/test.ts]');
112-
expect(form.elements[12].getAttribute('name')).toBe('files[src/app/test.html]');
113-
expect(form.elements[13].getAttribute('name')).toBe('files[src/app/src/detail.ts]');
102+
stackBlitzWriter
103+
.createStackBlitzForExample(testExampleId, data, false)
104+
.then(openBlitzFn => openBlitzFn());
105+
106+
flushMicrotasks();
107+
fakeExternalFileRequests();
108+
flushMicrotasks();
109+
110+
const expectedFiles = jasmine.objectContaining({
111+
'angular.json': 'fake',
112+
'src/main.ts': 'fake',
113+
'src/test.ts': 'fake',
114+
'src/index.html': `<this-is-the-comp-name></this-is-the-comp-name>`,
115+
'src/app/app.module.ts': `import {ExampleComponent, AdditionalComp} from './test';`,
116+
'src/app/test.ts': `ExampleComponent
117+
118+
/** Copyright 2021 Google LLC. All Rights Reserved.
119+
Use of this source code is governed by an MIT-style license that
120+
can be found in the LICENSE file at https://angular.io/license */`,
114121
});
122+
123+
expect(openProjectSpy).toHaveBeenCalledTimes(1);
124+
expect(openProjectSpy).toHaveBeenCalledWith(
125+
jasmine.objectContaining({files: expectedFiles}), {openFile: 'src/app/test.ts'});
115126
}));
116127
});
117-
118-
const FAKE_DOCS: {[key: string]: string} = {
119-
'/docs-content/examples-source/test.ts': 'ExampleComponent',
120-
'/docs-content/examples-source/test.html': `<example></example>`,
121-
'/docs-content/examples-source/src/detail.ts': 'DetailComponent',
122-
};
123-
124-
const TEST_URLS = [
125-
'/assets/stack-blitz/src/index.html',
126-
'/assets/stack-blitz/src/styles.scss',
127-
'/assets/stack-blitz/src/polyfills.ts',
128-
'/assets/stack-blitz/src/main.ts',
129-
'/assets/stack-blitz/src/app/material-module.ts',
130-
`/docs-content/examples-source/cdk/my-comp/${testExampleId}/test.ts`,
131-
`/docs-content/examples-source/cdk/my-comp/${testExampleId}/test.html`,
132-
`/docs-content/examples-source/cdk/my-comp/${testExampleId}/src/detail.ts`,
133-
];

0 commit comments

Comments
 (0)