From 6760672b0caa191bd8f9e532370f5c2522127f9b Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 13 Sep 2024 16:02:23 +0200 Subject: [PATCH 1/5] init `@tailwindcss/upgrade` --- packages/@tailwindcss-upgrade/README.md | 40 ++++++++++++++++++++ packages/@tailwindcss-upgrade/package.json | 35 +++++++++++++++++ packages/@tailwindcss-upgrade/src/index.ts | 7 ++++ packages/@tailwindcss-upgrade/tsconfig.json | 3 ++ packages/@tailwindcss-upgrade/tsup.config.ts | 8 ++++ pnpm-lock.yaml | 13 +++++++ 6 files changed, 106 insertions(+) create mode 100644 packages/@tailwindcss-upgrade/README.md create mode 100644 packages/@tailwindcss-upgrade/package.json create mode 100644 packages/@tailwindcss-upgrade/src/index.ts create mode 100644 packages/@tailwindcss-upgrade/tsconfig.json create mode 100644 packages/@tailwindcss-upgrade/tsup.config.ts diff --git a/packages/@tailwindcss-upgrade/README.md b/packages/@tailwindcss-upgrade/README.md new file mode 100644 index 000000000000..95ec9d87ddcc --- /dev/null +++ b/packages/@tailwindcss-upgrade/README.md @@ -0,0 +1,40 @@ +

+ + + + + Tailwind CSS + + +

+ +

+ A utility-first CSS framework for rapidly building custom user interfaces. +

+ +

+ Build Status + Total Downloads + Latest Release + License +

+ +--- + +## Documentation + +For full documentation, visit [tailwindcss.com](https://tailwindcss.com). + +## Community + +For help, discussion about best practices, or any other conversation that would benefit from being searchable: + +[Discuss Tailwind CSS on GitHub](https://github.com/tailwindcss/tailwindcss/discussions) + +For chatting with others using the framework: + +[Join the Tailwind CSS Discord Server](https://discord.gg/7NF8GNe) + +## Contributing + +If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindcss/tailwindcss/blob/next/.github/CONTRIBUTING.md) **before submitting a pull request**. diff --git a/packages/@tailwindcss-upgrade/package.json b/packages/@tailwindcss-upgrade/package.json new file mode 100644 index 000000000000..c2ff7e4c9d17 --- /dev/null +++ b/packages/@tailwindcss-upgrade/package.json @@ -0,0 +1,35 @@ +{ + "name": "@tailwindcss/upgrade", + "version": "4.0.0-alpha.24", + "description": "A utility-first CSS framework for rapidly building custom user interfaces.", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/tailwindlabs/tailwindcss.git", + "directory": "packages/@tailwindcss-cli" + }, + "bugs": "https://github.com/tailwindlabs/tailwindcss/issues", + "homepage": "https://tailwindcss.com", + "scripts": { + "lint": "tsc --noEmit", + "build": "tsup-node", + "dev": "pnpm run build -- --watch" + }, + "exports": { + "./package.json": "./package.json" + }, + "files": [ + "dist" + ], + "publishConfig": { + "provenance": true, + "access": "public" + }, + "dependencies": { + "postcss-import": "^16.1.0", + "postcss": "^8.4.41" + }, + "devDependencies": { + "@types/postcss-import": "^14.0.3" + } +} diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts new file mode 100644 index 000000000000..5fc4f5cd8dd1 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -0,0 +1,7 @@ +#!/usr/bin/env node + +console.log({ + argv: process.argv, +}) + +process.exit(0) diff --git a/packages/@tailwindcss-upgrade/tsconfig.json b/packages/@tailwindcss-upgrade/tsconfig.json new file mode 100644 index 000000000000..6ae022f65bf0 --- /dev/null +++ b/packages/@tailwindcss-upgrade/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../tsconfig.base.json", +} diff --git a/packages/@tailwindcss-upgrade/tsup.config.ts b/packages/@tailwindcss-upgrade/tsup.config.ts new file mode 100644 index 000000000000..915692c12afe --- /dev/null +++ b/packages/@tailwindcss-upgrade/tsup.config.ts @@ -0,0 +1,8 @@ +import {defineConfig} from 'tsup' + +export default defineConfig({ + format: ['esm'], + clean: true, + minify: true, + entry: ['src/index.ts'], +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 08d91bab8c94..8220caf54b10 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -281,6 +281,19 @@ importers: specifier: ^1.25.1 version: 1.26.0 + packages/@tailwindcss-upgrade: + dependencies: + postcss: + specifier: ^8.4.41 + version: 8.4.41 + postcss-import: + specifier: ^16.1.0 + version: 16.1.0(postcss@8.4.41) + devDependencies: + '@types/postcss-import': + specifier: ^14.0.3 + version: 14.0.3 + packages/@tailwindcss-vite: dependencies: '@tailwindcss/node': From 3d508f2d1072671ceaffb195d2a81694e2089e9d Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 16 Sep 2024 11:38:12 +0200 Subject: [PATCH 2/5] add codemod tooling + add codemod for migrating `@apply` --- packages/@tailwindcss-upgrade/package.json | 12 +++- .../src/codemods/migrate-at-apply.test.ts | 70 +++++++++++++++++++ .../src/codemods/migrate-at-apply.ts | 43 ++++++++++++ .../@tailwindcss-upgrade/src/index.test.ts | 28 ++++++++ packages/@tailwindcss-upgrade/src/index.ts | 54 ++++++++++++-- packages/@tailwindcss-upgrade/src/migrate.ts | 36 ++++++++++ pnpm-lock.yaml | 70 ++++++++++++++++--- 7 files changed, 295 insertions(+), 18 deletions(-) create mode 100644 packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.test.ts create mode 100644 packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.ts create mode 100644 packages/@tailwindcss-upgrade/src/index.test.ts create mode 100644 packages/@tailwindcss-upgrade/src/migrate.ts diff --git a/packages/@tailwindcss-upgrade/package.json b/packages/@tailwindcss-upgrade/package.json index c2ff7e4c9d17..de7120e053c5 100644 --- a/packages/@tailwindcss-upgrade/package.json +++ b/packages/@tailwindcss-upgrade/package.json @@ -15,6 +15,9 @@ "build": "tsup-node", "dev": "pnpm run build -- --watch" }, + "bin": { + "tailwindcss-upgrade": "./dist/index.mjs" + }, "exports": { "./package.json": "./package.json" }, @@ -26,10 +29,13 @@ "access": "public" }, "dependencies": { - "postcss-import": "^16.1.0", - "postcss": "^8.4.41" + "picocolors": "^1.0.1", + "postcss": "^8.4.41", + "postcss-import": "^16.1.0" }, "devDependencies": { - "@types/postcss-import": "^14.0.3" + "@types/node": "catalog:", + "@types/postcss-import": "^14.0.3", + "dedent": "1.5.3" } } diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.test.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.test.ts new file mode 100644 index 000000000000..f270472d1e65 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.test.ts @@ -0,0 +1,70 @@ +import dedent from 'dedent' +import postcss from 'postcss' +import { expect, it } from 'vitest' +import { migrateAtApply } from './migrate-at-apply' + +const css = dedent + +function migrate(input: string) { + return postcss() + .use(migrateAtApply()) + .process(input, { from: expect.getState().testPath }) + .then((result) => result.css) +} + +it('should not migrate `@apply`, when there are no issues', async () => { + expect( + await migrate(css` + .foo { + @apply flex flex-col items-center; + } + `), + ).toMatchInlineSnapshot(` + ".foo { + @apply flex flex-col items-center; + }" + `) +}) + +it('should append `!` to each utility, when using `!important`', async () => { + expect( + await migrate(css` + .foo { + @apply flex flex-col !important; + } + `), + ).toMatchInlineSnapshot(` + ".foo { + @apply flex! flex-col!; + }" + `) +}) + +// TODO: Handle SCSS syntax +it.skip('should append `!` to each utility, when using `#{!important}`', async () => { + expect( + await migrate(css` + .foo { + @apply flex flex-col #{!important}; + } + `), + ).toMatchInlineSnapshot(` + ".foo { + @apply flex! flex-col!; + }" + `) +}) + +it('should move the legacy `!` prefix, to the new `!` postfix notation', async () => { + expect( + await migrate(css` + .foo { + @apply !flex flex-col! hover:!items-start items-center; + } + `), + ).toMatchInlineSnapshot(` + ".foo { + @apply flex! flex-col! hover:items-start! items-center; + }" + `) +}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.ts new file mode 100644 index 000000000000..41ff03ec3323 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.ts @@ -0,0 +1,43 @@ +import type { AtRule, Plugin } from 'postcss' +import { segment } from '../../../tailwindcss/src/utils/segment' + +export function migrateAtApply(): Plugin { + function migrate(atRule: AtRule) { + let utilities = atRule.params.split(/(\s+)/) + let important = + utilities[utilities.length - 1] === '!important' || + utilities[utilities.length - 1] === '#{!important}' // Sass/SCSS + + if (important) utilities.pop() // Remove `!important` + + let params = utilities.map((part) => { + // Keep whitespace + if (part.trim() === '') return part + + let variants = segment(part, ':') + let utility = variants.pop()! + + // Apply the important modifier to all the rules if necessary + if (important && utility[0] !== '!' && utility[utility.length - 1] !== '!') { + utility += '!' + } + + // Migrate the important modifier to the end of the utility + if (utility[0] === '!') { + utility = `${utility.slice(1)}!` + } + + // Reconstruct the utility with the variants + return [...variants, utility].join(':') + }) + + atRule.params = params.join('').trim() + } + + return { + postcssPlugin: '@tailwindcss/upgrade/migrate-at-apply', + AtRule: { + apply: migrate, + }, + } +} diff --git a/packages/@tailwindcss-upgrade/src/index.test.ts b/packages/@tailwindcss-upgrade/src/index.test.ts new file mode 100644 index 000000000000..a043092d9ae6 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/index.test.ts @@ -0,0 +1,28 @@ +import dedent from 'dedent' +import { expect, it } from 'vitest' +import { migrateContents } from './migrate' + +const css = dedent + +it('should print the input as-is', async () => { + expect( + await migrateContents( + css` + /* above */ + .foo/* after */ { + /* above */ + color: /* before */ red /* after */; + /* below */ + } + `, + expect.getState().testPath, + ), + ).toMatchInlineSnapshot(` + "/* above */ + .foo/* after */ { + /* above */ + color: /* before */ red /* after */; + /* below */ + }" + `) +}) diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index 5fc4f5cd8dd1..aa3c4563f741 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -1,7 +1,53 @@ #!/usr/bin/env node -console.log({ - argv: process.argv, -}) +import { execSync } from 'node:child_process' +import path from 'node:path' +import pc from 'picocolors' +import { help } from '../../@tailwindcss-cli/src/commands/help' +import { args, type Arg } from '../../@tailwindcss-cli/src/utils/args' +import { eprintln, header, highlight, wordWrap } from '../../@tailwindcss-cli/src/utils/renderer' +import { migrate } from './migrate' -process.exit(0) +const options = { + '--help': { type: 'boolean', description: 'Display usage information', alias: '-h' }, + '--force': { type: 'boolean', description: 'Force the migration', alias: '-f' }, + '--version': { type: 'boolean', description: 'Display the version number', alias: '-v' }, +} satisfies Arg +const flags = args(options) + +if (flags['--help']) { + help({ + usage: ['npx @tailwindcss/upgrade'], + options, + }) + process.exit(0) +} + +const file = flags._[0] + +async function run() { + eprintln(header()) + eprintln() + + if (!flags['--force']) { + let stdout = execSync('git status --porcelain', { encoding: 'utf-8' }) + if (stdout.trim()) { + wordWrap( + 'Git directory is not clean. Please stash or commit your changes before migrating.', + process.stderr.columns - 5 - 4, + ).map((line) => eprintln(`${pc.red('\u2502')} ${line}`)) + wordWrap( + `You may use the ${highlight('--force')} flag to override this safety check.`, + process.stderr.columns - 2 - 4, + ).map((line) => eprintln(`${pc.red('\u2502')} ${line}`)) + eprintln() + process.exit(1) + } + } + + await migrate(path.resolve(process.cwd(), file)) +} + +run() + .then(() => process.exit(0)) + .catch(() => process.exit(1)) diff --git a/packages/@tailwindcss-upgrade/src/migrate.ts b/packages/@tailwindcss-upgrade/src/migrate.ts new file mode 100644 index 000000000000..a8ac2e7a5dc5 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/migrate.ts @@ -0,0 +1,36 @@ +import { execSync } from 'node:child_process' +import fs from 'node:fs/promises' +import path from 'node:path' +import pc from 'picocolors' +import postcss from 'postcss' +import { eprintln, wordWrap } from '../../@tailwindcss-cli/src/utils/renderer' +import { migrateAtApply } from './codemods/migrate-at-apply' + +export async function migrateContents(contents: string, file?: string) { + return postcss() + .use(migrateAtApply()) + .process(contents, { from: file }) + .then((result) => result.css) +} + +export async function migrate(file: string) { + let fullPath = path.resolve(process.cwd(), file) + let contents = await fs.readFile(fullPath, 'utf-8') + + await fs.writeFile(fullPath, await migrateContents(contents, fullPath)) + + let stdout = execSync('git status --porcelain', { encoding: 'utf-8' }) + if (stdout.trim()) { + wordWrap( + 'Migration complete. Verify the changes and commit them to your repository.', + process.stderr.columns - 5 - 4, + ).map((line) => eprintln(`${pc.green('\u2502')} ${line}`)) + eprintln() + } else { + wordWrap( + 'Migration complete. No changes were made to your repository.', + process.stderr.columns - 5 - 4, + ).map((line) => eprintln(`${pc.green('\u2502')} ${line}`)) + eprintln() + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8220caf54b10..b834f84ec0e4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -283,6 +283,9 @@ importers: packages/@tailwindcss-upgrade: dependencies: + picocolors: + specifier: ^1.0.1 + version: 1.0.1 postcss: specifier: ^8.4.41 version: 8.4.41 @@ -290,9 +293,15 @@ importers: specifier: ^16.1.0 version: 16.1.0(postcss@8.4.41) devDependencies: + '@types/node': + specifier: 'catalog:' + version: 20.14.13 '@types/postcss-import': specifier: ^14.0.3 version: 14.0.3 + dedent: + specifier: 1.5.3 + version: 1.5.3 packages/@tailwindcss-vite: dependencies: @@ -404,7 +413,7 @@ importers: version: link:../../packages/@tailwindcss-vite '@vitejs/plugin-react': specifier: ^4.3.1 - version: 4.3.1(vite@5.4.0(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6)) + version: 4.3.1(vite@5.4.0(@types/node@22.5.4)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6)) react: specifier: ^18.3.1 version: 18.3.1 @@ -426,10 +435,10 @@ importers: version: 1.1.22 vite: specifier: 'catalog:' - version: 5.4.0(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6) + version: 5.4.0(@types/node@22.5.4)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6) vite-plugin-handlebars: specifier: ^2.0.0 - version: 2.0.0(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6) + version: 2.0.0(@types/node@22.5.4)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6) packages: @@ -1034,6 +1043,7 @@ packages: '@parcel/watcher-darwin-arm64@2.4.2-alpha.0': resolution: {integrity: sha512-2xH4Ve7OKjIh+4YRfTN3HGJa2W8KTPLOALHZj5fxcbTPwaVxdpIRItDrcikUx2u3AzGAFme7F+AZZXHnf0F15Q==} engines: {node: '>= 10.0.0'} + cpu: [arm64] os: [darwin] '@parcel/watcher-darwin-x64@2.4.1': @@ -1045,6 +1055,7 @@ packages: '@parcel/watcher-darwin-x64@2.4.2-alpha.0': resolution: {integrity: sha512-xtjmXUH4YZVah5+7Q0nb+fpRP5qZn9cFfuPuZ4k77UfUGVwhacgZyIRQgIOwMP3GkgW4TsrKQaw1KIe7L1ZqcQ==} engines: {node: '>= 10.0.0'} + cpu: [x64] os: [darwin] '@parcel/watcher-freebsd-x64@2.4.1': @@ -1068,6 +1079,7 @@ packages: '@parcel/watcher-linux-arm64-glibc@2.4.2-alpha.0': resolution: {integrity: sha512-vIIOcZf+fgsRReIK3Fw0WINvGo9UwiXfisnqYRzfpNByRZvkEPkGTIVe8iiDp72NhPTVmwIvBqM6yKDzIaw8GQ==} engines: {node: '>= 10.0.0'} + cpu: [arm64] os: [linux] '@parcel/watcher-linux-arm64-musl@2.4.1': @@ -1079,6 +1091,7 @@ packages: '@parcel/watcher-linux-arm64-musl@2.4.2-alpha.0': resolution: {integrity: sha512-gXqEAoLG9bBCbQNUgqjSOxHcjpmCZmYT9M8UvrdTMgMYgXgiWcR8igKlPRd40mCIRZSkMpN2ScSy2WjQ0bQZnQ==} engines: {node: '>= 10.0.0'} + cpu: [arm64] os: [linux] '@parcel/watcher-linux-x64-glibc@2.4.1': @@ -1090,6 +1103,7 @@ packages: '@parcel/watcher-linux-x64-glibc@2.4.2-alpha.0': resolution: {integrity: sha512-/WJJ3Y46ubwQW+Z+mzpzK3pvqn/AT7MA63NB0+k9GTLNxJQZNREensMtpJ/FJ+LVIiraEHTY22KQrsx9+DeNbw==} engines: {node: '>= 10.0.0'} + cpu: [x64] os: [linux] '@parcel/watcher-linux-x64-musl@2.4.1': @@ -1101,6 +1115,7 @@ packages: '@parcel/watcher-linux-x64-musl@2.4.2-alpha.0': resolution: {integrity: sha512-1dz4fTM5HaANk3RSRmdhALT+bNqTHawVDL1D77HwV/FuF/kSjlM3rGrJuFaCKwQ5E8CInHCcobqMN8Jh8LYaRg==} engines: {node: '>= 10.0.0'} + cpu: [x64] os: [linux] '@parcel/watcher-win32-arm64@2.4.1': @@ -1124,6 +1139,7 @@ packages: '@parcel/watcher-win32-x64@2.4.2-alpha.0': resolution: {integrity: sha512-U2abMKF7JUiIxQkos19AvTLFcnl2Xn8yIW1kzu+7B0Lux4Gkuu/BUDBroaM1s6+hwgK63NOLq9itX2Y3GwUThg==} engines: {node: '>= 10.0.0'} + cpu: [x64] os: [win32] '@parcel/watcher@2.4.1': @@ -1252,6 +1268,9 @@ packages: '@types/node@20.14.13': resolution: {integrity: sha512-+bHoGiZb8UiQ0+WEtmph2IWQCjIqg8MDZMAV+ppRRhUZnquF5mQkP/9vpSwJClEiSM/C7fZZExPzfU0vJTyp8w==} + '@types/node@22.5.4': + resolution: {integrity: sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==} + '@types/postcss-import@14.0.3': resolution: {integrity: sha512-raZhRVTf6Vw5+QbmQ7LOHSDML71A5rj4+EqDzAbrZPfxfoGzFxMHRCq16VlddGIZpHELw0BG4G0YE2ANkdZiIQ==} @@ -1455,11 +1474,13 @@ packages: bun@1.1.22: resolution: {integrity: sha512-G2HCPhzhjDc2jEDkZsO9vwPlpHrTm7a8UVwx9oNS5bZqo5OcSK5GPuWYDWjj7+37bRk5OVLfeIvUMtSrbKeIjQ==} + cpu: [arm64, x64] os: [darwin, linux, win32] hasBin: true bun@1.1.26: resolution: {integrity: sha512-dWSewAqE7sVbYmflJxgG47dW4vmsbar7VAnQ4ao45y3ulr3n7CwdsMLFnzd28jhPRtF+rsaVK2y4OLIkP3OD4A==} + cpu: [arm64, x64] os: [darwin, linux, win32] hasBin: true @@ -2222,11 +2243,13 @@ packages: lightningcss-darwin-arm64@1.26.0: resolution: {integrity: sha512-n4TIvHO1NY1ondKFYpL2ZX0bcC2y6yjXMD6JfyizgR8BCFNEeArINDzEaeqlfX9bXz73Bpz/Ow0nu+1qiDrBKg==} engines: {node: '>= 12.0.0'} + cpu: [arm64] os: [darwin] lightningcss-darwin-x64@1.26.0: resolution: {integrity: sha512-Rf9HuHIDi1R6/zgBkJh25SiJHF+dm9axUZW/0UoYCW1/8HV0gMI0blARhH4z+REmWiU1yYT/KyNF3h7tHyRXUg==} engines: {node: '>= 12.0.0'} + cpu: [x64] os: [darwin] lightningcss-freebsd-x64@1.26.0: @@ -2244,21 +2267,25 @@ packages: lightningcss-linux-arm64-gnu@1.26.0: resolution: {integrity: sha512-iJmZM7fUyVjH+POtdiCtExG+67TtPUTer7K/5A8DIfmPfrmeGvzfRyBltGhQz13Wi15K1lf2cPYoRaRh6vcwNA==} engines: {node: '>= 12.0.0'} + cpu: [arm64] os: [linux] lightningcss-linux-arm64-musl@1.26.0: resolution: {integrity: sha512-XxoEL++tTkyuvu+wq/QS8bwyTXZv2y5XYCMcWL45b8XwkiS8eEEEej9BkMGSRwxa5J4K+LDeIhLrS23CpQyfig==} engines: {node: '>= 12.0.0'} + cpu: [arm64] os: [linux] lightningcss-linux-x64-gnu@1.26.0: resolution: {integrity: sha512-1dkTfZQAYLj8MUSkd6L/+TWTG8V6Kfrzfa0T1fSlXCXQHrt1HC1/UepXHtKHDt/9yFwyoeayivxXAsApVxn6zA==} engines: {node: '>= 12.0.0'} + cpu: [x64] os: [linux] lightningcss-linux-x64-musl@1.26.0: resolution: {integrity: sha512-yX3Rk9m00JGCUzuUhFEojY+jf/6zHs3XU8S8Vk+FRbnr4St7cjyMXdNjuA2LjiT8e7j8xHRCH8hyZ4H/btRE4A==} engines: {node: '>= 12.0.0'} + cpu: [x64] os: [linux] lightningcss-win32-arm64-msvc@1.26.0: @@ -2270,6 +2297,7 @@ packages: lightningcss-win32-x64-msvc@1.26.0: resolution: {integrity: sha512-pYS3EyGP3JRhfqEFYmfFDiZ9/pVNfy8jVIYtrx9TVNusVyDK3gpW1w/rbvroQ4bDJi7grdUtyrYU6V2xkY/bBw==} engines: {node: '>= 12.0.0'} + cpu: [x64] os: [win32] lightningcss@1.26.0: @@ -3013,6 +3041,9 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + update-browserslist-db@1.1.0: resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==} hasBin: true @@ -3749,6 +3780,10 @@ snapshots: dependencies: undici-types: 5.26.5 + '@types/node@22.5.4': + dependencies: + undici-types: 6.19.8 + '@types/postcss-import@14.0.3': dependencies: postcss: 8.4.41 @@ -3766,7 +3801,7 @@ snapshots: '@types/ws@8.5.12': dependencies: - '@types/node': 20.14.13 + '@types/node': 22.5.4 '@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4)': dependencies: @@ -3810,14 +3845,14 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vitejs/plugin-react@4.3.1(vite@5.4.0(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6))': + '@vitejs/plugin-react@4.3.1(vite@5.4.0(@types/node@22.5.4)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6))': dependencies: '@babel/core': 7.25.2 '@babel/plugin-transform-react-jsx-self': 7.24.7(@babel/core@7.25.2) '@babel/plugin-transform-react-jsx-source': 7.24.7(@babel/core@7.25.2) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 5.4.0(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6) + vite: 5.4.0(@types/node@22.5.4)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6) transitivePeerDependencies: - supports-color @@ -4383,7 +4418,7 @@ snapshots: eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-react: 7.35.0(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) @@ -4407,7 +4442,7 @@ snapshots: enhanced-resolve: 5.17.1 eslint: 8.57.0 eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.6 is-core-module: 2.15.0 @@ -4429,7 +4464,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -5772,6 +5807,8 @@ snapshots: undici-types@5.26.5: {} + undici-types@6.19.8: {} + update-browserslist-db@1.1.0(browserslist@4.23.2): dependencies: browserslist: 4.23.2 @@ -5800,10 +5837,10 @@ snapshots: - supports-color - terser - vite-plugin-handlebars@2.0.0(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6): + vite-plugin-handlebars@2.0.0(@types/node@22.5.4)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6): dependencies: handlebars: 4.7.8 - vite: 5.4.0(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6) + vite: 5.4.0(@types/node@22.5.4)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6) transitivePeerDependencies: - '@types/node' - less @@ -5825,6 +5862,17 @@ snapshots: lightningcss: 1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4) terser: 5.31.6 + vite@5.4.0(@types/node@22.5.4)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6): + dependencies: + esbuild: 0.21.5 + postcss: 8.4.41 + rollup: 4.20.0 + optionalDependencies: + '@types/node': 22.5.4 + fsevents: 2.3.3 + lightningcss: 1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4) + terser: 5.31.6 + vitest@2.0.5(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6): dependencies: '@ampproject/remapping': 2.3.0 From e2eb586a15b1196e404ef74cd60a627cf24ecc04 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 16 Sep 2024 12:15:21 +0200 Subject: [PATCH 3/5] copy utils from `@tailwindcss/cli` Copied some utils from `@tailwindcss/cli`. Initially referenced them, but we also rely on some dependencies that we have to install. One solution is to move it to an internal package and import it so that tsup can inline everything. This also has the same 'we need the correct dependencies' problem. Another solution is to create a proper `@tailwindcss/cli-utils` (or similar) package that we can import as a dependency. However, this one will require us to actually publish the package, and give it a proper name. --- packages/@tailwindcss-upgrade/package.json | 2 + .../src/commands/help/index.ts | 170 ++++++++++++++++++ packages/@tailwindcss-upgrade/src/index.ts | 6 +- packages/@tailwindcss-upgrade/src/migrate.ts | 2 +- .../src/utils/args.test.ts | 123 +++++++++++++ .../@tailwindcss-upgrade/src/utils/args.ts | 160 +++++++++++++++++ .../src/utils/format-ns.test.ts | 28 +++ .../src/utils/format-ns.ts | 23 +++ .../src/utils/renderer.ts | 102 +++++++++++ .../@tailwindcss-upgrade/src/utils/resolve.ts | 32 ++++ pnpm-lock.yaml | 6 + 11 files changed, 650 insertions(+), 4 deletions(-) create mode 100644 packages/@tailwindcss-upgrade/src/commands/help/index.ts create mode 100644 packages/@tailwindcss-upgrade/src/utils/args.test.ts create mode 100644 packages/@tailwindcss-upgrade/src/utils/args.ts create mode 100644 packages/@tailwindcss-upgrade/src/utils/format-ns.test.ts create mode 100644 packages/@tailwindcss-upgrade/src/utils/format-ns.ts create mode 100644 packages/@tailwindcss-upgrade/src/utils/renderer.ts create mode 100644 packages/@tailwindcss-upgrade/src/utils/resolve.ts diff --git a/packages/@tailwindcss-upgrade/package.json b/packages/@tailwindcss-upgrade/package.json index de7120e053c5..2f0791088c51 100644 --- a/packages/@tailwindcss-upgrade/package.json +++ b/packages/@tailwindcss-upgrade/package.json @@ -29,6 +29,8 @@ "access": "public" }, "dependencies": { + "enhanced-resolve": "^5.17.1", + "mri": "^1.2.0", "picocolors": "^1.0.1", "postcss": "^8.4.41", "postcss-import": "^16.1.0" diff --git a/packages/@tailwindcss-upgrade/src/commands/help/index.ts b/packages/@tailwindcss-upgrade/src/commands/help/index.ts new file mode 100644 index 000000000000..20ee78208440 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/commands/help/index.ts @@ -0,0 +1,170 @@ +import pc from 'picocolors' +import type { Arg } from '../../utils/args' +import { UI, header, highlight, indent, println, wordWrap } from '../../utils/renderer' + +export function help({ + invalid, + usage, + options, +}: { + invalid?: string + usage?: string[] + options?: Arg +}) { + // Available terminal width + let width = process.stdout.columns + + // Render header + println(header()) + + // Render the invalid command + if (invalid) { + println() + println(`${pc.dim('Invalid command:')} ${invalid}`) + } + + // Render usage + if (usage && usage.length > 0) { + println() + println(pc.dim('Usage:')) + for (let [idx, example] of usage.entries()) { + // Split the usage example into the command and its options. This allows + // us to wrap the options based on the available width of the terminal. + let command = example.slice(0, example.indexOf('[')) + let options = example.slice(example.indexOf('[')) + + // Make the options dimmed, to make them stand out less than the command + // itself. + options = options.replace(/\[.*?\]/g, (option) => pc.dim(option)) + + // The space between the command and the options. + let space = 1 + + // Wrap the options based on the available width of the terminal. + let lines = wordWrap(options, width - UI.indent - command.length - space) + + // Print an empty line between the usage examples if we need to split due + // to width constraints. This ensures that the usage examples are visually + // separated. + // + // E.g.: when enough space is available + // + // ``` + // Usage: + // tailwindcss build [--input input.css] [--output output.css] [--watch] [options...] + // tailwindcss other [--watch] [options...] + // ``` + // + // E.g.: when not enough space is available + // + // ``` + // Usage: + // tailwindcss build [--input input.css] [--output output.css] + // [--watch] [options...] + // + // tailwindcss other [--watch] [options...] + // ``` + if (lines.length > 1 && idx !== 0) { + println() + } + + // Print the usage examples based on available width of the terminal. + // + // E.g.: when enough space is available + // + // ``` + // Usage: + // tailwindcss [--input input.css] [--output output.css] [--watch] [options...] + // ``` + // + // E.g.: when not enough space is available + // + // ``` + // Usage: + // tailwindcss [--input input.css] [--output output.css] + // [--watch] [options...] + // ``` + // + // > Note how the second line is indented to align with the first line. + println(indent(`${command}${lines.shift()}`)) + for (let line of lines) { + println(indent(line, command.length)) + } + } + } + + // Render options + if (options) { + // Track the max alias length, this is used to indent the options that don't + // have an alias such that everything is aligned properly. + let maxAliasLength = 0 + for (let { alias } of Object.values(options)) { + if (alias) { + maxAliasLength = Math.max(maxAliasLength, alias.length) + } + } + + // The option strings, which are the combination of the `alias` and the + // `flag`, with the correct spacing. + let optionStrings: string[] = [] + + // Track the max option length, which is the longest combination of an + // `alias` followed by `, ` and followed by the `flag`. + let maxOptionLength = 0 + + for (let [flag, { alias }] of Object.entries(options)) { + // The option string, which is the combination of the alias and the flag + // but already properly indented based on the other aliases to ensure + // everything is aligned properly. + let option = [ + alias ? `${alias.padStart(maxAliasLength)}` : alias, + alias ? flag : ' '.repeat(maxAliasLength + 2 /* `, `.length */) + flag, + ] + .filter(Boolean) + .join(', ') + + optionStrings.push(option) + maxOptionLength = Math.max(maxOptionLength, option.length) + } + + println() + println(pc.dim('Options:')) + + // The minimum amount of dots between the option and the description. + let minimumGap = 8 + + for (let { description, default: defaultValue = null } of Object.values(options)) { + // The option to render + let option = optionStrings.shift() as string + + // The amount of dots to show between the option and the description. + let dotCount = minimumGap + (maxOptionLength - option.length) + + // To account for the space before and after the dots. + let spaces = 2 + + // The available width remaining for the description. + let availableWidth = width - option.length - dotCount - spaces - UI.indent + + // Wrap the description and the default value (if present), based on the + // available width. + let lines = wordWrap( + defaultValue !== null + ? `${description} ${pc.dim(`[default:\u202F${highlight(`${defaultValue}`)}]`)}` + : description, + availableWidth, + ) + + // Print the option, the spacer dots and the start of the description. + println( + indent(`${pc.blue(option)} ${pc.dim(pc.gray('\u00B7')).repeat(dotCount)} ${lines.shift()}`), + ) + + // Print the remaining lines of the description, indenting them to align + // with the start of the description. + for (let line of lines) { + println(indent(`${' '.repeat(option.length + dotCount + spaces)}${line}`)) + } + } + } +} diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index aa3c4563f741..e6fbbc1040df 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -3,10 +3,10 @@ import { execSync } from 'node:child_process' import path from 'node:path' import pc from 'picocolors' -import { help } from '../../@tailwindcss-cli/src/commands/help' -import { args, type Arg } from '../../@tailwindcss-cli/src/utils/args' -import { eprintln, header, highlight, wordWrap } from '../../@tailwindcss-cli/src/utils/renderer' +import { help } from './commands/help' import { migrate } from './migrate' +import { args, type Arg } from './utils/args' +import { eprintln, header, highlight, wordWrap } from './utils/renderer' const options = { '--help': { type: 'boolean', description: 'Display usage information', alias: '-h' }, diff --git a/packages/@tailwindcss-upgrade/src/migrate.ts b/packages/@tailwindcss-upgrade/src/migrate.ts index a8ac2e7a5dc5..d0cf71d12247 100644 --- a/packages/@tailwindcss-upgrade/src/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/migrate.ts @@ -3,8 +3,8 @@ import fs from 'node:fs/promises' import path from 'node:path' import pc from 'picocolors' import postcss from 'postcss' -import { eprintln, wordWrap } from '../../@tailwindcss-cli/src/utils/renderer' import { migrateAtApply } from './codemods/migrate-at-apply' +import { eprintln, wordWrap } from './utils/renderer' export async function migrateContents(contents: string, file?: string) { return postcss() diff --git a/packages/@tailwindcss-upgrade/src/utils/args.test.ts b/packages/@tailwindcss-upgrade/src/utils/args.test.ts new file mode 100644 index 000000000000..51d2c4747241 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/utils/args.test.ts @@ -0,0 +1,123 @@ +import { expect, it } from 'vitest' +import { args, type Arg } from './args' + +it('should be possible to parse a single argument', () => { + expect( + args( + { + '--input': { type: 'string', description: 'Input file' }, + }, + ['--input', 'input.css'], + ), + ).toMatchInlineSnapshot(` + { + "--input": "input.css", + "_": [], + } + `) +}) + +it('should fallback to the default value if no flag is passed', () => { + expect( + args( + { + '--input': { type: 'string', description: 'Input file', default: 'input.css' }, + }, + ['--other'], + ), + ).toMatchInlineSnapshot(` + { + "--input": "input.css", + "_": [], + } + `) +}) + +it('should fallback to null if no flag is passed and no default value is provided', () => { + expect( + args( + { + '--input': { type: 'string', description: 'Input file' }, + }, + ['--other'], + ), + ).toMatchInlineSnapshot(` + { + "--input": null, + "_": [], + } + `) +}) + +it('should be possible to parse a single argument using the shorthand alias', () => { + expect( + args( + { + '--input': { type: 'string', description: 'Input file', alias: '-i' }, + }, + ['-i', 'input.css'], + ), + ).toMatchInlineSnapshot(` + { + "--input": "input.css", + "_": [], + } + `) +}) + +it('should convert the incoming value to the correct type', () => { + expect( + args( + { + '--input': { type: 'string', description: 'Input file' }, + '--watch': { type: 'boolean', description: 'Watch mode' }, + '--retries': { type: 'number', description: 'Amount of retries' }, + }, + ['--input', 'input.css', '--watch', '--retries', '3'], + ), + ).toMatchInlineSnapshot(` + { + "--input": "input.css", + "--retries": 3, + "--watch": true, + "_": [], + } + `) +}) + +it('should be possible to provide multiple types, and convert the value to that type', () => { + let options = { + '--retries': { type: 'boolean | number | string', description: 'Retries' }, + } satisfies Arg + + expect(args(options, ['--retries'])).toMatchInlineSnapshot(` + { + "--retries": true, + "_": [], + } + `) + expect(args(options, ['--retries', 'true'])).toMatchInlineSnapshot(` + { + "--retries": true, + "_": [], + } + `) + expect(args(options, ['--retries', 'false'])).toMatchInlineSnapshot(` + { + "--retries": false, + "_": [], + } + `) + expect(args(options, ['--retries', '5'])).toMatchInlineSnapshot(` + { + "--retries": 5, + "_": [], + } + `) + expect(args(options, ['--retries', 'indefinitely'])).toMatchInlineSnapshot(` + { + "--retries": "indefinitely", + "_": [], + } + `) +}) diff --git a/packages/@tailwindcss-upgrade/src/utils/args.ts b/packages/@tailwindcss-upgrade/src/utils/args.ts new file mode 100644 index 000000000000..81bd847d50d1 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/utils/args.ts @@ -0,0 +1,160 @@ +import parse from 'mri' + +// Definition of the arguments for a command in the CLI. +export type Arg = { + [key: `--${string}`]: { + type: keyof Types + description: string + alias?: `-${string}` + default?: Types[keyof Types] + } +} + +// Each argument will have a type and we want to convert the incoming raw string +// based value to the correct type. We can't use pure TypeScript types because +// these don't exist at runtime. Instead, we define a string-based type that +// maps to a TypeScript type. +type Types = { + boolean: boolean + number: number | null + string: string | null + 'boolean | string': boolean | string | null + 'number | string': number | string | null + 'boolean | number': boolean | number | null + 'boolean | number | string': boolean | number | string | null +} + +// Convert the `Arg` type to a type that can be used at runtime. +// +// E.g.: +// +// Arg: +// ``` +// { '--input': { type: 'string', description: 'Input file', alias: '-i' } } +// ``` +// +// Command: +// ``` +// ./tailwindcss -i input.css +// ./tailwindcss --input input.css +// ``` +// +// Result type: +// ``` +// { +// _: string[], // All non-flag arguments +// '--input': string | null // The `--input` flag will be filled with `null`, if the flag is not used. +// // The `null` type will not be there if `default` is provided. +// } +// ``` +// +// Result runtime object: +// ``` +// { +// _: [], +// '--input': 'input.css' +// } +// ``` +export type Result = { + [K in keyof T]: T[K] extends { type: keyof Types; default?: any } + ? undefined extends T[K]['default'] + ? Types[T[K]['type']] + : NonNullable + : never +} & { + // All non-flag arguments + _: string[] +} + +export function args(options: T, argv = process.argv.slice(2)): Result { + let parsed = parse(argv) + + let result: { _: string[]; [key: string]: unknown } = { + _: parsed._, + } + + for (let [ + flag, + { type, alias, default: defaultValue = type === 'boolean' ? false : null }, + ] of Object.entries(options)) { + // Start with the default value + result[flag] = defaultValue + + // Try to find the `alias`, and map it to long form `flag` + if (alias) { + let key = alias.slice(1) + if (parsed[key] !== undefined) { + result[flag] = convert(parsed[key], type) + } + } + + // Try to find the long form `flag` + { + let key = flag.slice(2) + if (parsed[key] !== undefined) { + result[flag] = convert(parsed[key], type) + } + } + } + + return result as Result +} + +// --- + +type ArgumentType = string | boolean + +// Try to convert the raw incoming `value` (which will be a string or a boolean, +// this is coming from `mri`'s parse function'), to the correct type based on +// the `type` of the argument. +function convert(value: string | boolean, type: T) { + switch (type) { + case 'string': + return convertString(value) + case 'boolean': + return convertBoolean(value) + case 'number': + return convertNumber(value) + case 'boolean | string': + return convertBoolean(value) ?? convertString(value) + case 'number | string': + return convertNumber(value) ?? convertString(value) + case 'boolean | number': + return convertBoolean(value) ?? convertNumber(value) + case 'boolean | number | string': + return convertBoolean(value) ?? convertNumber(value) ?? convertString(value) + default: + throw new Error(`Unhandled type: ${type}`) + } +} + +function convertBoolean(value: ArgumentType) { + if (value === true || value === false) { + return value + } + + if (value === 'true') { + return true + } + + if (value === 'false') { + return false + } +} + +function convertNumber(value: ArgumentType) { + if (typeof value === 'number') { + return value + } + + { + let valueAsNumber = Number(value) + if (!Number.isNaN(valueAsNumber)) { + return valueAsNumber + } + } +} + +function convertString(value: ArgumentType) { + return `${value}` +} diff --git a/packages/@tailwindcss-upgrade/src/utils/format-ns.test.ts b/packages/@tailwindcss-upgrade/src/utils/format-ns.test.ts new file mode 100644 index 000000000000..a5382d21772e --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/utils/format-ns.test.ts @@ -0,0 +1,28 @@ +import { expect, it } from 'vitest' +import { formatNanoseconds } from './format-ns' + +it.each([ + [0, '0ns'], + [1, '1ns'], + [999, '999ns'], + [1000, '1µs'], + [1001, '1µs'], + [999999, '999µs'], + [1000000, '1ms'], + [1000001, '1ms'], + [999999999, '999ms'], + [1000000000, '1s'], + [1000000001, '1s'], + [59999999999, '59s'], + [60000000000, '1m'], + [60000000001, '1m'], + [3599999999999n, '59m'], + [3600000000000n, '1h'], + [3600000000001n, '1h'], + [86399999999999n, '23h'], + [86400000000000n, '1d'], + [86400000000001n, '1d'], + [8640000000000000n, '100d'], +])('should format %s nanoseconds as %s', (ns, expected) => { + expect(formatNanoseconds(ns)).toBe(expected) +}) diff --git a/packages/@tailwindcss-upgrade/src/utils/format-ns.ts b/packages/@tailwindcss-upgrade/src/utils/format-ns.ts new file mode 100644 index 000000000000..39889d401149 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/utils/format-ns.ts @@ -0,0 +1,23 @@ +export function formatNanoseconds(input: bigint | number) { + let ns = typeof input === 'number' ? BigInt(input) : input + + if (ns < 1_000n) return `${ns}ns` + ns /= 1_000n + + if (ns < 1_000n) return `${ns}µs` + ns /= 1_000n + + if (ns < 1_000n) return `${ns}ms` + ns /= 1_000n + + if (ns < 60n) return `${ns}s` + ns /= 60n + + if (ns < 60n) return `${ns}m` + ns /= 60n + + if (ns < 24n) return `${ns}h` + ns /= 24n + + return `${ns}d` +} diff --git a/packages/@tailwindcss-upgrade/src/utils/renderer.ts b/packages/@tailwindcss-upgrade/src/utils/renderer.ts new file mode 100644 index 000000000000..6b00985866cb --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/utils/renderer.ts @@ -0,0 +1,102 @@ +import fs from 'node:fs' +import path from 'node:path' +import { stripVTControlCharacters } from 'node:util' +import pc from 'picocolors' +import { resolve } from '../utils/resolve' +import { formatNanoseconds } from './format-ns' + +export const UI = { + indent: 2, +} +export function header() { + return `${pc.italic(pc.bold(pc.blue('\u2248')))} tailwindcss ${pc.blue(`v${getVersion()}`)}` +} + +export function highlight(file: string) { + return `${pc.dim(pc.blue('`'))}${pc.blue(file)}${pc.dim(pc.blue('`'))}` +} + +/** + * Convert an `absolute` path to a `relative` path from the current working + * directory. + */ +export function relative( + to: string, + from = process.cwd(), + { preferAbsoluteIfShorter = true } = {}, +) { + let result = path.relative(from, to) + if (!result.startsWith('..')) { + result = `.${path.sep}${result}` + } + + if (preferAbsoluteIfShorter && result.length > to.length) { + return to + } + + return result +} + +/** + * Wrap `text` into multiple lines based on the `width`. + */ +export function wordWrap(text: string, width: number) { + let words = text.split(' ') + let lines = [] + + let line = '' + let lineLength = 0 + for (let word of words) { + let wordLength = stripVTControlCharacters(word).length + + if (lineLength + wordLength + 1 > width) { + lines.push(line) + line = '' + lineLength = 0 + } + + line += (lineLength ? ' ' : '') + word + lineLength += wordLength + (lineLength ? 1 : 0) + } + + if (lineLength) { + lines.push(line) + } + + return lines +} + +/** + * Format a duration in nanoseconds to a more human readable format. + */ +export function formatDuration(ns: bigint) { + let formatted = formatNanoseconds(ns) + + if (ns <= 50 * 1e6) return pc.green(formatted) + if (ns <= 300 * 1e6) return pc.blue(formatted) + if (ns <= 1000 * 1e6) return pc.yellow(formatted) + + return pc.red(formatted) +} + +export function indent(value: string, offset = 0) { + return `${' '.repeat(offset + UI.indent)}${value}` +} + +// Rust inspired functions to print to the console: + +export function eprintln(value = '') { + process.stderr.write(`${value}\n`) +} + +export function println(value = '') { + process.stdout.write(`${value}\n`) +} + +function getVersion(): string { + if (typeof globalThis.__tw_version === 'string') { + return globalThis.__tw_version + } + let { version } = JSON.parse(fs.readFileSync(resolve('tailwindcss/package.json'), 'utf-8')) + return version +} diff --git a/packages/@tailwindcss-upgrade/src/utils/resolve.ts b/packages/@tailwindcss-upgrade/src/utils/resolve.ts new file mode 100644 index 000000000000..e9197e8741ab --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/utils/resolve.ts @@ -0,0 +1,32 @@ +import EnhancedResolve from 'enhanced-resolve' +import fs from 'node:fs' +import { createRequire } from 'node:module' + +const localResolve = createRequire(import.meta.url).resolve +export function resolve(id: string) { + if (typeof globalThis.__tw_resolve === 'function') { + let resolved = globalThis.__tw_resolve(id) + if (resolved) { + return resolved + } + } + return localResolve(id) +} + +const resolver = EnhancedResolve.ResolverFactory.createResolver({ + fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000), + useSyncFileSystemCalls: true, + extensions: ['.css'], + mainFields: ['style'], + conditionNames: ['style'], +}) +export function resolveCssId(id: string, base: string) { + if (typeof globalThis.__tw_resolve === 'function') { + let resolved = globalThis.__tw_resolve(id, base) + if (resolved) { + return resolved + } + } + + return resolver.resolveSync({}, base, id) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b834f84ec0e4..ef1cbb2a5b26 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -283,6 +283,12 @@ importers: packages/@tailwindcss-upgrade: dependencies: + enhanced-resolve: + specifier: ^5.17.1 + version: 5.17.1 + mri: + specifier: ^1.2.0 + version: 1.2.0 picocolors: specifier: ^1.0.1 version: 1.0.1 From 64850e0ebdc4ec517175ed6220b47b52f0130137 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 16 Sep 2024 12:42:02 +0200 Subject: [PATCH 4/5] migrate passed in files (or discover files) --- packages/@tailwindcss-upgrade/package.json | 4 +- packages/@tailwindcss-upgrade/src/index.ts | 47 ++++++++++++++++++-- packages/@tailwindcss-upgrade/src/migrate.ts | 18 -------- packages/@tailwindcss-upgrade/tsup.config.ts | 2 +- pnpm-lock.yaml | 6 +++ 5 files changed, 53 insertions(+), 24 deletions(-) diff --git a/packages/@tailwindcss-upgrade/package.json b/packages/@tailwindcss-upgrade/package.json index 2f0791088c51..95c3ed453dd1 100644 --- a/packages/@tailwindcss-upgrade/package.json +++ b/packages/@tailwindcss-upgrade/package.json @@ -30,10 +30,12 @@ }, "dependencies": { "enhanced-resolve": "^5.17.1", + "fast-glob": "^3.3.2", "mri": "^1.2.0", "picocolors": "^1.0.1", "postcss": "^8.4.41", - "postcss-import": "^16.1.0" + "postcss-import": "^16.1.0", + "tailwindcss": "workspace:^" }, "devDependencies": { "@types/node": "catalog:", diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index e6fbbc1040df..592c67ed36b0 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -1,5 +1,6 @@ #!/usr/bin/env node +import fastGlob from 'fast-glob' import { execSync } from 'node:child_process' import path from 'node:path' import pc from 'picocolors' @@ -23,8 +24,6 @@ if (flags['--help']) { process.exit(0) } -const file = flags._[0] - async function run() { eprintln(header()) eprintln() @@ -45,9 +44,49 @@ async function run() { } } - await migrate(path.resolve(process.cwd(), file)) + // Use provided files + let files = flags._.map((file) => path.resolve(process.cwd(), file)) + + // Discover CSS files in case no files were provided + if (files.length === 0) { + wordWrap( + 'No files provided. Searching for CSS files in the current directory and its subdirectories…', + process.stderr.columns - 5 - 4, + ).map((line) => eprintln(`${pc.blue('\u2502')} ${line}`)) + eprintln() + + files = await fastGlob(['**/*.css'], { + absolute: true, + ignore: ['**/node_modules', '**/vendor'], + }) + } + + // Ensure we are only dealing with CSS files + files = files.filter((file) => file.endsWith('.css')) + + // Migrate each file + await Promise.allSettled(files.map((file) => migrate(file))) + + // Figure out if we made any changes + let stdout = execSync('git status --porcelain', { encoding: 'utf-8' }) + if (stdout.trim()) { + wordWrap( + 'Migration complete. Verify the changes and commit them to your repository.', + process.stderr.columns - 5 - 4, + ).map((line) => eprintln(`${pc.green('\u2502')} ${line}`)) + eprintln() + } else { + wordWrap( + 'Migration complete. No changes were made to your repository.', + process.stderr.columns - 5 - 4, + ).map((line) => eprintln(`${pc.green('\u2502')} ${line}`)) + eprintln() + } } run() .then(() => process.exit(0)) - .catch(() => process.exit(1)) + .catch((err) => { + console.error(err) + process.exit(1) + }) diff --git a/packages/@tailwindcss-upgrade/src/migrate.ts b/packages/@tailwindcss-upgrade/src/migrate.ts index d0cf71d12247..68e0d336034a 100644 --- a/packages/@tailwindcss-upgrade/src/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/migrate.ts @@ -1,10 +1,7 @@ -import { execSync } from 'node:child_process' import fs from 'node:fs/promises' import path from 'node:path' -import pc from 'picocolors' import postcss from 'postcss' import { migrateAtApply } from './codemods/migrate-at-apply' -import { eprintln, wordWrap } from './utils/renderer' export async function migrateContents(contents: string, file?: string) { return postcss() @@ -18,19 +15,4 @@ export async function migrate(file: string) { let contents = await fs.readFile(fullPath, 'utf-8') await fs.writeFile(fullPath, await migrateContents(contents, fullPath)) - - let stdout = execSync('git status --porcelain', { encoding: 'utf-8' }) - if (stdout.trim()) { - wordWrap( - 'Migration complete. Verify the changes and commit them to your repository.', - process.stderr.columns - 5 - 4, - ).map((line) => eprintln(`${pc.green('\u2502')} ${line}`)) - eprintln() - } else { - wordWrap( - 'Migration complete. No changes were made to your repository.', - process.stderr.columns - 5 - 4, - ).map((line) => eprintln(`${pc.green('\u2502')} ${line}`)) - eprintln() - } } diff --git a/packages/@tailwindcss-upgrade/tsup.config.ts b/packages/@tailwindcss-upgrade/tsup.config.ts index 915692c12afe..7d82eee2c882 100644 --- a/packages/@tailwindcss-upgrade/tsup.config.ts +++ b/packages/@tailwindcss-upgrade/tsup.config.ts @@ -1,4 +1,4 @@ -import {defineConfig} from 'tsup' +import { defineConfig } from 'tsup' export default defineConfig({ format: ['esm'], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef1cbb2a5b26..ba22273e2f77 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -286,6 +286,9 @@ importers: enhanced-resolve: specifier: ^5.17.1 version: 5.17.1 + fast-glob: + specifier: ^3.3.2 + version: 3.3.2 mri: specifier: ^1.2.0 version: 1.2.0 @@ -298,6 +301,9 @@ importers: postcss-import: specifier: ^16.1.0 version: 16.1.0(postcss@8.4.41) + tailwindcss: + specifier: workspace:^ + version: link:../tailwindcss devDependencies: '@types/node': specifier: 'catalog:' From 64ac8889f90ae39e9fd98498d5c93f2c55910f75 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 16 Sep 2024 12:50:52 +0200 Subject: [PATCH 5/5] add `error`, `info`, `success`, and `warn` helpers --- packages/@tailwindcss-upgrade/src/index.ts | 32 ++++--------------- .../src/utils/renderer.ts | 28 ++++++++++++++++ 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts index 592c67ed36b0..19513d1cbe54 100644 --- a/packages/@tailwindcss-upgrade/src/index.ts +++ b/packages/@tailwindcss-upgrade/src/index.ts @@ -3,11 +3,10 @@ import fastGlob from 'fast-glob' import { execSync } from 'node:child_process' import path from 'node:path' -import pc from 'picocolors' import { help } from './commands/help' import { migrate } from './migrate' import { args, type Arg } from './utils/args' -import { eprintln, header, highlight, wordWrap } from './utils/renderer' +import { eprintln, error, header, highlight, info, success } from './utils/renderer' const options = { '--help': { type: 'boolean', description: 'Display usage information', alias: '-h' }, @@ -31,15 +30,8 @@ async function run() { if (!flags['--force']) { let stdout = execSync('git status --porcelain', { encoding: 'utf-8' }) if (stdout.trim()) { - wordWrap( - 'Git directory is not clean. Please stash or commit your changes before migrating.', - process.stderr.columns - 5 - 4, - ).map((line) => eprintln(`${pc.red('\u2502')} ${line}`)) - wordWrap( - `You may use the ${highlight('--force')} flag to override this safety check.`, - process.stderr.columns - 2 - 4, - ).map((line) => eprintln(`${pc.red('\u2502')} ${line}`)) - eprintln() + error('Git directory is not clean. Please stash or commit your changes before migrating.') + info(`You may use the ${highlight('--force')} flag to override this safety check.`) process.exit(1) } } @@ -49,11 +41,9 @@ async function run() { // Discover CSS files in case no files were provided if (files.length === 0) { - wordWrap( + info( 'No files provided. Searching for CSS files in the current directory and its subdirectories…', - process.stderr.columns - 5 - 4, - ).map((line) => eprintln(`${pc.blue('\u2502')} ${line}`)) - eprintln() + ) files = await fastGlob(['**/*.css'], { absolute: true, @@ -70,17 +60,9 @@ async function run() { // Figure out if we made any changes let stdout = execSync('git status --porcelain', { encoding: 'utf-8' }) if (stdout.trim()) { - wordWrap( - 'Migration complete. Verify the changes and commit them to your repository.', - process.stderr.columns - 5 - 4, - ).map((line) => eprintln(`${pc.green('\u2502')} ${line}`)) - eprintln() + success('Migration complete. Verify the changes and commit them to your repository.') } else { - wordWrap( - 'Migration complete. No changes were made to your repository.', - process.stderr.columns - 5 - 4, - ).map((line) => eprintln(`${pc.green('\u2502')} ${line}`)) - eprintln() + success('Migration complete. No changes were made to your repository.') } } diff --git a/packages/@tailwindcss-upgrade/src/utils/renderer.ts b/packages/@tailwindcss-upgrade/src/utils/renderer.ts index 6b00985866cb..b9b9245f7b04 100644 --- a/packages/@tailwindcss-upgrade/src/utils/renderer.ts +++ b/packages/@tailwindcss-upgrade/src/utils/renderer.ts @@ -83,6 +83,34 @@ export function indent(value: string, offset = 0) { return `${' '.repeat(offset + UI.indent)}${value}` } +export function success(message: string, print = eprintln) { + wordWrap(message, process.stderr.columns - 3).map((line) => { + return print(`${pc.green('\u2502')} ${line}`) + }) + print() +} + +export function info(message: string, print = eprintln) { + wordWrap(message, process.stderr.columns - 3).map((line) => { + return print(`${pc.blue('\u2502')} ${line}`) + }) + print() +} + +export function error(message: string, print = eprintln) { + wordWrap(message, process.stderr.columns - 3).map((line) => { + return print(`${pc.red('\u2502')} ${line}`) + }) + print() +} + +export function warn(message: string, print = eprintln) { + wordWrap(message, process.stderr.columns - 3).map((line) => { + return print(`${pc.yellow('\u2502')} ${line}`) + }) + print() +} + // Rust inspired functions to print to the console: export function eprintln(value = '') {