Skip to content

Commit ce6fd31

Browse files
committed
wip: ssr esm support
1 parent a6e3078 commit ce6fd31

File tree

11 files changed

+117
-37
lines changed

11 files changed

+117
-37
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import cjs from 'cjs-package'
2+
import { esm, cjsFromEsm } from 'esm-package'
3+
4+
export default { cjs, esm, cjsFromEsm }
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = 'cjs-dev'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = 'cjs-prod'
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "cjs-package",
3+
"version": "0.0.0",
4+
"exports": {
5+
".": {
6+
"development": "./index.dev.js",
7+
"default": "./index.js"
8+
}
9+
}
10+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import cjs from 'cjs-package'
2+
3+
export const esm = 'esm-dev'
4+
export const cjsFromEsm = cjs
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import cjs from 'cjs-package'
2+
3+
export const esm = 'esm-prod'
4+
export const cjsFromEsm = cjs
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "esm-package",
3+
"version": "0.0.0",
4+
"exports": {
5+
".": {
6+
"development": "./index.dev.mjs",
7+
"default": "./index.mjs"
8+
}
9+
}
10+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "test-ssr-esm",
3+
"private": true,
4+
"version": "0.0.0",
5+
"license": "MIT",
6+
"dependencies": {
7+
"cjs-package": "link:./cjs-package",
8+
"esm-package": "link:./esm-package",
9+
"vite": "link:../../vite"
10+
},
11+
"scripts": {
12+
"serve": "node server.mjs"
13+
}
14+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import path from 'path'
2+
import { fileURLToPath } from 'url'
3+
4+
import { install } from 'source-map-support'
5+
install()
6+
7+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
8+
9+
const vite = await import('vite')
10+
const server = await vite.createServer({
11+
root: path.join(__dirname, 'app'),
12+
// mode: 'production',
13+
ssr: {
14+
external: ['cjs-package', 'esm-package']
15+
},
16+
server: {
17+
middlewareMode: 'ssr'
18+
}
19+
})
20+
21+
const entryModule = await server.ssrLoadModule('/entry-server.jsx')
22+
console.log(entryModule.default)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* @type {import('vite').UserConfig}
3+
*/
4+
module.exports = {
5+
build: {
6+
minify: false
7+
}
8+
}

packages/vite/src/node/ssr/ssrModuleLoader.ts

Lines changed: 39 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import vm from 'vm'
2+
import fs from 'fs'
13
import path from 'path'
24
import { Module } from 'module'
35
import { ViteDevServer } from '..'
@@ -95,9 +97,7 @@ async function instantiateModule(
9597
extensions: ['.js', '.mjs', '.ts', '.jsx', '.tsx', '.json'],
9698
isBuild: true,
9799
isProduction,
98-
// Disable "module" condition.
99-
isRequire: true,
100-
mainFields: ['main'],
100+
mainFields: ['main', 'module'],
101101
root
102102
}
103103

@@ -108,7 +108,7 @@ async function instantiateModule(
108108
// account for multiple pending deps and duplicate imports.
109109
const pendingDeps: string[] = []
110110

111-
const ssrImport = async (dep: string) => {
111+
async function ssrImport(dep: string) {
112112
if (dep[0] !== '.' && dep[0] !== '/') {
113113
return nodeRequire(dep, mod.file, resolveOptions)
114114
}
@@ -128,7 +128,7 @@ async function instantiateModule(
128128
return moduleGraph.urlToModuleMap.get(dep)?.ssrModule
129129
}
130130

131-
const ssrDynamicImport = (dep: string) => {
131+
function ssrDynamicImport(dep: string) {
132132
// #3087 dynamic import vars is ignored at rewrite import path,
133133
// so here need process relative path
134134
if (dep[0] === '.') {
@@ -152,26 +152,25 @@ async function instantiateModule(
152152
}
153153

154154
const ssrImportMeta = { url }
155+
const ssrArguments = {
156+
global: context.global,
157+
[ssrModuleExportsKey]: ssrModule,
158+
[ssrImportMetaKey]: ssrImportMeta,
159+
[ssrImportKey]: ssrImport,
160+
[ssrDynamicImportKey]: ssrDynamicImport,
161+
[ssrExportAllKey]: ssrExportAll
162+
}
163+
155164
try {
156-
// eslint-disable-next-line @typescript-eslint/no-empty-function
157-
const AsyncFunction = async function () {}.constructor as typeof Function
158-
const initModule = new AsyncFunction(
159-
`global`,
160-
ssrModuleExportsKey,
161-
ssrImportMetaKey,
162-
ssrImportKey,
163-
ssrDynamicImportKey,
164-
ssrExportAllKey,
165-
result.code + `\n//# sourceURL=${mod.url}`
166-
)
167-
await initModule(
168-
context.global,
169-
ssrModule,
170-
ssrImportMeta,
171-
ssrImport,
172-
ssrDynamicImport,
173-
ssrExportAll
174-
)
165+
const ssrModuleImpl = `(0,async function(${Object.keys(ssrArguments)}){\n${
166+
result.code
167+
}\n})`
168+
const ssrModuleInit = vm.runInThisContext(ssrModuleImpl, {
169+
filename: mod.file || mod.url,
170+
columnOffset: 1,
171+
displayErrors: false
172+
})
173+
await ssrModuleInit(...Object.values(ssrArguments))
175174
} catch (e) {
176175
const stacktrace = ssrRewriteStacktrace(e.stack, moduleGraph)
177176
rebindErrorStacktrace(e, stacktrace)
@@ -188,17 +187,25 @@ async function instantiateModule(
188187
return Object.freeze(ssrModule)
189188
}
190189

191-
function nodeRequire(
190+
async function nodeRequire(
192191
id: string,
193192
importer: string | null,
194193
resolveOptions: InternalResolveOptions
195194
) {
196-
const loadModule = Module.createRequire(importer || resolveOptions.root + '/')
195+
const parentId = importer || resolveOptions.root + '/'
196+
let resolvedId: string | undefined
197+
198+
// Hook into `require` so that `resolveOptions` are respected.
199+
// Note: ESM-only dependencies don't use this hook at all.
197200
const unhookNodeResolve = hookNodeResolve(
198201
(nodeResolve) => (id, parent, isMain, options) => {
199202
if (id[0] === '.' || Module.builtinModules.includes(id)) {
200203
return nodeResolve(id, parent, isMain, options)
201204
}
205+
// No parent exists when an ESM package imports a CJS package.
206+
if (!parent) {
207+
return id
208+
}
202209
const resolved = tryNodeResolve(id, parent.id, resolveOptions, false)
203210
if (!resolved) {
204211
throw Error(`Cannot find module '${id}' imported from '${parent.id}'`)
@@ -207,19 +214,14 @@ function nodeRequire(
207214
}
208215
)
209216

210-
let mod: any
211217
try {
212-
mod = loadModule(id)
218+
// Resolve the import manually, to avoid the ESM resolver.
219+
resolvedId = fs.realpathSync.native(
220+
Module.createRequire(parentId).resolve(id)
221+
)
222+
// TypeScript transforms dynamic `import` so we must use eval.
223+
return await eval(`import("${resolvedId}")`)
213224
} finally {
214225
unhookNodeResolve()
215226
}
216-
217-
// rollup-style default import interop for cjs
218-
const defaultExport = mod.__esModule ? mod.default : mod
219-
return new Proxy(mod, {
220-
get(mod, prop) {
221-
if (prop === 'default') return defaultExport
222-
return mod[prop]
223-
}
224-
})
225227
}

0 commit comments

Comments
 (0)