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 ignore option for glob import #2495

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/guide/features.md
Expand Up @@ -248,6 +248,21 @@ const modules = {
}
```

You can also pass the second argument to ignore some files:

```js
const modules = import.meta.glob('./dir/*.js', '**/b*.*')
```

That will ignore all files whose name starts with `b`, the file `./dir/bar.js` will be ignored, the above will be transformed into the following:

```js
// code produced by vite
const modules = {
'./dir/foo.js': () => import('./dir/foo.js')
}
```

Note that:

- This is a Vite-only feature and is not a web or ES standard.
Expand Down
83 changes: 58 additions & 25 deletions packages/playground/glob-import/__tests__/glob-import.spec.ts
Expand Up @@ -3,7 +3,8 @@ import {
editFile,
isBuild,
removeFile,
untilUpdated
untilUpdated,
sortObjectDeep
} from '../../testUtils'

const filteredResult = {
Expand All @@ -14,21 +15,17 @@ const filteredResult = {

// json exports key order is altered during build, but it doesn't matter in
// terms of behavior since module exports are not ordered anyway
const json = isBuild
? {
msg: 'baz',
default: {
msg: 'baz'
}
}
: {
default: {
msg: 'baz'
},
msg: 'baz'
}
const json = {
msg: 'baz',
default: {
msg: 'baz'
}
}

const allResult = {
const allResult = sortObjectDeep({
'/dir/_ignored.js': {
msg: 'ignored'
},
// JSON file should be properly transformed
'/dir/baz.json': json,
'/dir/foo.js': {
Expand All @@ -43,7 +40,7 @@ const allResult = {
},
msg: 'bar'
}
}
})

test('should work', async () => {
expect(await page.textContent('.result')).toBe(
Expand All @@ -53,36 +50,72 @@ test('should work', async () => {

if (!isBuild) {
test('hmr for adding/removing files', async () => {
addFile('dir/a.js', '')
addFile('dir/+a.js', '')
await untilUpdated(
() => page.textContent('.result'),
JSON.stringify(
sortObjectDeep({
'/dir/+a.js': {},
...allResult
}),
null,
2
)
)

// edit the added file
editFile('dir/+a.js', () => 'export const msg ="a"')
await untilUpdated(
() => page.textContent('.result'),
JSON.stringify(
sortObjectDeep({
'/dir/+a.js': {
msg: 'a'
},
...allResult
}),
null,
2
)
)

removeFile('dir/+a.js')
await untilUpdated(
() => page.textContent('.result'),
JSON.stringify(allResult, null, 2)
)
})
test('hmr for adding/removing files with ignore option', async () => {
addFile('dir/_a.js', '')
await untilUpdated(
() => page.textContent('.result'),
JSON.stringify(
{
'/dir/a.js': {},
sortObjectDeep({
'/dir/_a.js': {},
...allResult
},
}),
null,
2
)
)

// edit the added file
editFile('dir/a.js', () => 'export const msg ="a"')
editFile('dir/_a.js', () => 'export const msg ="a"')
await untilUpdated(
() => page.textContent('.result'),
JSON.stringify(
{
'/dir/a.js': {
sortObjectDeep({
'/dir/_a.js': {
msg: 'a'
},
...allResult
},
}),
null,
2
)
)

removeFile('dir/a.js')
removeFile('dir/_a.js')
await untilUpdated(
() => page.textContent('.result'),
JSON.stringify(allResult, null, 2)
Expand Down
1 change: 1 addition & 0 deletions packages/playground/glob-import/dir/_ignored.js
@@ -0,0 +1 @@
export const msg = 'ignored'
2 changes: 1 addition & 1 deletion packages/playground/glob-import/dir/index.js
@@ -1,3 +1,3 @@
const modules = import.meta.globEager('./*.js')
const modules = import.meta.globEager('./*.js', './_*.js')

export { modules }
5 changes: 3 additions & 2 deletions packages/playground/glob-import/index.html
Expand Up @@ -2,7 +2,8 @@

<script type="module" src="./dir/index.js"></script>
<script type="module">
const modules = import.meta.glob('/dir/**')
import sortObjectDeep from './sortObjectDeep'
const modules = sortObjectDeep(import.meta.glob('/dir/**'))

for (const path in modules) {
modules[path]().then((mod) => {
Expand All @@ -14,7 +15,7 @@
Promise.all(keys.map((key) => modules[key]())).then((mods) => {
const res = {}
mods.forEach((m, i) => {
res[keys[i]] = m
res[keys[i]] = typeof m === 'object' ? sortObjectDeep(m) : m
})
document.querySelector('.result').textContent = JSON.stringify(res, null, 2)
})
Expand Down
15 changes: 15 additions & 0 deletions packages/playground/glob-import/sortObjectDeep.js
@@ -0,0 +1,15 @@
export default function sortObjectDeep(target) {
if (!target || typeof target !== 'object') return target
return Object.keys(target)
.sort()
.reduce(
(acc, item) => ({
...acc,
[item]:
typeof target[item] === 'object'
? sortObjectDeep(target[item])
: target[item]
}),
{}
)
}
46 changes: 46 additions & 0 deletions packages/playground/testUtils.ts
Expand Up @@ -114,3 +114,49 @@ export async function untilUpdated(
}
}
}

/**
* Sort an object by key, { c: 1, b: 3, a: 2, e: 4 } => { a: 2, b: 3, c: 1, e: 4 }
* @param target
*/
export function sortObjectDeep<T extends object = object>(target: T): T {
if (!target || typeof target !== 'object') return target
return Object.keys(target)
.sort()
.reduce<T>(
(acc, item) => ({
...acc,
[item]:
typeof target[item] === 'object'
? sortObjectDeep(target[item])
: target[item]
}),
{} as T
)
}

export function stringifyObjectWithSort(
value: any,
replacer?: (this: any, key: string, value: any) => any,
space?: string | number
): string
export function stringifyObjectWithSort(
value: any,
replacer?: (number | string)[] | null,
space?: string | number
): string
export function stringifyObjectWithSort(
value: any,
replacer?:
| ((this: any, key: string, value: any) => any)
| (number | string)[]
| null,
space?: string | number
): string {
const finalValue = typeof value === 'object' ? sortObjectDeep(value) : value
// Must check to fix overload of JSON.stringify
if (typeof replacer === 'function') {
return JSON.stringify(finalValue, replacer, space)
}
return JSON.stringify(finalValue, replacer, space)
}
6 changes: 4 additions & 2 deletions packages/vite/client.d.ts
Expand Up @@ -26,7 +26,8 @@ interface ImportMeta {
readonly env: ImportMetaEnv

glob(
pattern: string
pattern: string,
ignore?: string
): Record<
string,
() => Promise<{
Expand All @@ -35,7 +36,8 @@ interface ImportMeta {
>

globEager(
pattern: string
pattern: string,
ignore?: string
): Record<
string,
{
Expand Down
71 changes: 64 additions & 7 deletions packages/vite/src/node/importGlob.ts
Expand Up @@ -23,6 +23,7 @@ export async function transformImportGlob(
endIndex: number
isEager: boolean
pattern: string
ignore?: string
base: string
}> {
const isEager = source.slice(pos, pos + 21) === 'import.meta.globEager'
Expand All @@ -36,7 +37,7 @@ export async function transformImportGlob(
importer = cleanUrl(importer)
const importerBasename = path.basename(importer)

let [pattern, endIndex] = lexGlobPattern(source, pos)
let [pattern, ignore, endIndex] = lexGlobPattern(source, pos)
if (!pattern.startsWith('.') && !pattern.startsWith('/')) {
throw err(`pattern must start with "." or "/" (relative to project root)`)
}
Expand All @@ -59,7 +60,7 @@ export async function transformImportGlob(
}
const files = glob.sync(pattern, {
cwd: base,
ignore: ['**/node_modules/**']
ignore: ['**/node_modules/**', ...(ignore ? [ignore] : [])]
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we also overwrite the default ignore option if the user explicit set ignore option. In that case, we could also close #1903 by import.meta.glob('...', { ignore: [] }).

Copy link
Contributor

Choose a reason for hiding this comment

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

The other solution of #1903 would be something like import.meta.glob('...', { node_modules: true })

Copy link
Author

Choose a reason for hiding this comment

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

Ok, I will implement it later.

Copy link
Author

Choose a reason for hiding this comment

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

I think we should leave a ignoreNodeModules option (like your second solution) to support this, otherwise, the user will have to manually add the node_modules item every time the ignore option is used.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I'm also prefer option like import.meta.glob('...', { node_modules: true }). We don't need to solve #1903 in this PR. Maybe it's better to let the vite team consider weather should and how to support the feature in #1903.

})
const imports: string[] = []
let importsString = ``
Expand Down Expand Up @@ -102,6 +103,7 @@ export async function transformImportGlob(
endIndex,
isEager,
pattern,
ignore,
base
}
}
Expand All @@ -110,14 +112,30 @@ const enum LexerState {
inCall,
inSingleQuoteString,
inDoubleQuoteString,
inTemplateString
inTemplateString,
inComma
}

function lexGlobPattern(code: string, pos: number): [string, number] {
function lexGlobPattern(
code: string,
pos: number
): [string, string | undefined, number] {
const startPos = code.indexOf(`(`, pos) + 1
const [pattern, endIndexOfPattern] = lexString(code, startPos)
const posOfIgnorePattern = findNextArgumentPos(code, endIndexOfPattern)
const [ignore, endIndex] =
posOfIgnorePattern === -1
? [undefined, endIndexOfPattern]
: lexString(code, posOfIgnorePattern)

return [pattern, ignore, code.indexOf(`)`, endIndex) + 1]
}

function lexString(code: string, pos: number): [string, number] {
let state = LexerState.inCall
let pattern = ''

let i = code.indexOf(`(`, pos) + 1
let i = pos
outer: for (; i < code.length; i++) {
const char = code.charAt(i)
switch (state) {
Expand Down Expand Up @@ -159,12 +177,51 @@ function lexGlobPattern(code: string, pos: number): [string, number] {
throw new Error('unknown import.meta.glob lexer state')
}
}
return [pattern, code.indexOf(`)`, i) + 1]
return [pattern, i + 1]
}

// it will return -1 if not found next argument
function findNextArgumentPos(code: string, pos: number): number {
let state: LexerState.inComma | undefined = undefined
let i = pos
outer: for (; i < code.length; i++) {
const char = code.charAt(i)
switch (state) {
case undefined:
if (char === ',') {
state = LexerState.inComma
} else if (char === ')') {
i = -1
break outer
} else if (/\s/.test(char)) {
continue
} else {
error(i)
}
break
case LexerState.inComma:
if (char === `'`) {
break outer
} else if (char === `"`) {
break outer
} else if (char === '`') {
break outer
} else if (/\s/.test(char)) {
continue
} else {
error(i)
}
break
default:
throw new Error('unknown import.meta.glob lexer state')
}
}
return i
}

function error(pos: number) {
const err = new Error(
`import.meta.glob() can only accept string literals.`
`import.meta.glob() can only accept one or two string literals.`
) as RollupError
err.pos = pos
throw err
Expand Down