Skip to content

Commit

Permalink
Detect conflicting imports for potential utility files
Browse files Browse the repository at this point in the history
Co-authored-by: Robin Malfait <[email protected]>
  • Loading branch information
thecrypticace and RobinMalfait committed Oct 10, 2024
1 parent 42d79e0 commit 589d0e8
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 2 deletions.
65 changes: 65 additions & 0 deletions integrations/upgrade/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -808,3 +808,68 @@ test(
`)
},
)

test(
'migrate utility files imported by multiple roots',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "workspace:^",
"@tailwindcss/cli": "workspace:^",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.js': js`module.exports = {}`,
'src/index.html': html`
<div class="hover:thing"></div>
`,
'src/root.1.css': css`
@import 'tailwindcss/utilities';
@import './a.1.css' layer(utilities);
`,
'src/root.2.css': css`
@import 'tailwindcss/utilities';
@import './a.1.css' layer(components);
`,
'src/root.3.css': css`
@import 'tailwindcss/utilities';
@import './a.1.css';
`,
'src/a.1.css': css`
.foo-from-a {
color: red;
}
`,
},
},
async ({ fs, exec }) => {
let output = await exec('npx @tailwindcss/upgrade --force')

expect(output).toMatch(
/You have one or more stylesheets that are imported into a utility layer and non-utility layer./,
)

expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
"
--- ./src/a.1.css ---
.foo-from-a {
color: red;
}
--- ./src/root.1.css ---
@import 'tailwindcss/utilities' layer(utilities);
@import './a.1.css' layer(utilities);
--- ./src/root.2.css ---
@import 'tailwindcss/utilities' layer(utilities);
@import './a.1.css' layer(components);
--- ./src/root.3.css ---
@import 'tailwindcss/utilities' layer(utilities);
@import './a.1.css' layer(utilities);"
`)
},
)
2 changes: 1 addition & 1 deletion integrations/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export function test(
if (execOptions.ignoreStdErr !== true) console.error(stderr)
reject(error)
} else {
resolve(stdout.toString())
resolve(stdout.toString() + '\n\n' + stderr.toString())
}
},
)
Expand Down
55 changes: 54 additions & 1 deletion packages/@tailwindcss-upgrade/src/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { migrateAtLayerUtilities } from './codemods/migrate-at-layer-utilities'
import { migrateMediaScreen } from './codemods/migrate-media-screen'
import { migrateMissingLayers } from './codemods/migrate-missing-layers'
import { migrateTailwindDirectives } from './codemods/migrate-tailwind-directives'
import { Stylesheet, type StylesheetId } from './stylesheet'
import { Stylesheet, type StylesheetConnection, type StylesheetId } from './stylesheet'
import { resolveCssId } from './utils/resolve'
import { walk, WalkAction } from './utils/walk'

Expand Down Expand Up @@ -43,6 +43,8 @@ export async function migrate(stylesheet: Stylesheet, options: MigrateOptions) {
throw new Error('Cannot migrate a stylesheet without a file path')
}

if (!stylesheet.canMigrate) return

await migrateContents(stylesheet, options)
}

Expand Down Expand Up @@ -123,6 +125,57 @@ export async function analyze(stylesheets: Stylesheet[]) {

await processor.process(sheet.root, { from: sheet.file })
}

let commonPath = process.cwd()

function pathToString(path: StylesheetConnection[]) {
let parts: string[] = []

for (let connection of path) {
if (!connection.item.file) continue

let filePath = connection.item.file.replace(commonPath, '')
let layers = connection.meta.layers.join(', ')

if (layers.length > 0) {
parts.push(`${filePath} (layers: ${layers})`)
} else {
parts.push(filePath)
}
}

return parts.join(' <- ')
}

let lines: string[] = []

for (let sheet of stylesheets) {
if (!sheet.file) continue

let { convertablePaths, nonConvertablePaths } = sheet.analyzeImportPaths()
let isAmbiguous = convertablePaths.length > 0 && nonConvertablePaths.length > 0

if (!isAmbiguous) continue

sheet.canMigrate = false

let filePath = sheet.file.replace(commonPath, '')

for (let path of convertablePaths) {
lines.push(`- ${filePath} <- ${pathToString(path)}`)
}

for (let path of nonConvertablePaths) {
lines.push(`- ${filePath} <- ${pathToString(path)}`)
}
}

if (lines.length === 0) return

let error = `You have one or more stylesheets that are imported into a utility layer and non-utility layer.\n`
error += `We cannot convert stylesheets under these conditions. Please look at the following stylesheets:\n`

throw new Error(error + lines.join('\n'))
}

export async function split(stylesheets: Stylesheet[]) {
Expand Down
96 changes: 96 additions & 0 deletions packages/@tailwindcss-upgrade/src/stylesheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ export class Stylesheet {
*/
children = new Set<StylesheetConnection>()

/**
* Whether or not this stylesheet can be migrated
*/
canMigrate = true

/**
* Whether or not this stylesheet can be migrated
*/
Expand Down Expand Up @@ -123,6 +128,97 @@ export class Stylesheet {
return layers
}

/**
* Iterate all paths from a stylesheet through its ancestors to all roots
*
* For example, given the following structure:
*
* ```
* c.css
* -> a.1.css @import "…"
* -> a.css
* -> root.1.css (utility: no)
* -> root.2.css (utility: no)
* -> b.css
* -> root.1.css (utility: no)
* -> root.2.css (utility: no)
*
* -> a.2.css @import "…" layer(foo)
* -> a.css
* -> root.1.css (utility: no)
* -> root.2.css (utility: no)
* -> b.css
* -> root.1.css (utility: no)
* -> root.2.css (utility: no)
*
* -> b.1.css @import "…" layer(components / utilities)
* -> a.css
* -> root.1.css (utility: yes)
* -> root.2.css (utility: yes)
* -> b.css
* -> root.1.css (utility: yes)
* -> root.2.css (utility: yes)
* ```
*
* We can see there are a total of 12 import paths with various layers.
* We need to be able to iterate every one of these paths and inspect
* the layers used in each path..
*/
*pathsToRoot(): Iterable<StylesheetConnection[]> {
for (let { item, path } of walkDepth(this, (sheet) => sheet.parents)) {
// Skip over intermediate stylesheets since all paths from a leaf to a
// root will encompass all possible intermediate stylesheet paths.
if (item.parents.size > 0) {
continue
}

yield path
}
}

/**
* Analyze a stylesheets import paths to see if some can be considered
* for conversion to utility rules and others can't.
*
* If a stylesheet is imported directly or indirectly and some imports are in
* a utility layer and some are not that means that we can't safely convert
* the rules in the stylesheet to `@utility`. Doing so would mean that we
* would need to replicate the stylesheet and change one to have `@utility`
* rules and leave the other as is.
*
* We can see, given the same structure from the `pathsToRoot` example, that
* `css.css` is imported into different layers:
* - `a.1.css` has no layers and should not be converted
* - `a.2.css` has a layer `foo` and should not be converted
* - `b.1.css` has a layer `utilities` (or `components`) which should be
*
* Since this means that `c.css` must both not be converted and converted
* we can't do this without replicating the stylesheet, any ancestors, and
* adjusting imports which is a non-trivial task.
*/
analyzeImportPaths() {
let convertablePaths: StylesheetConnection[][] = []
let nonConvertablePaths: StylesheetConnection[][] = []

for (let path of this.pathsToRoot()) {
let isConvertable = false

for (let { meta } of path) {
for (let layer of meta.layers) {
isConvertable ||= layer === 'utilities' || layer === 'components'
}
}

if (isConvertable) {
convertablePaths.push(path)
} else {
nonConvertablePaths.push(path)
}
}

return { convertablePaths, nonConvertablePaths }
}

[util.inspect.custom]() {
return {
...this,
Expand Down

0 comments on commit 589d0e8

Please sign in to comment.