Skip to content

Ivy app shell #15517

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 7 commits into from
Sep 10, 2019
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
36 changes: 21 additions & 15 deletions packages/angular_devkit/build_angular/src/app-shell/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { JsonObject, experimental, join, normalize, resolve, schema } from '@ang
import { NodeJsSyncHost } from '@angular-devkit/core/node';
import * as fs from 'fs';
import * as path from 'path';
import { readTsconfig } from '../angular-cli-files/utilities/read-tsconfig';
import { augmentAppWithServiceWorker } from '../angular-cli-files/utilities/service-worker';
import { BrowserBuilderOutput } from '../browser';
import { Schema as BrowserBuilderSchema } from '../browser/schema';
Expand All @@ -42,30 +41,37 @@ async function _renderUniversal(
browserBuilderName,
);

// Determine if browser app was compiled using Ivy.
const { options: compilerOptions } = readTsconfig(browserOptions.tsConfig, context.workspaceRoot);
const ivy = compilerOptions.enableIvy;

// Initialize zone.js
const zonePackage = require.resolve('zone.js', { paths: [root] });
await import(zonePackage);

const {
AppServerModule,
AppServerModuleNgFactory,
renderModule,
renderModuleFactory,
} = await import(serverBundlePath);

let renderModuleFn: (module: unknown, options: {}) => Promise<string>;
let AppServerModuleDef: unknown;

if (renderModuleFactory && AppServerModuleNgFactory) {
renderModuleFn = renderModuleFactory;
AppServerModuleDef = AppServerModuleNgFactory;
} else if (renderModule && AppServerModule) {
renderModuleFn = renderModule;
AppServerModuleDef = AppServerModule;
} else {
throw new Error(`renderModule method and/or AppServerModule were not exported from: ${serverBundlePath}.`);
}

// Load platform server module renderer
const platformServerPackage = require.resolve('@angular/platform-server', { paths: [root] });
const renderOpts = {
document: indexHtml,
url: options.route,
};

// Render app to HTML using Ivy or VE
const html = await import(platformServerPackage)
// tslint:disable-next-line:no-implicit-dependencies
.then((m: typeof import('@angular/platform-server')) =>
ivy
? m.renderModule(require(serverBundlePath).AppServerModule, renderOpts)
: m.renderModuleFactory(require(serverBundlePath).AppServerModuleNgFactory, renderOpts),
);

const html = await renderModuleFn(AppServerModuleDef, renderOpts);
// Overwrite the client index file.
const outputIndexPath = options.outputIndexPath
? path.join(root, options.outputIndexPath)
Expand Down
4 changes: 2 additions & 2 deletions packages/angular_devkit/build_angular/src/server/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,8 @@
},
"bundleDependencies": {
"type": "string",
"description": "Available on server platform only. Which external dependencies to bundle into the module. By default, all of node_modules will be kept as requires.",
"default": "none",
"description": "Available on server platform only. Which external dependencies to bundle into the module. By default, all of node_modules will be bundled.",
"default": "all",
"enum": [
"none",
"all"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,9 @@
import { Architect } from '@angular-devkit/architect/src/architect';
import { getSystemPath, join, normalize, virtualFs } from '@angular-devkit/core';
import * as express from 'express'; // tslint:disable-line:no-implicit-dependencies
import { createArchitect, host, veEnabled } from '../utils';
import { createArchitect, host } from '../utils';


// DISABLED_FOR_IVY These should pass but are currently not supported
// See https://github.com/angular/angular-cli/issues/15383 for details.
(veEnabled ? describe : xdescribe)('AppShell Builder', () => {
describe('AppShell Builder', () => {
const target = { project: 'app', target: 'app-shell' };
let architect: Architect;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Architect } from '@angular-devkit/architect';
import { getSystemPath, join, normalize, virtualFs } from '@angular-devkit/core';
import { take, tap } from 'rxjs/operators';
import { BrowserBuilderOutput } from '../../src/browser';
import { BundleDependencies } from '../../src/server/schema';
import { createArchitect, host, veEnabled } from '../utils';


Expand Down Expand Up @@ -85,6 +86,7 @@ describe('Server Builder', () => {
});

const run = await architect.scheduleTarget(target, {
bundleDependencies: BundleDependencies.None,
sourceMap: {
styles: false,
scripts: true,
Expand All @@ -103,9 +105,10 @@ describe('Server Builder', () => {

await run.stop();
});
//

it('supports component styles sourcemaps', async () => {
const overrides = {
bundleDependencies: BundleDependencies.None,
sourceMap: {
styles: true,
scripts: true,
Expand Down
2 changes: 2 additions & 0 deletions packages/schematics/angular/migrations/update-9/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { updateLibraries } from './ivy-libraries';
import { updateNGSWConfig } from './ngsw-config';
import { updateApplicationTsConfigs } from './update-app-tsconfigs';
import { updateDependencies } from './update-dependencies';
import { updateServerMainFile } from './update-server-main-file';
import { updateWorkspaceConfig } from './update-workspace-config';

export default function(): Rule {
Expand All @@ -22,6 +23,7 @@ export default function(): Rule {
updateNGSWConfig(),
updateApplicationTsConfigs(),
updateDependencies(),
updateServerMainFile(),
(tree, context) => {
const packageChanges = tree.actions.some(a => a.path.endsWith('/package.json'));
if (packageChanges) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* @license
* Copyright Google Inc. 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 { Rule } from '@angular-devkit/schematics';
import * as ts from '../../third_party/github.com/Microsoft/TypeScript/lib/typescript';
import { findNodes } from '../../utility/ast-utils';
import { findPropertyInAstObject } from '../../utility/json-utils';
import { Builders } from '../../utility/workspace-models';
import { getTargets, getWorkspace } from './utils';

/**
* Update the `main.server.ts` file by adding exports to `renderModule` and `renderModuleFactory` which are
* now required for Universal and App-Shell for Ivy and `bundleDependencies`.
*/
export function updateServerMainFile(): Rule {
return tree => {
const workspace = getWorkspace(tree);

for (const { target } of getTargets(workspace, 'server', Builders.Server)) {
const options = findPropertyInAstObject(target, 'options');
if (!options || options.kind !== 'object') {
continue;
}

// find the main server file
const mainFile = findPropertyInAstObject(options, 'main');
if (!mainFile || typeof mainFile.value !== 'string') {
continue;
}

const mainFilePath = mainFile.value;

const content = tree.read(mainFilePath);
if (!content) {
continue;
}

const source = ts.createSourceFile(
mainFilePath,
content.toString().replace(/^\uFEFF/, ''),
ts.ScriptTarget.Latest,
true,
);

// find exports in main server file
const exportDeclarations = findNodes(source, ts.SyntaxKind.ExportDeclaration) as ts.ExportDeclaration[];

const platformServerExports = exportDeclarations.filter(({ moduleSpecifier }) => (
moduleSpecifier && ts.isStringLiteral(moduleSpecifier) && moduleSpecifier.text === '@angular/platform-server'
));

let hasRenderModule = false;
let hasRenderModuleFactory = false;

// find exports of renderModule or renderModuleFactory
for (const { exportClause } of platformServerExports) {
if (exportClause && ts.isNamedExports(exportClause)) {
if (!hasRenderModuleFactory) {
hasRenderModuleFactory = exportClause.elements.some(({ name }) => name.text === 'renderModuleFactory');
}

if (!hasRenderModule) {
hasRenderModule = exportClause.elements.some(({ name }) => name.text === 'renderModule');
}
}
}

if (hasRenderModule && hasRenderModuleFactory) {
// We have both required exports
continue;
}

let exportSpecifiers: ts.ExportSpecifier[] = [];
let updateExisting = false;

// Add missing exports
if (platformServerExports.length) {
const { exportClause } = platformServerExports[0] as ts.ExportDeclaration;
if (!exportClause) {
continue;
}

exportSpecifiers = [...exportClause.elements];
updateExisting = true;
}

if (!hasRenderModule) {
exportSpecifiers.push(ts.createExportSpecifier(
undefined,
ts.createIdentifier('renderModule'),
));
}

if (!hasRenderModuleFactory) {
exportSpecifiers.push(ts.createExportSpecifier(
undefined,
ts.createIdentifier('renderModuleFactory'),
));
}

// Create a TS printer to get the text of the export node
const printer = ts.createPrinter();

const moduleSpecifier = ts.createStringLiteral('@angular/platform-server');

// TypeScript will emit the Node with double quotes.
// In schematics we usually write code with a single quotes
// tslint:disable-next-line: no-any
(moduleSpecifier as any).singleQuote = true;

const newExportDeclarationText = printer.printNode(
ts.EmitHint.Unspecified,
ts.createExportDeclaration(
undefined,
undefined,
ts.createNamedExports(exportSpecifiers),
moduleSpecifier,
),
source,
);

const recorder = tree.beginUpdate(mainFilePath);
if (updateExisting) {
const start = platformServerExports[0].getStart();
const width = platformServerExports[0].getWidth();

recorder
.remove(start, width)
.insertLeft(start, newExportDeclarationText);
} else {
recorder.insertLeft(source.getWidth(), '\n' + newExportDeclarationText);
}

tree.commitUpdate(recorder);
}

return tree;
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* @license
* Copyright Google Inc. 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 { tags } from '@angular-devkit/core';
import { EmptyTree } from '@angular-devkit/schematics';
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';

const mainServerContent = tags.stripIndents`
import { enableProdMode } from '@angular/core';

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

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

export { AppServerModule } from './app/app.server.module';
`;

const mainServerFile = 'src/main.server.ts';

describe('Migration to version 9', () => {
describe('Migrate Server Main File', () => {
const schematicRunner = new SchematicTestRunner(
'migrations',
require.resolve('../migration-collection.json'),
);

let tree: UnitTestTree;

beforeEach(async () => {
tree = new UnitTestTree(new EmptyTree());
tree = await schematicRunner
.runExternalSchematicAsync(
require.resolve('../../collection.json'),
'ng-new',
{
name: 'migration-test',
version: '1.2.3',
directory: '.',
},
tree,
)
.toPromise();
tree = await schematicRunner
.runExternalSchematicAsync(
require.resolve('../../collection.json'),
'universal',
{
clientProject: 'migration-test',
},
tree,
)
.toPromise();
});

it(`should add exports from '@angular/platform-server'`, async () => {
tree.overwrite(mainServerFile, mainServerContent);
const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise();
expect(tree2.readContent(mainServerFile)).toContain(tags.stripIndents`
export { AppServerModule } from './app/app.server.module';
export { renderModule, renderModuleFactory } from '@angular/platform-server';
`);
});

it(`should add 'renderModule' and 'renderModuleFactory' to existing '@angular/platform-server' export`, async () => {
tree.overwrite(mainServerFile, tags.stripIndents`
${mainServerContent}
export { platformDynamicServer } from '@angular/platform-server';
export { PlatformConfig } from '@angular/platform-server';
`);
const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise();
expect(tree2.readContent(mainServerFile)).toContain(tags.stripIndents`
export { AppServerModule } from './app/app.server.module';
export { platformDynamicServer, renderModule, renderModuleFactory } from '@angular/platform-server';
export { PlatformConfig } from '@angular/platform-server';
`);
});

it(`should add 'renderModule' to existing '@angular/platform-server' export`, async () => {
tree.overwrite(mainServerFile, tags.stripIndents`
${mainServerContent}
export { platformDynamicServer, renderModuleFactory } from '@angular/platform-server';
`);
const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise();
expect(tree2.readContent(mainServerFile)).toContain(tags.stripIndents`
export { AppServerModule } from './app/app.server.module';
export { platformDynamicServer, renderModuleFactory, renderModule } from '@angular/platform-server';
`);
});

it(`should not update exports when 'renderModule' and 'renderModuleFactory' are already exported`, async () => {
const input = tags.stripIndents`
${mainServerContent}
export { renderModule, renderModuleFactory } from '@angular/platform-server';
`;

tree.overwrite(mainServerFile, input);
const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise();
expect(tree2.readContent(mainServerFile)).toBe(input);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ if (environment.production) {
}

export { <%= rootModuleClassName %> } from './app/<%= stripTsExtension(rootModuleFileName) %>';
export { renderModule, renderModuleFactory } from '@angular/platform-server';
Loading