@teambit/ui 0.0.568 → 0.0.572

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.
@@ -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);
@@ -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
+ };