Skip to content

Commit

Permalink
wip: ssr esm support
Browse files Browse the repository at this point in the history
  • Loading branch information
aleclarson committed Jul 30, 2021
1 parent a6e3078 commit 6a279e8
Show file tree
Hide file tree
Showing 11 changed files with 116 additions and 37 deletions.
4 changes: 4 additions & 0 deletions packages/playground/ssr-esm/app/entry-server.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import cjs from 'cjs-package'
import { esm, cjsFromEsm } from 'esm-package'

export default { cjs, esm, cjsFromEsm }
1 change: 1 addition & 0 deletions packages/playground/ssr-esm/cjs-package/index.dev.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 'cjs-dev'
1 change: 1 addition & 0 deletions packages/playground/ssr-esm/cjs-package/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 'cjs-prod'
10 changes: 10 additions & 0 deletions packages/playground/ssr-esm/cjs-package/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "cjs-package",
"version": "0.0.0",
"exports": {
".": {
"development": "./index.dev.js",
"default": "./index.js"
}
}
}
4 changes: 4 additions & 0 deletions packages/playground/ssr-esm/esm-package/index.dev.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import cjs from 'cjs-package'

export const esm = 'esm-dev'
export const cjsFromEsm = cjs
4 changes: 4 additions & 0 deletions packages/playground/ssr-esm/esm-package/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import cjs from 'cjs-package'

export const esm = 'esm-prod'
export const cjsFromEsm = cjs
10 changes: 10 additions & 0 deletions packages/playground/ssr-esm/esm-package/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "esm-package",
"version": "0.0.0",
"exports": {
".": {
"development": "./index.dev.mjs",
"default": "./index.mjs"
}
}
}
14 changes: 14 additions & 0 deletions packages/playground/ssr-esm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "test-ssr-esm",
"private": true,
"version": "0.0.0",
"license": "MIT",
"dependencies": {
"cjs-package": "link:./cjs-package",
"esm-package": "link:./esm-package",
"vite": "link:../../vite"
},
"scripts": {
"serve": "node server.mjs"
}
}
22 changes: 22 additions & 0 deletions packages/playground/ssr-esm/server.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import path from 'path'
import { fileURLToPath } from 'url'

import { install } from 'source-map-support'
install()

const __dirname = path.dirname(fileURLToPath(import.meta.url))

const vite = await import('vite')
const server = await vite.createServer({
root: path.join(__dirname, 'app'),
// mode: 'production',
ssr: {
external: ['cjs-package', 'esm-package']
},
server: {
middlewareMode: 'ssr'
}
})

const entryModule = await server.ssrLoadModule('/entry-server.jsx')
console.log(entryModule.default)
8 changes: 8 additions & 0 deletions packages/playground/ssr-esm/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* @type {import('vite').UserConfig}
*/
module.exports = {
build: {
minify: false
}
}
75 changes: 38 additions & 37 deletions packages/vite/src/node/ssr/ssrModuleLoader.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import vm from 'vm'
import fs from 'fs'
import path from 'path'
import { Module } from 'module'
import { ViteDevServer } from '..'
Expand Down Expand Up @@ -95,9 +97,7 @@ async function instantiateModule(
extensions: ['.js', '.mjs', '.ts', '.jsx', '.tsx', '.json'],
isBuild: true,
isProduction,
// Disable "module" condition.
isRequire: true,
mainFields: ['main'],
mainFields: ['main', 'module'],
root
}

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

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

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

const ssrImportMeta = { url }
const ssrArguments = {
global: context.global,
[ssrModuleExportsKey]: ssrModule,
[ssrImportMetaKey]: ssrImportMeta,
[ssrImportKey]: ssrImport,
[ssrDynamicImportKey]: ssrDynamicImport,
[ssrExportAllKey]: ssrExportAll
}

try {
// eslint-disable-next-line @typescript-eslint/no-empty-function
const AsyncFunction = async function () {}.constructor as typeof Function
const initModule = new AsyncFunction(
`global`,
ssrModuleExportsKey,
ssrImportMetaKey,
ssrImportKey,
ssrDynamicImportKey,
ssrExportAllKey,
result.code + `\n//# sourceURL=${mod.url}`
)
await initModule(
context.global,
ssrModule,
ssrImportMeta,
ssrImport,
ssrDynamicImport,
ssrExportAll
)
const ssrModuleImpl = `(0,async function(${Object.keys(ssrArguments)}){\n${
result.code
}\n})`
const ssrModuleInit = vm.runInThisContext(ssrModuleImpl, {

This comment has been minimized.

Copy link
@aleclarson

aleclarson Jul 30, 2021

Author Member

Using vm here is not strictly required. We could keep doing the AsyncFunction hack (see the left side of the diff). But ESM will be supported by the vm library in the future (see vm.Module, currently behind a CLI flag). Also, avoiding new AsyncFunction is a good idea, since vm has better support for breakpoints (though, we'll need to merge #3928 for that).

filename: mod.file || mod.url,
columnOffset: 1,
displayErrors: false
})
await ssrModuleInit(...Object.values(ssrArguments))
} catch (e) {
const stacktrace = ssrRewriteStacktrace(e.stack, moduleGraph)
rebindErrorStacktrace(e, stacktrace)
Expand All @@ -188,17 +187,24 @@ async function instantiateModule(
return Object.freeze(ssrModule)
}

function nodeRequire(
async function nodeRequire(
id: string,
importer: string | null,
resolveOptions: InternalResolveOptions
) {
const loadModule = Module.createRequire(importer || resolveOptions.root + '/')
let resolvedId: string | undefined

// Hook into `require` so that `resolveOptions` are respected.
// Note: ESM-only dependencies don't use this hook at all.

This comment has been minimized.

Copy link
@aleclarson

aleclarson Jul 30, 2021

Author Member

Clarification: ESM-only dependencies do use this hook when importing a CJS package, but the id string is already resolved to an absolute path, so there's not much we can do. In the future, we could reverse-engineer the id into a bare import if we wanted to force resolution with tryNodeResolve. But still, this hook is never used when a ESM-only package imports an ESM dependency.

const unhookNodeResolve = hookNodeResolve(
(nodeResolve) => (id, parent, isMain, options) => {
if (id[0] === '.' || Module.builtinModules.includes(id)) {
return nodeResolve(id, parent, isMain, options)
}
// No parent exists when an ESM package imports a CJS package.
if (!parent) {
return id
}
const resolved = tryNodeResolve(id, parent.id, resolveOptions, false)
if (!resolved) {
throw Error(`Cannot find module '${id}' imported from '${parent.id}'`)
Expand All @@ -207,19 +213,14 @@ function nodeRequire(
}
)

let mod: any
try {
mod = loadModule(id)
// Resolve the import manually, to avoid the ESM resolver.
resolvedId = fs.realpathSync.native(
Module.createRequire(importer || resolveOptions.root + '/').resolve(id)
)
// TypeScript transforms dynamic `import` so we must use eval.
return await eval(`import("${resolvedId}")`)

This comment has been minimized.

Copy link
@aleclarson

aleclarson Jul 30, 2021

Author Member

We await the import promise in case the dependency is a CJS package. In that case, the require hook will be needed for proper support of resolve.dedupe and mode Vite options.

} finally {
unhookNodeResolve()
}

// rollup-style default import interop for cjs
const defaultExport = mod.__esModule ? mod.default : mod
return new Proxy(mod, {
get(mod, prop) {
if (prop === 'default') return defaultExport
return mod[prop]
}
})
}

0 comments on commit 6a279e8

Please sign in to comment.