Skip to content

Commit a06245b

Browse files
philipp-spiessRobinMalfaitadamwathan
authored
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 <[email protected]> Co-authored-by: Adam Wathan <[email protected]>
1 parent c6572ab commit a06245b

File tree

6 files changed

+154
-17
lines changed

6 files changed

+154
-17
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- _Upgrade (experimental)_: Migrate `plugins` with options to CSS ([#14700](https://github.com/tailwindlabs/tailwindcss/pull/14700))
1313
- _Upgrade (experimental)_: Allow JS configuration files with `corePlugins` options to be migrated to CSS ([#14742](https://github.com/tailwindlabs/tailwindcss/pull/14742))
14+
- _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))
1415
- _Upgrade (experimental)_: Migrate `max-w-screen-*` utilities to `max-w-[var(…)]`([#14754](https://github.com/tailwindlabs/tailwindcss/pull/14754))
1516
- _Upgrade (experimental)_: Migrate `@variants` and `@responsive` directives ([#14748](https://github.com/tailwindlabs/tailwindcss/pull/14748))
1617
- _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
3637

3738
### Changed
3839

40+
- 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))
3941
- _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))
4042

4143
## [4.0.0-alpha.28] - 2024-10-17

packages/@tailwindcss-node/src/compile.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import EnhancedResolve from 'enhanced-resolve'
22
import { createJiti, type Jiti } from 'jiti'
33
import fs from 'node:fs'
44
import fsPromises from 'node:fs/promises'
5-
import path, { dirname, extname } from 'node:path'
5+
import path, { dirname } from 'node:path'
66
import { pathToFileURL } from 'node:url'
77
import {
88
__unstable__loadDesignSystem as ___unstable__loadDesignSystem,
@@ -120,21 +120,6 @@ async function resolveCssId(id: string, base: string): Promise<string | false |
120120
}
121121
}
122122

123-
// CSS imports that do not have a dir prefix are considered relative. Since
124-
// the resolver does not account for this, we need to do a first pass with an
125-
// assumed relative import by prefixing `./${path}`. We don't have to do this
126-
// when the path starts with a `.` or when the path has no extension (at which
127-
// case it's likely an npm package and not a relative stylesheet).
128-
let skipRelativeCheck = extname(id) === '' || id.startsWith('.')
129-
130-
if (!skipRelativeCheck) {
131-
try {
132-
let dotResolved = await runResolver(cssResolver, `./${id}`, base)
133-
if (!dotResolved) throw new Error()
134-
return dotResolved
135-
} catch {}
136-
}
137-
138123
return runResolver(cssResolver, id, base)
139124
}
140125

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.foo {
2+
color: red;
3+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { __unstable__loadDesignSystem } from '@tailwindcss/node'
2+
import dedent from 'dedent'
3+
import postcss from 'postcss'
4+
import { expect, it } from 'vitest'
5+
import type { UserConfig } from '../../../tailwindcss/src/compat/config/types'
6+
import { migrateImport } from './migrate-import'
7+
8+
const css = dedent
9+
10+
async function migrate(input: string, userConfig: UserConfig = {}) {
11+
return postcss()
12+
.use(
13+
migrateImport({
14+
designSystem: await __unstable__loadDesignSystem(`@import 'tailwindcss';`, {
15+
base: __dirname,
16+
}),
17+
userConfig,
18+
}),
19+
)
20+
.process(input, { from: expect.getState().testPath })
21+
.then((result) => result.css)
22+
}
23+
24+
it('prints relative file imports as relative paths', async () => {
25+
expect(
26+
await migrate(css`
27+
@import 'fixtures/test';
28+
@import 'fixtures/test.css';
29+
@import './fixtures/test.css';
30+
@import './fixtures/test';
31+
32+
@import 'fixtures/test' screen;
33+
@import 'fixtures/test.css' screen;
34+
@import './fixtures/test.css' screen;
35+
@import './fixtures/test' screen;
36+
37+
@import 'fixtures/test' supports(display: grid);
38+
@import 'fixtures/test.css' supports(display: grid);
39+
@import './fixtures/test.css' supports(display: grid);
40+
@import './fixtures/test' supports(display: grid);
41+
42+
@import 'fixtures/test' layer(utilities);
43+
@import 'fixtures/test.css' layer(utilities);
44+
@import './fixtures/test.css' layer(utilities);
45+
@import './fixtures/test' layer(utilities);
46+
47+
@import 'fixtures/test' theme(inline);
48+
@import 'fixtures/test.css' theme(inline);
49+
@import './fixtures/test.css' theme(inline);
50+
@import './fixtures/test' theme(inline);
51+
52+
@import 'fixtures/test' layer(utilities) supports(display: grid) screen and (min-width: 600px);
53+
@import 'fixtures/test.css' layer(utilities) supports(display: grid) screen and
54+
(min-width: 600px);
55+
@import './fixtures/test.css' layer(utilities) supports(display: grid) screen and
56+
(min-width: 600px);
57+
@import './fixtures/test' layer(utilities) supports(display: grid) screen and
58+
(min-width: 600px);
59+
60+
@import 'tailwindcss';
61+
@import 'tailwindcss/theme.css';
62+
@import 'tailwindcss/theme';
63+
`),
64+
).toMatchInlineSnapshot(`
65+
"@import './fixtures/test.css';
66+
@import './fixtures/test.css';
67+
@import './fixtures/test.css';
68+
@import './fixtures/test.css';
69+
70+
@import './fixtures/test.css' screen;
71+
@import './fixtures/test.css' screen;
72+
@import './fixtures/test.css' screen;
73+
@import './fixtures/test.css' screen;
74+
75+
@import './fixtures/test.css' supports(display: grid);
76+
@import './fixtures/test.css' supports(display: grid);
77+
@import './fixtures/test.css' supports(display: grid);
78+
@import './fixtures/test.css' supports(display: grid);
79+
80+
@import './fixtures/test.css' layer(utilities);
81+
@import './fixtures/test.css' layer(utilities);
82+
@import './fixtures/test.css' layer(utilities);
83+
@import './fixtures/test.css' layer(utilities);
84+
85+
@import './fixtures/test.css' theme(inline);
86+
@import './fixtures/test.css' theme(inline);
87+
@import './fixtures/test.css' theme(inline);
88+
@import './fixtures/test.css' theme(inline);
89+
90+
@import './fixtures/test.css' layer(utilities) supports(display: grid) screen and (min-width: 600px);
91+
@import './fixtures/test.css' layer(utilities) supports(display: grid) screen and
92+
(min-width: 600px);
93+
@import './fixtures/test.css' layer(utilities) supports(display: grid) screen and
94+
(min-width: 600px);
95+
@import './fixtures/test.css' layer(utilities) supports(display: grid) screen and
96+
(min-width: 600px);
97+
98+
@import 'tailwindcss';
99+
@import 'tailwindcss/theme.css';
100+
@import 'tailwindcss/theme';"
101+
`)
102+
})
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import fs from 'node:fs/promises'
2+
import { dirname, resolve } from 'node:path'
3+
import { type Plugin, type Root } from 'postcss'
4+
import { parseImportParams } from '../../../tailwindcss/src/at-import'
5+
import { segment } from '../../../tailwindcss/src/utils/segment'
6+
import * as ValueParser from '../../../tailwindcss/src/value-parser'
7+
8+
export function migrateImport(): Plugin {
9+
async function migrate(root: Root) {
10+
let file = root.source?.input.file
11+
if (!file) return
12+
13+
let promises: Promise<void>[] = []
14+
root.walkAtRules('import', (rule) => {
15+
let [firstParam, ...rest] = segment(rule.params, ' ')
16+
17+
let params = parseImportParams(ValueParser.parse(firstParam))
18+
19+
let isRelative = params.uri[0] === '.'
20+
let hasCssExtension = params.uri.endsWith('.css')
21+
22+
if (isRelative && hasCssExtension) {
23+
return
24+
}
25+
26+
let fullPath = resolve(dirname(file), params.uri)
27+
if (!hasCssExtension) fullPath += '.css'
28+
29+
promises.push(
30+
fs.stat(fullPath).then(() => {
31+
let ext = hasCssExtension ? '' : '.css'
32+
let path = isRelative ? params.uri : `./${params.uri}`
33+
rule.params = [`'${path}${ext}'`, ...rest].join(' ')
34+
}),
35+
)
36+
})
37+
38+
await Promise.allSettled(promises)
39+
}
40+
41+
return {
42+
postcssPlugin: '@tailwindcss/upgrade/migrate-import',
43+
OnceExit: migrate,
44+
}
45+
}

packages/tailwindcss/src/at-import.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export async function substituteAtImports(
6767
// `postcss-import` <https://github.com/postcss/postcss-import>
6868
// Copyright (c) 2014 Maxime Thirouin, Jason Campbell & Kevin Mårtensson
6969
// Released under the MIT License.
70-
function parseImportParams(params: ValueParser.ValueAstNode[]) {
70+
export function parseImportParams(params: ValueParser.ValueAstNode[]) {
7171
let uri
7272
let layer: string | null = null
7373
let media: string | null = null

0 commit comments

Comments
 (0)