Skip to content

Commit edabd34

Browse files
committed
add hoistStaticGlobParts
This is an implementation similar to what we already have in Rust. The idea is that we want to move the static parts to the `base` path. This allows us to use `..` in the pattern and the glob would be adjusted correctly. Without this, globby will error if your pattern escapes the base path by using `..` in the pattern.
1 parent 2c8e500 commit edabd34

File tree

3 files changed

+130
-3
lines changed

3 files changed

+130
-3
lines changed

packages/@tailwindcss-upgrade/src/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { migrate as migrateTemplate } from './template/migrate'
2020
import { prepareConfig } from './template/prepare-config'
2121
import { args, type Arg } from './utils/args'
2222
import { isRepoDirty } from './utils/git'
23+
import { hoistStaticGlobParts } from './utils/hoist-static-glob-parts'
2324
import { pkg } from './utils/packages'
2425
import { eprintln, error, header, highlight, info, success } from './utils/renderer'
2526

@@ -142,11 +143,11 @@ async function run() {
142143
info('Migrating templates using the provided configuration file.')
143144
for (let config of configBySheet.values()) {
144145
let set = new Set<string>()
145-
for (let { pattern, base } of config.globs) {
146-
let files = await globby([pattern], {
146+
for (let globEntry of config.globs.flatMap((entry) => hoistStaticGlobParts(entry))) {
147+
let files = await globby([globEntry.pattern], {
147148
absolute: true,
148149
gitignore: true,
149-
cwd: base,
150+
cwd: globEntry.base,
150151
})
151152

152153
for (let file of files) {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { expect, it } from 'vitest'
2+
import { hoistStaticGlobParts } from './hoist-static-glob-parts'
3+
4+
it.each([
5+
// A basic glob
6+
[
7+
{ base: '/projects/project-a', pattern: './src/**/*.html' },
8+
[{ base: '/projects/project-a/src', pattern: '**/*.html' }],
9+
],
10+
11+
// A glob pointing to a folder should result in `**/*`
12+
[
13+
{ base: '/projects/project-a', pattern: './src' },
14+
[{ base: '/projects/project-a/src', pattern: '**/*' }],
15+
],
16+
17+
// A glob pointing to a file, should result in the file as the pattern
18+
[
19+
{ base: '/projects/project-a', pattern: './src/index.html' },
20+
[{ base: '/projects/project-a/src', pattern: 'index.html' }],
21+
],
22+
23+
// A glob going up a directory, should result in the new directory as the base
24+
[
25+
{ base: '/projects/project-a', pattern: '../project-b/src/**/*.html' },
26+
[{ base: '/projects/project-b/src', pattern: '**/*.html' }],
27+
],
28+
29+
// A glob with curlies, should be expanded to multiple globs
30+
[
31+
{ base: '/projects/project-a', pattern: '../project-{b,c}/src/**/*.html' },
32+
[
33+
{ base: '/projects/project-b/src', pattern: '**/*.html' },
34+
{ base: '/projects/project-c/src', pattern: '**/*.html' },
35+
],
36+
],
37+
[
38+
{ base: '/projects/project-a', pattern: '../project-{b,c}/src/**/*.{js,html}' },
39+
[
40+
{ base: '/projects/project-b/src', pattern: '**/*.js' },
41+
{ base: '/projects/project-b/src', pattern: '**/*.html' },
42+
{ base: '/projects/project-c/src', pattern: '**/*.js' },
43+
{ base: '/projects/project-c/src', pattern: '**/*.html' },
44+
],
45+
],
46+
])('should hoist the static parts of the glob: %s', (input, output) => {
47+
expect(hoistStaticGlobParts(input)).toEqual(output)
48+
})
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import braces from 'braces'
2+
import path from 'node:path'
3+
4+
interface GlobEntry {
5+
base: string
6+
pattern: string
7+
}
8+
9+
export function hoistStaticGlobParts(entry: GlobEntry): GlobEntry[] {
10+
return braces(entry.pattern, { expand: true }).map((pattern) => {
11+
let clone = { ...entry }
12+
let [staticPart, dynamicPart] = splitPattern(pattern)
13+
14+
// Move static part into the `base`.
15+
if (staticPart !== null) {
16+
clone.base = path.resolve(entry.base, staticPart)
17+
} else {
18+
clone.base = path.resolve(entry.base)
19+
}
20+
21+
// Move dynamic part into the `pattern`.
22+
if (dynamicPart === null) {
23+
clone.pattern = '**/*'
24+
} else {
25+
clone.pattern = dynamicPart
26+
}
27+
28+
// If the pattern looks like a file, move the file name from the `base` to
29+
// the `pattern`.
30+
let file = path.basename(clone.base)
31+
if (file.includes('.')) {
32+
clone.pattern = file
33+
clone.base = path.dirname(clone.base)
34+
}
35+
36+
return clone
37+
})
38+
}
39+
40+
// Split a glob pattern into a `static` and `dynamic` part.
41+
//
42+
// Assumption: we assume that all globs are expanded, which means that the only
43+
// dynamic parts are using `*`.
44+
//
45+
// E.g.:
46+
// Original input: `../project-b/**/*.{html,js}`
47+
// Expanded input: `../project-b/**/*.html` & `../project-b/**/*.js`
48+
// Split on first input: ("../project-b", "**/*.html")
49+
// Split on second input: ("../project-b", "**/*.js")
50+
function splitPattern(pattern: string): [staticPart: string | null, dynamicPart: string | null] {
51+
// No dynamic parts, so we can just return the input as-is.
52+
if (!pattern.includes('*')) {
53+
return [pattern, null]
54+
}
55+
56+
let lastSlashPosition: number | null = null
57+
58+
for (let [i, c] of pattern.split('').entries()) {
59+
if (c === '/') {
60+
lastSlashPosition = i
61+
}
62+
63+
if (c === '*' || c === '!') {
64+
break
65+
}
66+
}
67+
68+
// Very first character is a `*`, therefore there is no static part, only a
69+
// dynamic part.
70+
if (lastSlashPosition === null) {
71+
return [null, pattern]
72+
}
73+
74+
let staticPart = pattern.slice(0, lastSlashPosition).trim()
75+
let dynamicPart = pattern.slice(lastSlashPosition + 1).trim()
76+
77+
return [staticPart || null, dynamicPart || null]
78+
}

0 commit comments

Comments
 (0)