Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add(prebundle): scope aspect #8028

Merged
merged 5 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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