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

feat: support moduleInfo.meta in dev server #5465

Merged
merged 7 commits into from
Nov 10, 2021
2 changes: 1 addition & 1 deletion packages/vite/src/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export type {
} from './plugins/resolve'
export type { WebSocketServer } from './server/ws'
export type { PluginContainer } from './server/pluginContainer'
export type { ModuleGraph, ModuleNode } from './server/moduleGraph'
export type { ModuleGraph, ModuleNode, ResolvedUrl } from './server/moduleGraph'
export type { ProxyOptions } from './server/middlewares/proxy'
export type {
TransformOptions,
Expand Down
117 changes: 117 additions & 0 deletions packages/vite/src/node/server/__tests__/pluginContainer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { resolveConfig, UserConfig } from '../../config'
import { Plugin } from '../../plugin'
import { ModuleGraph } from '../moduleGraph'
import { createPluginContainer, PluginContainer } from '../pluginContainer'

let resolveId: (id: string) => any
let moduleGraph: ModuleGraph

describe('plugin container', () => {
describe('getModuleInfo', () => {
beforeEach(() => {
moduleGraph = new ModuleGraph((id) => resolveId(id))
})

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

const metaArray: any[] = []
const plugin: Plugin = {
name: 'p1',
resolveId(id) {
if (id === entryUrl) {
// The module hasn't been resolved yet, so its info is null.
const moduleInfo = this.getModuleInfo(entryUrl)
expect(moduleInfo).toEqual(null)

return { id, meta: { x: 1 } }
}
},
load(id) {
if (id === entryUrl) {
const { meta } = this.getModuleInfo(entryUrl)
metaArray.push(meta)

return { code: 'export {}', meta: { x: 2 } }
}
},
transform(code, id) {
if (id === entryUrl) {
const { meta } = this.getModuleInfo(entryUrl)
metaArray.push(meta)

return { meta: { x: 3 } }
}
},
buildEnd() {
const { meta } = this.getModuleInfo(entryUrl)
metaArray.push(meta)
}
}

const container = await getPluginContainer({
plugins: [plugin]
})

const entryModule = await moduleGraph.ensureEntryFromUrl(entryUrl)
expect(entryModule.meta).toEqual({ x: 1 })

const loadResult: any = await container.load(entryUrl)
expect(loadResult?.meta).toEqual({ x: 2 })

await container.transform(loadResult.code, entryUrl)
await container.close()

expect(metaArray).toEqual([{ x: 1 }, { x: 2 }, { x: 3 }])
})

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

const plugin1: Plugin = {
name: 'p1',
resolveId(id) {
if (id === entryUrl) {
return { id, meta: { x: 1 } }
}
}
}

const plugin2: Plugin = {
name: 'p2',
load(id) {
if (id === entryUrl) {
const { meta } = this.getModuleInfo(entryUrl)
expect(meta).toEqual({ x: 1 })
return null
}
}
}

const container = await getPluginContainer({
plugins: [plugin1, plugin2]
})

await moduleGraph.ensureEntryFromUrl(entryUrl)
await container.load(entryUrl)

expect.assertions(1)
})
})
})

async function getPluginContainer(
inlineConfig?: UserConfig
): Promise<PluginContainer> {
const config = await resolveConfig(
{ configFile: false, ...inlineConfig },
'serve'
)

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

resolveId = (id) => container.resolveId(id)
const container = await createPluginContainer(config, moduleGraph)
return container
}
10 changes: 6 additions & 4 deletions packages/vite/src/node/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,9 +358,11 @@ export async function createServer(
...watchOptions
}) as FSWatcher

const plugins = config.plugins
const container = await createPluginContainer(config, watcher)
const moduleGraph = new ModuleGraph(container)
const moduleGraph: ModuleGraph = new ModuleGraph((url) =>
container.resolveId(url)
Copy link
Member Author

Choose a reason for hiding this comment

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

This creates a tight coupling between ModuleGraph and createPluginContainer, but I don't see any way to avoid that. And it's probably not a big deal.

)

const container = await createPluginContainer(config, moduleGraph, watcher)
const closeHttpServer = createServerCloseFn(httpServer)

// eslint-disable-next-line prefer-const
Expand Down Expand Up @@ -484,7 +486,7 @@ export async function createServer(

// apply server configuration hooks from plugins
const postHooks: ((() => void) | void)[] = []
for (const plugin of plugins) {
for (const plugin of config.plugins) {
if (plugin.configureServer) {
postHooks.push(await plugin.configureServer(server))
}
Expand Down
31 changes: 21 additions & 10 deletions packages/vite/src/node/server/moduleGraph.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { extname } from 'path'
import { ModuleInfo, PartialResolvedId } from 'rollup'
import { parse as parseUrl } from 'url'
import { isDirectCSSRequest } from '../plugins/css'
import {
cleanUrl,
Expand All @@ -8,8 +10,6 @@ import {
} from '../utils'
import { FS_PREFIX } from '../constants'
import { TransformResult } from './transformRequest'
import { PluginContainer } from './pluginContainer'
import { parse as parseUrl } from 'url'

export class ModuleNode {
/**
Expand All @@ -22,6 +22,8 @@ export class ModuleNode {
id: string | null = null
file: string | null = null
type: 'js' | 'css'
info?: ModuleInfo
meta?: Record<string, any>
importers = new Set<ModuleNode>()
importedModules = new Set<ModuleNode>()
acceptedHmrDeps = new Set<ModuleNode>()
Expand All @@ -45,17 +47,23 @@ function invalidateSSRModule(mod: ModuleNode, seen: Set<ModuleNode>) {
mod.ssrModule = null
mod.importers.forEach((importer) => invalidateSSRModule(importer, seen))
}

export type ResolvedUrl = [
url: string,
resolvedId: string,
meta: object | null | undefined
Copy link
Member

Choose a reason for hiding this comment

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

non-blocking: Is there a better type then object

]

export class ModuleGraph {
urlToModuleMap = new Map<string, ModuleNode>()
idToModuleMap = new Map<string, ModuleNode>()
// a single file may corresponds to multiple modules with different queries
fileToModulesMap = new Map<string, Set<ModuleNode>>()
safeModulesPath = new Set<string>()
container: PluginContainer

constructor(container: PluginContainer) {
this.container = container
}
constructor(
private resolveId: (url: string) => Promise<PartialResolvedId | null>
) {}

async getModuleByUrl(rawUrl: string): Promise<ModuleNode | undefined> {
const [url] = await this.resolveUrl(rawUrl)
Expand All @@ -81,6 +89,7 @@ export class ModuleGraph {
}

invalidateModule(mod: ModuleNode, seen: Set<ModuleNode> = new Set()): void {
mod.info = undefined
mod.transformResult = null
mod.ssrTransformResult = null
invalidateSSRModule(mod, seen)
Expand Down Expand Up @@ -140,10 +149,11 @@ export class ModuleGraph {
}

async ensureEntryFromUrl(rawUrl: string): Promise<ModuleNode> {
const [url, resolvedId] = await this.resolveUrl(rawUrl)
const [url, resolvedId, meta] = await this.resolveUrl(rawUrl)
let mod = this.urlToModuleMap.get(url)
if (!mod) {
mod = new ModuleNode(url)
if (meta) mod.meta = meta
this.urlToModuleMap.set(url, mod)
mod.id = resolvedId
this.idToModuleMap.set(resolvedId, mod)
Expand Down Expand Up @@ -187,14 +197,15 @@ export class ModuleGraph {
// 1. remove the HMR timestamp query (?t=xxxx)
// 2. resolve its extension so that urls with or without extension all map to
// the same module
async resolveUrl(url: string): Promise<[string, string]> {
async resolveUrl(url: string): Promise<ResolvedUrl> {
url = removeImportQuery(removeTimestampQuery(url))
const resolvedId = (await this.container.resolveId(url))?.id || url
const resolved = await this.resolveId(url)
const resolvedId = resolved?.id || url
const ext = extname(cleanUrl(resolvedId))
const { pathname, search, hash } = parseUrl(url)
if (ext && !pathname!.endsWith(ext)) {
url = pathname + ext + (search || '') + (hash || '')
}
return [url, resolvedId]
return [url, resolvedId, resolved?.meta]
}
}
71 changes: 58 additions & 13 deletions packages/vite/src/node/server/pluginContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import { FS_PREFIX } from '../constants'
import chalk from 'chalk'
import { ResolvedConfig } from '../config'
import { buildErrorMessage } from './middlewares/error'
import { ModuleGraph } from './moduleGraph'
import { performance } from 'perf_hooks'

export interface PluginContainerOptions {
Expand All @@ -81,6 +82,7 @@ export interface PluginContainerOptions {

export interface PluginContainer {
options: InputOptions
getModuleInfo(id: string): ModuleInfo | null
buildStart(options: InputOptions): Promise<void>
resolveId(
id: string,
Expand Down Expand Up @@ -128,6 +130,7 @@ export let parser = acorn.Parser.extend(

export async function createPluginContainer(
{ plugins, logger, root, build: { rollupOptions } }: ResolvedConfig,
moduleGraph?: ModuleGraph,
watcher?: FSWatcher
): Promise<PluginContainer> {
const isDebug = process.env.DEBUG
Expand All @@ -143,7 +146,6 @@ export async function createPluginContainer(

// ---------------------------------------------------------------------------

const MODULES = new Map()
const watchFiles = new Set<string>()

// get rollup version
Expand All @@ -167,6 +169,45 @@ export async function createPluginContainer(
)
}

// throw when an unsupported ModuleInfo property is accessed,
// so that incompatible plugins fail in a non-cryptic way.
const ModuleInfoProxy: ProxyHandler<ModuleInfo> = {
get(info: any, key: string) {
if (key in info) {
return info[key]
}
throw Error(
`[vite] The "${key}" property of ModuleInfo is not supported.`
)
}
}

// same default value of "moduleInfo.meta" as in Rollup
const EMPTY_OBJECT = Object.freeze({})

function getModuleInfo(id: string) {
const module = moduleGraph?.getModuleById(id)
if (!module) {
return null
}
if (!module.info) {
module.info = new Proxy(
{ id, meta: module.meta || EMPTY_OBJECT } as ModuleInfo,
ModuleInfoProxy
)
}
return module.info
}

function updateModuleInfo(id: string, { meta }: { meta?: object | null }) {
if (meta) {
const moduleInfo = getModuleInfo(id)
if (moduleInfo) {
moduleInfo.meta = { ...moduleInfo.meta, ...meta }
}
}
}

// we should create a new context for each async hook pipeline so that the
// active plugin in that pipeline can be tracked in a concurrency-safe manner.
// using a class to make creating new contexts more efficient
Expand Down Expand Up @@ -208,19 +249,13 @@ export async function createPluginContainer(
}

getModuleInfo(id: string) {
let mod = MODULES.get(id)
if (mod) return mod.info
mod = {
/** @type {import('rollup').ModuleInfo} */
// @ts-ignore-next
info: {}
}
MODULES.set(id, mod)
return mod.info
return getModuleInfo(id)
}

getModuleIds() {
return MODULES.keys()
return moduleGraph
? moduleGraph.idToModuleMap.keys()
: Array.prototype[Symbol.iterator]()
}

addWatchFile(id: string) {
Expand Down Expand Up @@ -416,6 +451,8 @@ export async function createPluginContainer(
}
})(),

getModuleInfo,

async buildStart() {
await Promise.all(
plugins.map((plugin) => {
Expand Down Expand Up @@ -500,6 +537,9 @@ export async function createPluginContainer(
ctx._activePlugin = plugin
const result = await plugin.load.call(ctx as any, id, { ssr })
if (result != null) {
if (isObject(result)) {
updateModuleInfo(id, result)
}
return result
}
}
Expand Down Expand Up @@ -531,8 +571,13 @@ export async function createPluginContainer(
prettifyUrl(id, root)
)
if (isObject(result)) {
code = result.code || ''
if (result.map) ctx.sourcemapChain.push(result.map)
if (result.code !== undefined) {
code = result.code
if (result.map) {
ctx.sourcemapChain.push(result.map)
}
}
updateModuleInfo(id, result)
} else {
code = result
}
Expand Down