Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

fix: always resolve runner's entry point #18013

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 1 addition & 23 deletions packages/vite/src/module-runner/runner.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import type { ViteHotContext } from 'types/hot'
import { HMRClient, HMRContext } from '../shared/hmr'
import {
cleanUrl,
isPrimitive,
isWindows,
unwrapId,
wrapId,
} from '../shared/utils'
import { cleanUrl, isPrimitive, isWindows, unwrapId } from '../shared/utils'
import { analyzeImportedModDifference } from '../shared/ssrTransform'
import { ModuleCacheMap } from './moduleCache'
import type {
Expand Down Expand Up @@ -93,7 +87,6 @@ export class ModuleRunner {
* URL to execute. Accepts file path, server path or id relative to the root.
*/
public async import<T = any>(url: string): Promise<T> {
url = this.normalizeEntryUrl(url)
const fetchedModule = await this.cachedModule(url)
return await this.cachedRequest(url, fetchedModule)
}
Expand Down Expand Up @@ -125,21 +118,6 @@ export class ModuleRunner {
return this.destroyed
}

// we don't use moduleCache.normalize because this URL doesn't have to follow the same rules
// this URL is something that user passes down manually, and is later resolved by fetchModule
// moduleCache.normalize is used on resolved "file" property
private normalizeEntryUrl(url: string) {
// expect fetchModule to resolve relative module correctly
if (url[0] === '.') {
return url
}
url = normalizeAbsoluteUrl(url, this.root)
// if it's a server url (starts with a slash), keep it, otherwise assume a virtual module
// /id.js -> /id.js
// virtual:custom -> /@id/virtual:custom
return url[0] === '/' ? url : wrapId(url)
}

private processImport(
exports: Record<string, any>,
fetchResult: ResolvedResult,
Expand Down
70 changes: 42 additions & 28 deletions packages/vite/src/node/plugins/importAnalysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { parseAst } from 'rollup/parseAst'
import type { StaticImport } from 'mlly'
import { ESM_STATIC_IMPORT_RE, parseStaticImport } from 'mlly'
import { makeLegalIdentifier } from '@rollup/pluginutils'
import type { PartialResolvedId } from 'rollup'
import {
CLIENT_DIR,
CLIENT_PUBLIC_PATH,
Expand Down Expand Up @@ -97,6 +98,46 @@ export function isExplicitImportRequired(url: string): boolean {
return !isJSRequest(url) && !isCSSRequest(url)
}

export function normalizeResolvedIdToUrl(
environment: DevEnvironment,
url: string,
resolved: PartialResolvedId,
): string {
const root = environment.config.root
const depsOptimizer = environment.depsOptimizer
const fsUtils = getFsUtils(environment.getTopLevelConfig())

// normalize all imports into resolved URLs
// e.g. `import 'foo'` -> `import '/@fs/.../node_modules/foo/index.js'`
if (resolved.id.startsWith(withTrailingSlash(root))) {
// in root: infer short absolute path from root
url = resolved.id.slice(root.length)
} else if (
depsOptimizer?.isOptimizedDepFile(resolved.id) ||
// vite-plugin-react isn't following the leading \0 virtual module convention.
// This is a temporary hack to avoid expensive fs checks for React apps.
// We'll remove this as soon we're able to fix the react plugins.
(resolved.id !== '/@react-refresh' &&
path.isAbsolute(resolved.id) &&
fsUtils.existsSync(cleanUrl(resolved.id)))
) {
// an optimized deps may not yet exists in the filesystem, or
// a regular file exists but is out of root: rewrite to absolute /@fs/ paths
url = path.posix.join(FS_PREFIX, resolved.id)
} else {
url = resolved.id
}

// if the resolved id is not a valid browser import specifier,
// prefix it to make it valid. We will strip this before feeding it
// back into the transform pipeline
if (url[0] !== '.' && url[0] !== '/') {
url = wrapId(resolved.id)
}

return url
}

function extractImportedBindings(
id: string,
source: string,
Expand Down Expand Up @@ -180,7 +221,6 @@ function extractImportedBindings(
*/
export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
const { root, base } = config
const fsUtils = getFsUtils(config)
const clientPublicPath = path.posix.join(base, CLIENT_PUBLIC_PATH)
const enablePartialAccept = config.experimental?.hmrPartialAccept
const matchAlias = getAliasPatternMatcher(config.resolve.alias)
Expand Down Expand Up @@ -331,33 +371,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
const isRelative = url[0] === '.'
const isSelfImport = !isRelative && cleanUrl(url) === cleanUrl(importer)

// normalize all imports into resolved URLs
// e.g. `import 'foo'` -> `import '/@fs/.../node_modules/foo/index.js'`
if (resolved.id.startsWith(withTrailingSlash(root))) {
// in root: infer short absolute path from root
url = resolved.id.slice(root.length)
} else if (
depsOptimizer?.isOptimizedDepFile(resolved.id) ||
// vite-plugin-react isn't following the leading \0 virtual module convention.
// This is a temporary hack to avoid expensive fs checks for React apps.
// We'll remove this as soon we're able to fix the react plugins.
(resolved.id !== '/@react-refresh' &&
path.isAbsolute(resolved.id) &&
fsUtils.existsSync(cleanUrl(resolved.id)))
) {
// an optimized deps may not yet exists in the filesystem, or
// a regular file exists but is out of root: rewrite to absolute /@fs/ paths
url = path.posix.join(FS_PREFIX, resolved.id)
} else {
url = resolved.id
}

// if the resolved id is not a valid browser import specifier,
// prefix it to make it valid. We will strip this before feeding it
// back into the transform pipeline
if (url[0] !== '.' && url[0] !== '/') {
url = wrapId(resolved.id)
}
url = normalizeResolvedIdToUrl(environment, url, resolved)

// make the URL browser-valid
if (environment.config.consumer === 'client') {
Expand Down
15 changes: 14 additions & 1 deletion packages/vite/src/node/ssr/fetchModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '../../shared/constants'
import { genSourceMapUrl } from '../server/sourcemap'
import type { DevEnvironment } from '../server/environment'
import { normalizeResolvedIdToUrl } from '../plugins/importAnalysis'

export interface FetchModuleOptions {
cached?: boolean
Expand All @@ -36,7 +37,9 @@ export async function fetchModule(
return { externalize: url, type: 'network' }
}

if (url[0] !== '.' && url[0] !== '/') {
// if there is no importer, the file is an entry point
// entry points are always internalized
if (importer && url[0] !== '.' && url[0] !== '/') {
const { isProduction, root } = environment.config
const { externalConditions, dedupe, preserveSymlinks } =
environment.config.resolve
Expand Down Expand Up @@ -82,6 +85,16 @@ export async function fetchModule(
return { externalize: file, type }
}

// this is an entry point module, very high chance it's not resolved yet
// for example: runner.import('./some-file') or runner.import('/some-file')
if (!importer) {
const resolved = await environment.pluginContainer.resolveId(url)
if (!resolved) {
throw new Error(`[vite] cannot find entry point module '${url}'.`)
}
url = normalizeResolvedIdToUrl(environment, url, resolved)
}

url = unwrapId(url)

let mod = await environment.moduleGraph.getModuleByUrl(url)
Expand Down
58 changes: 56 additions & 2 deletions playground/hmr-ssr/__tests__/hmr-ssr.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@ import fs from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, posix, resolve } from 'node:path'
import EventEmitter from 'node:events'
import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest'
import {
afterAll,
beforeAll,
describe,
expect,
onTestFinished,
test,
vi,
} from 'vitest'
import type { InlineConfig, Logger, ViteDevServer } from 'vite'
import { createServer, createServerModuleRunner } from 'vite'
import type { ModuleRunner } from 'vite/module-runner'
Expand Down Expand Up @@ -321,6 +329,51 @@ describe('hmr works correctly', () => {
// })
})

describe('self accept with different entry point formats', () => {
test.each(['./unresolved.ts', './unresolved', '/unresolved'])(
'accepts if entry point is relative to root',
async (entrypoint) => {
await setupModuleRunner(entrypoint, {}, '/unresolved.ts')

onTestFinished(() => {
const filepath = resolvePath('..', 'unresolved.ts')
fs.writeFileSync(filepath, originalFiles.get(filepath)!, 'utf-8')
})

const el = () => hmr('.app')
await untilConsoleLogAfter(
() =>
editFile('unresolved.ts', (code) =>
code.replace('const foo = 1', 'const foo = 2'),
),
[
'foo was: 1',
'(self-accepting 1) foo is now: 2',
'(self-accepting 2) foo is now: 2',
updated('/unresolved.ts'),
],
true,
)
await untilUpdated(() => el(), '2')

await untilConsoleLogAfter(
() =>
editFile('unresolved.ts', (code) =>
code.replace('const foo = 2', 'const foo = 3'),
),
[
'foo was: 2',
'(self-accepting 1) foo is now: 3',
'(self-accepting 2) foo is now: 3',
updated('/unresolved.ts'),
],
true,
)
await untilUpdated(() => el(), '3')
},
)
})

describe('acceptExports', () => {
const HOT_UPDATED = /hot updated/
const CONNECTED = /connected/
Expand Down Expand Up @@ -1101,6 +1154,7 @@ function createInMemoryLogger(logs: string[]) {
async function setupModuleRunner(
entrypoint: string,
serverOptions: InlineConfig = {},
waitForFile: string = entrypoint,
) {
if (server) {
await server.close()
Expand Down Expand Up @@ -1147,7 +1201,7 @@ async function setupModuleRunner(
},
})

await waitForWatcher(server, entrypoint)
await waitForWatcher(server, waitForFile)

await runner.import(entrypoint)

Expand Down
20 changes: 20 additions & 0 deletions playground/hmr-ssr/unresolved.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export const foo = 1
hmr('.app', foo)

if (import.meta.hot) {
import.meta.hot.accept(({ foo }) => {
log('(self-accepting 1) foo is now:', foo)
})

import.meta.hot.accept(({ foo }) => {
log('(self-accepting 2) foo is now:', foo)
})

import.meta.hot.dispose(() => {
log(`foo was:`, foo)
})
}

function hmr(key: string, value: unknown) {
;(globalThis.__HMR__ as any)[key] = String(value)
}
Loading