diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts index 881f40010..a833754e5 100644 --- a/integrations/upgrade/index.test.ts +++ b/integrations/upgrade/index.test.ts @@ -1,3 +1,4 @@ +import { expect } from 'vitest' import { css, html, js, json, test } from '../utils' test( @@ -261,3 +262,433 @@ test( ) }, ) + +test( + 'migrate utilities in an imported file', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "workspace:^", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'src/index.css': css` + @import 'tailwindcss'; + @import './utilities.css' layer(utilities); + `, + 'src/utilities.css': css` + .no-scrollbar::-webkit-scrollbar { + display: none; + } + + .no-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; + } + `, + }, + }, + async ({ fs, exec }) => { + await exec('npx @tailwindcss/upgrade --force') + + await fs.expectFileToContain( + 'src/index.css', + css` + @import 'tailwindcss'; + @import './utilities.css'; + `, + ) + + await fs.expectFileToContain( + 'src/utilities.css', + css` + @utility no-scrollbar { + &::-webkit-scrollbar { + display: none; + } + -ms-overflow-style: none; + scrollbar-width: none; + } + `, + ) + }, +) + +test( + 'wip it', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "workspace:^", + "@tailwindcss/cli": "workspace:^", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'src/index.html': html` +
+ `, + 'src/index.css': css` + @import 'tailwindcss/utilities'; + @import './a.css' layer(utilities); + @import './b.css' layer(components); + @import './c.css'; + `, + 'src/a.css': css` + @import './utilities.css'; + + .foo-from-a { + color: red; + } + `, + 'src/utilities.css': css` + #foo { + --keep: me; + } + + .foo-from-import { + color: blue; + } + `, + 'src/b.css': css` + @import './components.css'; + + .bar-from-b { + color: red; + } + `, + 'src/components.css': css` + .bar-from-import { + color: blue; + } + `, + 'src/c.css': css` + @import './c-2.css' layer(utilities); + .baz-from-c { + color: green; + } + `, + 'src/c-2.css': css` + @import './c-3.css'; + #baz { + --keep: me; + } + .baz-from-import { + color: yellow; + } + `, + 'src/c-3.css': css` + #baz { + --keep: me; + } + .baz-from-import { + color: yellow; + } + `, + }, + }, + async ({ fs, exec }) => { + await exec('npx @tailwindcss/upgrade --force') + + expect(await fs.read('src/index.css')).toMatchInlineSnapshot(` + "@import 'tailwindcss/utilities' layer(utilities); + @import './a.css' layer(utilities); + @import "./a.utilities.css"; + @import './b.css' layer(components); + @import "./b.utilities.css"; + @import './c.css' layer(utilities); + @import "./c.utilities.css";" + `) + expect(await fs.read('src/a.css')).toMatchInlineSnapshot(`"@import './utilities.css'"`) + expect(await fs.read('src/a.utilities.css')).toMatchInlineSnapshot(` + "@import "./utilities.utilities.css"; + @utility foo-from-a { + color: red; + }" + `) + + expect(await fs.read('src/utilities.css')).toMatchInlineSnapshot(` + "#foo { + --keep: me; + }" + `) + + expect(await fs.read('src/utilities.utilities.css')).toMatchInlineSnapshot(` + "@utility foo-from-import { + color: blue; + }" + `) + + expect(await fs.read('src/b.css')).toMatchInlineSnapshot(`""`) + expect(await fs.read('src/b.utilities.css')).toMatchInlineSnapshot(` + "@import "./components.css"; + @utility bar-from-b { + color: red; + }" + `) + + expect(await fs.read('src/c.css')).toMatchInlineSnapshot(` + "@import './c-2.css' layer(utilities); + .baz-from-c { + color: green; + }" + `) + expect(await fs.read('src/c.utilities.css')).toMatchInlineSnapshot( + `"@import "./c-2.utilities.css""`, + ) + + expect(await fs.read('src/c-2.css')).toMatchInlineSnapshot(` + "@import './c-3.css'; + #baz { + --keep: me; + }" + `) + expect(await fs.read('src/c-2.utilities.css')).toMatchInlineSnapshot(` + "@import "./c-3.utilities.css"; + @utility baz-from-import { + color: yellow; + }" + `) + + expect(await fs.read('src/c-3.css')).toMatchInlineSnapshot(` + "#baz { + --keep: me; + }" + `) + expect(await fs.read('src/c-3.utilities.css')).toMatchInlineSnapshot(` + "@utility baz-from-import { + color: yellow; + }" + `) + }, +) + +test( + 'deeply nested imports', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "workspace:^", + "@tailwindcss/cli": "workspace:^", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'src/index.html': html` + + `, + 'src/index.css': css` + @import 'tailwindcss/utilities'; + @import './a.1.css' layer(utilities); + `, + 'src/a.1.css': css` + @import './a.2.css'; + + #from-a-1 { + --keep: me; + } + + .from-a-1 { + color: red; + } + `, + 'src/a.2.css': css` + @import './a.3.css'; + + #from-a-2 { + --keep: me; + } + + .from-a-2 { + color: green; + } + `, + 'src/a.3.css': css` + #from-a-3 { + --keep: me; + } + + .from-a-3 { + color: blue; + } + `, + }, + }, + async ({ fs, exec }) => { + await exec('npx @tailwindcss/upgrade --force') + + expect(await fs.read('src/index.css')).toMatchInlineSnapshot(` + "@import 'tailwindcss/utilities' layer(utilities); + @import './a.1.css' layer(utilities); + @import "./a.1.utilities.css";" + `) + expect(await fs.read('src/a.1.css')).toMatchInlineSnapshot(` + "@import './a.2.css'; + + #from-a-1 { + --keep: me; + }" + `) + expect(await fs.read('src/a.1.utilities.css')).toMatchInlineSnapshot(` + "@import "./a.2.utilities.css"; + @utility from-a-1 { + color: red; + }" + `) + expect(await fs.read('src/a.2.css')).toMatchInlineSnapshot(` + "@import './a.3.css'; + + #from-a-2 { + --keep: me; + }" + `) + expect(await fs.read('src/a.2.utilities.css')).toMatchInlineSnapshot(` + "@import "./a.3.utilities.css"; + @utility from-a-2 { + color: green; + }" + `) + expect(await fs.read('src/a.3.css')).toMatchInlineSnapshot(` + "#from-a-3 { + --keep: me; + }" + `) + expect(await fs.read('src/a.3.utilities.css')).toMatchInlineSnapshot(` + "@utility from-a-3 { + color: blue; + }" + `) + }, +) + +test( + 'deeply nested imports', + { + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "workspace:^", + "@tailwindcss/cli": "workspace:^", + "@tailwindcss/upgrade": "workspace:^" + } + } + `, + 'src/index.html': html` + + `, + 'src/index.css': css` + @import 'tailwindcss/utilities'; + @import './a.1.css'; + `, + 'src/a.1.css': css` + @import './a.2.css' layer(utilities); + + #from-a-1 { + --keep: me; + } + + .from-a-1 { + color: red; + } + `, + 'src/a.2.css': css` + @import './a.3.css'; + + #from-a-2 { + --keep: me; + } + + .from-a-2 { + color: green; + } + `, + 'src/a.3.css': css` + @import './a.4.css'; + + #from-a-3 { + --keep: me; + } + + .from-a-3 { + color: blue; + } + `, + 'src/a.4.css': css` + #from-a-4 { + --keep: me; + } + + .from-a-4 { + color: blue; + } + `, + }, + }, + async ({ fs, exec }) => { + await exec('npx @tailwindcss/upgrade --force') + + expect(await fs.read('src/index.css')).toMatchInlineSnapshot(` + "@import 'tailwindcss/utilities' layer(utilities); + @import './a.1.css' layer(utilities); + @import "./a.1.utilities.css";" + `) + expect(await fs.read('src/a.1.css')).toMatchInlineSnapshot(` + "@import './a.2.css' layer(utilities); + + #from-a-1 { + --keep: me; + } + + .from-a-1 { + color: red; + }" + `) + expect(await fs.read('src/a.1.utilities.css')).toMatchInlineSnapshot( + `"@import "./a.2.utilities.css""`, + ) + expect(await fs.read('src/a.2.css')).toMatchInlineSnapshot(` + "@import './a.3.css'; + + #from-a-2 { + --keep: me; + }" + `) + expect(await fs.read('src/a.2.utilities.css')).toMatchInlineSnapshot(` + "@import "./a.3.utilities.css"; + @utility from-a-2 { + color: green; + }" + `) + expect(await fs.read('src/a.3.css')).toMatchInlineSnapshot(` + "@import './a.4.css'; + + #from-a-3 { + --keep: me; + }" + `) + expect(await fs.read('src/a.3.utilities.css')).toMatchInlineSnapshot(` + "@import "./a.4.utilities.css"; + @utility from-a-3 { + color: blue; + }" + `) + expect(await fs.read('src/a.4.css')).toMatchInlineSnapshot(` + "#from-a-4 { + --keep: me; + }" + `) + expect(await fs.read('src/a.4.utilities.css')).toMatchInlineSnapshot(` + "@utility from-a-4 { + color: blue; + }" + `) + }, +) diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.test.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.test.ts index f53e3f3c9..2bac6ba6c 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.test.ts @@ -1,16 +1,39 @@ import dedent from 'dedent' import postcss from 'postcss' import { describe, expect, it } from 'vitest' +import { Stylesheet } from '../stylesheet' import { formatNodes } from './format-nodes' import { migrateAtLayerUtilities } from './migrate-at-layer-utilities' const css = dedent -function migrate(input: string) { +async function migrate( + data: + | Stylesheet + | string + | { + root: postcss.Root + layers?: string[] + }, +) { + let stylesheet: Stylesheet + + if (typeof data === 'string') { + stylesheet = await Stylesheet.fromString(data) + } else if (data instanceof Stylesheet) { + stylesheet = data + } else { + stylesheet = await Stylesheet.fromRoot(data.root) + + for (let layer of data.layers ?? []) { + stylesheet.layers.add(layer) + } + } + return postcss() - .use(migrateAtLayerUtilities()) + .use(migrateAtLayerUtilities(stylesheet)) .use(formatNodes()) - .process(input, { from: expect.getState().testPath }) + .process(stylesheet.root!, { from: expect.getState().testPath }) .then((result) => result.css) } @@ -820,3 +843,213 @@ it('should not lose attribute selectors', async () => { }" `) }) + +describe('layered stylesheets', () => { + it('should transform classes to utilities inside a layered stylesheet (utilities)', async () => { + expect( + await migrate({ + root: postcss.parse(css` + /* Utility #1 */ + .foo { + /* Declarations: */ + color: red; + } + `), + layers: ['utilities'], + }), + ).toMatchInlineSnapshot(` + "@utility foo { + /* Utility #1 */ + /* Declarations: */ + color: red; + }" + `) + }) + + it('should transform classes to utilities inside a layered stylesheet (components)', async () => { + expect( + await migrate({ + root: postcss.parse(css` + /* Utility #1 */ + .foo { + /* Declarations: */ + color: red; + } + `), + layers: ['components'], + }), + ).toMatchInlineSnapshot(` + "@utility foo { + /* Utility #1 */ + /* Declarations: */ + color: red; + }" + `) + }) + + it('should NOT transform classes to utilities inside a non-utility, layered stylesheet', async () => { + expect( + await migrate({ + root: postcss.parse(css` + /* Utility #1 */ + .foo { + /* Declarations: */ + color: red; + } + `), + layers: ['foo'], + }), + ).toMatchInlineSnapshot(` + "/* Utility #1 */ + .foo { + /* Declarations: */ + color: red; + }" + `) + }) + + it('should handle non-classes in utility-layered stylesheets', async () => { + expect( + await migrate({ + root: postcss.parse(css` + /* Utility #1 */ + .foo { + /* Declarations: */ + color: red; + } + #main { + color: red; + } + `), + layers: ['utilities'], + }), + ).toMatchInlineSnapshot(` + " + #main { + color: red; + } + + @utility foo { + /* Utility #1 */ + /* Declarations: */ + color: red; + }" + `) + }) + + it('should handle non-classes in utility-layered stylesheets', async () => { + expect( + await migrate({ + root: postcss.parse(css` + @layer utilities { + @layer utilities { + /* Utility #1 */ + .foo { + /* Declarations: */ + color: red; + } + } + + /* Utility #2 */ + .bar { + /* Declarations: */ + color: red; + } + + #main { + color: red; + } + } + + /* Utility #3 */ + .baz { + /* Declarations: */ + color: red; + } + + #secondary { + color: red; + } + `), + layers: ['utilities'], + }), + ).toMatchInlineSnapshot(` + "@layer utilities { + + #main { + color: red; + } + } + + #secondary { + color: red; + } + + @utility foo { + @layer utilities { + @layer utilities { + /* Utility #1 */ + /* Declarations: */ + color: red; + } + } + } + + @utility bar { + @layer utilities { + /* Utility #2 */ + /* Declarations: */ + color: red; + } + } + + @utility baz { + /* Utility #3 */ + /* Declarations: */ + color: red; + }" + `) + }) + + it('imports are preserved in layered stylesheets', async () => { + expect( + await migrate({ + root: postcss.parse(css` + @import 'thing'; + + .foo { + color: red; + } + `), + layers: ['utilities'], + }), + ).toMatchInlineSnapshot(` + "@import 'thing'; + + @utility foo { + color: red; + }" + `) + }) + + it('charset is preserved in layered stylesheets', async () => { + expect( + await migrate({ + root: postcss.parse(css` + @charset "utf-8"; + + .foo { + color: red; + } + `), + layers: ['utilities'], + }), + ).toMatchInlineSnapshot(` + "@charset "utf-8"; + + @utility foo { + color: red; + }" + `) + }) +}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.ts index cedef0545..c362a9fbd 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-layer-utilities.ts @@ -1,9 +1,10 @@ import { type AtRule, type Comment, type Plugin, type Rule } from 'postcss' import SelectorParser from 'postcss-selector-parser' import { segment } from '../../../tailwindcss/src/utils/segment' +import { Stylesheet } from '../stylesheet' import { walk, WalkAction, walkDepth } from '../utils/walk' -export function migrateAtLayerUtilities(): Plugin { +export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin { function migrate(atRule: AtRule) { // Only migrate `@layer utilities` and `@layer components`. if (atRule.params !== 'utilities' && atRule.params !== 'components') return @@ -86,6 +87,12 @@ export function migrateAtLayerUtilities(): Plugin { clones.push(clone) walk(clone, (node) => { + if (node.type === 'atrule') { + if (!node.nodes || node.nodes?.length === 0) { + node.remove() + } + } + if (node.type !== 'rule') return // Fan out each utility into its own rule. @@ -186,7 +193,7 @@ export function migrateAtLayerUtilities(): Plugin { // Mark the node as pretty so that it gets formatted by Prettier later. clone.raws.tailwind_pretty = true - clone.raws.before += '\n\n' + clone.raws.before = `${clone.raws.before ?? ''}\n\n` } // Cleanup @@ -259,7 +266,16 @@ export function migrateAtLayerUtilities(): Plugin { return { postcssPlugin: '@tailwindcss/upgrade/migrate-at-layer-utilities', - OnceExit: (root) => { + OnceExit: (root, { atRule }) => { + let isUtilityStylesheet = + stylesheet.layers?.has('utilities') || stylesheet.layers?.has('components') + + if (isUtilityStylesheet) { + let rule = atRule({ name: 'layer', params: 'utilities' }) + rule.append(root.nodes) + root.append(rule) + } + // Migrate `@layer utilities` and `@layer components` into `@utility`. // Using this instead of the visitor API in case we want to use // postcss-nesting in the future. @@ -282,6 +298,17 @@ export function migrateAtLayerUtilities(): Plugin { } }) } + + // If the stylesheet is inside a layered import then we can remove the top-level layer directive we added + if (isUtilityStylesheet) { + root.each((node) => { + if (node.type !== 'atrule') return + if (node.name !== 'layer') return + if (node.params !== 'utilities') return + + node.replaceWith(node.nodes ?? []) + }) + } }, } } diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.ts index c32c60906..6d3de9fe6 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.ts @@ -71,6 +71,7 @@ export function migrateMissingLayers(): Plugin { if (node.name === 'import') { if (lastLayer !== '' && !node.params.includes('layer(')) { node.params += ` layer(${lastLayer})` + node.raws.tailwind_injected_layer = true } if (bucket.length > 0) { @@ -110,7 +111,7 @@ export function migrateMissingLayers(): Plugin { let target = nodes[0] let layerNode = new AtRule({ name: 'layer', - params: layerName || firstLayerName || '', + params: targetLayerName, nodes: nodes.map((node) => { // Keep the target node as-is, because we will be replacing that one // with the new layer node. diff --git a/packages/@tailwindcss-upgrade/src/index.test.ts b/packages/@tailwindcss-upgrade/src/index.test.ts index 4dbfaf46f..c6d817482 100644 --- a/packages/@tailwindcss-upgrade/src/index.test.ts +++ b/packages/@tailwindcss-upgrade/src/index.test.ts @@ -1,23 +1,27 @@ import dedent from 'dedent' +import postcss from 'postcss' import { expect, it } from 'vitest' +import { formatNodes } from './codemods/format-nodes' import { migrateContents } from './migrate' const css = dedent +function migrate(input: string) { + return migrateContents(input) + .then((result) => postcss([formatNodes()]).process(result.root, result.opts)) + .then((result) => result.css) +} + it('should print the input as-is', async () => { expect( - await migrateContents( - css` + await migrate(css` + /* above */ + .foo/* after */ { /* above */ - .foo/* after */ { - /* above */ - color: /* before */ red /* after */; - /* below */ - } - `, - {}, - expect.getState().testPath, - ), + color: /* before */ red /* after */; + /* below */ + } + `), ).toMatchInlineSnapshot(` "/* above */ .foo/* after */ { @@ -30,44 +34,41 @@ it('should print the input as-is', async () => { it('should migrate a stylesheet', async () => { expect( - await migrateContents( - css` - @tailwind base; + await migrate(css` + @tailwind base; - html { - overflow: hidden; + html { + overflow: hidden; + } + + @tailwind components; + + .a { + z-index: 1; + } + + @layer components { + .b { + z-index: 2; } + } - @tailwind components; + .c { + z-index: 3; + } - .a { - z-index: 1; + @tailwind utilities; + + .d { + z-index: 4; + } + + @layer utilities { + .e { + z-index: 5; } - - @layer components { - .b { - z-index: 2; - } - } - - .c { - z-index: 3; - } - - @tailwind utilities; - - .d { - z-index: 4; - } - - @layer utilities { - .e { - z-index: 5; - } - } - `, - {}, - ), + } + `), ).toMatchInlineSnapshot(` "@import 'tailwindcss'; @@ -107,17 +108,14 @@ it('should migrate a stylesheet', async () => { it('should migrate a stylesheet (with imports)', async () => { expect( - await migrateContents( - css` - @import 'tailwindcss/base'; - @import './my-base.css'; - @import 'tailwindcss/components'; - @import './my-components.css'; - @import 'tailwindcss/utilities'; - @import './my-utilities.css'; - `, - {}, - ), + await migrate(css` + @import 'tailwindcss/base'; + @import './my-base.css'; + @import 'tailwindcss/components'; + @import './my-components.css'; + @import 'tailwindcss/utilities'; + @import './my-utilities.css'; + `), ).toMatchInlineSnapshot(` "@import 'tailwindcss'; @import './my-base.css' layer(base); @@ -128,20 +126,17 @@ it('should migrate a stylesheet (with imports)', async () => { it('should migrate a stylesheet (with preceding rules that should be wrapped in an `@layer`)', async () => { expect( - await migrateContents( - css` - @charset "UTF-8"; - @layer foo, bar, baz; - /**! My license comment */ - html { - color: red; - } - @tailwind base; - @tailwind components; - @tailwind utilities; - `, - {}, - ), + await migrate(css` + @charset "UTF-8"; + @layer foo, bar, baz; + /**! My license comment */ + html { + color: red; + } + @tailwind base; + @tailwind components; + @tailwind utilities; + `), ).toMatchInlineSnapshot(` "@charset "UTF-8"; @layer foo, bar, baz; @@ -157,20 +152,17 @@ it('should migrate a stylesheet (with preceding rules that should be wrapped in it('should keep CSS as-is before existing `@layer` at-rules', async () => { expect( - await migrateContents( - css` - .foo { - color: blue; - } + await migrate(css` + .foo { + color: blue; + } - @layer components { - .bar { - color: red; - } + @layer components { + .bar { + color: red; } - `, - {}, - ), + } + `), ).toMatchInlineSnapshot(` ".foo { color: blue; diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index 573c13f48..e85b55895 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -1,11 +1,20 @@ #!/usr/bin/env node import { globby } from 'globby' +import fs from 'node:fs/promises' import path from 'node:path' +import postcss from 'postcss' import type { Config } from 'tailwindcss' import type { DesignSystem } from '../../tailwindcss/src/design-system' +import { formatNodes } from './codemods/format-nodes' import { help } from './commands/help' -import { migrate as migrateStylesheet } from './migrate' +import { + analyze as analyzeStylesheets, + migrate as migrateStylesheet, + split as splitStylesheets, + type MigrateOptions, +} from './migrate' +import { Stylesheet } from './stylesheet' import { migrate as migrateTemplate } from './template/migrate' import { parseConfig } from './template/parseConfig' import { args, type Arg } from './utils/args' @@ -109,17 +118,63 @@ async function run() { // Ensure we are only dealing with CSS files files = files.filter((file) => file.endsWith('.css')) + // Analyze the stylesheets + let loadResults = await Promise.allSettled(files.map((filepath) => Stylesheet.load(filepath))) + + // Load and parse all stylesheets + for (let result of loadResults) { + if (result.status === 'rejected') { + error(`${result.reason}`) + } + } + + let stylesheets = loadResults + .filter((result) => result.status === 'fulfilled') + .map((result) => result.value) + + // Analyze the stylesheets + try { + await analyzeStylesheets(stylesheets) + } catch (e: unknown) { + error(`${e}`) + } + // Migrate each file - await Promise.allSettled( - files.map((file) => - migrateStylesheet(file, { - newPrefix: parsedConfig?.newPrefix ?? undefined, - designSystem: parsedConfig?.designSystem, - userConfig: parsedConfig?.userConfig, - }), - ), + let options: MigrateOptions = { + newPrefix: parsedConfig?.newPrefix ?? undefined, + designSystem: parsedConfig?.designSystem, + userConfig: parsedConfig?.userConfig, + } + + let migrateResults = await Promise.allSettled( + stylesheets.map((sheet) => migrateStylesheet(sheet, options)), ) + for (let result of migrateResults) { + if (result.status === 'rejected') { + error(`${result.reason}`) + } + } + + // Split up stylesheets (as needed) + try { + await splitStylesheets(stylesheets) + } catch (e: unknown) { + error(`${e}`) + } + + // Format nodes + for (let sheet of stylesheets) { + await postcss([formatNodes()]).process(sheet.root!, { from: sheet.file! }) + } + + // Write all files to disk + for (let sheet of stylesheets) { + if (!sheet.file) continue + + await fs.writeFile(sheet.file, sheet.root.toString()) + } + success('Stylesheet migration complete.') } diff --git a/packages/@tailwindcss-upgrade/src/migrate.ts b/packages/@tailwindcss-upgrade/src/migrate.ts index 680e7c67b..b1d5b5f13 100644 --- a/packages/@tailwindcss-upgrade/src/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/migrate.ts @@ -1,13 +1,16 @@ -import fs from 'node:fs/promises' import path from 'node:path' import postcss from 'postcss' import type { Config } from 'tailwindcss' import type { DesignSystem } from '../../tailwindcss/src/design-system' -import { formatNodes } from './codemods/format-nodes' +import { DefaultMap } from '../../tailwindcss/src/utils/default-map' +import { segment } from '../../tailwindcss/src/utils/segment' import { migrateAtApply } from './codemods/migrate-at-apply' import { migrateAtLayerUtilities } from './codemods/migrate-at-layer-utilities' import { migrateMissingLayers } from './codemods/migrate-missing-layers' import { migrateTailwindDirectives } from './codemods/migrate-tailwind-directives' +import { Stylesheet, type StylesheetId } from './stylesheet' +import { resolveCssId } from './utils/resolve' +import { walk, WalkAction } from './utils/walk' export interface MigrateOptions { newPrefix?: string @@ -15,20 +18,342 @@ export interface MigrateOptions { userConfig?: Config } -export async function migrateContents(contents: string, options: MigrateOptions, file?: string) { +export async function migrateContents( + stylesheet: Stylesheet | string, + options: MigrateOptions = {}, +) { + if (typeof stylesheet === 'string') { + stylesheet = await Stylesheet.fromString(stylesheet) + } + return postcss() .use(migrateAtApply(options)) - .use(migrateAtLayerUtilities()) + .use(migrateAtLayerUtilities(stylesheet)) .use(migrateMissingLayers()) .use(migrateTailwindDirectives(options)) - .use(formatNodes()) - .process(contents, { from: file }) - .then((result) => result.css) + .process(stylesheet.root, { from: stylesheet.file ?? undefined }) } -export async function migrate(file: string, options: MigrateOptions) { - let fullPath = path.resolve(process.cwd(), file) - let contents = await fs.readFile(fullPath, 'utf-8') +export async function migrate(stylesheet: Stylesheet, options: MigrateOptions) { + if (!stylesheet.file) { + throw new Error('Cannot migrate a stylesheet without a file path') + } - await fs.writeFile(fullPath, await migrateContents(contents, options, fullPath)) + await migrateContents(stylesheet, options) } + +export async function analyze(stylesheets: Stylesheet[]) { + let stylesheetsByFile = new Map