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)) diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index 347e08e93063..e19681b4c91a 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -103,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; @@ -136,35 +139,26 @@ 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'; - @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' - export default { - theme: { - extend: { - colors: ({ colors }) => ({ - gray: colors.neutral, - }), - }, - }, - } satisfies Config " `) }, ) 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` @@ -179,13 +173,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 @@ -194,14 +185,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'; @@ -216,13 +206,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 @@ -232,36 +219,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'; `, }, }, @@ -280,11 +268,18 @@ test( " --- tailwind.config.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 " `) 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..bc80bbdf8245 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 { findStaticPlugins } from './utils/extract-static-plugins' 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,16 @@ export async function migrateJsConfig( if (themeConfig) cssConfigs.push(themeConfig) } + let simplePlugins = findStaticPlugins(source) + if (simplePlugins !== null) { + for (let plugin of simplePlugins) { + plugins.push({ base, path: plugin }) + } + } + return { sources, + plugins, css: cssConfigs.join('\n'), } } @@ -168,7 +179,9 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean { return ['string', 'number', 'boolean', 'undefined'].includes(typeof value) } - if (!isSimpleValue(unresolvedConfig)) { + // Plugins are more complex, so we have a special heuristics for them. + let { plugins, ...remainder } = unresolvedConfig + if (!isSimpleValue(remainder)) { return false } @@ -186,7 +199,7 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean { return false } - if (unresolvedConfig.plugins && unresolvedConfig.plugins.length > 0) { + if (findStaticPlugins(source) === null) { return false } diff --git a/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.test.ts b/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.test.ts new file mode 100644 index 000000000000..8f8892cb39f2 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.test.ts @@ -0,0 +1,201 @@ +import dedent from 'dedent' +import { describe, expect, test } from 'vitest' +import { extractStaticImportMap, findStaticPlugins } from './extract-static-plugins' + +const js = dedent + +describe('findStaticPlugins', () => { + test('parses all export styles', () => { + expect( + findStaticPlugins(js` + import plugin1 from './plugin1' + import * as plugin2 from './plugin2' + + export default { + plugins: [plugin1, plugin2, 'plugin3', require('./plugin4')] + } + `), + ).toEqual(['./plugin1', './plugin2', 'plugin3', './plugin4']) + + expect( + findStaticPlugins(js` + import plugin1 from './plugin1' + import * as plugin2 from './plugin2' + + export default { + plugins: [plugin1, plugin2, 'plugin3', require('./plugin4')] + } as any + `), + ).toEqual(['./plugin1', './plugin2', 'plugin3', './plugin4']) + + expect( + findStaticPlugins(js` + import plugin1 from './plugin1' + import * as plugin2 from './plugin2' + + export default { + plugins: [plugin1, plugin2, 'plugin3', require('./plugin4')] + } satisfies any + `), + ).toEqual(['./plugin1', './plugin2', 'plugin3', './plugin4']) + + expect( + findStaticPlugins(js` + const plugin1 = require('./plugin1') + + module.exports = { + plugins: [plugin1, 'plugin2', require('./plugin3')] + } as any + `), + ).toEqual(['./plugin1', 'plugin2', './plugin3']) + + expect( + findStaticPlugins(js` + const plugin1 = require('./plugin1') + + module.exports = { + plugins: [plugin1, 'plugin2', require('./plugin3')] + } satisfies any + `), + ).toEqual(['./plugin1', 'plugin2', './plugin3']) + + expect( + findStaticPlugins(js` + const plugin1 = require('./plugin1') + + module.exports = { + plugins: [plugin1, 'plugin2', require('./plugin3')] + } + `), + ).toEqual(['./plugin1', 'plugin2', './plugin3']) + }) + + test('bails out on inline plugins', () => { + expect( + findStaticPlugins(js` + import plugin1 from './plugin1' + + export default { + plugins: [plugin1, () => {} ] + } + `), + ).toEqual(null) + + expect( + findStaticPlugins(js` + let plugin1 = () => {} + + export default { + plugins: [plugin1] + } + `), + ).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` + import {plugin1} from './plugin1' + + export default { + plugins: [plugin1] + } + `), + ).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` + export default { + plugins: [] + } + `), + ).toEqual([]) + + expect( + findStaticPlugins(js` + export default {} + `), + ).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 }, + }) + }) + + 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-plugins.ts b/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.ts new file mode 100644 index 000000000000..4034ac42e2c4 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/utils/extract-static-plugins.ts @@ -0,0 +1,199 @@ +import Parser from 'tree-sitter' +import TS from 'tree-sitter-typescript' + +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 findStaticPlugins(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 + 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 + } + } + } + } + return plugins + } catch (error) { + console.error(error) + return null + } +} + +const IMPORT_QUERY = new Parser.Query( + TS.typescript, + treesitter` + ; ESM import + (import_statement + (import_clause + (identifier)? @default + (named_imports + (import_specifier + name: (identifier) @imported-name + alias: (identifier)? @imported-alias + ) + )? + (namespace_import (identifier) @imported-namespace)? + ) + (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 = IMPORT_QUERY.matches(root) + + let imports: Record = {} + for (let match of captures) { + 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, export: null }) + break + case 'imported-name': + 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('Unexpected alias: ' + JSON.stringify(captures, null, 2)) + } + let prevImport = toImport[toImport.length - 1] + let name = prevImport.name + prevImport.export = name + prevImport.name = capture.node.text + break + } + } + + for (let { name, export: exportSource } of toImport) { + imports[name] = { module: from, export: exportSource } + } + } + + return imports +} 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