Skip to content

Commit 253d6c6

Browse files
patak-devhakshu25sapphi-redbenmccann
authored
feat: add base option to import.meta.glob (vitejs#20163)
Co-authored-by: hakshu25 <[email protected]> Co-authored-by: 翠 / green <[email protected]> Co-authored-by: Ben McCann <[email protected]>
1 parent fceff60 commit 253d6c6

File tree

9 files changed

+181
-7
lines changed

9 files changed

+181
-7
lines changed

docs/guide/features.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,32 @@ const modules = import.meta.glob('./dir/*.js', {
580580
})
581581
```
582582

583+
#### Base Path
584+
585+
You can also use the `base` option to provide base path for the imports:
586+
587+
```ts twoslash
588+
import 'vite/client'
589+
// ---cut---
590+
const modulesWithBase = import.meta.glob('./**/*.js', {
591+
base: './base',
592+
})
593+
```
594+
595+
```ts
596+
// code produced by vite:
597+
const modulesWithBase = {
598+
'./dir/foo.js': () => import('./base/dir/foo.js'),
599+
'./dir/bar.js': () => import('./base/dir/bar.js'),
600+
}
601+
```
602+
603+
The base option can only be a directory path relative to the importer file or absolute against the project root. Aliases and virtual modules aren't supported.
604+
605+
Only the globs that are relative paths are interpreted as relative to the resolved base.
606+
607+
All the resulting module keys are modified to be relative to the base if provided.
608+
583609
### Glob Import Caveats
584610

585611
Note that:

packages/vite/src/node/__tests__/plugins/importGlob/__snapshots__/fixture.spec.ts.snap

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@ export const cleverCwd2 = /* #__PURE__ */ Object.assign({"./modules/a.ts": () =>
9292
9393
9494
95+
});
96+
export const customBase = /* #__PURE__ */ Object.assign({"./modules/a.ts": () => import("./modules/a.ts"),"./modules/b.ts": () => import("./modules/b.ts"),"./modules/index.ts": () => import("./modules/index.ts"),"./sibling.ts": () => import("./sibling.ts")});
97+
export const customRootBase = /* #__PURE__ */ Object.assign({"./a.ts": () => import("/fixture-b/a.ts"),"./b.ts": () => import("/fixture-b/b.ts"),"./index.ts": () => import("/fixture-b/index.ts")
98+
99+
});
100+
export const customBaseParent = /* #__PURE__ */ Object.assign({"../fixture-b/a.ts": () => import("/fixture-b/a.ts"),"../fixture-b/b.ts": () => import("/fixture-b/b.ts"),"../fixture-b/index.ts": () => import("/fixture-b/index.ts")
101+
95102
});
96103
"
97104
`;
@@ -188,11 +195,19 @@ export const cleverCwd2 = /* #__PURE__ */ Object.assign({"./modules/a.ts": () =>
188195
189196
190197
198+
});
199+
export const customBase = /* #__PURE__ */ Object.assign({"./modules/a.ts": () => import("./modules/a.ts"),"./modules/b.ts": () => import("./modules/b.ts"),"./modules/index.ts": () => import("./modules/index.ts"),"./sibling.ts": () => import("./sibling.ts")});
200+
export const customRootBase = /* #__PURE__ */ Object.assign({"./a.ts": () => import("/fixture-b/a.ts"),"./b.ts": () => import("/fixture-b/b.ts"),"./index.ts": () => import("/fixture-b/index.ts")
201+
202+
});
203+
export const customBaseParent = /* #__PURE__ */ Object.assign({"../fixture-b/a.ts": () => import("/fixture-b/a.ts"),"../fixture-b/b.ts": () => import("/fixture-b/b.ts"),"../fixture-b/index.ts": () => import("/fixture-b/index.ts")
204+
191205
});
192206
"
193207
`;
194208

195209
exports[`fixture > virtual modules 1`] = `
196210
"/* #__PURE__ */ Object.assign({"/modules/a.ts": () => import("/modules/a.ts"),"/modules/b.ts": () => import("/modules/b.ts"),"/modules/index.ts": () => import("/modules/index.ts")})
197-
/* #__PURE__ */ Object.assign({"/../fixture-b/a.ts": () => import("/../fixture-b/a.ts"),"/../fixture-b/b.ts": () => import("/../fixture-b/b.ts"),"/../fixture-b/index.ts": () => import("/../fixture-b/index.ts")})"
211+
/* #__PURE__ */ Object.assign({"/../fixture-b/a.ts": () => import("/../fixture-b/a.ts"),"/../fixture-b/b.ts": () => import("/../fixture-b/b.ts"),"/../fixture-b/index.ts": () => import("/../fixture-b/index.ts")})
212+
/* #__PURE__ */ Object.assign({"./a.ts": () => import("/modules/a.ts"),"./b.ts": () => import("/modules/b.ts"),"./index.ts": () => import("/modules/index.ts")})"
198213
`;

packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,13 @@ export const cleverCwd2 = import.meta.glob([
110110
'../fixture-b/*.ts',
111111
'!**/index.ts',
112112
])
113+
114+
export const customBase = import.meta.glob('./**/*.ts', { base: './' })
115+
116+
export const customRootBase = import.meta.glob('./**/*.ts', {
117+
base: '/fixture-b',
118+
})
119+
120+
export const customBaseParent = import.meta.glob('/fixture-b/**/*.ts', {
121+
base: '/fixture-a',
122+
})

packages/vite/src/node/__tests__/plugins/importGlob/fixture.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ describe('fixture', async () => {
4545
const code = [
4646
"import.meta.glob('/modules/*.ts')",
4747
"import.meta.glob(['/../fixture-b/*.ts'])",
48+
"import.meta.glob(['./*.ts'], { base: '/modules' })",
4849
].join('\n')
4950
expect(
5051
(

packages/vite/src/node/__tests__/plugins/importGlob/parse.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,28 @@ describe('parse positives', async () => {
141141
`)
142142
})
143143

144+
it('options with base', async () => {
145+
expect(
146+
await run(`
147+
import.meta.glob('./**/dir/*.md', {
148+
base: './path/to/base'
149+
})
150+
`),
151+
).toMatchInlineSnapshot(`
152+
[
153+
{
154+
"globs": [
155+
"./**/dir/*.md",
156+
],
157+
"options": {
158+
"base": "./path/to/base",
159+
},
160+
"start": 5,
161+
},
162+
]
163+
`)
164+
})
165+
144166
it('object properties - 1', async () => {
145167
expect(
146168
await run(`
@@ -376,4 +398,20 @@ describe('parse negatives', async () => {
376398
'[Error: Vite is unable to parse the glob options as the value is not static]',
377399
)
378400
})
401+
402+
it('options base', async () => {
403+
expect(
404+
await runError('import.meta.glob("./*.js", { base: 1 })'),
405+
).toMatchInlineSnapshot(
406+
'[Error: Expected glob option "base" to be of type string, but got number]',
407+
)
408+
expect(
409+
await runError('import.meta.glob("./*.js", { base: "foo" })'),
410+
).toMatchInlineSnapshot(
411+
"[Error: Option \"base\" must start with '/', './' or '../', but got \"foo\"]",
412+
)
413+
expect(
414+
await runError('import.meta.glob("./*.js", { base: "!/foo" })'),
415+
).toMatchInlineSnapshot('[Error: Option "base" cannot start with "!"]')
416+
})
379417
})

packages/vite/src/node/plugins/importMetaGlob.ts

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ const knownOptions = {
120120
import: ['string'],
121121
exhaustive: ['boolean'],
122122
query: ['object', 'string'],
123+
base: ['string'],
123124
}
124125

125126
const forceDefaultAs = ['raw', 'url']
@@ -165,6 +166,21 @@ function parseGlobOptions(
165166
}
166167
}
167168

169+
if (opts.base) {
170+
if (opts.base[0] === '!') {
171+
throw err('Option "base" cannot start with "!"', optsStartIndex)
172+
} else if (
173+
opts.base[0] !== '/' &&
174+
!opts.base.startsWith('./') &&
175+
!opts.base.startsWith('../')
176+
) {
177+
throw err(
178+
`Option "base" must start with '/', './' or '../', but got "${opts.base}"`,
179+
optsStartIndex,
180+
)
181+
}
182+
}
183+
168184
if (typeof opts.query === 'object') {
169185
for (const key in opts.query) {
170186
const value = opts.query[key]
@@ -309,7 +325,9 @@ export async function parseImportGlob(
309325
}
310326

311327
const globsResolved = await Promise.all(
312-
globs.map((glob) => toAbsoluteGlob(glob, root, importer, resolveId)),
328+
globs.map((glob) =>
329+
toAbsoluteGlob(glob, root, importer, resolveId, options.base),
330+
),
313331
)
314332
const isRelative = globs.every((i) => '.!'.includes(i[0]))
315333
const sliceCode = cleanCode.slice(0, start)
@@ -432,19 +450,35 @@ export async function transformGlobImport(
432450

433451
const resolvePaths = (file: string) => {
434452
if (!dir) {
435-
if (isRelative)
453+
if (!options.base && isRelative)
436454
throw new Error(
437455
"In virtual modules, all globs must start with '/'",
438456
)
439-
const filePath = `/${relative(root, file)}`
440-
return { filePath, importPath: filePath }
457+
const importPath = `/${relative(root, file)}`
458+
let filePath = options.base
459+
? `${relative(posix.join(root, options.base), file)}`
460+
: importPath
461+
if (options.base && filePath[0] !== '.') {
462+
filePath = `./${filePath}`
463+
}
464+
return { filePath, importPath }
441465
}
442466

443467
let importPath = relative(dir, file)
444468
if (importPath[0] !== '.') importPath = `./${importPath}`
445469

446470
let filePath: string
447-
if (isRelative) {
471+
if (options.base) {
472+
const resolvedBasePath = options.base[0] === '/' ? root : dir
473+
filePath = relative(
474+
posix.join(resolvedBasePath, options.base),
475+
file,
476+
)
477+
if (filePath[0] !== '.') filePath = `./${filePath}`
478+
if (options.base[0] === '/') {
479+
importPath = `/${relative(root, file)}`
480+
}
481+
} else if (isRelative) {
448482
filePath = importPath
449483
} else {
450484
filePath = relative(root, file)
@@ -582,14 +616,28 @@ export async function toAbsoluteGlob(
582616
root: string,
583617
importer: string | undefined,
584618
resolveId: IdResolver,
619+
base?: string,
585620
): Promise<string> {
586621
let pre = ''
587622
if (glob[0] === '!') {
588623
pre = '!'
589624
glob = glob.slice(1)
590625
}
591626
root = globSafePath(root)
592-
const dir = importer ? globSafePath(dirname(importer)) : root
627+
let dir
628+
if (base) {
629+
if (base.startsWith('/')) {
630+
dir = posix.join(root, base)
631+
} else {
632+
dir = posix.resolve(
633+
importer ? globSafePath(dirname(importer)) : root,
634+
base,
635+
)
636+
}
637+
} else {
638+
dir = importer ? globSafePath(dirname(importer)) : root
639+
}
640+
593641
if (glob[0] === '/') return pre + posix.join(root, glob.slice(1))
594642
if (glob.startsWith('./')) return pre + posix.join(dir, glob.slice(2))
595643
if (glob.startsWith('../')) return pre + posix.join(dir, glob)

packages/vite/types/importGlob.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ export interface ImportGlobOptions<
2828
* @default false
2929
*/
3030
exhaustive?: boolean
31+
/**
32+
* Base path to resolve relative paths.
33+
*/
34+
base?: string
3135
}
3236

3337
export type GeneralImportGlobOptions = ImportGlobOptions<boolean, string>

playground/glob-import/__tests__/glob-import.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ const relativeRawResult = {
8383
},
8484
}
8585

86+
const baseRawResult = {
87+
'./baz.json': {
88+
msg: 'baz',
89+
},
90+
}
91+
8692
test('should work', async () => {
8793
await withRetry(async () => {
8894
const actual = await page.textContent('.result')
@@ -247,3 +253,9 @@ test('subpath imports', async () => {
247253
test('#alias imports', async () => {
248254
expect(await page.textContent('.hash-alias-imports')).toMatch('bar foo')
249255
})
256+
257+
test('import base glob raw', async () => {
258+
expect(await page.textContent('.result-base')).toBe(
259+
JSON.stringify(baseRawResult, null, 2),
260+
)
261+
})

playground/glob-import/index.html

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ <h2>#alias imports</h2>
2727
<pre class="hash-alias-imports"></pre>
2828
<h2>In package</h2>
2929
<pre class="in-package"></pre>
30+
<h2>Base</h2>
31+
<pre class="result-base"></pre>
3032

3133
<script type="module" src="./dir/index.js"></script>
3234
<script type="module">
@@ -167,3 +169,21 @@ <h2>In package</h2>
167169
<script type="module">
168170
import '@vitejs/test-import-meta-glob-pkg'
169171
</script>
172+
173+
<script type="module">
174+
const baseModules = import.meta.glob('./*.json', {
175+
query: '?raw',
176+
eager: true,
177+
import: 'default',
178+
base: './dir',
179+
})
180+
const globBase = {}
181+
Object.keys(baseModules).forEach((key) => {
182+
globBase[key] = JSON.parse(baseModules[key])
183+
})
184+
document.querySelector('.result-base').textContent = JSON.stringify(
185+
globBase,
186+
null,
187+
2,
188+
)
189+
</script>

0 commit comments

Comments
 (0)