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
26 changes: 26 additions & 0 deletions packages/vite/src/node/__tests__/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(`
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) } }
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