Skip to content

Commit

Permalink
feat(react): add SSR support to React apps (#13234)
Browse files Browse the repository at this point in the history
  • Loading branch information
jaysoo committed Nov 21, 2022
1 parent f394608 commit 23e4fc7
Show file tree
Hide file tree
Showing 32 changed files with 741 additions and 144 deletions.
43 changes: 43 additions & 0 deletions docs/generated/packages/react.json
Expand Up @@ -1391,6 +1391,49 @@
"implementation": "/packages/react/src/generators/setup-tailwind/setup-tailwind#setupTailwindGenerator.ts",
"aliases": [],
"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": [
Expand Down
43 changes: 43 additions & 0 deletions docs/generated/packages/webpack.json
Expand Up @@ -785,6 +785,49 @@
"aliases": [],
"hidden": false,
"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"
}
]
}
5 changes: 3 additions & 2 deletions docs/packages.json
Expand Up @@ -300,7 +300,8 @@
"remote",
"cypress-component-configuration",
"component-test",
"setup-tailwind"
"setup-tailwind",
"setup-ssr"
]
}
},
Expand Down Expand Up @@ -390,7 +391,7 @@
"description": "The Nx Plugin for Webpack contains executors and generators that support building applications using Webpack.",
"path": "generated/packages/webpack.json",
"schemas": {
"executors": ["webpack", "dev-server"],
"executors": ["webpack", "dev-server", "ssr-dev-server"],
"generators": ["init", "webpack-project"]
}
},
Expand Down
18 changes: 15 additions & 3 deletions e2e/react/src/react.test.ts
Expand Up @@ -23,7 +23,7 @@ describe('React Applications', () => {

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 libName = uniq('lib');
const libWithNoComponents = uniq('lib');
Expand Down Expand Up @@ -60,6 +60,18 @@ describe('React Applications', () => {
checkLinter: 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);

it('should generate app with legacy-ie support', async () => {
Expand All @@ -86,7 +98,7 @@ describe('React Applications', () => {
checkFilesExist(...filesToCheck);

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);

Expand Down Expand Up @@ -149,7 +161,7 @@ describe('React Applications', () => {

if (opts.checkStyles) {
expect(readFile(`dist/apps/${appName}/index.html`)).toContain(
`<link rel="stylesheet" href="styles.css">`
'<link rel="stylesheet" href="styles.css">'
);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/esbuild/src/executors/esbuild/esbuild.impl.ts
Expand Up @@ -15,7 +15,7 @@ import { normalizeOptions } from './lib/normalize';

import { EsBuildExecutorOptions } from './schema';
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 { getExtraDependencies } from './lib/get-extra-dependencies';
import { DependentBuildableProjectNode } from '@nrwl/workspace/src/utilities/buildable-libs-utils';
Expand Down
@@ -1,4 +1,4 @@
export async function* combineAsyncIterators(
export async function* combineAsyncIterableIterators(
...iterators: { 0: AsyncIterableIterator<any> } & AsyncIterableIterator<any>[]
) {
let [options] = iterators;
Expand Down Expand Up @@ -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, () => {
test('simple callback', async () => {
Expand Down
20 changes: 20 additions & 0 deletions packages/js/src/utils/async-iterable/map-async-iteratable.spec.ts
@@ -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 changes: 22 additions & 0 deletions packages/js/src/utils/async-iterable/map-async-iteratable.ts
@@ -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();
}
25 changes: 25 additions & 0 deletions packages/js/src/utils/async-iterable/tap-async-iteratable.spec.ts
@@ -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 changes: 11 additions & 0 deletions packages/js/src/utils/async-iterable/tap-async-iteratable.ts
@@ -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;
});
}
2 changes: 1 addition & 1 deletion 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';
Expand Down
Expand Up @@ -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;
Expand Down
14 changes: 14 additions & 0 deletions packages/react/generators.json
Expand Up @@ -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": {
Expand Down Expand Up @@ -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
}
}
}

1 comment on commit 23e4fc7

@vercel
Copy link

@vercel vercel bot commented on 23e4fc7 Nov 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

nx-dev – ./

nx-dev-nrwl.vercel.app
nx-dev-git-master-nrwl.vercel.app
nx.dev
nx-five.vercel.app

Please sign in to comment.