- [x] simple bindings
- [x] simple events
- [ ] TODO-MVC App
+- [ ] transform
+ - [x] NodeTransform
+ - [ ] DirectiveTransform
- [ ] directives
- [x] `v-once`
- [x] `v-html`
column: number
}
+export type AllNode =
+ | ParentNode
+ | ExpressionNode
+ | TemplateChildNode
+ | AttributeNode
+ | DirectiveNode
+
export type ParentNode = RootNode | ElementNode | IfBranchNode | ForNode
export type ExpressionNode = SimpleExpressionNode | CompoundExpressionNode
export {
checkCompatEnabled,
warnDeprecation,
- CompilerDeprecationTypes
+ CompilerDeprecationTypes,
+ type CompilerCompatOptions
} from './compat/compatConfig'
}
}
+/** find directive */
export function findDir(
node: ElementNode,
name: string | RegExp,
`;
exports[`compile > directives > v-once > as root node 1`] = `
-"import { template, children, effect, setAttr } from 'vue/vapor';
+"import { template, children, setAttr } from 'vue/vapor';
const t0 = template('<div></div>');
export function render(_ctx) {
const n0 = t0();
const {
0: [n1],
} = children(n0);
- effect(() => {
- setAttr(n1, 'id', undefined, foo);
- });
+ setAttr(n1, 'id', undefined, foo);
return n0;
}
"
-import { BindingTypes, CompilerOptions, RootNode } from '@vue/compiler-dom'
+import { type RootNode, BindingTypes } from '@vue/compiler-dom'
+import {
+ type CompilerOptions,
+ VaporErrorCodes,
+ compile as _compile,
+} from '../src'
+
// TODO remove it
import { format } from 'prettier'
-import { compile as _compile } from '../src'
-import { ErrorCodes } from '../src/errors'
async function compile(
template: string | RootNode,
await compile(`<div v-bind:arg />`, { onError })
expect(onError.mock.calls[0][0]).toMatchObject({
- code: ErrorCodes.VAPOR_BIND_NO_EXPRESSION,
+ code: VaporErrorCodes.X_VAPOR_BIND_NO_EXPRESSION,
loc: {
start: {
line: 1,
const onError = vi.fn()
await compile(`<div v-on:click />`, { onError })
expect(onError.mock.calls[0][0]).toMatchObject({
- code: ErrorCodes.VAPOR_ON_NO_EXPRESSION,
+ code: VaporErrorCodes.X_VAPOR_ON_NO_EXPRESSION,
loc: {
start: {
line: 1,
test('basic', async () => {
const code = await compile(
`<div v-once>
- {{ msg }}
- <span :class="clz" />
- </div>`,
+ {{ msg }}
+ <span :class="clz" />
+ </div>`,
{
bindingMetadata: {
msg: BindingTypes.SETUP_REF,
expect(code).matchSnapshot()
})
- test.fails('as root node', async () => {
+ test('as root node', async () => {
const code = await compile(`<div :id="foo" v-once />`)
expect(code).toMatchSnapshot()
expect(code).not.contains('effect')
import source from './fixtures/counter.vue?raw'
test('fixtures', async () => {
- const { descriptor } = parse(source, { compiler: CompilerVapor })
+ const { descriptor } = parse(source, { compiler: CompilerVapor as any })
const script = compileScript(descriptor, {
id: 'counter.vue',
inlineTemplate: true,
- templateOptions: { compiler: CompilerVapor },
+ templateOptions: { compiler: CompilerVapor as any },
})
expect(script.content).matchSnapshot()
})
import {
type CodegenResult,
- type CompilerOptions,
+ type CompilerOptions as BaseCompilerOptions,
type RootNode,
+ type DirectiveTransform,
parse,
} from '@vue/compiler-dom'
-import { isString } from '@vue/shared'
-import { transform } from './transform'
+import { extend, isString } from '@vue/shared'
+import { NodeTransform, transform } from './transform'
import { generate } from './generate'
+import { defaultOnError, createCompilerError, VaporErrorCodes } from './errors'
+import { transformOnce } from './transforms/vOnce'
+import { HackOptions } from './hack'
+export type CompilerOptions = HackOptions<BaseCompilerOptions>
+
+// TODO: copied from @vue/compiler-core
// code/AST -> IR -> JS codegen
export function compile(
- template: string | RootNode,
+ source: string | RootNode,
options: CompilerOptions = {},
): CodegenResult {
- const ast = isString(template) ? parse(template, options) : template
- const ir = transform(ast, options)
- return generate(ir, options)
+ const onError = options.onError || defaultOnError
+ const isModuleMode = options.mode === 'module'
+ /* istanbul ignore if */
+ if (__BROWSER__) {
+ if (options.prefixIdentifiers === true) {
+ onError(createCompilerError(VaporErrorCodes.X_PREFIX_ID_NOT_SUPPORTED))
+ } else if (isModuleMode) {
+ onError(createCompilerError(VaporErrorCodes.X_MODULE_MODE_NOT_SUPPORTED))
+ }
+ }
+
+ const prefixIdentifiers =
+ !__BROWSER__ && (options.prefixIdentifiers === true || isModuleMode)
+
+ // TODO scope id
+ // if (options.scopeId && !isModuleMode) {
+ // onError(createCompilerError(ErrorCodes.X_SCOPE_ID_NOT_SUPPORTED))
+ // }
+
+ const ast = isString(source) ? parse(source, options) : source
+ const [nodeTransforms, directiveTransforms] =
+ getBaseTransformPreset(prefixIdentifiers)
+
+ if (!__BROWSER__ && options.isTS) {
+ const { expressionPlugins } = options
+ if (!expressionPlugins || !expressionPlugins.includes('typescript')) {
+ options.expressionPlugins = [...(expressionPlugins || []), 'typescript']
+ }
+ }
+
+ const ir = transform(
+ ast,
+ extend({}, options, {
+ prefixIdentifiers,
+ nodeTransforms: [
+ ...nodeTransforms,
+ ...(options.nodeTransforms || []), // user transforms
+ ],
+ directiveTransforms: extend(
+ {},
+ directiveTransforms,
+ options.directiveTransforms || {}, // user transforms
+ ),
+ }),
+ )
+
+ return generate(
+ ir,
+ extend({}, options, {
+ prefixIdentifiers,
+ }),
+ )
+}
+
+export type TransformPreset = [
+ NodeTransform[],
+ Record<string, DirectiveTransform>,
+]
+
+export function getBaseTransformPreset(
+ prefixIdentifiers?: boolean,
+): TransformPreset {
+ return [[transformOnce], {}]
}
-import { CompilerError } from '@vue/compiler-dom'
+import type { CompilerError } from '@vue/compiler-dom'
export { createCompilerError } from '@vue/compiler-dom'
-
export function defaultOnError(error: CompilerError) {
throw error
}
__DEV__ && console.warn(`[Vue warn] ${msg.message}`)
}
-export enum ErrorCodes {
+export enum VaporErrorCodes {
// transform errors
- VAPOR_BIND_NO_EXPRESSION,
- VAPOR_ON_NO_EXPRESSION,
+ X_VAPOR_BIND_NO_EXPRESSION,
+ X_VAPOR_ON_NO_EXPRESSION,
+
+ // generic errors
+ X_PREFIX_ID_NOT_SUPPORTED,
+ X_MODULE_MODE_NOT_SUPPORTED,
}
-export const errorMessages: Record<ErrorCodes, string> = {
+export const errorMessages: Record<VaporErrorCodes, string> = {
// transform errors
- [ErrorCodes.VAPOR_BIND_NO_EXPRESSION]: `v-bind is missing expression.`,
- [ErrorCodes.VAPOR_ON_NO_EXPRESSION]: `v-on is missing expression.`,
+ [VaporErrorCodes.X_VAPOR_BIND_NO_EXPRESSION]: `v-bind is missing expression.`,
+ [VaporErrorCodes.X_VAPOR_ON_NO_EXPRESSION]: `v-on is missing expression.`,
+
+ [VaporErrorCodes.X_PREFIX_ID_NOT_SUPPORTED]: `"prefixIdentifiers" option is not supported in this build of compiler.`,
+ [VaporErrorCodes.X_MODULE_MODE_NOT_SUPPORTED]: `ES module mode is not supported in this build of compiler.`,
}
--- /dev/null
+import type { Prettify } from '@vue/shared'
+import type { NodeTransform } from './transform'
+
+type Overwrite<T, U> = Pick<T, Exclude<keyof T, keyof U>> &
+ Pick<U, Extract<keyof U, keyof T>>
+
+export type HackOptions<T> = Prettify<
+ Overwrite<T, { nodeTransforms?: NodeTransform[] }>
+>
export { parse } from '@vue/compiler-dom'
export { transform } from './transform'
export { generate } from './generate'
-export { compile } from './compile'
+export { compile, type CompilerOptions } from './compile'
export * from './ir'
+export * from './errors'
import {
type RootNode,
- type Node,
type TemplateChildNode,
type ElementNode,
type AttributeNode,
type InterpolationNode,
- type TransformOptions,
+ type TransformOptions as BaseTransformOptions,
type DirectiveNode,
type ExpressionNode,
+ type ParentNode,
+ type AllNode,
NodeTypes,
BindingTypes,
+ CompilerCompatOptions,
} from '@vue/compiler-dom'
import {
type OperationNode,
IRNodeTypes,
DynamicInfo,
} from './ir'
-import { isVoidTag } from '@vue/shared'
+import { EMPTY_OBJ, NOOP, isArray, isVoidTag } from '@vue/shared'
import {
- ErrorCodes,
+ VaporErrorCodes,
createCompilerError,
defaultOnError,
defaultOnWarn,
} from './errors'
+import { HackOptions } from './hack'
-export interface TransformContext<T extends Node = Node> {
+export type NodeTransform = (
+ node: RootNode | TemplateChildNode,
+ context: TransformContext,
+) => void | (() => void) | (() => void)[]
+
+export type TransformOptions = HackOptions<BaseTransformOptions>
+
+export interface TransformContext<T extends AllNode = AllNode> {
node: T
- parent: TransformContext | null
+ parent: TransformContext<ParentNode> | null
root: TransformContext<RootNode>
index: number
- options: TransformOptions
+ options: Required<
+ Omit<TransformOptions, 'filename' | keyof CompilerCompatOptions>
+ >
template: string
dynamic: DynamicInfo
- once: boolean
+ inVOnce: boolean
reference(): number
increaseId(): number
helper(name: string): string
}
+// TODO use class for better perf
function createRootContext(
ir: RootIRNode,
node: RootNode,
- options: TransformOptions,
+ options: TransformOptions = {},
): TransformContext<RootNode> {
let globalId = 0
const { effect, operation: operation, helpers, vaporHelpers } = ir
parent: null,
index: 0,
root: null!, // set later
- options,
+ options: {
+ filename: '',
+ prefixIdentifiers: false,
+ hoistStatic: false,
+ hmr: false,
+ cacheHandlers: false,
+ nodeTransforms: [],
+ directiveTransforms: {},
+ transformHoist: null,
+ isBuiltInComponent: NOOP,
+ isCustomElement: NOOP,
+ expressionPlugins: [],
+ scopeId: null,
+ slotted: true,
+ ssr: false,
+ inSSR: false,
+ ssrCssVars: ``,
+ bindingMetadata: EMPTY_OBJ,
+ inline: false,
+ isTS: false,
+ onError: defaultOnError,
+ onWarn: defaultOnWarn,
+ ...options,
+ },
dynamic: ir.dynamic,
- once: false,
+ inVOnce: false,
increaseId: () => globalId++,
reference() {
return (this.dynamic.id = this.increaseId())
},
registerEffect(expr, operation) {
- if (this.once) {
+ if (this.inVOnce) {
return this.registerOperation(operation)
}
if (!effect[expr]) effect[expr] = []
function createContext<T extends TemplateChildNode>(
node: T,
- parent: TransformContext,
+ parent: TransformContext<ParentNode>,
index: number,
): TransformContext<T> {
const ctx: TransformContext<T> = {
const ctx = createRootContext(ir, root, options)
// TODO: transform presets, see packages/compiler-core/src/transforms
- transformChildren(ctx, true)
+ transformNode(ctx)
if (ir.template.length === 0) {
ir.template.push({
type: IRNodeTypes.FRAGMENT_FACTORY,
return ir
}
-function transformChildren(
- ctx: TransformContext<RootNode | ElementNode>,
- root?: boolean,
+function transformNode(
+ context: TransformContext<RootNode | TemplateChildNode>,
) {
+ let { node, index } = context
+
+ // apply transform plugins
+ const { nodeTransforms } = context.options
+ const exitFns = []
+ for (const nodeTransform of nodeTransforms) {
+ // TODO nodeTransform type
+ const onExit = nodeTransform(node, context as any)
+ if (onExit) {
+ if (isArray(onExit)) {
+ exitFns.push(...onExit)
+ } else {
+ exitFns.push(onExit)
+ }
+ }
+ if (!context.node) {
+ // node was removed
+ return
+ } else {
+ // node may have been replaced
+ node = context.node
+ }
+ }
+
+ if (node.type === NodeTypes.ROOT) {
+ transformChildren(context as TransformContext<RootNode>)
+ return
+ }
+
+ const parentChildren = context.parent!.node.children
+ const isFirst = index === 0
+ const isLast = index === parentChildren.length - 1
+
+ switch (node.type) {
+ case NodeTypes.ELEMENT: {
+ transformElement(context as TransformContext<ElementNode>)
+ break
+ }
+ case NodeTypes.TEXT: {
+ context.template += node.content
+ break
+ }
+ case NodeTypes.COMMENT: {
+ context.template += `<!--${node.content}-->`
+ break
+ }
+ case NodeTypes.INTERPOLATION: {
+ transformInterpolation(
+ context as TransformContext<InterpolationNode>,
+ isFirst,
+ isLast,
+ )
+ break
+ }
+ case NodeTypes.TEXT_CALL:
+ // never
+ break
+ default: {
+ // TODO handle other types
+ // CompoundExpressionNode
+ // IfNode
+ // IfBranchNode
+ // ForNode
+ context.template += `[type: ${node.type}]`
+ }
+ }
+
+ // exit transforms
+ context.node = node
+ let i = exitFns.length
+ while (i--) {
+ exitFns[i]()
+ }
+}
+
+function transformChildren(ctx: TransformContext<RootNode | ElementNode>) {
const {
node: { children },
} = ctx
const childrenTemplate: string[] = []
- children.forEach((child, i) => walkNode(child, i))
+ children.forEach((child, index) => {
+ const childContext = createContext(child, ctx, index)
+ transformNode(childContext)
+
+ childrenTemplate.push(childContext.template)
+ if (
+ childContext.dynamic.ghost ||
+ childContext.dynamic.referenced ||
+ childContext.dynamic.placeholder ||
+ Object.keys(childContext.dynamic.children).length
+ ) {
+ ctx.dynamic.children[index] = childContext.dynamic
+ }
+ })
processDynamicChildren()
ctx.template += childrenTemplate.join('')
- if (root) ctx.registerTemplate()
+ if (ctx.node.type === NodeTypes.ROOT) ctx.registerTemplate()
function processDynamicChildren() {
let prevChildren: DynamicInfo[] = []
}
}
}
-
- function walkNode(node: TemplateChildNode, index: number) {
- const child = createContext(node, ctx, index)
- const isFirst = index === 0
- const isLast = index === children.length - 1
-
- switch (node.type) {
- case NodeTypes.ELEMENT: {
- transformElement(child as TransformContext<ElementNode>)
- break
- }
- case NodeTypes.TEXT: {
- child.template += node.content
- break
- }
- case NodeTypes.COMMENT: {
- child.template += `<!--${node.content}-->`
- break
- }
- case NodeTypes.INTERPOLATION: {
- transformInterpolation(
- child as TransformContext<InterpolationNode>,
- isFirst,
- isLast,
- )
- break
- }
- case NodeTypes.TEXT_CALL:
- // never?
- break
- default: {
- // TODO handle other types
- // CompoundExpressionNode
- // IfNode
- // IfBranchNode
- // ForNode
- child.template += `[type: ${node.type}]`
- }
- }
-
- childrenTemplate.push(child.template)
-
- if (
- child.dynamic.ghost ||
- child.dynamic.referenced ||
- child.dynamic.placeholder ||
- Object.keys(child.dynamic.children).length
- ) {
- ctx.dynamic.children[index] = child.dynamic
- }
- }
}
function transformElement(ctx: TransformContext<ElementNode>) {
(exp.type === NodeTypes.SIMPLE_EXPRESSION && !exp.content.trim())
) {
ctx.options.onError!(
- createCompilerError(ErrorCodes.VAPOR_BIND_NO_EXPRESSION, loc),
+ createCompilerError(VaporErrorCodes.X_VAPOR_BIND_NO_EXPRESSION, loc),
)
return
}
case 'on': {
if (!exp && !modifiers.length) {
ctx.options.onError!(
- createCompilerError(ErrorCodes.VAPOR_ON_NO_EXPRESSION, loc),
+ createCompilerError(VaporErrorCodes.X_VAPOR_ON_NO_EXPRESSION, loc),
)
return
}
})
break
}
- case 'once': {
- ctx.once = true
- break
- }
case 'cloak': {
// do nothing
break
--- /dev/null
+import { NodeTypes, findDir } from '@vue/compiler-dom'
+import { NodeTransform } from '../transform'
+
+const seen = new WeakSet()
+
+export const transformOnce: NodeTransform = (node, context) => {
+ if (node.type === NodeTypes.ELEMENT && findDir(node, 'once', true)) {
+ if (seen.has(node) || context.inVOnce /* || context.inSSR */) {
+ return
+ }
+ seen.add(node)
+ context.inVOnce = true
+ }
+}
plugins: [
Vue({
template: {
- compiler: CompilerVapor
+ compiler: CompilerVapor as any
},
compiler: CompilerSFC
}),