Add PostCSS plugin to fix relative @content and @plugin paths in @imported files (#14063)
We noticed an issue that happened when handling relative file imports in the `@plugin` and the upcoming `@content` APIs. The problem arises from relative files that are inside `@import`ed stylesheets. Take, for example, the following folder structure: ```css /* src/index.css */ @import "./dir/index.css"; ``` ```css /* src/dir/index.css */ @plugin "../../plugin.ts"; ``` It's expected that the path is relative to the CSS file that defined it. However, right now, we use [`postcss-import`](https://github.com/postcss/postcss-import) to flatten the CSS file before running the tailwind build step. This causes these custom-properties to be inlined in a flat file which removes the information of which file is being referred: ```css /* src/flat.css */ @plugin "../../plugin.ts"; /* <- This is now pointing to the wrong file */ ``` There are generally two approaches that we can do to solve this: 1. **Handle `@import` flattening inside tailwindcss:** While generally this would give us more freedom and less dependencies, this would require some work to get all edge cases right. We need to support layers/conditional imports and also handle all relative urls for properties like `background-image`. 2. **Rewrite relative paths as a separate postcss visitor:** The approach this PR takes is instead to implement a custom postcss plugin that uses the AST to rewrite relative references inside `@plugin` and `@content`. This has the benefit of requiring little changes to our existing APIs. The rule is only enabled for relative references inside `@plugin` and `@content`, so the surface of this rule is very small. We can use this plugin inside all three current clients: - `@tailwindcss/postcss` obviously already uses postcss - `@tailwindcss/cli` also uses postcss to handle `@import` flattening - `@tailwindcss/vite` allows us to add custom postcss rules via the CSS pipeline. There are a few cases that we handle with care (e.g. in vite you can pass a string to the postcss config which is supposed to load the config from a file). To validate the changes, we have added both a list of unit test cases to the plugin itself as well as verified that all three clients are working as expected: - `@tailwindcss/postcss` now has an explicit test for this behavior - `@tailwindcss/cli` and `@tailwindcss/vite` were manually tested by updating the vite playground. The CLI was run with `--cwd playgrounds/vite/ -i ./src/app.css -o foo.css`: <img width="531" alt="Screenshot 2024-07-29 at 11 35 59" src="https://github.com/user-attachments/assets/78f0acdc-a46c-4c6c-917a-2916417b1001">
This commit is contained in:
parent
744e43fc0e
commit
b07832772a
22 changed files with 450 additions and 99 deletions
|
|
@ -12,7 +12,7 @@
|
|||
"homepage": "https://tailwindcss.com",
|
||||
"scripts": {
|
||||
"lint": "tsc --noEmit",
|
||||
"build": "tsup-node ./src/index.ts --format esm --minify --clean",
|
||||
"build": "tsup-node",
|
||||
"dev": "pnpm run build -- --watch"
|
||||
},
|
||||
"bin": {
|
||||
|
|
@ -36,7 +36,8 @@
|
|||
"picocolors": "^1.0.1",
|
||||
"postcss": "8.4.24",
|
||||
"postcss-import": "^16.1.0",
|
||||
"tailwindcss": "workspace:^"
|
||||
"tailwindcss": "workspace:^",
|
||||
"internal-postcss-fix-relative-paths": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/postcss-import": "^14.0.3"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import watcher from '@parcel/watcher'
|
||||
import { IO, Parsing, scanDir, scanFiles, type ChangedContent } from '@tailwindcss/oxide'
|
||||
import fixRelativePathsPlugin from 'internal-postcss-fix-relative-paths'
|
||||
import { Features, transform } from 'lightningcss'
|
||||
import { existsSync } from 'node:fs'
|
||||
import fs from 'node:fs/promises'
|
||||
|
|
@ -259,6 +260,7 @@ function handleImports(
|
|||
|
||||
return postcss()
|
||||
.use(atImport())
|
||||
.use(fixRelativePathsPlugin())
|
||||
.process(input, { from: file })
|
||||
.then((result) => [
|
||||
result.css,
|
||||
|
|
|
|||
9
packages/@tailwindcss-cli/tsup.config.ts
Normal file
9
packages/@tailwindcss-cli/tsup.config.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { defineConfig } from 'tsup'
|
||||
|
||||
export default defineConfig({
|
||||
format: ['esm'],
|
||||
clean: true,
|
||||
minify: true,
|
||||
entry: ['src/index.ts'],
|
||||
noExternal: ['internal-postcss-fix-relative-paths'],
|
||||
})
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
"homepage": "https://tailwindcss.com",
|
||||
"scripts": {
|
||||
"lint": "tsc --noEmit",
|
||||
"build": "tsup-node ./src/index.ts --format cjs,esm --dts --cjsInterop --splitting --minify --clean",
|
||||
"build": "tsup-node",
|
||||
"dev": "pnpm run build -- --watch"
|
||||
},
|
||||
"files": [
|
||||
|
|
@ -33,7 +33,8 @@
|
|||
"@tailwindcss/oxide": "workspace:^",
|
||||
"lightningcss": "^1.25.1",
|
||||
"postcss-import": "^16.1.0",
|
||||
"tailwindcss": "workspace:^"
|
||||
"tailwindcss": "workspace:^",
|
||||
"internal-postcss-fix-relative-paths": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.12.12",
|
||||
|
|
|
|||
1
packages/@tailwindcss-postcss/src/fixtures/example-project/src/relative-import.css
vendored
Normal file
1
packages/@tailwindcss-postcss/src/fixtures/example-project/src/relative-import.css
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
@plugin '../plugin.js';
|
||||
|
|
@ -144,7 +144,7 @@ describe('plugins', () => {
|
|||
let result = await processor.process(
|
||||
css`
|
||||
@import 'tailwindcss/utilities';
|
||||
@plugin 'internal-example-plugin';
|
||||
@plugin './plugin.js';
|
||||
`,
|
||||
{ from: INPUT_CSS_PATH },
|
||||
)
|
||||
|
|
@ -166,6 +166,36 @@ describe('plugins', () => {
|
|||
`)
|
||||
})
|
||||
|
||||
test('local CJS plugin from `@import`-ed file', async () => {
|
||||
let processor = postcss([
|
||||
tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }),
|
||||
])
|
||||
|
||||
let result = await processor.process(
|
||||
css`
|
||||
@import 'tailwindcss/utilities';
|
||||
@import '../example-project/src/relative-import.css';
|
||||
`,
|
||||
{ from: `${__dirname}/fixtures/another-project/input.css` },
|
||||
)
|
||||
|
||||
expect(result.css.trim()).toMatchInlineSnapshot(`
|
||||
".underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
|
||||
@media (inverted-colors: inverted) {
|
||||
.inverted\\:flex {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.hocus\\:underline:focus, .hocus\\:underline:hover {
|
||||
text-decoration-line: underline;
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('published CJS plugin', async () => {
|
||||
let processor = postcss([
|
||||
tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }),
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@ import { scanDir } from '@tailwindcss/oxide'
|
|||
import fs from 'fs'
|
||||
import { Features, transform } from 'lightningcss'
|
||||
import path from 'path'
|
||||
import postcss, { type AcceptedPlugin, type PluginCreator } from 'postcss'
|
||||
import postcss, { AtRule, type AcceptedPlugin, type PluginCreator } from 'postcss'
|
||||
import postcssImport from 'postcss-import'
|
||||
import { compile } from 'tailwindcss'
|
||||
import fixRelativePathsPlugin from '../../internal-postcss-fix-relative-paths/src'
|
||||
|
||||
/**
|
||||
* A Map that can generate default values for keys that don't exist.
|
||||
|
|
@ -48,114 +49,118 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
|
|||
}
|
||||
})
|
||||
|
||||
let hasApply: boolean, hasTailwind: boolean
|
||||
|
||||
return {
|
||||
postcssPlugin: '@tailwindcss/postcss',
|
||||
plugins: [
|
||||
// We need to run `postcss-import` first to handle `@import` rules.
|
||||
postcssImport(),
|
||||
fixRelativePathsPlugin(),
|
||||
|
||||
(root, result) => {
|
||||
let inputFile = result.opts.from ?? ''
|
||||
let context = cache.get(inputFile)
|
||||
|
||||
let rebuildStrategy: 'full' | 'incremental' = 'incremental'
|
||||
|
||||
// Track file modification times to CSS files
|
||||
{
|
||||
let files = result.messages.flatMap((message) => {
|
||||
if (message.type !== 'dependency') return []
|
||||
return message.file
|
||||
})
|
||||
files.push(inputFile)
|
||||
for (let file of files) {
|
||||
let changedTime = fs.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? null
|
||||
if (changedTime === null) {
|
||||
if (file === inputFile) {
|
||||
rebuildStrategy = 'full'
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
let prevTime = context.mtimes.get(file)
|
||||
if (prevTime === changedTime) continue
|
||||
|
||||
rebuildStrategy = 'full'
|
||||
context.mtimes.set(file, changedTime)
|
||||
}
|
||||
}
|
||||
|
||||
let hasApply = false
|
||||
let hasTailwind = false
|
||||
|
||||
root.walkAtRules((rule) => {
|
||||
{
|
||||
postcssPlugin: 'tailwindcss',
|
||||
Once() {
|
||||
// Reset some state between builds
|
||||
hasApply = false
|
||||
hasTailwind = false
|
||||
},
|
||||
AtRule(rule: AtRule) {
|
||||
if (rule.name === 'apply') {
|
||||
hasApply = true
|
||||
} else if (rule.name === 'tailwind') {
|
||||
hasApply = true
|
||||
hasTailwind = true
|
||||
// If we've found `@tailwind` then we already
|
||||
// know we have to run a "full" build
|
||||
return false
|
||||
}
|
||||
})
|
||||
},
|
||||
OnceExit(root, { result }) {
|
||||
let inputFile = result.opts.from ?? ''
|
||||
let context = cache.get(inputFile)
|
||||
|
||||
// Do nothing if neither `@tailwind` nor `@apply` is used
|
||||
if (!hasTailwind && !hasApply) return
|
||||
let rebuildStrategy: 'full' | 'incremental' = 'incremental'
|
||||
|
||||
let css = ''
|
||||
|
||||
// Look for candidates used to generate the CSS
|
||||
let { candidates, files, globs } = scanDir({ base, globs: true })
|
||||
|
||||
// Add all found files as direct dependencies
|
||||
for (let file of files) {
|
||||
result.messages.push({
|
||||
type: 'dependency',
|
||||
plugin: '@tailwindcss/postcss',
|
||||
file,
|
||||
parent: result.opts.from,
|
||||
})
|
||||
}
|
||||
|
||||
// Register dependencies so changes in `base` cause a rebuild while
|
||||
// giving tools like Vite or Parcel a glob that can be used to limit
|
||||
// the files that cause a rebuild to only those that match it.
|
||||
for (let { base, glob } of globs) {
|
||||
result.messages.push({
|
||||
type: 'dir-dependency',
|
||||
plugin: '@tailwindcss/postcss',
|
||||
dir: base,
|
||||
glob,
|
||||
parent: result.opts.from,
|
||||
})
|
||||
}
|
||||
|
||||
if (rebuildStrategy === 'full') {
|
||||
let basePath = path.dirname(path.resolve(inputFile))
|
||||
let { build } = compile(root.toString(), {
|
||||
loadPlugin: (pluginPath) => {
|
||||
if (pluginPath[0] === '.') {
|
||||
return require(path.resolve(basePath, pluginPath))
|
||||
// Track file modification times to CSS files
|
||||
{
|
||||
let files = result.messages.flatMap((message) => {
|
||||
if (message.type !== 'dependency') return []
|
||||
return message.file
|
||||
})
|
||||
files.push(inputFile)
|
||||
for (let file of files) {
|
||||
let changedTime = fs.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? null
|
||||
if (changedTime === null) {
|
||||
if (file === inputFile) {
|
||||
rebuildStrategy = 'full'
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
return require(pluginPath)
|
||||
},
|
||||
})
|
||||
context.build = build
|
||||
css = build(hasTailwind ? candidates : [])
|
||||
} else if (rebuildStrategy === 'incremental') {
|
||||
css = context.build!(candidates)
|
||||
}
|
||||
let prevTime = context.mtimes.get(file)
|
||||
if (prevTime === changedTime) continue
|
||||
|
||||
// Replace CSS
|
||||
if (css !== context.css && optimize) {
|
||||
context.optimizedCss = optimizeCss(css, {
|
||||
minify: typeof optimize === 'object' ? optimize.minify : true,
|
||||
})
|
||||
}
|
||||
context.css = css
|
||||
root.removeAll()
|
||||
root.append(postcss.parse(optimize ? context.optimizedCss : context.css, result.opts))
|
||||
rebuildStrategy = 'full'
|
||||
context.mtimes.set(file, changedTime)
|
||||
}
|
||||
}
|
||||
|
||||
// Do nothing if neither `@tailwind` nor `@apply` is used
|
||||
if (!hasTailwind && !hasApply) return
|
||||
|
||||
let css = ''
|
||||
|
||||
// Look for candidates used to generate the CSS
|
||||
let { candidates, files, globs } = scanDir({ base, globs: true })
|
||||
|
||||
// Add all found files as direct dependencies
|
||||
for (let file of files) {
|
||||
result.messages.push({
|
||||
type: 'dependency',
|
||||
plugin: '@tailwindcss/postcss',
|
||||
file,
|
||||
parent: result.opts.from,
|
||||
})
|
||||
}
|
||||
|
||||
// Register dependencies so changes in `base` cause a rebuild while
|
||||
// giving tools like Vite or Parcel a glob that can be used to limit
|
||||
// the files that cause a rebuild to only those that match it.
|
||||
for (let { base, glob } of globs) {
|
||||
result.messages.push({
|
||||
type: 'dir-dependency',
|
||||
plugin: '@tailwindcss/postcss',
|
||||
dir: base,
|
||||
glob,
|
||||
parent: result.opts.from,
|
||||
})
|
||||
}
|
||||
|
||||
if (rebuildStrategy === 'full') {
|
||||
let basePath = path.dirname(path.resolve(inputFile))
|
||||
let { build } = compile(root.toString(), {
|
||||
loadPlugin: (pluginPath) => {
|
||||
if (pluginPath[0] === '.') {
|
||||
return require(path.resolve(basePath, pluginPath))
|
||||
}
|
||||
|
||||
return require(pluginPath)
|
||||
},
|
||||
})
|
||||
context.build = build
|
||||
css = build(hasTailwind ? candidates : [])
|
||||
} else if (rebuildStrategy === 'incremental') {
|
||||
css = context.build!(candidates)
|
||||
}
|
||||
|
||||
// Replace CSS
|
||||
if (css !== context.css && optimize) {
|
||||
context.optimizedCss = optimizeCss(css, {
|
||||
minify: typeof optimize === 'object' ? optimize.minify : true,
|
||||
})
|
||||
}
|
||||
context.css = css
|
||||
root.removeAll()
|
||||
root.append(postcss.parse(optimize ? context.optimizedCss : context.css, result.opts))
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
12
packages/@tailwindcss-postcss/tsup.config.ts
Normal file
12
packages/@tailwindcss-postcss/tsup.config.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { defineConfig } from 'tsup'
|
||||
|
||||
export default defineConfig({
|
||||
format: ['esm', 'cjs'],
|
||||
clean: true,
|
||||
minify: true,
|
||||
splitting: true,
|
||||
cjsInterop: true,
|
||||
dts: true,
|
||||
entry: ['src/index.ts'],
|
||||
noExternal: ['internal-postcss-fix-relative-paths'],
|
||||
})
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
"bugs": "https://github.com/tailwindlabs/tailwindcss/issues",
|
||||
"homepage": "https://tailwindcss.com",
|
||||
"scripts": {
|
||||
"build": "tsup-node ./src/index.ts --format esm --dts --minify --clean",
|
||||
"build": "tsup-node",
|
||||
"dev": "pnpm run build -- --watch"
|
||||
},
|
||||
"files": [
|
||||
|
|
@ -30,10 +30,12 @@
|
|||
"dependencies": {
|
||||
"@tailwindcss/oxide": "workspace:^",
|
||||
"lightningcss": "^1.25.1",
|
||||
"postcss-load-config": "^6.0.1",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.12.12",
|
||||
"internal-postcss-fix-relative-paths": "workspace:^",
|
||||
"vite": "^5.2.11"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { IO, Parsing, scanFiles } from '@tailwindcss/oxide'
|
||||
import fixRelativePathsPlugin from 'internal-postcss-fix-relative-paths'
|
||||
import { Features, transform } from 'lightningcss'
|
||||
import path from 'path'
|
||||
import postcssrc from 'postcss-load-config'
|
||||
import { compile } from 'tailwindcss'
|
||||
import type { Plugin, Rollup, Update, ViteDevServer } from 'vite'
|
||||
|
||||
|
|
@ -101,7 +103,7 @@ export default function tailwindcss(): Plugin[] {
|
|||
|
||||
for (let plugin of cssPlugins) {
|
||||
if (!plugin.transform) continue
|
||||
const transformHandler =
|
||||
let transformHandler =
|
||||
'handler' in plugin.transform! ? plugin.transform.handler : plugin.transform!
|
||||
|
||||
try {
|
||||
|
|
@ -152,6 +154,55 @@ export default function tailwindcss(): Plugin[] {
|
|||
})
|
||||
},
|
||||
|
||||
// Append the postcss-fix-relative-paths plugin
|
||||
async config(config) {
|
||||
let postcssConfig = config.css?.postcss
|
||||
|
||||
if (typeof postcssConfig === 'string') {
|
||||
// We expand string configs to their PostCSS config object similar to
|
||||
// how Vite does it.
|
||||
// See: https://github.com/vitejs/vite/blob/440783953a55c6c63cd09ec8d13728dc4693073d/packages/vite/src/node/plugins/css.ts#L1580
|
||||
let searchPath = typeof postcssConfig === 'string' ? postcssConfig : config.root
|
||||
let parsedConfig = await postcssrc({}, searchPath).catch((e: Error) => {
|
||||
if (!e.message.includes('No PostCSS Config found')) {
|
||||
if (e instanceof Error) {
|
||||
let { name, message, stack } = e
|
||||
e.name = 'Failed to load PostCSS config'
|
||||
e.message = `Failed to load PostCSS config (searchPath: ${searchPath}): [${name}] ${message}\n${stack}`
|
||||
e.stack = '' // add stack to message to retain stack
|
||||
throw e
|
||||
} else {
|
||||
throw new Error(`Failed to load PostCSS config: ${e}`)
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
if (parsedConfig !== null) {
|
||||
postcssConfig = {
|
||||
options: parsedConfig.options,
|
||||
plugins: parsedConfig.plugins,
|
||||
} as any
|
||||
} else {
|
||||
postcssConfig = {}
|
||||
}
|
||||
config.css = { postcss: postcssConfig }
|
||||
}
|
||||
|
||||
// postcssConfig is no longer a string after the above. This test is to
|
||||
// avoid TypeScript errors below.
|
||||
if (typeof postcssConfig === 'string') {
|
||||
return
|
||||
}
|
||||
|
||||
if (!postcssConfig || !postcssConfig?.plugins) {
|
||||
config.css = config.css || {}
|
||||
config.css.postcss = postcssConfig || {}
|
||||
config.css.postcss.plugins = [fixRelativePathsPlugin() as any]
|
||||
} else {
|
||||
postcssConfig.plugins.push(fixRelativePathsPlugin() as any)
|
||||
}
|
||||
},
|
||||
|
||||
// Scan index.html for candidates
|
||||
transformIndexHtml(html) {
|
||||
let updated = scan(html, 'html')
|
||||
|
|
|
|||
10
packages/@tailwindcss-vite/tsup.config.ts
Normal file
10
packages/@tailwindcss-vite/tsup.config.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { defineConfig } from 'tsup'
|
||||
|
||||
export default defineConfig({
|
||||
format: ['esm'],
|
||||
clean: true,
|
||||
minify: true,
|
||||
dts: true,
|
||||
entry: ['src/index.ts'],
|
||||
noExternal: ['internal-postcss-fix-relative-paths'],
|
||||
})
|
||||
27
packages/internal-postcss-fix-relative-paths/package.json
Normal file
27
packages/internal-postcss-fix-relative-paths/package.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "internal-postcss-fix-relative-paths",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "tsc --noEmit",
|
||||
"build": "tsup-node ./src/index.ts --format cjs,esm --dts --cjsInterop --splitting --minify --clean",
|
||||
"dev": "pnpm run build -- --watch"
|
||||
},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.12.12",
|
||||
"@types/postcss-import": "^14.0.3",
|
||||
"postcss": "8.4.24",
|
||||
"postcss-import": "^16.1.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
@content "./**/*.ts";
|
||||
@plugin "./plugin.js";
|
||||
@plugin "./what\"s-this.js";
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
@plugin "/absolute/paths";
|
||||
@plugin "C:\Program Files\HAL 9000";
|
||||
@plugin "\\Media\Pictures\Worth\1000 words";
|
||||
@plugin "some-node-dep";
|
||||
|
|
@ -0,0 +1 @@
|
|||
@import '../../example-project/src/index.css';
|
||||
|
|
@ -0,0 +1 @@
|
|||
@import '../../example-project/src/invalid.css';
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
@import './plugins-in-sibling.css';
|
||||
|
||||
@plugin './plugin-in-root.ts';
|
||||
@plugin '../plugin-in-root.ts';
|
||||
@plugin 'plugin-in-root';
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
@plugin './plugin-in-sibling.ts';
|
||||
@plugin '../plugin-in-sibling.ts';
|
||||
@plugin 'plugin-in-sibling';
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import fs from 'node:fs'
|
||||
import postcss from 'postcss'
|
||||
import atImport from 'postcss-import'
|
||||
import { describe, expect, test } from 'vitest'
|
||||
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 css = fs.readFileSync(cssPath, 'utf-8')
|
||||
|
||||
let processor = postcss([atImport(), fixRelativePathsPlugin()])
|
||||
|
||||
let result = await processor.process(css, { from: cssPath })
|
||||
|
||||
expect(result.css.trim()).toMatchInlineSnapshot(`
|
||||
"@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 css = fs.readFileSync(cssPath, 'utf-8')
|
||||
|
||||
let processor = postcss([atImport(), fixRelativePathsPlugin()])
|
||||
|
||||
let result = await processor.process(css, { from: cssPath })
|
||||
|
||||
expect(result.css.trim()).toMatchInlineSnapshot(`
|
||||
"@plugin "/absolute/paths";
|
||||
@plugin "C:\\Program Files\\HAL 9000";
|
||||
@plugin "\\\\Media\\Pictures\\Worth\\1000 words";
|
||||
@plugin "some-node-dep";"
|
||||
`)
|
||||
})
|
||||
|
||||
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 css = fs.readFileSync(cssPath, 'utf-8')
|
||||
|
||||
let processor = postcss([atImport(), fixRelativePathsPlugin()])
|
||||
|
||||
let result = await processor.process(css, { from: cssPath })
|
||||
|
||||
expect(result.css.trim()).toMatchInlineSnapshot(`
|
||||
"@plugin './plugin-in-sibling.ts';
|
||||
@plugin '../plugin-in-sibling.ts';
|
||||
@plugin 'plugin-in-sibling';
|
||||
@plugin './plugin-in-root.ts';
|
||||
@plugin '../plugin-in-root.ts';
|
||||
@plugin 'plugin-in-root';"
|
||||
`)
|
||||
})
|
||||
})
|
||||
72
packages/internal-postcss-fix-relative-paths/src/index.ts
Normal file
72
packages/internal-postcss-fix-relative-paths/src/index.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import path from 'node:path'
|
||||
import type { AtRule, Container, Plugin } from 'postcss'
|
||||
|
||||
const SINGLE_QUOTE = "'"
|
||||
const DOUBLE_QUOTE = '"'
|
||||
|
||||
export default function fixRelativePathsPlugin(): Plugin {
|
||||
// Retain a list of touched at-rules to avoid infinite loops
|
||||
let touched: WeakSet<AtRule> = new WeakSet()
|
||||
|
||||
function fixRelativePath(atRule: AtRule) {
|
||||
let rootPath = getRoot(atRule)?.source?.input.file
|
||||
if (!rootPath) {
|
||||
return
|
||||
}
|
||||
|
||||
let inputFilePath = atRule?.source?.input.file
|
||||
if (!inputFilePath) {
|
||||
return
|
||||
}
|
||||
|
||||
if (touched.has(atRule)) {
|
||||
return
|
||||
}
|
||||
|
||||
let value = atRule.params[0]
|
||||
|
||||
let quote =
|
||||
value[0] === DOUBLE_QUOTE && value[value.length - 1] === DOUBLE_QUOTE
|
||||
? DOUBLE_QUOTE
|
||||
: value[0] === SINGLE_QUOTE && value[value.length - 1] === SINGLE_QUOTE
|
||||
? SINGLE_QUOTE
|
||||
: null
|
||||
if (!quote) {
|
||||
return
|
||||
}
|
||||
let content = atRule.params.slice(1, -1)
|
||||
|
||||
// We only want to rewrite relative paths.
|
||||
if (!content.startsWith('./') && !content.startsWith('../')) {
|
||||
return
|
||||
}
|
||||
|
||||
let rulePath = path.posix.join(path.posix.dirname(inputFilePath), content)
|
||||
let relative = path.posix.relative(path.posix.dirname(rootPath), rulePath)
|
||||
|
||||
// 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
|
||||
// consider the path relative
|
||||
if (!relative.startsWith('.')) {
|
||||
relative = './' + relative
|
||||
}
|
||||
|
||||
atRule.params = quote + 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: {
|
||||
content: fixRelativePath,
|
||||
plugin: fixRelativePath,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
}
|
||||
52
pnpm-lock.yaml
generated
52
pnpm-lock.yaml
generated
|
|
@ -109,6 +109,9 @@ importers:
|
|||
'@tailwindcss/oxide':
|
||||
specifier: workspace:^
|
||||
version: link:../../crates/node
|
||||
internal-postcss-fix-relative-paths:
|
||||
specifier: workspace:^
|
||||
version: link:../internal-postcss-fix-relative-paths
|
||||
lightningcss:
|
||||
specifier: ^1.25.1
|
||||
version: 1.25.1
|
||||
|
|
@ -137,6 +140,9 @@ importers:
|
|||
'@tailwindcss/oxide':
|
||||
specifier: workspace:^
|
||||
version: link:../../crates/node
|
||||
internal-postcss-fix-relative-paths:
|
||||
specifier: workspace:^
|
||||
version: link:../internal-postcss-fix-relative-paths
|
||||
lightningcss:
|
||||
specifier: ^1.25.1
|
||||
version: 1.25.1
|
||||
|
|
@ -168,6 +174,9 @@ importers:
|
|||
lightningcss:
|
||||
specifier: ^1.25.1
|
||||
version: 1.25.1
|
||||
postcss-load-config:
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1(postcss@8.4.38)(yaml@2.4.2)
|
||||
tailwindcss:
|
||||
specifier: workspace:^
|
||||
version: link:../tailwindcss
|
||||
|
|
@ -175,12 +184,30 @@ importers:
|
|||
'@types/node':
|
||||
specifier: ^20.12.12
|
||||
version: 20.12.12
|
||||
internal-postcss-fix-relative-paths:
|
||||
specifier: workspace:^
|
||||
version: link:../internal-postcss-fix-relative-paths
|
||||
vite:
|
||||
specifier: ^5.2.11
|
||||
version: 5.2.11(@types/node@20.12.12)(lightningcss@1.25.1)
|
||||
|
||||
packages/internal-example-plugin: {}
|
||||
|
||||
packages/internal-postcss-fix-relative-paths:
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^20.12.12
|
||||
version: 20.12.12
|
||||
'@types/postcss-import':
|
||||
specifier: ^14.0.3
|
||||
version: 14.0.3
|
||||
postcss:
|
||||
specifier: 8.4.24
|
||||
version: 8.4.24
|
||||
postcss-import:
|
||||
specifier: ^16.1.0
|
||||
version: 16.1.0(postcss@8.4.24)
|
||||
|
||||
packages/tailwindcss:
|
||||
devDependencies:
|
||||
'@tailwindcss/oxide':
|
||||
|
|
@ -2338,6 +2365,24 @@ packages:
|
|||
ts-node:
|
||||
optional: true
|
||||
|
||||
postcss-load-config@6.0.1:
|
||||
resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==}
|
||||
engines: {node: '>= 18'}
|
||||
peerDependencies:
|
||||
jiti: '>=1.21.0'
|
||||
postcss: '>=8.0.9'
|
||||
tsx: ^4.8.1
|
||||
yaml: ^2.4.2
|
||||
peerDependenciesMeta:
|
||||
jiti:
|
||||
optional: true
|
||||
postcss:
|
||||
optional: true
|
||||
tsx:
|
||||
optional: true
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
postcss-value-parser@4.2.0:
|
||||
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
||||
|
||||
|
|
@ -5034,6 +5079,13 @@ snapshots:
|
|||
optionalDependencies:
|
||||
postcss: 8.4.24
|
||||
|
||||
postcss-load-config@6.0.1(postcss@8.4.38)(yaml@2.4.2):
|
||||
dependencies:
|
||||
lilconfig: 3.1.1
|
||||
optionalDependencies:
|
||||
postcss: 8.4.38
|
||||
yaml: 2.4.2
|
||||
|
||||
postcss-value-parser@4.2.0: {}
|
||||
|
||||
postcss@8.4.24:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue