diff --git a/src/dev-bundler.ts b/src/dev-bundler.ts new file mode 100644 index 0000000..4638808 --- /dev/null +++ b/src/dev-bundler.ts @@ -0,0 +1,189 @@ +import { builtinModules } from 'module' +import * as vite from 'vite' +import { uniq, hashId } from './utils' + +interface TransformChunk { + id: string + code: string + deps: string[] + parents: string[] +} + +interface SSRTransformResult { + code: string + map: object + deps: string[] + dynamicDeps: string[] +} + +async function transformRequest (viteServer: vite.ViteDevServer, id) { + // Virtual modules start with `\0` + if (id && id.startsWith('/@id/__x00__')) { + id = '\0' + id.slice('/@id/__x00__'.length) + } + + if (id && id.startsWith('/@id/defaultexport')) { + id = id.slice('/@id/'.length) + } + + // Externals + if (builtinModules.includes(id)) { // || id.includes('node_modules')) { + return { + code: `(global, exports, importMeta, ssrImport, ssrDynamicImport, ssrExportAll) => { ssrExportAll(require('${id.replace(/^\/@fs/, '')}')) }`, + deps: [], + dynamicDeps: [] + } + } + + // Transform + const res: SSRTransformResult = await viteServer.transformRequest(id, { ssr: true }).catch((err) => { + // eslint-disable-next-line no-console + console.warn(`[SSR] Error transforming ${id}: ${err}`) + // console.error(err) + }) as SSRTransformResult || { code: '', map: {}, deps: [], dynamicDeps: [] } + + // Wrap into a vite module + const code = `async function (global, __vite_ssr_exports__, __vite_ssr_import_meta__, __vite_ssr_import__, __vite_ssr_dynamic_import__, __vite_ssr_exportAll__) { +const module = __createCJSModule__(__vite_ssr_exports__) +${res.code || '/* empty */'}; +}` + return { code, deps: res.deps || [], dynamicDeps: res.dynamicDeps || [] } +} + +async function transformRequestRecursive (viteServer: vite.ViteDevServer, id, parent = '', chunks: Record = {}) { + if (chunks[id]) { + chunks[id].parents.push(parent) + return + } + const res = await transformRequest(viteServer, id) + const deps = uniq([...res.deps, ...res.dynamicDeps]) + + chunks[id] = { + id, + code: res.code, + deps, + parents: [parent] + } as TransformChunk + for (const dep of deps) { + await transformRequestRecursive(viteServer, dep, id, chunks) + } + return Object.values(chunks) +} + +export async function bundleRequest (viteServer: vite.ViteDevServer, entryURL: string) { + const chunks = await transformRequestRecursive(viteServer, entryURL) + + const listIds = (ids: string[]) => ids.map(id => `// - ${id} (${hashId(id)})`).join('\n') + const chunksCode = chunks.map(chunk => ` +// -------------------- +// Request: ${chunk.id} +// Parents: \n${listIds(chunk.parents)} +// Dependencies: \n${listIds(chunk.deps)} +// -------------------- +const ${hashId(chunk.id)} = ${chunk.code} +`).join('\n') + + const manifestCode = 'const __modules__ = {\n' + + chunks.map(chunk => ` '${chunk.id}': ${hashId(chunk.id)}`).join(',\n') + '\n}' + + // https://github.com/vitejs/vite/blob/main/packages/vite/src/node/ssr/ssrModuleLoader.ts + const ssrModuleLoader = ` +const __pendingModules__ = new Map() +const __pendingImports__ = new Map() +const __ssrContext__ = { global: {} } + +function __ssrLoadModule__(url, urlStack = []) { + const pendingModule = __pendingModules__.get(url) + if (pendingModule) { return pendingModule } + const modulePromise = __instantiateModule__(url, urlStack) + __pendingModules__.set(url, modulePromise) + modulePromise.catch(() => { __pendingModules__.delete(url) }) + .finally(() => { __pendingModules__.delete(url) }) + return modulePromise +} + +function __createCJSModule__(exports) { + return { + get exports() { + if (!exports.default) + exports.default = {} + return exports.default + }, + set exports(v) { + exports.default = v + } + } +} + +async function __instantiateModule__(url, urlStack) { + const mod = __modules__[url] + if (mod.stubModule) { return mod.stubModule } + let stubModule = { [Symbol.toStringTag]: 'Module' } + Object.defineProperty(stubModule, '__esModule', { value: true }) + mod.stubModule = stubModule + const importMeta = { url, hot: { accept() {} } } + urlStack = urlStack.concat(url) + const isCircular = url => urlStack.includes(url) + const pendingDeps = [] + const ssrImport = async (dep) => { + // TODO: Handle externals if dep[0] !== '.' | '/' + if (!isCircular(dep) && !__pendingImports__.get(dep)?.some(isCircular)) { + pendingDeps.push(dep) + if (pendingDeps.length === 1) { + __pendingImports__.set(url, pendingDeps) + } + await __ssrLoadModule__(dep, urlStack) + if (pendingDeps.length === 1) { + __pendingImports__.delete(url) + } else { + pendingDeps.splice(pendingDeps.indexOf(dep), 1) + } + } + return __modules__[dep].stubModule + } + function ssrDynamicImport (dep) { + // TODO: Handle dynamic import starting with . relative to url + return ssrImport(dep) + } + + function ssrExportAll(sourceModule) { + for (const key in sourceModule) { + if (key !== 'default') { + try { + Object.defineProperty(stubModule, key, { + enumerable: true, + configurable: true, + get() { return sourceModule[key] } + }) + } catch (_err) { } + } + } + } + + await mod( + __ssrContext__.global, + stubModule, + importMeta, + ssrImport, + ssrDynamicImport, + ssrExportAll + ) + + // fix for cjs/esm misalignment + if (!('default' in stubModule)) { + stubModule.default = stubModule + } + + return stubModule +} +` + + const code = [ + chunksCode, + manifestCode, + ssrModuleLoader, + `module.exports = function (...args) { return __ssrLoadModule__('${entryURL}').then(i => i.default(...args)) }` + ].join('\n\n') + + return { code } +} diff --git a/src/server.ts b/src/server.ts index 9ffeb5b..98cc18b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,4 +1,3 @@ -import { builtinModules } from 'module' import { resolve } from 'pathe' import * as vite from 'vite' import { createVuePlugin } from 'vite-plugin-vue2' @@ -9,7 +8,7 @@ import { ViteBuildContext, ViteOptions } from './types' import { wpfs } from './utils/wpfs' import { jsxPlugin } from './plugins/jsx' import { generateDevSSRManifest } from './manifest' -import { uniq, hashId } from './utils' +import { bundleRequest } from './dev-bundler' export async function buildServer (ctx: ViteBuildContext) { // Workaround to disable HMR @@ -113,152 +112,3 @@ export async function buildServer (ctx: ViteBuildContext) { doBuild() }) } - -// ---- Vite Dev Bundler POC ---- - -interface TransformChunk { - id: string, - code: string, - deps: string[], - parents: string[] -} - -interface SSRTransformResult { - code: string, - map: object, - deps: string[] - dynamicDeps: string[] -} - -async function transformRequest (viteServer: vite.ViteDevServer, id) { - // Virtual modules start with `\0` - if (id && id.startsWith('/@id/__x00__')) { - id = '\0' + id.slice('/@id/__x00__'.length) - } - - if (id && id.startsWith('/@id/defaultexport')) { - id = id.slice('/@id/'.length) - } - - // Externals - if (builtinModules.includes(id)) { - return { - code: `() => require('${id}')`, - deps: [], - dynamicDeps: [] - } - } - - // Transform - const res: SSRTransformResult = await viteServer.transformRequest(id, { ssr: true }).catch((err) => { - // eslint-disable-next-line no-console - console.warn(`[SSR] Error transforming ${id}: ${err}`) - // console.error(err) - }) as SSRTransformResult || { code: '', map: {}, deps: [], dynamicDeps: [] } - - // Wrap into a vite module - const code = `async function () { -const exports = {} -const module = { exports } -const __vite_ssr_exports__ = exports; -const __vite_ssr_exportAll__ = __createViteSSRExportAll__(__vite_ssr_exports__) -${res.code || '/* empty */'}; -return module.exports; -}` - return { code, deps: res.deps || [], dynamicDeps: res.dynamicDeps || [] } -} - -async function transformRequestRecursive (viteServer: vite.ViteDevServer, id, parent = '', chunks: Record = {}) { - if (chunks[id]) { - chunks[id].parents.push(parent) - return - } - const res = await transformRequest(viteServer, id) - const deps = uniq([...res.deps, ...res.dynamicDeps]) - - chunks[id] = { - id, - code: res.code, - deps, - parents: [parent] - } as TransformChunk - for (const dep of deps) { - await transformRequestRecursive(viteServer, dep, id, chunks) - } - return Object.values(chunks) -} - -async function bundleRequest (viteServer: vite.ViteDevServer, id) { - const chunks = await transformRequestRecursive(viteServer, id) - - const listIds = ids => ids.map(id => `// - ${id} (${hashId(id)})`).join('\n') - const chunksCode = chunks.map(chunk => ` -// -------------------- -// Request: ${chunk.id} -// Parents: \n${listIds(chunk.parents)} -// Dependencies: \n${listIds(chunk.deps)} -// -------------------- -const ${hashId(chunk.id)} = ${chunk.code} -`).join('\n') - - const manifestCode = 'const $chunks = {\n' + - chunks.map(chunk => ` '${chunk.id}': ${hashId(chunk.id)}`).join(',\n') + '\n}' - - const dynamicImportCode = ` -const __vite_import_cache__ = Object.create({}) -function __vite_ssr_import__(id) { - if (!__vite_import_cache__[id]) { - __vite_import_cache__[id] = Promise.resolve($chunks[id]()) - .then(mod => { - if (mod && !('default' in mod)) { - mod.default = mod - } - return mod - }) - } - return __vite_import_cache__[id] -} -function __vite_ssr_dynamic_import__(id) { - return __vite_ssr_import__(id) -} -` - - // https://github.com/vitejs/vite/blob/fb406ce4c0fe6da3333c9d1c00477b2880d46352/packages/vite/src/node/ssr/ssrModuleLoader.ts#L121-L133 - const helpers = ` -function __createViteSSRExportAll__(ssrModule) { - return (sourceModule) => { - for (const key in sourceModule) { - if (key !== 'default') { - Object.defineProperty(ssrModule, key, { - enumerable: true, - configurable: true, - get() { - return sourceModule[key] - } - }) - } - } - } -} -` - - // TODO: implement real HMR - const metaPolyfill = ` -const __vite_ssr_import_meta__ = { - hot: { - accept() {} - } -} -` - - const code = [ - metaPolyfill, - chunksCode, - manifestCode, - dynamicImportCode, - helpers, - `module.exports = function (...args) { return ${hashId(id)}().then(r => r.default(...args)) }` - ].join('\n\n') - - return { code } -}