diff --git a/dashboard-plugin/FederationDashboardPlugin.js b/dashboard-plugin/FederationDashboardPlugin.js index f0080fca..a8d93f9f 100644 --- a/dashboard-plugin/FederationDashboardPlugin.js +++ b/dashboard-plugin/FederationDashboardPlugin.js @@ -5,10 +5,6 @@ const AutomaticVendorFederation = require("@module-federation/automatic-vendor-f const convertToGraph = require("./convertToGraph"); const mergeGraphs = require("./mergeGraphs"); const DefinePlugin = require("webpack/lib/DefinePlugin"); -const parser = require("@babel/parser"); -const generate = require("@babel/generator").default; -const traverse = require("@babel/traverse").default; -const { isNode } = require("@babel/types"); const webpack = require("webpack"); const PLUGIN_NAME = "FederationDashboardPlugin"; @@ -27,8 +23,11 @@ const findPackageJson = (filePath) => { return false; } if (fs.existsSync(path.join(filePath.join(path.sep), "package.json"))) { - return require(path.join(filePath.join(path.sep), "package.json")); + try { + return require(path.join(filePath.join(path.sep), "package.json")); + } catch (e) {} } + filePath.pop(); findPackageJson(filePath); }; @@ -67,6 +66,7 @@ class AddRuntimeRequiremetToPromiseExternal { } /** @typedef {import("webpack/lib/Compilation")} Compilation */ + /** @typedef {import("webpack/lib/Compiler")} Compiler */ /** @@ -81,11 +81,14 @@ class FederationDashboardPlugin { */ constructor(options) { this._options = Object.assign( - { debug: false, filename: "dashboard.json", useAST: false, fetchClient: false }, + { debug: false, filename: "dashboard.json", fetchClient: false }, options ); this._dashData = null; this.allArgumentsUsed = []; + if (this._options.debug) { + console.log("constructing federation dashboard", options); + } } /** @@ -93,8 +96,15 @@ class FederationDashboardPlugin { */ apply(compiler) { compiler.options.output.uniqueName = `v${Date.now()}`; + new AddRuntimeRequiremetToPromiseExternal().apply(compiler); - const FederationPlugin = compiler.options.plugins.find((plugin) => plugin.constructor.name === "ModuleFederationPlugin"); + const FederationPlugin = compiler.options.plugins.find((plugin) => { + return ( + plugin.constructor.name === "ModuleFederationPlugin" || + plugin.constructor.name === "NextFederationPlugin" + ); + }); + if (FederationPlugin) { this.FederationPluginOptions = Object.assign( {}, @@ -111,118 +121,35 @@ class FederationDashboardPlugin { this.FederationPluginOptions.name = this.FederationPluginOptions.name.replace("__REMOTE_VERSION__", ""); + compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => { compilation.hooks.processAssets.tapPromise( { name: PLUGIN_NAME, stage: compilation.constructor.PROCESS_ASSETS_STAGE_REPORT, }, - () => this.processWebpackGraph(compilation) + (assets) => { + return this.processWebpackGraph(compilation, assets); + } ); }); - if (this.FederationPluginOptions.name) { + if ( + this.FederationPluginOptions.name && + compiler.name !== "ChildFederationPlugin" + ) { new DefinePlugin({ - 'process.dashboardURL': JSON.stringify(this._options.dashboardURL), - "process.CURRENT_HOST": JSON.stringify( + "process.dashboardURL": JSON.stringify(this._options.dashboardURL), + "process.env.CURRENT_HOST": JSON.stringify( this.FederationPluginOptions.name ), }).apply(compiler); } } - parseModuleAst(compilation, callback) { - const filePaths = []; - const allArgumentsUsed = []; - // Explore each chunk (build output): - compilation.chunks.forEach((chunk) => { - // Explore each module within the chunk (built inputs): - chunk.getModules().forEach((module) => { - // Loop through all the dependencies that has the named export that we are looking for - const matchedNamedExports = module.dependencies.filter((dep) => dep.name === "federateComponent"); - - if (matchedNamedExports.length > 0 && module.resource) { - filePaths.push({ - resource: module.resource, - file: module.resourceResolveData.relativePath, - }); - } - }); - - filePaths.forEach(({ resource, file }) => { - const sourceCode = fs.readFileSync(resource).toString("utf-8"); - const ast = parser.parse(sourceCode, { - sourceType: "unambiguous", - plugins: ["jsx", "typescript"], - }); - - // traverse the abstract syntax tree - traverse(ast, { - /** - * We want to run a function depending on a found nodeType - * More node types are documented here: https://babeljs.io/docs/en/babel-types#api - */ - CallExpression: (path) => { - const { node } = path; - const { callee, arguments: args } = node; - - if (callee.loc.identifierName === "federateComponent") { - const argsAreStrings = args.every((arg) => arg.type === "StringLiteral"); - if (!argsAreStrings) { - return; - } - const argsValue = [file]; - - // we collect the JS representation of each argument used in this function call - for (let i = 0; i < args.length; i++) { - const a = args[i]; - let { code } = generate(a); - - if (code.startsWith("{")) { - // wrap it in parentheses, so when it's eval-ed, it is eval-ed correctly into an JS object - code = `(${code})`; - } - - const value = eval(code); - - // If the value is a Node, that means it was a variable name - // There is no easy way to resolve the variable real value, so we just skip any function calls - // that has variable as its args - if (isNode(value)) { - // by breaking out of the loop here, - // we also prevent this args to be pushed to `allArgumentsUsed` - break; - } else { - argsValue.push(value); - } - - if (i === args.length - 1) { - // push to the top level array - allArgumentsUsed.push(argsValue); - } - } - } - }, - }); - }); - }); - const uniqueArgs = allArgumentsUsed.reduce((acc, current) => { - const id = current.join("|"); - acc[id] = current; - return acc; - }, {}); - this.allArgumentsUsed = Object.values(uniqueArgs); - if (callback) callback(); - } - - processWebpackGraph(curCompiler, callback) { + processWebpackGraph(curCompiler, assets) { const liveStats = curCompiler.getStats(); const stats = liveStats.toJson(); - if (this._options.useAST) { - this.parseModuleAst(curCompiler); - } - - // fs.writeFileSync('stats.json', JSON.stringify(stats, null, 2)) // get RemoteEntryChunk const RemoteEntryChunk = this.getRemoteEntryChunk( @@ -233,9 +160,9 @@ class FederationDashboardPlugin { liveStats, this.FederationPluginOptions ); + const chunkDependencies = this.getChunkDependencies(validChunkArray); const vendorFederation = this.buildVendorFederationMap(liveStats); - const rawData = { name: this.FederationPluginOptions.name, remotes: this.FederationPluginOptions.remotes, @@ -264,7 +191,7 @@ class FederationDashboardPlugin { if (graphData) { const dashData = (this._dashData = JSON.stringify(graphData)); - this.writeStatsFiles(stats, dashData); + if (this._options.dashboardURL && !this._options.nextjs) { this.postDashboardData(dashData).catch((err) => { if (err) { @@ -274,6 +201,7 @@ class FederationDashboardPlugin { } }); } + return Promise.resolve().then(() => { const statsBuf = Buffer.from(dashData || "{}", "utf-8"); @@ -289,11 +217,18 @@ class FederationDashboardPlugin { if (curCompiler.emitAsset && this._options.filename) { const asset = curCompiler.getAsset(this._options.filename); if (asset) { + if (this._options.debug) { + console.log("updateAsset", this._options.filename); + } curCompiler.updateAsset(this._options.filename, source); } else { + if (this._options.debug) { + console.log("emitAsset", this._options.filename); + } curCompiler.emitAsset(this._options.filename, source); } } + // for versioned remote if ( curCompiler.emitAsset && @@ -303,7 +238,13 @@ class FederationDashboardPlugin { const remoteEntry = curCompiler.getAsset( this.FederationPluginOptions.filename ); - const cleanVersion = typeof rawData.version === "string" ? `_${rawData.version.split(".").join("_")}` : `_${rawData.version.toString()}`; + if (!remoteEntry) { + return Promise.resolve(); + } + const cleanVersion = + typeof rawData.version === "string" + ? `_${rawData.version.split(".").join("_")}` + : `_${rawData.version.toString()}`; let codeSource; if (!remoteEntry.source._value && remoteEntry.source.source) { @@ -337,33 +278,32 @@ class FederationDashboardPlugin { ); if (remoteEntry && graphData.version) { + const basename = path.basename( + this.FederationPluginOptions.filename + ); + const bustedName = this.FederationPluginOptions.filename.replace( + basename, + [graphData.version, basename].join(".") + ); curCompiler.updateAsset( this.FederationPluginOptions.filename, originalRemoteEntrySource ); - curCompiler.emitAsset( - [graphData.version, this.FederationPluginOptions.filename].join( - "." - ), - remoteEntrySource - ); + curCompiler.emitAsset(bustedName, remoteEntrySource); } } - if (callback) { - return void callback(); - } }); } } getRemoteEntryChunk(stats, FederationPluginOptions) { - - return stats.chunks.find((chunk) => chunk.names.find((name) => name === FederationPluginOptions.name)); + return stats.chunks.find((chunk) => + chunk.names.find((name) => name === FederationPluginOptions.name) + ); } getChunkDependencies(validChunkArray) { - return validChunkArray.reduce((acc, chunk) => { const subset = chunk.getAllReferencedChunks(); const stringifiableChunk = Array.from(subset).map((sub) => { @@ -462,8 +402,10 @@ class FederationDashboardPlugin { if (reason.userRequest) { try { // grab user required package.json - const subsetPackage = require(reason.userRequest + - "/package.json"); + const subsetPackage = require(path.join( + reason.userRequest, + "package.json" + )); directReasons.add(subsetPackage); } catch (e) {} @@ -476,63 +418,68 @@ class FederationDashboardPlugin { } // This is no longer needed - can be deleted or used for refactoring the asset emitter - writeStatsFiles(stats, dashData) { + writeStatsFiles(stats, dashData, assets) { if (this._options.filename) { const hashPath = path.join(stats.outputPath, this._options.filename); if (!fs.existsSync(stats.outputPath)) { - fs.mkdirSync(stats.outputPath); + fs.mkdirSync(stats.outputPath, { recursive: true }); } - fs.writeFile(hashPath, dashData, { encoding: "utf-8" }, () => {}); + fs.writeFileSync(hashPath, dashData, { encoding: "utf-8" }); } if (this._options.debug) { console.log( path.join(stats.outputPath, this.FederationPluginOptions.filename) ); } - const file = fs.readFileSync( - path.join(stats.outputPath, this.FederationPluginOptions.filename) - ); - const { version } = JSON.parse(dashData); - if (!version) { - throw new Error("no version provided, cannot version remote"); - } - if (this._options.debug) { - console.log( - path.join( - stats.outputPath, - version, - this.FederationPluginOptions.filename - ) - ); - } - fs.mkdir( - path.join(stats.outputPath, version), - { recursive: true }, - (err) => { - if (err) throw err; - fs.writeFile( + let file; + + try { + file = assets[this.FederationPluginOptions.filename]._value; + + const { version } = JSON.parse(dashData); + if (!version) { + throw new Error("no version provided, cannot version remote"); + } + if (this._options.debug) { + console.log( path.join( stats.outputPath, version, this.FederationPluginOptions.filename - ), - file, - (err) => { - if (this._options.debug) { - console.trace(err); - console.log( - "wrote versioned remote", - path.join( - stats.outputPath, - version, - this.FederationPluginOptions.filename - ) - ); - } - } + ) ); } - ); + fs.mkdir( + path.join(stats.outputPath, version), + { recursive: true }, + (err) => { + if (err) throw err; + fs.writeFile( + path.join( + stats.outputPath, + version, + this.FederationPluginOptions.filename + ), + file, + (err) => { + if (this._options.debug) { + console.trace(err); + console.log( + "wrote versioned remote", + path.join( + stats.outputPath, + version, + this.FederationPluginOptions.filename + ) + ); + } + } + ); + } + ); + } catch (e) { + console.log(e); + } const statsPath = path.join(stats.outputPath, "stats.json"); fs.writeFile( @@ -542,12 +489,23 @@ class FederationDashboardPlugin { () => {} ); } +} +class NextMedusaPlugin { + constructor(options) { + this._options = options; + if (this._options.debug) { + console.log("NextMedusaPlugin constructor", options); + } + } async postDashboardData(dashData) { if (!this._options.dashboardURL) { return Promise.resolve(); } - const client = this._options.fetchClient ? this._options.fetchClient : fetch; + + const client = this._options.fetchClient + ? this._options.fetchClient + : fetch; try { const res = await client(this._options.dashboardURL, { method: "POST", @@ -559,6 +517,8 @@ class FederationDashboardPlugin { }); if (!res.ok) throw new Error(res.statusText); + + return res; } catch (err) { console.warn( `Error posting data to dashboard URL: ${this._options.dashboardURL}` @@ -566,90 +526,70 @@ class FederationDashboardPlugin { console.error(err); } } -} - -class NextMedusaPlugin { - constructor(options) { - this._options = options; - } apply(compiler) { - const sidecarData = this._options.filename.includes("sidecar") - ? path.join(compiler.options.output.path, this._options.filename) - : path.join( - compiler.options.output.path, - `sidecar-${this._options.filename}` + if ( + !( + compiler.options.name === "client" || + compiler.options.name === "server" || + compiler.name === "ChildFederationPlugin" + ) + ) { + if (this._options.debug) { + console.log( + "not applying medusa plugin", + compiler.options.name, + compiler.name ); - const hostData = path.join( - compiler.options.output.path, - this._options.filename.replace("sidecar-", "") - ); + } + return; + } else if (this._options.debug) { + console.log( + "applying medusa plugin", + compiler.options.name, + compiler.name + ); + } + const filename = + compiler.name === "ChildFederationPlugin" + ? "dashboard-child.json" + : "dashboard.json"; - const MedusaPlugin = new FederationDashboardPlugin({ + new FederationDashboardPlugin({ ...this._options, + filename: compiler.options.name + "-" + filename, nextjs: true, - }); - MedusaPlugin.apply(compiler); + }).apply(compiler); - compiler.hooks.afterEmit.tap(PLUGIN_NAME, () => { - const sidecarData = path.join( - compiler.options.output.path, - `sidecar-${this._options.filename}` - ); - const hostData = path.join( - compiler.options.output.path, - this._options.filename.replace("sidecar-", "") - ); - if (fs.existsSync(sidecarData) && fs.existsSync(hostData)) { - fs.writeFileSync( - hostData, - JSON.stringify(mergeGraphs(require(sidecarData), require(hostData))) - ); + const hostData = path.join( + compiler.options.output.path, + compiler.options.name + "-" + filename + ); + compiler.hooks.done.tap(PLUGIN_NAME, () => { + if (fs.existsSync(hostData)) { + fs.writeFileSync(hostData, JSON.stringify(require(hostData))); } }); - compiler.hooks.done.tapAsync("NextMedusaPlugin", (stats, done) => { - if (fs.existsSync(sidecarData) && fs.existsSync(hostData)) { + compiler.hooks.afterDone.tap("NextMedusaPlugin", (stats) => { + if (fs.existsSync(hostData)) { const dashboardData = fs.readFileSync(hostData, "utf8"); - MedusaPlugin.postDashboardData(dashboardData).then(done).catch(done); - } else { - done(); + if (this._options.skipPost) { + console.info("Skipping post to dashboard"); + } else { + this.postDashboardData(dashboardData) + .then(() => { + console.info("Data has been successfully sent to the dashboard"); + }) + .catch((error) => { + console.error("Failed to send data to the dashboard:", error); + }); + } } }); } } -const withMedusa = - ({ name, ...medusaConfig }) => - (nextConfig = {}) => Object.assign({}, nextConfig, { - webpack(config, options) { - if ( - options.nextRuntime !== "edge" && - !options.isServer && - process.env.NODE_ENV === "production" - ) { - if (!name) { - throw new Error( - "Medusa needs a name for the app, please ensure plugin options has {name: }" - ); - } - config.plugins.push( - new NextMedusaPlugin({ - standalone: { name }, - ...medusaConfig, - }) - ); - } - - if (typeof nextConfig.webpack === "function") { - return nextConfig.webpack(config, options); - } - - return config; - }, - }); - module.exports = FederationDashboardPlugin; module.exports.clientVersion = require("./client-version"); module.exports.NextMedusaPlugin = NextMedusaPlugin; -module.exports.withMedusa = withMedusa; diff --git a/dashboard-plugin/README.md b/dashboard-plugin/README.md index 6a4d357b..c93e3b7a 100644 --- a/dashboard-plugin/README.md +++ b/dashboard-plugin/README.md @@ -17,8 +17,8 @@ const DashboardPlugin = require("@module-federation/dashboard-plugin"); ```js plugins: [ ...new DashboardPlugin({ - dashboardURL: "https://api.medusa.codes/update?token=writeToken", - }), + dashboardURL: "https://api.medusa.codes/update?token=writeToken" + }) ]; ``` @@ -46,12 +46,14 @@ plugins: [ ...new DashboardPlugin({ dashboardURL: "https://api.medusa.codes/update?token=writeToken", metadata: { + // baseurl is needed in order for medusa to know where to find the remoteEntry.js file + baseUrl: 'http://localhost:3001/assets/', source: { - url: "http://github.com/myorg/myproject/tree/master", + url: "http://github.com/myorg/myproject/tree/master" }, - remote: "http://localhost:8081/remoteEntry.js", - }, - }), + remote: "http://localhost:8081/remoteEntry.js" + } + }) ]; ``` @@ -100,28 +102,28 @@ module.exports = withPlugins( lodash: { import: "lodash", requiredVersion: require("lodash").version, - singleton: true, + singleton: true }, chakra: { shareKey: "@chakra-ui/react", - import: "@chakra-ui/react", + import: "@chakra-ui/react" }, "use-sse": { singleton: true, - requiredVersion: false, - }, - }, + requiredVersion: false + } + } }, { experiments: { flushChunks: true, - hot: true, - }, + hot: true + } } ), withMedusa({ name: "home", - + publishVersion: require("./package.json").version, filename: "dashboard.json", dashboardURL: `https://api.medusa.codes/update?token=${process.env.DASHBOARD_WRITE_TOKEN}`, @@ -131,13 +133,14 @@ module.exports = withPlugins( ? "https://" + process.env.VERCEL_URL : "http://localhost:3001", source: { - url: "https://github.com/module-federation/federation-dashboard/tree/master/dashboard-example/home", + url: + "https://github.com/module-federation/federation-dashboard/tree/master/dashboard-example/home" }, remote: process.env.VERCEL_URL ? "https://" + process.env.VERCEL_URL + "/remoteEntry.js" - : "http://localhost:3001/remoteEntry.js", - }, - }), + : "http://localhost:3001/remoteEntry.js" + } + }) ], nextConfig ); diff --git a/dashboard-plugin/client-version.js b/dashboard-plugin/client-version.js index 95a6af1a..09f121ad 100644 --- a/dashboard-plugin/client-version.js +++ b/dashboard-plugin/client-version.js @@ -1,4 +1,4 @@ -module.exports = ({ currentHost, remoteName, dashboardURL }) => { +module.exports = ({ currentHost, remoteName, dashboardURL, remoteBasePath }) => { //language=JS return `promise new Promise((resolve, reject) => { fetch("${dashboardURL}¤tHost=${currentHost}&remoteName=${remoteName}", { @@ -13,8 +13,9 @@ module.exports = ({ currentHost, remoteName, dashboardURL }) => { .then(function (data) { var name = data.name + "_" + data.version; var filename = data.version + '.remoteEntry.js'; - var url = new URL(filename, data.remoteURL) - + var path = \`${remoteBasePath}/\${filename}\`.replace(/\\/\\/+/g, '/'); + var url = new URL(path, data.remoteURL); + new Promise(function (resolve, reject) { var __webpack_error__ = new Error() if (typeof window[name] !== 'undefined') return resolve(); diff --git a/dashboard-plugin/convertToGraph.js b/dashboard-plugin/convertToGraph.js index 8eec647f..bb60fac4 100644 --- a/dashboard-plugin/convertToGraph.js +++ b/dashboard-plugin/convertToGraph.js @@ -10,7 +10,7 @@ const fs = require("fs"); */ function getLicenses(packageJson) { if (packageJson.licenses && packageJson.licenses instanceof Array) { - return packageJson.licenses.map((license) => license.type).join(", "); + return packageJson.licenses.map(license => license.type).join(", "); } else if (packageJson.licenses) { // TODO: Refactor this to reduce duplicate code. Note "licenses" vs "license". return ( @@ -38,7 +38,7 @@ const convertToGraph = ( group, functionRemotes, sha, - buildHash, + buildHash }, standalone ) => { @@ -53,22 +53,30 @@ const convertToGraph = ( const modulesObj = {}; const npmModules = new Map(); - modules.forEach((mod) => { + modules.forEach(mod => { const { identifier, reasons, moduleType, nameForCondition, size } = mod; const data = identifier.split(" "); if (moduleType === "remote-module") { if (data.length === 4) { const name = data[3].replace("./", ""); - let applicationID = data[2].replace("webpack/container/reference/", "") - if(applicationID.includes("?")){ - applicationID = new URLSearchParams(applicationID.split('?')[1]).get('remoteName'); + let applicationID = data[2].replace("webpack/container/reference/", ""); + if (applicationID.includes("?")) { + const params = new URLSearchParams(applicationID.split("?")[1]); + const remoteReference = + params.get("remoteName") || params.get("remote"); + if (remoteReference && remoteReference.includes("@")) { + const [global] = remoteReference.split("@"); + applicationID = global; + } else { + applicationID = remoteReference; + } } const consume = { consumingApplicationID: name, applicationID, name, - usedIn: new Set(), + usedIn: new Set() }; consumes.push(consume); @@ -91,7 +99,7 @@ const convertToGraph = ( name, applicationID: name, requires: new Set(), - file: file.import[0], + file: file.import[0] }; }); } else if (nameForCondition && nameForCondition.includes("node_modules")) { @@ -105,35 +113,38 @@ const convertToGraph = ( const npmModule = contextArray[contextArray.indexOf("node_modules") + 1]; const packageJsonFile = path.join(context, "package.json"); - const packageJson = JSON.parse(fs.readFileSync(packageJsonFile, "UTF-8")); + // if the package.json file exists, we can use it to get the name and version + if(!fs.existsSync(packageJsonFile)) { + const packageJson = JSON.parse(fs.readFileSync(packageJsonFile, "UTF-8")); - const existingPackage = npmModules.get(packageJson.name); - if (existingPackage) { - const existingReference = existingPackage[packageJson.version]; - const data = { - name: packageJson.name, - version: packageJson.version, - homepage: packageJson.homepage, - license: getLicenses(packageJson), - size: ((existingReference && existingReference.size) || 0) + size, - }; - if (existingReference) { - Object.assign(existingReference, data); - } else { - existingPackage[packageJson.version] = data; - } - npmModules.set(packageJson.name, existingPackage); - } else { - const newDep = { - [packageJson.version]: { + const existingPackage = npmModules.get(packageJson.name); + if (existingPackage) { + const existingReference = existingPackage[packageJson.version]; + const data = { name: packageJson.name, version: packageJson.version, homepage: packageJson.homepage, license: getLicenses(packageJson), - size, - }, - }; - npmModules.set(packageJson.name, newDep); + size: ((existingReference && existingReference.size) || 0) + size + }; + if (existingReference) { + Object.assign(existingReference, data); + } else { + existingPackage[packageJson.version] = data; + } + npmModules.set(packageJson.name, existingPackage); + } else { + const newDep = { + [packageJson.version]: { + name: packageJson.name, + version: packageJson.version, + homepage: packageJson.homepage, + license: getLicenses(packageJson), + size + } + }; + npmModules.set(packageJson.name, newDep); + } } } }); @@ -144,7 +155,9 @@ const convertToGraph = ( const versionVal = version.replace(`${name}-`, ""); if (dataFromGraph) { - const foundInGraph = Object.values(dataFromGraph).find((depData) => depData.version.startsWith(versionVal)); + const foundInGraph = Object.values(dataFromGraph).find(depData => + depData.version.startsWith(versionVal) + ); if (foundInGraph) { const { name, version, license, size } = foundInGraph; @@ -152,22 +165,22 @@ const convertToGraph = ( name, version, license, - size, + size }; } } return { name, - version: versionVal, + version: versionVal }; }); const convertedDeps = { dependencies: convertDeps(topLevelPackage.dependencies), devDependencies: convertDeps(topLevelPackage.devDependencies), - optionalDependencies: convertDeps(topLevelPackage.optionalDependencies), + optionalDependencies: convertDeps(topLevelPackage.optionalDependencies) }; - modules.forEach((mod) => { + modules.forEach(mod => { const { identifier, issuerName, reasons, moduleType } = mod; if (moduleType === "provide-module") { @@ -203,8 +216,8 @@ const convertToGraph = ( [ convertedDeps.dependencies, convertedDeps.devDependencies, - convertedDeps.optionalDependencies, - ].forEach((deps) => { + convertedDeps.optionalDependencies + ].forEach(deps => { const dep = deps.find(({ name }) => name === data[2]); if (dep) { version = dep.version; @@ -217,7 +230,7 @@ const convertToGraph = ( name, version, location: name, - applicationID: name, + applicationID: name }; } @@ -251,8 +264,8 @@ const convertToGraph = ( [ convertedDeps.dependencies, convertedDeps.devDependencies, - convertedDeps.optionalDependencies, - ].forEach((deps) => { + convertedDeps.optionalDependencies + ].forEach(deps => { const dep = deps.find(({ name }) => name === data[2]); if (dep) { version = dep.version; @@ -264,7 +277,7 @@ const convertToGraph = ( name: data[2], version, location: data[2], - applicationID: name, + applicationID: name }; }); @@ -275,10 +288,11 @@ const convertToGraph = ( const cleanName = name.replace("./", ""); const objectId = `${applicationID}/${cleanName}`; const cleanFile = file.replace("./", ""); - const foundExistingConsume = consumes.find((consumeObj) => ( - consumeObj.applicationID === applicationID && - consumeObj.name === cleanName - )); + const foundExistingConsume = consumes.find( + consumeObj => + consumeObj.applicationID === applicationID && + consumeObj.name === cleanName + ); if (foundExistingConsume) { foundExistingConsume.usedIn.add(cleanFile); return acc; @@ -291,7 +305,7 @@ const convertToGraph = ( applicationID, name: cleanName, consumingApplicationID: name, - usedIn: new Set([cleanFile]), + usedIn: new Set([cleanFile]) }; return acc; }, {}) @@ -310,23 +324,23 @@ const convertToGraph = ( metadata, versionData, overrides: Object.values(overrides), - consumes: consumes.map((con) => ({ + consumes: consumes.map(con => ({ ...con, - usedIn: Array.from(con.usedIn.values()).map((file) => ({ + usedIn: Array.from(con.usedIn.values()).map(file => ({ file, - url: `${sourceUrl}/${file}`, - })), + url: `${sourceUrl}/${file}` + })) })), - modules: Object.values(modulesObj).map((mod) => ({ + modules: Object.values(modulesObj).map(mod => ({ ...mod, - requires: Array.from(mod.requires.values()), + requires: Array.from(mod.requires.values()) })), environment, version, posted, group, sha, - buildHash, + buildHash }; }; diff --git a/dashboard-plugin/medusa-client.d.ts b/dashboard-plugin/medusa-client.d.ts new file mode 100644 index 00000000..f2526b9f --- /dev/null +++ b/dashboard-plugin/medusa-client.d.ts @@ -0,0 +1,28 @@ +declare module "medusa-client" { + type FetchClient = typeof fetch; + type Environment = string | undefined; + + interface RemoteResponse { + // Define the shape of the response object here + } + + type RemoteRequestOptions = { + fetchClient?: FetchClient; + environment?: Environment; + currentHost?: string; + remote: string; + token: string; + apiHost?: string; + }; + + function getRemote({ + fetchClient, + environment, + currentHost, + remote, + token, + apiHost + }: RemoteRequestOptions): Promise; + + export default getRemote; +} diff --git a/dashboard-plugin/medusa-client.js b/dashboard-plugin/medusa-client.js new file mode 100644 index 00000000..8762057b --- /dev/null +++ b/dashboard-plugin/medusa-client.js @@ -0,0 +1,19 @@ +module.exports = ({ + fetchClient, + environment, + currentHost, + remote, + token, + apiHost = "https://api.medusa.codes" +}) => + new Promise((resolve, reject) => { + let f = fetchClient || fetch; + let env = environment || process.env.NODE_ENV; + f( + `${apiHost}/env/${env}/get-remote?token=${token}&remoteName=${remote}¤tHost=${currentHost || + process.env.CURRENT_HOST}` + ) + .then(res => res.json()) + .then(resolve) + .catch(reject); + }); diff --git a/dashboard-plugin/medusa-delegate.d.ts b/dashboard-plugin/medusa-delegate.d.ts new file mode 100644 index 00000000..f90695ca --- /dev/null +++ b/dashboard-plugin/medusa-delegate.d.ts @@ -0,0 +1,28 @@ +declare module "medusa-delegate" { + type FetchClient = typeof fetch; + type Environment = string | undefined; + + interface RemoteResponse { + // Define the shape of the response object here + } + + interface DelegateOptions { + fetchClient?: FetchClient; + environment?: Environment; + currentHost?: string; + remote: string; + token: string; + apiHost?: string; + } + + function medusaDelegate({ + fetchClient, + environment, + currentHost, + remote, + token, + apiHost + }: DelegateOptions): Promise; + + export default medusaDelegate; +} diff --git a/dashboard-plugin/medusa-delegate.js b/dashboard-plugin/medusa-delegate.js index 7db3823f..e2ebdc81 100644 --- a/dashboard-plugin/medusa-delegate.js +++ b/dashboard-plugin/medusa-delegate.js @@ -1,36 +1,54 @@ -module.exports = ({fetchClient, currentHost, remoteName, dashboardURL})=> new Promise((resolve, reject) => { - fetchClient(`${dashboardURL}¤tHost=${currentHost}&remoteName=${remoteName}`, { - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - }) - .then((res) => res.json()) - .then((data) => { +module.exports = ({ + fetchClient, + environment, + currentHost, + remote, + token, + apiHost = "https://api.medusa.codes", + remoteName, + dashboardURL +}) => + new Promise((resolve, reject) => { + if (dashboardURL) { + console.error( + "medusa-delegate: dashboardURL is deprecated. Please use apiHost instead. Defaults to: https://api.medusa.codes" + ); + } + let f = fetchClient || fetch; + let env = environment || process.env.NODE_ENV; + f( + `${apiHost}/env/${env}/get-remote?token=${token}&remoteName=${remote || + remoteName}¤tHost=${currentHost || process.env.CURRENT_HOST}` + ) + .then(res => res.json()) + .then(data => { const name = `${data.name}_${data.version}`; const filename = `${data.version}.remoteEntry.js`; const url = new URL(filename, data.remoteURL); new Promise((resolve, reject) => { const __webpack_error__ = new Error(); - if (typeof window[name] !== 'undefined') return resolve( window[name] ); + if (typeof window[name] !== "undefined") return resolve(window[name]); __webpack_require__.l( url.href, - (event) => { - if (typeof window[name] !== 'undefined') return resolve( window[name] ); - const errorType = event && (event.type === 'load' ? 'missing' : event.type); + event => { + if (typeof window[name] !== "undefined") + return resolve(window[name]); + const errorType = + event && (event.type === "load" ? "missing" : event.type); const realSrc = event?.target?.src; - __webpack_error__.message = - `Loading script failed.\\n(${errorType}: ${realSrc})`; - __webpack_error__.name = 'ScriptExternalLoadError'; + __webpack_error__.message = `Loading script failed.\\n(${errorType}: ${realSrc})`; + __webpack_error__.name = "ScriptExternalLoadError"; __webpack_error__.type = errorType; __webpack_error__.request = realSrc; reject(__webpack_error__); }, - name, + name ); - }).then(() => { - resolve(window[name]) - }).catch(reject) - }) - }) + }) + .then(() => { + resolve(window[name]); + }) + .catch(reject); + }); + }); diff --git a/dashboard-plugin/mergeGraphs.js b/dashboard-plugin/mergeGraphs.js index 51277200..fc23909a 100644 --- a/dashboard-plugin/mergeGraphs.js +++ b/dashboard-plugin/mergeGraphs.js @@ -1,7 +1,7 @@ -const mergeWithoutDupe = (source) => +const mergeWithoutDupe = source => source.reduce((acc, item) => { if (typeof item === "object") { - const isDupe = acc.find((existing) => { + const isDupe = acc.find(existing => { return Object.entries(existing).every(([key, value]) => { return item[key] === value; }); @@ -14,73 +14,109 @@ const mergeWithoutDupe = (source) => } return acc; }, []); -module.exports = (graph1, graph2) => { - graph1.devDependencies = mergeWithoutDupe([ - ...graph2.devDependencies, - ...graph1.devDependencies, - ]); - graph1.dependencies = mergeWithoutDupe([ - ...graph2.dependencies, - ...graph1.dependencies, - ]); - //exposed - graph2.modules.forEach((hostModules) => { - const existing = graph1.modules.find((sidecarModules) => { - return ( - hostModules.id === sidecarModules.id && - hostModules.name === sidecarModules.name && - hostModules.file === sidecarModules.file && - hostModules.applicationID === sidecarModules.applicationID - ); - }); - if (existing) { - existing.requires = Array.from( - new Set([...existing.requires, ...hostModules.requires]) - ); - } else { - graph1.modules.push(hostModules); + +function mergeArrays(arr1, arr2) { + const merged = {}; + + // Merge arr1 into merged + arr1.forEach(obj => { + const key = obj.name + obj.version; + if (!merged[key]) { + merged[key] = obj; } }); - //shares - graph2.overrides.forEach((hostOverrides) => { - const existing = graph1.overrides.find((sidecarOverrides) => { - return ( - sidecarOverrides.id === hostOverrides.id && - sidecarOverrides.name === hostOverrides.name && - sidecarOverrides.version === hostOverrides.version && - sidecarOverrides.location === hostOverrides.location && - sidecarOverrides.applicationID === hostOverrides.applicationID - ); - }); - if (!existing) { - graph1.overrides.push(hostOverrides); + + // Merge arr2 into merged + arr2.forEach(obj => { + const key = obj.name + obj.version; + if (!merged[key]) { + merged[key] = obj; } }); - //consumes - graph2.consumes.forEach((hostConsumedModule) => { - const existing = graph1.consumes.find((sidecarConsumedModule) => { - return ( - sidecarConsumedModule.consumingApplicationID === + + // Convert merged object to array + const result = []; + Object.keys(merged).forEach(key => { + result.push(merged[key]); + }); + + return result; +} + +module.exports = (graph1, graph2) => { + if(graph2 && graph2.devDependencies) { + graph1.devDependencies = mergeArrays( + graph2.devDependencies, + graph1.devDependencies + ); + } + if(graph2 && graph2.dependencies) { + graph1.dependencies = mergeArrays(graph2.dependencies, graph1.dependencies); + } + if(graph2 && graph2.modules) { + //exposed + graph2.modules.forEach(hostModules => { + const existing = graph1.modules.find(sidecarModules => { + return ( + hostModules.id === sidecarModules.id && + hostModules.name === sidecarModules.name && + hostModules.file === sidecarModules.file && + hostModules.applicationID === sidecarModules.applicationID + ); + }); + if (existing) { + existing.requires = Array.from( + new Set([...existing.requires, ...hostModules.requires]) + ); + } else { + graph1.modules.push(hostModules); + } + }); + } + if(graph2 && graph2.overrides) { + //shares + graph2.overrides.forEach(hostOverrides => { + const existing = graph1.overrides.find(sidecarOverrides => { + return ( + sidecarOverrides.id === hostOverrides.id && + sidecarOverrides.name === hostOverrides.name && + sidecarOverrides.version === hostOverrides.version && + sidecarOverrides.location === hostOverrides.location && + sidecarOverrides.applicationID === hostOverrides.applicationID + ); + }); + if (!existing) { + graph1.overrides.push(hostOverrides); + } + }); + } + if(graph2 && graph2.consumes) { + //consumes + graph2.consumes.forEach(hostConsumedModule => { + const existing = graph1.consumes.find(sidecarConsumedModule => { + return ( + sidecarConsumedModule.consumingApplicationID === hostConsumedModule.consumingApplicationID && - sidecarConsumedModule.applicationID === + sidecarConsumedModule.applicationID === hostConsumedModule.applicationID && - sidecarConsumedModule.name === hostConsumedModule.name - ); - }); + sidecarConsumedModule.name === hostConsumedModule.name + ); + }); - if (existing) { - hostConsumedModule.usedIn.forEach((consumedModule) => { - const alreadyExists = existing.usedIn.find(({ file, url }) => { - return consumedModule.file === file && consumedModule.url === url; + if (existing) { + hostConsumedModule.usedIn.forEach(consumedModule => { + const alreadyExists = existing.usedIn.find(({file, url}) => { + return consumedModule.file === file && consumedModule.url === url; + }); + if (!alreadyExists) { + existing.usedIn.push(consumedModule); + } }); - if (!alreadyExists) { - existing.usedIn.push(consumedModule); - } - }); - } else { - graph1.consumes.push(hostConsumedModule); - } - }); + } else { + graph1.consumes.push(hostConsumedModule); + } + }); + } return graph1; }; diff --git a/dashboard-plugin/package.json b/dashboard-plugin/package.json index 78bacb11..688e4e64 100644 --- a/dashboard-plugin/package.json +++ b/dashboard-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@module-federation/dashboard-plugin", - "version": "2.7.0", + "version": "2.8.0-beta.8", "main": "FederationDashboardPlugin.js", "bin": "bin/federation-report.js", "license": "Apache-2.0", @@ -23,19 +23,17 @@ "bin", "helpers", "FederationDashboardPlugin.js", + "medusa-delegate.js", + "medusa-client.js", "convertToGraph.js", "mergeGraphs.js", + "*.d.ts", + "postDashboardData.js", "client-version.js", "LICENSE" ], "dependencies": { - "@babel/generator": "^7.15.4", - "@babel/parser": "^7.15.5", - "@babel/traverse": "^7.15.4", - "@babel/types": "^7.15.4", "@module-federation/automatic-vendor-federation": "^1.2.1", - "deepmerge": "^4.2.2", - "flatted": "^3.0.0", "node-fetch": "^2.6.0" }, "peerDependencies": { diff --git a/dashboard-plugin/postDashboardData.d.ts b/dashboard-plugin/postDashboardData.d.ts new file mode 100644 index 00000000..9300eb21 --- /dev/null +++ b/dashboard-plugin/postDashboardData.d.ts @@ -0,0 +1,12 @@ +declare module "postDashboardData" { + import { HeadersInit } from "node-fetch"; + + interface PostDashboardDataOptions { + data: string; + headers?: HeadersInit; + } + + export default function postDashboardData( + options: PostDashboardDataOptions + ): Promise; +} diff --git a/dashboard-plugin/postDashboardData.js b/dashboard-plugin/postDashboardData.js new file mode 100644 index 00000000..a1d5da36 --- /dev/null +++ b/dashboard-plugin/postDashboardData.js @@ -0,0 +1,24 @@ +const fetch = require("node-fetch"); +async function postDashboardData({ data, headers }) { + const client = this._options.fetchClient ? this._options.fetchClient : fetch; + try { + const res = await client(this._options.dashboardURL, { + method: "POST", + body: data, + headers: { + Accept: "application/json", + "Content-type": "application/json", + ...headers + } + }); + + if (!res.ok) throw new Error(res.statusText); + } catch (err) { + console.warn( + `Error posting data to dashboard URL: ${this._options.dashboardURL}` + ); + console.error(err); + } +} + +module.exports = postDashboardData; diff --git a/yarn.lock b/yarn.lock index b810d286..a143705c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -42,7 +42,7 @@ json5 "^2.1.2" semver "^6.3.0" -"@babel/generator@^7.15.4", "@babel/generator@^7.17.3", "@babel/generator@^7.17.7": +"@babel/generator@^7.17.3", "@babel/generator@^7.17.7": version "7.17.7" resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz" integrity sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w== @@ -159,7 +159,7 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.15.5", "@babel/parser@^7.16.7", "@babel/parser@^7.17.3", "@babel/parser@^7.17.7": +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.16.7", "@babel/parser@^7.17.3", "@babel/parser@^7.17.7": version "7.17.7" resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.17.7.tgz" integrity sha512-bm3AQf45vR4gKggRfvJdYJ0gFLoCbsPxiFLSH6hTVYABptNHY6l9NrhnucVjQ/X+SPtLANT9lc0fFhikj+VBRA== @@ -264,7 +264,7 @@ "@babel/parser" "^7.16.7" "@babel/types" "^7.16.7" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.15.4", "@babel/traverse@^7.17.3": +"@babel/traverse@^7.1.0", "@babel/traverse@^7.17.3": version "7.17.3" resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.3.tgz" integrity sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw== @@ -280,7 +280,7 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.15.4", "@babel/types@^7.16.7", "@babel/types@^7.17.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3": +"@babel/types@^7.0.0", "@babel/types@^7.16.7", "@babel/types@^7.17.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3": version "7.17.0" resolved "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz" integrity sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw== @@ -1405,6 +1405,11 @@ dependencies: find-package-json "^1.2.0" +"@module-federation/utilities@^1.3.0": + version "1.9.1" + resolved "https://registry.npmjs.org/@module-federation/utilities/-/utilities-1.9.1.tgz" + integrity sha512-EVPV90TSK+tSG6R7Kw65w7pRuI2phff8YFIazzGWNViZPRlTYS3c9pZWVPYGiwIz1+96P+yeNp/WQZrkyS8kpA== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" @@ -1650,9 +1655,9 @@ "@babel/types" "^7.3.0" "@types/eslint-scope@^3.7.0": - version "3.7.3" - resolved "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.3.tgz" - integrity sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g== + version "3.7.7" + resolved "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" + integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== dependencies: "@types/eslint" "*" "@types/estree" "*" @@ -1938,9 +1943,9 @@ acorn-globals@^6.0.0: acorn-walk "^7.1.1" acorn-import-assertions@^1.7.6: - version "1.8.0" - resolved "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz" - integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== + version "1.9.0" + resolved "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz" + integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== acorn-walk@^7.1.1: version "7.2.0" @@ -3165,9 +3170,9 @@ end-of-stream@^1.1.0: once "^1.4.0" enhanced-resolve@^5.8.3: - version "5.9.2" - resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz" - integrity sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA== + version "5.15.0" + resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" + integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -3579,11 +3584,6 @@ find-yarn-workspace-root2@1.2.16: micromatch "^4.0.2" pkg-dir "^4.2.0" -flatted@^3.0.0: - version "3.2.5" - resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz" - integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== - for-in@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz" @@ -3668,7 +3668,7 @@ fs.realpath@^1.0.0: fsevents@^2.1.2: version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== function-bind@^1.1.1: @@ -7845,9 +7845,9 @@ walker@^1.0.7, walker@~1.0.5: makeerror "1.0.12" watchpack@^2.2.0: - version "2.3.1" - resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz" - integrity sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA== + version "2.4.0" + resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== dependencies: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2"