@teambit/ui 0.0.568 → 0.0.569
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.
- package/compose.tsx +27 -0
- package/events/index.ts +1 -0
- package/events/ui-server-started-event.ts +14 -0
- package/exceptions/index.ts +2 -0
- package/exceptions/unknown-build-error.ts +5 -0
- package/exceptions/unknown-ui.ts +9 -0
- package/package-tar/teambit-ui-0.0.569.tgz +0 -0
- package/package.json +24 -39
- package/render-lifecycle.tsx +56 -0
- package/ssr/render-middleware.ts +129 -0
- package/ssr/request-browser.ts +86 -0
- package/ssr/request-server.ts +10 -0
- package/ssr/ssr-content.ts +9 -0
- package/start.cmd.tsx +93 -0
- package/types/asset.d.ts +29 -0
- package/types/style.d.ts +42 -0
- package/ui/client-context.tsx +28 -0
- package/ui-build.cmd.tsx +27 -0
- package/ui-root.tsx +59 -0
- package/ui.cli.rt.tsx +10 -0
- package/ui.ui.runtime.tsx +277 -0
- package/webpack/html.ts +24 -0
- package/webpack/postcss.config.ts +22 -0
- package/webpack/webpack.base.config.ts +392 -0
- package/webpack/webpack.browser.config.ts +125 -0
- package/webpack/webpack.dev.config.ts +338 -0
- package/webpack/webpack.ssr.config.ts +38 -0
package/ui-build.cmd.tsx
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Command } from '@teambit/cli';
|
|
2
|
+
import { UnknownBuildError } from './exceptions';
|
|
3
|
+
|
|
4
|
+
import { UiMain } from './ui.main.runtime';
|
|
5
|
+
|
|
6
|
+
export class UIBuildCmd implements Command {
|
|
7
|
+
name = 'ui-build [type]';
|
|
8
|
+
description = 'build production assets for deployment.';
|
|
9
|
+
alias = 'c';
|
|
10
|
+
group = 'development';
|
|
11
|
+
shortDescription = '';
|
|
12
|
+
options = [];
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
/**
|
|
16
|
+
* access to the extension instance.
|
|
17
|
+
*/
|
|
18
|
+
private ui: UiMain
|
|
19
|
+
) {}
|
|
20
|
+
|
|
21
|
+
async report([type]: [string]): Promise<string> {
|
|
22
|
+
// teambit.workspace/variants should be the one to take care of component patterns.
|
|
23
|
+
const stats = await this.ui.build(type);
|
|
24
|
+
if (!stats) throw new UnknownBuildError();
|
|
25
|
+
return stats.toString();
|
|
26
|
+
}
|
|
27
|
+
}
|
package/ui-root.tsx
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { AspectDefinition } from '@teambit/aspect-loader';
|
|
2
|
+
import { ComponentDir } from '@teambit/bundler';
|
|
3
|
+
import { Component } from '@teambit/component';
|
|
4
|
+
import { ProxyConfigArrayItem } from 'webpack-dev-server';
|
|
5
|
+
|
|
6
|
+
// TODO: remove this extends "ComponentDir", this should be part of the workspace alone since scope
|
|
7
|
+
// would never have componentDir and as it has nothing to do with `UIRoot`.
|
|
8
|
+
export interface UIRoot extends ComponentDir {
|
|
9
|
+
/**
|
|
10
|
+
* unique name of the ui.
|
|
11
|
+
*/
|
|
12
|
+
name: string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* path of the ui root.
|
|
16
|
+
*/
|
|
17
|
+
path: string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* name of the UI root config file.
|
|
21
|
+
*/
|
|
22
|
+
configFile: string;
|
|
23
|
+
|
|
24
|
+
buildOptions?: {
|
|
25
|
+
ssr?: boolean;
|
|
26
|
+
launchBrowserOnStart?: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* resolve all aspects in the UI root.
|
|
31
|
+
*/
|
|
32
|
+
resolveAspects(runtimeName: string): Promise<AspectDefinition[]>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* resolve components from a given pattern.
|
|
36
|
+
*/
|
|
37
|
+
resolvePattern?(pattern: string): Promise<Component[]>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* listener for when the dev server starts. can be used for running the watcher.
|
|
41
|
+
*/
|
|
42
|
+
postStart?(options: PostStartOptions): Promise<void>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* determine whether UI should get a priority.
|
|
46
|
+
*/
|
|
47
|
+
priority?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type ProxyEntry = ProxyConfigArrayItem & {
|
|
51
|
+
context: string[]; // limit type to simplify our code. (not required)
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type PostStartOptions = {
|
|
55
|
+
/**
|
|
56
|
+
* pattern for selecting components in the container.
|
|
57
|
+
*/
|
|
58
|
+
pattern?: string;
|
|
59
|
+
};
|
package/ui.cli.rt.tsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// import { CLIExtension } from '../cli';
|
|
2
|
+
// import { StartCmd } from './start.cmd';
|
|
3
|
+
// import { Workspace } from '../workspace';
|
|
4
|
+
// import { GraphQLExtension } from '../graphql';
|
|
5
|
+
|
|
6
|
+
// export default ([cli, envs, workspace, graphql]: [CLIExtension, Environments, Workspace, GraphQLExtension]) => {
|
|
7
|
+
// // const ui = new UIExtension(envs, graphql);
|
|
8
|
+
// cli.register(new StartCmd(ui, workspace));
|
|
9
|
+
// // return ui;
|
|
10
|
+
// }
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import type { GraphqlUI } from '@teambit/graphql';
|
|
2
|
+
import { GraphqlAspect } from '@teambit/graphql';
|
|
3
|
+
import { Slot, SlotRegistry } from '@teambit/harmony';
|
|
4
|
+
import type { ReactRouterUI } from '@teambit/react-router';
|
|
5
|
+
import { ReactRouterAspect } from '@teambit/react-router';
|
|
6
|
+
import { Html, MountPoint, mountPointId, ssrCleanup, Assets } from '@teambit/ui-foundation.ui.rendering.html';
|
|
7
|
+
|
|
8
|
+
import { merge } from 'webpack-merge';
|
|
9
|
+
import React, { ReactNode, ComponentType } from 'react';
|
|
10
|
+
import ReactDOM from 'react-dom';
|
|
11
|
+
import ReactDOMServer from 'react-dom/server';
|
|
12
|
+
import compact from 'lodash.compact';
|
|
13
|
+
|
|
14
|
+
import { Compose, Wrapper } from './compose';
|
|
15
|
+
import { UIRootFactory } from './ui-root.ui';
|
|
16
|
+
import { UIAspect, UIRuntime } from './ui.aspect';
|
|
17
|
+
import { ClientContext } from './ui/client-context';
|
|
18
|
+
import type { SsrContent } from './ssr/ssr-content';
|
|
19
|
+
import type { RequestServer } from './ssr/request-server';
|
|
20
|
+
import type { BrowserData } from './ssr/request-browser';
|
|
21
|
+
import { RenderLifecycle } from './render-lifecycle';
|
|
22
|
+
|
|
23
|
+
export type ContextProps<T = any> = { renderCtx?: T; children: ReactNode };
|
|
24
|
+
|
|
25
|
+
type HudSlot = SlotRegistry<ReactNode>;
|
|
26
|
+
type renderLifecycleSlot = SlotRegistry<RenderLifecycle>;
|
|
27
|
+
type UIRootRegistry = SlotRegistry<UIRootFactory>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* extension
|
|
31
|
+
*/
|
|
32
|
+
export class UiUI {
|
|
33
|
+
constructor(
|
|
34
|
+
/**
|
|
35
|
+
* react-router extension.
|
|
36
|
+
*/
|
|
37
|
+
private router: ReactRouterUI,
|
|
38
|
+
/**
|
|
39
|
+
* ui root registry.
|
|
40
|
+
*/
|
|
41
|
+
private uiRootSlot: UIRootRegistry,
|
|
42
|
+
/** slot for overlay ui elements */
|
|
43
|
+
private hudSlot: HudSlot,
|
|
44
|
+
/** hooks into the ssr render process */
|
|
45
|
+
private lifecycleSlot: renderLifecycleSlot
|
|
46
|
+
) {}
|
|
47
|
+
|
|
48
|
+
/** render and rehydrate client-side */
|
|
49
|
+
async render(rootExtension: string): Promise<void> {
|
|
50
|
+
const rootFactory = this.getRoot(rootExtension);
|
|
51
|
+
if (!rootFactory) throw new Error(`root: ${rootExtension} was not found`);
|
|
52
|
+
const uiRoot = rootFactory();
|
|
53
|
+
const initialLocation = `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
54
|
+
const routes = this.router.renderRoutes(uiRoot.routes, { initialLocation });
|
|
55
|
+
const hudItems = this.hudSlot.values();
|
|
56
|
+
const lifecycleHooks = this.lifecycleSlot.toArray();
|
|
57
|
+
|
|
58
|
+
// TODO - extract the logic from here down as reusable ssr machine
|
|
59
|
+
const deserializedState = await this.deserialize(lifecycleHooks);
|
|
60
|
+
let renderContexts = await this.triggerBrowserInit(lifecycleHooks, deserializedState);
|
|
61
|
+
const reactContexts = this.getReactContexts(lifecycleHooks, renderContexts);
|
|
62
|
+
|
|
63
|
+
const app = (
|
|
64
|
+
<Compose components={reactContexts}>
|
|
65
|
+
<ClientContext>
|
|
66
|
+
{hudItems}
|
|
67
|
+
{routes}
|
|
68
|
+
</ClientContext>
|
|
69
|
+
</Compose>
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
renderContexts = await this.triggerBeforeHydrateHook(renderContexts, lifecycleHooks, app);
|
|
73
|
+
|
|
74
|
+
const mountPoint = document.getElementById(mountPointId);
|
|
75
|
+
// .render() already runs `.hydrate()` behind the scenes.
|
|
76
|
+
// in the future, we may want to replace it with .hydrate()
|
|
77
|
+
ReactDOM.render(app, mountPoint);
|
|
78
|
+
|
|
79
|
+
await this.triggerHydrateHook(renderContexts, lifecycleHooks, mountPoint);
|
|
80
|
+
|
|
81
|
+
// remove ssr only styles
|
|
82
|
+
ssrCleanup();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** render dehydrated server-side */
|
|
86
|
+
async renderSsr(rootExtension: string, { assets, browser, server }: SsrContent = {}): Promise<string> {
|
|
87
|
+
const rootFactory = this.getRoot(rootExtension);
|
|
88
|
+
if (!rootFactory) throw new Error(`root: ${rootExtension} was not found`);
|
|
89
|
+
|
|
90
|
+
const uiRoot = rootFactory();
|
|
91
|
+
const routes = this.router.renderRoutes(uiRoot.routes, { initialLocation: browser?.location.url });
|
|
92
|
+
const hudItems = this.hudSlot.values();
|
|
93
|
+
|
|
94
|
+
// create array once to keep consistent indexes
|
|
95
|
+
const lifecycleHooks = this.lifecycleSlot.toArray();
|
|
96
|
+
|
|
97
|
+
// TODO - extract the logic from here down as reusable ssr machine
|
|
98
|
+
// (1) init
|
|
99
|
+
let renderContexts = await this.triggerServerInit(lifecycleHooks, browser, server);
|
|
100
|
+
const reactContexts = this.getReactContexts(lifecycleHooks, renderContexts);
|
|
101
|
+
|
|
102
|
+
// (2) make (virtual) dom
|
|
103
|
+
const app = (
|
|
104
|
+
<MountPoint>
|
|
105
|
+
<Compose components={reactContexts}>
|
|
106
|
+
<ClientContext>
|
|
107
|
+
{hudItems}
|
|
108
|
+
{routes}
|
|
109
|
+
</ClientContext>
|
|
110
|
+
</Compose>
|
|
111
|
+
</MountPoint>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// (3) render
|
|
115
|
+
renderContexts = await this.onBeforeRender(renderContexts, lifecycleHooks, app);
|
|
116
|
+
|
|
117
|
+
const renderedApp = ReactDOMServer.renderToString(app);
|
|
118
|
+
|
|
119
|
+
// (3) render html-template
|
|
120
|
+
const realtimeAssets = await this.serialize(lifecycleHooks, renderContexts, app);
|
|
121
|
+
// @ts-ignore // TODO upgrade 'webpack-merge'
|
|
122
|
+
const totalAssets = merge(assets, realtimeAssets) as Assets;
|
|
123
|
+
|
|
124
|
+
const html = <Html assets={totalAssets} withDevTools fullHeight ssr />;
|
|
125
|
+
const renderedHtml = `<!DOCTYPE html>${ReactDOMServer.renderToStaticMarkup(html)}`;
|
|
126
|
+
const fullHtml = Html.fillContent(renderedHtml, renderedApp);
|
|
127
|
+
|
|
128
|
+
// (4) serve
|
|
129
|
+
return fullHtml;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** adds elements to the Heads Up Display */
|
|
133
|
+
registerHudItem = (element: ReactNode) => {
|
|
134
|
+
this.hudSlot.register(element);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* adds global context at the ui root
|
|
139
|
+
* @deprecated replace with `.registerRenderHooks({ reactContext })`.
|
|
140
|
+
*/
|
|
141
|
+
registerContext<T>(context: ComponentType<ContextProps<T>>) {
|
|
142
|
+
this.lifecycleSlot.register({
|
|
143
|
+
reactContext: context,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
registerRoot(uiRoot: UIRootFactory) {
|
|
148
|
+
return this.uiRootSlot.register(uiRoot);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
registerRenderHooks<T, Y>(hooks: RenderLifecycle<T, Y>) {
|
|
152
|
+
return this.lifecycleSlot.register(hooks);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private triggerBrowserInit(lifecycleHooks: [string, RenderLifecycle<any, any>][], deserializedState: any[]) {
|
|
156
|
+
return Promise.all(lifecycleHooks.map(([, hooks], idx) => hooks.browserInit?.(deserializedState[idx])));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private triggerServerInit(
|
|
160
|
+
lifecycleHooks: [string, RenderLifecycle<any, any>][],
|
|
161
|
+
browser?: BrowserData,
|
|
162
|
+
server?: RequestServer
|
|
163
|
+
) {
|
|
164
|
+
return Promise.all(lifecycleHooks.map(([, hooks]) => hooks.serverInit?.({ browser, server })));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private getReactContexts(lifecycleHooks: [string, RenderLifecycle<any>][], renderContexts: any[]): Wrapper[] {
|
|
168
|
+
return compact(
|
|
169
|
+
lifecycleHooks.map(([, hooks], idx) => {
|
|
170
|
+
const renderCtx = renderContexts[idx];
|
|
171
|
+
const props = { renderCtx };
|
|
172
|
+
return hooks.reactContext ? [hooks.reactContext, props] : undefined;
|
|
173
|
+
})
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private async onBeforeRender(
|
|
178
|
+
renderContexts: any[],
|
|
179
|
+
lifecycleHooks: [string, RenderLifecycle<any>][],
|
|
180
|
+
app: JSX.Element
|
|
181
|
+
) {
|
|
182
|
+
await Promise.all(
|
|
183
|
+
lifecycleHooks.map(async ([, hooks], idx) => {
|
|
184
|
+
const ctx = renderContexts[idx];
|
|
185
|
+
const nextCtx = await hooks.onBeforeRender?.(ctx, app);
|
|
186
|
+
return nextCtx || ctx;
|
|
187
|
+
})
|
|
188
|
+
);
|
|
189
|
+
return renderContexts;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private triggerBeforeHydrateHook(
|
|
193
|
+
renderContexts: any[],
|
|
194
|
+
lifecycleHooks: [string, RenderLifecycle<any>][],
|
|
195
|
+
app: JSX.Element
|
|
196
|
+
) {
|
|
197
|
+
return Promise.all(
|
|
198
|
+
lifecycleHooks.map(async ([, hooks], idx) => {
|
|
199
|
+
const ctx = renderContexts[idx];
|
|
200
|
+
const nextCtx = await hooks.onBeforeHydrate?.(ctx, app);
|
|
201
|
+
return nextCtx || ctx;
|
|
202
|
+
})
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private async triggerHydrateHook(
|
|
207
|
+
renderContexts: any[],
|
|
208
|
+
lifecycleHooks: [string, RenderLifecycle<any, any>][],
|
|
209
|
+
mountPoint: HTMLElement | null
|
|
210
|
+
) {
|
|
211
|
+
await Promise.all(lifecycleHooks.map(([, hooks], idx) => hooks.onHydrate?.(renderContexts[idx], mountPoint)));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private async serialize(
|
|
215
|
+
lifecycleHooks: [string, RenderLifecycle][],
|
|
216
|
+
renderContexts: any[],
|
|
217
|
+
app: ReactNode
|
|
218
|
+
): Promise<Assets> {
|
|
219
|
+
const json = {};
|
|
220
|
+
|
|
221
|
+
await Promise.all(
|
|
222
|
+
lifecycleHooks.map(async ([key, hooks], idx) => {
|
|
223
|
+
const renderCtx = renderContexts[idx];
|
|
224
|
+
const result = await hooks.serialize?.(renderCtx, app);
|
|
225
|
+
|
|
226
|
+
if (!result) return;
|
|
227
|
+
if (result.json) json[key] = result.json;
|
|
228
|
+
})
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// more assets will be available in the future
|
|
232
|
+
return { json };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private async deserialize(lifecycleHooks: [string, RenderLifecycle][]) {
|
|
236
|
+
const rawAssets = Html.popAssets();
|
|
237
|
+
|
|
238
|
+
const deserialized = await Promise.all(
|
|
239
|
+
lifecycleHooks.map(async ([key, hooks]) => {
|
|
240
|
+
try {
|
|
241
|
+
const raw = rawAssets.get(key);
|
|
242
|
+
return hooks.deserialize?.(raw);
|
|
243
|
+
} catch (e) {
|
|
244
|
+
// eslint-disable-next-line no-console
|
|
245
|
+
console.error(`failed deserializing server state for aspect ${key}`, e);
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
return deserialized;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private getRoot(rootExtension: string) {
|
|
255
|
+
return this.uiRootSlot.get(rootExtension);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
static slots = [Slot.withType<UIRootFactory>(), Slot.withType<ReactNode>(), Slot.withType<RenderLifecycle>()];
|
|
259
|
+
|
|
260
|
+
static dependencies = [GraphqlAspect, ReactRouterAspect];
|
|
261
|
+
|
|
262
|
+
static runtime = UIRuntime;
|
|
263
|
+
|
|
264
|
+
static async provider(
|
|
265
|
+
[GraphqlUi, router]: [GraphqlUI, ReactRouterUI],
|
|
266
|
+
config,
|
|
267
|
+
[uiRootSlot, hudSlot, renderLifecycleSlot]: [UIRootRegistry, HudSlot, renderLifecycleSlot]
|
|
268
|
+
) {
|
|
269
|
+
const uiUi = new UiUI(router, uiRootSlot, hudSlot, renderLifecycleSlot);
|
|
270
|
+
|
|
271
|
+
uiUi.registerRenderHooks(GraphqlUi.renderHooks);
|
|
272
|
+
|
|
273
|
+
return uiUi;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
UIAspect.addRuntime(UiUI);
|
package/webpack/html.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** fallback html template for the main UI, in case ssr is not active */
|
|
2
|
+
export function html(title: string, withDevTools?: boolean) {
|
|
3
|
+
return () => `
|
|
4
|
+
<!DOCTYPE html>
|
|
5
|
+
<html lang="en">
|
|
6
|
+
<head>
|
|
7
|
+
<meta charset="utf-8">
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
9
|
+
<title>${title}</title>
|
|
10
|
+
<script>
|
|
11
|
+
// Allow to use react dev-tools inside the examples
|
|
12
|
+
${
|
|
13
|
+
withDevTools
|
|
14
|
+
? ''
|
|
15
|
+
: 'try { window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = window.parent.__REACT_DEVTOOLS_GLOBAL_HOOK__; } catch {}'
|
|
16
|
+
}
|
|
17
|
+
</script>
|
|
18
|
+
</head>
|
|
19
|
+
<body>
|
|
20
|
+
<div id="root"></div>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
23
|
+
`;
|
|
24
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// import postcssFlexbugsFixes from 'postcss-flexbugs-fixes';
|
|
2
|
+
|
|
3
|
+
export const postCssConfig = {
|
|
4
|
+
// Necessary for external CSS imports to work
|
|
5
|
+
// https://github.com/facebook/create-react-app/issues/2677
|
|
6
|
+
ident: 'postcss',
|
|
7
|
+
plugins: [
|
|
8
|
+
// eslint-disable-next-line global-require
|
|
9
|
+
require.resolve('postcss-flexbugs-fixes'),
|
|
10
|
+
// eslint-disable-next-line global-require
|
|
11
|
+
require('postcss-preset-env')({
|
|
12
|
+
autoprefixer: {
|
|
13
|
+
flexbox: 'no-2009',
|
|
14
|
+
},
|
|
15
|
+
stage: 3,
|
|
16
|
+
}),
|
|
17
|
+
// Adds PostCSS Normalize as the reset css with default options,
|
|
18
|
+
// so that it honors browserslist config in package.json
|
|
19
|
+
// which in turn let's users customize the target behavior as per their needs.
|
|
20
|
+
require.resolve('postcss-normalize'),
|
|
21
|
+
],
|
|
22
|
+
};
|