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
+ })
+}