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(imports): support for import attributes in Vite #17485

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export default tseslint.config(
'**/temp/**',
'**/.vitepress/cache/**',
'**/*.snap',
// eslint doesn't support import attributes until typescript 5.3
'playground/resolve/import-attributes.js',
'playground/import-attributes/import-assertion-dep/index.js',
],
},
eslint.configs.recommended,
Expand Down
5 changes: 4 additions & 1 deletion packages/vite/src/node/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,10 @@ export interface Plugin<A = any> extends RollupPlugin<A> {
(
this: PluginContext,
id: string,
options?: { ssr?: boolean },
options?: {
attributes?: Record<string, string> | null
ssr?: boolean
},
) => Promise<LoadResult> | LoadResult
>
transform?: ObjectHook<
Expand Down
18 changes: 15 additions & 3 deletions packages/vite/src/node/plugins/importAnalysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
} from '../server/hmr'
import {
createDebugger,
evalValue,
fsPathFromUrl,
generateCodeFrame,
injectQuery,
Expand Down Expand Up @@ -294,6 +295,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
url: string,
pos: number,
forceSkipImportAnalysis: boolean = false,
attributes?: Record<string, string>,
): Promise<[string, string]> => {
url = stripBase(url, base)

Expand All @@ -317,7 +319,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
}
}

const resolved = await this.resolve(url, importerFile)
const resolved = await this.resolve(url, importerFile, { attributes })

if (!resolved || resolved.meta?.['vite:alias']?.noResolved) {
// in ssr, we should let node handle the missing modules
Expand Down Expand Up @@ -491,8 +493,13 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {

const isDynamicImport = dynamicIndex > -1

// strip import attributes as we can process them ourselves
// Grab the import attributes
let importAttributes: Record<string, any> | undefined = undefined
if (!isDynamicImport && attributeIndex > -1) {
const raw = source.substring(attributeIndex, expEnd)
importAttributes = evalValue<{}>(raw)

// strip import attributes as we can process them ourselves
str().remove(end + 1, expEnd)
}

Expand Down Expand Up @@ -538,7 +545,12 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
}

// normalize
const [url, resolvedId] = await normalizeUrl(specifier, start)
const [url, resolvedId] = await normalizeUrl(
specifier,
start,
undefined,
importAttributes,
)

// record as safe modules
// safeModulesPath should not include the base prefix.
Expand Down
7 changes: 0 additions & 7 deletions packages/vite/src/node/plugins/importAnalysisBuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,20 +304,13 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {
for (let index = 0; index < imports.length; index++) {
const {
s: start,
e: end,
ss: expStart,
se: expEnd,
d: dynamicIndex,
a: attributeIndex,
} = imports[index]

const isDynamicImport = dynamicIndex > -1

// strip import attributes as we can process them ourselves
if (!isDynamicImport && attributeIndex > -1) {
str().remove(end + 1, expEnd)
}

if (
isDynamicImport &&
insertPreload &&
Expand Down
81 changes: 81 additions & 0 deletions packages/vite/src/node/server/__tests__/pluginContainer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,87 @@ describe('plugin container', () => {

expect.assertions(2)
})

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

const attributesArray: any[] = []
const plugin: Plugin = {
name: 'p1',
resolveId(id) {
if (id === entryUrl) {
return { id, attributes: { x: '1' } }
}
},
load(id) {
if (id === entryUrl) {
const { attributes } = this.getModuleInfo(id) ?? {}
attributesArray.push(attributes)
return { code: 'export {}', attributes: { x: '2' } }
}
},
transform(code, id) {
if (id === entryUrl) {
const { attributes } = this.getModuleInfo(entryUrl) ?? {}
attributesArray.push(attributes)

return { attributes: { x: '3' } }
}
},
buildEnd() {
const { attributes } = this.getModuleInfo(entryUrl) ?? {}
attributesArray.push(attributes)
},
}

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

const entryModule = await moduleGraph.ensureEntryFromUrl(entryUrl, false)
expect(entryModule.attributes).toEqual({ x: '1' })

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

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

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

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

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

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

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

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

expect.assertions(1)
})
})

describe('load', () => {
Expand Down
9 changes: 6 additions & 3 deletions packages/vite/src/node/server/moduleGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export class ModuleNode {
type: 'js' | 'css'
info?: ModuleInfo
meta?: Record<string, any>
attributes?: Record<string, string>
importers = new Set<ModuleNode>()
clientImportedModules = new Set<ModuleNode>()
ssrImportedModules = new Set<ModuleNode>()
Expand Down Expand Up @@ -90,6 +91,7 @@ export type ResolvedUrl = [
url: string,
resolvedId: string,
meta: object | null | undefined,
attributes: Record<string, string> | null | undefined,
]

export class ModuleGraph {
Expand Down Expand Up @@ -374,7 +376,7 @@ export class ModuleGraph {
return mod
}
const modPromise = (async () => {
const [url, resolvedId, meta] = await this._resolveUrl(
const [url, resolvedId, meta, attributes] = await this._resolveUrl(
rawUrl,
ssr,
resolved,
Expand All @@ -383,6 +385,7 @@ export class ModuleGraph {
if (!mod) {
mod = new ModuleNode(url, setIsSelfAccepting)
if (meta) mod.meta = meta
if (attributes) mod.attributes = attributes
this.urlToModuleMap.set(url, mod)
mod.id = resolvedId
this.idToModuleMap.set(resolvedId, mod)
Expand Down Expand Up @@ -442,7 +445,7 @@ export class ModuleGraph {
url = removeImportQuery(removeTimestampQuery(url))
const mod = await this._getUnresolvedUrlToModule(url, ssr)
if (mod?.id) {
return [mod.url, mod.id, mod.meta]
return [mod.url, mod.id, mod.meta, mod.attributes]
}
return this._resolveUrl(url, ssr)
}
Expand Down Expand Up @@ -516,6 +519,6 @@ export class ModuleGraph {
}
}
}
return [url, resolvedId, resolved?.meta]
return [url, resolvedId, resolved?.meta, resolved?.attributes]
}
}
27 changes: 21 additions & 6 deletions packages/vite/src/node/server/pluginContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,11 @@ class PluginContainer {
}
if (!module.info) {
module.info = new Proxy(
{ id, meta: module.meta || EMPTY_OBJECT } as ModuleInfo,
{
id,
meta: module.meta || EMPTY_OBJECT,
attributes: module.attributes || EMPTY_OBJECT,
} as ModuleInfo,
// throw when an unsupported ModuleInfo property is accessed,
// so that incompatible plugins fail in a non-cryptic way.
{
Expand Down Expand Up @@ -312,11 +316,12 @@ class PluginContainer {
const skip = options?.skip
const ssr = options?.ssr
const scan = !!options?.scan
const attributes = options?.attributes
const ctx = new ResolveIdContext(this, !!ssr, skip, scan)

const resolveStart = debugResolve ? performance.now() : 0
let id: string | null = null
const partial: Partial<PartialResolvedId> = {}
const partial: Partial<PartialResolvedId> = { attributes }

for (const plugin of this.getSortedPlugins('resolveId')) {
if (this._closed && !ssr) throwClosedServerError()
Expand All @@ -329,7 +334,7 @@ class PluginContainer {
const handler = getHookHandler(plugin.resolveId)
const result = await this.handleHookPromise(
handler.call(ctx as any, rawId, importer, {
attributes: options?.attributes ?? {},
attributes: attributes ?? {},
custom: options?.custom,
isEntry: !!options?.isEntry,
ssr,
Expand Down Expand Up @@ -380,9 +385,11 @@ class PluginContainer {
id: string,
options?: {
ssr?: boolean
attributes?: Record<string, string> | null
},
): Promise<LoadResult | null> {
const ssr = options?.ssr
const attributes = options?.attributes
const ctx = new LoadPluginContext(this, !!ssr)

for (const plugin of this.getSortedPlugins('load')) {
Expand All @@ -391,7 +398,7 @@ class PluginContainer {
ctx._plugin = plugin
const handler = getHookHandler(plugin.load)
const result = await this.handleHookPromise(
handler.call(ctx as any, id, { ssr }),
handler.call(ctx as any, id, { ssr, attributes }),
)
if (result != null) {
if (isObject(result)) {
Expand Down Expand Up @@ -565,6 +572,7 @@ class PluginContext implements Omit<RollupPluginContext, 'cache'> {

const loadResult = await this._container.load(options.id, {
ssr: this.ssr,
attributes: options.attributes,
})
const code = typeof loadResult === 'object' ? loadResult?.code : loadResult
if (code != null) {
Expand All @@ -579,11 +587,18 @@ class PluginContext implements Omit<RollupPluginContext, 'cache'> {
return moduleInfo
}

_updateModuleInfo(id: string, { meta }: { meta?: object | null }): void {
if (meta) {
_updateModuleInfo(
id: string,
{
meta,
attributes,
}: { meta?: object | null; attributes?: Record<string, any> | null },
): void {
if (meta || attributes) {
const moduleInfo = this.getModuleInfo(id)
if (moduleInfo) {
moduleInfo.meta = { ...moduleInfo.meta, ...meta }
moduleInfo.attributes = { ...moduleInfo.attributes, ...attributes }
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion packages/vite/src/node/server/transformRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,10 @@ async function loadAndTransform(

// load
const loadStart = debugLoad ? performance.now() : 0
const loadResult = await pluginContainer.load(id, { ssr })
const loadResult = await pluginContainer.load(id, {
ssr,
attributes: resolved?.attributes,
})
if (loadResult == null) {
// if this is an html request and there is no load result, skip ahead to
// SPA fallback.
Expand Down
10 changes: 10 additions & 0 deletions playground/import-attributes/__tests__/import-attributes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { expect, test } from 'vitest'
import { page } from '~utils'

test('from source code', async () => {
expect(await page.textContent('.src'), 'bar')
})

test('from dependency', async () => {
expect(await page.textContent('.dep'), 'world')
})
3 changes: 3 additions & 0 deletions playground/import-attributes/import-assertion-dep/data
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"hello": "world"
}
3 changes: 3 additions & 0 deletions playground/import-attributes/import-assertion-dep/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import json from './data' with { type: 'json' }

export const hello = json.hello
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "@vitejs/test-import-assertion-dep",
"private": true,
"version": "0.0.0",
"type": "module",
"exports": "./index.js"
}
19 changes: 19 additions & 0 deletions playground/import-attributes/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<h1>Import attribute</h1>

<h2>From source code</h2>
<p class="src"></p>

<h2>From dependency</h2>
<p class="dep"></p>

<script type="module">
import * as data from './json-file' with { type: 'json' }
text('.src', data.foo)

import * as depData from '@vitejs/test-import-assertion-dep'
text('.dep', depData.hello)

function text(el, text) {
document.querySelector(el).textContent = text
}
</script>
3 changes: 3 additions & 0 deletions playground/import-attributes/json-file
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"foo": "bar"
}
Loading
Loading