Skip to content

Commit

Permalink
add(prebundle): load ui from pre-bundle when start on bare scope (#8028)
Browse files Browse the repository at this point in the history
  • Loading branch information
leimonio authored Oct 16, 2023
1 parent 783ba81 commit 4fd6c44
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 57 deletions.
2 changes: 1 addition & 1 deletion scopes/scope/scope/scope.ui-root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class ScopeUIRoot implements UIRoot {

buildOptions = {
ssr: true,
prebundle: false,
prebundle: true,
};

resolveAspects(runtime: string, componentIds?: ComponentID[], opts?: ResolveAspectsOptions) {
Expand Down
35 changes: 26 additions & 9 deletions scopes/ui-foundation/ui/bundle-ui.task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ import { UIAspect, UiMain } from '@teambit/ui';

export const BUNDLE_UI_TASK_NAME = 'BundleUI';
export const BUNDLE_UI_DIR = 'ui-bundle';
export const UIROOT_ASPECT_IDS = {
SCOPE: 'teambit.scope/scope',
WORKSPACE: 'teambit.workspace/workspace',
};
export const BUNDLE_UIROOT_DIR = {
[UIROOT_ASPECT_IDS.SCOPE]: 'scope',
[UIROOT_ASPECT_IDS.WORKSPACE]: 'workspace',
};
export const BUNDLE_UI_HASH_FILENAME = '.hash';

export class BundleUiTask implements BuildTask {
Expand All @@ -24,11 +32,15 @@ export class BundleUiTask implements BuildTask {
return { componentsResults: [] };
}

const outputPath = join(capsule.path, BundleUiTask.getArtifactDirectory());
this.logger.info(`Generating UI bundle at ${outputPath}...`);
try {
await this.ui.build(undefined, outputPath);
await this.generateHash(outputPath);
await Promise.all(
Object.values(UIROOT_ASPECT_IDS).map(async (uiRootAspectId) => {
const outputPath = join(capsule.path, BundleUiTask.getArtifactDirectory(uiRootAspectId));
this.logger.info(`Generating UI bundle at ${outputPath}...`);
await this.ui.build(uiRootAspectId, outputPath);
await this.generateHash(outputPath);
})
);
} catch (error) {
this.logger.error('Generating UI bundle failed');
throw new Error('Generating UI bundle failed');
Expand All @@ -51,16 +63,21 @@ export class BundleUiTask implements BuildTask {
writeFileSync(join(outputPath, BUNDLE_UI_HASH_FILENAME), hash);
}

static getArtifactDirectory() {
return join('artifacts', BUNDLE_UI_DIR);
static getArtifactDirectory(uiRootAspectId) {
return join('artifacts', BUNDLE_UI_DIR, BUNDLE_UIROOT_DIR[uiRootAspectId]);
}

static getArtifactDef() {
const scopeRootDir = BundleUiTask.getArtifactDirectory(UIROOT_ASPECT_IDS.SCOPE);
const workspaceRootDir = BundleUiTask.getArtifactDirectory(UIROOT_ASPECT_IDS.WORKSPACE);
return [
{
name: BUNDLE_UI_DIR,
globPatterns: ['**'],
rootDir: BundleUiTask.getArtifactDirectory(),
name: `${BUNDLE_UI_DIR}-${BUNDLE_UIROOT_DIR[UIROOT_ASPECT_IDS.SCOPE]}`,
globPatterns: [`${scopeRootDir}/**`],
},
{
name: `${BUNDLE_UI_DIR}-${BUNDLE_UIROOT_DIR[UIROOT_ASPECT_IDS.WORKSPACE]}`,
globPatterns: [`${workspaceRootDir}/**`],
},
];
}
Expand Down
15 changes: 12 additions & 3 deletions scopes/ui-foundation/ui/start.cmd.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,18 +76,27 @@ export class StartCmd implements Command {

async render(
[userPattern]: StartArgs,
{ dev, port, rebuild, verbose, noBrowser, skipCompilation, skipUiBuild, uiRootName }: StartFlags
{
dev,
port,
rebuild,
verbose,
noBrowser,
skipCompilation,
skipUiBuild,
uiRootName: uiRootAspectIdOrName,
}: StartFlags
): Promise<React.ReactElement> {
this.logger.off();
if (!this.ui.isHostAvailable()) {
throw new BitError(
`bit start can only be run inside a bit workspace or a bit scope - please ensure you are running the command in the correct directory`
);
}
const appName = this.ui.getUiName(uiRootName);
const appName = this.ui.getUiName(uiRootAspectIdOrName);
await this.ui.invokePreStart({ skipCompilation });
const uiServer = this.ui.createRuntime({
uiRootName,
uiRootAspectIdOrName,
skipUiBuild,
pattern: userPattern,
dev,
Expand Down
1 change: 1 addition & 0 deletions scopes/ui-foundation/ui/ui-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export class UIServer {
const publicDir = `/${this.publicDir}`;
const defaultRoot = join(this.uiRoot.path, publicDir);
const root = bundleUiRoot || defaultRoot;
this.logger.debug(`UiServer, start from ${root}`);
const server = await this.graphql.createServer({ app });

// set up proxy, for things like preview, e.g. '/preview/teambit.react/react'
Expand Down
106 changes: 62 additions & 44 deletions scopes/ui-foundation/ui/ui.main.runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export type RuntimeOptions = {
* name of the UI root to load.
*/
uiRootName?: string;
uiRootAspectIdOrName?: string;

/**
* component selector pattern to load.
Expand Down Expand Up @@ -213,28 +214,28 @@ export class UiMain {
/**
* create a build of the given UI root.
*/
async build(uiRootName?: string, customOutputPath?: string): Promise<webpack.MultiStats | undefined> {
async build(uiRootAspectIdOrName?: string, customOutputPath?: string): Promise<webpack.MultiStats | undefined> {
// TODO: change to MultiStats from webpack once they export it in their types
this.logger.debug(`build, uiRootName: "${uiRootName}"`);
const maybeUiRoot = this.getUi(uiRootName);
this.logger.debug(`build, uiRootAspectIdOrName: "${uiRootAspectIdOrName}"`);
const maybeUiRoot = this.getUi(uiRootAspectIdOrName);

if (!maybeUiRoot) throw new UnknownUI(uiRootName, this.possibleUis());
const [name, uiRoot] = maybeUiRoot;
if (!maybeUiRoot) throw new UnknownUI(uiRootAspectIdOrName, this.possibleUis());
const [uiRootAspectId, uiRoot] = maybeUiRoot;

// TODO: @uri refactor all dev server related code to use the bundler extension instead.
const ssr = uiRoot.buildOptions?.ssr || false;
const mainEntry = await this.generateRoot(await uiRoot.resolveAspects(UIRuntime.name), name);
const mainEntry = await this.generateRoot(await uiRoot.resolveAspects(UIRuntime.name), uiRootAspectId);
const outputPath = customOutputPath || uiRoot.path;

const browserConfig = createWebpackConfig(outputPath, [mainEntry], uiRoot.name, await this.publicDir(uiRoot));
const ssrConfig = ssr && createSsrWebpackConfig(outputPath, [mainEntry], await this.publicDir(uiRoot));

const config = [browserConfig, ssrConfig].filter((x) => !!x) as webpack.Configuration[];
const compiler = webpack(config);
this.logger.debug(`build, uiRootName: "${uiRootName}" running webpack`);
this.logger.debug(`build, uiRootAspectIdOrName: "${uiRootAspectIdOrName}" running webpack`);
const compilerRun = promisify(compiler.run.bind(compiler));
const results = await compilerRun();
this.logger.debug(`build, uiRootName: "${uiRootName}" completed webpack`);
this.logger.debug(`build, uiRootAspectIdOrName: "${uiRootAspectIdOrName}" completed webpack`);
if (!results) throw new UnknownBuildError();
if (results?.hasErrors()) {
this.clearConsole();
Expand All @@ -258,25 +259,36 @@ export class UiMain {
/**
* create a Bit UI runtime.
*/
async createRuntime({ uiRootName, pattern, dev, port, rebuild, verbose, skipUiBuild }: RuntimeOptions) {
const maybeUiRoot = this.getUi(uiRootName);
if (!maybeUiRoot) throw new UnknownUI(uiRootName, this.possibleUis());

const [name, uiRoot] = maybeUiRoot;
async createRuntime({
uiRootName,
uiRootAspectIdOrName,
pattern,
dev,
port,
rebuild,
verbose,
skipUiBuild,
}: RuntimeOptions) {
// uiRootName to be deprecated
uiRootAspectIdOrName = uiRootName || uiRootAspectIdOrName;
const maybeUiRoot = this.getUi(uiRootAspectIdOrName);
if (!maybeUiRoot) throw new UnknownUI(uiRootAspectIdOrName, this.possibleUis());

const [uiRootAspectId, uiRoot] = maybeUiRoot;

const plugins = await this.initiatePlugins({
verbose,
pattern,
});

if (this.componentExtension.isHost(name)) this.componentExtension.setHostPriority(name);
if (this.componentExtension.isHost(uiRootAspectId)) this.componentExtension.setHostPriority(uiRootAspectId);

const publicDir = await this.publicDir(uiRoot);
const uiServer = UIServer.create({
express: this.express,
graphql: this.graphql,
uiRoot,
uiRootExtension: name,
uiRootExtension: uiRootAspectId,
ui: this,
logger: this.logger,
publicDir,
Expand All @@ -288,13 +300,15 @@ export class UiMain {
if (dev) {
await uiServer.dev({ portRange: port || this.config.portRange });
} else {
if (!skipUiBuild) await this.buildUI(name, uiRoot, rebuild);
const bundleUiPath = this.getBundleUiPath();
if (!skipUiBuild) await this.buildUI(uiRootAspectId, uiRoot, rebuild);
const bundleUiPath = this.getBundleUiPath(uiRootAspectId);
const bundleUiPublicPath = bundleUiPath ? join(bundleUiPath, publicDir) : undefined;
const bundleUiRoot =
this._isBundleUiServed && bundleUiPublicPath && existsSync(bundleUiPublicPath || '')
? bundleUiPublicPath
: undefined;
if (bundleUiRoot)
this.logger.debug(`UI createRuntime of ${uiRootAspectId}, bundle will be served from ${bundleUiRoot}`);
await uiServer.start({ portRange: port || this.config.portRange, bundleUiRoot });
}

Expand Down Expand Up @@ -396,11 +410,11 @@ export class UiMain {
/**
* get a UI runtime instance.
*/
getUi(uiRootName?: string): [string, UIRoot] | undefined {
if (uiRootName) {
const root = this.uiRootSlot.get(uiRootName) || this.getUiByName(uiRootName);
getUi(uiRootAspectIdOrName?: string): [string, UIRoot] | undefined {
if (uiRootAspectIdOrName) {
const root = this.uiRootSlot.get(uiRootAspectIdOrName) || this.getUiByName(uiRootAspectIdOrName);
if (!root) return undefined;
return [uiRootName, root];
return [uiRootAspectIdOrName, root];
}
const uis = this.uiRootSlot.toArray();
if (uis.length === 1) return uis[0];
Expand All @@ -411,8 +425,8 @@ export class UiMain {
return Boolean(this.componentExtension.getHost());
}

getUiName(uiRootName?: string): string | undefined {
const [, ui] = this.getUi(uiRootName) || [];
getUiName(uiRootAspectIdOrName?: string): string | undefined {
const [, ui] = this.getUi(uiRootAspectIdOrName) || [];
if (!ui) return undefined;

return ui.name;
Expand Down Expand Up @@ -461,42 +475,46 @@ export class UiMain {
return port;
}

private async buildUI(name: string, uiRoot: UIRoot, rebuild?: boolean): Promise<string> {
this.logger.debug(`buildUI, name ${name}`);
private async buildUI(uiRootAspectId: string, uiRoot: UIRoot, rebuild?: boolean): Promise<string> {
this.logger.debug(`buildUI, uiRootAspectId ${uiRootAspectId}`);

const overwrite = this.getOverwriteBuildFn();
if (overwrite) return overwrite(name, uiRoot, rebuild);
if (overwrite) return overwrite(uiRootAspectId, uiRoot, rebuild);

this._isBundleUiServed = await this.shouldServeBundleUi(uiRoot, rebuild);
await this.buildIfChanged(name, uiRoot, rebuild);
await this.buildIfNoBundle(name, uiRoot);
this._isBundleUiServed = await this.shouldServeBundleUi(uiRootAspectId, uiRoot, rebuild);
await this.buildIfChanged(uiRootAspectId, uiRoot, rebuild);
await this.buildIfNoBundle(uiRootAspectId, uiRoot);
return '';
}

private async shouldServeBundleUi(uiRoot: UIRoot, force: boolean | undefined): Promise<boolean> {
private async shouldServeBundleUi(
uiRootAspectId: string,
uiRoot: UIRoot,
force: boolean | undefined
): Promise<boolean> {
if (!uiRoot.buildOptions?.prebundle) {
return false;
}

const currentBundleUiHash = await this.createBundleUiHash(uiRoot);
const cachedBundleUiHash = this.readBundleUiHash();
const cachedBundleUiHash = this.readBundleUiHash(uiRootAspectId);
const isLocalBuildAvailable = existsSync(join(uiRoot.path, await this.publicDir(uiRoot)));

return currentBundleUiHash === cachedBundleUiHash && !isLocalBuildAvailable && !force;
}

async buildIfChanged(name: string, uiRoot: UIRoot, force: boolean | undefined): Promise<boolean> {
this.logger.debug(`buildIfChanged, name ${name}`);
async buildIfChanged(uiRootAspectId: string, uiRoot: UIRoot, force: boolean | undefined): Promise<boolean> {
this.logger.debug(`buildIfChanged, uiRootAspectId ${uiRootAspectId}`);

if (this._isBundleUiServed) {
this.logger.debug(`buildIfChanged, name ${name}, returned from ui bundle cache`);
this.logger.debug(`buildIfChanged, uiRootAspectId ${uiRootAspectId}, returned from ui bundle cache`);
return false;
}

const currentBuildUiHash = await this.createBuildUiHash(uiRoot);
const cachedBuildUiHash = await this.cache.get(uiRoot.path);
if (currentBuildUiHash === cachedBuildUiHash && !force) {
this.logger.debug(`buildIfChanged, name ${name}, returned from ui build cache`);
this.logger.debug(`buildIfChanged, uiRootAspectId ${uiRootAspectId}, returned from ui build cache`);
return false;
}

Expand All @@ -514,7 +532,7 @@ export class UiMain {
);
}

await this.build(name);
await this.build(uiRootAspectId);
await this.cache.set(uiRoot.path, currentBuildUiHash);
return true;
}
Expand Down Expand Up @@ -543,8 +561,8 @@ export class UiMain {
return sha1(aspectIds.join(''));
}

private readBundleUiHash() {
const bundleUiPathFromBvm = this.getBundleUiPath();
private readBundleUiHash(uiRootAspectId: string) {
const bundleUiPathFromBvm = this.getBundleUiPath(uiRootAspectId);
if (!bundleUiPathFromBvm) {
return '';
}
Expand All @@ -555,28 +573,28 @@ export class UiMain {
return '';
}

private getBundleUiPath(): string | undefined {
private getBundleUiPath(uiRootAspectId: string): string | undefined {
try {
const uiPathFromBvm = getAspectDirFromBvm(UIAspect.id);
return join(uiPathFromBvm, BundleUiTask.getArtifactDirectory());
return join(uiPathFromBvm, BundleUiTask.getArtifactDirectory(uiRootAspectId));
} catch (err) {
this.logger.info(`getBundleUiPath, getAspectDirFromBvm failed with err: ${err}`);
this.logger.error(`getBundleUiPath, getAspectDirFromBvm failed with err: ${err}`);
return undefined;
}
}

private async buildIfNoBundle(name: string, uiRoot: UIRoot): Promise<boolean> {
private async buildIfNoBundle(uiRootAspectId: string, uiRoot: UIRoot): Promise<boolean> {
if (this._isBundleUiServed) return false;

const config = createWebpackConfig(
uiRoot.path,
[await this.generateRoot(await uiRoot.resolveAspects(UIRuntime.name), name)],
[await this.generateRoot(await uiRoot.resolveAspects(UIRuntime.name), uiRootAspectId)],
uiRoot.name,
await this.publicDir(uiRoot)
);
if (config.output?.path && fs.pathExistsSync(config.output.path)) return false;
const hash = await this.createBuildUiHash(uiRoot);
await this.build(name);
await this.build(uiRootAspectId);
await this.cache.set(uiRoot.path, hash);
return true;
}
Expand Down

0 comments on commit 4fd6c44

Please sign in to comment.