From be3a52ec2813b211de1d550e054ce9353715b349 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 11 Oct 2024 14:17:50 +0200 Subject: [PATCH 01/14] Detect static plugins in the JS config and migrate it to CSS --- integrations/upgrade/js-config.test.ts | 16 +- packages/@tailwindcss-upgrade/package.json | 4 +- .../src/codemods/migrate-config.ts | 12 +- .../src/migrate-js-config.ts | 18 +- .../src/utils/extract-static-imports.test.ts | 89 +++++++++ .../src/utils/extract-static-imports.ts | 173 ++++++++++++++++++ packages/@tailwindcss-upgrade/tsconfig.json | 3 + pnpm-lock.yaml | 41 +++++ 8 files changed, 350 insertions(+), 6 deletions(-) create mode 100644 packages/@tailwindcss-upgrade/src/utils/extract-static-imports.test.ts create mode 100644 packages/@tailwindcss-upgrade/src/utils/extract-static-imports.ts diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index 347e08e93063..45248b8a1b97 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -8,6 +8,7 @@ test( 'package.json': json` { "dependencies": { + "@tailwindcss/typography": "^0.5.15", "@tailwindcss/upgrade": "workspace:^" } } @@ -15,8 +16,10 @@ test( 'tailwind.config.ts': ts` import { type Config } from 'tailwindcss' import defaultTheme from 'tailwindcss/defaultTheme' + import typography from '@tailwindcss/typography' + import customPlugin from './custom-plugin' - module.exports = { + export default { darkMode: 'selector', content: ['./src/**/*.{html,js}', './my-app/**/*.{html,js}'], theme: { @@ -50,9 +53,15 @@ test( }, }, }, - plugins: [], + plugins: [typography, customPlugin], } satisfies Config `, + 'custom-plugin.js': ts` + export default function ({ addVariant }) { + addVariant('inverted', '@media (inverted-colors: inverted)') + addVariant('hocus', ['&:focus', '&:hover']) + } + `, 'src/input.css': css` @tailwind base; @tailwind components; @@ -71,6 +80,9 @@ test( @source './**/*.{html,js}'; @source '../my-app/**/*.{html,js}'; + @plugin '@tailwindcss/typography'; + @plugin '../custom-plugin'; + @variant dark (&:where(.dark, .dark *)); @theme { diff --git a/packages/@tailwindcss-upgrade/package.json b/packages/@tailwindcss-upgrade/package.json index 7e0425be41a5..048a470a7368 100644 --- a/packages/@tailwindcss-upgrade/package.json +++ b/packages/@tailwindcss-upgrade/package.json @@ -39,7 +39,9 @@ "postcss-selector-parser": "^6.1.2", "prettier": "^3.3.3", "string-byte-slice": "^3.0.0", - "tailwindcss": "workspace:^" + "tailwindcss": "workspace:^", + "tree-sitter": "^0.21.1", + "tree-sitter-typescript": "^0.23.0" }, "devDependencies": { "@types/node": "catalog:", diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts index 1aadec96623d..178122f1d05e 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-config.ts @@ -55,11 +55,21 @@ export function migrateConfig( let absolute = path.resolve(source.base, source.pattern) css += `@source '${relativeToStylesheet(sheet, absolute)}';\n` } - if (jsConfigMigration.sources.length > 0) { css = css + '\n' } + for (let plugin of jsConfigMigration.plugins) { + let relative = + plugin.path[0] === '.' + ? relativeToStylesheet(sheet, path.resolve(plugin.base, plugin.path)) + : plugin.path + css += `@plugin '${relative}';\n` + } + if (jsConfigMigration.plugins.length > 0) { + css = css + '\n' + } + cssConfig.append(postcss.parse(css + jsConfigMigration.css)) } diff --git a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts index 18e6712c8884..2a7c4bef0844 100644 --- a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts +++ b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts @@ -12,6 +12,7 @@ import { deepMerge } from '../../tailwindcss/src/compat/config/deep-merge' import { mergeThemeExtension } from '../../tailwindcss/src/compat/config/resolve-config' import type { ThemeConfig } from '../../tailwindcss/src/compat/config/types' import { darkModePlugin } from '../../tailwindcss/src/compat/dark-mode' +import { findSimplePlugins } from './utils/extract-static-imports' import { info } from './utils/renderer' const __filename = fileURLToPath(import.meta.url) @@ -21,6 +22,7 @@ export type JSConfigMigration = // Could not convert the config file, need to inject it as-is in a @config directive null | { sources: { base: string; pattern: string }[] + plugins: { base: string; path: string }[] css: string } @@ -41,6 +43,7 @@ export async function migrateJsConfig( } let sources: { base: string; pattern: string }[] = [] + let plugins: { base: string; path: string }[] = [] let cssConfigs: string[] = [] if ('darkMode' in unresolvedConfig) { @@ -56,8 +59,17 @@ export async function migrateJsConfig( if (themeConfig) cssConfigs.push(themeConfig) } + let simplePlugins = findSimplePlugins(source) + console.log(simplePlugins) + if (simplePlugins !== null) { + for (let plugin of simplePlugins) { + plugins.push({ base, path: plugin }) + } + } + return { sources, + plugins, css: cssConfigs.join('\n'), } } @@ -168,7 +180,9 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean { return ['string', 'number', 'boolean', 'undefined'].includes(typeof value) } - if (!isSimpleValue(unresolvedConfig)) { + // Plugins can be complex, we have a special heuristics for them! + let { plugins, ...remainder } = unresolvedConfig + if (!isSimpleValue(remainder)) { return false } @@ -186,7 +200,7 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean { return false } - if (unresolvedConfig.plugins && unresolvedConfig.plugins.length > 0) { + if (findSimplePlugins(source) === null) { return false } diff --git a/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.test.ts b/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.test.ts new file mode 100644 index 000000000000..62a20808a8c7 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.test.ts @@ -0,0 +1,89 @@ +import dedent from 'dedent' +import { expect, test } from 'vitest' +import { extractStaticImportMap, findSimplePlugins } from './extract-static-imports' + +const js = dedent + +test('extracts different kind of imports from a source file', () => { + let extracted = extractStaticImportMap(js` + import plugin1 from './plugin1' + // import * as plugin2 from './plugin2' + import plugin6, { plugin3, plugin4, default as plugin5 } from './plugin3' + // import * as plugin7, { plugin8, foo as plugin9 } from './plugin7' + + // const plugin6 = require('plugin6') + // const {plugin7} = require('plugin7') + // const {foo: plugin8} = require('plugin8') + // let plugin9 = require('plugin9') + // let {plugin10} = require('plugin10') + // let {foo: plugin11} = require('plugin11') + // let plugin12 = require('plugin12') + // let {plugin13} = require('plugin13') + // let {foo: plugin14} = require('plugin14') + `) + + expect(extracted).toEqual({ + plugin1: { module: './plugin1', export: null }, + // plugin2: { module: './plugin2', export: 'plugin2' }, + plugin3: { module: './plugin3', export: 'plugin3' }, + plugin4: { module: './plugin3', export: 'plugin4' }, + plugin5: { module: './plugin3', export: 'default' }, + plugin6: { module: './plugin3', export: null }, + + // plugin6: { module: 'plugin6', export: null }, + // plugin7: { module: 'plugin7', export: 'plugin7' }, + // plugin8: { module: 'plugin8', export: 'foo' }, + // plugin9: { module: 'plugin9', export: null }, + // plugin10: { module: 'plugin10', export: 'plugin10' }, + // plugin11: { module: 'plugin11', export: 'foo' }, + // plugin12: { module: 'plugin12', export: null }, + // plugin13: { module: 'plugin13', export: 'plugin13' }, + // plugin14: { module: 'plugin14', export: 'foo' }, + }) +}) + +test('find simple plugins', () => { + expect( + findSimplePlugins(js` + import plugin1 from './plugin1' + + export default { + plugins: [plugin1, 'plugin2'] + } + `), + ).toEqual(['./plugin1', 'plugin2']) + + expect( + findSimplePlugins(js` + import plugin1 from './plugin1' + + export default { + plugins: [plugin1, () => {} ] + } + `), + ).toEqual(null) + + expect( + findSimplePlugins(js` + import {plugin1} from './plugin1' + + export default { + plugins: [plugin1] + } + `), + ).toEqual(null) + + expect( + findSimplePlugins(js` + export default { + plugins: [] + } + `), + ).toEqual([]) + + expect( + findSimplePlugins(js` + export default {} + `), + ).toEqual([]) +}) diff --git a/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.ts b/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.ts new file mode 100644 index 000000000000..65495778cf09 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.ts @@ -0,0 +1,173 @@ +import Parser from 'tree-sitter' +import TS from 'tree-sitter-typescript' + +console.log({ TS }) +let parser = new Parser() +parser.setLanguage(TS.typescript) +const treesitter = String.raw + +const IMPORT_QUERY = new Parser.Query( + TS.typescript, + treesitter` + ; import foo, {foo as bar} from './foo' + (import_statement + (import_clause + (identifier)? @default + (named_imports + (import_specifier + name: (identifier) @imported-name + alias: (identifier)? @imported-alias + ) + )? + ) + (string + (string_fragment) @imported-from) + )`, + + // ; import * as foo from './foo' + // (import_statement + // (import_clause + // (namespace_import + // (identifier) @imported-name)) + // (string + // (string_fragment) @imported-from) + // ) + // `, +) + +export function extractStaticImportMap(source: string) { + let tree = parser.parse(source) + let root = tree.rootNode + + let captures = IMPORT_QUERY.matches(root) + + let imports: Record = {} + for (let match of captures) { + let toImport: { name: string; source: null | string }[] = [] + let from = '' + for (let i = 0; i < match.captures.length; i++) { + let capture = match.captures[i] + + switch (capture.name) { + case 'default': + toImport.push({ name: capture.node.text, source: null }) + break + case 'imported-name': + toImport.push({ name: capture.node.text, source: capture.node.text }) + break + case 'imported-from': + from = capture.node.text + break + case 'imported-alias': + if (toImport.length < 1) { + throw new Error() + } + let prevImport = toImport[toImport.length - 1] + let name = prevImport.name + prevImport.source = name + prevImport.name = capture.node.text + break + } + } + + for (let { name, source } of toImport) { + imports[name] = { module: from, export: source } + } + + // console.log({ imported, from }) + + // console.log(match.captures.map((c) => ({ name: c.name, text: c.node.text }))) + // console.log('+++++++++++++++++++++++++==') + // let name = match.captures.find((c) => c.name === 'imported-name') + // let from = match.captures.find((c) => c.name === 'imported-from') + + // if (!name || !from) continue + + // imports[name.node.text] = { module: from.node.text, export: null } + } + + return imports +} + +const PLUGINS_QUERY = new Parser.Query( + TS.typescript, + treesitter` + (export_statement + value: (satisfies_expression (object + (pair + key: (property_identifier) @name (#eq? @name "plugins") + value: (array) @imports + ) + ))? + value: (as_expression (object + (pair + key: (property_identifier) @name (#eq? @name "plugins") + value: (array) @imports + ) + ))? + value: (object + (pair + key: (property_identifier) @name (#eq? @name "plugins") + value: (array) @imports + ) + )? + ) + `, +) +export function findSimplePlugins(source: string): string[] | null { + try { + let tree = parser.parse(source) + let root = tree.rootNode + + let imports = extractStaticImportMap(source) + let captures = PLUGINS_QUERY.matches(root) + + console.dir({ captures }, { depth: 4 }) + + let plugins = [] + for (let match of captures) { + for (let capture of match.captures) { + if (capture.name !== 'imports') continue + + for (let pluginDefinition of capture.node.children) { + if ( + pluginDefinition.type === '[' || + pluginDefinition.type === ']' || + pluginDefinition.type === ',' + ) + continue + + switch (pluginDefinition.type) { + case 'identifier': + let source = imports[pluginDefinition.text] + if (!source || source.export !== null) { + return null + } + plugins.push(source.module) + break + case 'string': + plugins.push(pluginDefinition.children[1].text) + break + default: + return null + } + } + } + + // console.log({ imported, from }) + + // console.log(match.captures.map((c) => ({ name: c.name, text: c.node.text }))) + // console.log('+++++++++++++++++++++++++==') + // let name = match.captures.find((c) => c.name === 'imported-name') + // let from = match.captures.find((c) => c.name === 'imported-from') + + // if (!name || !from) continue + + // imports[name.node.text] = { module: from.node.text, export: null } + } + return plugins + } catch (error) { + console.error(error) + return null + } +} diff --git a/packages/@tailwindcss-upgrade/tsconfig.json b/packages/@tailwindcss-upgrade/tsconfig.json index 6ae022f65bf0..b7ac105af223 100644 --- a/packages/@tailwindcss-upgrade/tsconfig.json +++ b/packages/@tailwindcss-upgrade/tsconfig.json @@ -1,3 +1,6 @@ { "extends": "../tsconfig.base.json", + "compilerOptions": { + "allowSyntheticDefaultImports":true + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1651527e5b8b..da24cf908079 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -312,6 +312,12 @@ importers: tailwindcss: specifier: workspace:^ version: link:../tailwindcss + tree-sitter: + specifier: ^0.21.1 + version: 0.21.1 + tree-sitter-typescript: + specifier: ^0.23.0 + version: 0.23.0(tree-sitter@0.21.1) devDependencies: '@types/node': specifier: 'catalog:' @@ -2384,6 +2390,14 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-addon-api@8.1.0: + resolution: {integrity: sha512-yBY+qqWSv3dWKGODD6OGE6GnTX7Q2r+4+DfpqxHSHh8x0B4EKP9+wVGLS6U/AM1vxSNNmUEuIV5EGhYwPpfOwQ==} + engines: {node: ^18 || ^20 || >= 21} + + node-gyp-build@4.8.2: + resolution: {integrity: sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw==} + hasBin: true + node-releases@2.0.18: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} @@ -2917,6 +2931,18 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + tree-sitter-typescript@0.23.0: + resolution: {integrity: sha512-hRy5O9d+9ON4HxIWWxkI4zonrw2v/WNN1JoiGW5HkXfC9K2R3p53ugMvs6Vs4T7ASCwggsoQ75LNdgpExC/zgQ==} + peerDependencies: + tree-sitter: ^0.21.0 + tree_sitter: '*' + peerDependenciesMeta: + tree_sitter: + optional: true + + tree-sitter@0.21.1: + resolution: {integrity: sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==} + ts-api-utils@1.3.0: resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} engines: {node: '>=16'} @@ -5145,6 +5171,10 @@ snapshots: node-addon-api@7.1.1: {} + node-addon-api@8.1.0: {} + + node-gyp-build@4.8.2: {} + node-releases@2.0.18: {} normalize-path@3.0.0: {} @@ -5677,6 +5707,17 @@ snapshots: tree-kill@1.2.2: {} + tree-sitter-typescript@0.23.0(tree-sitter@0.21.1): + dependencies: + node-addon-api: 8.1.0 + node-gyp-build: 4.8.2 + tree-sitter: 0.21.1 + + tree-sitter@0.21.1: + dependencies: + node-addon-api: 8.1.0 + node-gyp-build: 4.8.2 + ts-api-utils@1.3.0(typescript@5.5.4): dependencies: typescript: 5.5.4 From dc793f176897dbaa94ab3b18e0cfc2c02e79b76f Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 11 Oct 2024 17:50:36 +0200 Subject: [PATCH 02/14] Parse all ESM import types --- .../src/utils/extract-static-imports.test.ts | 25 +++----- .../src/utils/extract-static-imports.ts | 61 ++++++------------- 2 files changed, 29 insertions(+), 57 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.test.ts b/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.test.ts index 62a20808a8c7..6f1a3dada681 100644 --- a/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.test.ts +++ b/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.test.ts @@ -4,31 +4,23 @@ import { extractStaticImportMap, findSimplePlugins } from './extract-static-impo const js = dedent -test('extracts different kind of imports from a source file', () => { +test('extracts different kind of imports from an ESM file', () => { let extracted = extractStaticImportMap(js` import plugin1 from './plugin1' - // import * as plugin2 from './plugin2' + import * as plugin2 from './plugin2' import plugin6, { plugin3, plugin4, default as plugin5 } from './plugin3' - // import * as plugin7, { plugin8, foo as plugin9 } from './plugin7' - - // const plugin6 = require('plugin6') - // const {plugin7} = require('plugin7') - // const {foo: plugin8} = require('plugin8') - // let plugin9 = require('plugin9') - // let {plugin10} = require('plugin10') - // let {foo: plugin11} = require('plugin11') - // let plugin12 = require('plugin12') - // let {plugin13} = require('plugin13') - // let {foo: plugin14} = require('plugin14') + import plugin8, * as plugin7 from './plugin7' `) expect(extracted).toEqual({ plugin1: { module: './plugin1', export: null }, - // plugin2: { module: './plugin2', export: 'plugin2' }, + plugin2: { module: './plugin2', export: '*' }, plugin3: { module: './plugin3', export: 'plugin3' }, plugin4: { module: './plugin3', export: 'plugin4' }, plugin5: { module: './plugin3', export: 'default' }, plugin6: { module: './plugin3', export: null }, + plugin7: { module: './plugin7', export: '*' }, + plugin8: { module: './plugin7', export: null }, // plugin6: { module: 'plugin6', export: null }, // plugin7: { module: 'plugin7', export: 'plugin7' }, @@ -46,12 +38,13 @@ test('find simple plugins', () => { expect( findSimplePlugins(js` import plugin1 from './plugin1' + import * as plugin2 from './plugin2' export default { - plugins: [plugin1, 'plugin2'] + plugins: [plugin1, plugin2, 'plugin3'] } `), - ).toEqual(['./plugin1', 'plugin2']) + ).toEqual(['./plugin1', './plugin2', 'plugin3']) expect( findSimplePlugins(js` diff --git a/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.ts b/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.ts index 65495778cf09..9c04794ff485 100644 --- a/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.ts +++ b/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.ts @@ -1,89 +1,70 @@ import Parser from 'tree-sitter' import TS from 'tree-sitter-typescript' -console.log({ TS }) let parser = new Parser() parser.setLanguage(TS.typescript) const treesitter = String.raw -const IMPORT_QUERY = new Parser.Query( +const ESM_IMPORT_QUERY = new Parser.Query( TS.typescript, treesitter` - ; import foo, {foo as bar} from './foo' (import_statement (import_clause (identifier)? @default - (named_imports - (import_specifier - name: (identifier) @imported-name - alias: (identifier)? @imported-alias - ) - )? + (named_imports + (import_specifier + name: (identifier) @imported-name + alias: (identifier)? @imported-alias + ) + )? + (namespace_import (identifier) @imported-namespace)? ) (string (string_fragment) @imported-from) )`, - - // ; import * as foo from './foo' - // (import_statement - // (import_clause - // (namespace_import - // (identifier) @imported-name)) - // (string - // (string_fragment) @imported-from) - // ) - // `, ) export function extractStaticImportMap(source: string) { let tree = parser.parse(source) let root = tree.rootNode - let captures = IMPORT_QUERY.matches(root) + let captures = ESM_IMPORT_QUERY.matches(root) let imports: Record = {} for (let match of captures) { - let toImport: { name: string; source: null | string }[] = [] + let toImport: { name: string; export: null | string }[] = [] let from = '' for (let i = 0; i < match.captures.length; i++) { let capture = match.captures[i] switch (capture.name) { case 'default': - toImport.push({ name: capture.node.text, source: null }) + toImport.push({ name: capture.node.text, export: null }) break case 'imported-name': - toImport.push({ name: capture.node.text, source: capture.node.text }) + toImport.push({ name: capture.node.text, export: capture.node.text }) break case 'imported-from': from = capture.node.text break + case 'imported-namespace': + toImport.push({ name: capture.node.text, export: '*' }) + break case 'imported-alias': if (toImport.length < 1) { - throw new Error() + throw new Error('Unexpected alias: ' + JSON.stringify(captures, null, 2)) } let prevImport = toImport[toImport.length - 1] let name = prevImport.name - prevImport.source = name + prevImport.export = name prevImport.name = capture.node.text break } } - for (let { name, source } of toImport) { - imports[name] = { module: from, export: source } + for (let { name, export: exportSource } of toImport) { + imports[name] = { module: from, export: exportSource } } - - // console.log({ imported, from }) - - // console.log(match.captures.map((c) => ({ name: c.name, text: c.node.text }))) - // console.log('+++++++++++++++++++++++++==') - // let name = match.captures.find((c) => c.name === 'imported-name') - // let from = match.captures.find((c) => c.name === 'imported-from') - - // if (!name || !from) continue - - // imports[name.node.text] = { module: from.node.text, export: null } } return imports @@ -122,8 +103,6 @@ export function findSimplePlugins(source: string): string[] | null { let imports = extractStaticImportMap(source) let captures = PLUGINS_QUERY.matches(root) - console.dir({ captures }, { depth: 4 }) - let plugins = [] for (let match of captures) { for (let capture of match.captures) { @@ -140,7 +119,7 @@ export function findSimplePlugins(source: string): string[] | null { switch (pluginDefinition.type) { case 'identifier': let source = imports[pluginDefinition.text] - if (!source || source.export !== null) { + if (!source || (source.export !== null && source.export !== '*')) { return null } plugins.push(source.module) From c1df729c7ca39e5ce0f8222baab92d99074c8256 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 11 Oct 2024 17:53:54 +0200 Subject: [PATCH 03/14] Add a test case with inline plugins --- .../src/utils/extract-static-imports.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.test.ts b/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.test.ts index 6f1a3dada681..4140d04d878c 100644 --- a/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.test.ts +++ b/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.test.ts @@ -56,6 +56,16 @@ test('find simple plugins', () => { `), ).toEqual(null) + expect( + findSimplePlugins(js` + let plugin1 = () => {} + + export default { + plugins: [plugin1] + } + `), + ).toEqual(null) + expect( findSimplePlugins(js` import {plugin1} from './plugin1' From d07eb800f074974ee69bf0cfac43dd2dad5c092b Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 11 Oct 2024 17:54:37 +0200 Subject: [PATCH 04/14] Remove leftover code --- .../src/utils/extract-static-imports.test.ts | 10 ---------- .../src/utils/extract-static-imports.ts | 11 ----------- 2 files changed, 21 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.test.ts b/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.test.ts index 4140d04d878c..9b516f996a59 100644 --- a/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.test.ts +++ b/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.test.ts @@ -21,16 +21,6 @@ test('extracts different kind of imports from an ESM file', () => { plugin6: { module: './plugin3', export: null }, plugin7: { module: './plugin7', export: '*' }, plugin8: { module: './plugin7', export: null }, - - // plugin6: { module: 'plugin6', export: null }, - // plugin7: { module: 'plugin7', export: 'plugin7' }, - // plugin8: { module: 'plugin8', export: 'foo' }, - // plugin9: { module: 'plugin9', export: null }, - // plugin10: { module: 'plugin10', export: 'plugin10' }, - // plugin11: { module: 'plugin11', export: 'foo' }, - // plugin12: { module: 'plugin12', export: null }, - // plugin13: { module: 'plugin13', export: 'plugin13' }, - // plugin14: { module: 'plugin14', export: 'foo' }, }) }) diff --git a/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.ts b/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.ts index 9c04794ff485..7087248ff9f5 100644 --- a/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.ts +++ b/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.ts @@ -132,17 +132,6 @@ export function findSimplePlugins(source: string): string[] | null { } } } - - // console.log({ imported, from }) - - // console.log(match.captures.map((c) => ({ name: c.name, text: c.node.text }))) - // console.log('+++++++++++++++++++++++++==') - // let name = match.captures.find((c) => c.name === 'imported-name') - // let from = match.captures.find((c) => c.name === 'imported-from') - - // if (!name || !from) continue - - // imports[name.node.text] = { module: from.node.text, export: null } } return plugins } catch (error) { From 5f12fa5fabbb57f6cb0458e5b54c9dc099a225d9 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 11 Oct 2024 18:03:41 +0200 Subject: [PATCH 05/14] Improve test harness --- .../src/utils/extract-static-imports.test.ts | 143 +++++++++++---- .../src/utils/extract-static-imports.ts | 167 ++++++++++-------- 2 files changed, 201 insertions(+), 109 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.test.ts b/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.test.ts index 9b516f996a59..51deada16522 100644 --- a/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.test.ts +++ b/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.test.ts @@ -1,82 +1,147 @@ import dedent from 'dedent' -import { expect, test } from 'vitest' +import { describe, expect, test } from 'vitest' import { extractStaticImportMap, findSimplePlugins } from './extract-static-imports' const js = dedent -test('extracts different kind of imports from an ESM file', () => { - let extracted = extractStaticImportMap(js` - import plugin1 from './plugin1' - import * as plugin2 from './plugin2' - import plugin6, { plugin3, plugin4, default as plugin5 } from './plugin3' - import plugin8, * as plugin7 from './plugin7' - `) +describe('findSimplePlugins', () => { + test('parses all export styles', () => { + expect( + findSimplePlugins(js` + import plugin1 from './plugin1' + import * as plugin2 from './plugin2' - expect(extracted).toEqual({ - plugin1: { module: './plugin1', export: null }, - plugin2: { module: './plugin2', export: '*' }, - plugin3: { module: './plugin3', export: 'plugin3' }, - plugin4: { module: './plugin3', export: 'plugin4' }, - plugin5: { module: './plugin3', export: 'default' }, - plugin6: { module: './plugin3', export: null }, - plugin7: { module: './plugin7', export: '*' }, - plugin8: { module: './plugin7', export: null }, - }) -}) + export default { + plugins: [plugin1, plugin2, 'plugin3'] + } + `), + ).toEqual(['./plugin1', './plugin2', 'plugin3']) + + expect( + findSimplePlugins(js` + import plugin1 from './plugin1' + import * as plugin2 from './plugin2' + + export default { + plugins: [plugin1, plugin2, 'plugin3'] + } as any + `), + ).toEqual(['./plugin1', './plugin2', 'plugin3']) + + expect( + findSimplePlugins(js` + import plugin1 from './plugin1' + import * as plugin2 from './plugin2' + + export default { + plugins: [plugin1, plugin2, 'plugin3'] + } satisfies any + `), + ).toEqual(['./plugin1', './plugin2', 'plugin3']) + + expect( + findSimplePlugins(js` + import plugin1 from './plugin1' + import * as plugin2 from './plugin2' + + module.exports = { + plugins: [plugin1, plugin2, 'plugin3'] + } as any + `), + ).toEqual(['./plugin1', './plugin2', 'plugin3']) -test('find simple plugins', () => { - expect( - findSimplePlugins(js` + expect( + findSimplePlugins(js` + import plugin1 from './plugin1' + import * as plugin2 from './plugin2' + + module.exports = { + plugins: [plugin1, plugin2, 'plugin3'] + } satisfies any + `), + ).toEqual(['./plugin1', './plugin2', 'plugin3']) + + expect( + findSimplePlugins(js` import plugin1 from './plugin1' import * as plugin2 from './plugin2' - export default { + module.exports = { plugins: [plugin1, plugin2, 'plugin3'] } `), - ).toEqual(['./plugin1', './plugin2', 'plugin3']) + ).toEqual(['./plugin1', './plugin2', 'plugin3']) + }) - expect( - findSimplePlugins(js` + test('bails out on inline plugins', () => { + expect( + findSimplePlugins(js` import plugin1 from './plugin1' export default { plugins: [plugin1, () => {} ] } `), - ).toEqual(null) + ).toEqual(null) - expect( - findSimplePlugins(js` + expect( + findSimplePlugins(js` let plugin1 = () => {} export default { plugins: [plugin1] } `), - ).toEqual(null) + ).toEqual(null) + }) - expect( - findSimplePlugins(js` + test('bails out on named imports for plugins', () => { + expect( + findSimplePlugins(js` import {plugin1} from './plugin1' export default { plugins: [plugin1] } `), - ).toEqual(null) + ).toEqual(null) + }) - expect( - findSimplePlugins(js` + test('returns no plugins if none are exported', () => { + expect( + findSimplePlugins(js` export default { plugins: [] } `), - ).toEqual([]) + ).toEqual([]) - expect( - findSimplePlugins(js` + expect( + findSimplePlugins(js` export default {} `), - ).toEqual([]) + ).toEqual([]) + }) +}) + +describe('extractStaticImportMap', () => { + test('extracts different kind of imports from an ESM file', () => { + let extracted = extractStaticImportMap(js` + import plugin1 from './plugin1' + import * as plugin2 from './plugin2' + import plugin6, { plugin3, plugin4, default as plugin5 } from './plugin3' + import plugin8, * as plugin7 from './plugin7' + `) + + expect(extracted).toEqual({ + plugin1: { module: './plugin1', export: null }, + plugin2: { module: './plugin2', export: '*' }, + plugin3: { module: './plugin3', export: 'plugin3' }, + plugin4: { module: './plugin3', export: 'plugin4' }, + plugin5: { module: './plugin3', export: 'default' }, + plugin6: { module: './plugin3', export: null }, + plugin7: { module: './plugin7', export: '*' }, + plugin8: { module: './plugin7', export: null }, + }) + }) }) diff --git a/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.ts b/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.ts index 7087248ff9f5..4c7f0df691a5 100644 --- a/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.ts +++ b/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.ts @@ -5,6 +5,103 @@ let parser = new Parser() parser.setLanguage(TS.typescript) const treesitter = String.raw +const PLUGINS_QUERY = new Parser.Query( + TS.typescript, + treesitter` + ; export default = {} + (export_statement + value: (satisfies_expression (object + (pair + key: (property_identifier) @name (#eq? @name "plugins") + value: (array) @imports + ) + ))? + value: (as_expression (object + (pair + key: (property_identifier) @name (#eq? @name "plugins") + value: (array) @imports + ) + ))? + value: (object + (pair + key: (property_identifier) @name (#eq? @name "plugins") + value: (array) @imports + ) + )? + ) + + ; module.exports = {} + (expression_statement + (assignment_expression + left: (member_expression) @left (#eq? @left "module.exports") + right: (satisfies_expression (object + (pair + key: (property_identifier) @name (#eq? @name "plugins") + value: (array) @imports + ) + ))? + right: (as_expression (object + (pair + key: (property_identifier) @name (#eq? @name "plugins") + value: (array) @imports + ) + ))? + right: (object + (pair + key: (property_identifier) @name (#eq? @name "plugins") + value: (array) @imports + ) + )? + ) + ) + + `, +) +export function findSimplePlugins(source: string): string[] | null { + try { + let tree = parser.parse(source) + let root = tree.rootNode + + let imports = extractStaticImportMap(source) + let captures = PLUGINS_QUERY.matches(root) + + let plugins = [] + for (let match of captures) { + for (let capture of match.captures) { + if (capture.name !== 'imports') continue + + for (let pluginDefinition of capture.node.children) { + if ( + pluginDefinition.type === '[' || + pluginDefinition.type === ']' || + pluginDefinition.type === ',' + ) + continue + + switch (pluginDefinition.type) { + case 'identifier': + let source = imports[pluginDefinition.text] + if (!source || (source.export !== null && source.export !== '*')) { + return null + } + plugins.push(source.module) + break + case 'string': + plugins.push(pluginDefinition.children[1].text) + break + default: + return null + } + } + } + } + return plugins + } catch (error) { + console.error(error) + return null + } +} + const ESM_IMPORT_QUERY = new Parser.Query( TS.typescript, treesitter` @@ -69,73 +166,3 @@ export function extractStaticImportMap(source: string) { return imports } - -const PLUGINS_QUERY = new Parser.Query( - TS.typescript, - treesitter` - (export_statement - value: (satisfies_expression (object - (pair - key: (property_identifier) @name (#eq? @name "plugins") - value: (array) @imports - ) - ))? - value: (as_expression (object - (pair - key: (property_identifier) @name (#eq? @name "plugins") - value: (array) @imports - ) - ))? - value: (object - (pair - key: (property_identifier) @name (#eq? @name "plugins") - value: (array) @imports - ) - )? - ) - `, -) -export function findSimplePlugins(source: string): string[] | null { - try { - let tree = parser.parse(source) - let root = tree.rootNode - - let imports = extractStaticImportMap(source) - let captures = PLUGINS_QUERY.matches(root) - - let plugins = [] - for (let match of captures) { - for (let capture of match.captures) { - if (capture.name !== 'imports') continue - - for (let pluginDefinition of capture.node.children) { - if ( - pluginDefinition.type === '[' || - pluginDefinition.type === ']' || - pluginDefinition.type === ',' - ) - continue - - switch (pluginDefinition.type) { - case 'identifier': - let source = imports[pluginDefinition.text] - if (!source || (source.export !== null && source.export !== '*')) { - return null - } - plugins.push(source.module) - break - case 'string': - plugins.push(pluginDefinition.children[1].text) - break - default: - return null - } - } - } - } - return plugins - } catch (error) { - console.error(error) - return null - } -} From 28ad88593984ee2757875c707cb15df9dc664316 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Mon, 14 Oct 2024 14:17:18 +0200 Subject: [PATCH 06/14] Add support for reading CJS imports --- .../src/utils/extract-static-imports.test.ts | 37 +++++++++++++--- .../src/utils/extract-static-imports.ts | 43 +++++++++++++++---- 2 files changed, 66 insertions(+), 14 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.test.ts b/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.test.ts index 51deada16522..fc40b219b632 100644 --- a/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.test.ts +++ b/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.test.ts @@ -127,11 +127,11 @@ describe('findSimplePlugins', () => { describe('extractStaticImportMap', () => { test('extracts different kind of imports from an ESM file', () => { let extracted = extractStaticImportMap(js` - import plugin1 from './plugin1' - import * as plugin2 from './plugin2' - import plugin6, { plugin3, plugin4, default as plugin5 } from './plugin3' - import plugin8, * as plugin7 from './plugin7' - `) + import plugin1 from './plugin1' + import * as plugin2 from './plugin2' + import plugin6, { plugin3, plugin4, default as plugin5 } from './plugin3' + import plugin8, * as plugin7 from './plugin7' + `) expect(extracted).toEqual({ plugin1: { module: './plugin1', export: null }, @@ -144,4 +144,31 @@ describe('extractStaticImportMap', () => { plugin8: { module: './plugin7', export: null }, }) }) + + test('extracts different kind of imports from an CJS file', () => { + let extracted = extractStaticImportMap(js` + const plugin1 = require('./plugin1') + let plugin2 = require('./plugin2') + var plugin3 = require('./plugin3') + + const {plugin4, foo: plugin5, ...plugin6} = require('./plugin4') + let {plugin7, foo: plugin8, ...plugin9} = require('./plugin5') + var {plugin10, foo: plugin11, ...plugin12} = require('./plugin6') + `) + + expect(extracted).toEqual({ + plugin1: { module: './plugin1', export: null }, + plugin2: { module: './plugin2', export: null }, + plugin3: { module: './plugin3', export: null }, + plugin4: { module: './plugin4', export: 'plugin4' }, + plugin5: { module: './plugin4', export: 'foo' }, + plugin6: { module: './plugin4', export: '*' }, + plugin7: { module: './plugin5', export: 'plugin7' }, + plugin8: { module: './plugin5', export: 'foo' }, + plugin9: { module: './plugin5', export: '*' }, + plugin10: { module: './plugin6', export: 'plugin10' }, + plugin11: { module: './plugin6', export: 'foo' }, + plugin12: { module: './plugin6', export: '*' }, + }) + }) }) diff --git a/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.ts b/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.ts index 4c7f0df691a5..3f31567ba21a 100644 --- a/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.ts +++ b/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.ts @@ -12,19 +12,19 @@ const PLUGINS_QUERY = new Parser.Query( (export_statement value: (satisfies_expression (object (pair - key: (property_identifier) @name (#eq? @name "plugins") + key: (property_identifier) @_name (#eq? @_name "plugins") value: (array) @imports ) ))? value: (as_expression (object (pair - key: (property_identifier) @name (#eq? @name "plugins") + key: (property_identifier) @_name (#eq? @_name "plugins") value: (array) @imports ) ))? value: (object (pair - key: (property_identifier) @name (#eq? @name "plugins") + key: (property_identifier) @_name (#eq? @_name "plugins") value: (array) @imports ) )? @@ -36,19 +36,19 @@ const PLUGINS_QUERY = new Parser.Query( left: (member_expression) @left (#eq? @left "module.exports") right: (satisfies_expression (object (pair - key: (property_identifier) @name (#eq? @name "plugins") + key: (property_identifier) @_name (#eq? @_name "plugins") value: (array) @imports ) ))? right: (as_expression (object (pair - key: (property_identifier) @name (#eq? @name "plugins") + key: (property_identifier) @_name (#eq? @_name "plugins") value: (array) @imports ) ))? right: (object (pair - key: (property_identifier) @name (#eq? @name "plugins") + key: (property_identifier) @_name (#eq? @_name "plugins") value: (array) @imports ) )? @@ -102,9 +102,10 @@ export function findSimplePlugins(source: string): string[] | null { } } -const ESM_IMPORT_QUERY = new Parser.Query( +const IMPORT_QUERY = new Parser.Query( TS.typescript, treesitter` + ; ESM import (import_statement (import_clause (identifier)? @default @@ -118,14 +119,38 @@ const ESM_IMPORT_QUERY = new Parser.Query( ) (string (string_fragment) @imported-from) - )`, + ) + + ; CJS require + (variable_declarator + name: (identifier)? @default + name: (object_pattern + (shorthand_property_identifier_pattern)? @imported-name + (pair_pattern + key: (property_identifier) @imported-name + value: (identifier) @imported-alias + )? + (rest_pattern + (identifier) @imported-namespace + )? + )? + value: (call_expression + function: (identifier) @_fn (#eq? @_fn "require") + arguments: (arguments + (string + (string_fragment) @imported-from + ) + ) + ) + ) + `, ) export function extractStaticImportMap(source: string) { let tree = parser.parse(source) let root = tree.rootNode - let captures = ESM_IMPORT_QUERY.matches(root) + let captures = IMPORT_QUERY.matches(root) let imports: Record = {} for (let match of captures) { From 7bfd701305f6eeba5cc492d194b04282a5314abf Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Mon, 14 Oct 2024 14:21:58 +0200 Subject: [PATCH 07/14] Update CJS tests --- .../src/migrate-js-config.ts | 7 ++- ...test.ts => extract-static-plugins.test.ts} | 51 +++++++++---------- ...c-imports.ts => extract-static-plugins.ts} | 3 +- 3 files changed, 28 insertions(+), 33 deletions(-) rename packages/@tailwindcss-upgrade/src/utils/{extract-static-imports.test.ts => extract-static-plugins.test.ts} (79%) rename packages/@tailwindcss-upgrade/src/utils/{extract-static-imports.ts => extract-static-plugins.ts} (98%) diff --git a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts index 2a7c4bef0844..71ad0a7c4c7a 100644 --- a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts +++ b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts @@ -12,7 +12,7 @@ import { deepMerge } from '../../tailwindcss/src/compat/config/deep-merge' import { mergeThemeExtension } from '../../tailwindcss/src/compat/config/resolve-config' import type { ThemeConfig } from '../../tailwindcss/src/compat/config/types' import { darkModePlugin } from '../../tailwindcss/src/compat/dark-mode' -import { findSimplePlugins } from './utils/extract-static-imports' +import { findStaticPlugins } from './utils/extract-static-plugins' import { info } from './utils/renderer' const __filename = fileURLToPath(import.meta.url) @@ -59,8 +59,7 @@ export async function migrateJsConfig( if (themeConfig) cssConfigs.push(themeConfig) } - let simplePlugins = findSimplePlugins(source) - console.log(simplePlugins) + let simplePlugins = findStaticPlugins(source) if (simplePlugins !== null) { for (let plugin of simplePlugins) { plugins.push({ base, path: plugin }) @@ -200,7 +199,7 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean { return false } - if (findSimplePlugins(source) === null) { + if (findStaticPlugins(source) === null) { return false } diff --git a/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.test.ts b/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.test.ts similarity index 79% rename from packages/@tailwindcss-upgrade/src/utils/extract-static-imports.test.ts rename to packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.test.ts index fc40b219b632..43664f6932d5 100644 --- a/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.test.ts +++ b/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.test.ts @@ -1,13 +1,13 @@ import dedent from 'dedent' import { describe, expect, test } from 'vitest' -import { extractStaticImportMap, findSimplePlugins } from './extract-static-imports' +import { extractStaticImportMap, findStaticPlugins } from './extract-static-plugins' const js = dedent -describe('findSimplePlugins', () => { +describe('findStaticPlugins', () => { test('parses all export styles', () => { expect( - findSimplePlugins(js` + findStaticPlugins(js` import plugin1 from './plugin1' import * as plugin2 from './plugin2' @@ -18,7 +18,7 @@ describe('findSimplePlugins', () => { ).toEqual(['./plugin1', './plugin2', 'plugin3']) expect( - findSimplePlugins(js` + findStaticPlugins(js` import plugin1 from './plugin1' import * as plugin2 from './plugin2' @@ -29,7 +29,7 @@ describe('findSimplePlugins', () => { ).toEqual(['./plugin1', './plugin2', 'plugin3']) expect( - findSimplePlugins(js` + findStaticPlugins(js` import plugin1 from './plugin1' import * as plugin2 from './plugin2' @@ -40,42 +40,39 @@ describe('findSimplePlugins', () => { ).toEqual(['./plugin1', './plugin2', 'plugin3']) expect( - findSimplePlugins(js` - import plugin1 from './plugin1' - import * as plugin2 from './plugin2' + findStaticPlugins(js` + const plugin1 = require('./plugin1') module.exports = { - plugins: [plugin1, plugin2, 'plugin3'] + plugins: [plugin1, 'plugin2'] } as any `), - ).toEqual(['./plugin1', './plugin2', 'plugin3']) + ).toEqual(['./plugin1', 'plugin2']) expect( - findSimplePlugins(js` - import plugin1 from './plugin1' - import * as plugin2 from './plugin2' + findStaticPlugins(js` + const plugin1 = require('./plugin1') module.exports = { - plugins: [plugin1, plugin2, 'plugin3'] + plugins: [plugin1, 'plugin2'] } satisfies any `), - ).toEqual(['./plugin1', './plugin2', 'plugin3']) + ).toEqual(['./plugin1', 'plugin2']) expect( - findSimplePlugins(js` - import plugin1 from './plugin1' - import * as plugin2 from './plugin2' + findStaticPlugins(js` + const plugin1 = require('./plugin1') - module.exports = { - plugins: [plugin1, plugin2, 'plugin3'] - } + module.exports = { + plugins: [plugin1, 'plugin2'] + } `), - ).toEqual(['./plugin1', './plugin2', 'plugin3']) + ).toEqual(['./plugin1', 'plugin2']) }) test('bails out on inline plugins', () => { expect( - findSimplePlugins(js` + findStaticPlugins(js` import plugin1 from './plugin1' export default { @@ -85,7 +82,7 @@ describe('findSimplePlugins', () => { ).toEqual(null) expect( - findSimplePlugins(js` + findStaticPlugins(js` let plugin1 = () => {} export default { @@ -97,7 +94,7 @@ describe('findSimplePlugins', () => { test('bails out on named imports for plugins', () => { expect( - findSimplePlugins(js` + findStaticPlugins(js` import {plugin1} from './plugin1' export default { @@ -109,7 +106,7 @@ describe('findSimplePlugins', () => { test('returns no plugins if none are exported', () => { expect( - findSimplePlugins(js` + findStaticPlugins(js` export default { plugins: [] } @@ -117,7 +114,7 @@ describe('findSimplePlugins', () => { ).toEqual([]) expect( - findSimplePlugins(js` + findStaticPlugins(js` export default {} `), ).toEqual([]) diff --git a/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.ts b/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.ts similarity index 98% rename from packages/@tailwindcss-upgrade/src/utils/extract-static-imports.ts rename to packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.ts index 3f31567ba21a..1b5ad518b949 100644 --- a/packages/@tailwindcss-upgrade/src/utils/extract-static-imports.ts +++ b/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.ts @@ -54,10 +54,9 @@ const PLUGINS_QUERY = new Parser.Query( )? ) ) - `, ) -export function findSimplePlugins(source: string): string[] | null { +export function findStaticPlugins(source: string): string[] | null { try { let tree = parser.parse(source) let root = tree.rootNode From 8ab196e707875f80ecdbf60c9b95a3f8ef0db374 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Mon, 14 Oct 2024 14:27:10 +0200 Subject: [PATCH 08/14] Update comment --- packages/@tailwindcss-upgrade/src/migrate-js-config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts index 71ad0a7c4c7a..bc80bbdf8245 100644 --- a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts +++ b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts @@ -179,7 +179,7 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean { return ['string', 'number', 'boolean', 'undefined'].includes(typeof value) } - // Plugins can be complex, we have a special heuristics for them! + // Plugins are more complex, so we have a special heuristics for them. let { plugins, ...remainder } = unresolvedConfig if (!isSimpleValue(remainder)) { return false From c1bcd2c219734f3b0b3910c8abbb3a60d1958085 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Mon, 14 Oct 2024 15:51:31 +0200 Subject: [PATCH 09/14] Support `plugin: [require('my-pluign')]` --- .../src/utils/extract-static-plugins.test.ts | 34 ++++++++++++------- .../src/utils/extract-static-plugins.ts | 7 ++++ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.test.ts b/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.test.ts index 43664f6932d5..427801ed61d3 100644 --- a/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.test.ts +++ b/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.test.ts @@ -12,10 +12,10 @@ describe('findStaticPlugins', () => { import * as plugin2 from './plugin2' export default { - plugins: [plugin1, plugin2, 'plugin3'] + plugins: [plugin1, plugin2, 'plugin3', require('./plugin4')] } `), - ).toEqual(['./plugin1', './plugin2', 'plugin3']) + ).toEqual(['./plugin1', './plugin2', 'plugin3', './plugin4']) expect( findStaticPlugins(js` @@ -23,10 +23,10 @@ describe('findStaticPlugins', () => { import * as plugin2 from './plugin2' export default { - plugins: [plugin1, plugin2, 'plugin3'] + plugins: [plugin1, plugin2, 'plugin3', require('./plugin4')] } as any `), - ).toEqual(['./plugin1', './plugin2', 'plugin3']) + ).toEqual(['./plugin1', './plugin2', 'plugin3', './plugin4']) expect( findStaticPlugins(js` @@ -34,40 +34,40 @@ describe('findStaticPlugins', () => { import * as plugin2 from './plugin2' export default { - plugins: [plugin1, plugin2, 'plugin3'] + plugins: [plugin1, plugin2, 'plugin3', require('./plugin4')] } satisfies any `), - ).toEqual(['./plugin1', './plugin2', 'plugin3']) + ).toEqual(['./plugin1', './plugin2', 'plugin3', './plugin4']) expect( findStaticPlugins(js` const plugin1 = require('./plugin1') module.exports = { - plugins: [plugin1, 'plugin2'] + plugins: [plugin1, 'plugin2', require('./plugin3')] } as any `), - ).toEqual(['./plugin1', 'plugin2']) + ).toEqual(['./plugin1', 'plugin2', './plugin3']) expect( findStaticPlugins(js` const plugin1 = require('./plugin1') module.exports = { - plugins: [plugin1, 'plugin2'] + plugins: [plugin1, 'plugin2', require('./plugin3')] } satisfies any `), - ).toEqual(['./plugin1', 'plugin2']) + ).toEqual(['./plugin1', 'plugin2', './plugin3']) expect( findStaticPlugins(js` const plugin1 = require('./plugin1') module.exports = { - plugins: [plugin1, 'plugin2'] + plugins: [plugin1, 'plugin2', require('./plugin3')] } `), - ).toEqual(['./plugin1', 'plugin2']) + ).toEqual(['./plugin1', 'plugin2', './plugin3']) }) test('bails out on inline plugins', () => { @@ -92,6 +92,16 @@ describe('findStaticPlugins', () => { ).toEqual(null) }) + test('bails out on non `require` calls', () => { + expect( + findStaticPlugins(js` + export default { + plugins: [frequire('./plugin1')] + } + `), + ).toEqual(null) + }) + test('bails out on named imports for plugins', () => { expect( findStaticPlugins(js` diff --git a/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.ts b/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.ts index 1b5ad518b949..78cee53f233c 100644 --- a/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.ts +++ b/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.ts @@ -88,6 +88,13 @@ export function findStaticPlugins(source: string): string[] | null { case 'string': plugins.push(pluginDefinition.children[1].text) break + case 'call_expression': + // allow require('..') calls + if (pluginDefinition.children?.[0]?.text !== 'require') return null + let firstArgument = pluginDefinition.children?.[1]?.children?.[1]?.children?.[1]?.text + if (typeof firstArgument !== 'string') return null + plugins.push(firstArgument) + break default: return null } From e2730eb9102bec6b663b1e519ff2af65303da15b Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Mon, 14 Oct 2024 15:56:36 +0200 Subject: [PATCH 10/14] Fix typo --- .../@tailwindcss-upgrade/src/utils/extract-static-plugins.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.ts b/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.ts index 78cee53f233c..4034ac42e2c4 100644 --- a/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.ts +++ b/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.ts @@ -8,7 +8,7 @@ const treesitter = String.raw const PLUGINS_QUERY = new Parser.Query( TS.typescript, treesitter` - ; export default = {} + ; export default {} (export_statement value: (satisfies_expression (object (pair From c68258fb0fcaaa705f8cb86498a22b62ba56a46f Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Mon, 14 Oct 2024 16:48:22 +0200 Subject: [PATCH 11/14] Add test for plugins with options --- .../src/utils/extract-static-plugins.test.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.test.ts b/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.test.ts index 427801ed61d3..8f8892cb39f2 100644 --- a/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.test.ts +++ b/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.test.ts @@ -114,6 +114,26 @@ describe('findStaticPlugins', () => { ).toEqual(null) }) + test('bails for plugins with options', () => { + expect( + findStaticPlugins(js` + import plugin1 from './plugin1' + + export default { + plugins: [plugin1({foo:'bar'})] + } + `), + ).toEqual(null) + + expect( + findStaticPlugins(js` + export default { + plugins: [require('@tailwindcss/typography')({foo:'bar'})] + } + `), + ).toEqual(null) + }) + test('returns no plugins if none are exported', () => { expect( findStaticPlugins(js` From 03d6f3cf6abc70f098d93510c85e4f3294ca8734 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Mon, 14 Oct 2024 16:50:15 +0200 Subject: [PATCH 12/14] Update tests --- integrations/upgrade/js-config.test.ts | 108 ++++++++++--------------- 1 file changed, 43 insertions(+), 65 deletions(-) diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index 45248b8a1b97..4882fa2b1ae1 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -8,7 +8,6 @@ test( 'package.json': json` { "dependencies": { - "@tailwindcss/typography": "^0.5.15", "@tailwindcss/upgrade": "workspace:^" } } @@ -16,10 +15,8 @@ test( 'tailwind.config.ts': ts` import { type Config } from 'tailwindcss' import defaultTheme from 'tailwindcss/defaultTheme' - import typography from '@tailwindcss/typography' - import customPlugin from './custom-plugin' - export default { + module.exports = { darkMode: 'selector', content: ['./src/**/*.{html,js}', './my-app/**/*.{html,js}'], theme: { @@ -53,15 +50,9 @@ test( }, }, }, - plugins: [typography, customPlugin], + plugins: [], } satisfies Config `, - 'custom-plugin.js': ts` - export default function ({ addVariant }) { - addVariant('inverted', '@media (inverted-colors: inverted)') - addVariant('hocus', ['&:focus', '&:hover']) - } - `, 'src/input.css': css` @tailwind base; @tailwind components; @@ -80,9 +71,6 @@ test( @source './**/*.{html,js}'; @source '../my-app/**/*.{html,js}'; - @plugin '@tailwindcss/typography'; - @plugin '../custom-plugin'; - @variant dark (&:where(.dark, .dark *)); @theme { @@ -115,29 +103,32 @@ test( ) test( - 'does not upgrade JS config files with functions in the theme config', + 'upgrades JS config files with plugins', { fs: { 'package.json': json` { "dependencies": { + "@tailwindcss/typography": "^0.5.15", "@tailwindcss/upgrade": "workspace:^" } } `, 'tailwind.config.ts': ts` import { type Config } from 'tailwindcss' + import typography from '@tailwindcss/typography' + import customPlugin from './custom-plugin' export default { - theme: { - extend: { - colors: ({ colors }) => ({ - gray: colors.neutral, - }), - }, - }, + plugins: [typography, customPlugin], } satisfies Config `, + 'custom-plugin.js': ts` + export default function ({ addVariant }) { + addVariant('inverted', '@media (inverted-colors: inverted)') + addVariant('hocus', ['&:focus', '&:hover']) + } + `, 'src/input.css': css` @tailwind base; @tailwind components; @@ -148,7 +139,7 @@ test( async ({ exec, fs }) => { await exec('npx @tailwindcss/upgrade') - expect(await fs.dumpFiles('src/**/*.{css,ts}')).toMatchInlineSnapshot(` + expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` " --- src/input.css --- @import 'tailwindcss'; @@ -160,15 +151,11 @@ test( " --- tailwind.config.ts --- import { type Config } from 'tailwindcss' + import typography from '@tailwindcss/typography' + import customPlugin from './custom-plugin' export default { - theme: { - extend: { - colors: ({ colors }) => ({ - gray: colors.neutral, - }), - }, - }, + plugins: [typography, customPlugin], } satisfies Config " `) @@ -176,7 +163,7 @@ test( ) test( - 'does not upgrade JS config files with theme keys contributed to by plugins in the theme config', + 'does not upgrade JS config files with functions in the theme config', { fs: { 'package.json': json` @@ -191,13 +178,10 @@ test( export default { theme: { - typography: { - DEFAULT: { - css: { - '--tw-prose-body': 'red', - color: 'var(--tw-prose-body)', - }, - }, + extend: { + colors: ({ colors }) => ({ + gray: colors.neutral, + }), }, }, } satisfies Config @@ -206,14 +190,13 @@ test( @tailwind base; @tailwind components; @tailwind utilities; - @config '../tailwind.config.ts'; `, }, }, async ({ exec, fs }) => { await exec('npx @tailwindcss/upgrade') - expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(` + expect(await fs.dumpFiles('src/**/*.{css,ts}')).toMatchInlineSnapshot(` " --- src/input.css --- @import 'tailwindcss'; @@ -228,13 +211,10 @@ test( export default { theme: { - typography: { - DEFAULT: { - css: { - '--tw-prose-body': 'red', - color: 'var(--tw-prose-body)', - }, - }, + extend: { + colors: ({ colors }) => ({ + gray: colors.neutral, + }), }, }, } satisfies Config @@ -244,36 +224,37 @@ test( ) test( - 'does not upgrade JS config files with plugins', + 'does not upgrade JS config files with theme keys contributed to by plugins in the theme config', { fs: { 'package.json': json` { "dependencies": { - "@tailwindcss/typography": "^0.5.15", "@tailwindcss/upgrade": "workspace:^" } } `, 'tailwind.config.ts': ts` import { type Config } from 'tailwindcss' - import typography from '@tailwindcss/typography' - import customPlugin from './custom-plugin' export default { - plugins: [typography, customPlugin], + theme: { + typography: { + DEFAULT: { + css: { + '--tw-prose-body': 'red', + color: 'var(--tw-prose-body)', + }, + }, + }, + }, } satisfies Config `, - 'custom-plugin.js': ts` - export default function ({ addVariant }) { - addVariant('inverted', '@media (inverted-colors: inverted)') - addVariant('hocus', ['&:focus', '&:hover']) - } - `, 'src/input.css': css` @tailwind base; @tailwind components; @tailwind utilities; + @config '../tailwind.config.ts'; `, }, }, @@ -290,14 +271,11 @@ test( expect(await fs.dumpFiles('tailwind.config.ts')).toMatchInlineSnapshot(` " - --- tailwind.config.ts --- - import { type Config } from 'tailwindcss' - import typography from '@tailwindcss/typography' - import customPlugin from './custom-plugin' + --- src/input.css --- + @import 'tailwindcss'; - export default { - plugins: [typography, customPlugin], - } satisfies Config + @plugin '@tailwindcss/typography'; + @plugin '../custom-plugin'; " `) }, From 652dd6a00d611563a9a658077d4eeca9ada7609e Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Mon, 14 Oct 2024 16:50:54 +0200 Subject: [PATCH 13/14] Link PR to change log entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c126b06986e..f9c5023d7611 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - _Upgrade (experimental)_: Migrate v3 PostCSS setups to v4 in some cases ([#14612](https://github.com/tailwindlabs/tailwindcss/pull/14612)) - _Upgrade (experimental)_: Automatically discover JavaScript config files ([#14597](https://github.com/tailwindlabs/tailwindcss/pull/14597)) - _Upgrade (experimental)_: Migrate legacy classes to the v4 alternative ([#14643](https://github.com/tailwindlabs/tailwindcss/pull/14643)) -- _Upgrade (experimental)_: Migrate static JS configurations to CSS ([#14639](https://github.com/tailwindlabs/tailwindcss/pull/14639), [#14650](https://github.com/tailwindlabs/tailwindcss/pull/14650)) +- _Upgrade (experimental)_: Migrate static JS configurations to CSS ([#14639](https://github.com/tailwindlabs/tailwindcss/pull/14639), [#14650](https://github.com/tailwindlabs/tailwindcss/pull/14650), [#14648](https://github.com/tailwindlabs/tailwindcss/pull/14648)) - _Upgrade (experimental)_: Migrate `@media screen(…)` when running codemods ([#14603](https://github.com/tailwindlabs/tailwindcss/pull/14603)) - _Upgrade (experimental)_: Inject `@config "…"` when a `tailwind.config.{js,ts,…}` is detected ([#14635](https://github.com/tailwindlabs/tailwindcss/pull/14635)) - _Upgrade (experimental)_: Migrate `aria-*`, `data-*`, and `supports-*` variants from arbitrary values to bare values ([#14644](https://github.com/tailwindlabs/tailwindcss/pull/14644)) From 66417a82341dc6470486f32c6774ae5a96aa9678 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Mon, 14 Oct 2024 17:00:30 +0200 Subject: [PATCH 14/14] Update snapshots --- integrations/upgrade/js-config.test.ts | 29 +++++++++++++++----------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index 4882fa2b1ae1..e19681b4c91a 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -143,20 +143,15 @@ test( " --- src/input.css --- @import 'tailwindcss'; - @config '../tailwind.config.ts'; + + @plugin '@tailwindcss/typography'; + @plugin '../custom-plugin'; " `) expect(await fs.dumpFiles('tailwind.config.ts')).toMatchInlineSnapshot(` " - --- tailwind.config.ts --- - import { type Config } from 'tailwindcss' - import typography from '@tailwindcss/typography' - import customPlugin from './custom-plugin' - export default { - plugins: [typography, customPlugin], - } satisfies Config " `) }, @@ -271,11 +266,21 @@ test( expect(await fs.dumpFiles('tailwind.config.ts')).toMatchInlineSnapshot(` " - --- src/input.css --- - @import 'tailwindcss'; + --- tailwind.config.ts --- + import { type Config } from 'tailwindcss' - @plugin '@tailwindcss/typography'; - @plugin '../custom-plugin'; + export default { + theme: { + typography: { + DEFAULT: { + css: { + '--tw-prose-body': 'red', + color: 'var(--tw-prose-body)', + }, + }, + }, + }, + } satisfies Config " `) },