From f572cb0878451c2e7c0a33accb19f43ec7957831 Mon Sep 17 00:00:00 2001 From: Ben March Date: Sun, 7 Jul 2024 15:21:16 -0500 Subject: [PATCH 1/2] feat(nextjs-mf): added nextjs-ssr-manifest example --- nextjs-ssr-manifest/README.md | 61 + .../checkout/components/exposedTitle.js | 19 + .../checkout/components/sample.module.css | 3 + .../checkout/components/test.js | 4 + nextjs-ssr-manifest/checkout/next.config.js | 33 + nextjs-ssr-manifest/checkout/package.json | 17 + nextjs-ssr-manifest/checkout/pages-map.js | 3 + nextjs-ssr-manifest/checkout/pages/_app.js | 27 + .../checkout/pages/_document.js | 45 + .../checkout/pages/checkout.js | 45 + nextjs-ssr-manifest/checkout/pages/index.js | 6 + .../checkout/pages/p/[...slug].js | 4 + nextjs-ssr-manifest/checkout/pages/shop.js | 6 + .../checkout/public/favicon.ico | Bin 0 -> 15086 bytes nextjs-ssr-manifest/cypress.env.json | 4 + nextjs-ssr-manifest/e2e/checkApps.cy.ts | 407 ++++ .../home/components/helloWorld.js | 3 + nextjs-ssr-manifest/home/components/nav.js | 65 + nextjs-ssr-manifest/home/next.config.js | 33 + nextjs-ssr-manifest/home/package.json | 18 + nextjs-ssr-manifest/home/pages-map.js | 3 + nextjs-ssr-manifest/home/pages/_app.js | 70 + nextjs-ssr-manifest/home/pages/_document.js | 45 + nextjs-ssr-manifest/home/pages/checkout.js | 5 + nextjs-ssr-manifest/home/pages/index.js | 103 + nextjs-ssr-manifest/home/pages/p/[...slug].js | 4 + nextjs-ssr-manifest/home/pages/shop.js | 4 + nextjs-ssr-manifest/home/public/favicon.ico | Bin 0 -> 15086 bytes nextjs-ssr-manifest/index.spec.js | 83 + nextjs-ssr-manifest/package.json | 17 + nextjs-ssr-manifest/pnpm-lock.yaml | 2093 +++++++++++++++++ nextjs-ssr-manifest/pnpm-workspace.yaml | 4 + .../shop/components/exposedTitle.js | 18 + nextjs-ssr-manifest/shop/components/nav.js | 54 + nextjs-ssr-manifest/shop/next.config.js | 33 + nextjs-ssr-manifest/shop/package.json | 18 + nextjs-ssr-manifest/shop/pages-map.js | 4 + nextjs-ssr-manifest/shop/pages/_app.js | 21 + nextjs-ssr-manifest/shop/pages/_document.js | 45 + nextjs-ssr-manifest/shop/pages/checkout.js | 4 + nextjs-ssr-manifest/shop/pages/index.js | 5 + nextjs-ssr-manifest/shop/pages/p/[...slug].js | 10 + nextjs-ssr-manifest/shop/pages/shop.js | 53 + nextjs-ssr-manifest/shop/public/favicon.ico | Bin 0 -> 15086 bytes 44 files changed, 3499 insertions(+) create mode 100644 nextjs-ssr-manifest/README.md create mode 100644 nextjs-ssr-manifest/checkout/components/exposedTitle.js create mode 100644 nextjs-ssr-manifest/checkout/components/sample.module.css create mode 100644 nextjs-ssr-manifest/checkout/components/test.js create mode 100644 nextjs-ssr-manifest/checkout/next.config.js create mode 100644 nextjs-ssr-manifest/checkout/package.json create mode 100644 nextjs-ssr-manifest/checkout/pages-map.js create mode 100644 nextjs-ssr-manifest/checkout/pages/_app.js create mode 100644 nextjs-ssr-manifest/checkout/pages/_document.js create mode 100644 nextjs-ssr-manifest/checkout/pages/checkout.js create mode 100644 nextjs-ssr-manifest/checkout/pages/index.js create mode 100644 nextjs-ssr-manifest/checkout/pages/p/[...slug].js create mode 100644 nextjs-ssr-manifest/checkout/pages/shop.js create mode 100644 nextjs-ssr-manifest/checkout/public/favicon.ico create mode 100644 nextjs-ssr-manifest/cypress.env.json create mode 100644 nextjs-ssr-manifest/e2e/checkApps.cy.ts create mode 100644 nextjs-ssr-manifest/home/components/helloWorld.js create mode 100644 nextjs-ssr-manifest/home/components/nav.js create mode 100644 nextjs-ssr-manifest/home/next.config.js create mode 100644 nextjs-ssr-manifest/home/package.json create mode 100644 nextjs-ssr-manifest/home/pages-map.js create mode 100644 nextjs-ssr-manifest/home/pages/_app.js create mode 100644 nextjs-ssr-manifest/home/pages/_document.js create mode 100644 nextjs-ssr-manifest/home/pages/checkout.js create mode 100644 nextjs-ssr-manifest/home/pages/index.js create mode 100644 nextjs-ssr-manifest/home/pages/p/[...slug].js create mode 100644 nextjs-ssr-manifest/home/pages/shop.js create mode 100644 nextjs-ssr-manifest/home/public/favicon.ico create mode 100644 nextjs-ssr-manifest/index.spec.js create mode 100644 nextjs-ssr-manifest/package.json create mode 100644 nextjs-ssr-manifest/pnpm-lock.yaml create mode 100644 nextjs-ssr-manifest/pnpm-workspace.yaml create mode 100644 nextjs-ssr-manifest/shop/components/exposedTitle.js create mode 100644 nextjs-ssr-manifest/shop/components/nav.js create mode 100644 nextjs-ssr-manifest/shop/next.config.js create mode 100644 nextjs-ssr-manifest/shop/package.json create mode 100644 nextjs-ssr-manifest/shop/pages-map.js create mode 100644 nextjs-ssr-manifest/shop/pages/_app.js create mode 100644 nextjs-ssr-manifest/shop/pages/_document.js create mode 100644 nextjs-ssr-manifest/shop/pages/checkout.js create mode 100644 nextjs-ssr-manifest/shop/pages/index.js create mode 100644 nextjs-ssr-manifest/shop/pages/p/[...slug].js create mode 100644 nextjs-ssr-manifest/shop/pages/shop.js create mode 100644 nextjs-ssr-manifest/shop/public/favicon.ico diff --git a/nextjs-ssr-manifest/README.md b/nextjs-ssr-manifest/README.md new file mode 100644 index 00000000000..995dfbad5ad --- /dev/null +++ b/nextjs-ssr-manifest/README.md @@ -0,0 +1,61 @@ +# Next.js with Module Federation + +## Getting Started + +2. run `pnpm run start` and browse to `http://localhost:3001` or one of the others + +# We are available to consult + +Contact me zackary.l.jackson@gmail.com or @ScriptedAlchemy on Twitter + +## How it works?! + +This implementaion leverages our propriatery _Software Streams_ which allow me to stream commonjs modules at runtime to consuming apps. +We have never made software streaming avaliable to the general public, while we have used it for 2 years and run several backends off the technology - its remained a heavily guarded secret. Software Streams is how SSR works, on the client side we are using enhanced federation interfaces to ensure that the top-level api works as expected. + +It should allow `import()`, `require`, `import from` to work - this has been tested serverside but i have not yet tested anything else other than import() on the client. + +There has been a leaked copy of an alpha from a year and a half ago for software streams. While it does work, there are several security flaws. The federation group has spend signigicant amounts of time enhancing streaming. + +In the future, when this plugin is out of beta - we are planning to build in stream encryption to ensure that code streamed has not been manipulated in any way. +This would rely on a salted cypher key that consumer and remote would know at build time. + +We are also looking into running streamed software in a WASM isolate that cannot perform any damage, has no access to host resources. This would make it possible to execute untrusted code. + +For the time being - I strongly suggest only federating trusted software between servers. + +## Security + +In order to make this plugin work right out the box, the commonjs modules are exposed via `_next/static/ssr*` i strongly suggest having a CDN or piece of middleware that only allows access to this path from internal network or VPN. You do not want the public internet to be able to reach that path. You are exposing server code, where `process.browser` is not applied to tree shake server secrets since this is server code. + +## Context + +We have three next.js applications + +- `checkout` - port 3000 +- `home` - port 3001 +- `shop` - port 3002 + +The applications utilize omnidirectional routing and pages or components are able to be federated between applications like a SPA + +I am using hooks here to ensure multiple copies of react are not loaded into scope on server or client. + +The omnidirectional routing now hooks into webpack federation loading functions, so when dynamically loading remotes - you can use the same functions that webpack uses to load remotes when theres a know static import like `home/title` + +I am using hooks here to ensure multiple copies of react are not loaded into scope on server or client. + +### Sharing + +Next.js has all its internal modules pre-shared vis `@module-federation/nextjs-mf` you do need to share react via the plugin in order to ensure that the share scope runtime requirements are included - since you cannot share modules in a normal manner, like nextjs internls, the pre-shared modules are attached at runtime to the share scope. Any exta code you share is processed via the plugin which reconfigures sharing properly to work with next.js limitations. + +The sharing limit is due to next not having any async boundary, theres no way to "pause" the application while webpack orchestrates share scope. + +I am investigating new methods that may solve the module sharing problem in next.js, however this is a complex problem to solve and requires enormus amounts of knowladge around how webpack and federation work inside the module graph. + +# Running Cypress E2E Tests + +To run tests in interactive mode, run `npm run cypress:debug` from the root directory of the project. It will open Cypress Test Runner and allow to run tests in interactive mode. [More info about "How to run tests"](../cypress/README.md#how-to-run-tests) + +To build app and run test in headless mode, run `yarn e2e:ci`. It will build app and run tests for this workspace in headless mode. If tets failed cypress will create `cypress` directory in sample root folder with screenshots and videos. + +["Best Practices, Rules amd more interesting information here](../cypress/README.md) diff --git a/nextjs-ssr-manifest/checkout/components/exposedTitle.js b/nextjs-ssr-manifest/checkout/components/exposedTitle.js new file mode 100644 index 00000000000..6a25851bccd --- /dev/null +++ b/nextjs-ssr-manifest/checkout/components/exposedTitle.js @@ -0,0 +1,19 @@ +import React, { useEffect } from 'react'; +import styles from './sample.module.css'; +const ExportredTitle = () => { + console.log('---------loading remote component---------'); + useEffect(() => { + console.log('HOOKS WORKS'); + }, []); + return ( +
+

+ {' '} + This came fom checkout !!! +

+

And it works like a charm v2

+
+ ); +}; + +export default ExportredTitle; diff --git a/nextjs-ssr-manifest/checkout/components/sample.module.css b/nextjs-ssr-manifest/checkout/components/sample.module.css new file mode 100644 index 00000000000..79cf82f0e6e --- /dev/null +++ b/nextjs-ssr-manifest/checkout/components/sample.module.css @@ -0,0 +1,3 @@ +.thing { + color: red; +} diff --git a/nextjs-ssr-manifest/checkout/components/test.js b/nextjs-ssr-manifest/checkout/components/test.js new file mode 100644 index 00000000000..18c3d7e2a3d --- /dev/null +++ b/nextjs-ssr-manifest/checkout/components/test.js @@ -0,0 +1,4 @@ +export default function ClientOnlyComponent() { + console.log('it render'); + return Hello Client!; +} diff --git a/nextjs-ssr-manifest/checkout/next.config.js b/nextjs-ssr-manifest/checkout/next.config.js new file mode 100644 index 00000000000..25705f18d6b --- /dev/null +++ b/nextjs-ssr-manifest/checkout/next.config.js @@ -0,0 +1,33 @@ +const NextFederationPlugin = require('@module-federation/nextjs-mf'); +// this enables you to use import() and the webpack parser +// loading remotes on demand, not ideal for SSR +const remotes = isServer => { + const location = isServer ? 'ssr' : 'chunks'; + return { + home: `home@http://localhost:3001/_next/static/${location}/mf-manifest.json`, + shop: `shop@http://localhost:3002/_next/static/${location}/mf-manifest.json`, + }; +}; +module.exports = { + webpack(config, options) { + config.plugins.push( + new NextFederationPlugin({ + name: 'checkout', + filename: 'static/chunks/remoteEntry.js', + dts: false, + exposes: { + './title': './components/exposedTitle.js', + './checkout': './pages/checkout.js', + './pages-map': './pages-map.js', + }, + remotes: remotes(options.isServer), + shared: {}, + extraOptions: { + exposePages: true, + }, + }), + ); + + return config; + }, +}; diff --git a/nextjs-ssr-manifest/checkout/package.json b/nextjs-ssr-manifest/checkout/package.json new file mode 100644 index 00000000000..3c00f68e591 --- /dev/null +++ b/nextjs-ssr-manifest/checkout/package.json @@ -0,0 +1,17 @@ +{ + "name": "nextjs-ssr-manifest_checkout", + "version": "0.1.0", + "scripts": { + "dev": "rm -rf .next; NEXT_PRIVATE_LOCAL_WEBPACK=true next dev", + "build": "NEXT_PRIVATE_LOCAL_WEBPACK=true next build", + "start": "NEXT_PRIVATE_LOCAL_WEBPACK=true NODE_ENV=production next start" + }, + "dependencies": { + "@module-federation/nextjs-mf": "8.3.28", + "lodash": "4.17.21", + "next": "^14.1.4", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "webpack": "5.92.1" + } +} diff --git a/nextjs-ssr-manifest/checkout/pages-map.js b/nextjs-ssr-manifest/checkout/pages-map.js new file mode 100644 index 00000000000..f64893c49d1 --- /dev/null +++ b/nextjs-ssr-manifest/checkout/pages-map.js @@ -0,0 +1,3 @@ +export default { + '/checkout': './checkout', +}; diff --git a/nextjs-ssr-manifest/checkout/pages/_app.js b/nextjs-ssr-manifest/checkout/pages/_app.js new file mode 100644 index 00000000000..08abba9a02d --- /dev/null +++ b/nextjs-ssr-manifest/checkout/pages/_app.js @@ -0,0 +1,27 @@ +import { Suspense, lazy } from 'react'; +import App from 'next/app'; +import dynamic from 'next/dynamic'; +const Nav = lazy(() => { + console.log(import('home/nav')); + return import('home/nav'); +}); + +function MyApp({ Component, pageProps }) { + console.log('in app'); + return ( + <> + +