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 }),