Skip to content

Commit 87b2795

Browse files
Nextjs support (#424)
* Adding git sha to object * enable stats file output * removing comment * relocate git sha calc to function body * bump package version * adding support for next.js sidecars * fix version * created withMedusa composer for next.js Simplify api on next apps * tests * fix duplications * merge upstream, prettier source * move sidecar stuff to nextMedusaPlugin * move sidecar stuff to nextMedusaPlugin * dont crash if no modules are exposed * dedupe app graph data * improve graph merging * improve graph merging
1 parent fd09af4 commit 87b2795

File tree

10 files changed

+84064
-60885
lines changed

10 files changed

+84064
-60885
lines changed

dashboard-example/.env

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
DASHBOARD_WRITE_TOKEN=9e07eb1c-0e7f-4373-8635-cfabf5843086
2-
DASHBOARD_READ_TOKEN=6118f0cd-9c70-4191-9abb-292e6d616bc6
1+
DASHBOARD_READ_TOKEN=e75c10ba-749d-4451-8fb0-4ad972ebf970
2+
DASHBOARD_READ_WRITE=c754d13b-a294-462e-b0ef-71d2ad307426
33
DASHBOARD_BASE_URL=http://localhost:3333

dashboard-plugin/FederationDashboardPlugin.js

Lines changed: 118 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ const path = require("path");
33
const fetch = require("node-fetch");
44
const AutomaticVendorFederation = require("@module-federation/automatic-vendor-federation");
55
const convertToGraph = require("./convertToGraph");
6+
const mergeGraphs = require("./mergeGraphs");
67
const DefinePlugin = require("webpack/lib/DefinePlugin");
78
const parser = require("@babel/parser");
89
const generate = require("@babel/generator").default;
910
const traverse = require("@babel/traverse").default;
1011
const { isNode } = require("@babel/types");
1112
const webpack = require("webpack");
12-
/** @typedef {import('webpack/lib/Compilation')} Compilation */
13-
/** @typedef {import('webpack/lib/Compiler')} Compiler */
13+
/** @typedef {import("webpack/lib/Compilation")} Compilation */
14+
/** @typedef {import("webpack/lib/Compiler")} Compiler */
1415

1516
/**
1617
* @typedef FederationDashboardPluginOptions
@@ -44,7 +45,8 @@ class FederationDashboardPlugin {
4445
if (FederationPlugin) {
4546
this.FederationPluginOptions = Object.assign(
4647
{},
47-
FederationPlugin._options
48+
FederationPlugin._options,
49+
this._options.standalone || {}
4850
);
4951
} else if (this._options.standalone) {
5052
this.FederationPluginOptions = this._options.standalone;
@@ -53,6 +55,7 @@ class FederationDashboardPlugin {
5355
"Dashboard plugin is missing Module Federation or standalone option"
5456
);
5557
}
58+
5659
this.FederationPluginOptions.name =
5760
this.FederationPluginOptions.name.replace("__REMOTE_VERSION__", "");
5861
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
@@ -61,7 +64,7 @@ class FederationDashboardPlugin {
6164
name: PLUGIN_NAME,
6265
stage: compilation.constructor.PROCESS_ASSETS_STAGE_REPORT,
6366
},
64-
() => this.processWebpackGraph(compilation)
67+
() => this.processWebpackGraph(compilation)
6568
);
6669
});
6770

@@ -177,13 +180,14 @@ class FederationDashboardPlugin {
177180
this.parseModuleAst(curCompiler);
178181
}
179182

180-
let gitSha
183+
let gitSha;
181184
try {
182-
gitSha = require('child_process')
183-
.execSync('git rev-parse HEAD')
184-
.toString().trim()
185-
} catch(e) {
186-
console.error(e)
185+
gitSha = require("child_process")
186+
.execSync("git rev-parse HEAD")
187+
.toString()
188+
.trim();
189+
} catch (e) {
190+
console.error(e);
187191
}
188192

189193
// filter modules
@@ -227,19 +231,15 @@ class FederationDashboardPlugin {
227231

228232
if (graphData) {
229233
const dashData = (this._dashData = JSON.stringify(graphData));
230-
231234
// this.writeStatsFiles(stats, dashData);
232-
233-
if (this._options.dashboardURL) {
234-
this.postDashboardData(dashData)
235-
.then(() => {})
236-
.catch((err) => {
237-
if (err) {
238-
curCompiler.errors.push(err);
239-
// eslint-disable-next-line promise/no-callback-in-promise
240-
throw err;
241-
}
242-
});
235+
if (this._options.dashboardURL && !this._options.nextjs) {
236+
this.postDashboardData(dashData).catch((err) => {
237+
if (err) {
238+
curCompiler.errors.push(err);
239+
// eslint-disable-next-line promise/no-callback-in-promise
240+
throw err;
241+
}
242+
});
243243
}
244244
return Promise.resolve().then(() => {
245245
const statsBuf = Buffer.from(dashData || "{}", "utf-8");
@@ -255,15 +255,18 @@ class FederationDashboardPlugin {
255255
// for dashboard.json
256256
if (curCompiler.emitAsset && this._options.filename) {
257257
const asset = curCompiler.getAsset(this._options.filename);
258-
259258
if (asset) {
260259
curCompiler.updateAsset(this._options.filename, source);
261260
} else {
262261
curCompiler.emitAsset(this._options.filename, source);
263262
}
264263
}
265264
// for versioned remote
266-
if (curCompiler.emitAsset && this.FederationPluginOptions.filename) {
265+
if (
266+
curCompiler.emitAsset &&
267+
this.FederationPluginOptions.filename &&
268+
Object.keys(this.FederationPluginOptions.exposes || {}).length !== 0
269+
) {
267270
const remoteEntry = curCompiler.getAsset(
268271
this.FederationPluginOptions.filename
269272
);
@@ -368,12 +371,9 @@ class FederationDashboardPlugin {
368371
buildVendorFederationMap(liveStats) {
369372
const vendorFederation = {};
370373
let packageJson;
371-
this._webpackContext = liveStats.compilation.options.context;
372374
try {
373-
packageJson = require(path.join(
374-
liveStats.compilation.options.context,
375-
"package.json"
376-
));
375+
packageJson = require(this._options.packageJsonPath ||
376+
path.join(liveStats.compilation.options.context, "package.json"));
377377
this._packageJson = packageJson;
378378
} catch (e) {}
379379

@@ -530,6 +530,9 @@ class FederationDashboardPlugin {
530530
}
531531

532532
async postDashboardData(dashData) {
533+
if (!this._options.dashboardURL) {
534+
return Promise.resolve();
535+
}
533536
try {
534537
const res = await fetch(this._options.dashboardURL, {
535538
method: "POST",
@@ -540,7 +543,7 @@ class FederationDashboardPlugin {
540543
},
541544
});
542545

543-
if (!res.ok) throw new Error(msg);
546+
if (!res.ok) throw new Error(res.statusText);
544547
} catch (err) {
545548
console.warn(
546549
`Error posting data to dashboard URL: ${this._options.dashboardURL}`
@@ -550,6 +553,90 @@ class FederationDashboardPlugin {
550553
}
551554
}
552555

553-
FederationDashboardPlugin.clientVersion = require("./client-version");
556+
class NextMedusaPlugin {
557+
constructor(options) {
558+
this._options = options;
559+
}
560+
561+
apply(compiler) {
562+
const sidecarData = this._options.filename.includes("sidecar")
563+
? path.join(compiler.options.output.path, this._options.filename)
564+
: path.join(
565+
compiler.options.output.path,
566+
"sidecar-" + this._options.filename
567+
);
568+
const hostData = path.join(
569+
compiler.options.output.path,
570+
this._options.filename.replace("sidecar-", "")
571+
);
572+
573+
const MedusaPlugin = new FederationDashboardPlugin({
574+
...this._options,
575+
nextjs: true,
576+
});
577+
MedusaPlugin.apply(compiler);
578+
579+
compiler.hooks.afterEmit.tap(PLUGIN_NAME, () => {
580+
const sidecarData = path.join(
581+
compiler.options.output.path,
582+
"sidecar-" + this._options.filename
583+
);
584+
const hostData = path.join(
585+
compiler.options.output.path,
586+
this._options.filename.replace("sidecar-", "")
587+
);
588+
if (fs.existsSync(sidecarData) && fs.existsSync(hostData)) {
589+
fs.writeFileSync(
590+
hostData,
591+
JSON.stringify(mergeGraphs(require(sidecarData), require(hostData)))
592+
);
593+
}
594+
});
595+
596+
compiler.hooks.done.tapAsync("NextMedusaPlugin", (stats, done) => {
597+
if (fs.existsSync(sidecarData) && fs.existsSync(hostData)) {
598+
const dashboardData = fs.readFileSync(hostData, "utf8");
599+
MedusaPlugin.postDashboardData(dashboardData).then(done).catch(done);
600+
} else {
601+
done();
602+
}
603+
});
604+
}
605+
}
606+
607+
const withMedusa =
608+
({ name, ...medusaConfig }) =>
609+
(nextConfig = {}) => {
610+
return Object.assign({}, nextConfig, {
611+
webpack(config, options) {
612+
if (
613+
options.nextRuntime !== "edge" &&
614+
!options.isServer &&
615+
process.env.NODE_ENV === "production"
616+
) {
617+
if (!name) {
618+
throw new Error(
619+
"Medusa needs a name for the app, please ensure plugin options has {name: <appname>}"
620+
);
621+
}
622+
config.plugins.push(
623+
new NextMedusaPlugin({
624+
standalone: { name },
625+
...medusaConfig,
626+
})
627+
);
628+
}
629+
630+
if (typeof nextConfig.webpack === "function") {
631+
return nextConfig.webpack(config, options);
632+
}
633+
634+
return config;
635+
},
636+
});
637+
};
554638

555639
module.exports = FederationDashboardPlugin;
640+
module.exports.clientVersion = require("./client-version");
641+
module.exports.NextMedusaPlugin = NextMedusaPlugin;
642+
module.exports.withMedusa = withMedusa;

dashboard-plugin/README.md

Lines changed: 76 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,25 @@ const DashboardPlugin = require("@module-federation/dashboard-plugin");
1717
```js
1818
plugins: [
1919
...new DashboardPlugin({
20-
dashboardURL:
21-
"https://federation-dashboard-alpha.vercel.app/api/update?token=writeToken",
20+
dashboardURL: "https://www.medusa.codes/api/update?token=writeToken",
2221
}),
2322
];
2423
```
2524

26-
This will post the `ModuleFederationPlugin` metrics to the update endpoint at `https://federation-dashboard-alpha.vercel.app/api/update?token=writeToken`.
25+
This will post the `ModuleFederationPlugin` metrics to the update endpoint at `https://www.medusa.codes/api/update?token=writeToken`.
2726

28-
**In order to send data to Medusa, you need to create a write token.** It can be configured here: https://federation-dashboard-alpha.vercel.app/settings
27+
**In order to send data to Medusa, you need to create a write token.** It can be configured here: https://www.medusa.codes/settings
2928

3029
There are also other options:
3130

32-
| Key | Description |
33-
| -------------- | --------------------------------------------------------------------------------------- |
34-
| dashboardURL | The URL of the dashboard endpoint. |
35-
| metadata | Any additional metadata you want to apply to this application for use in the dashboard. |
36-
| filename | The file path where the dashboard data. |
37-
| standalone | For use without ModuleFederationPlugin |
38-
| publishVersion | Used for versioned remotes. '1.0.0' will be used for each remote if not passed |
31+
| Key | Description |
32+
| --------------- | ------------------------------------------------------------------------------------------- |
33+
| dashboardURL | The URL of the dashboard endpoint. |
34+
| metadata | Any additional metadata you want to apply to this application for use in the dashboard. |
35+
| filename | The file path where the dashboard data. |
36+
| standalone | For use without ModuleFederationPlugin |
37+
| publishVersion | Used for versioned remotes. '1.0.0' will be used for each remote if not passed |
38+
| packageJsonPath | custom path to package.json file, helpful if you get a `topLevelPackage.dependencies` error |
3939

4040
## Metadata
4141

@@ -44,8 +44,7 @@ Metadata is _optional_ and is specified as an object.
4444
```js
4545
plugins: [
4646
...new DashboardPlugin({
47-
dashboardURL:
48-
"https://federation-dashboard-alpha.vercel.app/api/update?token=writeToken",
47+
dashboardURL: "https://www.medusa.codes/api/update?token=writeToken",
4948
metadata: {
5049
source: {
5150
url: "http://github.com/myorg/myproject/tree/master",
@@ -68,3 +67,67 @@ You can add whatever keys you want to `metadata`, but there are some keys that t
6867
This is useful when Module Federation is not used, options can be passed that are usually inferred from Module Federation Options
6968

7069
- `name`: the name of the app, must be unique
70+
71+
## Next.js
72+
73+
Next requires its own specific integration due to how Module Federation works on this platform.
74+
75+
```js
76+
const { withMedusa } = require("@module-federation/dashboard-plugin");
77+
const withPlugins = require("next-compose-plugins");
78+
const { withFederatedSidecar } = require("@module-federation/nextjs-ssr");
79+
80+
module.exports = withPlugins(
81+
[
82+
withFederatedSidecar(
83+
{
84+
name,
85+
filename: "static/chunks/remoteEntry.js",
86+
exposes,
87+
remotes,
88+
shared: {
89+
lodash: {
90+
import: "lodash",
91+
requiredVersion: require("lodash").version,
92+
singleton: true,
93+
},
94+
chakra: {
95+
shareKey: "@chakra-ui/react",
96+
import: "@chakra-ui/react",
97+
},
98+
"use-sse": {
99+
singleton: true,
100+
requiredVersion: false,
101+
},
102+
},
103+
},
104+
{
105+
experiments: {
106+
flushChunks: true,
107+
hot: true,
108+
},
109+
}
110+
),
111+
withMedusa({
112+
name: "home",
113+
publishVersion: require("./package.json").version,
114+
filename: "dashboard.json",
115+
dashboardURL: `https://www.medusa.codes/api/update?token=${process.env.DASHBOARD_WRITE_TOKEN}`,
116+
versionChangeWebhook: "http://cnn.com/",
117+
metadata: {
118+
clientUrl: "http://localhost:3333",
119+
baseUrl: process.env.VERCEL_URL
120+
? "https://" + process.env.VERCEL_URL
121+
: "http://localhost:3001",
122+
source: {
123+
url: "https://github.com/module-federation/federation-dashboard/tree/master/dashboard-example/home",
124+
},
125+
remote: process.env.VERCEL_URL
126+
? "https://" + process.env.VERCEL_URL + "/remoteEntry.js"
127+
: "http://localhost:3001/remoteEntry.js",
128+
},
129+
}),
130+
],
131+
nextConfig
132+
);
133+
```

dashboard-plugin/convertToGraph.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const convertToGraph = (
1313
group,
1414
functionRemotes,
1515
sha,
16-
buildHash
16+
buildHash,
1717
},
1818
standalone
1919
) => {
@@ -89,9 +89,12 @@ const convertToGraph = (
8989
}
9090
if (reasons) {
9191
reasons.forEach(({ module }) => {
92-
const moduleMinusExtension = module.replace(".js", "");
93-
if (modulesObj[moduleMinusExtension]) {
94-
modulesObj[moduleMinusExtension].requires.add(data[2]);
92+
// filters out entrypoints
93+
if (module) {
94+
const moduleMinusExtension = module.replace(".js", "");
95+
if (modulesObj[moduleMinusExtension]) {
96+
modulesObj[moduleMinusExtension].requires.add(data[2]);
97+
}
9598
}
9699
});
97100
}
@@ -177,7 +180,7 @@ const convertToGraph = (
177180
posted,
178181
group,
179182
sha,
180-
buildHash
183+
buildHash,
181184
};
182185

183186
return out;

0 commit comments

Comments
 (0)