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: data missing from moduleInfo.meta #7477

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions packages/vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
"es-module-lexer": "^1.1.0",
"estree-walker": "^3.0.1",
"etag": "^1.8.1",
"fast-deep-equal": "^3.1.3",
"fast-glob": "^3.2.12",
"http-proxy": "^1.18.1",
"json5": "^2.2.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export const DEFAULT_ASSETS_RE = new RegExp(
`\\.(` + KNOWN_ASSET_TYPES.join('|') + `)(\\?.*)?$`
)

export const DEP_VERSION_RE = /[?&](v=[\w.-]+)\b/
export const DEP_VERSION_RE = /(\?|&)(v=[\w.-]+)(?:&|$)/

export const loopbackHosts = new Set([
'localhost',
Expand Down
3 changes: 2 additions & 1 deletion packages/vite/src/node/plugins/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,8 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
await fileToUrl(file, config, this),
(config.server?.origin ?? '') + devBase
),
ssr
ssr,
false
Copy link
Member Author

Choose a reason for hiding this comment

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

refactor: The default value of isSelfAccepting is now undefined, so we need to pass an explicit false value wherever nothing was previously passed.

)
)
}
Expand Down
254 changes: 154 additions & 100 deletions packages/vite/src/node/plugins/importAnalysis.ts

Large diffs are not rendered by default.

24 changes: 19 additions & 5 deletions packages/vite/src/node/server/__tests__/moduleGraph.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,35 @@ describe('moduleGraph', () => {
moduleGraph.invalidateModule(entryModule)
expect(entryModule.ssrError).toBe(null)
})
})

describe('ensureEntryFromUrl', () => {
it('reuses an entry with same resolved id', async () => {
const moduleGraph = new ModuleGraph(async (url) => {
if (url === '/xx.js') {
return { id: '/x.js', meta: { vite: 'test' } }
} else {
return { id: url, meta: { vite: 'test' } }
}
})

const mod1 = await moduleGraph.ensureEntryFromUrl('/x.js', false)
const mod2 = await moduleGraph.ensureEntryFromUrl('/xx.js', false)
expect(mod1 === mod2).to.be.true
})

it('ensureEntryFromUrl should based on resolvedId', async () => {
it('creates a new entry if resolved "meta" differs', async () => {
const moduleGraph = new ModuleGraph(async (url) => {
if (url === '/xx.js') {
return { id: '/x.js' }
} else {
return { id: url }
return { id: url, meta: { vite: 'test' } }
}
})
const meta = { vite: 'test' }

const mod1 = await moduleGraph.ensureEntryFromUrl('/x.js', false)
mod1.meta = meta
const mod2 = await moduleGraph.ensureEntryFromUrl('/xx.js', false)
expect(mod2.meta).to.equal(meta)
expect(mod1 === mod2).to.be.false
})
})
})
54 changes: 53 additions & 1 deletion packages/vite/src/node/server/__tests__/pluginContainer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type { Plugin } from '../../plugin'
import { ModuleGraph } from '../moduleGraph'
import type { PluginContainer } from '../pluginContainer'
import { createPluginContainer } from '../pluginContainer'
import { importAnalysisPlugin } from '../../plugins/importAnalysis'
import type { ViteDevServer } from '..'

let resolveId: (id: string) => any
let moduleGraph: ModuleGraph
Expand Down Expand Up @@ -68,6 +70,44 @@ describe('plugin container', () => {
expect(metaArray).toEqual([{ x: 1 }, { x: 2 }, { x: 3 }])
})

it('preserves metadata calculated from import query string', async () => {
const entryUrl = '/main.js'
const testedId = 'x.js'

const plugin: Plugin = {
name: 'p1',
resolveId(url) {
if (url === entryUrl) {
return url
}
const [id, query] = url.split('?')
const match = query?.match(/test=([^&]+)/)
if (match) {
return { id, meta: { test: +match[1] } }
}
},
load(id) {
if (id === entryUrl) {
return `import "${testedId}?test=1"`
} else if (id === testedId) {
const meta = this.getModuleInfo(id).meta
expect(meta.test).toBe(1)
return ''
}
}
}

const container = await getPluginContainer({ plugins: [plugin] })
await moduleGraph.ensureEntryFromUrl(entryUrl, false)
await container.transform(
(await container.load(entryUrl)) as any,
entryUrl
)

await container.load(testedId)
expect.assertions(1)
})

it('can pass metadata between plugins', async () => {
const entryUrl = '/x.js'

Expand Down Expand Up @@ -153,13 +193,25 @@ async function getPluginContainer(
inlineConfig?: UserConfig
): Promise<PluginContainer> {
const config = await resolveConfig(
{ configFile: false, ...inlineConfig },
{
configFile: false,
server: { preTransformRequests: false },
...inlineConfig
},
'serve'
)

// @ts-ignore: This plugin requires a ViteDevServer instance.
config.plugins = config.plugins.filter((p) => !/pre-alias/.test(p.name))

// @ts-ignore: So does this one and this mock one seems to work
const iap = config.plugins.find((p) => p.name === 'vite:import-analysis')!
if (typeof iap.configureServer === 'function') {
iap.configureServer(<ViteDevServer>{ moduleGraph })
} else {
throw 'Expected vite:import-analysis plugin to have a "configureServer" method'
}

resolveId = (id) => container.resolveId(id)
const container = await createPluginContainer(config, moduleGraph)
return container
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ export async function createServer(
ws.on('vite:invalidate', async ({ path }: InvalidatePayload) => {
const mod = moduleGraph.urlToModuleMap.get(path)
if (mod && mod.isSelfAccepting && mod.lastHMRTimestamp > 0) {
const file = getShortName(mod.file!, config.root)
const file = getShortName(mod.file, config.root)
Copy link
Member Author

Choose a reason for hiding this comment

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

refactor: Removing nullability of ModuleNode.file as discussed with @patak-dev

updateModules(file, [...mod.importers], mod.lastHMRTimestamp, server)
}
})
Expand Down
73 changes: 49 additions & 24 deletions packages/vite/src/node/server/moduleGraph.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { extname } from 'node:path'
import type { ModuleInfo, PartialResolvedId } from 'rollup'
import deepEqual from 'fast-deep-equal'
import { isDirectCSSRequest } from '../plugins/css'
import {
cleanUrl,
normalizePath,
removeDepVersionQuery,
removeImportQuery,
removeTimestampQuery
} from '../utils'
Expand All @@ -15,11 +17,11 @@ export class ModuleNode {
* Public served url path, starts with /
*/
url: string
file: string
/**
* Resolved file system path + query
*/
id: string | null = null
file: string | null = null
type: 'js' | 'css'
info?: ModuleInfo
meta?: Record<string, any>
Expand All @@ -37,14 +39,14 @@ export class ModuleNode {
lastInvalidationTimestamp = 0

/**
* @param setIsSelfAccepting - set `false` to set `isSelfAccepting` later. e.g. #7870
* @param allowHmrPropagation - set `isSelfAccepting` to false to ensure it's
* never undefined (which prevents HMR update propagation, see #7870)
*/
constructor(url: string, setIsSelfAccepting = true) {
constructor(url: string, file: string, isSelfAccepting?: boolean) {
this.url = url
this.file = file
this.type = isDirectCSSRequest(url) ? 'css' : 'js'
if (setIsSelfAccepting) {
this.isSelfAccepting = false
}
this.isSelfAccepting = isSelfAccepting
}
}

Expand Down Expand Up @@ -87,7 +89,8 @@ export class ModuleGraph {
}

getModuleById(id: string): ModuleNode | undefined {
return this.idToModuleMap.get(removeTimestampQuery(id))
const unhashedId = removeDepVersionQuery(id)
return this.idToModuleMap.get(unhashedId) || this.idToModuleMap.get(id)
}

getModulesByFile(file: string): Set<ModuleNode> | undefined {
Expand Down Expand Up @@ -149,7 +152,7 @@ export class ModuleGraph {
for (const imported of importedModules) {
const dep =
typeof imported === 'string'
? await this.ensureEntryFromUrl(imported, ssr)
? this.urlToModuleMap.get(imported)!
Copy link
Member Author

Choose a reason for hiding this comment

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

Anything in importedModules has a module node at this point, thanks to the importAnalysis plugin.

: imported
dep.importers.add(mod)
nextImports.add(dep)
Expand All @@ -169,7 +172,7 @@ export class ModuleGraph {
for (const accepted of acceptedModules) {
const dep =
typeof accepted === 'string'
? await this.ensureEntryFromUrl(accepted, ssr)
? await this.ensureEntryFromUrl(accepted, ssr, false)
: accepted
deps.add(dep)
}
Expand All @@ -179,20 +182,33 @@ export class ModuleGraph {
return noLongerImported
}

async ensureEntryFromUrl(
rawUrl: string,
ssr?: boolean,
setIsSelfAccepting = true
): Promise<ModuleNode> {
const [url, resolvedId, meta] = await this.resolveUrl(rawUrl, ssr)
let mod = this.idToModuleMap.get(resolvedId)
ensureEntryFromResolved(
Copy link
Member Author

Choose a reason for hiding this comment

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

A new method that allows adding a module node without calling resolveUrl again.

[url, resolvedId, meta]: ResolvedUrl,
isSelfAccepting?: boolean
): ModuleNode {
const unhashedId = removeDepVersionQuery(resolvedId)
if (this.idToModuleMap.has(unhashedId)) {
resolvedId = unhashedId
Comment on lines +189 to +191
Copy link
Member Author

Choose a reason for hiding this comment

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

We have to strip the version query here and see if a module node exists without, as we can't safely remove it from inside ensureEntryFromUrl.

}
const modForId = this.idToModuleMap.get(resolvedId)
const modForUrl = this.urlToModuleMap.get(url)

// When metadata is derived from the browser URL, we cannot reuse the module
// node for the resolved Rollup id, as load/transform hooks may use the
// metadata to produce a different result.
let mod = (deepEqual(meta, modForId?.meta) && modForId) || modForUrl

if (!mod) {
mod = new ModuleNode(url, setIsSelfAccepting)
if (meta) mod.meta = meta
this.urlToModuleMap.set(url, mod)
const file = cleanUrl(resolvedId)
mod = new ModuleNode(url, file, isSelfAccepting)
mod.id = resolvedId
this.idToModuleMap.set(resolvedId, mod)
const file = (mod.file = cleanUrl(resolvedId))
if (meta) {
mod.meta = meta
}
this.urlToModuleMap.set(url, mod)
if (!modForId) {
this.idToModuleMap.set(resolvedId, mod)
}
let fileMappedModules = this.fileToModulesMap.get(file)
if (!fileMappedModules) {
fileMappedModules = new Set()
Expand All @@ -202,12 +218,22 @@ export class ModuleGraph {
}
// multiple urls can map to the same module and id, make sure we register
// the url to the existing module in that case
else if (!this.urlToModuleMap.has(url)) {
else if (!modForUrl) {
this.urlToModuleMap.set(url, mod)
}

return mod
}

async ensureEntryFromUrl(
rawUrl: string,
ssr?: boolean,
isSelfAccepting?: boolean
): Promise<ModuleNode> {
const resolved = await this.resolveUrl(rawUrl, ssr)
return this.ensureEntryFromResolved(resolved, isSelfAccepting)
}

// some deps, like a css file referenced via @import, don't have its own
// url because they are inlined into the main css import. But they still
// need to be represented in the module graph so that they can trigger
Expand All @@ -227,8 +253,7 @@ export class ModuleGraph {
}
}

const mod = new ModuleNode(url)
mod.file = file
const mod = new ModuleNode(url, file)
fileMappedModules.add(mod)
return mod
}
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/server/transformRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ async function loadAndTransform(
throw err
}
// ensure module in graph after successful load
const mod = await moduleGraph.ensureEntryFromUrl(url, ssr)
const mod = await moduleGraph.ensureEntryFromUrl(url, ssr, false)
ensureWatchedFile(watcher, mod.file, root)

// transform
Expand Down
4 changes: 2 additions & 2 deletions packages/vite/src/node/ssr/ssrModuleLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ async function instantiateModule(

const ssrImportMeta = {
// The filesystem URL, matching native Node.js modules
url: pathToFileURL(mod.file!).toString()
url: pathToFileURL(mod.file).toString()
}

urlStack = urlStack.concat(url)
Expand Down Expand Up @@ -137,7 +137,7 @@ async function instantiateModule(

const ssrImport = async (dep: string) => {
if (dep[0] !== '.' && dep[0] !== '/') {
return nodeImport(dep, mod.file!, resolveOptions)
return nodeImport(dep, mod.file, resolveOptions)
}
// convert to rollup URL because `pendingImports`, `moduleGraph.urlToModuleMap` requires that
dep = unwrapId(dep)
Expand Down
23 changes: 15 additions & 8 deletions packages/vite/src/node/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ import {
OPTIMIZABLE_ENTRY_RE,
VALID_ID_PREFIX,
loopbackHosts,
wildcardHosts
wildcardHosts,
DEP_VERSION_RE
} from './constants'
import type { DepOptimizationConfig } from './optimizer'
import type { ResolvedConfig } from './config'
Expand Down Expand Up @@ -278,10 +279,10 @@ 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
}
url = cleanUrl(url)
if (!path.extname(url) && !url.endsWith('/')) {
return true
}
Expand Down Expand Up @@ -312,16 +313,22 @@ const internalPrefixes = [
]
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 removeQuery = (url: string, queryRE: RegExp): string =>
url.replace(queryRE, '$1').replace(trailingSeparatorRE, '')

export const removeImportQuery = (url: string): string =>
removeQuery(url, importQueryRE)

export const removeDirectQuery = (url: string): string =>
removeQuery(url, directRequestRE)

export const removeDepVersionQuery = (url: string): string =>
removeQuery(url, DEP_VERSION_RE)

export function injectQuery(url: string, queryToInject: string): string {
// encode percents for consistent behavior with pathToFileURL
Expand Down
Loading