Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(runtime): share more code between runtime and main bundle #16063

Merged
merged 5 commits into from Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 4 additions & 16 deletions packages/vite/src/node/ssr/fetchModule.ts
@@ -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`
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed charset=utf-8 part.
JSON should be always utf-8 and the charset in mime doesn't have any effect according to the RFC.

}\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
@@ -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
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
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
@@ -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
@@ -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