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

Commit d8be7af

Browse files
CaerusKaruvikerman
authored andcommitted
fix(express-engine): add bundleDependencies and lazy-loading fixes (#1167)
* fix(express-engine): add bundleDependencies and lazy-loading fixes * fixup! fix(express-engine): add bundleDependencies and lazy-loading fixes * fixup! fix(express-engine): add bundleDependencies and lazy-loading fixes * fixup! fix(express-engine): add bundleDependencies and lazy-loading fixes * fixup! fix(express-engine): add bundleDependencies and lazy-loading fixes * fixup! fix(express-engine): add bundleDependencies and lazy-loading fixes * fixup! fix(express-engine): add bundleDependencies and lazy-loading fixes * fixup! fix(express-engine): add bundleDependencies and lazy-loading fixes * fixup! fix(express-engine): add bundleDependencies and lazy-loading fixes
1 parent 22a23bb commit d8be7af

15 files changed

Lines changed: 425 additions & 356 deletions

File tree

.circleci/config.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# fetched by the Webtesting rules. Therefore for jobs that run tests with Bazel, we don't need a
1313
# docker image with browsers pre-installed.
1414
# **NOTE**: If you change the the version of the docker images, also change the `cache_key` suffix.
15-
var_1: &docker_image angular/ngcontainer:0.10.0
15+
var_1: &docker_image circleci/node:10.12-browsers
1616
var_2: &cache_key v2-nguniversal-{{ .Branch }}-{{ checksum "yarn.lock" }}-node-10.12
1717

1818
# Settings common to each job
@@ -80,8 +80,9 @@ jobs:
8080
- *checkout_code
8181
- *restore_cache
8282
- *copy_bazel_config
83+
- *yarn_install
8384

84-
- run: bazel test //...
85+
- run: yarn bazel test //...
8586

8687
# Note: We want to save the cache in this job because the workspace cache also
8788
# includes the Bazel repository cache that will be updated in this job.

integration/express-engine/e2e/helloworld-spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,8 @@ describe('Hello world E2E Tests', function() {
4242
// Test the contents from the server.
4343
const serverDiv = browser.driver.findElement(by.css('span.href-check'));
4444
expect(serverDiv.getText()).toMatch('http://localhost:9876/helloworld');
45+
46+
// Make sure there were no client side errors.
47+
verifyNoBrowserErrors();
4548
});
4649
});

integration/express-engine/src/server.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,14 @@ enableProdMode();
2525
app.use('/built', express.static('built'));
2626

2727
// Keep the browser logs free of errors.
28-
app.get('/favicon.ico', (req, res) => { res.send(''); });
28+
app.get('/favicon.ico', (_, res) => { res.send(''); });
2929

3030
//-----------ADD YOUR SERVER SIDE RENDERED APP HERE ----------------------
31-
app.get('/helloworld', (req: Request, res) =>
32-
ngExpressEngine({bootstrap: HelloWorldServerModuleNgFactory})('built/src/index.html', {
31+
app.get('/helloworld', (req: Request, res) => ngExpressEngine({bootstrap: HelloWorldServerModuleNgFactory})('built/src/index.html', {
3332
bootstrap: HelloWorldServerModuleNgFactory,
3433
req,
3534
document: helloworld,
36-
}, (err, html) => res.send(html))
35+
}, (_, html) => res.send(html))
3736
);
3837

3938
app.listen(9876, function() { console.log('Server listening on port 9876!'); });

integration/run_tests.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ cd "$(dirname "$0")"
1111
readonly basedir=$(pwd)/..
1212

1313
# Track payload size functions
14-
if [$CI != true]; then
14+
if !($CI); then
1515
# Not on CircleCI so let's build the packages-dist directory.
1616
# This should be fast on incremental re-build.
1717
${basedir}/scripts/build-modules-dist.sh

modules/express-engine/schematics/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ ts_library(
2424
module_name = "@nguniversal/express-engine/schematics",
2525
tsconfig = ":tsconfig.json",
2626
deps = [
27+
"@npm//@angular-devkit/core",
2728
"@npm//@angular-devkit/schematics",
2829
"@npm//@schematics/angular",
2930
"@npm//@types/jasmine",
@@ -56,6 +57,7 @@ ng_test_library(
5657
tsconfig = ":tsconfig.json",
5758
deps = [
5859
":schematics",
60+
"@npm//@angular-devkit/core",
5961
"@npm//@angular-devkit/schematics",
6062
"@npm//@schematics/angular",
6163
],

modules/express-engine/schematics/install/files/root/__serverFileName@stripTsExtension__.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,33 @@
1+
/**
2+
* *** NOTE ON IMPORTING FROM ANGULAR AND NGUNIVERSAL IN THIS FILE ***
3+
*
4+
* If your application uses third-party dependencies, you'll need to
5+
* either use Webpack or the Angular CLI's `bundleDependencies` feature
6+
* in order to adequately package them for use on the server without a
7+
* node_modules directory.
8+
*
9+
* However, due to the nature of the CLI's `bundleDependencies`, importing
10+
* Angular in this file will create a different instance of Angular than
11+
* the version in the compiled application code. This leads to unavoidable
12+
* conflicts. Therefore, please do not explicitly import from @angular or
13+
* @nguniversal in this file. You can export any needed resources
14+
* from your application's main.server.ts file, as seen below with the
15+
* import for `ngExpressEngine`.
16+
*/
17+
118
import 'zone.js/dist/zone-node';
2-
import {enableProdMode} from '@angular/core';
3-
// Express Engine
4-
import {ngExpressEngine} from '@nguniversal/express-engine';
5-
// Import module map for lazy loading
6-
import {provideModuleMap} from '@nguniversal/module-map-ngfactory-loader';
719

820
import * as express from 'express';
921
import {join} from 'path';
1022

11-
// Faster server renders w/ Prod mode (dev mode never needed)
12-
enableProdMode();
13-
1423
// Express server
1524
const app = express();
1625

1726
const PORT = process.env.PORT || <%= serverPort %>;
1827
const DIST_FOLDER = join(process.cwd(), '<%= getBrowserDistDirectory() %>');
1928

2029
// * NOTE :: leave this as require() since this file is built Dynamically from webpack
21-
const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./<%= getServerDistDirectory() %>/main');
30+
const {AppServerModuleNgFactory, LAZY_MODULE_MAP, ngExpressEngine, provideModuleMap} = require('./<%= getServerDistDirectory() %>/main');
2231

2332
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
2433
app.engine('html', ngExpressEngine({

modules/express-engine/schematics/install/files/root/webpack.server.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ module.exports = {
99
// This is our Express server for Dynamic universal
1010
server: './<%= stripTsExtension(serverFileName) %>.ts'
1111
},
12+
externals: {
13+
'./<%= getServerDistDirectory() %>/main': 'require("./server/main")'
14+
},
1215
target: 'node',
1316
resolve: { extensions: ['.ts', '.js'] },
1417
optimization: {
@@ -20,6 +23,7 @@ module.exports = {
2023
filename: '[name].js'
2124
},
2225
module: {
26+
noParse: /polyfills-.*\.js/,
2327
rules: [
2428
{ test: /\.ts$/, loader: 'ts-loader' },
2529
{

modules/express-engine/schematics/install/index.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,15 @@ describe('Universal Schematic', () => {
124124
const contents = tree.readContent(filePath);
125125
expect(contents).toContain('ModuleMapLoaderModule');
126126
});
127+
128+
it('should add exports to main server file', async () => {
129+
const tree = await schematicRunner
130+
.runSchematicAsync('ng-add', defaultOptions, appTree)
131+
.toPromise();
132+
const filePath = '/projects/bar/src/main.server.ts';
133+
const contents = tree.readContent(filePath);
134+
console.log({contents});
135+
expect(contents).toContain('ngExpressEngine');
136+
expect(contents).toContain('provideModuleMap');
137+
});
127138
});

modules/express-engine/schematics/install/index.ts

Lines changed: 72 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,24 @@ import {
2121
url,
2222
} from '@angular-devkit/schematics';
2323
import {NodePackageInstallTask} from '@angular-devkit/schematics/tasks';
24-
import {getWorkspace, getWorkspacePath} from '@schematics/angular/utility/config';
24+
import {getWorkspace} from '@schematics/angular/utility/config';
2525
import {Schema as UniversalOptions} from './schema';
2626
import {
2727
addPackageJsonDependency,
2828
NodeDependencyType,
2929
} from '@schematics/angular/utility/dependencies';
30-
import {BrowserBuilderOptions} from '@schematics/angular/utility/workspace-models';
3130
import {getProject} from '@schematics/angular/utility/project';
32-
import {
33-
getProjectTargets,
34-
targetBuildNotFoundError,
35-
} from '@schematics/angular/utility/project-targets';
31+
import {getProjectTargets} from '@schematics/angular/utility/project-targets';
3632
import {InsertChange} from '@schematics/angular/utility/change';
37-
import {addSymbolToNgModuleMetadata, insertImport} from '@schematics/angular/utility/ast-utils';
33+
import {
34+
addSymbolToNgModuleMetadata,
35+
findNodes,
36+
insertAfterLastOccurrence,
37+
insertImport
38+
} from '@schematics/angular/utility/ast-utils';
3839
import * as ts from 'typescript';
39-
import {findAppServerModulePath} from './utils';
40+
import {findAppServerModulePath, generateExport, getTsSourceFile, getTsSourceText} from './utils';
41+
import {updateWorkspace} from '@schematics/angular/utility/workspace';
4042

4143
// TODO(CaerusKaru): make these configurable
4244
const BROWSER_DIST = 'dist/browser';
@@ -100,54 +102,50 @@ function addDependenciesAndScripts(options: UniversalOptions): Rule {
100102
pkg.scripts['serve:ssr'] = `node dist/${serverFileName}`;
101103
pkg.scripts['build:ssr'] = 'npm run build:client-and-server-bundles && npm run compile:server';
102104
pkg.scripts['build:client-and-server-bundles'] =
103-
`ng build --prod && ng run ${options.clientProject}:server:production`;
105+
// tslint:disable:max-line-length
106+
`ng build --prod && ng run ${options.clientProject}:server:production --bundleDependencies all`;
104107

105108
host.overwrite(pkgPath, JSON.stringify(pkg, null, 2));
106109

107110
return host;
108111
};
109112
}
110113

111-
function updateConfigFile(options: UniversalOptions): Rule {
112-
return (host: Tree) => {
113-
const workspace = getWorkspace(host);
114-
if (!workspace.projects[options.clientProject]) {
115-
throw new SchematicsException(`Client app ${options.clientProject} not found.`);
116-
}
117-
118-
const clientProject = workspace.projects[options.clientProject];
119-
if (!clientProject.architect) {
120-
throw new Error('Client project architect not found.');
121-
}
122-
123-
// We have to check if the project config has a server target, because
124-
// if the Universal step in this schematic isn't run, it can't be guaranteed
125-
// to exist
126-
if (!clientProject.architect.server) {
127-
return;
128-
}
129-
130-
clientProject.architect.server.configurations = {
131-
production: {
132-
fileReplacements: [
133-
{
134-
replace: 'src/environments/environment.ts',
135-
with: 'src/environments/environment.prod.ts'
136-
}
137-
]
114+
function updateConfigFile(options: UniversalOptions) {
115+
return updateWorkspace((workspace => {
116+
const clientProject = workspace.projects.get(options.clientProject);
117+
if (clientProject) {
118+
const buildTarget = clientProject.targets.get('build');
119+
const serverTarget = clientProject.targets.get('build');
120+
121+
// We have to check if the project config has a server target, because
122+
// if the Universal step in this schematic isn't run, it can't be guaranteed
123+
// to exist
124+
if (!serverTarget || !buildTarget) {
125+
return;
138126
}
139-
};
140-
// TODO(CaerusKaru): make this configurable
141-
clientProject.architect.server.options.outputPath = SERVER_DIST;
142-
// TODO(CaerusKaru): make this configurable
143-
(clientProject.architect.build.options as BrowserBuilderOptions).outputPath = BROWSER_DIST;
144-
145-
const workspacePath = getWorkspacePath(host);
146127

147-
host.overwrite(workspacePath, JSON.stringify(workspace, null, 2));
148-
149-
return host;
150-
};
128+
serverTarget.configurations = {
129+
production: {
130+
fileReplacements: [
131+
{
132+
replace: 'src/environments/environment.ts',
133+
with: 'src/environments/environment.prod.ts'
134+
}
135+
]
136+
}
137+
};
138+
139+
serverTarget.options = {
140+
...serverTarget.options,
141+
outputPath: SERVER_DIST,
142+
};
143+
144+
buildTarget.options = {
145+
outputPath: BROWSER_DIST,
146+
};
147+
}
148+
}));
151149
}
152150

153151
function addModuleMapLoader(options: UniversalOptions): Rule {
@@ -160,7 +158,6 @@ function addModuleMapLoader(options: UniversalOptions): Rule {
160158
return;
161159
}
162160
const mainPath = normalize('/' + clientTargets.server.options.main);
163-
164161
const appServerModuleRelativePath = findAppServerModulePath(host, mainPath);
165162
const modulePath = normalize(
166163
`/${clientProject.root}/src/${appServerModuleRelativePath}.ts`);
@@ -192,15 +189,33 @@ function addModuleMapLoader(options: UniversalOptions): Rule {
192189
};
193190
}
194191

195-
function getTsSourceFile(host: Tree, path: string): ts.SourceFile {
196-
const buffer = host.read(path);
197-
if (!buffer) {
198-
throw new SchematicsException(`Could not read file (${path}).`);
199-
}
200-
const content = buffer.toString();
201-
const source = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true);
192+
function addExports(options: UniversalOptions): Rule {
193+
return (host: Tree) => {
194+
const clientProject = getProject(host, options.clientProject);
195+
const clientTargets = getProjectTargets(clientProject);
196+
197+
if (!clientTargets.server) {
198+
// If they skipped Universal schematics and don't have a server target,
199+
// just get out
200+
return;
201+
}
202202

203-
return source;
203+
const mainPath = normalize('/' + clientTargets.server.options.main);
204+
const mainSourceFile = getTsSourceFile(host, mainPath);
205+
let mainText = getTsSourceText(host, mainPath);
206+
const mainRecorder = host.beginUpdate(mainPath);
207+
const expressEngineExport = generateExport(mainSourceFile, ['ngExpressEngine'],
208+
'@nguniversal/express-engine');
209+
const moduleMapExport = generateExport(mainSourceFile, ['provideModuleMap'],
210+
'@nguniversal/module-map-ngfactory-loader');
211+
const exports = findNodes(mainSourceFile, ts.SyntaxKind.ExportDeclaration);
212+
const addedExports = `\n${expressEngineExport}\n${moduleMapExport}\n`;
213+
const exportChange = insertAfterLastOccurrence(exports, addedExports, mainText,
214+
0) as InsertChange;
215+
216+
mainRecorder.insertLeft(exportChange.pos, exportChange.toAdd);
217+
host.commitUpdate(mainRecorder);
218+
};
204219
}
205220

206221
export default function (options: UniversalOptions): Rule {
@@ -234,6 +249,7 @@ export default function (options: UniversalOptions): Rule {
234249
mergeWith(rootSource),
235250
addDependenciesAndScripts(options),
236251
addModuleMapLoader(options),
252+
addExports(options),
237253
]);
238254
};
239255
}

modules/express-engine/schematics/install/utils.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,21 @@ import { SchematicsException, Tree } from '@angular-devkit/schematics';
99
import * as ts from 'typescript';
1010
import {getSourceNodes} from '@schematics/angular/utility/ast-utils';
1111

12-
export function findAppServerModuleExport(host: Tree,
13-
mainPath: string): ts.ExportDeclaration | null {
14-
const mainBuffer = host.read(mainPath);
15-
if (!mainBuffer) {
16-
throw new SchematicsException(`Main file (${mainPath}) not found`);
12+
export function getTsSourceText(host: Tree, path: string): string {
13+
const buffer = host.read(path);
14+
if (!buffer) {
15+
throw new SchematicsException(`Could not read file (${path}).`);
1716
}
18-
const mainText = mainBuffer.toString('utf-8');
19-
const source = ts.createSourceFile(mainPath, mainText, ts.ScriptTarget.Latest, true);
17+
return buffer.toString();
18+
}
19+
20+
export function getTsSourceFile(host: Tree, path: string): ts.SourceFile {
21+
return ts.createSourceFile(path, getTsSourceText(host, path), ts.ScriptTarget.Latest, true);
22+
}
2023

24+
export function findAppServerModuleExport(host: Tree,
25+
mainPath: string): ts.ExportDeclaration | null {
26+
const source = getTsSourceFile(host, mainPath);
2127
const allNodes = getSourceNodes(source);
2228

2329
let exportDeclaration: ts.ExportDeclaration | null = null;
@@ -49,6 +55,20 @@ export function findAppServerModulePath(host: Tree, mainPath: string): string {
4955
throw new SchematicsException('Could not find app server module export');
5056
}
5157

52-
const moduleSpecifier = exportDeclaration.moduleSpecifier.getText();
58+
const moduleSpecifier = exportDeclaration.moduleSpecifier!.getText();
5359
return moduleSpecifier.substring(1, moduleSpecifier.length - 1);
5460
}
61+
62+
export function generateExport(sourceFile: ts.SourceFile,
63+
elements: string[],
64+
module: string): string {
65+
const printer = ts.createPrinter();
66+
const exports = elements.map(element =>
67+
ts.createExportSpecifier(undefined, element));
68+
const namedExports = ts.createNamedExports(exports);
69+
const moduleSpecifier = ts.createStringLiteral(module);
70+
const exportDeclaration = ts.createExportDeclaration(undefined, undefined,
71+
namedExports, moduleSpecifier);
72+
73+
return printer.printNode(ts.EmitHint.Unspecified, exportDeclaration, sourceFile);
74+
}

0 commit comments

Comments
 (0)