mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-19 20:52:06 +08:00
The `esModuleInterop` option is recommended to be enable by TypeScript and corrects several assumptions TypeScript would otherwise make when importing CommonJS files. This option change helps ensure compatibility as packages move towards ESM. Reference: https://www.typescriptlang.org/tsconfig#esModuleInterop
303 lines
9.5 KiB
TypeScript
303 lines
9.5 KiB
TypeScript
/**
|
|
* @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.io/license
|
|
*/
|
|
|
|
import { Architect } from '@angular-devkit/architect';
|
|
import { getSystemPath, join, normalize, virtualFs } from '@angular-devkit/core';
|
|
import express from 'express'; // eslint-disable-line import/no-extraneous-dependencies
|
|
import * as http from 'http';
|
|
import { AddressInfo } from 'net';
|
|
import { createArchitect, host } from '../test-utils';
|
|
|
|
describe('AppShell Builder', () => {
|
|
const target = { project: 'app', target: 'app-shell' };
|
|
let architect: Architect;
|
|
|
|
beforeEach(async () => {
|
|
await host.initialize().toPromise();
|
|
architect = (await createArchitect(host.root())).architect;
|
|
});
|
|
afterEach(async () => host.restore().toPromise());
|
|
|
|
const appShellRouteFiles = {
|
|
'src/styles.css': `
|
|
p { color: #000 }
|
|
`,
|
|
'src/app/app-shell/app-shell.component.html': `
|
|
<p>
|
|
app-shell works!
|
|
</p>
|
|
`,
|
|
'src/app/app-shell/app-shell.component.ts': `
|
|
import { Component, OnInit } from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'app-app-shell',
|
|
templateUrl: './app-shell.component.html',
|
|
})
|
|
export class AppShellComponent implements OnInit {
|
|
|
|
constructor() { }
|
|
|
|
ngOnInit() {
|
|
}
|
|
|
|
}
|
|
`,
|
|
'src/app/app.module.ts': `
|
|
import { BrowserModule } from '@angular/platform-browser';
|
|
import { NgModule } from '@angular/core';
|
|
|
|
import { AppRoutingModule } from './app-routing.module';
|
|
import { AppComponent } from './app.component';
|
|
import { environment } from '../environments/environment';
|
|
import { RouterModule } from '@angular/router';
|
|
|
|
@NgModule({
|
|
declarations: [
|
|
AppComponent
|
|
],
|
|
imports: [
|
|
BrowserModule.withServerTransition({ appId: 'serverApp' }),
|
|
AppRoutingModule,
|
|
RouterModule
|
|
],
|
|
providers: [],
|
|
bootstrap: [AppComponent]
|
|
})
|
|
export class AppModule { }
|
|
`,
|
|
'src/app/app.server.module.ts': `
|
|
import { NgModule } from '@angular/core';
|
|
import { ServerModule } from '@angular/platform-server';
|
|
|
|
import { AppModule } from './app.module';
|
|
import { AppComponent } from './app.component';
|
|
import { Routes, RouterModule } from '@angular/router';
|
|
import { AppShellComponent } from './app-shell/app-shell.component';
|
|
|
|
const routes: Routes = [ { path: 'shell', component: AppShellComponent }];
|
|
|
|
@NgModule({
|
|
imports: [
|
|
AppModule,
|
|
ServerModule,
|
|
RouterModule.forRoot(routes),
|
|
],
|
|
bootstrap: [AppComponent],
|
|
declarations: [AppShellComponent],
|
|
})
|
|
export class AppServerModule {}
|
|
`,
|
|
'src/main.ts': `
|
|
import { enableProdMode } from '@angular/core';
|
|
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
|
|
|
import { AppModule } from './app/app.module';
|
|
import { environment } from './environments/environment';
|
|
|
|
if (environment.production) {
|
|
enableProdMode();
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
platformBrowserDynamic().bootstrapModule(AppModule)
|
|
.catch(err => console.log(err));
|
|
});
|
|
`,
|
|
'src/app/app-routing.module.ts': `
|
|
import { NgModule } from '@angular/core';
|
|
import { Routes, RouterModule } from '@angular/router';
|
|
|
|
const routes: Routes = [];
|
|
|
|
@NgModule({
|
|
imports: [RouterModule.forRoot(routes)],
|
|
exports: [RouterModule]
|
|
})
|
|
export class AppRoutingModule { }
|
|
`,
|
|
'src/app/app.component.html': `
|
|
<router-outlet></router-outlet>
|
|
`,
|
|
};
|
|
|
|
it('works (basic)', async () => {
|
|
host.replaceInFile(
|
|
'src/app/app.module.ts',
|
|
/ {4}BrowserModule/,
|
|
`
|
|
BrowserModule.withServerTransition({ appId: 'some-app' })
|
|
`,
|
|
);
|
|
|
|
const run = await architect.scheduleTarget(target);
|
|
const output = await run.result;
|
|
await run.stop();
|
|
|
|
expect(output.success).toBe(true);
|
|
|
|
const fileName = 'dist/index.html';
|
|
const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName)));
|
|
expect(content).toMatch(/Welcome to app!/);
|
|
});
|
|
|
|
it('works with route', async () => {
|
|
host.writeMultipleFiles(appShellRouteFiles);
|
|
const overrides = { route: 'shell' };
|
|
|
|
const run = await architect.scheduleTarget(target, overrides);
|
|
const output = await run.result;
|
|
await run.stop();
|
|
|
|
expect(output.success).toBe(true);
|
|
const fileName = 'dist/index.html';
|
|
const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName)));
|
|
expect(content).toContain('app-shell works!');
|
|
});
|
|
|
|
it('works with route and service-worker', async () => {
|
|
host.writeMultipleFiles(appShellRouteFiles);
|
|
host.writeMultipleFiles({
|
|
'src/ngsw-config.json': `
|
|
{
|
|
"index": "/index.html",
|
|
"assetGroups": [{
|
|
"name": "app",
|
|
"installMode": "prefetch",
|
|
"resources": {
|
|
"files": [
|
|
"/favicon.ico",
|
|
"/index.html",
|
|
"/*.css",
|
|
"/*.js"
|
|
]
|
|
}
|
|
}, {
|
|
"name": "assets",
|
|
"installMode": "lazy",
|
|
"updateMode": "prefetch",
|
|
"resources": {
|
|
"files": [
|
|
"/assets/**"
|
|
]
|
|
}
|
|
}]
|
|
}
|
|
`,
|
|
'src/app/app.module.ts': `
|
|
import { BrowserModule } from '@angular/platform-browser';
|
|
import { NgModule } from '@angular/core';
|
|
|
|
import { AppRoutingModule } from './app-routing.module';
|
|
import { AppComponent } from './app.component';
|
|
import { ServiceWorkerModule } from '@angular/service-worker';
|
|
import { environment } from '../environments/environment';
|
|
import { RouterModule } from '@angular/router';
|
|
|
|
@NgModule({
|
|
declarations: [
|
|
AppComponent
|
|
],
|
|
imports: [
|
|
BrowserModule.withServerTransition({ appId: 'serverApp' }),
|
|
AppRoutingModule,
|
|
ServiceWorkerModule.register('/ngsw-worker.js', { enabled: environment.production }),
|
|
RouterModule
|
|
],
|
|
providers: [],
|
|
bootstrap: [AppComponent]
|
|
})
|
|
export class AppModule { }
|
|
`,
|
|
'e2e/app.e2e-spec.ts': `
|
|
import { browser, by, element } from 'protractor';
|
|
|
|
it('should have ngsw in normal state', () => {
|
|
browser.get('/');
|
|
// Wait for service worker to load.
|
|
browser.sleep(2000);
|
|
browser.waitForAngularEnabled(false);
|
|
browser.get('/ngsw/state');
|
|
// Should have updated, and be in normal state.
|
|
expect(element(by.css('pre')).getText()).not.toContain('Last update check: never');
|
|
expect(element(by.css('pre')).getText()).toContain('Driver state: NORMAL');
|
|
});
|
|
`,
|
|
});
|
|
// This should match the browser target prod config.
|
|
host.replaceInFile(
|
|
'angular.json',
|
|
'"buildOptimizer": true',
|
|
'"buildOptimizer": true, "serviceWorker": true',
|
|
);
|
|
|
|
// We're changing the workspace file so we need to recreate the Architect instance.
|
|
architect = (await createArchitect(host.root())).architect;
|
|
|
|
const overrides = { route: 'shell' };
|
|
const run = await architect.scheduleTarget(
|
|
{ ...target, configuration: 'production' },
|
|
overrides,
|
|
);
|
|
const output = await run.result;
|
|
await run.stop();
|
|
|
|
expect(output.success).toBe(true);
|
|
|
|
// Make sure the index is pre-rendering the route.
|
|
const fileName = 'dist/index.html';
|
|
const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName)));
|
|
expect(content).toContain('app-shell works!');
|
|
|
|
// Serve the app using a simple static server.
|
|
const app = express();
|
|
app.use('/', express.static(getSystemPath(join(host.root(), 'dist')) + '/'));
|
|
const server = await new Promise<http.Server>((resolve) => {
|
|
const innerServer = app.listen(0, 'localhost', () => resolve(innerServer));
|
|
});
|
|
try {
|
|
const serverPort = (server.address() as AddressInfo).port;
|
|
// Load app in protractor, then check service worker status.
|
|
const protractorRun = await architect.scheduleTarget(
|
|
{ project: 'app-e2e', target: 'e2e' },
|
|
{ baseUrl: `http://localhost:${serverPort}/`, devServerTarget: '' },
|
|
);
|
|
|
|
const protractorOutput = await protractorRun.result;
|
|
await protractorRun.stop();
|
|
|
|
expect(protractorOutput.success).toBe(true);
|
|
} finally {
|
|
// Close the express server.
|
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
}
|
|
});
|
|
|
|
it('critical CSS is inlined', async () => {
|
|
host.writeMultipleFiles(appShellRouteFiles);
|
|
const overrides = {
|
|
route: 'shell',
|
|
browserTarget: 'app:build:production,inline-critical-css',
|
|
};
|
|
|
|
const run = await architect.scheduleTarget(target, overrides);
|
|
const output = await run.result;
|
|
await run.stop();
|
|
|
|
expect(output.success).toBe(true);
|
|
const fileName = 'dist/index.html';
|
|
const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName)));
|
|
|
|
expect(content).toContain('app-shell works!');
|
|
expect(content).toContain('p{color:#000;}');
|
|
expect(content).toMatch(
|
|
/<link rel="stylesheet" href="styles\.[a-z0-9]+\.css" media="print" onload="this\.media='all'">/,
|
|
);
|
|
});
|
|
});
|