Skip to content

Commit

Permalink
fix: escape glob path (#9842)
Browse files Browse the repository at this point in the history
  • Loading branch information
dominikg committed Aug 29, 2022
1 parent e35a58b commit 6be971e
Show file tree
Hide file tree
Showing 10 changed files with 111 additions and 3 deletions.
36 changes: 33 additions & 3 deletions packages/vite/src/node/plugins/importMetaGlob.ts
Expand Up @@ -463,6 +463,34 @@ type IdResolver = (
importer?: string
) => Promise<string | undefined> | string | undefined

function globSafePath(path: string) {
// slash path to ensure \ is converted to / as \ could lead to a double escape scenario
// see https://github.com/mrmlnc/fast-glob#advanced-syntax
return fg.escapePath(normalizePath(path))
}

function lastNthChar(str: string, n: number) {
return str.charAt(str.length - 1 - n)
}

function globSafeResolvedPath(resolved: string, glob: string) {
// we have to escape special glob characters in the resolved path, but keep the user specified globby suffix
// walk back both strings until a character difference is found
// then slice up the resolved path at that pos and escape the first part
let numEqual = 0
const maxEqual = Math.min(resolved.length, glob.length)
while (
numEqual < maxEqual &&
lastNthChar(resolved, numEqual) === lastNthChar(glob, numEqual)
) {
numEqual += 1
}
const staticPartEnd = resolved.length - numEqual
const staticPart = resolved.slice(0, staticPartEnd)
const dynamicPart = resolved.slice(staticPartEnd)
return globSafePath(staticPart) + dynamicPart
}

export async function toAbsoluteGlob(
glob: string,
root: string,
Expand All @@ -474,15 +502,17 @@ export async function toAbsoluteGlob(
pre = '!'
glob = glob.slice(1)
}

const dir = importer ? dirname(importer) : root
root = globSafePath(root)
const dir = importer ? globSafePath(dirname(importer)) : root
if (glob.startsWith('/')) 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)
if (glob.startsWith('**')) return pre + glob

const resolved = normalizePath((await resolveId(glob, importer)) || glob)
if (isAbsolute(resolved)) return pre + resolved
if (isAbsolute(resolved)) {
return pre + globSafeResolvedPath(resolved, glob)
}

throw new Error(
`Invalid glob: "${glob}" (resolved: "${resolved}"). It must start with '/' or './'`
Expand Down
26 changes: 26 additions & 0 deletions playground/glob-import/__tests__/glob-import.spec.ts
@@ -1,3 +1,5 @@
import path from 'node:path'
import { readdir } from 'node:fs/promises'
import { expect, test } from 'vitest'
import {
addFile,
Expand Down Expand Up @@ -187,3 +189,27 @@ test('tree-shake eager css', async () => {
expect(content).not.toMatch('.tree-shake-eager-css')
}
})

test('escapes special chars in globs without mangling user supplied glob suffix', async () => {
// the escape dir contains subdirectories where each has a name that needs escaping for glob safety
// inside each of them is a glob.js that exports the result of a relative glob `./**/*.js`
// and an alias glob `@escape_<dirname>_mod/**/*.js`. The matching aliases are generated in vite.config.ts
// index.html has a script that loads all these glob.js files and prints the globs that returned the expected result
// this test finally compares the printed output of index.js with the list of directories with special chars,
// expecting that they all work
const files = await readdir(path.join(__dirname, '..', 'escape'), {
withFileTypes: true
})
const expectedNames = files
.filter((f) => f.isDirectory())
.map((f) => `/escape/${f.name}/glob.js`)
.sort()
const foundRelativeNames = (await page.textContent('.escape-relative'))
.split('\n')
.sort()
expect(expectedNames).toEqual(foundRelativeNames)
const foundAliasNames = (await page.textContent('.escape-alias'))
.split('\n')
.sort()
expect(expectedNames).toEqual(foundAliasNames)
})
5 changes: 5 additions & 0 deletions playground/glob-import/escape/(parenthesis)/glob.js
@@ -0,0 +1,5 @@
const relative = import.meta.glob('./**/*.js', { eager: true })
const alias = import.meta.glob('@escape_(parenthesis)_mod/**/*.js', {
eager: true
})
export { relative, alias }
1 change: 1 addition & 0 deletions playground/glob-import/escape/(parenthesis)/mod/index.js
@@ -0,0 +1 @@
export const msg = 'foo'
5 changes: 5 additions & 0 deletions playground/glob-import/escape/[brackets]/glob.js
@@ -0,0 +1,5 @@
const relative = import.meta.glob('./**/*.js', { eager: true })
const alias = import.meta.glob('@escape_[brackets]_mod/**/*.js', {
eager: true
})
export { relative, alias }
1 change: 1 addition & 0 deletions playground/glob-import/escape/[brackets]/mod/index.js
@@ -0,0 +1 @@
export const msg = 'foo'
3 changes: 3 additions & 0 deletions playground/glob-import/escape/{curlies}/glob.js
@@ -0,0 +1,3 @@
const relative = import.meta.glob('./**/*.js', { eager: true })
const alias = import.meta.glob('@escape_{curlies}_mod/**/*.js', { eager: true })
export { relative, alias }
1 change: 1 addition & 0 deletions playground/glob-import/escape/{curlies}/mod/index.js
@@ -0,0 +1 @@
export const msg = 'foo'
22 changes: 22 additions & 0 deletions playground/glob-import/index.html
Expand Up @@ -17,6 +17,10 @@ <h2>Tree shake Eager CSS</h2>
<p class="tree-shake-eager-css">Should be orange</p>
<p class="no-tree-shake-eager-css">Should be orange</p>
<pre class="no-tree-shake-eager-css-result"></pre>
<h2>Escape relative glob</h2>
<pre class="escape-relative"></pre>
<h2>Escape alias glob</h2>
<pre class="escape-alias"></pre>

<script type="module" src="./dir/index.js"></script>
<script type="module">
Expand Down Expand Up @@ -118,3 +122,21 @@ <h2>Tree shake Eager CSS</h2>
document.querySelector('.no-tree-shake-eager-css-result').textContent =
results['/no-tree-shake.css'].default
</script>

<script type="module">
const globs = import.meta.glob('/escape/**/glob.js', {
eager: true
})
console.log(globs)
globalThis.globs = globs
const relative = Object.entries(globs)
.filter(([_, mod]) => Object.keys(mod?.relative ?? {}).length === 1)
.map(([glob]) => glob)
document.querySelector('.escape-relative').textContent = relative
.sort()
.join('\n')
const alias = Object.entries(globs)
.filter(([_, mod]) => Object.keys(mod?.alias ?? {}).length === 1)
.map(([glob]) => glob)
document.querySelector('.escape-alias').textContent = alias.sort().join('\n')
</script>
14 changes: 14 additions & 0 deletions playground/glob-import/vite.config.ts
@@ -1,9 +1,23 @@
import fs from 'node:fs'
import path from 'node:path'
import { defineConfig } from 'vite'

const escapeAliases = fs
.readdirSync(path.join(__dirname, 'escape'), { withFileTypes: true })
.filter((f) => f.isDirectory())
.map((f) => f.name)
.reduce((aliases: Record<string, string>, dir) => {
aliases[`@escape_${dir}_mod`] = path.resolve(
__dirname,
`./escape/${dir}/mod`
)
return aliases
}, {})

export default defineConfig({
resolve: {
alias: {
...escapeAliases,
'@dir': path.resolve(__dirname, './dir/')
}
},
Expand Down

0 comments on commit 6be971e

Please sign in to comment.