Skip to content

Commit

Permalink
fix(@nguniversal/builders): Initialize zone.js once in rendering workers
Browse files Browse the repository at this point in the history
This is a fix for `Node > 16.17.1: Method Promise.prototype.then called on incompatible receiver` when using `@angular/localize`.
  • Loading branch information
alan-agius4 committed Nov 15, 2022
1 parent 7ebba3c commit ba967ba
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 14 deletions.
6 changes: 6 additions & 0 deletions modules/builders/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ ts_library(
deps = [
"//modules/common/clover/server",
"//modules/common/engine",
"//modules/common/tools",
"@npm//@angular-devkit/architect",
"@npm//@angular-devkit/build-angular",
"@npm//@angular-devkit/core",
"@npm//@angular/core",
"@npm//@angular/platform-server",
"@npm//@types/browser-sync",
"@npm//@types/express",
"@npm//guess-parser",
Expand All @@ -44,6 +47,9 @@ pkg_npm(
package_name = "@nguniversal/builders",
srcs = [":builders_assets"],
tags = ["release"],
visibility = [
"//integration:__subpackages__",
],
deps = [":builders"],
)

Expand Down
9 changes: 4 additions & 5 deletions modules/builders/src/prerender/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,10 @@ import * as fs from 'fs';
import ora from 'ora';
import * as path from 'path';
import Piscina from 'piscina';
import { promisify } from 'util';
import { PrerenderBuilderOptions, PrerenderBuilderOutput } from './models';
import { getIndexOutputFile, getRoutes } from './utils';
import { RenderOptions, RenderResult } from './worker';

export const readFile = promisify(fs.readFile);

type BuildBuilderOutput = BuilderOutput & {
baseOutputPath: string;
outputPaths: string[];
Expand Down Expand Up @@ -103,11 +100,13 @@ async function _renderUniversal(
browserOptions.optimization,
);

const zonePackage = require.resolve('zone.js', { paths: [context.workspaceRoot] });

const { baseOutputPath = '' } = serverResult;
const worker = new Piscina({
filename: path.join(__dirname, 'worker.js'),
name: 'render',
maxThreads: numProcesses,
workerData: { zonePackage },
});

try {
Expand All @@ -134,7 +133,7 @@ async function _renderUniversal(
serverBundlePath,
};

return worker.run(options, { name: 'render' });
return worker.run(options);
}),
)) as RenderResult[];
let numErrors = 0;
Expand Down
64 changes: 55 additions & 9 deletions modules/builders/src/prerender/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/

import type { Type } from '@angular/core';
import type * as platformServer from '@angular/platform-server';
import type { ɵInlineCriticalCssProcessor } from '@nguniversal/common/tools';
import * as fs from 'fs';
import * as path from 'path';
import { workerData } from 'worker_threads';
import { loadEsmModule } from '../utils/utils';

export interface RenderOptions {
Expand All @@ -24,10 +28,18 @@ export interface RenderResult {
warnings?: string[];
}

/**
* The fully resolved path to the zone.js package that will be loaded during worker initialization.
* This is passed as workerData when setting up the worker via the `piscina` package.
*/
const { zonePackage } = workerData as {
zonePackage: string;
};

/**
* Renders each route in routes and writes them to <outputPath>/<route>/index.html.
*/
export async function render({
async function render({
indexFile,
deployUrl,
minifyCss,
Expand All @@ -41,31 +53,38 @@ export async function render({
const outputFolderPath = path.join(outputPath, route);
const outputIndexPath = path.join(outputFolderPath, 'index.html');

const { ɵInlineCriticalCssProcessor: InlineCriticalCssProcessor } = await loadEsmModule<
typeof import('@nguniversal/common/tools')
>('@nguniversal/common/tools');
const { AppServerModule, renderModule } = (await import(serverBundlePath)) as {
renderModule: typeof platformServer.renderModule | undefined;
AppServerModule: Type<unknown> | undefined;
};

const { renderModule, AppServerModule } = await import(serverBundlePath);
if (!renderModule) {
throw new Error(`renderModule was not exported from: ${serverBundlePath}.`);
}

if (!AppServerModule) {
throw new Error(`AppServerModule was not exported from: ${serverBundlePath}.`);
}

const indexBaseName = fs.existsSync(path.join(outputPath, 'index.original.html'))
? 'index.original.html'
: indexFile;
const browserIndexInputPath = path.join(outputPath, indexBaseName);
let indexHtml = await fs.promises.readFile(browserIndexInputPath, 'utf8');
indexHtml = indexHtml.replace(
let document = await fs.promises.readFile(browserIndexInputPath, 'utf8');
document = document.replace(
'</html>',
'<!-- This page was prerendered with Angular Universal -->\n</html>',
);
if (inlineCriticalCss) {
// Workaround for https://github.com/GoogleChromeLabs/critters/issues/64
indexHtml = indexHtml.replace(
document = document.replace(
/ media="print" onload="this\.media='all'"><noscript><link .+?><\/noscript>/g,
'>',
);
}

let html = await renderModule(AppServerModule, {
document: indexHtml,
document,
url: route,
});

Expand Down Expand Up @@ -94,3 +113,30 @@ export async function render({

return result;
}

let InlineCriticalCssProcessor: typeof ɵInlineCriticalCssProcessor;
/**
* Initializes the worker when it is first created by loading the Zone.js package
* into the worker instance.
*
* @returns A promise resolving to the render function of the worker.
*/
async function initialize() {
const { ɵInlineCriticalCssProcessor } = await loadEsmModule<
typeof import('@nguniversal/common/tools')
>('@nguniversal/common/tools');

InlineCriticalCssProcessor = ɵInlineCriticalCssProcessor;

// Setup Zone.js
await import(zonePackage);

// Return the render function for use
return render;
}

/**
* The default export will be the promise returned by the initialize function.
* This is awaited by piscina prior to using the Worker.
*/
export default initialize();

0 comments on commit ba967ba

Please sign in to comment.