feat: binding

This commit is contained in:
三咲智子 Kevin Deng 2023-11-23 23:42:08 +08:00
parent 717aad275d
commit 1d2f66e111
No known key found for this signature in database
GPG Key ID: 69992F2250DFD93E
13 changed files with 496 additions and 114 deletions

View File

@ -0,0 +1,5 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "all"
}

View File

@ -1,27 +1,79 @@
import {
CodegenContext,
CodegenOptions,
CodegenResult
CodegenResult,
} from '@vue/compiler-dom'
import { RootIRNode } from './transform'
import { DynamicChildren, IRNodeTypes, RootIRNode } from './transform'
// IR -> JS codegen
export function generate(
ast: RootIRNode,
ir: RootIRNode,
options: CodegenOptions & {
onContextCreated?: (context: CodegenContext) => void
} = {}
} = {},
): CodegenResult {
let code = ''
let preamble = "import { template } from 'vue/vapor'\n"
let preamble = `import { watchEffect } from 'vue'
import { template, setAttr, setText, children, on, insert } from 'vue/vapor'\n`
const isSetupInlined = !!options.inline
preamble += ast.template
preamble += ir.template
.map((template, i) => `const t${i} = template(\`${template.template}\`)\n`)
.join('')
code += 'const root = t0()\n'
code += `const root = t0()\n`
if (ir.children[0]) {
code += `const {${genChildrens(
ir.children[0].children,
)}} = children(root)\n`
}
for (const opration of ir.opration) {
switch (opration.type) {
case IRNodeTypes.TEXT_NODE: {
code += `const n${opration.id} = document.createTextNode(${opration.content})\n`
break
}
case IRNodeTypes.INSERT_NODE:
{
let anchor = ''
if (typeof opration.anchor === 'number') {
anchor = `, n${opration.anchor}`
} else if (opration.anchor === 'first') {
anchor = `, 0 /* InsertPosition.FIRST */`
}
code += `insert(n${opration.element}, n${opration.parent}${anchor})\n`
}
break
}
}
for (const [expr, effects] of Object.entries(ir.effect)) {
let scope = `watchEffect(() => {\n`
for (const effect of effects) {
switch (effect.type) {
case IRNodeTypes.SET_PROP:
scope += `setAttr(n${effect.element}, ${JSON.stringify(
effect.name,
)}, undefined, ${expr})\n`
break
case IRNodeTypes.SET_TEXT:
scope += `setText(n${effect.element}, undefined, ${expr})\n`
break
case IRNodeTypes.SET_EVENT:
scope += `on(n${effect.element}, ${JSON.stringify(
effect.name,
)}, ${expr})\n`
break
}
}
scope += '})\n'
code += scope
}
code += 'return root'
const functionName = options.ssr ? `ssrRender` : `render`
@ -33,7 +85,22 @@ export function generate(
return {
code,
ast: ast as any,
preamble
ast: ir as any,
preamble,
}
}
function genChildrens(children: DynamicChildren) {
let str = ''
for (const [index, child] of Object.entries(children)) {
str += ` ${index}: [`
if (child.store) {
str += `n${child.id}`
}
if (Object.keys(child.children).length) {
str += `, {${genChildrens(child.children)}}`
}
str += '],'
}
return str
}

View File

@ -1,17 +1,25 @@
import {
type NodeTypes,
RootNode,
Node,
TemplateChildNode,
ElementNode,
AttributeNode,
SourceLocation,
NodeTypes,
InterpolationNode,
TransformOptions
TransformOptions,
DirectiveNode,
} from '@vue/compiler-dom'
export const enum IRNodeTypes {
ROOT,
TEMPLATE_GENERATOR
TEMPLATE_GENERATOR,
SET_PROP,
SET_TEXT,
SET_EVENT,
INSERT_NODE,
TEXT_NODE,
}
export interface IRNode {
@ -22,6 +30,9 @@ export interface IRNode {
export interface RootIRNode extends IRNode {
type: IRNodeTypes.ROOT
template: Array<TemplateGeneratorIRNode>
children: DynamicChildren
effect: Record<string, EffectNode[]>
opration: OprationNode[]
helpers: Set<string>
}
@ -30,89 +41,349 @@ export interface TemplateGeneratorIRNode extends IRNode {
template: string
}
export interface SetPropIRNode extends IRNode {
type: IRNodeTypes.SET_PROP
element: number
name: string
}
export interface SetTextIRNode extends IRNode {
type: IRNodeTypes.SET_TEXT
element: number
}
export interface SetEventIRNode extends IRNode {
type: IRNodeTypes.SET_EVENT
element: number
name: string
}
export interface TextNodeIRNode extends IRNode {
type: IRNodeTypes.TEXT_NODE
id: number
content: string
}
export interface InsertNodeIRNode extends IRNode {
type: IRNodeTypes.INSERT_NODE
element: number
parent: number
anchor: number | 'first' | 'last'
}
export type EffectNode = SetPropIRNode | SetTextIRNode | SetEventIRNode
export type OprationNode = TextNodeIRNode | InsertNodeIRNode
export interface DynamicChild {
id: number | null
store: boolean
children: DynamicChildren
}
export type DynamicChildren = Record<number, DynamicChild>
export interface TransformContext<T extends Node = Node> {
node: T
parent: TransformContext | null
root: TransformContext<RootNode>
index: number
options: TransformOptions
ir: RootIRNode
template: string
children: DynamicChildren
store: boolean
ghost: boolean
getElementId(): number
registerEffect(expr: string, effectNode: EffectNode): void
registerTemplate(): number
}
function createRootContext(
ir: RootIRNode,
node: RootNode,
options: TransformOptions,
): TransformContext<RootNode> {
let i = 0
const { effect: bindings } = ir
const ctx: TransformContext<RootNode> = {
node,
parent: null,
index: 0,
root: undefined as any, // set later
options,
ir,
children: {},
store: false,
ghost: false,
getElementId: () => i++,
registerEffect(expr, effectNode) {
if (!bindings[expr]) bindings[expr] = []
bindings[expr].push(effectNode)
},
template: '',
registerTemplate() {
if (!ctx.template) return -1
const idx = ir.template.findIndex((t) => t.template === ctx.template)
if (idx !== -1) return idx
ir.template.push({
type: IRNodeTypes.TEMPLATE_GENERATOR,
template: ctx.template,
loc: node.loc,
})
return ir.template.length - 1
},
}
ctx.root = ctx
return ctx
}
function createContext<T extends TemplateChildNode>(
node: T,
parent: TransformContext,
index: number,
): TransformContext<T> {
let id: number | undefined
const getElementId = () => {
if (id !== undefined) return id
return (id = parent.root.getElementId())
}
const children = {}
const ctx: TransformContext<T> = {
...parent,
node,
parent,
index,
get template() {
return parent.template
},
set template(t) {
parent.template = t
},
getElementId,
children,
store: false,
}
return ctx
}
// AST -> IR
export function transform(
root: RootNode,
options: TransformOptions = {}
options: TransformOptions = {},
): RootIRNode {
const template = transformChildren(root.children)
// {
// type: IRNodeTypes.TEMPLATE_GENERATOR,
// template,
// loc: root.loc
// }
return {
const ir: RootIRNode = {
type: IRNodeTypes.ROOT,
loc: root.loc,
template: [
{
type: IRNodeTypes.TEMPLATE_GENERATOR,
template,
loc: root.loc
}
],
helpers: new Set(['template'])
template: [],
children: {},
effect: Object.create(null),
opration: [],
helpers: new Set(['template']),
}
const ctx = createRootContext(ir, root, options)
transformChildren(ctx, true)
ctx.registerTemplate()
ir.children = ctx.children
console.log(JSON.stringify(ir, undefined, 2))
return ir
}
function transformChildren(children: TemplateChildNode[]) {
let template: string = ''
children.forEach((child, i) => walkNode(child))
return template
function transformChildren(
ctx: TransformContext<RootNode | ElementNode>,
root?: boolean,
) {
const {
node: { children },
} = ctx
let index = 0
children.forEach((child, i) => walkNode(child, i))
function walkNode(node: TemplateChildNode, i: number) {
const child = createContext(node, ctx, index)
const isFirst = i === 0
const isLast = i === children.length - 1
function walkNode(node: TemplateChildNode) {
switch (node.type) {
case 1 satisfies NodeTypes.ELEMENT: {
template += transformElement(node)
transformElement(child as TransformContext<ElementNode>)
break
}
case 2 satisfies NodeTypes.TEXT:
template += node.content
case 2 satisfies NodeTypes.TEXT: {
ctx.template += node.content
break
case 3 satisfies NodeTypes.COMMENT:
template += `<!--${node.content}-->`
}
case 3 satisfies NodeTypes.COMMENT: {
ctx.template += `<!--${node.content}-->`
break
case 5 satisfies NodeTypes.INTERPOLATION:
template += transformInterpolation(node)
}
case 5 satisfies NodeTypes.INTERPOLATION: {
transformInterpolation(
child as TransformContext<InterpolationNode>,
isFirst,
isLast,
)
break
// case 12 satisfies NodeTypes.TEXT_CALL:
// template += node.content
default:
template += `[${node.type}]`
}
default: {
ctx.template += `[type: ${node.type}]`
}
}
if (Object.keys(child.children).length > 0 || child.store)
ctx.children[index] = {
id: child.store ? child.getElementId() : null,
store: child.store,
children: child.children,
}
if (!child.ghost) index++
}
}
function transformInterpolation(node: InterpolationNode) {
// TODO
if (node.content.type === (4 satisfies NodeTypes.SIMPLE_EXPRESSION)) {
return `{{ ${node.content.content} }}`
}
return '[EXP]'
// return `{{${node.content.content}}}`
}
function transformElement(node: ElementNode) {
function transformElement(ctx: TransformContext<ElementNode>) {
const { node } = ctx
const { tag, props, children } = node
let template = `<${tag}`
const propsTemplate = props
.filter(
(prop): prop is AttributeNode =>
prop.type === (6 satisfies NodeTypes.ATTRIBUTE)
)
.map(prop => transformProp(prop))
.join(' ')
if (propsTemplate) template += ' ' + propsTemplate
template += `>`
ctx.template += `<${tag}`
props.forEach((prop) => transformProp(prop, ctx))
ctx.template += node.isSelfClosing ? '/>' : `>`
if (children.length > 0) {
template += transformChildren(children)
transformChildren(ctx)
}
if (!node.isSelfClosing) ctx.template += `</${tag}>`
}
function transformInterpolation(
ctx: TransformContext<InterpolationNode>,
isFirst: boolean,
isLast: boolean,
) {
const { node } = ctx
if (node.content.type === (4 satisfies NodeTypes.SIMPLE_EXPRESSION)) {
const expr = processExpression(ctx, node.content.content)
const parent = ctx.parent!
const parentId = parent.getElementId()
parent.store = true
if (isFirst && isLast) {
ctx.registerEffect(expr, {
type: IRNodeTypes.SET_TEXT,
loc: node.loc,
element: parentId,
})
} else {
let id: number
let anchor: number | 'first' | 'last'
if (!isFirst && !isLast) {
id = ctx.root.getElementId()
anchor = ctx.getElementId()
ctx.template += '<!>'
ctx.store = true
} else {
id = ctx.getElementId()
ctx.ghost = true
anchor = isFirst ? 'first' : 'last'
}
ctx.ir.opration.push(
{
type: IRNodeTypes.TEXT_NODE,
loc: node.loc,
id,
content: expr,
},
{
type: IRNodeTypes.INSERT_NODE,
loc: node.loc,
element: id,
parent: parentId,
anchor,
},
)
ctx.registerEffect(expr, {
type: IRNodeTypes.SET_TEXT,
loc: node.loc,
element: id,
})
}
} else {
// TODO
}
// TODO
}
function transformProp(
node: DirectiveNode | AttributeNode,
ctx: TransformContext<ElementNode>,
): void {
const { name } = node
if (node.type === (6 satisfies NodeTypes.ATTRIBUTE)) {
if (node.value) {
ctx.template += ` ${name}="${node.value.content}"`
} else {
ctx.template += ` ${name}`
}
return
}
template += `</${tag}>`
if (!node.exp) {
// TODO
return
} else if (node.exp.type === (8 satisfies NodeTypes.COMPOUND_EXPRESSION)) {
// TODO
return
} else if (
!node.arg ||
node.arg.type === (8 satisfies NodeTypes.COMPOUND_EXPRESSION)
) {
// TODO
return
}
return template
const expr = processExpression(ctx, node.exp.content)
ctx.store = true
if (name === 'bind') {
ctx.registerEffect(expr, {
type: IRNodeTypes.SET_PROP,
loc: node.loc,
element: ctx.getElementId(),
name: node.arg.content,
})
} else if (name === 'on') {
ctx.registerEffect(expr, {
type: IRNodeTypes.SET_EVENT,
loc: node.loc,
element: ctx.getElementId(),
name: node.arg.content,
})
}
}
function transformProp(prop: AttributeNode) {
const { name, value } = prop
if (value) return `${name}="${value.content}"`
return name
function processExpression(ctx: TransformContext, expr: string) {
if (ctx.options.bindingMetadata?.[expr] === 'setup-ref') {
expr += '.value'
}
return expr
}

View File

@ -1,2 +1,8 @@
export { template } from './template'
export { render } from './render'
export * from './render'
export * from './on'
type Children = Record<number, [ChildNode, Children]>
export function children(n: ChildNode): Children {
return { ...Array.from(n.childNodes).map(n => [n, children(n)]) }
}

View File

@ -0,0 +1,8 @@
export const on = (
el: any,
event: string,
handler: () => any,
options?: EventListenerOptions
) => {
el.addEventListener(event, handler, options)
}

View File

@ -29,11 +29,22 @@ export function normalizeContainer(container: string | ParentNode): ParentNode {
: container
}
export const enum InsertPosition {
FIRST,
LAST
}
export function insert(
block: Block,
parent: ParentNode,
anchor: Node | null = null
anchor: Node | InsertPosition | null = null
) {
anchor =
typeof anchor === 'number'
? anchor === InsertPosition.FIRST
? parent.firstChild
: null
: anchor
// if (!isHydrating) {
if (block instanceof Node) {
parent.insertBefore(block, anchor)

View File

@ -11,6 +11,7 @@
"enableNonBrowserBranches": true
},
"dependencies": {
"@vue/compiler-vapor": "workspace:^",
"monaco-editor": "^0.44.0",
"source-map-js": "^1.0.2"
}

View File

@ -1,5 +1,6 @@
import * as m from 'monaco-editor'
import { compile, CompilerError, CompilerOptions } from '@vue/compiler-dom'
import { CompilerError, CompilerOptions } from '@vue/compiler-dom'
import { compile } from '@vue/compiler-vapor'
import { compile as ssrCompile } from '@vue/compiler-ssr'
import {
defaultOptions,
@ -92,8 +93,8 @@ window.init = () => {
console.log(`AST: `, ast)
console.log(`Options: `, toRaw(compilerOptions))
lastSuccessfulCode = code + `\n\n// Check the console for the AST`
lastSuccessfulMap = new SourceMapConsumer(map!)
lastSuccessfulMap!.computeColumnSpans()
// lastSuccessfulMap = new SourceMapConsumer(map!)
// lastSuccessfulMap!.computeColumnSpans()
} catch (e: any) {
lastSuccessfulCode = `/* ERROR: ${e.message} (see console for more info) */`
console.error(e)

View File

@ -10,8 +10,8 @@
"vue": "workspace:*"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.4.0",
"vite": "^4.5.0",
"@vitejs/plugin-vue": "link:/Users/kevin/Developer/open-source/vite-plugin-vue/packages/plugin-vue",
"vite": "^5.0.2",
"vite-plugin-inspect": "^0.7.42"
}
}

View File

@ -1,14 +1,31 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ref, computed } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2)
const inc = () => count.value++
const dec = () => count.value--
// @ts-expect-error
globalThis.count = count
// @ts-expect-error
globalThis.double = double
// @ts-expect-error
globalThis.inc = inc
// @ts-expect-error
globalThis.dec = dec
</script>
<template>
<div>
<h1 class="red">Hello world</h1>
<!-- {{ count }} -->
<button style="font-weight: bold">Inc</button>
<h1 class="red">Counter</h1>
<div>The number is {{ count }}.</div>
<div>{{ count }} * 2 = {{ double }}</div>
<div style="display: flex; gap: 8px">
<button @click="inc">inc</button>
<button @click="dec">dec</button>
</div>
</div>
</template>
@ -16,4 +33,10 @@ const count = ref(0)
.red {
color: red;
}
html {
color-scheme: dark;
background-color: #000;
padding: 10px;
}
</style>

View File

@ -1,5 +1,8 @@
import { render } from 'vue/vapor'
import App from './App.vue'
// @ts-expect-error
render(App.render, '#app')
render(() => {
// @ts-expect-error
const returned = App.setup({}, { expose() {} })
return App.render(returned)
}, '#app')

View File

@ -7,8 +7,10 @@ export default defineConfig({
build: {
target: 'esnext'
},
clearScreen: false,
plugins: [
Vue({
isProduction: true,
template: {
compiler: CompilerVapor
}

View File

@ -395,6 +395,9 @@ importers:
packages/template-explorer:
dependencies:
'@vue/compiler-vapor':
specifier: workspace:^
version: link:../compiler-vapor
monaco-editor:
specifier: ^0.44.0
version: 0.44.0
@ -451,14 +454,14 @@ importers:
version: link:../packages/vue
devDependencies:
'@vitejs/plugin-vue':
specifier: ^4.4.0
version: 4.4.0(vite@4.5.0)(vue@packages+vue)
specifier: link:/Users/kevin/Developer/open-source/vite-plugin-vue/packages/plugin-vue
version: link:../../../vite-plugin-vue/packages/plugin-vue
vite:
specifier: ^4.5.0
version: 4.5.0(@types/node@20.9.0)(terser@5.22.0)
specifier: ^5.0.2
version: 5.0.2(@types/node@20.9.0)(terser@5.22.0)
vite-plugin-inspect:
specifier: ^0.7.42
version: 0.7.42(rollup@4.1.4)(vite@4.5.0)
version: 0.7.42(rollup@4.1.4)(vite@5.0.2)
packages:
@ -1728,17 +1731,6 @@ packages:
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
dev: true
/@vitejs/plugin-vue@4.4.0(vite@4.5.0)(vue@packages+vue):
resolution: {integrity: sha512-xdguqb+VUwiRpSg+nsc2HtbAUSGak25DXYvpQQi4RVU1Xq1uworyoH/md9Rfd8zMmPR/pSghr309QNcftUVseg==}
engines: {node: ^14.18.0 || >=16.0.0}
peerDependencies:
vite: ^4.0.0
vue: ^3.2.25
dependencies:
vite: 4.5.0(@types/node@20.9.0)(terser@5.22.0)
vue: link:packages/vue
dev: true
/@vitejs/plugin-vue@4.4.0(vite@5.0.0)(vue@packages+vue):
resolution: {integrity: sha512-xdguqb+VUwiRpSg+nsc2HtbAUSGak25DXYvpQQi4RVU1Xq1uworyoH/md9Rfd8zMmPR/pSghr309QNcftUVseg==}
engines: {node: ^14.18.0 || >=16.0.0}
@ -5337,14 +5329,6 @@ packages:
rollup: 4.1.4
dev: true
/rollup@3.29.4:
resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==}
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
hasBin: true
optionalDependencies:
fsevents: 2.3.3
dev: true
/rollup@4.1.4:
resolution: {integrity: sha512-U8Yk1lQRKqCkDBip/pMYT+IKaN7b7UesK3fLSTuHBoBJacCE+oBqo/dfG/gkUdQNNB2OBmRP98cn2C2bkYZkyw==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@ -6187,7 +6171,7 @@ packages:
mlly: 1.4.2
pathe: 1.1.1
picocolors: 1.0.0
vite: 5.0.0(@types/node@20.9.0)(terser@5.22.0)
vite: 5.0.2(@types/node@20.9.0)(terser@5.22.0)
transitivePeerDependencies:
- '@types/node'
- less
@ -6199,7 +6183,7 @@ packages:
- terser
dev: true
/vite-plugin-inspect@0.7.42(rollup@4.1.4)(vite@4.5.0):
/vite-plugin-inspect@0.7.42(rollup@4.1.4)(vite@5.0.2):
resolution: {integrity: sha512-JCyX86wr3siQc+p9Kd0t8VkFHAJag0RaQVIpdFGSv5FEaePEVB6+V/RGtz2dQkkGSXQzRWrPs4cU3dRKg32bXw==}
engines: {node: '>=14'}
peerDependencies:
@ -6217,18 +6201,18 @@ packages:
open: 9.1.0
picocolors: 1.0.0
sirv: 2.0.3
vite: 4.5.0(@types/node@20.9.0)(terser@5.22.0)
vite: 5.0.2(@types/node@20.9.0)(terser@5.22.0)
transitivePeerDependencies:
- rollup
- supports-color
dev: true
/vite@4.5.0(@types/node@20.9.0)(terser@5.22.0):
resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==}
engines: {node: ^14.18.0 || >=16.0.0}
/vite@5.0.0(@types/node@20.9.0)(terser@5.22.0):
resolution: {integrity: sha512-ESJVM59mdyGpsiNAeHQOR/0fqNoOyWPYesFto8FFZugfmhdHx8Fzd8sF3Q/xkVhZsyOxHfdM7ieiVAorI9RjFw==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
'@types/node': '>= 14'
'@types/node': ^18.0.0 || >=20.0.0
less: '*'
lightningcss: ^1.21.0
sass: '*'
@ -6252,16 +6236,16 @@ packages:
optional: true
dependencies:
'@types/node': 20.9.0
esbuild: 0.18.20
esbuild: 0.19.5
postcss: 8.4.31
rollup: 3.29.4
rollup: 4.4.1
terser: 5.22.0
optionalDependencies:
fsevents: 2.3.3
dev: true
/vite@5.0.0(@types/node@20.9.0)(terser@5.22.0):
resolution: {integrity: sha512-ESJVM59mdyGpsiNAeHQOR/0fqNoOyWPYesFto8FFZugfmhdHx8Fzd8sF3Q/xkVhZsyOxHfdM7ieiVAorI9RjFw==}
/vite@5.0.2(@types/node@20.9.0)(terser@5.22.0):
resolution: {integrity: sha512-6CCq1CAJCNM1ya2ZZA7+jS2KgnhbzvxakmlIjN24cF/PXhRMzpM/z8QgsVJA/Dm5fWUWnVEsmtBoMhmerPxT0g==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies: