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

Commit e688092

Browse files
authored
fix(hapi-engine): add bundleDependencies and lazy-loading fixes (#1188)
1 parent 86d1fe1 commit e688092

6 files changed

Lines changed: 140 additions & 74 deletions

File tree

modules/hapi-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/hapi-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/hapi-engine/schematics/install/files/root/__serverFileName@stripTsExtension__.ts

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

821
import {Request, Server} from 'hapi';
922
import * as Inert from 'inert';
1023
import {join} from 'path';
1124

12-
// Faster server renders w/ Prod mode (dev mode never needed)
13-
enableProdMode();
14-
1525
// Hapi server
1626
const PORT = process.env.PORT || <%= serverPort %>;
1727
const server = new Server({ port: PORT, host: 'localhost' });
1828

1929
const DIST_FOLDER = join(process.cwd(), 'dist');
2030

2131
// * NOTE :: leave this as require() since this file is built Dynamically from webpack
22-
const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./<%= clientProject %>-server/main');
32+
const {AppServerModuleNgFactory, LAZY_MODULE_MAP, ngHapiEngine, provideModuleMap} = require('./<%= clientProject %>-server/main');
2333

2434
server.route({
2535
method: 'GET',

modules/hapi-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 Hapi 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/hapi-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('ngHapiEngine');
136+
expect(contents).toContain('provideModuleMap');
137+
});
127138
});

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

Lines changed: 76 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,38 @@
77
*/
88
import {experimental, strings, normalize} from '@angular-devkit/core';
99
import {
10-
Rule,
11-
SchematicContext,
12-
SchematicsException,
13-
Tree,
1410
apply,
1511
chain,
12+
externalSchematic,
13+
filter,
1614
mergeWith,
15+
noop,
16+
Rule,
17+
SchematicContext,
18+
SchematicsException,
1719
template,
20+
Tree,
1821
url,
19-
noop,
20-
filter,
21-
externalSchematic,
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';
26-
import {BrowserBuilderOptions} from '@schematics/angular/utility/workspace-models';
2726
import {
2827
addPackageJsonDependency,
2928
NodeDependencyType,
3029
} from '@schematics/angular/utility/dependencies';
3130
import {getProject} from '@schematics/angular/utility/project';
3231
import {getProjectTargets} from '@schematics/angular/utility/project-targets';
33-
import * as ts from 'typescript';
34-
import {findAppServerModulePath} from './utils';
35-
import {addSymbolToNgModuleMetadata, insertImport} from '@schematics/angular/utility/ast-utils';
3632
import {InsertChange} from '@schematics/angular/utility/change';
33+
import {
34+
addSymbolToNgModuleMetadata,
35+
findNodes,
36+
insertAfterLastOccurrence,
37+
insertImport
38+
} from '@schematics/angular/utility/ast-utils';
39+
import * as ts from 'typescript';
40+
import {findAppServerModulePath, generateExport, getTsSourceFile, getTsSourceText} from './utils';
41+
import {updateWorkspace} from '@schematics/angular/utility/workspace';
3742

3843
// TODO(CaerusKaru): make these configurable
3944
const BROWSER_DIST = 'dist/browser';
@@ -102,54 +107,50 @@ function addDependenciesAndScripts(options: UniversalOptions): Rule {
102107
pkg.scripts['serve:ssr'] = `node dist/${serverFileName}`;
103108
pkg.scripts['build:ssr'] = 'npm run build:client-and-server-bundles && npm run compile:server';
104109
pkg.scripts['build:client-and-server-bundles'] =
105-
`ng build --prod && ng run ${options.clientProject}:server:production`;
110+
// tslint:disable:max-line-length
111+
`ng build --prod && ng run ${options.clientProject}:server:production --bundleDependencies all`;
106112

107113
host.overwrite(pkgPath, JSON.stringify(pkg, null, 2));
108114

109115
return host;
110116
};
111117
}
112118

113-
function updateConfigFile(options: UniversalOptions): Rule {
114-
return (host: Tree) => {
115-
const workspace = getWorkspace(host);
116-
if (!workspace.projects[options.clientProject]) {
117-
throw new SchematicsException(`Client app ${options.clientProject} not found.`);
118-
}
119-
120-
const clientProject = workspace.projects[options.clientProject];
121-
if (!clientProject.architect) {
122-
throw new Error('Client project architect not found.');
123-
}
119+
function updateConfigFile(options: UniversalOptions) {
120+
return updateWorkspace((workspace => {
121+
const clientProject = workspace.projects.get(options.clientProject);
122+
if (clientProject) {
123+
const buildTarget = clientProject.targets.get('build');
124+
const serverTarget = clientProject.targets.get('build');
124125

125-
// We have to check if the project config has a server target, because
126-
// if the Universal step in this schematic isn't run, it can't be guaranteed
127-
// to exist
128-
if (!clientProject.architect.server) {
129-
return;
130-
}
131-
132-
clientProject.architect.server.configurations = {
133-
production: {
134-
fileReplacements: [
135-
{
136-
replace: 'src/environments/environment.ts',
137-
with: 'src/environments/environment.prod.ts'
138-
}
139-
]
126+
// We have to check if the project config has a server target, because
127+
// if the Universal step in this schematic isn't run, it can't be guaranteed
128+
// to exist
129+
if (!serverTarget || !buildTarget) {
130+
return;
140131
}
141-
};
142-
// TODO(CaerusKaru): make this configurable
143-
clientProject.architect.server.options.outputPath = SERVER_DIST;
144-
// TODO(CaerusKaru): make this configurable
145-
(clientProject.architect.build.options as BrowserBuilderOptions).outputPath = BROWSER_DIST;
146132

147-
const workspacePath = getWorkspacePath(host);
133+
serverTarget.configurations = {
134+
production: {
135+
fileReplacements: [
136+
{
137+
replace: 'src/environments/environment.ts',
138+
with: 'src/environments/environment.prod.ts'
139+
}
140+
]
141+
}
142+
};
148143

149-
host.overwrite(workspacePath, JSON.stringify(workspace, null, 2));
144+
serverTarget.options = {
145+
...serverTarget.options,
146+
outputPath: SERVER_DIST,
147+
};
150148

151-
return host;
152-
};
149+
buildTarget.options = {
150+
outputPath: BROWSER_DIST,
151+
};
152+
}
153+
}));
153154
}
154155

155156
function addModuleMapLoader(options: UniversalOptions): Rule {
@@ -162,7 +163,6 @@ function addModuleMapLoader(options: UniversalOptions): Rule {
162163
return;
163164
}
164165
const mainPath = normalize('/' + clientTargets.server.options.main);
165-
166166
const appServerModuleRelativePath = findAppServerModulePath(host, mainPath);
167167
const modulePath = normalize(
168168
`/${clientProject.root}/src/${appServerModuleRelativePath}.ts`);
@@ -194,15 +194,33 @@ function addModuleMapLoader(options: UniversalOptions): Rule {
194194
};
195195
}
196196

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

205-
return source;
208+
const mainPath = normalize('/' + clientTargets.server.options.main);
209+
const mainSourceFile = getTsSourceFile(host, mainPath);
210+
let mainText = getTsSourceText(host, mainPath);
211+
const mainRecorder = host.beginUpdate(mainPath);
212+
const hapiEngineExport = generateExport(mainSourceFile, ['ngHapiEngine'],
213+
'@nguniversal/hapi-engine');
214+
const moduleMapExport = generateExport(mainSourceFile, ['provideModuleMap'],
215+
'@nguniversal/module-map-ngfactory-loader');
216+
const exports = findNodes(mainSourceFile, ts.SyntaxKind.ExportDeclaration);
217+
const addedExports = `\n${hapiEngineExport}\n${moduleMapExport}\n`;
218+
const exportChange = insertAfterLastOccurrence(exports, addedExports, mainText,
219+
0) as InsertChange;
220+
221+
mainRecorder.insertLeft(exportChange.pos, exportChange.toAdd);
222+
host.commitUpdate(mainRecorder);
223+
};
206224
}
207225

208226
export default function (options: UniversalOptions): Rule {
@@ -236,6 +254,7 @@ export default function (options: UniversalOptions): Rule {
236254
mergeWith(rootSource),
237255
addDependenciesAndScripts(options),
238256
addModuleMapLoader(options),
257+
addExports(options),
239258
]);
240259
};
241260
}

modules/hapi-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)