From 5fcab69d34a1d1067aefde3e3a79f3f5e85f6c27 Mon Sep 17 00:00:00 2001 From: Rahim Date: Fri, 19 Jun 2026 20:13:11 -0700 Subject: [PATCH] refactor(compiler)!: consolidate jsx surface --- packages/compiler/package.json | 22 +++--- packages/compiler/src/ast/index.ts | 3 - .../{plugins => bundlers}/tests/vite.test.ts | 0 .../src/{plugins => bundlers}/vite.ts | 0 packages/compiler/src/compile.ts | 4 +- packages/compiler/src/config.ts | 13 ++-- packages/compiler/src/index.ts | 32 -------- packages/compiler/src/jsx/index.ts | 17 ++++ .../src/{ => jsx}/matchers/has-child.ts | 0 .../compiler/src/{ => jsx}/matchers/index.ts | 0 .../compiler/src/{ => jsx}/matchers/tag.ts | 4 +- .../src/{react => jsx/transforms}/add-prop.ts | 2 +- .../transforms}/child-as-prop.ts | 2 +- .../compiler/src/{ => jsx}/transforms/jsx.ts | 0 .../src/{ => jsx}/transforms/replace.ts | 5 +- .../compiler/src/{ => jsx}/transforms/wrap.ts | 7 +- packages/compiler/src/react/index.ts | 11 --- packages/compiler/src/styles/analyze.ts | 2 +- .../compiler/src/styles/tests/analyze.test.ts | 8 +- packages/compiler/src/tailwind/naming.ts | 3 +- packages/compiler/src/tailwind/plugin.ts | 2 +- .../src/tailwind/tests/naming.test.ts | 2 +- .../src/tailwind/tests/plugin.test.ts | 72 ++++++++--------- packages/compiler/src/tests/compile.test.ts | 77 +++++++++++-------- .../compiler/src/tests/diagnostics.test.ts | 5 +- .../compiler/src/tests/integration.test.ts | 16 ++-- packages/compiler/src/transforms/imports.ts | 2 +- packages/compiler/src/transforms/index.ts | 10 +++ .../tests/drop-unused-imports.test.ts | 4 +- .../tests/drop-unused-locals.test.ts | 4 +- packages/compiler/tsdown.config.ts | 8 +- 31 files changed, 165 insertions(+), 172 deletions(-) delete mode 100644 packages/compiler/src/ast/index.ts rename packages/compiler/src/{plugins => bundlers}/tests/vite.test.ts (100%) rename packages/compiler/src/{plugins => bundlers}/vite.ts (100%) create mode 100644 packages/compiler/src/jsx/index.ts rename packages/compiler/src/{ => jsx}/matchers/has-child.ts (100%) rename packages/compiler/src/{ => jsx}/matchers/index.ts (100%) rename packages/compiler/src/{ => jsx}/matchers/tag.ts (88%) rename packages/compiler/src/{react => jsx/transforms}/add-prop.ts (97%) rename packages/compiler/src/{react => jsx/transforms}/child-as-prop.ts (97%) rename packages/compiler/src/{ => jsx}/transforms/jsx.ts (100%) rename packages/compiler/src/{ => jsx}/transforms/replace.ts (94%) rename packages/compiler/src/{ => jsx}/transforms/wrap.ts (86%) delete mode 100644 packages/compiler/src/react/index.ts create mode 100644 packages/compiler/src/transforms/index.ts diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 471e5281..50fb73ec 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -16,20 +16,20 @@ "default": "./dist/index.js" }, "./vite": { - "types": "./dist/plugins/vite.d.ts", - "default": "./dist/plugins/vite.js" + "types": "./dist/bundlers/vite.d.ts", + "default": "./dist/bundlers/vite.js" }, - "./ast": { - "types": "./dist/ast/index.d.ts", - "default": "./dist/ast/index.js" + "./jsx": { + "types": "./dist/jsx/index.d.ts", + "default": "./dist/jsx/index.js" }, - "./matchers": { - "types": "./dist/matchers/index.d.ts", - "default": "./dist/matchers/index.js" + "./diagnostics": { + "types": "./dist/diagnostics.d.ts", + "default": "./dist/diagnostics.js" }, - "./react": { - "types": "./dist/react/index.d.ts", - "default": "./dist/react/index.js" + "./transforms": { + "types": "./dist/transforms/index.d.ts", + "default": "./dist/transforms/index.js" }, "./styles": { "types": "./dist/styles/index.d.ts", diff --git a/packages/compiler/src/ast/index.ts b/packages/compiler/src/ast/index.ts deleted file mode 100644 index 5f657e6f..00000000 --- a/packages/compiler/src/ast/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { type ParseOptions, type ParseResult, parse } from '../parse'; -export { type AddImportContext, type AddImportRef, addNamedImport } from '../transforms/add-import'; -export { type ImportRef, type ImportRewriteOptions, type ImportRule, transformImports } from '../transforms/imports'; diff --git a/packages/compiler/src/plugins/tests/vite.test.ts b/packages/compiler/src/bundlers/tests/vite.test.ts similarity index 100% rename from packages/compiler/src/plugins/tests/vite.test.ts rename to packages/compiler/src/bundlers/tests/vite.test.ts diff --git a/packages/compiler/src/plugins/vite.ts b/packages/compiler/src/bundlers/vite.ts similarity index 100% rename from packages/compiler/src/plugins/vite.ts rename to packages/compiler/src/bundlers/vite.ts diff --git a/packages/compiler/src/compile.ts b/packages/compiler/src/compile.ts index 9d240a76..7073c617 100644 --- a/packages/compiler/src/compile.ts +++ b/packages/compiler/src/compile.ts @@ -6,7 +6,7 @@ import { type CompilerDiagnostic, type CompilerPipelineStep, type CompilerTransform, - react, + jsx, } from './config'; import { fatalDiagnosticFromError, withDiagnosticSource } from './diagnostics'; import { parse } from './parse'; @@ -59,7 +59,7 @@ const printer = ts.createPrinter({ export async function compile(source: string, options: CompileOptions = {}): Promise { const filename = options.filename ?? 'input.tsx'; const config = options.config ?? {}; - const target = config.target ?? react(); + const target = config.target ?? jsx(); const assets: CompilerAsset[] = []; const diagnostics: CompilerDiagnostic[] = []; const context: CompilerContext = { diff --git a/packages/compiler/src/config.ts b/packages/compiler/src/config.ts index a5de1d34..81be0dea 100644 --- a/packages/compiler/src/config.ts +++ b/packages/compiler/src/config.ts @@ -41,11 +41,8 @@ export interface StylePipeline { setup(context: CompilerContext): CompilerPipelineStep | Promise; } -/** - * Per-target compile configuration. Currently only `react` is shipped, but - * the shape is extensible for `html`/etc. - */ -export interface ReactTargetOptions { +/** Per-target compile configuration for JSX transforms. */ +export interface JsxTargetOptions { /** Per-source-module rewrite rules. */ imports?: Record | undefined; /** Transforms applied in order after `transformImports`. */ @@ -53,7 +50,7 @@ export interface ReactTargetOptions { } export interface CompilerTarget { - name: 'react' | 'html'; + name: 'jsx'; imports?: Record | undefined; transforms?: readonly CompilerTransform[] | undefined; } @@ -68,9 +65,9 @@ export function defineConfig(config: Config return config; } -export function react(options: ReactTargetOptions = {}): CompilerTarget { +export function jsx(options: JsxTargetOptions = {}): CompilerTarget { return { - name: 'react', + name: 'jsx', ...(options.imports ? { imports: options.imports } : {}), ...(options.transforms ? { transforms: options.transforms } : {}), }; diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index f5ff93e0..56ebd0ff 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -8,37 +8,5 @@ export { type CompilerTarget, type CompilerTransform, defineConfig, - type ReactTargetOptions, - react, type StylePipeline, } from './config'; -export { - compilerDiagnosticToJsonEvent, - type DiagnosticFormat, - type DiagnosticJsonEvent, - type DiagnosticJsonFrameLine, - type DiagnosticLocation, - type DiagnosticSummaryJsonEvent, - diagnosticLocationFromNode, - diagnosticSummaryToJsonEvent, - type FormatDiagnosticOptions, - formatCompilerDiagnostic, - formatCompilerDiagnosticJsonLine, - formatDiagnosticSummaryJsonLine, - LogLevel, - type LogLevelName, - mapLogLevelStringToNumber, - mapLogLevelToString, - shouldUseColor, -} from './diagnostics'; -export { type TailwindMode, type TailwindOptions, tailwind } from './tailwind'; -export type { ImportRef, ImportRule } from './transforms/imports'; -export { - accessPath, - type JsxChildReplacement, - jsxExpression, - propertyAccess, - type ReplaceJsxChildOptions, - readStringAttribute, - replaceJsxChild, -} from './transforms/jsx'; diff --git a/packages/compiler/src/jsx/index.ts b/packages/compiler/src/jsx/index.ts new file mode 100644 index 00000000..c393d664 --- /dev/null +++ b/packages/compiler/src/jsx/index.ts @@ -0,0 +1,17 @@ +export { type JsxTargetOptions, jsx } from '../config'; +export type { ImportRef } from '../transforms/imports'; +export { hasChild } from './matchers/has-child'; +export { anyTag, byTag, type JsxElementLike, type Matcher, tagName } from './matchers/tag'; +export { type AddPropImportRef, type AddPropOptions, addProp } from './transforms/add-prop'; +export { type ChildAsPropOptions, childAsProp } from './transforms/child-as-prop'; +export { + accessPath, + type JsxChildReplacement, + jsxExpression, + propertyAccess, + type ReplaceJsxChildOptions, + readStringAttribute, + replaceJsxChild, +} from './transforms/jsx'; +export { type ReplaceOptions, replace } from './transforms/replace'; +export { type WrapOptions, wrap } from './transforms/wrap'; diff --git a/packages/compiler/src/matchers/has-child.ts b/packages/compiler/src/jsx/matchers/has-child.ts similarity index 100% rename from packages/compiler/src/matchers/has-child.ts rename to packages/compiler/src/jsx/matchers/has-child.ts diff --git a/packages/compiler/src/matchers/index.ts b/packages/compiler/src/jsx/matchers/index.ts similarity index 100% rename from packages/compiler/src/matchers/index.ts rename to packages/compiler/src/jsx/matchers/index.ts diff --git a/packages/compiler/src/matchers/tag.ts b/packages/compiler/src/jsx/matchers/tag.ts similarity index 88% rename from packages/compiler/src/matchers/tag.ts rename to packages/compiler/src/jsx/matchers/tag.ts index 3c52daff..42352f9a 100644 --- a/packages/compiler/src/matchers/tag.ts +++ b/packages/compiler/src/jsx/matchers/tag.ts @@ -1,6 +1,6 @@ import ts from 'typescript'; -/** A JSX element that helpers can transform — either an open/close pair or self-closing. */ +/** A JSX element that helpers can transform: either an open/close pair or self-closing. */ export type JsxElementLike = ts.JsxElement | ts.JsxSelfClosingElement; /** Predicate over a JSX element. Single shape across `replace`, `wrap`, `childAsProp`, `addProp`. */ @@ -16,7 +16,7 @@ function readTag(name: ts.JsxTagNameExpression): string { if (ts.isIdentifier(name)) return name.text; if (ts.isPropertyAccessExpression(name)) return `${readTag(name.expression as ts.JsxTagNameExpression)}.${name.name.text}`; - // ThisExpression / JsxNamespacedName — uncommon in our skins; fall back to source text. + // ThisExpression / JsxNamespacedName: uncommon in our skins; fall back to source text. return name.getText(); } diff --git a/packages/compiler/src/react/add-prop.ts b/packages/compiler/src/jsx/transforms/add-prop.ts similarity index 97% rename from packages/compiler/src/react/add-prop.ts rename to packages/compiler/src/jsx/transforms/add-prop.ts index 29f12941..4a87e9ec 100644 --- a/packages/compiler/src/react/add-prop.ts +++ b/packages/compiler/src/jsx/transforms/add-prop.ts @@ -1,6 +1,6 @@ import ts from 'typescript'; +import { type AddImportContext, addNamedImport } from '../../transforms/add-import'; import type { JsxElementLike, Matcher } from '../matchers'; -import { type AddImportContext, addNamedImport } from '../transforms/add-import'; export interface AddPropImportRef { source: string; diff --git a/packages/compiler/src/react/child-as-prop.ts b/packages/compiler/src/jsx/transforms/child-as-prop.ts similarity index 97% rename from packages/compiler/src/react/child-as-prop.ts rename to packages/compiler/src/jsx/transforms/child-as-prop.ts index f1730f56..361284c1 100644 --- a/packages/compiler/src/react/child-as-prop.ts +++ b/packages/compiler/src/jsx/transforms/child-as-prop.ts @@ -11,7 +11,7 @@ export interface ChildAsPropOptions { * named prop (turning the element into a self-closing form): * * - * → }/> + * -> }/> * * Skips no-op cases: * - element is already self-closing diff --git a/packages/compiler/src/transforms/jsx.ts b/packages/compiler/src/jsx/transforms/jsx.ts similarity index 100% rename from packages/compiler/src/transforms/jsx.ts rename to packages/compiler/src/jsx/transforms/jsx.ts diff --git a/packages/compiler/src/transforms/replace.ts b/packages/compiler/src/jsx/transforms/replace.ts similarity index 94% rename from packages/compiler/src/transforms/replace.ts rename to packages/compiler/src/jsx/transforms/replace.ts index b1bb4f31..183376e1 100644 --- a/packages/compiler/src/transforms/replace.ts +++ b/packages/compiler/src/jsx/transforms/replace.ts @@ -1,10 +1,11 @@ import ts from 'typescript'; +import { type AddImportContext, addNamedImport } from '../../transforms/add-import'; +import type { ImportRef } from '../../transforms/imports'; import type { JsxElementLike, Matcher } from '../matchers'; -import { type AddImportContext, type AddImportRef, addNamedImport } from './add-import'; export interface ReplaceOptions { match: Matcher; - with: AddImportRef; + with: ImportRef; /** Reshape the new element's attributes from the original's. Defaults to passthrough. */ mapProps?: (original: ts.JsxAttributes, factory: ts.NodeFactory) => ts.JsxAttributes; /** Reshape the new element's children from the original's. Defaults to passthrough (open form only). */ diff --git a/packages/compiler/src/transforms/wrap.ts b/packages/compiler/src/jsx/transforms/wrap.ts similarity index 86% rename from packages/compiler/src/transforms/wrap.ts rename to packages/compiler/src/jsx/transforms/wrap.ts index 733f57ad..83c97f16 100644 --- a/packages/compiler/src/transforms/wrap.ts +++ b/packages/compiler/src/jsx/transforms/wrap.ts @@ -1,15 +1,16 @@ import ts from 'typescript'; +import { type AddImportContext, addNamedImport } from '../../transforms/add-import'; +import type { ImportRef } from '../../transforms/imports'; import type { JsxElementLike, Matcher } from '../matchers'; -import { type AddImportContext, type AddImportRef, addNamedImport } from './add-import'; export interface WrapOptions { match: Matcher; - with: AddImportRef; + with: ImportRef; } /** * Wrap a matched JSX subtree with another component: - * + * -> * The wrapper's import is added if missing. */ export function wrap(opts: WrapOptions, ctx: AddImportContext = {}): ts.TransformerFactory { diff --git a/packages/compiler/src/react/index.ts b/packages/compiler/src/react/index.ts deleted file mode 100644 index bda5a249..00000000 --- a/packages/compiler/src/react/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * React-target plugins for `@videojs/compiler`. Houses the framework-pattern - * helpers that lower constrained JSX into React-friendly component shapes. - * Re-exports `replace` and `wrap` for convenience so a config can import - * everything from one subpath. - */ - -export { type ReplaceOptions, replace } from '../transforms/replace'; -export { type WrapOptions, wrap } from '../transforms/wrap'; -export { type AddPropImportRef, type AddPropOptions, addProp } from './add-prop'; -export { type ChildAsPropOptions, childAsProp } from './child-as-prop'; diff --git a/packages/compiler/src/styles/analyze.ts b/packages/compiler/src/styles/analyze.ts index 3cc0edc3..634c0616 100644 --- a/packages/compiler/src/styles/analyze.ts +++ b/packages/compiler/src/styles/analyze.ts @@ -1,5 +1,5 @@ import ts from 'typescript'; -import type { JsxElementLike } from '../matchers'; +import type { JsxElementLike } from '../jsx'; /** * A single segment within a `className` attribute. Either a literal class diff --git a/packages/compiler/src/styles/tests/analyze.test.ts b/packages/compiler/src/styles/tests/analyze.test.ts index c9f0d27e..656e4c86 100644 --- a/packages/compiler/src/styles/tests/analyze.test.ts +++ b/packages/compiler/src/styles/tests/analyze.test.ts @@ -1,7 +1,7 @@ import ts from 'typescript'; import { describe, expect, it } from 'vitest'; import { compile } from '../../compile'; -import { react } from '../../config'; +import { jsx } from '../../config'; import type { StyleAttributeInfo, StyleAttributeSegmentsInfo, StyleSegment } from '../analyze'; import { analyzeStyles } from '../analyze'; @@ -9,7 +9,7 @@ async function collectSegments(source: string): Promise { const collected: StyleAttributeInfo[] = []; await compile(source, { config: { - target: react({ + target: jsx({ transforms: [ analyzeStyles({ visit: (info) => { @@ -25,7 +25,7 @@ async function collectSegments(source: string): Promise { } const compileWithTransform = (source: string, transform: ReturnType) => - compile(source, { config: { target: react({ transforms: [transform] }) } }); + compile(source, { config: { target: jsx({ transforms: [transform] }) } }); const collapse = (s: string): string => s.replace(/\s+/g, ''); @@ -109,7 +109,7 @@ describe('analyzeStyles — decomposition', () => { const infos: StyleAttributeInfo[] = []; await compile(source, { config: { - target: react({ + target: jsx({ transforms: [ analyzeStyles({ mergeFn: 'twMerge', diff --git a/packages/compiler/src/tailwind/naming.ts b/packages/compiler/src/tailwind/naming.ts index a61f8729..cfdb9b60 100644 --- a/packages/compiler/src/tailwind/naming.ts +++ b/packages/compiler/src/tailwind/naming.ts @@ -1,7 +1,6 @@ import { kebabCase } from '@videojs/utils/string'; import { type DiagnosticLocation, diagnosticLocationFromNode } from '../diagnostics'; -import type { JsxElementLike } from '../matchers'; -import { tagName } from '../matchers'; +import { type JsxElementLike, tagName } from '../jsx'; import type { StyleSegment } from '../styles'; /** Result of deriving a CSS class name for a JSX element. */ diff --git a/packages/compiler/src/tailwind/plugin.ts b/packages/compiler/src/tailwind/plugin.ts index 83b0440f..dadaa121 100644 --- a/packages/compiler/src/tailwind/plugin.ts +++ b/packages/compiler/src/tailwind/plugin.ts @@ -3,7 +3,7 @@ import { basename, dirname, extname, isAbsolute, join, resolve as resolvePath } import ts from 'typescript'; import type { CompilerContext, StylePipeline } from '../config'; import { diagnosticLocationFromNode } from '../diagnostics'; -import { tagName } from '../matchers'; +import { tagName } from '../jsx'; import { analyzeStyles, type StyleSegment, type StyleVisitor } from '../styles'; import { decompose, type UtilityCss } from './decompose'; import { type DesignSystem, loadDesignSystem } from './design-system'; diff --git a/packages/compiler/src/tailwind/tests/naming.test.ts b/packages/compiler/src/tailwind/tests/naming.test.ts index 020423ba..8a8ee6ea 100644 --- a/packages/compiler/src/tailwind/tests/naming.test.ts +++ b/packages/compiler/src/tailwind/tests/naming.test.ts @@ -1,6 +1,6 @@ import ts from 'typescript'; import { describe, expect, it } from 'vitest'; -import type { JsxElementLike } from '../../matchers'; +import type { JsxElementLike } from '../../jsx'; import { parse } from '../../parse'; import type { StyleSegment } from '../../styles'; import { DiagnosticError, deriveClassName } from '../naming'; diff --git a/packages/compiler/src/tailwind/tests/plugin.test.ts b/packages/compiler/src/tailwind/tests/plugin.test.ts index ceb41b03..d829b461 100644 --- a/packages/compiler/src/tailwind/tests/plugin.test.ts +++ b/packages/compiler/src/tailwind/tests/plugin.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { compile as compileSource } from '../../compile'; -import { type CompilerTransform, react } from '../../config'; +import { type CompilerTransform, jsx } from '../../config'; import type { DesignSystem } from '../design-system'; import { loadDesignSystem } from '../design-system'; import type { CompiledRule } from '../emit'; @@ -50,10 +50,10 @@ const compile = ( source: string, options: { filename?: string | undefined; - target?: 'react' | undefined; + target?: 'jsx' | undefined; plugins?: readonly CompilerTransform[] | undefined; } = {} -) => compileSource(source, { filename: options.filename, config: { target: react({ transforms: options.plugins }) } }); +) => compileSource(source, { filename: options.filename, config: { target: jsx({ transforms: options.plugins }) } }); const compileTailwind = (source: string, options: Parameters[0], filename?: string) => compileSource(source, { filename, config: { styles: tailwind(options) } }); @@ -62,7 +62,7 @@ describe('tailwindPlugin — mode: preserve', () => { it('preserves static className values', async () => { const source = `function App(){ return ; }`; const { code } = await compile(source, { - target: 'react', + target: 'jsx', plugins: [tailwindPlugin({ design, mode: 'preserve' })], }); expect(code).toContain('"flex items-center"'); @@ -72,7 +72,7 @@ describe('tailwindPlugin — mode: preserve', () => { const source = `function App(){ return ; }`; let called = 0; await compile(source, { - target: 'react', + target: 'jsx', plugins: [ tailwindPlugin({ design, @@ -91,7 +91,7 @@ describe('tailwindPlugin — mode: inline', () => { it('preserves static className values', async () => { const source = `function App(){ return ; }`; const { code } = await compile(source, { - target: 'react', + target: 'jsx', plugins: [tailwindPlugin({ design, mode: 'inline' })], }); expect(code).toContain('"flex items-center"'); @@ -100,7 +100,7 @@ describe('tailwindPlugin — mode: inline', () => { it('folds static cn calls', async () => { const source = `function App(){ return ; }`; const { code } = await compile(source, { - target: 'react', + target: 'jsx', plugins: [tailwindPlugin({ design, mode: 'inline' })], }); expect(code).toContain('"flex items-center gap-2"'); @@ -119,7 +119,7 @@ function App(){ return ; }`; const sourcePath = writeFixture('skin.tsx', source); const { code } = await compile(source, { - target: 'react', + target: 'jsx', filename: sourcePath, plugins: [tailwindPlugin({ design, mode: 'inline', sourcePath })], }); @@ -131,7 +131,7 @@ function App(){ return ; }`; function App(){ return ; }`; const sourcePath = writeFixture('skin.tsx', source); const { code } = await compile(source, { - target: 'react', + target: 'jsx', filename: sourcePath, plugins: [tailwindPlugin({ design, mode: 'inline', sourcePath })], }); @@ -141,7 +141,7 @@ function App(){ return ; }`; it('leaves dynamic cn calls untouched', async () => { const source = `function App(){ return ; }`; const { code } = await compile(source, { - target: 'react', + target: 'jsx', plugins: [tailwindPlugin({ design, mode: 'inline' })], }); expect(code).toMatch(/cn\(/); @@ -152,7 +152,7 @@ describe('tailwindPlugin — mode: extract', () => { it('replaces static utilities with component class names', async () => { const source = `function App(){ return ; }`; const { code } = await compile(source, { - target: 'react', + target: 'jsx', plugins: [tailwindPlugin({ design, mode: 'extract' })], }); expect(code).toContain('"play-button"'); @@ -162,7 +162,7 @@ describe('tailwindPlugin — mode: extract', () => { it('preserves group marker classes', async () => { const source = `function App(){ return ; }`; const { code } = await compile(source, { - target: 'react', + target: 'jsx', plugins: [tailwindPlugin({ design, mode: 'extract' })], }); // `group` produces no declarations but is required by descendant @@ -174,7 +174,7 @@ describe('tailwindPlugin — mode: extract', () => { const source = `function App(){ return ; }`; let captured: readonly CompiledRule[] | undefined; const { code } = await compile(source, { - target: 'react', + target: 'jsx', plugins: [ tailwindPlugin({ design, @@ -194,7 +194,7 @@ describe('tailwindPlugin — mode: extract', () => { it('keeps dynamic cn expressions', async () => { const source = `function App(){ return ; }`; const { code } = await compile(source, { - target: 'react', + target: 'jsx', plugins: [tailwindPlugin({ design, mode: 'extract' })], }); expect(code).toMatch(/cn\("play-button group",\s*extra\)/); @@ -204,7 +204,7 @@ describe('tailwindPlugin — mode: extract', () => { const source = `function App(){ return
; }`; await expect( compile(source, { - target: 'react', + target: 'jsx', plugins: [tailwindPlugin({ design, mode: 'extract' })], }) ).rejects.toThrow(/class name 'seek-icon' is derived from elements with different styles/); @@ -213,7 +213,7 @@ describe('tailwindPlugin — mode: extract', () => { it('allows preserved marker classes next to matching generated styles', async () => { const source = `function App(){ return
; }`; const { code } = await compile(source, { - target: 'react', + target: 'jsx', plugins: [tailwindPlugin({ design, mode: 'extract' })], }); expect(code).toContain('"menu-item legacy-submenu"'); @@ -223,7 +223,7 @@ describe('tailwindPlugin — mode: extract', () => { it('handles duplicate component styles', async () => { const source = `function App(){ return
; }`; const { code } = await compile(source, { - target: 'react', + target: 'jsx', plugins: [tailwindPlugin({ design, mode: 'extract' })], }); expect(code).toContain('"play-button"'); @@ -232,7 +232,7 @@ describe('tailwindPlugin — mode: extract', () => { it('derives class names from style member expressions', async () => { const source = `function App(){ return
; }`; const { code } = await compile(source, { - target: 'react', + target: 'jsx', plugins: [tailwindPlugin({ design, mode: 'extract' })], }); expect(code).toContain('"buffering-indicator"'); @@ -250,7 +250,7 @@ function App(){ return
; }`; const { code } = await compile(source, { filename: sourcePath, - target: 'react', + target: 'jsx', plugins: [tailwindPlugin({ design, mode: 'extract', sourcePath })], }); expect(code).toContain('"slider-root"'); @@ -270,7 +270,7 @@ function App(){ return
{ const source = `function App(){ return
; }`; const { code } = await compile(source, { - target: 'react', + target: 'jsx', plugins: [tailwindPlugin({ design, mode: 'extract' })], }); expect(code).toContain('"button-group-start"'); @@ -289,7 +289,7 @@ function App(){ return
{ const source = `function App(){ return ; }`; const { code } = await compile(source, { - target: 'react', + target: 'jsx', plugins: [tailwindPlugin({ design, mode: 'extract' })], }); expect(code).toContain('"menu-item"'); @@ -306,7 +306,7 @@ function App(){ return { const source = `function App(){ return
; }`; const { code } = await compile(source, { - target: 'react', + target: 'jsx', plugins: [tailwindPlugin({ design, mode: 'extract' })], }); @@ -348,7 +348,7 @@ function App({ type, className }){ it('applies component class overrides', async () => { const source = `function App(){ return ; }`; const { code } = await compile(source, { - target: 'react', + target: 'jsx', plugins: [ tailwindPlugin({ design, @@ -363,7 +363,7 @@ function App({ type, className }){ it('applies transformed generated class names', async () => { const source = `function App(){ return ; }`; const { code } = await compile(source, { - target: 'react', + target: 'jsx', plugins: [ tailwindPlugin({ design, @@ -379,7 +379,7 @@ function App({ type, className }){ const source = `function App(){ return ; }`; let captured: readonly CompiledRule[] | undefined; await compile(source, { - target: 'react', + target: 'jsx', plugins: [ tailwindPlugin({ design, @@ -400,7 +400,7 @@ function App({ type, className }){ const source = `function App(){ return ; }`; let captured: readonly CompiledRule[] | undefined; await compile(source, { - target: 'react', + target: 'jsx', plugins: [ tailwindPlugin({ design, @@ -429,7 +429,7 @@ function App(){ return ; }`; let captured: readonly CompiledRule[] | undefined; await compile(source, { - target: 'react', + target: 'jsx', filename: sourcePath, plugins: [ tailwindPlugin({ @@ -459,7 +459,7 @@ function App(){ return ; }`; let captured: readonly CompiledRule[] | undefined; const { code } = await compile(source, { - target: 'react', + target: 'jsx', filename: sourcePath, plugins: [ tailwindPlugin({ @@ -482,7 +482,7 @@ function App(){ return ; }`; const source = `function App(){ return ; }`; let captured: readonly CompiledRule[] | undefined; await compile(source, { - target: 'react', + target: 'jsx', plugins: [ tailwindPlugin({ design, @@ -501,7 +501,7 @@ function App(){ return ; }`; const source = `function App(){ return ; }`; let captured: readonly CompiledRule[] | undefined; const { code } = await compile(source, { - target: 'react', + target: 'jsx', plugins: [ tailwindPlugin({ design, @@ -531,7 +531,7 @@ function App(){ return ; }`; let captured: readonly CompiledRule[] | undefined; const { code } = await compile(source, { - target: 'react', + target: 'jsx', filename: sourcePath, plugins: [ tailwindPlugin({ @@ -553,7 +553,7 @@ function App(){ return ; }`; it('preserves dynamic cn suffixes after extraction', async () => { const source = `function App({ extra }){ return ; }`; const { code } = await compile(source, { - target: 'react', + target: 'jsx', plugins: [tailwindPlugin({ design, mode: 'extract' })], }); expect(code).toMatch(/cn\("play-button",\s*extra\)/); @@ -565,7 +565,7 @@ function App(){ return ; }`; }`; let captured: readonly CompiledRule[] | undefined; const { code } = await compile(source, { - target: 'react', + target: 'jsx', plugins: [ tailwindPlugin({ design, diff --git a/packages/compiler/src/tests/compile.test.ts b/packages/compiler/src/tests/compile.test.ts index 04339c26..43b5d96f 100644 --- a/packages/compiler/src/tests/compile.test.ts +++ b/packages/compiler/src/tests/compile.test.ts @@ -1,8 +1,21 @@ import { describe, expect, it } from 'vitest'; -import { accessPath, compile, jsxExpression, type ReactTargetOptions, react, replaceJsxChild } from '..'; -import { parse } from '../ast'; -import { anyTag, byTag, hasChild } from '../matchers'; -import { addProp, childAsProp, replace, wrap } from '../react'; +import { compile } from '..'; +import { + accessPath, + addProp, + anyTag, + byTag, + childAsProp, + hasChild, + type JsxElementLike, + type JsxTargetOptions, + jsx, + jsxExpression, + replace, + replaceJsxChild, + wrap, +} from '../jsx'; +import { parse } from '../parse'; /** * The TS printer emits `` (space before slash) — collapse all whitespace @@ -10,8 +23,8 @@ import { addProp, childAsProp, replace, wrap } from '../react'; */ const collapse = (s: string): string => s.replace(/\s+/g, ''); -const compileReact = (source: string, options: ReactTargetOptions = {}) => - compile(source, { config: { target: react(options) } }); +const compileJsx = (source: string, options: JsxTargetOptions = {}) => + compile(source, { config: { target: jsx(options) } }); describe('parse', () => { it('produces a TSX SourceFile with parent pointers set', () => { @@ -24,7 +37,7 @@ describe('parse', () => { describe('compile (no transforms)', () => { it('round-trips a simple TSX module', async () => { const source = `import { Foo } from 'bar';\nexport function App() { return ; }\n`; - const { code } = await compileReact(source); + const { code } = await compileJsx(source); // Identifier and JSX preserved; quote/whitespace style is whatever the printer decides. expect(code).toContain('Foo'); expect(code).toContain('bar'); @@ -35,16 +48,16 @@ describe('compile (no transforms)', () => { describe('compile (transformImports — bare-string rule)', () => { it('rewrites the module specifier and leaves identifiers untouched', async () => { const source = `import { PlayIcon } from '@videojs/icons/components';\nconst _x = PlayIcon;`; - const { code } = await compileReact(source, { - imports: { '@videojs/icons/components': '@videojs/icons/react' }, + const { code } = await compileJsx(source, { + imports: { '@videojs/icons/components': '@videojs/icons/jsx' }, }); - expect(code).toContain(`import { PlayIcon } from "@videojs/icons/react"`); + expect(code).toContain(`import { PlayIcon } from "@videojs/icons/jsx"`); }); it('leaves unrelated imports untouched', async () => { const source = `import { Other } from 'unrelated';\nimport { PlayIcon } from '@videojs/icons/components';\nconst _ = [Other, PlayIcon];`; - const { code } = await compileReact(source, { - imports: { '@videojs/icons/components': '@videojs/icons/react' }, + const { code } = await compileJsx(source, { + imports: { '@videojs/icons/components': '@videojs/icons/jsx' }, }); expect(code).toMatch(/from ['"]unrelated['"]/); expect(code).toContain('PlayIcon'); @@ -54,7 +67,7 @@ describe('compile (transformImports — bare-string rule)', () => { describe('compile (transformImports — function rule)', () => { it('rewrites per-identifier source and bucket-merges by resolved target', async () => { const source = `import { Alpha, Beta } from '@fixture/components';\nconst _ = [Alpha, Beta];`; - const { code } = await compileReact(source, { + const { code } = await compileJsx(source, { imports: { '@fixture/components': (name) => ({ source: `./ui/${name.toLowerCase()}`, name }), }, @@ -65,7 +78,7 @@ describe('compile (transformImports — function rule)', () => { it('renames identifiers when the rule returns a different `name`', async () => { const source = `import { OldName } from 'src';\nconst _ = OldName;`; - const { code } = await compileReact(source, { + const { code } = await compileJsx(source, { imports: { src: (_name) => ({ source: 'dst', name: 'NewName' }) }, }); expect(code).toContain(`import { NewName as OldName } from "dst"`); @@ -75,7 +88,7 @@ describe('compile (transformImports — function rule)', () => { describe('replace', () => { it('substitutes a matched element with a new tag and adds the import', async () => { const source = `function App(){ return ; }`; - const { code } = await compileReact(source, { + const { code } = await compileJsx(source, { transforms: [replace({ match: byTag('Old'), with: { source: 'pkg', name: 'New' } })], }); expect(code).toContain(` { it('preserves children when matching an open element', async () => { const source = `function App(){ return ; }`; - const { code } = await compileReact(source, { + const { code } = await compileJsx(source, { transforms: [replace({ match: byTag('Old'), with: { source: 'pkg', name: 'New' } })], }); expect(collapse(code)).toContain(collapse(``)); @@ -94,7 +107,7 @@ describe('replace', () => { describe('wrap', () => { it('wraps a matched element with a new tag and adds the import', async () => { const source = `function App(){ return ; }`; - const { code } = await compileReact(source, { + const { code } = await compileJsx(source, { transforms: [wrap({ match: byTag('Inner'), with: { source: 'pkg', name: 'Outer' } })], }); expect(collapse(code)).toContain(collapse(``)); @@ -105,7 +118,7 @@ describe('wrap', () => { describe('childAsProp', () => { it('lifts a single JSX-element child into the named prop', async () => { const source = `function App(){ return ; }`; - const { code } = await compileReact(source, { + const { code } = await compileJsx(source, { transforms: [childAsProp({ match: byTag('T'), prop: 'render' })], }); expect(collapse(code)).toContain(collapse(`}/>`)); @@ -113,7 +126,7 @@ describe('childAsProp', () => { it('skips when prop is already set', async () => { const source = `function App(){ return }>; }`; - const { code } = await compileReact(source, { + const { code } = await compileJsx(source, { transforms: [childAsProp({ match: byTag('T'), prop: 'render' })], }); expect(collapse(code)).toContain(collapse(``)); @@ -122,7 +135,7 @@ describe('childAsProp', () => { it('skips when there are multiple JSX-element children', async () => { const source = `function App(){ return ; }`; - const { code } = await compileReact(source, { + const { code } = await compileJsx(source, { transforms: [childAsProp({ match: byTag('T'), prop: 'render' })], }); expect(collapse(code)).toContain(collapse(``)); @@ -130,7 +143,7 @@ describe('childAsProp', () => { it('matches an array of tags via anyTag', async () => { const source = `function App(){ return <>; }`; - const { code } = await compileReact(source, { + const { code } = await compileJsx(source, { transforms: [childAsProp({ match: anyTag(['T1', 'T2']), prop: 'render' })], }); const trimmed = collapse(code); @@ -142,7 +155,7 @@ describe('childAsProp', () => { describe('replaceJsxChild', () => { it('replaces matched JSX children with expression helpers', async () => { const source = `function App({ values }){ return ; }`; - const { code } = await compileReact(source, { + const { code } = await compileJsx(source, { transforms: [ replaceJsxChild({ match: byTag('Token'), @@ -158,7 +171,7 @@ describe('replaceJsxChild', () => { describe('addProp', () => { it('emits a JSX value by default and adds the import', async () => { const source = `function App(){ return ; }`; - const { code } = await compileReact(source, { + const { code } = await compileJsx(source, { transforms: [ addProp({ match: byTag('PlayButton'), prop: 'render', value: { source: './button', name: 'Button' } }), ], @@ -169,7 +182,7 @@ describe('addProp', () => { it('emits a bare reference when kind is "ref"', async () => { const source = `function App(){ return ; }`; - const { code } = await compileReact(source, { + const { code } = await compileJsx(source, { transforms: [ addProp({ match: byTag('PlayButton'), @@ -183,7 +196,7 @@ describe('addProp', () => { it('skips elements where the prop is already set', async () => { const source = `function App(){ return }/>; }`; - const { code } = await compileReact(source, { + const { code } = await compileJsx(source, { transforms: [ addProp({ match: byTag('PlayButton'), prop: 'render', value: { source: './button', name: 'Button' } }), ], @@ -194,7 +207,7 @@ describe('addProp', () => { it('overwrites the existing prop when overwrite is true', async () => { const source = `function App(){ return }/>; }`; - const { code } = await compileReact(source, { + const { code } = await compileJsx(source, { transforms: [ addProp({ match: byTag('PlayButton'), @@ -211,7 +224,7 @@ describe('addProp', () => { describe('matchers', () => { it('byTag supports dotted tags', async () => { const source = `function App(){ return ; }`; - const { code } = await compileReact(source, { + const { code } = await compileJsx(source, { transforms: [replace({ match: byTag('Popover.Root'), with: { source: 'pkg', name: 'NewRoot' } })], }); expect(code).toContain(` { it('byTag honours `when` refinement', async () => { const source = `function App(){ return <>; }`; - const isA1 = (node: import('../matchers').JsxElementLike) => { + const isA1 = (node: JsxElementLike) => { const attrs = 'attributes' in node ? node.attributes : (node as never); const props = (attrs as { properties?: ReadonlyArray<{ initializer?: { text?: string } }> }).properties ?? []; return props.some((p) => p.initializer?.text === '1'); }; - const { code } = await compileReact(source, { + const { code } = await compileJsx(source, { transforms: [replace({ match: byTag('Foo', { when: isA1 }), with: { source: 'pkg', name: 'Bar' } })], }); expect(code).toContain(` { it('hasChild matches direct children only by default', async () => { const source = `function App(){ return <>
; }`; - const { code } = await compileReact(source, { + const { code } = await compileJsx(source, { transforms: [replace({ match: byTag('A', { when: hasChild(byTag('B')) }), with: { source: 'p', name: 'Z' } })], }); // First has direct child → replaced; second has only a nested → not replaced. @@ -243,7 +256,7 @@ describe('matchers', () => { it('hasChild with deep:true matches descendants', async () => { const source = `function App(){ return
; }`; - const { code } = await compileReact(source, { + const { code } = await compileJsx(source, { transforms: [ replace({ match: byTag('A', { when: hasChild(byTag('B'), { deep: true }) }), @@ -258,7 +271,7 @@ describe('matchers', () => { const source = `function App(){ return <>; }`; - const { code } = await compileReact(source, { + const { code } = await compileJsx(source, { transforms: [ replace({ match: byTag('Outer', { when: hasChild(byTag('Inner', { when: hasChild(byTag('Target')) })) }), diff --git a/packages/compiler/src/tests/diagnostics.test.ts b/packages/compiler/src/tests/diagnostics.test.ts index fc7edf8a..09c94ea9 100644 --- a/packages/compiler/src/tests/diagnostics.test.ts +++ b/packages/compiler/src/tests/diagnostics.test.ts @@ -1,12 +1,13 @@ import type ts from 'typescript'; import { describe, expect, it } from 'vitest'; -import { CompilerError, compile, react } from '..'; +import { CompilerError, compile } from '..'; import { diagnosticLocationFromNode, formatCompilerDiagnostic, formatCompilerDiagnosticJsonLine, formatDiagnosticSummaryJsonLine, } from '../diagnostics'; +import { jsx } from '../jsx'; import { DiagnosticError } from '../tailwind'; describe('formatCompilerDiagnostic', () => { @@ -96,7 +97,7 @@ describe('CompilerError diagnostics', () => { try { await compile(`export function App(){ return ; }`, { filename: '/workspace/skin.tsx', - config: { target: react({ transforms: [transform()] }) }, + config: { target: jsx({ transforms: [transform()] }) }, }); throw new Error('Expected compile to fail'); } catch (error) { diff --git a/packages/compiler/src/tests/integration.test.ts b/packages/compiler/src/tests/integration.test.ts index c8cef653..29f1c843 100644 --- a/packages/compiler/src/tests/integration.test.ts +++ b/packages/compiler/src/tests/integration.test.ts @@ -2,9 +2,9 @@ import { readFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { beforeAll, describe, expect, it } from 'vitest'; -import { compile, type ImportRule, react } from '..'; -import { anyTag, byTag, hasChild } from '../matchers'; -import { childAsProp, replace } from '../react'; +import { compile } from '..'; +import { anyTag, byTag, childAsProp, hasChild, jsx, replace } from '../jsx'; +import type { ImportRule } from '../transforms'; const __dirname = dirname(fileURLToPath(import.meta.url)); const skinSource = resolve(__dirname, 'fixtures/video-skin.tsx'); @@ -12,12 +12,12 @@ const skinSource = resolve(__dirname, 'fixtures/video-skin.tsx'); /** * End-to-end smoke test: feed a representative constrained-JSX video skin * (vendored under `fixtures/`) through `compile()` with the same shape - * a React package build hook uses, and sanity-check the output's structural + * a package build hook uses, and sanity-check the output's structural * shape. Snapshot-style assertions intentionally use `.toContain` over a full * snapshot to keep the test resilient to incidental whitespace differences * from the TS printer. */ -describe('integration: default/video skin → React', () => { +describe('integration: default/video skin → JSX', () => { const source = readFileSync(skinSource, 'utf8'); let code = ''; @@ -26,14 +26,14 @@ describe('integration: default/video skin → React', () => { source: `./src/ui/${name.replace(/^[A-Z]/, (m) => m.toLowerCase()).replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}`, name, }), - '@fixture/icons/components': '@fixture/icons/react', + '@fixture/icons/components': '@fixture/icons/jsx', '../tailwind': '@videojs/skins/default/tailwind', }; beforeAll(async () => { const result = await compile(source, { config: { - target: react({ + target: jsx({ imports, transforms: [ replace({ @@ -60,7 +60,7 @@ describe('integration: default/video skin → React', () => { }); it('rewrites icon component imports', () => { - expect(code).toContain('@fixture/icons/react'); + expect(code).toContain('@fixture/icons/jsx'); expect(code).not.toContain('@fixture/icons/components'); }); diff --git a/packages/compiler/src/transforms/imports.ts b/packages/compiler/src/transforms/imports.ts index 87d0e16c..8d48321a 100644 --- a/packages/compiler/src/transforms/imports.ts +++ b/packages/compiler/src/transforms/imports.ts @@ -3,7 +3,7 @@ import ts from 'typescript'; /** * Per-identifier rewrite target. `source` may be either a bare specifier - * (`@videojs/icons/react`) or a relative path. Relative paths are resolved + * (`@fixture/icons/jsx`) or a relative path. Relative paths are resolved * against the configured `configDir` and re-projected as a relative path from * the output file at print time. */ diff --git a/packages/compiler/src/transforms/index.ts b/packages/compiler/src/transforms/index.ts new file mode 100644 index 00000000..c4da8d53 --- /dev/null +++ b/packages/compiler/src/transforms/index.ts @@ -0,0 +1,10 @@ +export { type AddImportContext, type AddImportRef, addNamedImport } from './add-import'; +export { dropUnusedImports } from './drop-unused-imports'; +export { dropUnusedLocals } from './drop-unused-locals'; +export { + type ImportRef, + type ImportRewriteOptions, + type ImportRule, + resolveRelative, + transformImports, +} from './imports'; diff --git a/packages/compiler/src/transforms/tests/drop-unused-imports.test.ts b/packages/compiler/src/transforms/tests/drop-unused-imports.test.ts index 85d626b9..82b814ce 100644 --- a/packages/compiler/src/transforms/tests/drop-unused-imports.test.ts +++ b/packages/compiler/src/transforms/tests/drop-unused-imports.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it } from 'vitest'; import { compile } from '../../compile'; -import { react } from '../../config'; +import { jsx } from '../../config'; import { dropUnusedImports } from '../drop-unused-imports'; const wrap = async (source: string): Promise => - (await compile(source, { config: { target: react({ transforms: [dropUnusedImports()] }) } })).code; + (await compile(source, { config: { target: jsx({ transforms: [dropUnusedImports()] }) } })).code; describe('dropUnusedImports', () => { it('does not count intrinsic JSX tag names as import references', async () => { diff --git a/packages/compiler/src/transforms/tests/drop-unused-locals.test.ts b/packages/compiler/src/transforms/tests/drop-unused-locals.test.ts index 9b5d64bc..451920a8 100644 --- a/packages/compiler/src/transforms/tests/drop-unused-locals.test.ts +++ b/packages/compiler/src/transforms/tests/drop-unused-locals.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it } from 'vitest'; import { compile } from '../../compile'; -import { react } from '../../config'; +import { jsx } from '../../config'; import { dropUnusedLocals } from '../drop-unused-locals'; const wrap = async (source: string): Promise => - (await compile(source, { config: { target: react({ transforms: [dropUnusedLocals()] }) } })).code; + (await compile(source, { config: { target: jsx({ transforms: [dropUnusedLocals()] }) } })).code; describe('dropUnusedLocals', () => { it('drops an unused cn() local', async () => { diff --git a/packages/compiler/tsdown.config.ts b/packages/compiler/tsdown.config.ts index d1765729..b2780a36 100644 --- a/packages/compiler/tsdown.config.ts +++ b/packages/compiler/tsdown.config.ts @@ -4,12 +4,12 @@ export default defineConfig({ entry: { index: './src/index.ts', cli: './src/cli.ts', - 'plugins/vite': './src/plugins/vite.ts', - 'ast/index': './src/ast/index.ts', - 'matchers/index': './src/matchers/index.ts', - 'react/index': './src/react/index.ts', + 'bundlers/vite': './src/bundlers/vite.ts', + diagnostics: './src/diagnostics.ts', + 'jsx/index': './src/jsx/index.ts', 'styles/index': './src/styles/index.ts', 'tailwind/index': './src/tailwind/index.ts', + 'transforms/index': './src/transforms/index.ts', }, platform: 'neutral', format: 'es',