Skip to content

Commit

Permalink
feat(prerender): add buildId, hydrate externals, DOMContentLoaded
Browse files Browse the repository at this point in the history
- Fire DOMContentLoaded events when prerendering done
- Add data-stencil-static id to prerendered `<html>`
- Collect page.state data into build results
- Add "externals" option for modules to exclude from hydrate bundle
  • Loading branch information
adamdbradley committed Sep 16, 2020
1 parent be20372 commit 4d49c63
Show file tree
Hide file tree
Showing 12 changed files with 260 additions and 138 deletions.
81 changes: 9 additions & 72 deletions src/compiler/prerender/prerender-config.ts
Original file line number Diff line number Diff line change
@@ -1,85 +1,22 @@
import type * as d from '../../declarations';
import { catchError, loadTypeScriptDiagnostics } from '@utils';
import { IS_NODE_ENV, requireFunc } from '../sys/environment';
import { resolve } from 'path';
import ts from 'typescript';
import { isString } from '@utils';
import { nodeRequire } from '../sys/node-require';

export const getPrerenderConfig = (diagnostics: d.Diagnostic[], prerenderConfigPath: string) => {
const prerenderConfig: d.PrerenderConfig = {};

if (typeof prerenderConfigPath === 'string') {
if (IS_NODE_ENV) {
const userPrerenderConfig = nodeRequireTsConfig(diagnostics, prerenderConfigPath);
if (userPrerenderConfig) {
Object.assign(prerenderConfig, userPrerenderConfig);
}
}
}

return prerenderConfig;
};

const nodeRequireTsConfig = (diagnostics: d.Diagnostic[], prerenderConfigPath: string) => {
let prerenderConfig: d.PrerenderConfig = {};

try {
const fs: typeof import('fs') = requireFunc('fs');

// ensure we cleared out node's internal require() cache for this file
delete require.cache[resolve(prerenderConfigPath)];
if (isString(prerenderConfigPath)) {
const results = nodeRequire(prerenderConfigPath);
diagnostics.push(...results.diagnostics);

// let's override node's require for a second
// don't worry, we'll revert this when we're done
require.extensions['.ts'] = (module: NodeModuleWithCompile, fileName: string) => {
let sourceText = fs.readFileSync(fileName, 'utf8');

if (prerenderConfigPath.endsWith('.ts')) {
// looks like we've got a typed config file
// let's transpile it to .js quick
// const ts = require('typescript');
const tsResults = ts.transpileModule(sourceText, {
fileName,
compilerOptions: {
module: ts.ModuleKind.CommonJS,
moduleResolution: ts.ModuleResolutionKind.NodeJs,
esModuleInterop: true,
target: ts.ScriptTarget.ES2017,
allowJs: true,
},
});
sourceText = tsResults.outputText;

if (tsResults.diagnostics.length > 0) {
diagnostics.push(...loadTypeScriptDiagnostics(tsResults.diagnostics));
}
} else {
// quick hack to turn a modern es module
// into and old school commonjs module
sourceText = sourceText.replace(/export\s+\w+\s+(\w+)/gm, 'exports.$1');
}

module._compile(sourceText, fileName);
};

// let's do this!
const m = requireFunc(prerenderConfigPath);
if (m != null && typeof m === 'object') {
if (m.config != null && typeof m.config === 'object') {
prerenderConfig = m.config;
if (results.module != null && typeof results.module === 'object') {
if (results.module.config != null && typeof results.module.config === 'object') {
Object.assign(prerenderConfig, results.module.config);
} else {
prerenderConfig = m;
Object.assign(prerenderConfig, results.module);
}
}
} catch (e) {
catchError(diagnostics, e);
}

// all set, let's go ahead and reset the require back to the default
require.extensions['.ts'] = undefined;

return prerenderConfig;
};

interface NodeModuleWithCompile extends NodeModule {
_compile(code: string, filename: string): any;
}
53 changes: 43 additions & 10 deletions src/compiler/prerender/prerender-main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type * as d from '../../declarations';
import { buildError, catchError, hasError } from '@utils';
import { buildError, catchError, hasError, isString } from '@utils';
import { createHydrateBuildId } from '../../hydrate/runner/render-utils';
import { createWorkerContext } from '../worker/worker-thread';
import { createWorkerMainContext } from '../worker/main-thread';
import { drainPrerenderQueue, initializePrerenderEntryUrls } from './prerender-queue';
Expand All @@ -14,29 +15,40 @@ import { isOutputTargetWww } from '../output-targets/output-utils';

export const createPrerenderer = async (config: d.Config) => {
const start = (opts: d.PrerenderStartOptions) => {
return runPrerender(config, opts.hydrateAppFilePath, opts.componentGraph, opts.srcIndexHtmlPath);
return runPrerender(config, opts.hydrateAppFilePath, opts.componentGraph, opts.srcIndexHtmlPath, opts.buildId);
};
return {
start,
};
};

const runPrerender = async (config: d.Config, hydrateAppFilePath: string, componentGraph: d.BuildResultsComponentGraph, srcIndexHtmlPath: string) => {
const runPrerender = async (
config: d.Config,
hydrateAppFilePath: string,
componentGraph: d.BuildResultsComponentGraph,
srcIndexHtmlPath: string,
buildId: string,
) => {
const startTime = Date.now();
const diagnostics: d.Diagnostic[] = [];
const results: d.PrerenderResults = {
buildId,
diagnostics,
urls: 0,
duration: 0,
average: 0,
};
const outputTargets = config.outputTargets.filter(isOutputTargetWww).filter(o => typeof o.indexHtml === 'string');
const outputTargets = config.outputTargets.filter(isOutputTargetWww).filter(o => isString(o.indexHtml));

if (!isString(results.buildId)) {
results.buildId = createHydrateBuildId();
}

if (outputTargets.length === 0) {
return results;
}

if (typeof hydrateAppFilePath !== 'string') {
if (!isString(hydrateAppFilePath)) {
const diagnostic = buildError(diagnostics);
diagnostic.header = `Prerender Error`;
diagnostic.messageText = `Build results missing "hydrateAppFilePath"`;
Expand Down Expand Up @@ -77,7 +89,17 @@ const runPrerender = async (config: d.Config, hydrateAppFilePath: string, compon
try {
await Promise.all(
outputTargets.map(outputTarget => {
return runPrerenderOutputTarget(workerCtx, results, diagnostics, config, devServer, hydrateAppFilePath, componentGraph, srcIndexHtmlPath, outputTarget);
return runPrerenderOutputTarget(
workerCtx,
results,
diagnostics,
config,
devServer,
hydrateAppFilePath,
componentGraph,
srcIndexHtmlPath,
outputTarget,
);
}),
);
} catch (e) {
Expand Down Expand Up @@ -130,7 +152,6 @@ const runPrerenderOutputTarget = async (
// get the prerender urls to queue up
const prerenderDiagnostics: d.Diagnostic[] = [];
const manager: d.PrerenderManager = {
id: `${Math.random() * Number.MAX_VALUE}`,
prerenderUrlWorker: (prerenderRequest: d.PrerenderUrlRequest) => workerCtx.prerenderWorker(prerenderRequest),
componentGraphPath: null,
config: config,
Expand Down Expand Up @@ -163,8 +184,16 @@ const runPrerenderOutputTarget = async (
return;
}

const templateData = await generateTemplateHtml(config, prerenderConfig, diagnostics, manager.isDebug, srcIndexHtmlPath, outputTarget, hydrateOpts);
if (diagnostics.length > 0 || !templateData || typeof templateData.html !== 'string') {
const templateData = await generateTemplateHtml(
config,
prerenderConfig,
diagnostics,
manager.isDebug,
srcIndexHtmlPath,
outputTarget,
hydrateOpts,
);
if (diagnostics.length > 0 || !templateData || !isString(templateData.html)) {
return;
}

Expand Down Expand Up @@ -231,7 +260,11 @@ const createPrerenderTemplate = async (config: d.Config, templateHtml: string) =
return templateId;
};

const createComponentGraphPath = (config: d.Config, componentGraph: d.BuildResultsComponentGraph, outputTarget: d.OutputTargetWww) => {
const createComponentGraphPath = (
config: d.Config,
componentGraph: d.BuildResultsComponentGraph,
outputTarget: d.OutputTargetWww,
) => {
if (componentGraph) {
const content = getComponentPathContent(componentGraph, outputTarget);
const hash = config.sys.generateContentHash(content);
Expand Down
12 changes: 7 additions & 5 deletions src/compiler/prerender/prerender-queue.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type * as d from '../../declarations';
import { buildError, catchError } from '@utils';
import { buildError, catchError, isFunction, isString } from '@utils';
import { crawlAnchorsForNextUrls } from './crawl-urls';
import { getWriteFilePathFromUrlPath } from './prerendered-write-path';
import { relative } from 'path';
Expand Down Expand Up @@ -43,7 +43,7 @@ export const initializePrerenderEntryUrls = (results: d.PrerenderResults, manage
};

const addUrlToPendingQueue = (manager: d.PrerenderManager, queueUrl: string, fromUrl: string) => {
if (typeof queueUrl !== 'string' || queueUrl === '') {
if (!isString(queueUrl) || queueUrl === '') {
return;
}
if (manager.urlsPending.has(queueUrl)) {
Expand Down Expand Up @@ -93,7 +93,7 @@ export const drainPrerenderQueue = (results: d.PrerenderResults, manager: d.Prer
}

if (manager.urlsProcessing.size === 0 && manager.urlsPending.size === 0) {
if (typeof manager.resolve === 'function') {
if (isFunction(manager.resolve)) {
// we're not actively processing anything
// and there aren't anymore urls in the queue to be prerendered
// so looks like our job here is done, good work team
Expand All @@ -113,8 +113,8 @@ const prerenderUrl = async (results: d.PrerenderResults, manager: d.PrerenderMan
}

const prerenderRequest: d.PrerenderUrlRequest = {
id: manager.id,
baseUrl: manager.outputTarget.baseUrl,
buildId: results.buildId,
componentGraphPath: manager.componentGraphPath,
devServerHostUrl: manager.devServerHostUrl,
hydrateAppFilePath: manager.hydrateAppFilePath,
Expand Down Expand Up @@ -158,7 +158,9 @@ const prerenderUrl = async (results: d.PrerenderResults, manager: d.PrerenderMan

const urlsCompletedSize = manager.urlsCompleted.size;
if (manager.progressLogger && urlsCompletedSize > 1) {
manager.progressLogger.update(` prerendered ${urlsCompletedSize} urls: ${manager.config.logger.dim(previewUrl)}`);
manager.progressLogger.update(
` prerendered ${urlsCompletedSize} urls: ${manager.config.logger.dim(previewUrl)}`,
);
}

// let's try to drain the queue again and let this
Expand Down
18 changes: 12 additions & 6 deletions src/compiler/prerender/prerender-template-html.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import type * as d from '../../declarations';
import { catchError, isPromise } from '@utils';
import { hasStencilScript, inlineExternalStyleSheets, minifyScriptElements, minifyStyleElements, removeStencilScripts } from './prerender-optimize';
import { catchError, isPromise, isFunction, isString } from '@utils';
import {
hasStencilScript,
inlineExternalStyleSheets,
minifyScriptElements,
minifyStyleElements,
removeStencilScripts,
} from './prerender-optimize';
import { createDocument, serializeNodeToHtml } from '@stencil/core/mock-doc';

export const generateTemplateHtml = async (
Expand All @@ -13,12 +19,12 @@ export const generateTemplateHtml = async (
hydrateOpts: d.PrerenderHydrateOptions,
) => {
try {
if (typeof srcIndexHtmlPath !== 'string') {
if (!isString(srcIndexHtmlPath)) {
srcIndexHtmlPath = outputTarget.indexHtml;
}

let templateHtml: string;
if (typeof prerenderConfig.loadTemplate === 'function') {
if (isFunction(prerenderConfig.loadTemplate)) {
const loadTemplateResult = prerenderConfig.loadTemplate(srcIndexHtmlPath);
if (isPromise(loadTemplateResult)) {
templateHtml = await loadTemplateResult;
Expand Down Expand Up @@ -72,7 +78,7 @@ export const generateTemplateHtml = async (
}
}

if (typeof prerenderConfig.beforeSerializeTemplate === 'function') {
if (isFunction(prerenderConfig.beforeSerializeTemplate)) {
const beforeSerializeResults = prerenderConfig.beforeSerializeTemplate(doc);
if (isPromise(beforeSerializeResults)) {
doc = await beforeSerializeResults;
Expand All @@ -83,7 +89,7 @@ export const generateTemplateHtml = async (

let html = serializeNodeToHtml(doc);

if (typeof prerenderConfig.afterSerializeTemplate === 'function') {
if (isFunction(prerenderConfig.afterSerializeTemplate)) {
const afterSerializeResults = prerenderConfig.afterSerializeTemplate(html);
if (isPromise(afterSerializeResults)) {
html = await afterSerializeResults;
Expand Down
22 changes: 19 additions & 3 deletions src/compiler/prerender/prerender-worker.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import type * as d from '../../declarations';
import { addModulePreloads, excludeStaticComponents, minifyScriptElements, minifyStyleElements, removeModulePreloads, removeStencilScripts } from './prerender-optimize';
import {
addModulePreloads,
excludeStaticComponents,
minifyScriptElements,
minifyStyleElements,
removeModulePreloads,
removeStencilScripts,
} from './prerender-optimize';
import { catchError, isPromise, isRootPath, normalizePath, isFunction } from '@utils';
import { crawlAnchorsForNextUrls } from './crawl-urls';
import { getHydrateOptions } from './prerender-hydrate-options';
Expand All @@ -16,7 +23,6 @@ const prerenderCtx = {
export const prerenderWorker = async (sys: d.CompilerSystem, prerenderRequest: d.PrerenderUrlRequest) => {
// worker thread!
const results: d.PrerenderUrlResults = {
id: prerenderRequest.id,
diagnostics: [],
anchorUrls: [],
filePath: prerenderRequest.writeToFilePath,
Expand Down Expand Up @@ -58,6 +64,10 @@ export const prerenderWorker = async (sys: d.CompilerSystem, prerenderRequest: d
hydrateOpts.clientHydrateAnnotations = false;
}

if (typeof hydrateOpts.buildId !== 'string') {
hydrateOpts.buildId = prerenderRequest.buildId;
}

if (typeof prerenderConfig.beforeHydrate === 'function') {
try {
const rtn = prerenderConfig.beforeHydrate(doc, url);
Expand Down Expand Up @@ -118,7 +128,13 @@ export const prerenderWorker = async (sys: d.CompilerSystem, prerenderRequest: d

if (prerenderConfig.crawlUrls !== false) {
const baseUrl = new URL(prerenderRequest.baseUrl);
results.anchorUrls = crawlAnchorsForNextUrls(prerenderConfig, results.diagnostics, baseUrl, url, hydrateResults.anchors);
results.anchorUrls = crawlAnchorsForNextUrls(
prerenderConfig,
results.diagnostics,
baseUrl,
url,
hydrateResults.anchors,
);
}

if (typeof prerenderConfig.afterHydrate === 'function') {
Expand Down
Loading

0 comments on commit 4d49c63

Please sign in to comment.