diff --git a/scopes/compilation/bundler/browser-runtime.ts b/scopes/compilation/bundler/browser-runtime.ts index 6d5340093a54..3cb0ee7a0e59 100644 --- a/scopes/compilation/bundler/browser-runtime.ts +++ b/scopes/compilation/bundler/browser-runtime.ts @@ -2,4 +2,5 @@ import { ExecutionContext } from '@teambit/envs'; export type BrowserRuntime = { entry: (context: ExecutionContext) => Promise; + exposes?: (context: ExecutionContext) => Promise>; }; diff --git a/scopes/compilation/bundler/component-server.ts b/scopes/compilation/bundler/component-server.ts index 27d1ebc1711f..d1cce0166658 100644 --- a/scopes/compilation/bundler/component-server.ts +++ b/scopes/compilation/bundler/component-server.ts @@ -14,9 +14,6 @@ export class ComponentServer { // why is this here errors?: Error[]; constructor( - /** - * browser runtime slot - */ private pubsub: PubsubMain, /** @@ -48,9 +45,13 @@ export class ComponentServer { return this._port; } + set port(port: number) { + this._port = port; + } + _port: number; async listen() { - const port = await selectPort(this.portRange); + const port = this.port ?? (await selectPort(this.portRange)); this._port = port; const server = await this.devServer.listen(port); const address = server.address(); diff --git a/scopes/compilation/bundler/dev-server-context.ts b/scopes/compilation/bundler/dev-server-context.ts index c7e35181fc6a..44cd4ecc39a0 100644 --- a/scopes/compilation/bundler/dev-server-context.ts +++ b/scopes/compilation/bundler/dev-server-context.ts @@ -17,6 +17,16 @@ export type Target = { * output path of the target */ outputPath: string; + + /** + * module federation namespace name + */ + mfName?: string; + + /** + * module federation exposed module + */ + mfExposes?: Record; }; export interface BundlerContext extends BuildContext { @@ -45,4 +55,8 @@ export interface DevServerContext extends ExecutionContext { * title of the page. */ title?: string; + + port?: number; + + exposes?: Record; } diff --git a/scopes/compilation/bundler/dev-server.service.ts b/scopes/compilation/bundler/dev-server.service.ts index 4a9a5aba0b3a..4f749f4e57fd 100644 --- a/scopes/compilation/bundler/dev-server.service.ts +++ b/scopes/compilation/bundler/dev-server.service.ts @@ -6,6 +6,8 @@ import { ComponentServer } from './component-server'; import { DevServer } from './dev-server'; import { DevServerContext } from './dev-server-context'; import { getEntry } from './get-entry'; +import { getExposes } from './get-exposes'; +import { selectPort } from './select-port'; export type DevServerServiceOptions = { dedicatedEnvDevServers?: string[] }; @@ -56,16 +58,25 @@ export class DevServerService implements EnvService { acc[envId] = [context]; return acc; }, {}); + const portRange = [3300, 3400]; + const usedPorts: number[] = []; const servers = await Promise.all( Object.entries(byOriginalEnv).map(async ([id, contextList]) => { let mainContext = contextList.find((context) => context.envDefinition.id === id); if (!mainContext) mainContext = contextList[0]; const additionalContexts = contextList.filter((context) => context.envDefinition.id !== id); - const devServerContext = await this.buildContext(mainContext, additionalContexts); - const devServer: DevServer = await devServerContext.envRuntime.env.getDevServer(devServerContext); + this.enrichContextWithComponentsAndRelatedContext(mainContext, additionalContexts); + const envDevServerContext = await this.buildEnvServerContext(mainContext); + const envDevServer: DevServer = envDevServerContext.envRuntime.env.getDevServer(envDevServerContext); + const port = await selectPort(portRange, usedPorts); + usedPorts.push(port); + envDevServerContext.port = port; - return new ComponentServer(this.pubsub, devServerContext, [3300, 3400], devServer); + // TODO: consider change this to a new class called EnvServer + const componentServer = new ComponentServer(this.pubsub, envDevServerContext, portRange, envDevServer); + componentServer.port = port; + return componentServer; }) ); @@ -85,18 +96,26 @@ export class DevServerService implements EnvService { /** * builds the execution context for the dev server. */ - private async buildContext( + private enrichContextWithComponentsAndRelatedContext( context: ExecutionContext, additionalContexts: ExecutionContext[] = [] - ): Promise { + ): void { context.relatedContexts = additionalContexts.map((ctx) => ctx.envDefinition.id); context.components = context.components.concat(this.getComponentsFromContexts(additionalContexts)); + } + /** + * builds the execution context for the dev server. + */ + private async buildEnvServerContext(context: ExecutionContext): Promise { + const entry = await getEntry(context, this.runtimeSlot); + const exposes = await getExposes(context, this.runtimeSlot); return Object.assign(context, { - entry: await getEntry(context, this.runtimeSlot), + entry, // don't start with a leading "/" because it generates errors on Windows rootPath: `preview/${context.envRuntime.id}`, publicPath: `/public`, + exposes, }); } } diff --git a/scopes/compilation/bundler/env-server.ts b/scopes/compilation/bundler/env-server.ts new file mode 100644 index 000000000000..46eb557814be --- /dev/null +++ b/scopes/compilation/bundler/env-server.ts @@ -0,0 +1,93 @@ +import { Component } from '@teambit/component'; +import { ExecutionContext } from '@teambit/envs'; +import { PubsubMain } from '@teambit/pubsub'; + +import { AddressInfo } from 'net'; + +import { DevServer } from './dev-server'; +import { BindError } from './exceptions'; +import { EnvsServerStartedEvent } from './events'; +import { BundlerAspect } from './bundler.aspect'; +import { selectPort } from './select-port'; + +export class EnvServer { + // why is this here + errors?: Error[]; + constructor( + /** + * browser runtime slot + */ + private pubsub: PubsubMain, + + /** + * components contained in the existing component server. + */ + readonly context: ExecutionContext, + + /** + * port range of the component server. + */ + readonly portRange: number[], + + /** + * env dev server. + */ + readonly devServer: DevServer + ) {} + + hostname: string | undefined; + + /** + * determine whether component server contains a component. + */ + hasComponent(component: Component) { + return this.context.components.find((contextComponent) => contextComponent.equals(component)); + } + + get port() { + return this._port; + } + + _port: number; + async listen() { + const port = await selectPort(this.portRange); + this._port = port; + const server = await this.devServer.listen(port); + const address = server.address(); + const hostname = this.getHostname(address); + if (!address) throw new BindError(); + this.hostname = hostname; + + this.pubsub.pub(BundlerAspect.id, this.cresateEnvServerStartedEvent(server, this.context, hostname, port)); + } + + private getHostname(address: string | AddressInfo | null) { + if (address === null) throw new BindError(); + if (typeof address === 'string') return address; + + let hostname = address.address; + if (hostname === '::') { + hostname = 'localhost'; + } + + return hostname; + } + + private onChange() {} + + private cresateEnvServerStartedEvent: (DevServer, ExecutionContext, string, number) => EnvsServerStartedEvent = ( + envServer, + context, + hostname, + port + ) => { + return new EnvsServerStartedEvent(Date.now(), envServer, context, hostname, port); + }; + + /** + * get the url of the component server. + */ + get url() { + return `/preview/${this.context.envRuntime.id}`; + } +} diff --git a/scopes/compilation/bundler/events/envs-server-started-event.ts b/scopes/compilation/bundler/events/envs-server-started-event.ts new file mode 100644 index 000000000000..a7d0f5bc3c68 --- /dev/null +++ b/scopes/compilation/bundler/events/envs-server-started-event.ts @@ -0,0 +1,20 @@ +/* eslint-disable max-classes-per-file */ + +import { BitBaseEvent } from '@teambit/pubsub'; + +class EnvsServerStartedEventData { + constructor(readonly EnvsServer, readonly context, readonly hostname, readonly port) {} +} + +export class EnvsServerStartedEvent extends BitBaseEvent { + static readonly TYPE = 'components-server-started'; + + constructor(readonly timestamp, readonly envsServer, readonly context, readonly hostname, readonly port) { + super( + EnvsServerStartedEvent.TYPE, + '0.0.1', + timestamp, + new EnvsServerStartedEventData(envsServer, context, hostname, port) + ); + } +} diff --git a/scopes/compilation/bundler/events/index.ts b/scopes/compilation/bundler/events/index.ts index 26346db696ac..5893c5d2be07 100644 --- a/scopes/compilation/bundler/events/index.ts +++ b/scopes/compilation/bundler/events/index.ts @@ -1 +1,2 @@ export * from './components-server-started-event'; +export * from './envs-server-started-event'; diff --git a/scopes/compilation/bundler/get-exposes.ts b/scopes/compilation/bundler/get-exposes.ts new file mode 100644 index 000000000000..daba6447653a --- /dev/null +++ b/scopes/compilation/bundler/get-exposes.ts @@ -0,0 +1,24 @@ +import { ExecutionContext } from '@teambit/envs'; +import { BrowserRuntimeSlot } from './bundler.main.runtime'; + +/** + * computes the bundler entry. + */ +export async function getExposes( + context: ExecutionContext, + runtimeSlot: BrowserRuntimeSlot +): Promise> { + // TODO: refactor this away from here and use computePaths instead + const slotEntries = await Promise.all( + runtimeSlot.values().map(async (browserRuntime) => browserRuntime.exposes?.(context)) + ); + + const exposes = slotEntries.reduce((acc, current) => { + if (current) { + acc = Object.assign(acc, current); + } + return acc; + }, {}); + + return exposes || {}; +} diff --git a/scopes/compilation/bundler/select-port.ts b/scopes/compilation/bundler/select-port.ts index 89bd18c5a552..405f6348a9ee 100644 --- a/scopes/compilation/bundler/select-port.ts +++ b/scopes/compilation/bundler/select-port.ts @@ -3,6 +3,6 @@ import { Port } from '@teambit/toolbox.network.get-port'; /** * get an available port between range 3000 to 3200 or from port range */ -export async function selectPort(range: number[] | number): Promise { - return Port.getPortFromRange(range); +export async function selectPort(range: number[] | number, usedPorts?: number[]): Promise { + return Port.getPortFromRange(range, usedPorts); } diff --git a/scopes/component/component/component-factory.ts b/scopes/component/component/component-factory.ts index 3cf6e0cd9c5f..648edc11fcd0 100644 --- a/scopes/component/component/component-factory.ts +++ b/scopes/component/component/component-factory.ts @@ -99,6 +99,11 @@ export interface ComponentFactory { */ hasIdNested(componentId: ComponentID, includeCache?: boolean): Promise; + /** + * Get temp dir for the host + */ + getTempDir(id: string): string; + /** * determine whether host should be the prior one in case multiple hosts persist. */ diff --git a/scopes/compositions/compositions/compositions.preview.runtime.ts b/scopes/compositions/compositions/compositions.preview.runtime.ts index 73f7af359d36..279fe5d4902d 100644 --- a/scopes/compositions/compositions/compositions.preview.runtime.ts +++ b/scopes/compositions/compositions/compositions.preview.runtime.ts @@ -18,20 +18,26 @@ export class CompositionsPreview { private preview: PreviewPreview ) {} - render(componentId: string, modules: PreviewModule, otherPreviewDefs, context: RenderingContext) { + async render(componentId: string, modules: PreviewModule, otherPreviewDefs, context: RenderingContext) { + console.log('im in render of composition'); if (!modules.componentMap[componentId]) return; - const compositions = this.selectPreviewModel(componentId, modules); + const compositions = await this.selectPreviewModel(componentId, modules); const active = this.getActiveComposition(compositions); modules.mainModule.default(active, context); } /** gets relevant information for this preview to render */ - selectPreviewModel(componentId: string, previewModule: PreviewModule) { - const files = previewModule.componentMap[componentId] || []; + async selectPreviewModel(componentId: string, previewModule: PreviewModule) { + // const files = (await previewModule.componentMap[componentId]()) || []; + const allFunc = previewModule.componentMap[componentId]; + const promises = allFunc.map((func) => func()); + const files = await Promise.all(promises); + console.log('selectPreviewModel', files); // allow compositions to come from many files. It is assumed they will have unique named + // const combined = Object.assign({}, ...files); const combined = Object.assign({}, ...files); return combined; } diff --git a/scopes/docs/docs/docs.preview.runtime.tsx b/scopes/docs/docs/docs.preview.runtime.tsx index cadd53721366..34db93a8b8c0 100644 --- a/scopes/docs/docs/docs.preview.runtime.tsx +++ b/scopes/docs/docs/docs.preview.runtime.tsx @@ -11,14 +11,17 @@ export class DocsPreview { private preview: PreviewPreview ) {} - render = (componentId: string, modules: PreviewModule, [compositions]: [any], context: RenderingContext) => { + render = async (componentId: string, modules: PreviewModule, [compositions]: [any], context: RenderingContext) => { const docsModule = this.selectPreviewModel(componentId, modules); modules.mainModule.default(NoopProvider, componentId, docsModule, compositions, context); }; - selectPreviewModel(componentId: string, modules: PreviewModule) { - const relevant = modules.componentMap[componentId]; + async selectPreviewModel(componentId: string, modules: PreviewModule) { + // const relevant = modules.componentMap[componentId]; + const allFunc = modules.componentMap[componentId]; + const relevant = await allFunc[0](); + if (!relevant) return undefined; // only one doc file is supported. diff --git a/scopes/envs/envs/environments.main.runtime.ts b/scopes/envs/envs/environments.main.runtime.ts index a60573f2d1ff..361d5ec4f27c 100644 --- a/scopes/envs/envs/environments.main.runtime.ts +++ b/scopes/envs/envs/environments.main.runtime.ts @@ -3,6 +3,7 @@ import { Component, ComponentAspect, ComponentMain, ComponentID, AspectData } fr import { GraphqlAspect, GraphqlMain } from '@teambit/graphql'; import { Harmony, Slot, SlotRegistry } from '@teambit/harmony'; import { Logger, LoggerAspect, LoggerMain } from '@teambit/logger'; +import { flatten } from 'lodash'; import { ExtensionDataList, ExtensionDataEntry } from '@teambit/legacy/dist/consumer/config/extension-data'; import findDuplications from '@teambit/legacy/dist/utils/array/find-duplications'; import { EnvService } from './services'; @@ -81,6 +82,20 @@ export class EnvsMain { return this.createRuntime(components); } + /** + * list all registered envs. + */ + listEnvs(): Environment[] { + return flatten(this.envSlot.values()); + } + + /** + * list all registered envs ids. + */ + listEnvsIds(): string[] { + return this.envSlot.toArray().map(([envId]) => envId); + } + /** * get the configured default env. */ diff --git a/scopes/explorer/insights/all-insights/duplicate-dependencies.ts b/scopes/explorer/insights/all-insights/duplicate-dependencies.ts index 8a3270d894bd..54c979f2279b 100644 --- a/scopes/explorer/insights/all-insights/duplicate-dependencies.ts +++ b/scopes/explorer/insights/all-insights/duplicate-dependencies.ts @@ -68,9 +68,10 @@ export default class DuplicateDependencies implements Insight { return formatted; } - private getDependents( - priorVersions: VersionSubgraph[] - ): { totalOutdatedDependents: number; dependentsByVersion: VersionWithDependents[] } { + private getDependents(priorVersions: VersionSubgraph[]): { + totalOutdatedDependents: number; + dependentsByVersion: VersionWithDependents[]; + } { let totalOutdatedDependents = 0; const dependentsByVersion: VersionWithDependents[] = []; priorVersions.forEach((pVersion: VersionSubgraph) => { diff --git a/scopes/harmony/aspect/aspect.env.ts b/scopes/harmony/aspect/aspect.env.ts index 0ed9999799e6..34ec16391d05 100644 --- a/scopes/harmony/aspect/aspect.env.ts +++ b/scopes/harmony/aspect/aspect.env.ts @@ -1,10 +1,12 @@ import { BabelMain } from '@teambit/babel'; import { CompilerAspect, CompilerMain, Compiler } from '@teambit/compiler'; -import { Environment } from '@teambit/envs'; +import { Environment, EnvsMain } from '@teambit/envs'; import { merge } from 'lodash'; import { TsConfigSourceFile } from 'typescript'; +import { PreviewMain } from '@teambit/preview'; import { ReactEnv } from '@teambit/react'; import { babelConfig } from './babel/babel-config'; +// import { BundleEnvTask } from './bundle-env.task'; const tsconfig = require('./typescript/tsconfig.json'); @@ -14,7 +16,13 @@ export const AspectEnvType = 'aspect'; * a component environment built for [Aspects](https://reactjs.org) . */ export class AspectEnv implements Environment { - constructor(private reactEnv: ReactEnv, private babel: BabelMain, private compiler: CompilerMain) {} + constructor( + private reactEnv: ReactEnv, + private babel: BabelMain, + private compiler: CompilerMain, + private envs: EnvsMain, + private preview: PreviewMain + ) {} icon = 'https://static.bit.dev/extensions-icons/default.svg'; @@ -76,10 +84,13 @@ export class AspectEnv implements Environment { const pipeWithoutCompiler = this.reactEnv.getBuildPipe().filter((task) => task.aspectId !== CompilerAspect.id); + // const bundleEnvTask = new BundleEnvTask(this.envs, this.preview); + return [ this.compiler.createTask('TypescriptCompiler', tsCompiler), // for d.ts files this.compiler.createTask('BabelCompiler', babelCompiler), // for dists ...pipeWithoutCompiler, + // bundleEnvTask, ]; } } diff --git a/scopes/harmony/aspect/aspect.main.runtime.ts b/scopes/harmony/aspect/aspect.main.runtime.ts index 72a0b214c798..db5642481b63 100644 --- a/scopes/harmony/aspect/aspect.main.runtime.ts +++ b/scopes/harmony/aspect/aspect.main.runtime.ts @@ -6,6 +6,7 @@ import { ReactAspect, ReactMain } from '@teambit/react'; import { GeneratorAspect, GeneratorMain } from '@teambit/generator'; import { BabelAspect, BabelMain } from '@teambit/babel'; import { CompilerAspect, CompilerMain } from '@teambit/compiler'; +import { PreviewAspect, PreviewMain } from '@teambit/preview'; import { AspectAspect } from './aspect.aspect'; import { AspectEnv } from './aspect.env'; import { CoreExporterTask } from './core-exporter.task'; @@ -26,22 +27,27 @@ export class AspectMain { ReactAspect, EnvsAspect, BuilderAspect, + PreviewAspect, AspectLoaderAspect, CompilerAspect, BabelAspect, GeneratorAspect, ]; - static async provider([react, envs, builder, aspectLoader, compiler, babel, generator]: [ + static async provider([react, envs, builder, preview, aspectLoader, compiler, babel, generator]: [ ReactMain, EnvsMain, BuilderMain, + PreviewMain, AspectLoaderMain, CompilerMain, BabelMain, GeneratorMain ]) { - const aspectEnv = envs.merge(new AspectEnv(react.reactEnv, babel, compiler), react.reactEnv); + const aspectEnv = envs.merge( + new AspectEnv(react.reactEnv, babel, compiler, envs, preview), + react.reactEnv + ); const coreExporterTask = new CoreExporterTask(aspectEnv, aspectLoader); if (!__dirname.includes('@teambit/bit')) { builder.registerBuildTasks([coreExporterTask]); diff --git a/scopes/harmony/cache/cache.spec.ts b/scopes/harmony/cache/cache.spec.ts index 392a9d9f704a..f167721774be 100644 --- a/scopes/harmony/cache/cache.spec.ts +++ b/scopes/harmony/cache/cache.spec.ts @@ -3,8 +3,6 @@ import { rmdirSync } from 'fs'; import { expect } from 'chai'; import { Logger } from '@teambit/logger'; import { CacheMain } from './cache.main.runtime'; - - describe('Cache Aspect', () => { const cacheDirectory = `/tmp/bit/${v4()}`; diff --git a/scopes/html/html/html.aspect.ts b/scopes/html/html/html.aspect.ts index d6cbcd090ed0..c1c5b7830dd9 100644 --- a/scopes/html/html/html.aspect.ts +++ b/scopes/html/html/html.aspect.ts @@ -2,6 +2,5 @@ import { Aspect } from '@teambit/harmony'; export const HtmlAspect = Aspect.create({ id: 'teambit.html/html', - defaultConfig: {} + defaultConfig: {}, }); - \ No newline at end of file diff --git a/scopes/html/html/interfaces.ts b/scopes/html/html/interfaces.ts index 955de2430ef2..ad3ada18bbd4 100644 --- a/scopes/html/html/interfaces.ts +++ b/scopes/html/html/interfaces.ts @@ -1,3 +1,3 @@ -export type HtmlFunctionComposition = (element: HTMLElement) => void +export type HtmlFunctionComposition = (element: HTMLElement) => void; -export type HtmlComposition = HtmlFunctionComposition | string | Element | HTMLDocument; \ No newline at end of file +export type HtmlComposition = HtmlFunctionComposition | string | Element | HTMLDocument; diff --git a/scopes/html/modules/create-element-from-string/create-element-from-string.ts b/scopes/html/modules/create-element-from-string/create-element-from-string.ts index a48e56de392e..96ae4d3fa138 100644 --- a/scopes/html/modules/create-element-from-string/create-element-from-string.ts +++ b/scopes/html/modules/create-element-from-string/create-element-from-string.ts @@ -1,4 +1,4 @@ export function createElementFromString(htmlString: string) { - const htmlFragment = document.createRange().createContextualFragment(htmlString); - return htmlFragment; + const htmlFragment = document.createRange().createContextualFragment(htmlString); + return htmlFragment; } diff --git a/scopes/html/modules/create-element-from-string/index.ts b/scopes/html/modules/create-element-from-string/index.ts index 06ff73a4d835..2a178b61420d 100644 --- a/scopes/html/modules/create-element-from-string/index.ts +++ b/scopes/html/modules/create-element-from-string/index.ts @@ -1 +1 @@ -export { createElementFromString } from './create-element-from-string'; \ No newline at end of file +export { createElementFromString } from './create-element-from-string'; diff --git a/scopes/html/modules/fetch-html-from-url/fetch-html-from-url.ts b/scopes/html/modules/fetch-html-from-url/fetch-html-from-url.ts index 83cb0cd99d2d..b607dee99033 100644 --- a/scopes/html/modules/fetch-html-from-url/fetch-html-from-url.ts +++ b/scopes/html/modules/fetch-html-from-url/fetch-html-from-url.ts @@ -1,6 +1,5 @@ - export async function fetchHtmlFromUrl(url: string) { - return fetch(url) - .then(response => response.text()) - .then(data => data) -} \ No newline at end of file + return fetch(url) + .then((response) => response.text()) + .then((data) => data); +} diff --git a/scopes/html/modules/fetch-html-from-url/index.ts b/scopes/html/modules/fetch-html-from-url/index.ts index b4822166ac6d..3e8434f47bb0 100644 --- a/scopes/html/modules/fetch-html-from-url/index.ts +++ b/scopes/html/modules/fetch-html-from-url/index.ts @@ -1 +1 @@ -export { fetchHtmlFromUrl } from './fetch-html-from-url'; \ No newline at end of file +export { fetchHtmlFromUrl } from './fetch-html-from-url'; diff --git a/scopes/html/modules/render-template/index.ts b/scopes/html/modules/render-template/index.ts index 220b18df0d0c..052923dde3d1 100644 --- a/scopes/html/modules/render-template/index.ts +++ b/scopes/html/modules/render-template/index.ts @@ -1 +1 @@ -export { renderTemplate } from './render-template'; \ No newline at end of file +export { renderTemplate } from './render-template'; diff --git a/scopes/html/modules/render-template/render-template.ts b/scopes/html/modules/render-template/render-template.ts index f232ab51cb36..0bc1cb1084b8 100644 --- a/scopes/html/modules/render-template/render-template.ts +++ b/scopes/html/modules/render-template/render-template.ts @@ -1,5 +1,5 @@ -import { createElementFromString } from '@teambit/html.modules.create-element-from-string' +import { createElementFromString } from '@teambit/html.modules.create-element-from-string'; export function renderTemplate(target: HTMLElement, template: string) { - target.appendChild(createElementFromString(template)); -} \ No newline at end of file + target.appendChild(createElementFromString(template)); +} diff --git a/scopes/preview/preview/bundle-env.task.ts b/scopes/preview/preview/bundle-env.task.ts new file mode 100644 index 000000000000..ea2b5dc904b5 --- /dev/null +++ b/scopes/preview/preview/bundle-env.task.ts @@ -0,0 +1,70 @@ +import { resolve } from 'path'; + +import { BuildTask, BuiltTaskResult, BuildContext } from '@teambit/builder'; +import { PreviewMain } from '@teambit/preview'; +import { EnvsMain, ExecutionContext } from '@teambit/envs'; +import { Bundler, BundlerContext, Target } from '@teambit/bundler'; + +import { PreviewAspect } from './preview.aspect'; +// import { AspectAspect } from './aspect.aspect'; + +export const TASK_NAME = 'GenerateEnvPreview'; + +export class GenerateEnvPreviewTask implements BuildTask { + name = TASK_NAME; + aspectId = PreviewAspect.id; + + constructor(private envs: EnvsMain, private preview: PreviewMain) {} + + async execute(context: BuildContext): Promise { + console.log('im inside bundle env task'); + // const envsIds = this.envs.listEnvsIds(); + // const allEnvResults = await mapSeries( + // envsIds, + // async (envId): Promise => { + // const capsules = context.capsuleNetwork.seedersCapsules; + // const capsule = this.getCapsule(capsules, envId); + // if (!capsule) return undefined; + + const defs = this.preview.getDefs(); + const url = `/preview/${context.envRuntime.id}`; + // TODO: make the name exported from the strategy itself and take it from there + const bundlingStrategy = this.preview.getBundlingStrategy('env-mf'); + + const targets: Target[] = await bundlingStrategy.computeTargets(context, defs); + + const bundlerContext: BundlerContext = Object.assign(context, { + targets, + entry: [], + publicPath: this.getPreviewDirectory(context), + rootPath: url, + }); + + const bundler: Bundler = await context.env.getEnvBundler(bundlerContext); + const bundlerResults = await bundler.run(); + + return bundlingStrategy.computeResults(bundlerContext, bundlerResults); + // } + // ); + + // const finalResult: BuiltTaskResult = { + // componentsResults: [], + // artifacts: [] + // } + // allEnvResults.forEach((envResult) => { + // finalResult.componentsResults = finalResult.componentsResults.concat(envResult?.componentsResults || []) + // finalResult.artifacts = (finalResult.artifacts || []).concat(envResult?.artifacts || []) + // }, finalResult); + // return finalResult; + } + + getPreviewDirectory(context: ExecutionContext) { + const outputPath = resolve(`${context.id}/public`); + return outputPath; + } + + // private getCapsule(capsules: Capsule[], aspectId: string) { + // const aspectCapsuleId = ComponentID.fromString(aspectId).toStringWithoutVersion(); + // return capsules.find((capsule) => capsule.component.id.toStringWithoutVersion() === aspectCapsuleId); + // } +} diff --git a/scopes/preview/preview/bundling-strategy.ts b/scopes/preview/preview/bundling-strategy.ts index d1d7d39b9a65..344a0d2d0f3f 100644 --- a/scopes/preview/preview/bundling-strategy.ts +++ b/scopes/preview/preview/bundling-strategy.ts @@ -12,10 +12,14 @@ export interface BundlingStrategy { /** * compute bundling targets for the build context. */ - computeTargets(context: BuildContext, previewDefs: PreviewDefinition[], previewTask: PreviewTask): Promise; + computeTargets(context: BuildContext, previewDefs: PreviewDefinition[], previewTask?: PreviewTask): Promise; /** * compute the results of the bundler. */ - computeResults(context: BundlerContext, results: BundlerResult[], previewTask: PreviewTask): Promise; + computeResults( + context: BundlerContext, + results: BundlerResult[], + previewTask?: PreviewTask + ): Promise; } diff --git a/scopes/preview/preview/compute-exposes.ts b/scopes/preview/preview/compute-exposes.ts new file mode 100644 index 000000000000..db62da33dbf4 --- /dev/null +++ b/scopes/preview/preview/compute-exposes.ts @@ -0,0 +1,84 @@ +import { Component, ComponentMap } from '@teambit/component'; +import { Compiler } from '@teambit/compiler'; +import { join } from 'path'; +import { AbstractVinyl } from '@teambit/legacy/dist/consumer/component/sources'; +import { BuildContext } from '@teambit/builder'; +import { normalizeMfName } from './normalize-mf-name'; +import { PreviewDefinition } from './preview-definition'; + +export async function computeExposes( + rootPath: string, + defs: PreviewDefinition[], + component: Component, + compiler: Compiler +): Promise> { + const compFullName = component.id.fullName; + const compIdCamel = normalizeMfName(compFullName); + const mainFile = component.state._consumer.mainFile; + const mainFilePath = join(rootPath, compiler.getDistPathBySrcPath(mainFile)); + const exposes = { + [`./${compIdCamel}`]: mainFilePath, + }; + + const moduleMapsPromise = defs.map(async (previewDef) => { + const moduleMap = await previewDef.getModuleMap([component]); + const currentExposes = getExposedModuleByPreviewDefPrefixAndModuleMap( + rootPath, + compFullName, + previewDef.prefix, + moduleMap, + compiler.getDistPathBySrcPath.bind(compiler) + ); + Object.assign(exposes, currentExposes); + }); + + await Promise.all(moduleMapsPromise); + return exposes; +} + +export function getExposedModuleByPreviewDefPrefixAndModuleMap( + rootPath: string, + compFullName: string, + previewDefPrefix: string, + moduleMap: ComponentMap, + getDistPathBySrcPath: (string) => string +): Record { + const paths = moduleMap.map((files) => { + return files.map((file) => join(rootPath, getDistPathBySrcPath(file.relative))); + }); + const exposes = {}; + paths.toArray().map(([, files], index) => { + files.map((filePath) => { + const exposedModule = getExposedModuleByPreviewDefPrefixFileAndIndex( + compFullName, + previewDefPrefix, + filePath, + index + ); + Object.assign(exposes, { + [exposedModule.exposedKey]: exposedModule.exposedVal, + }); + return undefined; + }); + return undefined; + }); + return exposes; +} + +export function getExposedModuleByPreviewDefPrefixFileAndIndex( + compFullName: string, + previewDefPrefix: string, + filePath: string, + index: number +): { exposedKey: string; exposedVal: string } { + const exposedKey = `./${computeExposeKey(compFullName, previewDefPrefix, index)}`; + return { + exposedKey, + exposedVal: filePath, + }; +} + +export function computeExposeKey(componentFullName: string, previewDefPrefix: string, index: number): string { + const compNameNormalized = normalizeMfName(componentFullName); + return `${compNameNormalized}_${previewDefPrefix}_${index}`; +} diff --git a/scopes/preview/preview/create-core-root.ts b/scopes/preview/preview/create-core-root.ts new file mode 100644 index 000000000000..c01d4463534a --- /dev/null +++ b/scopes/preview/preview/create-core-root.ts @@ -0,0 +1,93 @@ +import { AspectDefinition } from '@teambit/aspect-loader'; +import { toWindowsCompatiblePath } from '@teambit/toolbox.path.to-windows-compatible-path'; +import { camelCase } from 'lodash'; +import { parse } from 'path'; + +import { UIAspect } from '../../ui-foundation/ui/ui.aspect'; + +export function createCoreRoot( + aspectDefs: AspectDefinition[], + rootExtensionName?: string, + rootAspect = UIAspect.id, + runtime = 'ui' +) { + const rootId = rootExtensionName ? `'${rootExtensionName}'` : ''; + const coreIdentifiers = getIdentifiers(aspectDefs, 'Aspect'); + + const coreIdSetters = getIdSetters(aspectDefs, 'Aspect'); + + return ` +${createImports(aspectDefs)} + +const isBrowser = typeof window !== "undefined"; +${coreIdSetters.join('\n')} + +export function render(config, hostAspectsIdentifiers = [], ...props){ + if (!isBrowser) return; + const coreIdentifiers = ${coreIdentifiers}; + const allIdentifiers = coreIdentifiers.concat(hostAspectsIdentifiers); + return Harmony.load(allIdentifiers, '${runtime}', config) + .then((harmony) => { + return harmony + .run() + .then(() => { + const rootExtension = harmony.get('${rootAspect}'); + + if (isBrowser) { + return rootExtension.render(${rootId}, ...props); + } else { + return rootExtension.renderSsr(${rootId}, ...props); + } + }) + .catch((err) => { + throw err; + }); + }); +} +`; +} + +function createImports(aspectDefs: AspectDefinition[]) { + const defs = aspectDefs.filter((def) => def.runtimePath); + + return `import { Harmony } from '@teambit/harmony'; +${getImportStatements(aspectDefs, 'aspectPath', 'Aspect')} +${getImportStatements(defs, 'runtimePath', 'Runtime')}`; +} + +function getImportStatements(aspectDefs: AspectDefinition[], pathProp: string, suffix: string): string { + return aspectDefs + .map( + (aspectDef) => + `import ${getIdentifier(aspectDef, suffix)} from '${toWindowsCompatiblePath(aspectDef[pathProp])}';` + ) + .join('\n'); +} + +function getIdentifiers(aspectDefs: AspectDefinition[], suffix: string): string[] { + return aspectDefs.map((aspectDef) => `${getIdentifier(aspectDef, suffix)}`); +} + +function getIdSetters(defs: AspectDefinition[], suffix: string) { + return defs + .map((def) => { + if (!def.getId) return undefined; + return `${getIdentifier(def, suffix)}.id = '${def.getId}';`; + }) + .filter((val) => !!val); +} + +function getIdentifier(aspectDef: AspectDefinition, suffix: string): string { + if (!aspectDef.component && !aspectDef.local) { + return getCoreIdentifier(aspectDef.aspectPath, suffix); + } + return getRegularAspectIdentifier(aspectDef, suffix); +} + +function getRegularAspectIdentifier(aspectDef: AspectDefinition, suffix: string): string { + return camelCase(`${parse(aspectDef.aspectPath).base.replace(/\./, '__').replace('@', '__')}${suffix}`); +} + +function getCoreIdentifier(path: string, suffix: string): string { + return camelCase(`${parse(path).name.split('.')[0]}${suffix}`); +} diff --git a/scopes/preview/preview/create-host-root.ts b/scopes/preview/preview/create-host-root.ts new file mode 100644 index 000000000000..80d99399911c --- /dev/null +++ b/scopes/preview/preview/create-host-root.ts @@ -0,0 +1,65 @@ +import { AspectDefinition } from '@teambit/aspect-loader'; +import { toWindowsCompatiblePath } from '@teambit/toolbox.path.to-windows-compatible-path'; +import { camelCase } from 'lodash'; +import { parse } from 'path'; + +export function createHostRoot(aspectDefs: AspectDefinition[], coreRootPath: string, config = {}) { + const identifiers = getIdentifiers(aspectDefs, 'Aspect'); + const idSetters = getIdSetters(aspectDefs, 'Aspect'); + + return ` +${createImports(aspectDefs)} +const config = JSON.parse('${toWindowsCompatiblePath(JSON.stringify(config))}'); +${idSetters.join('\n')} + +const coreRoot = import('${coreRootPath}').then(coreRoot => { + const render = coreRoot.render; + render(config, [${identifiers.join()}]); +}); +`; +} + +function createImports(aspectDefs: AspectDefinition[]) { + const defs = aspectDefs.filter((def) => def.runtimePath); + + return `import { Harmony } from '@teambit/harmony'; +${getImportStatements(aspectDefs, 'aspectPath', 'Aspect')} +${getImportStatements(defs, 'runtimePath', 'Runtime')}`; +} + +function getImportStatements(aspectDefs: AspectDefinition[], pathProp: string, suffix: string): string { + return aspectDefs + .map( + (aspectDef) => + `import ${getIdentifier(aspectDef, suffix)} from '${toWindowsCompatiblePath(aspectDef[pathProp])}';` + ) + .join('\n'); +} + +function getIdentifiers(aspectDefs: AspectDefinition[] = [], suffix: string): string[] { + return aspectDefs.map((aspectDef) => `${getIdentifier(aspectDef, suffix)}`); +} + +function getIdSetters(defs: AspectDefinition[], suffix: string) { + return defs + .map((def) => { + if (!def.getId) return undefined; + return `${getIdentifier(def, suffix)}.id = '${def.getId}';`; + }) + .filter((val) => !!val); +} + +function getIdentifier(aspectDef: AspectDefinition, suffix: string): string { + if (!aspectDef.component && !aspectDef.local) { + return getCoreIdentifier(aspectDef.aspectPath, suffix); + } + return getRegularAspectIdentifier(aspectDef, suffix); +} + +function getRegularAspectIdentifier(aspectDef: AspectDefinition, suffix: string): string { + return camelCase(`${parse(aspectDef.aspectPath).base.replace(/\./, '__').replace('@', '__')}${suffix}`); +} + +function getCoreIdentifier(path: string, suffix: string): string { + return camelCase(`${parse(path).name.split('.')[0]}${suffix}`); +} diff --git a/scopes/preview/preview/create-root-bootstrap.ts b/scopes/preview/preview/create-root-bootstrap.ts new file mode 100644 index 000000000000..a3ff562433a8 --- /dev/null +++ b/scopes/preview/preview/create-root-bootstrap.ts @@ -0,0 +1,16 @@ +export async function createRootBootstrap(rootPath: string) { + return ` + console.log('create root bootstrap'); + // import React from 'react'; + // const importReactP = import('react'); + // async function load(){ + // await importReactP; + // }() + +export default async function bootstrap() { + // await importReactP; + return import('./${rootPath}') +} +bootstrap(); +`; +} diff --git a/scopes/preview/preview/generate-bootstrap-file.ts b/scopes/preview/preview/generate-bootstrap-file.ts new file mode 100644 index 000000000000..3c46ba16a4fc --- /dev/null +++ b/scopes/preview/preview/generate-bootstrap-file.ts @@ -0,0 +1,7 @@ +export function generateBootstrapFile(filePaths: string[]): string { + return `${filePaths.map(importOneFile).join('\n')}`; +} + +function importOneFile(filePath: string) { + return `import '${filePath}'`; +} diff --git a/scopes/preview/preview/generate-link.ts b/scopes/preview/preview/generate-link.ts index 7a204e3eff2b..198dd6dd3ebc 100644 --- a/scopes/preview/preview/generate-link.ts +++ b/scopes/preview/preview/generate-link.ts @@ -1,5 +1,6 @@ import { toWindowsCompatiblePath } from '@teambit/toolbox.path.to-windows-compatible-path'; import type { ComponentMap } from '@teambit/component'; +import { camelCase } from 'lodash'; // :TODO refactor to building an AST and generate source code based on it. export function generateLink(prefix: string, componentMap: ComponentMap, defaultModule?: string): string { @@ -16,6 +17,6 @@ linkModules('${prefix}', defaultModule, { .join(', ')}]`; }) .join(',\n')} -}); +}); `; } diff --git a/scopes/preview/preview/generate-mf-link.ts b/scopes/preview/preview/generate-mf-link.ts new file mode 100644 index 000000000000..48cb6419c3ed --- /dev/null +++ b/scopes/preview/preview/generate-mf-link.ts @@ -0,0 +1,40 @@ +import { toWindowsCompatiblePath } from '@teambit/toolbox.path.to-windows-compatible-path'; +import type { ComponentMap } from '@teambit/component'; +import { computeExposeKey } from './compute-exposes'; + +// :TODO refactor to building an AST and generate source code based on it. +export function generateMfLink(prefix: string, componentMap: ComponentMap, defaultModule?: string): string { + return ` + console.log('mf link file'); +const promises = [ + // import { linkModules } from '${toWindowsCompatiblePath(require.resolve('./preview.preview.runtime'))}'; + import('${toWindowsCompatiblePath(require.resolve('./preview.preview.runtime'))}').then(Module => Module.linkModules), + // import harmony from '${toWindowsCompatiblePath(require.resolve('@teambit/harmony'))}'; + import('${toWindowsCompatiblePath(require.resolve('@teambit/harmony'))}') +]; +Promise.all(promises).then(([linkModules, harmony]) => { + console.log('inside mf link promise all'); + ${defaultModule ? `const defaultModule = require('${toWindowsCompatiblePath(defaultModule)}'` : ''}); + linkModules('${prefix}', defaultModule, { + ${componentMap + .toArray() + .map(([component, modulePaths]: any) => { + const compFullName = component.id.fullName; + return `'${compFullName}': [${modulePaths + .map((path, index) => { + const exposedKey = computeExposeKey(compFullName, prefix, index); + // TODO: take teambitReactReactMf dynamically + return `() => { + console.log('inside link modules'); + return import('teambitReactReactMf/${exposedKey}').then((Module) => { + console.log('exposedKey module', Module); + return Module; + })}`; + }) + .join(', ')}]`; + }) + .join(',\n')} + }); +}); +`; +} diff --git a/scopes/preview/preview/normalize-mf-name.ts b/scopes/preview/preview/normalize-mf-name.ts new file mode 100644 index 000000000000..1f8d766bd01a --- /dev/null +++ b/scopes/preview/preview/normalize-mf-name.ts @@ -0,0 +1,5 @@ +import { camelCase } from 'lodash'; + +export function normalizeMfName(componentId: string): string { + return camelCase(componentId); +} diff --git a/scopes/preview/preview/preview.main.runtime.tsx b/scopes/preview/preview/preview.main.runtime.tsx index 4dc129f8b761..54fa1a222dea 100644 --- a/scopes/preview/preview/preview.main.runtime.tsx +++ b/scopes/preview/preview/preview.main.runtime.tsx @@ -1,4 +1,6 @@ -import { BuilderAspect, BuilderMain } from '@teambit/builder'; +import { sha1 } from '@teambit/legacy/dist/utils'; +import { Compiler } from '@teambit/compiler'; +import { BuilderAspect, BuilderMain, BuildContext } from '@teambit/builder'; import { BundlerAspect, BundlerMain } from '@teambit/bundler'; import { PubsubAspect, PubsubMain } from '@teambit/pubsub'; import { MainRuntime } from '@teambit/cli'; @@ -8,15 +10,16 @@ import { Slot, SlotRegistry, Harmony } from '@teambit/harmony'; import { UIAspect, UiMain } from '@teambit/ui'; import { CACHE_ROOT } from '@teambit/legacy/dist/constants'; import objectHash from 'object-hash'; -import { uniq } from 'lodash'; -import { writeFileSync, existsSync, mkdirSync } from 'fs-extra'; -import { join } from 'path'; +import { uniq, groupBy } from 'lodash'; +import fs, { writeFileSync, existsSync, mkdirSync } from 'fs-extra'; +import { join, resolve } from 'path'; import { PkgAspect, PkgMain } from '@teambit/pkg'; import { AspectDefinition, AspectLoaderMain, AspectLoaderAspect } from '@teambit/aspect-loader'; import WorkspaceAspect, { Workspace } from '@teambit/workspace'; import { LoggerAspect, LoggerMain, Logger } from '@teambit/logger'; import { PreviewArtifactNotFound, BundlingStrategyNotFound } from './exceptions'; import { generateLink } from './generate-link'; +import { generateMfLink } from './generate-mf-link'; import { PreviewArtifact } from './preview-artifact'; import { PreviewDefinition } from './preview-definition'; import { PreviewAspect, PreviewRuntime } from './preview.aspect'; @@ -26,6 +29,13 @@ import { BundlingStrategy } from './bundling-strategy'; import { EnvBundlingStrategy, ComponentBundlingStrategy } from './strategies'; import { ExecutionRef } from './execution-ref'; import { PreviewStartPlugin } from './preview.start-plugin'; +import { computeExposes } from './compute-exposes'; +import { generateBootstrapFile } from './generate-bootstrap-file'; +import { EnvMfBundlingStrategy } from './strategies/env-mf-strategy'; +import { GenerateEnvPreviewTask } from './bundle-env.task'; +import { createHostRoot } from './create-host-root'; +import { createCoreRoot } from './create-core-root'; +import { createRootBootstrap } from './create-root-bootstrap'; const noopResult = { results: [], @@ -103,6 +113,29 @@ export class PreviewMain { const contents = generateLink(prefix, moduleMap, defaultModule); const hash = objectHash(contents); const targetPath = join(dirName, `__${prefix}-${this.timestamp}.js`); + console.log('targetPath', targetPath); + + // write only if link has changed (prevents triggering fs watches) + if (this.writeHash.get(targetPath) !== hash) { + writeFileSync(targetPath, contents); + this.writeHash.set(targetPath, hash); + } + + return targetPath; + } + + async writeMfLink( + prefix: string, + // context: ExecutionContext, + moduleMap: ComponentMap, + defaultModule: string | undefined, + dirName: string + ) { + // const exposes = await this.computeExposesFromExecutionContext(context); + + const contents = generateMfLink(prefix, moduleMap, defaultModule); + const hash = objectHash(contents); + const targetPath = join(dirName, `__${prefix}-${this.timestamp}.js`); // write only if link has changed (prevents triggering fs watches) if (this.writeHash.get(targetPath) !== hash) { @@ -115,6 +148,7 @@ export class PreviewMain { private executionRefs = new Map(); + // TODO: consolidate code duplication with the env-strategy computePaths logic private async getPreviewTarget( /** execution context (of the specific env) */ context: ExecutionContext @@ -127,11 +161,13 @@ export class PreviewMain { const previewRuntime = await this.writePreviewRuntime(context); const linkFiles = await this.updateLinkFiles(context.components, context); - - return [...linkFiles, previewRuntime]; + // throw new Error('g'); + const { bootstrapFileName } = this.createBootstrapFile([...linkFiles, previewRuntime], context); + const indexEntryPath = this.createIndexEntryFile(bootstrapFileName, context); + return [indexEntryPath]; } - private updateLinkFiles(components: Component[] = [], context: ExecutionContext) { + private async updateLinkFiles(components: Component[] = [], context: ExecutionContext, useMf = true) { const previews = this.previewSlot.values(); const paths = previews.map(async (previewDef) => { const templatePath = await previewDef.renderTemplatePath?.(context); @@ -153,25 +189,136 @@ export class PreviewMain { }); const dirPath = join(this.tempFolder, context.id); + console.log('dirPath', dirPath); if (!existsSync(dirPath)) mkdirSync(dirPath, { recursive: true }); - const link = this.writeLink(previewDef.prefix, withPaths, templatePath, dirPath); + const link = useMf + ? await this.writeMfLink(previewDef.prefix, withPaths, templatePath, dirPath) + : this.writeLink(previewDef.prefix, withPaths, templatePath, dirPath); return link; }); return Promise.all(paths); } - async writePreviewRuntime(context: { components: Component[] }) { + public createIndexEntryFile(bootstrapFileName: string, context: ExecutionContext, rootDir = this.tempFolder) { + const dirName = join(rootDir, context.id); + const contents = `import('./${bootstrapFileName}')`; + const hash = objectHash(contents); + const targetPath = join(dirName, `__index-${this.timestamp}.js`); + console.log('createIndexEntryFile', targetPath); + + // write only if link has changed (prevents triggering fs watches) + if (this.writeHash.get(targetPath) !== hash) { + writeFileSync(targetPath, contents); + this.writeHash.set(targetPath, hash); + } + return targetPath; + } + + public createBootstrapFile(entryFilesPaths: string[], context: ExecutionContext, rootDir = this.tempFolder) { + const contents = generateBootstrapFile(entryFilesPaths); + const dirName = join(rootDir, context.id); + const hash = objectHash(contents); + const fileName = `__bootstrap-${this.timestamp}.js`; + const targetPath = join(dirName, fileName); + console.log('createBootstrapFile', targetPath); + + // write only if link has changed (prevents triggering fs watches) + if (this.writeHash.get(targetPath) !== hash) { + writeFileSync(targetPath, contents); + this.writeHash.set(targetPath, hash); + } + return { bootstrapPath: targetPath, bootstrapFileName: fileName }; + } + + async writePreviewRuntime(context: { components: Component[] }, rootDir = this.tempFolder) { const ui = this.ui.getUi(); if (!ui) throw new Error('ui not found'); const [name, uiRoot] = ui; const resolvedAspects = await uiRoot.resolveAspects(PreviewRuntime.name); const filteredAspects = this.filterAspectsByExecutionContext(resolvedAspects, context); - const filePath = await this.ui.generateRoot(filteredAspects, name, 'preview', PreviewAspect.id); + const filePath = await this.generateRootForMf(filteredAspects, name, 'preview', PreviewAspect.id, rootDir); + console.log('filePath', filePath); return filePath; } + /** + * generate the root file of the UI runtime. + */ + async generateRootForMf( + aspectDefs: AspectDefinition[], + rootExtensionName: string, + runtimeName = PreviewRuntime.name, + rootAspect = UIAspect.id, + rootTempDir = this.tempFolder + ) { + // const rootRelativePath = `${runtimeName}.root${sha1(contents)}.js`; + // const filepath = resolve(join(__dirname, rootRelativePath)); + const aspectsGroups = groupBy(aspectDefs, (def) => { + const id = def.getId; + if (!id) return 'host'; + if (this.aspectLoader.isCoreAspect(id)) return 'core'; + return 'host'; + }); + + // const coreRootFilePath = this.writeCoreUiRoot(aspectsGroups.core, rootExtensionName, runtimeName, rootAspect); + const { fullPath: coreRootFilePath } = this.writeCoreUiRoot( + aspectsGroups.core, + rootExtensionName, + runtimeName, + rootAspect + ); + const hostRootFilePath = this.writeHostUIRoot(aspectsGroups.host, coreRootFilePath, runtimeName, rootTempDir); + + const rootBootstrapContents = await createRootBootstrap(hostRootFilePath.relativePath); + const rootBootstrapRelativePath = `${runtimeName}.root${sha1(rootBootstrapContents)}-bootstrap.js`; + const rootBootstrapPath = resolve(join(rootTempDir, rootBootstrapRelativePath)); + if (fs.existsSync(rootBootstrapPath)) return rootBootstrapPath; + fs.outputFileSync(rootBootstrapPath, rootBootstrapContents); + console.log('rootBootstrapPath', rootBootstrapPath); + throw new Error('g'); + return rootBootstrapPath; + } + + /** + * Generate a file which contains all the core ui aspects and the harmony config to load them + * This will get an harmony config, and host specific aspects to load + * and load the harmony instance + */ + private writeCoreUiRoot( + coreAspects: AspectDefinition[], + rootExtensionName: string, + runtimeName = UIRuntime.name, + rootAspect = UIAspect.id + ) { + const contents = createCoreRoot(coreAspects, rootExtensionName, rootAspect, runtimeName); + const rootRelativePath = `${runtimeName}.core.root.${sha1(contents)}.js`; + const filepath = resolve(join(__dirname, rootRelativePath)); + console.log('core ui root', filepath); + if (fs.existsSync(filepath)) return { fullPath: filepath, relativePath: rootRelativePath }; + fs.outputFileSync(filepath, contents); + return { fullPath: filepath, relativePath: rootRelativePath }; + } + + /** + * Generate a file which contains host (workspace/scope) specific ui aspects. and the harmony config to load them + */ + private writeHostUIRoot( + hostAspects: AspectDefinition[] = [], + coreRootPath: string, + runtimeName = UIRuntime.name, + rootTempDir = this.tempFolder + ) { + const contents = createHostRoot(hostAspects, coreRootPath, this.harmony.config.toObject()); + const rootRelativePath = `${runtimeName}.host.root.${sha1(contents)}.js`; + const filepath = resolve(join(rootTempDir, rootRelativePath)); + console.log('host ui root', filepath); + if (fs.existsSync(filepath)) return { fullPath: filepath, relativePath: rootRelativePath }; + fs.outputFileSync(filepath, contents); + return { fullPath: filepath, relativePath: rootRelativePath }; + } + /** * Filter the aspects to have only aspects that are: * 1. core aspects @@ -197,7 +344,7 @@ export class PreviewMain { } private getDefaultStrategies() { - return [new EnvBundlingStrategy(this), new ComponentBundlingStrategy()]; + return [new EnvBundlingStrategy(this), new ComponentBundlingStrategy(), new EnvMfBundlingStrategy(this)]; } // TODO - executionContext should be responsible for updating components list, and emit 'update' events @@ -236,9 +383,8 @@ export class PreviewMain { /** * return the configured bundling strategy. */ - getBundlingStrategy(): BundlingStrategy { + getBundlingStrategy(strategyName = this.config.bundlingStrategy): BundlingStrategy { const defaultStrategies = this.getDefaultStrategies(); - const strategyName = this.config.bundlingStrategy; const strategies = this.bundlingStrategySlot.values().concat(defaultStrategies); const selected = strategies.find((strategy) => { return strategy.name === strategyName; @@ -264,6 +410,25 @@ export class PreviewMain { this.previewSlot.register(previewDef); } + async computeExposesFromExecutionContext( + context: ExecutionContext + // context: BuildContext + ): Promise> { + const defs = this.getDefs(); + const components = context.components; + const compiler = context.envRuntime.env.getCompiler(); + const allExposes = {}; + const promises = components.map(async (component) => { + const componentModulePath = this.workspace.componentModulePath(component); + const exposes = await computeExposes(componentModulePath, defs, component, compiler); + Object.assign(allExposes, exposes); + return undefined; + }); + await Promise.all(promises); + + return allExposes; + } + static slots = [Slot.withType(), Slot.withType()]; static runtime = MainRuntime; @@ -324,10 +489,12 @@ export class PreviewMain { bundler.registerTarget([ { entry: preview.getPreviewTarget.bind(preview), + exposes: preview.computeExposesFromExecutionContext.bind(preview), }, ]); - if (!config.disabled) builder.registerBuildTasks([new PreviewTask(bundler, preview)]); + if (!config.disabled) + builder.registerBuildTasks([new PreviewTask(bundler, preview), new GenerateEnvPreviewTask(envs, preview)]); if (workspace) { workspace.registerOnComponentAdd((c) => diff --git a/scopes/preview/preview/preview.preview.runtime.tsx b/scopes/preview/preview/preview.preview.runtime.tsx index d27feaa9c232..f522c75306b1 100644 --- a/scopes/preview/preview/preview.preview.runtime.tsx +++ b/scopes/preview/preview/preview.preview.runtime.tsx @@ -44,7 +44,7 @@ export class PreviewPreview { /** * render the preview. */ - render = () => { + render = async () => { const { previewName, componentId } = this.getLocation(); const name = previewName || this.getDefault(); @@ -52,16 +52,16 @@ export class PreviewPreview { if (!preview || !componentId) { throw new PreviewNotFound(previewName); } - const includes = (preview.include || []) - .map((prevName) => { - const includedPreview = this.getPreview(prevName); - if (!includedPreview) return undefined; + const includesP = (preview.include || []).map((prevName) => { + const includedPreview = this.getPreview(prevName); + if (!includedPreview) return undefined; - return includedPreview.selectPreviewModel?.(componentId.fullName, PREVIEW_MODULES[prevName]); - }) - .filter((module) => !!module); + return includedPreview.selectPreviewModel?.(componentId.fullName, PREVIEW_MODULES[prevName]); + }); + const includes = await Promise.all(includesP); + const filteredIncludes = includes.filter((module) => !!module); - return preview.render(componentId.fullName, PREVIEW_MODULES[name], includes, this.getRenderingContext()); + return preview.render(componentId.fullName, PREVIEW_MODULES[name], filteredIncludes, this.getRenderingContext()); }; /** diff --git a/scopes/preview/preview/preview.route.ts b/scopes/preview/preview/preview.route.ts index e63c18ac5649..a9df8e7205b8 100644 --- a/scopes/preview/preview/preview.route.ts +++ b/scopes/preview/preview/preview.route.ts @@ -20,6 +20,7 @@ export class PreviewRoute implements Route { const component: any = req.component as any; if (!component) throw new Error(`preview failed to get a component object, url ${req.url}`); const artifact = await this.preview.getPreview(component); + console.log('im here'); // TODO: please fix file path concatenation here. const file = artifact.getFile(`public/${req.params.previewPath || 'index.html'}`); // TODO: 404 again how to handle. diff --git a/scopes/preview/preview/preview.task.ts b/scopes/preview/preview/preview.task.ts index a1125f7d0018..788e70eeb8de 100644 --- a/scopes/preview/preview/preview.task.ts +++ b/scopes/preview/preview/preview.task.ts @@ -1,14 +1,9 @@ -import { resolve, join } from 'path'; +import { resolve } from 'path'; import { ExecutionContext } from '@teambit/envs'; import { BuildContext, BuiltTaskResult, BuildTask, TaskLocation } from '@teambit/builder'; import { Bundler, BundlerContext, BundlerMain, Target } from '@teambit/bundler'; -import { Compiler } from '@teambit/compiler'; -import { ComponentMap } from '@teambit/component'; -import { Capsule } from '@teambit/isolator'; -import { AbstractVinyl } from '@teambit/legacy/dist/consumer/component/sources'; -import { flatten } from 'lodash'; -import { PreviewDefinition } from './preview-definition'; import { PreviewMain } from './preview.main.runtime'; +import { PreviewAspect } from './preview.aspect'; export class PreviewTask implements BuildTask { constructor( @@ -23,7 +18,7 @@ export class PreviewTask implements BuildTask { private preview: PreviewMain ) {} - aspectId = 'teambit.preview/preview'; + aspectId = PreviewAspect.id; name = 'GeneratePreview'; location: TaskLocation = 'end'; @@ -41,51 +36,14 @@ export class PreviewTask implements BuildTask { rootPath: url, }); - const bundler: Bundler = await context.env.getBundler(bundlerContext, []); + const bundler: Bundler = await context.env.getBundler(bundlerContext); const bundlerResults = await bundler.run(); return bundlingStrategy.computeResults(bundlerContext, bundlerResults, this); } - async computePaths(capsule: Capsule, defs: PreviewDefinition[], context: BuildContext): Promise { - const previewMain = await this.preview.writePreviewRuntime(context); - - const moduleMapsPromise = defs.map(async (previewDef) => { - const moduleMap = await previewDef.getModuleMap([capsule.component]); - const paths = this.getPathsFromMap(capsule, moduleMap, context); - const template = previewDef.renderTemplatePath ? await previewDef.renderTemplatePath(context) : 'undefined'; - - const link = this.preview.writeLink( - previewDef.prefix, - paths, - previewDef.renderTemplatePath ? await previewDef.renderTemplatePath(context) : undefined, - capsule.path - ); - - const files = flatten(paths.toArray().map(([, file]) => file)).concat([link]); - - if (template) return files.concat([template]); - return files; - }); - - const moduleMaps = await Promise.all(moduleMapsPromise); - - return flatten(moduleMaps.concat([previewMain])); - } - getPreviewDirectory(context: ExecutionContext) { const outputPath = resolve(`${context.id}/public`); return outputPath; } - - getPathsFromMap( - capsule: Capsule, - moduleMap: ComponentMap, - context: BuildContext - ): ComponentMap { - const compiler: Compiler = context.env.getCompiler(context); - return moduleMap.map((files) => { - return files.map((file) => join(capsule.path, compiler.getDistPathBySrcPath(file.relative))); - }); - } } diff --git a/scopes/preview/preview/strategies/component-strategy.ts b/scopes/preview/preview/strategies/component-strategy.ts index 9d05e072badb..29238e846700 100644 --- a/scopes/preview/preview/strategies/component-strategy.ts +++ b/scopes/preview/preview/strategies/component-strategy.ts @@ -1,34 +1,89 @@ -import { BuildContext } from '@teambit/builder'; +import { Compiler } from '@teambit/compiler'; +import { ComponentMap } from '@teambit/component'; +import { Capsule } from '@teambit/isolator'; +import { AbstractVinyl } from '@teambit/legacy/dist/consumer/component/sources'; +import { join } from 'path'; +import { ArtifactDefinition, BuildContext } from '@teambit/builder'; import { Target, BundlerResult, BundlerContext } from '@teambit/bundler'; +import fs from 'fs-extra'; import { BundlingStrategy } from '../bundling-strategy'; import { PreviewDefinition } from '../preview-definition'; import { PreviewTask } from '../preview.task'; +import { normalizeMfName } from '../normalize-mf-name'; +import { computeExposes } from '../compute-exposes'; export class ComponentBundlingStrategy implements BundlingStrategy { name = 'component'; - computeTargets(context: BuildContext, previewDefs: PreviewDefinition[], previewTask: PreviewTask): Promise { + computeTargets(context: BuildContext, previewDefs: PreviewDefinition[]): Promise { return Promise.all( - context.capsuleNetwork.graphCapsules.map(async (capsule) => { + context.capsuleNetwork.seedersCapsules.map(async (capsule) => { + const component = capsule.component; + const entry = await this.writeEmptyEntryFile(capsule); + const exposes = await this.computeExposes(capsule, previewDefs, context); return { - entries: await previewTask.computePaths(capsule, previewDefs, context), - components: [capsule.component], + entries: [entry], + mfName: normalizeMfName(component.id.fullName), + mfExposes: exposes, + components: [component], outputPath: capsule.path, }; }) ); } - async computeResults(context: BundlerContext, results: BundlerResult[], previewTask: PreviewTask) { + async computeExposes( + capsule: Capsule, + defs: PreviewDefinition[], + context: BuildContext + ): Promise> { + return computeExposes(capsule.path, defs, capsule.component, context.env.getCompiler()); + } + + async writeEmptyEntryFile(capsule: Capsule): Promise { + const tempFolder = join(capsule.path, '__temp'); + await fs.ensureDir(tempFolder); + const filePath = join(tempFolder, 'emptyFile.js'); + await fs.writeFile(filePath, ''); + return filePath; + } + + async computeResults(context: BundlerContext, results: BundlerResult[]) { + const componentsResults = results.map((result) => { + return { + errors: result.errors, + component: result.components[0], + warning: result.warnings, + }; + }); + const artifacts = this.getArtifactDef(); + + console.log('comp strategy, componentsResults', componentsResults); + console.log('comp strategy, artifacts', artifacts); return { - componentsResults: results.map((result) => { - return { - errors: result.errors, - component: result.components[0], - warning: result.warnings, - }; - }), - artifacts: [{ name: 'preview', globPatterns: [previewTask.getPreviewDirectory(context)] }], + componentsResults, + artifacts, }; } + + private getArtifactDef(): ArtifactDefinition[] { + return [ + { + name: 'federated-module', + globPatterns: ['public/**'], + description: 'a federated module of the component', + }, + ]; + } + + getPathsFromMap( + capsule: Capsule, + moduleMap: ComponentMap, + context: BuildContext + ): ComponentMap { + const compiler: Compiler = context.env.getCompiler(context); + return moduleMap.map((files) => { + return files.map((file) => join(capsule.path, compiler.getDistPathBySrcPath(file.relative))); + }); + } } diff --git a/scopes/preview/preview/strategies/env-mf-strategy.ts b/scopes/preview/preview/strategies/env-mf-strategy.ts new file mode 100644 index 000000000000..683e4bdeb3dc --- /dev/null +++ b/scopes/preview/preview/strategies/env-mf-strategy.ts @@ -0,0 +1,126 @@ +import { join, resolve } from 'path'; +import { existsSync, mkdirpSync } from 'fs-extra'; +import { flatten } from 'lodash'; +import { ComponentMap } from '@teambit/component'; +import { Compiler } from '@teambit/compiler'; +import { AbstractVinyl } from '@teambit/legacy/dist/consumer/component/sources'; +import { Capsule } from '@teambit/isolator'; +import { BuildContext, ComponentResult } from '@teambit/builder'; +import { BundlerResult, BundlerContext } from '@teambit/bundler'; +import { BundlingStrategy } from '../bundling-strategy'; +import { PreviewDefinition } from '../preview-definition'; +import { PreviewMain } from '../preview.main.runtime'; + +/** + * bundles all components in a given env into the same bundle. + */ +export class EnvMfBundlingStrategy implements BundlingStrategy { + name = 'env-mf'; + + constructor(private preview: PreviewMain) {} + + async computeTargets(context: BuildContext, previewDefs: PreviewDefinition[]) { + const outputPath = this.getOutputPath(context); + console.log('computeTargets'); + console.log('outputPath', outputPath); + if (!existsSync(outputPath)) mkdirpSync(outputPath); + const entries = await this.computePaths(outputPath, previewDefs, context); + + return [ + { + entries, + components: context.components, + outputPath, + }, + ]; + } + + async computeResults(context: BundlerContext, results: BundlerResult[]) { + const result = results[0]; + + const componentsResults: ComponentResult[] = result.components.map((component) => { + return { + component, + errors: result.errors.map((err) => (typeof err === 'string' ? err : err.message)), + warning: result.warnings, + }; + }); + + const artifacts = this.getArtifactDef(context); + + console.log('componentsResults', componentsResults); + console.log('artifacts', artifacts); + + return { + componentsResults, + artifacts, + }; + } + + private getArtifactDef(context: BuildContext) { + // eslint-disable-next-line @typescript-eslint/prefer-as-const + const env: 'env' = 'env'; + const rootDir = this.getDirName(context); + + return [ + { + name: 'preview', + globPatterns: ['public/**'], + rootDir, + context: env, + }, + ]; + } + + getDirName(context: BuildContext) { + const envName = context.id.replace('/', '__'); + return `${envName}-preview`; + } + + private getOutputPath(context: BuildContext) { + return resolve(`${context.capsuleNetwork.capsulesRootDir}/${this.getDirName(context)}`); + } + + private getPaths(context: BuildContext, files: AbstractVinyl[], capsule: Capsule) { + const compiler: Compiler = context.env.getCompiler(); + return files.map((file) => join(capsule.path, compiler.getDistPathBySrcPath(file.relative))); + } + + private async computePaths(outputPath: string, defs: PreviewDefinition[], context: BuildContext): Promise { + const previewMain = await this.preview.writePreviewRuntime(context); + const linkFilesP = defs.map(async (previewDef) => { + const moduleMap = await previewDef.getModuleMap(context.components); + + const paths = ComponentMap.as(context.components, (component) => { + const capsule = context.capsuleNetwork.graphCapsules.getCapsule(component.id); + const maybeFiles = moduleMap.byComponent(component); + if (!maybeFiles || !capsule) return []; + const [, files] = maybeFiles; + const compiledPaths = this.getPaths(context, files, capsule); + return compiledPaths; + }); + + // const template = previewDef.renderTemplatePath ? await previewDef.renderTemplatePath(context) : 'undefined'; + + const link = await this.preview.writeMfLink( + previewDef.prefix, + paths, + previewDef.renderTemplatePath ? await previewDef.renderTemplatePath(context) : undefined, + outputPath + ); + + // const files = flatten(paths.toArray().map(([, file]) => file)).concat([link]); + + // if (template) return files.concat([template]); + // return files; + return link; + }); + const linkFiles = await Promise.all(linkFilesP); + + const { bootstrapFileName } = this.preview.createBootstrapFile([...linkFiles, previewMain], context); + const indexEntryPath = this.preview.createIndexEntryFile(bootstrapFileName, context); + return [indexEntryPath]; + + // return flatten(moduleMaps.concat([previewMain])); + } +} diff --git a/scopes/preview/preview/strategies/env-strategy.ts b/scopes/preview/preview/strategies/env-strategy.ts index e9cf0b873479..13d16aeef793 100644 --- a/scopes/preview/preview/strategies/env-strategy.ts +++ b/scopes/preview/preview/strategies/env-strategy.ts @@ -21,11 +21,14 @@ export class EnvBundlingStrategy implements BundlingStrategy { async computeTargets(context: BuildContext, previewDefs: PreviewDefinition[]) { const outputPath = this.getOutputPath(context); + console.log('computeTargets'); + console.log('outputPath', outputPath); if (!existsSync(outputPath)) mkdirpSync(outputPath); + const entries = await this.computePaths(outputPath, previewDefs, context); return [ { - entries: await this.computePaths(outputPath, previewDefs, context), + entries, components: context.components, outputPath, }, diff --git a/scopes/react/react/bootstrap.tsx b/scopes/react/react/bootstrap.tsx new file mode 100644 index 000000000000..98052df952ff --- /dev/null +++ b/scopes/react/react/bootstrap.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { RenderingContext } from '@teambit/preview'; + +import { CompositionsApp } from './compositions-app'; + +/** + * this mounts compositions into the DOM in the component preview. + * this function can be overridden through ReactAspect.overrideCompositionsMounter() API + * to apply custom logic for component DOM mounting. + */ +export default (Composition: React.ComponentType, previewContext: RenderingContext) => { + ReactDOM.render( + , + document.getElementById('root') + ); +}; diff --git a/scopes/react/react/mount.tsx b/scopes/react/react/mount.tsx index 98052df952ff..5fd0d505bb64 100644 --- a/scopes/react/react/mount.tsx +++ b/scopes/react/react/mount.tsx @@ -1,17 +1,6 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import { RenderingContext } from '@teambit/preview'; - -import { CompositionsApp } from './compositions-app'; - -/** - * this mounts compositions into the DOM in the component preview. - * this function can be overridden through ReactAspect.overrideCompositionsMounter() API - * to apply custom logic for component DOM mounting. - */ -export default (Composition: React.ComponentType, previewContext: RenderingContext) => { - ReactDOM.render( - , - document.getElementById('root') - ); -}; +export default function bootstrap(arg1, arg2) { + console.log('arg1', arg1); + console.log('arg2', arg2); + // eslint-disable-next-line + import('./bootstrap').then((module) => module.default(arg1, arg2)); +} diff --git a/scopes/react/react/react.env.ts b/scopes/react/react/react.env.ts index c689047527fe..f3827e31d0d9 100644 --- a/scopes/react/react/react.env.ts +++ b/scopes/react/react/react.env.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'os'; import { Component } from '@teambit/component'; import { ComponentUrl } from '@teambit/component.modules.component-url'; import { BuildTask } from '@teambit/builder'; -import { merge, omit } from 'lodash'; +import { camelCase, merge, omit } from 'lodash'; import { Bundler, BundlerContext, DevServer, DevServerContext } from '@teambit/bundler'; import { CompilerMain } from '@teambit/compiler'; import { @@ -32,12 +32,10 @@ import type { ComponentMeta } from '@teambit/react.babel.bit-react-transformer'; import { SchemaExtractor } from '@teambit/schema'; import { join, resolve } from 'path'; import { outputFileSync } from 'fs-extra'; -import { Configuration } from 'webpack'; // Makes sure the @teambit/react.ui.docs-app is a dependency // TODO: remove this import once we can set policy from component to component with workspace version. Then set it via the component.json // eslint-disable-next-line @typescript-eslint/no-unused-vars -import { ReactMainConfig } from './react.main.runtime'; -import { ReactAspect } from './react.aspect'; +import docs from '@teambit/react.ui.docs-app'; // webpack configs for both components and envs import basePreviewConfigFactory from './webpack/webpack.config.base'; @@ -45,12 +43,17 @@ import basePreviewProdConfigFactory from './webpack/webpack.config.base.prod'; // webpack configs for envs only // import devPreviewConfigFactory from './webpack/webpack.config.preview.dev'; +import envPreviewBaseConfigFactory from './webpack/webpack.config.env.base'; import envPreviewDevConfigFactory from './webpack/webpack.config.env.dev'; // webpack configs for components only +import componentPreviewBaseConfigFactory from './webpack/webpack.config.component.base'; import componentPreviewProdConfigFactory from './webpack/webpack.config.component.prod'; import componentPreviewDevConfigFactory from './webpack/webpack.config.component.dev'; +import { ReactMainConfig } from './react.main.runtime'; +import { ReactAspect } from './react.aspect'; + export const AspectEnvType = 'react'; const defaultTsConfig = require('./typescript/tsconfig.json'); const buildTsConfig = require('./typescript/tsconfig.build.json'); @@ -234,54 +237,124 @@ export class ReactEnv * required for `bit start` */ getDevServer(context: DevServerContext, transformers: WebpackConfigTransformer[] = []): DevServer { + console.log('context.entry', context.entry); const baseConfig = basePreviewConfigFactory(false); + const mfName = camelCase(`${context.id.toString()}_MF`); + // TODO: take the port dynamically + const port = context.port; + const rootPath = context.rootPath; + + const envBaseConfig = envPreviewBaseConfigFactory(mfName, 'http://localhost', port, rootPath || ''); + console.log('envBaseConfig', require('util').inspect(envBaseConfig, { depth: 10 })); + const envDevConfig = envPreviewDevConfigFactory(context.id); + // const fileMapPath = this.writeFileMap(context.components, true); + + const componentBaseConfig = componentPreviewBaseConfigFactory(mfName, context.exposes); // const componentDevConfig = componentPreviewDevConfigFactory(fileMapPath, this.workspace.path); - // const componentDevConfig = componentPreviewDevConfigFactory(this.workspace.path, context.id); const componentDevConfig = componentPreviewDevConfigFactory(this.workspace.path); - + // const defaultConfig = this.getDevWebpackConfig(context); const defaultTransformer: WebpackConfigTransformer = (configMutator) => { - const merged = configMutator.merge([baseConfig, envDevConfig, componentDevConfig]); + const merged = configMutator.merge([ + baseConfig, + envBaseConfig, + envDevConfig, + componentBaseConfig, + componentDevConfig, + ]); + const allMfInstances = merged.raw.plugins?.filter( + (plugin) => plugin.constructor.name === 'ModuleFederationPlugin' + ); + if (!allMfInstances || allMfInstances?.length < 2) { + return merged; + } + const mergedMfConfig = allMfInstances.reduce((acc, curr) => { + // @ts-ignore + return Object.assign(acc, curr._options); + }, {}); + // @ts-ignore + allMfInstances[0]._options = mergedMfConfig; + const mutatedPlugins = merged.raw.plugins?.filter( + (plugin) => plugin.constructor.name !== 'ModuleFederationPlugin' + ); + mutatedPlugins?.push(allMfInstances[0]); + merged.raw.plugins = mutatedPlugins; return merged; }; return this.webpack.createDevServer(context, [defaultTransformer, ...transformers]); } - async getBundler(context: BundlerContext, transformers: WebpackConfigTransformer[] = []): Promise { - // const fileMapPath = this.writeFileMap(context.components); + /** + * returns and configures the React component dev server. + */ + // getEnvDevServer(context: DevServerContext, transformers: WebpackConfigTransformer[] = []): DevServer { + // console.log('context.entry', context.entry); + // const baseConfig = basePreviewConfigFactory(false); + // const envBaseConfig = envPreviewBaseConfigFactory(); + // const envDevConfig = envPreviewDevConfigFactory(context.id); + // // const defaultConfig = this.getDevWebpackConfig(context); + // const defaultTransformer: WebpackConfigTransformer = (configMutator) => { + // return configMutator.merge([baseConfig, envBaseConfig, envDevConfig]); + // }; + + // return this.webpack.createDevServer(context, [defaultTransformer, ...transformers]); + // } + + /** + * returns and configures the React component dev server. + */ + // getComponentsDevServers(context: DevServerContext, transformers: WebpackConfigTransformer[] = []): DevServer { + // // const defaultConfig = this.getDevWebpackConfig(context); + // const fileMapPath = this.writeFileMap(context.components, true); + // const mfName = camelCase(`${context.id.toString()}_MF`); + // const baseConfig = basePreviewConfigFactory(false); + + // const componentBaseConfig = componentPreviewBaseConfigFactory(mfName); + // const componentDevConfig = componentPreviewDevConfigFactory(fileMapPath, this.workspace.path); + // const defaultTransformer: WebpackConfigTransformer = (configMutator) => { + // return configMutator.merge([baseConfig, componentBaseConfig, componentDevConfig]); + // }; + + // return this.webpack.createDevServer(context, [defaultTransformer, ...transformers]); + // } + + async getEnvBundler(context: BundlerContext, transformers: WebpackConfigTransformer[] = []): Promise { const baseConfig = basePreviewConfigFactory(true); const baseProdConfig = basePreviewProdConfigFactory(); - // const componentProdConfig = componentPreviewProdConfigFactory(fileMapPath); - const componentProdConfig = componentPreviewProdConfigFactory(); + const mfName = camelCase(`${context.id.toString()}_MF`); + // TODO: take the port dynamically + const port = 3000; + const rootPath = context.rootPath; + const envBaseConfig = envPreviewBaseConfigFactory(mfName, 'http://localhost', port, rootPath || ''); const defaultTransformer: WebpackConfigTransformer = (configMutator) => { - const merged = configMutator.merge([baseConfig, baseProdConfig, componentProdConfig]); - return merged; + return configMutator.merge([baseConfig, baseProdConfig, envBaseConfig]); }; return this.webpack.createBundler(context, [defaultTransformer, ...transformers]); } - private getEntriesFromWebpackConfig(config?: Configuration): string[] { - if (!config || !config.entry) { - return []; - } - if (typeof config.entry === 'string') { - return [config.entry]; - } - if (Array.isArray(config.entry)) { - let entries: string[] = []; - entries = config.entry.reduce((acc, entry) => { - if (typeof entry === 'string') { - acc.push(entry); - } - return acc; - }, entries); - return entries; - } - return []; + async getComponentBundler(context: BundlerContext, transformers: WebpackConfigTransformer[] = []): Promise { + // const fileMapPath = this.writeFileMap(context.components); + const baseConfig = basePreviewConfigFactory(true); + const baseProdConfig = basePreviewProdConfigFactory(); + // const prodComponentConfig = componentPreviewProdConfigFactory(fileMapPath); + const prodComponentConfig = componentPreviewProdConfigFactory(); + const defaultTransformer: WebpackConfigTransformer = (configMutator, mutatorContext) => { + if (!mutatorContext.target?.mfName) { + throw new Error(`missing module federation name for ${mutatorContext.target?.components[0].id.toString()}`); + } + const baseComponentConfig = componentPreviewBaseConfigFactory( + mutatorContext.target?.mfName, + mutatorContext.target?.mfExposes + ); + + return configMutator.merge([baseConfig, baseProdConfig, baseComponentConfig, prodComponentConfig]); + }; + + return this.webpack.createComponentsBundler(context, [defaultTransformer, ...transformers]); } /** diff --git a/scopes/react/react/typescript/tsconfig.build.json b/scopes/react/react/typescript/tsconfig.build.json index b0368944f941..5ef6d695c215 100644 --- a/scopes/react/react/typescript/tsconfig.build.json +++ b/scopes/react/react/typescript/tsconfig.build.json @@ -3,8 +3,8 @@ "lib": [ "es2019", "DOM", "ES6" ,"DOM.Iterable" ], - "target": "es2015", - "module": "commonjs", + "target": "es5", + "module": "es6", "jsx": "react", "allowJs": true, "composite": true, diff --git a/scopes/react/react/webpack/webpack.config.base.prod.ts b/scopes/react/react/webpack/webpack.config.base.prod.ts index 3e800267c5ef..c396ac8f7bdd 100644 --- a/scopes/react/react/webpack/webpack.config.base.prod.ts +++ b/scopes/react/react/webpack/webpack.config.base.prod.ts @@ -3,6 +3,9 @@ import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; import TerserPlugin from 'terser-webpack-plugin'; import { Configuration } from 'webpack'; import { WebpackManifestPlugin } from 'webpack-manifest-plugin'; +// Make sure the bit-react-transformer is a dependency +// TODO: remove it once we can set policy from component to component then set it via the component.json +import '@teambit/react.babel.bit-react-transformer'; // Source maps are resource heavy and can cause out of memory issue for large source files. const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false'; @@ -13,45 +16,45 @@ const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false'; export default function (): Configuration { return { optimization: { - minimize: true, + // minimize: true, minimizer: [ // This is only used in production mode - new TerserPlugin({ - terserOptions: { - parse: { - // We want terser to parse ecma 8 code. However, we don't want it - // to apply any minification steps that turns valid ecma 5 code - // into invalid ecma 5 code. This is why the 'compress' and 'output' - // sections only apply transformations that are ecma 5 safe - // https://github.com/facebook/create-react-app/pull/4234 - ecma: 8, - }, - compress: { - ecma: 5, - warnings: false, - // Disabled because of an issue with Uglify breaking seemingly valid code: - // https://github.com/facebook/create-react-app/issues/2376 - // Pending further investigation: - // https://github.com/mishoo/UglifyJS2/issues/2011 - comparisons: false, - // Disabled because of an issue with Terser breaking valid code: - // https://github.com/facebook/create-react-app/issues/5250 - // Pending further investigation: - // https://github.com/terser-js/terser/issues/120 - inline: 2, - }, - mangle: { - safari10: true, - }, - output: { - ecma: 5, - comments: false, - // Turned on because emoji and regex is not minified properly using default - // https://github.com/facebook/create-react-app/issues/2488 - ascii_only: true, - }, - }, - }), + // new TerserPlugin({ + // terserOptions: { + // parse: { + // // We want terser to parse ecma 8 code. However, we don't want it + // // to apply any minification steps that turns valid ecma 5 code + // // into invalid ecma 5 code. This is why the 'compress' and 'output' + // // sections only apply transformations that are ecma 5 safe + // // https://github.com/facebook/create-react-app/pull/4234 + // ecma: 8, + // }, + // compress: { + // ecma: 5, + // warnings: false, + // // Disabled because of an issue with Uglify breaking seemingly valid code: + // // https://github.com/facebook/create-react-app/issues/2376 + // // Pending further investigation: + // // https://github.com/mishoo/UglifyJS2/issues/2011 + // comparisons: false, + // // Disabled because of an issue with Terser breaking valid code: + // // https://github.com/facebook/create-react-app/issues/5250 + // // Pending further investigation: + // // https://github.com/terser-js/terser/issues/120 + // inline: 2, + // }, + // mangle: { + // safari10: true, + // }, + // output: { + // ecma: 5, + // comments: false, + // // Turned on because emoji and regex is not minified properly using default + // // https://github.com/facebook/create-react-app/issues/2488 + // ascii_only: true, + // }, + // }, + // }), new CssMinimizerPlugin({ sourceMap: shouldUseSourceMap, minimizerOptions: { @@ -67,16 +70,16 @@ export default function (): Configuration { // Automatically split vendor and commons // https://twitter.com/wSokra/status/969633336732905474 // https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366 - splitChunks: { - chunks: 'all', - // name: false, - }, + // splitChunks: { + // chunks: 'all', + // // name: false, + // }, // Keep the runtime chunk separated to enable long term caching // https://twitter.com/wSokra/status/969679223278505985 // https://github.com/facebook/create-react-app/issues/5358 - runtimeChunk: { - name: (entrypoint) => `runtime-${entrypoint.name}`, - }, + // runtimeChunk: { + // name: (entrypoint) => `runtime-${entrypoint.name}`, + // }, }, plugins: [ diff --git a/scopes/react/react/webpack/webpack.config.base.ts b/scopes/react/react/webpack/webpack.config.base.ts index 37b85b140bbc..e5d17d19f493 100644 --- a/scopes/react/react/webpack/webpack.config.base.ts +++ b/scopes/react/react/webpack/webpack.config.base.ts @@ -119,7 +119,6 @@ export default function (isEnvProduction = false): Configuration { // See https://github.com/webpack/webpack/issues/6571 sideEffects: true, }, - // Process application JS with Babel. // The preset includes JSX, Flow, TypeScript, and some ESnext features. { @@ -170,7 +169,7 @@ export default function (isEnvProduction = false): Configuration { // MDX support (move to the mdx aspect and extend from there) { test: /\.mdx?$/, - exclude: [/node_modules/], + // exclude: [/node_modules/], use: [ { loader: require.resolve('babel-loader'), @@ -337,6 +336,27 @@ export default function (isEnvProduction = false): Configuration { ], }, plugins: [ + new webpack.container.ModuleFederationPlugin({ + // library: { + // type: 'var', + // name: 'module_federation_namespace', + // }, + // TODO: add the version of button to the name + // remoteType: 'commonjs', + // TODO: think about it (maybe we want eager in component level but not in env level) + shared: { + react: { + eager: true, + singleton: true, + requiredVersion: '^17.0.0', + }, + 'react-dom': { + eager: true, + singleton: true, + requiredVersion: '^17.0.0', + }, + }, + }), new MiniCssExtractPlugin({ // Options similar to the same options in webpackOptions.output // both options are optional @@ -348,6 +368,7 @@ export default function (isEnvProduction = false): Configuration { // solution that requires the user to opt into importing specific locales. // https://github.com/jmblog/how-to-optimize-momentjs-with-webpack // You can remove this if you don't use Moment.js: + new webpack.IgnorePlugin({ resourceRegExp: /^\.\/locale$/, contextRegExp: /moment$/, diff --git a/scopes/react/react/webpack/webpack.config.component.base.ts b/scopes/react/react/webpack/webpack.config.component.base.ts new file mode 100644 index 000000000000..3af58d911cf2 --- /dev/null +++ b/scopes/react/react/webpack/webpack.config.component.base.ts @@ -0,0 +1,25 @@ +import webpack, { Configuration } from 'webpack'; +import { ComponentID } from '@teambit/component-id'; + +// This is the production and development configuration. +// It is focused on developer experience, fast rebuilds, and a minimal bundle. +// eslint-disable-next-line complexity +export default function (mfName: string, mfExposes: Record = {}): Configuration { + return { + plugins: [ + new webpack.container.ModuleFederationPlugin({ + filename: 'remote-entry.js', + // name: 'module_federation_namespace', + name: mfName, + exposes: mfExposes, + // exposes: { + // TODO: take the dist file programmatically + // [`./${buttonId}`]: '/Users/giladshoham/Library/Caches/Bit/capsules/d3522af33785e04e8b1199864b9f46951ea3c008/my-scope_ui_button/dist/button.js', + // [`./${buttonId}_composition`]: '/Users/giladshoham/Library/Caches/Bit/capsules/d3522af33785e04e8b1199864b9f46951ea3c008/my-scope_ui_button/dist/button.composition.js', + // [`./${buttonId}_docs`]: '/Users/giladshoham/Library/Caches/Bit/capsules/d3522af33785e04e8b1199864b9f46951ea3c008/my-scope_ui_button/dist/button.docs.js', + // [`./${buttonId}`]: '/Users/giladshoham/Library/Caches/Bit/capsules/d3522af33785e04e8b1199864b9f46951ea3c008/my-scope_ui_button/dist/button.js', + // }, + }), + ], + }; +} diff --git a/scopes/react/react/webpack/webpack.config.env.base.ts b/scopes/react/react/webpack/webpack.config.env.base.ts new file mode 100644 index 000000000000..a1c0fddf4ce8 --- /dev/null +++ b/scopes/react/react/webpack/webpack.config.env.base.ts @@ -0,0 +1,23 @@ +import webpack, { Configuration } from 'webpack'; + +// This is the production and development configuration. +// It is focused on developer experience, fast rebuilds, and a minimal bundle. +// eslint-disable-next-line complexity +export default function ( + mfName: string, + server: string, + port = 3000, + rootPath: string, + remoteEntryName = 'remote-entry.js' +): Configuration { + return { + plugins: [ + new webpack.container.ModuleFederationPlugin({ + // TODO: implement + remotes: { + [mfName]: `${mfName}@${server}:${port}${rootPath}/${remoteEntryName}`, + }, + }), + ], + }; +} diff --git a/scopes/react/ui/docs-app/bootstrap.tsx b/scopes/react/ui/docs-app/bootstrap.tsx new file mode 100644 index 000000000000..1114d4850477 --- /dev/null +++ b/scopes/react/ui/docs-app/bootstrap.tsx @@ -0,0 +1,36 @@ +import { RenderingContext } from '@teambit/preview'; +import React from 'react'; +import ReactDOM from 'react-dom'; + +import { DocsApp } from './docs-app'; +import type { DocsFile } from './examples-overview/example'; + +export type ReactDocsRootParams = [ + /* Provider: */ React.ComponentType | undefined, + /* componentId: */ string, + /* docs: */ DocsFile | undefined, + /* compositions: */ Record, + /* context: */ RenderingContext +]; + +export default function DocsRoot( + Provider: React.ComponentType | undefined, + componentId: string, + docs: DocsFile | undefined, + compositions: any, + context: RenderingContext +) { + ReactDOM.render( + , + document.getElementById('root') + ); +} + +// hot reloading works when components are in a different file. +// do not declare react components here. diff --git a/scopes/react/ui/docs-app/index.tsx b/scopes/react/ui/docs-app/index.tsx index 1114d4850477..984e3476e055 100644 --- a/scopes/react/ui/docs-app/index.tsx +++ b/scopes/react/ui/docs-app/index.tsx @@ -1,36 +1,9 @@ -import { RenderingContext } from '@teambit/preview'; -import React from 'react'; -import ReactDOM from 'react-dom'; +export type { ReactDocsRootParams } from './bootstrap'; -import { DocsApp } from './docs-app'; -import type { DocsFile } from './examples-overview/example'; +export default async function bootstrap(arg1, arg2, arg3, arg4, arg5) { + // eslint-disable-next-line + debugger; -export type ReactDocsRootParams = [ - /* Provider: */ React.ComponentType | undefined, - /* componentId: */ string, - /* docs: */ DocsFile | undefined, - /* compositions: */ Record, - /* context: */ RenderingContext -]; - -export default function DocsRoot( - Provider: React.ComponentType | undefined, - componentId: string, - docs: DocsFile | undefined, - compositions: any, - context: RenderingContext -) { - ReactDOM.render( - , - document.getElementById('root') - ); + // const resolvedCompositions = await arg3; + return import('./bootstrap').then((module) => module.default(arg1, arg2, arg3, arg4, arg5)); } - -// hot reloading works when components are in a different file. -// do not declare react components here. diff --git a/scopes/react/ui/docs-app/symlink-component-list.md b/scopes/react/ui/docs-app/symlink-component-list.md index 6a87e9bef25a..128df3cc4b7b 100644 --- a/scopes/react/ui/docs-app/symlink-component-list.md +++ b/scopes/react/ui/docs-app/symlink-component-list.md @@ -1,5 +1,6 @@ # necessary symlinks to create to run project -***or change imports in `base.tsx` to local clone of react-new-project*** + +**_or change imports in `base.tsx` to local clone of react-new-project_** - @bit/bit.test-scope.ui.consumable-link - @bit/bit.test-scope.ui.property-table @@ -7,7 +8,8 @@ - @bit/bit.test-scope.ui.section ## examples: -ln -s ~/dev/bit/react-new-project/node_modules/@bit/bit.test-scope.ui.consumable-link ./node_modules/@bit/bit.test-scope.ui.consumable-link -ln -s {path to react new project}/node_modules/@bit/bit.test-scope.ui.property-table ./node_modules/@bit/bit.test-scope.ui.property-table -ln -s {path to react new project}/node_modules/@bit/bit.test-scope.ui.linked-heading ./node_modules/@bit/bit.test-scope.ui.linked-heading -ln -s {path to react new project}/node_modules/@bit/bit.test-scope.ui.section ./node_modules/@bit/bit.test-scope.ui.section + +ln -s ~/dev/bit/react-new-project/node_modules/@bit/bit.test-scope.ui.consumable-link ./node_modules/@bit/bit.test-scope.ui.consumable-link +ln -s {path to react new project}/node_modules/@bit/bit.test-scope.ui.property-table ./node_modules/@bit/bit.test-scope.ui.property-table +ln -s {path to react new project}/node_modules/@bit/bit.test-scope.ui.linked-heading ./node_modules/@bit/bit.test-scope.ui.linked-heading +ln -s {path to react new project}/node_modules/@bit/bit.test-scope.ui.section ./node_modules/@bit/bit.test-scope.ui.section diff --git a/scopes/scope/scope/scope.main.runtime.ts b/scopes/scope/scope/scope.main.runtime.ts index c5012a43d587..21fc3d4338c0 100644 --- a/scopes/scope/scope/scope.main.runtime.ts +++ b/scopes/scope/scope/scope.main.runtime.ts @@ -372,6 +372,18 @@ export class ScopeMain implements ComponentFactory { }); } + /** + * Provides a temp folder, unique per key. + */ + getTempDir( + /* + * unique key, i.e. aspect or component id + */ + id: string + ): string { + return this.legacyScope.tmp.composePath(id); + } + async getResolvedAspects(components: Component[]) { if (!components.length) return []; const network = await this.isolator.isolateComponents( diff --git a/scopes/toolbox/network/get-port/locked.ts b/scopes/toolbox/network/get-port/locked.ts index 21dec7665f6c..24f1210ef3d5 100644 --- a/scopes/toolbox/network/get-port/locked.ts +++ b/scopes/toolbox/network/get-port/locked.ts @@ -1,5 +1,5 @@ export class Locked extends Error { - constructor(port: number) { - super(`${port} is locked`); - } - } \ No newline at end of file + constructor(port: number) { + super(`${port} is locked`); + } +} diff --git a/scopes/ui-foundation/ui/ui.main.runtime.ts b/scopes/ui-foundation/ui/ui.main.runtime.ts index af3cc4d606d2..6368684f8154 100644 --- a/scopes/ui-foundation/ui/ui.main.runtime.ts +++ b/scopes/ui-foundation/ui/ui.main.runtime.ts @@ -2,11 +2,13 @@ import type { AspectMain } from '@teambit/aspect'; import { ComponentType } from 'react'; import { AspectDefinition } from '@teambit/aspect-loader'; import { CacheAspect, CacheMain } from '@teambit/cache'; +import { AspectLoaderAspect, AspectLoaderMain } from '@teambit/aspect-loader'; import { CLIAspect, CLIMain, MainRuntime } from '@teambit/cli'; import type { ComponentMain } from '@teambit/component'; import { ComponentAspect } from '@teambit/component'; import { ExpressAspect, ExpressMain } from '@teambit/express'; import type { GraphqlMain } from '@teambit/graphql'; +import { CACHE_ROOT } from '@teambit/legacy/dist/constants'; import { GraphqlAspect } from '@teambit/graphql'; import chalk from 'chalk'; import { Slot, SlotRegistry, Harmony } from '@teambit/harmony'; @@ -19,7 +21,6 @@ import { join, resolve } from 'path'; import { promisify } from 'util'; import webpack from 'webpack'; import { UiServerStartedEvent } from './events'; -import { createRoot } from './create-root'; import { UnknownUI, UnknownBuildError } from './exceptions'; import { StartCmd } from './start.cmd'; import { UIBuildCmd } from './ui-build.cmd'; @@ -30,8 +31,19 @@ import { OpenBrowser } from './open-browser'; import createWebpackConfig from './webpack/webpack.browser.config'; import createSsrWebpackConfig from './webpack/webpack.ssr.config'; import { StartPlugin, StartPluginOptions } from './start-plugin'; +import { createRoot } from './create-root'; -export type UIDeps = [PubsubMain, CLIMain, GraphqlMain, ExpressMain, ComponentMain, CacheMain, LoggerMain, AspectMain]; +export type UIDeps = [ + PubsubMain, + CLIMain, + GraphqlMain, + ExpressMain, + ComponentMain, + CacheMain, + LoggerMain, + AspectLoaderMain, + AspectMain +]; export type UIRootRegistry = SlotRegistry; @@ -113,6 +125,8 @@ export type RuntimeOptions = { rebuild?: boolean; }; +const DEFAULT_TEMP_DIR = join(CACHE_ROOT, UIAspect.id); + export class UiMain { constructor( /** @@ -172,11 +186,17 @@ export class UiMain { */ private logger: Logger, + private aspectLoader: AspectLoaderMain, + private harmony: Harmony, private startPluginSlot: StartPluginSlot ) {} + get tempFolder(): string { + return this.componentExtension.getHost()?.getTempDir(UIAspect.id) || DEFAULT_TEMP_DIR; + } + async publicDir(uiRoot: UIRoot) { const overwriteFn = this.getOverwritePublic(); if (overwriteFn) { @@ -213,7 +233,7 @@ export class UiMain { const [name, uiRoot] = maybeUiRoot; // TODO: @uri refactor all dev server related code to use the bundler extension instead. - const ssr = uiRoot.buildOptions?.ssr || false; + const ssr = false; const mainEntry = await this.generateRoot(await uiRoot.resolveAspects(UIRuntime.name), name); const browserConfig = createWebpackConfig(uiRoot.path, [mainEntry], uiRoot.name, await this.publicDir(uiRoot)); @@ -410,6 +430,7 @@ export class UiMain { this.harmony.config.toObject() ); const filepath = resolve(join(__dirname, `${runtimeName}.root${sha1(contents)}.js`)); + console.log('generateRoot path', filepath); if (fs.existsSync(filepath)) return filepath; fs.outputFileSync(filepath, contents); return filepath; @@ -510,6 +531,7 @@ export class UiMain { ComponentAspect, CacheAspect, LoggerAspect, + AspectLoaderAspect, ]; static slots = [ @@ -522,7 +544,7 @@ export class UiMain { ]; static async provider( - [pubsub, cli, graphql, express, componentExtension, cache, loggerMain]: UIDeps, + [pubsub, cli, graphql, express, componentExtension, cache, loggerMain, aspectLoader]: UIDeps, config, [uiRootSlot, preStartSlot, onStartSlot, publicDirOverwriteSlot, buildMethodOverwriteSlot, proxyGetterSlot]: [ UIRootRegistry, @@ -550,6 +572,7 @@ export class UiMain { componentExtension, cache, logger, + aspectLoader, harmony, proxyGetterSlot ); diff --git a/scopes/webpack/webpack/config/webpack.config.ts b/scopes/webpack/webpack/config/webpack.config.ts index f9246f635934..cfbc520474f5 100644 --- a/scopes/webpack/webpack/config/webpack.config.ts +++ b/scopes/webpack/webpack/config/webpack.config.ts @@ -24,11 +24,12 @@ export function configFactory(entries: string[], rootPath: string): Configuratio // webpack uses `publicPath` to determine where the app is being served from. // It requires a trailing slash, or the file assets will get an incorrect path. // We inferred the "public path" (such as / or /my-project) from homepage. - publicPath: ``, + publicPath: 'auto', // this defaults to 'window', but by setting it to 'this' then // module chunks which are built will work in web workers as well. // Commented out to use the default (self) as according to tobias with webpack5 self is working with workers as well // globalObject: 'this', + uniqueName: 'react_env_namespace', }, resolve: { diff --git a/scopes/webpack/webpack/webpack.main.runtime.ts b/scopes/webpack/webpack/webpack.main.runtime.ts index fc73192d19b7..221477f7d640 100644 --- a/scopes/webpack/webpack/webpack.main.runtime.ts +++ b/scopes/webpack/webpack/webpack.main.runtime.ts @@ -25,6 +25,7 @@ import { WebpackDevServer } from './webpack.dev-server'; export type WebpackConfigTransformContext = { mode: BundlerMode; + target?: Target; }; export type WebpackConfigTransformer = ( config: WebpackConfigMutator, @@ -69,7 +70,26 @@ export class WebpackMain { const configMutator = new WebpackConfigMutator(config); const transformerContext: WebpackConfigTransformContext = { mode: 'dev' }; const afterMutation = runTransformersWithContext(configMutator.clone(), transformers, transformerContext); + console.log(afterMutation.raw.entry); + // @ts-ignore - fix this + return new WebpackDevServer(afterMutation.raw, webpack, WsDevServer); + } + createComponentDevServer(context: DevServerContext, transformers: WebpackConfigTransformer[] = []): DevServer { + const config = this.createDevServerConfig( + context.entry, + this.workspace.path, + context.id, + context.rootPath, + context.publicPath, + context.title + ) as any; + const configMutator = new WebpackConfigMutator(config); + const transformerContext: WebpackConfigTransformContext = { + mode: 'dev', + }; + const afterMutation = runTransformersWithContext(configMutator.clone(), transformers, transformerContext); + console.log(afterMutation.raw.entry); // @ts-ignore - fix this return new WebpackDevServer(afterMutation.raw, webpack, WsDevServer); } @@ -86,6 +106,20 @@ export class WebpackMain { const afterMutation = runTransformersWithContext(configMutator.clone(), transformers, transformerContext); return afterMutation.raw; }); + return new WebpackBundler(context.targets, mutatedConfigs, this.logger); + } + + createComponentsBundler(context: BundlerContext, transformers: WebpackConfigTransformer[] = []) { + const mutatedConfigs = context.targets.map((target) => { + const baseConfig = previewConfigFactory(target.entries, target.outputPath); + const transformerContext: WebpackConfigTransformContext = { + mode: 'prod', + target, + }; + const configMutator = new WebpackConfigMutator(baseConfig); + const afterMutation = runTransformersWithContext(configMutator.clone(), transformers, transformerContext); + return afterMutation.raw; + }); return new WebpackBundler(context.targets, mutatedConfigs, this.logger, webpack); } diff --git a/scopes/workspace/workspace/workspace.ts b/scopes/workspace/workspace/workspace.ts index 594686e8f058..1d715f5b3973 100644 --- a/scopes/workspace/workspace/workspace.ts +++ b/scopes/workspace/workspace/workspace.ts @@ -1060,6 +1060,12 @@ export class Workspace implements ComponentFactory { return cacheDir; } + componentModulePath(component: Component): string { + const packageName = componentIdToPackageName(component.state._consumer); + const modulePath = path.join(this.path, 'node_modules', packageName); + return modulePath; + } + async requireComponents(components: Component[]): Promise { let missingPaths = false; const stringIds: string[] = [];