diff --git a/README.md b/README.md
index 0c1ad0197..b7100cc01 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,9 @@ PR are welcome! However, please create an issue before you start to work on it,
- [x] simple bindings
- [x] simple events
- [ ] TODO-MVC App
+- [ ] transform
+ - [x] NodeTransform
+ - [ ] DirectiveTransform
- [ ] directives
- [x] `v-once`
- [x] `v-html`
diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts
index 2bc85bf53..07741aea1 100644
--- a/packages/compiler-core/src/ast.ts
+++ b/packages/compiler-core/src/ast.ts
@@ -85,6 +85,13 @@ export interface Position {
column: number
}
+export type AllNode =
+ | ParentNode
+ | ExpressionNode
+ | TemplateChildNode
+ | AttributeNode
+ | DirectiveNode
+
export type ParentNode = RootNode | ElementNode | IfBranchNode | ForNode
export type ExpressionNode = SimpleExpressionNode | CompoundExpressionNode
diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts
index 74ca59e69..09259cc7b 100644
--- a/packages/compiler-core/src/index.ts
+++ b/packages/compiler-core/src/index.ts
@@ -68,5 +68,6 @@ export { generateCodeFrame } from '@vue/shared'
export {
checkCompatEnabled,
warnDeprecation,
- CompilerDeprecationTypes
+ CompilerDeprecationTypes,
+ type CompilerCompatOptions
} from './compat/compatConfig'
diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts
index a159d2eed..a1e084eb9 100644
--- a/packages/compiler-core/src/utils.ts
+++ b/packages/compiler-core/src/utils.ts
@@ -224,6 +224,7 @@ export function assert(condition: boolean, msg?: string) {
}
}
+/** find directive */
export function findDir(
node: ElementNode,
name: string | RegExp,
diff --git a/packages/compiler-vapor/__tests__/__snapshots__/compile.test.ts.snap b/packages/compiler-vapor/__tests__/__snapshots__/compile.test.ts.snap
index 5090fc42e..1682d354d 100644
--- a/packages/compiler-vapor/__tests__/__snapshots__/compile.test.ts.snap
+++ b/packages/compiler-vapor/__tests__/__snapshots__/compile.test.ts.snap
@@ -104,16 +104,14 @@ export function render(_ctx) {
`;
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('
');
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;
}
"
diff --git a/packages/compiler-vapor/__tests__/compile.test.ts b/packages/compiler-vapor/__tests__/compile.test.ts
index b73270c38..21046f388 100644
--- a/packages/compiler-vapor/__tests__/compile.test.ts
+++ b/packages/compiler-vapor/__tests__/compile.test.ts
@@ -1,8 +1,12 @@
-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,
@@ -78,7 +82,7 @@ describe('compile', () => {
await compile(``, { 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,
@@ -107,7 +111,7 @@ describe('compile', () => {
const onError = vi.fn()
await compile(``, { 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,
@@ -170,9 +174,9 @@ describe('compile', () => {
test('basic', async () => {
const code = await compile(
`
- {{ msg }}
-
-
`,
+ {{ msg }}
+
+ `,
{
bindingMetadata: {
msg: BindingTypes.SETUP_REF,
@@ -183,7 +187,7 @@ describe('compile', () => {
expect(code).matchSnapshot()
})
- test.fails('as root node', async () => {
+ test('as root node', async () => {
const code = await compile(``)
expect(code).toMatchSnapshot()
expect(code).not.contains('effect')
diff --git a/packages/compiler-vapor/__tests__/fixtures.test.ts b/packages/compiler-vapor/__tests__/fixtures.test.ts
index 7ece9c981..223c8eb50 100644
--- a/packages/compiler-vapor/__tests__/fixtures.test.ts
+++ b/packages/compiler-vapor/__tests__/fixtures.test.ts
@@ -3,11 +3,11 @@ import { parse, compileScript } from '@vue/compiler-sfc'
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()
})
diff --git a/packages/compiler-vapor/src/compile.ts b/packages/compiler-vapor/src/compile.ts
index c8a23dd63..62b4f27de 100644
--- a/packages/compiler-vapor/src/compile.ts
+++ b/packages/compiler-vapor/src/compile.ts
@@ -1,19 +1,86 @@
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
+
+// 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,
+]
+
+export function getBaseTransformPreset(
+ prefixIdentifiers?: boolean,
+): TransformPreset {
+ return [[transformOnce], {}]
}
diff --git a/packages/compiler-vapor/src/errors.ts b/packages/compiler-vapor/src/errors.ts
index 4f89b5ead..b95eca3c7 100644
--- a/packages/compiler-vapor/src/errors.ts
+++ b/packages/compiler-vapor/src/errors.ts
@@ -1,7 +1,6 @@
-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
}
@@ -10,14 +9,21 @@ export function defaultOnWarn(msg: CompilerError) {
__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 = {
+export const errorMessages: Record = {
// 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.`,
}
diff --git a/packages/compiler-vapor/src/hack.ts b/packages/compiler-vapor/src/hack.ts
new file mode 100644
index 000000000..6b74dceb6
--- /dev/null
+++ b/packages/compiler-vapor/src/hack.ts
@@ -0,0 +1,9 @@
+import type { Prettify } from '@vue/shared'
+import type { NodeTransform } from './transform'
+
+type Overwrite = Pick> &
+ Pick>
+
+export type HackOptions = Prettify<
+ Overwrite
+>
diff --git a/packages/compiler-vapor/src/index.ts b/packages/compiler-vapor/src/index.ts
index d64408d70..1e7b4bd9f 100644
--- a/packages/compiler-vapor/src/index.ts
+++ b/packages/compiler-vapor/src/index.ts
@@ -1,5 +1,6 @@
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'
diff --git a/packages/compiler-vapor/src/transform.ts b/packages/compiler-vapor/src/transform.ts
index fed7b5a9d..dd88fecee 100644
--- a/packages/compiler-vapor/src/transform.ts
+++ b/packages/compiler-vapor/src/transform.ts
@@ -1,15 +1,17 @@
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,
@@ -17,25 +19,35 @@ import {
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 {
+export type NodeTransform = (
+ node: RootNode | TemplateChildNode,
+ context: TransformContext,
+) => void | (() => void) | (() => void)[]
+
+export type TransformOptions = HackOptions
+
+export interface TransformContext {
node: T
- parent: TransformContext | null
+ parent: TransformContext | null
root: TransformContext
index: number
- options: TransformOptions
+ options: Required<
+ Omit
+ >
template: string
dynamic: DynamicInfo
- once: boolean
+ inVOnce: boolean
reference(): number
increaseId(): number
@@ -45,10 +57,11 @@ export interface TransformContext {
helper(name: string): string
}
+// TODO use class for better perf
function createRootContext(
ir: RootIRNode,
node: RootNode,
- options: TransformOptions,
+ options: TransformOptions = {},
): TransformContext {
let globalId = 0
const { effect, operation: operation, helpers, vaporHelpers } = ir
@@ -58,9 +71,32 @@ function createRootContext(
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() {
@@ -69,7 +105,7 @@ function createRootContext(
return (this.dynamic.id = this.increaseId())
},
registerEffect(expr, operation) {
- if (this.once) {
+ if (this.inVOnce) {
return this.registerOperation(operation)
}
if (!effect[expr]) effect[expr] = []
@@ -110,7 +146,7 @@ function createRootContext(
function createContext(
node: T,
- parent: TransformContext,
+ parent: TransformContext,
index: number,
): TransformContext {
const ctx: TransformContext = {
@@ -159,7 +195,7 @@ export function transform(
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,
@@ -170,20 +206,108 @@ export function transform(
return ir
}
-function transformChildren(
- ctx: TransformContext,
- root?: boolean,
+function transformNode(
+ context: TransformContext,
) {
+ 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)
+ 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)
+ break
+ }
+ case NodeTypes.TEXT: {
+ context.template += node.content
+ break
+ }
+ case NodeTypes.COMMENT: {
+ context.template += ``
+ break
+ }
+ case NodeTypes.INTERPOLATION: {
+ transformInterpolation(
+ context as TransformContext,
+ 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) {
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[] = []
@@ -229,57 +353,6 @@ function transformChildren(
}
}
}
-
- 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)
- break
- }
- case NodeTypes.TEXT: {
- child.template += node.content
- break
- }
- case NodeTypes.COMMENT: {
- child.template += ``
- break
- }
- case NodeTypes.INTERPOLATION: {
- transformInterpolation(
- child as TransformContext,
- 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) {
@@ -365,7 +438,7 @@ function transformProp(
(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
}
@@ -394,7 +467,7 @@ function transformProp(
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
}
@@ -441,10 +514,6 @@ function transformProp(
})
break
}
- case 'once': {
- ctx.once = true
- break
- }
case 'cloak': {
// do nothing
break
diff --git a/packages/compiler-vapor/src/transforms/vOnce.ts b/packages/compiler-vapor/src/transforms/vOnce.ts
new file mode 100644
index 000000000..a22ee469a
--- /dev/null
+++ b/packages/compiler-vapor/src/transforms/vOnce.ts
@@ -0,0 +1,14 @@
+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
+ }
+}
diff --git a/playground/vite.config.ts b/playground/vite.config.ts
index fb76f6266..6f5a88d2a 100644
--- a/playground/vite.config.ts
+++ b/playground/vite.config.ts
@@ -13,7 +13,7 @@ export default defineConfig({
plugins: [
Vue({
template: {
- compiler: CompilerVapor
+ compiler: CompilerVapor as any
},
compiler: CompilerSFC
}),