Skip to content

Commit a7477e7

Browse files
committed
feat: support children & sibling references
1 parent f9889af commit a7477e7

12 files changed

+204
-30
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@
122122
}
123123
},
124124
"dependencies": {
125+
"@babel/parser": "^7.24.7",
125126
"@vue-macros/common": "^1.10.4",
126127
"@vue-vapor/compiler-vapor": "3.2024.0-63dbc26",
127128
"@vue-vapor/vue": "3.2024.0-63dbc26",

pnpm-lock.yaml

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/core/compiler/compile.ts

+15-5
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import {
66
} from '@vue/compiler-dom'
77
import { extend, isString } from '@vue/shared'
88
import {
9+
type VaporCodegenResult as BaseVaporCodegenResult,
910
type HackOptions,
10-
type VaporCodegenResult,
1111
generate,
1212
} from '@vue-vapor/compiler-vapor'
13-
import { babelParse } from '@vue-macros/common'
13+
import { type Overwrite, babelParse } from '@vue-macros/common'
1414
import {
1515
type DirectiveTransform,
1616
type NodeTransform,
@@ -19,10 +19,15 @@ import {
1919

2020
import { transformElement } from './transforms/transformElement'
2121
import { transformChildren } from './transforms/transformChildren'
22-
import { IRNodeTypes, type RootNode } from './ir'
22+
import { IRNodeTypes, type RootIRNode, type RootNode } from './ir'
2323
import { transformText } from './transforms/transformText'
2424
import type { JSXElement, JSXFragment, Program } from '@babel/types'
2525

26+
export interface VaporCodegenResult
27+
extends Omit<BaseVaporCodegenResult, 'ast'> {
28+
ast: RootIRNode
29+
}
30+
2631
// code/AST -> IR (transform) -> JS (generate)
2732
export function compile(
2833
source: string | Program,
@@ -100,10 +105,15 @@ export function compile(
100105
}),
101106
)
102107

103-
return generate(ir as any, resolvedOptions)
108+
return generate(ir as any, resolvedOptions) as unknown as VaporCodegenResult
104109
}
105110

106-
export type CompilerOptions = HackOptions<BaseCompilerOptions>
111+
export type CompilerOptions = Overwrite<
112+
HackOptions<BaseCompilerOptions>,
113+
{
114+
nodeTransforms?: NodeTransform[]
115+
}
116+
>
107117
export type TransformPreset = [
108118
NodeTransform[],
109119
Record<string, DirectiveTransform>,

src/core/compiler/index.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,13 @@
1-
export * from './compile'
1+
export { generate } from '@vue-vapor/compiler-vapor'
2+
3+
export {
4+
// wrapTemplate,
5+
compile,
6+
type CompilerOptions,
7+
type TransformPreset,
8+
} from './compile'
29
export * from './transform'
10+
11+
export { transformText } from './transforms/transformText'
12+
export { transformElement } from './transforms/transformElement'
13+
export { transformChildren } from './transforms/transformChildren'

src/core/compiler/transform.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {
2-
type TransformOptions as BaseTransformOptions,
32
type CommentNode,
43
type CompilerCompatOptions,
54
type SimpleExpressionNode,
@@ -9,7 +8,6 @@ import {
98
import { EMPTY_OBJ, NOOP, extend, isArray } from '@vue/shared'
109
import {
1110
DynamicFlag,
12-
type HackOptions,
1311
type IRDynamicInfo,
1412
IRNodeTypes,
1513
type IRSlots,
@@ -18,6 +16,7 @@ import {
1816
} from '@vue-vapor/compiler-vapor'
1917
import { newBlock, newDynamic } from './transforms/utils'
2018
import { isConstantExpression } from './utils'
19+
import type { CompilerOptions } from './compile'
2120
import type { JSXAttribute, JSXElement } from '@babel/types'
2221
import type { BlockIRNode, RootIRNode, RootNode } from './ir/index'
2322

@@ -42,7 +41,7 @@ export interface DirectiveTransformResult {
4241
modelModifiers?: string[]
4342
}
4443

45-
export type TransformOptions = HackOptions<BaseTransformOptions>
44+
export type TransformOptions = CompilerOptions
4645

4746
const defaultOptions = {
4847
filename: '',
@@ -212,7 +211,7 @@ export function transform(
212211
return ir
213212
}
214213

215-
export function transformNode(context: TransformContext<JSXElement>) {
214+
export function transformNode(context: TransformContext<BlockIRNode['node']>) {
216215
let { node } = context
217216

218217
// apply transform plugins
@@ -243,7 +242,7 @@ export function transformNode(context: TransformContext<JSXElement>) {
243242
exitFns[i]()
244243
}
245244

246-
if (!context.node.start) {
245+
if (context.node.type === IRNodeTypes.ROOT) {
247246
context.registerTemplate()
248247
}
249248
}

src/core/compiler/transforms/transformChildren.ts

+12-3
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,18 @@ export const transformChildren: NodeTransform = (node, context) => {
1212

1313
if (!isFragment && node.type !== 'JSXElement') return
1414

15-
for (const [i, child] of (node.children || [])
16-
.filter((i) => (i.type === 'JSXText' ? i.value.trim() : true))
17-
.entries()) {
15+
Array.from(node.children).forEach((child, index) => {
16+
if (child.type === 'JSXText' && !child.value.trim()) {
17+
child.value = ' '
18+
if (!index) {
19+
node.children.splice(0, 1)
20+
} else if (index === node.children.length) {
21+
node.children.splice(-1, 1)
22+
}
23+
}
24+
})
25+
26+
for (const [i, child] of node.children.entries()) {
1827
const childContext = context.create(child, i)
1928
transformNode(childContext)
2029

src/core/compiler/transforms/transformText.ts

+16-7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { type ParseResult, parseExpression } from '@babel/parser'
12
import {
23
type BlockIRNode,
34
DynamicFlag,
@@ -11,6 +12,7 @@ import {
1112
resolveSimpleExpression,
1213
} from '../utils'
1314
import type {
15+
Expression,
1416
JSXElement,
1517
JSXExpressionContainer,
1618
JSXText,
@@ -89,14 +91,21 @@ function createTextLikeExpression(node: TextLike, context: TransformContext) {
8991
if (node.type === 'JSXText') {
9092
return resolveSimpleExpression(node.value, true, node.loc!)
9193
} else {
92-
return {
93-
...resolveSimpleExpression(
94-
context.ir.source.slice(node.expression.start!, node.expression.end!),
95-
false,
96-
node.loc!,
97-
),
98-
ast: node.expression,
94+
const source = context.ir.source.slice(
95+
node.expression.start!,
96+
node.expression.end!,
97+
)
98+
let ast: false | ParseResult<Expression> = false
99+
if (context.options.prefixIdentifiers) {
100+
ast = parseExpression(` ${source}`, {
101+
sourceType: 'module',
102+
plugins: context.options.expressionPlugins,
103+
})
99104
}
105+
106+
const result = resolveSimpleExpression(source, false, node.loc!)
107+
result.ast = ast
108+
return result
100109
}
101110
}
102111

src/core/compiler/utils.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
createSimpleExpression,
1010
isLiteralWhitelisted,
1111
} from '@vue/compiler-dom'
12-
import htmlTags from 'html-tags'
12+
import htmlTags, { type HtmlTags } from 'html-tags'
1313
import svgTags from 'svg-tags'
1414
import { EMPTY_EXPRESSION } from './transforms/utils'
1515
import type { VaporDirectiveNode } from './ir'
@@ -106,9 +106,7 @@ export function resolveSimpleExpression(
106106
export function isComponent(node: Node) {
107107
if (node.type === 'JSXIdentifier') {
108108
const name = node.name
109-
return (
110-
!htmlTags.includes(name as htmlTags.htmlTags) && !svgTags.includes(name)
111-
)
109+
return !htmlTags.includes(name as HtmlTags) && !svgTags.includes(name)
112110
} else {
113111
return node.type === 'JSXMemberExpression'
114112
}

test/compile.spec.ts

+36-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { describe, expect, test } from 'vitest'
2+
import { BindingTypes } from '@vue/compiler-dom'
23
import { type CompilerOptions, compile as _compile } from '../src/core/compiler'
34

45
export function compile(template: string, options: CompilerOptions = {}) {
@@ -14,11 +15,13 @@ describe('compile', () => {
1415
const { code } = compile(
1516
`<div>
1617
<div>hello</div>
18+
<input />
19+
<span />
1720
</div>`,
1821
)
1922
expect(code).toMatchInlineSnapshot(`
2023
"import { template as _template } from 'vue/vapor';
21-
const t0 = _template("<div><div>hello</div></div>")
24+
const t0 = _template("<div><div>hello</div> <input> <span></span></div>")
2225
2326
export function render(_ctx) {
2427
const n0 = t0()
@@ -28,16 +31,46 @@ describe('compile', () => {
2831
})
2932

3033
test('dynamic root', () => {
31-
const { code } = compile(`<><div>{1 + a}</div></>`)
34+
const { code } = compile(`<>{ 1 }{ 2 }</>`)
35+
expect(code).toMatchInlineSnapshot(`
36+
"import { createTextNode as _createTextNode } from 'vue/vapor';
37+
38+
export function render(_ctx) {
39+
const n0 = _createTextNode([1, 2])
40+
return n0
41+
}"
42+
`)
43+
})
44+
45+
test('dynamic root', () => {
46+
const { code } = compile(`<div>{a +b + c }</div>`)
3247
expect(code).toMatchInlineSnapshot(`
3348
"import { renderEffect as _renderEffect, setText as _setText, template as _template } from 'vue/vapor';
3449
const t0 = _template("<div></div>")
3550
3651
export function render(_ctx) {
3752
const n0 = t0()
38-
_renderEffect(() => _setText(n0, 1 + a_ctx.))
53+
_renderEffect(() => _setText(n0, _ctx.a +_ctx.b + _ctx.c))
3954
return n0
4055
}"
4156
`)
4257
})
58+
59+
describe('expression parsing', () => {
60+
test('interpolation', () => {
61+
const { code } = compile(`<>{ a + b }</>`, {
62+
inline: true,
63+
bindingMetadata: {
64+
b: BindingTypes.SETUP_REF,
65+
},
66+
})
67+
expect(code).toMatchInlineSnapshot(`
68+
"(() => {
69+
const n0 = _createTextNode(() => [a + b.value])
70+
return n0
71+
})()"
72+
`)
73+
// expect(code).contains('a + b.value')
74+
})
75+
})
4376
})

test/transforms/_utils.ts

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { babelParse } from '@vue-macros/common'
2+
import {
3+
type CompilerOptions,
4+
generate,
5+
transform,
6+
} from '../../src/core/compiler'
7+
import { IRNodeTypes, type RootNode } from '../../src/core/compiler/ir'
8+
import type { JSXElement, JSXFragment } from '@babel/types'
9+
10+
export function makeCompile(options: CompilerOptions = {}) {
11+
return (source: string, overrideOptions: CompilerOptions = {}) => {
12+
const {
13+
body: [statement],
14+
} = babelParse(source)
15+
let children!: JSXElement[] | JSXFragment['children']
16+
if (statement.type === 'ExpressionStatement') {
17+
children =
18+
statement.expression.type === 'JSXFragment'
19+
? statement.expression.children
20+
: statement.expression.type === 'JSXElement'
21+
? [statement.expression]
22+
: []
23+
}
24+
const ast: RootNode = {
25+
type: IRNodeTypes.ROOT,
26+
children,
27+
source,
28+
components: [],
29+
directives: [],
30+
helpers: new Set(),
31+
temps: 0,
32+
}
33+
const ir = transform(ast, {
34+
prefixIdentifiers: true,
35+
...options,
36+
...overrideOptions,
37+
}) as any
38+
const { code, helpers, vaporHelpers } = generate(ir, {
39+
prefixIdentifiers: true,
40+
...options,
41+
...overrideOptions,
42+
})
43+
return { ast, ir, code, helpers, vaporHelpers }
44+
}
45+
}
+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { describe, expect, test } from 'vitest'
2+
3+
import {
4+
compile,
5+
transformChildren,
6+
transformElement,
7+
transformText,
8+
// transformVIf,
9+
} from '../../src/core/compiler'
10+
import { makeCompile } from './_utils'
11+
12+
const compileWithElementTransform = makeCompile({
13+
nodeTransforms: [
14+
transformText,
15+
// transformVIf,
16+
transformElement,
17+
transformChildren,
18+
],
19+
})
20+
21+
describe('compiler: children transform', () => {
22+
test.todo('basic')
23+
24+
test('children & sibling references', () => {
25+
const { code, vaporHelpers } = compileWithElementTransform(
26+
`<div>
27+
<p>{ first }</p>
28+
{ second }
29+
<p>{ forth }</p>
30+
</div>`,
31+
)
32+
expect(code).toMatchInlineSnapshot(`
33+
"import { next as _next, createTextNode as _createTextNode, insert as _insert, renderEffect as _renderEffect, setText as _setText, template as _template } from 'vue/vapor';
34+
const t0 = _template("<div><p></p> <!><p></p></div>")
35+
36+
export function render(_ctx) {
37+
const n4 = t0()
38+
const n0 = n4.firstChild
39+
const n3 = _next(n0, 2)
40+
const n2 = n3.nextSibling
41+
const n1 = _createTextNode(() => [_ctx.second, " "])
42+
_insert(n1, n4, n3)
43+
_renderEffect(() => _setText(n0, _ctx.first))
44+
_renderEffect(() => _setText(n2, _ctx.forth))
45+
return n4
46+
}"
47+
`)
48+
expect(Array.from(vaporHelpers)).containSubset([
49+
'next',
50+
'setText',
51+
'createTextNode',
52+
'insert',
53+
'template',
54+
])
55+
})
56+
})

0 commit comments

Comments
 (0)