diff --git a/package.json b/package.json index b557778df8c..fcd962c97c6 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ ], "license": "MIT", "scripts": { + "buildLibs": "nx run-many --target=buildLib --maxParallel=1", "build": "nx build --verbose --output-style=stream", "start": "nx build && nx run-many --target=serve --verbose --output-style=stream", "prod": "nx build && nx run-many --target=build --verbose --output-style=stream", diff --git a/packages/nextjs-mf/project.json b/packages/nextjs-mf/project.json index b57a9065f73..79d0d92d369 100644 --- a/packages/nextjs-mf/project.json +++ b/packages/nextjs-mf/project.json @@ -15,6 +15,24 @@ "assets": ["packages/nextjs-mf/*.md"] } }, + "buildLib": { + "executor": "@nrwl/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/packages/nextjs-mf", + "main": "packages/nextjs-mf/src/index.js", + "tsConfig": "packages/nextjs-mf/tsconfig.lib.json", + "buildableProjectDepsInPackageJsonType": "dependencies", + "updateBuildableProjectDepsInPackageJson": true, + "assets": ["packages/nextjs-mf/*.md"] + }, + "dependsOn": [ + { + "projects": "dependencies", + "target": "buildLib" + } + ] + }, "publish": { "executor": "@nrwl/workspace:run-commands", "options": { @@ -22,7 +40,7 @@ }, "dependsOn": [ { - "projects": "self", + "projects": "dependencies", "target": "build" } ] diff --git a/packages/nextjs-mf/src/include-defaults-sc.ts b/packages/nextjs-mf/src/include-defaults-sc.ts new file mode 100644 index 00000000000..f35be9d1cd0 --- /dev/null +++ b/packages/nextjs-mf/src/include-defaults-sc.ts @@ -0,0 +1,31 @@ +// this is needed to ensure webpack does not attempt to tree shake unused modules. Since these should always come from host +// require('react?shared'); +require('next/link?shared'); +require('next/link'); +require('next/navigation'); +require('next/link?client'); +require('next/head?shared'); +require('next/script?shared'); +require('next/dist/client/components/layout-router?shared') +require('next/dist/client/components/app-router?shared') +require('next/dist/client/components/render-from-template-context?shared') +require('next/dist/client/components/request-async-storage?shared') +require('next/dist/client/components/static-generation-async-storage?shared') +require('next/dist/compiled/react-server-dom-webpack/server.browser') +// require('next/dist/client/components/error-boundary?shared') +try { + require('next/dist/compiled/react/react.shared-subset'); + require('next/dist/compiled/react/react.shared-subset?shared'); +} catch (e) {} +//require('next/dynamic?shared'); +require('next/navigation?client') +require('next/navigation?shared') +if(typeof window === 'undefined') { + require('next/dist/client/components/hooks-server-context?shared'); +} +//@ts-ignore +if (process.env.NODE_ENV === 'development') { + require('react/jsx-dev-runtime'); +} + +module.exports = {}; diff --git a/packages/nextjs-mf/src/include-defaults.ts b/packages/nextjs-mf/src/include-defaults.ts index 5ad9557624f..c9b2d1b1c9d 100644 --- a/packages/nextjs-mf/src/include-defaults.ts +++ b/packages/nextjs-mf/src/include-defaults.ts @@ -2,11 +2,17 @@ require('react'); require('react-dom'); require('next/link'); +require('next/link?shared'); +require('next/link?client'); require('next/router'); require('next/head'); require('next/script'); require('next/dynamic'); require('styled-jsx'); +require('next/navigation'); +require('next/navigation?client') +require('next/navigation?shared') + if (process.env['NODE_ENV'] === 'development') { require('react/jsx-dev-runtime'); diff --git a/packages/nextjs-mf/src/internal.ts b/packages/nextjs-mf/src/internal.ts index 7b14d5b56fd..5aa68213f34 100644 --- a/packages/nextjs-mf/src/internal.ts +++ b/packages/nextjs-mf/src/internal.ts @@ -1,78 +1,201 @@ -import type { Compiler } from 'webpack'; +import type { Compiler } from "webpack"; import type { ModuleFederationPluginOptions, Shared, SharedConfig, - SharedObject, -} from '@module-federation/utilities'; + SharedObject +} from "@module-federation/utilities"; -import path from 'path'; -import { parseOptions } from 'webpack/lib/container/options'; -import { isRequiredVersion } from 'webpack/lib/sharing/utils'; +import path from "path"; +import { parseOptions } from "webpack/lib/container/options"; +import { isRequiredVersion } from "webpack/lib/sharing/utils"; -import { extractUrlAndGlobal } from '@module-federation/utilities'; +import { extractUrlAndGlobal } from "@module-federation/utilities"; // the share scope we attach by default // in hosts we re-key them to prevent webpack moving the modules into their own chunks (cause eager error) // in remote these are marked as import:false as we always expect the host to prove them export const DEFAULT_SHARE_SCOPE: SharedObject = { - react: { + "react/": { singleton: true, - requiredVersion: false, + requiredVersion: false }, - 'react/jsx-runtime': { - singleton: true, + + // "react": { + // singleton: true, + // requiredVersion: false, + // }, + + // 'next/': { + // singleton: true, + // requiredVersion: false, + // }, + // 'react-dom': { + // singleton: true, + // requiredVersion: false, + // }, + "next/dynamic": { requiredVersion: false, + singleton: true }, - 'react-dom': { - singleton: true, + "styled-jsx": { requiredVersion: false, + singleton: true }, - 'next/dynamic': { + "styled-jsx/style": { requiredVersion: false, - singleton: true, + singleton: true }, - 'styled-jsx': { + "next/router": { requiredVersion: false, - singleton: true, + singleton: true }, - 'styled-jsx/style': { + "next/script": { requiredVersion: false, - singleton: true, + singleton: true }, - 'next/link': { + "next/head": { requiredVersion: false, - singleton: true, + singleton: true }, - 'next/router': { + "next/link": { requiredVersion: false, + singleton: true + } +}; + +export const RSC_SHARE_SCOPE: SharedObject = { + "next/link": { + import: "next/link", singleton: true, - }, - 'next/script': { requiredVersion: false, - singleton: true, + shareKey: "next/link" }, - 'next/head': { - requiredVersion: false, + "next/dist/compiled/react/": { singleton: true, + requiredVersion: false }, + // "next/headers": { + // requiredVersion: false, + // singleton: true, + // }, + // "next/dist/client/components/layout-router?shared":{ + // requiredVersion: false, + // singleton: true, + // import: "next/dist/client/components/layout-router?shared", + // shareKey: "next/dist/client/components/layout-router", + // }, + // "next/dist/client/components/app-router?shared":{ + // requiredVersion: false, + // singleton: true, + // import: "next/dist/client/components/app-router?shared", + // shareKey: "next/dist/client/components/app-router", + // }, + // "next/dist/client/components/hooks-server-context":{ + // import:"next/dist/client/components/hooks-server-context", + // shareKey: "next/dist/client/components/hooks-server-context", + // requiredVersion: false, + // singleton: true, + // }, + // "next/dist/client/components/render-from-template-context?shared":{ + // requiredVersion: false, + // singleton: true, + // import: "next/dist/client/components/render-from-template-context?shared", + // shareKey: "next/dist/client/components/render-from-template-context", + // }, + // "next/dist/client/components/request-async-storage?shared":{ + // requiredVersion: false, + // singleton: true, + // import: "next/dist/client/components/request-async-storage?shared", + // shareKey: "next/dist/client/components/request-async-storage", + // }, + // "next/dist/client/components/static-generation-async-storage?shared":{ + // import: "next/dist/client/components/static-generation-async-storage?shared", + // shareKey: "next/dist/client/components/static-generation-async-storage", + // requiredVersion: false, + // singleton: true, + // }, + // "next/dist/compiled/react-server-dom-webpack/server.browser?shared":{ + // requiredVersion: false, + // singleton: true, + // import: "next/dist/compiled/react-server-dom-webpack/server.browser?shared", + // shareKey: "next/dist/compiled/react-server-dom-webpack/server.browser", + // }, + // "react": { + // import: "react", + // shareKey: "react", + // singleton: false, + // requiredVersion: false, + // }, + // "next/dist/compiled/react/react.shared-subset":{ + // requiredVersion: false, + // singleton: true, + // import: "next/dist/compiled/react/react.shared-subset?shared", + // shareKey: "next/dist/compiled/react/react.shared-subset", + // }, + // "next/dist/client/components/error-boundary":{ + // requiredVersion: false, + // singleton: true, + // import: "next/dist/client/components/error-boundary?shared", + // shareKey: "next/dist/client/components/error-boundary", + // }, + // "next/dist/compiled/": { + // singleton: true, + // requiredVersion: false, + // }, + // // 'next/dynamic': { + // // import: "next/dynamic?shared", + // // requiredVersion: false, + // // singleton: true, + // // }, + // // "private-next-rsc-mod-ref-proxy": { + // // singleton:true, + // // requiredVersion: false, + // // }, + // // "next/script?shared": { + // // import: "next/script?shared", + // // shareKey: "next/script", + // // singleton: true, + // // requiredVersion: false, + // // }, + "next/navigation": { + shareKey: "next/navigation", + import: "next/navigation", + singleton: true, + requiredVersion: false + } }; +export const EXTERNAL_NEXT_DEPS = [ + "next/dist/shared/lib/app-router-context", + "react/jsx-dev-runtime" +]; + // put host in-front of any shared module key, so "hostreact" export const reKeyHostShared = ( - options: Shared = {} + options: Shared = {}, + compilerOptions: object, + hasAppDir: boolean, + isServer: boolean ): Record => { const shared = { ...options, ...DEFAULT_SHARE_SCOPE, + // if react server components / has app dir + ...((hasAppDir) ? RSC_SHARE_SCOPE : {}) } as Record; return Object.entries(shared).reduce((acc, item) => { const [itemKey, shareOptions] = item; - const shareKey = 'host' + ((item as any).shareKey || itemKey); + const shareKey = "host" + ((item as any).shareKey || itemKey); + + acc[shareKey] = shareOptions; + //acc[shareKey].eager = true; + + if (!shareOptions.import) { acc[shareKey].import = itemKey; } @@ -84,6 +207,29 @@ export const reKeyHostShared = ( if (DEFAULT_SHARE_SCOPE[itemKey]) { acc[shareKey].packageName = itemKey; } +// console.log('reKeyHostShared', {hasAppDir,isServer, itemKey, inScope: RSC_SHARE_SCOPE[itemKey]}) + if (hasAppDir && RSC_SHARE_SCOPE[itemKey]) { + const layer = !isServer ? "shared" : "client"; + // if(!['next/link'].includes(itemKey)) { + // if (shareOptions.import && !shareOptions.import.includes("?")) { + // acc[shareKey].import = `${acc[shareKey].import}?${layer}`; + // } + // } + //@ts-ignore + // const {rootDir,hasServerComponents,isServerLayer} = compilerOptions.module.rules[7].oneOf[2].use.options + // //@ts-ignore + // console.log(compilerOptions.module.rules[7].oneOf[2].use.options) + // //@ts-ignore + // console.log(compilerOptions.module.rules[7].oneOf[4].use.options) + // //@ts-ignore + // const flightLoader = compilerOptions.resolveLoader.alias['next-flight-loader']; + // //@ts-ignore + // const swcLoader = compilerOptions.resolveLoader.alias['next-swc-loader'] + `?${JSON.stringify({rootDir,hasServerComponents,isServer,isServerLayer, isServer})}` + // //@ts-ignore + // const appLoader = compilerOptions.resolveLoader.alias['next-app-loader']; + // acc[shareKey].import = `${flightLoader}!${swcLoader}!${shareOptions.import || itemKey}`; + // console.log(acc[shareKey]); + } return acc; }, {} as Record); @@ -179,7 +325,7 @@ export const internalizeSharedPackages = ( // take original externals regex const backupExternals = compiler.options.externals[0]; // if externals is a function (like when you're not running in serverless mode or creating a single build) - if (typeof backupExternals === 'function') { + if (typeof backupExternals === "function") { // replace externals function with short-circuit, or fall back to original algo compiler.options.externals[0] = (mod, callback) => { if (!internalizableKeys.some((v) => mod.request?.includes(v))) { @@ -192,14 +338,16 @@ export const internalizeSharedPackages = ( } }; -export const externalizedShares: SharedObject = Object.entries( - DEFAULT_SHARE_SCOPE +export const externalizedShares = (isServer: boolean): SharedObject => Object.entries( + { ...DEFAULT_SHARE_SCOPE, ...RSC_SHARE_SCOPE } ).reduce((acc, item) => { const [key, value] = item as [string, SharedConfig]; - + // if (!isServer) { + // if (key.includes("next/navigation")) return acc; + // } acc[key] = { ...value, import: false }; - if (key === 'react/jsx-runtime') { + if (key === "react/jsx-runtime") { delete (acc[key] as SharedConfig).import; } @@ -208,12 +356,12 @@ export const externalizedShares: SharedObject = Object.entries( // determine output base path, derives .next folder location export const getOutputPath = (compiler: Compiler) => { - const isServer = compiler.options.target !== 'client'; + const isServer = compiler.options.target !== "client"; let outputPath: string | string[] | undefined = compiler.options.output.path?.split(path.sep); const foundIndex = outputPath?.findIndex((i) => { - return i === (isServer ? 'server' : 'static'); + return i === (isServer ? "server" : "static"); }); outputPath = outputPath @@ -224,21 +372,21 @@ export const getOutputPath = (compiler: Compiler) => { }; export const removePlugins = [ - 'NextJsRequireCacheHotReloader', - 'BuildManifestPlugin', - 'WellKnownErrorsPlugin', - 'WebpackBuildEventsPlugin', - 'HotModuleReplacementPlugin', - 'NextMiniCssExtractPlugin', - 'NextFederationPlugin', - 'CopyFilePlugin', - 'ProfilingPlugin', - 'DropClientPage', - 'ReactFreshWebpackPlugin', + "NextJsRequireCacheHotReloader", + "BuildManifestPlugin", + "WellKnownErrorsPlugin", + "WebpackBuildEventsPlugin", + "HotModuleReplacementPlugin", + "NextMiniCssExtractPlugin", + "NextFederationPlugin", + "CopyFilePlugin", + "ProfilingPlugin", + "DropClientPage", + "ReactFreshWebpackPlugin" ]; export const parseRemoteSyntax = (remote: string) => { - if (typeof remote === 'string' && remote.includes('@')) { + if (typeof remote === "string" && remote.includes("@")) { const [url, global] = extractUrlAndGlobal(remote); return generateRemoteTemplate(url, global); } @@ -248,8 +396,8 @@ export const parseRemoteSyntax = (remote: string) => { export const parseRemotes = (remotes: Record) => { return Object.entries(remotes).reduce((acc, remote) => { - if (!remote[1].startsWith('promise ') && remote[1].includes('@')) { - acc[remote[0]] = 'promise ' + parseRemoteSyntax(remote[1]); + if (!remote[1].startsWith("promise ") && remote[1].includes("@")) { + acc[remote[0]] = "promise " + parseRemoteSyntax(remote[1]); return acc; } acc[remote[0]] = remote[1]; @@ -261,19 +409,19 @@ const parseShareOptions = (options: ModuleFederationPluginOptions) => { const sharedOptions: [string, SharedConfig][] = parseOptions( options.shared, (item: string, key: string) => { - if (typeof item !== 'string') - throw new Error('Unexpected array in shared'); + if (typeof item !== "string") + throw new Error("Unexpected array in shared"); /** @type {SharedConfig} */ const config = item === key || !isRequiredVersion(item) ? { - import: item, - } + import: item + } : { - import: key, - requiredVersion: item, - }; + import: key, + requiredVersion: item + }; return config; }, (item: any) => item @@ -288,7 +436,7 @@ const parseShareOptions = (options: ModuleFederationPluginOptions) => { strictVersion: options.strictVersion, singleton: options.singleton, packageName: options.packageName, - eager: options.eager, + eager: options.eager }; return acc; }, {} as Record); @@ -299,9 +447,9 @@ export const toDisplayErrors = (err: Error[]) => { .map((error) => { let message = error.message; if (error.stack) { - message += '\n' + error.stack; + message += "\n" + error.stack; } return message; }) - .join('\n'); + .join("\n"); }; diff --git a/packages/nextjs-mf/src/loaders/async-boundary-loader.ts b/packages/nextjs-mf/src/loaders/async-boundary-loader.ts index 598184bedbf..bf0c5d6a66b 100644 --- a/packages/nextjs-mf/src/loaders/async-boundary-loader.ts +++ b/packages/nextjs-mf/src/loaders/async-boundary-loader.ts @@ -10,7 +10,7 @@ const pageTemplate = (request: string) => { return ( ` import dynamic from "next/dynamic" - const AsyncBoundary = dynamic(() => import("${request}"), {suspense:true}); + const AsyncBoundary = dynamic(() => import("${request}")); export default AsyncBoundary; ` ) diff --git a/packages/nextjs-mf/src/loaders/patchDefaultSharedLoaderSc.ts b/packages/nextjs-mf/src/loaders/patchDefaultSharedLoaderSc.ts new file mode 100644 index 00000000000..0bcea6a1e25 --- /dev/null +++ b/packages/nextjs-mf/src/loaders/patchDefaultSharedLoaderSc.ts @@ -0,0 +1,67 @@ +import type {LoaderContext} from 'webpack'; + +import path from 'path'; + +/** + * + * Requires `include-defaults.js` with required shared libs + * + */ +export default function patchDefaultSharedLoader( + this: LoaderContext>, + content: string +) { + if (content.includes('include-defaults')) { + // If already patched, return + return content; + } + + // avoid absolute paths as they break hashing when the root for the project is moved + // @see https://webpack.js.org/contribute/writing-a-loader/#absolute-paths + const pathIncludeDefaults = path.relative( + this.context, + path.resolve(__dirname, '../include-defaults.js') + ); + + const pathIncludeDefaultsServerComponents = path.relative( + this.context, + path.resolve(__dirname, '../include-defaults-sc.js') + ); + + if(/^['"]use client['"]/.test(content)) { + return [ + '', + 'if(typeof window === "undefined"){', + `async function patchShareScope() { + __webpack_share_scopes__.rsc = { + // "next/link": ()=>()=>require("next/link"), + // "next/navigation": ()=>()=>require("next/navigation?shared"), + // "next/dist/client/components/hooks-server-context": ()=>()=>require("next/dist/client/components/hooks-server-context"), + // "react": ()=>()=>require("react"), + } + };`, + 'await patchShareScope();', + '}', + `require(${JSON.stringify('./' + pathIncludeDefaults)});`, + content + ] + } + return [ + '', + 'if(typeof window === "undefined"){', + `async function patchShareScope() { + __webpack_share_scopes__.rsc = { + // "next/link": ()=>()=>require("next/link"), + // "next/navigation": ()=>()=>require("next/navigation?shared"), + // "next/dist/client/components/hooks-server-context": ()=>()=>require("next/dist/client/components/hooks-server-context"), + // "react": ()=>()=>require("react?shared"), + } + };`, + 'await patchShareScope();', + '}', + `require(${JSON.stringify('./' + pathIncludeDefaultsServerComponents)});`, + content + ].join("\n") + +} + diff --git a/packages/nextjs-mf/src/loaders/rscLoader.js b/packages/nextjs-mf/src/loaders/rscLoader.js new file mode 100644 index 00000000000..9c2737d1432 --- /dev/null +++ b/packages/nextjs-mf/src/loaders/rscLoader.js @@ -0,0 +1,4 @@ +export const pitch = function pitch(request) { + console.log(request, this) + return false +} diff --git a/packages/nextjs-mf/src/plugins/ChildFederationPlugin.ts b/packages/nextjs-mf/src/plugins/ChildFederationPlugin.ts index 609d86231c3..18abe92d45a 100644 --- a/packages/nextjs-mf/src/plugins/ChildFederationPlugin.ts +++ b/packages/nextjs-mf/src/plugins/ChildFederationPlugin.ts @@ -15,6 +15,7 @@ import {hasLoader, injectRuleLoader} from '../loaders/helpers'; import { DEFAULT_SHARE_SCOPE, + EXTERNAL_NEXT_DEPS, getOutputPath, externalizedShares, removePlugins, @@ -122,11 +123,12 @@ export class ChildFederationPlugin { shared: { ...(this._extraOptions.skipSharingNextInternals ? {} - : externalizedShares), + : externalizedShares(isServer)), ...this._options.shared, }, }; + if (compiler.options.name === 'client') { plugins = [ new FederationPlugin(federationPluginOptions), @@ -142,24 +144,44 @@ export class ChildFederationPlugin { new AddRuntimeRequirementToPromiseExternal(), ]; } else if (compiler.options.name === 'server') { + async function handleExternals( + context: string, + request: string, + dependencyType: string, + layer: string | null, + getResolve: ( + options: any + ) => ( + resolveContext: string, + resolveRequest: string + ) => Promise<[string | null, boolean]> + ) { + //@ts-ignore + const externalResult = await compiler.options.externals[0](context, request, dependencyType, layer, getResolve) + console.log(externalResult) + return externalResult + } const { StreamingTargetPlugin, NodeFederationPlugin, } = require('@module-federation/node'); + plugins = [ new NodeFederationPlugin(federationPluginOptions, { ModuleFederationPlugin: FederationPlugin, }), new webpack.node.NodeTemplatePlugin(childOutput), - //TODO: Externals function needs to internalize any shared module for host and remote build + //TODO: check if app directory exists, if so share react and react dom in the scope, dont externalize it new webpack.ExternalsPlugin(compiler.options.externalsType, [ // next dynamic needs to be within webpack, cannot be externalized - ...Object.keys(DEFAULT_SHARE_SCOPE).filter( - (k) => (k !== 'next/dynamic' && k !== 'next/link' && k !== 'next/script') - ), - 'react/jsx-runtime', - 'react/jsx-dev-runtime', + //@ts-ignore + ...EXTERNAL_NEXT_DEPS, + // @ts-ignore + // handleExternals, + // ...Object.keys(DEFAULT_SHARE_SCOPE).filter( + // (k) => (k !== 'next/dynamic' && k !== 'next/link' && k !== 'next/script' && k !== 'next/head' && k !== 'react-dom' && k !== 'next/' && k !== 'react/' ) + // ), ]), // new LoaderTargetPlugin('async-node'), new StreamingTargetPlugin(federationPluginOptions, { @@ -186,6 +208,8 @@ export class ChildFederationPlugin { childCompiler.outputPath = outputPath; + childCompiler.options.resolve = compiler.options.resolve; + childCompiler.options.module.rules.forEach((rule) => { // next-image-loader fix which adds remote's hostname to the assets url if ( diff --git a/packages/nextjs-mf/src/plugins/NextFederationPlugin.ts b/packages/nextjs-mf/src/plugins/NextFederationPlugin.ts index 4fee1c7b675..1b98de5767c 100644 --- a/packages/nextjs-mf/src/plugins/NextFederationPlugin.ts +++ b/packages/nextjs-mf/src/plugins/NextFederationPlugin.ts @@ -19,7 +19,8 @@ import AddRuntimeRequirementToPromiseExternal from './AddRuntimeRequirementToPro import ChildFederationPlugin from './ChildFederationPlugin'; import DevHmrFixInvalidPongPlugin from './DevHmrFixInvalidPongPlugin'; - +import fs from 'fs'; +import * as NextConstants from 'next/dist/lib/constants' export class NextFederationPlugin { private _options: ModuleFederationPluginOptions; private _extraOptions: NextFederationPluginExtraOptions; @@ -36,7 +37,6 @@ export class NextFederationPlugin { ...extraOptions, }; } - apply(compiler: Compiler) { if (!compiler.options.name) { throw new Error('name is not defined in Compiler options'); @@ -45,11 +45,17 @@ export class NextFederationPlugin { if (!this._options.filename) { throw new Error('filename is not defined in NextFederation options'); } + compiler.options.devtool = 'source-map'; if (!['server', 'client'].includes(compiler.options.name)) { return } + + const hasAppDir = fs.existsSync(path.join(compiler.context, 'app')) + + + const isServer = compiler.options.name === 'server'; const webpack = compiler.webpack; @@ -102,11 +108,57 @@ export class NextFederationPlugin { }; } - //patch next + // patch next compiler.options.module.rules.push({ - test(req: string) { - if (req.includes(path.join(compiler.context, 'pages')) || req.includes(path.join(compiler.context, 'app'))) { - return /\.(js|jsx|ts|tsx|md|mdx|mjs)$/i.test(req) + test(request: string) { + if (request.includes(path.join(compiler.context, 'pages'))) { + return /\.(js|jsx|ts|tsx|md|mdx|mjs)$/i.test(request) + } + if(compiler.options.name === 'client') { + return /app-router/.test(request) + } + return false + }, + // include: compiler.context, + // exclude: /node_modules/, + loader: path.resolve( + __dirname, + '../loaders/patchDefaultSharedLoader' + ), + }); + + if(isServer && hasAppDir) { + //@ts-ignore + const originalSwcServerLoader = compiler.options.module.rules[7].oneOf[2]; + //@ts-ignore + // compiler.options.module.rules[7].oneOf[4].resourceQuery = /!shared/ + //@ts-ignore + if(NextConstants.WEBPACK_LAYERS) { + compiler.options.module.rules.unshift({ + resourceQuery: /shared/, + //@ts-ignore + layer: NextConstants.WEBPACK_LAYERS.server, + }) + compiler.options.module.rules.unshift({ + resourceQuery: /client/, + //@ts-ignore + layer: NextConstants.WEBPACK_LAYERS.client, + }) + } + // compiler.options.module.rules[7].oneOf.push({ + // resourceQuery: /shared/, + // //@ts-ignore + // layer: originalSwcServerLoader.layer, + // }); + //@ts-ignore + // orinignalSwcServerLoader.resourceQuery = /!shared/; + // compiler.options.module.rules[7].oneOf[2].use.options + } + //patch server components + compiler.options.module.rules.push({ + test(request: string) { + if(request.includes(path.join(compiler.context, 'app'))) { + return /(page|layout)\.(js|jsx|ts|tsx|md|mdx|mjs)$/i.test(request) } return false }, @@ -114,15 +166,18 @@ export class NextFederationPlugin { exclude: /node_modules/, loader: path.resolve( __dirname, - '../loaders/patchDefaultSharedLoader' + '../loaders/patchDefaultSharedLoaderSc' ), }); if (this._extraOptions.automaticAsyncBoundary) { compiler.options.module.rules.push({ test: (request: string) => { - if (request.includes(path.join(compiler.context, 'pages')) || request.includes(path.join(compiler.context, 'app'))) { + if (request.includes(path.join(compiler.context, 'pages'))) { return /\.(js|jsx|ts|tsx|md|mdx|mjs)$/i.test(request) } + if(request.includes(path.join(compiler.context, 'app'))) { + return /(page|layout)\.(js|jsx|ts|tsx|md|mdx|mjs)$/i.test(request) + } return false }, exclude: [/node_modules/, /_document/, /_middleware/], @@ -149,7 +204,15 @@ export class NextFederationPlugin { // ignore edge runtime and middleware builds if (ModuleFederationPlugin) { - const internalShare = reKeyHostShared(this._options.shared); + console.log('FEDERATION ACTIVE') + //@ts-ignore + const internalShare = reKeyHostShared( + this._options.shared, + //@ts-ignore + compiler.options, + hasAppDir, + isServer + ); const hostFederationPluginOptions: ModuleFederationPluginOptions = { ...this._options, exposes: {}, @@ -168,6 +231,7 @@ export class NextFederationPlugin { ModuleFederationPlugin, }).apply(compiler); + new ChildFederationPlugin(this._options, this._extraOptions).apply( compiler ); diff --git a/packages/nextjs-mf/utils/build-utils.ts b/packages/nextjs-mf/utils/build-utils.ts index 067e2d0de93..08175f7f776 100644 --- a/packages/nextjs-mf/utils/build-utils.ts +++ b/packages/nextjs-mf/utils/build-utils.ts @@ -60,6 +60,7 @@ const IsomorphicRemoteTemplate = function () { return resolve(); } } else { + // @ts-ignore if(!global.__remote_scope__) { // create a global scope for container, similar to how remotes are set on window in the browser @@ -68,11 +69,23 @@ const IsomorphicRemoteTemplate = function () { _config: {}, } } +//@ts-ignore + if(globalThis && !globalThis.__remote_scope__) { + //@ts-ignore + globalThis.__remote_scope__ = global.__remote_scope__; + } // @ts-ignore if (typeof global.__remote_scope__[remote.global] !== 'undefined') { return resolve(); } + // @ts-ignore + if(globalThis) { + // @ts-ignore + if (typeof globalThis.__remote_scope__[remote.global] !== 'undefined') { + return resolve(); + } + } } (__webpack_require__ as any).l( @@ -87,6 +100,13 @@ const IsomorphicRemoteTemplate = function () { if (typeof global.__remote_scope__[remote.global] !== 'undefined') { return resolve(); } + // @ts-ignore + if(globalThis) { + // @ts-ignore + if (typeof globalThis.__remote_scope__[remote.global] !== 'undefined') { + return resolve(); + } + } } var errorType = @@ -113,7 +133,7 @@ const IsomorphicRemoteTemplate = function () { ); }).then(function () { //@ts-ignore - const globalScope = typeof window !== 'undefined' ? window : global.__remote_scope__; + const globalScope = typeof window !== 'undefined' ? window : global.__remote_scope__ || globalThis.__remote_scope__; const remoteGlobal = globalScope[ remote.global ] as unknown as WebpackRemoteContainer & { diff --git a/packages/node/project.json b/packages/node/project.json index 57c27f5aed8..959a9450629 100644 --- a/packages/node/project.json +++ b/packages/node/project.json @@ -15,6 +15,18 @@ "assets": ["packages/node/*.md"] } }, + "buildLib": { + "executor": "@nrwl/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/packages/node", + "main": "packages/node/src/index.js", + "tsConfig": "packages/node/tsconfig.lib.json", + "updateBuildableProjectDepsInPackageJson": true, + "buildableProjectDepsInPackageJsonType": "dependencies", + "assets": ["packages/node/*.md"] + } + }, "publish": { "executor": "@nrwl/workspace:run-commands", "options": { diff --git a/packages/node/src/plugins/ChunkCorrelationPlugin.js b/packages/node/src/plugins/ChunkCorrelationPlugin.js index 307911eab7b..fcd6fd5a61c 100644 --- a/packages/node/src/plugins/ChunkCorrelationPlugin.js +++ b/packages/node/src/plugins/ChunkCorrelationPlugin.js @@ -367,6 +367,17 @@ class FederationStatsPlugin { */ apply(compiler) { const federationPlugins = + compiler.options.plugins && + compiler.options.plugins.filter( + (plugin) => + plugin.constructor.name === 'NextFederationPlugin' + ); + if (!federationPlugins || federationPlugins.length === 0) { + console.error('No ModuleFederationPlugin(s) found.'); + return; + } + + const hasExposes = compiler.options.plugins && compiler.options.plugins.filter( (plugin) => @@ -374,11 +385,11 @@ class FederationStatsPlugin { plugin._options.exposes ); - if (!federationPlugins || federationPlugins.length === 0) { - console.error('No ModuleFederationPlugin(s) found.'); + if (!hasExposes || hasExposes.length === 0) { return; } + compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => { compilation.hooks.processAssets.tapPromise( { diff --git a/packages/node/src/plugins/LoadFileChunkLoadingRuntimeModule.ts b/packages/node/src/plugins/LoadFileChunkLoadingRuntimeModule.ts index cd932d9ce20..96f1c3ab0ce 100644 --- a/packages/node/src/plugins/LoadFileChunkLoadingRuntimeModule.ts +++ b/packages/node/src/plugins/LoadFileChunkLoadingRuntimeModule.ts @@ -202,6 +202,15 @@ class ReadFileChunkLoadingRuntimeModule extends RuntimeModule { ]), "}", ]), + Template.indent([ + `if(globalThis && !globalThis.__remote_scope__) {`, + Template.indent(["// create a global scope for container, similar to how remotes are set on window in the browser", + `globalThis.__remote_scope__ = {`, + "_config: {},", + "}", + ]), + "}", + ]), Template.indent([ executeLoadTemplate, `executeLoad(url,callback,chunkId)`, @@ -270,6 +279,11 @@ class ReadFileChunkLoadingRuntimeModule extends RuntimeModule { )};`, 'Object.assign(global.__remote_scope__._config, remotes)', 'const remoteRegistry = global.__remote_scope__._config', + `if(globalThis.__remote_scope__) {`, + Template.indent([ + `Object.assign(globalThis.__remote_scope__._config, remotes)`, + ]), + '}', /* TODO: keying by global should be ok, but need to verify - need to deal with when user passes promise new promise() global will/should still exist - but can only be known at runtime */ diff --git a/packages/node/src/plugins/NodeFederationPlugin.ts b/packages/node/src/plugins/NodeFederationPlugin.ts index 1f81ea8b0db..bcdc3cbabe8 100644 --- a/packages/node/src/plugins/NodeFederationPlugin.ts +++ b/packages/node/src/plugins/NodeFederationPlugin.ts @@ -58,7 +58,7 @@ const executeLoadTemplate = ` return res.text(); }).then(function(scriptContent){ try { - const vmContext = { exports, require, module, global, __filename, __dirname, URL, ...global}; + const vmContext = {exports, require, module, global, __filename, __dirname, URL,console,process,Buffer, ...global, globalThis, ...globalThis, Error}; const remote = vm.runInNewContext(scriptContent + '\\nmodule.exports', vmContext, { filename: 'node-federation-loader-' + moduleName + '.vm' }); /* TODO: need something like a chunk loading queue, this can lead to async issues @@ -119,6 +119,12 @@ export const generateRemoteTemplate = (url: string, global: any) => { } } + if(globalThis && !globalThis.__remote_scope__) { + globalThis.__remote_scope__ = { + _config: {}, + } + } + if (typeof global.__remote_scope__[${JSON.stringify(global)}] !== 'undefined') return resolve(global.__remote_scope__[${JSON.stringify(global)}]); global.__remote_scope__._config[${JSON.stringify(global)}] = ${JSON.stringify(url)}; var __webpack_error__ = new Error(); @@ -163,6 +169,8 @@ export const generateRemoteTemplate = (url: string, global: any) => { // proxy.init(__webpack_require__.S.default); // } catch(e) {} // } + + console.log('remote',remote); return remote.get(arg).then((f)=>{ const m = f(); return ()=>new Proxy(m, { @@ -176,13 +184,23 @@ export const generateRemoteTemplate = (url: string, global: any) => { init: function(shareScope) { const handler = { get(target, prop) { + if (target[prop]) { - Object.values(target[prop]).forEach(function(o) { - if(o.from === '_N_E') { - o.loaded = 1 - } - }) - } + console.log('rsc share scope', __webpack_require__.S.rsc); + if(__webpack_require__.S.rsc && __webpack_require__.S.rsc[prop]) { + console.log('server component', __webpack_require__.S.rsc[prop]); + console.log('targeted prop', target[prop]) + const moduleKey = Object.keys(target[prop])[0] + console.log('found module key',target[prop][moduleKey]); + target[prop][moduleKey].get = __webpack_require__.S.rsc[prop]; + } + Object.values(target[prop]).forEach(function(o) { + if(o.from === '_N_E') { + o.loaded = 1 + } + }) + } + return target[prop] }, set(target, property, value) { diff --git a/packages/node/src/plugins/loadScript.ts b/packages/node/src/plugins/loadScript.ts index 24083351788..440799ae328 100644 --- a/packages/node/src/plugins/loadScript.ts +++ b/packages/node/src/plugins/loadScript.ts @@ -64,7 +64,7 @@ export const executeLoadTemplate = ` return res.text(); }).then(function (scriptContent) { try { - const vmContext = {exports, require, module, global, __filename, __dirname, URL,console,process,Buffer, ...global}; + const vmContext = {exports, require, module, global, __filename, __dirname, URL,console,process,Buffer, ...global, globalThis, ...globalThis, Error}; const remote = vm.runInNewContext(scriptContent + '\\nmodule.exports', vmContext, {filename: 'node-federation-loader-' + name + '.vm'}); global.__remote_scope__[name] = remote[name] || remote; global.__remote_scope__._config[name] = url; diff --git a/packages/utilities/project.json b/packages/utilities/project.json index e8a968fb935..34480433f52 100644 --- a/packages/utilities/project.json +++ b/packages/utilities/project.json @@ -13,6 +13,16 @@ "assets": ["packages/utilities/*.md"] } }, + "buildLib": { + "executor": "@nrwl/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/packages/utilities", + "main": "packages/utilities/src/index.ts", + "tsConfig": "packages/utilities/tsconfig.lib.json", + "assets": ["packages/utilities/*.md"] + } + }, "publish": { "executor": "@nrwl/workspace:run-commands", "options": { diff --git a/packages/utilities/src/utils/common.ts b/packages/utilities/src/utils/common.ts index 3777a8ee02c..8f9f8dddd80 100644 --- a/packages/utilities/src/utils/common.ts +++ b/packages/utilities/src/utils/common.ts @@ -59,6 +59,7 @@ export const runtimeRemotes = Object.entries(remoteVars).reduce(function ( }, {} as RuntimeRemotesMap); + export const remotes = runtimeRemotes; /** @@ -78,7 +79,7 @@ export const injectScript = ( typeof keyOrRuntimeRemoteItem === 'string' ? runtimeRemotes[keyOrRuntimeRemoteItem] : keyOrRuntimeRemoteItem; - +console.log({reference}); if (reference.asyncContainer) { // @ts-ignore asyncContainer = typeof reference.asyncContainer.then === 'function' ? reference.asyncContainer : reference.asyncContainer(); @@ -100,7 +101,10 @@ export const injectScript = ( const globalScope = //@ts-ignore typeof window !== 'undefined' ? window : global.__remote_scope__; // TODO: fix types - + // @ts-ignore + console.log(eval('global.__remote_scope__')); + // @ts-ignore + console.log({globalScope: __webpack_require__.g}); asyncContainer = new Promise(function (resolve, reject) { function resolveRemoteGlobal() { const asyncContainer = globalScope[ diff --git a/yarn.lock b/yarn.lock index f91a7691c39..b7a85adaccf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1645,15 +1645,27 @@ "@module-federation/nextjs-mf@link:./dist/packages/nextjs-mf": version "0.0.0" + uid "" "@module-federation/nextjs-mf@link:dist/packages/nextjs-mf": version "0.0.0" + uid "" + +"@module-federation/node@0.8.2": + version "0.0.0" + uid "" "@module-federation/node@link:./dist/packages/node": version "0.0.0" + uid "" + +"@module-federation/utilities@0.4.1": + version "0.0.0" + uid "" "@module-federation/utilities@link:./dist/packages/utilities": version "0.0.0" + uid "" "@next/env@13.0.0": version "13.0.0" @@ -5484,6 +5496,13 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== +encoding@^0.1.13: + version "0.1.13" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== + dependencies: + iconv-lite "^0.6.2" + end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -5958,7 +5977,7 @@ event-emitter@^0.3.5: d "1" es5-ext "~0.10.14" -eventemitter3@^4.0.0, eventemitter3@^4.0.4: +eventemitter3@^4.0.0, eventemitter3@^4.0.4, eventemitter3@^4.0.7: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== @@ -6110,7 +6129,7 @@ fast-glob@3.2.7: merge2 "^1.3.0" micromatch "^4.0.4" -fast-glob@^3.0.3, fast-glob@^3.2.12, fast-glob@^3.2.5, fast-glob@^3.2.7, fast-glob@^3.2.9: +fast-glob@^3.0.3, fast-glob@^3.2.11, fast-glob@^3.2.12, fast-glob@^3.2.5, fast-glob@^3.2.7, fast-glob@^3.2.9: version "3.2.12" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== @@ -7086,7 +7105,7 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@0.6.3, iconv-lite@^0.6.3: +iconv-lite@0.6.3, iconv-lite@^0.6.2, iconv-lite@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== @@ -8960,7 +8979,7 @@ next-tick@1, next-tick@^1.1.0: resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== -next@13.0.0: +next@13.0.0, "next@^12 || ^13": version "13.0.0" resolved "https://registry.yarnpkg.com/next/-/next-13.0.0.tgz#6f07064a4f374562cf58677bef4dd06326ca648b" integrity sha512-puH1WGM6rGeFOoFdXXYfUxN9Sgi4LMytCV5HkQJvVUOhHfC1DoVqOfvzaEteyp6P04IW+gbtK2Q9pInVSrltPA== @@ -9003,7 +9022,7 @@ node-addon-api@^3.2.1: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== -node-fetch@2.6.7: +node-fetch@2.6.7, node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== @@ -10774,7 +10793,7 @@ react-test-renderer@18.2.0: react-shallow-renderer "^16.15.0" scheduler "^0.23.0" -react@18.2.0: +react@18.2.0, "react@^17 || ^18": version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==