From a06245b3feb5b8b4d7633e9beba3383a6676d088 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 22 Oct 2024 22:30:41 +0200 Subject: [PATCH] Upgrade: Rewrite imports of relative files to use relative file paths (#14755) When we implemented the CSS import resolution system, we found out a detail about CSS imports in that files without a relative path prefix would still be relative to the source file. E.g.: ```css @import 'foo.css'; ``` Should first look for the file `foo.css` in the same directory. To make this cost as cheap as possible, we limited this by a heuristics to only apply the auto-relative imports for files with a file extension. Naturally, while testing v4 on more templates, we found that it's common for people to omit the file extension when loading css file. The above could also be written as such: ```css @import 'foo'; ``` To improve this, we have two options: - We either remove the heuristics, making every `@import` more expensive because we have to check for relative files. - We upgrade our codemods to rewrite `@import` statements to be explicitly relative. Because we really care about performance, we opted to go with the latter option. This PR adds the codemod and removes the heuristics so we resolve CSS files similar to how you would resolve JS files. --------- Co-authored-by: Robin Malfait Co-authored-by: Adam Wathan --- CHANGELOG.md | 2 + packages/@tailwindcss-node/src/compile.ts | 17 +-- .../src/codemods/fixtures/test.css | 3 + .../src/codemods/migrate-import.test.ts | 102 ++++++++++++++++++ .../src/codemods/migrate-import.ts | 45 ++++++++ packages/tailwindcss/src/at-import.ts | 2 +- 6 files changed, 154 insertions(+), 17 deletions(-) create mode 100644 packages/@tailwindcss-upgrade/src/codemods/fixtures/test.css create mode 100644 packages/@tailwindcss-upgrade/src/codemods/migrate-import.test.ts create mode 100644 packages/@tailwindcss-upgrade/src/codemods/migrate-import.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 891e64756..1898bb4a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - _Upgrade (experimental)_: Migrate `plugins` with options to CSS ([#14700](https://github.com/tailwindlabs/tailwindcss/pull/14700)) - _Upgrade (experimental)_: Allow JS configuration files with `corePlugins` options to be migrated to CSS ([#14742](https://github.com/tailwindlabs/tailwindcss/pull/14742)) +- _Upgrade (experimental)_: Migrate `@import` statements for relative CSS files to use relative path syntax (e.g. `./file.css`) ([#14755](https://github.com/tailwindlabs/tailwindcss/pull/14755)) - _Upgrade (experimental)_: Migrate `max-w-screen-*` utilities to `max-w-[var(…)]`([#14754](https://github.com/tailwindlabs/tailwindcss/pull/14754)) - _Upgrade (experimental)_: Migrate `@variants` and `@responsive` directives ([#14748](https://github.com/tailwindlabs/tailwindcss/pull/14748)) - _Upgrade (experimental)_: Migrate `@screen` directive ([#14749](https://github.com/tailwindlabs/tailwindcss/pull/14749)) @@ -36,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Require a relative path prefix for importing relative CSS files (e.g. `@import './styles.css'` instead of `@import 'styles.css'`) ([#14755](https://github.com/tailwindlabs/tailwindcss/pull/14755)) - _Upgrade (experimental)_: Don't create `@source` rules for `content` paths that are already covered by automatic source detection ([#14714](https://github.com/tailwindlabs/tailwindcss/pull/14714)) ## [4.0.0-alpha.28] - 2024-10-17 diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts index 25a2d037d..4804a1c8b 100644 --- a/packages/@tailwindcss-node/src/compile.ts +++ b/packages/@tailwindcss-node/src/compile.ts @@ -2,7 +2,7 @@ import EnhancedResolve from 'enhanced-resolve' import { createJiti, type Jiti } from 'jiti' import fs from 'node:fs' import fsPromises from 'node:fs/promises' -import path, { dirname, extname } from 'node:path' +import path, { dirname } from 'node:path' import { pathToFileURL } from 'node:url' import { __unstable__loadDesignSystem as ___unstable__loadDesignSystem, @@ -120,21 +120,6 @@ async function resolveCssId(id: string, base: string): Promise result.css) +} + +it('prints relative file imports as relative paths', async () => { + expect( + await migrate(css` + @import 'fixtures/test'; + @import 'fixtures/test.css'; + @import './fixtures/test.css'; + @import './fixtures/test'; + + @import 'fixtures/test' screen; + @import 'fixtures/test.css' screen; + @import './fixtures/test.css' screen; + @import './fixtures/test' screen; + + @import 'fixtures/test' supports(display: grid); + @import 'fixtures/test.css' supports(display: grid); + @import './fixtures/test.css' supports(display: grid); + @import './fixtures/test' supports(display: grid); + + @import 'fixtures/test' layer(utilities); + @import 'fixtures/test.css' layer(utilities); + @import './fixtures/test.css' layer(utilities); + @import './fixtures/test' layer(utilities); + + @import 'fixtures/test' theme(inline); + @import 'fixtures/test.css' theme(inline); + @import './fixtures/test.css' theme(inline); + @import './fixtures/test' theme(inline); + + @import 'fixtures/test' layer(utilities) supports(display: grid) screen and (min-width: 600px); + @import 'fixtures/test.css' layer(utilities) supports(display: grid) screen and + (min-width: 600px); + @import './fixtures/test.css' layer(utilities) supports(display: grid) screen and + (min-width: 600px); + @import './fixtures/test' layer(utilities) supports(display: grid) screen and + (min-width: 600px); + + @import 'tailwindcss'; + @import 'tailwindcss/theme.css'; + @import 'tailwindcss/theme'; + `), + ).toMatchInlineSnapshot(` + "@import './fixtures/test.css'; + @import './fixtures/test.css'; + @import './fixtures/test.css'; + @import './fixtures/test.css'; + + @import './fixtures/test.css' screen; + @import './fixtures/test.css' screen; + @import './fixtures/test.css' screen; + @import './fixtures/test.css' screen; + + @import './fixtures/test.css' supports(display: grid); + @import './fixtures/test.css' supports(display: grid); + @import './fixtures/test.css' supports(display: grid); + @import './fixtures/test.css' supports(display: grid); + + @import './fixtures/test.css' layer(utilities); + @import './fixtures/test.css' layer(utilities); + @import './fixtures/test.css' layer(utilities); + @import './fixtures/test.css' layer(utilities); + + @import './fixtures/test.css' theme(inline); + @import './fixtures/test.css' theme(inline); + @import './fixtures/test.css' theme(inline); + @import './fixtures/test.css' theme(inline); + + @import './fixtures/test.css' layer(utilities) supports(display: grid) screen and (min-width: 600px); + @import './fixtures/test.css' layer(utilities) supports(display: grid) screen and + (min-width: 600px); + @import './fixtures/test.css' layer(utilities) supports(display: grid) screen and + (min-width: 600px); + @import './fixtures/test.css' layer(utilities) supports(display: grid) screen and + (min-width: 600px); + + @import 'tailwindcss'; + @import 'tailwindcss/theme.css'; + @import 'tailwindcss/theme';" + `) +}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-import.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-import.ts new file mode 100644 index 000000000..a34f2d18e --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-import.ts @@ -0,0 +1,45 @@ +import fs from 'node:fs/promises' +import { dirname, resolve } from 'node:path' +import { type Plugin, type Root } from 'postcss' +import { parseImportParams } from '../../../tailwindcss/src/at-import' +import { segment } from '../../../tailwindcss/src/utils/segment' +import * as ValueParser from '../../../tailwindcss/src/value-parser' + +export function migrateImport(): Plugin { + async function migrate(root: Root) { + let file = root.source?.input.file + if (!file) return + + let promises: Promise[] = [] + root.walkAtRules('import', (rule) => { + let [firstParam, ...rest] = segment(rule.params, ' ') + + let params = parseImportParams(ValueParser.parse(firstParam)) + + let isRelative = params.uri[0] === '.' + let hasCssExtension = params.uri.endsWith('.css') + + if (isRelative && hasCssExtension) { + return + } + + let fullPath = resolve(dirname(file), params.uri) + if (!hasCssExtension) fullPath += '.css' + + promises.push( + fs.stat(fullPath).then(() => { + let ext = hasCssExtension ? '' : '.css' + let path = isRelative ? params.uri : `./${params.uri}` + rule.params = [`'${path}${ext}'`, ...rest].join(' ') + }), + ) + }) + + await Promise.allSettled(promises) + } + + return { + postcssPlugin: '@tailwindcss/upgrade/migrate-import', + OnceExit: migrate, + } +} diff --git a/packages/tailwindcss/src/at-import.ts b/packages/tailwindcss/src/at-import.ts index a06000826..29c0ce2c5 100644 --- a/packages/tailwindcss/src/at-import.ts +++ b/packages/tailwindcss/src/at-import.ts @@ -67,7 +67,7 @@ export async function substituteAtImports( // `postcss-import` // Copyright (c) 2014 Maxime Thirouin, Jason Campbell & Kevin Mårtensson // Released under the MIT License. -function parseImportParams(params: ValueParser.ValueAstNode[]) { +export function parseImportParams(params: ValueParser.ValueAstNode[]) { let uri let layer: string | null = null let media: string | null = null