Skip to content

Commit 62835f7

Browse files
authored
feat(ssr): support import.meta.resolve in module runner (#20260)
1 parent 4be5270 commit 62835f7

File tree

12 files changed

+171
-41
lines changed

12 files changed

+171
-41
lines changed

docs/guide/api-environment-runtimes.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,12 +153,17 @@ Module runner exposes `import` method. When Vite server triggers `full-reload` H
153153
**Example Usage:**
154154

155155
```js
156-
import { ModuleRunner, ESModulesEvaluator } from 'vite/module-runner'
156+
import {
157+
ModuleRunner,
158+
ESModulesEvaluator,
159+
createNodeImportMeta,
160+
} from 'vite/module-runner'
157161
import { transport } from './rpc-implementation.js'
158162

159163
const moduleRunner = new ModuleRunner(
160164
{
161165
transport,
166+
createImportMeta: createNodeImportMeta, // if the module runner runs in Node.js
162167
},
163168
new ESModulesEvaluator(),
164169
)
@@ -278,7 +283,11 @@ You need to couple it with the `HotChannel` instance on the server like in this
278283
```js [worker.js]
279284
import { parentPort } from 'node:worker_threads'
280285
import { fileURLToPath } from 'node:url'
281-
import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner'
286+
import {
287+
ESModulesEvaluator,
288+
ModuleRunner,
289+
createNodeImportMeta,
290+
} from 'vite/module-runner'
282291

283292
/** @type {import('vite/module-runner').ModuleRunnerTransport} */
284293
const transport = {
@@ -294,6 +303,7 @@ const transport = {
294303
const runner = new ModuleRunner(
295304
{
296305
transport,
306+
createImportMeta: createNodeImportMeta,
297307
},
298308
new ESModulesEvaluator(),
299309
)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { isWindows } from '../shared/utils'
2+
import {
3+
type ImportMetaResolver,
4+
createImportMetaResolver,
5+
} from './importMetaResolver'
6+
import type { ModuleRunnerImportMeta } from './types'
7+
import { posixDirname, posixPathToFileHref, toWindowsPath } from './utils'
8+
9+
const envProxy = new Proxy({} as any, {
10+
get(_, p) {
11+
throw new Error(
12+
`[module runner] Dynamic access of "import.meta.env" is not supported. Please, use "import.meta.env.${String(p)}" instead.`,
13+
)
14+
},
15+
})
16+
17+
export function createDefaultImportMeta(
18+
modulePath: string,
19+
): ModuleRunnerImportMeta {
20+
const href = posixPathToFileHref(modulePath)
21+
const filename = modulePath
22+
const dirname = posixDirname(modulePath)
23+
return {
24+
filename: isWindows ? toWindowsPath(filename) : filename,
25+
dirname: isWindows ? toWindowsPath(dirname) : dirname,
26+
url: href,
27+
env: envProxy,
28+
resolve(_id: string, _parent?: string) {
29+
throw new Error('[module runner] "import.meta.resolve" is not supported.')
30+
},
31+
// should be replaced during transformation
32+
glob() {
33+
throw new Error(
34+
`[module runner] "import.meta.glob" is statically replaced during ` +
35+
`file transformation. Make sure to reference it by the full name.`,
36+
)
37+
},
38+
}
39+
}
40+
41+
let importMetaResolverCache: Promise<ImportMetaResolver | undefined> | undefined
42+
43+
/**
44+
* Create import.meta object for Node.js.
45+
*/
46+
export async function createNodeImportMeta(
47+
modulePath: string,
48+
): Promise<ModuleRunnerImportMeta> {
49+
const defaultMeta = createDefaultImportMeta(modulePath)
50+
const href = defaultMeta.url
51+
52+
importMetaResolverCache ??= createImportMetaResolver()
53+
const importMetaResolver = await importMetaResolverCache
54+
55+
return {
56+
...defaultMeta,
57+
resolve(id: string, parent?: string) {
58+
const resolver = importMetaResolver ?? defaultMeta.resolve
59+
return resolver(id, parent ?? href)
60+
},
61+
}
62+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
export type ImportMetaResolver = (specifier: string, importer: string) => string
2+
3+
const customizationHookNamespace = 'vite-module-runner:import-meta-resolve/v1/'
4+
const customizationHooksModule = /* js */ `
5+
6+
export async function resolve(specifier, context, nextResolve) {
7+
if (specifier.startsWith(${JSON.stringify(customizationHookNamespace)})) {
8+
const data = specifier.slice(${JSON.stringify(customizationHookNamespace)}.length)
9+
const [parsedSpecifier, parsedImporter] = JSON.parse(data)
10+
specifier = parsedSpecifier
11+
context.parentURL = parsedImporter
12+
}
13+
14+
return nextResolve(specifier, context)
15+
}
16+
17+
`
18+
19+
export async function createImportMetaResolver(): Promise<
20+
ImportMetaResolver | undefined
21+
> {
22+
let module: typeof import('node:module')
23+
try {
24+
module = (await import('node:module')).Module
25+
} catch {
26+
return
27+
}
28+
// `module.Module` may be `undefined` when `node:module` is mocked
29+
if (!module?.register) {
30+
return
31+
}
32+
33+
try {
34+
const hookModuleContent = `data:text/javascript,${encodeURI(customizationHooksModule)}`
35+
module.register(hookModuleContent)
36+
} catch (e) {
37+
// For `--experimental-network-imports` flag that exists in Node before v22
38+
if ('code' in e && e.code === 'ERR_NETWORK_IMPORT_DISALLOWED') {
39+
return
40+
}
41+
throw e
42+
}
43+
44+
return (specifier: string, importer: string) =>
45+
import.meta.resolve(
46+
`${customizationHookNamespace}${JSON.stringify([specifier, importer])}`,
47+
)
48+
}

packages/vite/src/module-runner/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ export {
77
} from './evaluatedModules'
88
export { ModuleRunner } from './runner'
99
export { ESModulesEvaluator } from './esmEvaluator'
10+
export {
11+
createDefaultImportMeta,
12+
createNodeImportMeta,
13+
} from './createImportMeta'
1014

1115
export { createWebSocketModuleRunnerTransport } from '../shared/moduleRunnerTransport'
1216

packages/vite/src/module-runner/runner.ts

Lines changed: 7 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ViteHotContext } from 'types/hot'
22
import { HMRClient, HMRContext, type HMRLogger } from '../shared/hmr'
3-
import { cleanUrl, isPrimitive, isWindows } from '../shared/utils'
3+
import { cleanUrl, isPrimitive } from '../shared/utils'
44
import { analyzeImportedModDifference } from '../shared/ssrTransform'
55
import {
66
type NormalizedModuleRunnerTransport,
@@ -11,17 +11,11 @@ import { EvaluatedModules } from './evaluatedModules'
1111
import type {
1212
ModuleEvaluator,
1313
ModuleRunnerContext,
14-
ModuleRunnerImportMeta,
1514
ModuleRunnerOptions,
1615
ResolvedResult,
1716
SSRImportMetadata,
1817
} from './types'
19-
import {
20-
posixDirname,
21-
posixPathToFileHref,
22-
posixResolve,
23-
toWindowsPath,
24-
} from './utils'
18+
import { posixDirname, posixPathToFileHref, posixResolve } from './utils'
2519
import {
2620
ssrDynamicImportKey,
2721
ssrExportAllKey,
@@ -34,6 +28,7 @@ import { hmrLogger, silentConsole } from './hmrLogger'
3428
import { createHMRHandlerForRunner } from './hmrHandler'
3529
import { enableSourceMapSupport } from './sourcemap/index'
3630
import { ESModulesEvaluator } from './esmEvaluator'
31+
import { createDefaultImportMeta } from './createImportMeta'
3732

3833
interface ModuleRunnerDebugger {
3934
(formatter: unknown, ...args: unknown[]): void
@@ -43,13 +38,6 @@ export class ModuleRunner {
4338
public evaluatedModules: EvaluatedModules
4439
public hmrClient?: HMRClient
4540

46-
private readonly envProxy = new Proxy({} as any, {
47-
get(_, p) {
48-
throw new Error(
49-
`[module runner] Dynamic access of "import.meta.env" is not supported. Please, use "import.meta.env.${String(p)}" instead.`,
50-
)
51-
},
52-
})
5341
private readonly transport: NormalizedModuleRunnerTransport
5442
private readonly resetSourceMapSupport?: () => void
5543
private readonly concurrentModuleNodePromises = new Map<
@@ -351,29 +339,13 @@ export class ModuleRunner {
351339
)
352340
}
353341

342+
const createImportMeta =
343+
this.options.createImportMeta ?? createDefaultImportMeta
344+
354345
const modulePath = cleanUrl(file || moduleId)
355346
// disambiguate the `<UNIT>:/` on windows: see nodejs/node#31710
356347
const href = posixPathToFileHref(modulePath)
357-
const filename = modulePath
358-
const dirname = posixDirname(modulePath)
359-
const meta: ModuleRunnerImportMeta = {
360-
filename: isWindows ? toWindowsPath(filename) : filename,
361-
dirname: isWindows ? toWindowsPath(dirname) : dirname,
362-
url: href,
363-
env: this.envProxy,
364-
resolve(_id, _parent?) {
365-
throw new Error(
366-
'[module runner] "import.meta.resolve" is not supported.',
367-
)
368-
},
369-
// should be replaced during transformation
370-
glob() {
371-
throw new Error(
372-
`[module runner] "import.meta.glob" is statically replaced during ` +
373-
`file transformation. Make sure to reference it by the full name.`,
374-
)
375-
},
376-
}
348+
const meta = await createImportMeta(modulePath)
377349
const exports = Object.create(null)
378350
Object.defineProperty(exports, Symbol.toStringTag, {
379351
value: 'Module',

packages/vite/src/module-runner/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,14 @@ export interface ModuleRunnerOptions {
105105
* @default true
106106
*/
107107
hmr?: boolean | ModuleRunnerHmr
108+
/**
109+
* Create import.meta object for the module.
110+
*
111+
* @default createDefaultImportMeta
112+
*/
113+
createImportMeta?: (
114+
modulePath: string,
115+
) => ModuleRunnerImportMeta | Promise<ModuleRunnerImportMeta>
108116
/**
109117
* Custom module cache. If not provided, creates a separate module cache for each ModuleRunner instance.
110118
*/

packages/vite/src/node/ssr/runtime/__tests__/fixtures/worker.invoke.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @ts-check
22

33
import { BroadcastChannel, parentPort } from 'node:worker_threads'
4-
import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner'
4+
import { ESModulesEvaluator, ModuleRunner, createNodeImportMeta } from 'vite/module-runner'
55
import { createBirpc } from 'birpc'
66

77
if (!parentPort) {
@@ -27,6 +27,7 @@ const runner = new ModuleRunner(
2727
return rpc.invoke(data)
2828
},
2929
},
30+
createImportMeta: createNodeImportMeta,
3031
hmr: false,
3132
},
3233
new ESModulesEvaluator(),

packages/vite/src/node/ssr/runtime/__tests__/fixtures/worker.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @ts-check
22

33
import { BroadcastChannel, parentPort } from 'node:worker_threads'
4-
import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner'
4+
import { ESModulesEvaluator, ModuleRunner, createNodeImportMeta } from 'vite/module-runner'
55

66
if (!parentPort) {
77
throw new Error('File "worker.js" must be run in a worker thread')
@@ -24,6 +24,7 @@ const messagePortTransport = {
2424
const runner = new ModuleRunner(
2525
{
2626
transport: messagePortTransport,
27+
createImportMeta: createNodeImportMeta,
2728
},
2829
new ESModulesEvaluator(),
2930
)

packages/vite/src/node/ssr/runtime/serverModuleRunner.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { existsSync, readFileSync } from 'node:fs'
22
import type { HotPayload } from 'types/hmrPayload'
3-
import { ModuleRunner } from 'vite/module-runner'
3+
import { ModuleRunner, createNodeImportMeta } from 'vite/module-runner'
44
import type {
55
ModuleEvaluator,
66
ModuleRunnerHmr,
@@ -130,6 +130,7 @@ export function createServerModuleRunner(
130130
channel: environment.hot as NormalizedServerHotChannel,
131131
}),
132132
hmr,
133+
createImportMeta: createNodeImportMeta,
133134
sourcemapInterceptor: resolveSourceMapOptions(options),
134135
},
135136
options.evaluator,

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import colors from 'picocolors'
22
import type { EvaluatedModuleNode } from 'vite/module-runner'
3-
import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner'
3+
import {
4+
ESModulesEvaluator,
5+
ModuleRunner,
6+
createNodeImportMeta,
7+
} from 'vite/module-runner'
48
import type { ViteDevServer } from '../server'
59
import { unwrapId } from '../../shared/utils'
610
import type { DevEnvironment } from '../server/environment'
@@ -69,6 +73,7 @@ class SSRCompatModuleRunner extends ModuleRunner {
6973
transport: createServerModuleRunnerTransport({
7074
channel: environment.hot as NormalizedServerHotChannel,
7175
}),
76+
createImportMeta: createNodeImportMeta,
7277
sourcemapInterceptor: false,
7378
hmr: false,
7479
},

0 commit comments

Comments
 (0)