Skip to content
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ node_modules
!**/glob-import/root/dir/node_modules
!**/fixtures/glob-exports/node_modules
!**/fixtures/glob-exports/node_modules/my-pkg/dist
!**/fixtures/config/native-compat/json-named-import-bare/node_modules
!**/fixtures/config/native-compat/json-named-import-ok/node_modules
playground-temp
temp
TODOs.md
Expand Down
2 changes: 2 additions & 0 deletions docs/guide/api-environment-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,8 @@ export default defineConfig({
})
```

Note that the plugin returned from `applyToEnvironment` or `perEnvironmentPlugin` should not use Vite-specific hooks.

The `applyToEnvironment` hook is called at config time, currently after `configResolved` due to projects in the ecosystem modifying the plugins in it. Environment plugins resolution may be moved before `configResolved` in the future.

## Application-Plugin Communication
Expand Down
63 changes: 63 additions & 0 deletions packages/vite/src/node/__tests__/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1167,6 +1167,43 @@ describe('resolveConfig', () => {
await resolveConfig({ root: './inc?ud#s*', customLogger: logger }, 'build')
})

test('warns about ignored hooks returned from applyToEnvironment', async () => {
const warn = vi.fn()
const logger = createLogger('info', {
console: { warn } as unknown as Console,
})

await resolveConfig(
{
configFile: false,
customLogger: logger,
plugins: [
{
name: 'parent-plugin',
applyToEnvironment() {
return {
name: 'environment-plugin',
config: () => undefined,
configEnvironment: () => undefined,
configureServer: () => undefined,
configResolved: () => undefined,
resolveId: () => undefined,
}
},
},
],
},
'serve',
)

expect(warn).toHaveBeenCalledOnce()
expect(warn).toHaveBeenCalledWith(
expect.stringContaining(
'Plugin "environment-plugin" defines Vite-specific hooks (config, configEnvironment, configureServer, configResolved) in a plugin returned from applyToEnvironment. These hooks will be ignored.',
),
)
})

test('syncs `build.rollupOptions` and `build.rolldownOptions`', async () => {
const resolved = await resolveConfig({}, 'build')
expect(resolved.build!.rollupOptions).toStrictEqual(
Expand Down Expand Up @@ -1728,6 +1765,32 @@ describe('loadConfigFromFile', () => {
expect(await loadWithWarnings('json-ok')).toHaveLength(0)
})

test('warns on named import from JSON module', async () => {
const messages = await loadWithWarnings('json-named-import')
expect(messages).toMatchInlineSnapshot(`
[
"(!) Your Vite config uses features that are unsupported by \`configLoader: 'native'\`, which is planned to become the default in a future major version of Vite:
- named import from JSON module "./data.json" (vite.config.js:1:10). JSON modules only provide a default export per spec. Use the default import and access the property
Set \`VITE_CONFIG_NATIVE_IGNORE_WARNING=true\` to suppress this warning.",
]
`)
})

test('warns on named import from a bare JSON specifier', async () => {
const messages = await loadWithWarnings('json-named-import-bare')
expect(messages).toMatchInlineSnapshot(`
[
"(!) Your Vite config uses features that are unsupported by \`configLoader: 'native'\`, which is planned to become the default in a future major version of Vite:
- named import from JSON module "some-pkg/package.json" (vite.config.js:1:10). JSON modules only provide a default export per spec. Use the default import and access the property
Set \`VITE_CONFIG_NATIVE_IGNORE_WARNING=true\` to suppress this warning.",
]
`)
})

test('does not warn on JSON default imports (`default as` included)', async () => {
expect(await loadWithWarnings('json-named-import-ok')).toHaveLength(0)
})

test('warns on an extension-less import that resolves to JSON', async () => {
const messages = await loadWithWarnings('json-extensionless')
expect(messages).toMatchInlineSnapshot(`
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { version } from 'some-pkg/package.json' with { type: 'json' }

export default { define: { VERSION: JSON.stringify(version) } }
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "version": "1.0.0" }

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import somePkg from 'some-pkg/package.json' with { type: 'json' }
import data, { default as dataAlias } from './data.json' with { type: 'json' }

export default {
define: {
VERSION: JSON.stringify(data.version + dataAlias.version + somePkg.version),
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "version": "1.0.0" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { version } from './data.json' with { type: 'json' }

export default { define: { VERSION: JSON.stringify(version) } }
20 changes: 20 additions & 0 deletions packages/vite/src/node/__tests__/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,26 @@ describe('processSrcSetSync', () => {
).toBe('"/base/nested/asset.png" 1x, "/base/nested/asset.png" 2x')
})

test('keep the url when a density descriptor omits its leading zero', async () => {
const devBase = '/base/'
expect(
processSrcSetSync(
'./nested/asset.png .5x, ./nested/asset.png 1x',
({ url }) => path.posix.join(devBase, url),
),
).toBe('/base/nested/asset.png .5x, /base/nested/asset.png 1x')
})

test('keep the quoted url when a density descriptor omits its leading zero', async () => {
const devBase = '/base/'
expect(
processSrcSetSync(
'"./nested/asset.png" .75x,"./nested/asset.png" 1x',
({ url }) => `"${path.posix.join(devBase, url.slice(1, -1))}"`,
),
).toBe('"/base/nested/asset.png" .75x, "/base/nested/asset.png" 1x')
})

test('should not split the comma inside base64 value', async () => {
const base64 =
'data:image/avif;base64,aA+/0= 400w, data:image/avif;base64,bB+/9= 800w'
Expand Down
80 changes: 72 additions & 8 deletions packages/vite/src/node/nativeConfigCompat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type NativeConfigIncompatibilityType =
| 'extensionless-import'
| 'directory-index-import'
| 'json-without-attributes'
| 'json-named-import'
| 'esm-syntax-in-cjs'

export interface NativeConfigIncompatibility {
Expand All @@ -31,6 +32,8 @@ export interface ConfigImportRef {
line: number
column: number
hasTypeJsonAttribute: boolean
/** position of the first non-default named binding */
namedImportLoc?: { line: number; column: number }
}

const jsTsExtRE = /\.[cm]?[jt]sx?$/
Expand All @@ -56,14 +59,36 @@ export function classifyImportRef(
const base = { file, line, column, specifier }

if (specifier.endsWith('.json')) {
if (ref.hasTypeJsonAttribute) return undefined
return { type: 'json-without-attributes', ...base }
if (!ref.hasTypeJsonAttribute) {
return { type: 'json-without-attributes', ...base }
}
if (ref.namedImportLoc) {
return {
type: 'json-named-import',
file,
line: ref.namedImportLoc.line,
column: ref.namedImportLoc.column,
specifier,
}
}
return undefined
}

if (!resolvedId) return undefined

if (resolvedId.endsWith('.json') && !ref.hasTypeJsonAttribute) {
return { type: 'json-without-attributes', ...base }
if (resolvedId.endsWith('.json')) {
if (!ref.hasTypeJsonAttribute) {
return { type: 'json-without-attributes', ...base }
}
if (ref.namedImportLoc) {
return {
type: 'json-named-import',
file,
line: ref.namedImportLoc.line,
column: ref.namedImportLoc.column,
specifier,
}
}
}

const lastSegment = lastSegmentOf(specifier)
Expand All @@ -89,6 +114,24 @@ const hasTypeJson = (
return key === 'type' && attr.value?.value === 'json'
})

const findNonDefaultNamedBinding = (
specifiers: ESTree.Node[],
): ESTree.Node | undefined =>
specifiers.find((s) => {
if (s.type === 'ImportSpecifier') {
return !isDefaultModuleExportName(s.imported)
}
if (s.type === 'ExportSpecifier') {
return !isDefaultModuleExportName(s.local)
}
return false
})

const isDefaultModuleExportName = (node: ESTree.ModuleExportName): boolean =>
node.type === 'Identifier'
? node.name === 'default'
: node.value === 'default'

const DIRNAME_FILENAME = {
__dirname: 'dirname',
__filename: 'filename',
Expand All @@ -104,14 +147,23 @@ export function analyzeConfigModuleReferences(
const addImportRef = (
source: ESTree.StringLiteral,
hasTypeJsonAttribute: boolean,
namedBinding: ESTree.Node | undefined,
): void => {
if (!isPathSpecifier(source.value)) return
// bare specifiers are skipped except for the JSON checks, which classify
// from the specifier alone (`vue/package.json` etc.)
if (!isPathSpecifier(source.value) && !source.value.endsWith('.json')) {
return
}
const { line, column } = numberToPos(code, source.start)
const namedImportLoc = namedBinding
? numberToPos(code, namedBinding.start)
: undefined
imports.push({
specifier: source.value,
line,
column,
hasTypeJsonAttribute,
namedImportLoc,
})
}

Expand All @@ -120,20 +172,30 @@ export function analyzeConfigModuleReferences(
const node = _node as ESTree.Node
switch (node.type) {
case 'ImportDeclaration':
addImportRef(node.source, hasTypeJson(node.attributes))
addImportRef(
node.source,
hasTypeJson(node.attributes),
findNonDefaultNamedBinding(node.specifiers),
)
break
case 'ExportNamedDeclaration':
case 'ExportAllDeclaration':
if (node.source)
addImportRef(node.source, hasTypeJson(node.attributes))
addImportRef(
node.source,
hasTypeJson(node.attributes),
node.type === 'ExportNamedDeclaration'
? findNonDefaultNamedBinding(node.specifiers)
: undefined,
)
break
case 'ImportExpression':
if (
node.source.type === 'Literal' &&
typeof node.source.value === 'string'
) {
// if a second (options) arg is present, assume the required attributes is set
addImportRef(node.source, node.options != null)
addImportRef(node.source, node.options != null, undefined)
}
break
}
Expand Down Expand Up @@ -194,6 +256,8 @@ function describeIncompatibility(
return item.specifier?.endsWith('.json')
? `JSON import "${item.specifier}" without import attributes (${loc}). Add \`with { type: 'json' }\``
: `import "${item.specifier}" resolves to a JSON file (${loc}). Import it with a \`.json\` extension and \`with { type: 'json' }\``
case 'json-named-import':
return `named import from JSON module "${item.specifier}" (${loc}). JSON modules only provide a default export per spec. Use the default import and access the property`
case 'esm-syntax-in-cjs':
return `ESM syntax in a file loaded as CommonJS (${loc}). Use a \`.mjs\` extension or set \`"type": "module"\` in the closest package.json`
}
Expand Down
26 changes: 21 additions & 5 deletions packages/vite/src/node/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,13 @@ export type PluginOption = Thenable<
| PluginOption[]
>

const ignoredEnvironmentPluginHooks = [
'config',
'configEnvironment',
'configureServer',
'configResolved',
] as const

export async function resolveEnvironmentPlugins(
environment: PartialEnvironment,
): Promise<Plugin[]> {
Expand All @@ -412,11 +419,20 @@ export async function resolveEnvironmentPlugins(
continue
}
if (applied !== true) {
environmentPlugins.push(
...((await asyncFlatten(arraify(applied))).filter(
Boolean,
) as Plugin[]),
)
const appliedPlugins = (await asyncFlatten(arraify(applied))).filter(
Boolean,
) as Plugin[]
for (const appliedPlugin of appliedPlugins) {
const ignoredHooks = ignoredEnvironmentPluginHooks.filter(
(hook) => appliedPlugin[hook],
)
if (ignoredHooks.length > 0) {
environment.logger.warnOnce(
`Plugin "${appliedPlugin.name}" defines Vite-specific hooks (${ignoredHooks.join(', ')}) in a plugin returned from applyToEnvironment. These hooks will be ignored.`,
)
}
}
environmentPlugins.push(...appliedPlugins)
continue
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -814,7 +814,7 @@ function joinSrcset(ret: ImageCandidate[]) {
The `descriptor` is anything after the space and before the comma.
*/
const imageCandidateRegex =
/(?:^|\s|(?<=,))(?<url>[\w-]+\([^)]*\)|"[^"]*"|'[^']*'|[^,]\S*[^,])\s*(?:\s(?<descriptor>\w[^,]+))?(?:,|$)/g
/(?:^|\s|(?<=,))(?<url>[\w-]+\([^)]*\)|"[^"]*"|'[^']*'|[^,]\S*[^,])\s*(?:\s(?<descriptor>[\w.][^,]+))?(?:,|$)/g
const escapedSpaceCharacters = /(?: |\\t|\\n|\\f|\\r)+/g

export function parseSrcset(string: string): ImageCandidate[] {
Expand Down
Loading