Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for using the vite dev server on remotes #551

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@
"workspaces": [
"packages/**"
],
"files": [
"packages/lib/dist/*",
"packages/types/*"
],
"main": "./packages/lib/dist/index.js",
"module": "./packages/lib/dist/index.mjs",
"types": "./packages/lib/types/index.d.ts",
"exports": {
".": {
"types": "./packages/lib/types/index.d.ts",
"import": "./packages/lib/dist/index.mjs",
"require": "./packages/lib/dist/index.js"
}
},
"engines": {
"node": "^14.18.0 || >=16.0.0",
"pnpm": ">=8.0.1"
Expand All @@ -12,7 +26,7 @@
"license": "MulanPSL-2.0",
"scripts": {
"preinstall": "npx only-allow pnpm",
"prepare": "husky install",
"prepare": "pnpm run build",
"postinstall": "npx playwright install",
"lint-staged": "lint-staged",
"format": "prettier -w packages/lib/**/*.ts",
Expand Down
1 change: 1 addition & 0 deletions packages/examples/react-vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"build:remotes": "pnpm --parallel --filter \"./remote\" build",
"serve:remotes": "pnpm --parallel --filter \"./remote\" serve",
"dev:hosts": "pnpm --filter \"./host\" dev",
"dev:remotes": "pnpm --filter \"./remote\" dev",
"stop": "kill-port --port 5000,5001"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions packages/examples/vue3-advanced-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"build:remotes": "pnpm --parallel --filter \"./team-blue\" --filter \"./team-green\" build",
"serve:remotes": "pnpm --parallel --filter \"./team-blue\" --filter \"./team-green\" serve",
"dev:hosts": "pnpm --filter \"./team-red\" dev",
"dev:remotes": "pnpm --parallel --filter \"./team-blue\" --filter \"./team-green\" dev",
"stop": "kill-port --port 5000,5001,5002",
"clean": "pnpm run clean"
},
Expand Down
80 changes: 77 additions & 3 deletions packages/lib/src/dev/expose-development.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,91 @@
// SPDX-License-Identifier: MulanPSL-2.0
// *****************************************************************************

import { parseExposeOptions } from '../utils'
import { parsedOptions } from '../public'
import { resolve } from 'path'
import { getModuleMarker, normalizePath, parseExposeOptions } from '../utils'
import { EXTERNALS, SHARED, builderInfo, parsedOptions } from '../public'
import type { VitePluginFederationOptions } from 'types'
import type { PluginHooks } from '../../types/pluginHooks'
import { UserConfig, ViteDevServer } from 'vite'
import { importShared } from './import-shared'

export function devExposePlugin(
options: VitePluginFederationOptions
): PluginHooks {
parsedOptions.devExpose = parseExposeOptions(options)
let moduleMap = ''
let remoteFile: string | null = null

const exposeModules = (baseDir) => {
for (const item of parsedOptions.devExpose) {
const moduleName = getModuleMarker(`\${${item[0]}}`, SHARED)
EXTERNALS.push(moduleName)
const importPath = normalizePath(item[1].import)
const exposeFilepath = normalizePath(resolve(item[1].import))
moduleMap += `\n"${item[0]}":() => {
return __federation_import('/${importPath}', '${baseDir}@fs/${exposeFilepath}').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},`
}
}

const buildRemoteFile = (baseDir) => {
return `(${importShared})();
import RefreshRuntime from "${baseDir}@react-refresh"
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
const exportSet = new Set(['Module', '__esModule', 'default', '_export_sfc']);
let moduleMap = {
${moduleMap}
};
const __federation_import = async (urlImportPath, fsImportPath) => {
let importedModule;
try {
return await import(fsImportPath);
}catch(ex) {
return await import(urlImportPath);
}
};
export const get =(module) => {
if(!moduleMap[module]) throw new Error('Can not find remote module ' + module)
return moduleMap[module]();
};
export const init =(shareScope) => {
globalThis.__federation_shared__= globalThis.__federation_shared__|| {};
Object.entries(shareScope).forEach(([key, value]) => {
const versionKey = Object.keys(value)[0];
const versionValue = Object.values(value)[0];
const scope = versionValue.scope || 'default'
globalThis.__federation_shared__[scope] = globalThis.__federation_shared__[scope] || {};
const shared= globalThis.__federation_shared__[scope];
(shared[key] = shared[key]||{})[versionKey] = versionValue;
});
}
`
}

return {
name: 'originjs:expose-development'
name: 'originjs:expose-development',
config: (config: UserConfig) => {
if (config.base) {
exposeModules(config.base)
remoteFile = buildRemoteFile(config.base)
}
},
configureServer: (server: ViteDevServer) => {
const remoteFilePath = `${builderInfo.assetsDir}/${options.filename}`
server.middlewares.use((req, res, next) => {
if (req.url && req.url.includes(remoteFilePath)) {
res.writeHead(200, 'OK', {
'Content-Type': 'text/javascript',
'Access-Control-Allow-Origin': '*'
})
res.write(remoteFile)
res.end()
} else {
next()
}
})
}
}
}
40 changes: 40 additions & 0 deletions packages/lib/src/dev/import-shared.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export const importShared = function () {
if (!globalThis.importShared) {
const moduleCache = Object.create(null)
const getSharedFromRuntime = async (name, shareScope) => {
let module = null
if (globalThis?.__federation_shared__?.[shareScope]?.[name]) {
const versionObj = globalThis.__federation_shared__[shareScope][name]
const versionValue = Object.values(versionObj)[0]
module = await (await versionValue.get())()
}
if (module) {
return flattenModule(module, name)
}
}
const flattenModule = (module, name) => {
// use a shared module which export default a function will getting error 'TypeError: xxx is not a function'
if (typeof module.default === 'function') {
Object.keys(module).forEach((key) => {
if (key !== 'default') {
module.default[key] = module[key]
}
})
moduleCache[name] = module.default
return module.default
}
if (module.default) module = Object.assign({}, module.default, module)
moduleCache[name] = module
return module
}
globalThis.importShared = async (name, shareScope = 'default') => {
try {
return moduleCache[name]
? new Promise((r) => r(moduleCache[name]))
: (await getSharedFromRuntime(name, shareScope)) || null
} catch (ex) {
console.log(ex)
}
}
}
}
111 changes: 109 additions & 2 deletions packages/lib/src/dev/remote-development.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
// *****************************************************************************

import type { UserConfig } from 'vite'
import type { ConfigTypeSet, VitePluginFederationOptions } from 'types'
import type {
ConfigTypeSet,
ExposesConfig,
VitePluginFederationOptions
} from 'types'
import { walk } from 'estree-walker'
import MagicString from 'magic-string'
import { readFileSync } from 'fs'
Expand All @@ -30,6 +34,10 @@ import {
} from '../utils'
import { builderInfo, parsedOptions, devRemotes } from '../public'
import type { PluginHooks } from '../../types/pluginHooks'
import { Literal } from 'estree'
import { importShared } from './import-shared'

const exposedItems: string[] = []

export function devRemotePlugin(
options: VitePluginFederationOptions
Expand Down Expand Up @@ -206,6 +214,8 @@ export {__federation_method_ensure, __federation_method_getRemote , __federation
return
}

code += `(${importShared})();\n`

let ast: AcornNode | null = null
try {
ast = this.parse(code)
Expand All @@ -215,20 +225,94 @@ export {__federation_method_ensure, __federation_method_getRemote , __federation
if (!ast) {
return null
}

const magicString = new MagicString(code)
const hasStaticImported = new Map<string, string>()

let requiresRuntime = false
let manualRequired: any = null // set static import if exists
walk(ast, {
enter(node: any) {
if (
node.type === 'MemberExpression' &&
node.object.type === 'MemberExpression' &&
node.object.object.type === 'MetaProperty' &&
node.object.object.meta.name === 'import' &&
node.object.property.type === 'Identifier' &&
node.object.property.name === 'env' &&
node.property.name === 'BASE_URL'
) {
const serverPort = viteDevServer.config.inlineConfig.server?.port
const baseUrlFromConfig =
viteDevServer.config.env.BASE_URL &&
viteDevServer.config.env.BASE_URL !== '/'
? viteDevServer.config.env.BASE_URL
: ''
// This assumes that the dev server will always be running on localhost. That's probably not a good assumption, but I don't know how to work around it right now.
const baseUrl = `"//localhost:${serverPort}${baseUrlFromConfig}"`
magicString.overwrite(node.start, node.end, baseUrl)
node = { type: 'Literal', value: baseUrl } as Literal
}
if (
node.type === 'ImportDeclaration' &&
node.source?.value === 'virtual:__federation__'
) {
manualRequired = node
}
if (
isExposed(id, parsedOptions.devExpose) &&
node.type === 'ImportDeclaration' &&
node.source?.value
) {
const moduleName = node.source.value
if (
parsedOptions.devShared.some(
(sharedInfo) => sharedInfo[0] === moduleName
)
) {
const namedImportDeclaration: (string | never)[] = []
let defaultImportDeclaration: string | null = null
if (!node.specifiers?.length) {
// invalid import , like import './__federation_shared_lib.js' , and remove it
magicString.remove(node.start, node.end)
} else {
node.specifiers.forEach((specify) => {
if (specify.imported?.name) {
namedImportDeclaration.push(
`${
specify.imported.name === specify.local.name
? specify.imported.name
: `${specify.imported.name}:${specify.local.name}`
}`
)
} else {
defaultImportDeclaration = specify.local.name
}
})

if (defaultImportDeclaration && namedImportDeclaration.length) {
// import a, {b} from 'c' -> const a = await importShared('c'); const {b} = a;
const imports = namedImportDeclaration.join(',')
const line = `const ${defaultImportDeclaration} = await importShared('${moduleName}') || await import('${moduleName}');\nconst {${imports}} = ${defaultImportDeclaration};\n`

magicString.overwrite(node.start, node.end, line)
} else if (defaultImportDeclaration) {
magicString.overwrite(
node.start,
node.end,
`const ${defaultImportDeclaration} = await importShared('${moduleName}') || await import('${moduleName}');\n`
)
} else if (namedImportDeclaration.length) {
magicString.overwrite(
node.start,
node.end,
`const {${namedImportDeclaration.join(
','
)}} = await importShared('${moduleName}') || await import('${moduleName}');\n`
)
}
}
}
}

if (
(node.type === 'ImportExpression' ||
Expand Down Expand Up @@ -411,4 +495,27 @@ export {__federation_method_ensure, __federation_method_getRemote , __federation
}
return res
}
function isExposed(id: string, options: (string | ConfigTypeSet)[]) {
if (exposedItems.includes(id)) {
return true
}
if (options.length >= 2 && (options[1] as ExposesConfig).import) {
if (normalizePath((options[1] as ExposesConfig).import)) {
return true
}
}
for (let i = 0, length = options.length; i < length; i++) {
const item = options[i]
if (
Array.isArray(item) &&
item.length >= 2 &&
(item[1] as ExposesConfig).import
) {
if (normalizePath((item[1] as ExposesConfig).import)) {
return true
}
}
}
return false
}
}
Loading