From 17d28c131712d4a84737fbc3d91f5dd759f19cfa Mon Sep 17 00:00:00 2001 From: Gilad Shoham Date: Tue, 18 May 2021 23:12:38 +0300 Subject: [PATCH 01/18] WIP - first draft --- .../webpack/webpack.main.runtime.ts | 1 + .../preview/preview/preview.main.runtime.tsx | 3 ++ .../preview/strategies/env-strategy.ts | 1 + scopes/react/react/bootstrap.tsx | 48 +++++++++++++++++ scopes/react/react/docs/bootstrap.tsx | 28 ++++++++++ scopes/react/react/docs/index.tsx | 30 ++--------- scopes/react/react/mount.tsx | 52 ++----------------- .../webpack/webpack.config.preview.dev.ts | 45 +++++++++++----- 8 files changed, 121 insertions(+), 87 deletions(-) create mode 100644 scopes/react/react/bootstrap.tsx create mode 100644 scopes/react/react/docs/bootstrap.tsx diff --git a/scopes/compilation/webpack/webpack.main.runtime.ts b/scopes/compilation/webpack/webpack.main.runtime.ts index f562dae5f714..097a47d40bd6 100644 --- a/scopes/compilation/webpack/webpack.main.runtime.ts +++ b/scopes/compilation/webpack/webpack.main.runtime.ts @@ -71,6 +71,7 @@ export class WebpackMain { mode: 'dev', }; const afterMutation = runTransformersWithContext(configMutator.clone(), transformers, transformerContext); + console.log(afterMutation.raw.entry); // @ts-ignore - fix this return new WebpackDevServer(afterMutation.raw); } diff --git a/scopes/preview/preview/preview.main.runtime.tsx b/scopes/preview/preview/preview.main.runtime.tsx index e42ccaaf2129..550fb62d37af 100644 --- a/scopes/preview/preview/preview.main.runtime.tsx +++ b/scopes/preview/preview/preview.main.runtime.tsx @@ -83,6 +83,7 @@ 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) { @@ -121,6 +122,7 @@ 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); @@ -140,6 +142,7 @@ export class PreviewMain { 'preview', PreviewAspect.id ); + console.log('filePath', filePath); return filePath; } diff --git a/scopes/preview/preview/strategies/env-strategy.ts b/scopes/preview/preview/strategies/env-strategy.ts index dfe4a1d87d0c..9f5a7118fc6d 100644 --- a/scopes/preview/preview/strategies/env-strategy.ts +++ b/scopes/preview/preview/strategies/env-strategy.ts @@ -21,6 +21,7 @@ export class EnvBundlingStrategy implements BundlingStrategy { async computeTargets(context: BuildContext, previewDefs: PreviewDefinition[]) { const outputPath = this.getOutputPath(context); + console.log(outputPath); if (!existsSync(outputPath)) mkdirpSync(outputPath); return [ diff --git a/scopes/react/react/bootstrap.tsx b/scopes/react/react/bootstrap.tsx new file mode 100644 index 000000000000..89e918c1cb6a --- /dev/null +++ b/scopes/react/react/bootstrap.tsx @@ -0,0 +1,48 @@ +import React, { ComponentType, ReactNode } from 'react'; +import ReactDOM from 'react-dom'; +import { RenderingContext } from '@teambit/preview'; +import { StandaloneNotFoundPage } from '@teambit/ui.pages.standalone-not-found-page'; +import { ReactAspect } from './react.aspect'; + +function wrap(Component: ComponentType, WrapperComponent?: ComponentType): ComponentType { + function Wrapper({ children }: { children?: ReactNode }) { + if (!WrapperComponent) return {children}; + + return ( + + {children} + + ); + } + + return Wrapper; +} + +/** + * HOC to wrap and mount all registered providers into the DOM. + */ +export function withProviders(providers: ComponentType[] = []) { + return providers.reduce( + (MainProvider, Provider) => { + if (!MainProvider) return wrap(Provider); + return wrap(Provider, MainProvider); + }, + ({ children }) =>
{children}
+ ); +} + +/** + * 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 = StandaloneNotFoundPage, previewContext: RenderingContext) => { + const reactContext = previewContext.get(ReactAspect.id); + const Provider = withProviders(reactContext?.providers); + ReactDOM.render( + + + , + document.getElementById('root') + ); +}; diff --git a/scopes/react/react/docs/bootstrap.tsx b/scopes/react/react/docs/bootstrap.tsx new file mode 100644 index 000000000000..5fac5769350b --- /dev/null +++ b/scopes/react/react/docs/bootstrap.tsx @@ -0,0 +1,28 @@ +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 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/react/docs/index.tsx b/scopes/react/react/docs/index.tsx index 5fac5769350b..cf6b458b1914 100644 --- a/scopes/react/react/docs/index.tsx +++ b/scopes/react/react/docs/index.tsx @@ -1,28 +1,4 @@ -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 default function DocsRoot( - Provider: React.ComponentType | undefined, - componentId: string, - docs: DocsFile | undefined, - compositions: any, - context: RenderingContext -) { - ReactDOM.render( - , - document.getElementById('root') - ); +export default function bootstrap(arg1, arg2, arg3, arg4, arg5) { + // eslint-disable-next-line + 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/react/mount.tsx b/scopes/react/react/mount.tsx index 89e918c1cb6a..5fd0d505bb64 100644 --- a/scopes/react/react/mount.tsx +++ b/scopes/react/react/mount.tsx @@ -1,48 +1,6 @@ -import React, { ComponentType, ReactNode } from 'react'; -import ReactDOM from 'react-dom'; -import { RenderingContext } from '@teambit/preview'; -import { StandaloneNotFoundPage } from '@teambit/ui.pages.standalone-not-found-page'; -import { ReactAspect } from './react.aspect'; - -function wrap(Component: ComponentType, WrapperComponent?: ComponentType): ComponentType { - function Wrapper({ children }: { children?: ReactNode }) { - if (!WrapperComponent) return {children}; - - return ( - - {children} - - ); - } - - return Wrapper; +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)); } - -/** - * HOC to wrap and mount all registered providers into the DOM. - */ -export function withProviders(providers: ComponentType[] = []) { - return providers.reduce( - (MainProvider, Provider) => { - if (!MainProvider) return wrap(Provider); - return wrap(Provider, MainProvider); - }, - ({ children }) =>
{children}
- ); -} - -/** - * 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 = StandaloneNotFoundPage, previewContext: RenderingContext) => { - const reactContext = previewContext.get(ReactAspect.id); - const Provider = withProviders(reactContext?.providers); - ReactDOM.render( - - - , - document.getElementById('root') - ); -}; diff --git a/scopes/react/react/webpack/webpack.config.preview.dev.ts b/scopes/react/react/webpack/webpack.config.preview.dev.ts index dc7d0ba5d73f..e20144a44703 100644 --- a/scopes/react/react/webpack/webpack.config.preview.dev.ts +++ b/scopes/react/react/webpack/webpack.config.preview.dev.ts @@ -1,5 +1,5 @@ import '@teambit/ui.mdx-scope-context'; -import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'; +// import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'; import { ComponentID } from '@teambit/component-id'; import path from 'path'; // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -45,6 +45,7 @@ export default function ({ envId, fileMapPath, workDir }: Options): WebpackConfi path: `_hmr/${envId}`, }, }, + cache: false, module: { rules: [ { @@ -104,7 +105,7 @@ export default function ({ envId, fileMapPath, workDir }: Options): WebpackConfi require.resolve('babel-preset-react-app'), ], plugins: [ - require.resolve('react-refresh/babel'), + // require.resolve('react-refresh/babel'), // for component highlighting in preview. [ require.resolve('@teambit/babel.bit-react-transformer'), @@ -127,7 +128,7 @@ export default function ({ envId, fileMapPath, workDir }: Options): WebpackConfi babelrc: false, configFile: false, presets: [require.resolve('@babel/preset-react'), require.resolve('@babel/preset-env')], - plugins: [require.resolve('react-refresh/babel')], + // plugins: [require.resolve('react-refresh/babel')], }, }, { @@ -245,17 +246,35 @@ export default function ({ envId, fileMapPath, workDir }: Options): WebpackConfi }, plugins: [ - new ReactRefreshWebpackPlugin({ - overlay: { - sockPath: `_hmr/${envId}`, - // TODO: check why webpackHotDevClient and react-error-overlay are not responding for runtime - // errors - entry: require.resolve('./react-hot-dev-client'), - module: require.resolve('./refresh'), + // new ReactRefreshWebpackPlugin({ + // overlay: { + // sockPath: `_hmr/${envId}`, + // // TODO: check why webpackHotDevClient and react-error-overlay are not responding for runtime + // // errors + // entry: require.resolve('./react-hot-dev-client'), + // module: require.resolve('./refresh'), + // }, + // include: [/\.(js|jsx|tsx|ts|mdx|md)$/], + // // TODO: use a more specific exclude for our selfs + // exclude: [/dist/, /node_modules/], + // }), + new webpack.container.ModuleFederationPlugin({ + // remoteType: 'commonjs', + shared: { + react: { + eager: true, + singleton: true, + requiredVersion: '^16.14.0', + }, + 'react-dom': { + eager: true, + singleton: true, + requiredVersion: '^16.14.0', + }, + }, + remotes: { + // 'versioned-federated-module': 'versioned-federated-module', }, - include: [/\.(js|jsx|tsx|ts|mdx|md)$/], - // TODO: use a more specific exclude for our selfs - exclude: [/dist/, /node_modules/], }), ], }; From 8047bc2452f8a5f8643186646310c5e39c7a2d83 Mon Sep 17 00:00:00 2001 From: Gilad Shoham Date: Thu, 20 May 2021 13:36:47 +0300 Subject: [PATCH 02/18] first draft of federated button --- .../webpack/config/webpack.config.ts | 3 +- .../react/typescript/tsconfig.build.json | 4 +- .../react/webpack/webpack.config.preview.ts | 122 +++++++++++------- 3 files changed, 82 insertions(+), 47 deletions(-) diff --git a/scopes/compilation/webpack/config/webpack.config.ts b/scopes/compilation/webpack/config/webpack.config.ts index 8b36a40d2428..9db547b0d63c 100644 --- a/scopes/compilation/webpack/config/webpack.config.ts +++ b/scopes/compilation/webpack/config/webpack.config.ts @@ -23,11 +23,12 @@ export function configFactory(entries, rootPath): Configuration { // 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', + library: 'module_federation_namespace', }, resolve: { 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.preview.ts b/scopes/react/react/webpack/webpack.config.preview.ts index 92048944fd3e..7f77530d6374 100644 --- a/scopes/react/react/webpack/webpack.config.preview.ts +++ b/scopes/react/react/webpack/webpack.config.preview.ts @@ -6,6 +6,7 @@ import webpack, { Configuration } from 'webpack'; import { WebpackManifestPlugin } from 'webpack-manifest-plugin'; import WorkboxWebpackPlugin from 'workbox-webpack-plugin'; import * as stylesRegexps from '@teambit/modules.style-regexps'; +import fs from 'fs-extra'; import { postCssConfig } from './postcss.config'; // 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 @@ -36,6 +37,10 @@ const imageInlineSizeLimit = parseInt(process.env.IMAGE_INLINE_SIZE_LIMIT || '10 // It is focused on developer experience, fast rebuilds, and a minimal bundle. // eslint-disable-next-line complexity export default function (fileMapPath: string): Configuration { + const fileMapContent = JSON.parse(fs.readFileSync(fileMapPath).toString('utf8')); + const buttonIndexFilePath: string = + Object.keys(fileMapContent).find((filePath) => filePath.includes('index')) || fileMapContent[0]; + const buttonId = fileMapContent[buttonIndexFilePath].id; const isEnvProduction = true; // Variable used for enabling profiling in Production @@ -91,45 +96,45 @@ export default function (fileMapPath: string): 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: { @@ -145,16 +150,16 @@ export default function (fileMapPath: string): 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}`, + // }, }, resolve: { // These are the reasonable defaults supported by the Node ecosystem. @@ -400,6 +405,35 @@ export default function (fileMapPath: string): Configuration { ], }, plugins: [ + new webpack.container.ModuleFederationPlugin({ + filename: 'remote-entry.js', + name: 'module_federation_namespace', + library: { + type: 'var', + name: 'module_federation_namespace', + }, + // TODO: add the version of button to the name + // remoteType: 'commonjs', + shared: { + react: { + eager: true, + singleton: true, + requiredVersion: '^16.14.0', + }, + 'react-dom': { + eager: true, + singleton: true, + requiredVersion: '^16.14.0', + }, + }, + exposes: { + // TODO: take the dist file programmatically + [`./${buttonId}`]: '/Users/giladshoham/Library/Caches/Bit/capsules/d3522af33785e04e8b1199864b9f46951ea3c008/my-scope_ui_button/dist/button.js', + }, + remotes: { + // 'versioned-federated-module': 'versioned-federated-module', + }, + }), new MiniCssExtractPlugin({ // Options similar to the same options in webpackOptions.output // both options are optional From 267df71a6fb029632d944c5a8bafa0975c98b68a Mon Sep 17 00:00:00 2001 From: Gilad Shoham Date: Thu, 20 May 2021 13:45:09 +0300 Subject: [PATCH 03/18] do no use library var --- scopes/react/react/webpack/webpack.config.preview.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scopes/react/react/webpack/webpack.config.preview.ts b/scopes/react/react/webpack/webpack.config.preview.ts index 7f77530d6374..916caf115169 100644 --- a/scopes/react/react/webpack/webpack.config.preview.ts +++ b/scopes/react/react/webpack/webpack.config.preview.ts @@ -408,10 +408,10 @@ export default function (fileMapPath: string): Configuration { new webpack.container.ModuleFederationPlugin({ filename: 'remote-entry.js', name: 'module_federation_namespace', - library: { - type: 'var', - name: 'module_federation_namespace', - }, + // library: { + // type: 'var', + // name: 'module_federation_namespace', + // }, // TODO: add the version of button to the name // remoteType: 'commonjs', shared: { From 04810599964bfbf42e5ff182b01f34d76e156046 Mon Sep 17 00:00:00 2001 From: Gilad Shoham Date: Thu, 20 May 2021 13:48:59 +0300 Subject: [PATCH 04/18] change library to uniqeName --- scopes/compilation/webpack/config/webpack.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scopes/compilation/webpack/config/webpack.config.ts b/scopes/compilation/webpack/config/webpack.config.ts index 9db547b0d63c..f4fd5cfd910a 100644 --- a/scopes/compilation/webpack/config/webpack.config.ts +++ b/scopes/compilation/webpack/config/webpack.config.ts @@ -28,7 +28,7 @@ export function configFactory(entries, rootPath): Configuration { // 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', - library: 'module_federation_namespace', + uniqueName: 'react_env_namespace', }, resolve: { From 02091b0d1c624dfefe30595f13afda92ba16a2a8 Mon Sep 17 00:00:00 2001 From: Gilad Shoham Date: Thu, 20 May 2021 16:24:07 +0300 Subject: [PATCH 05/18] more updates --- scopes/react/react/webpack/webpack.config.preview.dev.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scopes/react/react/webpack/webpack.config.preview.dev.ts b/scopes/react/react/webpack/webpack.config.preview.dev.ts index e20144a44703..3cf17d23a0eb 100644 --- a/scopes/react/react/webpack/webpack.config.preview.dev.ts +++ b/scopes/react/react/webpack/webpack.config.preview.dev.ts @@ -259,14 +259,18 @@ export default function ({ envId, fileMapPath, workDir }: Options): WebpackConfi // exclude: [/dist/, /node_modules/], // }), new webpack.container.ModuleFederationPlugin({ + filename: 'remote-entry.js', + name: 'module_federation_namespace', // remoteType: 'commonjs', shared: { react: { + // TODO: make sure we can remove the eager here by adding bootstrap for everything eager: true, singleton: true, requiredVersion: '^16.14.0', }, 'react-dom': { + // TODO: make sure we can remove the eager here by adding bootstrap for everything eager: true, singleton: true, requiredVersion: '^16.14.0', From 95ac06021e2213b631b47fbd6619d5d00d0a91f8 Mon Sep 17 00:00:00 2001 From: Gilad Shoham Date: Wed, 26 May 2021 01:27:45 +0300 Subject: [PATCH 06/18] first working version of component level bundling --- .../compilation/bundler/dev-server-context.ts | 10 +++ .../webpack/webpack.main.runtime.ts | 15 ++++ .../preview/preview/preview.main.runtime.tsx | 1 + scopes/preview/preview/preview.task.ts | 47 +--------- .../preview/strategies/component-strategy.ts | 86 ++++++++++++++++++- .../preview/strategies/env-strategy.ts | 6 +- scopes/react/react/react.env.ts | 28 +++++- ...view.ts => webpack.config.base.preview.ts} | 65 +------------- .../webpack.config.component.preview.ts | 81 +++++++++++++++++ .../webpack/webpack.config.env.preview.ts | 34 ++++++++ 10 files changed, 256 insertions(+), 117 deletions(-) rename scopes/react/react/webpack/{webpack.config.preview.ts => webpack.config.base.preview.ts} (87%) create mode 100644 scopes/react/react/webpack/webpack.config.component.preview.ts create mode 100644 scopes/react/react/webpack/webpack.config.env.preview.ts diff --git a/scopes/compilation/bundler/dev-server-context.ts b/scopes/compilation/bundler/dev-server-context.ts index eb3d132b7ee4..8e025bc50399 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 { diff --git a/scopes/compilation/webpack/webpack.main.runtime.ts b/scopes/compilation/webpack/webpack.main.runtime.ts index 3763f5139e20..421772cab342 100644 --- a/scopes/compilation/webpack/webpack.main.runtime.ts +++ b/scopes/compilation/webpack/webpack.main.runtime.ts @@ -23,6 +23,7 @@ import { WebpackDevServer } from './webpack.dev-server'; export type WebpackConfigTransformContext = { mode: BundlerMode; + target?: Target; }; export type WebpackConfigTransformer = ( config: WebpackConfigMutator, @@ -93,6 +94,20 @@ export class WebpackMain { 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); + } + private createPreviewConfig(targets: Target[]) { return targets.map((target) => { return previewConfigFactory(target.entries, target.outputPath); diff --git a/scopes/preview/preview/preview.main.runtime.tsx b/scopes/preview/preview/preview.main.runtime.tsx index 550fb62d37af..29b713eb637d 100644 --- a/scopes/preview/preview/preview.main.runtime.tsx +++ b/scopes/preview/preview/preview.main.runtime.tsx @@ -97,6 +97,7 @@ export class PreviewMain { private execContexts = new Map(); private componentsByAspect = new Map(); + // TODO: consolidate code duplication with the env-strategy computePaths logic private async getPreviewTarget( /** execution context (of the specific env) */ context: ExecutionContext diff --git a/scopes/preview/preview/preview.task.ts b/scopes/preview/preview/preview.task.ts index 7137a862ce3d..472326602a4c 100644 --- a/scopes/preview/preview/preview.task.ts +++ b/scopes/preview/preview/preview.task.ts @@ -1,13 +1,7 @@ -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'; export class PreviewTask implements BuildTask { @@ -41,51 +35,14 @@ export class PreviewTask implements BuildTask { rootPath: url, }); - const bundler: Bundler = await context.env.getBundler(bundlerContext); + const bundler: Bundler = await context.env.getComponentBundler(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(); - - 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..f2801d792e07 100644 --- a/scopes/preview/preview/strategies/component-strategy.ts +++ b/scopes/preview/preview/strategies/component-strategy.ts @@ -1,5 +1,12 @@ +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 { BuildContext } from '@teambit/builder'; import { Target, BundlerResult, BundlerContext } from '@teambit/bundler'; +import { camelCase } from 'lodash'; +import fs from 'fs-extra'; import { BundlingStrategy } from '../bundling-strategy'; import { PreviewDefinition } from '../preview-definition'; import { PreviewTask } from '../preview.task'; @@ -7,18 +14,62 @@ import { PreviewTask } from '../preview.task'; 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 computeExposes( + capsule: Capsule, + defs: PreviewDefinition[], + context: BuildContext + ): Promise> { + const compIdCamel = normalizeMfName(capsule.component.id.fullName); + const compiler: Compiler = context.env.getCompiler(context); + const mainFile = capsule.component.state._consumer.mainFile; + const mainFilePath = join(capsule.path, compiler.getDistPathBySrcPath(mainFile)); + const exposes = { + [`./${compIdCamel}`]: mainFilePath, + }; + + const moduleMapsPromise = defs.map(async (previewDef) => { + const moduleMap = await previewDef.getModuleMap([capsule.component]); + const paths = this.getPathsFromMap(capsule, moduleMap, context); + paths.toArray().map(([, files], index) => { + files.map((filePath) => { + Object.assign(exposes, { + [`./${compIdCamel}_${previewDef.prefix}_${index}`]: filePath, + }); + return undefined; + }); + return undefined; + }); + }); + + await Promise.all(moduleMapsPromise); + return exposes; + } + + 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[], previewTask: PreviewTask) { return { componentsResults: results.map((result) => { @@ -31,4 +82,31 @@ export class ComponentBundlingStrategy implements BundlingStrategy { artifacts: [{ name: 'preview', globPatterns: [previewTask.getPreviewDirectory(context)] }], }; } + + 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))); + }); + } +} + +// link-file.js +// new webpack.container.ModuleFederationPlugin({ +// exposes: { +// // TODO: take the dist file programmatically +// [`./${buttonId}`]: '/Users/giladshoham/Library/Caches/Bit/capsules/d3522af33785e04e8b1199864b9f46951ea3c008/my-scope_ui_button/dist/button.js', +// [`./${buttonId}_composition_1`]: '/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', +// }, +// defaultEposes: './index' +// import ('uiButton') +// }), + +function normalizeMfName(componentId: string): string { + return camelCase(componentId); } diff --git a/scopes/preview/preview/strategies/env-strategy.ts b/scopes/preview/preview/strategies/env-strategy.ts index 9f5a7118fc6d..40322a82f736 100644 --- a/scopes/preview/preview/strategies/env-strategy.ts +++ b/scopes/preview/preview/strategies/env-strategy.ts @@ -21,12 +21,14 @@ export class EnvBundlingStrategy implements BundlingStrategy { async computeTargets(context: BuildContext, previewDefs: PreviewDefinition[]) { const outputPath = this.getOutputPath(context); - console.log(outputPath); + 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/react.env.ts b/scopes/react/react/react.env.ts index 0c5260e24df3..2ff07b3c3a2a 100644 --- a/scopes/react/react/react.env.ts +++ b/scopes/react/react/react.env.ts @@ -22,7 +22,9 @@ import { outputFileSync } from 'fs-extra'; import { Configuration } from 'webpack'; import { ReactMainConfig } from './react.main.runtime'; import devPreviewConfigFactory from './webpack/webpack.config.preview.dev'; -import previewConfigFactory from './webpack/webpack.config.preview'; +import basePreviewConfigFactory from './webpack/webpack.config.base.preview'; +import envPreviewConfigFactory from './webpack/webpack.config.env.preview'; +import componentPreviewConfigFactory from './webpack/webpack.config.component.preview'; import { eslintConfig } from './eslint/eslintrc'; import { ReactAspect } from './react.aspect'; @@ -185,9 +187,8 @@ export class ReactEnv implements Environment { return this.webpack.createDevServer(context, [defaultTransformer, ...transformers]); } - async getBundler(context: BundlerContext, transformers: WebpackConfigTransformer[] = []): Promise { - const path = this.writeFileMap(context.components); - const defaultConfig = previewConfigFactory(path); + async getEnvBundler(context: BundlerContext, transformers: WebpackConfigTransformer[] = []): Promise { + const defaultConfig = envPreviewConfigFactory(); const defaultTransformer: WebpackConfigTransformer = (configMutator) => { return configMutator.merge([defaultConfig]); }; @@ -195,6 +196,25 @@ export class ReactEnv implements Environment { return this.webpack.createBundler(context, [defaultTransformer, ...transformers]); } + async getComponentBundler(context: BundlerContext, transformers: WebpackConfigTransformer[] = []): Promise { + const fileMapPath = this.writeFileMap(context.components); + const baseConfig = basePreviewConfigFactory(); + const defaultTransformer: WebpackConfigTransformer = (configMutator, mutatorContext) => { + if (!mutatorContext.target?.mfName) { + throw new Error(`missing module federation name for ${mutatorContext.target?.components[0].id.toString()}`); + } + const componentConfig = componentPreviewConfigFactory( + mutatorContext.target?.mfName, + mutatorContext.target?.mfExposes, + fileMapPath + ); + + return configMutator.merge([baseConfig, componentConfig]); + }; + + return this.webpack.createComponentsBundler(context, [defaultTransformer, ...transformers]); + } + private getEntriesFromWebpackConfig(config?: Configuration): string[] { if (!config || !config.entry) { return []; diff --git a/scopes/react/react/webpack/webpack.config.preview.ts b/scopes/react/react/webpack/webpack.config.base.preview.ts similarity index 87% rename from scopes/react/react/webpack/webpack.config.preview.ts rename to scopes/react/react/webpack/webpack.config.base.preview.ts index 216317176270..2c2ac58f7700 100644 --- a/scopes/react/react/webpack/webpack.config.preview.ts +++ b/scopes/react/react/webpack/webpack.config.base.preview.ts @@ -4,10 +4,7 @@ import getCSSModuleLocalIdent from 'react-dev-utils/getCSSModuleLocalIdent'; import TerserPlugin from 'terser-webpack-plugin'; import webpack, { Configuration } from 'webpack'; import { WebpackManifestPlugin } from 'webpack-manifest-plugin'; -import WorkboxWebpackPlugin from 'workbox-webpack-plugin'; import * as stylesRegexps from '@teambit/webpack.modules.style-regexps'; -import { ComponentID } from '@teambit/component-id'; -import fs from 'fs-extra'; import { postCssConfig } from './postcss.config'; // 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 @@ -37,11 +34,7 @@ const imageInlineSizeLimit = parseInt(process.env.IMAGE_INLINE_SIZE_LIMIT || '10 // 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 (fileMapPath: string): Configuration { - const fileMapContent = JSON.parse(fs.readFileSync(fileMapPath).toString('utf8')); - const buttonIndexFilePath: string = - Object.keys(fileMapContent).find((filePath) => filePath.includes('index')) || fileMapContent[0]; - const buttonId = fileMapContent[buttonIndexFilePath].id; +export default function (): Configuration { const isEnvProduction = true; // Variable used for enabling profiling in Production @@ -179,6 +172,7 @@ export default function (fileMapPath: string): Configuration { '@teambit/mdx.ui.mdx-scope-context': require.resolve('@teambit/mdx.ui.mdx-scope-context'), 'react-dom/server': require.resolve('react-dom/server'), 'react-dom': require.resolve('react-dom'), + // TODO: move to react-native only 'react-native': 'react-native-web', '@mdx-js/react': require.resolve('@mdx-js/react'), // Allows for better profiling with ReactDevTools @@ -222,29 +216,6 @@ export default function (fileMapPath: string): Configuration { sideEffects: true, }, - { - test: /\.js$/, - include: [/node_modules/, /\/dist\//], - exclude: /@teambit\/legacy/, - descriptionData: { componentId: ComponentID.isValidObject }, - use: [ - { - loader: require.resolve('babel-loader'), - options: { - babelrc: false, - configFile: false, - plugins: [ - // for component highlighting in preview. - [require.resolve('@teambit/react.babel.bit-react-transformer')], - ], - // turn off all optimizations (only slow down for node_modules) - compact: false, - minified: false, - }, - }, - ], - }, - // Process application JS with Babel. // The preset includes JSX, Flow, TypeScript, and some ESnext features. { @@ -261,12 +232,6 @@ export default function (fileMapPath: string): Configuration { customize: require.resolve('babel-preset-react-app/webpack-overrides'), presets: [require.resolve('@babel/preset-react')], plugins: [ - [ - require.resolve('@teambit/react.babel.bit-react-transformer'), - { - componentFilesPath: fileMapPath, - }, - ], [ require.resolve('babel-plugin-named-asset-import'), { @@ -444,6 +409,7 @@ export default function (fileMapPath: string): Configuration { // }, // 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, @@ -456,13 +422,6 @@ export default function (fileMapPath: string): Configuration { requiredVersion: '^16.14.0', }, }, - exposes: { - // TODO: take the dist file programmatically - [`./${buttonId}`]: '/Users/giladshoham/Library/Caches/Bit/capsules/d3522af33785e04e8b1199864b9f46951ea3c008/my-scope_ui_button/dist/button.js', - }, - remotes: { - // 'versioned-federated-module': 'versioned-federated-module', - }, }), new MiniCssExtractPlugin({ // Options similar to the same options in webpackOptions.output @@ -500,24 +459,6 @@ export default function (fileMapPath: string): Configuration { resourceRegExp: /^\.\/locale$/, contextRegExp: /moment$/, }), - // Generate a service worker script that will precache, and keep up to date, - // the HTML & assets that are part of the webpack build. - isEnvProduction && - new WorkboxWebpackPlugin.GenerateSW({ - clientsClaim: true, - exclude: [/\.map$/, /asset-manifest\.json$/], - // importWorkboxFrom: 'cdn', - navigateFallback: 'public/index.html', - navigateFallbackDenylist: [ - // Exclude URLs starting with /_, as they're likely an API call - new RegExp('^/_'), - // Exclude any URLs whose last part seems to be a file extension - // as they're likely a resource and not a SPA route. - // URLs containing a "?" character won't be blacklisted as they're likely - // a route with query params (e.g. auth callbacks). - new RegExp('/[^/?]+\\.[^/]+$'), - ], - }), ].filter(Boolean), // Turn off performance processing because we utilize // our own hints via the FileSizeReporter diff --git a/scopes/react/react/webpack/webpack.config.component.preview.ts b/scopes/react/react/webpack/webpack.config.component.preview.ts new file mode 100644 index 000000000000..b05ff1da3ef1 --- /dev/null +++ b/scopes/react/react/webpack/webpack.config.component.preview.ts @@ -0,0 +1,81 @@ +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 = {}, fileMapPath: string): Configuration { + return { + module: { + rules: [ + { + test: /\.js$/, + include: [/node_modules/, /\/dist\//], + exclude: /@teambit\/legacy/, + descriptionData: { componentId: ComponentID.isValidObject }, + use: [ + { + loader: require.resolve('babel-loader'), + options: { + babelrc: false, + configFile: false, + plugins: [ + // for component highlighting in preview. + [require.resolve('@teambit/react.babel.bit-react-transformer')], + ], + // turn off all optimizations (only slow down for node_modules) + compact: false, + minified: false, + }, + }, + ], + }, + { + test: /\.(js|mjs|jsx|ts|tsx)$/, + exclude: [/node_modules/, /\/dist\//], + // consider: limit loader to files only in a capsule that has bitid in package.json + // descriptionData: { componentId: ComponentID.isValidObject }, + // // or + // include: capsulePaths + loader: require.resolve('babel-loader'), + options: { + babelrc: false, + configFile: false, + // customize: require.resolve('babel-preset-react-app/webpack-overrides'), + // presets: [require.resolve('@babel/preset-react')], + plugins: [ + [ + require.resolve('@teambit/react.babel.bit-react-transformer'), + { + componentFilesPath: fileMapPath, + }, + ], + ], + // This is a feature of `babel-loader` for webpack (not Babel itself). + // It enables caching results in ./node_modules/.cache/babel-loader/ + // directory for faster rebuilds. + cacheDirectory: true, + // See #6846 for context on why cacheCompression is disabled + cacheCompression: false, + compact: true, + }, + }, + ], + }, + 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.preview.ts b/scopes/react/react/webpack/webpack.config.env.preview.ts new file mode 100644 index 000000000000..f62185f75221 --- /dev/null +++ b/scopes/react/react/webpack/webpack.config.env.preview.ts @@ -0,0 +1,34 @@ +import webpack, { Configuration } from 'webpack'; +import fs from 'fs-extra'; +import WorkboxWebpackPlugin from 'workbox-webpack-plugin'; + +// 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 (): Configuration { + return { + plugins: [ + new webpack.container.ModuleFederationPlugin({ + // TODO: implement + remotes: {}, + }), + // Generate a service worker script that will precache, and keep up to date, + // the HTML & assets that are part of the webpack build. + new WorkboxWebpackPlugin.GenerateSW({ + clientsClaim: true, + exclude: [/\.map$/, /asset-manifest\.json$/], + // importWorkboxFrom: 'cdn', + navigateFallback: 'public/index.html', + navigateFallbackDenylist: [ + // Exclude URLs starting with /_, as they're likely an API call + new RegExp('^/_'), + // Exclude any URLs whose last part seems to be a file extension + // as they're likely a resource and not a SPA route. + // URLs containing a "?" character won't be blacklisted as they're likely + // a route with query params (e.g. auth callbacks). + new RegExp('/[^/?]+\\.[^/]+$'), + ], + }), + ], + }; +} From a5d50ca94b6c611420aafd4db03135392e1f2dd7 Mon Sep 17 00:00:00 2001 From: Gilad Shoham Date: Mon, 14 Jun 2021 13:28:32 +0300 Subject: [PATCH 07/18] WIP - bit start with MF --- .../compilation/bundler/dev-server.service.ts | 17 +- scopes/compilation/bundler/env-server.ts | 93 ++++++ .../events/envs-server-started-event.ts | 20 ++ scopes/compilation/bundler/events/index.ts | 1 + scopes/preview/preview/generate-link copy.ts | 21 ++ scopes/preview/preview/generate-link.ts | 6 +- scopes/react/react/react.env.ts | 59 ++-- .../webpack.config.base.preview.prod.ts | 111 +++++++ .../webpack/webpack.config.base.preview.ts | 95 +----- .../webpack.config.component.base.preview.ts | 25 ++ .../webpack.config.component.preview.dev.ts | 77 +++++ ... webpack.config.component.preview.prod.ts} | 17 +- ....ts => webpack.config.env.base.preview.ts} | 0 .../webpack/webpack.config.env.preview.dev.ts | 17 + .../webpack/webpack.config.preview.dev.ts | 304 ------------------ .../webpack/webpack/webpack.main.runtime.ts | 19 ++ 16 files changed, 432 insertions(+), 450 deletions(-) create mode 100644 scopes/compilation/bundler/env-server.ts create mode 100644 scopes/compilation/bundler/events/envs-server-started-event.ts create mode 100644 scopes/preview/preview/generate-link copy.ts create mode 100644 scopes/react/react/webpack/webpack.config.base.preview.prod.ts create mode 100644 scopes/react/react/webpack/webpack.config.component.base.preview.ts create mode 100644 scopes/react/react/webpack/webpack.config.component.preview.dev.ts rename scopes/react/react/webpack/{webpack.config.component.preview.ts => webpack.config.component.preview.prod.ts} (68%) rename scopes/react/react/webpack/{webpack.config.env.preview.ts => webpack.config.env.base.preview.ts} (100%) create mode 100644 scopes/react/react/webpack/webpack.config.env.preview.dev.ts delete mode 100644 scopes/react/react/webpack/webpack.config.preview.dev.ts diff --git a/scopes/compilation/bundler/dev-server.service.ts b/scopes/compilation/bundler/dev-server.service.ts index 67ba2db7625d..5cd91186ed5f 100644 --- a/scopes/compilation/bundler/dev-server.service.ts +++ b/scopes/compilation/bundler/dev-server.service.ts @@ -62,10 +62,12 @@ export class DevServerService implements EnvService { 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 = devServerContext.envRuntime.env.getDevServer(devServerContext); + this.enrichContextWithComponentsAndRelatedContext(mainContext, additionalContexts); + const envDevServerContext = await this.buildEnvServerContext(mainContext); + const envDevServer: DevServer = envDevServerContext.envRuntime.env.getEnvDevServer(envDevServerContext); - return new ComponentServer(this.pubsub, devServerContext, [3300, 3400], devServer); + // TODO: consider change this to a new class called EnvServer + return new ComponentServer(this.pubsub, envDevServerContext, [3300, 3400], envDevServer); }) ); @@ -85,13 +87,18 @@ 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 { return Object.assign(context, { entry: await getEntry(context, this.runtimeSlot), rootPath: `/preview/${context.envRuntime.id}`, 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/preview/preview/generate-link copy.ts b/scopes/preview/preview/generate-link copy.ts new file mode 100644 index 000000000000..7a204e3eff2b --- /dev/null +++ b/scopes/preview/preview/generate-link copy.ts @@ -0,0 +1,21 @@ +import { toWindowsCompatiblePath } from '@teambit/toolbox.path.to-windows-compatible-path'; +import type { ComponentMap } from '@teambit/component'; + +// :TODO refactor to building an AST and generate source code based on it. +export function generateLink(prefix: string, componentMap: ComponentMap, defaultModule?: string): string { + return ` +import { linkModules } from '${toWindowsCompatiblePath(require.resolve('./preview.preview.runtime'))}'; +import harmony from '${toWindowsCompatiblePath(require.resolve('@teambit/harmony'))}'; +${defaultModule ? `const defaultModule = require('${toWindowsCompatiblePath(defaultModule)}'` : ''}); +linkModules('${prefix}', defaultModule, { + ${componentMap + .toArray() + .map(([component, modulePaths]: any) => { + return `'${component.id.fullName}': [${modulePaths + .map((path) => `require('${toWindowsCompatiblePath(path)}')`) + .join(', ')}]`; + }) + .join(',\n')} +}); +`; +} diff --git a/scopes/preview/preview/generate-link.ts b/scopes/preview/preview/generate-link.ts index 7a204e3eff2b..16cca94b50f7 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,9 @@ linkModules('${prefix}', defaultModule, { .join(', ')}]`; }) .join(',\n')} -}); +}); +// import("uiButton/uiButton").then(Module => console.log('dadsafasdf', Module)) + + `; } diff --git a/scopes/react/react/react.env.ts b/scopes/react/react/react.env.ts index 3f8d1f9d3555..ceee29eed556 100644 --- a/scopes/react/react/react.env.ts +++ b/scopes/react/react/react.env.ts @@ -23,8 +23,11 @@ import { Configuration } from 'webpack'; import { ReactMainConfig } from './react.main.runtime'; import devPreviewConfigFactory from './webpack/webpack.config.preview.dev'; import basePreviewConfigFactory from './webpack/webpack.config.base.preview'; -import envPreviewConfigFactory from './webpack/webpack.config.env.preview'; -import componentPreviewConfigFactory from './webpack/webpack.config.component.preview'; +import basePreviewProdConfigFactory from './webpack/webpack.config.base.preview.prod'; +import envPreviewProdConfigFactory from './webpack/webpack.config.env.base.preview'; +import componentPreviewProdConfigFactory from './webpack/webpack.config.component.preview.prod'; +import componentPreviewBaseConfigFactory from './webpack/webpack.config.component.base.preview'; +import componentPreviewDevConfigFactory from './webpack/webpack.config.component.preview.dev'; import { eslintConfig } from './eslint/eslintrc'; import { ReactAspect } from './react.aspect'; @@ -178,7 +181,8 @@ export class ReactEnv implements Environment { /** * returns and configures the React component dev server. */ - getDevServer(context: DevServerContext, transformers: WebpackConfigTransformer[] = []): DevServer { + getEnvDevServer(context: DevServerContext, transformers: WebpackConfigTransformer[] = []): DevServer { + console.log('context.entry', context.entry); const defaultConfig = this.getDevWebpackConfig(context); const defaultTransformer: WebpackConfigTransformer = (configMutator) => { return configMutator.merge([defaultConfig]); @@ -187,54 +191,49 @@ export class ReactEnv implements Environment { return this.webpack.createDevServer(context, [defaultTransformer, ...transformers]); } - async getEnvBundler(context: BundlerContext, transformers: WebpackConfigTransformer[] = []): Promise { - const defaultConfig = envPreviewConfigFactory(); + /** + * returns and configures the React component dev server. + */ + getComponentsDevServers(context: DevServerContext, transformers: WebpackConfigTransformer[] = []): DevServer { + const defaultConfig = this.getDevWebpackConfig(context); const defaultTransformer: WebpackConfigTransformer = (configMutator) => { return configMutator.merge([defaultConfig]); }; + return this.webpack.createDevServer(context, [defaultTransformer, ...transformers]); + } + + async getEnvBundler(context: BundlerContext, transformers: WebpackConfigTransformer[] = []): Promise { + const baseConfig = basePreviewConfigFactory(true); + const baseProdConfig = basePreviewProdConfigFactory(); + const defaultConfig = envPreviewProdConfigFactory(); + const defaultTransformer: WebpackConfigTransformer = (configMutator) => { + return configMutator.merge([baseConfig, baseProdConfig, defaultConfig]); + }; + return this.webpack.createBundler(context, [defaultTransformer, ...transformers]); } async getComponentBundler(context: BundlerContext, transformers: WebpackConfigTransformer[] = []): Promise { const fileMapPath = this.writeFileMap(context.components); - const baseConfig = basePreviewConfigFactory(); + const baseConfig = basePreviewConfigFactory(true); + const baseProdConfig = basePreviewProdConfigFactory(); + const prodComponentConfig = componentPreviewProdConfigFactory(fileMapPath); const defaultTransformer: WebpackConfigTransformer = (configMutator, mutatorContext) => { if (!mutatorContext.target?.mfName) { throw new Error(`missing module federation name for ${mutatorContext.target?.components[0].id.toString()}`); } - const componentConfig = componentPreviewConfigFactory( + const baseComponentConfig = componentPreviewBaseConfigFactory( mutatorContext.target?.mfName, - mutatorContext.target?.mfExposes, - fileMapPath + mutatorContext.target?.mfExposes ); - return configMutator.merge([baseConfig, componentConfig]); + return configMutator.merge([baseConfig, baseProdConfig, baseComponentConfig, prodComponentConfig]); }; return this.webpack.createComponentsBundler(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 []; - } - /** * return a path to a docs template. */ diff --git a/scopes/react/react/webpack/webpack.config.base.preview.prod.ts b/scopes/react/react/webpack/webpack.config.base.preview.prod.ts new file mode 100644 index 000000000000..6116c6e26c09 --- /dev/null +++ b/scopes/react/react/webpack/webpack.config.base.preview.prod.ts @@ -0,0 +1,111 @@ +import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; +import TerserPlugin from 'terser-webpack-plugin'; +import webpack, { 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'; + +// 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 (): Configuration { + return { + optimization: { + // 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 CssMinimizerPlugin({ + sourceMap: shouldUseSourceMap, + minimizerOptions: { + preset: [ + 'default', + { + minifyFontValues: { removeQuotes: false }, + }, + ], + }, + }), + ], + // 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, + // }, + // 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}`, + // }, + }, + + plugins: [ + // Generate an asset manifest file with the following content: + // - "files" key: Mapping of all asset filenames to their corresponding + // output file so that tools can pick it up without having to parse + // `index.html` + // can be used to reconstruct the HTML if necessary + new WebpackManifestPlugin({ + fileName: 'asset-manifest.json', + publicPath: 'public', + generate: (seed, files, entrypoints) => { + const manifestFiles = files.reduce((manifest, file) => { + manifest[file.name] = file.path; + return manifest; + }, seed); + const entrypointFiles = entrypoints.main.filter((fileName) => !fileName.endsWith('.map')); + + return { + files: manifestFiles, + entrypoints: entrypointFiles, + }; + }, + }), + ].filter(Boolean), + // Turn off performance processing because we utilize + // our own hints via the FileSizeReporter + performance: false, + }; +} diff --git a/scopes/react/react/webpack/webpack.config.base.preview.ts b/scopes/react/react/webpack/webpack.config.base.preview.ts index ed33c173a57a..7d07f212ed57 100644 --- a/scopes/react/react/webpack/webpack.config.base.preview.ts +++ b/scopes/react/react/webpack/webpack.config.base.preview.ts @@ -1,7 +1,5 @@ import MiniCssExtractPlugin from 'mini-css-extract-plugin'; -import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; import getCSSModuleLocalIdent from 'react-dev-utils/getCSSModuleLocalIdent'; -import TerserPlugin from 'terser-webpack-plugin'; import webpack, { Configuration } from 'webpack'; import { WebpackManifestPlugin } from 'webpack-manifest-plugin'; import * as stylesRegexps from '@teambit/webpack.modules.style-regexps'; @@ -34,9 +32,7 @@ const imageInlineSizeLimit = parseInt(process.env.IMAGE_INLINE_SIZE_LIMIT || '10 // 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 (): Configuration { - const isEnvProduction = true; - +export default function (isEnvProduction = false): Configuration { // Variable used for enabling profiling in Production // passed into alias object. Uses a flag if passed into the build command const isEnvProductionProfile = process.argv.includes('--profile'); @@ -89,72 +85,6 @@ export default function (): Configuration { }; return { - optimization: { - // 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 CssMinimizerPlugin({ - sourceMap: shouldUseSourceMap, - minimizerOptions: { - preset: [ - 'default', - { - minifyFontValues: { removeQuotes: false }, - }, - ], - }, - }), - ], - // 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, - // }, - // 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}`, - // }, - }, resolve: { // These are the reasonable defaults supported by the Node ecosystem. // We also include JSX as a common component filename extension to support @@ -399,8 +329,6 @@ export default function (): Configuration { }, plugins: [ new webpack.container.ModuleFederationPlugin({ - filename: 'remote-entry.js', - name: 'module_federation_namespace', // library: { // type: 'var', // name: 'module_federation_namespace', @@ -427,27 +355,6 @@ export default function (): Configuration { filename: 'static/css/[name].[contenthash:8].css', chunkFilename: 'static/css/[name].[contenthash:8].chunk.css', }), - // Generate an asset manifest file with the following content: - // - "files" key: Mapping of all asset filenames to their corresponding - // output file so that tools can pick it up without having to parse - // `index.html` - // can be used to reconstruct the HTML if necessary - new WebpackManifestPlugin({ - fileName: 'asset-manifest.json', - publicPath: 'public', - generate: (seed, files, entrypoints) => { - const manifestFiles = files.reduce((manifest, file) => { - manifest[file.name] = file.path; - return manifest; - }, seed); - const entrypointFiles = entrypoints.main.filter((fileName) => !fileName.endsWith('.map')); - - return { - files: manifestFiles, - entrypoints: entrypointFiles, - }; - }, - }), // Moment.js is an extremely popular library that bundles large locale files // by default due to how webpack interprets its code. This is a practical // solution that requires the user to opt into importing specific locales. diff --git a/scopes/react/react/webpack/webpack.config.component.base.preview.ts b/scopes/react/react/webpack/webpack.config.component.base.preview.ts new file mode 100644 index 000000000000..3af58d911cf2 --- /dev/null +++ b/scopes/react/react/webpack/webpack.config.component.base.preview.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.component.preview.dev.ts b/scopes/react/react/webpack/webpack.config.component.preview.dev.ts new file mode 100644 index 000000000000..55c8d94d7a05 --- /dev/null +++ b/scopes/react/react/webpack/webpack.config.component.preview.dev.ts @@ -0,0 +1,77 @@ +import path from 'path'; +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 (fileMapPath: string, workDir: string): Configuration { + return { + module: { + rules: [ + { + test: /\.js$/, + enforce: 'pre', + // limit loader to files in the current project, + // to skip any files linked from other projects (like Bit itself) + include: path.join(workDir, 'node_modules'), + // only apply to packages with componentId in their package.json (ie. bit components) + descriptionData: { componentId: (value) => !!value }, + use: [require.resolve('source-map-loader')], + }, + { + test: /\.js$/, + // limit loader to files in the current project, + // to skip any files linked from other projects (like Bit itself) + include: path.join(workDir, 'node_modules'), + // only apply to packages with componentId in their package.json (ie. bit components) + descriptionData: { componentId: ComponentID.isValidObject }, + use: [ + { + loader: require.resolve('babel-loader'), + options: { + babelrc: false, + configFile: false, + plugins: [ + // for component highlighting in preview. + [require.resolve('@teambit/react.babel.bit-react-transformer')], + ], + // turn off all optimizations (only slow down for node_modules) + compact: false, + minified: false, + }, + }, + ], + }, + { + test: /\.(mjs|js|jsx|tsx|ts)$/, + // TODO: use a more specific exclude for our selfs + exclude: [/node_modules/, /dist/], + include: workDir, + resolve: { + fullySpecified: false, + }, + loader: require.resolve('babel-loader'), + options: { + babelrc: false, + configFile: false, + presets: [ + // Preset includes JSX, TypeScript, and some ESnext features + require.resolve('babel-preset-react-app'), + ], + plugins: [ + // require.resolve('react-refresh/babel'), + // for component highlighting in preview. + [ + require.resolve('@teambit/react.babel.bit-react-transformer'), + { + componentFilesPath: fileMapPath, + }, + ], + ], + }, + }, + ], + }, + }; +} diff --git a/scopes/react/react/webpack/webpack.config.component.preview.ts b/scopes/react/react/webpack/webpack.config.component.preview.prod.ts similarity index 68% rename from scopes/react/react/webpack/webpack.config.component.preview.ts rename to scopes/react/react/webpack/webpack.config.component.preview.prod.ts index b05ff1da3ef1..2aae91cf4af7 100644 --- a/scopes/react/react/webpack/webpack.config.component.preview.ts +++ b/scopes/react/react/webpack/webpack.config.component.preview.prod.ts @@ -4,7 +4,7 @@ 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 = {}, fileMapPath: string): Configuration { +export default function (fileMapPath: string): Configuration { return { module: { rules: [ @@ -62,20 +62,5 @@ export default function (mfName: string, mfExposes: Record = {}, }, ], }, - 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.preview.ts b/scopes/react/react/webpack/webpack.config.env.base.preview.ts similarity index 100% rename from scopes/react/react/webpack/webpack.config.env.preview.ts rename to scopes/react/react/webpack/webpack.config.env.base.preview.ts diff --git a/scopes/react/react/webpack/webpack.config.env.preview.dev.ts b/scopes/react/react/webpack/webpack.config.env.preview.dev.ts new file mode 100644 index 000000000000..241e1fdfa289 --- /dev/null +++ b/scopes/react/react/webpack/webpack.config.env.preview.dev.ts @@ -0,0 +1,17 @@ +import webpack, { Configuration } from 'webpack'; +import fs from 'fs-extra'; +import WorkboxWebpackPlugin from 'workbox-webpack-plugin'; + +// 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 (envId: string): Configuration { + return { + devServer: { + // @ts-ignore - remove this once there is types package for webpack-dev-server v4 + client: { + path: `_hmr/${envId}`, + }, + }, + }; +} diff --git a/scopes/react/react/webpack/webpack.config.preview.dev.ts b/scopes/react/react/webpack/webpack.config.preview.dev.ts deleted file mode 100644 index 2c8650e4c6a8..000000000000 --- a/scopes/react/react/webpack/webpack.config.preview.dev.ts +++ /dev/null @@ -1,304 +0,0 @@ -import '@teambit/mdx.ui.mdx-scope-context'; -// import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'; -import { ComponentID } from '@teambit/component-id'; -import path from 'path'; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import webpack from 'webpack'; -import * as stylesRegexps from '@teambit/webpack.modules.style-regexps'; - -import type { WebpackConfigWithDevServer } from '@teambit/webpack'; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import * as mdxLoader from '@teambit/mdx.modules.mdx-loader'; -// 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'; - -/* - * Webpack config for Preview Dev mode, - * i.e. bundle docs & compositions for react components in a local workspace. - */ - -const moduleFileExtensions = [ - 'web.js', - 'js', - 'web.ts', - 'ts', - 'web.mjs', - 'mjs', - 'web.tsx', - 'tsx', - 'json', - 'web.jsx', - 'jsx', - 'mdx', - 'md', -]; - -type Options = { envId: string; fileMapPath: string; workDir: string }; - -const imageInlineSizeLimit = parseInt(process.env.IMAGE_INLINE_SIZE_LIMIT || '10000'); - -export default function ({ envId, fileMapPath, workDir }: Options): WebpackConfigWithDevServer { - return { - devServer: { - // @ts-ignore - remove this once there is types package for webpack-dev-server v4 - client: { - path: `_hmr/${envId}`, - }, - }, - cache: false, - module: { - rules: [ - { - test: /\.m?js/, - resolve: { - fullySpecified: false, - }, - }, - { - test: /\.js$/, - enforce: 'pre', - // limit loader to files in the current project, - // to skip any files linked from other projects (like Bit itself) - include: path.join(workDir, 'node_modules'), - // only apply to packages with componentId in their package.json (ie. bit components) - descriptionData: { componentId: (value) => !!value }, - use: [require.resolve('source-map-loader')], - }, - { - test: /\.js$/, - // limit loader to files in the current project, - // to skip any files linked from other projects (like Bit itself) - include: path.join(workDir, 'node_modules'), - // only apply to packages with componentId in their package.json (ie. bit components) - descriptionData: { componentId: ComponentID.isValidObject }, - use: [ - { - loader: require.resolve('babel-loader'), - options: { - babelrc: false, - configFile: false, - plugins: [ - // for component highlighting in preview. - [require.resolve('@teambit/react.babel.bit-react-transformer')], - ], - // turn off all optimizations (only slow down for node_modules) - compact: false, - minified: false, - }, - }, - ], - }, - { - test: /\.(mjs|js|jsx|tsx|ts)$/, - // TODO: use a more specific exclude for our selfs - exclude: [/node_modules/, /dist/], - include: workDir, - resolve: { - fullySpecified: false, - }, - loader: require.resolve('babel-loader'), - options: { - babelrc: false, - configFile: false, - presets: [ - // Preset includes JSX, TypeScript, and some ESnext features - require.resolve('babel-preset-react-app'), - ], - plugins: [ - // require.resolve('react-refresh/babel'), - // for component highlighting in preview. - [ - require.resolve('@teambit/react.babel.bit-react-transformer'), - { - componentFilesPath: fileMapPath, - }, - ], - ], - }, - }, - - { - test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/], - type: 'asset', - parser: { - dataUrlCondition: { - maxSize: imageInlineSizeLimit, - }, - }, - }, - - { - test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/, - type: 'asset', - }, - - // MDX support (move to the mdx aspect and extend from there) - { - test: /\.mdx?$/, - exclude: [/node_modules/, /dist/], - use: [ - { - loader: require.resolve('babel-loader'), - options: { - babelrc: false, - configFile: false, - presets: [require.resolve('@babel/preset-react'), require.resolve('@babel/preset-env')], - // plugins: [require.resolve('react-refresh/babel')], - }, - }, - { - loader: require.resolve('@teambit/mdx.modules.mdx-loader'), - }, - ], - }, - { - test: stylesRegexps.sassModuleRegex, - use: [ - require.resolve('style-loader'), - { - loader: require.resolve('css-loader'), - options: { - modules: { - localIdentName: '[name]__[local]--[hash:base64:5]', - }, - sourceMap: true, - }, - }, - { - loader: require.resolve('sass-loader'), - options: { - sourceMap: true, - }, - }, - ], - }, - { - test: stylesRegexps.sassNoModuleRegex, - use: [ - require.resolve('style-loader'), - require.resolve('css-loader'), - { - loader: require.resolve('sass-loader'), - options: { - sourceMap: true, - }, - }, - ], - }, - { - test: stylesRegexps.lessModuleRegex, - use: [ - require.resolve('style-loader'), - { - loader: require.resolve('css-loader'), - options: { - modules: { - localIdentName: '[name]__[local]--[hash:base64:5]', - }, - sourceMap: true, - }, - }, - { - loader: require.resolve('less-loader'), - options: { - sourceMap: true, - }, - }, - ], - }, - { - test: stylesRegexps.lessNoModuleRegex, - use: [ - require.resolve('style-loader'), - require.resolve('css-loader'), - { - loader: require.resolve('less-loader'), - options: { - sourceMap: true, - }, - }, - ], - }, - { - test: stylesRegexps.cssModuleRegex, - use: [ - require.resolve('style-loader'), - { - loader: require.resolve('css-loader'), - options: { - modules: { - localIdentName: '[name]__[local]--[hash:base64:5]', - }, - sourceMap: true, - }, - }, - ], - }, - { - test: stylesRegexps.cssNoModulesRegex, - use: [require.resolve('style-loader'), require.resolve('css-loader')], - }, - ], - }, - resolve: { - // These are the reasonable defaults supported by the Node ecosystem. - // We also include JSX as a common component filename extension to support - // some tools, although we do not recommend using it, see: - // https://github.com/facebook/create-react-app/issues/290 - // `web` extension prefixes have been added for better support - // for React Native Web. - extensions: moduleFileExtensions.map((ext) => `.${ext}`), - - // this is for resolving react from env and not from consuming project - alias: { - 'react/jsx-dev-runtime': require.resolve('react/jsx-dev-runtime.js'), - 'react/jsx-runtime': require.resolve('react/jsx-runtime.js'), - react: require.resolve('react'), - '@teambit/mdx.ui.mdx-scope-context': require.resolve('@teambit/mdx.ui.mdx-scope-context'), - 'react-dom/server': require.resolve('react-dom/server'), - 'react-dom': require.resolve('react-dom'), - '@mdx-js/react': require.resolve('@mdx-js/react'), - // 'react-refresh/runtime': require.resolve('react-refresh/runtime'), - }, - }, - - plugins: [ - // new ReactRefreshWebpackPlugin({ - // overlay: { - // sockPath: `_hmr/${envId}`, - // // TODO: check why webpackHotDevClient and react-error-overlay are not responding for runtime - // // errors - // entry: require.resolve('./react-hot-dev-client'), - // module: require.resolve('./refresh'), - // }, - // include: [/\.(js|jsx|tsx|ts|mdx|md)$/], - // // TODO: use a more specific exclude for our selfs - // exclude: [/dist/, /node_modules/], - // }), - new webpack.container.ModuleFederationPlugin({ - filename: 'remote-entry.js', - name: 'module_federation_namespace', - // remoteType: 'commonjs', - shared: { - react: { - // TODO: make sure we can remove the eager here by adding bootstrap for everything - eager: true, - singleton: true, - requiredVersion: '^17.0.0', - }, - 'react-dom': { - // TODO: make sure we can remove the eager here by adding bootstrap for everything - eager: true, - singleton: true, - requiredVersion: '^17.0.0', - }, - }, - remotes: { - // 'versioned-federated-module': 'versioned-federated-module', - }, - }), - ], - }; -} diff --git a/scopes/webpack/webpack/webpack.main.runtime.ts b/scopes/webpack/webpack/webpack.main.runtime.ts index 421772cab342..7a8b53ad54ca 100644 --- a/scopes/webpack/webpack/webpack.main.runtime.ts +++ b/scopes/webpack/webpack/webpack.main.runtime.ts @@ -77,6 +77,25 @@ export class WebpackMain { return new WebpackDevServer(afterMutation.raw); } + 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); + } + mergeConfig(target: any, source: any): any { return merge(target, source); } From a855644d21c3dcf7943a6f60d3fbfabe90343e51 Mon Sep 17 00:00:00 2001 From: Gilad Shoham Date: Tue, 15 Jun 2021 00:38:56 +0300 Subject: [PATCH 08/18] dev server is start working --- scopes/compilation/bundler/browser-runtime.ts | 1 + .../compilation/bundler/component-server.ts | 9 +- .../compilation/bundler/dev-server-context.ts | 4 + .../compilation/bundler/dev-server.service.ts | 18 ++- scopes/compilation/bundler/get-exposes.ts | 24 ++++ scopes/compilation/bundler/select-port.ts | 4 +- scopes/preview/preview/compute-exposes.ts | 39 +++++++ scopes/preview/preview/normalize-mf-name.ts | 5 + .../preview/preview/preview.main.runtime.tsx | 27 ++++- .../preview/strategies/component-strategy.ts | 32 +----- scopes/react/react/react.env.ts | 107 ++++++++++++++---- ...ew.prod.ts => webpack.config.base.prod.ts} | 18 +++ ...base.preview.ts => webpack.config.base.ts} | 2 +- ...ew.ts => webpack.config.component.base.ts} | 0 ...dev.ts => webpack.config.component.dev.ts} | 0 ...od.ts => webpack.config.component.prod.ts} | 0 .../webpack.config.env.base.preview.ts | 34 ------ .../react/webpack/webpack.config.env.base.ts | 22 ++++ ...eview.dev.ts => webpack.config.env.dev.ts} | 0 scopes/workspace/workspace/workspace.ts | 6 + 20 files changed, 257 insertions(+), 95 deletions(-) create mode 100644 scopes/compilation/bundler/get-exposes.ts create mode 100644 scopes/preview/preview/compute-exposes.ts create mode 100644 scopes/preview/preview/normalize-mf-name.ts rename scopes/react/react/webpack/{webpack.config.base.preview.prod.ts => webpack.config.base.prod.ts} (83%) rename scopes/react/react/webpack/{webpack.config.base.preview.ts => webpack.config.base.ts} (99%) rename scopes/react/react/webpack/{webpack.config.component.base.preview.ts => webpack.config.component.base.ts} (100%) rename scopes/react/react/webpack/{webpack.config.component.preview.dev.ts => webpack.config.component.dev.ts} (100%) rename scopes/react/react/webpack/{webpack.config.component.preview.prod.ts => webpack.config.component.prod.ts} (100%) delete mode 100644 scopes/react/react/webpack/webpack.config.env.base.preview.ts create mode 100644 scopes/react/react/webpack/webpack.config.env.base.ts rename scopes/react/react/webpack/{webpack.config.env.preview.dev.ts => webpack.config.env.dev.ts} (100%) 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 8e025bc50399..e6650261e6c7 100644 --- a/scopes/compilation/bundler/dev-server-context.ts +++ b/scopes/compilation/bundler/dev-server-context.ts @@ -55,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 5cd91186ed5f..ff33c0646ef1 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,6 +58,8 @@ 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]) => { @@ -64,10 +68,15 @@ export class DevServerService implements EnvService { const additionalContexts = contextList.filter((context) => context.envDefinition.id !== id); this.enrichContextWithComponentsAndRelatedContext(mainContext, additionalContexts); const envDevServerContext = await this.buildEnvServerContext(mainContext); - const envDevServer: DevServer = envDevServerContext.envRuntime.env.getEnvDevServer(envDevServerContext); + const envDevServer: DevServer = envDevServerContext.envRuntime.env.getDevServer(envDevServerContext); + const port = await selectPort(portRange, usedPorts); + usedPorts.push(port); + envDevServerContext.port = port; // TODO: consider change this to a new class called EnvServer - return new ComponentServer(this.pubsub, envDevServerContext, [3300, 3400], envDevServer); + const componentServer = new ComponentServer(this.pubsub, envDevServerContext, portRange, envDevServer); + componentServer.port = port; + return componentServer; }) ); @@ -99,10 +108,13 @@ export class DevServerService implements EnvService { * 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, rootPath: `/preview/${context.envRuntime.id}`, publicPath: `/public`, + exposes, }); } } 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/preview/preview/compute-exposes.ts b/scopes/preview/preview/compute-exposes.ts new file mode 100644 index 000000000000..bc234bfc56b7 --- /dev/null +++ b/scopes/preview/preview/compute-exposes.ts @@ -0,0 +1,39 @@ +import { Component } from '@teambit/component'; +import { Compiler } from '@teambit/compiler'; +import { join } from 'path'; +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 compIdCamel = normalizeMfName(component.id.fullName); + 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 paths = moduleMap.map((files) => { + return files.map((file) => join(rootPath, compiler.getDistPathBySrcPath(file.relative))); + }); + paths.toArray().map(([, files], index) => { + files.map((filePath) => { + Object.assign(exposes, { + [`./${compIdCamel}_${previewDef.prefix}_${index}`]: filePath, + }); + return undefined; + }); + return undefined; + }); + }); + + await Promise.all(moduleMapsPromise); + return exposes; +} 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 e55937ea0ee4..93787f79cf79 100644 --- a/scopes/preview/preview/preview.main.runtime.tsx +++ b/scopes/preview/preview/preview.main.runtime.tsx @@ -1,4 +1,5 @@ -import { BuilderAspect, BuilderMain } from '@teambit/builder'; +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'; @@ -24,6 +25,7 @@ import { BundlingStrategy } from './bundling-strategy'; import { EnvBundlingStrategy, ComponentBundlingStrategy } from './strategies'; import { RuntimeComponents } from './runtime-components'; import { PreviewStartPlugin } from './preview.start-plugin'; +import { computeExposes } from './compute-exposes'; const noopResult = { results: [], @@ -57,6 +59,8 @@ export class PreviewMain { private envs: EnvsMain, + private workspace: Workspace, + private aspectLoader: AspectLoaderMain, readonly config: PreviewConfig, @@ -241,6 +245,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; @@ -280,6 +303,7 @@ export class PreviewMain { previewSlot, uiMain, envs, + workspace, aspectLoader, config, bundlingStrategySlot, @@ -293,6 +317,7 @@ export class PreviewMain { bundler.registerTarget([ { entry: preview.getPreviewTarget.bind(preview), + exposes: preview.computeExposesFromExecutionContext.bind(preview), }, ]); diff --git a/scopes/preview/preview/strategies/component-strategy.ts b/scopes/preview/preview/strategies/component-strategy.ts index f2801d792e07..ad052730e4c3 100644 --- a/scopes/preview/preview/strategies/component-strategy.ts +++ b/scopes/preview/preview/strategies/component-strategy.ts @@ -5,11 +5,12 @@ import { AbstractVinyl } from '@teambit/legacy/dist/consumer/component/sources'; import { join } from 'path'; import { BuildContext } from '@teambit/builder'; import { Target, BundlerResult, BundlerContext } from '@teambit/bundler'; -import { camelCase } from 'lodash'; 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'; @@ -36,30 +37,7 @@ export class ComponentBundlingStrategy implements BundlingStrategy { defs: PreviewDefinition[], context: BuildContext ): Promise> { - const compIdCamel = normalizeMfName(capsule.component.id.fullName); - const compiler: Compiler = context.env.getCompiler(context); - const mainFile = capsule.component.state._consumer.mainFile; - const mainFilePath = join(capsule.path, compiler.getDistPathBySrcPath(mainFile)); - const exposes = { - [`./${compIdCamel}`]: mainFilePath, - }; - - const moduleMapsPromise = defs.map(async (previewDef) => { - const moduleMap = await previewDef.getModuleMap([capsule.component]); - const paths = this.getPathsFromMap(capsule, moduleMap, context); - paths.toArray().map(([, files], index) => { - files.map((filePath) => { - Object.assign(exposes, { - [`./${compIdCamel}_${previewDef.prefix}_${index}`]: filePath, - }); - return undefined; - }); - return undefined; - }); - }); - - await Promise.all(moduleMapsPromise); - return exposes; + return computeExposes(capsule.path, defs, capsule.component, context.env.getCompiler()); } async writeEmptyEntryFile(capsule: Capsule): Promise { @@ -106,7 +84,3 @@ export class ComponentBundlingStrategy implements BundlingStrategy { // defaultEposes: './index' // import ('uiButton') // }), - -function normalizeMfName(componentId: string): string { - return camelCase(componentId); -} diff --git a/scopes/react/react/react.env.ts b/scopes/react/react/react.env.ts index ceee29eed556..76437b7dcaba 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 { Environment } from '@teambit/envs'; @@ -21,13 +21,21 @@ import { join, resolve } from 'path'; import { outputFileSync } from 'fs-extra'; import { Configuration } from 'webpack'; import { ReactMainConfig } from './react.main.runtime'; -import devPreviewConfigFactory from './webpack/webpack.config.preview.dev'; -import basePreviewConfigFactory from './webpack/webpack.config.base.preview'; -import basePreviewProdConfigFactory from './webpack/webpack.config.base.preview.prod'; -import envPreviewProdConfigFactory from './webpack/webpack.config.env.base.preview'; -import componentPreviewProdConfigFactory from './webpack/webpack.config.component.preview.prod'; -import componentPreviewBaseConfigFactory from './webpack/webpack.config.component.base.preview'; -import componentPreviewDevConfigFactory from './webpack/webpack.config.component.preview.dev'; + +// webpack configs for both components and envs +import basePreviewConfigFactory from './webpack/webpack.config.base'; +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 { eslintConfig } from './eslint/eslintrc'; import { ReactAspect } from './react.aspect'; @@ -161,9 +169,8 @@ export class ReactEnv implements Environment { * get the default react webpack config. */ private getDevWebpackConfig(context: DevServerContext): Configuration { - const fileMapPath = this.writeFileMap(context.components, true); - - return devPreviewConfigFactory({ envId: context.id, fileMapPath, workDir: this.workspace.path }); + // const fileMapPath = this.writeFileMap(context.components, true); + // return devPreviewConfigFactory({ envId: context.id, fileMapPath, workDir: this.workspace.path }); } getDevEnvId(id?: string) { @@ -181,11 +188,47 @@ export class ReactEnv implements Environment { /** * returns and configures the React component dev server. */ - getEnvDevServer(context: DevServerContext, transformers: WebpackConfigTransformer[] = []): DevServer { + getDevServer(context: DevServerContext, transformers: WebpackConfigTransformer[] = []): DevServer { console.log('context.entry', context.entry); - const defaultConfig = this.getDevWebpackConfig(context); + const baseConfig = basePreviewConfigFactory(false); + const mfName = camelCase(`${context.id.toString()}_MF`); + // TODO: take the port dynamically + const port = context.port; + + const envBaseConfig = envPreviewBaseConfigFactory(mfName, 'localhost', port); + 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 defaultConfig = this.getDevWebpackConfig(context); const defaultTransformer: WebpackConfigTransformer = (configMutator) => { - return configMutator.merge([defaultConfig]); + 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]); @@ -194,14 +237,36 @@ export class ReactEnv implements Environment { /** * returns and configures the React component dev server. */ - getComponentsDevServers(context: DevServerContext, transformers: WebpackConfigTransformer[] = []): DevServer { - const defaultConfig = this.getDevWebpackConfig(context); - const defaultTransformer: WebpackConfigTransformer = (configMutator) => { - return configMutator.merge([defaultConfig]); - }; + // 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]); + // } - 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); diff --git a/scopes/react/react/webpack/webpack.config.base.preview.prod.ts b/scopes/react/react/webpack/webpack.config.base.prod.ts similarity index 83% rename from scopes/react/react/webpack/webpack.config.base.preview.prod.ts rename to scopes/react/react/webpack/webpack.config.base.prod.ts index 6116c6e26c09..e3c7b291d36d 100644 --- a/scopes/react/react/webpack/webpack.config.base.preview.prod.ts +++ b/scopes/react/react/webpack/webpack.config.base.prod.ts @@ -1,3 +1,4 @@ +import WorkboxWebpackPlugin from 'workbox-webpack-plugin'; import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; import TerserPlugin from 'terser-webpack-plugin'; import webpack, { Configuration } from 'webpack'; @@ -103,6 +104,23 @@ export default function (): Configuration { }; }, }), + // Generate a service worker script that will precache, and keep up to date, + // the HTML & assets that are part of the webpack build. + new WorkboxWebpackPlugin.GenerateSW({ + clientsClaim: true, + exclude: [/\.map$/, /asset-manifest\.json$/], + // importWorkboxFrom: 'cdn', + navigateFallback: 'public/index.html', + navigateFallbackDenylist: [ + // Exclude URLs starting with /_, as they're likely an API call + new RegExp('^/_'), + // Exclude any URLs whose last part seems to be a file extension + // as they're likely a resource and not a SPA route. + // URLs containing a "?" character won't be blacklisted as they're likely + // a route with query params (e.g. auth callbacks). + new RegExp('/[^/?]+\\.[^/]+$'), + ], + }), ].filter(Boolean), // Turn off performance processing because we utilize // our own hints via the FileSizeReporter diff --git a/scopes/react/react/webpack/webpack.config.base.preview.ts b/scopes/react/react/webpack/webpack.config.base.ts similarity index 99% rename from scopes/react/react/webpack/webpack.config.base.preview.ts rename to scopes/react/react/webpack/webpack.config.base.ts index 7d07f212ed57..9c8956e96e26 100644 --- a/scopes/react/react/webpack/webpack.config.base.preview.ts +++ b/scopes/react/react/webpack/webpack.config.base.ts @@ -209,7 +209,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'), diff --git a/scopes/react/react/webpack/webpack.config.component.base.preview.ts b/scopes/react/react/webpack/webpack.config.component.base.ts similarity index 100% rename from scopes/react/react/webpack/webpack.config.component.base.preview.ts rename to scopes/react/react/webpack/webpack.config.component.base.ts diff --git a/scopes/react/react/webpack/webpack.config.component.preview.dev.ts b/scopes/react/react/webpack/webpack.config.component.dev.ts similarity index 100% rename from scopes/react/react/webpack/webpack.config.component.preview.dev.ts rename to scopes/react/react/webpack/webpack.config.component.dev.ts diff --git a/scopes/react/react/webpack/webpack.config.component.preview.prod.ts b/scopes/react/react/webpack/webpack.config.component.prod.ts similarity index 100% rename from scopes/react/react/webpack/webpack.config.component.preview.prod.ts rename to scopes/react/react/webpack/webpack.config.component.prod.ts diff --git a/scopes/react/react/webpack/webpack.config.env.base.preview.ts b/scopes/react/react/webpack/webpack.config.env.base.preview.ts deleted file mode 100644 index f62185f75221..000000000000 --- a/scopes/react/react/webpack/webpack.config.env.base.preview.ts +++ /dev/null @@ -1,34 +0,0 @@ -import webpack, { Configuration } from 'webpack'; -import fs from 'fs-extra'; -import WorkboxWebpackPlugin from 'workbox-webpack-plugin'; - -// 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 (): Configuration { - return { - plugins: [ - new webpack.container.ModuleFederationPlugin({ - // TODO: implement - remotes: {}, - }), - // Generate a service worker script that will precache, and keep up to date, - // the HTML & assets that are part of the webpack build. - new WorkboxWebpackPlugin.GenerateSW({ - clientsClaim: true, - exclude: [/\.map$/, /asset-manifest\.json$/], - // importWorkboxFrom: 'cdn', - navigateFallback: 'public/index.html', - navigateFallbackDenylist: [ - // Exclude URLs starting with /_, as they're likely an API call - new RegExp('^/_'), - // Exclude any URLs whose last part seems to be a file extension - // as they're likely a resource and not a SPA route. - // URLs containing a "?" character won't be blacklisted as they're likely - // a route with query params (e.g. auth callbacks). - new RegExp('/[^/?]+\\.[^/]+$'), - ], - }), - ], - }; -} 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..d8824161cc40 --- /dev/null +++ b/scopes/react/react/webpack/webpack.config.env.base.ts @@ -0,0 +1,22 @@ +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, + remoteEntryName = 'remote-entry.js' +): Configuration { + return { + plugins: [ + new webpack.container.ModuleFederationPlugin({ + // TODO: implement + remotes: { + [mfName]: `${mfName}@${server}:${port}/${remoteEntryName}`, + }, + }), + ], + }; +} diff --git a/scopes/react/react/webpack/webpack.config.env.preview.dev.ts b/scopes/react/react/webpack/webpack.config.env.dev.ts similarity index 100% rename from scopes/react/react/webpack/webpack.config.env.preview.dev.ts rename to scopes/react/react/webpack/webpack.config.env.dev.ts diff --git a/scopes/workspace/workspace/workspace.ts b/scopes/workspace/workspace/workspace.ts index acad1b396567..aa2b7d6d2524 100644 --- a/scopes/workspace/workspace/workspace.ts +++ b/scopes/workspace/workspace/workspace.ts @@ -1023,6 +1023,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[] = []; From 6ea5f5461b747fd04c42f7701cfa7eaac6b5c6c9 Mon Sep 17 00:00:00 2001 From: Gilad Shoham Date: Wed, 23 Jun 2021 11:00:56 +0300 Subject: [PATCH 09/18] fixing merge issues --- scopes/react/react/webpack/webpack.config.base.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scopes/react/react/webpack/webpack.config.base.ts b/scopes/react/react/webpack/webpack.config.base.ts index 2028efa85b0c..2e3a90d49ee5 100644 --- a/scopes/react/react/webpack/webpack.config.base.ts +++ b/scopes/react/react/webpack/webpack.config.base.ts @@ -118,6 +118,7 @@ export default function (isEnvProduction = false): Configuration { // Remove this when webpack adds a warning or an error for this. // 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. { @@ -353,11 +354,13 @@ export default function (isEnvProduction = false): Configuration { // both options are optional filename: 'static/css/[name].[contenthash:8].css', chunkFilename: 'static/css/[name].[contenthash:8].chunk.css', + }), // Moment.js is an extremely popular library that bundles large locale files // by default due to how webpack interprets its code. This is a practical // 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$/, From b29541024e95418ed40e7c7edd2ed275bc87f3ff Mon Sep 17 00:00:00 2001 From: Gilad Shoham Date: Wed, 23 Jun 2021 14:14:10 +0300 Subject: [PATCH 10/18] start making the overview/composition using the MF modules --- .../compositions.preview.runtime.ts | 4 ++ scopes/preview/preview/compute-exposes.ts | 71 +++++++++++++++---- scopes/preview/preview/generate-link copy.ts | 21 ------ scopes/preview/preview/generate-link.ts | 3 - scopes/preview/preview/generate-mf-link.ts | 42 +++++++++++ .../preview/preview/preview.main.runtime.tsx | 33 ++++++++- scopes/preview/preview/preview.route.ts | 1 + scopes/react/react/mount.tsx | 1 + scopes/react/react/react.env.ts | 5 +- .../react/webpack/webpack.config.env.base.ts | 3 +- .../ui-foundation/ui/create-root-bootstrap.ts | 12 ++++ scopes/ui-foundation/ui/create-root.ts | 2 + scopes/ui-foundation/ui/ui.main.runtime.ts | 14 ++-- 13 files changed, 167 insertions(+), 45 deletions(-) delete mode 100644 scopes/preview/preview/generate-link copy.ts create mode 100644 scopes/preview/preview/generate-mf-link.ts create mode 100644 scopes/ui-foundation/ui/create-root-bootstrap.ts diff --git a/scopes/compositions/compositions/compositions.preview.runtime.ts b/scopes/compositions/compositions/compositions.preview.runtime.ts index 73f7af359d36..402fccf11262 100644 --- a/scopes/compositions/compositions/compositions.preview.runtime.ts +++ b/scopes/compositions/compositions/compositions.preview.runtime.ts @@ -19,7 +19,9 @@ export class CompositionsPreview { ) {} render(componentId: string, modules: PreviewModule, otherPreviewDefs, context: RenderingContext) { + console.log('im in render of composition'); if (!modules.componentMap[componentId]) return; + debugger; const compositions = this.selectPreviewModel(componentId, modules); const active = this.getActiveComposition(compositions); @@ -29,7 +31,9 @@ export class CompositionsPreview { /** gets relevant information for this preview to render */ selectPreviewModel(componentId: string, previewModule: PreviewModule) { + debugger; const files = previewModule.componentMap[componentId] || []; + console.log('selectPreviewModel', files); // allow compositions to come from many files. It is assumed they will have unique named const combined = Object.assign({}, ...files); diff --git a/scopes/preview/preview/compute-exposes.ts b/scopes/preview/preview/compute-exposes.ts index bc234bfc56b7..8f84e34a204b 100644 --- a/scopes/preview/preview/compute-exposes.ts +++ b/scopes/preview/preview/compute-exposes.ts @@ -1,6 +1,7 @@ -import { Component } from '@teambit/component'; +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'; @@ -11,7 +12,8 @@ export async function computeExposes( component: Component, compiler: Compiler ): Promise> { - const compIdCamel = normalizeMfName(component.id.fullName); + const compFullName = component.id.fullName; + const compIdCamel = normalizeMfName(compFullName); const mainFile = component.state._consumer.mainFile; const mainFilePath = join(rootPath, compiler.getDistPathBySrcPath(mainFile)); const exposes = { @@ -20,20 +22,63 @@ export async function computeExposes( const moduleMapsPromise = defs.map(async (previewDef) => { const moduleMap = await previewDef.getModuleMap([component]); - const paths = moduleMap.map((files) => { - return files.map((file) => join(rootPath, compiler.getDistPathBySrcPath(file.relative))); - }); - paths.toArray().map(([, files], index) => { - files.map((filePath) => { - Object.assign(exposes, { - [`./${compIdCamel}_${previewDef.prefix}_${index}`]: filePath, - }); - return undefined; + 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; }); - - await Promise.all(moduleMapsPromise); 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/generate-link copy.ts b/scopes/preview/preview/generate-link copy.ts deleted file mode 100644 index 7a204e3eff2b..000000000000 --- a/scopes/preview/preview/generate-link copy.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { toWindowsCompatiblePath } from '@teambit/toolbox.path.to-windows-compatible-path'; -import type { ComponentMap } from '@teambit/component'; - -// :TODO refactor to building an AST and generate source code based on it. -export function generateLink(prefix: string, componentMap: ComponentMap, defaultModule?: string): string { - return ` -import { linkModules } from '${toWindowsCompatiblePath(require.resolve('./preview.preview.runtime'))}'; -import harmony from '${toWindowsCompatiblePath(require.resolve('@teambit/harmony'))}'; -${defaultModule ? `const defaultModule = require('${toWindowsCompatiblePath(defaultModule)}'` : ''}); -linkModules('${prefix}', defaultModule, { - ${componentMap - .toArray() - .map(([component, modulePaths]: any) => { - return `'${component.id.fullName}': [${modulePaths - .map((path) => `require('${toWindowsCompatiblePath(path)}')`) - .join(', ')}]`; - }) - .join(',\n')} -}); -`; -} diff --git a/scopes/preview/preview/generate-link.ts b/scopes/preview/preview/generate-link.ts index 16cca94b50f7..198dd6dd3ebc 100644 --- a/scopes/preview/preview/generate-link.ts +++ b/scopes/preview/preview/generate-link.ts @@ -18,8 +18,5 @@ linkModules('${prefix}', defaultModule, { }) .join(',\n')} }); -// import("uiButton/uiButton").then(Module => console.log('dadsafasdf', Module)) - - `; } diff --git a/scopes/preview/preview/generate-mf-link.ts b/scopes/preview/preview/generate-mf-link.ts new file mode 100644 index 000000000000..6a2d56a626bf --- /dev/null +++ b/scopes/preview/preview/generate-mf-link.ts @@ -0,0 +1,42 @@ +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'); + // debugger +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 `() => { + debugger; + console.log('inside link modules'); + import('teambitReactReactMf/${exposedKey}').then((Module) => { + console.log('exposedKey module', Module); + return Module; + })}`; + }) + .join(', ')}]`; + }) + .join(',\n')} + }); +}); +`; +} diff --git a/scopes/preview/preview/preview.main.runtime.tsx b/scopes/preview/preview/preview.main.runtime.tsx index 93787f79cf79..07f9f6d54971 100644 --- a/scopes/preview/preview/preview.main.runtime.tsx +++ b/scopes/preview/preview/preview.main.runtime.tsx @@ -16,6 +16,7 @@ import { AspectDefinition, AspectLoaderMain, AspectLoaderAspect } from '@teambit import WorkspaceAspect, { Workspace } from '@teambit/workspace'; 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'; @@ -107,6 +108,31 @@ export class PreviewMain { return targetPath; } + async writeMfLink( + prefix: string, + context: ExecutionContext, + moduleMap: ComponentMap, + defaultModule: string | undefined, + dirName: string + ) { + const exposes = await this.computeExposesFromExecutionContext(context); + console.log('exposes', exposes); + console.log('moduleMap', require('util').inspect(moduleMap, { depth: 3 })); + + const contents = generateMfLink(prefix, moduleMap, defaultModule); + const hash = objectHash(contents); + const targetPath = join(dirName, `__${prefix}-${this.timestamp}.js`); + console.log('targetPath MF link', 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; + } + private execContexts = new Map(); private componentsByAspect = new Map(); @@ -121,11 +147,12 @@ export class PreviewMain { const previewRuntime = await this.writePreviewRuntime(context); const linkFiles = await this.updateLinkFiles(context.components, context); + // throw new Error('g'); return [...linkFiles, previewRuntime]; } - 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); @@ -139,7 +166,9 @@ export class PreviewMain { 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, context, withPaths, templatePath, dirPath) + : this.writeLink(previewDef.prefix, withPaths, templatePath, dirPath); return link; }); 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/react/react/mount.tsx b/scopes/react/react/mount.tsx index 5fd0d505bb64..4a805e2130c8 100644 --- a/scopes/react/react/mount.tsx +++ b/scopes/react/react/mount.tsx @@ -1,4 +1,5 @@ export default function bootstrap(arg1, arg2) { + debugger; console.log('arg1', arg1); console.log('arg2', arg2); // eslint-disable-next-line diff --git a/scopes/react/react/react.env.ts b/scopes/react/react/react.env.ts index e86f4bf5d23d..b833058b7f15 100644 --- a/scopes/react/react/react.env.ts +++ b/scopes/react/react/react.env.ts @@ -186,8 +186,11 @@ export class ReactEnv implements Environment { const mfName = camelCase(`${context.id.toString()}_MF`); // TODO: take the port dynamically const port = context.port; + const rootPath = context.rootPath; + + const envBaseConfig = envPreviewBaseConfigFactory(mfName, 'localhost', port, rootPath || ''); + console.log('envBaseConfig', require('util').inspect(envBaseConfig, { depth: 10 })); - const envBaseConfig = envPreviewBaseConfigFactory(mfName, 'localhost', port); const envDevConfig = envPreviewDevConfigFactory(context.id); const fileMapPath = this.writeFileMap(context.components, true); diff --git a/scopes/react/react/webpack/webpack.config.env.base.ts b/scopes/react/react/webpack/webpack.config.env.base.ts index d8824161cc40..a1c0fddf4ce8 100644 --- a/scopes/react/react/webpack/webpack.config.env.base.ts +++ b/scopes/react/react/webpack/webpack.config.env.base.ts @@ -7,6 +7,7 @@ export default function ( mfName: string, server: string, port = 3000, + rootPath: string, remoteEntryName = 'remote-entry.js' ): Configuration { return { @@ -14,7 +15,7 @@ export default function ( new webpack.container.ModuleFederationPlugin({ // TODO: implement remotes: { - [mfName]: `${mfName}@${server}:${port}/${remoteEntryName}`, + [mfName]: `${mfName}@${server}:${port}${rootPath}/${remoteEntryName}`, }, }), ], diff --git a/scopes/ui-foundation/ui/create-root-bootstrap.ts b/scopes/ui-foundation/ui/create-root-bootstrap.ts new file mode 100644 index 000000000000..7520bbbcc8e8 --- /dev/null +++ b/scopes/ui-foundation/ui/create-root-bootstrap.ts @@ -0,0 +1,12 @@ +export async function createRootBootstrap(rootPath: string) { + return `export default function bootstrap() { +debugger +// import('./${rootPath}').then((Module) => { +// debugger; +// return Module +// }); +return import('./${rootPath}') +} +bootstrap(); +`; +} diff --git a/scopes/ui-foundation/ui/create-root.ts b/scopes/ui-foundation/ui/create-root.ts index 09a3041a8ad4..5a6457903d12 100644 --- a/scopes/ui-foundation/ui/create-root.ts +++ b/scopes/ui-foundation/ui/create-root.ts @@ -18,6 +18,7 @@ export async function createRoot( const idSetters = getIdSetters(aspectDefs, 'Aspect'); return ` + debugger ${createImports(aspectDefs)} const isBrowser = typeof window !== "undefined"; @@ -33,6 +34,7 @@ export function render(...props){ const rootExtension = harmony.get('${rootAspect}'); if (isBrowser) { + debugger return rootExtension.render(${rootId}, ...props); } else { return rootExtension.renderSsr(${rootId}, ...props); diff --git a/scopes/ui-foundation/ui/ui.main.runtime.ts b/scopes/ui-foundation/ui/ui.main.runtime.ts index d26c4a3e5e17..0be0a41bb938 100644 --- a/scopes/ui-foundation/ui/ui.main.runtime.ts +++ b/scopes/ui-foundation/ui/ui.main.runtime.ts @@ -30,6 +30,7 @@ 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 { createRootBootstrap } from './create-root-bootstrap'; export type UIDeps = [PubsubMain, CLIMain, GraphqlMain, ExpressMain, ComponentMain, CacheMain, LoggerMain, AspectMain]; @@ -196,7 +197,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)); @@ -377,10 +378,15 @@ export class UiMain { runtimeName, this.harmony.config.toObject() ); - const filepath = resolve(join(__dirname, `${runtimeName}.root${sha1(contents)}.js`)); - if (fs.existsSync(filepath)) return filepath; + const rootRelativePath = `${runtimeName}.root${sha1(contents)}.js`; + const filepath = resolve(join(__dirname, rootRelativePath)); + const rootBootstrapContents = await createRootBootstrap(rootRelativePath); + const rootBootstrapRelativePath = `${runtimeName}.root${sha1(rootBootstrapContents)}-bootstrap.js`; + const rootBootstrapPath = resolve(join(__dirname, rootBootstrapRelativePath)); + if (fs.existsSync(filepath) && fs.existsSync(rootBootstrapPath)) return rootBootstrapPath; fs.outputFileSync(filepath, contents); - return filepath; + fs.outputFileSync(rootBootstrapPath, rootBootstrapContents); + return rootBootstrapPath; } private async selectPort() { From 02473ef8c6466857f8fe5ac3ba6250da9ab0b0e0 Mon Sep 17 00:00:00 2001 From: Gilad Shoham Date: Mon, 28 Jun 2021 00:29:57 +0300 Subject: [PATCH 11/18] overview and composition tabs are working with MF --- .../compositions.preview.runtime.ts | 14 ++++--- scopes/docs/docs/docs.preview.runtime.tsx | 9 +++-- scopes/preview/preview/compute-exposes.ts | 4 +- .../preview/generate-bootstrap-file.ts | 7 ++++ scopes/preview/preview/generate-mf-link.ts | 4 +- .../preview/preview/preview.main.runtime.tsx | 37 ++++++++++++++++++- .../preview/preview.preview.runtime.tsx | 18 ++++----- scopes/react/react/docs/index.tsx | 6 ++- scopes/react/react/mount.tsx | 1 - scopes/react/react/react.env.ts | 2 +- .../ui-foundation/ui/create-root-bootstrap.ts | 16 +++++--- scopes/ui-foundation/ui/create-root.ts | 2 - 12 files changed, 83 insertions(+), 37 deletions(-) create mode 100644 scopes/preview/preview/generate-bootstrap-file.ts diff --git a/scopes/compositions/compositions/compositions.preview.runtime.ts b/scopes/compositions/compositions/compositions.preview.runtime.ts index 402fccf11262..279fe5d4902d 100644 --- a/scopes/compositions/compositions/compositions.preview.runtime.ts +++ b/scopes/compositions/compositions/compositions.preview.runtime.ts @@ -18,24 +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; - debugger; - 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) { - debugger; - 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/preview/preview/compute-exposes.ts b/scopes/preview/preview/compute-exposes.ts index 8f84e34a204b..db62da33dbf4 100644 --- a/scopes/preview/preview/compute-exposes.ts +++ b/scopes/preview/preview/compute-exposes.ts @@ -71,7 +71,7 @@ export function getExposedModuleByPreviewDefPrefixFileAndIndex( filePath: string, index: number ): { exposedKey: string; exposedVal: string } { - const exposedKey = computeExposeKey(compFullName, previewDefPrefix, index); + const exposedKey = `./${computeExposeKey(compFullName, previewDefPrefix, index)}`; return { exposedKey, exposedVal: filePath, @@ -80,5 +80,5 @@ export function getExposedModuleByPreviewDefPrefixFileAndIndex( export function computeExposeKey(componentFullName: string, previewDefPrefix: string, index: number): string { const compNameNormalized = normalizeMfName(componentFullName); - return `./${compNameNormalized}_${previewDefPrefix}_${index}`; + return `${compNameNormalized}_${previewDefPrefix}_${index}`; } 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-mf-link.ts b/scopes/preview/preview/generate-mf-link.ts index 6a2d56a626bf..48cb6419c3ed 100644 --- a/scopes/preview/preview/generate-mf-link.ts +++ b/scopes/preview/preview/generate-mf-link.ts @@ -6,7 +6,6 @@ import { computeExposeKey } from './compute-exposes'; export function generateMfLink(prefix: string, componentMap: ComponentMap, defaultModule?: string): string { return ` console.log('mf link file'); - // debugger const promises = [ // import { linkModules } from '${toWindowsCompatiblePath(require.resolve('./preview.preview.runtime'))}'; import('${toWindowsCompatiblePath(require.resolve('./preview.preview.runtime'))}').then(Module => Module.linkModules), @@ -26,9 +25,8 @@ Promise.all(promises).then(([linkModules, harmony]) => { const exposedKey = computeExposeKey(compFullName, prefix, index); // TODO: take teambitReactReactMf dynamically return `() => { - debugger; console.log('inside link modules'); - import('teambitReactReactMf/${exposedKey}').then((Module) => { + return import('teambitReactReactMf/${exposedKey}').then((Module) => { console.log('exposedKey module', Module); return Module; })}`; diff --git a/scopes/preview/preview/preview.main.runtime.tsx b/scopes/preview/preview/preview.main.runtime.tsx index 07f9f6d54971..ad7b7edfe2b7 100644 --- a/scopes/preview/preview/preview.main.runtime.tsx +++ b/scopes/preview/preview/preview.main.runtime.tsx @@ -27,6 +27,7 @@ import { EnvBundlingStrategy, ComponentBundlingStrategy } from './strategies'; import { RuntimeComponents } from './runtime-components'; import { PreviewStartPlugin } from './preview.start-plugin'; import { computeExposes } from './compute-exposes'; +import { generateBootstrapFile } from './generate-bootstrap-file'; const noopResult = { results: [], @@ -148,8 +149,9 @@ export class PreviewMain { const previewRuntime = await this.writePreviewRuntime(context); const linkFiles = await this.updateLinkFiles(context.components, context); // throw new Error('g'); - - return [...linkFiles, previewRuntime]; + const { bootstrapFileName } = this.createBootstrapFile([...linkFiles, previewRuntime], context); + const indexEntryPath = this.createIndexEntryFile(bootstrapFileName, context); + return [indexEntryPath]; } private async updateLinkFiles(components: Component[] = [], context: ExecutionContext, useMf = true) { @@ -175,6 +177,37 @@ export class PreviewMain { return Promise.all(paths); } + private createIndexEntryFile(bootstrapFileName: string, context: ExecutionContext) { + const dirName = join(this.tempFolder, 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; + } + + private createBootstrapFile(entryFilesPaths: string[], context: ExecutionContext) { + const contents = generateBootstrapFile(entryFilesPaths); + const dirName = join(this.tempFolder, 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[] }) { const ui = this.ui.getUi(); if (!ui) throw new Error('ui not found'); 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/react/react/docs/index.tsx b/scopes/react/react/docs/index.tsx index cf6b458b1914..38c2a1064770 100644 --- a/scopes/react/react/docs/index.tsx +++ b/scopes/react/react/docs/index.tsx @@ -1,4 +1,6 @@ -export default function bootstrap(arg1, arg2, arg3, arg4, arg5) { +export default async function bootstrap(arg1, arg2, arg3, arg4, arg5) { // eslint-disable-next-line - import('./bootstrap').then((module) => module.default(arg1, arg2, arg3, arg4, arg5)); + debugger; + // const resolvedCompositions = await arg3; + return import('./bootstrap').then((module) => module.default(arg1, arg2, arg3, arg4, arg5)); } diff --git a/scopes/react/react/mount.tsx b/scopes/react/react/mount.tsx index 4a805e2130c8..5fd0d505bb64 100644 --- a/scopes/react/react/mount.tsx +++ b/scopes/react/react/mount.tsx @@ -1,5 +1,4 @@ export default function bootstrap(arg1, arg2) { - debugger; console.log('arg1', arg1); console.log('arg2', arg2); // eslint-disable-next-line diff --git a/scopes/react/react/react.env.ts b/scopes/react/react/react.env.ts index b833058b7f15..1fe14e4de80f 100644 --- a/scopes/react/react/react.env.ts +++ b/scopes/react/react/react.env.ts @@ -188,7 +188,7 @@ export class ReactEnv implements Environment { const port = context.port; const rootPath = context.rootPath; - const envBaseConfig = envPreviewBaseConfigFactory(mfName, 'localhost', port, rootPath || ''); + const envBaseConfig = envPreviewBaseConfigFactory(mfName, 'http://localhost', port, rootPath || ''); console.log('envBaseConfig', require('util').inspect(envBaseConfig, { depth: 10 })); const envDevConfig = envPreviewDevConfigFactory(context.id); diff --git a/scopes/ui-foundation/ui/create-root-bootstrap.ts b/scopes/ui-foundation/ui/create-root-bootstrap.ts index 7520bbbcc8e8..0a1c2fdac892 100644 --- a/scopes/ui-foundation/ui/create-root-bootstrap.ts +++ b/scopes/ui-foundation/ui/create-root-bootstrap.ts @@ -1,10 +1,14 @@ export async function createRootBootstrap(rootPath: string) { - return `export default function bootstrap() { -debugger -// import('./${rootPath}').then((Module) => { -// debugger; -// return Module -// }); + 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/ui-foundation/ui/create-root.ts b/scopes/ui-foundation/ui/create-root.ts index 5a6457903d12..09a3041a8ad4 100644 --- a/scopes/ui-foundation/ui/create-root.ts +++ b/scopes/ui-foundation/ui/create-root.ts @@ -18,7 +18,6 @@ export async function createRoot( const idSetters = getIdSetters(aspectDefs, 'Aspect'); return ` - debugger ${createImports(aspectDefs)} const isBrowser = typeof window !== "undefined"; @@ -34,7 +33,6 @@ export function render(...props){ const rootExtension = harmony.get('${rootAspect}'); if (isBrowser) { - debugger return rootExtension.render(${rootId}, ...props); } else { return rootExtension.renderSsr(${rootId}, ...props); From 3df4db525119c57f0c95cf9895d1b0fe635b86eb Mon Sep 17 00:00:00 2001 From: Gilad Shoham Date: Tue, 29 Jun 2021 20:57:23 +0300 Subject: [PATCH 12/18] linting --- scopes/preview/preview/preview.main.runtime.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/scopes/preview/preview/preview.main.runtime.tsx b/scopes/preview/preview/preview.main.runtime.tsx index 9fc3e8362681..e536a304e75c 100644 --- a/scopes/preview/preview/preview.main.runtime.tsx +++ b/scopes/preview/preview/preview.main.runtime.tsx @@ -12,7 +12,6 @@ import objectHash from 'object-hash'; import { uniq } from 'lodash'; import { writeFileSync, existsSync, mkdirSync } from 'fs-extra'; import { join } from 'path'; -import { Compiler } from '@teambit/compiler'; import { PkgAspect, PkgMain } from '@teambit/pkg'; import { AspectDefinition, AspectLoaderMain, AspectLoaderAspect } from '@teambit/aspect-loader'; import WorkspaceAspect, { Workspace } from '@teambit/workspace'; From 15c500b2d69fbfd577d09200835bbe48d196492f Mon Sep 17 00:00:00 2001 From: Gilad Shoham Date: Tue, 29 Jun 2021 23:39:12 +0300 Subject: [PATCH 13/18] creating a task to bundle the link files/env itself --- scopes/envs/envs/environments.main.runtime.ts | 15 +++ scopes/harmony/aspect/aspect.env.ts | 15 ++- scopes/harmony/aspect/aspect.main.runtime.ts | 10 +- scopes/preview/preview/bundle-env.task.ts | 70 ++++++++++ scopes/preview/preview/bundling-strategy.ts | 8 +- .../preview/preview/preview.main.runtime.tsx | 27 ++-- scopes/preview/preview/preview.task.ts | 3 +- .../preview/strategies/component-strategy.ts | 47 ++++--- .../preview/strategies/env-mf-strategy.ts | 126 ++++++++++++++++++ yarn.lock | 61 +++------ 10 files changed, 300 insertions(+), 82 deletions(-) create mode 100644 scopes/preview/preview/bundle-env.task.ts create mode 100644 scopes/preview/preview/strategies/env-mf-strategy.ts 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/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/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/preview.main.runtime.tsx b/scopes/preview/preview/preview.main.runtime.tsx index e536a304e75c..6fe40232b694 100644 --- a/scopes/preview/preview/preview.main.runtime.tsx +++ b/scopes/preview/preview/preview.main.runtime.tsx @@ -29,6 +29,8 @@ import { RuntimeComponents } from './runtime-components'; 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'; const noopResult = { results: [], @@ -116,19 +118,16 @@ export class PreviewMain { async writeMfLink( prefix: string, - context: ExecutionContext, + // context: ExecutionContext, moduleMap: ComponentMap, defaultModule: string | undefined, dirName: string ) { - const exposes = await this.computeExposesFromExecutionContext(context); - console.log('exposes', exposes); - console.log('moduleMap', require('util').inspect(moduleMap, { depth: 3 })); + // const exposes = await this.computeExposesFromExecutionContext(context); const contents = generateMfLink(prefix, moduleMap, defaultModule); const hash = objectHash(contents); const targetPath = join(dirName, `__${prefix}-${this.timestamp}.js`); - console.log('targetPath MF link', targetPath); // write only if link has changed (prevents triggering fs watches) if (this.writeHash.get(targetPath) !== hash) { @@ -184,7 +183,7 @@ export class PreviewMain { if (!existsSync(dirPath)) mkdirSync(dirPath, { recursive: true }); const link = useMf - ? await this.writeMfLink(previewDef.prefix, context, withPaths, templatePath, dirPath) + ? await this.writeMfLink(previewDef.prefix, withPaths, templatePath, dirPath) : this.writeLink(previewDef.prefix, withPaths, templatePath, dirPath); return link; }); @@ -192,8 +191,8 @@ export class PreviewMain { return Promise.all(paths); } - private createIndexEntryFile(bootstrapFileName: string, context: ExecutionContext) { - const dirName = join(this.tempFolder, context.id); + 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`); @@ -207,9 +206,9 @@ export class PreviewMain { return targetPath; } - private createBootstrapFile(entryFilesPaths: string[], context: ExecutionContext) { + public createBootstrapFile(entryFilesPaths: string[], context: ExecutionContext, rootDir = this.tempFolder) { const contents = generateBootstrapFile(entryFilesPaths); - const dirName = join(this.tempFolder, context.id); + const dirName = join(rootDir, context.id); const hash = objectHash(contents); const fileName = `__bootstrap-${this.timestamp}.js`; const targetPath = join(dirName, fileName); @@ -259,7 +258,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 @@ -294,9 +293,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; @@ -400,7 +398,8 @@ export class PreviewMain { }, ]); - 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.task.ts b/scopes/preview/preview/preview.task.ts index 472326602a4c..ecabc2fa5dc5 100644 --- a/scopes/preview/preview/preview.task.ts +++ b/scopes/preview/preview/preview.task.ts @@ -3,6 +3,7 @@ import { ExecutionContext } from '@teambit/envs'; import { BuildContext, BuiltTaskResult, BuildTask, TaskLocation } from '@teambit/builder'; import { Bundler, BundlerContext, BundlerMain, Target } from '@teambit/bundler'; import { PreviewMain } from './preview.main.runtime'; +import { PreviewAspect } from './preview.aspect'; export class PreviewTask implements BuildTask { constructor( @@ -17,7 +18,7 @@ export class PreviewTask implements BuildTask { private preview: PreviewMain ) {} - aspectId = 'teambit.preview/preview'; + aspectId = PreviewAspect.id; name = 'GeneratePreview'; location: TaskLocation = 'end'; diff --git a/scopes/preview/preview/strategies/component-strategy.ts b/scopes/preview/preview/strategies/component-strategy.ts index ad052730e4c3..29238e846700 100644 --- a/scopes/preview/preview/strategies/component-strategy.ts +++ b/scopes/preview/preview/strategies/component-strategy.ts @@ -3,7 +3,7 @@ import { ComponentMap } from '@teambit/component'; import { Capsule } from '@teambit/isolator'; import { AbstractVinyl } from '@teambit/legacy/dist/consumer/component/sources'; import { join } from 'path'; -import { BuildContext } from '@teambit/builder'; +import { ArtifactDefinition, BuildContext } from '@teambit/builder'; import { Target, BundlerResult, BundlerContext } from '@teambit/bundler'; import fs from 'fs-extra'; import { BundlingStrategy } from '../bundling-strategy'; @@ -48,19 +48,34 @@ export class ComponentBundlingStrategy implements BundlingStrategy { return filePath; } - async computeResults(context: BundlerContext, results: BundlerResult[], previewTask: PreviewTask) { + 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, @@ -72,15 +87,3 @@ export class ComponentBundlingStrategy implements BundlingStrategy { }); } } - -// link-file.js -// new webpack.container.ModuleFederationPlugin({ -// exposes: { -// // TODO: take the dist file programmatically -// [`./${buttonId}`]: '/Users/giladshoham/Library/Caches/Bit/capsules/d3522af33785e04e8b1199864b9f46951ea3c008/my-scope_ui_button/dist/button.js', -// [`./${buttonId}_composition_1`]: '/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', -// }, -// defaultEposes: './index' -// import ('uiButton') -// }), 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/yarn.lock b/yarn.lock index 40e88953d6f1..615178f5f478 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6431,6 +6431,19 @@ __metadata: languageName: node linkType: hard +"@teambit/documenter.ui.section@npm:4.0.1": + version: 4.0.1 + resolution: "@teambit/documenter.ui.section@npm:4.0.1" + dependencies: + classnames: ^2.2.6 + core-js: ^3.0.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 + react-dom: ^16.8.0 || ^17.0.0 + checksum: d159fcdabdc1068735784a530dd29a2d488eff9559ca820eb1caadd902fc0d494fc3e173f755fa949d9ebf77e8a6a4e8b60a56205f9fca301a429d0e14f4410a + languageName: node + linkType: hard + "@teambit/documenter.ui.separator@npm:1.0.3": version: 1.0.3 resolution: "@teambit/documenter.ui.separator@npm:1.0.3::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40teambit%2Fdocumenter.ui.separator%2F-%2Fdocumenter.ui.separator-1.0.3.tgz" @@ -6825,13 +6838,13 @@ __metadata: languageName: node linkType: hard -"@teambit/legacy@npm:1.0.108": - version: 1.0.108 - resolution: "@teambit/legacy@npm:1.0.108" +"@teambit/legacy@npm:1.0.109": + version: 1.0.109 + resolution: "@teambit/legacy@npm:1.0.109" dependencies: "@babel/core": 7.12.17 "@babel/runtime": 7.12.18 - "@teambit/network.agent": 0.0.1 + "@teambit/toolbox.network.agent": 0.0.116 "@vuedoc/parser": 2.4.0 acorn: 6.4.2 ajv: 6.12.6 @@ -6950,7 +6963,7 @@ __metadata: yn: 2.0.0 bin: bit: bin/bit.js - checksum: 7832ba655b1e3d7f93ddca04069b1242e7fda249f08be16e86fb9202a7d088a3be10e6e78baef5c17eb387035aea546be15a523fe3a22b6252d96763e4a7768c + checksum: cfde2fd07dc9fd00dc3381289eb74308823d538abeb10f337c94a95bd3ec69dc7e6424d6cd870cf794957cbf94db49d191b33d32fa78966ebf48d3b0276531b6 languageName: node linkType: hard @@ -7088,7 +7101,7 @@ __metadata: "@teambit/evangelist.surfaces.dropdown": 1.0.2 "@teambit/evangelist.surfaces.tooltip": 1.0.1 "@teambit/harmony": 0.2.11 - "@teambit/legacy": 1.0.108 + "@teambit/legacy": 1.0.109 "@teambit/mdx.ui.mdx-scope-context": 0.0.368 "@teambit/react.instructions.react.adding-compositions": 0.0.5 "@teambit/react.instructions.react.adding-tests": 0.0.5 @@ -7485,7 +7498,7 @@ __metadata: yargs: 17.0.1 yn: 2.0.0 peerDependencies: - "@teambit/legacy": 1.0.108 + "@teambit/legacy": 1.0.109 bin: bit: bin/bit.js languageName: unknown @@ -7533,38 +7546,6 @@ __metadata: languageName: node linkType: hard -"@teambit/network.agent@npm:0.0.1": - version: 0.0.1 - resolution: "@teambit/network.agent@npm:0.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40teambit%2Fnetwork.agent%2F-%2Fnetwork.agent-0.0.1.tgz" - dependencies: - "@teambit/network.proxy-agent": 0.0.1 - agentkeepalive: 4.1.4 - core-js: 3.8.3 - peerDependencies: - "@teambit/legacy": 1.0.38 - react: 16.13.1 - react-dom: 16.13.1 - checksum: 5cea7497801060f00ad8cd090a8861383dd2d043b4df4a91c08369ebd18943b463c9fe9c06dbf4b3b1daf00b2281252cf84db5cd1a2b84e1fdcc6f08b81304b5 - languageName: node - linkType: hard - -"@teambit/network.proxy-agent@npm:0.0.1": - version: 0.0.1 - resolution: "@teambit/network.proxy-agent@npm:0.0.1::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40teambit%2Fnetwork.proxy-agent%2F-%2Fnetwork.proxy-agent-0.0.1.tgz" - dependencies: - "@teambit/network.agent": 0.0.1 - core-js: 3.8.3 - http-proxy-agent: 4.0.1 - https-proxy-agent: 5.0.0 - socks-proxy-agent: 5.0.0 - peerDependencies: - "@teambit/legacy": 1.0.38 - react: 16.13.1 - react-dom: 16.13.1 - checksum: d9b2008ddbe2eb2043b50b44c75ca70ca44c9e6583674925915edfc23087d0fbd9e8bb5bd8475c06345a0c3b5661265d8a12c5d893e930508c4b6b284addfef9 - languageName: node - linkType: hard - "@teambit/react.instructions.react.adding-compositions@npm:0.0.5": version: 0.0.5 resolution: "@teambit/react.instructions.react.adding-compositions@npm:0.0.5::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40teambit%2Freact.instructions.react.adding-compositions%2F-%2Freact.instructions.react.adding-compositions-0.0.5.tgz" @@ -15851,6 +15832,8 @@ __metadata: "docs-app-55ecfa@workspace:scopes/react/ui/docs-app": version: 0.0.0-use.local resolution: "docs-app-55ecfa@workspace:scopes/react/ui/docs-app" + dependencies: + "@teambit/documenter.ui.section": 4.0.1 languageName: unknown linkType: soft From d9ca320a0d06e880fe3ff6970532ffc9ba07c3f4 Mon Sep 17 00:00:00 2001 From: Gilad Shoham Date: Sun, 18 Jul 2021 21:41:17 +0300 Subject: [PATCH 14/18] compile fix --- scopes/react/react/react.env.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/scopes/react/react/react.env.ts b/scopes/react/react/react.env.ts index c83e46803231..e8dac77cea42 100644 --- a/scopes/react/react/react.env.ts +++ b/scopes/react/react/react.env.ts @@ -52,7 +52,6 @@ import componentPreviewProdConfigFactory from './webpack/webpack.config.componen import componentPreviewDevConfigFactory from './webpack/webpack.config.component.dev'; import { ReactMainConfig } from './react.main.runtime'; -import { eslintConfig } from './eslint/eslintrc'; import { ReactAspect } from './react.aspect'; export const AspectEnvType = 'react'; From 6de14e914fdc284780a1fd65926433f4768b9e88 Mon Sep 17 00:00:00 2001 From: Gilad Shoham Date: Sun, 18 Jul 2021 22:11:30 +0300 Subject: [PATCH 15/18] merge fixes --- .../webpack/webpack/webpack.main.runtime.ts | 2 +- yarn.lock | 40 +++++++++++-------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/scopes/webpack/webpack/webpack.main.runtime.ts b/scopes/webpack/webpack/webpack.main.runtime.ts index 771e09783733..221477f7d640 100644 --- a/scopes/webpack/webpack/webpack.main.runtime.ts +++ b/scopes/webpack/webpack/webpack.main.runtime.ts @@ -72,7 +72,7 @@ export class WebpackMain { const afterMutation = runTransformersWithContext(configMutator.clone(), transformers, transformerContext); console.log(afterMutation.raw.entry); // @ts-ignore - fix this - return new WebpackDevServer(afterMutation.raw); + return new WebpackDevServer(afterMutation.raw, webpack, WsDevServer); } createComponentDevServer(context: DevServerContext, transformers: WebpackConfigTransformer[] = []): DevServer { diff --git a/yarn.lock b/yarn.lock index 1a5e19eba517..414aa62c2eb3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6950,7 +6950,6 @@ __metadata: "@teambit/evangelist.surfaces.tooltip": 1.0.1 "@teambit/harmony": 0.2.11 "@teambit/legacy": 1.0.124 - "@teambit/mdx.ui.mdx-scope-context": 0.0.368 "@teambit/network.agent": 0.0.1 "@teambit/react.instructions.react.adding-compositions": 0.0.5 "@teambit/react.instructions.react.adding-tests": 0.0.5 @@ -6999,7 +6998,7 @@ __metadata: "@types/pino": 6.3.6 "@types/prettier": 2.3.2 "@types/puppeteer": 3.0.5 - "@types/react": ^17.0.8 + "@types/react": 17.0.8 "@types/react-dom": ^17.0.5 "@types/react-router-dom": 5.1.7 "@types/react-tabs": 2.3.2 @@ -7202,6 +7201,7 @@ __metadata: mz: 2.7.0 nanoid: 3.1.20 nerf-dart: 1.0.0 + new-url-loader: 0.1.1 node-fetch: 2.6.1 node-source-walk: 4.2.0 normalize-path: 2.1.1 @@ -7350,25 +7350,11 @@ __metadata: yn: 2.0.0 peerDependencies: "@teambit/legacy": 1.0.124 - jest: 26.6.3 bin: bit: bin/bit.js languageName: unknown linkType: soft -"@teambit/mdx.ui.mdx-scope-context@npm:0.0.368": - version: 0.0.368 - resolution: "@teambit/mdx.ui.mdx-scope-context@npm:0.0.368::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40teambit%2Fmdx.ui.mdx-scope-context%2F-%2Fmdx.ui.mdx-scope-context-0.0.368.tgz" - dependencies: - core-js: ^3.0.0 - peerDependencies: - "@teambit/legacy": 1.0.86 - react: ^16.8.0 || ^17.0.0 - react-dom: ^16.8.0 || ^17.0.0 - checksum: 1fa908b761a60bb0f8f5e0d0e4984bd5cabbb51136ab43bad71732e75e6c2d9306fe160725dfc0f11f5528d515bc79c38ee8b627f525ea6ff38b626e4606fb0a - languageName: node - linkType: hard - "@teambit/model.composition-id@npm:0.0.259": version: 0.0.259 resolution: "@teambit/model.composition-id@npm:0.0.259::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40teambit%2Fmodel.composition-id%2F-%2Fmodel.composition-id-0.0.259.tgz" @@ -8540,7 +8526,7 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:*, @types/react@npm:^17.0.8": +"@types/react@npm:*": version: 17.0.13 resolution: "@types/react@npm:17.0.13" dependencies: @@ -8551,6 +8537,17 @@ __metadata: languageName: node linkType: hard +"@types/react@npm:17.0.8": + version: 17.0.8 + resolution: "@types/react@npm:17.0.8" + dependencies: + "@types/prop-types": "*" + "@types/scheduler": "*" + csstype: ^3.0.2 + checksum: 6398dc200623dbf213747f3a1cf9f3c3bcac29b519e49c9fcb81d2665dadb21155c485381cddbb04b483a5bf3ddddefaff1a1a6ef4fa5080b1b9b144c789673e + languageName: node + linkType: hard + "@types/resolve@npm:1.17.1": version: 1.17.1 resolution: "@types/resolve@npm:1.17.1" @@ -24849,6 +24846,15 @@ __metadata: languageName: node linkType: hard +"new-url-loader@npm:0.1.1": + version: 0.1.1 + resolution: "new-url-loader@npm:0.1.1" + peerDependencies: + webpack: ^5.0.0 + checksum: 8b060835c49dfa8f8ec7c2319e453d99a4e5c25c9c5f6f69bea76c42e386f46ca0eab0e6e984bc8915b40078ae27775a66a3d76235ee80ac034db48af11a0ca2 + languageName: node + linkType: hard + "next-path@npm:^1.0.0": version: 1.0.0 resolution: "next-path@npm:1.0.0" From 580d41bcea1b47719841347b82f7434bc3fbfcf3 Mon Sep 17 00:00:00 2001 From: Gilad Shoham Date: Wed, 21 Jul 2021 14:57:57 +0300 Subject: [PATCH 16/18] split ui root file into 2 different files - core aspects and host aspects --- .../component/component/component-factory.ts | 5 + .../preview/preview/preview.main.runtime.tsx | 4 +- scopes/scope/scope/scope.main.runtime.ts | 12 +++ scopes/ui-foundation/ui/create-core-root.ts | 93 ++++++++++++++++ scopes/ui-foundation/ui/create-host-root.ts | 65 +++++++++++ .../ui-foundation/ui/create-root-bootstrap.ts | 2 +- scopes/ui-foundation/ui/ui.main.runtime.ts | 102 +++++++++++++++--- 7 files changed, 263 insertions(+), 20 deletions(-) create mode 100644 scopes/ui-foundation/ui/create-core-root.ts create mode 100644 scopes/ui-foundation/ui/create-host-root.ts 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/preview/preview/preview.main.runtime.tsx b/scopes/preview/preview/preview.main.runtime.tsx index 996f72b5e42a..985cd5783e17 100644 --- a/scopes/preview/preview/preview.main.runtime.tsx +++ b/scopes/preview/preview/preview.main.runtime.tsx @@ -228,13 +228,13 @@ export class PreviewMain { return { bootstrapPath: targetPath, bootstrapFileName: fileName }; } - async writePreviewRuntime(context: { components: Component[] }) { + 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.ui.generateRoot(filteredAspects, name, 'preview', PreviewAspect.id, rootDir); console.log('filePath', filePath); return filePath; } 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/ui-foundation/ui/create-core-root.ts b/scopes/ui-foundation/ui/create-core-root.ts new file mode 100644 index 000000000000..8cc7ab92d64a --- /dev/null +++ b/scopes/ui-foundation/ui/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.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/ui-foundation/ui/create-host-root.ts b/scopes/ui-foundation/ui/create-host-root.ts new file mode 100644 index 000000000000..6ed31d15d9b7 --- /dev/null +++ b/scopes/ui-foundation/ui/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[], coreRootName: 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('${coreRootName}').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/ui-foundation/ui/create-root-bootstrap.ts b/scopes/ui-foundation/ui/create-root-bootstrap.ts index 0a1c2fdac892..a3ff562433a8 100644 --- a/scopes/ui-foundation/ui/create-root-bootstrap.ts +++ b/scopes/ui-foundation/ui/create-root-bootstrap.ts @@ -9,7 +9,7 @@ export async function createRootBootstrap(rootPath: string) { export default async function bootstrap() { // await importReactP; -return import('./${rootPath}') + return import('./${rootPath}') } bootstrap(); `; diff --git a/scopes/ui-foundation/ui/ui.main.runtime.ts b/scopes/ui-foundation/ui/ui.main.runtime.ts index 12921493a8d1..ee3a5eb7d31e 100644 --- a/scopes/ui-foundation/ui/ui.main.runtime.ts +++ b/scopes/ui-foundation/ui/ui.main.runtime.ts @@ -1,12 +1,15 @@ import type { AspectMain } from '@teambit/aspect'; +import { groupBy } from 'lodash'; 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 +22,8 @@ import { join, resolve } from 'path'; import { promisify } from 'util'; import webpack from 'webpack'; import { UiServerStartedEvent } from './events'; -import { createRoot } from './create-root'; +import { createHostRoot } from './create-host-root'; +import { createCoreRoot } from './create-core-root'; import { UnknownUI, UnknownBuildError } from './exceptions'; import { StartCmd } from './start.cmd'; import { UIBuildCmd } from './ui-build.cmd'; @@ -32,7 +36,17 @@ import createSsrWebpackConfig from './webpack/webpack.ssr.config'; import { StartPlugin, StartPluginOptions } from './start-plugin'; import { createRootBootstrap } from './create-root-bootstrap'; -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; @@ -112,6 +126,8 @@ export type RuntimeOptions = { rebuild?: boolean; }; +const DEFAULT_TEMP_DIR = join(CACHE_ROOT, UIAspect.id); + export class UiMain { constructor( /** @@ -171,11 +187,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) { @@ -399,26 +421,70 @@ export class UiMain { aspectDefs: AspectDefinition[], rootExtensionName: string, runtimeName = UIRuntime.name, - rootAspect = UIAspect.id + rootAspect = UIAspect.id, + rootTempDir = this.tempFolder ) { - const contents = await createRoot( - aspectDefs, - rootExtensionName, - rootAspect, - runtimeName, - this.harmony.config.toObject() - ); - const rootRelativePath = `${runtimeName}.root${sha1(contents)}.js`; - const filepath = resolve(join(__dirname, rootRelativePath)); - const rootBootstrapContents = await createRootBootstrap(rootRelativePath); + // 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); + this.writeCoreUiRoot(aspectsGroups.core, rootExtensionName, runtimeName, rootAspect); + const hostRootFilePath = this.writeHostUIRoot(aspectsGroups.host, 'coreRootMfName', runtimeName, rootTempDir); + + const rootBootstrapContents = await createRootBootstrap(hostRootFilePath.relativePath); const rootBootstrapRelativePath = `${runtimeName}.root${sha1(rootBootstrapContents)}-bootstrap.js`; - const rootBootstrapPath = resolve(join(__dirname, rootBootstrapRelativePath)); - if (fs.existsSync(filepath) && fs.existsSync(rootBootstrapPath)) return rootBootstrapPath; - fs.outputFileSync(filepath, contents); + 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[] = [], + coreRootName: string, + runtimeName = UIRuntime.name, + rootTempDir = this.tempFolder + ) { + const contents = createHostRoot(hostAspects, coreRootName, 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 }; + } + private async selectPort() { const [from, to] = this.config.portRange; const usedPorts = (await this.cache.get(`${from}${to}`)) || []; @@ -514,6 +580,7 @@ export class UiMain { ComponentAspect, CacheAspect, LoggerAspect, + AspectLoaderAspect, ]; static slots = [ @@ -526,7 +593,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, @@ -554,6 +621,7 @@ export class UiMain { componentExtension, cache, logger, + aspectLoader, harmony, proxyGetterSlot ); From e0dc3b6ac9838c407dac8c5ff4fad41f39ae3a22 Mon Sep 17 00:00:00 2001 From: Gilad Shoham Date: Mon, 26 Jul 2021 10:21:32 +0300 Subject: [PATCH 17/18] import core root from file --- scopes/ui-foundation/ui/create-host-root.ts | 4 ++-- scopes/ui-foundation/ui/ui.main.runtime.ts | 13 +++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/scopes/ui-foundation/ui/create-host-root.ts b/scopes/ui-foundation/ui/create-host-root.ts index 6ed31d15d9b7..80d99399911c 100644 --- a/scopes/ui-foundation/ui/create-host-root.ts +++ b/scopes/ui-foundation/ui/create-host-root.ts @@ -3,7 +3,7 @@ import { toWindowsCompatiblePath } from '@teambit/toolbox.path.to-windows-compat import { camelCase } from 'lodash'; import { parse } from 'path'; -export function createHostRoot(aspectDefs: AspectDefinition[], coreRootName: string, config = {}) { +export function createHostRoot(aspectDefs: AspectDefinition[], coreRootPath: string, config = {}) { const identifiers = getIdentifiers(aspectDefs, 'Aspect'); const idSetters = getIdSetters(aspectDefs, 'Aspect'); @@ -12,7 +12,7 @@ ${createImports(aspectDefs)} const config = JSON.parse('${toWindowsCompatiblePath(JSON.stringify(config))}'); ${idSetters.join('\n')} -const coreRoot = import('${coreRootName}').then(coreRoot => { +const coreRoot = import('${coreRootPath}').then(coreRoot => { const render = coreRoot.render; render(config, [${identifiers.join()}]); }); diff --git a/scopes/ui-foundation/ui/ui.main.runtime.ts b/scopes/ui-foundation/ui/ui.main.runtime.ts index 2a3c972dfb1f..67120f21bd47 100644 --- a/scopes/ui-foundation/ui/ui.main.runtime.ts +++ b/scopes/ui-foundation/ui/ui.main.runtime.ts @@ -436,8 +436,13 @@ export class UiMain { }); // const coreRootFilePath = this.writeCoreUiRoot(aspectsGroups.core, rootExtensionName, runtimeName, rootAspect); - this.writeCoreUiRoot(aspectsGroups.core, rootExtensionName, runtimeName, rootAspect); - const hostRootFilePath = this.writeHostUIRoot(aspectsGroups.host, 'coreRootMfName', runtimeName, rootTempDir); + 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`; @@ -474,11 +479,11 @@ export class UiMain { */ private writeHostUIRoot( hostAspects: AspectDefinition[] = [], - coreRootName: string, + coreRootPath: string, runtimeName = UIRuntime.name, rootTempDir = this.tempFolder ) { - const contents = createHostRoot(hostAspects, coreRootName, this.harmony.config.toObject()); + 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); From c102480e892cfe5f966426c9f1cc736c93cf9ec3 Mon Sep 17 00:00:00 2001 From: Gilad Shoham Date: Mon, 26 Jul 2021 11:42:13 +0300 Subject: [PATCH 18/18] moving create of preview server entries to the preview aspect --- .../preview}/create-core-root.ts | 2 +- .../preview}/create-host-root.ts | 0 .../preview}/create-root-bootstrap.ts | 0 .../preview/preview/preview.main.runtime.tsx | 88 ++++++++++++++++++- scopes/ui-foundation/ui/ui.main.runtime.ts | 76 +++------------- 5 files changed, 95 insertions(+), 71 deletions(-) rename scopes/{ui-foundation/ui => preview/preview}/create-core-root.ts (97%) rename scopes/{ui-foundation/ui => preview/preview}/create-host-root.ts (100%) rename scopes/{ui-foundation/ui => preview/preview}/create-root-bootstrap.ts (100%) diff --git a/scopes/ui-foundation/ui/create-core-root.ts b/scopes/preview/preview/create-core-root.ts similarity index 97% rename from scopes/ui-foundation/ui/create-core-root.ts rename to scopes/preview/preview/create-core-root.ts index 8cc7ab92d64a..c01d4463534a 100644 --- a/scopes/ui-foundation/ui/create-core-root.ts +++ b/scopes/preview/preview/create-core-root.ts @@ -3,7 +3,7 @@ import { toWindowsCompatiblePath } from '@teambit/toolbox.path.to-windows-compat import { camelCase } from 'lodash'; import { parse } from 'path'; -import { UIAspect } from './ui.aspect'; +import { UIAspect } from '../../ui-foundation/ui/ui.aspect'; export function createCoreRoot( aspectDefs: AspectDefinition[], diff --git a/scopes/ui-foundation/ui/create-host-root.ts b/scopes/preview/preview/create-host-root.ts similarity index 100% rename from scopes/ui-foundation/ui/create-host-root.ts rename to scopes/preview/preview/create-host-root.ts diff --git a/scopes/ui-foundation/ui/create-root-bootstrap.ts b/scopes/preview/preview/create-root-bootstrap.ts similarity index 100% rename from scopes/ui-foundation/ui/create-root-bootstrap.ts rename to scopes/preview/preview/create-root-bootstrap.ts diff --git a/scopes/preview/preview/preview.main.runtime.tsx b/scopes/preview/preview/preview.main.runtime.tsx index 985cd5783e17..54fa1a222dea 100644 --- a/scopes/preview/preview/preview.main.runtime.tsx +++ b/scopes/preview/preview/preview.main.runtime.tsx @@ -1,3 +1,4 @@ +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'; @@ -9,9 +10,9 @@ 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'; @@ -32,6 +33,9 @@ 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: [], @@ -234,11 +238,87 @@ export class PreviewMain { 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, rootDir); + 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 diff --git a/scopes/ui-foundation/ui/ui.main.runtime.ts b/scopes/ui-foundation/ui/ui.main.runtime.ts index 67120f21bd47..6368684f8154 100644 --- a/scopes/ui-foundation/ui/ui.main.runtime.ts +++ b/scopes/ui-foundation/ui/ui.main.runtime.ts @@ -1,5 +1,4 @@ import type { AspectMain } from '@teambit/aspect'; -import { groupBy } from 'lodash'; import { ComponentType } from 'react'; import { AspectDefinition } from '@teambit/aspect-loader'; import { CacheAspect, CacheMain } from '@teambit/cache'; @@ -22,8 +21,6 @@ import { join, resolve } from 'path'; import { promisify } from 'util'; import webpack from 'webpack'; import { UiServerStartedEvent } from './events'; -import { createHostRoot } from './create-host-root'; -import { createCoreRoot } from './create-core-root'; import { UnknownUI, UnknownBuildError } from './exceptions'; import { StartCmd } from './start.cmd'; import { UIBuildCmd } from './ui-build.cmd'; @@ -34,7 +31,7 @@ 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 { createRootBootstrap } from './create-root-bootstrap'; +import { createRoot } from './create-root'; export type UIDeps = [ PubsubMain, @@ -423,73 +420,20 @@ export class UiMain { aspectDefs: AspectDefinition[], rootExtensionName: string, runtimeName = UIRuntime.name, - rootAspect = UIAspect.id, - rootTempDir = this.tempFolder + rootAspect = UIAspect.id ) { - // 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, + const contents = await createRoot( + aspectDefs, rootExtensionName, + rootAspect, runtimeName, - rootAspect + this.harmony.config.toObject() ); - 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 }; + 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 { fullPath: filepath, relativePath: rootRelativePath }; + return filepath; } private async selectPort() {