diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 0c08f7d9..c9712177 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -75,35 +75,34 @@ export { type StyleVisitorResult, } from './styles'; export { - type CreateHelpers, - type EditHelpers, - type FunctionDeclarationContext, - type FunctionDeclarationEdit, - type FunctionDeclarationEditOptions, + type ConstStatementOptions, type FunctionPropSpec, + type FunctionSelection, + type ImportOptions, type ImportReference, - type InterfaceDeclarationContext, - type InterfaceDeclarationEdit, - type InterfaceDeclarationEditOptions, type InterfacePropertyContext, - type InterfacePropertyEdit, - type InterfacePropertyEditOptions, + type InterfacePropertySelection, + type InterfaceSelection, type JsxElementContext, - type JsxElementEdit, - type JsxElementEditOptions, + type JsxElementReplacement, + type JsxElementSelection, + type JsxHelpers, type JsxPropContext, - type JsxPropEditOptions, + type JsxPropsSelection, type JsxPropsSpec, type JsxPropValue, - type MatchHelpers, type MatchPredicate, - type RefHelpers, + type ModuleSelection, + type StatementHelpers, + type StatementSpec, type TransformCallback, type TransformHelpers, type TransformOptions, type TransformStep, + type TypeHelpers, transform, - type ValueOnlyIfOptions, + type ValueArrayOptions, + type ValueHelpers, type ValueReference, } from './transform'; export { diff --git a/packages/compiler/src/tests/transform.test.ts b/packages/compiler/src/tests/transform.test.ts index 148a9245..ee19fdd7 100644 --- a/packages/compiler/src/tests/transform.test.ts +++ b/packages/compiler/src/tests/transform.test.ts @@ -34,67 +34,38 @@ export function Skin({ children, className }: SkinProps) { config: { plugins: [ transform((code) => { - const cn = code.ref.import('@fixture/style', 'cn'); - const BaseSkinProps = code.ref.import('@fixture/react', 'BaseSkinProps', { type: true }); - const Button = code.ref.import('@fixture/renderers', 'Button'); - const SliderTrack = code.ref.import('@fixture/renderers', 'SliderTrack'); - const isString = code.ref.import('@fixture/predicate', 'isString'); - const ReactNode = code.ref.import('react', 'ReactNode', { type: true }); + const cn = code.import('@fixture/style', 'cn'); + const BaseSkinProps = code.import('@fixture/react', 'BaseSkinProps', { type: true }); + const Button = code.import('@fixture/renderers', 'Button'); + const SliderTrack = code.import('@fixture/renderers', 'SliderTrack'); + const isString = code.import('@fixture/predicate', 'isString'); + const ReactNode = code.import('react', 'ReactNode', { type: true }); return [ - code.edit.import.rewrite({ '@fixture/core': '@fixture/react' }), - code.edit.jsx.element({ - when: code.match.jsx.tag('Tooltip.Trigger'), - transform: code.edit.jsx.moveChildToProp('render'), - }), - code.edit.jsx.element({ - when: code.match.jsx.tag('Controls.Root'), - transform: code.edit.jsx.addProp('data-controls', ''), - }), - code.edit.jsx.element({ - when: code.match.jsx.tag('Container'), - transform: code.edit.jsx.addPropsSpread('rest'), - }), - code.edit.jsx.element({ - when: code.match.jsx.tag('Poster'), - transform: () => { - return code.create.jsx.renderIf( - 'poster', - code.create.jsx.element('Poster', { - src: code.create.value.onlyIf({ value: 'poster', condition: isString }), - }) - ); - }, - }), - code.edit.jsx.element({ - when: code.match.jsx.tag('PlayButton'), - transform: code.edit.jsx.addProp('render', code.create.jsx.element(Button)), - }), - code.edit.jsx.element({ - when: code.match.jsx.tag(/^Slider\.(Track)$/), - transform: (element, context) => { - const part = context.tagName.split('.')[1]; - return part === 'Track' ? code.edit.jsx.replaceTag(SliderTrack)(element, context) : undefined; - }, - }), - code.edit.jsx.prop({ - when: code.match.all(code.match.jsx.prop('className'), code.match.value.array()), - transform: ({ value }) => code.create.value.call(cn, code.create.value.arrayItems(value)), - }), - code.edit.interface.declaration({ - when: code.match.interface.name('SkinProps'), - transform: code.edit.interface.extends(BaseSkinProps), - }), - code.edit.interface.property({ - when: code.match.all(code.match.interface.name(/Props$/), code.match.interface.property('children')), - transform: code.edit.interface.setType(() => - code.create.type.union(code.create.type.named(ReactNode), code.create.type.undefined()) - ), - }), - code.edit.function.declaration({ - when: code.match.function.name('Skin'), - transform: code.edit.function.addProps(['poster', { name: 'rest', spread: true }]), - }), + code.imports({ '@fixture/core': '@fixture/react' }), + code.jsx.element('Tooltip.Trigger').childToProp('render'), + code.jsx.element('Controls.Root').addProp('data-controls', ''), + code.jsx.element('Container').spreadProps('rest'), + code.jsx.element('Poster').replace(() => + code.jsx.if( + 'poster', + code.jsx.create('Poster', { + src: code.value.when('poster', isString), + }) + ) + ), + code.jsx.element('PlayButton').addProp('render', code.jsx.create(Button)), + code.jsx.element(/^Slider\.(Track)$/).replace(SliderTrack), + code.jsx + .props('className') + .where(code.value.isArray()) + .replace(({ value }) => code.value.call(cn, code.value.arrayItems(value))), + code.interface('SkinProps').extends(BaseSkinProps), + code + .interface(/Props$/) + .property('children') + .setType(() => code.type.union(code.type.named(ReactNode), code.type.undefined())), + code.function('Skin').addProps(['poster', { name: 'rest', spread: true }]), ]; }), ], @@ -121,6 +92,67 @@ export function Skin({ children, className }: SkinProps) { expect(compact(result.code)).toContain(compact('')); expect(compact(result.code)).toContain(compact(' { + const source = `export function Skin(){ return ; }`; + + const result = await compile(source, { + config: { + plugins: [ + transform((code) => { + const Button = code.import('@fixture/renderers', 'Button', { default: true }); + + return [code.jsx.element('Action').addProp('render', code.jsx.create(Button))]; + }), + ], + }, + }); + + expect(result.code).toContain('import Button from "@fixture/renderers"'); + expect(compact(result.code)).toContain(compact('} />')); + }); + + it('adds module constants and function-scope statements', async () => { + const source = `import { Container } from '@fixture/core'; + +export function DefaultVideoSkin({ poster }) { + return ; +} +`; + + const result = await compile(source, { + config: { + plugins: [ + transform((code) => { + const usePoster = code.import('@fixture/react', 'usePoster'); + + return [ + code.module.prepend( + code.statement.const('TOP_ACTIONS', code.value.array([code.value.string('togglePaused')]), { + asConst: true, + export: true, + }) + ), + code + .function('DefaultVideoSkin') + .prepend(code.statement.const('posterState', code.value.call(usePoster, ['poster']))), + code + .function('DefaultVideoSkin') + .beforeReturn(code.statement.const('ready', code.value.call('Boolean', ['posterState']))), + ]; + }), + ], + }, + }); + + const compactCode = compact(result.code); + + expect(result.code.indexOf('import { Container }')).toBeLessThan(result.code.indexOf('export const TOP_ACTIONS')); + expect(result.code).toContain('import { usePoster } from "@fixture/react"'); + expect(compactCode).toContain(compact('export const TOP_ACTIONS = ["togglePaused"] as const;')); + expect(compactCode).toContain(compact('const posterState = usePoster(poster);')); + expect(compactCode).toContain(compact('const ready = Boolean(posterState);return ;')); + }); }); describe('compileProject', () => { @@ -137,14 +169,7 @@ describe('compileProject', () => { entryFileNames: '[name].tsx', banner: '// Generated\n', }, - plugins: [ - transform((code) => [ - code.edit.jsx.element({ - when: code.match.jsx.tag('Root'), - transform: code.edit.jsx.addProp('data-root', ''), - }), - ]), - ], + plugins: [transform((code) => [code.jsx.element('Root').addProp('data-root', '')])], }, { configDir: workDir } ); diff --git a/packages/compiler/src/transform.ts b/packages/compiler/src/transform.ts index 27b85ea5..2306f83f 100644 --- a/packages/compiler/src/transform.ts +++ b/packages/compiler/src/transform.ts @@ -8,11 +8,17 @@ import { type ImportRewriteOptions, type ImportRule, transformImports } from './ export interface ImportReference { readonly source: string; readonly name: string; + readonly default?: boolean | undefined; readonly type?: boolean | undefined; } +export interface ImportOptions { + default?: boolean | undefined; + type?: boolean | undefined; +} + export interface RefHelpers { - import(source: string, name: string, options?: { type?: boolean | undefined }): ImportReference; + import(source: string, name: string, options?: ImportOptions): ImportReference; } export type ValueReference = string | ImportReference | ts.Expression; @@ -48,11 +54,14 @@ export interface MatchHelpers { export interface CreateHelpers { value: { and(left: ValueReference, right: ts.Expression): ts.BinaryExpression; + array(items: readonly ValueReference[], options?: ValueArrayOptions): ts.ArrayLiteralExpression | ts.AsExpression; arrayItems(value: ts.Expression): ts.Expression[]; - call(callee: ValueReference, args: readonly ts.Expression[]): ts.CallExpression; + call(callee: ValueReference, args: readonly ValueReference[]): ts.CallExpression; conditional(test: ts.Expression, whenTrue: ts.Expression, whenFalse: ts.Expression): ts.ConditionalExpression; identifier(value: string | ImportReference): ts.Identifier; + number(value: number): ts.NumericLiteral; onlyIf(options: ValueOnlyIfOptions): ts.ConditionalExpression; + string(value: string): ts.StringLiteral; undefined(): ts.Identifier; }; jsx: { @@ -93,11 +102,98 @@ export interface EditHelpers { }; } +export interface ValueHelpers { + and(left: ValueReference, right: ts.Expression): ts.BinaryExpression; + array(items: readonly ValueReference[], options?: ValueArrayOptions): ts.ArrayLiteralExpression | ts.AsExpression; + arrayItems(value: ts.Expression): ts.Expression[]; + call(callee: ValueReference, args: readonly ValueReference[]): ts.CallExpression; + conditional(test: ts.Expression, whenTrue: ts.Expression, whenFalse: ts.Expression): ts.ConditionalExpression; + identifier(value: string | ImportReference): ts.Identifier; + isArray(): MatchPredicate; + number(value: number): ts.NumericLiteral; + string(value: string): ts.StringLiteral; + when( + value: ValueReference, + condition: ValueReference, + fallback?: ValueReference | undefined + ): ts.ConditionalExpression; + undefined(): ts.Identifier; +} + +export interface JsxHelpers { + create(tag: string | ImportReference, props?: JsxPropsSpec): ts.JsxSelfClosingElement; + element(tag: string | RegExp): JsxElementSelection; + if(test: ValueReference, element: ts.Expression): ts.JsxExpression; + props(name: string): JsxPropsSelection; +} + +export interface ValueArrayOptions { + asConst?: boolean | undefined; +} + +export interface JsxElementSelection { + addProp(name: string, value?: JsxPropValue): CompilerTransform; + childToProp(prop: string): CompilerTransform; + replace(replacement: string | ImportReference | JsxElementReplacement): CompilerTransform; + spreadProps(value: ValueReference): CompilerTransform; +} + +export type JsxElementReplacement = (context: JsxElementContext) => ts.Node | undefined; + +export interface JsxPropsSelection { + replace(transform: (context: JsxPropContext) => ts.Expression | undefined): CompilerTransform; + where(predicate: MatchPredicate): JsxPropsSelection; +} + +export interface TypeHelpers { + named(value: string | ImportReference): ts.TypeReferenceNode; + union(...types: readonly ts.TypeNode[]): ts.UnionTypeNode; + undefined(): ts.KeywordTypeNode; +} + +export interface InterfaceSelection { + extends(value: string | ImportReference): CompilerTransform; + property(name: string): InterfacePropertySelection; +} + +export interface InterfacePropertySelection { + setType(type: (context: InterfacePropertyContext) => ts.TypeNode): CompilerTransform; +} + +export interface FunctionSelection { + addProps(props: readonly FunctionPropSpec[], parameterIndex?: number): CompilerTransform; + append(statements: StatementSpec): CompilerTransform; + beforeReturn(statements: StatementSpec): CompilerTransform; + prepend(statements: StatementSpec): CompilerTransform; +} + +export interface ModuleSelection { + append(statements: StatementSpec): CompilerTransform; + prepend(statements: StatementSpec): CompilerTransform; +} + +export interface StatementHelpers { + const(name: string, initializer: ValueReference, options?: ConstStatementOptions): ts.VariableStatement; +} + +export interface ConstStatementOptions { + asConst?: boolean | undefined; + export?: boolean | undefined; + type?: ts.TypeNode | undefined; +} + +export type StatementSpec = ts.Statement | readonly ts.Statement[]; + export interface TransformHelpers { - ref: RefHelpers; - match: MatchHelpers; - create: CreateHelpers; - edit: EditHelpers; + import(source: string, name: string, options?: ImportOptions): ImportReference; + imports(rules: Record): CompilerTransform; + function(name: string | RegExp): FunctionSelection; + interface(name: string | RegExp): InterfaceSelection; + jsx: JsxHelpers; + module: ModuleSelection; + statement: StatementHelpers; + type: TypeHelpers; + value: ValueHelpers; } export type TransformStep = CompilerTransform | CompilerPlugin | null | undefined | false; @@ -221,6 +317,7 @@ function createTransformHelpers(refs: MutableImportReference[], context: Compile [IMPORT_REF_SYMBOL]: true, source, name, + default: options.default, type: options.type, used: false, } as MutableImportReference; @@ -232,8 +329,146 @@ function createTransformHelpers(refs: MutableImportReference[], context: Compile const match = createMatchHelpers(); const create = createCreateHelpers(); const edit = createEditHelpers(context); + const statement = createStatementHelpers(); + const value = createValueHelpers(match, create); - return { ref, match, create, edit }; + return { + import: ref.import, + imports: (rules) => edit.import.rewrite(rules), + function: (name) => createFunctionSelection(name, match, edit), + interface: (name) => createInterfaceSelection(name, match, edit), + jsx: createJsxHelpers(match, create, edit), + module: createModuleSelection(), + statement, + type: create.type, + value, + }; +} + +function createValueHelpers(match: MatchHelpers, create: CreateHelpers): ValueHelpers { + return { + and: create.value.and, + array: create.value.array, + arrayItems: create.value.arrayItems, + call: create.value.call, + conditional: create.value.conditional, + identifier: create.value.identifier, + isArray: match.value.array, + number: create.value.number, + string: create.value.string, + when(value, condition, fallback) { + return create.value.onlyIf({ value, condition, ...(fallback === undefined ? {} : { fallback }) }); + }, + undefined: create.value.undefined, + }; +} + +function createStatementHelpers(): StatementHelpers { + return { + const(name, initializer, options = {}) { + const modifiers = options.export ? [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)] : undefined; + const expression = options.asConst ? asConst(valueFromReference(initializer)) : valueFromReference(initializer); + + return ts.factory.createVariableStatement( + modifiers, + ts.factory.createVariableDeclarationList( + [ + ts.factory.createVariableDeclaration( + ts.factory.createIdentifier(name), + undefined, + options.type, + expression + ), + ], + ts.NodeFlags.Const + ) + ); + }, + }; +} + +function createModuleSelection(): ModuleSelection { + return { + append: (statements) => editModuleStatements('append', statements), + prepend: (statements) => editModuleStatements('prepend', statements), + }; +} + +function createJsxHelpers(match: MatchHelpers, create: CreateHelpers, edit: EditHelpers): JsxHelpers { + return { + create: create.jsx.element, + element: (tag) => createJsxElementSelection(tag, match, edit), + if: create.jsx.renderIf, + props: (name) => createJsxPropsSelection(name, match, edit), + }; +} + +function createJsxElementSelection(tag: string | RegExp, match: MatchHelpers, edit: EditHelpers): JsxElementSelection { + const when = match.jsx.tag(tag); + return { + addProp: (name, value) => edit.jsx.element({ when, transform: edit.jsx.addProp(name, value) }), + childToProp: (prop) => edit.jsx.element({ when, transform: edit.jsx.moveChildToProp(prop) }), + replace(replacement) { + const transform: JsxElementEdit = + typeof replacement === 'function' + ? (_element, context) => replacement(context) + : edit.jsx.replaceTag(replacement); + return edit.jsx.element({ when, transform }); + }, + spreadProps: (value) => edit.jsx.element({ when, transform: edit.jsx.addPropsSpread(value) }), + }; +} + +function createJsxPropsSelection( + name: string, + match: MatchHelpers, + edit: EditHelpers, + predicates: readonly MatchPredicate[] = [] +): JsxPropsSelection { + const when = match.all(match.jsx.prop(name), ...predicates); + return { + replace: (transform) => edit.jsx.prop({ when, transform }), + where: (predicate) => createJsxPropsSelection(name, match, edit, [...predicates, predicate]), + }; +} + +function createInterfaceSelection(name: string | RegExp, match: MatchHelpers, edit: EditHelpers): InterfaceSelection { + const when = match.interface.name(name); + return { + extends: (value) => edit.interface.declaration({ when, transform: edit.interface.extends(value) }), + property: (property) => ({ + setType: (type) => + edit.interface.property({ + when: match.all(when, match.interface.property(property)), + transform: edit.interface.setType(type), + }), + }), + }; +} + +function createFunctionSelection(name: string | RegExp, match: MatchHelpers, edit: EditHelpers): FunctionSelection { + const when = match.function.name(name); + return { + addProps: (props, parameterIndex) => + edit.function.declaration({ when, transform: edit.function.addProps(props, parameterIndex) }), + append: (statements) => + edit.function.declaration({ + when, + transform: ({ function: declaration, factory }) => editFunctionBody(declaration, 'append', statements, factory), + }), + beforeReturn: (statements) => + edit.function.declaration({ + when, + transform: ({ function: declaration, factory }) => + editFunctionBody(declaration, 'beforeReturn', statements, factory), + }), + prepend: (statements) => + edit.function.declaration({ + when, + transform: ({ function: declaration, factory }) => + editFunctionBody(declaration, 'prepend', statements, factory), + }), + }; } function createMatchHelpers(): MatchHelpers { @@ -291,12 +526,16 @@ function createCreateHelpers(): CreateHelpers { right ); }, + array(items, options = {}) { + const array = ts.factory.createArrayLiteralExpression(items.map(valueFromReference)); + return options.asConst ? asConst(array) : array; + }, arrayItems(value) { if (!ts.isArrayLiteralExpression(value)) return []; return value.elements.filter((item): item is ts.Expression => !ts.isSpreadElement(item)); }, call(callee, args) { - return ts.factory.createCallExpression(valueFromReference(callee), undefined, [...args]); + return ts.factory.createCallExpression(valueFromReference(callee), undefined, args.map(valueFromReference)); }, conditional(test, whenTrue, whenFalse) { return ts.factory.createConditionalExpression( @@ -311,6 +550,9 @@ function createCreateHelpers(): CreateHelpers { if (isImportReference(value)) value.used = true; return ts.factory.createIdentifier(typeof value === 'string' ? value : value.name); }, + number(value) { + return ts.factory.createNumericLiteral(value); + }, onlyIf(options) { const value = valueFromReference(options.value); return ts.factory.createConditionalExpression( @@ -323,6 +565,9 @@ function createCreateHelpers(): CreateHelpers { : valueFromReference(options.fallback) ); }, + string(value) { + return ts.factory.createStringLiteral(value); + }, undefined() { return ts.factory.createIdentifier('undefined'); }, @@ -589,7 +834,12 @@ function materializeImportRefs(refs: readonly MutableImportReference[], context: let result = sourceFile; for (const ref of refs) { if (!ref.used) continue; - result = addNamedImport(result, { source: ref.source, name: ref.name, type: ref.type }, factory, context); + result = addNamedImport( + result, + { source: ref.source, name: ref.name, default: ref.default, type: ref.type }, + factory, + context + ); } return result; }; @@ -608,6 +858,82 @@ function pipeTransforms(transforms: readonly CompilerTransform[]): CompilerTrans }; } +function editModuleStatements(position: 'prepend' | 'append', statements: StatementSpec): CompilerTransform { + const nextStatements = normalizeStatements(statements); + return (context) => { + const factory = context.factory; + + return (sourceFile) => { + if (nextStatements.length === 0) return sourceFile; + + if (position === 'append') { + return factory.updateSourceFile(sourceFile, [...sourceFile.statements, ...nextStatements]); + } + + let insertIndex = 0; + for (let i = 0; i < sourceFile.statements.length; i++) { + if (ts.isImportDeclaration(sourceFile.statements[i]!)) insertIndex = i + 1; + } + + return factory.updateSourceFile(sourceFile, [ + ...sourceFile.statements.slice(0, insertIndex), + ...nextStatements, + ...sourceFile.statements.slice(insertIndex), + ]); + }; + }; +} + +function editFunctionBody( + declaration: ts.FunctionDeclaration, + position: 'prepend' | 'append' | 'beforeReturn', + statements: StatementSpec, + factory: ts.NodeFactory +): ts.FunctionDeclaration | undefined { + if (!declaration.body) return undefined; + + const nextStatements = normalizeStatements(statements); + if (nextStatements.length === 0) return undefined; + + let bodyStatements: ts.Statement[]; + if (position === 'prepend') { + bodyStatements = [...nextStatements, ...declaration.body.statements]; + } else if (position === 'append') { + bodyStatements = [...declaration.body.statements, ...nextStatements]; + } else { + const returnIndex = declaration.body.statements.findIndex(ts.isReturnStatement); + const insertIndex = returnIndex >= 0 ? returnIndex : declaration.body.statements.length; + bodyStatements = [ + ...declaration.body.statements.slice(0, insertIndex), + ...nextStatements, + ...declaration.body.statements.slice(insertIndex), + ]; + } + + return factory.updateFunctionDeclaration( + declaration, + declaration.modifiers, + declaration.asteriskToken, + declaration.name, + declaration.typeParameters, + declaration.parameters, + declaration.type, + factory.updateBlock(declaration.body, bodyStatements) + ); +} + +function normalizeStatements(statements: StatementSpec): ts.Statement[] { + return isStatementArray(statements) ? [...statements] : [statements]; +} + +function isStatementArray(statements: StatementSpec): statements is readonly ts.Statement[] { + return Array.isArray(statements); +} + +function asConst(expression: ts.Expression): ts.AsExpression { + return ts.factory.createAsExpression(expression, ts.factory.createTypeReferenceNode('const')); +} + function liftSingleChildToProp( element: JsxElementLike, prop: string, diff --git a/packages/compiler/src/transforms/add-import.ts b/packages/compiler/src/transforms/add-import.ts index 26adaa34..5f3fc04b 100644 --- a/packages/compiler/src/transforms/add-import.ts +++ b/packages/compiler/src/transforms/add-import.ts @@ -4,6 +4,7 @@ import { resolveRelative } from './imports'; export interface AddImportRef { source: string; name: string; + default?: boolean | undefined; type?: boolean | undefined; } @@ -36,6 +37,25 @@ export function addNamedImport( if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue; if (stmt.moduleSpecifier.text !== target) continue; const clause = stmt.importClause; + if (ref.default) { + if (!clause) continue; + if (clause.isTypeOnly !== Boolean(ref.type)) continue; + if (clause.name?.text === ref.name) return sourceFile; + if (clause.name) continue; + + const updated = factory.updateImportDeclaration( + stmt, + stmt.modifiers, + factory.createImportClause(clause.isTypeOnly, factory.createIdentifier(ref.name), clause.namedBindings), + stmt.moduleSpecifier, + stmt.attributes + ); + return factory.updateSourceFile( + sourceFile, + sourceFile.statements.map((s) => (s === stmt ? updated : s)) + ); + } + if (!clause?.namedBindings || !ts.isNamedImports(clause.namedBindings)) continue; if (clause.isTypeOnly && !ref.type) continue; if (clause.namedBindings.elements.some((e) => e.name.text === ref.name)) { @@ -70,8 +90,12 @@ export function addNamedImport( undefined, factory.createImportClause( Boolean(ref.type), - undefined, - factory.createNamedImports([factory.createImportSpecifier(false, undefined, factory.createIdentifier(ref.name))]) + ref.default ? factory.createIdentifier(ref.name) : undefined, + ref.default + ? undefined + : factory.createNamedImports([ + factory.createImportSpecifier(false, undefined, factory.createIdentifier(ref.name)), + ]) ), factory.createStringLiteral(target) ); diff --git a/packages/react/skins.compiler.config.ts b/packages/react/skins.compiler.config.ts index c89513ef..e7b8be9d 100644 --- a/packages/react/skins.compiler.config.ts +++ b/packages/react/skins.compiler.config.ts @@ -6,8 +6,6 @@ const ICON_COMPONENTS_SOURCE = '@videojs/icons/components'; const SOURCE_TAILWIND_SOURCE = './tailwind/video.tailwind'; const GENERATED_TAILWIND_SOURCE = '@videojs/skins/default/tailwind/video.tailwind'; -const BUTTON_RENDER_TARGET = /Button$/; - export default defineConfig({ input: { 'default-video': '../skins/src/default/video.skin.tsx', @@ -20,74 +18,54 @@ export default defineConfig({ plugins: [ transform((code) => { // Types - const BaseVideoSkinProps = code.ref.import('./src/presets/types', 'BaseVideoSkinProps', { type: true }); - const ReactNode = code.ref.import('react', 'ReactNode', { type: true }); + const BaseVideoSkinProps = code.import('./src/presets/types', 'BaseVideoSkinProps', { type: true }); + const ReactNode = code.import('react', 'ReactNode', { type: true }); // Utils - const cn = code.ref.import('@videojs/utils/style', 'cn'); - const isString = code.ref.import('@videojs/utils/predicate', 'isString'); - const isRenderProp = code.ref.import('@/utils/use-render', 'isRenderProp'); + const cn = code.import('@videojs/utils/style', 'cn'); + const isString = code.import('@videojs/utils/predicate', 'isString'); + const isRenderProp = code.import('@/utils/use-render', 'isRenderProp'); // Components - const Poster = code.ref.import('@/ui/poster', 'Poster'); + const Poster = code.import('@/ui/poster', 'Poster'); return [ - code.edit.import.rewrite({ + code.imports({ [CORE_COMPONENTS_SOURCE]: coreComponentImport, [ICON_COMPONENTS_SOURCE]: '@/icons', [SOURCE_TAILWIND_SOURCE]: GENERATED_TAILWIND_SOURCE, }), + tailwind({ mode: 'preserve' }), - code.edit.jsx.element({ - when: code.match.jsx.tag(/^(Tooltip|Popover)\.Trigger$/), - transform: code.edit.jsx.moveChildToProp('render'), - }), - code.edit.jsx.element({ - when: code.match.jsx.tag('Controls.Root'), - transform: code.edit.jsx.addProp('data-controls', ''), - }), - code.edit.jsx.element({ - when: code.match.jsx.tag('Container'), - transform: code.edit.jsx.addPropsSpread('rest'), - }), - code.edit.jsx.element({ - when: code.match.jsx.tag('Text'), - transform: code.edit.jsx.replaceTag('span'), - }), - code.edit.jsx.element({ - when: code.match.jsx.tag('Poster'), - transform: () => { - return code.create.jsx.renderIf( - 'poster', - code.create.jsx.element(Poster, { - src: code.create.value.onlyIf({ value: 'poster', condition: isString }), - render: code.create.value.onlyIf({ value: 'poster', condition: isRenderProp }), - }) - ); - }, - }), - code.edit.jsx.element({ - when: code.match.jsx.tag(BUTTON_RENDER_TARGET), - transform: code.edit.jsx.addProp('type', 'button'), - }), - code.edit.jsx.prop({ - when: code.match.all(code.match.jsx.prop('className'), code.match.value.array()), - transform: ({ value }) => code.create.value.call(cn, code.create.value.arrayItems(value)), - }), - code.edit.interface.property({ - when: code.match.all(code.match.interface.name(/Props$/), code.match.interface.property('children')), - transform: code.edit.interface.setType(() => - code.create.type.union(code.create.type.named(ReactNode), code.create.type.undefined()) - ), - }), - code.edit.interface.declaration({ - when: code.match.interface.name('DefaultVideoSkinProps'), - transform: code.edit.interface.extends(BaseVideoSkinProps), - }), - code.edit.function.declaration({ - when: code.match.function.name('DefaultVideoSkin'), - transform: code.edit.function.addProps(['poster', { name: 'rest', spread: true }]), - }), + + code + .interface(/Props$/) + .property('children') + .setType(() => code.type.union(code.type.named(ReactNode), code.type.undefined())), + + code.interface('DefaultVideoSkinProps').extends(BaseVideoSkinProps), + + code.jsx.element(/^(Tooltip|Popover)\.Trigger$/).childToProp('render'), + code.jsx.element('Controls.Root').addProp('data-controls', ''), + code.jsx.element('Container').spreadProps('rest'), + code.jsx.element('Text').replace('span'), + + code.jsx.element('Poster').replace(() => + code.jsx.if( + 'poster', + code.jsx.create(Poster, { + src: code.value.when('poster', isString), + render: code.value.when('poster', isRenderProp), + }) + ) + ), + + code.jsx + .props('className') + .where(code.value.isArray()) + .replace(({ value }) => code.value.call(cn, code.value.arrayItems(value))), + + code.function('DefaultVideoSkin').addProps(['poster', { name: 'rest', spread: true }]), ]; }), ], diff --git a/packages/react/src/presets/tests/skins-compiler-config.test.ts b/packages/react/src/presets/tests/skins-compiler-config.test.ts index e1035d50..aeedf047 100644 --- a/packages/react/src/presets/tests/skins-compiler-config.test.ts +++ b/packages/react/src/presets/tests/skins-compiler-config.test.ts @@ -105,15 +105,11 @@ describe('skins compiler config', () => { ) ); expect(compactCode).toContain(compact('')); - expect(compactCode).toContain( - compact('') - ); + expect(compactCode).toContain(compact('')); + expect(compactCode).toContain(compact('')); expect(compactCode).toContain(compact('')); expect(compactCode).toContain(compact('{SEEK_TIME}')); - expect(compactCode).toContain( - compact('') - ); + expect(compactCode).toContain(compact('')); expect(compactCode).toContain(compact('{SEEK_TIME}')); expect(compactCode).not.toContain('iconContainer'); expect(compactCode).not.toContain(' { expect(compactCode).toContain(compact('')); expect(compactCode).toContain(compact('')); expect(compactCode).toContain(compact('} />')); - expect(compactCode).toContain(compact('')); + expect(compactCode).toContain(compact('')); expect(compactCode).toContain(compact('')); expect(compactCode).toContain(compact('')); expect(compactCode).toContain(