home app fragment

This commit is contained in:
ScriptedAlchemy 2024-07-21 18:35:43 +08:00
parent 03eba8f996
commit 1b33835b14
27 changed files with 781 additions and 2 deletions

View file

@ -1,8 +1,8 @@
{
"private": true,
"scripts": {
"dev": "next",
"build": "next build",
"dev": "RSPACK_CONFIG_VALIDATE=loose-silent next",
"build": "cross-env RSPACK_CONFIG_VALIDATE=loose-silent NEXT_TELEMETRY_DISABLED=1 node --trace-deprecation --enable-source-maps ../../packages/next/dist/bin/next build",
"start": "next start"
},
"dependencies": {

View file

@ -0,0 +1,77 @@
import React from "react";
import { Menu, Layout } from "antd";
import { useRouter } from "next/router";
import "./menu";
const SharedNav = () => {
const { asPath, push } = useRouter();
let activeMenu;
if (asPath === "/" || asPath.startsWith("/home")) {
activeMenu = "/";
} else if (asPath.startsWith("/shop")) {
activeMenu = "/shop";
} else if (asPath.startsWith("/checkout")) {
activeMenu = "/checkout";
}
const menuItems = [
{
className: "home-menu-link",
label: (
<>
Home <sup>3000</sup>
</>
),
key: "/",
onMouseEnter: () => {},
},
{
className: "shop-menu-link",
label: (
<>
Shop <sup>3001</sup>
</>
),
key: "/shop",
},
{
className: "checkout-menu-link",
label: (
<>
Checkout <sup>3002</sup>
</>
),
key: "/checkout",
},
];
return (
<Layout.Header>
<div className="header-logo">nextjs-mf</div>
<Menu
theme="dark"
mode="horizontal"
selectedKeys={activeMenu ? [activeMenu] : undefined}
onClick={({ key }) => {
push(key);
}}
items={menuItems}
/>
<style jsx>
{`
.header-logo {
float: left;
width: 200px;
height: 31px;
margin-right: 24px;
color: white;
font-size: 2rem;
}
`}
</style>
</Layout.Header>
);
};
export default SharedNav;

View file

@ -0,0 +1,37 @@
import type { ItemType } from "antd/es/menu/interface";
import { useRouter } from "next/router";
import { Menu } from "antd";
const menuItems: ItemType[] = [
{ label: "Main home", key: "/" },
{ label: "Test hook from remote", key: "/home/test-remote-hook" },
{ label: "Test broken remotes", key: "/home/test-broken-remotes" },
{ label: "Exposed pages", key: "/home/exposed-pages" },
{
label: "Exposed components",
type: "group",
children: [{ label: "home/SharedNav", key: "/home/test-shared-nav" }],
},
];
export default function AppMenu() {
const router = useRouter();
return (
<>
<div
style={{ padding: "10px", fontWeight: 600, backgroundColor: "#fff" }}
>
Home App Menu
</div>
<Menu
mode="inline"
selectedKeys={[router.asPath]}
style={{ height: "100%" }}
onClick={({ key }) => router.push(key)}
items={menuItems}
/>
</>
);
}

View file

@ -0,0 +1,3 @@
.tjhin {
display: flex;
}

View file

@ -0,0 +1,144 @@
import { getH1, getH3 } from "../support/app.po";
describe("3000-home/", () => {
beforeEach(() => cy.visit("/"));
describe("Warmup Next", () => {
xit("warms pages concurrently", () => {
const urls = [
"/shop",
"/checkout",
"/checkout/test-title",
"/checkout/test-check-button",
"/api/test",
];
urls.forEach((url) => {
cy.request(url); // This makes a GET request, not a full page visit
});
});
});
describe("Welcome message", () => {
it("should display welcome message", () => {
getH1().contains("This is SPA combined");
});
});
describe("Image checks", () => {
xit("should check that the home-webpack-png and shop-webpack-png images are not 404", () => {
// Get the src attribute of the home-webpack-png image
cy.debug()
.get("img.home-webpack-png")
.invoke("attr", "src")
.then((src) => {
cy.log(src);
cy.request(src).its("status").should("eq", 200);
});
// Get the src attribute of the shop-webpack-png image
cy.get("img.shop-webpack-png")
.invoke("attr", "src")
.then((src) => {
// Send a GET request to the src URL
cy.request(src).its("status").should("eq", 200);
});
});
});
describe("Routing checks", () => {
it("check that clicking back and forwards in client side routeing still renders the content correctly", () => {
cy.visit("/shop");
cy.wait(3000);
cy.url().should("include", "/shop");
getH1().contains("Shop Page");
//eslint-disable-next-line
cy.wait(3000);
cy.get(".home-menu-link").contains("Home 3000");
cy.get(".home-menu-link").click();
cy.wait(2000);
cy.url().should("include", "/");
cy.wait(700);
getH1().contains("This is SPA combined");
});
});
describe("3000-home/checkout", () => {
beforeEach(() => {
cy.visit("/checkout");
cy.visit("/");
cy.visit("/checkout");
});
describe("Welcome message", () => {
it("should display welcome message", () => {
getH1().contains("checkout page");
});
});
describe("Tag checks", () => {
it("should check that a .description + pre tag exists", () => {
cy.get(".description").should("exist");
cy.get("main pre").should("exist");
});
});
describe("3000-home/checkout/test-title", () => {
beforeEach(() => cy.visit("/checkout/test-title"));
it("should display welcome message", () => {
getH3().contains("This title came");
});
});
describe("3000-home/checkout/test-check-button", () => {
beforeEach(() => cy.visit("/checkout/test-check-button"));
it("should display welcome message", () => {
cy.get("button").contains("Button");
});
});
});
describe("3000-home/shop", () => {
beforeEach(() => cy.visit("/shop"));
describe("Welcome message", () => {
it("should display welcome message", () => {
getH1().contains("Shop Page");
});
});
describe("Image checks", () => {
xit("should check that shop-webpack-png images are not 404", () => {
// Get the src attribute of the shop-webpack-png image
cy.get("img.shop-webpack-png")
.invoke("attr", "src")
.then((src) => {
// Send a GET request to the src URL
cy.request(src).its("status").should("eq", 200);
});
});
it("should check that shop-webpack-png images are not 404 between route clicks", () => {
cy.visit("/");
cy.visit("/shop");
cy.url().should("include", "/shop");
getH1().contains("Shop Page");
cy.get(".home-menu-link").click();
//eslint-disable-next-line
cy.wait(2999);
cy.get("img.shop-webpack-png")
.invoke("attr", "src")
.then((src) => {
// Send a GET request to the src URL
cy.request(src).its("status").should("eq", 200);
});
});
});
describe("Tag checks", () => {
it("should check that a .description + pre tag exists", () => {
cy.get(".description + pre").should("exist");
});
});
});
});

View file

@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View file

@ -0,0 +1,3 @@
export const getH1 = () => cy.get("h1");
export const getH2 = () => cy.get("h2");
export const getH3 = () => cy.get("h3");

View file

@ -0,0 +1,35 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
login(email: string, password: string): void;
}
}
// -- This is a parent command --
Cypress.Commands.add("login", (email, password) => {
console.log("Custom command example: Login", email, password);
});
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })

View file

@ -0,0 +1,17 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.ts using ES2015 syntax:
import "./commands";

View file

@ -0,0 +1,20 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"allowJs": true,
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["cypress", "node"],
"sourceMap": false
},
"include": [
"**/*.ts",
"**/*.js",
"../cypress.config.ts",
"../**/*.cy.ts",
"../**/*.cy.tsx",
"../**/*.cy.js",
"../**/*.cy.jsx",
"../**/*.d.ts"
]
}

View file

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View file

@ -0,0 +1,71 @@
const { withNx } = require("@nx/next/plugins/with-nx");
const NextFederationPlugin = require("@module-federation/nextjs-mf");
/**
* @type {import('@nx/next/plugins/with-nx').WithNxOptions}
**/
const nextConfig = {
nx: {
// Set this to true if you would like to to use SVGR
// See: https://github.com/gregberge/svgr
svgr: false,
},
webpack(config, options) {
const { isServer } = options;
config.watchOptions = {
ignored: ["**/node_modules/**", "**/@mf-types/**"],
};
// used for testing build output snapshots
const remotes = {
checkout: `checkout@http://localhost:3002/_next/static/${
isServer ? "ssr" : "chunks"
}/remoteEntry.js`,
home_app: `home_app@http://localhost:3000/_next/static/${
isServer ? "ssr" : "chunks"
}/remoteEntry.js`,
shop: `shop@http://localhost:3001/_next/static/${
isServer ? "ssr" : "chunks"
}/remoteEntry.js`,
};
config.plugins.push(
new NextFederationPlugin({
name: "home_app",
filename: "static/chunks/remoteEntry.js",
remotes: {
shop: remotes.shop,
checkout: remotes.checkout,
},
exposes: {
"./SharedNav": "./components/SharedNav",
"./menu": "./components/menu",
},
shared: {
"lodash/": {},
antd: {
requiredVersion: "5.19.1",
version: "5.19.1",
},
"@ant-design/": {
singleton: true,
},
},
extraOptions: {
debug: false,
exposePages: true,
enableImageLoaderFix: true,
enableUrlLoaderFix: true,
},
}),
);
config.plugins.push({
name: "xxx",
apply(compiler) {
compiler.options.devtool = false;
},
});
return config;
},
};
module.exports = withNx(nextConfig);

View file

@ -0,0 +1,29 @@
{
"name": "@module-federation/3000-home",
"version": "1.0.0",
"private": true,
"dependencies": {
"next": "latest",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"antd": "5.19.1",
"@ant-design/cssinjs": "^1.21.0",
"buffer": "5.7.1",
"encoding": "0.1.13",
"eslint-scope": "7.2.2",
"events": "3.3.0",
"js-cookie": "3.0.5",
"lodash": "4.17.21",
"node-fetch": "2.7.0",
"schema-utils": "3.3.0",
"terser-webpack-plugin": "5.3.10",
"typescript": "5.3.3",
"upath": "2.0.1",
"url": "0.11.3",
"util": "0.12.5"
},
"scripts": {
"dev": "RSPACK_CONFIG_VALIDATE=loose-silent next",
"build": "cross-env RSPACK_CONFIG_VALIDATE=loose-silent NEXT_TELEMETRY_DISABLED=1 node --trace-deprecation --enable-source-maps ../../packages/next/dist/bin/next build"
}
}

View file

@ -0,0 +1,70 @@
import * as React from "react";
import { useState } from "react";
import App from "next/app";
import { Layout, version, ConfigProvider } from "antd";
import { StyleProvider } from "@ant-design/cssinjs";
import Router, { useRouter } from "next/router";
const SharedNav = React.lazy(() => import("../components/SharedNav"));
import HostAppMenu from "../components/menu";
function MyApp(props) {
const { Component, pageProps } = props;
const { asPath } = useRouter();
const [MenuComponent, setMenuComponent] = useState(() => HostAppMenu);
const handleRouteChange = async (url) => {
if (url.startsWith("/shop")) {
} else if (url.startsWith("/checkout")) {
} else {
setMenuComponent(() => HostAppMenu);
}
};
// handle first route hit.
React.useEffect(() => {
handleRouteChange(asPath);
}, [asPath]);
//handle route change
React.useEffect(() => {
// Step 3: Subscribe on events
Router.events.on("routeChangeStart", handleRouteChange);
return () => {
Router.events.off("routeChangeStart", handleRouteChange);
};
}, []);
return (
<StyleProvider layer>
<ConfigProvider theme={{ hashed: false }}>
<Layout style={{ minHeight: "100vh" }} prefixCls={"dd"}>
<React.Suspense>
<SharedNav />
</React.Suspense>
<Layout>
<Layout.Sider width={200}>
<MenuComponent />
</Layout.Sider>
<Layout>
<Layout.Content style={{ background: "#fff", padding: 20 }}>
<Component {...pageProps} />
</Layout.Content>
<Layout.Footer
style={{
background: "#fff",
color: "#999",
textAlign: "center",
}}
>
antd@{version}
</Layout.Footer>
</Layout>
</Layout>
</Layout>
</ConfigProvider>
</StyleProvider>
);
}
MyApp.getInitialProps = async (ctx) => {
return App.getInitialProps(ctx);
};
export default MyApp;

View file

@ -0,0 +1,26 @@
import React from "react";
import Document, { Html, Head, Main, NextScript } from "next/document";
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
};
}
render() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;

View file

@ -0,0 +1,3 @@
export default function handler(req, res) {
res.status(200).json({ name: "John Doe" });
}

View file

@ -0,0 +1,19 @@
/* eslint-disable */
import { useState, useEffect } from "react";
export default function ExposedPages() {
const [pageMap] = useState("");
const [pageMapV2] = useState("");
return (
<>
<h1>This app exposes the following pages:</h1>
<h2>./pages-map</h2>
<pre>{JSON.stringify(pageMap, undefined, 2)}</pre>
<h2>./pages-map-v2</h2>
<pre>{JSON.stringify(pageMapV2, undefined, 2)}</pre>
</>
);
}

View file

@ -0,0 +1,22 @@
/* eslint-disable */
import Link from "next/link";
export default function TestBrokenRemotes() {
return (
<div>
<h2>This page is a test for broken remoteEntries.js</h2>
<p>
Check unresolved host {" "}
<Link href="/unresolved-host">/unresolved-host</Link> (on
http://localhost:<b>3333</b>/_next/static/chunks/remoteEntry.js)
</p>
<p>
Check wrong response for remoteEntry {" "}
<Link href="/wrong-entry">/wrong-entry</Link> (on
http://localhost:3000/_next/static/chunks/remoteEntry<b>Wrong</b>
.js)
</p>
</div>
);
}

View file

@ -0,0 +1,16 @@
import { NextPage } from "next";
const TestRemoteHook: NextPage = () => {
return (
<>
<div>
Page with custom remote hook. You must see text in red box below:
</div>
<div style={{ border: "1px solid red", padding: 5 }}>
blank text for now
</div>
</>
);
};
export default TestRemoteHook;

View file

@ -0,0 +1,9 @@
import SharedNav from "../../components/SharedNav";
export default function TestSharedNav() {
return (
<div>
<SharedNav />
</div>
);
}

View file

@ -0,0 +1,139 @@
/* eslint-disable */
import React from "react";
import Head from "next/head";
const Home = () => {
return (
<>
<Head>
<title>Home</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<h1 style={{ fontSize: "2em" }}>
This is SPA combined from 3 different nextjs applications.
</h1>
<p className="description">
They utilize omnidirectional routing and pages or components are able to
be federated between applications.
</p>
<p>You may open any application by clicking on the links below:</p>
<ul>
<li>
<a
href="#reloadPage"
onClick={() => (window.location.href = "http://localhost:3000")}
>
localhost:3000
</a>
{" "}
<b>home</b>
</li>
<li>
<a
href="#reloadPage"
onClick={() => (window.location.href = "http://localhost:3001")}
>
localhost:3001
</a>
{" "}
<b>shop</b>
</li>
<li>
<a
href="#reloadPage"
onClick={() => (window.location.href = "http://localhost:3002")}
>
localhost:3002
</a>
{" "}
<b>checkout</b>
</li>
</ul>
<h2 style={{ marginTop: "30px" }}>Federation test cases</h2>
<table border={1} cellPadding={5}>
<thead>
<tr>
<td></td>
<td>Test case</td>
<td>Expected</td>
<td>Actual</td>
</tr>
</thead>
<tbody>
<tr>
<td></td>
<td>
Loading remote component (CheckoutTitle) from localhost:3002
<br />
<blockquote>
lazy(()=&gt;import(&apos;checkout/CheckoutTitle&apos;))
</blockquote>
</td>
<td>
<h3>This title came from checkout with hooks data!!!</h3>
</td>
<td>old</td>
</tr>
<tr>
<td></td>
<td>
Load federated component from checkout with old antd version
</td>
<td>[Button from antd@5.18.3]</td>
<td>test</td>
</tr>
<tr>
<td></td>
<td>
Loading remote component with PNG image from localhost:3001
<br />
<blockquote>(check publicPath fix in image-loader)</blockquote>
</td>
<td>
<img className="home-webpack-png" src="./webpack.png" />
</td>
<td>other thing</td>
</tr>
<tr>
<td></td>
<td>
Loading remote component with SVG from localhost:3001
<br />
<blockquote>(check publicPath fix in url-loader)</blockquote>
</td>
<td>
<img src="./webpack.svg" />
</td>
<td>anothaaaaaa</td>
</tr>
</tbody>
</table>
<h2 style={{ marginTop: "30px" }}>Other problems to fix:</h2>
<ul>
<li>
🐞 Incorrectly exposed modules in next.config.js (e.g. typo in path)
do not throw an error in console
</li>
<li>
📝 Try to introduce a remote entry loading according to prefix path.
It will be nice runtime improvement if you have eg 20 apps and load
just one remoteEntry instead of all of them.
</li>
<li>
📝 It will be nice to regenerate remoteEntry if new page was added in
remote app.
</li>
<li>
📝 Remote components do not regenerate chunks if they were changed.
</li>
</ul>
</>
);
};
Home.getInitialProps = () => {
console.log("home calls get initial props");
return {};
};
export default Home;

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,5 @@
<svg viewBox="0 0 3046.7 875.7" xmlns="http://www.w3.org/2000/svg">
<path d="m387 0 387 218.9v437.9l-387 218.9-387-218.9v-437.9z" fill="#fff"/>
<path d="m704.9 641.7-305.1 172.6v-134.4l190.1-104.6zm20.9-18.9v-360.9l-111.6 64.5v232zm-657.9 18.9 305.1 172.6v-134.4l-190.2-104.6zm-20.9-18.9v-360.9l111.6 64.5v232zm13.1-384.3 312.9-177v129.9l-200.5 110.3-1.6.9zm652.6 0-312.9-177v129.9l200.5 110.2 1.6.9z" fill="#8ed6fb"/>
<path d="m373 649.3-187.6-103.2v-204.3l187.6 108.3zm26.8 0 187.6-103.1v-204.4l-187.6 108.3zm-201.7-331.1 188.3-103.5 188.3 103.5-188.3 108.7z" fill="#1c78c0"/>
</svg>

After

Width:  |  Height:  |  Size: 592 B

View file

@ -0,0 +1,5 @@
declare module "shop/useCustomRemoteHook";
declare module "shop/WebpackSvg";
declare module "shop/WebpackPng";
declare module "checkout/CheckoutTitle";
declare module "checkout/ButtonOldAnt";

View file

@ -0,0 +1,19 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "es2017",
"jsx": "preserve",
"module": "esnext",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"resolveJsonModule": true,
"isolatedModules": true,
"incremental": true
},
"include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "next-env.d.ts"],
"exclude": ["node_modules", "jest.config.ts"]
}