diff --git a/.gitignore b/.gitignore index ae361ca..81ea095 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .idea +lib + # Logs logs *.log diff --git a/__tests__/__fixtures__/x-if-else/actual.js b/__tests__/__fixtures__/x-if-else/actual.js new file mode 100644 index 0000000..9509430 --- /dev/null +++ b/__tests__/__fixtures__/x-if-else/actual.js @@ -0,0 +1,11 @@ +import { createElement } from 'react'; + +function Foo(props) { + return ( + + First + FirstSecondThird + Third + + ) +} diff --git a/__tests__/__fixtures__/x-if-else/expected.js b/__tests__/__fixtures__/x-if-else/expected.js new file mode 100644 index 0000000..2758368 --- /dev/null +++ b/__tests__/__fixtures__/x-if-else/expected.js @@ -0,0 +1,67 @@ +function _extends() { + _extends = Object.assign || function(target) { + for(var i = 1; i < arguments.length; i++){ + var source = arguments[i]; + for(var key in source){ + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + return target; + }; + return _extends.apply(this, arguments); +} +import { createCondition as __create_condition__ } from "babel-runtime-jsx-plus"; +import { createElement } from "react"; +function Foo(props) { + return __create_condition__([ + [ + function() { + return true; + }, + function() { + return React.createElement(View, _extends({}, props, { + className: "container" + }), __create_condition__([ + [ + function() { + return condition; + }, + function() { + return React.createElement(View, null, "First"); + } + ] + ]), __create_condition__([ + [ + function() { + return condition; + }, + function() { + return React.createElement(View, null, "First"); + } + ], + [ + function() { + return another; + }, + function() { + return React.createElement(View, null, "Second"); + } + ], + [ + function() { + return true; + }, + function() { + return React.createElement(View, null, "Third"); + } + ] + ]), /*#__PURE__*/ React.createElement(View, { + "x-else": true + }, "Third")); + } + ] + ]); +} + diff --git a/__tests__/__fixtures__/x-if/actual.js b/__tests__/__fixtures__/x-if/actual.js new file mode 100644 index 0000000..03fdcb4 --- /dev/null +++ b/__tests__/__fixtures__/x-if/actual.js @@ -0,0 +1,9 @@ +import { createElement } from 'react'; + +function Foo(props) { + return ( + + First + + ) +} diff --git a/__tests__/__fixtures__/x-if/expected.js b/__tests__/__fixtures__/x-if/expected.js new file mode 100644 index 0000000..f5a8160 --- /dev/null +++ b/__tests__/__fixtures__/x-if/expected.js @@ -0,0 +1,40 @@ +function _extends() { + _extends = Object.assign || function(target) { + for(var i = 1; i < arguments.length; i++){ + var source = arguments[i]; + for(var key in source){ + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + return target; + }; + return _extends.apply(this, arguments); +} +import { createCondition as __create_condition__ } from "babel-runtime-jsx-plus"; +import { createElement } from "react"; +function Foo(props) { + return __create_condition__([ + [ + function() { + return true; + }, + function() { + return React.createElement(View, _extends({}, props, { + className: "container" + }), __create_condition__([ + [ + function() { + return condition; + }, + function() { + return React.createElement(View, null, "First"); + } + ] + ])); + } + ] + ]); +} + diff --git a/__tests__/usage.js b/__tests__/usage.js index 7648680..788e61e 100644 --- a/__tests__/usage.js +++ b/__tests__/usage.js @@ -1,11 +1,27 @@ const swc = require('@swc/core') const path = require('path') -const ConsoleStripper = require(path.join(__dirname, '../lib/index.js')).default; +const fs = require('fs'); +const JSXConditionTransformPlugin = require(path.join(__dirname, '../lib/index.js')).default; -it('should strip console call', () => { - const output = swc.transformSync(`console.log('Foo')`, { - plugin: (m) => (new ConsoleStripper()).visitModule(m), - }); +describe('', () => { + const fixturesDir = path.join(__dirname, '__fixtures__'); + fs.readdirSync(fixturesDir).map((caseName) => { + it(`should ${caseName.split('-').join(' ')}`, () => { + const fixtureDir = path.join(fixturesDir, caseName); + const actualPath = path.join(fixtureDir, 'actual.js'); + const actualCode = fs.readFileSync(actualPath, {encoding: 'utf-8'}); + const expectedCode = fs.readFileSync(path.join(fixtureDir, 'expected.js'), { encoding: 'utf-8' }); + + const transformedOutput = swc.transformSync(actualCode, { + jsc: { + parser: { + jsx: true + }, + }, + plugin: JSXConditionTransformPlugin + }); - expect(output.code.replace(/\n/g, '')).toBe('void 0;') -}) + expect(transformedOutput.code.trim()).toBe(expectedCode.trim()); + }); + }); +}); diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..41a1d66 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,8 @@ +// Sync object +/** @type {import('@jest/types').Config.InitialOptions} */ +const config = { + verbose: true, + testMatch: ['!**/__fixtures__/**', '**/__tests__/**/*.js'] +}; + +module.exports = config; diff --git a/lib/index.d.ts b/lib/index.d.ts deleted file mode 100644 index 0d7328f..0000000 --- a/lib/index.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { CallExpression, Expression } from '@swc/core'; -import Visitor from '@swc/core/Visitor'; -export default class ConsoleStripper extends Visitor { - visitCallExpression(e: CallExpression): Expression; -} diff --git a/lib/index.js b/lib/index.js deleted file mode 100644 index 4971725..0000000 --- a/lib/index.js +++ /dev/null @@ -1,29 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const Visitor_1 = __importDefault(require("@swc/core/Visitor")); -class ConsoleStripper extends Visitor_1.default { - visitCallExpression(e) { - if (e.callee.type !== 'MemberExpression') { - return e; - } - if (e.callee.object.type === 'Identifier' && e.callee.object.value === 'console') { - if (e.callee.property.type === 'Identifier') { - return { - type: "UnaryExpression", - span: e.span, - operator: 'void', - argument: { - type: 'NumericLiteral', - span: e.span, - value: 0 - } - }; - } - } - return e; - } -} -exports.default = ConsoleStripper; diff --git a/package.json b/package.json index c68b3c1..5350161 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,11 @@ { "name": "swc-plugin-transform-jsx-condition", - "version": "0.1.0", + "version": "0.1.0-beta.1", "description": "Support of transform jsx condition directive based on SWC", "main": "lib/index.js", "scripts": { "build": "tsc -d", + "build:watch": "tsc -d --watch", "test": "jest" }, "types": "./lib/index.d.ts", @@ -13,12 +14,13 @@ "url": "git+https://github.com/jsx-plus/swc-plugin-transform-jsx-condition.git" }, "devDependencies": { - "@swc/core": "^1.2.203", "jest": "^24.9.0", "typescript": "^4.7.3" }, - "dependencies": {}, - "author": "andycall", + "dependencies": { + "@swc/core": "^1.2.203" + }, + "author": "jsx-plus ", "license": "MIT", "bugs": { "url": "https://github.com/jsx-plus/swc-plugin-transform-jsx-condition/issues" diff --git a/src/index.ts b/src/index.ts index 70bf835..31973f1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,27 +1,203 @@ -import {CallExpression, Expression} from '@swc/core'; -import Visitor from '@swc/core/Visitor' - -export default class ConsoleStripper extends Visitor { - visitCallExpression(e: CallExpression): Expression { - if (e.callee.type !== 'MemberExpression') { - return e; - } - - if (e.callee.object.type === 'Identifier' && e.callee.object.value === 'console') { - if (e.callee.property.type === 'Identifier') { - return { - type: "UnaryExpression", - span: e.span, - operator: 'void', - argument: { - type: 'NumericLiteral', - span: e.span, - value: 0 +import { + Expression, + JSXElement, JSXText, Program, +} from '@swc/core'; +import Visitor from '@swc/core/Visitor'; +import { + ExprOrSpread, JSXElementChild +} from '@swc/core/types'; +import { + buildArrayExpression, + buildArrowFunctionExpression, buildBooleanLiteral, buildCallExpression, buildIdentifier, buildImportDeclaration, + buildJSXElement, + buildJSXExpressionContainer, buildJSXText, buildNamedImportSpecifier, buildNullLiteral, buildStringLiteral +} from './utils'; + +enum JSXConditionType { + if = 'x-if', + else = 'x-else', + elseif = 'x-elseif' +} + +function isJSXCondition(n: JSXElement) { + let opening = n.opening; + let openingAttributes = opening.attributes; + + if (openingAttributes) { + for (let attribute of openingAttributes) { + if (attribute.type === 'JSXAttribute' && attribute.name.type === 'Identifier') { + switch (attribute.name.value) { + case JSXConditionType.if: + case JSXConditionType.else: + case JSXConditionType.elseif: { + return true; } } } } + } + return false; +} + +type JSXCondition = { + type: JSXConditionType; + expression?: Expression; +} + +function getJSXCondition(n: JSXElement): JSXCondition | undefined { + let opening = n.opening; + let openingAttributes = opening.attributes; + if (!openingAttributes) return undefined; - return e + for (let attribute of openingAttributes) { + if (attribute.type === 'JSXAttribute' && attribute.name.type === 'Identifier') { + switch (attribute.name.value) { + case JSXConditionType.if: + case JSXConditionType.else: + case JSXConditionType.elseif: { + if (attribute.value?.type === 'JSXExpressionContainer') { + return { + type: attribute.name.value, + expression: attribute.value.expression + }; + } + if (attribute.value === null) { + return { + type: attribute.name.value, + expression: buildNullLiteral() + } + } + } + } + } } + + return undefined; +} + +function JSXConditionToStandard(n: JSXElement) { + let openingAttributes = n.opening.attributes; + + if (openingAttributes) { + openingAttributes = openingAttributes.filter((attribute) => { + if (attribute.type === 'JSXAttribute' && attribute.name.type === 'Identifier') { + switch (attribute.name.value) { + case JSXConditionType.if: + case JSXConditionType.else: + case JSXConditionType.elseif: { + return false; + } + } + } + return true; + }); + } + return buildJSXElement({ + ...n.opening, + attributes: openingAttributes + }, n.children, n.closing) +} + + +function transformJSXCondition(n: JSXElement, currentList: JSXElementChild[], currentIndex: number): JSXElement | JSXText { + n.children = n.children.map((c, i) => { + if (c.type === 'JSXElement') { + return transformJSXCondition(c, n.children, i); + } + return c; + }); + + if (!isJSXCondition(n)) { + return n; + } + + let condition = getJSXCondition(n)!; + if (condition.type === JSXConditionType.else || condition.type === JSXConditionType.elseif) { + // @ts-ignore + if (n.__skip) { + return buildJSXText(''); + } + + return n; + } + + let isRoot = currentIndex === -1; + + type JSXConditionExpression = { + condition: Expression; + jsxElement: JSXElement; + }; + + let conditions: JSXConditionExpression[] = [ + { + condition: condition!.expression!, + jsxElement: n + } + ]; + + let continueSearch = true; + let indent = 1; + let nextJSXKind: JSXCondition | undefined; + do { + let nextSibling = currentList[currentIndex + indent]; + if (nextSibling && nextSibling.type === 'JSXText' && nextSibling.value.trim() === '') { + indent++; + } else if (nextSibling && nextSibling.type === 'JSXElement' && (nextJSXKind = getJSXCondition(nextSibling)) && nextJSXKind && nextJSXKind.type != JSXConditionType.if) { + conditions.push({ + condition: nextJSXKind.type === JSXConditionType.elseif ? getJSXCondition(nextSibling)!.expression! : buildBooleanLiteral(true), + jsxElement: nextSibling + }); + // @ts-ignore + nextSibling.__skip = true; + continueSearch = nextJSXKind.type === JSXConditionType.elseif; + indent++; + } else { + continueSearch = false; + } + } while (continueSearch); + + let elements: ExprOrSpread[] = conditions.map((con) => { + return { + expression: buildArrayExpression([ + { + expression: buildArrowFunctionExpression([], con.condition) + }, + { + expression: buildArrowFunctionExpression([], JSXConditionToStandard(con.jsxElement)) + } + ]) + } + }); + + let body = buildCallExpression(buildIdentifier('__create_condition__', false), [ + { + expression: buildArrayExpression(elements) + } + ]) as any; + + + return isRoot ? body : buildJSXExpressionContainer(body); +} + +class JSXConditionTransformer extends Visitor { + visitJSXElement(n: JSXElement): JSXElement { + if (isJSXCondition(n)) { + return transformJSXCondition(n, [], -1) as JSXElement; + } + + return n; + } +} + +export default function JSXConditionTransformPlugin(m: Program): Program { + let result = new JSXConditionTransformer().visitProgram(m); + let babelImport = buildImportDeclaration([ + buildNamedImportSpecifier( + buildIdentifier('__create_condition__', false), + buildIdentifier('createCondition', false) + ) + ], buildStringLiteral('babel-runtime-jsx-plus')); + result.body.unshift(babelImport as any); + + return result; } diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..c508129 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,122 @@ +import { + ArrowFunctionExpression, BooleanLiteral, + CallExpression, + Expression, Identifier, ImportDeclaration, + JSXElement, + JSXExpressionContainer, JSXText, + NullLiteral +} from '@swc/core'; +import { + Argument, + ArrayExpression, BlockStatement, + ExprOrSpread, + HasSpan, Import, ImportSpecifier, + JSXClosingElement, + JSXElementChild, + JSXOpeningElement, NamedImportSpecifier, + Node, Pattern, StringLiteral, Super, TsTypeParameterInstantiation +} from '@swc/core/types'; + +export function buildBaseExpression(other: any): Node & HasSpan & T { + return { + ...other, + span: { + start: 0, + end: 0, + ctxt: 0 + }, + } +} + +export function buildArrayExpression(elements: (ExprOrSpread | undefined)[]): ArrayExpression { + return buildBaseExpression({ + type: 'ArrayExpression', + elements: elements + }); +} + +export function buildJSXElement(opening: JSXOpeningElement, children: JSXElementChild[], closing?: JSXClosingElement): JSXElement { + return buildBaseExpression({ + type: 'JSXElement', + opening: opening, + children: children, + closing: closing + }); +} + +export function buildArrowFunctionExpression(params: Pattern[], body: BlockStatement | Expression): ArrowFunctionExpression { + return buildBaseExpression({ + type: 'ArrowFunctionExpression', + params: params, + body: body, + async: false, + generator: false + }); +} + +export function buildNullLiteral(): NullLiteral { + return buildBaseExpression({ + type: 'NullLiteral' + }); +} + +export function buildJSXExpressionContainer(expression: Expression): JSXExpressionContainer { + return buildBaseExpression({ + type: 'JSXExpressionContainer', + expression: expression + }); +} + +export function buildImportDeclaration(specifiers: ImportSpecifier[], source: StringLiteral): ImportDeclaration { + return buildBaseExpression({ + type: 'ImportDeclaration', + specifiers: specifiers, + source: source + }); +} + +export function buildStringLiteral(value: string): StringLiteral { + return buildBaseExpression({ + type: 'StringLiteral', + value: value + }); +} + +export function buildJSXText(value: ''): JSXText { + return buildBaseExpression({ + type: 'JSXText', + value: value, + raw: value + }) +} + +export function buildBooleanLiteral(value: boolean) { + return buildBaseExpression({ + type: 'BooleanLiteral', + value: value + }); +} + +export function buildNamedImportSpecifier(local: Identifier, imported: Identifier | null): NamedImportSpecifier { + return buildBaseExpression({ + type: 'ImportSpecifier', + local: local, + imported: imported + }); +} + +export function buildCallExpression(callee: Expression | Super | Import, args: Argument[]): CallExpression { + return buildBaseExpression({ + type: 'CallExpression', + callee: callee, + arguments: args + }) +} + +export function buildIdentifier(name: string, optional: boolean): Identifier { + return buildBaseExpression({ + type: 'Identifier', + value: name, + optional: optional + }) +}