From 472385e6ec4b21e3167c7abf9769883d1c9675f8 Mon Sep 17 00:00:00 2001 From: btea <2356281422@qq.com> Date: Tue, 1 Sep 2026 11:48:39 +0800 Subject: [PATCH 1/3] feat(config): warn on named imports from JSON modules (#23378) --- .gitignore | 2 + .../vite/src/node/__tests__/config.spec.ts | 26 ++++++ .../node_modules/some-pkg/package.json | 4 + .../json-named-import-bare/vite.config.js | 3 + .../json-named-import-ok/data.json | 1 + .../node_modules/some-pkg/package.json | 4 + .../json-named-import-ok/vite.config.js | 8 ++ .../native-compat/json-named-import/data.json | 1 + .../json-named-import/vite.config.js | 3 + packages/vite/src/node/nativeConfigCompat.ts | 80 +++++++++++++++++-- 10 files changed, 124 insertions(+), 8 deletions(-) create mode 100644 packages/vite/src/node/__tests__/fixtures/config/native-compat/json-named-import-bare/node_modules/some-pkg/package.json create mode 100644 packages/vite/src/node/__tests__/fixtures/config/native-compat/json-named-import-bare/vite.config.js create mode 100644 packages/vite/src/node/__tests__/fixtures/config/native-compat/json-named-import-ok/data.json create mode 100644 packages/vite/src/node/__tests__/fixtures/config/native-compat/json-named-import-ok/node_modules/some-pkg/package.json create mode 100644 packages/vite/src/node/__tests__/fixtures/config/native-compat/json-named-import-ok/vite.config.js create mode 100644 packages/vite/src/node/__tests__/fixtures/config/native-compat/json-named-import/data.json create mode 100644 packages/vite/src/node/__tests__/fixtures/config/native-compat/json-named-import/vite.config.js diff --git a/.gitignore b/.gitignore index e5390bb32f579f..eb6ae07f5f9705 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/packages/vite/src/node/__tests__/config.spec.ts b/packages/vite/src/node/__tests__/config.spec.ts index 589602dc0df54d..58fdeeedf5bef2 100644 --- a/packages/vite/src/node/__tests__/config.spec.ts +++ b/packages/vite/src/node/__tests__/config.spec.ts @@ -1728,6 +1728,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(` diff --git a/packages/vite/src/node/__tests__/fixtures/config/native-compat/json-named-import-bare/node_modules/some-pkg/package.json b/packages/vite/src/node/__tests__/fixtures/config/native-compat/json-named-import-bare/node_modules/some-pkg/package.json new file mode 100644 index 00000000000000..16b2018255024a --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/config/native-compat/json-named-import-bare/node_modules/some-pkg/package.json @@ -0,0 +1,4 @@ +{ + "name": "some-pkg", + "version": "1.0.0" +} diff --git a/packages/vite/src/node/__tests__/fixtures/config/native-compat/json-named-import-bare/vite.config.js b/packages/vite/src/node/__tests__/fixtures/config/native-compat/json-named-import-bare/vite.config.js new file mode 100644 index 00000000000000..78537a5a64beec --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/config/native-compat/json-named-import-bare/vite.config.js @@ -0,0 +1,3 @@ +import { version } from 'some-pkg/package.json' with { type: 'json' } + +export default { define: { VERSION: JSON.stringify(version) } } diff --git a/packages/vite/src/node/__tests__/fixtures/config/native-compat/json-named-import-ok/data.json b/packages/vite/src/node/__tests__/fixtures/config/native-compat/json-named-import-ok/data.json new file mode 100644 index 00000000000000..012d571881352f --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/config/native-compat/json-named-import-ok/data.json @@ -0,0 +1 @@ +{ "version": "1.0.0" } diff --git a/packages/vite/src/node/__tests__/fixtures/config/native-compat/json-named-import-ok/node_modules/some-pkg/package.json b/packages/vite/src/node/__tests__/fixtures/config/native-compat/json-named-import-ok/node_modules/some-pkg/package.json new file mode 100644 index 00000000000000..16b2018255024a --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/config/native-compat/json-named-import-ok/node_modules/some-pkg/package.json @@ -0,0 +1,4 @@ +{ + "name": "some-pkg", + "version": "1.0.0" +} diff --git a/packages/vite/src/node/__tests__/fixtures/config/native-compat/json-named-import-ok/vite.config.js b/packages/vite/src/node/__tests__/fixtures/config/native-compat/json-named-import-ok/vite.config.js new file mode 100644 index 00000000000000..98db49c38132e4 --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/config/native-compat/json-named-import-ok/vite.config.js @@ -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), + }, +} diff --git a/packages/vite/src/node/__tests__/fixtures/config/native-compat/json-named-import/data.json b/packages/vite/src/node/__tests__/fixtures/config/native-compat/json-named-import/data.json new file mode 100644 index 00000000000000..012d571881352f --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/config/native-compat/json-named-import/data.json @@ -0,0 +1 @@ +{ "version": "1.0.0" } diff --git a/packages/vite/src/node/__tests__/fixtures/config/native-compat/json-named-import/vite.config.js b/packages/vite/src/node/__tests__/fixtures/config/native-compat/json-named-import/vite.config.js new file mode 100644 index 00000000000000..e4906dc1c1d03b --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/config/native-compat/json-named-import/vite.config.js @@ -0,0 +1,3 @@ +import { version } from './data.json' with { type: 'json' } + +export default { define: { VERSION: JSON.stringify(version) } } diff --git a/packages/vite/src/node/nativeConfigCompat.ts b/packages/vite/src/node/nativeConfigCompat.ts index 744968a0bbbd37..0bbef4afbabbaf 100644 --- a/packages/vite/src/node/nativeConfigCompat.ts +++ b/packages/vite/src/node/nativeConfigCompat.ts @@ -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 { @@ -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?$/ @@ -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) @@ -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', @@ -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, }) } @@ -120,12 +172,22 @@ 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 ( @@ -133,7 +195,7 @@ export function analyzeConfigModuleReferences( 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 } @@ -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` } From fdef04f112aadfea40ad3c448d96a49a04c168bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=A0?= Date: Tue, 1 Sep 2026 13:55:00 +0900 Subject: [PATCH 2/3] feat: add warning for unsupported hooks in plugin returned from `applyToEnvironment` hook (#23191) --- docs/guide/api-environment-plugins.md | 2 + .../vite/src/node/__tests__/config.spec.ts | 37 +++++++++++++++++++ packages/vite/src/node/plugin.ts | 26 ++++++++++--- 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/docs/guide/api-environment-plugins.md b/docs/guide/api-environment-plugins.md index cc0b7994bc2b6e..272adac8edc6d7 100644 --- a/docs/guide/api-environment-plugins.md +++ b/docs/guide/api-environment-plugins.md @@ -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 diff --git a/packages/vite/src/node/__tests__/config.spec.ts b/packages/vite/src/node/__tests__/config.spec.ts index 58fdeeedf5bef2..0dcd4645745d85 100644 --- a/packages/vite/src/node/__tests__/config.spec.ts +++ b/packages/vite/src/node/__tests__/config.spec.ts @@ -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( diff --git a/packages/vite/src/node/plugin.ts b/packages/vite/src/node/plugin.ts index e64071f47081d8..71ed105e361744 100644 --- a/packages/vite/src/node/plugin.ts +++ b/packages/vite/src/node/plugin.ts @@ -401,6 +401,13 @@ export type PluginOption = Thenable< | PluginOption[] > +const ignoredEnvironmentPluginHooks = [ + 'config', + 'configEnvironment', + 'configureServer', + 'configResolved', +] as const + export async function resolveEnvironmentPlugins( environment: PartialEnvironment, ): Promise { @@ -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 } } From b50e1b4a3d66128a4076e19769b2e29657985516 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Viet <123613986+NgoQuocViet2001@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:10:09 +0700 Subject: [PATCH 3/3] fix(utils): handle dot in srcset density descriptor (#23346) --- .../vite/src/node/__tests__/utils.spec.ts | 20 +++++++++++++++++++ packages/vite/src/node/utils.ts | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/vite/src/node/__tests__/utils.spec.ts b/packages/vite/src/node/__tests__/utils.spec.ts index 5d39ccbb75ee00..83d565011a5115 100644 --- a/packages/vite/src/node/__tests__/utils.spec.ts +++ b/packages/vite/src/node/__tests__/utils.spec.ts @@ -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' diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index b872ea8d26c41c..2207e00f13a426 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -814,7 +814,7 @@ function joinSrcset(ret: ImageCandidate[]) { The `descriptor` is anything after the space and before the comma. */ const imageCandidateRegex = - /(?:^|\s|(?<=,))(?[\w-]+\([^)]*\)|"[^"]*"|'[^']*'|[^,]\S*[^,])\s*(?:\s(?\w[^,]+))?(?:,|$)/g + /(?:^|\s|(?<=,))(?[\w-]+\([^)]*\)|"[^"]*"|'[^']*'|[^,]\S*[^,])\s*(?:\s(?[\w.][^,]+))?(?:,|$)/g const escapedSpaceCharacters = /(?: |\\t|\\n|\\f|\\r)+/g export function parseSrcset(string: string): ImageCandidate[] {