Skip to content

Commit 93be84e

Browse files
authored
refactor(runtime): share more code between runtime and main bundle (#16063)
1 parent 8f74ce4 commit 93be84e

File tree

12 files changed

+143
-234
lines changed

12 files changed

+143
-234
lines changed

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

+4-16
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { pathToFileURL } from 'node:url'
22
import type { ModuleNode, TransformResult, ViteDevServer } from '..'
3-
import type { PackageCache } from '../packages'
43
import type { InternalResolveOptionsWithOverrideConditions } from '../plugins/resolve'
54
import { tryNodeResolve } from '../plugins/resolve'
65
import { isBuiltin, isExternalUrl, isFilePathESM } from '../utils'
@@ -9,14 +8,8 @@ import { unwrapId } from '../../shared/utils'
98
import {
109
SOURCEMAPPING_URL,
1110
VITE_RUNTIME_SOURCEMAPPING_SOURCE,
12-
VITE_RUNTIME_SOURCEMAPPING_URL,
1311
} from '../../shared/constants'
14-
15-
interface NodeImportResolveOptions
16-
extends InternalResolveOptionsWithOverrideConditions {
17-
legacyProxySsrExternalModules?: boolean
18-
packageCache?: PackageCache
19-
}
12+
import { genSourceMapUrl } from '../server/sourcemap'
2013

2114
export interface FetchModuleOptions {
2215
inlineSourceMap?: boolean
@@ -51,7 +44,7 @@ export async function fetchModule(
5144
} = server.config
5245
const overrideConditions = ssr.resolve?.externalConditions || []
5346

54-
const resolveOptions: NodeImportResolveOptions = {
47+
const resolveOptions: InternalResolveOptionsWithOverrideConditions = {
5548
mainFields: ['main'],
5649
conditions: [],
5750
overrideConditions: [...overrideConditions, 'production', 'development'],
@@ -62,8 +55,6 @@ export async function fetchModule(
6255
isProduction,
6356
root,
6457
ssrConfig: ssr,
65-
legacyProxySsrExternalModules:
66-
server.config.legacy?.proxySsrExternalModules,
6758
packageCache: server.config.packageCache,
6859
}
6960

@@ -148,13 +139,10 @@ function inlineSourceMap(
148139
if (OTHER_SOURCE_MAP_REGEXP.test(code))
149140
code = code.replace(OTHER_SOURCE_MAP_REGEXP, '')
150141

151-
const sourceMap = Buffer.from(
152-
JSON.stringify(processSourceMap?.(map) || map),
153-
'utf-8',
154-
).toString('base64')
142+
const sourceMap = processSourceMap?.(map) || map
155143
result.code = `${code.trimEnd()}\n//# sourceURL=${
156144
mod.id
157-
}\n${VITE_RUNTIME_SOURCEMAPPING_SOURCE}\n//# ${VITE_RUNTIME_SOURCEMAPPING_URL};base64,${sourceMap}\n`
145+
}\n${VITE_RUNTIME_SOURCEMAPPING_SOURCE}\n//# ${SOURCEMAPPING_URL}=${genSourceMapUrl(sourceMap)}\n`
158146

159147
return result
160148
}

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

+3-11
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
11
import type { ViteDevServer } from '../server'
22
import type { FetchResult } from '../../runtime/types'
3+
import { asyncFunctionDeclarationPaddingLineCount } from '../../shared/utils'
34
import { fetchModule } from './fetchModule'
45

5-
// eslint-disable-next-line @typescript-eslint/no-empty-function
6-
const AsyncFunction = async function () {}.constructor as typeof Function
7-
const fnDeclarationLineCount = (() => {
8-
const body = '/*code*/'
9-
const source = new AsyncFunction('a', 'b', body).toString()
10-
return source.slice(0, source.indexOf(body)).split('\n').length - 1
11-
})()
12-
136
export function ssrFetchModule(
147
server: ViteDevServer,
158
id: string,
@@ -19,9 +12,8 @@ export function ssrFetchModule(
1912
processSourceMap(map) {
2013
// this assumes that "new AsyncFunction" is used to create the module
2114
return Object.assign({}, map, {
22-
// currently we need to offset the line
23-
// https://github.com/nodejs/node/issues/43047#issuecomment-1180632750
24-
mappings: ';'.repeat(fnDeclarationLineCount) + map.mappings,
15+
mappings:
16+
';'.repeat(asyncFunctionDeclarationPaddingLineCount) + map.mappings,
2517
})
2618
},
2719
})

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

+20-96
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,17 @@ import { transformRequest } from '../server/transformRequest'
77
import type { InternalResolveOptionsWithOverrideConditions } from '../plugins/resolve'
88
import { tryNodeResolve } from '../plugins/resolve'
99
import { genSourceMapUrl } from '../server/sourcemap'
10-
import type { PackageCache } from '../packages'
11-
import { unwrapId } from '../../shared/utils'
10+
import {
11+
AsyncFunction,
12+
asyncFunctionDeclarationPaddingLineCount,
13+
unwrapId,
14+
} from '../../shared/utils'
15+
import {
16+
type SSRImportBaseMetadata,
17+
analyzeImportedModDifference,
18+
proxyGuardOnlyEsm,
19+
} from '../../shared/ssrTransform'
20+
import { SOURCEMAPPING_URL } from '../../shared/constants'
1221
import {
1322
ssrDynamicImportKey,
1423
ssrExportAllKey,
@@ -27,31 +36,6 @@ type SSRModule = Record<string, any>
2736
interface NodeImportResolveOptions
2837
extends InternalResolveOptionsWithOverrideConditions {
2938
legacyProxySsrExternalModules?: boolean
30-
packageCache?: PackageCache
31-
}
32-
33-
interface SSRImportMetadata {
34-
isDynamicImport?: boolean
35-
/**
36-
* Imported names before being transformed to `ssrImportKey`
37-
*
38-
* import foo, { bar as baz, qux } from 'hello'
39-
* => ['default', 'bar', 'qux']
40-
*
41-
* import * as namespace from 'world
42-
* => undefined
43-
*/
44-
importedNames?: string[]
45-
}
46-
47-
// eslint-disable-next-line @typescript-eslint/no-empty-function
48-
const AsyncFunction = async function () {}.constructor as typeof Function
49-
let fnDeclarationLineCount = 0
50-
{
51-
const body = '/*code*/'
52-
const source = new AsyncFunction('a', 'b', body).toString()
53-
fnDeclarationLineCount =
54-
source.slice(0, source.indexOf(body)).split('\n').length - 1
5539
}
5640

5741
const pendingModules = new Map<string, Promise<SSRModule>>()
@@ -165,7 +149,7 @@ async function instantiateModule(
165149
// account for multiple pending deps and duplicate imports.
166150
const pendingDeps: string[] = []
167151

168-
const ssrImport = async (dep: string, metadata?: SSRImportMetadata) => {
152+
const ssrImport = async (dep: string, metadata?: SSRImportBaseMetadata) => {
169153
try {
170154
if (dep[0] !== '.' && dep[0] !== '/') {
171155
return await nodeImport(dep, mod.file!, resolveOptions, metadata)
@@ -227,12 +211,11 @@ async function instantiateModule(
227211
let sourceMapSuffix = ''
228212
if (result.map && 'version' in result.map) {
229213
const moduleSourceMap = Object.assign({}, result.map, {
230-
// currently we need to offset the line
231-
// https://github.com/nodejs/node/issues/43047#issuecomment-1180632750
232-
mappings: ';'.repeat(fnDeclarationLineCount) + result.map.mappings,
214+
mappings:
215+
';'.repeat(asyncFunctionDeclarationPaddingLineCount) +
216+
result.map.mappings,
233217
})
234-
sourceMapSuffix =
235-
'\n//# sourceMappingURL=' + genSourceMapUrl(moduleSourceMap)
218+
sourceMapSuffix = `\n//# ${SOURCEMAPPING_URL}=${genSourceMapUrl(moduleSourceMap)}`
236219
}
237220

238221
try {
@@ -289,7 +272,7 @@ async function nodeImport(
289272
id: string,
290273
importer: string,
291274
resolveOptions: NodeImportResolveOptions,
292-
metadata?: SSRImportMetadata,
275+
metadata?: SSRImportBaseMetadata,
293276
) {
294277
let url: string
295278
let filePath: string | undefined
@@ -322,10 +305,11 @@ async function nodeImport(
322305
} else if (filePath) {
323306
analyzeImportedModDifference(
324307
mod,
325-
filePath,
326308
id,
309+
isFilePathESM(filePath, resolveOptions.packageCache)
310+
? 'module'
311+
: undefined,
327312
metadata,
328-
resolveOptions.packageCache,
329313
)
330314
return proxyGuardOnlyEsm(mod, id)
331315
} else {
@@ -358,63 +342,3 @@ function proxyESM(mod: any) {
358342
function isPrimitive(value: any) {
359343
return !value || (typeof value !== 'object' && typeof value !== 'function')
360344
}
361-
362-
/**
363-
* Vite converts `import { } from 'foo'` to `const _ = __vite_ssr_import__('foo')`.
364-
* Top-level imports and dynamic imports work slightly differently in Node.js.
365-
* This function normalizes the differences so it matches prod behaviour.
366-
*/
367-
function analyzeImportedModDifference(
368-
mod: any,
369-
filePath: string,
370-
rawId: string,
371-
metadata?: SSRImportMetadata,
372-
packageCache?: PackageCache,
373-
) {
374-
// No normalization needed if the user already dynamic imports this module
375-
if (metadata?.isDynamicImport) return
376-
// If file path is ESM, everything should be fine
377-
if (isFilePathESM(filePath, packageCache)) return
378-
379-
// For non-ESM, named imports is done via static analysis with cjs-module-lexer in Node.js.
380-
// If the user named imports a specifier that can't be analyzed, error.
381-
if (metadata?.importedNames?.length) {
382-
const missingBindings = metadata.importedNames.filter((s) => !(s in mod))
383-
if (missingBindings.length) {
384-
const lastBinding = missingBindings[missingBindings.length - 1]
385-
// Copied from Node.js
386-
throw new SyntaxError(`\
387-
[vite] Named export '${lastBinding}' not found. The requested module '${rawId}' is a CommonJS module, which may not support all module.exports as named exports.
388-
CommonJS modules can always be imported via the default export, for example using:
389-
390-
import pkg from '${rawId}';
391-
const {${missingBindings.join(', ')}} = pkg;
392-
`)
393-
}
394-
}
395-
}
396-
397-
/**
398-
* Guard invalid named exports only, similar to how Node.js errors for top-level imports.
399-
* But since we transform as dynamic imports, we need to emulate the error manually.
400-
*/
401-
function proxyGuardOnlyEsm(
402-
mod: any,
403-
rawId: string,
404-
metadata?: SSRImportMetadata,
405-
) {
406-
// If the module doesn't import anything explicitly, e.g. `import 'foo'` or
407-
// `import * as foo from 'foo'`, we can skip the proxy guard.
408-
if (!metadata?.importedNames?.length) return mod
409-
410-
return new Proxy(mod, {
411-
get(mod, prop) {
412-
if (prop !== 'then' && !(prop in mod)) {
413-
throw new SyntaxError(
414-
`[vite] The requested module '${rawId}' does not provide an export named '${prop.toString()}'`,
415-
)
416-
}
417-
return mod[prop]
418-
},
419-
})
420-
}

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

+1-13
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { parseAstAsync as rollupParseAstAsync } from 'rollup/parseAst'
1616
import type { TransformResult } from '../server/transformRequest'
1717
import { combineSourcemaps, isDefined } from '../utils'
1818
import { isJSONRequest } from '../plugins/json'
19+
import type { DefineImportMetadata } from '../../shared/ssrTransform'
1920

2021
type Node = _Node & {
2122
start: number
@@ -28,19 +29,6 @@ interface TransformOptions {
2829
}
2930
}
3031

31-
interface DefineImportMetadata {
32-
/**
33-
* Imported names of an import statement, e.g.
34-
*
35-
* import foo, { bar as baz, qux } from 'hello'
36-
* => ['default', 'bar', 'qux']
37-
*
38-
* import * as namespace from 'world
39-
* => undefined
40-
*/
41-
importedNames?: string[]
42-
}
43-
4432
export const ssrModuleExportsKey = `__vite_ssr_exports__`
4533
export const ssrImportKey = `__vite_ssr_import__`
4634
export const ssrDynamicImportKey = `__vite_ssr_dynamic_import__`

packages/vite/src/runtime/esmRunner.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { AsyncFunction } from '../shared/utils'
12
import {
23
ssrDynamicImportKey,
34
ssrExportAllKey,
@@ -7,9 +8,6 @@ import {
78
} from './constants'
89
import type { ViteModuleRunner, ViteRuntimeModuleContext } from './types'
910

10-
// eslint-disable-next-line @typescript-eslint/no-empty-function
11-
const AsyncFunction = async function () {}.constructor as typeof Function
12-
1311
export class ESModulesRunner implements ViteModuleRunner {
1412
async runViteModule(
1513
context: ViteRuntimeModuleContext,

packages/vite/src/runtime/moduleCache.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { isWindows, withTrailingSlash } from '../shared/utils'
2-
import { VITE_RUNTIME_SOURCEMAPPING_URL } from '../shared/constants'
1+
import { isWindows, slash, withTrailingSlash } from '../shared/utils'
2+
import { SOURCEMAPPING_URL } from '../shared/constants'
33
import { decodeBase64 } from './utils'
44
import { DecodedMap } from './sourcemap/decoder'
55
import type { ModuleCache } from './types'
66

77
const VITE_RUNTIME_SOURCEMAPPING_REGEXP = new RegExp(
8-
`//# ${VITE_RUNTIME_SOURCEMAPPING_URL};base64,(.+)`,
8+
`//# ${SOURCEMAPPING_URL}=data:application/json;base64,(.+)`,
99
)
1010

1111
export class ModuleCacheMap extends Map<string, ModuleCache> {
@@ -180,8 +180,7 @@ function normalizeModuleId(file: string, root: string): string {
180180
if (prefixedBuiltins.has(file)) return file
181181

182182
// unix style, but Windows path still starts with the drive letter to check the root
183-
let unixFile = file
184-
.replace(/\\/g, '/')
183+
let unixFile = slash(file)
185184
.replace(/^\/@fs\//, isWindows ? '' : '/')
186185
.replace(/^node:/, '')
187186
.replace(/^\/+/, '/')

0 commit comments

Comments
 (0)