Skip to content
This repository was archived by the owner on Nov 17, 2022. It is now read-only.

Commit 9d0b27b

Browse files
authored
feat: plugin jsx plus (#529)
* feat: create plugin jsx plus * feat: add jsx plus plugin * chore: dismiss all lint problems * feat: add include and exclude * docs: update docs of jsx plus * fix: in case of auto lint fix * fix: lint problem * fix: compatible with node 14 * chore: update version * chore: update pnpm lock * fix: dev dep
1 parent 88d2749 commit 9d0b27b

File tree

17 files changed

+617
-63
lines changed

17 files changed

+617
-63
lines changed
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { defineConfig } from '@ice/app';
22
import compatRax from '@ice/plugin-rax-compat';
3+
import jsxPlus from '@ice/plugin-jsx-plus';
34

45
export default defineConfig({
56
publicPath: '/',
6-
plugins: [compatRax()],
7+
plugins: [
8+
compatRax(),
9+
jsxPlus(),
10+
],
711
});

examples/rax-project/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"dependencies": {
1212
"@ice/app": "workspace:*",
1313
"@ice/plugin-rax-compat": "workspace:*",
14+
"@ice/plugin-jsx-plus": "workspace:*",
1415
"@ice/appear": "workspace:*",
1516
"@ice/runtime": "workspace:*",
1617
"rax": "^1.2.2",
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { createElement } from 'rax';
2+
3+
export default function JSXPlusDemo() {
4+
const list = [0, 1, 2, 3];
5+
const val = 'foo';
6+
return (
7+
// x-class
8+
<div x-class={{ item: true, active: val }}>
9+
{/* x-if */}
10+
<div x-if="YES">Should Show</div>
11+
<div x-if={false}>Should Hide</div>
12+
13+
{/* x-for */}
14+
{/* eslint-disable-next-line */}
15+
<span x-for={item in list} key={item}> {item} </span>
16+
17+
{/* Fragment */}
18+
<>
19+
<div>Fragment 1</div>
20+
<div>Fragment 2</div>
21+
<div>Fragment 3</div>
22+
</>
23+
</div>
24+
);
25+
}

examples/with-plugin-request/src/pages/home.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export default function Home() {
88

99
useEffect(() => {
1010
request();
11+
// eslint-disable-next-line
1112
}, []);
1213

1314
if (error) {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Changelog
2+
3+
## 1.0.0
4+
5+
Initial implementation.

packages/plugin-jsx-plus/README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# `plugin-jsx-plus`
2+
3+
This plugin adds support for JSX+ syntax to the icejs 3.
4+
5+
## Definition of JSX Plus
6+
https://github.com/jsx-plus/jsx-plus
7+
8+
## Usage
9+
10+
```js
11+
import { defineConfig } from '@ice/app';
12+
import jsxplus from '@ice/plugin-jsx-plus';
13+
14+
export default defineConfig({
15+
plugins: [
16+
jsxplus({
17+
// options
18+
}),
19+
],
20+
});
21+
```
22+
23+
## Options
24+
25+
- `include`: `(string | RegExp)[]`
26+
- Files to be included.
27+
- Default: the project `src` directory.
28+
- `exclude`: `(string | RegExp)[]`
29+
- Files to be excluded.
30+
- Default: `[]`
31+
- `extensions`: `string[]`
32+
- File extensions to be processed.
33+
- Default: `['.jsx', '.tsx']`
34+
35+
> If `include` and `exclude` are both set, `exclude` will be priority executed.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"name": "@ice/plugin-jsx-plus",
3+
"version": "1.0.0",
4+
"description": "",
5+
"license": "MIT",
6+
"type": "module",
7+
"exports": {
8+
".": {
9+
"types": "./esm/index.d.ts",
10+
"import": "./esm/index.js",
11+
"default": "./esm/index.js"
12+
},
13+
"./types": {
14+
"types": "./esm/types.d.ts",
15+
"import": "./esm/types.js",
16+
"default": "./esm/types.js"
17+
},
18+
"./esm/types": {
19+
"types": "./esm/types.d.ts",
20+
"import": "./esm/types.js",
21+
"default": "./esm/types.js"
22+
}
23+
},
24+
"main": "./esm/index.js",
25+
"types": "./esm/index.d.ts",
26+
"files": [
27+
"esm",
28+
"!esm/**/*.map"
29+
],
30+
"devDependencies": {
31+
"@ice/types": "^1.0.0",
32+
"@types/react": "^18.0.20",
33+
"@types/react-dom": "^18.0.6"
34+
},
35+
"repository": {
36+
"type": "http",
37+
"url": "https://github.com/ice-lab/ice-next/tree/master/packages/plugin-jsx-plus"
38+
},
39+
"scripts": {
40+
"watch": "tsc -w",
41+
"build": "tsc"
42+
},
43+
"dependencies": {
44+
"@babel/core": "^7.19.1",
45+
"babel-plugin-transform-jsx-class": "^0.1.3",
46+
"babel-plugin-transform-jsx-condition": "^0.1.2",
47+
"babel-plugin-transform-jsx-fragment": "^0.1.4",
48+
"babel-plugin-transform-jsx-list": "^0.1.2",
49+
"babel-plugin-transform-jsx-memo": "^0.1.4",
50+
"babel-plugin-transform-jsx-slot": "^0.1.2",
51+
"babel-runtime-jsx-plus": "^0.1.5"
52+
}
53+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import path from 'path';
2+
import { createRequire } from 'module';
3+
import type { Plugin } from '@ice/types';
4+
import { transformSync } from '@babel/core';
5+
6+
const require = createRequire(import.meta.url);
7+
const runtimePackage = 'babel-runtime-jsx-plus';
8+
const runtimePackagePath = require.resolve(runtimePackage);
9+
10+
const babelPlugins = [
11+
'babel-plugin-transform-jsx-list',
12+
'babel-plugin-transform-jsx-condition',
13+
'babel-plugin-transform-jsx-memo',
14+
'babel-plugin-transform-jsx-slot',
15+
['babel-plugin-transform-jsx-fragment', { moduleName: 'react' }],
16+
'babel-plugin-transform-jsx-class',
17+
];
18+
19+
const babelTransformOptions = {
20+
babelrc: false,
21+
configFile: false,
22+
parserOpts: {
23+
sourceType: 'module',
24+
allowAwaitOutsideFunction: true,
25+
// ts syntax had already been transformed by swc plugin.
26+
plugins: [
27+
'jsx',
28+
'importMeta',
29+
'topLevelAwait',
30+
'classProperties',
31+
'classPrivateMethods',
32+
],
33+
generatorOpts: {
34+
decoratorsBeforeExport: true,
35+
},
36+
},
37+
plugins: [],
38+
};
39+
40+
babelPlugins.forEach((plugin) => {
41+
if (typeof plugin === 'string') {
42+
babelTransformOptions.plugins.push(require.resolve(plugin));
43+
} else if (Array.isArray(plugin)) {
44+
const pluginName = plugin[0] as string;
45+
const pluginOption = plugin[1];
46+
babelTransformOptions.plugins.push([require.resolve(pluginName), pluginOption]);
47+
}
48+
});
49+
50+
export function idFilter(options: JSXPlusOptions, id: string): boolean {
51+
const extFilter = (id) => options.extensions.some((ext) => id.endsWith(ext));
52+
53+
if (options.exclude) {
54+
for (const pattern of options.exclude) {
55+
if (typeof pattern === 'string') {
56+
if (id.indexOf(pattern) > -1) {
57+
return false;
58+
}
59+
} else if (pattern instanceof RegExp && pattern.test(id)) {
60+
return false;
61+
}
62+
}
63+
}
64+
65+
if (options.include) {
66+
for (const pattern of options.include) {
67+
if (typeof pattern === 'string') {
68+
if (id.indexOf(pattern) > -1) {
69+
return extFilter(id);
70+
}
71+
} else if (pattern instanceof RegExp && pattern.test(id)) {
72+
return extFilter(id);
73+
}
74+
}
75+
}
76+
77+
return false;
78+
}
79+
80+
export interface JSXPlusOptions {
81+
include?: (string | RegExp)[];
82+
exclude?: (string | RegExp)[];
83+
extensions?: string[];
84+
}
85+
86+
const plugin: Plugin<JSXPlusOptions> = (options: JSXPlusOptions = {}) => ({
87+
name: '@ice/plugin-jsx-plus',
88+
setup: ({ onGetConfig, context }) => {
89+
// Default include all files in `src`.
90+
if (!options.include) {
91+
const sourceDir = path.join(context.rootDir, 'src');
92+
options.include = [sourceDir];
93+
}
94+
95+
// Default include all files with `.tsx` and `.jsx` extensions.
96+
if (!options.extensions) {
97+
options.extensions = ['.tsx', '.jsx'];
98+
}
99+
100+
function jsxPlusTransformer(source, id) {
101+
if (idFilter(options, id)) {
102+
try {
103+
const options = Object.assign({
104+
filename: id,
105+
sourceFileName: id,
106+
}, babelTransformOptions);
107+
if (/\.tsx?$/.test(id)) {
108+
// When routes file is a typescript file, add ts parser plugins.
109+
options.parserOpts.plugins.push('typescript');
110+
options.parserOpts.plugins.push('decorators-legacy'); // allowing decorators by default
111+
}
112+
113+
const { code, map } = transformSync(source, options);
114+
return { code, map };
115+
} catch (compileError) {
116+
console.error(compileError);
117+
return { code: source, map: null };
118+
}
119+
}
120+
return { code: source, map: null };
121+
}
122+
123+
onGetConfig((config) => {
124+
// Add runtime alias.
125+
if (!config.alias) {
126+
config.alias = {};
127+
}
128+
config.alias[runtimePackage] = runtimePackagePath;
129+
130+
// Apply babel jsx plus transformer.
131+
if (!config.transforms) {
132+
config.transforms = [];
133+
}
134+
config.transforms.push(jsxPlusTransformer);
135+
});
136+
},
137+
});
138+
139+
export default plugin;

packages/plugin-jsx-plus/src/types.ts

Whitespace-only changes.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { expect, it, describe } from 'vitest';
2+
import { default as jsxPlus, idFilter } from '../src';
3+
4+
describe('JSX Plus Plugin', () => {
5+
describe('Id filter', () => {
6+
it('default', () => {
7+
expect(idFilter({}, '/bar/a.tsx')).toBeFalsy();
8+
});
9+
10+
it('include', () => {
11+
const options = {
12+
include: [/bar/, 'foo'],
13+
extensions: ['.jsx', '.tsx'],
14+
};
15+
16+
expect(idFilter(options, '/bar/a.tsx')).toBeTruthy();
17+
expect(idFilter(options, '/foo/a.tsx')).toBeTruthy();
18+
});
19+
20+
it('exclude', () => {
21+
expect(idFilter({
22+
exclude: ['foo'],
23+
include: [/bar/],
24+
extensions: ['.jsx', '.tsx'],
25+
}, '/foo/bar/a.tsx')).toBeFalsy();
26+
27+
expect(idFilter({
28+
exclude: [/foo/],
29+
include: [/bar/],
30+
extensions: ['.jsx', '.tsx'],
31+
}, '/foo/bar/a.tsx')).toBeFalsy();
32+
});
33+
34+
it('extensions', () => {
35+
const options = {
36+
include: [/bar/],
37+
extensions: ['.jsx', '.tsx', '.custom.ext'],
38+
};
39+
expect(idFilter(options, '/foo/bar/a.tsx.custom.ext')).toBeTruthy();
40+
});
41+
});
42+
43+
describe('Plugin', () => {
44+
it('default', () => {
45+
const plugin = jsxPlus({
46+
include: ['foo'],
47+
});
48+
// @ts-ignore
49+
expect(plugin.name).toBe('@ice/plugin-jsx-plus');
50+
const fakeConfig = {};
51+
function onGetConfig(fn) {
52+
fn(fakeConfig);
53+
}
54+
const context = {
55+
rootDir: '/foo/bar',
56+
};
57+
// @ts-ignore
58+
plugin.setup({ onGetConfig, context });
59+
expect(fakeConfig['alias']['babel-runtime-jsx-plus']).toBeDefined();
60+
expect(Array.isArray(fakeConfig['transforms'])).toBeTruthy();
61+
expect(fakeConfig['transforms'].length).toBe(1);
62+
});
63+
64+
it('transformer', () => {
65+
const plugin = jsxPlus({
66+
include: ['foo'],
67+
});
68+
const fakeConfig = {};
69+
function onGetConfig(fn) {
70+
fn(fakeConfig);
71+
}
72+
const context = {
73+
rootDir: '/foo/bar',
74+
};
75+
// @ts-ignore
76+
plugin.setup({ onGetConfig, context });
77+
78+
const transformer = fakeConfig['transforms'][0];
79+
const ret = transformer('<div x-if={false} />', '/foo/bar/a.tsx');
80+
expect(ret.code).toBe(`import { createCondition as __create_condition__ } from "babel-runtime-jsx-plus";
81+
82+
__create_condition__([[() => false, () => <div />]]);`);
83+
});
84+
});
85+
});

0 commit comments

Comments
 (0)