Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fd262c0
feat: parse base path in import.meta.glob
hakshu25 Oct 29, 2024
09a3e9c
feat: add support for custom base path in import.meta.glob
hakshu25 Oct 29, 2024
6b17fd7
test: add integration test for base option of glob-import
hakshu25 Oct 29, 2024
ae89d14
feat: add validation to check if the base option doesn't start with '!'
hakshu25 Oct 30, 2024
5db0640
fix: resolved relative paths to options.base
hakshu25 Oct 31, 2024
f6d73df
docs: add usage of base path for glob import
hakshu25 Oct 31, 2024
f206609
docs: remove one example
sapphi-red Nov 1, 2024
7f0f5e7
fix: ensure glob import keys start with './' when using base option
hakshu25 Nov 2, 2024
1cd16f3
docs: fix glob import paths to use base directory
hakshu25 Nov 2, 2024
51e76be
fix: ensure glob import keys start with './' for base directory in gl…
hakshu25 Nov 2, 2024
b07d814
feat: support alias in glob with options.base
hakshu25 Nov 5, 2024
2ec1175
feat: support relative glob in virtual modules
hakshu25 Nov 5, 2024
76dc01e
test: fix test to treat as base instead of prefix
hakshu25 Nov 6, 2024
500cb14
docs: fix glob import example to treat as base instead of prefix
hakshu25 Nov 6, 2024
f6c657d
fix: changed to update dir to glob to act as base instead of prefix
hakshu25 Nov 7, 2024
1a26072
test: fix e2e test glob in base option of import glob
hakshu25 Nov 8, 2024
e69c80b
chore: merge main
patak-cat Jun 5, 2025
e384e64
chore: remove base+alias related code and clarify docs
patak-cat Jun 5, 2025
08a1cbc
chore: update
patak-cat Jun 5, 2025
50b5723
chore: add test case
patak-cat Jun 6, 2025
8c3f886
chore: update snapshot
patak-cat Jun 6, 2025
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
26 changes: 26 additions & 0 deletions docs/guide/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,32 @@ const modules = import.meta.glob('./dir/*.js', {
})
```

#### Base Path

You can also use the `base` option to provide base path for the imports:

```ts twoslash
import 'vite/client'
// ---cut---
const modulesWithBase = import.meta.glob('./**/*.js', {
base: './base',
})
```

```ts
// code produced by vite:
const modulesWithBase = {
'./dir/foo.js': () => import('./base/dir/foo.js'),
'./dir/bar.js': () => import('./base/dir/bar.js'),
}
```

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.

Only the globs that are relative paths are interpreted as relative to the resolved base.

All the resulting module keys are modified to be relative to the base if provided.

### Glob Import Caveats

Note that:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ export const cleverCwd2 = /* #__PURE__ */ Object.assign({"./modules/a.ts": () =>



});
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")});
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")

});
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")

});
"
`;
Expand Down Expand Up @@ -188,11 +195,19 @@ export const cleverCwd2 = /* #__PURE__ */ Object.assign({"./modules/a.ts": () =>



});
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")});
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")

});
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")

});
"
`;

exports[`fixture > virtual modules 1`] = `
"/* #__PURE__ */ Object.assign({"/modules/a.ts": () => import("/modules/a.ts"),"/modules/b.ts": () => import("/modules/b.ts"),"/modules/index.ts": () => import("/modules/index.ts")})
/* #__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")})"
/* #__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")})
/* #__PURE__ */ Object.assign({"./a.ts": () => import("/modules/a.ts"),"./b.ts": () => import("/modules/b.ts"),"./index.ts": () => import("/modules/index.ts")})"
`;
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,13 @@ export const cleverCwd2 = import.meta.glob([
'../fixture-b/*.ts',
'!**/index.ts',
])

export const customBase = import.meta.glob('./**/*.ts', { base: './' })

export const customRootBase = import.meta.glob('./**/*.ts', {
base: '/fixture-b',
})
Comment thread
patak-cat marked this conversation as resolved.

export const customBaseParent = import.meta.glob('/fixture-b/**/*.ts', {
base: '/fixture-a',
})
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ describe('fixture', async () => {
const code = [
"import.meta.glob('/modules/*.ts')",
"import.meta.glob(['/../fixture-b/*.ts'])",
"import.meta.glob(['./*.ts'], { base: '/modules' })",
].join('\n')
expect(
(
Expand Down
38 changes: 38 additions & 0 deletions packages/vite/src/node/__tests__/plugins/importGlob/parse.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,28 @@ describe('parse positives', async () => {
`)
})

it('options with base', async () => {
expect(
await run(`
import.meta.glob('./**/dir/*.md', {
base: './path/to/base'
})
`),
).toMatchInlineSnapshot(`
[
{
"globs": [
"./**/dir/*.md",
],
"options": {
"base": "./path/to/base",
},
"start": 5,
},
]
`)
})

it('object properties - 1', async () => {
expect(
await run(`
Expand Down Expand Up @@ -376,4 +398,20 @@ describe('parse negatives', async () => {
'[Error: Vite is unable to parse the glob options as the value is not static]',
)
})

it('options base', async () => {
expect(
await runError('import.meta.glob("./*.js", { base: 1 })'),
).toMatchInlineSnapshot(
'[Error: Expected glob option "base" to be of type string, but got number]',
)
expect(
await runError('import.meta.glob("./*.js", { base: "foo" })'),
).toMatchInlineSnapshot(
"[Error: Option \"base\" must start with '/', './' or '../', but got \"foo\"]",
)
expect(
await runError('import.meta.glob("./*.js", { base: "!/foo" })'),
).toMatchInlineSnapshot('[Error: Option "base" cannot start with "!"]')
})
})
60 changes: 54 additions & 6 deletions packages/vite/src/node/plugins/importMetaGlob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ const knownOptions = {
import: ['string'],
exhaustive: ['boolean'],
query: ['object', 'string'],
base: ['string'],
}

const forceDefaultAs = ['raw', 'url']
Expand Down Expand Up @@ -165,6 +166,21 @@ function parseGlobOptions(
}
}

if (opts.base) {
if (opts.base[0] === '!') {
throw err('Option "base" cannot start with "!"', optsStartIndex)
} else if (
opts.base[0] !== '/' &&
!opts.base.startsWith('./') &&
!opts.base.startsWith('../')
) {
throw err(
`Option "base" must start with '/', './' or '../', but got "${opts.base}"`,
optsStartIndex,
)
}
}

if (typeof opts.query === 'object') {
for (const key in opts.query) {
const value = opts.query[key]
Expand Down Expand Up @@ -309,7 +325,9 @@ export async function parseImportGlob(
}

const globsResolved = await Promise.all(
globs.map((glob) => toAbsoluteGlob(glob, root, importer, resolveId)),
globs.map((glob) =>
toAbsoluteGlob(glob, root, importer, resolveId, options.base),
),
)
const isRelative = globs.every((i) => '.!'.includes(i[0]))
const sliceCode = cleanCode.slice(0, start)
Expand Down Expand Up @@ -432,19 +450,35 @@ export async function transformGlobImport(

const resolvePaths = (file: string) => {
if (!dir) {
if (isRelative)
if (!options.base && isRelative)
throw new Error(
"In virtual modules, all globs must start with '/'",
)
const filePath = `/${relative(root, file)}`
return { filePath, importPath: filePath }
const importPath = `/${relative(root, file)}`
let filePath = options.base
? `${relative(posix.join(root, options.base), file)}`
: importPath
if (options.base && filePath[0] !== '.') {
filePath = `./${filePath}`
}
return { filePath, importPath }
}

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

let filePath: string
if (isRelative) {
if (options.base) {
const resolvedBasePath = options.base[0] === '/' ? root : dir
filePath = relative(
posix.join(resolvedBasePath, options.base),
file,
)
if (filePath[0] !== '.') filePath = `./${filePath}`
if (options.base[0] === '/') {
importPath = `/${relative(root, file)}`
}
} else if (isRelative) {
filePath = importPath
} else {
filePath = relative(root, file)
Expand Down Expand Up @@ -582,14 +616,28 @@ export async function toAbsoluteGlob(
root: string,
importer: string | undefined,
resolveId: IdResolver,
base?: string,
): Promise<string> {
let pre = ''
if (glob[0] === '!') {
pre = '!'
glob = glob.slice(1)
}
root = globSafePath(root)
const dir = importer ? globSafePath(dirname(importer)) : root
let dir
if (base) {
if (base.startsWith('/')) {
dir = posix.join(root, base)
} else {
dir = posix.resolve(
importer ? globSafePath(dirname(importer)) : root,
base,
)
}
} else {
dir = importer ? globSafePath(dirname(importer)) : root
}

if (glob[0] === '/') return pre + posix.join(root, glob.slice(1))
if (glob.startsWith('./')) return pre + posix.join(dir, glob.slice(2))
if (glob.startsWith('../')) return pre + posix.join(dir, glob)
Expand Down
4 changes: 4 additions & 0 deletions packages/vite/types/importGlob.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export interface ImportGlobOptions<
* @default false
*/
exhaustive?: boolean
/**
* Base path to resolve relative paths.
*/
base?: string
}

export type GeneralImportGlobOptions = ImportGlobOptions<boolean, string>
Expand Down
12 changes: 12 additions & 0 deletions playground/glob-import/__tests__/glob-import.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ const relativeRawResult = {
},
}

const baseRawResult = {
'./baz.json': {
msg: 'baz',
},
}

test('should work', async () => {
await withRetry(async () => {
const actual = await page.textContent('.result')
Expand Down Expand Up @@ -247,3 +253,9 @@ test('subpath imports', async () => {
test('#alias imports', async () => {
expect(await page.textContent('.hash-alias-imports')).toMatch('bar foo')
})

test('import base glob raw', async () => {
expect(await page.textContent('.result-base')).toBe(
JSON.stringify(baseRawResult, null, 2),
)
})
20 changes: 20 additions & 0 deletions playground/glob-import/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ <h2>#alias imports</h2>
<pre class="hash-alias-imports"></pre>
<h2>In package</h2>
<pre class="in-package"></pre>
<h2>Base</h2>
<pre class="result-base"></pre>

<script type="module" src="./dir/index.js"></script>
<script type="module">
Expand Down Expand Up @@ -167,3 +169,21 @@ <h2>In package</h2>
<script type="module">
import '@vitejs/test-import-meta-glob-pkg'
</script>

<script type="module">
const baseModules = import.meta.glob('./*.json', {
query: '?raw',
eager: true,
import: 'default',
base: './dir',
})
const globBase = {}
Object.keys(baseModules).forEach((key) => {
globBase[key] = JSON.parse(baseModules[key])
})
document.querySelector('.result-base').textContent = JSON.stringify(
globBase,
null,
2,
)
</script>