Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Windows support for escaped glob syntax #12718

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- Ensure max specificity of `0,0,1` for button and input Preflight rules ([#12735](https://github.com/tailwindlabs/tailwindcss/pull/12735))
- Improve glob handling for folders with `(`, `)`, `[` or `]` in the file path ([#12715](https://github.com/tailwindlabs/tailwindcss/pull/12715))
- Improve glob handling for folders with `(`, `)`, `[` or `]` in the file path ([#12715](https://github.com/tailwindlabs/tailwindcss/pull/12715), [#12718](https://github.com/tailwindlabs/tailwindcss/pull/12718))
- Split `:has` rules when using `experimental.optimizeUniversalDefaults` ([#12736](https://github.com/tailwindlabs/tailwindcss/pull/12736))

### Added
Expand Down
4 changes: 3 additions & 1 deletion integrations/content-resolution/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
"version": "0.0.0",
"scripts": {
"build": "NODE_ENV=production postcss ./src/index.css -o ./dist/main.css --verbose",
"test": "jest --runInBand --forceExit"
"test": "jest --runInBand --forceExit",
"prewip": "npm run --prefix=../../ build",
"wip": "npx postcss ./src/index.css -o ./dist/main.css"
},
"jest": {
"testTimeout": 10000,
Expand Down
1 change: 1 addition & 0 deletions integrations/content-resolution/src/escapes/(test)/2.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="mr-2"></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="mr-3"></div>
1 change: 1 addition & 0 deletions integrations/content-resolution/src/escapes/1.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="mr-1"></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="mr-5"></div>
1 change: 1 addition & 0 deletions integrations/content-resolution/src/escapes/[test]/4.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="mr-4"></div>
37 changes: 37 additions & 0 deletions integrations/content-resolution/tests/content.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -285,3 +285,40 @@ it('it can resolve symlinks for files when relative to the config', async () =>
}
`)
})

it('handles some special glob characters in paths', async () => {
await writeConfigs({
both: {
content: {
relative: true,
files: [
'./src/escapes/1.html',
'./src/escapes/(test)/2.html',
'./src/escapes/(test)/[test]/3.html',
'./src/escapes/[test]/4.html',
'./src/escapes/[test]/(test)/5.html',
],
},
},
})

let result = await build({ cwd: path.resolve(__dirname, '..') })

expect(result.css).toMatchFormattedCss(css`
.mr-1 {
margin-right: 0.25rem;
}
.mr-2 {
margin-right: 0.5rem;
}
.mr-3 {
margin-right: 0.75rem;
}
.mr-4 {
margin-right: 1rem;
}
.mr-5 {
margin-right: 1.25rem;
}
`)
})
40 changes: 24 additions & 16 deletions integrations/execute.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,29 @@ function debounce(fn, ms) {
}
}

function resolveCommandPath(root, command) {
let paths = [
path.resolve(root, 'node_modules', '.bin', command),
path.resolve(root, '..', '..', 'node_modules', '.bin', command),
]

if (path.sep === '\\') {
paths = [
path.resolve(root, 'node_modules', '.bin', `${command}.cmd`),
path.resolve(root, '..', '..', 'node_modules', '.bin', `${command}.cmd`),
...paths,
]
}

for (let filepath of paths) {
if (fs.existsSync(filepath)) {
return filepath
}
}

return `npx ${command}`
}

module.exports = function $(command, options = {}) {
let abortController = new AbortController()
let root = resolveToolRoot()
Expand All @@ -30,22 +53,7 @@ module.exports = function $(command, options = {}) {
: (() => {
let args = command.trim().split(/\s+/)
command = args.shift()
command =
command === 'node'
? command
: (function () {
let local = path.resolve(root, 'node_modules', '.bin', command)
if (fs.existsSync(local)) {
return local
}

let hoisted = path.resolve(root, '..', '..', 'node_modules', '.bin', command)
if (fs.existsSync(hoisted)) {
return hoisted
}

return `npx ${command}`
})()
command = command === 'node' ? command : resolveCommandPath(root, command)
return [command, args]
})()

Expand Down
5 changes: 2 additions & 3 deletions integrations/resolve-tool-root.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ let path = require('path')

module.exports = function resolveToolRoot() {
let { testPath } = expect.getState()
let separator = '/' // TODO: Does this resolve correctly on windows, or should we use `path.sep` instead.

return path.resolve(
__dirname,
testPath
.replace(__dirname + separator, '')
.split(separator)
.replace(__dirname + path.sep, '')
.split(path.sep)
.shift()
)
}
68 changes: 51 additions & 17 deletions src/lib/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,18 @@ function normalizePath(path) {
}
}

// Modified part: instead of purely splitting on `\\` and `/`, we split on
// Modified part:

// Assumption: `\\\\[` or `\\\\(` means that the first `\\` is the path separator, and the second
// `\\` is the escape for the special `[]` and `()` characters and therefore we want to rewrite
// it as `/` and then the escape `\\` which will result in `/\\`.
path = path.replace(/\\\\([\[\]\(\)])/g, '/\\$1')

// Instead of purely splitting on `\\` and `/`, we split on
// `/` and `\\` that is _not_ followed by any of the following characters: ()[]
// This is to ensure that we keep the escaping of brackets and parentheses
let segs = path.split(/[/\\]+(?![\(\)\[\]])/)

return prefix + segs.join('/')
}

Expand Down Expand Up @@ -103,9 +111,10 @@ export function parseCandidateFiles(context, tailwindConfig) {
skip: [context.userConfigPath],
})

console.log({ files })

// Normalize the file globs
files = files.filter((filePath) => typeof filePath === 'string')
files = files.map(normalizePath)

// Split into included and excluded globs
let tasks = fastGlob.generateTasks(files)
Expand All @@ -123,15 +132,23 @@ export function parseCandidateFiles(context, tailwindConfig) {

let paths = [...included, ...excluded]

console.log('parseCandidateFiles 0', { paths })

// Resolve paths relative to the config file or cwd
paths = resolveRelativePaths(context, paths)

console.log('parseCandidateFiles 1', { paths })

// Resolve symlinks if possible
paths = paths.flatMap(resolvePathSymlinks)

console.log('parseCandidateFiles 2', { paths })

// Update cached patterns
paths = paths.map(resolveGlobPattern)

console.log('parseCandidateFiles 3', { paths })

return paths
}

Expand All @@ -143,8 +160,21 @@ export function parseCandidateFiles(context, tailwindConfig) {
*/
function parseFilePath(filePath, ignore) {
// Escape special characters in the file path such as: ()[]
// But only if the special character isn't already escaped
filePath = filePath.replace(/(?<!\\)([\[\]\(\)])/g, '\\$1')
// But only if the special character isn't already escaped (and balanced)
filePath = filePath
.replace(/(\\)?\[(.*?)\]/g, (match, prefix, contents) => {
return match.startsWith('\\[') && match.endsWith('\\]')
? match
: `${prefix || ''}\\[${contents}\\]`
})
.replace(/(\\)?\((.*?)\)/g, (match, prefix, contents) => {
return match.startsWith('\\(') && match.endsWith('\\)')
? match
: `${prefix || ''}\\(${contents}\\)`
})

// Normalize the file path for Windows
filePath = normalizePath(filePath)

let contentPath = {
original: filePath,
Expand All @@ -167,17 +197,10 @@ function parseFilePath(filePath, ignore) {
* @returns {ContentPath}
*/
function resolveGlobPattern(contentPath) {
// This is required for Windows support to properly pick up Glob paths.
// Afaik, this technically shouldn't be needed but there's probably
// some internal, direct path matching with a normalized path in
// a package which can't handle mixed directory separators
let base = normalizePath(contentPath.base)

// If the user's file path contains any special characters (like parens) for instance fast-glob
// is like "OOOH SHINY" and treats them as such. So we have to escape the base path to fix this
base = fastGlob.escapePath(base)
contentPath.pattern = contentPath.glob
? `${contentPath.base}/${contentPath.glob}`
: contentPath.base

contentPath.pattern = contentPath.glob ? `${base}/${contentPath.glob}` : base
contentPath.pattern = contentPath.ignore ? `!${contentPath.pattern}` : contentPath.pattern

return contentPath
Expand All @@ -196,11 +219,19 @@ function resolveRelativePaths(context, contentPaths) {

// Resolve base paths relative to the config file (when possible) if the experimental flag is enabled
if (context.userConfigPath && context.tailwindConfig.content.relative) {
resolveFrom = [path.dirname(context.userConfigPath)]
resolveFrom = [path.posix.dirname(context.userConfigPath)]
}

return contentPaths.map((contentPath) => {
contentPath.base = path.resolve(...resolveFrom, contentPath.base)
contentPath.base = path.posix.resolve(...resolveFrom, contentPath.base)

if (
path.sep === '\\' &&
contentPath.base.startsWith('/') &&
!contentPath.base.startsWith('//?/')
) {
contentPath.base = `C:${contentPath.base}`
}

return contentPath
})
Expand All @@ -219,7 +250,7 @@ function resolvePathSymlinks(contentPath) {
let paths = [contentPath]

try {
let resolvedPath = fs.realpathSync(contentPath.base)
let resolvedPath = normalizePath(fs.realpathSync(contentPath.base))
if (resolvedPath !== contentPath.base) {
paths.push({
...contentPath,
Expand Down Expand Up @@ -267,6 +298,9 @@ function resolveChangedFiles(candidateFiles, fileModifiedMap) {
let changedFiles = new Set()
env.DEBUG && console.time('Finding changed files')
let files = fastGlob.sync(paths, { absolute: true })

console.log({ paths, files })

for (let file of files) {
let prevModified = fileModifiedMap.get(file) || -Infinity
let modified = fs.statSync(file).mtimeMs
Expand Down