Skip to content

feat(@ngtools/webpack): replace bootstrap code for server apps #8951

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 9, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/@ngtools/webpack/src/angular_compiler_plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { resolveEntryModuleFromMain } from './entry_resolver';
import {
replaceBootstrap,
replaceServerBootstrap,
exportNgFactory,
exportLazyModuleMap,
removeDecorators,
Expand Down Expand Up @@ -692,7 +693,9 @@ export class AngularCompilerPlugin implements Tapable {
} else if (this._platform === PLATFORM.Server) {
this._transformers.push(exportLazyModuleMap(isMainPath, getLazyRoutes));
if (!this._JitMode) {
this._transformers.push(exportNgFactory(isMainPath, getEntryModule));
this._transformers.push(
exportNgFactory(isMainPath, getEntryModule),
replaceServerBootstrap(isMainPath, getEntryModule, getTypeChecker));
}
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/@ngtools/webpack/src/transformers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from './make_transform';
export * from './insert_import';
export * from './elide_imports';
export * from './replace_bootstrap';
export * from './replace_server_bootstrap';
export * from './export_ngfactory';
export * from './export_lazy_module_map';
export * from './register_locale_data';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import { oneLine, stripIndent } from 'common-tags';
import { createTypescriptContext, transformTypescript } from './ast_helpers';
import { replaceServerBootstrap } from './replace_server_bootstrap';

describe('@ngtools/webpack transformers', () => {
describe('replace_server_bootstrap', () => {
it('should replace bootstrap', () => {
const input = stripIndent`
import { enableProdMode } from '@angular/core';
import { platformDynamicServer } from '@angular/platform-server';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
enableProdMode();
}

platformDynamicServer().bootstrapModule(AppModule);
`;

// tslint:disable:max-line-length
const output = stripIndent`
import { enableProdMode } from '@angular/core';
import { environment } from './environments/environment';

import * as __NgCli_bootstrap_1 from "./app/app.module.ngfactory";
import * as __NgCli_bootstrap_2 from "@angular/platform-server";

if (environment.production) {
enableProdMode();
}
__NgCli_bootstrap_2.platformServer().bootstrapModuleFactory(__NgCli_bootstrap_1.AppModuleNgFactory);
`;
// tslint:enable:max-line-length

const { program, compilerHost } = createTypescriptContext(input);
const transformer = replaceServerBootstrap(
() => true,
() => ({ path: '/project/src/app/app.module', className: 'AppModule' }),
() => program.getTypeChecker(),
);
const result = transformTypescript(undefined, [transformer], program, compilerHost);

expect(oneLine`${result}`).toEqual(oneLine`${output}`);
});

it('should replace renderModule', () => {
const input = stripIndent`
import { enableProdMode } from '@angular/core';
import { renderModule } from '@angular/platform-server';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
enableProdMode();
}

renderModule(AppModule, {
document: '<app-root></app-root>',
url: '/'
});
`;

// tslint:disable:max-line-length
const output = stripIndent`
import { enableProdMode } from '@angular/core';
import { environment } from './environments/environment';

import * as __NgCli_bootstrap_1 from "./app/app.module.ngfactory";
import * as __NgCli_bootstrap_2 from "@angular/platform-server";

if (environment.production) {
enableProdMode();
}
__NgCli_bootstrap_2.renderModuleFactory(__NgCli_bootstrap_1.AppModuleNgFactory, {
document: '<app-root></app-root>',
url: '/'
});
`;
// tslint:enable:max-line-length

const { program, compilerHost } = createTypescriptContext(input);
const transformer = replaceServerBootstrap(
() => true,
() => ({ path: '/project/src/app/app.module', className: 'AppModule' }),
() => program.getTypeChecker(),
);
const result = transformTypescript(undefined, [transformer], program, compilerHost);

expect(oneLine`${result}`).toEqual(oneLine`${output}`);
});

it('should replace when the module is used in a config object', () => {
const input = stripIndent`
import * as express from 'express';

import { enableProdMode } from '@angular/core';
import { ngExpressEngine } from '@nguniversal/express-engine';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
enableProdMode();
}

const server = express();
server.engine('html', ngExpressEngine({
bootstrap: AppModule
}));
`;

// tslint:disable:max-line-length
const output = stripIndent`
import * as express from 'express';

import { enableProdMode } from '@angular/core';
import { ngExpressEngine } from '@nguniversal/express-engine';

import { environment } from './environments/environment';

import * as __NgCli_bootstrap_1 from "./app/app.module.ngfactory";

if (environment.production) {
enableProdMode();
}

const server = express();
server.engine('html', ngExpressEngine({
bootstrap: __NgCli_bootstrap_1.AppModuleNgFactory
}));
`;
// tslint:enable:max-line-length

const { program, compilerHost } = createTypescriptContext(input);
const transformer = replaceServerBootstrap(
() => true,
() => ({ path: '/project/src/app/app.module', className: 'AppModule' }),
() => program.getTypeChecker(),
);
const result = transformTypescript(undefined, [transformer], program, compilerHost);

expect(oneLine`${result}`).toEqual(oneLine`${output}`);
});

it('should replace bootstrap when barrel files are used', () => {
const input = stripIndent`
import { enableProdMode } from '@angular/core';
import { platformDynamicServer } from '@angular/platform-browser-dynamic';

import { AppModule } from './app';
import { environment } from './environments/environment';

if (environment.production) {
enableProdMode();
}

platformDynamicServer().bootstrapModule(AppModule);
`;

// tslint:disable:max-line-length
const output = stripIndent`
import { enableProdMode } from '@angular/core';
import { environment } from './environments/environment';

import * as __NgCli_bootstrap_1 from "./app/app.module.ngfactory";
import * as __NgCli_bootstrap_2 from "@angular/platform-server";

if (environment.production) {
enableProdMode();
}
__NgCli_bootstrap_2.platformServer().bootstrapModuleFactory(__NgCli_bootstrap_1.AppModuleNgFactory);
`;
// tslint:enable:max-line-length

const { program, compilerHost } = createTypescriptContext(input);
const transformer = replaceServerBootstrap(
() => true,
() => ({ path: '/project/src/app/app.module', className: 'AppModule' }),
() => program.getTypeChecker(),
);
const result = transformTypescript(undefined, [transformer], program, compilerHost);

expect(oneLine`${result}`).toEqual(oneLine`${output}`);
});

it('should not replace bootstrap when there is no entry module', () => {
const input = stripIndent`
import { enableProdMode } from '@angular/core';
import { platformDynamicServer } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
enableProdMode();
}

platformDynamicServer().bootstrapModule(AppModule);
`;

const { program, compilerHost } = createTypescriptContext(input);
const transformer = replaceServerBootstrap(
() => true,
() => undefined,
() => program.getTypeChecker(),
);
const result = transformTypescript(undefined, [transformer], program, compilerHost);

expect(oneLine`${result}`).toEqual(oneLine`${input}`);
});
});
});
135 changes: 135 additions & 0 deletions packages/@ngtools/webpack/src/transformers/replace_server_bootstrap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// @ignoreDep typescript
import * as ts from 'typescript';
import { relative, dirname } from 'path';

import { collectDeepNodes } from './ast_helpers';
import { insertStarImport } from './insert_import';
import { StandardTransform, ReplaceNodeOperation, TransformOperation } from './interfaces';
import { makeTransform } from './make_transform';

export function replaceServerBootstrap(
shouldTransform: (fileName: string) => boolean,
getEntryModule: () => { path: string, className: string },
getTypeChecker: () => ts.TypeChecker,
): ts.TransformerFactory<ts.SourceFile> {

const standardTransform: StandardTransform = function (sourceFile: ts.SourceFile) {
const ops: TransformOperation[] = [];

const entryModule = getEntryModule();

if (!shouldTransform(sourceFile.fileName) || !entryModule) {
return ops;
}

// Find all identifiers.
const entryModuleIdentifiers = collectDeepNodes<ts.Identifier>(sourceFile,
ts.SyntaxKind.Identifier)
.filter(identifier => identifier.text === entryModule.className);

if (entryModuleIdentifiers.length === 0) {
return [];
}

const relativeEntryModulePath = relative(dirname(sourceFile.fileName), entryModule.path);
const normalizedEntryModulePath = `./${relativeEntryModulePath}`.replace(/\\/g, '/');
const factoryClassName = entryModule.className + 'NgFactory';
const factoryModulePath = normalizedEntryModulePath + '.ngfactory';

// Find the bootstrap calls.
entryModuleIdentifiers.forEach(entryModuleIdentifier => {
if (!entryModuleIdentifier.parent) {
return;
}

if (entryModuleIdentifier.parent.kind !== ts.SyntaxKind.CallExpression &&
entryModuleIdentifier.parent.kind !== ts.SyntaxKind.PropertyAssignment) {
return;
}

if (entryModuleIdentifier.parent.kind === ts.SyntaxKind.CallExpression) {
// Figure out if it's a `platformDynamicServer().bootstrapModule(AppModule)` call.

const callExpr = entryModuleIdentifier.parent as ts.CallExpression;

if (callExpr.expression.kind === ts.SyntaxKind.PropertyAccessExpression) {

const propAccessExpr = callExpr.expression as ts.PropertyAccessExpression;

if (!(propAccessExpr.name.text === 'bootstrapModule'
&& propAccessExpr.expression.kind === ts.SyntaxKind.CallExpression)) {
return;
}

const bootstrapModuleIdentifier = propAccessExpr.name;
const innerCallExpr = propAccessExpr.expression as ts.CallExpression;

if (!(
innerCallExpr.expression.kind === ts.SyntaxKind.Identifier
&& (innerCallExpr.expression as ts.Identifier).text === 'platformDynamicServer'
)) {
return;
}

const platformDynamicServerIdentifier = innerCallExpr.expression as ts.Identifier;

const idPlatformServer = ts.createUniqueName('__NgCli_bootstrap_');
const idNgFactory = ts.createUniqueName('__NgCli_bootstrap_');

// Add the transform operations.
ops.push(
// Replace the entry module import.
...insertStarImport(sourceFile, idNgFactory, factoryModulePath),
new ReplaceNodeOperation(sourceFile, entryModuleIdentifier,
ts.createPropertyAccess(idNgFactory, ts.createIdentifier(factoryClassName))),
// Replace the platformBrowserDynamic import.
...insertStarImport(sourceFile, idPlatformServer, '@angular/platform-server'),
new ReplaceNodeOperation(sourceFile, platformDynamicServerIdentifier,
ts.createPropertyAccess(idPlatformServer, 'platformServer')),
new ReplaceNodeOperation(sourceFile, bootstrapModuleIdentifier,
ts.createIdentifier('bootstrapModuleFactory')),
);
} else if (callExpr.expression.kind === ts.SyntaxKind.Identifier) {
// Figure out if it is renderModule

const identifierExpr = callExpr.expression as ts.Identifier;

if (identifierExpr.text !== 'renderModule') {
return;
}

const renderModuleIdentifier = identifierExpr as ts.Identifier;

const idPlatformServer = ts.createUniqueName('__NgCli_bootstrap_');
const idNgFactory = ts.createUniqueName('__NgCli_bootstrap_');

ops.push(
// Replace the entry module import.
...insertStarImport(sourceFile, idNgFactory, factoryModulePath),
new ReplaceNodeOperation(sourceFile, entryModuleIdentifier,
ts.createPropertyAccess(idNgFactory, ts.createIdentifier(factoryClassName))),
// Replace the renderModule import.
...insertStarImport(sourceFile, idPlatformServer, '@angular/platform-server'),
new ReplaceNodeOperation(sourceFile, renderModuleIdentifier,
ts.createPropertyAccess(idPlatformServer, 'renderModuleFactory')),
);
}
} else if (entryModuleIdentifier.parent.kind === ts.SyntaxKind.PropertyAssignment) {
// This is for things that accept a module as a property in a config object
// .ie the express engine

const idNgFactory = ts.createUniqueName('__NgCli_bootstrap_');

ops.push(
...insertStarImport(sourceFile, idNgFactory, factoryModulePath),
new ReplaceNodeOperation(sourceFile, entryModuleIdentifier,
ts.createPropertyAccess(idNgFactory, ts.createIdentifier(factoryClassName)))
);
}
});

return ops;
};

return makeTransform(standardTransform, getTypeChecker);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<div>
<h1>hello world</h1>
<a [routerLink]="['lazy']">lazy</a>
<router-outlet></router-outlet>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:host {
background-color: blue;
}
Loading