From f6362b6455baaed0ce7eeeed0ae656259037f886 Mon Sep 17 00:00:00 2001 From: sun0day Date: Mon, 3 Jul 2023 22:53:44 +0800 Subject: [PATCH 01/14] fix(sourcemap): preserve original sourcesContent (#13662) --- packages/vite/src/node/plugins/css.ts | 2 +- .../src/node/plugins/importAnalysisBuild.ts | 9 ++++---- .../src/node/server/middlewares/indexHtml.ts | 2 +- packages/vite/src/node/server/sourcemap.ts | 21 ++++++++++++------- .../vite/src/node/server/transformRequest.ts | 2 +- packages/vite/src/node/ssr/ssrTransform.ts | 20 +++++++----------- packages/vite/src/node/utils.ts | 21 +++++++------------ 7 files changed, 37 insertions(+), 40 deletions(-) diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index c13683c43905e7..8d6bb21129580f 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -439,7 +439,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { const getContentWithSourcemap = async (content: string) => { if (config.css?.devSourcemap) { const sourcemap = this.getCombinedSourcemap() - if (sourcemap.mappings && !sourcemap.sourcesContent) { + if (sourcemap.mappings) { await injectSourcesContent(sourcemap, cleanUrl(id), config.logger) } return getCodeWithSourcemap('css', content, sourcemap) diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index 8e8482776d9c03..d74987a6bca258 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -652,11 +652,10 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { source: chunk.fileName, hires: true, }) - const map = combineSourcemaps( - chunk.fileName, - [nextMap as RawSourceMap, chunk.map as RawSourceMap], - false, - ) as SourceMap + const map = combineSourcemaps(chunk.fileName, [ + nextMap as RawSourceMap, + chunk.map as RawSourceMap, + ]) as SourceMap map.toUrl = () => genSourceMapUrl(map) chunk.map = map diff --git a/packages/vite/src/node/server/middlewares/indexHtml.ts b/packages/vite/src/node/server/middlewares/indexHtml.ts index 3948f997a3bb91..838d5a1bf7c0e3 100644 --- a/packages/vite/src/node/server/middlewares/indexHtml.ts +++ b/packages/vite/src/node/server/middlewares/indexHtml.ts @@ -283,7 +283,7 @@ const devHtmlHook: IndexHtmlTransformHook = async ( let content = '' if (result) { if (result.map) { - if (result.map.mappings && !result.map.sourcesContent) { + if (result.map.mappings) { await injectSourcesContent( result.map, proxyModulePath, diff --git a/packages/vite/src/node/server/sourcemap.ts b/packages/vite/src/node/server/sourcemap.ts index a2e8c55542fc8e..c8f2c8d88fdacd 100644 --- a/packages/vite/src/node/server/sourcemap.ts +++ b/packages/vite/src/node/server/sourcemap.ts @@ -33,22 +33,29 @@ export async function injectSourcesContent( } catch {} const missingSources: string[] = [] - map.sourcesContent = await Promise.all( - map.sources.map((sourcePath) => { + const sourcesContent = map.sourcesContent || [] + await Promise.all( + map.sources.map(async (sourcePath, index) => { + let content = null if (sourcePath && !virtualSourceRE.test(sourcePath)) { sourcePath = decodeURI(sourcePath) if (sourceRoot) { sourcePath = path.resolve(sourceRoot, sourcePath) } - return fsp.readFile(sourcePath, 'utf-8').catch(() => { - missingSources.push(sourcePath) - return null - }) + // inject content from source file when sourcesContent is null + content = + sourcesContent[index] ?? + (await fsp.readFile(sourcePath, 'utf-8').catch(() => { + missingSources.push(sourcePath) + return null + })) } - return null + sourcesContent[index] = content }), ) + map.sourcesContent = sourcesContent + // Use this command… // DEBUG="vite:sourcemap" vite build // …to log the missing sources. diff --git a/packages/vite/src/node/server/transformRequest.ts b/packages/vite/src/node/server/transformRequest.ts index 92f9abbc04a4fd..2c9c58c47d7ea3 100644 --- a/packages/vite/src/node/server/transformRequest.ts +++ b/packages/vite/src/node/server/transformRequest.ts @@ -285,7 +285,7 @@ async function loadAndTransform( if (map && mod.file) { map = (typeof map === 'string' ? JSON.parse(map) : map) as SourceMap - if (map.mappings && !map.sourcesContent) { + if (map.mappings) { await injectSourcesContent(map, mod.file, logger) } diff --git a/packages/vite/src/node/ssr/ssrTransform.ts b/packages/vite/src/node/ssr/ssrTransform.ts index ec15226ddb6f09..d582ee0ef25c26 100644 --- a/packages/vite/src/node/ssr/ssrTransform.ts +++ b/packages/vite/src/node/ssr/ssrTransform.ts @@ -276,18 +276,14 @@ async function ssrTransformScript( let map = s.generateMap({ hires: true }) if (inMap && inMap.mappings && inMap.sources.length > 0) { - map = combineSourcemaps( - url, - [ - { - ...map, - sources: inMap.sources, - sourcesContent: inMap.sourcesContent, - } as RawSourceMap, - inMap as RawSourceMap, - ], - false, - ) as SourceMap + map = combineSourcemaps(url, [ + { + ...map, + sources: inMap.sources, + sourcesContent: inMap.sourcesContent, + } as RawSourceMap, + inMap as RawSourceMap, + ]) as SourceMap } else { map.sources = [path.basename(url)] // needs to use originalCode instead of code diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index c1dbc05c204e46..1ac8617f6bc7ac 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -748,7 +748,6 @@ const nullSourceMap: RawSourceMap = { export function combineSourcemaps( filename: string, sourcemapList: Array, - excludeContent = true, ): RawSourceMap { if ( sourcemapList.length === 0 || @@ -778,19 +777,15 @@ export function combineSourcemaps( const useArrayInterface = sourcemapList.slice(0, -1).find((m) => m.sources.length !== 1) === undefined if (useArrayInterface) { - map = remapping(sourcemapList, () => null, excludeContent) + map = remapping(sourcemapList, () => null) } else { - map = remapping( - sourcemapList[0], - function loader(sourcefile) { - if (sourcefile === escapedFilename && sourcemapList[mapIndex]) { - return sourcemapList[mapIndex++] - } else { - return null - } - }, - excludeContent, - ) + map = remapping(sourcemapList[0], function loader(sourcefile) { + if (sourcefile === escapedFilename && sourcemapList[mapIndex]) { + return sourcemapList[mapIndex++] + } else { + return null + } + }) } if (!map.file) { delete map.file From 2b1ffe86328f9d06ef9528ee117b61889893ddcc Mon Sep 17 00:00:00 2001 From: patak Date: Mon, 3 Jul 2023 16:58:54 +0200 Subject: [PATCH 02/14] release: v4.4.0-beta.4 --- packages/vite/CHANGELOG.md | 16 ++++++++++++++++ packages/vite/package.json | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/vite/CHANGELOG.md b/packages/vite/CHANGELOG.md index 4c1cf08782b269..e8f3e59a9fd014 100644 --- a/packages/vite/CHANGELOG.md +++ b/packages/vite/CHANGELOG.md @@ -1,3 +1,19 @@ +## 4.4.0-beta.4 (2023-07-03) + +* fix: lightningCSS should load external URL in CSS file (#13692) ([8517645](https://github.com/vitejs/vite/commit/8517645)), closes [#13692](https://github.com/vitejs/vite/issues/13692) +* fix: shortcut open browser when set host (#13677) ([6f1c55e](https://github.com/vitejs/vite/commit/6f1c55e)), closes [#13677](https://github.com/vitejs/vite/issues/13677) +* fix(cli): convert the sourcemap option to boolean (fix #13638) (#13663) ([d444bfe](https://github.com/vitejs/vite/commit/d444bfe)), closes [#13638](https://github.com/vitejs/vite/issues/13638) [#13663](https://github.com/vitejs/vite/issues/13663) +* fix(css): use esbuild legalComments config when minifying CSS (#13661) ([2d9008e](https://github.com/vitejs/vite/commit/2d9008e)), closes [#13661](https://github.com/vitejs/vite/issues/13661) +* fix(sourcemap): preserve original sourcesContent (#13662) ([f6362b6](https://github.com/vitejs/vite/commit/f6362b6)), closes [#13662](https://github.com/vitejs/vite/issues/13662) +* fix(ssr): transform superclass identifier (#13635) ([c5b2c8f](https://github.com/vitejs/vite/commit/c5b2c8f)), closes [#13635](https://github.com/vitejs/vite/issues/13635) +* chore: fix pnpm bug with version-less workspace (#13700) ([e48d35d](https://github.com/vitejs/vite/commit/e48d35d)), closes [#13700](https://github.com/vitejs/vite/issues/13700) +* chore: show error position (#13623) ([90271a6](https://github.com/vitejs/vite/commit/90271a6)), closes [#13623](https://github.com/vitejs/vite/issues/13623) +* chore(deps): update all non-major dependencies (#13633) ([c72fb9b](https://github.com/vitejs/vite/commit/c72fb9b)), closes [#13633](https://github.com/vitejs/vite/issues/13633) +* feat: preview mode add keyboard shortcuts (#12968) ([126e93e](https://github.com/vitejs/vite/commit/126e93e)), closes [#12968](https://github.com/vitejs/vite/issues/12968) +* feat: update esbuild to 0.18.10 (#13644) ([f900acd](https://github.com/vitejs/vite/commit/f900acd)), closes [#13644](https://github.com/vitejs/vite/issues/13644) + + + ## 4.4.0-beta.3 (2023-06-25) * chore: upgrade rollup to 3.25.2 (#13608) ([5497abe](https://github.com/vitejs/vite/commit/5497abe)), closes [#13608](https://github.com/vitejs/vite/issues/13608) diff --git a/packages/vite/package.json b/packages/vite/package.json index b6304977f220d6..2160520019cc50 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -1,6 +1,6 @@ { "name": "vite", - "version": "4.4.0-beta.3", + "version": "4.4.0-beta.4", "type": "module", "license": "MIT", "author": "Evan You", From a36d107cbce28bbf22ea49a44ff6a53f5dcb2c74 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 23 Feb 2023 11:12:40 +0100 Subject: [PATCH 03/14] feat: add vite-node into core --- packages/vite/package.json | 3 + packages/vite/rollup.config.ts | 7 + packages/vite/src/node/index.ts | 8 + packages/vite/src/node/plugins/css.ts | 21 +- packages/vite/src/node/server/moduleGraph.ts | 4 +- .../__tests__/fixtures/modules/ts-module.ts | 7 + .../node/ssr/__tests__/runtimeClient.spec.ts | 18 + packages/vite/src/node/ssr/runtimeClient.ts | 566 ++++++++++++++++++ .../vite/src/node/ssr/ssrRuntimeServer.ts | 356 +++++++++++ packages/vite/src/node/utils.ts | 100 +--- packages/vite/src/node/utils/shared.ts | 162 +++++ pnpm-lock.yaml | 62 ++ 12 files changed, 1216 insertions(+), 98 deletions(-) create mode 100644 packages/vite/src/node/ssr/__tests__/fixtures/modules/ts-module.ts create mode 100644 packages/vite/src/node/ssr/__tests__/runtimeClient.spec.ts create mode 100644 packages/vite/src/node/ssr/runtimeClient.ts create mode 100644 packages/vite/src/node/ssr/ssrRuntimeServer.ts create mode 100644 packages/vite/src/node/utils/shared.ts diff --git a/packages/vite/package.json b/packages/vite/package.json index 2160520019cc50..c223292de8593c 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -27,6 +27,9 @@ "./client": { "types": "./client.d.ts" }, + "./runtime/client": { + "import": "./dist/node/runtime/client.js" + }, "./dist/client/*": "./dist/client/*", "./package.json": "./package.json" }, diff --git a/packages/vite/rollup.config.ts b/packages/vite/rollup.config.ts index ebfe0fcacb4cc0..a94d5ed68ae14f 100644 --- a/packages/vite/rollup.config.ts +++ b/packages/vite/rollup.config.ts @@ -153,6 +153,13 @@ function createNodeConfig(isProduction: boolean) { index: path.resolve(__dirname, 'src/node/index.ts'), cli: path.resolve(__dirname, 'src/node/cli.ts'), constants: path.resolve(__dirname, 'src/node/constants.ts'), + 'runtime/client': path.resolve( + __dirname, + 'src/node/ssr/runtimeClient.ts', + ), + // this needs to be a separate entry point, so it's not bundled into a giant chunk + // we don't want to import big chunks at runtime because it slows down startup time + 'runtime/utils': path.resolve(__dirname, 'src/node/utils/shared.ts'), }, output: { ...sharedNodeOptions.output, diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index ffa1d4ecd83a49..ccc5543b37b14b 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -145,3 +145,11 @@ export type { RollupCommonJSOptions } from 'dep-types/commonjs' export type { RollupDynamicImportVarsOptions } from 'dep-types/dynamicImportVars' export type { Matcher, AnymatchPattern, AnymatchFn } from 'dep-types/anymatch' export type { LightningCSSOptions } from 'dep-types/lightningcss' + +// TODO: naming? we already have ViteDevServer, it should probably just be a prt of it +export { ViteServer as ViteRuntimeServer } from './ssr/ssrRuntimeServer' +export type { + ViteRuntimeFetchResult, + ViteRuntimeServerDepsHandlingOptions, + ViteRuntimeServerOptions, +} from './ssr/ssrRuntimeServer' diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 8d6bb21129580f..c77a40eef108b4 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -45,7 +45,9 @@ import { emptyCssComments, generateCodeFrame, getHash, + isCSSRequest, isDataUrl, + isDirectCSSRequest, isExternalUrl, isObject, joinUrlSegments, @@ -71,6 +73,12 @@ import { } from './asset' import type { ESBuildOptions } from './esbuild' +export { + isCSSRequest, + isDirectCSSRequest, + isDirectRequest, +} from '../utils/shared' + // const debug = createDebugger('vite:css') export interface CSSOptions { @@ -158,8 +166,8 @@ export function resolveCSSOptions( } const cssModuleRE = new RegExp(`\\.module${CSS_LANGS_RE.source}`) -const directRequestRE = /[?&]direct\b/ -const htmlProxyRE = /[?&]html-proxy\b/ + +const htmlProxyRE = /(?:\?|&)html-proxy\b/ const commonjsProxyRE = /\?commonjs-proxy/ const inlineRE = /[?&]inline\b/ const inlineCSSRE = /[?&]inline-css\b/ @@ -187,18 +195,9 @@ type CssLang = | keyof typeof PreprocessLang | keyof typeof PostCssDialectLang -export const isCSSRequest = (request: string): boolean => - CSS_LANGS_RE.test(request) - export const isModuleCSSRequest = (request: string): boolean => cssModuleRE.test(request) -export const isDirectCSSRequest = (request: string): boolean => - CSS_LANGS_RE.test(request) && directRequestRE.test(request) - -export const isDirectRequest = (request: string): boolean => - directRequestRE.test(request) - const cssModulesCache = new WeakMap< ResolvedConfig, Map> diff --git a/packages/vite/src/node/server/moduleGraph.ts b/packages/vite/src/node/server/moduleGraph.ts index 18e0c39c56c735..b0958468a713e0 100644 --- a/packages/vite/src/node/server/moduleGraph.ts +++ b/packages/vite/src/node/server/moduleGraph.ts @@ -1,12 +1,12 @@ import { extname } from 'node:path' import type { ModuleInfo, PartialResolvedId } from 'rollup' -import { isDirectCSSRequest } from '../plugins/css' import { cleanUrl, + isDirectCSSRequest, normalizePath, removeImportQuery, removeTimestampQuery, -} from '../utils' +} from '../utils/shared' import { FS_PREFIX } from '../constants' import type { TransformResult } from './transformRequest' diff --git a/packages/vite/src/node/ssr/__tests__/fixtures/modules/ts-module.ts b/packages/vite/src/node/ssr/__tests__/fixtures/modules/ts-module.ts new file mode 100644 index 00000000000000..1e18017f565de8 --- /dev/null +++ b/packages/vite/src/node/ssr/__tests__/fixtures/modules/ts-module.ts @@ -0,0 +1,7 @@ +interface ReturnValue { + foo: string +} + +export function hello(foo: string): ReturnValue { + return { foo } +} diff --git a/packages/vite/src/node/ssr/__tests__/runtimeClient.spec.ts b/packages/vite/src/node/ssr/__tests__/runtimeClient.spec.ts new file mode 100644 index 00000000000000..de47f50dd2e8ee --- /dev/null +++ b/packages/vite/src/node/ssr/__tests__/runtimeClient.spec.ts @@ -0,0 +1,18 @@ +/* eslint-disable node/no-missing-import */ +// @ts-expect-error TODO: add types, finalize entry point +import { ViteRuntimeClient } from 'vite/runtime/client' +import { ViteRuntimeServer, createServer } from 'vite' +import { expect, test } from 'vitest' + +test('runtime executes a module', async () => { + const server = await createServer({ + root: __dirname, + }) + const node = new ViteRuntimeServer(server) + const client = new ViteRuntimeClient({ + root: __dirname, + fetchModule: (id) => node.fetchModule(id), + }) + const module = await client.executeId('./fixtures/modules/ts-module.ts') + expect(module.hello('bar')).toEqual({ foo: 'bar' }) +}) diff --git a/packages/vite/src/node/ssr/runtimeClient.ts b/packages/vite/src/node/ssr/runtimeClient.ts new file mode 100644 index 00000000000000..22501b8d524122 --- /dev/null +++ b/packages/vite/src/node/ssr/runtimeClient.ts @@ -0,0 +1,566 @@ +import { fileURLToPath, pathToFileURL } from 'node:url' +import { builtinModules, createRequire } from 'node:module' +import { dirname } from 'node:path' +import vm from 'node:vm' +import type { PartialResolvedId } from 'rollup' +import type { ViteHotContext } from 'types/hot' +import { + CLIENT_PUBLIC_PATH, + ENV_PUBLIC_PATH, + VALID_ID_PREFIX, +} from '../constants' +import { + cleanUrl, + normalizeModuleId, + normalizeRequestId, + slash, + toFilePath, +} from '../utils/shared' +import type { ViteRuntimeFetchResult } from './ssrRuntimeServer' + +// we don't use mlly's "isNodeBuiltin" because it's bundled in a big chunk, +// and we don't want to rely on it +function isNodeBuiltin(id: string): boolean { + if (id.startsWith('node:')) return true + return builtinModules.includes(id) +} + +// const debugExecute = createDebug('vite-runtime:client:execute') +// const debugNative = createDebug('vite-runtime:client:native') + +// TODO: use logger +// eslint-disable-next-line no-console +const warn = console.warn + +const clientStub = { + injectQuery: (id: string): string => id, + createHotContext(): ViteHotContext { + return { + data: {}, + acceptExports: () => {}, + send: () => {}, + accept: () => {}, + prune: () => {}, + dispose: () => {}, + invalidate: () => {}, + on: () => {}, + } + }, + updateStyle(id: string, css: string): void { + if (typeof document === 'undefined') return + + const element = document.getElementById(id) + if (element) element.remove() + + const head = document.querySelector('head') + const style = document.createElement('style') + style.setAttribute('type', 'text/css') + style.id = id + style.innerHTML = css + head?.appendChild(style) + }, +} + +export interface ModuleCache { + promise?: Promise + exports?: any + evaluated?: boolean + resolving?: boolean + code?: string + /** + * Module ids that imports this module + */ + importers?: Set +} + +export class ModuleCacheMap extends Map { + normalizePath(fsPath: string): string { + return normalizeModuleId(fsPath) + } + + /** + * Assign partial data to the map + */ + update(fsPath: string, mod: Partial): this { + fsPath = this.normalizePath(fsPath) + if (!super.has(fsPath)) super.set(fsPath, mod) + else Object.assign(super.get(fsPath) as ModuleCache, mod) + return this + } + + setByModuleId(modulePath: string, mod: ModuleCache): this { + return super.set(modulePath, mod) + } + + override set(fsPath: string, mod: ModuleCache): this { + return this.setByModuleId(this.normalizePath(fsPath), mod) + } + + getByModuleId(modulePath: string): ModuleCache { + if (!super.has(modulePath)) super.set(modulePath, {}) + return super.get(modulePath)! + } + + override get(fsPath: string): ModuleCache { + return this.getByModuleId(this.normalizePath(fsPath)) + } + + deleteByModuleId(modulePath: string): boolean { + return super.delete(modulePath) + } + + override delete(fsPath: string): boolean { + return this.deleteByModuleId(this.normalizePath(fsPath)) + } + + /** + * Invalidate modules that dependent on the given modules, up to the main entry + */ + invalidateDepTree( + ids: string[] | Set, + invalidated = new Set(), + ): Set { + for (const _id of ids) { + const id = this.normalizePath(_id) + if (invalidated.has(id)) continue + invalidated.add(id) + const mod = super.get(id) + if (mod?.importers) this.invalidateDepTree(mod.importers, invalidated) + super.delete(id) + } + return invalidated + } + + /** + * Invalidate dependency modules of the given modules, down to the bottom-level dependencies + */ + invalidateSubDepTree( + ids: string[] | Set, + invalidated = new Set(), + ): Set { + for (const _id of ids) { + const id = this.normalizePath(_id) + if (invalidated.has(id)) continue + invalidated.add(id) + const subIds = Array.from(super.entries()) + .filter(([, mod]) => mod.importers?.has(id)) + .map(([key]) => key) + subIds.length && this.invalidateSubDepTree(subIds, invalidated) + super.delete(id) + } + return invalidated + } +} + +export const DEFAULT_REQUEST_STUBS = { + '/@vite/client': clientStub, + '@vite/client': clientStub, +} + +interface ViteRuntimeClientOptions { + moduleCache?: ModuleCacheMap + root: string + fetchModule: (id: string) => Promise + resolveId?: ( + id: string, + importer?: string, + ) => Promise + createHotContext?: ( + importCallback: (id: string) => any, + id: string, + ) => ViteHotContext + interopDefault?: boolean + requestStubs?: Record + base?: string + debug?: boolean +} + +export class ViteRuntimeClient { + private debug: boolean + + public options: ViteRuntimeClientOptions + public moduleCache: ModuleCacheMap + public evaluationQueue = new Set() + + constructor(options: ViteRuntimeClientOptions) { + this.moduleCache = options.moduleCache ?? new ModuleCacheMap() + this.options = options + this.debug = + options.debug ?? + (typeof process !== 'undefined' + ? !!process.env.VITE_RUNTIME_CLIENT_DEBUG_RUNNER + : false) + } + + public async executeFile(file: string): Promise { + const url = `/@fs/${slash(file)}` + return await this.cachedRequest(url, url, []) + } + + public async executeId(rawId: string): Promise { + const [id, url] = await this.resolveUrl(rawId) + return await this.cachedRequest(id, url, []) + } + + /** @internal */ + protected async cachedRequest( + id: string, + fsPath: string, + callstack: string[], + ): Promise { + const importee = callstack[callstack.length - 1] + + const mod = this.moduleCache.getByModuleId(fsPath) + + if (!mod.importers) mod.importers = new Set() + if (importee) mod.importers.add(importee) + + // the callstack reference itself circularly + if (callstack.includes(fsPath) && mod.exports) return mod.exports + + // cached module + if (mod.promise) return mod.promise + + mod.evaluated = false + const promise = this.directRequest(id, fsPath, callstack) + + try { + return await promise + } finally { + mod.evaluated = true + } + } + + protected shouldResolveId(id: string, _importee?: string): boolean { + return ( + ![CLIENT_PUBLIC_PATH, ENV_PUBLIC_PATH].includes(id) && !isNodeBuiltin(id) + ) + } + + private async _resolveUrl( + id: string, + importee?: string, + ): Promise<[url: string, fsPath: string]> { + // we don't pass down importee here, because otherwise Vite doesn't resolve it correctly + // should be checked before normalization, because it removes this prefix + if (importee && id.startsWith(VALID_ID_PREFIX)) importee = undefined + id = normalizeRequestId(id, this.options.base) + if (!this.shouldResolveId(id)) return [id, id] + // we always want to have actual file path here, so we check if file actually exists + // if it's a virtual module, we do not care for it + const { path, exists } = toFilePath(id, this.options.root) + if (!this.options.resolveId || exists) return [id, path] + const resolved = await this.options.resolveId(id, importee) + const resolvedId = resolved + ? normalizeRequestId(resolved.id, this.options.base) + : id + // to be compatible with dependencies that do not resolve id + const fsPath = resolved ? resolvedId : path + return [resolvedId, fsPath] + } + + protected async resolveUrl( + id: string, + importee?: string, + ): Promise<[url: string, fsPath: string]> { + const resolveKey = `resolve:${id}` + // put info about new import as soon as possible, so we can start tracking it + this.evaluationQueue.add(resolveKey) + try { + return await this._resolveUrl(id, importee) + } finally { + this.evaluationQueue.delete(resolveKey) + } + } + + /** @internal */ + protected async dependencyRequest( + id: string, + fsPath: string, + callstack: string[], + ): Promise { + const getStack = () => { + return `stack:\n${[...callstack, fsPath] + .reverse() + .map((p) => `- ${p}`) + .join('\n')}` + } + + let debugTimer: any + if (this.debug) + debugTimer = setTimeout( + () => + warn(() => `module ${fsPath} takes over 2s to load.\n${getStack()}`), + 2000, + ) + + try { + if (callstack.includes(fsPath)) { + const depExports = this.moduleCache.getByModuleId(fsPath)?.exports + if (depExports) return depExports + throw new Error( + `[vite-runtime-client] Failed to resolve circular dependency, ${getStack()}`, + ) + } + + return await this.cachedRequest(id, fsPath, callstack) + } finally { + if (debugTimer) clearTimeout(debugTimer) + } + } + + /** @internal */ + protected async directRequest( + id: string, + fsPath: string, + _callstack: string[], + ): Promise { + const moduleId = normalizeModuleId(fsPath) + const callstack = [..._callstack, moduleId] + + const mod = this.moduleCache.getByModuleId(moduleId) + + const request = async (dep: string) => { + const [id, depFsPath] = await this.resolveUrl(dep, fsPath) + return this.dependencyRequest(id, depFsPath, callstack) + } + + const requestStubs = this.options.requestStubs || DEFAULT_REQUEST_STUBS + if (id in requestStubs) return requestStubs[id] + + let { code: transformed, externalize } = await this.options.fetchModule(id) + + if (externalize) { + // debugNative(externalize) + const exports = await this.interopedImport(externalize) + mod.exports = exports + return exports + } + + if (transformed == null) + throw new Error( + `[vite-runtime-client] Failed to load "${id}" imported from ${ + callstack[callstack.length - 2] + }`, + ) + + const modulePath = cleanUrl(moduleId) + // disambiguate the `:/` on windows: see nodejs/node#31710 + const href = pathToFileURL(modulePath).href + const meta = { url: href } + const exports = Object.create(null) + Object.defineProperty(exports, Symbol.toStringTag, { + value: 'Module', + enumerable: false, + configurable: false, + }) + Object.defineProperty(exports, '__esModule', { value: true }) + // this proxy is triggered only on exports.{name} and module.exports access + // inside the module itself. imported module is always "exports" + const cjsExports = new Proxy(exports, { + get: (target, p, receiver) => { + if (Reflect.has(target, p)) return Reflect.get(target, p, receiver) + return Reflect.get(Object.prototype, p, receiver) + }, + getPrototypeOf: () => Object.prototype, + set: (_, p, value) => { + // treat "module.exports =" the same as "exports.default =" to not have nested "default.default", + // so "exports.default" becomes the actual module + if ( + p === 'default' && + this.shouldInterop(modulePath, { default: value }) + ) { + exportAll(cjsExports, value) + exports.default = value + return true + } + + if (!Reflect.has(exports, 'default')) exports.default = {} + + // returns undefined, when accessing named exports, if default is not an object + // but is still present inside hasOwnKeys, this is Node behaviour for CJS + if (isPrimitive(exports.default)) { + defineExport(exports, p, () => undefined) + return true + } + + exports.default[p] = value + if (p !== 'default') defineExport(exports, p, () => value) + + return true + }, + }) + + Object.assign(mod, { code: transformed, exports }) + + const __filename = fileURLToPath(href) + const moduleProxy = { + set exports(value) { + exportAll(cjsExports, value) + exports.default = value + }, + get exports() { + return cjsExports + }, + } + + // Vite hot context + let hotContext: ViteHotContext | undefined + if (this.options.createHotContext) { + Object.defineProperty(meta, 'hot', { + enumerable: true, + get: () => { + hotContext ||= this.options.createHotContext?.( + (id) => this.executeId(id), + `/@fs/${fsPath}`, + ) + return hotContext + }, + set: (value) => { + hotContext = value + }, + }) + } + + // Be careful when changing this + // changing context will change amount of code added on line :114 (vm.runInThisContext) + // this messes up sourcemaps for coverage + // adjust `offset` variable in packages/coverage-c8/src/provider.ts#86 if you do change this + const context = this.prepareContext({ + // esm transformed by Vite + __vite_ssr_import__: request, + __vite_ssr_dynamic_import__: request, + __vite_ssr_exports__: exports, + __vite_ssr_exportAll__: (obj: any) => exportAll(exports, obj), + __vite_ssr_import_meta__: meta, + + // cjs compact + require: createRequire(href), + exports: cjsExports, + module: moduleProxy, + __filename, + __dirname: dirname(__filename), + }) + + // debugExecute(__filename) + + // remove shebang + if (transformed[0] === '#') + transformed = transformed.replace(/^#!.*/, (s) => ' '.repeat(s.length)) + + // add 'use strict' since ESM enables it by default + const codeDefinition = `'use strict';async (${Object.keys(context).join( + ',', + )})=>{{` + const code = `${codeDefinition}${transformed}\n}}` + const fn = vm.runInThisContext(code, { + filename: __filename, + lineOffset: 0, + columnOffset: -codeDefinition.length, + }) + + await fn(...Object.values(context)) + + return this.freezeModule(exports) + } + + protected freezeModule(exports: T): T { + return Object.freeze(exports) + } + + protected prepareContext>(context: T): T { + return context + } + + /** + * Define if a module should be interop-ed + * This function mostly for the ability to override by subclass + */ + protected shouldInterop(path: string, mod: any): boolean { + if (this.options.interopDefault === false) return false + // never interop ESM modules + // TODO: should also skip for `.js` with `type="module"` + return !path.endsWith('.mjs') && 'default' in mod + } + + /** + * Import a module and interop it + */ + protected async interopedImport(path: string): Promise { + const importedModule = await import(path) + + if (!this.shouldInterop(path, importedModule)) return importedModule + + const { mod, defaultExport } = interopModule(importedModule) + + return new Proxy(mod, { + get(mod, prop) { + if (prop === 'default') return defaultExport + return mod[prop] ?? defaultExport?.[prop] + }, + has(mod, prop) { + if (prop === 'default') return defaultExport !== undefined + return prop in mod || (defaultExport && prop in defaultExport) + }, + getOwnPropertyDescriptor(mod, prop) { + const descriptor = Reflect.getOwnPropertyDescriptor(mod, prop) + if (descriptor) return descriptor + if (prop === 'default' && defaultExport !== undefined) { + return { + value: defaultExport, + enumerable: true, + configurable: true, + } + } + }, + }) + } +} + +function interopModule(mod: any) { + if (isPrimitive(mod)) { + return { + mod: { default: mod }, + defaultExport: mod, + } + } + + let defaultExport = 'default' in mod ? mod.default : mod + + if (!isPrimitive(defaultExport) && '__esModule' in defaultExport) { + mod = defaultExport + if ('default' in defaultExport) defaultExport = defaultExport.default + } + + return { mod, defaultExport } +} + +function defineExport(exports: any, key: string | symbol, value: () => any) { + Object.defineProperty(exports, key, { + enumerable: true, + configurable: true, + get: value, + }) +} + +function exportAll(exports: any, sourceModule: any) { + // vitest/#1120 when a module exports itself it causes + // call stack error + if (exports === sourceModule) return + + // we don't want to have "string" to be made into {0:"s",1:"t",2:"r",3:"i",4:"n",5:"g"} + // the same goes for arrays: [1,2,3] -> {0:1,1:2,2:3} + if (isPrimitive(sourceModule) || Array.isArray(sourceModule)) return + + for (const key in sourceModule) { + if (key !== 'default') { + try { + defineExport(exports, key, () => sourceModule[key]) + } catch (_err) {} + } + } +} + +function isPrimitive(value: any) { + return !value || (typeof value !== 'object' && typeof value !== 'function') +} diff --git a/packages/vite/src/node/ssr/ssrRuntimeServer.ts b/packages/vite/src/node/ssr/ssrRuntimeServer.ts new file mode 100644 index 00000000000000..09bf428704a5a2 --- /dev/null +++ b/packages/vite/src/node/ssr/ssrRuntimeServer.ts @@ -0,0 +1,356 @@ +import path from 'node:path' +import { existsSync } from 'node:fs' +import { isNodeBuiltin, isValidNodeImport } from 'mlly' +import type { PartialResolvedId, SourceMap } from 'rollup' +import { KNOWN_ASSET_TYPES } from '../constants' +import type { TransformResult } from '../server/transformRequest' +import { + normalizeModuleId, + normalizePath, + slash, + toFilePath, +} from '../utils/shared' +import type { ViteDevServer } from '../server' + +export interface ViteRuntimeServerOptions { + /** + * Inject inline sourcemap to modules + * @default 'inline' + */ + sourcemap?: 'inline' | boolean + /** + * Deps handling + */ + deps?: ViteRuntimeServerDepsHandlingOptions + /** + * Transform method for modules + */ + transformMode?: { + ssr?: RegExp[] + web?: RegExp[] + } + + // debug?: DebuggerOptions +} + +export interface ViteRuntimeServerDepsHandlingOptions { + external?: (string | RegExp)[] + inline?: (string | RegExp)[] | true + /** + * Try to guess the CJS version of a package when it's invalid ESM + * @default false + */ + fallbackCJS?: boolean +} + +export interface ViteRuntimeFetchResult { + code?: string + externalize?: string + map?: SourceMap | null +} + +const ESM_EXT_RE = /\.(es|esm|esm-browser|esm-bundler|es6|module)\.js$/ +const ESM_FOLDER_RE = /\/(es|esm)\/(.*\.js)$/ + +const defaultInline = [ + /virtual:/, + /\.[mc]?ts$/, + + // special Vite query strings + /[?&](init|raw|url|inline)\b/, + // Vite returns a string for assets imports, even if it's inside "node_modules" + new RegExp(`\\.(${KNOWN_ASSET_TYPES.join('|')})$`), +] + +const depsExternal = [/\.cjs\.js$/, /\.mjs$/] + +export function guessCJSversion(id: string): string | undefined { + if (id.match(ESM_EXT_RE)) { + for (const i of [ + id.replace(ESM_EXT_RE, '.mjs'), + id.replace(ESM_EXT_RE, '.umd.js'), + id.replace(ESM_EXT_RE, '.cjs.js'), + id.replace(ESM_EXT_RE, '.js'), + ]) { + if (existsSync(i)) return i + } + } + if (id.match(ESM_FOLDER_RE)) { + for (const i of [ + id.replace(ESM_FOLDER_RE, '/umd/$1'), + id.replace(ESM_FOLDER_RE, '/cjs/$1'), + id.replace(ESM_FOLDER_RE, '/lib/$1'), + id.replace(ESM_FOLDER_RE, '/$1'), + ]) { + if (existsSync(i)) return i + } + } +} + +const _defaultExternalizeCache = new Map>() +export async function shouldExternalize( + id: string, + options?: ViteRuntimeServerDepsHandlingOptions, + cache = _defaultExternalizeCache, +): Promise { + if (!cache.has(id)) cache.set(id, _shouldExternalize(id, options)) + return cache.get(id)! +} + +async function _shouldExternalize( + id: string, + options?: ViteRuntimeServerDepsHandlingOptions, +): Promise { + if (isNodeBuiltin(id)) return id + + // data: should be processed by native import, + // since it is a feature of ESM + if (id.startsWith('data:')) return id + + id = patchWindowsImportPath(id) + + if (matchExternalizePattern(id, options?.inline)) return false + if (matchExternalizePattern(id, options?.external)) return id + + const isNodeModule = id.includes('/node_modules/') + const guessCJS = isNodeModule && options?.fallbackCJS + id = guessCJS ? guessCJSversion(id) || id : id + + if (matchExternalizePattern(id, defaultInline)) return false + if (matchExternalizePattern(id, depsExternal)) return id + + const isDist = id.includes('/dist/') + if ((isNodeModule || isDist) && (await isValidNodeImport(id))) return id + + return false +} + +function matchExternalizePattern( + id: string, + patterns?: (string | RegExp)[] | true, +) { + if (patterns == null) return false + if (patterns === true) return true + for (const ex of patterns) { + if (typeof ex === 'string') { + if (id.includes(`/node_modules/${ex}/`)) return true + } else { + if (ex.test(id)) return true + } + } + return false +} + +function patchWindowsImportPath(path: string) { + if (path.match(/^\w:\\/)) return `file:///${slash(path)}` + else if (path.match(/^\w:\//)) return `file:///${path}` + else return path +} + +let SOURCEMAPPING_URL = 'sourceMa' +SOURCEMAPPING_URL += 'ppingURL' + +const VITE_NODE_SOURCEMAPPING_SOURCE = '//# sourceMappingConsumer=vite' +const VITE_NODE_SOURCEMAPPING_URL = `${SOURCEMAPPING_URL}=data:application/json;charset=utf-8` +// const VITE_NODE_SOURCEMAPPING_REGEXP = new RegExp(`//# ${VITE_NODE_SOURCEMAPPING_URL};base64,(.+)`) + +export function withInlineSourcemap(result: TransformResult): TransformResult { + const map = result.map + let code = result.code + + if (!map || code.includes(VITE_NODE_SOURCEMAPPING_SOURCE)) return result + + // to reduce the payload size, we only inline vite source map, because it's also the only one we use + const OTHER_SOURCE_MAP_REGEXP = new RegExp( + `//# ${SOURCEMAPPING_URL}=data:application/json[^,]+base64,(.+)`, + 'g', + ) + while (OTHER_SOURCE_MAP_REGEXP.test(code)) + code = code.replace(OTHER_SOURCE_MAP_REGEXP, '') + + const sourceMap = Buffer.from(JSON.stringify(map), 'utf-8').toString('base64') + result.code = `${code.trimEnd()}\n\n${VITE_NODE_SOURCEMAPPING_SOURCE}\n//# ${VITE_NODE_SOURCEMAPPING_URL};base64,${sourceMap}\n` + + return result +} + +export class ViteServer { + private fetchPromiseMap = new Map>() + private transformPromiseMap = new Map< + string, + Promise + >() + + fetchCache = new Map< + string, + { + duration?: number + timestamp: number + result: ViteRuntimeFetchResult + } + >() + + externalizeCache = new Map>() + + // debugger?: Debugger + + constructor( + public server: ViteDevServer, + public options: ViteRuntimeServerOptions = {}, + ) { + const ssrOptions = server.config.ssr + if (ssrOptions) { + options.deps ??= {} + + // we don't externalize ssr, because it has different semantics in Vite + // if (ssrOptions.external) { + // options.deps.external ??= [] + // options.deps.external.push(...ssrOptions.external) + // } + + if (ssrOptions.noExternal === true) { + options.deps.inline ??= true + } else if (options.deps.inline !== true) { + options.deps.inline ??= [] + const noExternal = Array.isArray(ssrOptions.noExternal) + ? ssrOptions.noExternal + : (ssrOptions.noExternal && [ssrOptions.noExternal]) || [] + options.deps.inline.push(...noExternal) + } + } + // if (process.env.VITE_NODE_DEBUG_DUMP) { + // options.debug = Object.assign({ + // dumpModules: !!process.env.VITE_NODE_DEBUG_DUMP, + // loadDumppedModules: process.env.VITE_NODE_DEBUG_DUMP === 'load', + // }, options.debug ?? {}) + // } + // if (options.debug) + // this.debugger = new Debugger(server.config.root, options.debug!) + } + + shouldExternalize(id: string): Promise { + return shouldExternalize(id, this.options.deps, this.externalizeCache) + } + + async resolveId( + id: string, + importer?: string, + ): Promise { + if (importer && !importer.startsWith(this.server.config.root)) + importer = normalizePath(path.resolve(this.server.config.root, importer)) + const mode = (importer && this.getTransformMode(importer)) || 'ssr' + return this.server.pluginContainer.resolveId(id, importer, { + ssr: mode === 'ssr', + }) + } + + async fetchModule(id: string): Promise { + id = normalizeModuleId(id) + // reuse transform for concurrent requests + if (!this.fetchPromiseMap.has(id)) { + this.fetchPromiseMap.set( + id, + this._fetchModule(id) + .then((r) => { + return this.options.sourcemap !== true + ? { ...r, map: undefined } + : r + }) + .finally(() => { + this.fetchPromiseMap.delete(id) + }), + ) + } + return this.fetchPromiseMap.get(id)! + } + + async transformRequest( + id: string, + ): Promise { + // reuse transform for concurrent requests + if (!this.transformPromiseMap.has(id)) { + this.transformPromiseMap.set( + id, + this._transformRequest(id).finally(() => { + this.transformPromiseMap.delete(id) + }), + ) + } + return this.transformPromiseMap.get(id)! + } + + getTransformMode(id: string): 'web' | 'ssr' { + const withoutQuery = id.split('?')[0] + + if (this.options.transformMode?.web?.some((r) => withoutQuery.match(r))) + return 'web' + if (this.options.transformMode?.ssr?.some((r) => withoutQuery.match(r))) + return 'ssr' + + if (withoutQuery.match(/\.([cm]?[jt]sx?|json)$/)) return 'ssr' + return 'web' + } + + private async _fetchModule(id: string): Promise { + let result: ViteRuntimeFetchResult + + const { path: filePath } = toFilePath(id, this.server.config.root) + + const module = this.server.moduleGraph.getModuleById(id) + const timestamp = module ? module.lastHMRTimestamp : null + const cache = this.fetchCache.get(filePath) + if (timestamp && cache && cache.timestamp >= timestamp) return cache.result + + const time = Date.now() + const externalize = await this.shouldExternalize(filePath) + let duration: number | undefined + if (typeof externalize === 'string') { + result = { externalize } + // this.debugger?.recordExternalize(id, externalize) + } else { + const start = performance.now() + const r = await this._transformRequest(id) + duration = performance.now() - start + result = { code: r?.code, map: r?.map } + } + + this.fetchCache.set(filePath, { + duration, + timestamp: time, + result, + }) + + return result + } + + private async _transformRequest(id: string) { + // debugRequest(id) + + let result: TransformResult | null = null + + // if (this.options.debug?.loadDumppedModules) { + // result = await this.debugger?.loadDump(id) ?? null + // if (result) + // return result + // } + + if (this.getTransformMode(id) === 'web') { + // for components like Vue, we want to use the client side + // plugins but then convert the code to be consumed by the server + result = await this.server.transformRequest(id) + if (result) + result = await this.server.ssrTransform(result.code, result.map, id) + } else { + result = await this.server.transformRequest(id, { ssr: true }) + } + + const sourcemap = this.options.sourcemap ?? 'inline' + if (sourcemap === 'inline' && result && !id.includes('node_modules')) + withInlineSourcemap(result) + + // if (this.options.debug?.dumpModules) + // await this.debugger?.dumpFile(id, result) + + return result + } +} diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 1ac8617f6bc7ac..c112420dd5bb61 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -20,8 +20,6 @@ import type { TransformResult } from 'rollup' import { createFilter as _createFilter } from '@rollup/pluginutils' import { CLIENT_ENTRY, - CLIENT_PUBLIC_PATH, - ENV_PUBLIC_PATH, FS_PREFIX, NULL_BYTE_PLACEHOLDER, OPTIMIZABLE_ENTRY_RE, @@ -29,11 +27,18 @@ import { loopbackHosts, wildcardHosts, } from './constants' +import { + cleanUrl, + isWindows, + normalizePath, + removeTimestampQuery, +} from './utils/shared' import type { DepOptimizationConfig } from './optimizer' import type { ResolvedConfig } from './config' import type { ResolvedServerUrls, ViteDevServer } from './server' -import { resolvePackageData } from './packages' -import type { CommonServerOptions } from '.' +import { resolvePackageData, type CommonServerOptions } from './index' + +export * from './utils/shared' /** * Inlined to keep `@rollup/pluginutils` in devDependencies @@ -49,11 +54,6 @@ export const createFilter = _createFilter as ( options?: { resolve?: string | false | null }, ) => (id: string | unknown) => boolean -const windowsSlashRE = /\\/g -export function slash(p: string): string { - return p.replace(windowsSlashRE, '/') -} - /** * Prepend `/@id/` and replace null byte so the id is URL-safe. * This is prepended to resolved ids that are not valid browser @@ -202,14 +202,8 @@ export function isUrl(path: string): boolean { export const isCaseInsensitiveFS = testCaseInsensitiveFS() -export const isWindows = os.platform() === 'win32' - const VOLUME_RE = /^[A-Z]:/i -export function normalizePath(id: string): string { - return path.posix.normalize(isWindows ? slash(id) : id) -} - export function fsPathFromId(id: string): string { const fsPath = normalizePath( id.startsWith(FS_PREFIX) ? id.slice(FS_PREFIX.length) : id, @@ -256,77 +250,13 @@ export function isSameFileUri(file1: string, file2: string): boolean { ) } -export const queryRE = /\?.*$/s - -const postfixRE = /[?#].*$/s -export function cleanUrl(url: string): string { - return url.replace(postfixRE, '') -} - -export const externalRE = /^(https?:)?\/\// -export const isExternalUrl = (url: string): boolean => externalRE.test(url) - -export const dataUrlRE = /^\s*data:/i -export const isDataUrl = (url: string): boolean => dataUrlRE.test(url) - -export const virtualModuleRE = /^virtual-module:.*/ -export const virtualModulePrefix = 'virtual-module:' - -const knownJsSrcRE = /\.(?:[jt]sx?|m[jt]s|vue|marko|svelte|astro|imba)(?:$|\?)/ -export const isJSRequest = (url: string): boolean => { - url = cleanUrl(url) - if (knownJsSrcRE.test(url)) { - return true - } - if (!path.extname(url) && url[url.length - 1] !== '/') { - return true +export function getPotentialTsSrcPaths(filePath: string): string[] { + const [name, type, query = ''] = filePath.split(/(\.(?:[cm]?js|jsx))(\?.*)?$/) + const paths = [name + type.replace('js', 'ts') + query] + if (!type.endsWith('x')) { + paths.push(name + type.replace('js', 'tsx') + query) } - return false -} - -const knownTsRE = /\.(?:ts|mts|cts|tsx)(?:$|\?)/ -export const isTsRequest = (url: string): boolean => knownTsRE.test(url) - -const importQueryRE = /(\?|&)import=?(?:&|$)/ -const directRequestRE = /(\?|&)direct=?(?:&|$)/ -const internalPrefixes = [ - FS_PREFIX, - VALID_ID_PREFIX, - CLIENT_PUBLIC_PATH, - ENV_PUBLIC_PATH, -] -const InternalPrefixRE = new RegExp(`^(?:${internalPrefixes.join('|')})`) -const trailingSeparatorRE = /[?&]$/ -export const isImportRequest = (url: string): boolean => importQueryRE.test(url) -export const isInternalRequest = (url: string): boolean => - InternalPrefixRE.test(url) - -export function removeImportQuery(url: string): string { - return url.replace(importQueryRE, '$1').replace(trailingSeparatorRE, '') -} -export function removeDirectQuery(url: string): string { - return url.replace(directRequestRE, '$1').replace(trailingSeparatorRE, '') -} - -const replacePercentageRE = /%/g -export function injectQuery(url: string, queryToInject: string): string { - // encode percents for consistent behavior with pathToFileURL - // see #2614 for details - const resolvedUrl = new URL( - url.replace(replacePercentageRE, '%25'), - 'relative:///', - ) - const { search, hash } = resolvedUrl - let pathname = cleanUrl(url) - pathname = isWindows ? slash(pathname) : pathname - return `${pathname}?${queryToInject}${search ? `&` + search.slice(1) : ''}${ - hash ?? '' - }` -} - -const timestampRE = /\bt=\d{13}&?\b/ -export function removeTimestampQuery(url: string): string { - return url.replace(timestampRE, '').replace(trailingSeparatorRE, '') + return paths } export async function asyncReplace( diff --git a/packages/vite/src/node/utils/shared.ts b/packages/vite/src/node/utils/shared.ts new file mode 100644 index 00000000000000..eb956ae7f2b3d9 --- /dev/null +++ b/packages/vite/src/node/utils/shared.ts @@ -0,0 +1,162 @@ +import path from 'node:path' +import os from 'node:os' +import { existsSync } from 'node:fs' +import { fileURLToPath, pathToFileURL } from 'node:url' +import { + CLIENT_PUBLIC_PATH, + CSS_LANGS_RE, + ENV_PUBLIC_PATH, + FS_PREFIX, + VALID_ID_PREFIX, +} from '../constants' + +export const isWindows = os.platform() === 'win32' + +export const queryRE = /\?.*$/s +export const hashRE = /#.*$/s + +export const cleanUrl = (url: string): string => + url.replace(hashRE, '').replace(queryRE, '') + +export function slash(p: string): string { + return p.replace(/\\/g, '/') +} + +export function normalizePath(id: string): string { + return path.posix.normalize(isWindows ? slash(id) : id) +} + +const importQueryRE = /(\?|&)import=?(?:&|$)/ +const directRequestRE = /(\?|&)direct=?(?:&|$)/ +const internalPrefixes = [ + FS_PREFIX, + VALID_ID_PREFIX, + CLIENT_PUBLIC_PATH, + ENV_PUBLIC_PATH, +] +const InternalPrefixRE = new RegExp(`^(?:${internalPrefixes.join('|')})`) +const trailingSeparatorRE = /[?&]$/ +export const isImportRequest = (url: string): boolean => importQueryRE.test(url) +export const isInternalRequest = (url: string): boolean => + InternalPrefixRE.test(url) + +export function removeImportQuery(url: string): string { + return url.replace(importQueryRE, '$1').replace(trailingSeparatorRE, '') +} +export function removeDirectQuery(url: string): string { + return url.replace(directRequestRE, '$1').replace(trailingSeparatorRE, '') +} + +export function injectQuery(url: string, queryToInject: string): string { + // encode percents for consistent behavior with pathToFileURL + // see #2614 for details + const resolvedUrl = new URL(url.replace(/%/g, '%25'), 'relative:///') + const { search, hash } = resolvedUrl + let pathname = cleanUrl(url) + pathname = isWindows ? slash(pathname) : pathname + return `${pathname}?${queryToInject}${search ? `&` + search.slice(1) : ''}${ + hash ?? '' + }` +} + +const timestampRE = /\bt=\d{13}&?\b/ +export function removeTimestampQuery(url: string): string { + return url.replace(timestampRE, '').replace(trailingSeparatorRE, '') +} + +export const externalRE = /^(https?:)?\/\// +export const isExternalUrl = (url: string): boolean => externalRE.test(url) + +export const dataUrlRE = /^\s*data:/i +export const isDataUrl = (url: string): boolean => dataUrlRE.test(url) + +export const virtualModuleRE = /^virtual-module:.*/ +export const virtualModulePrefix = 'virtual-module:' + +const knownJsSrcRE = /\.(?:[jt]sx?|m[jt]s|vue|marko|svelte|astro|imba)(?:$|\?)/ +export const isJSRequest = (url: string): boolean => { + url = cleanUrl(url) + if (knownJsSrcRE.test(url)) { + return true + } + if (!path.extname(url) && !url.endsWith('/')) { + return true + } + return false +} + +const knownTsRE = /\.(?:ts|mts|cts|tsx)$/ +const knownTsOutputRE = /\.(?:js|mjs|cjs|jsx)$/ +export const isTsRequest = (url: string): boolean => knownTsRE.test(url) +export const isPossibleTsOutput = (url: string): boolean => + knownTsOutputRE.test(cleanUrl(url)) + +export const isCSSRequest = (request: string): boolean => + CSS_LANGS_RE.test(request) + +const directExistanceRequestRE = /(?:\?|&)direct\b/ + +export const isDirectCSSRequest = (request: string): boolean => + CSS_LANGS_RE.test(request) && directExistanceRequestRE.test(request) + +export const isDirectRequest = (request: string): boolean => + directExistanceRequestRE.test(request) + +export function normalizeRequestId(id: string, base?: string): string { + if (base && id.startsWith(base)) id = `/${id.slice(base.length)}` + + return id + .replace(/^\/@id\/__x00__/, '\0') // virtual modules start with `\0` + .replace(/^\/@id\//, '') + .replace(/^__vite-browser-external:/, '') + .replace(/^file:/, '') + .replace(/^\/+/, '/') // remove duplicate leading slashes + .replace(/\?v=\w+/, '?') // remove ?v= query + .replace(/&v=\w+/, '') // remove &v= query + .replace(/\?t=\w+/, '?') // remove ?t= query + .replace(/&t=\w+/, '') // remove &t= query + .replace(/\?import/, '?') // remove ?import query + .replace(/&import/, '') // remove &import query + .replace(/\?&/, '?') // replace ?& with just ? + .replace(/\?+$/, '') // remove end query mark +} + +export function normalizeModuleId(id: string): string { + return id + .replace(/\\/g, '/') + .replace(/^\/@fs\//, isWindows ? '' : '/') + .replace(/^file:\//, '/') + .replace(/^node:/, '') + .replace(/^\/+/, '/') +} + +export function toFilePath( + id: string, + root: string, +): { path: string; exists: boolean } { + id = normalizePath(id) + + let { absolute, exists } = (() => { + if (id.startsWith('/@fs/')) return { absolute: id.slice(4), exists: true } + // check if /src/module.js -> /src/module.js + if (!id.startsWith(root) && id.startsWith('/')) { + const resolved = path.resolve(root, id.slice(1)) + if (existsSync(cleanUrl(resolved))) + return { absolute: resolved, exists: true } + } else if (id.startsWith(root) && existsSync(cleanUrl(id))) { + return { absolute: id, exists: true } + } + return { absolute: id, exists: false } + })() + + if (absolute.startsWith('//')) absolute = absolute.slice(1) + + // disambiguate the `:/` on windows: see nodejs/node#31710 + return { + path: + isWindows && absolute.startsWith('/') + ? slash(fileURLToPath(pathToFileURL(absolute.slice(1)).href)) + : absolute, + exists, + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 161c84820f9aae..740ad553de1578 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10588,16 +10588,19 @@ packages: file:playground/alias/dir/module: resolution: {directory: playground/alias/dir/module, type: directory} name: '@vitejs/test-aliased-module' + version: 0.0.0 dev: false file:playground/css/css-js-dep: resolution: {directory: playground/css/css-js-dep, type: directory} name: '@vitejs/test-css-js-dep' + version: 1.0.0 dev: true file:playground/css/css-proxy-dep: resolution: {directory: playground/css/css-proxy-dep, type: directory} name: '@vitejs/test-css-proxy-dep' + version: 1.0.0 dependencies: '@vitejs/test-css-proxy-dep-nested': file:playground/css/css-proxy-dep-nested dev: true @@ -10605,20 +10608,24 @@ packages: file:playground/css/css-proxy-dep-nested: resolution: {directory: playground/css/css-proxy-dep-nested, type: directory} name: '@vitejs/test-css-proxy-dep-nested' + version: 1.0.0 file:playground/define/commonjs-dep: resolution: {directory: playground/define/commonjs-dep, type: directory} name: '@vitejs/test-commonjs-dep' + version: 1.0.0 dev: false file:playground/dynamic-import/pkg: resolution: {directory: playground/dynamic-import/pkg, type: directory} name: '@vitejs/test-pkg' + version: 1.0.0 dev: false file:playground/external/dep-that-imports: resolution: {directory: playground/external/dep-that-imports, type: directory} name: '@vitejs/test-dep-that-imports' + version: 0.0.0 dependencies: slash3: /slash@3.0.0 slash5: /slash@5.1.0 @@ -10628,6 +10635,7 @@ packages: file:playground/external/dep-that-requires: resolution: {directory: playground/external/dep-that-requires, type: directory} name: '@vitejs/test-dep-that-requires' + version: 0.0.0 dependencies: slash3: /slash@3.0.0 slash5: /slash@5.1.0 @@ -10637,31 +10645,37 @@ packages: file:playground/import-assertion/import-assertion-dep: resolution: {directory: playground/import-assertion/import-assertion-dep, type: directory} name: '@vitejs/test-import-assertion-dep' + version: 0.0.0 dev: false file:playground/json/json-module: resolution: {directory: playground/json/json-module, type: directory} name: '@vitejs/test-json-module' + version: 0.0.0 dev: true file:playground/minify/dir/module: resolution: {directory: playground/minify/dir/module, type: directory} name: '@vitejs/test-minify' + version: 0.0.0 dev: false file:playground/optimize-deps-no-discovery/dep-no-discovery: resolution: {directory: playground/optimize-deps-no-discovery/dep-no-discovery, type: directory} name: '@vitejs/test-dep-no-discovery' + version: 1.0.0 dev: false file:playground/optimize-deps/added-in-entries: resolution: {directory: playground/optimize-deps/added-in-entries, type: directory} name: '@vitejs/test-added-in-entries' + version: 1.0.0 dev: false file:playground/optimize-deps/dep-alias-using-absolute-path: resolution: {directory: playground/optimize-deps/dep-alias-using-absolute-path, type: directory} name: '@vitejs/test-dep-alias-using-absolute-path' + version: 1.0.0 dependencies: lodash: 4.17.21 dev: false @@ -10669,81 +10683,97 @@ packages: file:playground/optimize-deps/dep-cjs-browser-field-bare: resolution: {directory: playground/optimize-deps/dep-cjs-browser-field-bare, type: directory} name: '@vitejs/test-dep-cjs-browser-field-bare' + version: 0.0.0 dev: false file:playground/optimize-deps/dep-cjs-compiled-from-cjs: resolution: {directory: playground/optimize-deps/dep-cjs-compiled-from-cjs, type: directory} name: '@vitejs/test-dep-cjs-compiled-from-cjs' + version: 0.0.0 dev: false file:playground/optimize-deps/dep-cjs-compiled-from-esm: resolution: {directory: playground/optimize-deps/dep-cjs-compiled-from-esm, type: directory} name: '@vitejs/test-dep-cjs-compiled-from-esm' + version: 0.0.0 dev: false file:playground/optimize-deps/dep-cjs-with-assets: resolution: {directory: playground/optimize-deps/dep-cjs-with-assets, type: directory} name: '@vitejs/test-dep-cjs-with-assets' + version: 0.0.0 dev: false file:playground/optimize-deps/dep-css-require: resolution: {directory: playground/optimize-deps/dep-css-require, type: directory} name: '@vitejs/test-dep-css-require' + version: 0.0.0 dev: false file:playground/optimize-deps/dep-esbuild-plugin-transform: resolution: {directory: playground/optimize-deps/dep-esbuild-plugin-transform, type: directory} name: '@vitejs/test-dep-esbuild-plugin-transform' + version: 0.0.0 dev: false file:playground/optimize-deps/dep-node-env: resolution: {directory: playground/optimize-deps/dep-node-env, type: directory} name: '@vitejs/test-dep-node-env' + version: 1.0.0 dev: false file:playground/optimize-deps/dep-non-optimized: resolution: {directory: playground/optimize-deps/dep-non-optimized, type: directory} name: '@vitejs/test-dep-non-optimized' + version: 1.0.0 dev: false file:playground/optimize-deps/dep-not-js: resolution: {directory: playground/optimize-deps/dep-not-js, type: directory} name: '@vitejs/test-dep-not-js' + version: 1.0.0 dev: false file:playground/optimize-deps/dep-optimize-exports-with-glob: resolution: {directory: playground/optimize-deps/dep-optimize-exports-with-glob, type: directory} name: '@vitejs/test-dep-optimize-exports-with-glob' + version: 1.0.0 dev: false file:playground/optimize-deps/dep-optimize-with-glob: resolution: {directory: playground/optimize-deps/dep-optimize-with-glob, type: directory} name: '@vitejs/test-dep-optimize-with-glob' + version: 1.0.0 dev: false file:playground/optimize-deps/dep-relative-to-main: resolution: {directory: playground/optimize-deps/dep-relative-to-main, type: directory} name: '@vitejs/test-dep-relative-to-main' + version: 1.0.0 dev: false file:playground/optimize-deps/dep-with-builtin-module-cjs: resolution: {directory: playground/optimize-deps/dep-with-builtin-module-cjs, type: directory} name: '@vitejs/test-dep-with-builtin-module-cjs' + version: 0.0.0 dev: false file:playground/optimize-deps/dep-with-builtin-module-esm: resolution: {directory: playground/optimize-deps/dep-with-builtin-module-esm, type: directory} name: '@vitejs/test-dep-with-builtin-module-esm' + version: 0.0.0 dev: false file:playground/optimize-deps/dep-with-dynamic-import: resolution: {directory: playground/optimize-deps/dep-with-dynamic-import, type: directory} name: '@vitejs/test-dep-with-dynamic-import' + version: 0.0.0 dev: false file:playground/optimize-deps/dep-with-optional-peer-dep: resolution: {directory: playground/optimize-deps/dep-with-optional-peer-dep, type: directory} name: '@vitejs/test-dep-with-optional-peer-dep' + version: 0.0.0 peerDependencies: foobar: 0.0.0 peerDependenciesMeta: @@ -10754,6 +10784,7 @@ packages: file:playground/optimize-deps/nested-exclude: resolution: {directory: playground/optimize-deps/nested-exclude, type: directory} name: '@vitejs/test-nested-exclude' + version: 1.0.0 dependencies: '@vitejs/test-nested-include': file:playground/optimize-deps/nested-include nested-include: file:playground/optimize-deps/nested-include @@ -10762,11 +10793,13 @@ packages: file:playground/optimize-deps/nested-include: resolution: {directory: playground/optimize-deps/nested-include, type: directory} name: '@vitejs/test-nested-include' + version: 1.0.0 dev: false file:playground/optimize-missing-deps/missing-dep: resolution: {directory: playground/optimize-missing-deps/missing-dep, type: directory} name: '@vitejs/test-missing-dep' + version: 0.0.0 dependencies: '@vitejs/test-multi-entry-dep': file:playground/optimize-missing-deps/multi-entry-dep multi-entry-dep: file:playground/optimize-missing-deps/multi-entry-dep @@ -10775,15 +10808,18 @@ packages: file:playground/optimize-missing-deps/multi-entry-dep: resolution: {directory: playground/optimize-missing-deps/multi-entry-dep, type: directory} name: '@vitejs/test-multi-entry-dep' + version: 0.0.0 dev: false file:playground/preload/dep-a: resolution: {directory: playground/preload/dep-a, type: directory} name: '@vitejs/test-dep-a' + version: 0.0.0 file:playground/preload/dep-including-a: resolution: {directory: playground/preload/dep-including-a, type: directory} name: '@vitejs/test-dep-including-a' + version: 0.0.0 dependencies: '@vitejs/test-dep-a': file:playground/preload/dep-a dep-a: file:playground/preload/dep-a @@ -10792,26 +10828,31 @@ packages: file:playground/ssr-deps/css-lib: resolution: {directory: playground/ssr-deps/css-lib, type: directory} name: '@vitejs/test-css-lib' + version: 0.0.0 dev: false file:playground/ssr-deps/define-properties-exports: resolution: {directory: playground/ssr-deps/define-properties-exports, type: directory} name: '@vitejs/test-define-properties-exports' + version: 0.0.0 dev: false file:playground/ssr-deps/define-property-exports: resolution: {directory: playground/ssr-deps/define-property-exports, type: directory} name: '@vitejs/test-define-property-exports' + version: 0.0.0 dev: false file:playground/ssr-deps/external-entry: resolution: {directory: playground/ssr-deps/external-entry, type: directory} name: '@vitejs/test-external-entry' + version: 0.0.0 dev: false file:playground/ssr-deps/external-using-external-entry: resolution: {directory: playground/ssr-deps/external-using-external-entry, type: directory} name: '@vitejs/test-external-using-external-entry' + version: 0.0.0 dependencies: external-entry: file:playground/ssr-deps/external-entry dev: false @@ -10819,6 +10860,7 @@ packages: file:playground/ssr-deps/forwarded-export: resolution: {directory: playground/ssr-deps/forwarded-export, type: directory} name: '@vitejs/test-forwarded-export' + version: 0.0.0 dependencies: object-assigned-exports: file:playground/ssr-deps/object-assigned-exports dev: false @@ -10826,46 +10868,55 @@ packages: file:playground/ssr-deps/import-builtin-cjs: resolution: {directory: playground/ssr-deps/import-builtin-cjs, type: directory} name: '@vitejs/test-import-builtin' + version: 0.0.0 dev: false file:playground/ssr-deps/module-condition: resolution: {directory: playground/ssr-deps/module-condition, type: directory} name: '@vitejs/test-module-condition' + version: 0.0.0 dev: false file:playground/ssr-deps/nested-external: resolution: {directory: playground/ssr-deps/nested-external, type: directory} name: '@vitejs/test-nested-external' + version: 0.0.0 dev: false file:playground/ssr-deps/nested-external-cjs: resolution: {directory: playground/ssr-deps/nested-external-cjs, type: directory} name: nested-external-cjs + version: 0.0.0 dev: false file:playground/ssr-deps/no-external-cjs: resolution: {directory: playground/ssr-deps/no-external-cjs, type: directory} name: '@vitejs/test-no-external-cjs' + version: 0.0.0 dev: false file:playground/ssr-deps/no-external-css: resolution: {directory: playground/ssr-deps/no-external-css, type: directory} name: '@vitejs/test-no-external-css' + version: 0.0.0 dev: false file:playground/ssr-deps/object-assigned-exports: resolution: {directory: playground/ssr-deps/object-assigned-exports, type: directory} name: '@vitejs/test-object-assigned-exports' + version: 0.0.0 dev: false file:playground/ssr-deps/only-object-assigned-exports: resolution: {directory: playground/ssr-deps/only-object-assigned-exports, type: directory} name: '@vitejs/test-only-object-assigned-exports' + version: 0.0.0 dev: false file:playground/ssr-deps/optimized-with-nested-external: resolution: {directory: playground/ssr-deps/optimized-with-nested-external, type: directory} name: '@vitejs/test-optimized-with-nested-external' + version: 0.0.0 dependencies: nested-external: file:playground/ssr-deps/nested-external dev: false @@ -10873,36 +10924,43 @@ packages: file:playground/ssr-deps/pkg-exports: resolution: {directory: playground/ssr-deps/pkg-exports, type: directory} name: '@vitejs/test-pkg-exports' + version: 0.0.0 dev: false file:playground/ssr-deps/primitive-export: resolution: {directory: playground/ssr-deps/primitive-export, type: directory} name: '@vitejs/test-primitive-export' + version: 0.0.0 dev: false file:playground/ssr-deps/read-file-content: resolution: {directory: playground/ssr-deps/read-file-content, type: directory} name: '@vitejs/test-read-file-content' + version: 0.0.0 dev: false file:playground/ssr-deps/require-absolute: resolution: {directory: playground/ssr-deps/require-absolute, type: directory} name: '@vitejs/test-require-absolute' + version: 0.0.0 dev: false file:playground/ssr-deps/ts-transpiled-exports: resolution: {directory: playground/ssr-deps/ts-transpiled-exports, type: directory} name: '@vitejs/test-ts-transpiled-exports' + version: 0.0.0 dev: false file:playground/ssr-noexternal/external-cjs: resolution: {directory: playground/ssr-noexternal/external-cjs, type: directory} name: '@vitejs/test-external-cjs' + version: 0.0.0 dev: false file:playground/ssr-noexternal/require-external-cjs: resolution: {directory: playground/ssr-noexternal/require-external-cjs, type: directory} name: '@vitejs/test-require-external-cjs' + version: 0.0.0 dependencies: '@vitejs/external-cjs': file:playground/ssr-noexternal/external-cjs '@vitejs/test-external-cjs': file:playground/ssr-noexternal/external-cjs @@ -10911,19 +10969,23 @@ packages: file:playground/ssr-resolve/deep-import: resolution: {directory: playground/ssr-resolve/deep-import, type: directory} name: '@vitejs/test-deep-import' + version: 0.0.0 dev: false file:playground/ssr-resolve/entries: resolution: {directory: playground/ssr-resolve/entries, type: directory} name: '@vitejs/test-entries' + version: 0.0.0 dev: false file:playground/ssr-resolve/pkg-exports: resolution: {directory: playground/ssr-resolve/pkg-exports, type: directory} name: '@vitejs/test-resolve-pkg-exports' + version: 0.0.0 dev: false file:playground/worker/dep-to-optimize: resolution: {directory: playground/worker/dep-to-optimize, type: directory} name: '@vitejs/test-dep-to-optimize' + version: 1.0.0 dev: false From 2cbbf7a4497ae4cc283053389e591cd29d3c3fa0 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 23 Feb 2023 11:25:51 +0100 Subject: [PATCH 04/14] chore: kill server --- packages/vite/src/node/ssr/__tests__/runtimeClient.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vite/src/node/ssr/__tests__/runtimeClient.spec.ts b/packages/vite/src/node/ssr/__tests__/runtimeClient.spec.ts index de47f50dd2e8ee..5ddf321b644fb9 100644 --- a/packages/vite/src/node/ssr/__tests__/runtimeClient.spec.ts +++ b/packages/vite/src/node/ssr/__tests__/runtimeClient.spec.ts @@ -15,4 +15,5 @@ test('runtime executes a module', async () => { }) const module = await client.executeId('./fixtures/modules/ts-module.ts') expect(module.hello('bar')).toEqual({ foo: 'bar' }) + await server.close() }) From 8d7df1bb5483626a66620d33ae8c5f28f4eaa1e3 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 23 Feb 2023 11:27:56 +0100 Subject: [PATCH 05/14] chore: use performance form "perf_hooks" --- packages/vite/src/node/ssr/ssrRuntimeServer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vite/src/node/ssr/ssrRuntimeServer.ts b/packages/vite/src/node/ssr/ssrRuntimeServer.ts index 09bf428704a5a2..9e037b65ff6266 100644 --- a/packages/vite/src/node/ssr/ssrRuntimeServer.ts +++ b/packages/vite/src/node/ssr/ssrRuntimeServer.ts @@ -1,5 +1,6 @@ import path from 'node:path' import { existsSync } from 'node:fs' +import { performance } from 'node:perf_hooks' import { isNodeBuiltin, isValidNodeImport } from 'mlly' import type { PartialResolvedId, SourceMap } from 'rollup' import { KNOWN_ASSET_TYPES } from '../constants' From bfe0d1e440bb47b47dea20439c4e3b8f127bc892 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 23 Feb 2023 13:00:31 +0100 Subject: [PATCH 06/14] refactor: move "isBuiltin" to shared --- packages/vite/src/node/ssr/runtimeClient.ts | 14 ++------ packages/vite/src/node/utils.ts | 37 ++------------------- packages/vite/src/node/utils/shared.ts | 27 +++++++++++++++ 3 files changed, 33 insertions(+), 45 deletions(-) diff --git a/packages/vite/src/node/ssr/runtimeClient.ts b/packages/vite/src/node/ssr/runtimeClient.ts index 22501b8d524122..024b86848b8542 100644 --- a/packages/vite/src/node/ssr/runtimeClient.ts +++ b/packages/vite/src/node/ssr/runtimeClient.ts @@ -1,5 +1,5 @@ import { fileURLToPath, pathToFileURL } from 'node:url' -import { builtinModules, createRequire } from 'node:module' +import { createRequire } from 'node:module' import { dirname } from 'node:path' import vm from 'node:vm' import type { PartialResolvedId } from 'rollup' @@ -11,6 +11,7 @@ import { } from '../constants' import { cleanUrl, + isBuiltin, normalizeModuleId, normalizeRequestId, slash, @@ -18,13 +19,6 @@ import { } from '../utils/shared' import type { ViteRuntimeFetchResult } from './ssrRuntimeServer' -// we don't use mlly's "isNodeBuiltin" because it's bundled in a big chunk, -// and we don't want to rely on it -function isNodeBuiltin(id: string): boolean { - if (id.startsWith('node:')) return true - return builtinModules.includes(id) -} - // const debugExecute = createDebug('vite-runtime:client:execute') // const debugNative = createDebug('vite-runtime:client:native') @@ -232,9 +226,7 @@ export class ViteRuntimeClient { } protected shouldResolveId(id: string, _importee?: string): boolean { - return ( - ![CLIENT_PUBLIC_PATH, ENV_PUBLIC_PATH].includes(id) && !isNodeBuiltin(id) - ) + return ![CLIENT_PUBLIC_PATH, ENV_PUBLIC_PATH].includes(id) && !isBuiltin(id) } private async _resolveUrl( diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index c112420dd5bb61..6f32b3cdfae4c0 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -3,8 +3,8 @@ import os from 'node:os' import path from 'node:path' import { exec } from 'node:child_process' import { createHash } from 'node:crypto' -import { URL, URLSearchParams, fileURLToPath } from 'node:url' -import { builtinModules, createRequire } from 'node:module' +import { URLSearchParams, fileURLToPath } from 'node:url' +import { createRequire } from 'node:module' import { promises as dns } from 'node:dns' import { performance } from 'node:perf_hooks' import type { AddressInfo, Server } from 'node:net' @@ -36,7 +36,7 @@ import { import type { DepOptimizationConfig } from './optimizer' import type { ResolvedConfig } from './config' import type { ResolvedServerUrls, ViteDevServer } from './server' -import { resolvePackageData, type CommonServerOptions } from './index' +import { type CommonServerOptions, resolvePackageData } from './index' export * from './utils/shared' @@ -88,37 +88,6 @@ export const flattenId = (id: string): string => export const normalizeId = (id: string): string => id.replace(replaceNestedIdRE, ' > ') -//TODO: revisit later to see if the edge case that "compiling using node v12 code to be run in node v16 in the server" is what we intend to support. -const builtins = new Set([ - ...builtinModules, - 'assert/strict', - 'diagnostics_channel', - 'dns/promises', - 'fs/promises', - 'path/posix', - 'path/win32', - 'readline/promises', - 'stream/consumers', - 'stream/promises', - 'stream/web', - 'timers/promises', - 'util/types', - 'wasi', -]) - -const NODE_BUILTIN_NAMESPACE = 'node:' -export function isBuiltin(id: string): boolean { - return builtins.has( - id.startsWith(NODE_BUILTIN_NAMESPACE) - ? id.slice(NODE_BUILTIN_NAMESPACE.length) - : id, - ) -} - -export function isInNodeModules(id: string): boolean { - return id.includes('node_modules') -} - export function moduleListContains( moduleList: string[] | undefined, id: string, diff --git a/packages/vite/src/node/utils/shared.ts b/packages/vite/src/node/utils/shared.ts index eb956ae7f2b3d9..78508572351e4e 100644 --- a/packages/vite/src/node/utils/shared.ts +++ b/packages/vite/src/node/utils/shared.ts @@ -1,5 +1,6 @@ import path from 'node:path' import os from 'node:os' +import { builtinModules } from 'node:module' import { existsSync } from 'node:fs' import { fileURLToPath, pathToFileURL } from 'node:url' import { @@ -160,3 +161,29 @@ export function toFilePath( exists, } } + +//TODO: revisit later to see if the edge case that "compiling using node v12 code to be run in node v16 in the server" is what we intend to support. +const builtins = new Set([ + ...builtinModules, + 'assert/strict', + 'diagnostics_channel', + 'dns/promises', + 'fs/promises', + 'path/posix', + 'path/win32', + 'readline/promises', + 'stream/consumers', + 'stream/promises', + 'stream/web', + 'timers/promises', + 'util/types', + 'wasi', +]) + +export function isBuiltin(id: string): boolean { + return builtins.has(id.replace(/^node:/, '')) +} + +export function isInNodeModules(id: string): boolean { + return id.includes('node_modules') +} From 8b0534b5c4c7845a5a8d825aa549d97641748b82 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 23 Feb 2023 23:13:51 +0100 Subject: [PATCH 07/14] chore: use fetchModule instead of a custom ViteRuntimeServer --- packages/vite/src/node/index.ts | 9 +- packages/vite/src/node/server/index.ts | 7 + .../node/ssr/__tests__/runtimeClient.spec.ts | 5 +- packages/vite/src/node/ssr/runtimeClient.ts | 4 +- packages/vite/src/node/ssr/ssrFetchModule.ts | 62 +++ .../vite/src/node/ssr/ssrRuntimeServer.ts | 357 ------------------ 6 files changed, 75 insertions(+), 369 deletions(-) create mode 100644 packages/vite/src/node/ssr/ssrFetchModule.ts delete mode 100644 packages/vite/src/node/ssr/ssrRuntimeServer.ts diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index ccc5543b37b14b..6cc36b1eb581e9 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -146,10 +146,5 @@ export type { RollupDynamicImportVarsOptions } from 'dep-types/dynamicImportVars export type { Matcher, AnymatchPattern, AnymatchFn } from 'dep-types/anymatch' export type { LightningCSSOptions } from 'dep-types/lightningcss' -// TODO: naming? we already have ViteDevServer, it should probably just be a prt of it -export { ViteServer as ViteRuntimeServer } from './ssr/ssrRuntimeServer' -export type { - ViteRuntimeFetchResult, - ViteRuntimeServerDepsHandlingOptions, - ViteRuntimeServerOptions, -} from './ssr/ssrRuntimeServer' +export { ssrFetchModule } from './ssr/ssrFetchModule' +export type { FetchModuleResult as ViteRuntimeFetchResult } from './ssr/ssrFetchModule' diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index f41ba379eb91e7..17f34cc04f8306 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -46,6 +46,8 @@ import { CLIENT_DIR, DEFAULT_DEV_PORT } from '../constants' import type { Logger } from '../logger' import { printServerUrls } from '../logger' import { resolveChokidarOptions } from '../watch' +import type { FetchModuleResult } from '../ssr/ssrFetchModule' +import { ssrFetchModule } from '../ssr/ssrFetchModule' import type { PluginContainer } from './pluginContainer' import { createPluginContainer } from './pluginContainer' import type { WebSocketServer } from './ws' @@ -230,6 +232,8 @@ export interface ViteDevServer { html: string, originalUrl?: string, ): Promise + // TODO: docs + fetchModule(url: string, ssr?: boolean): Promise /** * Transform module code into SSR format. */ @@ -416,6 +420,9 @@ export async function _createServer( ssrRewriteStacktrace(stack: string) { return ssrRewriteStacktrace(stack, moduleGraph) }, + fetchModule(url, ssr = true) { + return ssrFetchModule(server, url, ssr) + }, async reloadModule(module) { if (serverConfig.hmr !== false && module.file) { updateModules(module.file, [module], Date.now(), server) diff --git a/packages/vite/src/node/ssr/__tests__/runtimeClient.spec.ts b/packages/vite/src/node/ssr/__tests__/runtimeClient.spec.ts index 5ddf321b644fb9..19b0bb27dcfccb 100644 --- a/packages/vite/src/node/ssr/__tests__/runtimeClient.spec.ts +++ b/packages/vite/src/node/ssr/__tests__/runtimeClient.spec.ts @@ -1,17 +1,16 @@ /* eslint-disable node/no-missing-import */ // @ts-expect-error TODO: add types, finalize entry point import { ViteRuntimeClient } from 'vite/runtime/client' -import { ViteRuntimeServer, createServer } from 'vite' +import { createServer } from 'vite' import { expect, test } from 'vitest' test('runtime executes a module', async () => { const server = await createServer({ root: __dirname, }) - const node = new ViteRuntimeServer(server) const client = new ViteRuntimeClient({ root: __dirname, - fetchModule: (id) => node.fetchModule(id), + fetchModule: (id) => server.fetchModule(id), }) const module = await client.executeId('./fixtures/modules/ts-module.ts') expect(module.hello('bar')).toEqual({ foo: 'bar' }) diff --git a/packages/vite/src/node/ssr/runtimeClient.ts b/packages/vite/src/node/ssr/runtimeClient.ts index 024b86848b8542..66bbbfc3a6193a 100644 --- a/packages/vite/src/node/ssr/runtimeClient.ts +++ b/packages/vite/src/node/ssr/runtimeClient.ts @@ -17,7 +17,7 @@ import { slash, toFilePath, } from '../utils/shared' -import type { ViteRuntimeFetchResult } from './ssrRuntimeServer' +import type { FetchModuleResult } from './ssrFetchModule' // const debugExecute = createDebug('vite-runtime:client:execute') // const debugNative = createDebug('vite-runtime:client:native') @@ -154,7 +154,7 @@ export const DEFAULT_REQUEST_STUBS = { interface ViteRuntimeClientOptions { moduleCache?: ModuleCacheMap root: string - fetchModule: (id: string) => Promise + fetchModule: (id: string) => Promise resolveId?: ( id: string, importer?: string, diff --git a/packages/vite/src/node/ssr/ssrFetchModule.ts b/packages/vite/src/node/ssr/ssrFetchModule.ts new file mode 100644 index 00000000000000..2afd7794409816 --- /dev/null +++ b/packages/vite/src/node/ssr/ssrFetchModule.ts @@ -0,0 +1,62 @@ +import type { SourceMap } from 'rollup' +import type { TransformResult } from '../server/transformRequest' +import type { ViteDevServer } from '../server' +import { shouldExternalizeForSSR } from './ssrExternal' + +export async function ssrFetchModule( + server: ViteDevServer, + id: string, + ssr = true, +): Promise { + if (shouldExternalizeForSSR(id, server.config)) { + return { externalize: id } + } + + let result: TransformResult | null + + if (!ssr) { + result = await server.transformRequest(id) + if (result) result = await server.ssrTransform(result.code, result.map, id) + } else { + result = await server.transformRequest(id, { ssr: true }) + } + + // TODO + // const sourcemap = server.config.sourcemap ?? 'inline' + if (result && !id.includes('node_modules')) withInlineSourcemap(result) + + return { code: result?.code } +} + +export interface FetchModuleResult { + code?: string + externalize?: string + map?: SourceMap | null +} + +let SOURCEMAPPING_URL = 'sourceMa' +SOURCEMAPPING_URL += 'ppingURL' + +const VITE_NODE_SOURCEMAPPING_SOURCE = '//# sourceMappingConsumer=vite' +const VITE_NODE_SOURCEMAPPING_URL = `${SOURCEMAPPING_URL}=data:application/json;charset=utf-8` +// const VITE_NODE_SOURCEMAPPING_REGEXP = new RegExp(`//# ${VITE_NODE_SOURCEMAPPING_URL};base64,(.+)`) + +export function withInlineSourcemap(result: TransformResult): TransformResult { + const map = result.map + let code = result.code + + if (!map || code.includes(VITE_NODE_SOURCEMAPPING_SOURCE)) return result + + // to reduce the payload size, we only inline vite source map, because it's also the only one we use + const OTHER_SOURCE_MAP_REGEXP = new RegExp( + `//# ${SOURCEMAPPING_URL}=data:application/json[^,]+base64,(.+)`, + 'g', + ) + while (OTHER_SOURCE_MAP_REGEXP.test(code)) + code = code.replace(OTHER_SOURCE_MAP_REGEXP, '') + + const sourceMap = Buffer.from(JSON.stringify(map), 'utf-8').toString('base64') + result.code = `${code.trimEnd()}\n\n${VITE_NODE_SOURCEMAPPING_SOURCE}\n//# ${VITE_NODE_SOURCEMAPPING_URL};base64,${sourceMap}\n` + + return result +} diff --git a/packages/vite/src/node/ssr/ssrRuntimeServer.ts b/packages/vite/src/node/ssr/ssrRuntimeServer.ts deleted file mode 100644 index 9e037b65ff6266..00000000000000 --- a/packages/vite/src/node/ssr/ssrRuntimeServer.ts +++ /dev/null @@ -1,357 +0,0 @@ -import path from 'node:path' -import { existsSync } from 'node:fs' -import { performance } from 'node:perf_hooks' -import { isNodeBuiltin, isValidNodeImport } from 'mlly' -import type { PartialResolvedId, SourceMap } from 'rollup' -import { KNOWN_ASSET_TYPES } from '../constants' -import type { TransformResult } from '../server/transformRequest' -import { - normalizeModuleId, - normalizePath, - slash, - toFilePath, -} from '../utils/shared' -import type { ViteDevServer } from '../server' - -export interface ViteRuntimeServerOptions { - /** - * Inject inline sourcemap to modules - * @default 'inline' - */ - sourcemap?: 'inline' | boolean - /** - * Deps handling - */ - deps?: ViteRuntimeServerDepsHandlingOptions - /** - * Transform method for modules - */ - transformMode?: { - ssr?: RegExp[] - web?: RegExp[] - } - - // debug?: DebuggerOptions -} - -export interface ViteRuntimeServerDepsHandlingOptions { - external?: (string | RegExp)[] - inline?: (string | RegExp)[] | true - /** - * Try to guess the CJS version of a package when it's invalid ESM - * @default false - */ - fallbackCJS?: boolean -} - -export interface ViteRuntimeFetchResult { - code?: string - externalize?: string - map?: SourceMap | null -} - -const ESM_EXT_RE = /\.(es|esm|esm-browser|esm-bundler|es6|module)\.js$/ -const ESM_FOLDER_RE = /\/(es|esm)\/(.*\.js)$/ - -const defaultInline = [ - /virtual:/, - /\.[mc]?ts$/, - - // special Vite query strings - /[?&](init|raw|url|inline)\b/, - // Vite returns a string for assets imports, even if it's inside "node_modules" - new RegExp(`\\.(${KNOWN_ASSET_TYPES.join('|')})$`), -] - -const depsExternal = [/\.cjs\.js$/, /\.mjs$/] - -export function guessCJSversion(id: string): string | undefined { - if (id.match(ESM_EXT_RE)) { - for (const i of [ - id.replace(ESM_EXT_RE, '.mjs'), - id.replace(ESM_EXT_RE, '.umd.js'), - id.replace(ESM_EXT_RE, '.cjs.js'), - id.replace(ESM_EXT_RE, '.js'), - ]) { - if (existsSync(i)) return i - } - } - if (id.match(ESM_FOLDER_RE)) { - for (const i of [ - id.replace(ESM_FOLDER_RE, '/umd/$1'), - id.replace(ESM_FOLDER_RE, '/cjs/$1'), - id.replace(ESM_FOLDER_RE, '/lib/$1'), - id.replace(ESM_FOLDER_RE, '/$1'), - ]) { - if (existsSync(i)) return i - } - } -} - -const _defaultExternalizeCache = new Map>() -export async function shouldExternalize( - id: string, - options?: ViteRuntimeServerDepsHandlingOptions, - cache = _defaultExternalizeCache, -): Promise { - if (!cache.has(id)) cache.set(id, _shouldExternalize(id, options)) - return cache.get(id)! -} - -async function _shouldExternalize( - id: string, - options?: ViteRuntimeServerDepsHandlingOptions, -): Promise { - if (isNodeBuiltin(id)) return id - - // data: should be processed by native import, - // since it is a feature of ESM - if (id.startsWith('data:')) return id - - id = patchWindowsImportPath(id) - - if (matchExternalizePattern(id, options?.inline)) return false - if (matchExternalizePattern(id, options?.external)) return id - - const isNodeModule = id.includes('/node_modules/') - const guessCJS = isNodeModule && options?.fallbackCJS - id = guessCJS ? guessCJSversion(id) || id : id - - if (matchExternalizePattern(id, defaultInline)) return false - if (matchExternalizePattern(id, depsExternal)) return id - - const isDist = id.includes('/dist/') - if ((isNodeModule || isDist) && (await isValidNodeImport(id))) return id - - return false -} - -function matchExternalizePattern( - id: string, - patterns?: (string | RegExp)[] | true, -) { - if (patterns == null) return false - if (patterns === true) return true - for (const ex of patterns) { - if (typeof ex === 'string') { - if (id.includes(`/node_modules/${ex}/`)) return true - } else { - if (ex.test(id)) return true - } - } - return false -} - -function patchWindowsImportPath(path: string) { - if (path.match(/^\w:\\/)) return `file:///${slash(path)}` - else if (path.match(/^\w:\//)) return `file:///${path}` - else return path -} - -let SOURCEMAPPING_URL = 'sourceMa' -SOURCEMAPPING_URL += 'ppingURL' - -const VITE_NODE_SOURCEMAPPING_SOURCE = '//# sourceMappingConsumer=vite' -const VITE_NODE_SOURCEMAPPING_URL = `${SOURCEMAPPING_URL}=data:application/json;charset=utf-8` -// const VITE_NODE_SOURCEMAPPING_REGEXP = new RegExp(`//# ${VITE_NODE_SOURCEMAPPING_URL};base64,(.+)`) - -export function withInlineSourcemap(result: TransformResult): TransformResult { - const map = result.map - let code = result.code - - if (!map || code.includes(VITE_NODE_SOURCEMAPPING_SOURCE)) return result - - // to reduce the payload size, we only inline vite source map, because it's also the only one we use - const OTHER_SOURCE_MAP_REGEXP = new RegExp( - `//# ${SOURCEMAPPING_URL}=data:application/json[^,]+base64,(.+)`, - 'g', - ) - while (OTHER_SOURCE_MAP_REGEXP.test(code)) - code = code.replace(OTHER_SOURCE_MAP_REGEXP, '') - - const sourceMap = Buffer.from(JSON.stringify(map), 'utf-8').toString('base64') - result.code = `${code.trimEnd()}\n\n${VITE_NODE_SOURCEMAPPING_SOURCE}\n//# ${VITE_NODE_SOURCEMAPPING_URL};base64,${sourceMap}\n` - - return result -} - -export class ViteServer { - private fetchPromiseMap = new Map>() - private transformPromiseMap = new Map< - string, - Promise - >() - - fetchCache = new Map< - string, - { - duration?: number - timestamp: number - result: ViteRuntimeFetchResult - } - >() - - externalizeCache = new Map>() - - // debugger?: Debugger - - constructor( - public server: ViteDevServer, - public options: ViteRuntimeServerOptions = {}, - ) { - const ssrOptions = server.config.ssr - if (ssrOptions) { - options.deps ??= {} - - // we don't externalize ssr, because it has different semantics in Vite - // if (ssrOptions.external) { - // options.deps.external ??= [] - // options.deps.external.push(...ssrOptions.external) - // } - - if (ssrOptions.noExternal === true) { - options.deps.inline ??= true - } else if (options.deps.inline !== true) { - options.deps.inline ??= [] - const noExternal = Array.isArray(ssrOptions.noExternal) - ? ssrOptions.noExternal - : (ssrOptions.noExternal && [ssrOptions.noExternal]) || [] - options.deps.inline.push(...noExternal) - } - } - // if (process.env.VITE_NODE_DEBUG_DUMP) { - // options.debug = Object.assign({ - // dumpModules: !!process.env.VITE_NODE_DEBUG_DUMP, - // loadDumppedModules: process.env.VITE_NODE_DEBUG_DUMP === 'load', - // }, options.debug ?? {}) - // } - // if (options.debug) - // this.debugger = new Debugger(server.config.root, options.debug!) - } - - shouldExternalize(id: string): Promise { - return shouldExternalize(id, this.options.deps, this.externalizeCache) - } - - async resolveId( - id: string, - importer?: string, - ): Promise { - if (importer && !importer.startsWith(this.server.config.root)) - importer = normalizePath(path.resolve(this.server.config.root, importer)) - const mode = (importer && this.getTransformMode(importer)) || 'ssr' - return this.server.pluginContainer.resolveId(id, importer, { - ssr: mode === 'ssr', - }) - } - - async fetchModule(id: string): Promise { - id = normalizeModuleId(id) - // reuse transform for concurrent requests - if (!this.fetchPromiseMap.has(id)) { - this.fetchPromiseMap.set( - id, - this._fetchModule(id) - .then((r) => { - return this.options.sourcemap !== true - ? { ...r, map: undefined } - : r - }) - .finally(() => { - this.fetchPromiseMap.delete(id) - }), - ) - } - return this.fetchPromiseMap.get(id)! - } - - async transformRequest( - id: string, - ): Promise { - // reuse transform for concurrent requests - if (!this.transformPromiseMap.has(id)) { - this.transformPromiseMap.set( - id, - this._transformRequest(id).finally(() => { - this.transformPromiseMap.delete(id) - }), - ) - } - return this.transformPromiseMap.get(id)! - } - - getTransformMode(id: string): 'web' | 'ssr' { - const withoutQuery = id.split('?')[0] - - if (this.options.transformMode?.web?.some((r) => withoutQuery.match(r))) - return 'web' - if (this.options.transformMode?.ssr?.some((r) => withoutQuery.match(r))) - return 'ssr' - - if (withoutQuery.match(/\.([cm]?[jt]sx?|json)$/)) return 'ssr' - return 'web' - } - - private async _fetchModule(id: string): Promise { - let result: ViteRuntimeFetchResult - - const { path: filePath } = toFilePath(id, this.server.config.root) - - const module = this.server.moduleGraph.getModuleById(id) - const timestamp = module ? module.lastHMRTimestamp : null - const cache = this.fetchCache.get(filePath) - if (timestamp && cache && cache.timestamp >= timestamp) return cache.result - - const time = Date.now() - const externalize = await this.shouldExternalize(filePath) - let duration: number | undefined - if (typeof externalize === 'string') { - result = { externalize } - // this.debugger?.recordExternalize(id, externalize) - } else { - const start = performance.now() - const r = await this._transformRequest(id) - duration = performance.now() - start - result = { code: r?.code, map: r?.map } - } - - this.fetchCache.set(filePath, { - duration, - timestamp: time, - result, - }) - - return result - } - - private async _transformRequest(id: string) { - // debugRequest(id) - - let result: TransformResult | null = null - - // if (this.options.debug?.loadDumppedModules) { - // result = await this.debugger?.loadDump(id) ?? null - // if (result) - // return result - // } - - if (this.getTransformMode(id) === 'web') { - // for components like Vue, we want to use the client side - // plugins but then convert the code to be consumed by the server - result = await this.server.transformRequest(id) - if (result) - result = await this.server.ssrTransform(result.code, result.map, id) - } else { - result = await this.server.transformRequest(id, { ssr: true }) - } - - const sourcemap = this.options.sourcemap ?? 'inline' - if (sourcemap === 'inline' && result && !id.includes('node_modules')) - withInlineSourcemap(result) - - // if (this.options.debug?.dumpModules) - // await this.debugger?.dumpFile(id, result) - - return result - } -} From d580dcd7a09d75f39875845154f6526329d1b5f2 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 23 Feb 2023 23:14:56 +0100 Subject: [PATCH 08/14] chore: add example --- ts-module.ts | 11 +++++++++++ vite-node.mjs | 25 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 ts-module.ts create mode 100644 vite-node.mjs diff --git a/ts-module.ts b/ts-module.ts new file mode 100644 index 00000000000000..48b8f59013de2b --- /dev/null +++ b/ts-module.ts @@ -0,0 +1,11 @@ +import { Buffer } from 'node:buffer' +import { defineComponent } from 'vue' + +interface ReturnValue { + foo: string +} + +export function hello(foo: string): ReturnValue { + defineComponent + return { foo: Buffer.from(foo).toString('base64') } +} diff --git a/vite-node.mjs b/vite-node.mjs new file mode 100644 index 00000000000000..8d8da92449a4b0 --- /dev/null +++ b/vite-node.mjs @@ -0,0 +1,25 @@ +// just an example to test vite-node +// run with: "node vite-node.mjs" +/* eslint-disable node/no-missing-import */ +import { dirname } from 'node:path' +import { ViteRuntimeClient } from 'vite/runtime/client' +import { createServer } from 'vite' +const dir = dirname(new URL(import.meta.url).pathname) + +const server = await createServer({ + root: dir, + ssr: { + noExternal: ['vue'], + }, +}) +const client = new ViteRuntimeClient({ + root: dir, + fetchModule: async (id) => { + const res = await server.fetchModule(id, true) + console.log(res) + return res + }, +}) +const module = await client.executeId('./ts-module.ts') +console.log(module.hello('bar')) +await server.close() From c3d006b713f14c74b078438341a87e728943084c Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 23 Feb 2023 23:23:01 +0100 Subject: [PATCH 09/14] chore: initialize optimizer when fetching a module --- packages/vite/src/node/server/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 17f34cc04f8306..8a0ea667e8f73e 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -420,7 +420,11 @@ export async function _createServer( ssrRewriteStacktrace(stack: string) { return ssrRewriteStacktrace(stack, moduleGraph) }, - fetchModule(url, ssr = true) { + async fetchModule(url, ssr = true) { + if (isDepsOptimizerEnabled(config, true)) { + await initDevSsrDepsOptimizer(config, server) + } + await updateCjsSsrExternals(server) return ssrFetchModule(server, url, ssr) }, async reloadModule(module) { From b30f732eee1d725b260e219ed5b0092b61d5f17e Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 20 Mar 2023 10:06:09 +0100 Subject: [PATCH 10/14] chore: sourcemap is optional --- packages/vite/src/node/index.ts | 2 +- packages/vite/src/node/server/index.ts | 14 +++++++++---- packages/vite/src/node/ssr/ssrFetchModule.ts | 22 ++++++++++++-------- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index 6cc36b1eb581e9..bc80bc7365f475 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -146,5 +146,5 @@ export type { RollupDynamicImportVarsOptions } from 'dep-types/dynamicImportVars export type { Matcher, AnymatchPattern, AnymatchFn } from 'dep-types/anymatch' export type { LightningCSSOptions } from 'dep-types/lightningcss' -export { ssrFetchModule } from './ssr/ssrFetchModule' +export { ssrFetchModule, withInlineSourcemap } from './ssr/ssrFetchModule' export type { FetchModuleResult as ViteRuntimeFetchResult } from './ssr/ssrFetchModule' diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 8a0ea667e8f73e..de33a6effe4d46 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -46,7 +46,10 @@ import { CLIENT_DIR, DEFAULT_DEV_PORT } from '../constants' import type { Logger } from '../logger' import { printServerUrls } from '../logger' import { resolveChokidarOptions } from '../watch' -import type { FetchModuleResult } from '../ssr/ssrFetchModule' +import type { + FetchModuleOptions, + FetchModuleResult, +} from '../ssr/ssrFetchModule' import { ssrFetchModule } from '../ssr/ssrFetchModule' import type { PluginContainer } from './pluginContainer' import { createPluginContainer } from './pluginContainer' @@ -233,7 +236,10 @@ export interface ViteDevServer { originalUrl?: string, ): Promise // TODO: docs - fetchModule(url: string, ssr?: boolean): Promise + fetchModule( + url: string, + options?: FetchModuleOptions, + ): Promise /** * Transform module code into SSR format. */ @@ -420,12 +426,12 @@ export async function _createServer( ssrRewriteStacktrace(stack: string) { return ssrRewriteStacktrace(stack, moduleGraph) }, - async fetchModule(url, ssr = true) { + async fetchModule(url, options: FetchModuleOptions = {}) { if (isDepsOptimizerEnabled(config, true)) { await initDevSsrDepsOptimizer(config, server) } await updateCjsSsrExternals(server) - return ssrFetchModule(server, url, ssr) + return ssrFetchModule(server, url, options) }, async reloadModule(module) { if (serverConfig.hmr !== false && module.file) { diff --git a/packages/vite/src/node/ssr/ssrFetchModule.ts b/packages/vite/src/node/ssr/ssrFetchModule.ts index 2afd7794409816..9e8f0a87496f06 100644 --- a/packages/vite/src/node/ssr/ssrFetchModule.ts +++ b/packages/vite/src/node/ssr/ssrFetchModule.ts @@ -3,10 +3,15 @@ import type { TransformResult } from '../server/transformRequest' import type { ViteDevServer } from '../server' import { shouldExternalizeForSSR } from './ssrExternal' +export interface FetchModuleOptions { + ssr?: boolean + sourcemep?: boolean | 'inline' +} + export async function ssrFetchModule( server: ViteDevServer, id: string, - ssr = true, + { ssr = true, sourcemep = 'inline' }: FetchModuleOptions, ): Promise { if (shouldExternalizeForSSR(id, server.config)) { return { externalize: id } @@ -21,11 +26,10 @@ export async function ssrFetchModule( result = await server.transformRequest(id, { ssr: true }) } - // TODO - // const sourcemap = server.config.sourcemap ?? 'inline' - if (result && !id.includes('node_modules')) withInlineSourcemap(result) + if (sourcemep === 'inline' && result && !id.includes('node_modules')) + withInlineSourcemap(result) - return { code: result?.code } + return { code: result?.code, map: result?.map } } export interface FetchModuleResult { @@ -37,15 +41,15 @@ export interface FetchModuleResult { let SOURCEMAPPING_URL = 'sourceMa' SOURCEMAPPING_URL += 'ppingURL' -const VITE_NODE_SOURCEMAPPING_SOURCE = '//# sourceMappingConsumer=vite' -const VITE_NODE_SOURCEMAPPING_URL = `${SOURCEMAPPING_URL}=data:application/json;charset=utf-8` +const VITE_SOURCEMAPPING_SOURCE = '//# sourceMappingConsumer=vite' +const VITE_SOURCEMAPPING_URL = `${SOURCEMAPPING_URL}=data:application/json;charset=utf-8` // const VITE_NODE_SOURCEMAPPING_REGEXP = new RegExp(`//# ${VITE_NODE_SOURCEMAPPING_URL};base64,(.+)`) export function withInlineSourcemap(result: TransformResult): TransformResult { const map = result.map let code = result.code - if (!map || code.includes(VITE_NODE_SOURCEMAPPING_SOURCE)) return result + if (!map || code.includes(VITE_SOURCEMAPPING_SOURCE)) return result // to reduce the payload size, we only inline vite source map, because it's also the only one we use const OTHER_SOURCE_MAP_REGEXP = new RegExp( @@ -56,7 +60,7 @@ export function withInlineSourcemap(result: TransformResult): TransformResult { code = code.replace(OTHER_SOURCE_MAP_REGEXP, '') const sourceMap = Buffer.from(JSON.stringify(map), 'utf-8').toString('base64') - result.code = `${code.trimEnd()}\n\n${VITE_NODE_SOURCEMAPPING_SOURCE}\n//# ${VITE_NODE_SOURCEMAPPING_URL};base64,${sourceMap}\n` + result.code = `${code.trimEnd()}\n\n${VITE_SOURCEMAPPING_SOURCE}\n//# ${VITE_SOURCEMAPPING_URL};base64,${sourceMap}\n` return result } From 7094c37b3f4f707fd4f7f867b3bf9f4d81534c57 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 3 Jul 2023 16:24:46 +0200 Subject: [PATCH 11/14] chore: revert not intended changes --- packages/vite/src/node/plugins/css.ts | 2 +- pnpm-lock.yaml | 62 --------------------------- 2 files changed, 1 insertion(+), 63 deletions(-) diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index c77a40eef108b4..d7c0d6d71760b4 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -167,7 +167,7 @@ export function resolveCSSOptions( const cssModuleRE = new RegExp(`\\.module${CSS_LANGS_RE.source}`) -const htmlProxyRE = /(?:\?|&)html-proxy\b/ +const htmlProxyRE = /[?&]html-proxy\b/ const commonjsProxyRE = /\?commonjs-proxy/ const inlineRE = /[?&]inline\b/ const inlineCSSRE = /[?&]inline-css\b/ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 740ad553de1578..161c84820f9aae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10588,19 +10588,16 @@ packages: file:playground/alias/dir/module: resolution: {directory: playground/alias/dir/module, type: directory} name: '@vitejs/test-aliased-module' - version: 0.0.0 dev: false file:playground/css/css-js-dep: resolution: {directory: playground/css/css-js-dep, type: directory} name: '@vitejs/test-css-js-dep' - version: 1.0.0 dev: true file:playground/css/css-proxy-dep: resolution: {directory: playground/css/css-proxy-dep, type: directory} name: '@vitejs/test-css-proxy-dep' - version: 1.0.0 dependencies: '@vitejs/test-css-proxy-dep-nested': file:playground/css/css-proxy-dep-nested dev: true @@ -10608,24 +10605,20 @@ packages: file:playground/css/css-proxy-dep-nested: resolution: {directory: playground/css/css-proxy-dep-nested, type: directory} name: '@vitejs/test-css-proxy-dep-nested' - version: 1.0.0 file:playground/define/commonjs-dep: resolution: {directory: playground/define/commonjs-dep, type: directory} name: '@vitejs/test-commonjs-dep' - version: 1.0.0 dev: false file:playground/dynamic-import/pkg: resolution: {directory: playground/dynamic-import/pkg, type: directory} name: '@vitejs/test-pkg' - version: 1.0.0 dev: false file:playground/external/dep-that-imports: resolution: {directory: playground/external/dep-that-imports, type: directory} name: '@vitejs/test-dep-that-imports' - version: 0.0.0 dependencies: slash3: /slash@3.0.0 slash5: /slash@5.1.0 @@ -10635,7 +10628,6 @@ packages: file:playground/external/dep-that-requires: resolution: {directory: playground/external/dep-that-requires, type: directory} name: '@vitejs/test-dep-that-requires' - version: 0.0.0 dependencies: slash3: /slash@3.0.0 slash5: /slash@5.1.0 @@ -10645,37 +10637,31 @@ packages: file:playground/import-assertion/import-assertion-dep: resolution: {directory: playground/import-assertion/import-assertion-dep, type: directory} name: '@vitejs/test-import-assertion-dep' - version: 0.0.0 dev: false file:playground/json/json-module: resolution: {directory: playground/json/json-module, type: directory} name: '@vitejs/test-json-module' - version: 0.0.0 dev: true file:playground/minify/dir/module: resolution: {directory: playground/minify/dir/module, type: directory} name: '@vitejs/test-minify' - version: 0.0.0 dev: false file:playground/optimize-deps-no-discovery/dep-no-discovery: resolution: {directory: playground/optimize-deps-no-discovery/dep-no-discovery, type: directory} name: '@vitejs/test-dep-no-discovery' - version: 1.0.0 dev: false file:playground/optimize-deps/added-in-entries: resolution: {directory: playground/optimize-deps/added-in-entries, type: directory} name: '@vitejs/test-added-in-entries' - version: 1.0.0 dev: false file:playground/optimize-deps/dep-alias-using-absolute-path: resolution: {directory: playground/optimize-deps/dep-alias-using-absolute-path, type: directory} name: '@vitejs/test-dep-alias-using-absolute-path' - version: 1.0.0 dependencies: lodash: 4.17.21 dev: false @@ -10683,97 +10669,81 @@ packages: file:playground/optimize-deps/dep-cjs-browser-field-bare: resolution: {directory: playground/optimize-deps/dep-cjs-browser-field-bare, type: directory} name: '@vitejs/test-dep-cjs-browser-field-bare' - version: 0.0.0 dev: false file:playground/optimize-deps/dep-cjs-compiled-from-cjs: resolution: {directory: playground/optimize-deps/dep-cjs-compiled-from-cjs, type: directory} name: '@vitejs/test-dep-cjs-compiled-from-cjs' - version: 0.0.0 dev: false file:playground/optimize-deps/dep-cjs-compiled-from-esm: resolution: {directory: playground/optimize-deps/dep-cjs-compiled-from-esm, type: directory} name: '@vitejs/test-dep-cjs-compiled-from-esm' - version: 0.0.0 dev: false file:playground/optimize-deps/dep-cjs-with-assets: resolution: {directory: playground/optimize-deps/dep-cjs-with-assets, type: directory} name: '@vitejs/test-dep-cjs-with-assets' - version: 0.0.0 dev: false file:playground/optimize-deps/dep-css-require: resolution: {directory: playground/optimize-deps/dep-css-require, type: directory} name: '@vitejs/test-dep-css-require' - version: 0.0.0 dev: false file:playground/optimize-deps/dep-esbuild-plugin-transform: resolution: {directory: playground/optimize-deps/dep-esbuild-plugin-transform, type: directory} name: '@vitejs/test-dep-esbuild-plugin-transform' - version: 0.0.0 dev: false file:playground/optimize-deps/dep-node-env: resolution: {directory: playground/optimize-deps/dep-node-env, type: directory} name: '@vitejs/test-dep-node-env' - version: 1.0.0 dev: false file:playground/optimize-deps/dep-non-optimized: resolution: {directory: playground/optimize-deps/dep-non-optimized, type: directory} name: '@vitejs/test-dep-non-optimized' - version: 1.0.0 dev: false file:playground/optimize-deps/dep-not-js: resolution: {directory: playground/optimize-deps/dep-not-js, type: directory} name: '@vitejs/test-dep-not-js' - version: 1.0.0 dev: false file:playground/optimize-deps/dep-optimize-exports-with-glob: resolution: {directory: playground/optimize-deps/dep-optimize-exports-with-glob, type: directory} name: '@vitejs/test-dep-optimize-exports-with-glob' - version: 1.0.0 dev: false file:playground/optimize-deps/dep-optimize-with-glob: resolution: {directory: playground/optimize-deps/dep-optimize-with-glob, type: directory} name: '@vitejs/test-dep-optimize-with-glob' - version: 1.0.0 dev: false file:playground/optimize-deps/dep-relative-to-main: resolution: {directory: playground/optimize-deps/dep-relative-to-main, type: directory} name: '@vitejs/test-dep-relative-to-main' - version: 1.0.0 dev: false file:playground/optimize-deps/dep-with-builtin-module-cjs: resolution: {directory: playground/optimize-deps/dep-with-builtin-module-cjs, type: directory} name: '@vitejs/test-dep-with-builtin-module-cjs' - version: 0.0.0 dev: false file:playground/optimize-deps/dep-with-builtin-module-esm: resolution: {directory: playground/optimize-deps/dep-with-builtin-module-esm, type: directory} name: '@vitejs/test-dep-with-builtin-module-esm' - version: 0.0.0 dev: false file:playground/optimize-deps/dep-with-dynamic-import: resolution: {directory: playground/optimize-deps/dep-with-dynamic-import, type: directory} name: '@vitejs/test-dep-with-dynamic-import' - version: 0.0.0 dev: false file:playground/optimize-deps/dep-with-optional-peer-dep: resolution: {directory: playground/optimize-deps/dep-with-optional-peer-dep, type: directory} name: '@vitejs/test-dep-with-optional-peer-dep' - version: 0.0.0 peerDependencies: foobar: 0.0.0 peerDependenciesMeta: @@ -10784,7 +10754,6 @@ packages: file:playground/optimize-deps/nested-exclude: resolution: {directory: playground/optimize-deps/nested-exclude, type: directory} name: '@vitejs/test-nested-exclude' - version: 1.0.0 dependencies: '@vitejs/test-nested-include': file:playground/optimize-deps/nested-include nested-include: file:playground/optimize-deps/nested-include @@ -10793,13 +10762,11 @@ packages: file:playground/optimize-deps/nested-include: resolution: {directory: playground/optimize-deps/nested-include, type: directory} name: '@vitejs/test-nested-include' - version: 1.0.0 dev: false file:playground/optimize-missing-deps/missing-dep: resolution: {directory: playground/optimize-missing-deps/missing-dep, type: directory} name: '@vitejs/test-missing-dep' - version: 0.0.0 dependencies: '@vitejs/test-multi-entry-dep': file:playground/optimize-missing-deps/multi-entry-dep multi-entry-dep: file:playground/optimize-missing-deps/multi-entry-dep @@ -10808,18 +10775,15 @@ packages: file:playground/optimize-missing-deps/multi-entry-dep: resolution: {directory: playground/optimize-missing-deps/multi-entry-dep, type: directory} name: '@vitejs/test-multi-entry-dep' - version: 0.0.0 dev: false file:playground/preload/dep-a: resolution: {directory: playground/preload/dep-a, type: directory} name: '@vitejs/test-dep-a' - version: 0.0.0 file:playground/preload/dep-including-a: resolution: {directory: playground/preload/dep-including-a, type: directory} name: '@vitejs/test-dep-including-a' - version: 0.0.0 dependencies: '@vitejs/test-dep-a': file:playground/preload/dep-a dep-a: file:playground/preload/dep-a @@ -10828,31 +10792,26 @@ packages: file:playground/ssr-deps/css-lib: resolution: {directory: playground/ssr-deps/css-lib, type: directory} name: '@vitejs/test-css-lib' - version: 0.0.0 dev: false file:playground/ssr-deps/define-properties-exports: resolution: {directory: playground/ssr-deps/define-properties-exports, type: directory} name: '@vitejs/test-define-properties-exports' - version: 0.0.0 dev: false file:playground/ssr-deps/define-property-exports: resolution: {directory: playground/ssr-deps/define-property-exports, type: directory} name: '@vitejs/test-define-property-exports' - version: 0.0.0 dev: false file:playground/ssr-deps/external-entry: resolution: {directory: playground/ssr-deps/external-entry, type: directory} name: '@vitejs/test-external-entry' - version: 0.0.0 dev: false file:playground/ssr-deps/external-using-external-entry: resolution: {directory: playground/ssr-deps/external-using-external-entry, type: directory} name: '@vitejs/test-external-using-external-entry' - version: 0.0.0 dependencies: external-entry: file:playground/ssr-deps/external-entry dev: false @@ -10860,7 +10819,6 @@ packages: file:playground/ssr-deps/forwarded-export: resolution: {directory: playground/ssr-deps/forwarded-export, type: directory} name: '@vitejs/test-forwarded-export' - version: 0.0.0 dependencies: object-assigned-exports: file:playground/ssr-deps/object-assigned-exports dev: false @@ -10868,55 +10826,46 @@ packages: file:playground/ssr-deps/import-builtin-cjs: resolution: {directory: playground/ssr-deps/import-builtin-cjs, type: directory} name: '@vitejs/test-import-builtin' - version: 0.0.0 dev: false file:playground/ssr-deps/module-condition: resolution: {directory: playground/ssr-deps/module-condition, type: directory} name: '@vitejs/test-module-condition' - version: 0.0.0 dev: false file:playground/ssr-deps/nested-external: resolution: {directory: playground/ssr-deps/nested-external, type: directory} name: '@vitejs/test-nested-external' - version: 0.0.0 dev: false file:playground/ssr-deps/nested-external-cjs: resolution: {directory: playground/ssr-deps/nested-external-cjs, type: directory} name: nested-external-cjs - version: 0.0.0 dev: false file:playground/ssr-deps/no-external-cjs: resolution: {directory: playground/ssr-deps/no-external-cjs, type: directory} name: '@vitejs/test-no-external-cjs' - version: 0.0.0 dev: false file:playground/ssr-deps/no-external-css: resolution: {directory: playground/ssr-deps/no-external-css, type: directory} name: '@vitejs/test-no-external-css' - version: 0.0.0 dev: false file:playground/ssr-deps/object-assigned-exports: resolution: {directory: playground/ssr-deps/object-assigned-exports, type: directory} name: '@vitejs/test-object-assigned-exports' - version: 0.0.0 dev: false file:playground/ssr-deps/only-object-assigned-exports: resolution: {directory: playground/ssr-deps/only-object-assigned-exports, type: directory} name: '@vitejs/test-only-object-assigned-exports' - version: 0.0.0 dev: false file:playground/ssr-deps/optimized-with-nested-external: resolution: {directory: playground/ssr-deps/optimized-with-nested-external, type: directory} name: '@vitejs/test-optimized-with-nested-external' - version: 0.0.0 dependencies: nested-external: file:playground/ssr-deps/nested-external dev: false @@ -10924,43 +10873,36 @@ packages: file:playground/ssr-deps/pkg-exports: resolution: {directory: playground/ssr-deps/pkg-exports, type: directory} name: '@vitejs/test-pkg-exports' - version: 0.0.0 dev: false file:playground/ssr-deps/primitive-export: resolution: {directory: playground/ssr-deps/primitive-export, type: directory} name: '@vitejs/test-primitive-export' - version: 0.0.0 dev: false file:playground/ssr-deps/read-file-content: resolution: {directory: playground/ssr-deps/read-file-content, type: directory} name: '@vitejs/test-read-file-content' - version: 0.0.0 dev: false file:playground/ssr-deps/require-absolute: resolution: {directory: playground/ssr-deps/require-absolute, type: directory} name: '@vitejs/test-require-absolute' - version: 0.0.0 dev: false file:playground/ssr-deps/ts-transpiled-exports: resolution: {directory: playground/ssr-deps/ts-transpiled-exports, type: directory} name: '@vitejs/test-ts-transpiled-exports' - version: 0.0.0 dev: false file:playground/ssr-noexternal/external-cjs: resolution: {directory: playground/ssr-noexternal/external-cjs, type: directory} name: '@vitejs/test-external-cjs' - version: 0.0.0 dev: false file:playground/ssr-noexternal/require-external-cjs: resolution: {directory: playground/ssr-noexternal/require-external-cjs, type: directory} name: '@vitejs/test-require-external-cjs' - version: 0.0.0 dependencies: '@vitejs/external-cjs': file:playground/ssr-noexternal/external-cjs '@vitejs/test-external-cjs': file:playground/ssr-noexternal/external-cjs @@ -10969,23 +10911,19 @@ packages: file:playground/ssr-resolve/deep-import: resolution: {directory: playground/ssr-resolve/deep-import, type: directory} name: '@vitejs/test-deep-import' - version: 0.0.0 dev: false file:playground/ssr-resolve/entries: resolution: {directory: playground/ssr-resolve/entries, type: directory} name: '@vitejs/test-entries' - version: 0.0.0 dev: false file:playground/ssr-resolve/pkg-exports: resolution: {directory: playground/ssr-resolve/pkg-exports, type: directory} name: '@vitejs/test-resolve-pkg-exports' - version: 0.0.0 dev: false file:playground/worker/dep-to-optimize: resolution: {directory: playground/worker/dep-to-optimize, type: directory} name: '@vitejs/test-dep-to-optimize' - version: 1.0.0 dev: false From 620572d2e887c1b89622e02fce7cf3e9fe215542 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 3 Jul 2023 16:56:37 +0200 Subject: [PATCH 12/14] chore: update vite-node to the latest version --- packages/vite/src/node/index.ts | 7 +- packages/vite/src/node/server/index.ts | 6 +- packages/vite/src/node/ssr/runtimeClient.ts | 160 +++--- .../vite/src/node/ssr/sourceMapHandler.ts | 462 ++++++++++++++++++ packages/vite/src/node/ssr/ssrFetchModule.ts | 45 +- packages/vite/src/node/ssr/ssrSourceMap.ts | 65 +++ 6 files changed, 636 insertions(+), 109 deletions(-) create mode 100644 packages/vite/src/node/ssr/sourceMapHandler.ts create mode 100644 packages/vite/src/node/ssr/ssrSourceMap.ts diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index bc80bc7365f475..1ec8044ecfa6bb 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -146,5 +146,10 @@ export type { RollupDynamicImportVarsOptions } from 'dep-types/dynamicImportVars export type { Matcher, AnymatchPattern, AnymatchFn } from 'dep-types/anymatch' export type { LightningCSSOptions } from 'dep-types/lightningcss' -export { ssrFetchModule, withInlineSourcemap } from './ssr/ssrFetchModule' +export { ssrFetchModule } from './ssr/ssrFetchModule' +export { withInlineSourcemap } from './ssr/ssrSourceMap' +export { + installSourceMapHandler, + resetRetrieveSourceMapHandlers, +} from './ssr/sourceMapHandler' export type { FetchModuleResult as ViteRuntimeFetchResult } from './ssr/ssrFetchModule' diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index de33a6effe4d46..cac77ce697cc06 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -236,7 +236,7 @@ export interface ViteDevServer { originalUrl?: string, ): Promise // TODO: docs - fetchModule( + ssrFetchModule( url: string, options?: FetchModuleOptions, ): Promise @@ -426,12 +426,12 @@ export async function _createServer( ssrRewriteStacktrace(stack: string) { return ssrRewriteStacktrace(stack, moduleGraph) }, - async fetchModule(url, options: FetchModuleOptions = {}) { + async ssrFetchModule(url, options: FetchModuleOptions = {}) { if (isDepsOptimizerEnabled(config, true)) { await initDevSsrDepsOptimizer(config, server) } await updateCjsSsrExternals(server) - return ssrFetchModule(server, url, options) + return ssrFetchModule(server, url, undefined, options) }, async reloadModule(module) { if (serverConfig.hmr !== false && module.file) { diff --git a/packages/vite/src/node/ssr/runtimeClient.ts b/packages/vite/src/node/ssr/runtimeClient.ts index 66bbbfc3a6193a..d17c98169546b1 100644 --- a/packages/vite/src/node/ssr/runtimeClient.ts +++ b/packages/vite/src/node/ssr/runtimeClient.ts @@ -1,8 +1,8 @@ import { fileURLToPath, pathToFileURL } from 'node:url' import { createRequire } from 'node:module' -import { dirname } from 'node:path' +import { dirname, resolve } from 'node:path' import vm from 'node:vm' -import type { PartialResolvedId } from 'rollup' +import type { PartialResolvedId, SourceMap } from 'rollup' import type { ViteHotContext } from 'types/hot' import { CLIENT_PUBLIC_PATH, @@ -18,6 +18,7 @@ import { toFilePath, } from '../utils/shared' import type { FetchModuleResult } from './ssrFetchModule' +import { extractSourceMap } from './ssrSourceMap' // const debugExecute = createDebug('vite-runtime:client:execute') // const debugNative = createDebug('vite-runtime:client:native') @@ -61,10 +62,9 @@ export interface ModuleCache { evaluated?: boolean resolving?: boolean code?: string - /** - * Module ids that imports this module - */ - importers?: Set + map?: SourceMap | null | undefined + importers: Set + imports: Set } export class ModuleCacheMap extends Map { @@ -75,15 +75,17 @@ export class ModuleCacheMap extends Map { /** * Assign partial data to the map */ - update(fsPath: string, mod: Partial): this { + update(fsPath: string, mod: ModuleCache): this { fsPath = this.normalizePath(fsPath) - if (!super.has(fsPath)) super.set(fsPath, mod) + if (!super.has(fsPath)) this.setByModuleId(fsPath, mod) else Object.assign(super.get(fsPath) as ModuleCache, mod) return this } - setByModuleId(modulePath: string, mod: ModuleCache): this { - return super.set(modulePath, mod) + setByModuleId(modulePath: string, mod: Partial): this { + if (!mod.imports) mod.imports = new Set() + if (!mod.importers) mod.importers = new Set() + return super.set(modulePath, mod as ModuleCache) } override set(fsPath: string, mod: ModuleCache): this { @@ -91,7 +93,8 @@ export class ModuleCacheMap extends Map { } getByModuleId(modulePath: string): ModuleCache { - if (!super.has(modulePath)) super.set(modulePath, {}) + if (!super.has(modulePath)) this.setByModuleId(modulePath, {}) + return super.get(modulePath)! } @@ -107,6 +110,15 @@ export class ModuleCacheMap extends Map { return this.deleteByModuleId(this.normalizePath(fsPath)) } + invalidateModule(mod: ModuleCache): void { + delete mod.evaluated + delete mod.resolving + delete mod.promise + delete mod.exports + mod.importers?.clear() + mod.imports?.clear() + } + /** * Invalidate modules that dependent on the given modules, up to the main entry */ @@ -144,6 +156,20 @@ export class ModuleCacheMap extends Map { } return invalidated } + + /** + * Return parsed source map based on inlined source map of the module + */ + getSourceMap(id: string): SourceMap | null | undefined { + const cache = this.get(id) + if (cache.map) return cache.map + const map = cache.code && extractSourceMap(cache.code) + if (map) { + cache.map = map + return map + } + return null + } } export const DEFAULT_REQUEST_STUBS = { @@ -187,7 +213,7 @@ export class ViteRuntimeClient { } public async executeFile(file: string): Promise { - const url = `/@fs/${slash(file)}` + const url = `/@fs/${slash(resolve(file))}` return await this.cachedRequest(url, url, []) } @@ -204,29 +230,54 @@ export class ViteRuntimeClient { ): Promise { const importee = callstack[callstack.length - 1] - const mod = this.moduleCache.getByModuleId(fsPath) + const mod = this.moduleCache.get(fsPath) + const { imports, importers } = mod - if (!mod.importers) mod.importers = new Set() - if (importee) mod.importers.add(importee) + if (importee) importers.add(importee) - // the callstack reference itself circularly - if (callstack.includes(fsPath) && mod.exports) return mod.exports + const getStack = () => + `stack:\n${[...callstack, fsPath] + .reverse() + .map((p) => ` - ${p}`) + .join('\n')}` - // cached module - if (mod.promise) return mod.promise + // check circular dependency + if ( + callstack.includes(fsPath) || + Array.from(imports.values()).some((i) => importers.has(i)) + ) { + if (mod.exports) return mod.exports + } - mod.evaluated = false - const promise = this.directRequest(id, fsPath, callstack) + let debugTimer: any + if (this.debug) + debugTimer = setTimeout( + () => + warn( + `[vite-node] module ${fsPath} takes over 2s to load.\n${getStack()}`, + ), + 2000, + ) try { + // cached module + if (mod.promise) return await mod.promise + + const promise = this.directRequest(id, fsPath, callstack) + Object.assign(mod, { promise, evaluated: false }) return await promise } finally { mod.evaluated = true + if (debugTimer) clearTimeout(debugTimer) } } protected shouldResolveId(id: string, _importee?: string): boolean { - return ![CLIENT_PUBLIC_PATH, ENV_PUBLIC_PATH].includes(id) && !isBuiltin(id) + return ( + ![CLIENT_PUBLIC_PATH, ENV_PUBLIC_PATH].includes(id) && + !isBuiltin(id) && + !id.startsWith('data:') + ) } private async _resolveUrl( @@ -236,19 +287,15 @@ export class ViteRuntimeClient { // we don't pass down importee here, because otherwise Vite doesn't resolve it correctly // should be checked before normalization, because it removes this prefix if (importee && id.startsWith(VALID_ID_PREFIX)) importee = undefined - id = normalizeRequestId(id, this.options.base) - if (!this.shouldResolveId(id)) return [id, id] - // we always want to have actual file path here, so we check if file actually exists - // if it's a virtual module, we do not care for it - const { path, exists } = toFilePath(id, this.options.root) - if (!this.options.resolveId || exists) return [id, path] - const resolved = await this.options.resolveId(id, importee) + const dep = normalizeRequestId(id, this.options.base) + if (!this.shouldResolveId(dep)) return [dep, dep] + const { path, exists } = toFilePath(dep, this.options.root) + if (!this.options.resolveId || exists) return [dep, path] + const resolved = await this.options.resolveId(dep, importee) const resolvedId = resolved ? normalizeRequestId(resolved.id, this.options.base) - : id - // to be compatible with dependencies that do not resolve id - const fsPath = resolved ? resolvedId : path - return [resolvedId, fsPath] + : dep + return [resolvedId, resolvedId] } protected async resolveUrl( @@ -271,34 +318,7 @@ export class ViteRuntimeClient { fsPath: string, callstack: string[], ): Promise { - const getStack = () => { - return `stack:\n${[...callstack, fsPath] - .reverse() - .map((p) => `- ${p}`) - .join('\n')}` - } - - let debugTimer: any - if (this.debug) - debugTimer = setTimeout( - () => - warn(() => `module ${fsPath} takes over 2s to load.\n${getStack()}`), - 2000, - ) - - try { - if (callstack.includes(fsPath)) { - const depExports = this.moduleCache.getByModuleId(fsPath)?.exports - if (depExports) return depExports - throw new Error( - `[vite-runtime-client] Failed to resolve circular dependency, ${getStack()}`, - ) - } - - return await this.cachedRequest(id, fsPath, callstack) - } finally { - if (debugTimer) clearTimeout(debugTimer) - } + return await this.cachedRequest(id, fsPath, callstack) } /** @internal */ @@ -313,12 +333,16 @@ export class ViteRuntimeClient { const mod = this.moduleCache.getByModuleId(moduleId) const request = async (dep: string) => { - const [id, depFsPath] = await this.resolveUrl(dep, fsPath) + const [id, depFsPath] = await this.resolveUrl(String(dep), fsPath) + const depMod = this.moduleCache.getByModuleId(depFsPath) + depMod.importers.add(moduleId) + mod.imports.add(depFsPath) + return this.dependencyRequest(id, depFsPath, callstack) } const requestStubs = this.options.requestStubs || DEFAULT_REQUEST_STUBS - if (id in requestStubs) return requestStubs[id] + if (id in requestStubs) return (requestStubs as any)[id] let { code: transformed, externalize } = await this.options.fetchModule(id) @@ -346,7 +370,6 @@ export class ViteRuntimeClient { enumerable: false, configurable: false, }) - Object.defineProperty(exports, '__esModule', { value: true }) // this proxy is triggered only on exports.{name} and module.exports access // inside the module itself. imported module is always "exports" const cjsExports = new Proxy(exports, { @@ -360,7 +383,8 @@ export class ViteRuntimeClient { // so "exports.default" becomes the actual module if ( p === 'default' && - this.shouldInterop(modulePath, { default: value }) + this.shouldInterop(modulePath, { default: value }) && + cjsExports !== value ) { exportAll(cjsExports, value) exports.default = value @@ -384,7 +408,6 @@ export class ViteRuntimeClient { }) Object.assign(mod, { code: transformed, exports }) - const __filename = fileURLToPath(href) const moduleProxy = { set exports(value) { @@ -414,10 +437,6 @@ export class ViteRuntimeClient { }) } - // Be careful when changing this - // changing context will change amount of code added on line :114 (vm.runInThisContext) - // this messes up sourcemaps for coverage - // adjust `offset` variable in packages/coverage-c8/src/provider.ts#86 if you do change this const context = this.prepareContext({ // esm transformed by Vite __vite_ssr_import__: request, @@ -444,6 +463,7 @@ export class ViteRuntimeClient { const codeDefinition = `'use strict';async (${Object.keys(context).join( ',', )})=>{{` + process.env.CODE_DEFINITION_LENGTH = String(codeDefinition.length) const code = `${codeDefinition}${transformed}\n}}` const fn = vm.runInThisContext(code, { filename: __filename, diff --git a/packages/vite/src/node/ssr/sourceMapHandler.ts b/packages/vite/src/node/ssr/sourceMapHandler.ts new file mode 100644 index 00000000000000..5508b570f5ea28 --- /dev/null +++ b/packages/vite/src/node/ssr/sourceMapHandler.ts @@ -0,0 +1,462 @@ +// adapted from https://github.com/evanw/node-source-map-support/blob/master/source-map-support.js +// we need this because "--enable-source-maps" flag doesn't support VM +// we make a custom implementatin because we don't need some features from source-map-support, +// like polyfilled Buffer, browser support, etc. + +import path from 'node:path' +import fs from 'node:fs' +import type { OriginalMapping, SourceMapInput } from '@jridgewell/trace-mapping' +import { TraceMap, originalPositionFor } from '@jridgewell/trace-mapping' + +// Only install once if called multiple times +let errorFormatterInstalled = false + +// If true, the caches are reset before a stack trace formatting operation +const emptyCacheBetweenOperations = false + +// Maps a file path to a string containing the file contents +let fileContentsCache: Record = {} + +// Maps a file path to a source map for that file +let sourceMapCache: Record< + string, + { url: string | null; map: TraceMap | null } +> = {} + +// Regex for detecting source maps +const reSourceMap = /^data:application\/json[^,]+base64,/ + +type RetrieveFileHandler = (path: string) => string | null | undefined +type RetrieveMapHandler = ( + source: string, +) => { url: string; map?: string | SourceMapInput | null } | null | undefined + +// Priority list of retrieve handlers +let retrieveFileHandlers: RetrieveFileHandler[] = [] +let retrieveMapHandlers: RetrieveMapHandler[] = [] + +function globalProcessVersion() { + if (typeof process === 'object' && process != null) return process.version + else return '' +} + +function handlerExec(list: ((arg: string) => T)[]) { + return function (arg: string) { + for (let i = 0; i < list.length; i++) { + const ret = list[i](arg) + if (ret) return ret + } + return null + } +} + +let retrieveFile = handlerExec(retrieveFileHandlers) + +retrieveFileHandlers.push((path) => { + // Trim the path to make sure there is no extra whitespace. + path = path.trim() + if (path.startsWith('file:')) { + // existsSync/readFileSync can't handle file protocol, but once stripped, it works + path = path.replace(/file:\/\/\/(\w:)?/, (protocol, drive) => { + return drive + ? '' // file:///C:/dir/file -> C:/dir/file + : '/' // file:///root-dir/file -> /root-dir/file + }) + } + if (path in fileContentsCache) return fileContentsCache[path] + + let contents = '' + try { + if (fs.existsSync(path)) contents = fs.readFileSync(path, 'utf8') + } catch (er) { + /* ignore any errors */ + } + + return (fileContentsCache[path] = contents) +}) + +// Support URLs relative to a directory, but be careful about a protocol prefix +function supportRelativeURL(file: string, url: string) { + if (!file) return url + const dir = path.dirname(file) + const match = /^\w+:\/\/[^/]*/.exec(dir) + let protocol = match ? match[0] : '' + const startPath = dir.slice(protocol.length) + if (protocol && /^\/\w:/.test(startPath)) { + // handle file:///C:/ paths + protocol += '/' + return ( + protocol + + path.resolve(dir.slice(protocol.length), url).replace(/\\/g, '/') + ) + } + return protocol + path.resolve(dir.slice(protocol.length), url) +} + +function retrieveSourceMapURL(source: string) { + // Get the URL of the source map + const fileData = retrieveFile(source) + if (!fileData) return null + const re = + /\/\/[@#]\s*sourceMappingURL=([^\s'"]+)\s*$|\/\*[@#]\s*sourceMappingURL=[^\s*'"]+\s*\*\/\s*$/gm + // Keep executing the search to find the *last* sourceMappingURL to avoid + // picking up sourceMappingURLs from comments, strings, etc. + let lastMatch, match + + while ((match = re.exec(fileData))) lastMatch = match + if (!lastMatch) return null + return lastMatch[1] +} + +// Can be overridden by the retrieveSourceMap option to install. Takes a +// generated source filename; returns a {map, optional url} object, or null if +// there is no source map. The map field may be either a string or the parsed +// JSON object (ie, it must be a valid argument to the SourceMapConsumer +// constructor). +let retrieveSourceMap = handlerExec(retrieveMapHandlers) +retrieveMapHandlers.push((source) => { + let sourceMappingURL = retrieveSourceMapURL(source) + if (!sourceMappingURL) return null + + // Read the contents of the source map + let sourceMapData + if (reSourceMap.test(sourceMappingURL)) { + // Support source map URL as a data url + const rawData = sourceMappingURL.slice(sourceMappingURL.indexOf(',') + 1) + sourceMapData = Buffer.from(rawData, 'base64').toString() + sourceMappingURL = source + } else { + // Support source map URLs relative to the source URL + sourceMappingURL = supportRelativeURL(source, sourceMappingURL) + sourceMapData = retrieveFile(sourceMappingURL) + } + + if (!sourceMapData) return null + + return { + url: sourceMappingURL, + map: sourceMapData, + } +}) + +// interface Position { +// source: string +// line: number +// column: number +// } + +function mapSourcePosition(position: OriginalMapping) { + if (!position.source) return position + let sourceMap = sourceMapCache[position.source] + if (!sourceMap) { + // Call the (overrideable) retrieveSourceMap function to get the source map. + const urlAndMap = retrieveSourceMap(position.source) + if (urlAndMap && urlAndMap.map) { + sourceMap = sourceMapCache[position.source] = { + url: urlAndMap.url, + map: new TraceMap(urlAndMap.map), + } + + // Load all sources stored inline with the source map into the file cache + // to pretend like they are already loaded. They may not exist on disk. + if (sourceMap.map?.sourcesContent) { + sourceMap.map.sources.forEach((source, i) => { + const contents = sourceMap.map?.sourcesContent?.[i] + if (contents && source && sourceMap.url) { + const url = supportRelativeURL(sourceMap.url, source) + fileContentsCache[url] = contents + } + }) + } + } else { + sourceMap = sourceMapCache[position.source] = { + url: null, + map: null, + } + } + } + + // Resolve the source URL relative to the URL of the source map + if (sourceMap && sourceMap.map && sourceMap.url) { + const originalPosition = originalPositionFor(sourceMap.map, position) + + // Only return the original position if a matching line was found. If no + // matching line is found then we return position instead, which will cause + // the stack trace to print the path and line for the compiled file. It is + // better to give a precise location in the compiled file than a vague + // location in the original file. + if (originalPosition.source != null) { + originalPosition.source = supportRelativeURL( + sourceMap.url, + originalPosition.source, + ) + return originalPosition + } + } + + return position +} + +// Parses code generated by FormatEvalOrigin(), a function inside V8: +// https://code.google.com/p/v8/source/browse/trunk/src/messages.js +function mapEvalOrigin(origin: string): string { + // Most eval() calls are in this format + let match = /^eval at ([^(]+) \((.+):(\d+):(\d+)\)$/.exec(origin) + if (match) { + const position = mapSourcePosition({ + name: null, + source: match[2], + line: +match[3], + column: +match[4] - 1, + }) + return `eval at ${match[1]} (${position.source}:${position.line}:${ + position.column + 1 + })` + } + + // Parse nested eval() calls using recursion + match = /^eval at ([^(]+) \((.+)\)$/.exec(origin) + if (match) return `eval at ${match[1]} (${mapEvalOrigin(match[2])})` + + // Make sure we still return useful information if we didn't find anything + return origin +} + +interface CallSite extends NodeJS.CallSite { + getScriptNameOrSourceURL(): string +} + +// This is copied almost verbatim from the V8 source code at +// https://code.google.com/p/v8/source/browse/trunk/src/messages.js. The +// implementation of wrapCallSite() used to just forward to the actual source +// code of CallSite.prototype.toString but unfortunately a new release of V8 +// did something to the prototype chain and broke the shim. The only fix I +// could find was copy/paste. +function CallSiteToString(this: CallSite) { + let fileName + let fileLocation = '' + if (this.isNative()) { + fileLocation = 'native' + } else { + fileName = this.getScriptNameOrSourceURL() + if (!fileName && this.isEval()) { + fileLocation = this.getEvalOrigin() as string + fileLocation += ', ' // Expecting source position to follow. + } + + if (fileName) { + fileLocation += fileName + } else { + // Source code does not originate from a file and is not native, but we + // can still get the source position inside the source string, e.g. in + // an eval string. + fileLocation += '' + } + const lineNumber = this.getLineNumber() + if (lineNumber != null) { + fileLocation += `:${lineNumber}` + const columnNumber = this.getColumnNumber() + if (columnNumber) fileLocation += `:${columnNumber}` + } + } + + let line = '' + const functionName = this.getFunctionName() + let addSuffix = true + const isConstructor = this.isConstructor() + const isMethodCall = !(this.isToplevel() || isConstructor) + if (isMethodCall) { + let typeName = this.getTypeName() + // Fixes shim to be backward compatable with Node v0 to v4 + if (typeName === '[object Object]') typeName = 'null' + + const methodName = this.getMethodName() + if (functionName) { + if (typeName && functionName.indexOf(typeName) !== 0) + line += `${typeName}.` + + line += functionName + if ( + methodName && + functionName.indexOf(`.${methodName}`) !== + functionName.length - methodName.length - 1 + ) + line += ` [as ${methodName}]` + } else { + line += `${typeName}.${methodName || ''}` + } + } else if (isConstructor) { + line += `new ${functionName || ''}` + } else if (functionName) { + line += functionName + } else { + line += fileLocation + addSuffix = false + } + if (addSuffix) line += ` (${fileLocation})` + + return line +} + +function cloneCallSite(frame: CallSite) { + const object = {} as CallSite + Object.getOwnPropertyNames(Object.getPrototypeOf(frame)).forEach((name) => { + const key = name as keyof CallSite + // @ts-expect-error difficult to type + object[key] = /^(?:is|get)/.test(name) + ? function () { + return frame[key].call(frame) + } + : frame[key] + }) + object.toString = CallSiteToString + return object +} + +interface State { + nextPosition: null | OriginalMapping + curPosition: null | OriginalMapping +} + +function wrapCallSite(frame: CallSite, state: State) { + // provides interface backward compatibility + if (state === undefined) state = { nextPosition: null, curPosition: null } + + if (frame.isNative()) { + state.curPosition = null + return frame + } + + // Most call sites will return the source file from getFileName(), but code + // passed to eval() ending in "//# sourceURL=..." will return the source file + // from getScriptNameOrSourceURL() instead + const source = frame.getFileName() || frame.getScriptNameOrSourceURL() + if (source) { + const line = frame.getLineNumber() as number + let column = (frame.getColumnNumber() as number) - 1 + + // Fix position in Node where some (internal) code is prepended. + // See https://github.com/evanw/node-source-map-support/issues/36 + // Header removed in node at ^10.16 || >=11.11.0 + // v11 is not an LTS candidate, we can just test the one version with it. + // Test node versions for: 10.16-19, 10.20+, 12-19, 20-99, 100+, or 11.11 + const noHeader = + /^v(?:10\.1[6-9]|10\.[2-9]\d|10\.\d{3,}|1[2-9]\d*|[2-9]\d|\d{3,}|11\.11)/ + const headerLength = noHeader.test(globalProcessVersion()) ? 0 : 62 + if (line === 1 && column > headerLength && !frame.isEval()) + column -= headerLength + + const position = mapSourcePosition({ + name: null, + source, + line, + column, + }) + state.curPosition = position + frame = cloneCallSite(frame) + const originalFunctionName = frame.getFunctionName + frame.getFunctionName = function () { + if (state.nextPosition == null) return originalFunctionName() + + return state.nextPosition.name || originalFunctionName() + } + frame.getFileName = function () { + return position.source ?? undefined + } + frame.getLineNumber = function () { + return position.line + } + frame.getColumnNumber = function () { + return position.column + 1 + } + frame.getScriptNameOrSourceURL = function () { + return position.source as string + } + return frame + } + + // Code called using eval() needs special handling + let origin = frame.isEval() && frame.getEvalOrigin() + if (origin) { + origin = mapEvalOrigin(origin) + frame = cloneCallSite(frame) + frame.getEvalOrigin = function () { + return origin || undefined + } + return frame + } + + // If we get here then we were unable to change the source position + return frame +} + +// This function is part of the V8 stack trace API, for more info see: +// https://v8.dev/docs/stack-trace-api +function prepareStackTrace(error: Error, stack: CallSite[]) { + if (emptyCacheBetweenOperations) { + fileContentsCache = {} + sourceMapCache = {} + } + + const name = error.name || 'Error' + const message = error.message || '' + const errorString = `${name}: ${message}` + + const state = { nextPosition: null, curPosition: null } + const processedStack = [] + for (let i = stack.length - 1; i >= 0; i--) { + processedStack.push(`\n at ${wrapCallSite(stack[i], state)}`) + state.nextPosition = state.curPosition + } + state.curPosition = state.nextPosition = null + return errorString + processedStack.reverse().join('') +} + +const originalRetrieveFileHandlers = retrieveFileHandlers.slice(0) +const originalRetrieveMapHandlers = retrieveMapHandlers.slice(0) + +interface Options { + hookRequire?: boolean + overrideRetrieveFile?: boolean + overrideRetrieveSourceMap?: boolean + retrieveFile?: RetrieveFileHandler + retrieveSourceMap?: RetrieveMapHandler +} + +export const installSourceMapHandler = function (options: Options): void { + options = options || {} + + // Allow sources to be found by methods other than reading the files + // directly from disk. + if (options.retrieveFile) { + if (options.overrideRetrieveFile) retrieveFileHandlers.length = 0 + + retrieveFileHandlers.unshift(options.retrieveFile) + } + + // Allow source maps to be found by methods other than reading the files + // directly from disk. + if (options.retrieveSourceMap) { + if (options.overrideRetrieveSourceMap) retrieveMapHandlers.length = 0 + + retrieveMapHandlers.unshift(options.retrieveSourceMap) + } + + // Install the error reformatter + if (!errorFormatterInstalled) { + errorFormatterInstalled = true + Error.prepareStackTrace = + prepareStackTrace as ErrorConstructor['prepareStackTrace'] + } +} + +export const resetRetrieveSourceMapHandlers = function (): void { + retrieveFileHandlers.length = 0 + retrieveMapHandlers.length = 0 + + retrieveFileHandlers = originalRetrieveFileHandlers.slice(0) + retrieveMapHandlers = originalRetrieveMapHandlers.slice(0) + + retrieveSourceMap = handlerExec(retrieveMapHandlers) + retrieveFile = handlerExec(retrieveFileHandlers) +} diff --git a/packages/vite/src/node/ssr/ssrFetchModule.ts b/packages/vite/src/node/ssr/ssrFetchModule.ts index 9e8f0a87496f06..60dc708f0a2f4d 100644 --- a/packages/vite/src/node/ssr/ssrFetchModule.ts +++ b/packages/vite/src/node/ssr/ssrFetchModule.ts @@ -2,18 +2,26 @@ import type { SourceMap } from 'rollup' import type { TransformResult } from '../server/transformRequest' import type { ViteDevServer } from '../server' import { shouldExternalizeForSSR } from './ssrExternal' +import { withInlineSourcemap } from './ssrSourceMap' export interface FetchModuleOptions { ssr?: boolean sourcemep?: boolean | 'inline' } +export interface FetchModuleResult { + code?: string + externalize?: string + map?: SourceMap | null +} + export async function ssrFetchModule( server: ViteDevServer, id: string, - { ssr = true, sourcemep = 'inline' }: FetchModuleOptions, + importer?: string | undefined, + { ssr = true, sourcemep = 'inline' }: FetchModuleOptions = {}, ): Promise { - if (shouldExternalizeForSSR(id, server.config)) { + if (ssr && shouldExternalizeForSSR(id, importer, server.config)) { return { externalize: id } } @@ -31,36 +39,3 @@ export async function ssrFetchModule( return { code: result?.code, map: result?.map } } - -export interface FetchModuleResult { - code?: string - externalize?: string - map?: SourceMap | null -} - -let SOURCEMAPPING_URL = 'sourceMa' -SOURCEMAPPING_URL += 'ppingURL' - -const VITE_SOURCEMAPPING_SOURCE = '//# sourceMappingConsumer=vite' -const VITE_SOURCEMAPPING_URL = `${SOURCEMAPPING_URL}=data:application/json;charset=utf-8` -// const VITE_NODE_SOURCEMAPPING_REGEXP = new RegExp(`//# ${VITE_NODE_SOURCEMAPPING_URL};base64,(.+)`) - -export function withInlineSourcemap(result: TransformResult): TransformResult { - const map = result.map - let code = result.code - - if (!map || code.includes(VITE_SOURCEMAPPING_SOURCE)) return result - - // to reduce the payload size, we only inline vite source map, because it's also the only one we use - const OTHER_SOURCE_MAP_REGEXP = new RegExp( - `//# ${SOURCEMAPPING_URL}=data:application/json[^,]+base64,(.+)`, - 'g', - ) - while (OTHER_SOURCE_MAP_REGEXP.test(code)) - code = code.replace(OTHER_SOURCE_MAP_REGEXP, '') - - const sourceMap = Buffer.from(JSON.stringify(map), 'utf-8').toString('base64') - result.code = `${code.trimEnd()}\n\n${VITE_SOURCEMAPPING_SOURCE}\n//# ${VITE_SOURCEMAPPING_URL};base64,${sourceMap}\n` - - return result -} diff --git a/packages/vite/src/node/ssr/ssrSourceMap.ts b/packages/vite/src/node/ssr/ssrSourceMap.ts new file mode 100644 index 00000000000000..20e0fe144c353d --- /dev/null +++ b/packages/vite/src/node/ssr/ssrSourceMap.ts @@ -0,0 +1,65 @@ +import type { TransformResult } from 'vite' +import type { EncodedSourceMap } from '@jridgewell/trace-mapping' +import type { SourceMap } from 'rollup' +import { installSourceMapHandler } from './sourceMapHandler' + +interface InstallSourceMapSupportOptions { + getSourceMap: (source: string) => EncodedSourceMap | null | undefined +} + +let SOURCEMAPPING_URL = 'sourceMa' +SOURCEMAPPING_URL += 'ppingURL' + +const VITE_SOURCEMAPPING_SOURCE = '//# sourceMappingSource=vite-runtime-client' +const VITE_SOURCEMAPPING_URL = `${SOURCEMAPPING_URL}=data:application/json;charset=utf-8` +const VITE_SOURCEMAPPING_REGEXP = new RegExp( + `//# ${VITE_SOURCEMAPPING_URL};base64,(.+)`, +) + +export function withInlineSourcemap(result: TransformResult): TransformResult { + const map = result.map + let code = result.code + + if (!map || code.includes(VITE_SOURCEMAPPING_SOURCE)) return result + + // to reduce the payload size, we only inline vite node source map, because it's also the only one we use + const OTHER_SOURCE_MAP_REGEXP = new RegExp( + `//# ${SOURCEMAPPING_URL}=data:application/json[^,]+base64,([A-Za-z0-9+/=]+)$`, + 'gm', + ) + while (OTHER_SOURCE_MAP_REGEXP.test(code)) + code = code.replace(OTHER_SOURCE_MAP_REGEXP, '') + + const sourceMap = Buffer.from(JSON.stringify(map), 'utf-8').toString('base64') + result.code = `${code.trimEnd()}\n\n${VITE_SOURCEMAPPING_SOURCE}\n//# ${VITE_SOURCEMAPPING_URL};base64,${sourceMap}\n` + + return result +} + +export function extractSourceMap(code: string): SourceMap | null { + const mapString = code.match(VITE_SOURCEMAPPING_REGEXP)?.[1] + if (mapString) + return JSON.parse(Buffer.from(mapString, 'base64').toString('utf-8')) + return null +} + +let sourceMapHanlderInstalled = true + +export function installSourcemapsSupport( + options: InstallSourceMapSupportOptions, +): void { + if (sourceMapHanlderInstalled) return + installSourceMapHandler({ + retrieveSourceMap(source) { + const map = options.getSourceMap(source) + if (map) { + return { + url: source, + map, + } + } + return null + }, + }) + sourceMapHanlderInstalled = true +} From 2fc2e3364ec2c9b90c76fa5932a2be1665effefd Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 3 Jul 2023 17:04:02 +0200 Subject: [PATCH 13/14] chore: fix source-map --- packages/vite/src/node/ssr/ssrSourceMap.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/vite/src/node/ssr/ssrSourceMap.ts b/packages/vite/src/node/ssr/ssrSourceMap.ts index 20e0fe144c353d..d3194c12d613f8 100644 --- a/packages/vite/src/node/ssr/ssrSourceMap.ts +++ b/packages/vite/src/node/ssr/ssrSourceMap.ts @@ -1,7 +1,7 @@ -import type { TransformResult } from 'vite' import type { EncodedSourceMap } from '@jridgewell/trace-mapping' import type { SourceMap } from 'rollup' import { installSourceMapHandler } from './sourceMapHandler' +import type { FetchModuleResult } from './ssrFetchModule' interface InstallSourceMapSupportOptions { getSourceMap: (source: string) => EncodedSourceMap | null | undefined @@ -16,9 +16,11 @@ const VITE_SOURCEMAPPING_REGEXP = new RegExp( `//# ${VITE_SOURCEMAPPING_URL};base64,(.+)`, ) -export function withInlineSourcemap(result: TransformResult): TransformResult { +export function withInlineSourcemap( + result: FetchModuleResult, +): FetchModuleResult { const map = result.map - let code = result.code + let code = result.code! if (!map || code.includes(VITE_SOURCEMAPPING_SOURCE)) return result From 6b796ced86548ad0035a23921aeed1fc26c4a5b5 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 3 Jul 2023 17:21:17 +0200 Subject: [PATCH 14/14] chore: fix import from chunk --- packages/vite/src/node/constants.ts | 7 +++++++ packages/vite/src/node/ssr/runtimeClient.ts | 23 +++++++++++++++++++-- packages/vite/src/node/ssr/ssrSourceMap.ts | 22 +++++--------------- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/packages/vite/src/node/constants.ts b/packages/vite/src/node/constants.ts index 414d1b6430918f..b6472c39b2bba3 100644 --- a/packages/vite/src/node/constants.ts +++ b/packages/vite/src/node/constants.ts @@ -151,3 +151,10 @@ export const wildcardHosts = new Set([ export const DEFAULT_DEV_PORT = 5173 export const DEFAULT_PREVIEW_PORT = 4173 + +export let SOURCEMAPPING_URL = 'sourceMa' +SOURCEMAPPING_URL += 'ppingURL' + +export const VITE_SOURCEMAPPING_SOURCE = + '//# sourceMappingSource=vite-runtime-client' +export const VITE_SOURCEMAPPING_URL = `${SOURCEMAPPING_URL}=data:application/json;charset=utf-8` diff --git a/packages/vite/src/node/ssr/runtimeClient.ts b/packages/vite/src/node/ssr/runtimeClient.ts index d17c98169546b1..5a3d26f0ad7edb 100644 --- a/packages/vite/src/node/ssr/runtimeClient.ts +++ b/packages/vite/src/node/ssr/runtimeClient.ts @@ -1,3 +1,6 @@ +// runtime client should be lightweight so we import only the bare minimum +// for this we also have a separate utils file for shared utils + import { fileURLToPath, pathToFileURL } from 'node:url' import { createRequire } from 'node:module' import { dirname, resolve } from 'node:path' @@ -8,6 +11,7 @@ import { CLIENT_PUBLIC_PATH, ENV_PUBLIC_PATH, VALID_ID_PREFIX, + VITE_SOURCEMAPPING_URL, } from '../constants' import { cleanUrl, @@ -18,7 +22,17 @@ import { toFilePath, } from '../utils/shared' import type { FetchModuleResult } from './ssrFetchModule' -import { extractSourceMap } from './ssrSourceMap' + +const VITE_SOURCEMAPPING_REGEXP = new RegExp( + `//# ${VITE_SOURCEMAPPING_URL};base64,(.+)`, +) + +export function extractSourceMap(code: string): SourceMap | null { + const mapString = code.match(VITE_SOURCEMAPPING_REGEXP)?.[1] + if (mapString) + return JSON.parse(Buffer.from(mapString, 'base64').toString('utf-8')) + return null +} // const debugExecute = createDebug('vite-runtime:client:execute') // const debugNative = createDebug('vite-runtime:client:native') @@ -562,7 +576,12 @@ function exportAll(exports: any, sourceModule: any) { // we don't want to have "string" to be made into {0:"s",1:"t",2:"r",3:"i",4:"n",5:"g"} // the same goes for arrays: [1,2,3] -> {0:1,1:2,2:3} - if (isPrimitive(sourceModule) || Array.isArray(sourceModule)) return + if ( + isPrimitive(sourceModule) || + Array.isArray(sourceModule) || + sourceModule instanceof Promise + ) + return for (const key in sourceModule) { if (key !== 'default') { diff --git a/packages/vite/src/node/ssr/ssrSourceMap.ts b/packages/vite/src/node/ssr/ssrSourceMap.ts index d3194c12d613f8..7f33e01a9b61ec 100644 --- a/packages/vite/src/node/ssr/ssrSourceMap.ts +++ b/packages/vite/src/node/ssr/ssrSourceMap.ts @@ -1,5 +1,9 @@ import type { EncodedSourceMap } from '@jridgewell/trace-mapping' -import type { SourceMap } from 'rollup' +import { + SOURCEMAPPING_URL, + VITE_SOURCEMAPPING_SOURCE, + VITE_SOURCEMAPPING_URL, +} from '../constants' import { installSourceMapHandler } from './sourceMapHandler' import type { FetchModuleResult } from './ssrFetchModule' @@ -7,15 +11,6 @@ interface InstallSourceMapSupportOptions { getSourceMap: (source: string) => EncodedSourceMap | null | undefined } -let SOURCEMAPPING_URL = 'sourceMa' -SOURCEMAPPING_URL += 'ppingURL' - -const VITE_SOURCEMAPPING_SOURCE = '//# sourceMappingSource=vite-runtime-client' -const VITE_SOURCEMAPPING_URL = `${SOURCEMAPPING_URL}=data:application/json;charset=utf-8` -const VITE_SOURCEMAPPING_REGEXP = new RegExp( - `//# ${VITE_SOURCEMAPPING_URL};base64,(.+)`, -) - export function withInlineSourcemap( result: FetchModuleResult, ): FetchModuleResult { @@ -38,13 +33,6 @@ export function withInlineSourcemap( return result } -export function extractSourceMap(code: string): SourceMap | null { - const mapString = code.match(VITE_SOURCEMAPPING_REGEXP)?.[1] - if (mapString) - return JSON.parse(Buffer.from(mapString, 'base64').toString('utf-8')) - return null -} - let sourceMapHanlderInstalled = true export function installSourcemapsSupport(