@teambit/react 0.0.802 → 0.0.803

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/apps/web/react-app-options.ts +3 -3
  2. package/apps/web/react.app-type.ts +5 -2
  3. package/apps/web/react.application.ts +117 -23
  4. package/apps/web/ssr/ssr-express.ts +81 -0
  5. package/apps/web/webpack/mutators.ts +20 -0
  6. package/apps/web/webpack/webpack.app.ssr.config.ts +37 -0
  7. package/artifacts/__bit_junit.xml +2 -2
  8. package/artifacts/env-template/public/{697.d653451cb84e67a5a1e6.js → 994.bb73ab7fa3ec9ae9dc3b.js} +2 -2
  9. package/artifacts/env-template/public/assets-manifest.json +12 -12
  10. package/artifacts/env-template/public/{compositions.4f32d45facc10b788b84.js → compositions.0ddd91d78bab5438b2a3.js} +1 -1
  11. package/artifacts/env-template/public/compositions.html +1 -1
  12. package/artifacts/env-template/public/{overview.3b7b230063483eef660c.js → overview.952a5d7fc353f1eb661a.js} +1 -1
  13. package/artifacts/env-template/public/overview.html +1 -1
  14. package/artifacts/env-template/public/{peers.185e5d6ee27c15e8a771.js → peers.c5645a5909148532bb95.js} +1 -1
  15. package/artifacts/env-template/public/{preview-root.291f6f0c253a631863cb.js → preview-root.3a0d522c80063747b87d.js} +1 -1
  16. package/artifacts/env-template/public/static/css/{697.90cfc900.css → 994.90cfc900.css} +0 -0
  17. package/artifacts/preview/teambit_react_react-preview.js +2 -2
  18. package/{compositions-1659151732866.js → compositions-1659271062546.js} +0 -0
  19. package/dist/apps/web/react-app-options.d.ts +3 -3
  20. package/dist/apps/web/react-app-options.js.map +1 -1
  21. package/dist/apps/web/react.app-type.d.ts +3 -1
  22. package/dist/apps/web/react.app-type.js +3 -2
  23. package/dist/apps/web/react.app-type.js.map +1 -1
  24. package/dist/apps/web/react.application.d.ts +11 -4
  25. package/dist/apps/web/react.application.js +134 -14
  26. package/dist/apps/web/react.application.js.map +1 -1
  27. package/dist/apps/web/ssr/ssr-express.d.ts +15 -0
  28. package/dist/apps/web/ssr/ssr-express.js +151 -0
  29. package/dist/apps/web/ssr/ssr-express.js.map +1 -0
  30. package/dist/apps/web/webpack/mutators.d.ts +3 -0
  31. package/dist/apps/web/webpack/mutators.js +27 -0
  32. package/dist/apps/web/webpack/mutators.js.map +1 -0
  33. package/dist/apps/web/webpack/webpack.app.ssr.config.d.ts +15 -0
  34. package/dist/apps/web/webpack/webpack.app.ssr.config.js +62 -0
  35. package/dist/apps/web/webpack/webpack.app.ssr.config.js.map +1 -0
  36. package/dist/react.main.runtime.js +1 -1
  37. package/dist/react.main.runtime.js.map +1 -1
  38. package/{overview-1659151732866.js → overview-1659271062546.js} +0 -0
  39. package/package-tar/teambit-react-0.0.803.tgz +0 -0
  40. package/package.json +38 -31
  41. package/{preview-1659151732866.js → preview-1659271062546.js} +2 -2
  42. package/package-tar/teambit-react-0.0.802.tgz +0 -0
@@ -73,9 +73,9 @@ export type ReactAppOptions = {
73
73
  entry: string[] | (() => Promise<string[]>);
74
74
 
75
75
  /**
76
- * use server-side rendering for the app.
76
+ * path to server-rendered entrypoint of the app
77
77
  */
78
- ssr?: boolean;
78
+ ssr?: string | (() => Promise<string>);
79
79
 
80
80
  /**
81
81
  * instance of bundler to use. default is Webpack.
@@ -108,7 +108,7 @@ export type ReactAppOptions = {
108
108
  /**
109
109
  * ranges of ports to use to run the app server.
110
110
  */
111
- portRange?: number[];
111
+ portRange?: [start: number, end: number];
112
112
 
113
113
  /**
114
114
  * favicon for the app. You can pass an abs path (using require.resolve()) or a url.
@@ -1,15 +1,17 @@
1
1
  import { ApplicationType } from '@teambit/application';
2
+ import type { Logger } from '@teambit/logger';
2
3
  import { ReactAppOptions } from './react-app-options';
3
4
  import { ReactApp } from './react.application';
4
5
  import { ReactEnv } from '../../react.env';
5
6
 
6
7
  export class ReactAppType implements ApplicationType<ReactAppOptions> {
7
- constructor(readonly name: string, private reactEnv: ReactEnv) {}
8
+ constructor(readonly name: string, private reactEnv: ReactEnv, private logger?: Logger) {}
8
9
 
9
10
  createApp(options: ReactAppOptions) {
10
11
  return new ReactApp(
11
12
  options.name,
12
13
  options.entry,
14
+ options.ssr,
13
15
  options.portRange || [3000, 4000],
14
16
  this.reactEnv,
15
17
  options.prerender,
@@ -17,7 +19,8 @@ export class ReactAppType implements ApplicationType<ReactAppOptions> {
17
19
  options.devServer,
18
20
  options.webpackTransformers,
19
21
  options.deploy,
20
- options.favicon
22
+ options.favicon,
23
+ this.logger
21
24
  );
22
25
  }
23
26
  }
@@ -1,10 +1,12 @@
1
- import { join, basename } from 'path';
2
- import { Application, AppContext, AppBuildContext } from '@teambit/application';
3
- import { Bundler, DevServer, BundlerContext, DevServerContext, BundlerHtmlConfig } from '@teambit/bundler';
1
+ import { join, resolve, basename } from 'path';
2
+ import { Application, AppContext, AppBuildContext, AppResult } from '@teambit/application';
3
+ import type { Bundler, DevServer, BundlerContext, DevServerContext, BundlerHtmlConfig } from '@teambit/bundler';
4
4
  import { Port } from '@teambit/toolbox.network.get-port';
5
+ import type { Logger } from '@teambit/logger';
5
6
  import { remove } from 'lodash';
6
7
  import TerserPlugin from 'terser-webpack-plugin';
7
8
  import { WebpackConfigTransformer } from '@teambit/webpack';
9
+ import { BitError } from '@teambit/bit-error';
8
10
  import { ReactEnv } from '../../react.env';
9
11
  import { prerenderPlugin } from './plugins';
10
12
  import { ReactAppBuildResult } from './react-build-result';
@@ -12,19 +14,24 @@ import { ReactAppPrerenderOptions } from './react-app-options';
12
14
  import { html } from '../../webpack';
13
15
  import { ReactDeployContext } from '.';
14
16
  import { computeResults } from './compute-results';
17
+ import { clientConfig, ssrConfig, calcOutputPath } from './webpack/webpack.app.ssr.config';
18
+ import { addDevServer, setOutput } from './webpack/mutators';
19
+ import { createExpressSsr, loadSsrApp, parseAssets } from './ssr/ssr-express';
15
20
 
16
21
  export class ReactApp implements Application {
17
22
  constructor(
18
23
  readonly name: string,
19
24
  readonly entry: string[] | (() => Promise<string[]>),
20
- readonly portRange: number[],
25
+ readonly ssr: string | (() => Promise<string>) | undefined,
26
+ readonly portRange: [number, number],
21
27
  private reactEnv: ReactEnv,
22
28
  readonly prerender?: ReactAppPrerenderOptions,
23
29
  readonly bundler?: Bundler,
24
30
  readonly devServer?: DevServer,
25
31
  readonly transformers: WebpackConfigTransformer[] = [],
26
32
  readonly deploy?: (context: ReactDeployContext) => Promise<void>,
27
- readonly favicon?: string
33
+ readonly favicon?: string,
34
+ private logger?: Logger
28
35
  ) {}
29
36
  readonly applicationType = 'react-common-js';
30
37
  readonly dir = 'public';
@@ -32,32 +39,112 @@ export class ReactApp implements Application {
32
39
  async run(context: AppContext): Promise<number> {
33
40
  const [from, to] = this.portRange;
34
41
  const port = await Port.getPort(from, to);
42
+
35
43
  if (this.devServer) {
36
44
  await this.devServer.listen(port);
37
45
  return port;
38
46
  }
47
+
39
48
  const devServerContext = await this.getDevServerContext(context);
40
- const devServer = this.reactEnv.getDevServer(devServerContext, [
41
- (configMutator) =>
42
- configMutator.addTopLevel('devServer', {
43
- historyApiFallback: {
44
- index: '/index.html',
45
- disableDotRule: true,
46
- headers: {
47
- 'Access-Control-Allow-Headers': '*',
48
- },
49
- },
50
- }),
51
- (configMutator) => {
52
- if (!configMutator.raw.output) configMutator.raw.output = {};
53
- configMutator.raw.output.publicPath = '/';
49
+ const devServer = this.reactEnv.getDevServer(devServerContext, [addDevServer, setOutput, ...this.transformers]);
50
+ await devServer.listen(port);
51
+ return port;
52
+ }
53
+
54
+ async runSsr(context: AppContext): Promise<AppResult> {
55
+ const [from, to] = this.portRange;
56
+ const port = await Port.getPort(from, to);
57
+
58
+ // bundle client
59
+ const clientBundle = await this.buildClient(context);
60
+ if (clientBundle.errors.length > 0) return { errors: clientBundle.errors };
61
+ this.logger?.info('[react.application] [ssr] client bundle - complete');
62
+
63
+ // bundle server
64
+ const serverBundle = await this.buildSsr(context);
65
+ if (serverBundle.errors.length > 0) return { errors: serverBundle.errors };
66
+ this.logger?.info('[react.application] [ssr] server bundle - complete');
67
+
68
+ // load server-side runtime
69
+ const app = await loadSsrApp(context.workdir, context.appName);
70
+ this.logger?.info('[react.application] [ssr] bundle code - loaded');
71
+
72
+ const expressApp = createExpressSsr({
73
+ name: context.appName,
74
+ workdir: context.workdir,
75
+ port,
76
+ app,
77
+ assets: parseAssets(clientBundle.assets),
78
+ logger: this.logger,
79
+ });
54
80
 
55
- return configMutator;
81
+ expressApp.listen(port);
82
+ return { port };
83
+ }
84
+
85
+ private async buildClient(context: AppContext) {
86
+ const htmlConfig: BundlerHtmlConfig[] = [
87
+ {
88
+ title: context.appName,
89
+ templateContent: html(context.appName),
90
+ minify: false,
91
+ favicon: this.favicon,
56
92
  },
93
+ ];
94
+
95
+ // extend, including prototype methods
96
+ const ctx: BundlerContext = Object.assign(Object.create(context), {
97
+ html: htmlConfig,
98
+ targets: [
99
+ {
100
+ entries: await this.getEntries(),
101
+ components: [context.appComponent],
102
+ outputPath: resolve(context.workdir, calcOutputPath(context.appName, 'browser')),
103
+ hostDependencies: await this.getPeers(),
104
+ aliasHostDependencies: true,
105
+ },
106
+ ],
107
+
108
+ // @ts-ignore
109
+ capsuleNetwork: undefined,
110
+ previousTasksResults: [],
111
+ });
112
+
113
+ const bundler = await this.reactEnv.getBundler(ctx, [
114
+ (config) => config.merge([clientConfig()]),
57
115
  ...this.transformers,
58
116
  ]);
59
- await devServer.listen(port);
60
- return port;
117
+
118
+ const bundleResult = await bundler.run();
119
+ return bundleResult[0];
120
+ }
121
+
122
+ private async buildSsr(context: AppContext) {
123
+ // extend, including prototype methods
124
+ const ctx: BundlerContext = Object.assign(Object.create(context), {
125
+ ...context,
126
+ targets: [
127
+ {
128
+ entries: await this.getSsrEntries(),
129
+ components: [context.appComponent],
130
+ outputPath: resolve(context.workdir, calcOutputPath(context.appName, 'ssr')),
131
+ hostDependencies: await this.getPeers(),
132
+ aliasHostDependencies: true,
133
+ },
134
+ ],
135
+
136
+ // @ts-ignore
137
+ capsuleNetwork: undefined,
138
+ previousTasksResults: [],
139
+ });
140
+
141
+ const bundler = await this.reactEnv.getBundler(ctx, [
142
+ (config) => config.merge([ssrConfig()]),
143
+ ...this.transformers,
144
+ ]);
145
+
146
+ const bundleResult = await bundler.run();
147
+ return bundleResult[0];
61
148
  }
62
149
 
63
150
  async build(context: AppBuildContext): Promise<ReactAppBuildResult> {
@@ -85,7 +172,7 @@ export class ReactApp implements Application {
85
172
  }
86
173
  private async getDefaultBundler(context: AppBuildContext) {
87
174
  const { capsule } = context;
88
- const reactEnv: ReactEnv = context.env;
175
+ const reactEnv: ReactEnv = context.env as ReactEnv;
89
176
  const publicDir = this.getPublicDir(context.artifactsDir);
90
177
  const outputPath = join(capsule.path, publicDir);
91
178
  const { distDir } = reactEnv.getCompiler();
@@ -99,6 +186,7 @@ export class ReactApp implements Application {
99
186
  components: [capsule?.component],
100
187
  entries,
101
188
  outputPath,
189
+ hostRootDir: capsule?.path,
102
190
  hostDependencies: await this.getPeers(),
103
191
  aliasHostDependencies: true,
104
192
  },
@@ -194,6 +282,12 @@ export class ReactApp implements Application {
194
282
  return this.entry();
195
283
  }
196
284
 
285
+ async getSsrEntries(): Promise<string[]> {
286
+ if (!this.ssr) throw new BitError('tried to build ssr without ssr entries');
287
+ if (typeof this.ssr === 'string') return [this.ssr];
288
+ return [await this.ssr()];
289
+ }
290
+
197
291
  private async getDevServerContext(context: AppContext): Promise<DevServerContext> {
198
292
  const entries = await this.getEntries();
199
293
  return Object.assign(context, {
@@ -0,0 +1,81 @@
1
+ import { Asset } from '@teambit/bundler';
2
+ import { Logger } from '@teambit/logger';
3
+ import { serverError } from '@teambit/ui-foundation.ui.pages.static-error';
4
+ import { browserFromExpress } from '@teambit/react.rendering.ssr';
5
+ import type { HtmlAssets, SsrSession } from '@teambit/react.rendering.ssr';
6
+
7
+ import Express from 'express';
8
+ import * as fs from 'fs-extra';
9
+ import { resolve } from 'path';
10
+ import urlJoin from 'url-join';
11
+
12
+ import { calcOutputPath, PUBLIC_PATH, SSR_ENTRY_FILE } from '../webpack/webpack.app.ssr.config';
13
+
14
+ const MAGIC_FOLDER = 'public'; // idk where this is comping from
15
+
16
+ type ExpressSsrOptions = {
17
+ name: string;
18
+ workdir: string;
19
+ port: number;
20
+ app: any;
21
+ assets: HtmlAssets;
22
+ logger?: Logger;
23
+ };
24
+
25
+ export function createExpressSsr({ name, workdir, port, app, assets, logger }: ExpressSsrOptions) {
26
+ const express = Express();
27
+
28
+ const publicFolder = resolve(workdir, calcOutputPath(name, 'browser'), MAGIC_FOLDER);
29
+
30
+ express.use(PUBLIC_PATH, Express.static(publicFolder));
31
+ express.use((request, response, next) => {
32
+ if (request.query._rendering !== 'client') {
33
+ next();
34
+ return;
35
+ }
36
+ response.sendFile(resolve(workdir, calcOutputPath(name, 'browser'), MAGIC_FOLDER, 'index.html'));
37
+ });
38
+ express.use(async (request, response) => {
39
+ logger?.info(`[react.application] [ssr] handling "${request.url}"`);
40
+ const browser = browserFromExpress(request, port);
41
+ const session: SsrSession = { assets, browser, request, response };
42
+
43
+ try {
44
+ // the app itself controls the response
45
+ await app(session);
46
+ logger?.console(`[react.application] [ssr] success "${request.url}"`);
47
+ } catch (error: any) {
48
+ logger?.consoleError(`[react.application] [ssr] error at "${request.url}"`, error);
49
+ response.status(500).send(serverError());
50
+ }
51
+ });
52
+
53
+ return express;
54
+ }
55
+
56
+ export async function loadSsrApp(workdir: string, appName: string) {
57
+ const entryFile = resolve(workdir, calcOutputPath(appName, 'ssr'), MAGIC_FOLDER, SSR_ENTRY_FILE);
58
+ if (!fs.existsSync(entryFile)) throw new Error(`expected ssr bundle entry file at "${entryFile}"`);
59
+
60
+ const entry = await import(entryFile);
61
+ const app = entry?.default;
62
+ if (!app) throw new Error(`bundle entry file has no default export (at "${entryFile}")`);
63
+
64
+ return app;
65
+ }
66
+
67
+ export function parseAssets(assets: Asset[], publicPath = PUBLIC_PATH): HtmlAssets {
68
+ const deadAssets = assets.filter((x) => !x.name);
69
+ if (deadAssets.length > 0) throw new Error('missing some build assets (maybe need to turn on cachedAssets, etc)');
70
+
71
+ return {
72
+ css: assets
73
+ .map((x) => x.name)
74
+ .filter((name) => name?.endsWith('.css'))
75
+ .map((name) => urlJoin(publicPath, name)),
76
+ js: assets
77
+ .map((x) => x.name)
78
+ .filter((name) => name?.endsWith('.js'))
79
+ .map((name) => urlJoin(publicPath, name)),
80
+ };
81
+ }
@@ -0,0 +1,20 @@
1
+ import { WebpackConfigMutator } from '@teambit/webpack';
2
+
3
+ export function addDevServer(configMutator: WebpackConfigMutator) {
4
+ return configMutator.addTopLevel('devServer', {
5
+ historyApiFallback: {
6
+ index: '/index.html',
7
+ disableDotRule: true,
8
+ },
9
+ headers: {
10
+ 'Access-Control-Allow-Origin': '*',
11
+ },
12
+ });
13
+ }
14
+
15
+ export function setOutput(configMutator: WebpackConfigMutator) {
16
+ if (!configMutator.raw.output) configMutator.raw.output = {};
17
+ configMutator.raw.output.publicPath = '/';
18
+
19
+ return configMutator;
20
+ }
@@ -0,0 +1,37 @@
1
+ import { join } from 'path';
2
+ import type { Configuration } from 'webpack';
3
+
4
+ const OUTPUT_FOLDER = 'public/bit-apps';
5
+ export const SSR_ENTRY_FILE = 'index.js';
6
+ /** _external_ base url for static files. (is not a folder)
7
+ * @example
8
+ * "http://localhost:3000/public/main.88a88d47d86aaabc0230.js"
9
+ */
10
+ export const PUBLIC_PATH = '/public';
11
+
12
+ enum Targets {
13
+ ssr = 'ssr',
14
+ browser = 'browser',
15
+ }
16
+
17
+ export function calcOutputPath(appName: string, target: Targets | keyof typeof Targets) {
18
+ return join(OUTPUT_FOLDER, appName, target);
19
+ }
20
+
21
+ export function clientConfig(): Configuration {
22
+ return {
23
+ output: { publicPath: PUBLIC_PATH, filename: '[name].[chunkhash].js' },
24
+ };
25
+ }
26
+
27
+ export function ssrConfig(): Configuration {
28
+ return {
29
+ target: 'node',
30
+ devtool: 'eval-cheap-source-map',
31
+
32
+ output: {
33
+ libraryTarget: 'commonjs',
34
+ filename: SSR_ENTRY_FILE,
35
+ },
36
+ };
37
+ }
@@ -1,6 +1,6 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
2
  <testsuites>
3
- <testsuite name="teambit.react/react@0.0.802" timestamp="2022-07-30T03:49:48.838Z" tests="1" failures="0" errors="0" skipped="0">
4
- <testcase classname="index-file.spec.js" name="should match snapshot" time="0.02"/>
3
+ <testsuite name="teambit.react/react@0.0.803" timestamp="2022-07-31T12:58:27.264Z" tests="1" failures="0" errors="0" skipped="0">
4
+ <testcase classname="index-file.spec.js" name="should match snapshot" time="0.186"/>
5
5
  </testsuite>
6
6
  </testsuites>