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

Commit d08aed6

Browse files
devversionjelbourn
authored andcommitted
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 d08aed6

Some content is hidden

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

41 files changed

+21517
-810
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",

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

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,39 @@ import {StackBlitzWriter} from './stack-blitz-writer';
1111
})
1212
export class StackBlitzButton {
1313
/**
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
14+
* The button becomes disabled if the user hovers over the button before the StackBlitz
15+
* is ready for opening. After the StackBlitz is ready, the button becomes enabled again.
16+
*
17+
* The StackBlitz preparation usually happens extremely quickly, but we handle the case of the
1718
* StackBlitz not yet being ready for people with poor network connections or slow devices.
1819
*/
1920
isDisabled = false;
21+
2022
exampleData: ExampleData | undefined;
2123

2224
/**
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.
25+
* Function that can be invoked to open the StackBlitz window synchronously.
26+
*
27+
* **Note**: All files for the StackBlitz need to be loaded and prepared ahead-of-time,
28+
* because doing so on-demand will cause Firefox to block the submit as a popup as the
29+
* form submission (used internally to create the StackBlitz) didn't happen within the
30+
* same tick as the user interaction.
2731
*/
28-
private _stackBlitzForm: HTMLFormElement | undefined;
32+
private _openStackBlitzFn: (() => void) | null = null;
2933

3034
@HostListener('mouseover')
3135
onMouseOver() {
32-
this.isDisabled = !this._stackBlitzForm;
36+
this.isDisabled = this._openStackBlitzFn === null;
3337
}
3438

3539
@Input()
3640
set example(example: string | undefined) {
3741
if (example) {
3842
const isTest = example.includes('harness');
3943
this.exampleData = new ExampleData(example);
40-
this.stackBlitzWriter.constructStackBlitzForm(example, this.exampleData, isTest)
41-
.then(form => {
42-
this._stackBlitzForm = form;
44+
this.stackBlitzWriter.createStackBlitzForExample(example, this.exampleData, isTest)
45+
.then(openFn => {
46+
this._openStackBlitzFn = openFn;
4347
this.isDisabled = false;
4448
});
4549
} else {
@@ -49,16 +53,8 @@ export class StackBlitzButton {
4953

5054
constructor(private stackBlitzWriter: StackBlitzWriter) {}
5155

52-
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);
61-
}
56+
async openStackBlitz(): Promise<void> {
57+
this._openStackBlitzFn?.();
6258
}
6359
}
6460

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)