├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── __tests__
├── jsx.spec.tsx
├── tsconfig.json
└── v-model.spec.tsx
├── api-extractor.json
├── babel.config.js
├── global.d.ts
├── index.js
├── jest-setup.ts
├── jest.config.js
├── jsx-dev-runtime.js
├── jsx-runtime.js
├── package.json
├── pnpm-lock.yaml
├── rollup.config.js
├── src
├── dev.ts
├── index.ts
└── jsx.ts
└── tsconfig.json
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 |
3 | on:
4 | push:
5 | branches:
6 | - '**'
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | test:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v2
16 |
17 | - name: Install pnpm
18 | uses: pnpm/action-setup@v2.0.1
19 | with:
20 | version: 6.15.1
21 |
22 | - name: Set node version to 16
23 | uses: actions/setup-node@v2
24 | with:
25 | node-version: 16
26 | cache: 'pnpm'
27 |
28 | - run: pnpm bootstrap
29 |
30 | - name: Run unit tests
31 | run: pnpm run test
32 |
33 | - uses: codecov/codecov-action@v2
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 | temp
85 |
86 | # Gatsby files
87 | .cache/
88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
89 | # https://nextjs.org/blog/next-9-1#public-directory-support
90 | # public
91 |
92 | # vuepress build output
93 | .vuepress/dist
94 |
95 | # Serverless directories
96 | .serverless/
97 |
98 | # FuseBox cache
99 | .fusebox/
100 |
101 | # DynamoDB Local files
102 | .dynamodb/
103 |
104 | # TernJS port file
105 | .tern-port
106 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/typescript/lib"
3 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 doly mood
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vue-jsx-runtime [](https://www.npmjs.com/package/vue-jsx-runtime) [](https://github.com/dolymood/vue-jsx-runtime/actions/workflows/test.yml) [](https://codecov.io/github/dolymood/vue-jsx-runtime)
2 |
3 | Vue 3 jsx runtime support.
4 |
5 | The background https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html . With new jsx runtime support, which means a JSX ast standard, every lib can have its own jsx syntax with small limits.
6 |
7 | [Examples](https://github.com/dolymood/vue-jsx-runtime-examples) with TS:
8 |
9 | - [vite](https://github.com/dolymood/vue-jsx-runtime-examples/tree/main/vite) with esbuild usage
10 | - [vue-cli](https://github.com/dolymood/vue-jsx-runtime-examples/tree/main/vue-cli) with babel usage
11 | - [swc](https://github.com/dolymood/vue-jsx-runtime-examples/tree/main/swc)
12 |
13 | TODO:
14 |
15 | - optimize, transformOn, isCustomElement ...
16 | - dev validation
17 | - more tests
18 | - more features
19 |
20 | ## Installation
21 |
22 | Install the plugin with:
23 |
24 | ```bash
25 | pnpm add vue-jsx-runtime
26 | # or
27 | npm install vue-jsx-runtime
28 | ```
29 |
30 | ## Usage
31 |
32 | ### In Babel
33 |
34 | ```js
35 | // babel plugin
36 | plugins: [
37 | [
38 | // add @babel/plugin-transform-react-jsx
39 | '@babel/plugin-transform-react-jsx',
40 | {
41 | throwIfNamespace: false,
42 | runtime: 'automatic',
43 | importSource: 'vue-jsx-runtime'
44 | }
45 | ],
46 | ],
47 | ```
48 | ### In TypeScript
49 |
50 | `tsconfig.json`:
51 |
52 | ```json
53 | {
54 | "compilerOptions": {
55 | "jsx": "react-jsxdev", /* 'react-jsx' or 'react-jsxdev'. You can also use 'preserve' to use babel or other tools to handle jsx*/
56 | "jsxImportSource": "vue-jsx-runtime"
57 | }
58 | }
59 | ```
60 |
61 | If you used with Babel, you need to set the config:
62 |
63 | ```json
64 | {
65 | "compilerOptions": {
66 | "jsx": "preserve", /* 'react-jsx' or 'react-jsxdev'. You can also use 'preserve' to use babel or other tools to handle jsx*/
67 | }
68 | }
69 | ```
70 |
71 | ### In Other Tools
72 |
73 | If you use some tool which support jsx-runtime, like [swc](https://swc.rs/), you can use like this:
74 |
75 | `.swcrc`:
76 |
77 | ```json
78 | {
79 | "jsc": {
80 | "transform": {
81 | "react": {
82 | "runtime": "automatic",
83 | "importSource": "vue-jsx-runtime"
84 | }
85 | }
86 | }
87 | }
88 | ```
89 |
90 | More details, see https://swc.rs/docs/configuration/compilation#jsctransformreact
91 |
92 | About [esbuild](https://github.com/evanw/esbuild/), see https://github.com/evanw/esbuild/issues/1172 and a hack way https://github.com/evanw/esbuild/issues/832 . https://github.com/evanw/esbuild/issues/334#issuecomment-711444731 .
93 |
94 | ## Syntax
95 |
96 | ### Content
97 |
98 | Functional component:
99 |
100 | ```jsx
101 | const App = () =>
Vue 3.0
;
102 | ```
103 |
104 | with render
105 |
106 | ```jsx
107 | const App = {
108 | render() {
109 | return Vue 3.0
;
110 | },
111 | }
112 | ```
113 |
114 | ```jsx
115 | import { withModifiers, defineComponent } from "vue";
116 |
117 | const App = defineComponent({
118 | setup() {
119 | const count = ref(0);
120 |
121 | const inc = () => {
122 | count.value++;
123 | };
124 |
125 | return () => (
126 | {count.value}
127 | );
128 | },
129 | });
130 | ```
131 |
132 | Fragment
133 |
134 | ```jsx
135 | const App = () => (
136 | <>
137 | I'm
138 | Fragment
139 | >
140 | );
141 | ```
142 |
143 | ### Attributes / Props
144 |
145 | ```jsx
146 | const App = () => ;
147 | ```
148 |
149 | with a dynamic binding:
150 |
151 | ```jsx
152 | const placeholderText = "email";
153 | const App = () => ;
154 | ```
155 |
156 | ### Directives
157 |
158 | #### v-show
159 |
160 | ```jsx
161 | const App = {
162 | data() {
163 | return { visible: true };
164 | },
165 | render() {
166 | return ;
167 | },
168 | };
169 | ```
170 |
171 | #### v-model
172 |
173 | A little different with `@vue/babel-plugin-jsx`.
174 |
175 | Syntax:
176 | ```
177 | v-model={[object, ["path/key"], argument, ["modifier"]]}
178 | ```
179 |
180 | ##### Recommend:
181 |
182 | ```jsx
183 | const val = ref(1); // val.value will be 1
184 | // jsx
185 | // do not use v-model={val.value}
186 | ```
187 |
188 | ```jsx
189 |
190 | ```
191 |
192 | `v-model` will use `val["value"]` to getter or setter by default.
193 |
194 | ##### Other usage
195 |
196 | ```jsx
197 | const val = ref(1);
198 |
199 |
200 | ```
201 |
202 | ```jsx
203 |
204 | ```
205 |
206 | Will compile to:
207 |
208 | ```js
209 | h(A, {
210 | argument: val["value"],
211 | argumentModifiers: {
212 | modifier: true,
213 | },
214 | "onUpdate:argument": ($event) => (val["value"] = $event),
215 | });
216 | ```
217 |
218 | #### custom directive
219 |
220 | Recommended when using string arguments
221 |
222 | ```jsx
223 | const App = {
224 | directives: { custom: customDirective },
225 | setup() {
226 | return () => ;
227 | },
228 | };
229 | ```
230 |
231 | ```jsx
232 | const App = {
233 | directives: { custom: customDirective },
234 | setup() {
235 | return () => ;
236 | },
237 | };
238 | ```
239 |
240 | ### Slot
241 |
242 | #### Recommend
243 |
244 | Use object slots:
245 |
246 | ```jsx
247 | const A = (props, { slots }) => (
248 | <>
249 | { slots.default ? slots.default() : 'foo' }
250 | { slots.bar?.() }
251 | >
252 | );
253 |
254 | const App = {
255 | setup() {
256 | return () => (
257 | <>
258 |
259 | {{
260 | default: () => A
,
261 | bar: () => B,
262 | }}
263 |
264 | {() => "foo"}
265 | >
266 | );
267 | },
268 | };
269 | ```
270 |
271 | #### Use v-slots
272 |
273 | > Note: In `jsx`, _`v-slot`_ should be replace with **`v-slots`**
274 |
275 | ```jsx
276 | const App = {
277 | setup() {
278 | const slots = {
279 | bar: () => B,
280 | };
281 | return () => (
282 |
283 | A
284 |
285 | );
286 | },
287 | };
288 | // or
289 | const App = {
290 | setup() {
291 | const slots = {
292 | default: () => A
,
293 | bar: () => B,
294 | };
295 | return () => ;
296 | },
297 | };
298 | ```
299 |
300 | ## Different with [vue jsx-next](https://github.com/vuejs/jsx-next)
301 |
302 | - `jsx-next` is a plugin for `Babel` only.
303 | - `vue-jsx-runtime` can be used with `Babel`, `TypeScript`, `swc`, `esbuild` and more.
304 |
305 | `vue-jsx-runtime` limits:
306 |
307 | - can not merge ele/component props
308 | - `v-model` syntax is little different with `jsx-next` - `v-model`
309 |
--------------------------------------------------------------------------------
/__tests__/jsx.spec.tsx:
--------------------------------------------------------------------------------
1 | // use https://github.com/vuejs/jsx-next/tree/dev/packages/babel-plugin-jsx test cases
2 | import {
3 | reactive,
4 | ref,
5 | defineComponent,
6 | CSSProperties,
7 | ComponentPublicInstance,
8 | Transition,
9 | } from 'vue'
10 | import { shallowMount, mount, VueWrapper } from '@vue/test-utils'
11 |
12 | const patchFlagExpect = (
13 | wrapper: VueWrapper,
14 | flag: number,
15 | dynamic: string[] | null,
16 | ) => {
17 | // todo patchFlag
18 | // const { patchFlag, dynamicProps } = wrapper.vm.$.subTree as any
19 |
20 | // expect(patchFlag).toBe(flag)
21 | // expect(dynamicProps).toEqual(dynamic)
22 | }
23 |
24 | describe('Transform JSX', () => {
25 | test('should render with render function', () => {
26 | const wrapper = shallowMount({
27 | render() {
28 | return 123
29 | },
30 | })
31 | expect(wrapper.text()).toBe('123')
32 | })
33 |
34 | test('should render with setup', () => {
35 | const wrapper = shallowMount({
36 | setup() {
37 | return () => 123
38 | },
39 | })
40 | expect(wrapper.text()).toBe('123')
41 | })
42 |
43 | test('Extracts attrs', () => {
44 | const wrapper = shallowMount({
45 | setup() {
46 | return () =>
47 | },
48 | })
49 | expect(wrapper.element.id).toBe('hi')
50 | })
51 |
52 | test('Binds attrs', () => {
53 | const id = 'foo'
54 | const wrapper = shallowMount({
55 | setup() {
56 | return () => {id}
57 | },
58 | })
59 | expect(wrapper.text()).toBe('foo')
60 | })
61 |
62 | test('should not fallthrough with inheritAttrs: false', () => {
63 | const Child = defineComponent({
64 | props: {
65 | foo: Number,
66 | },
67 | setup(props) {
68 | return () => {props.foo}
69 | },
70 | })
71 |
72 | Child.inheritAttrs = false
73 |
74 | const wrapper = mount({
75 | render() {
76 | return
77 | },
78 | })
79 | expect(wrapper.classes()).toStrictEqual([])
80 | expect(wrapper.text()).toBe('1')
81 | })
82 |
83 | test('Fragment', () => {
84 | const Child = () => 123
85 |
86 | Child.inheritAttrs = false
87 |
88 | const wrapper = mount({
89 | setup() {
90 | return () => (
91 | <>
92 |
93 | 456
94 | >
95 | )
96 | },
97 | })
98 |
99 | expect(wrapper.html()).toBe('123
456
')
100 | })
101 |
102 | test('nested component', () => {
103 | const A = {
104 | B: defineComponent({
105 | setup() {
106 | return () => 123
107 | },
108 | }),
109 | }
110 |
111 | A.B.inheritAttrs = false
112 |
113 | const wrapper = mount(() => )
114 |
115 | expect(wrapper.html()).toBe('123
')
116 | })
117 |
118 | test('xlink:href', () => {
119 | const wrapper = shallowMount({
120 | setup() {
121 | return () =>
122 | },
123 | })
124 | expect(wrapper.attributes()['xlink:href']).toBe('#name')
125 | })
126 |
127 | test('Merge class', () => {
128 | const wrapper = shallowMount({
129 | setup() {
130 | // @ts-ignore
131 | return () =>
132 | },
133 | })
134 | // DIFFERENT: vue-jsx-runtime can not merge props
135 | // expect(wrapper.classes().sort()).toEqual(['a', 'b'].sort())
136 | expect(wrapper.classes().sort()).toEqual(['b'].sort())
137 | })
138 |
139 | test('Merge style', () => {
140 | const propsA = {
141 | style: {
142 | color: 'red',
143 | } as CSSProperties,
144 | }
145 | const propsB = {
146 | style: {
147 | color: 'blue',
148 | width: '300px',
149 | height: '300px',
150 | } as CSSProperties,
151 | }
152 | const wrapper = shallowMount({
153 | setup() {
154 | // @ts-ignore
155 | return () =>
156 | },
157 | })
158 | expect(wrapper.html()).toBe(
159 | '',
160 | )
161 | })
162 |
163 | test('JSXSpreadChild', () => {
164 | const a = ['1', '2']
165 | const wrapper = shallowMount({
166 | setup() {
167 | return () => {[...a]}
168 | },
169 | })
170 | expect(wrapper.text()).toBe('12')
171 | })
172 |
173 | test('domProps input[value]', () => {
174 | const val = 'foo'
175 | const wrapper = shallowMount({
176 | setup() {
177 | return () =>
178 | },
179 | })
180 | expect(wrapper.html()).toBe('')
181 | })
182 |
183 | test('domProps input[checked]', () => {
184 | const val = true
185 | const wrapper = shallowMount({
186 | setup() {
187 | return () =>
188 | },
189 | })
190 |
191 | expect(wrapper.vm.$.subTree?.props?.checked).toBe(val)
192 | })
193 |
194 | test('domProps option[selected]', () => {
195 | const val = true
196 | const wrapper = shallowMount({
197 | render() {
198 | return
199 | },
200 | })
201 | expect(wrapper.vm.$.subTree?.props?.selected).toBe(val)
202 | })
203 |
204 | test('domProps video[muted]', () => {
205 | const val = true
206 | const wrapper = shallowMount({
207 | render() {
208 | return
209 | },
210 | })
211 |
212 | expect(wrapper.vm.$.subTree?.props?.muted).toBe(val)
213 | })
214 |
215 | test('Spread (single object expression)', () => {
216 | const props = {
217 | id: '1',
218 | }
219 | const wrapper = shallowMount({
220 | render() {
221 | return 123
222 | },
223 | })
224 | expect(wrapper.html()).toBe('123
')
225 | })
226 |
227 | test('Spread (mixed)', async () => {
228 | const calls: number[] = []
229 | // DIFFERENT: vue-jsx-runtime can not merge props
230 | const data = {
231 | id: 'hehe',
232 | // onClick() {
233 | // calls.push(3)
234 | // },
235 | innerHTML: '2',
236 | // class: ['a', 'b'],
237 | }
238 |
239 | const wrapper = shallowMount({
240 | setup() {
241 | return () => (
242 | calls.push(4)}
247 | hook-insert={() => calls.push(2)}
248 | />
249 | )
250 | },
251 | })
252 |
253 | expect(wrapper.attributes('id')).toBe('hehe')
254 | expect(wrapper.attributes('href')).toBe('huhu')
255 | expect(wrapper.text()).toBe('2')
256 | expect(wrapper.classes()).toEqual(expect.arrayContaining(['a', 'b', 'c']))
257 |
258 | await wrapper.trigger('click')
259 |
260 | // expect(calls).toEqual(expect.arrayContaining([3, 4]))
261 | expect(calls).toEqual(expect.arrayContaining([4]))
262 | })
263 | })
264 |
265 | describe('directive', () => {
266 | test('vHtml', () => {
267 | const wrapper = shallowMount({
268 | setup() {
269 | const html = 'foo
'
270 | return () =>
271 | },
272 | })
273 | expect(wrapper.html()).toBe('foo
')
274 | })
275 |
276 | test('vText', () => {
277 | const text = 'foo'
278 | const wrapper = shallowMount({
279 | setup() {
280 | return () =>
281 | },
282 | })
283 | expect(wrapper.html()).toBe('foo
')
284 | })
285 | })
286 |
287 | describe('slots', () => {
288 | test('with default', () => {
289 | const A = defineComponent({
290 | setup(_, { slots }) {
291 | return () => (
292 |
293 | {slots.default?.()}
294 | {slots.foo?.('val')}
295 |
296 | )
297 | },
298 | })
299 |
300 | A.inheritAttrs = false
301 |
302 | const wrapper = mount({
303 | setup() {
304 | return () => (
305 | val }}>
306 | default
307 |
308 | )
309 | },
310 | })
311 |
312 | expect(wrapper.html()).toBe('defaultval
')
313 | })
314 |
315 | test('without default', () => {
316 | const A = defineComponent({
317 | setup(_, { slots }) {
318 | return () => {slots.foo?.('foo')}
319 | },
320 | })
321 |
322 | A.inheritAttrs = false
323 |
324 | const wrapper = mount({
325 | setup() {
326 | return () => val }} />
327 | },
328 | })
329 |
330 | expect(wrapper.html()).toBe('foo
')
331 | })
332 | })
333 |
334 | describe('PatchFlags', () => {
335 | test('static', () => {
336 | const wrapper = shallowMount({
337 | setup() {
338 | return () => static
339 | },
340 | })
341 | patchFlagExpect(wrapper, 0, null)
342 | })
343 |
344 | test('props', async () => {
345 | const wrapper = mount({
346 | setup() {
347 | const visible = ref(true)
348 | const onClick = () => {
349 | visible.value = false
350 | }
351 | return () => (
352 |
353 | NEED_PATCH
354 |
355 | )
356 | },
357 | })
358 |
359 | patchFlagExpect(wrapper, 8, ['onClick'])
360 | await wrapper.trigger('click')
361 | expect(wrapper.html()).toBe('NEED_PATCH
')
362 | })
363 |
364 | test('full props', async () => {
365 | // DIFFERENT: vue-jsx-runtime can not merge props
366 | const wrapper = mount({
367 | setup() {
368 | const bindProps = reactive({ class: 'a', style: { marginTop: 10 } })
369 | const onClick = () => {
370 | bindProps.class = 'b'
371 | }
372 |
373 | return () => (
374 |
375 | full props
376 |
377 | )
378 | },
379 | })
380 | patchFlagExpect(wrapper, 16, ['onClick'])
381 |
382 | await wrapper.trigger('click')
383 |
384 | expect(wrapper.classes().sort()).toEqual(['b'].sort())
385 | })
386 | })
387 |
388 | describe('variables outside slots', () => {
389 | const A = defineComponent({
390 | props: {
391 | inc: Function,
392 | },
393 | render() {
394 | return this.$slots.default?.()
395 | },
396 | })
397 |
398 | A.inheritAttrs = false
399 |
400 | test('internal', async () => {
401 | const wrapper = mount(
402 | defineComponent({
403 | data() {
404 | return {
405 | val: 0,
406 | }
407 | },
408 | methods: {
409 | inc() {
410 | this.val += 1
411 | },
412 | },
413 | render() {
414 | const attrs = {
415 | innerHTML: `${this.val}`,
416 | }
417 | return (
418 |
419 |
420 |
421 |
422 |
423 |
424 |
427 |
428 | )
429 | },
430 | }),
431 | )
432 |
433 | expect(wrapper.get('#textarea').element.innerHTML).toBe('0')
434 | await wrapper.get('#button').trigger('click')
435 | expect(wrapper.get('#textarea').element.innerHTML).toBe('1')
436 | })
437 |
438 | test('forwarded', async () => {
439 | const wrapper = mount({
440 | data() {
441 | return {
442 | val: 0,
443 | }
444 | },
445 | methods: {
446 | inc() {
447 | this.val += 1
448 | },
449 | },
450 | render() {
451 | const attrs = {
452 | innerHTML: `${this.val}`,
453 | }
454 | const textarea =
455 | return (
456 |
457 | {textarea}
458 |
461 |
462 | )
463 | },
464 | })
465 |
466 | expect(wrapper.get('#textarea').element.innerHTML).toBe('0')
467 | await wrapper.get('#button').trigger('click')
468 | expect(wrapper.get('#textarea').element.innerHTML).toBe('1')
469 | })
470 | })
471 |
472 | test('reassign variable as component should work', () => {
473 | let a: any = 1
474 |
475 | const A = defineComponent({
476 | setup(_, { slots }) {
477 | return () => {slots.default!()}
478 | },
479 | })
480 |
481 | /* eslint-disable */
482 | const _a2 = 2
483 | a = _a2
484 | /* eslint-enable */
485 |
486 | a = {a}
487 |
488 | const wrapper = mount({
489 | render() {
490 | return a
491 | },
492 | })
493 |
494 | expect(wrapper.html()).toBe('2')
495 | })
496 |
497 | describe('should support passing object slots via JSX children', () => {
498 | const A = defineComponent({
499 | setup(_, { slots }) {
500 | return () => (
501 |
502 | {slots.default?.()}
503 | {slots.foo?.()}
504 |
505 | )
506 | },
507 | })
508 |
509 | test('single expression, variable', () => {
510 | const slots = { default: () => 1, foo: () => 2 }
511 |
512 | const wrapper = mount({
513 | render() {
514 | return {slots}
515 | },
516 | })
517 |
518 | expect(wrapper.html()).toBe('12')
519 | })
520 |
521 | test('single expression, object literal', () => {
522 | const wrapper = mount({
523 | render() {
524 | return {{ default: () => 1, foo: () => 2 }}
525 | },
526 | })
527 |
528 | expect(wrapper.html()).toBe('12')
529 | })
530 |
531 | test('single expression, object literal', () => {
532 | const wrapper = mount({
533 | render() {
534 | return {{ default: () => 1, foo: () => 2 }}
535 | },
536 | })
537 |
538 | expect(wrapper.html()).toBe('12')
539 | })
540 |
541 | test('single expression, non-literal value', () => {
542 | const foo = () => 1
543 |
544 | const wrapper = mount({
545 | render() {
546 | return {foo()}
547 | },
548 | })
549 |
550 | expect(wrapper.html()).toBe('1')
551 | })
552 |
553 | test('single expression, function expression', () => {
554 | const wrapper = mount({
555 | render() {
556 | return {() => 'foo'}
557 | },
558 | })
559 |
560 | expect(wrapper.html()).toBe('foo')
561 | })
562 |
563 | test('single expression, function expression variable', () => {
564 | const foo = () => 'foo'
565 |
566 | const wrapper = mount({
567 | render() {
568 | return {foo}
569 | },
570 | })
571 |
572 | expect(wrapper.html()).toBe('foo')
573 | })
574 |
575 | test('single expression, array map expression', () => {
576 | const data = ['A', 'B', 'C']
577 |
578 | const wrapper = mount({
579 | render() {
580 | return (
581 | <>
582 | {data.map((item) => (
583 |
584 | {item}
585 |
586 | ))}
587 | >
588 | )
589 | },
590 | })
591 |
592 | expect(wrapper.html()).toBe(
593 | 'ABC',
594 | )
595 | })
596 |
597 | test('xx', () => {
598 | const data = ['A', 'B', 'C']
599 |
600 | const wrapper = mount({
601 | render() {
602 | return (
603 | <>
604 | {data.map((item) => (
605 | {() => {item}}
606 | ))}
607 | >
608 | )
609 | },
610 | })
611 |
612 | expect(wrapper.html()).toBe(
613 | 'ABC',
614 | )
615 | })
616 | })
617 |
--------------------------------------------------------------------------------
/__tests__/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "include": [
4 | "./"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/__tests__/v-model.spec.tsx:
--------------------------------------------------------------------------------
1 | // use https://github.com/vuejs/jsx-next/tree/dev/packages/babel-plugin-jsx test cases
2 | import { shallowMount, mount } from '@vue/test-utils'
3 | import { defineComponent, VNode, ref } from 'vue'
4 |
5 | // v-model={[context, path, arg, modifers]}
6 |
7 | test('input[type="checkbox"] should work', async () => {
8 | const wrapper = shallowMount({
9 | data() {
10 | return {
11 | test: true,
12 | }
13 | },
14 | render() {
15 | return
16 | },
17 | }, {
18 | // fix jsdom https://github.com/jsdom/jsdom/commit/1da0a4c1f4a0e9f8fa5576a9c5e2897b8307f7ab
19 | attachTo: document.body
20 | })
21 |
22 | expect(wrapper.vm.$el.checked).toBe(true)
23 | wrapper.vm.test = false
24 | await wrapper.vm.$nextTick()
25 | expect(wrapper.vm.$el.checked).toBe(false)
26 | expect(wrapper.vm.test).toBe(false)
27 | await wrapper.trigger('click')
28 | expect(wrapper.vm.$el.checked).toBe(true)
29 | expect(wrapper.vm.test).toBe(true)
30 | })
31 |
32 | test('input[type="radio"] should work', async () => {
33 | const test = ref('1')
34 | const wrapper = mount({
35 | setup() {
36 | return () => {
37 | return (
38 | <>
39 |
40 |
41 | >
42 | )
43 | }
44 | }
45 | }, {
46 | // fix jsdom https://github.com/jsdom/jsdom/commit/1da0a4c1f4a0e9f8fa5576a9c5e2897b8307f7ab
47 | attachTo: document.body
48 | })
49 |
50 | const [a, b] = wrapper.vm.$.subTree.children as VNode[]
51 |
52 | expect(a.el!.checked).toBe(true)
53 | // wrapper.vm.test = '2'
54 | test.value = '2'
55 | await wrapper.vm.$nextTick()
56 | expect(a.el!.checked).toBe(false)
57 | expect(b.el!.checked).toBe(true)
58 | await a.el!.click()
59 | expect(a.el!.checked).toBe(true)
60 | expect(b.el!.checked).toBe(false)
61 | // expect(wrapper.vm.test).toBe('1')
62 | expect(test.value).toBe('1')
63 | })
64 |
65 | test('select should work with value bindings', async () => {
66 | const wrapper = shallowMount({
67 | data: () => ({
68 | test: 2,
69 | }),
70 | render() {
71 | return (
72 |
77 | )
78 | },
79 | })
80 |
81 | const el = wrapper.vm.$el
82 |
83 | expect(el.value).toBe('2')
84 | expect(el.children[1].selected).toBe(true)
85 | wrapper.vm.test = 3
86 | await wrapper.vm.$nextTick()
87 | expect(el.value).toBe('3')
88 | expect(el.children[2].selected).toBe(true)
89 |
90 | el.value = '1'
91 | await wrapper.trigger('change')
92 | expect(wrapper.vm.test).toBe('1')
93 |
94 | el.value = '2'
95 | await wrapper.trigger('change')
96 | expect(wrapper.vm.test).toBe(2)
97 | })
98 |
99 | test('textarea should update value both ways', async () => {
100 | const wrapper = shallowMount({
101 | data: () => ({
102 | test: 'b',
103 | }),
104 | render() {
105 | return
106 | },
107 | })
108 | const el = wrapper.vm.$el
109 |
110 | expect(el.value).toBe('b')
111 | wrapper.vm.test = 'a'
112 | await wrapper.vm.$nextTick()
113 | expect(el.value).toBe('a')
114 | el.value = 'c'
115 | await wrapper.trigger('input')
116 | expect(wrapper.vm.test).toBe('c')
117 | })
118 |
119 | test('input[type="text"] should update value both ways', async () => {
120 | const wrapper = shallowMount({
121 | data: () => ({
122 | test: 'b',
123 | }),
124 | render() {
125 | return
126 | },
127 | })
128 | const el = wrapper.vm.$el
129 |
130 | expect(el.value).toBe('b')
131 | wrapper.vm.test = 'a'
132 | await wrapper.vm.$nextTick()
133 | expect(el.value).toBe('a')
134 | el.value = 'c'
135 | await wrapper.trigger('input')
136 | expect(wrapper.vm.test).toBe('c')
137 | })
138 |
139 | test('input[type="text"] .lazy modifier', async () => {
140 | const wrapper = shallowMount({
141 | data: () => ({
142 | test: 'b',
143 | }),
144 | render() {
145 | return
146 | },
147 | })
148 | const el = wrapper.vm.$el
149 |
150 | expect(el.value).toBe('b')
151 | expect(wrapper.vm.test).toBe('b')
152 | el.value = 'c'
153 | await wrapper.trigger('input')
154 | expect(wrapper.vm.test).toBe('b')
155 | el.value = 'c'
156 | await wrapper.trigger('change')
157 | expect(wrapper.vm.test).toBe('c')
158 | })
159 |
160 | test('dynamic type should work', async () => {
161 | const wrapper = shallowMount({
162 | data() {
163 | return {
164 | test: true,
165 | type: 'checkbox',
166 | }
167 | },
168 | render() {
169 | return
170 | },
171 | })
172 |
173 | expect(wrapper.vm.$el.checked).toBe(true)
174 | wrapper.vm.test = false
175 | await wrapper.vm.$nextTick()
176 | expect(wrapper.vm.$el.checked).toBe(false)
177 | })
178 |
179 | test('underscore modifier should work', async () => {
180 | const wrapper = shallowMount({
181 | data: () => ({
182 | test: 'b',
183 | }),
184 | render() {
185 | return
186 | },
187 | })
188 | const el = wrapper.vm.$el
189 |
190 | expect(el.value).toBe('b')
191 | expect(wrapper.vm.test).toBe('b')
192 | el.value = 'c'
193 | await wrapper.trigger('input')
194 | expect(wrapper.vm.test).toBe('b')
195 | el.value = 'c'
196 | await wrapper.trigger('change')
197 | expect(wrapper.vm.test).toBe('c')
198 | })
199 |
200 | test('underscore modifier should work in custom component', async () => {
201 | const Child = defineComponent({
202 | emits: ['update:modelValue'],
203 | props: {
204 | modelValue: {
205 | type: Number,
206 | default: 0,
207 | },
208 | modelModifiers: {
209 | default: () => ({ double: false }),
210 | },
211 | },
212 | setup(props, { emit }) {
213 | const handleClick = () => {
214 | emit('update:modelValue', 3)
215 | }
216 | return () => (
217 |
218 | {props.modelModifiers.double
219 | ? props.modelValue * 2
220 | : props.modelValue}
221 |
222 | )
223 | },
224 | })
225 |
226 | const wrapper = mount({
227 | data() {
228 | return {
229 | foo: 1,
230 | }
231 | },
232 | render() {
233 | return
234 | },
235 | })
236 |
237 | expect(wrapper.html()).toBe('2
')
238 | wrapper.vm.$data.foo += 1
239 | await wrapper.vm.$nextTick()
240 | expect(wrapper.html()).toBe('4
')
241 | await wrapper.trigger('click')
242 | expect(wrapper.html()).toBe('6
')
243 | })
244 |
245 | test('Named model', async () => {
246 | const Child = defineComponent({
247 | emits: ['update:value'],
248 | props: {
249 | value: {
250 | type: Number,
251 | default: 0,
252 | },
253 | },
254 | setup(props, { emit }) {
255 | const handleClick = () => {
256 | emit('update:value', 2)
257 | }
258 | return () => (
259 | { props.value }
260 | )
261 | },
262 | })
263 |
264 | const wrapper = mount({
265 | data: () => ({
266 | foo: 0,
267 | }),
268 | render() {
269 | return
270 | },
271 | })
272 |
273 | expect(wrapper.html()).toBe('0
')
274 | wrapper.vm.$data.foo += 1
275 | await wrapper.vm.$nextTick()
276 | expect(wrapper.html()).toBe('1
')
277 | await wrapper.trigger('click')
278 | expect(wrapper.html()).toBe('2
')
279 | })
280 |
--------------------------------------------------------------------------------
/api-extractor.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
3 |
4 | "mainEntryPointFilePath": "./dist/index.d.ts",
5 |
6 | "apiReport": {
7 | "enabled": true,
8 | "reportFolder": "/temp/"
9 | },
10 |
11 | "docModel": {
12 | "enabled": true
13 | },
14 |
15 | "dtsRollup": {
16 | "enabled": true,
17 | "untrimmedFilePath": "./dist/vue-jsx-runtime.d.ts"
18 | },
19 |
20 | "tsdocMetadata": {
21 | "enabled": false
22 | },
23 |
24 | "messages": {
25 | "compilerMessageReporting": {
26 | "default": {
27 | "logLevel": "warning"
28 | }
29 | },
30 |
31 | "extractorMessageReporting": {
32 | "default": {
33 | "logLevel": "warning",
34 | "addToApiReportFile": true
35 | },
36 |
37 | "ae-missing-release-tag": {
38 | "logLevel": "none"
39 | }
40 | },
41 |
42 | "tsdocMessageReporting": {
43 | "default": {
44 | "logLevel": "warning"
45 | }
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | /* istanbul ignore next */
2 | module.exports = {
3 | presets: [
4 | '@babel/preset-env',
5 | ],
6 | plugins: [
7 | [
8 | '@babel/plugin-transform-react-jsx',
9 | {
10 | throwIfNamespace: false, // defaults to true
11 | runtime: 'automatic', // defaults to classic
12 | importSource: 'vue' // defaults to react
13 | }
14 | ],
15 | ],
16 | };
17 |
--------------------------------------------------------------------------------
/global.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'vue/jsx-runtime' {
2 | export const jsx: Function
3 | export const jsxs: Function
4 | export const jsxDEV: Function
5 | }
6 | declare module 'vue/jsx-dev-runtime' {
7 | export const jsx: Function
8 | export const jsxs: Function
9 | export const jsxDEV: Function
10 | }
11 |
12 | declare var __DEV__: boolean
13 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | if (process.env.NODE_ENV === 'production') {
4 | module.exports = require('./dist/vue-jsx-runtime.prod.cjs')
5 | } else {
6 | module.exports = require('./dist/vue-jsx-runtime.cjs')
7 | }
8 |
--------------------------------------------------------------------------------
/jest-setup.ts:
--------------------------------------------------------------------------------
1 | import 'regenerator-runtime/runtime';
2 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | collectCoverage: true,
3 | coverageDirectory: 'coverage',
4 | coverageReporters: ['html', 'lcov', 'text'],
5 | collectCoverageFrom: [
6 | 'src/*.ts'
7 | ],
8 | coveragePathIgnorePatterns: [
9 | '/node_modules/',
10 | '\\.d\\.ts$'
11 | ],
12 | rootDir: __dirname,
13 | testEnvironment: 'jsdom',
14 | setupFiles: ['./jest-setup.ts'],
15 | moduleNameMapper: {
16 | "vue/jsx-runtime": ["/src"],
17 | "vue/jsx-dev-runtime": ["/src"]
18 | },
19 | transform: {
20 | '\\.(ts|tsx)$': 'ts-jest',
21 | },
22 | globals: {
23 | __DEV__: true,
24 | 'ts-jest': {
25 | babelConfig: true,
26 | },
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/jsx-dev-runtime.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | if (process.env.NODE_ENV === 'production') {
4 | module.exports = require('./dist/vue-jsx-runtime.prod.cjs')
5 | } else {
6 | module.exports = require('./dist/vue-jsx-runtime.cjs')
7 | }
8 |
--------------------------------------------------------------------------------
/jsx-runtime.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | if (process.env.NODE_ENV === 'production') {
4 | module.exports = require('./dist/vue-jsx-runtime.prod.cjs')
5 | } else {
6 | module.exports = require('./dist/vue-jsx-runtime.cjs')
7 | }
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-jsx-runtime",
3 | "version": "0.1.0",
4 | "description": "vue jsx runtime support",
5 | "main": "index.js",
6 | "module": "dist/vue-jsx-runtime.mjs",
7 | "unpkg": "dist/vue-jsx-runtime.iife.prod.js",
8 | "jsdelivr": "dist/vue-jsx-runtime.iife.prod.js",
9 | "types": "dist/vue-jsx-runtime.d.ts",
10 | "exports": {
11 | ".": {
12 | "browser": "./dist/vue-jsx-runtime.esm-browser.js",
13 | "node": {
14 | "import": {
15 | "production": "./dist/vue-jsx-runtime.prod.cjs",
16 | "development": "./dist/vue-jsx-runtime.mjs",
17 | "default": "./dist/vue-jsx-runtime.mjs"
18 | },
19 | "require": {
20 | "production": "./dist/vue-jsx-runtime.prod.cjs",
21 | "development": "./dist/vue-jsx-runtime.cjs",
22 | "default": "./index.js"
23 | }
24 | },
25 | "import": "./dist/vue-jsx-runtime.mjs"
26 | },
27 | "./jsx-runtime": {
28 | "browser": "./dist/vue-jsx-runtime.esm-browser.js",
29 | "node": {
30 | "import": {
31 | "production": "./dist/vue-jsx-runtime.prod.cjs",
32 | "development": "./dist/vue-jsx-runtime.mjs",
33 | "default": "./dist/vue-jsx-runtime.mjs"
34 | },
35 | "require": {
36 | "production": "./dist/vue-jsx-runtime.prod.cjs",
37 | "development": "./dist/vue-jsx-runtime.cjs",
38 | "default": "./index.js"
39 | }
40 | },
41 | "import": "./dist/vue-jsx-runtime.mjs"
42 | },
43 | "./jsx-dev-runtime": {
44 | "browser": "./dist/vue-jsx-runtime.esm-browser.js",
45 | "node": {
46 | "import": {
47 | "production": "./dist/vue-jsx-runtime.prod.cjs",
48 | "development": "./dist/vue-jsx-runtime.mjs",
49 | "default": "./dist/vue-jsx-runtime.mjs"
50 | },
51 | "require": {
52 | "production": "./dist/vue-jsx-runtime.prod.cjs",
53 | "development": "./dist/vue-jsx-runtime.cjs",
54 | "default": "./index.js"
55 | }
56 | },
57 | "import": "./dist/vue-jsx-runtime.mjs"
58 | },
59 | "./package.json": "./package.json",
60 | "./dist/*": "./dist/*"
61 | },
62 | "sideEffects": false,
63 | "scripts": {
64 | "bootstrap": "pnpm install --shamefully-hoist",
65 | "release": "pnpm test && pnpm build && pnpm publish --access=public",
66 | "build": "rm -rf dist && pnpm build:js && pnpm build:dts",
67 | "build:js": "rollup -c ./rollup.config.js",
68 | "build:dts": "api-extractor run --local --verbose",
69 | "test": "pnpm test:unit",
70 | "test:unit": "jest --coverage"
71 | },
72 | "files": [
73 | "dist",
74 | "jsx-runtime.js",
75 | "jsx-dev-runtime.js"
76 | ],
77 | "repository": {
78 | "type": "git",
79 | "url": "git+https://github.com/dolymood/vue-jsx-runtime.git"
80 | },
81 | "keywords": [
82 | "jsx",
83 | "jsx-runtime",
84 | "vue"
85 | ],
86 | "peerDependencies": {
87 | "vue": "3"
88 | },
89 | "license": "MIT",
90 | "bugs": {
91 | "url": "https://github.com/dolymood/vue-jsx-runtime/issues"
92 | },
93 | "homepage": "https://github.com/dolymood/vue-jsx-runtime",
94 | "devDependencies": {
95 | "@babel/core": "^7.16.0",
96 | "@babel/plugin-transform-react-jsx": "^7.16.0",
97 | "@babel/preset-env": "^7.16.4",
98 | "@microsoft/api-extractor": "^7.19.2",
99 | "@rollup/plugin-alias": "^3.1.8",
100 | "@rollup/plugin-commonjs": "^21.0.1",
101 | "@rollup/plugin-node-resolve": "^13.1.1",
102 | "@rollup/plugin-replace": "^3.0.0",
103 | "@types/jest": "^27.0.3",
104 | "@vue/shared": "^3.2.24",
105 | "@vue/test-utils": "2.0.0-beta.2",
106 | "jest": "^27.4.4",
107 | "regenerator-runtime": "^0.13.9",
108 | "rollup": "^2.61.1",
109 | "rollup-plugin-terser": "^7.0.2",
110 | "rollup-plugin-typescript2": "^0.31.1",
111 | "ts-jest": "^27.1.1",
112 | "typescript": "^4.5.3",
113 | "vue": "^3.2.24"
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import path from 'path'
3 | import ts from 'rollup-plugin-typescript2'
4 | import replace from '@rollup/plugin-replace'
5 | import resolve from '@rollup/plugin-node-resolve'
6 | import commonjs from '@rollup/plugin-commonjs'
7 |
8 | const pkg = require('./package.json')
9 | const name = pkg.name
10 |
11 | const banner = `/*!
12 | * ${pkg.name} v${pkg.version}
13 | * (c) ${new Date().getFullYear()} dolymood (dolymood@gmail.com)
14 | * @license MIT
15 | */`
16 |
17 | // ensure TS checks only once for each build
18 | let hasTSChecked = false
19 |
20 | const outputConfigs = {
21 | // each file name has the format: `dist/${name}.${format}.js`
22 | // format being a key of this object
23 | mjs: {
24 | file: pkg.module,
25 | format: `es`,
26 | },
27 | cjs: {
28 | file: pkg.module.replace('mjs', 'cjs'),
29 | format: `cjs`,
30 | },
31 | global: {
32 | file: pkg.unpkg,
33 | format: `iife`,
34 | },
35 | browser: {
36 | file: `dist/${name}.esm-browser.js`,
37 | format: `es`,
38 | },
39 | }
40 |
41 | const packageBuilds = Object.keys(outputConfigs)
42 | const packageConfigs = packageBuilds.map((format) =>
43 | createConfig(format, outputConfigs[format])
44 | )
45 |
46 | // only add the production ready if we are bundling the options
47 | packageBuilds.forEach((buildName) => {
48 | if (buildName === 'cjs') {
49 | packageConfigs.push(createProductionConfig(buildName))
50 | } else if (buildName === 'global') {
51 | packageConfigs.push(createMinifiedConfig(buildName))
52 | }
53 | })
54 |
55 | export default packageConfigs
56 |
57 | function createConfig(buildName, output, plugins = []) {
58 | output.sourcemap = !!process.env.SOURCE_MAP
59 | output.banner = banner
60 | output.externalLiveBindings = false
61 | output.globals = {
62 | 'vue': 'Vue'
63 | }
64 |
65 | const isProductionBuild = /\.prod\.[cmj]s$/.test(output.file)
66 | const isGlobalBuild = buildName === 'global'
67 | const isRawESMBuild = buildName === 'browser'
68 | const isNodeBuild = buildName === 'cjs'
69 | const isBundlerESMBuild = buildName === 'browser' || buildName === 'mjs'
70 |
71 | if (isGlobalBuild) output.name = 'VueJSXRuntime'
72 |
73 | const shouldEmitDeclarations = !hasTSChecked
74 |
75 | const tsPlugin = ts({
76 | check: !hasTSChecked,
77 | tsconfig: path.resolve(__dirname, './tsconfig.json'),
78 | cacheRoot: path.resolve(__dirname, './node_modules/.rts2_cache'),
79 | tsconfigOverride: {
80 | compilerOptions: {
81 | sourceMap: output.sourcemap,
82 | declaration: shouldEmitDeclarations,
83 | declarationMap: shouldEmitDeclarations,
84 | },
85 | exclude: ['__tests__'],
86 | },
87 | })
88 | // we only need to check TS and generate declarations once for each build.
89 | // it also seems to run into weird issues when checking multiple times
90 | // during a single build.
91 | hasTSChecked = true
92 |
93 | const external = ['vue']
94 | if (!isGlobalBuild) {
95 | external.push.apply(external, ['@vue/shared'])
96 | }
97 |
98 | const nodePlugins = [resolve(), commonjs()]
99 |
100 | return {
101 | input: `src/index.ts`,
102 | // Global and Browser ESM builds inlines everything so that they can be
103 | // used alone.
104 | external,
105 | plugins: [
106 | tsPlugin,
107 | createReplacePlugin(
108 | isProductionBuild,
109 | isBundlerESMBuild,
110 | // isBrowserBuild?
111 | isNodeBuild
112 | ),
113 | ...nodePlugins,
114 | ...plugins,
115 | ],
116 | output,
117 | // onwarn: (msg, warn) => {
118 | // if (!/Circular/.test(msg)) {
119 | // warn(msg)
120 | // }
121 | // },
122 | }
123 | }
124 |
125 | function createReplacePlugin(
126 | isProduction,
127 | isBundlerESMBuild,
128 | isNodeBuild
129 | ) {
130 | const replacements = {
131 | __DEV__:
132 | isBundlerESMBuild || (isNodeBuild && !isProduction)
133 | ? // preserve to be handled by bundlers
134 | `(process.env.NODE_ENV !== 'production')`
135 | : // hard coded dev/prod builds
136 | JSON.stringify(!isProduction)
137 | }
138 | // allow inline overrides like
139 | //__RUNTIME_COMPILE__=true yarn build
140 | Object.keys(replacements).forEach((key) => {
141 | if (key in process.env) {
142 | replacements[key] = process.env[key]
143 | }
144 | })
145 | return replace({
146 | preventAssignment: true,
147 | values: replacements,
148 | })
149 | }
150 |
151 | function createProductionConfig(format) {
152 | const extension = format === 'cjs' ? 'cjs' : 'js'
153 | const descriptor = format === 'cjs' ? '' : `.${format}`
154 | return createConfig(format, {
155 | file: `dist/${name}${descriptor}.prod.${extension}`,
156 | format: outputConfigs[format].format,
157 | })
158 | }
159 |
160 | function createMinifiedConfig(format) {
161 | const { terser } = require('rollup-plugin-terser')
162 | return createConfig(
163 | format,
164 | {
165 | file: `dist/${name}.${format === 'global' ? 'iife' : format}.prod.js`,
166 | format: outputConfigs[format].format,
167 | },
168 | [
169 | terser({
170 | module: /^esm/.test(format),
171 | compress: {
172 | ecma: 2015,
173 | pure_getters: true,
174 | },
175 | }),
176 | ]
177 | )
178 | }
179 |
--------------------------------------------------------------------------------
/src/dev.ts:
--------------------------------------------------------------------------------
1 | import { VNodeTypes } from 'vue'
2 | import { jsx } from './jsx'
3 |
4 | export function jsxDEV(
5 | type: VNodeTypes,
6 | config: Record = {},
7 | maybeKey?: string,
8 | source?: object,
9 | self?: any
10 | ) {
11 | // istanbul ignore next
12 | if (__DEV__) {
13 | return jsx(type, config, maybeKey, source, self)
14 | }
15 | }
16 |
17 | export function jsxWithValidation(
18 | type: VNodeTypes,
19 | props: Record = {},
20 | key?: string,
21 | isStaticChildren?: boolean,
22 | source?: object,
23 | self?: any
24 | ) {
25 | return jsxDEV(type, props, key, source, self)
26 | }
27 |
28 | // istanbul ignore next
29 | export function jsxWithValidationStatic(
30 | type: VNodeTypes,
31 | props: Record = {},
32 | key?: string
33 | ) {
34 | if (__DEV__) {
35 | return jsxWithValidation(type, props, key, true)
36 | }
37 | }
38 |
39 | // istanbul ignore next
40 | export function jsxWithValidationDynamic(
41 | type: VNodeTypes,
42 | props: Record = {},
43 | key?: string
44 | ) {
45 | if (__DEV__) {
46 | return jsxWithValidation(type, props, key, false)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { Fragment } from 'vue'
2 | import { jsx as prodJsx, jsxs as prodJsxs } from './jsx'
3 | import {
4 | jsxWithValidationStatic,
5 | jsxWithValidationDynamic,
6 | jsxWithValidation
7 | } from './dev'
8 |
9 | // istanbul ignore next
10 | const jsx = __DEV__ ? jsxWithValidationDynamic : prodJsx
11 | // istanbul ignore next
12 | const jsxs = __DEV__ ? jsxWithValidationStatic : prodJsxs
13 | // istanbul ignore next
14 | const jsxDEV = __DEV__ ? jsxWithValidation : undefined
15 |
16 | export { Fragment, jsx, jsxs, jsxDEV }
17 |
--------------------------------------------------------------------------------
/src/jsx.ts:
--------------------------------------------------------------------------------
1 | import {
2 | VNodeTypes,
3 | DirectiveArguments,
4 | createVNode,
5 | toDisplayString,
6 | resolveDirective,
7 | withDirectives,
8 | vShow,
9 | vModelRadio,
10 | vModelCheckbox,
11 | vModelText,
12 | vModelSelect,
13 | vModelDynamic,
14 | isVNode
15 | } from 'vue'
16 |
17 | const hasOwnProperty = Object.prototype.hasOwnProperty
18 | const hasOwn = (val: object, key: string) => hasOwnProperty.call(val, key)
19 | const isFunction = (val: any) => typeof val === 'function'
20 | const isString = (val: any) => typeof val === 'string'
21 | const isSymbol = (val: any) => typeof val === 'symbol'
22 | const isObject = (val: any) => val !== null && typeof val === 'object'
23 | const isArray = Array.isArray
24 |
25 | const vModelEleDirTypes: Record = {
26 | select: vModelSelect,
27 | textarea: vModelText
28 | }
29 |
30 | const vModelDirTypes: Record = {
31 | default: vModelText,
32 | radio: vModelRadio,
33 | checkbox: vModelCheckbox,
34 | dynamic: vModelDynamic
35 | }
36 |
37 | type PropHandler = (
38 | type: VNodeTypes,
39 | val: any,
40 | props: Record,
41 | directives: Array,
42 | config: Record,
43 | arg?: string,
44 | // modifiers?: string | string[],
45 | name?: string
46 | ) => any
47 | type DirValue = [object, string | any[], string | string[], string[]] | any
48 |
49 | const DIR_CASES: Record = {
50 | // directives
51 | html: (type: VNodeTypes, val, props) => {
52 | props.innerHTML = val
53 | },
54 | text: (type: VNodeTypes, val, props) => {
55 | props.textContent = toDisplayString(val)
56 | },
57 | show: (type: VNodeTypes, val, props, directives) => {
58 | directives.push([vShow, val])
59 | },
60 | model: (type: VNodeTypes, val: DirValue, props, directives, config, arg) => {
61 | const isPlainNode = isString(type) || isSymbol(type)
62 |
63 | const [{ get, set }, args, modifiers] = processDirVal(val, arg)
64 |
65 | const prop = args || 'modelValue'
66 |
67 | props[prop] = get()
68 |
69 | const fixProp = prop === 'modelValue' ? 'model' : prop
70 | const fixModifiersKey = `${fixProp}Modifiers`
71 | props[fixModifiersKey] = modifiers
72 |
73 | props[`onUpdate:${prop}`] = set
74 | const eleDir = vModelEleDirTypes[type as string]
75 | const typeDir = (isString(config.type) || !config.type) ? (vModelDirTypes[config.type] || vModelDirTypes.default) : vModelDirTypes.dynamic
76 | const directiveName = eleDir || typeDir
77 | directives.push([
78 | directiveName,
79 | props[prop],
80 | isPlainNode ? undefined : prop,
81 | props[fixModifiersKey]
82 | ])
83 | if (isPlainNode) {
84 | // delete props value
85 | delete props[prop]
86 | delete props[fixModifiersKey]
87 | } else {
88 | // component, do not need to add vModel dir
89 | directives.pop()
90 | }
91 | },
92 | slots: (type: VNodeTypes, val, props, directives, config) => {
93 | PROP_CASES.children(type, config.children, props, directives, config)
94 | const children = props.children
95 | if (!children || !children.default) {
96 | // just normal nodes
97 | let defaultSlot = children
98 | if (children) {
99 | if (!isFunction(children)) {
100 | defaultSlot = () => children
101 | }
102 | }
103 | props.children = {}
104 | if (defaultSlot) {
105 | props.children.default = defaultSlot
106 | }
107 | }
108 | Object.assign(props.children, val)
109 | },
110 | __custom__: (type: VNodeTypes, val: DirValue, props, directives, config, arg, name) => {
111 | const [{ get, set }, args, modifiers] = processDirVal(val, arg)
112 |
113 | props[`onUpdate:${name}`] = set
114 |
115 | directives.push([
116 | resolveDirective(name!),
117 | get(),
118 | args,
119 | modifiers
120 | ])
121 | }
122 | }
123 |
124 | const PROP_CASES: Record = {
125 | children: (type: VNodeTypes, nodes: VNodeTypes | VNodeTypes[], props) => {
126 | if (props.__processed__children) return
127 | props.__processed__children = true
128 | if (isString(type) || isSymbol(type)) {
129 | if (!isArray(nodes) && isVNode(nodes)) {
130 | nodes = [nodes]
131 | }
132 | props.children = nodes
133 | } else {
134 | // component
135 | if (isVNode(nodes) || isArray(nodes) || !isObject(nodes)) {
136 | // nodes
137 | props.children = {
138 | default: isFunction(nodes) ? nodes : () => nodes
139 | }
140 | } else {
141 | // is object slots
142 | props.children = nodes
143 | }
144 | }
145 | }
146 | }
147 |
148 | // todo
149 | const reversedProps = {
150 | children: 1,
151 | __processed__children: 1
152 | }
153 |
154 | const xlinkRE = /^xlink([A-Z])/
155 |
156 | export function jsx(
157 | type: VNodeTypes,
158 | config: Record = {},
159 | maybeKey?: string,
160 | source?: object,
161 | self?: any
162 | ) {
163 | let propName: string
164 | const directives: DirectiveArguments = []
165 |
166 | const props: Record = {}
167 |
168 | for (propName in config) {
169 | if (hasOwn(config, propName)) {
170 | propName = propName as string
171 | const val = config[propName]
172 | // /^(v-[A-Za-z0-9-]|:|\.|@|#)/
173 | if (/^(v-[A-Za-z0-9-])/.test(propName)) {
174 | // directive
175 | // /(?:^v-([a-z0-9-]+))?(?:(?::|^\.|^@|^#)(\[[^\]]+\]|[^\.]+))?(.+)?$/i
176 | const match = /(?:^v-([a-z0-9-]+))?(?:(?::)([^\.]+))?(.+)?$/i.exec(propName)
177 | const dirName = match![1]
178 | const arg = match![2]
179 |
180 | // let modifiers = match![3] && match![3].slice(1).split('.') as string[]
181 | const dirFn = DIR_CASES[dirName] || DIR_CASES.__custom__
182 | dirFn(type, val, props, directives, config, arg, dirName)
183 | } else {
184 | if (hasOwn(PROP_CASES, propName)) {
185 | const propVal = PROP_CASES[propName](type, val, props, directives, config)
186 | if (propVal !== undefined) {
187 | props[propName] = propVal
188 | }
189 | } else {
190 | if (propName.match(xlinkRE)) {
191 | propName = propName.replace(xlinkRE, (_, firstCharacter) => `xlink:${firstCharacter.toLowerCase()}`)
192 | }
193 | props[propName] = val
194 | }
195 | }
196 | }
197 | }
198 |
199 | const children = props.children
200 |
201 | Object.keys(reversedProps).forEach((k) => {
202 | delete props[k]
203 | })
204 |
205 | if (maybeKey !== undefined && !hasOwn(props, 'key')) {
206 | props.key = '' + maybeKey
207 | }
208 | const vnode = createVNode(type, props, children)
209 | if (directives.length) {
210 | return withDirectives(vnode, directives)
211 | }
212 | return vnode
213 | }
214 |
215 | export const jsxs = jsx
216 |
217 | function processDirVal (val: DirValue, arg?: string) {
218 | if (!isArray(val)) {
219 | val = [val, 'value']
220 | }
221 |
222 | let [value, path, args, modifiers] = val
223 |
224 | if (isArray(args)) {
225 | // args is modifiers
226 | modifiers = args
227 | args = undefined
228 | }
229 |
230 | if (!arg) {
231 | arg = args
232 | }
233 |
234 | const _modifiers = modifiers ? processModifiers(modifiers) : undefined
235 |
236 | const paths = isArray(path) ? path : [path]
237 |
238 | const get = () => {
239 | return baseGetSet(value, paths)
240 | }
241 | const set = (v: any) => {
242 | return baseGetSet(value, paths, v)
243 | }
244 |
245 | return [{ get, set }, arg, _modifiers] as const
246 | }
247 |
248 | function processModifiers (modifiers: string | string[]) {
249 | modifiers = Array.isArray(modifiers) ? modifiers : modifiers ? [modifiers] : []
250 | const realModifiers: Record = {}
251 | modifiers.forEach((modifier) => {
252 | realModifiers[modifier] = true
253 | })
254 | return realModifiers
255 | }
256 |
257 | function baseGetSet(object: object, paths: string[], set?: any) {
258 | let val: any = object
259 |
260 | let index = 0
261 | const length = paths.length
262 | const max = length - 1
263 |
264 | while (val != null && index <= max) {
265 | if (set && index === max) {
266 | val[paths[index]] = set
267 | return set
268 | }
269 | val = val[paths[index++]]
270 | }
271 |
272 | return (index && index == length) ? val : undefined
273 | }
274 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 |
5 | /* Basic Options */
6 | // "incremental": true, /* Enable incremental compilation */
7 | "target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
8 | "module": "ESNext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
9 | "lib": ["ESNext", "DOM"], /* Specify library files to be included in the compilation. */
10 | // "allowJs": true, /* Allow javascript files to be compiled. */
11 | // "checkJs": true, /* Report errors in .js files. */
12 | "jsx": "react-jsxdev", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
13 | "jsxImportSource": "vue",
14 | "declaration": true, /* Generates corresponding '.d.ts' file. */
15 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
16 | // "sourceMap": true, /* Generates corresponding '.map' file. */
17 | // "outFile": "./", /* Concatenate and emit output to single file. */
18 | "outDir": "dist", /* Redirect output structure to the directory. */
19 | "rootDirs": ["./src"], /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
20 | // "composite": true, /* Enable project compilation */
21 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
22 | // "removeComments": true, /* Do not emit comments to output. */
23 | // "noEmit": true, /* Do not emit outputs. */
24 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
25 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
26 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
27 |
28 | /* Strict Type-Checking Options */
29 | "strict": true, /* Enable all strict type-checking options. */
30 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
31 | // "strictNullChecks": true, /* Enable strict null checks. */
32 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
33 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
34 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
35 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
36 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
37 |
38 | /* Additional Checks */
39 | // "noUnusedLocals": true, /* Report errors on unused locals. */
40 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
41 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
42 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
43 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
44 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */
45 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
46 |
47 | /* Module Resolution Options */
48 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
49 | // "baseUrl": ".", /* Base directory to resolve non-absolute module names. */
50 | "paths": {
51 | "vue/jsx-runtime": ["./src/"],
52 | "vue/jsx-dev-runtime": ["./src/"]
53 | },
54 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
55 | // "typeRoots": [], /* List of folders to include type definitions from. */
56 | "types": ["jest", "node"], /* Type declaration files to be included in compilation. */
57 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
58 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
59 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
60 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
61 |
62 | /* Source Map Options */
63 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
64 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
65 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
66 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
67 |
68 | /* Experimental Options */
69 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
70 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
71 |
72 | /* Advanced Options */
73 | "skipLibCheck": true, /* Skip type checking of declaration files. */
74 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
75 | },
76 | "include": [
77 | "src",
78 | "global.d.ts"
79 | ],
80 | "exclude": [
81 | "node_modules"
82 | ]
83 | }
84 |
--------------------------------------------------------------------------------