-
I have a microfrontend setup with multiple vite apps. I want one app to be able to load dependencies served by the other app to avoid having multiple instances of the same module existing at the same time. I know I can use Essentially, I want to be able to access the bundled code via a static url e.g. Is there a plugin that can do that? Or maybe someone can point me to how I can get the resolved path of a bare import from a vite dev server? |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment
-
Ended up writing this plugin: import { URL, fileURLToPath } from 'node:url'
import * as fs from 'node:fs'
import * as path from 'node:path'
import * as walk from 'acorn-walk'
import { parse } from 'acorn'
import * as resolve from 'resolve.exports'
import findRoot from 'find-root'
import { mergeConfig } from 'vite'
/**
* Allows other vite apps to load dependencies from this vite app.
*
* In serve mode: adds /shared-deps?import=<dep> endpoint to the dev server, redirecting to the path resolved by vite
* In build mode: bundles each dependency into a separate bundle
*
* @param externals {string[]}
* @param dependencies {Record<string, string>}
* @param subpaths {Record<string, string[]>}
* @return {import('vite').Plugin}
*/
export const sharedDepsPlugin = ({ externals, dependencies, subpaths }) => {
const virtualModuleId = '/virtual:shared-deps-plugin:main'
const resolvedVirtualModuleId = `\0${virtualModuleId}`
const getImportsText = () => {
const imports = externals.map(importPath => `"${importPath}": import("${importPath}")`).join(',')
return `export default {${imports}}`
}
const getImportsPaths = (script) => {
const ast = parse(script, {
ecmaVersion: 'latest',
sourceType: 'module',
})
const imports = new Map()
walk.ancestor(ast, {
// Find the right node
// This can be the string literal in this: vue: import('/node_modules/...')
// Or this for modules with borked default exports:
// moment-timezone: import('/node_modules/...').then(() => ...)
Literal(node, _, ancestors) {
if (typeof node.value === 'string') {
if (ancestors.length >= 6) {
const property = ancestors[3]
const importExpression = ancestors[ancestors.length - 2]
if (
importExpression.type === 'ImportExpression'
&& property.type === 'Property'
&& ancestors[2].type === 'ObjectExpression'
&& ancestors[1].type === 'ExportDefaultDeclaration'
&& ancestors[0].type === 'Program'
) {
const { key } = property
if (key.type === 'Literal' && typeof key.value === 'string') {
imports.set(key.value, node.value)
}
}
}
}
},
})
return imports
}
// Get the correct entrypoint for the package
// The node ecosystem is a mess and I hate it
const getEsmExport = (name) => {
const defaultExport = fileURLToPath(import.meta.resolve(name))
const root = findRoot(defaultExport)
const packageJson = JSON.parse(fs.readFileSync(`${root}/package.json`, 'utf-8'))
const exports = resolve.exports(packageJson, '.', { browser: true })
const legacyExport = resolve.legacy(packageJson)
const relativePath = ((exports?.length ? exports[0] : legacyExport) ?? defaultExport)
return path.resolve(root, relativePath)
}
return {
name: 'vite-plugin-shared-deps',
config(config) {
const dependencyNames = Object.keys(dependencies)
const rollupEntries = Object.fromEntries(
dependencyNames.flatMap((name) => {
if (name in subpaths) {
return subpaths[name].map((subpath) => {
const path = [name, subpath.replace(/^\.\/?/, '')].filter(Boolean).join('/')
return [path, getEsmExport(path)]
})
}
else {
return [[name, getEsmExport(name)]]
}
}),
)
return mergeConfig(config, {
optimizeDeps: {
include: externals,
},
build: {
rollupOptions: {
input: rollupEntries,
},
},
})
},
// 1. Resolve virtual import to a special id that will be skipped by other loaders
resolveId(id) {
return id === virtualModuleId ? resolvedVirtualModuleId : undefined
},
// 2. Create a virtual main file that has the imports of all external dependencies
// vite will rewrite the imports to point to the bundles it generates in dev mode
load(id) {
return id === resolvedVirtualModuleId ? getImportsText() : undefined
},
// 3. Add an additional endpoint to the dev server that will find the resolved url in the generated main file
// and redirect the request to that url
configureServer(server) {
server.middlewares.use('/shared-deps', async (req, res) => {
const baseHeaders = {
'Access-Control-Allow-Origin': '*',
}
const origin = server.resolvedUrls?.local[0]
if (!origin) {
res.writeHead(500, baseHeaders)
res.end()
return
}
const reqUrlParams = new URL(req.url, origin).searchParams
const reqImportPath = reqUrlParams.get('import')
if (!reqImportPath) {
res.writeHead(400, baseHeaders)
res.end()
return
}
const script = await fetch(new URL(virtualModuleId, origin)).then(response => response.text())
const imports = getImportsPaths(script)
const resolvedImportPath = imports.get(reqImportPath)
if (!resolvedImportPath) {
res.writeHead(404)
res.end()
return
}
res.writeHead(302, {
...baseHeaders,
// Redirect to the resolved url
Location: resolvedImportPath,
})
res.end()
})
},
}
}
{
name: 'vite-plugin-insert-importmaps',
transformIndexHtml(html, ctx) {
if (ctx.path !== '/index.html') {
return html
}
// buildImports should create the importmap that points to /shared-deps?import=<dep> or js file depending on command
const importmap = { imports: buildImports() }
const importmapJson = JSON.stringify(importmap)
return [{
tag: 'script',
attrs: {
type: 'importmap',
},
children: importmapJson,
}]
},
} |
Beta Was this translation helpful? Give feedback.
Ended up writing this plugin: