Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
Prev Previous commit
Next Next commit
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.
  • Loading branch information
RobinMalfait committed Nov 7, 2024
commit 722fe77c8ff84fd1884c5929d6ca9ce5eb7de65c
7 changes: 4 additions & 3 deletions packages/@tailwindcss-upgrade/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { migrate as migrateTemplate } from './template/migrate'
import { prepareConfig } from './template/prepare-config'
import { args, type Arg } from './utils/args'
import { isRepoDirty } from './utils/git'
import { hoistStaticGlobParts } from './utils/hoist-static-glob-parts'
import { pkg } from './utils/packages'
import { eprintln, error, header, highlight, info, success } from './utils/renderer'

Expand Down Expand Up @@ -143,11 +144,11 @@ async function run() {
info('Migrating templates using the provided configuration file.')
for (let config of configBySheet.values()) {
let set = new Set<string>()
for (let { pattern, base } of config.globs) {
let files = await globby([pattern], {
for (let globEntry of config.globs.flatMap((entry) => hoistStaticGlobParts(entry))) {
let files = await globby([globEntry.pattern], {
absolute: true,
gitignore: true,
cwd: base,
cwd: globEntry.base,
})

for (let file of files) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { expect, it } from 'vitest'
import { hoistStaticGlobParts } from './hoist-static-glob-parts'

it.each([
// A basic glob
[
{ base: '/projects/project-a', pattern: './src/**/*.html' },
[{ base: '/projects/project-a/src', pattern: '**/*.html' }],
],

// A glob pointing to a folder should result in `**/*`
[
{ base: '/projects/project-a', pattern: './src' },
[{ base: '/projects/project-a/src', pattern: '**/*' }],
],

// A glob pointing to a file, should result in the file as the pattern
[
{ base: '/projects/project-a', pattern: './src/index.html' },
[{ base: '/projects/project-a/src', pattern: 'index.html' }],
],

// A glob going up a directory, should result in the new directory as the base
[
{ base: '/projects/project-a', pattern: '../project-b/src/**/*.html' },
[{ base: '/projects/project-b/src', pattern: '**/*.html' }],
],

// A glob with curlies, should be expanded to multiple globs
[
{ base: '/projects/project-a', pattern: '../project-{b,c}/src/**/*.html' },
[
{ base: '/projects/project-b/src', pattern: '**/*.html' },
{ base: '/projects/project-c/src', pattern: '**/*.html' },
],
],
[
{ base: '/projects/project-a', pattern: '../project-{b,c}/src/**/*.{js,html}' },
[
{ base: '/projects/project-b/src', pattern: '**/*.js' },
{ base: '/projects/project-b/src', pattern: '**/*.html' },
{ base: '/projects/project-c/src', pattern: '**/*.js' },
{ base: '/projects/project-c/src', pattern: '**/*.html' },
],
],
])('should hoist the static parts of the glob: %s', (input, output) => {
expect(hoistStaticGlobParts(input)).toEqual(output)
})
78 changes: 78 additions & 0 deletions packages/@tailwindcss-upgrade/src/utils/hoist-static-glob-parts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import braces from 'braces'
import path from 'node:path'

interface GlobEntry {
base: string
pattern: string
}

export function hoistStaticGlobParts(entry: GlobEntry): GlobEntry[] {
return braces(entry.pattern, { expand: true }).map((pattern) => {
let clone = { ...entry }
let [staticPart, dynamicPart] = splitPattern(pattern)

// Move static part into the `base`.
if (staticPart !== null) {
clone.base = path.resolve(entry.base, staticPart)
} else {
clone.base = path.resolve(entry.base)
}

// Move dynamic part into the `pattern`.
if (dynamicPart === null) {
clone.pattern = '**/*'
} else {
clone.pattern = dynamicPart
}

// If the pattern looks like a file, move the file name from the `base` to
// the `pattern`.
let file = path.basename(clone.base)
if (file.includes('.')) {
clone.pattern = file
clone.base = path.dirname(clone.base)
}

return clone
})
}

// Split a glob pattern into a `static` and `dynamic` part.
//
// Assumption: we assume that all globs are expanded, which means that the only
// dynamic parts are using `*`.
//
// E.g.:
// Original input: `../project-b/**/*.{html,js}`
// Expanded input: `../project-b/**/*.html` & `../project-b/**/*.js`
// Split on first input: ("../project-b", "**/*.html")
// Split on second input: ("../project-b", "**/*.js")
function splitPattern(pattern: string): [staticPart: string | null, dynamicPart: string | null] {
// No dynamic parts, so we can just return the input as-is.
if (!pattern.includes('*')) {
return [pattern, null]
}

let lastSlashPosition: number | null = null

for (let [i, c] of pattern.split('').entries()) {
if (c === '/') {
lastSlashPosition = i
}

if (c === '*' || c === '!') {
break
}
}

// Very first character is a `*`, therefore there is no static part, only a
// dynamic part.
if (lastSlashPosition === null) {
return [null, pattern]
}

let staticPart = pattern.slice(0, lastSlashPosition).trim()
let dynamicPart = pattern.slice(lastSlashPosition + 1).trim()

return [staticPart || null, dynamicPart || null]
}