diff --git a/.gitignore b/.gitignore
index 6704566..c3c6bb1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
+lib
+
# Logs
logs
*.log
@@ -102,3 +104,5 @@ dist
# TernJS port file
.tern-port
+
+package-lock.json
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..639900d
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..cbb3781
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/swc-plugin-transform-jsx-list.iml b/.idea/swc-plugin-transform-jsx-list.iml
new file mode 100644
index 0000000..d6ebd48
--- /dev/null
+++ b/.idea/swc-plugin-transform-jsx-list.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/__tests__/__fixtures__/example/actual.js b/__tests__/__fixtures__/example/actual.js
new file mode 100644
index 0000000..659a542
--- /dev/null
+++ b/__tests__/__fixtures__/example/actual.js
@@ -0,0 +1,12 @@
+import { createElement } from 'react';
+
+function Foo() {
+ return (
+
+ hello
+ item: {item}
+ key: {key}, item: {item}
+ key: {key}, item: {item}
+
+ )
+}
diff --git a/__tests__/__fixtures__/example/expected.js b/__tests__/__fixtures__/example/expected.js
new file mode 100644
index 0000000..85f3a9d
--- /dev/null
+++ b/__tests__/__fixtures__/example/expected.js
@@ -0,0 +1,14 @@
+import { createList as __create_list__ } from "babel-runtime-jsx-plus";
+import { createElement } from "react";
+function Foo() {
+ return /*#__PURE__*/ React.createElement(View, null, __create_list__.call(this, array, function() {
+ return React.createElement(View, null, "hello");
+ }), __create_list__.call(this, array, function(item) {
+ return React.createElement(View, null, "item: ", item);
+ }), __create_list__.call(this, foo, function(item, key) {
+ return React.createElement(View, null, "key: ", key, ", item: ", item);
+ }), __create_list__.call(this, exp(), function(item, key) {
+ return React.createElement(View, null, "key: ", key, ", item: ", item);
+ }));
+}
+
diff --git a/__tests__/__fixtures__/side-effects/actual.js b/__tests__/__fixtures__/side-effects/actual.js
new file mode 100644
index 0000000..bd360bc
--- /dev/null
+++ b/__tests__/__fixtures__/side-effects/actual.js
@@ -0,0 +1,11 @@
+const items = [1, 2, 3, 4];
+
+export default function List() {
+ return (
+
+ );
+}
diff --git a/__tests__/__fixtures__/side-effects/expected.js b/__tests__/__fixtures__/side-effects/expected.js
new file mode 100644
index 0000000..4911c0d
--- /dev/null
+++ b/__tests__/__fixtures__/side-effects/expected.js
@@ -0,0 +1,13 @@
+import { createList as __create_list__ } from "babel-runtime-jsx-plus";
+var items = [
+ 1,
+ 2,
+ 3,
+ 4
+];
+export default function List() {
+ return /*#__PURE__*/ React.createElement("div", null, __create_list__.call(this, items, function(it, idx) {
+ return React.createElement("div", null, /*#__PURE__*/ React.createElement("span", null, it));
+ }));
+};
+
diff --git a/__tests__/usage.js b/__tests__/usage.js
new file mode 100644
index 0000000..788e61e
--- /dev/null
+++ b/__tests__/usage.js
@@ -0,0 +1,27 @@
+const swc = require('@swc/core')
+const path = require('path')
+const fs = require('fs');
+const JSXConditionTransformPlugin = require(path.join(__dirname, '../lib/index.js')).default;
+
+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(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/package.json b/package.json
new file mode 100644
index 0000000..770d559
--- /dev/null
+++ b/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "swc-plugin-transform-jsx-list",
+ "version": "0.1.0-beta.1",
+ "description": "Support of transform jsx list directive based on SWC",
+ "main": "lib/index.js",
+ "scripts": {
+ "build": "tsc -d",
+ "build:watch": "tsc -d --watch",
+ "test": "jest"
+ },
+ "types": "./lib/index.d.ts",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/jsx-plus/swc-plugin-transform-jsx-list.git"
+ },
+ "devDependencies": {
+ "jest": "^24.9.0",
+ "typescript": "^4.7.3"
+ },
+ "dependencies": {
+ "@swc/core": "^1.2.203"
+ },
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/jsx-plus/swc-plugin-transform-jsx-list/issues"
+ },
+ "homepage": "https://github.com/jsx-plus/swc-plugin-transform-jsx-list#readme"
+}
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..2c17b0e
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,149 @@
+import {
+ Expression, JSXAttribute, JSXAttrValue,
+ JSXElement, JSXExpression, JSXText, Program,
+} from '@swc/core';
+import Visitor from '@swc/core/Visitor';
+import {
+ ExprOrSpread, JSXElementChild, Pattern
+} from '@swc/core/types';
+import {
+ buildArrayExpression,
+ buildArrowFunctionExpression,
+ buildBooleanLiteral,
+ buildCallExpression,
+ buildIdentifier,
+ buildImportDeclaration,
+ buildJSXElement,
+ buildJSXExpressionContainer,
+ buildJSXText,
+ buildMemberExpression,
+ buildNamedImportSpecifier,
+ buildNullLiteral,
+ buildStringLiteral, buildThisExpression
+} from './utils';
+
+function JSXListToStandard(n: JSXElement) {
+ let openingAttributes = n.opening.attributes;
+
+ if (openingAttributes) {
+ openingAttributes = openingAttributes.filter((attribute) => {
+ if (attribute.type === 'JSXAttribute' && attribute.name.type === 'Identifier' && attribute.name.value === 'x-for') {
+ return false;
+ }
+ return true;
+ });
+ }
+ return buildJSXElement({
+ ...n.opening,
+ attributes: openingAttributes
+ }, n.children, n.closing)
+}
+
+function transformJSXList(n: JSXElement, currentList: JSXElementChild[], currentIndex: number): JSXElement | JSXText {
+ n.children = n.children.map((c, i) => {
+ if (c.type === 'JSXElement' && isJSXList(c)) {
+ return transformJSXList(c, n.children, i);
+ }
+ return c;
+ });
+
+ if (isJSXList(n)) {
+ let attrValue = getJSXList(n);
+ if (!attrValue || attrValue.type !== 'JSXExpressionContainer') {
+ console.warn('ignore x-for due to stynax error.');
+ return n;
+ }
+
+ // @ts-ignore
+ if (n.__listHandled) return n;
+ // @ts-ignore
+ n.__listHandled = true;
+
+ let { expression } = attrValue;
+ let params: Pattern[] = [];
+ let iterValue: Expression;
+
+ if (expression.type === 'BinaryExpression' && expression.operator === 'in') {
+ // x-for={(item, index) in value}
+ const { left, right } = expression;
+ iterValue = right;
+
+ if (left.type === 'ParenthesisExpression' && left.expression.type === 'SequenceExpression') {
+ // x-for={(item, key) in value}
+ params = left.expression.expressions;
+ } else if (left.type === 'Identifier') {
+ // x-for={item in value}
+ params.push(buildIdentifier(left.value));
+ } else {
+ // x-for={??? in value}
+ throw new Error('Stynax error of x-for.');
+ }
+ } else {
+ // x-for={value}, x-for={callExp()}, ...
+ iterValue = expression;
+ }
+
+ let callee = buildMemberExpression(buildIdentifier('__create_list__'), buildIdentifier('call'));
+ let body = buildCallExpression(callee, [
+ {
+ expression: buildThisExpression()
+ },
+ {
+ expression: iterValue
+ },
+ {
+ expression: buildArrowFunctionExpression(params, JSXListToStandard(n))
+ }
+ ]) as any;
+
+ return buildJSXExpressionContainer(body) as any;
+ }
+
+ return n;
+}
+
+function getJSXList(n: JSXElement): JSXAttrValue | undefined {
+ let opening = n.opening;
+ let openingAttributes = opening.attributes;
+
+ if (openingAttributes) {
+ for (let attribute of openingAttributes) {
+ if (attribute.type === 'JSXAttribute' && attribute.name.type === 'Identifier' && attribute.name.value === 'x-for') {
+ return attribute.value;
+ }
+ }
+ }
+}
+
+function isJSXList(n: JSXElement): boolean {
+ let opening = n.opening;
+ let openingAttributes = opening.attributes;
+
+ if (openingAttributes) {
+ for (let attribute of openingAttributes) {
+ if (attribute.type === 'JSXAttribute' && attribute.name.type === 'Identifier' && attribute.name.value === 'x-for') {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+class JSXListTransformer extends Visitor {
+ visitJSXElement(n: JSXElement): JSXElement {
+ return transformJSXList(n, [], -1) as JSXElement;
+ }
+}
+
+export default function JSXConditionTransformPlugin(m: Program): Program {
+ let result = new JSXListTransformer().visitProgram(m);
+ let babelImport = buildImportDeclaration([
+ buildNamedImportSpecifier(
+ buildIdentifier('__create_list__', false),
+ buildIdentifier('createList', 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..6f5cfe7
--- /dev/null
+++ b/src/utils.ts
@@ -0,0 +1,136 @@
+import {
+ ArrowFunctionExpression, BooleanLiteral,
+ CallExpression,
+ Expression, Identifier, ImportDeclaration,
+ JSXElement,
+ JSXExpressionContainer, JSXText, MemberExpression,
+ NullLiteral, ThisExpression
+} 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 buildMemberExpression(object: Identifier, property: Identifier): MemberExpression {
+ return buildBaseExpression({
+ type: 'MemberExpression',
+ object: object,
+ property: property
+ })
+}
+
+export function buildCallExpression(callee: Expression | Super | Import, args: Argument[]): CallExpression {
+ return buildBaseExpression({
+ type: 'CallExpression',
+ callee: callee,
+ arguments: args
+ })
+}
+
+export function buildThisExpression(): ThisExpression {
+ return buildBaseExpression({
+ type: 'ThisExpression'
+ });
+}
+
+export function buildIdentifier(name: string, optional?: boolean): Identifier {
+ return buildBaseExpression({
+ type: 'Identifier',
+ value: name,
+ optional: optional
+ })
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..cf5b89b
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,66 @@
+{
+ "compilerOptions": {
+ /* Basic Options */
+ // "incremental": true, /* Enable incremental compilation */
+ "target": "ES6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
+ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
+ // "lib": [], /* Specify library files to be included in the compilation. */
+ // "allowJs": true, /* Allow javascript files to be compiled. */
+ // "checkJs": true, /* Report errors in .js files. */
+ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
+ "declaration": true, /* Generates corresponding '.d.ts' file. */
+ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
+ // "sourceMap": true, /* Generates corresponding '.map' file. */
+ // "outFile": "./", /* Concatenate and emit output to single file. */
+ "outDir": "./lib", /* Redirect output structure to the directory. */
+ "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
+ // "composite": true, /* Enable project compilation */
+ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
+ // "removeComments": true, /* Do not emit comments to output. */
+ // "noEmit": true, /* Do not emit outputs. */
+ // "importHelpers": true, /* Import emit helpers from 'tslib'. */
+ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
+ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
+ /* Strict Type-Checking Options */
+ "strict": true, /* Enable all strict type-checking options. */
+ // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
+ // "strictNullChecks": true, /* Enable strict null checks. */
+ // "strictFunctionTypes": true, /* Enable strict checking of function types. */
+ // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
+ // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
+ // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
+ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
+ /* Additional Checks */
+ // "noUnusedLocals": true, /* Report errors on unused locals. */
+ // "noUnusedParameters": true, /* Report errors on unused parameters. */
+ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
+ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
+ /* Module Resolution Options */
+ // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
+ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
+ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
+ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
+ // "typeRoots": [], /* List of folders to include type definitions from. */
+ // "types": [], /* Type declaration files to be included in compilation. */
+ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
+ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
+ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
+ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
+ /* Source Map Options */
+ // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
+ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
+ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
+ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
+ /* Experimental Options */
+ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
+ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
+ /* Advanced Options */
+ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
+ },
+ "include": [
+ "src/*.ts"
+ ],
+ "exclude": [
+ "lib/"
+ ]
+}