diff --git a/README.md b/README.md index 58c14df4bcf..615ceacb289 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ This repository is to showcase examples of how Webpack 5's new Module Federation - [x] [NextJS SSR](./nextjs-ssr/README.md) — Powered by software streams, with [nextjs-ssr](https://github.com/module-federation/module-federation-examples/tree/master/nextjs-ssr) (currently in closed beta testing) - [x] [Building A Plugin-based Workflow Designer With Angular and Module Federation](https://github.com/manfredsteyer/module-federation-with-angular-dynamic-workflow-designer) — External Example - [x] [Vue.js](./vue3-demo/README.md) — Simple host/remote (render function / sfc) example using Vue 3.0. +- [x] [Vue2 SSR](./genesis/README.md) — This example demonstrates module as a service # Notes diff --git a/genesis/README.md b/genesis/README.md new file mode 100644 index 00000000000..e87b2781257 --- /dev/null +++ b/genesis/README.md @@ -0,0 +1,12 @@ +# Vue Genesis Example + +[This example demonstrates module as a service](https://github.com/fmfe/genesis/blob/master/docs/zh-CN/why.md#%E4%BB%80%E4%B9%88%E6%98%AF%E6%A8%A1%E5%9D%97%E5%8D%B3%E6%9C%8D%E5%8A%A1) + +- `ssr-mf-about` About service +- `ssr-mf-home` Home service + +# Running Demo +Run `yarn && yarn dev` or `yarn && yarn build && yarn start` + +- [localhost:3001](http://localhost:3001) (ssr-mf-home) +- [localhost:3002](http://localhost:3002/about) (ssr-mf-about) \ No newline at end of file diff --git a/genesis/lerna.json b/genesis/lerna.json new file mode 100644 index 00000000000..352f7710b8b --- /dev/null +++ b/genesis/lerna.json @@ -0,0 +1,5 @@ +{ + "version": "0.0.0", + "npmClient": "yarn", + "useWorkspaces": true +} diff --git a/genesis/package.json b/genesis/package.json new file mode 100644 index 00000000000..f1b6bec6cc6 --- /dev/null +++ b/genesis/package.json @@ -0,0 +1,18 @@ +{ + "private": true, + "scripts": { + "dev": "concurrently --raw \"lerna run --scope=ssr-mf-about dev\" \"lerna run --scope=ssr-mf-home dev\"", + "build": "FORCE_COLOR=1 lerna run --scope=ssr-mf-about build && FORCE_COLOR=1 lerna run --scope=ssr-mf-home build", + "start": "concurrently --raw \"lerna run --scope=ssr-mf-about start\" \"lerna run --scope=ssr-mf-home start\"" + }, + "devDependencies": { + "concurrently": "7.0.0", + "lerna": "4.0.0" + }, + "workspaces": { + "packages": [ + "ssr-mf-about", + "ssr-mf-home" + ] + } +} diff --git a/genesis/ssr-mf-about/genesis.build.ts b/genesis/ssr-mf-about/genesis.build.ts new file mode 100644 index 00000000000..6d9c804384b --- /dev/null +++ b/genesis/ssr-mf-about/genesis.build.ts @@ -0,0 +1,9 @@ +import { Build } from '@fmfe/genesis-compiler'; + +import { ssr } from './genesis'; + +const start = () => { + const build = new Build(ssr); + return build.start(); +}; +start(); diff --git a/genesis/ssr-mf-about/genesis.dev.ts b/genesis/ssr-mf-about/genesis.dev.ts new file mode 100644 index 00000000000..abb221b7729 --- /dev/null +++ b/genesis/ssr-mf-about/genesis.dev.ts @@ -0,0 +1,13 @@ +import { Watch } from '@fmfe/genesis-compiler'; + +import { app, ssr, startApp } from './genesis'; + +const start = async () => { + const watch = new Watch(ssr); + await watch.start(); + const renderer = watch.renderer; + app.use(watch.devMiddleware); + app.use(watch.hotMiddleware); + startApp(renderer); +}; +start(); diff --git a/genesis/ssr-mf-about/genesis.prod.ts b/genesis/ssr-mf-about/genesis.prod.ts new file mode 100644 index 00000000000..fa8ffe53c83 --- /dev/null +++ b/genesis/ssr-mf-about/genesis.prod.ts @@ -0,0 +1,15 @@ +import express from 'express'; + +import { app, ssr, startApp } from './genesis'; + +const renderer = ssr.createRenderer(); + +app.use( + renderer.staticPublicPath, + express.static(renderer.staticDir, { + immutable: true, + maxAge: '31536000000' + }) +); + +startApp(renderer); diff --git a/genesis/ssr-mf-about/genesis.ts b/genesis/ssr-mf-about/genesis.ts new file mode 100644 index 00000000000..48ec778d162 --- /dev/null +++ b/genesis/ssr-mf-about/genesis.ts @@ -0,0 +1,61 @@ +import { MF, Renderer, SSR } from '@fmfe/genesis-core'; +import express from 'express'; +import path from 'path'; + +export const app = express(); + +export const ssr = new SSR({ + name: 'ssr-mf-about', + build: { + extractCSS: false + } +}); + +export const mf = new MF(ssr, { + exposes: { + './src/routes': './src/routes.ts' + }, + remotes: [ + { + name: 'ssr-mf-home', + clientOrigin: 'http://localhost:3001', + serverOrigin: 'http://localhost:3001' + } + ], + shared: { + vue: { + singleton: true + }, + 'vue-router': { + singleton: true + }, + 'vue-meta': { + singleton: true + } + }, + typesDir: path.resolve('./types/ssr-mf-about') +}); + +app.get(mf.manifestRoutePath, async (req, res, next) => { + const t = Number(req.query.t); + const maxAwait = 1000 * 60; + await mf.exposes.getManifest(t, maxAwait); + next(); +}); + +export const startApp = (renderer: Renderer) => { + mf.remote.init(renderer); + mf.remote.polling(); + app.get('/about', async (req, res, next) => { + try { + const result = await renderer.renderHtml({ + req, + res + }); + res.send(result.data); + } catch (e) { + next(e); + } + }); + app.listen(3002, () => console.log(`http://localhost:3002`)); +}; diff --git a/genesis/ssr-mf-about/package.json b/genesis/ssr-mf-about/package.json new file mode 100644 index 00000000000..b376cafa647 --- /dev/null +++ b/genesis/ssr-mf-about/package.json @@ -0,0 +1,27 @@ +{ + "name": "ssr-mf-about", + "version": "2.0.10", + "main": "index.js", + "license": "MIT", + "private": true, + "scripts": { + "dev": "genesis-ts-node --project=./tsconfig.node.json genesis.dev", + "build": "rm -rf dist types && npm run build:dts && npm run build:vue && npm run build:node", + "build:node": "NODE_ENV=production genesis-tsc --build ./tsconfig.node.json", + "build:vue": "NODE_ENV=production genesis-ts-node --project=./tsconfig.node.json genesis.build", + "build:dts": "genesis-vue-tsc --declaration --emitDeclarationOnly", + "type-check": "genesis-vue-tsc --noEmit", + "start": "NODE_ENV=production node dist/genesis.prod" + }, + "devDependencies": { + "@fmfe/genesis-compiler": "2.0.10", + "@types/express": "^4.17.13", + "vue": "^2.6.14", + "vue-meta": "^2.4.0", + "vue-router": "^3.5.3" + }, + "dependencies": { + "@fmfe/genesis-core": "2.0.10", + "express": "^4.17.2" + } +} diff --git a/genesis/ssr-mf-about/src/entry-client.ts b/genesis/ssr-mf-about/src/entry-client.ts new file mode 100644 index 00000000000..9557fc8f5a7 --- /dev/null +++ b/genesis/ssr-mf-about/src/entry-client.ts @@ -0,0 +1 @@ +export { default } from 'ssr-mf-home/src/common/create-app-client'; diff --git a/genesis/ssr-mf-about/src/entry-server.ts b/genesis/ssr-mf-about/src/entry-server.ts new file mode 100644 index 00000000000..c55ced690f5 --- /dev/null +++ b/genesis/ssr-mf-about/src/entry-server.ts @@ -0,0 +1,5 @@ +import { createApp } from 'ssr-mf-home/src/common/create-app'; + +import { routes } from './routes'; + +export default createApp(routes); diff --git a/genesis/ssr-mf-about/src/images/logo.svg b/genesis/ssr-mf-about/src/images/logo.svg new file mode 100644 index 00000000000..74c2e0ff297 --- /dev/null +++ b/genesis/ssr-mf-about/src/images/logo.svg @@ -0,0 +1,26 @@ + + + ssr + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/genesis/ssr-mf-about/src/index.html b/genesis/ssr-mf-about/src/index.html new file mode 100644 index 00000000000..0499fb3fd2a --- /dev/null +++ b/genesis/ssr-mf-about/src/index.html @@ -0,0 +1,16 @@ + + + + + <%- meta %> + <%- title %> + <%- style %> + + + + <%-html %> + <%- scriptState %> + <%- script %> + + + \ No newline at end of file diff --git a/genesis/ssr-mf-about/src/routes.ts b/genesis/ssr-mf-about/src/routes.ts new file mode 100644 index 00000000000..e0e20b1c6ef --- /dev/null +++ b/genesis/ssr-mf-about/src/routes.ts @@ -0,0 +1,8 @@ +import { RouteConfig } from 'vue-router'; + +export const routes: RouteConfig[] = [ + { + path: '/about', + component: () => import('./views/about.vue').then((m) => m.default) + } +]; diff --git a/genesis/ssr-mf-about/src/shims-vue.d.ts b/genesis/ssr-mf-about/src/shims-vue.d.ts new file mode 100644 index 00000000000..1a9f5b5f195 --- /dev/null +++ b/genesis/ssr-mf-about/src/shims-vue.d.ts @@ -0,0 +1,4 @@ +declare module '*.vue' { + import Vue from 'vue'; + export default Vue; +} diff --git a/genesis/ssr-mf-about/src/views/about.vue b/genesis/ssr-mf-about/src/views/about.vue new file mode 100644 index 00000000000..82e61dbd568 --- /dev/null +++ b/genesis/ssr-mf-about/src/views/about.vue @@ -0,0 +1,19 @@ + + diff --git a/genesis/ssr-mf-about/tsconfig.json b/genesis/ssr-mf-about/tsconfig.json new file mode 100644 index 00000000000..0e210fe8c39 --- /dev/null +++ b/genesis/ssr-mf-about/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "moduleResolution": "node", + "esModuleInterop": true, + "experimentalDecorators": true, + "allowJs": true, + "sourceMap": true, + "strict": true, + "noEmit": false, + "noUnusedLocals": true, + "skipLibCheck": true, + "noImplicitAny": false, + "resolveJsonModule": true, + "baseUrl": "./", + "declaration": true, + "declarationDir": "./types", + "types": [ + "@types/node" + ], + "allowSyntheticDefaultImports": true + }, + "exclude": [ + "dist", + "types" + ] +} \ No newline at end of file diff --git a/genesis/ssr-mf-about/tsconfig.node.json b/genesis/ssr-mf-about/tsconfig.node.json new file mode 100644 index 00000000000..dbbaa7bd64b --- /dev/null +++ b/genesis/ssr-mf-about/tsconfig.node.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "sourceMap": false, + "noEmit": false, + "target": "ES2018", + "module": "CommonJS", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "declaration": false, + "declarationDir": null, + "esModuleInterop": true, + "outDir": "./dist" + }, + "exclude": [ + "dist", + "types", + "src" + ] +} \ No newline at end of file diff --git a/genesis/ssr-mf-home/genesis.build.ts b/genesis/ssr-mf-home/genesis.build.ts new file mode 100644 index 00000000000..6d9c804384b --- /dev/null +++ b/genesis/ssr-mf-home/genesis.build.ts @@ -0,0 +1,9 @@ +import { Build } from '@fmfe/genesis-compiler'; + +import { ssr } from './genesis'; + +const start = () => { + const build = new Build(ssr); + return build.start(); +}; +start(); diff --git a/genesis/ssr-mf-home/genesis.dev.ts b/genesis/ssr-mf-home/genesis.dev.ts new file mode 100644 index 00000000000..abb221b7729 --- /dev/null +++ b/genesis/ssr-mf-home/genesis.dev.ts @@ -0,0 +1,13 @@ +import { Watch } from '@fmfe/genesis-compiler'; + +import { app, ssr, startApp } from './genesis'; + +const start = async () => { + const watch = new Watch(ssr); + await watch.start(); + const renderer = watch.renderer; + app.use(watch.devMiddleware); + app.use(watch.hotMiddleware); + startApp(renderer); +}; +start(); diff --git a/genesis/ssr-mf-home/genesis.prod.ts b/genesis/ssr-mf-home/genesis.prod.ts new file mode 100644 index 00000000000..fa8ffe53c83 --- /dev/null +++ b/genesis/ssr-mf-home/genesis.prod.ts @@ -0,0 +1,15 @@ +import express from 'express'; + +import { app, ssr, startApp } from './genesis'; + +const renderer = ssr.createRenderer(); + +app.use( + renderer.staticPublicPath, + express.static(renderer.staticDir, { + immutable: true, + maxAge: '31536000000' + }) +); + +startApp(renderer); diff --git a/genesis/ssr-mf-home/genesis.ts b/genesis/ssr-mf-home/genesis.ts new file mode 100644 index 00000000000..2d27a8b2713 --- /dev/null +++ b/genesis/ssr-mf-home/genesis.ts @@ -0,0 +1,55 @@ +import { MF, Renderer, SSR } from '@fmfe/genesis-core'; +import express from 'express'; +import path from 'path'; + +export const app = express(); + +export const ssr = new SSR({ + name: 'ssr-mf-home', + build: { + extractCSS: false + } +}); + +export const mf = new MF(ssr, { + shared: { + vue: { + singleton: true + }, + 'vue-router': { + singleton: true + }, + 'vue-meta': { + singleton: true + } + }, + exposes: { + './src/common/create-app-client': 'src/common/create-app-client.ts', + './src/common/create-app': 'src/common/create-app.ts' + }, + remotes: [ + { + name: 'ssr-mf-about', + clientOrigin: 'http://localhost:3002', + serverOrigin: 'http://localhost:3002' + } + ], + typesDir: path.resolve('./types/ssr-mf-about') +}); + +export const startApp = (renderer: Renderer) => { + mf.remote.init(renderer); + mf.remote.polling(); + app.get('/', async (req, res, next) => { + try { + const result = await renderer.renderHtml({ + req, + res + }); + res.send(result.data); + } catch (e) { + next(e); + } + }); + app.listen(3001, () => console.log(`http://localhost:3001`)); +}; diff --git a/genesis/ssr-mf-home/package.json b/genesis/ssr-mf-home/package.json new file mode 100644 index 00000000000..8867aec8d6a --- /dev/null +++ b/genesis/ssr-mf-home/package.json @@ -0,0 +1,27 @@ +{ + "name": "ssr-mf-home", + "version": "2.0.10", + "main": "index.js", + "license": "MIT", + "private": true, + "scripts": { + "dev": "genesis-ts-node --project=./tsconfig.node.json genesis.dev", + "build": "rm -rf dist types && npm run build:dts && npm run build:vue && npm run build:node", + "build:node": "NODE_ENV=production genesis-tsc --build ./tsconfig.node.json", + "build:vue": "NODE_ENV=production genesis-ts-node --project=./tsconfig.node.json genesis.build", + "build:dts": "genesis-vue-tsc --declaration --emitDeclarationOnly", + "type-check": "genesis-vue-tsc --noEmit", + "start": "NODE_ENV=production node dist/genesis.prod" + }, + "devDependencies": { + "@fmfe/genesis-compiler": "2.0.10", + "@types/express": "^4.17.13", + "vue": "^2.6.14", + "vue-meta": "^2.4.0", + "vue-router": "^3.5.3" + }, + "dependencies": { + "@fmfe/genesis-core": "2.0.10", + "express": "^4.17.2" + } +} diff --git a/genesis/ssr-mf-home/src/common/app.vue b/genesis/ssr-mf-home/src/common/app.vue new file mode 100644 index 00000000000..900ff5bff09 --- /dev/null +++ b/genesis/ssr-mf-home/src/common/app.vue @@ -0,0 +1,21 @@ + + diff --git a/genesis/ssr-mf-home/src/common/create-app-client.ts b/genesis/ssr-mf-home/src/common/create-app-client.ts new file mode 100644 index 00000000000..f4dcbe96716 --- /dev/null +++ b/genesis/ssr-mf-home/src/common/create-app-client.ts @@ -0,0 +1,6 @@ +import { routes as about } from 'ssr-mf-about/src/routes'; + +import { routes as home } from '../routes'; +import { createApp } from './create-app'; + +export default createApp([...home, ...about]); diff --git a/genesis/ssr-mf-home/src/common/create-app.ts b/genesis/ssr-mf-home/src/common/create-app.ts new file mode 100644 index 00000000000..28f7e156d6f --- /dev/null +++ b/genesis/ssr-mf-home/src/common/create-app.ts @@ -0,0 +1,47 @@ +import { ClientOptions, RenderContext } from '@fmfe/genesis-core'; +import Vue from 'vue'; +import Meta from 'vue-meta'; +import Router, { RouteConfig } from 'vue-router'; + +import App from './app.vue'; + +Vue.use(Meta).use(Router); + +export function createApp(routes: RouteConfig[]) { + return async (context: RenderContext | ClientOptions) => { + const router = new Router({ + mode: 'history', + routes + }); + const url = context.env === 'client' ? context.url : context.data.url; + await router.push(url); + const app = new Vue({ + router, + render(h) { + return h(App); + } + }); + if (context.env === 'server') { + context.beforeRender(() => { + const { title, link, style, script, meta } = app + .$meta() + .inject(); + appendText(context.data, 'title', title?.text() ?? ''); + appendText(context.data, 'meta', meta?.text() ?? ''); + appendText(context.data, 'style', style?.text() ?? ''); + appendText(context.data, 'style', link?.text() ?? ''); + appendText(context.data, 'script', script?.text() ?? ''); + }); + } + return app; + }; +} + +function appendText(data: Record, key: string, value: string) { + if (typeof data[key] !== 'string') { + data[key] = ''; + } + if (value) { + data[key] += value; + } +} diff --git a/genesis/ssr-mf-home/src/entry-client.ts b/genesis/ssr-mf-home/src/entry-client.ts new file mode 100644 index 00000000000..4343373737d --- /dev/null +++ b/genesis/ssr-mf-home/src/entry-client.ts @@ -0,0 +1 @@ +export { default } from './common/create-app-client'; diff --git a/genesis/ssr-mf-home/src/entry-server.ts b/genesis/ssr-mf-home/src/entry-server.ts new file mode 100644 index 00000000000..3994e1b0c58 --- /dev/null +++ b/genesis/ssr-mf-home/src/entry-server.ts @@ -0,0 +1,4 @@ +import { createApp } from './common/create-app'; +import { routes } from './routes'; + +export default createApp(routes); diff --git a/genesis/ssr-mf-home/src/images/logo.svg b/genesis/ssr-mf-home/src/images/logo.svg new file mode 100644 index 00000000000..74c2e0ff297 --- /dev/null +++ b/genesis/ssr-mf-home/src/images/logo.svg @@ -0,0 +1,26 @@ + + + ssr + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/genesis/ssr-mf-home/src/index.html b/genesis/ssr-mf-home/src/index.html new file mode 100644 index 00000000000..0499fb3fd2a --- /dev/null +++ b/genesis/ssr-mf-home/src/index.html @@ -0,0 +1,16 @@ + + + + + <%- meta %> + <%- title %> + <%- style %> + + + + <%-html %> + <%- scriptState %> + <%- script %> + + + \ No newline at end of file diff --git a/genesis/ssr-mf-home/src/routes.ts b/genesis/ssr-mf-home/src/routes.ts new file mode 100644 index 00000000000..b748b207e16 --- /dev/null +++ b/genesis/ssr-mf-home/src/routes.ts @@ -0,0 +1,8 @@ +import { RouteConfig } from 'vue-router'; + +export const routes: RouteConfig[] = [ + { + path: '/', + component: () => import('./views/home.vue').then((m) => m.default) + } +]; diff --git a/genesis/ssr-mf-home/src/shims-vue.d.ts b/genesis/ssr-mf-home/src/shims-vue.d.ts new file mode 100644 index 00000000000..1a9f5b5f195 --- /dev/null +++ b/genesis/ssr-mf-home/src/shims-vue.d.ts @@ -0,0 +1,4 @@ +declare module '*.vue' { + import Vue from 'vue'; + export default Vue; +} diff --git a/genesis/ssr-mf-home/src/views/home.vue b/genesis/ssr-mf-home/src/views/home.vue new file mode 100644 index 00000000000..3bfe49522f3 --- /dev/null +++ b/genesis/ssr-mf-home/src/views/home.vue @@ -0,0 +1,18 @@ + + diff --git a/genesis/ssr-mf-home/tsconfig.json b/genesis/ssr-mf-home/tsconfig.json new file mode 100644 index 00000000000..0e210fe8c39 --- /dev/null +++ b/genesis/ssr-mf-home/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "moduleResolution": "node", + "esModuleInterop": true, + "experimentalDecorators": true, + "allowJs": true, + "sourceMap": true, + "strict": true, + "noEmit": false, + "noUnusedLocals": true, + "skipLibCheck": true, + "noImplicitAny": false, + "resolveJsonModule": true, + "baseUrl": "./", + "declaration": true, + "declarationDir": "./types", + "types": [ + "@types/node" + ], + "allowSyntheticDefaultImports": true + }, + "exclude": [ + "dist", + "types" + ] +} \ No newline at end of file diff --git a/genesis/ssr-mf-home/tsconfig.node.json b/genesis/ssr-mf-home/tsconfig.node.json new file mode 100644 index 00000000000..dbbaa7bd64b --- /dev/null +++ b/genesis/ssr-mf-home/tsconfig.node.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "sourceMap": false, + "noEmit": false, + "target": "ES2018", + "module": "CommonJS", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "declaration": false, + "declarationDir": null, + "esModuleInterop": true, + "outDir": "./dist" + }, + "exclude": [ + "dist", + "types", + "src" + ] +} \ No newline at end of file diff --git a/package.json b/package.json index d16b9e6f84c..be704a501db 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "./advanced-api/automatic-vendor-sharing/*", "./nextjs-bi-directional/*", "./vue3-demo/*", - "./vue-cli/*" + "./vue-cli/*", + "./genesis/*" ], "nohoist": [ "**/svelte",