Skip to content

Commit

Permalink
refactor(runtime): share more code between runtime and main bundle (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
sapphi-red authored Mar 5, 2024
1 parent 8f74ce4 commit 93be84e
Show file tree
Hide file tree
Showing 12 changed files with 143 additions and 234 deletions.
20 changes: 4 additions & 16 deletions packages/vite/src/node/ssr/fetchModule.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { pathToFileURL } from 'node:url'
import type { ModuleNode, TransformResult, ViteDevServer } from '..'
import type { PackageCache } from '../packages'
import type { InternalResolveOptionsWithOverrideConditions } from '../plugins/resolve'
import { tryNodeResolve } from '../plugins/resolve'
import { isBuiltin, isExternalUrl, isFilePathESM } from '../utils'
Expand All @@ -9,14 +8,8 @@ import { unwrapId } from '../../shared/utils'
import {
SOURCEMAPPING_URL,
VITE_RUNTIME_SOURCEMAPPING_SOURCE,
VITE_RUNTIME_SOURCEMAPPING_URL,
} from '../../shared/constants'

interface NodeImportResolveOptions
extends InternalResolveOptionsWithOverrideConditions {
legacyProxySsrExternalModules?: boolean
packageCache?: PackageCache
}
import { genSourceMapUrl } from '../server/sourcemap'

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

const resolveOptions: NodeImportResolveOptions = {
const resolveOptions: InternalResolveOptionsWithOverrideConditions = {
mainFields: ['main'],
conditions: [],
overrideConditions: [...overrideConditions, 'production', 'development'],
Expand All @@ -62,8 +55,6 @@ export async function fetchModule(
isProduction,
root,
ssrConfig: ssr,
legacyProxySsrExternalModules:
server.config.legacy?.proxySsrExternalModules,
packageCache: server.config.packageCache,
}

Expand Down Expand Up @@ -148,13 +139,10 @@ function inlineSourceMap(
if (OTHER_SOURCE_MAP_REGEXP.test(code))
code = code.replace(OTHER_SOURCE_MAP_REGEXP, '')

const sourceMap = Buffer.from(
JSON.stringify(processSourceMap?.(map) || map),
'utf-8',
).toString('base64')
const sourceMap = processSourceMap?.(map) || map
result.code = `${code.trimEnd()}\n//# sourceURL=${
mod.id
}\n${VITE_RUNTIME_SOURCEMAPPING_SOURCE}\n//# ${VITE_RUNTIME_SOURCEMAPPING_URL};base64,${sourceMap}\n`
}\n${VITE_RUNTIME_SOURCEMAPPING_SOURCE}\n//# ${SOURCEMAPPING_URL}=${genSourceMapUrl(sourceMap)}\n`

return result
}
14 changes: 3 additions & 11 deletions packages/vite/src/node/ssr/ssrFetchModule.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
import type { ViteDevServer } from '../server'
import type { FetchResult } from '../../runtime/types'
import { asyncFunctionDeclarationPaddingLineCount } from '../../shared/utils'
import { fetchModule } from './fetchModule'

// eslint-disable-next-line @typescript-eslint/no-empty-function
const AsyncFunction = async function () {}.constructor as typeof Function
const fnDeclarationLineCount = (() => {
const body = '/*code*/'
const source = new AsyncFunction('a', 'b', body).toString()
return source.slice(0, source.indexOf(body)).split('\n').length - 1
})()

export function ssrFetchModule(
server: ViteDevServer,
id: string,
Expand All @@ -19,9 +12,8 @@ export function ssrFetchModule(
processSourceMap(map) {
// this assumes that "new AsyncFunction" is used to create the module
return Object.assign({}, map, {
// currently we need to offset the line
// https://github.com/nodejs/node/issues/43047#issuecomment-1180632750
mappings: ';'.repeat(fnDeclarationLineCount) + map.mappings,
mappings:
';'.repeat(asyncFunctionDeclarationPaddingLineCount) + map.mappings,
})
},
})
Expand Down
116 changes: 20 additions & 96 deletions packages/vite/src/node/ssr/ssrModuleLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,17 @@ import { transformRequest } from '../server/transformRequest'
import type { InternalResolveOptionsWithOverrideConditions } from '../plugins/resolve'
import { tryNodeResolve } from '../plugins/resolve'
import { genSourceMapUrl } from '../server/sourcemap'
import type { PackageCache } from '../packages'
import { unwrapId } from '../../shared/utils'
import {
AsyncFunction,
asyncFunctionDeclarationPaddingLineCount,
unwrapId,
} from '../../shared/utils'
import {
type SSRImportBaseMetadata,
analyzeImportedModDifference,
proxyGuardOnlyEsm,
} from '../../shared/ssrTransform'
import { SOURCEMAPPING_URL } from '../../shared/constants'
import {
ssrDynamicImportKey,
ssrExportAllKey,
Expand All @@ -27,31 +36,6 @@ type SSRModule = Record<string, any>
interface NodeImportResolveOptions
extends InternalResolveOptionsWithOverrideConditions {
legacyProxySsrExternalModules?: boolean
packageCache?: PackageCache
}

interface SSRImportMetadata {
isDynamicImport?: boolean
/**
* Imported names before being transformed to `ssrImportKey`
*
* import foo, { bar as baz, qux } from 'hello'
* => ['default', 'bar', 'qux']
*
* import * as namespace from 'world
* => undefined
*/
importedNames?: string[]
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
const AsyncFunction = async function () {}.constructor as typeof Function
let fnDeclarationLineCount = 0
{
const body = '/*code*/'
const source = new AsyncFunction('a', 'b', body).toString()
fnDeclarationLineCount =
source.slice(0, source.indexOf(body)).split('\n').length - 1
}

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

const ssrImport = async (dep: string, metadata?: SSRImportMetadata) => {
const ssrImport = async (dep: string, metadata?: SSRImportBaseMetadata) => {
try {
if (dep[0] !== '.' && dep[0] !== '/') {
return await nodeImport(dep, mod.file!, resolveOptions, metadata)
Expand Down Expand Up @@ -227,12 +211,11 @@ async function instantiateModule(
let sourceMapSuffix = ''
if (result.map && 'version' in result.map) {
const moduleSourceMap = Object.assign({}, result.map, {
// currently we need to offset the line
// https://github.com/nodejs/node/issues/43047#issuecomment-1180632750
mappings: ';'.repeat(fnDeclarationLineCount) + result.map.mappings,
mappings:
';'.repeat(asyncFunctionDeclarationPaddingLineCount) +
result.map.mappings,
})
sourceMapSuffix =
'\n//# sourceMappingURL=' + genSourceMapUrl(moduleSourceMap)
sourceMapSuffix = `\n//# ${SOURCEMAPPING_URL}=${genSourceMapUrl(moduleSourceMap)}`
}

try {
Expand Down Expand Up @@ -289,7 +272,7 @@ async function nodeImport(
id: string,
importer: string,
resolveOptions: NodeImportResolveOptions,
metadata?: SSRImportMetadata,
metadata?: SSRImportBaseMetadata,
) {
let url: string
let filePath: string | undefined
Expand Down Expand Up @@ -322,10 +305,11 @@ async function nodeImport(
} else if (filePath) {
analyzeImportedModDifference(
mod,
filePath,
id,
isFilePathESM(filePath, resolveOptions.packageCache)
? 'module'
: undefined,
metadata,
resolveOptions.packageCache,
)
return proxyGuardOnlyEsm(mod, id)
} else {
Expand Down Expand Up @@ -358,63 +342,3 @@ function proxyESM(mod: any) {
function isPrimitive(value: any) {
return !value || (typeof value !== 'object' && typeof value !== 'function')
}

/**
* Vite converts `import { } from 'foo'` to `const _ = __vite_ssr_import__('foo')`.
* Top-level imports and dynamic imports work slightly differently in Node.js.
* This function normalizes the differences so it matches prod behaviour.
*/
function analyzeImportedModDifference(
mod: any,
filePath: string,
rawId: string,
metadata?: SSRImportMetadata,
packageCache?: PackageCache,
) {
// No normalization needed if the user already dynamic imports this module
if (metadata?.isDynamicImport) return
// If file path is ESM, everything should be fine
if (isFilePathESM(filePath, packageCache)) return

// For non-ESM, named imports is done via static analysis with cjs-module-lexer in Node.js.
// If the user named imports a specifier that can't be analyzed, error.
if (metadata?.importedNames?.length) {
const missingBindings = metadata.importedNames.filter((s) => !(s in mod))
if (missingBindings.length) {
const lastBinding = missingBindings[missingBindings.length - 1]
// Copied from Node.js
throw new SyntaxError(`\
[vite] Named export '${lastBinding}' not found. The requested module '${rawId}' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:
import pkg from '${rawId}';
const {${missingBindings.join(', ')}} = pkg;
`)
}
}
}

/**
* Guard invalid named exports only, similar to how Node.js errors for top-level imports.
* But since we transform as dynamic imports, we need to emulate the error manually.
*/
function proxyGuardOnlyEsm(
mod: any,
rawId: string,
metadata?: SSRImportMetadata,
) {
// If the module doesn't import anything explicitly, e.g. `import 'foo'` or
// `import * as foo from 'foo'`, we can skip the proxy guard.
if (!metadata?.importedNames?.length) return mod

return new Proxy(mod, {
get(mod, prop) {
if (prop !== 'then' && !(prop in mod)) {
throw new SyntaxError(
`[vite] The requested module '${rawId}' does not provide an export named '${prop.toString()}'`,
)
}
return mod[prop]
},
})
}
14 changes: 1 addition & 13 deletions packages/vite/src/node/ssr/ssrTransform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { parseAstAsync as rollupParseAstAsync } from 'rollup/parseAst'
import type { TransformResult } from '../server/transformRequest'
import { combineSourcemaps, isDefined } from '../utils'
import { isJSONRequest } from '../plugins/json'
import type { DefineImportMetadata } from '../../shared/ssrTransform'

type Node = _Node & {
start: number
Expand All @@ -28,19 +29,6 @@ interface TransformOptions {
}
}

interface DefineImportMetadata {
/**
* Imported names of an import statement, e.g.
*
* import foo, { bar as baz, qux } from 'hello'
* => ['default', 'bar', 'qux']
*
* import * as namespace from 'world
* => undefined
*/
importedNames?: string[]
}

export const ssrModuleExportsKey = `__vite_ssr_exports__`
export const ssrImportKey = `__vite_ssr_import__`
export const ssrDynamicImportKey = `__vite_ssr_dynamic_import__`
Expand Down
4 changes: 1 addition & 3 deletions packages/vite/src/runtime/esmRunner.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AsyncFunction } from '../shared/utils'
import {
ssrDynamicImportKey,
ssrExportAllKey,
Expand All @@ -7,9 +8,6 @@ import {
} from './constants'
import type { ViteModuleRunner, ViteRuntimeModuleContext } from './types'

// eslint-disable-next-line @typescript-eslint/no-empty-function
const AsyncFunction = async function () {}.constructor as typeof Function

export class ESModulesRunner implements ViteModuleRunner {
async runViteModule(
context: ViteRuntimeModuleContext,
Expand Down
9 changes: 4 additions & 5 deletions packages/vite/src/runtime/moduleCache.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { isWindows, withTrailingSlash } from '../shared/utils'
import { VITE_RUNTIME_SOURCEMAPPING_URL } from '../shared/constants'
import { isWindows, slash, withTrailingSlash } from '../shared/utils'
import { SOURCEMAPPING_URL } from '../shared/constants'
import { decodeBase64 } from './utils'
import { DecodedMap } from './sourcemap/decoder'
import type { ModuleCache } from './types'

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

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

// unix style, but Windows path still starts with the drive letter to check the root
let unixFile = file
.replace(/\\/g, '/')
let unixFile = slash(file)
.replace(/^\/@fs\//, isWindows ? '' : '/')
.replace(/^node:/, '')
.replace(/^\/+/, '/')
Expand Down
Loading

0 comments on commit 93be84e

Please sign in to comment.