diff --git a/packages/internal-postcss-fix-relative-paths/src/fixtures/example-project/src/index.css b/packages/internal-postcss-fix-relative-paths/src/fixtures/example-project/src/index.css index 112e82e59673..f71b89bbb05b 100644 --- a/packages/internal-postcss-fix-relative-paths/src/fixtures/example-project/src/index.css +++ b/packages/internal-postcss-fix-relative-paths/src/fixtures/example-project/src/index.css @@ -1,3 +1,4 @@ @content "./**/*.ts"; +@content "!./**/*.ts"; @plugin "./plugin.js"; @plugin "./what\"s-this.js"; diff --git a/packages/internal-postcss-fix-relative-paths/src/index.test.ts b/packages/internal-postcss-fix-relative-paths/src/index.test.ts index c66699c6582c..5c8f7a58ba99 100644 --- a/packages/internal-postcss-fix-relative-paths/src/index.test.ts +++ b/packages/internal-postcss-fix-relative-paths/src/index.test.ts @@ -1,4 +1,5 @@ import fs from 'node:fs' +import path from 'node:path' import postcss from 'postcss' import atImport from 'postcss-import' import { describe, expect, test } from 'vitest' @@ -6,7 +7,7 @@ import fixRelativePathsPlugin from '.' describe('fixRelativePathsPlugin', () => { test('rewrites @content and @plugin to be relative to the initial css file', async () => { - let cssPath = `${__dirname}/fixtures/external-import/src/index.css` + let cssPath = path.join(__dirname, 'fixtures', 'external-import', 'src', 'index.css') let css = fs.readFileSync(cssPath, 'utf-8') let processor = postcss([atImport(), fixRelativePathsPlugin()]) @@ -15,13 +16,14 @@ describe('fixRelativePathsPlugin', () => { expect(result.css.trim()).toMatchInlineSnapshot(` "@content "../../example-project/src/**/*.ts"; + @content "!../../example-project/src/**/*.ts"; @plugin "../../example-project/src/plugin.js"; @plugin "../../example-project/src/what\\"s-this.js";" `) }) test('should not rewrite non-relative paths', async () => { - let cssPath = `${__dirname}/fixtures/external-import/src/invalid.css` + let cssPath = path.join(__dirname, 'fixtures', 'external-import', 'src', 'invalid.css') let css = fs.readFileSync(cssPath, 'utf-8') let processor = postcss([atImport(), fixRelativePathsPlugin()]) @@ -37,7 +39,7 @@ describe('fixRelativePathsPlugin', () => { }) test('should return relative paths even if the file is resolved in the same basedir as the root stylesheet', async () => { - let cssPath = `${__dirname}/fixtures/external-import/src/plugins-in-root.css` + let cssPath = path.join(__dirname, 'fixtures', 'external-import', 'src', 'plugins-in-root.css') let css = fs.readFileSync(cssPath, 'utf-8') let processor = postcss([atImport(), fixRelativePathsPlugin()]) diff --git a/packages/internal-postcss-fix-relative-paths/src/index.ts b/packages/internal-postcss-fix-relative-paths/src/index.ts index 0193284fe197..5eb20b05ce27 100644 --- a/packages/internal-postcss-fix-relative-paths/src/index.ts +++ b/packages/internal-postcss-fix-relative-paths/src/index.ts @@ -1,5 +1,6 @@ import path from 'node:path' -import type { AtRule, Container, Plugin } from 'postcss' +import type { AtRule, Plugin } from 'postcss' +import { normalizePath } from './normalize-path' const SINGLE_QUOTE = "'" const DOUBLE_QUOTE = '"' @@ -9,12 +10,12 @@ export default function fixRelativePathsPlugin(): Plugin { let touched: WeakSet = new WeakSet() function fixRelativePath(atRule: AtRule) { - let rootPath = getRoot(atRule)?.source?.input.file + let rootPath = atRule.root().source?.input.file if (!rootPath) { return } - let inputFilePath = atRule?.source?.input.file + let inputFilePath = atRule.source?.input.file if (!inputFilePath) { return } @@ -34,15 +35,24 @@ export default function fixRelativePathsPlugin(): Plugin { if (!quote) { return } - let content = atRule.params.slice(1, -1) + let glob = atRule.params.slice(1, -1) + + // Handle eventual negative rules. We only support one level of negation. + let negativePrefix = '' + if (glob.startsWith('!')) { + glob = glob.slice(1) + negativePrefix = '!' + } // We only want to rewrite relative paths. - if (!content.startsWith('./') && !content.startsWith('../')) { + if (!glob.startsWith('./') && !glob.startsWith('../')) { return } - let rulePath = path.posix.join(path.posix.dirname(inputFilePath), content) - let relative = path.posix.relative(path.posix.dirname(rootPath), rulePath) + let absoluteGlob = path.posix.join(normalizePath(path.dirname(inputFilePath)), glob) + let absoluteRootPosixPath = path.posix.dirname(normalizePath(rootPath)) + + let relative = path.posix.relative(absoluteRootPosixPath, absoluteGlob) // If the path points to a file in the same directory, `path.relative` will // remove the leading `./` and we need to add it back in order to still @@ -51,17 +61,10 @@ export default function fixRelativePathsPlugin(): Plugin { relative = './' + relative } - atRule.params = quote + relative + quote + atRule.params = quote + negativePrefix + relative + quote touched.add(atRule) } - function getRoot(node: AtRule | Container | undefined): Container | undefined { - if (node?.parent) { - return getRoot(node.parent as Container) - } - return node - } - return { postcssPlugin: 'tailwindcss-postcss-fix-relative-paths', AtRule: { diff --git a/packages/internal-postcss-fix-relative-paths/src/normalize-path.ts b/packages/internal-postcss-fix-relative-paths/src/normalize-path.ts new file mode 100644 index 000000000000..a8184ef2317c --- /dev/null +++ b/packages/internal-postcss-fix-relative-paths/src/normalize-path.ts @@ -0,0 +1,47 @@ +// Inlined version of `normalize-path` +// Copyright (c) 2014-2018, Jon Schlinkert. +// Released under the MIT License. +function normalizePathBase(path: string, stripTrailing?: boolean) { + if (typeof path !== 'string') { + throw new TypeError('expected path to be a string') + } + + if (path === '\\' || path === '/') return '/' + + var len = path.length + if (len <= 1) return path + + // ensure that win32 namespaces has two leading slashes, so that the path is + // handled properly by the win32 version of path.parse() after being normalized + // https://msdn.microsoft.com/library/windows/desktop/aa365247(v=vs.85).aspx#namespaces + var prefix = '' + if (len > 4 && path[3] === '\\') { + var ch = path[2] + if ((ch === '?' || ch === '.') && path.slice(0, 2) === '\\\\') { + path = path.slice(2) + prefix = '//' + } + } + + var segs = path.split(/[/\\]+/) + if (stripTrailing !== false && segs[segs.length - 1] === '') { + segs.pop() + } + return prefix + segs.join('/') +} + +export function normalizePath(originalPath: string) { + let normalized = normalizePathBase(originalPath) + + // Make sure Windows network share paths are normalized properly + // They have to begin with two slashes or they won't resolve correctly + if ( + originalPath.startsWith('\\\\') && + normalized.startsWith('/') && + !normalized.startsWith('//') + ) { + return `/${normalized}` + } + + return normalized +}