,
+ fn: (input: I) => void
+) {
+ return yield* mapAsyncIterable(data, (x) => {
+ fn(x);
+ return x;
+ });
+}
diff --git a/packages/js/src/utils/swc/compile-swc.ts b/packages/js/src/utils/swc/compile-swc.ts
index 59b10b3c34..c597470802 100644
--- a/packages/js/src/utils/swc/compile-swc.ts
+++ b/packages/js/src/utils/swc/compile-swc.ts
@@ -1,7 +1,7 @@
import { cacheDir, ExecutorContext, logger } from '@nrwl/devkit';
import { exec, execSync } from 'child_process';
import { removeSync } from 'fs-extra';
-import { createAsyncIterable } from '../create-async-iterable/create-async-iteratable';
+import { createAsyncIterable } from '../async-iterable/create-async-iterable';
import { NormalizedSwcExecutorOptions, SwcCliOptions } from '../schema';
import { printDiagnostics } from '../typescript/print-diagnostics';
import { runTypeCheck, TypeCheckOptions } from '../typescript/run-type-check';
diff --git a/packages/js/src/utils/typescript/compile-typescript-files.ts b/packages/js/src/utils/typescript/compile-typescript-files.ts
index 1dde8e35f8..88e5d13110 100644
--- a/packages/js/src/utils/typescript/compile-typescript-files.ts
+++ b/packages/js/src/utils/typescript/compile-typescript-files.ts
@@ -4,7 +4,7 @@ import {
TypeScriptCompilationOptions,
} from '@nrwl/workspace/src/utilities/typescript/compilation';
import type { Diagnostic } from 'typescript';
-import { createAsyncIterable } from '../create-async-iterable/create-async-iteratable';
+import { createAsyncIterable } from '../async-iterable/create-async-iterable';
import { NormalizedExecutorOptions } from '../schema';
const TYPESCRIPT_FOUND_N_ERRORS_WATCHING_FOR_FILE_CHANGES = 6194;
diff --git a/packages/react/generators.json b/packages/react/generators.json
index 54627227f8..7232910d7f 100644
--- a/packages/react/generators.json
+++ b/packages/react/generators.json
@@ -95,6 +95,13 @@
"schema": "./src/generators/setup-tailwind/schema.json",
"description": "Set up Tailwind configuration for a project.",
"hidden": false
+ },
+
+ "setup-ssr": {
+ "factory": "./src/generators/setup-ssr/setup-ssr#setupSsrSchematic",
+ "schema": "./src/generators/setup-ssr/schema.json",
+ "description": "Set up SSR configuration for a project.",
+ "hidden": false
}
},
"generators": {
@@ -204,6 +211,13 @@
"schema": "./src/generators/setup-tailwind/schema.json",
"description": "Set up Tailwind configuration for a project.",
"hidden": false
+ },
+
+ "setup-ssr": {
+ "factory": "./src/generators/setup-ssr/setup-ssr#setupSsrGenerator",
+ "schema": "./src/generators/setup-ssr/schema.json",
+ "description": "Set up SSR configuration for a project.",
+ "hidden": false
}
}
}
diff --git a/packages/react/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts b/packages/react/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts
index 0b0bdb35fa..39ca116422 100644
--- a/packages/react/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts
+++ b/packages/react/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts
@@ -3,10 +3,8 @@ import devServerExecutor from '@nrwl/webpack/src/executors/dev-server/dev-server
import { WebDevServerOptions } from '@nrwl/webpack/src/executors/dev-server/schema';
import { join } from 'path';
import * as chalk from 'chalk';
-import {
- combineAsyncIterators,
- tapAsyncIterator,
-} from '../../utils/async-iterator';
+import { combineAsyncIterableIterators } from '@nrwl/js/src/utils/async-iterable/combine-async-iteratable-iterators';
+import { tapAsyncIterator } from '@nrwl/js/src/utils/async-iterable/tap-async-iteratable';
type ModuleFederationDevServerOptions = WebDevServerOptions & {
devRemotes?: string | string[];
@@ -50,7 +48,7 @@ export default async function* moduleFederationDevServer(
for (const app of knownRemotes) {
const [appName] = Array.isArray(app) ? app : [app];
const isDev = devServeApps.includes(appName);
- iter = combineAsyncIterators(
+ iter = combineAsyncIterableIterators(
iter,
await runExecutor(
{
diff --git a/packages/react/src/generators/setup-ssr/files/server.ts__tmpl__ b/packages/react/src/generators/setup-ssr/files/server.ts__tmpl__
new file mode 100644
index 0000000000..51bb68c042
--- /dev/null
+++ b/packages/react/src/generators/setup-ssr/files/server.ts__tmpl__
@@ -0,0 +1,24 @@
+import express from 'express';
+import { handleRequest } from './src/main.server';
+import * as path from 'path';
+
+const port = process.env['port'] || 4200;
+const app = express();
+
+const browserDist = path.join(process.cwd(), '<%= browserBuildOutputPath %>');
+const indexPath =path.join(browserDist, 'index.html');
+
+ app.get(
+ '*.*',
+ express.static(browserDist, {
+ maxAge: '1y',
+ })
+ );
+
+app.use('*', handleRequest(indexPath));
+
+const server = app.listen(port, () => {
+ // Server has started
+});
+
+server.on('error', console.error);
diff --git a/packages/react/src/generators/setup-ssr/files/src/main.server.tsx__tmpl__ b/packages/react/src/generators/setup-ssr/files/src/main.server.tsx__tmpl__
new file mode 100644
index 0000000000..a371c8b7c2
--- /dev/null
+++ b/packages/react/src/generators/setup-ssr/files/src/main.server.tsx__tmpl__
@@ -0,0 +1,47 @@
+import type { Request, Response } from 'express';
+import * as fs from 'fs';
+import * as ReactDOMServer from 'react-dom/server';
+import isbot from 'isbot'
+
+import App from './<%= appComponentImport %>';
+
+
+let indexHtml: null | string = null;
+
+export function handleRequest(indexPath: string) {
+ return function render(req: Request, res: Response) {
+ let didError = false;
+
+ if (!indexHtml) {
+ indexHtml = fs.readFileSync(indexPath).toString();
+ }
+
+ const [htmlStart, htmlEnd] = indexHtml.split(``);
+
+ // For bots (e.g. search engines), the content will not be streamed but render all at once.
+ // For users, content should be streamed to the user as they are ready.
+ const callbackName = isbot(req.headers['user-agent']) ? 'onAllReady' : 'onShellReady';
+
+ const stream = ReactDOMServer.renderToPipeableStream(
+ ,
+ {
+ [callbackName]() {
+ res.statusCode = didError ? 500 : 200;
+ res.setHeader('Content-type', 'text/html; charset=utf-8');
+ res.write(`${htmlStart}`);
+ stream.pipe(res);
+ res.write(`
${htmlEnd}`);
+ },
+ onShellError(error) {
+ console.error(error);
+ res.statusCode = 500;
+ res.send('Server Error
');
+ },
+ onError(error) {
+ didError = true;
+ console.error(error);
+ }
+ }
+ );
+ }
+}
diff --git a/packages/react/src/generators/setup-ssr/files/tsconfig.server.json__tmpl__ b/packages/react/src/generators/setup-ssr/files/tsconfig.server.json__tmpl__
new file mode 100644
index 0000000000..6a76a67360
--- /dev/null
+++ b/packages/react/src/generators/setup-ssr/files/tsconfig.server.json__tmpl__
@@ -0,0 +1,14 @@
+{
+ "extends": "./tsconfig.app.json",
+ "compilerOptions": {
+ "outDir": "../../out-tsc/server",
+ "target": "es2019",
+ "types": [
+ "node"
+ ]
+ },
+ "include": [
+ "src/main.server.tsx",
+ "server.ts",
+ ]
+}
\ No newline at end of file
diff --git a/packages/react/src/generators/setup-ssr/schema.d.ts b/packages/react/src/generators/setup-ssr/schema.d.ts
new file mode 100644
index 0000000000..50b3735c93
--- /dev/null
+++ b/packages/react/src/generators/setup-ssr/schema.d.ts
@@ -0,0 +1,6 @@
+export interface Schema {
+ project: string;
+ appComponentImportPath: string;
+ serverPort?: number;
+ skipFormat?: boolean;
+}
diff --git a/packages/react/src/generators/setup-ssr/schema.json b/packages/react/src/generators/setup-ssr/schema.json
new file mode 100644
index 0000000000..2d6470fbab
--- /dev/null
+++ b/packages/react/src/generators/setup-ssr/schema.json
@@ -0,0 +1,36 @@
+{
+ "$schema": "http://json-schema.org/schema",
+ "$id": "GeneratorAngularUniversalSetup",
+ "cli": "nx",
+ "title": "Generate Angular Universal (SSR) setup for an Angular App",
+ "description": "Create the additional configuration required to enable SSR via Angular Universal for an Angular application.",
+ "type": "object",
+ "properties": {
+ "project": {
+ "type": "string",
+ "description": "The name of the application to add SSR support to.",
+ "$default": {
+ "$source": "argv",
+ "index": 0
+ },
+ "x-prompt": "What app would you like to add SSR support to?",
+ "x-dropdown": "projects"
+ },
+ "appComponentImportPath": {
+ "type": "string",
+ "description": "The import path of the component, relative to project sourceRoot.",
+ "default": "app/app"
+ },
+ "serverPort": {
+ "type": "number",
+ "default": 4200,
+ "description": "The port for the Express server."
+ },
+ "skipFormat": {
+ "type": "boolean",
+ "description": "Skip formatting the workspace after the generator completes."
+ }
+ },
+ "required": ["project"],
+ "additionalProperties": false
+}
diff --git a/packages/react/src/generators/setup-ssr/setup-ssr.ts b/packages/react/src/generators/setup-ssr/setup-ssr.ts
new file mode 100644
index 0000000000..d98c033c4f
--- /dev/null
+++ b/packages/react/src/generators/setup-ssr/setup-ssr.ts
@@ -0,0 +1,175 @@
+import {
+ addDependenciesToPackageJson,
+ convertNxGenerator,
+ formatFiles,
+ generateFiles,
+ joinPathFragments,
+ readProjectConfiguration,
+ readWorkspaceConfiguration,
+ Tree,
+ updateProjectConfiguration,
+ updateWorkspaceConfiguration,
+} from '@nrwl/devkit';
+
+import type { Schema } from './schema';
+import {
+ expressVersion,
+ isbotVersion,
+ typesExpressVersion,
+} from '../../utils/versions';
+
+export async function setupSsrGenerator(tree: Tree, options: Schema) {
+ const projectConfig = readProjectConfiguration(tree, options.project);
+ const projectRoot = projectConfig.root;
+ const appImportCandidates = [
+ options.appComponentImportPath,
+ 'app',
+ 'App',
+ 'app/App',
+ 'App/App',
+ ];
+ const appComponentImport = appImportCandidates.find(
+ (app) =>
+ tree.exists(joinPathFragments(projectConfig.sourceRoot, `${app}.tsx`)) ||
+ tree.exists(joinPathFragments(projectConfig.sourceRoot, `${app}.jsx`)) ||
+ tree.exists(joinPathFragments(projectConfig.sourceRoot, `${app}.js`))
+ );
+
+ if (!appComponentImport) {
+ throw new Error(
+ `Cannot find an import path for component. Try passing setting --appComponentImportPath option.`
+ );
+ }
+
+ if (!projectConfig.targets.build || !projectConfig.targets.serve) {
+ throw new Error(
+ `Project ${options.project} does not have build and serve targets`
+ );
+ }
+
+ if (projectConfig.targets.server) {
+ throw new Error(`Project ${options.project} already has a server target.`);
+ }
+
+ const originalOutputPath = projectConfig.targets.build?.options?.outputPath;
+
+ if (!originalOutputPath) {
+ throw new Error(
+ `Project ${options.project} does not contain a outputPath for the build target.`
+ );
+ }
+
+ projectConfig.targets.build.options.outputPath = joinPathFragments(
+ originalOutputPath,
+ 'browser'
+ );
+ projectConfig.targets = {
+ ...projectConfig.targets,
+ server: {
+ executor: '@nrwl/webpack:webpack',
+ outputs: ['{options.outputPath}'],
+ defaultConfiguration: 'production',
+ options: {
+ target: 'node',
+ main: `${projectRoot}/server.ts`,
+ outputPath: joinPathFragments(originalOutputPath, 'server'),
+ tsConfig: `${projectRoot}/tsconfig.server.json`,
+ compiler: 'babel',
+ externalDependencies: 'all',
+ outputHashing: 'none',
+ webpackConfig: '@nrwl/react/plugins/webpack',
+ },
+ configurations: {
+ development: {
+ optimization: false,
+ sourceMap: true,
+ },
+ production: {
+ fileReplacements: [
+ {
+ replace: `${projectRoot}/src/environments/environment.ts`,
+ with: `${projectRoot}/src/environments/environment.prod.ts`,
+ },
+ ],
+ sourceMap: false,
+ },
+ },
+ },
+ 'serve-browser': projectConfig.targets.serve,
+ 'serve-server': {
+ executor: '@nrwl/js:node',
+ defaultConfiguration: 'development',
+ options: {
+ buildTarget: `${options.project}:server:development`,
+ buildTargetOptions: {
+ watch: true,
+ },
+ },
+ configurations: {
+ development: {},
+ production: {
+ buildTarget: `${options.project}:server:production`,
+ },
+ },
+ },
+ serve: {
+ executor: '@nrwl/webpack:ssr-dev-server',
+ defaultConfiguration: 'development',
+ options: {
+ browserTarget: `${options.project}:build:development`,
+ serverTarget: `${options.project}:serve-server:development`,
+ port: options.serverPort,
+ browserTargetOptions: {
+ watch: true,
+ },
+ },
+ configurations: {
+ development: {},
+ production: {
+ browserTarget: `${options.project}:build:production`,
+ serverTarget: `${options.project}:serve-server:production`,
+ },
+ },
+ },
+ };
+
+ updateProjectConfiguration(tree, options.project, projectConfig);
+
+ const workspace = readWorkspaceConfiguration(tree);
+ if (
+ workspace.tasksRunnerOptions?.default &&
+ !workspace.tasksRunnerOptions.default.options.cacheableOperations['server']
+ ) {
+ workspace.tasksRunnerOptions.default.options.cacheableOperations = [
+ ...workspace.tasksRunnerOptions.default.options.cacheableOperations,
+ 'server',
+ ];
+ }
+
+ generateFiles(tree, joinPathFragments(__dirname, 'files'), projectRoot, {
+ tmpl: '',
+ appComponentImport,
+ browserBuildOutputPath: projectConfig.targets.build.options.outputPath,
+ });
+
+ updateWorkspaceConfiguration(tree, workspace);
+
+ const installTask = addDependenciesToPackageJson(
+ tree,
+ {
+ express: expressVersion,
+ isbot: isbotVersion,
+ },
+ {
+ '@types/express': typesExpressVersion,
+ }
+ );
+
+ await formatFiles(tree);
+
+ return installTask;
+}
+
+export default setupSsrGenerator;
+
+export const setupSsrSchematic = convertNxGenerator(setupSsrGenerator);
diff --git a/packages/react/src/utils/async-iterator.spec.ts b/packages/react/src/utils/async-iterator.spec.ts
deleted file mode 100644
index 7ba53d2d7f..0000000000
--- a/packages/react/src/utils/async-iterator.spec.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import {
- mapAsyncIterator,
- combineAsyncIterators,
- tapAsyncIterator,
-} from './async-iterator';
-
-function delay(ms: number) {
- return new Promise((resolve) => setTimeout(resolve, ms));
-}
-
-describe('combineAsyncIterators', () => {
- it('should merge iterators', async () => {
- async function* a() {
- await delay(20);
- yield 'a';
- }
-
- async function* b() {
- await delay(0);
- yield 'b';
- }
-
- const c = combineAsyncIterators(a(), b());
- const results = [];
-
- for await (const x of c) {
- results.push(x);
- }
-
- expect(results).toEqual(['b', 'a']);
- });
-
- it('should throw when one iterator throws', async () => {
- async function* a() {
- await delay(20);
- yield 'a';
- }
-
- async function* b() {
- throw new Error('threw in b');
- }
-
- const c = combineAsyncIterators(a(), b());
-
- async function* d() {
- yield* c;
- }
-
- try {
- for await (const x of d()) {
- }
- throw new Error('should not reach here');
- } catch (e) {
- expect(e.message).toMatch(/threw in b/);
- }
- });
-});
-
-describe('mapAsyncIterator', () => {
- it('should map over values', async () => {
- async function* f() {
- yield 1;
- yield 2;
- yield 3;
- }
-
- const c = mapAsyncIterator(f(), (x) => x * 2);
- const results = [];
-
- for await (const x of c) {
- results.push(x);
- }
-
- expect(results).toEqual([2, 4, 6]);
- });
-});
-
-describe('tapAsyncIterator', () => {
- it('should tap values', async () => {
- async function* f() {
- yield 1;
- yield 2;
- yield 3;
- }
-
- const tapped = [];
- const results = [];
-
- const c = tapAsyncIterator(f(), (x) => {
- tapped.push(`tap: ${x}`);
- });
-
- for await (const x of c) {
- results.push(x);
- }
-
- expect(tapped).toEqual(['tap: 1', 'tap: 2', 'tap: 3']);
- expect(results).toEqual([1, 2, 3]);
- });
-});
diff --git a/packages/react/src/utils/versions.ts b/packages/react/src/utils/versions.ts
index 68f328a551..bea00cb0bd 100755
--- a/packages/react/src/utils/versions.ts
+++ b/packages/react/src/utils/versions.ts
@@ -40,3 +40,7 @@ export const tsLibVersion = '^2.3.0';
export const postcssVersion = '8.4.16';
export const tailwindcssVersion = '3.1.8';
export const autoprefixerVersion = '10.4.12';
+
+export const expressVersion = '^4.18.1';
+export const typesExpressVersion = '4.17.13';
+export const isbotVersion = '^3.6.5';
diff --git a/packages/webpack/executors.json b/packages/webpack/executors.json
index 506ddcab74..41fc3cf300 100644
--- a/packages/webpack/executors.json
+++ b/packages/webpack/executors.json
@@ -9,6 +9,11 @@
"implementation": "./src/executors/dev-server/compat",
"schema": "./src/executors/dev-server/schema.json",
"description": "Serve a web application."
+ },
+ "ssr-dev-server": {
+ "implementation": "./src/executors/ssr-dev-server/compat",
+ "schema": "./src/executors/ssr-dev-server/schema.json",
+ "description": "Serve a SSR application."
}
},
"executors": {
@@ -21,6 +26,11 @@
"implementation": "./src/executors/dev-server/dev-server.impl",
"schema": "./src/executors/dev-server/schema.json",
"description": "Serve a web application."
+ },
+ "ssr-dev-server": {
+ "implementation": "./src/executors/ssr-dev-server/ssr-dev-server.impl",
+ "schema": "./src/executors/ssr-dev-server/schema.json",
+ "description": "Serve a SSR application."
}
}
}
diff --git a/packages/webpack/src/executors/ssr-dev-server/compat.ts b/packages/webpack/src/executors/ssr-dev-server/compat.ts
new file mode 100644
index 0000000000..1c387a219e
--- /dev/null
+++ b/packages/webpack/src/executors/ssr-dev-server/compat.ts
@@ -0,0 +1,5 @@
+import { convertNxExecutor } from '@nrwl/devkit';
+
+import ssrDevServerExecutor from './ssr-dev-server.impl';
+
+export default convertNxExecutor(ssrDevServerExecutor);
diff --git a/packages/webpack/src/executors/ssr-dev-server/lib/wait-until-server-is-listening.ts b/packages/webpack/src/executors/ssr-dev-server/lib/wait-until-server-is-listening.ts
new file mode 100644
index 0000000000..35de9978cd
--- /dev/null
+++ b/packages/webpack/src/executors/ssr-dev-server/lib/wait-until-server-is-listening.ts
@@ -0,0 +1,38 @@
+import * as net from 'net';
+
+export function waitUntilServerIsListening(port: number): Promise {
+ const allowedErrorCodes = ['ECONNREFUSED', 'ECONNRESET'];
+ const maxAttempts = 25;
+ let attempts = 0;
+ const client = new net.Socket();
+ const cleanup = () => {
+ client.removeAllListeners('connect');
+ client.removeAllListeners('error');
+ client.end();
+ client.destroy();
+ client.unref();
+ };
+
+ return new Promise((resolve, reject) => {
+ const listen = () => {
+ client.once('connect', () => {
+ cleanup();
+ resolve();
+ });
+ client.on('error', (err) => {
+ if (
+ attempts > maxAttempts ||
+ !allowedErrorCodes.includes(err['code'])
+ ) {
+ cleanup();
+ reject(err);
+ } else {
+ attempts++;
+ setTimeout(listen, 100 * attempts);
+ }
+ });
+ client.connect({ port, host: 'localhost' });
+ };
+ listen();
+ });
+}
diff --git a/packages/webpack/src/executors/ssr-dev-server/schema.d.ts b/packages/webpack/src/executors/ssr-dev-server/schema.d.ts
new file mode 100644
index 0000000000..ac62f9eab2
--- /dev/null
+++ b/packages/webpack/src/executors/ssr-dev-server/schema.d.ts
@@ -0,0 +1,11 @@
+interface TargetOptions {
+ [key: string]: string | boolean | number | TargetOptions;
+}
+
+export interface WebSsrDevServerOptions {
+ browserTarget: string;
+ serverTarget: string;
+ port: number;
+ browserTargetOptions: TargetOptions;
+ serverTargetOptions: TargetOptions;
+}
diff --git a/packages/webpack/src/executors/ssr-dev-server/schema.json b/packages/webpack/src/executors/ssr-dev-server/schema.json
new file mode 100644
index 0000000000..fd21ad5ef1
--- /dev/null
+++ b/packages/webpack/src/executors/ssr-dev-server/schema.json
@@ -0,0 +1,34 @@
+{
+ "version": 2,
+ "outputCapture": "direct-nodejs",
+ "title": "Web SSR Dev Server",
+ "description": "Serve a SSR application.",
+ "cli": "nx",
+ "type": "object",
+ "properties": {
+ "browserTarget": {
+ "type": "string",
+ "description": "Target which builds the browser application."
+ },
+ "serverTarget": {
+ "type": "string",
+ "description": "Target which builds the server application."
+ },
+ "port": {
+ "type": "number",
+ "description": "The port to be set on `process.env.PORT` for use in the server.",
+ "default": 4200
+ },
+ "browserTargetOptions": {
+ "type": "object",
+ "description": "Additional options to pass into the browser build target.",
+ "default": {}
+ },
+ "serverTargetOptions": {
+ "type": "object",
+ "description": "Additional options to pass into the server build target.",
+ "default": {}
+ }
+ },
+ "required": ["browserTarget", "serverTarget"]
+}
diff --git a/packages/webpack/src/executors/ssr-dev-server/ssr-dev-server.impl.ts b/packages/webpack/src/executors/ssr-dev-server/ssr-dev-server.impl.ts
new file mode 100644
index 0000000000..6d3a127bad
--- /dev/null
+++ b/packages/webpack/src/executors/ssr-dev-server/ssr-dev-server.impl.ts
@@ -0,0 +1,72 @@
+import {
+ ExecutorContext,
+ parseTargetString,
+ readTargetOptions,
+ runExecutor,
+} from '@nrwl/devkit';
+import * as chalk from 'chalk';
+import { combineAsyncIterableIterators } from '@nrwl/js/src/utils/async-iterable/combine-async-iteratable-iterators';
+
+import { WebpackExecutorOptions } from '../webpack/schema';
+import { WebSsrDevServerOptions } from './schema';
+import { waitUntilServerIsListening } from './lib/wait-until-server-is-listening';
+
+export async function* ssrDevServerExecutor(
+ options: WebSsrDevServerOptions,
+ context: ExecutorContext
+) {
+ const browserTarget = parseTargetString(options.browserTarget);
+ const serverTarget = parseTargetString(options.serverTarget);
+ const browserOptions = readTargetOptions(
+ browserTarget,
+ context
+ );
+ const serverOptions = readTargetOptions(
+ serverTarget,
+ context
+ );
+
+ const runBrowser = await runExecutor<{
+ success: boolean;
+ baseUrl?: string;
+ }>(
+ browserTarget,
+ { ...browserOptions, ...options.browserTargetOptions },
+ context
+ );
+ const runServer = await runExecutor<{
+ success: boolean;
+ baseUrl?: string;
+ }>(
+ serverTarget,
+ { ...serverOptions, ...options.serverTargetOptions },
+ context
+ );
+ let browserBuilt = false;
+ let nodeStarted = false;
+ const combined = combineAsyncIterableIterators(runBrowser, runServer);
+
+ process.env['port'] = `${options.port}`;
+
+ for await (const output of combined) {
+ if (!output.success) throw new Error('Could not build application');
+ if (output.options.target === 'node') {
+ nodeStarted = true;
+ } else if (output.options?.target === 'web') {
+ browserBuilt = true;
+ }
+
+ if (nodeStarted && browserBuilt) {
+ await waitUntilServerIsListening(options.port);
+ console.log(
+ `[ ${chalk.green('ready')} ] on http://localhost:${options.port}`
+ );
+ yield {
+ ...output,
+ baseUrl: `http://localhost:${options.port}`,
+ };
+ }
+ }
+}
+
+export default ssrDevServerExecutor;
diff --git a/packages/webpack/src/executors/webpack/webpack.impl.ts b/packages/webpack/src/executors/webpack/webpack.impl.ts
index f41c38268e..454dc4fd44 100644
--- a/packages/webpack/src/executors/webpack/webpack.impl.ts
+++ b/packages/webpack/src/executors/webpack/webpack.impl.ts
@@ -91,11 +91,16 @@ async function getWebpackConfigs(
}
export type WebpackExecutorEvent =
- | { success: false; outfile?: string }
+ | {
+ success: false;
+ outfile?: string;
+ options?: WebpackExecutorOptions;
+ }
| {
success: true;
outfile: string;
emittedFiles: EmittedFile[];
+ options?: WebpackExecutorOptions;
};
export async function* webpackExecutor(
@@ -129,6 +134,7 @@ export async function* webpackExecutor(
options.outputPath,
options.outputFileName
),
+ options,
};
}
}
@@ -204,6 +210,7 @@ export async function* webpackExecutor(
options.outputFileName
),
emittedFiles: [...emittedFiles1, ...emittedFiles2],
+ options,
};
})
)