-
Notifications
You must be signed in to change notification settings - Fork 480
feat(schematics): add prerendering scripts to express-schematic #1206
Changes from all commits
49c446e
723274d
74439c0
7d2a286
722f3dd
b1defe3
2aeb2ba
bff5e73
ea2853a
7cf2c46
9edda23
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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'); | ||
|
||
const routeData = require('./routes.json'); | ||
|
||
// Faster server renders w/ Prod mode (dev mode never needed) | ||
enableProdMode(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
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 |
---|---|---|
|
@@ -16,5 +16,8 @@ | |
"dom" | ||
] | ||
}, | ||
"include": ["<%= stripTsExtension(serverFileName) %>.ts"] | ||
"include": [ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are missing the closing There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
---|---|---|
|
@@ -58,6 +58,7 @@ function getClientProject( | |
|
||
function addDependenciesAndScripts(options: UniversalOptions): Rule { | ||
return (host: Tree) => { | ||
|
||
addPackageJsonDependency(host, { | ||
type: NodeDependencyType.Default, | ||
name: '@nguniversal/express-engine', | ||
|
@@ -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 | ||
|
@@ -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); | ||
|
@@ -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'], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Some more context here: angular/angular-cli#15517 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
||
|
@@ -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); | ||
|
@@ -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({ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,6 +25,11 @@ | |
"format": "path", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
"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", | ||
|
@@ -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", | ||
|
There was a problem hiding this comment.
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
?There was a problem hiding this comment.
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!