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

feat(schematics): add prerendering scripts to express-schematic #1206

Closed
wants to merge 11 commits into from
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import 'zone.js/dist/zone-node';

import 'reflect-metadata';
import {readFileSync, writeFileSync, existsSync, mkdirSync} from 'fs';
import {join} from 'path';
import * as fs from 'fs';

// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const {AppServerModuleNgFactory, LAZY_MODULE_MAP, provideModuleMap, renderModuleFactory, enableProdMode} = require('./<%= getServerDistDirectory() %>/main');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't renderModuleFactory, enableProdMode in @angular/platform-server and @angular/core?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes they are but they're actually exported in the server/main.ts file. This prevents us from having 2 copies of Angular.
Something @vikerman actually figured out!


const routeData = require('./routes.json');

// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still needed since it's already defined in main.server.ts?


const BROWSER_FOLDER = join(process.cwd(), '<%= getBrowserDistDirectory() %>');

// Load the index.html file containing references to your application bundle.
const index = readFileSync(join('browser', 'index.html'), 'utf8');

let previousRender = Promise.resolve();

// Iterate each route path
routeData.routes.forEach(route => {
const fullPath = join(BROWSER_FOLDER, route);

// Make sure the directory structure is there
if (!existsSync(fullPath)) {
mkdirSync(fullPath);
}

// Writes rendered HTML to index.html, replacing the file if it already exists.
previousRender = previousRender.then(_ => renderModuleFactory(AppServerModuleNgFactory, {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is an error here, would this throw with unhandled promise rejection?

Copy link
Contributor Author

@MarkPieszak MarkPieszak Sep 3, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it actually throws it regardless, which is great! For example a (<any>window).test = '123'; thrown in an app components code (that will pass ng build, etc) will throw an error when trying to build the prerender like shown below.

image

document: index,
url: route,
extraProviders: [
provideModuleMap(LAZY_MODULE_MAP)
]
})).then(html => writeFileSync(join(fullPath, 'index.html'), html));
});

const siteMap = `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
${routeData.routes.map(route => `<url>
<loc>${routeData.hostname ? routeData.hostname : ''}${route}</loc>
</url>`)}
</urlset>`;

fs.writeFile(join(BROWSER_FOLDER, 'sitemap.xml'), siteMap, 'utf8', (err) => {
if (err) {
throw err;
}
console.log('Sitemap has been created.');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"hostname": "",
"routes": [
"/"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,8 @@
"dom"
]
},
"include": ["<%= stripTsExtension(serverFileName) %>.ts"]
"include": [
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are missing the closing ]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, fixed!

"<%= stripTsExtension(serverFileName) %>.ts"<% if (!skipPrerender) { %>,
<%= stripTsExtension(prerenderFileName) %>.ts"<% } %>
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ module.exports = {
mode: 'none',
entry: {
// This is our Express server for Dynamic universal
server: './<%= stripTsExtension(serverFileName) %>.ts'
server: './<%= stripTsExtension(serverFileName) %>.ts'<% if (!skipPrerender) { %>,
// This is our script for Static Prerendering
prerender: './<%= stripTsExtension(prerenderFileName) %>'<% } %>
},
externals: {
'./<%= getServerDistDirectory() %>/main': 'require("./server/main")'
Expand Down
58 changes: 57 additions & 1 deletion modules/express-engine/schematics/install/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ function getClientProject(

function addDependenciesAndScripts(options: UniversalOptions): Rule {
return (host: Tree) => {

addPackageJsonDependency(host, {
type: NodeDependencyType.Default,
name: '@nguniversal/express-engine',
Expand Down Expand Up @@ -99,7 +100,11 @@ function addDependenciesAndScripts(options: UniversalOptions): Rule {
pkg.scripts['compile:server'] = options.webpack ?
'webpack --config webpack.server.config.js --progress --colors' :
`tsc -p ${serverFileName}.tsconfig.json`;

pkg.scripts['serve:ssr'] = `node dist/${serverFileName}`;
pkg.scripts['build:prerender'] =
// tslint:disable-next-line: max-line-length
`npm run build:client-and-server-bundles && npm run compile:server && node dist/${options.prerenderFileName}`,
pkg.scripts['build:ssr'] = 'npm run build:client-and-server-bundles && npm run compile:server';
pkg.scripts['build:client-and-server-bundles'] =
// tslint:disable:max-line-length
Expand All @@ -111,6 +116,26 @@ function addDependenciesAndScripts(options: UniversalOptions): Rule {
};
}

function existingAppUpdatePrerenderScripts(options: UniversalOptions): Rule {
return (host: Tree) => {

const pkgPath = '/package.json';
const buffer = host.read(pkgPath);
if (buffer === null) {
throw new SchematicsException('Could not find package.json');
}

const pkg = JSON.parse(buffer.toString());
pkg.scripts['build:prerender'] =
// tslint:disable-next-line: max-line-length
`npm run build:client-and-server-bundles && npm run compile:server && node dist/${options.prerenderFileName}`,

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

return host;
};
}

function updateConfigFile(options: UniversalOptions) {
return updateWorkspace((workspace => {
const clientProject = workspace.projects.get(options.clientProject);
Expand Down Expand Up @@ -194,12 +219,16 @@ function addExports(options: UniversalOptions): Rule {
const mainSourceFile = getTsSourceFile(host, mainPath);
let mainText = getTsSourceText(host, mainPath);
const mainRecorder = host.beginUpdate(mainPath);
const enableProdModeExport = generateExport(mainSourceFile, ['enableProdMode'],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: we should actually check if these exports already exists prior to adding them. Since, we will add these exports in the universal schematics as well

'@angular/core');
const renderModuleFactoryExport = generateExport(mainSourceFile, ['renderModuleFactory'],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this target version 8 or 9? Is so probable we should change renderModuleFactory to renderModule

Some more context here: angular/angular-cli#15517

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually regarding the later two comments if we wait for version 9 you don’t need to add them because they will be adde by default in the cli version 9.

Also, for existing project I will do a migration to add these exports

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually regarding the later two comments if we wait for version 9 you don’t need to add them because they will be adde by default in the cli version 9.

Also, for existing project I will do a migration to add these exports

'@angular/platform-server');
const expressEngineExport = generateExport(mainSourceFile, ['ngExpressEngine'],
'@nguniversal/express-engine');
const moduleMapExport = generateExport(mainSourceFile, ['provideModuleMap'],
'@nguniversal/module-map-ngfactory-loader');
const exports = findNodes(mainSourceFile, ts.SyntaxKind.ExportDeclaration);
const addedExports = `\n${expressEngineExport}\n${moduleMapExport}\n`;
const addedExports = `\n${expressEngineExport}\n${moduleMapExport}\n${renderModuleFactoryExport}\n${enableProdModeExport}\n`;
const exportChange = insertAfterLastOccurrence(exports, addedExports, mainText,
0) as InsertChange;

Expand All @@ -208,6 +237,27 @@ function addExports(options: UniversalOptions): Rule {
};
}

function updateExistingProjectPrerenderOnly(options: UniversalOptions) {
const rootSource = apply(url('./files/root'), [
filter(path => !path.startsWith('__prerenderFileName')),
options.webpack ?
filter(path => !path.includes('tsconfig')) : filter(path => !path.startsWith('webpack')),
template({
...strings,
...options as object,
stripTsExtension: (s: string) => s.replace(/\.ts$/, ''),
getBrowserDistDirectory: () => BROWSER_DIST,
getServerDistDirectory: () => SERVER_DIST,
})
]);

return chain([
mergeWith(rootSource),
existingAppUpdatePrerenderScripts(options),
addExports(options),
]);
}

export default function (options: UniversalOptions): Rule {
return (host: Tree, context: SchematicContext) => {
const clientProject = getClientProject(host, options);
Expand All @@ -219,8 +269,14 @@ export default function (options: UniversalOptions): Rule {
context.addTask(new NodePackageInstallTask());
}

if (options.prerenderOnly) {
return updateExistingProjectPrerenderOnly(options);
}

const rootSource = apply(url('./files/root'), [
options.skipServer ? filter(path => !path.startsWith('__serverFileName')) : noop(),
options.skipPrerender ? filter(path => !path.startsWith('__prerenderFileName')) : noop(),
options.skipPrerender ? filter(path => !path.startsWith('__routes')) : noop(),
options.webpack ?
filter(path => !path.includes('tsconfig')) : filter(path => !path.startsWith('webpack')),
template({
Expand Down
10 changes: 10 additions & 0 deletions modules/express-engine/schematics/install/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
"format": "path",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

skipPrerender doesn't seem to be defined in the schema.

"description": "The name of the test entry-point file."
},
"prerenderFileName": {
"type": "string",
"default": "prerender.ts",
"description": "The name of the Prerender server file."
},
"serverFileName": {
"type": "string",
"default": "server.ts",
Expand Down Expand Up @@ -78,6 +83,11 @@
"type": "boolean",
"default": false
},
"prerenderOnly": {
"description": "Add only missing pieces for Pre-rendering to previously installed Universal Schematic",
"type": "boolean",
"default": false
},
"webpack": {
"description": "Whether to add webpack configuration files",
"type": "boolean",
Expand Down
12 changes: 12 additions & 0 deletions modules/express-engine/schematics/install/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export interface Schema {
* The name of the test entry-point file.
*/
test?: string;
/**
* The name of the Prerender script file.
*/
prerenderFileName?: string;
/**
* The name of the Express server file.
*/
Expand Down Expand Up @@ -55,6 +59,10 @@ export interface Schema {
* Skip installing dependency packages.
*/
skipInstall?: boolean;
/**
* Skip adding Prerender script file.
*/
skipPrerender?: boolean;
/**
* Skip adding Express server file.
*/
Expand All @@ -63,6 +71,10 @@ export interface Schema {
* Skip the Angular Universal schematic
*/
skipUniversal?: boolean;
/**
* Add only missing pieces for Pre-rendering to previously installed Universal Schematic
*/
prerenderOnly?: boolean;
/**
* Whether to add webpack configuration files
*/
Expand Down