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`
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
}
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 {
export interface RootIRNode extends IRNode {
type: IRNodeTypes.ROOT
template: Array<TemplateGeneratorIRNode>
+ children: DynamicChildren
+ effect: Record<string, EffectNode[]>
+ opration: OprationNode[]
helpers: Set<string>
}
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
+function transformElement(ctx: TransformContext<ElementNode>) {
+ const { node } = ctx
+ const { tag, props, children } = node
+
+ ctx.template += `<${tag}`
+
+ props.forEach((prop) => transformProp(prop, ctx))
+ ctx.template += node.isSelfClosing ? '/>' : `>`
+
+ if (children.length > 0) {
+ 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)) {
- return `{{ ${node.content.content} }}`
+ 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
}
- return '[EXP]'
- // return `{{${node.content.content}}}`
+ // TODO
}
-function transformElement(node: ElementNode) {
- 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 += `>`
+function transformProp(
+ node: DirectiveNode | AttributeNode,
+ ctx: TransformContext<ElementNode>,
+): void {
+ const { name } = node
- if (children.length > 0) {
- template += transformChildren(children)
+ 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
}
packages/template-explorer:
dependencies:
+ '@vue/compiler-vapor':
+ specifier: workspace:^
+ version: link:../compiler-vapor
monaco-editor:
specifier: ^0.44.0
version: 0.44.0
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:
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}
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'}
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
- 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:
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: '*'
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: