feat(react): add SSR support to React apps (#13234)
This commit is contained in:
parent
f394608658
commit
23e4fc77c9
@ -1391,6 +1391,49 @@
|
|||||||
"implementation": "/packages/react/src/generators/setup-tailwind/setup-tailwind#setupTailwindGenerator.ts",
|
"implementation": "/packages/react/src/generators/setup-tailwind/setup-tailwind#setupTailwindGenerator.ts",
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"path": "/packages/react/src/generators/setup-tailwind/schema.json"
|
"path": "/packages/react/src/generators/setup-tailwind/schema.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "setup-ssr",
|
||||||
|
"factory": "./src/generators/setup-ssr/setup-ssr#setupSsrGenerator",
|
||||||
|
"schema": {
|
||||||
|
"$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 <App/> 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,
|
||||||
|
"presets": []
|
||||||
|
},
|
||||||
|
"description": "Set up SSR configuration for a project.",
|
||||||
|
"hidden": false,
|
||||||
|
"implementation": "/packages/react/src/generators/setup-ssr/setup-ssr#setupSsrGenerator.ts",
|
||||||
|
"aliases": [],
|
||||||
|
"path": "/packages/react/src/generators/setup-ssr/schema.json"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"executors": [
|
"executors": [
|
||||||
|
|||||||
@ -785,6 +785,49 @@
|
|||||||
"aliases": [],
|
"aliases": [],
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"path": "/packages/webpack/src/executors/dev-server/schema.json"
|
"path": "/packages/webpack/src/executors/dev-server/schema.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ssr-dev-server",
|
||||||
|
"implementation": "/packages/webpack/src/executors/ssr-dev-server/ssr-dev-server.impl.ts",
|
||||||
|
"schema": {
|
||||||
|
"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"],
|
||||||
|
"presets": []
|
||||||
|
},
|
||||||
|
"description": "Serve a SSR application.",
|
||||||
|
"aliases": [],
|
||||||
|
"hidden": false,
|
||||||
|
"path": "/packages/webpack/src/executors/ssr-dev-server/schema.json"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -300,7 +300,8 @@
|
|||||||
"remote",
|
"remote",
|
||||||
"cypress-component-configuration",
|
"cypress-component-configuration",
|
||||||
"component-test",
|
"component-test",
|
||||||
"setup-tailwind"
|
"setup-tailwind",
|
||||||
|
"setup-ssr"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -390,7 +391,7 @@
|
|||||||
"description": "The Nx Plugin for Webpack contains executors and generators that support building applications using Webpack.",
|
"description": "The Nx Plugin for Webpack contains executors and generators that support building applications using Webpack.",
|
||||||
"path": "generated/packages/webpack.json",
|
"path": "generated/packages/webpack.json",
|
||||||
"schemas": {
|
"schemas": {
|
||||||
"executors": ["webpack", "dev-server"],
|
"executors": ["webpack", "dev-server", "ssr-dev-server"],
|
||||||
"generators": ["init", "webpack-project"]
|
"generators": ["init", "webpack-project"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -23,7 +23,7 @@ describe('React Applications', () => {
|
|||||||
|
|
||||||
afterEach(() => cleanupProject());
|
afterEach(() => cleanupProject());
|
||||||
|
|
||||||
it('should be able to generate a react app + lib', async () => {
|
it('should be able to generate a react app + lib (with CSR and SSR)', async () => {
|
||||||
const appName = uniq('app');
|
const appName = uniq('app');
|
||||||
const libName = uniq('lib');
|
const libName = uniq('lib');
|
||||||
const libWithNoComponents = uniq('lib');
|
const libWithNoComponents = uniq('lib');
|
||||||
@ -60,6 +60,18 @@ describe('React Applications', () => {
|
|||||||
checkLinter: true,
|
checkLinter: true,
|
||||||
checkE2E: true,
|
checkE2E: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set up SSR and check app
|
||||||
|
runCLI(`generate @nrwl/react:setup-ssr ${appName}`);
|
||||||
|
checkFilesExist(`apps/${appName}/src/main.server.tsx`);
|
||||||
|
checkFilesExist(`apps/${appName}/server.ts`);
|
||||||
|
|
||||||
|
await testGeneratedApp(appName, {
|
||||||
|
checkSourceMap: false,
|
||||||
|
checkStyles: false,
|
||||||
|
checkLinter: false,
|
||||||
|
checkE2E: true,
|
||||||
|
});
|
||||||
}, 500000);
|
}, 500000);
|
||||||
|
|
||||||
it('should generate app with legacy-ie support', async () => {
|
it('should generate app with legacy-ie support', async () => {
|
||||||
@ -86,7 +98,7 @@ describe('React Applications', () => {
|
|||||||
checkFilesExist(...filesToCheck);
|
checkFilesExist(...filesToCheck);
|
||||||
|
|
||||||
expect(readFile(`dist/apps/${appName}/index.html`)).toContain(
|
expect(readFile(`dist/apps/${appName}/index.html`)).toContain(
|
||||||
`<script src="main.js" type="module"></script><script src="main.es5.js" nomodule defer></script>`
|
'<script src="main.js" type="module"></script><script src="main.es5.js" nomodule defer></script>'
|
||||||
);
|
);
|
||||||
}, 250_000);
|
}, 250_000);
|
||||||
|
|
||||||
@ -149,7 +161,7 @@ describe('React Applications', () => {
|
|||||||
|
|
||||||
if (opts.checkStyles) {
|
if (opts.checkStyles) {
|
||||||
expect(readFile(`dist/apps/${appName}/index.html`)).toContain(
|
expect(readFile(`dist/apps/${appName}/index.html`)).toContain(
|
||||||
`<link rel="stylesheet" href="styles.css">`
|
'<link rel="stylesheet" href="styles.css">'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import { normalizeOptions } from './lib/normalize';
|
|||||||
|
|
||||||
import { EsBuildExecutorOptions } from './schema';
|
import { EsBuildExecutorOptions } from './schema';
|
||||||
import { removeSync, writeJsonSync } from 'fs-extra';
|
import { removeSync, writeJsonSync } from 'fs-extra';
|
||||||
import { createAsyncIterable } from '@nrwl/js/src/utils/create-async-iterable/create-async-iteratable';
|
import { createAsyncIterable } from '@nrwl/js/src/utils/async-iterable/create-async-iterable';
|
||||||
import { buildEsbuildOptions } from './lib/build-esbuild-options';
|
import { buildEsbuildOptions } from './lib/build-esbuild-options';
|
||||||
import { getExtraDependencies } from './lib/get-extra-dependencies';
|
import { getExtraDependencies } from './lib/get-extra-dependencies';
|
||||||
import { DependentBuildableProjectNode } from '@nrwl/workspace/src/utilities/buildable-libs-utils';
|
import { DependentBuildableProjectNode } from '@nrwl/workspace/src/utilities/buildable-libs-utils';
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
export async function* combineAsyncIterators(
|
export async function* combineAsyncIterableIterators(
|
||||||
...iterators: { 0: AsyncIterableIterator<any> } & AsyncIterableIterator<any>[]
|
...iterators: { 0: AsyncIterableIterator<any> } & AsyncIterableIterator<any>[]
|
||||||
) {
|
) {
|
||||||
let [options] = iterators;
|
let [options] = iterators;
|
||||||
@ -48,31 +48,3 @@ function getNextAsyncIteratorFactory(options) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function* mapAsyncIterator<T = any, I = any, O = any>(
|
|
||||||
data: AsyncIterableIterator<T>,
|
|
||||||
transform: (input: I, index?: number, data?: AsyncIterableIterator<T>) => O
|
|
||||||
) {
|
|
||||||
async function* f() {
|
|
||||||
const generator = data[Symbol.asyncIterator] || data[Symbol.iterator];
|
|
||||||
const iterator = generator.call(data);
|
|
||||||
let index = 0;
|
|
||||||
let item = await iterator.next();
|
|
||||||
while (!item.done) {
|
|
||||||
yield await transform(await item.value, index, data);
|
|
||||||
index++;
|
|
||||||
item = await iterator.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return yield* f();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function* tapAsyncIterator<T = any, I = any, O = any>(
|
|
||||||
data: AsyncIterableIterator<T>,
|
|
||||||
fn: (input: I) => void
|
|
||||||
) {
|
|
||||||
return yield* mapAsyncIterator(data, (x) => {
|
|
||||||
fn(x);
|
|
||||||
return x;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
import { combineAsyncIterableIterators } from './combine-async-iteratable-iterators';
|
||||||
|
|
||||||
|
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 = combineAsyncIterableIterators(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 = combineAsyncIterableIterators(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/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { createAsyncIterable } from './create-async-iteratable';
|
import { createAsyncIterable } from './create-async-iterable';
|
||||||
|
|
||||||
describe(createAsyncIterable.name, () => {
|
describe(createAsyncIterable.name, () => {
|
||||||
test('simple callback', async () => {
|
test('simple callback', async () => {
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import { mapAsyncIterable } from './map-async-iteratable';
|
||||||
|
|
||||||
|
describe('mapAsyncIterator', () => {
|
||||||
|
it('should map over values', async () => {
|
||||||
|
async function* f() {
|
||||||
|
yield 1;
|
||||||
|
yield 2;
|
||||||
|
yield 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
const c = mapAsyncIterable(f(), (x) => x * 2);
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
for await (const x of c) {
|
||||||
|
results.push(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(results).toEqual([2, 4, 6]);
|
||||||
|
});
|
||||||
|
});
|
||||||
22
packages/js/src/utils/async-iterable/map-async-iteratable.ts
Normal file
22
packages/js/src/utils/async-iterable/map-async-iteratable.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
export async function* mapAsyncIterable<T = any, I = any, O = any>(
|
||||||
|
data: AsyncIterable<T> | AsyncIterableIterator<T>,
|
||||||
|
transform: (
|
||||||
|
input: I,
|
||||||
|
index?: number,
|
||||||
|
data?: AsyncIterable<T> | AsyncIterableIterator<T>
|
||||||
|
) => O
|
||||||
|
) {
|
||||||
|
async function* f() {
|
||||||
|
const generator = data[Symbol.asyncIterator] || data[Symbol.iterator];
|
||||||
|
const iterator = generator.call(data);
|
||||||
|
let index = 0;
|
||||||
|
let item = await iterator.next();
|
||||||
|
while (!item.done) {
|
||||||
|
yield await transform(await item.value, index, data);
|
||||||
|
index++;
|
||||||
|
item = await iterator.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return yield* f();
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { tapAsyncIterator } from './tap-async-iteratable';
|
||||||
|
|
||||||
|
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]);
|
||||||
|
});
|
||||||
|
});
|
||||||
11
packages/js/src/utils/async-iterable/tap-async-iteratable.ts
Normal file
11
packages/js/src/utils/async-iterable/tap-async-iteratable.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { mapAsyncIterable } from './map-async-iteratable';
|
||||||
|
|
||||||
|
export async function* tapAsyncIterator<T = any, I = any, O = any>(
|
||||||
|
data: AsyncIterable<T> | AsyncIterableIterator<T>,
|
||||||
|
fn: (input: I) => void
|
||||||
|
) {
|
||||||
|
return yield* mapAsyncIterable(data, (x) => {
|
||||||
|
fn(x);
|
||||||
|
return x;
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { cacheDir, ExecutorContext, logger } from '@nrwl/devkit';
|
import { cacheDir, ExecutorContext, logger } from '@nrwl/devkit';
|
||||||
import { exec, execSync } from 'child_process';
|
import { exec, execSync } from 'child_process';
|
||||||
import { removeSync } from 'fs-extra';
|
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 { NormalizedSwcExecutorOptions, SwcCliOptions } from '../schema';
|
||||||
import { printDiagnostics } from '../typescript/print-diagnostics';
|
import { printDiagnostics } from '../typescript/print-diagnostics';
|
||||||
import { runTypeCheck, TypeCheckOptions } from '../typescript/run-type-check';
|
import { runTypeCheck, TypeCheckOptions } from '../typescript/run-type-check';
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import {
|
|||||||
TypeScriptCompilationOptions,
|
TypeScriptCompilationOptions,
|
||||||
} from '@nrwl/workspace/src/utilities/typescript/compilation';
|
} from '@nrwl/workspace/src/utilities/typescript/compilation';
|
||||||
import type { Diagnostic } from 'typescript';
|
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';
|
import { NormalizedExecutorOptions } from '../schema';
|
||||||
|
|
||||||
const TYPESCRIPT_FOUND_N_ERRORS_WATCHING_FOR_FILE_CHANGES = 6194;
|
const TYPESCRIPT_FOUND_N_ERRORS_WATCHING_FOR_FILE_CHANGES = 6194;
|
||||||
|
|||||||
@ -95,6 +95,13 @@
|
|||||||
"schema": "./src/generators/setup-tailwind/schema.json",
|
"schema": "./src/generators/setup-tailwind/schema.json",
|
||||||
"description": "Set up Tailwind configuration for a project.",
|
"description": "Set up Tailwind configuration for a project.",
|
||||||
"hidden": false
|
"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": {
|
"generators": {
|
||||||
@ -204,6 +211,13 @@
|
|||||||
"schema": "./src/generators/setup-tailwind/schema.json",
|
"schema": "./src/generators/setup-tailwind/schema.json",
|
||||||
"description": "Set up Tailwind configuration for a project.",
|
"description": "Set up Tailwind configuration for a project.",
|
||||||
"hidden": false
|
"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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 { WebDevServerOptions } from '@nrwl/webpack/src/executors/dev-server/schema';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import * as chalk from 'chalk';
|
import * as chalk from 'chalk';
|
||||||
import {
|
import { combineAsyncIterableIterators } from '@nrwl/js/src/utils/async-iterable/combine-async-iteratable-iterators';
|
||||||
combineAsyncIterators,
|
import { tapAsyncIterator } from '@nrwl/js/src/utils/async-iterable/tap-async-iteratable';
|
||||||
tapAsyncIterator,
|
|
||||||
} from '../../utils/async-iterator';
|
|
||||||
|
|
||||||
type ModuleFederationDevServerOptions = WebDevServerOptions & {
|
type ModuleFederationDevServerOptions = WebDevServerOptions & {
|
||||||
devRemotes?: string | string[];
|
devRemotes?: string | string[];
|
||||||
@ -50,7 +48,7 @@ export default async function* moduleFederationDevServer(
|
|||||||
for (const app of knownRemotes) {
|
for (const app of knownRemotes) {
|
||||||
const [appName] = Array.isArray(app) ? app : [app];
|
const [appName] = Array.isArray(app) ? app : [app];
|
||||||
const isDev = devServeApps.includes(appName);
|
const isDev = devServeApps.includes(appName);
|
||||||
iter = combineAsyncIterators(
|
iter = combineAsyncIterableIterators(
|
||||||
iter,
|
iter,
|
||||||
await runExecutor(
|
await runExecutor(
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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);
|
||||||
@ -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(`<div id="root"></div>`);
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
<App />,
|
||||||
|
{
|
||||||
|
[callbackName]() {
|
||||||
|
res.statusCode = didError ? 500 : 200;
|
||||||
|
res.setHeader('Content-type', 'text/html; charset=utf-8');
|
||||||
|
res.write(`${htmlStart}<div id="root">`);
|
||||||
|
stream.pipe(res);
|
||||||
|
res.write(`</div>${htmlEnd}`);
|
||||||
|
},
|
||||||
|
onShellError(error) {
|
||||||
|
console.error(error);
|
||||||
|
res.statusCode = 500;
|
||||||
|
res.send('<!doctype html><h1>Server Error</h1>');
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
didError = true;
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.app.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../out-tsc/server",
|
||||||
|
"target": "es2019",
|
||||||
|
"types": [
|
||||||
|
"node"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/main.server.tsx",
|
||||||
|
"server.ts",
|
||||||
|
]
|
||||||
|
}
|
||||||
6
packages/react/src/generators/setup-ssr/schema.d.ts
vendored
Normal file
6
packages/react/src/generators/setup-ssr/schema.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export interface Schema {
|
||||||
|
project: string;
|
||||||
|
appComponentImportPath: string;
|
||||||
|
serverPort?: number;
|
||||||
|
skipFormat?: boolean;
|
||||||
|
}
|
||||||
36
packages/react/src/generators/setup-ssr/schema.json
Normal file
36
packages/react/src/generators/setup-ssr/schema.json
Normal file
@ -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 <App/> 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
|
||||||
|
}
|
||||||
175
packages/react/src/generators/setup-ssr/setup-ssr.ts
Normal file
175
packages/react/src/generators/setup-ssr/setup-ssr.ts
Normal file
@ -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 <App/> 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);
|
||||||
@ -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]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -40,3 +40,7 @@ export const tsLibVersion = '^2.3.0';
|
|||||||
export const postcssVersion = '8.4.16';
|
export const postcssVersion = '8.4.16';
|
||||||
export const tailwindcssVersion = '3.1.8';
|
export const tailwindcssVersion = '3.1.8';
|
||||||
export const autoprefixerVersion = '10.4.12';
|
export const autoprefixerVersion = '10.4.12';
|
||||||
|
|
||||||
|
export const expressVersion = '^4.18.1';
|
||||||
|
export const typesExpressVersion = '4.17.13';
|
||||||
|
export const isbotVersion = '^3.6.5';
|
||||||
|
|||||||
@ -9,6 +9,11 @@
|
|||||||
"implementation": "./src/executors/dev-server/compat",
|
"implementation": "./src/executors/dev-server/compat",
|
||||||
"schema": "./src/executors/dev-server/schema.json",
|
"schema": "./src/executors/dev-server/schema.json",
|
||||||
"description": "Serve a web application."
|
"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": {
|
"executors": {
|
||||||
@ -21,6 +26,11 @@
|
|||||||
"implementation": "./src/executors/dev-server/dev-server.impl",
|
"implementation": "./src/executors/dev-server/dev-server.impl",
|
||||||
"schema": "./src/executors/dev-server/schema.json",
|
"schema": "./src/executors/dev-server/schema.json",
|
||||||
"description": "Serve a web application."
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
packages/webpack/src/executors/ssr-dev-server/compat.ts
Normal file
5
packages/webpack/src/executors/ssr-dev-server/compat.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { convertNxExecutor } from '@nrwl/devkit';
|
||||||
|
|
||||||
|
import ssrDevServerExecutor from './ssr-dev-server.impl';
|
||||||
|
|
||||||
|
export default convertNxExecutor(ssrDevServerExecutor);
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
import * as net from 'net';
|
||||||
|
|
||||||
|
export function waitUntilServerIsListening(port: number): Promise<void> {
|
||||||
|
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<void>((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();
|
||||||
|
});
|
||||||
|
}
|
||||||
11
packages/webpack/src/executors/ssr-dev-server/schema.d.ts
vendored
Normal file
11
packages/webpack/src/executors/ssr-dev-server/schema.d.ts
vendored
Normal file
@ -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;
|
||||||
|
}
|
||||||
34
packages/webpack/src/executors/ssr-dev-server/schema.json
Normal file
34
packages/webpack/src/executors/ssr-dev-server/schema.json
Normal file
@ -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"]
|
||||||
|
}
|
||||||
@ -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<WebpackExecutorOptions>(
|
||||||
|
browserTarget,
|
||||||
|
context
|
||||||
|
);
|
||||||
|
const serverOptions = readTargetOptions<WebpackExecutorOptions>(
|
||||||
|
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;
|
||||||
@ -91,11 +91,16 @@ async function getWebpackConfigs(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type WebpackExecutorEvent =
|
export type WebpackExecutorEvent =
|
||||||
| { success: false; outfile?: string }
|
| {
|
||||||
|
success: false;
|
||||||
|
outfile?: string;
|
||||||
|
options?: WebpackExecutorOptions;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
success: true;
|
success: true;
|
||||||
outfile: string;
|
outfile: string;
|
||||||
emittedFiles: EmittedFile[];
|
emittedFiles: EmittedFile[];
|
||||||
|
options?: WebpackExecutorOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function* webpackExecutor(
|
export async function* webpackExecutor(
|
||||||
@ -129,6 +134,7 @@ export async function* webpackExecutor(
|
|||||||
options.outputPath,
|
options.outputPath,
|
||||||
options.outputFileName
|
options.outputFileName
|
||||||
),
|
),
|
||||||
|
options,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -204,6 +210,7 @@ export async function* webpackExecutor(
|
|||||||
options.outputFileName
|
options.outputFileName
|
||||||
),
|
),
|
||||||
emittedFiles: [...emittedFiles1, ...emittedFiles2],
|
emittedFiles: [...emittedFiles1, ...emittedFiles2],
|
||||||
|
options,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user