Skip to content

Commit 305642b

Browse files
authored
feat(preview | dev server): dynamically generate dev servers for new components (teambit#9651)
1 parent e33c932 commit 305642b

File tree

20 files changed

+684
-177
lines changed

20 files changed

+684
-177
lines changed

components/ui/workspace-component-card/workspace-component-card.tsx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import React, { useEffect } from 'react';
1+
import React, { useEffect, useRef } from 'react';
22
import { ComponentDescriptor } from '@teambit/component-descriptor';
33
import classNames from 'classnames';
44
import { ScopeID } from '@teambit/scopes.scope-id';
55
import { ComponentCard, type ComponentCardPluginType, type PluginProps } from '@teambit/explorer.ui.component-card';
66
import { ComponentModel } from '@teambit/component';
77
import { LoadPreview } from '@teambit/workspace.ui.load-preview';
8-
98
import styles from './workspace-component-card.module.scss';
109

1110
export type WorkspaceComponentCardProps = {
@@ -31,27 +30,41 @@ export function WorkspaceComponentCard({
3130
...rest
3231
}: WorkspaceComponentCardProps) {
3332
const [shouldShowPreviewState, togglePreview] = React.useState<boolean>(Boolean(shouldShowPreviewStateFromProps));
33+
const prevServerUrlRef = useRef(component.server?.url);
34+
35+
useEffect(() => {
36+
const currentServerUrl = component.server?.url;
37+
if (prevServerUrlRef.current !== currentServerUrl && shouldShowPreviewState) {
38+
togglePreview(false);
39+
setTimeout(() => togglePreview(true), 50);
40+
}
41+
prevServerUrlRef.current = currentServerUrl;
42+
}, [component.server?.url]);
3443

3544
useEffect(() => {
3645
togglePreview(Boolean(shouldShowPreviewStateFromProps));
3746
}, [shouldShowPreviewStateFromProps]);
47+
3848
const showPreview = (e: React.MouseEvent<HTMLDivElement>) => {
3949
e.stopPropagation();
4050
if (!shouldShowPreviewState) {
4151
togglePreview(true);
4252
}
4353
};
54+
4455
const loadPreviewBtnVisible =
4556
component.compositions.length > 0 && component?.buildStatus !== 'pending' && !shouldShowPreviewState;
57+
4658
const updatedPlugins = React.useMemo(() => {
4759
return plugins?.map((plugin) => {
4860
if (plugin.preview) {
4961
const Preview = plugin.preview;
5062
return {
5163
...plugin,
5264
preview: function PreviewWrapper(props) {
65+
const serverKey = component.server?.url || 'no-server';
5366
return (
54-
<div className={styles.previewWrapper}>
67+
<div key={serverKey}>
5568
<Preview {...props} shouldShowPreview={shouldShowPreviewState} />
5669
</div>
5770
);
@@ -60,12 +73,14 @@ export function WorkspaceComponentCard({
6073
}
6174
return plugin;
6275
});
63-
}, [shouldShowPreviewState, component.compositions.length]);
76+
}, [shouldShowPreviewState, component.compositions.length, component.server?.url]);
77+
6478
if (component.deprecation?.isDeprecate) return null;
79+
6580
return (
6681
<div key={component.id.toString()} className={classNames(styles.cardWrapper, className)} {...rest}>
6782
{loadPreviewBtnVisible && <LoadPreview className={styles.loadPreview} onClick={showPreview} />}
6883
<ComponentCard component={componentDescriptor} plugins={updatedPlugins} displayOwnerDetails="all" scope={scope} />
6984
</div>
7085
);
71-
}
86+
}

scopes/compilation/bundler/bundler.main.runtime.ts

Lines changed: 93 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
import { PubsubAspect, PubsubMain } from '@teambit/pubsub';
22
import { MainRuntime } from '@teambit/cli';
3-
import { Component, ComponentAspect } from '@teambit/component';
3+
import { Component } from '@teambit/component';
44
import { DependencyResolverAspect, DependencyResolverMain } from '@teambit/dependency-resolver';
55
import { EnvsAspect, EnvsMain } from '@teambit/envs';
66
import { GraphqlAspect, GraphqlMain } from '@teambit/graphql';
77
import { Slot, SlotRegistry } from '@teambit/harmony';
88
import { BrowserRuntime } from './browser-runtime';
99
import { BundlerAspect } from './bundler.aspect';
1010
import { ComponentServer } from './component-server';
11+
import { NewDevServersCreatedEvent } from './events';
1112
import { BundlerContext } from './bundler-context';
1213
import { devServerSchema } from './dev-server.graphql';
1314
import { DevServerService } from './dev-server.service';
1415
import { BundlerService } from './bundler.service';
1516
import { DevServer } from './dev-server';
1617

1718
export type DevServerTransformer = (devServer: DevServer, { envId }: { envId: string }) => DevServer;
18-
19+
export type OnPreDevServerCreated = (newCompsWithoutDevServer: Component[]) => Promise<void>;
1920
export type BrowserRuntimeSlot = SlotRegistry<BrowserRuntime>;
2021
export type DevServerTransformerSlot = SlotRegistry<DevServerTransformer>;
22+
export type OnPreDevServerCreatedSlot = SlotRegistry<OnPreDevServerCreated>;
2123

2224
export type BundlerConfig = {
2325
dedicatedEnvDevServers: string[];
@@ -27,6 +29,11 @@ export type BundlerConfig = {
2729
* bundler extension.
2830
*/
2931
export class BundlerMain {
32+
/**
33+
* component servers.
34+
*/
35+
private _componentServers: ComponentServer[] = [];
36+
3037
constructor(
3138
readonly config: BundlerConfig,
3239
/**
@@ -52,25 +59,49 @@ export class BundlerMain {
5259
/**
5360
* dev server transformer slot.
5461
*/
55-
private devServerTransformerSlot: DevServerTransformerSlot
56-
) {}
62+
private devServerTransformerSlot: DevServerTransformerSlot,
5763

58-
/**
59-
* load all given components in corresponding dev servers.
60-
* @param components defaults to all components in the workspace.
61-
*/
62-
async devServer(components: Component[]): Promise<ComponentServer[]> {
64+
/**
65+
* pre-dev-server operation slot.
66+
*/
67+
private onPreDevServerCreatedSlot: OnPreDevServerCreatedSlot,
68+
69+
private graphql: GraphqlMain,
70+
) {
71+
}
72+
73+
async addNewDevServers(newCompsWithoutDevServers: Component[]): Promise<ComponentServer[]> {
74+
const newComponents = newCompsWithoutDevServers.filter((component) => {
75+
return !this.getComponentServer(component);
76+
});
77+
78+
if (newComponents.length === 0) {
79+
return [];
80+
}
81+
82+
await Promise.all(
83+
this.onPreDevServerCreatedSlot.values().map(subscriberFn => subscriberFn(newComponents))
84+
);
85+
86+
return this.devServer(newComponents, { configureProxy: true });
87+
}
88+
89+
async devServer(components: Component[], opts: { configureProxy?: boolean } = {}): Promise<ComponentServer[]> {
6390
const envRuntime = await this.envs.createEnvironment(components);
64-
// TODO: this must be refactored away from here. this logic should be in the Preview.
65-
// @ts-ignore
66-
const servers: ComponentServer[] = await envRuntime.runOnce<ComponentServer[]>(this.devService, {
91+
const servers: ComponentServer[] = await envRuntime.runOnce<ComponentServer>(this.devService, {
6792
dedicatedEnvDevServers: this.config.dedicatedEnvDevServers,
6893
});
94+
if (opts.configureProxy) {
95+
this.pubsub.pub(BundlerAspect.id, new NewDevServersCreatedEvent(
96+
servers,
97+
Date.now(),
98+
this.graphql,
99+
true
100+
));
101+
}
69102
this._componentServers = servers;
70-
71103
this.indexByComponent();
72-
73-
return this._componentServers;
104+
return servers;
74105
}
75106

76107
/**
@@ -84,13 +115,12 @@ export class BundlerMain {
84115
(componentServer) =>
85116
componentServer.context.relatedContexts.includes(envId) || componentServer.context.id === envId
86117
);
87-
88118
return server;
89119
}
90120

91121
/**
92122
* compute entry files for bundling components in a given execution context.
93-
*/
123+
*/
94124
async computeEntries(context: BundlerContext) {
95125
const slotEntries = await Promise.all(
96126
this.runtimeSlot.values().map(async (browserRuntime) => browserRuntime.entry(context))
@@ -126,34 +156,69 @@ export class BundlerMain {
126156
}
127157

128158
/**
129-
* component servers.
159+
* register a new pre-dev-server compiler.
160+
* @param onPreDevServerCreated
130161
*/
131-
private _componentServers: null | ComponentServer[];
162+
registerOnPreDevServerCreated(onPreDevServerCreated: OnPreDevServerCreated) {
163+
this.onPreDevServerCreatedSlot.register(onPreDevServerCreated);
164+
return this;
165+
}
132166

133-
private indexByComponent() {}
167+
private indexByComponent() { }
134168

135-
static slots = [Slot.withType<BrowserRuntime>(), Slot.withType<DevServerTransformerSlot>()];
169+
static slots = [
170+
Slot.withType<BrowserRuntime>(),
171+
Slot.withType<DevServerTransformerSlot>(),
172+
Slot.withType<OnPreDevServerCreatedSlot>()
173+
];
136174

137175
static runtime = MainRuntime;
138-
static dependencies = [PubsubAspect, EnvsAspect, GraphqlAspect, DependencyResolverAspect, ComponentAspect];
176+
static dependencies = [
177+
PubsubAspect,
178+
EnvsAspect,
179+
GraphqlAspect,
180+
DependencyResolverAspect,
181+
];
139182

140183
static defaultConfig = {
141184
dedicatedEnvDevServers: [],
142185
};
143186

144187
static async provider(
145-
[pubsub, envs, graphql, dependencyResolver]: [PubsubMain, EnvsMain, GraphqlMain, DependencyResolverMain],
188+
[pubsub, envs, graphql, dependencyResolver]:
189+
[
190+
PubsubMain,
191+
EnvsMain,
192+
GraphqlMain,
193+
DependencyResolverMain,
194+
],
146195
config,
147-
[runtimeSlot, devServerTransformerSlot]: [BrowserRuntimeSlot, DevServerTransformerSlot]
196+
[runtimeSlot,
197+
devServerTransformerSlot,
198+
onPreDevServerCreatedSlot
199+
]: [
200+
BrowserRuntimeSlot,
201+
DevServerTransformerSlot,
202+
OnPreDevServerCreatedSlot
203+
]
148204
) {
205+
149206
const devServerService = new DevServerService(pubsub, dependencyResolver, runtimeSlot, devServerTransformerSlot);
150-
const bundler = new BundlerMain(config, pubsub, envs, devServerService, runtimeSlot, devServerTransformerSlot);
207+
const bundler = new BundlerMain(
208+
config,
209+
pubsub,
210+
envs,
211+
devServerService,
212+
runtimeSlot,
213+
devServerTransformerSlot,
214+
onPreDevServerCreatedSlot,
215+
graphql,
216+
);
151217
envs.registerService(devServerService, new BundlerService());
152-
153-
graphql.register(() => devServerSchema(bundler));
218+
graphql.register(() => devServerSchema(bundler, graphql));
154219

155220
return bundler;
156221
}
157222
}
158223

159-
BundlerAspect.addRuntime(BundlerMain);
224+
BundlerAspect.addRuntime(BundlerMain);

scopes/compilation/bundler/component-server.ts

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { Component } from '@teambit/component';
22
import { ExecutionContext } from '@teambit/envs';
33
import { PubsubMain } from '@teambit/pubsub';
4-
54
import { AddressInfo } from 'net';
6-
5+
import { Server } from 'http';
76
import { DevServer } from './dev-server';
87
import { BindError } from './exceptions';
98
import { ComponentsServerStartedEvent } from './events';
@@ -33,10 +32,19 @@ export class ComponentServer {
3332
* env dev server.
3433
*/
3534
readonly devServer: DevServer
36-
) {}
35+
) { }
3736

3837
hostname: string | undefined;
38+
private _server?: Server;
39+
private _isRestarting: boolean = false;
40+
41+
get server() {
42+
return this._server;
43+
}
3944

45+
get envId() {
46+
return this.context.envRuntime.id;
47+
};
4048
/**
4149
* determine whether component server contains a component.
4250
*/
@@ -49,15 +57,55 @@ export class ComponentServer {
4957
}
5058

5159
_port: number;
52-
async listen() {
53-
const port = await selectPort(this.portRange);
60+
61+
async listen(specificPort?: number) {
62+
const port = specificPort || await selectPort(this.portRange);
5463
this._port = port;
55-
const server = await this.devServer.listen(port);
56-
const address = server.address();
64+
this._server = await this.devServer.listen(port);
65+
const address = this._server.address();
5766
const hostname = this.getHostname(address);
5867
if (!address) throw new BindError();
5968
this.hostname = hostname;
60-
this.pubsub.pub(BundlerAspect.id, this.createComponentsServerStartedEvent(server, this.context, hostname, port));
69+
70+
this.pubsub.pub(
71+
BundlerAspect.id,
72+
this.createComponentsServerStartedEvent(this, this.context, hostname, port)
73+
);
74+
}
75+
76+
async close(): Promise<void> {
77+
if (!this.server) return;
78+
79+
return new Promise<void>((resolve, reject) => {
80+
this.server?.close((err) => {
81+
if (err) {
82+
reject(err);
83+
} else {
84+
this._server = undefined;
85+
this.hostname = undefined;
86+
resolve();
87+
}
88+
});
89+
});
90+
}
91+
92+
async restart(useNewPort = false): Promise<void> {
93+
if (this._isRestarting) {
94+
// add a logger here once we start using this API
95+
return;
96+
}
97+
this._isRestarting = true;
98+
try {
99+
await this.close();
100+
await this.listen(useNewPort ? undefined : this._port);
101+
}
102+
catch (error) {
103+
if (!this.errors) this.errors = [];
104+
this.errors.push(error as Error);
105+
}
106+
finally {
107+
this._isRestarting = false;
108+
}
61109
}
62110

63111
private getHostname(address: string | AddressInfo | null) {
@@ -72,11 +120,11 @@ export class ComponentServer {
72120
return hostname;
73121
}
74122

75-
private onChange() {}
123+
private onChange() { }
76124

77125
private createComponentsServerStartedEvent: (
78-
DevServer,
79-
ExecutionContext,
126+
componentsServer,
127+
context,
80128
string,
81129
number
82130
) => ComponentsServerStartedEvent = (componentsServer, context, hostname, port) => {

0 commit comments

Comments
 (0)