diff --git a/packages/compiler-core/__tests__/__snapshots__/scopeId.spec.ts.snap b/packages/compiler-core/__tests__/__snapshots__/scopeId.spec.ts.snap new file mode 100644 index 00000000..ea08f272 --- /dev/null +++ b/packages/compiler-core/__tests__/__snapshots__/scopeId.spec.ts.snap @@ -0,0 +1,95 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`scopeId compiler support should push scopeId for hoisted nodes 1`] = ` +"import { createVNode, createBlock, openBlock, withScopeId, pushScopeId, popScopeId } from \\"vue\\" +const withId = withScopeId(\\"test\\") + +pushScopeId(\\"test\\") +const _hoisted_1 = createVNode(\\"div\\", null, \\"hello\\") +const _hoisted_2 = createVNode(\\"div\\", null, \\"world\\") +popScopeId() + +export default withId(function render() { + const _ctx = this + return (openBlock(), createBlock(\\"div\\", null, [ + _hoisted_1, + _hoisted_2 + ])) +})" +`; + +exports[`scopeId compiler support should wrap default slot 1`] = ` +"import { createVNode, resolveComponent, createBlock, openBlock, withScopeId } from \\"vue\\" +const withId = withScopeId(\\"test\\") + +export default withId(function render() { + const _ctx = this + const _component_Child = resolveComponent(\\"Child\\") + + return (openBlock(), createBlock(_component_Child, null, { + default: withId(() => [ + createVNode(\\"div\\") + ]), + _compiled: true + })) +})" +`; + +exports[`scopeId compiler support should wrap dynamic slots 1`] = ` +"import { createVNode, resolveComponent, renderList, createSlots, createBlock, openBlock, withScopeId } from \\"vue\\" +const withId = withScopeId(\\"test\\") + +export default withId(function render() { + const _ctx = this + const _component_Child = resolveComponent(\\"Child\\") + + return (openBlock(), createBlock(_component_Child, null, createSlots({ _compiled: true }, [ + (_ctx.ok) + ? { + name: \\"foo\\", + fn: withId(() => [ + createVNode(\\"div\\") + ]) + } + : undefined, + renderList(_ctx.list, (i) => { + return { + name: i, + fn: withId(() => [ + createVNode(\\"div\\") + ]) + } + }) + ]), 512 /* DYNAMIC_SLOTS */)) +})" +`; + +exports[`scopeId compiler support should wrap named slots 1`] = ` +"import { toString, createTextVNode, createVNode, resolveComponent, createBlock, openBlock, withScopeId } from \\"vue\\" +const withId = withScopeId(\\"test\\") + +export default withId(function render() { + const _ctx = this + const _component_Child = resolveComponent(\\"Child\\") + + return (openBlock(), createBlock(_component_Child, null, { + foo: withId(({ msg }) => [ + createTextVNode(toString(msg), 1 /* TEXT */) + ]), + bar: withId(() => [ + createVNode(\\"div\\") + ]), + _compiled: true + })) +})" +`; + +exports[`scopeId compiler support should wrap render function 1`] = ` +"import { createVNode, createBlock, openBlock, withScopeId } from \\"vue\\" +const withId = withScopeId(\\"test\\") + +export default withId(function render() { + const _ctx = this + return (openBlock(), createBlock(\\"div\\")) +})" +`; diff --git a/packages/compiler-core/__tests__/scopeId.spec.ts b/packages/compiler-core/__tests__/scopeId.spec.ts new file mode 100644 index 00000000..37cde9c2 --- /dev/null +++ b/packages/compiler-core/__tests__/scopeId.spec.ts @@ -0,0 +1,91 @@ +import { baseCompile } from '../src/compile' +import { + WITH_SCOPE_ID, + PUSH_SCOPE_ID, + POP_SCOPE_ID +} from '../src/runtimeHelpers' + +describe('scopeId compiler support', () => { + test('should only work in module mode', () => { + expect(() => { + baseCompile(``, { scopeId: 'test' }) + }).toThrow(`"scopeId" option is only supported in module mode`) + }) + + test('should wrap render function', () => { + const { ast, code } = baseCompile(`
`, { + mode: 'module', + scopeId: 'test' + }) + expect(ast.helpers).toContain(WITH_SCOPE_ID) + expect(code).toMatch(`const withId = withScopeId("test")`) + expect(code).toMatch(`export default withId(function render() {`) + expect(code).toMatchSnapshot() + }) + + test('should wrap default slot', () => { + const { code } = baseCompile(`
`, { + mode: 'module', + scopeId: 'test' + }) + expect(code).toMatch(`default: withId(() => [`) + expect(code).toMatchSnapshot() + }) + + test('should wrap named slots', () => { + const { code } = baseCompile( + ` + + + + `, + { + mode: 'module', + scopeId: 'test' + } + ) + expect(code).toMatch(`foo: withId(({ msg }) => [`) + expect(code).toMatch(`bar: withId(() => [`) + expect(code).toMatchSnapshot() + }) + + test('should wrap dynamic slots', () => { + const { code } = baseCompile( + ` + + + + `, + { + mode: 'module', + scopeId: 'test' + } + ) + expect(code).toMatch(/name: "foo",\s+fn: withId\(/) + expect(code).toMatch(/name: i,\s+fn: withId\(/) + expect(code).toMatchSnapshot() + }) + + test('should push scopeId for hoisted nodes', () => { + const { ast, code } = baseCompile( + `
hello
world
`, + { + mode: 'module', + scopeId: 'test', + hoistStatic: true + } + ) + expect(ast.helpers).toContain(PUSH_SCOPE_ID) + expect(ast.helpers).toContain(POP_SCOPE_ID) + expect(ast.hoists.length).toBe(2) + expect(code).toMatch( + [ + `pushScopeId("test")`, + `const _hoisted_1 = createVNode("div", null, "hello")`, + `const _hoisted_2 = createVNode("div", null, "world")`, + `popScopeId()` + ].join('\n') + ) + expect(code).toMatchSnapshot() + }) +}) diff --git a/packages/compiler-core/src/compile.ts b/packages/compiler-core/src/compile.ts index 0ff53166..d6214137 100644 --- a/packages/compiler-core/src/compile.ts +++ b/packages/compiler-core/src/compile.ts @@ -17,28 +17,33 @@ import { transformOnce } from './transforms/vOnce' import { transformModel } from './transforms/vModel' import { defaultOnError, createCompilerError, ErrorCodes } from './errors' -// we name it `baseCompile` so that higher order compilers like @vue/compiler-dom -// can export `compile` while re-exporting everything else. +// we name it `baseCompile` so that higher order compilers like +// @vue/compiler-dom can export `compile` while re-exporting everything else. export function baseCompile( template: string | RootNode, options: CompilerOptions = {} ): CodegenResult { + const onError = options.onError || defaultOnError + const isModuleMode = options.mode === 'module' /* istanbul ignore if */ if (__BROWSER__) { - const onError = options.onError || defaultOnError if (options.prefixIdentifiers === true) { onError(createCompilerError(ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED)) - } else if (options.mode === 'module') { + } else if (isModuleMode) { onError(createCompilerError(ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED)) } } - const ast = isString(template) ? parse(template, options) : template - const prefixIdentifiers = - !__BROWSER__ && - (options.prefixIdentifiers === true || options.mode === 'module') + !__BROWSER__ && (options.prefixIdentifiers === true || isModuleMode) + if (!prefixIdentifiers && options.cacheHandlers) { + onError(createCompilerError(ErrorCodes.X_CACHE_HANDLER_NOT_SUPPORTED)) + } + if (options.scopeId && !isModuleMode) { + onError(createCompilerError(ErrorCodes.X_SCOPE_ID_NOT_SUPPORTED)) + } + const ast = isString(template) ? parse(template, options) : template transform(ast, { ...options, prefixIdentifiers, diff --git a/packages/compiler-core/src/errors.ts b/packages/compiler-core/src/errors.ts index b507a416..71c19e26 100644 --- a/packages/compiler-core/src/errors.ts +++ b/packages/compiler-core/src/errors.ts @@ -86,6 +86,8 @@ export const enum ErrorCodes { // generic errors X_PREFIX_ID_NOT_SUPPORTED, X_MODULE_MODE_NOT_SUPPORTED, + X_CACHE_HANDLER_NOT_SUPPORTED, + X_SCOPE_ID_NOT_SUPPORTED, // Special value for higher-order compilers to pick up the last code // to avoid collision of error codes. This should always be kept as the last @@ -177,5 +179,7 @@ export const errorMessages: { [code: number]: string } = { // generic errors [ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED]: `"prefixIdentifiers" option is not supported in this build of compiler.`, - [ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED]: `ES module mode is not supported in this build of compiler.` + [ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED]: `ES module mode is not supported in this build of compiler.`, + [ErrorCodes.X_CACHE_HANDLER_NOT_SUPPORTED]: `"cacheHandlers" option is only supported when the "prefixIdentifiers" option is enabled.`, + [ErrorCodes.X_SCOPE_ID_NOT_SUPPORTED]: `"scopeId" option is only supported in module mode.` } diff --git a/packages/compiler-core/src/options.ts b/packages/compiler-core/src/options.ts index b2d7e878..2e08cdae 100644 --- a/packages/compiler-core/src/options.ts +++ b/packages/compiler-core/src/options.ts @@ -28,17 +28,21 @@ export interface TransformOptions { directiveTransforms?: { [name: string]: DirectiveTransform } isBuiltInComponent?: (tag: string) => symbol | void // Transform expressions like {{ foo }} to `_ctx.foo`. - // Default: mode === 'module' + // - This is force-enabled in module mode, since modules are by default strict + // and cannot use `with` + // - Default: mode === 'module' prefixIdentifiers?: boolean // Hoist static VNodes and props objects to `_hoisted_x` constants - // Default: false + // - Default: false hoistStatic?: boolean // Cache v-on handlers to avoid creating new inline functions on each render, // also avoids the need for dynamically patching the handlers by wrapping it. // e.g `@click="foo"` by default is compiled to `{ onClick: foo }`. With this // option it's compiled to: // `{ onClick: _cache[0] || (_cache[0] = e => _ctx.foo(e)) }` - // Default: false + // - Requires "prefixIdentifiers" to be enabled because it relies on scope + // analysis to determine if a handler is safe to cache. + // - Default: false cacheHandlers?: boolean onError?: (error: CompilerError) => void } @@ -49,18 +53,20 @@ export interface CodegenOptions { // - Function mode will generate a single `const { helpers... } = Vue` // statement and return the render function. It is meant to be used with // `new Function(code)()` to generate a render function at runtime. - // Default: 'function' + // - Default: 'function' mode?: 'module' | 'function' // Prefix suitable identifiers with _ctx. // If this option is false, the generated code will be wrapped in a // `with (this) { ... }` block. - // Default: false + // - This is force-enabled in module mode, since modules are by default strict + // and cannot use `with` + // - Default: mode === 'module' prefixIdentifiers?: boolean // Generate source map? - // Default: false + // - Default: false sourceMap?: boolean // Filename for source map generation. - // Default: `template.vue.html` + // - Default: `template.vue.html` filename?: string // SFC scoped styles ID scopeId?: string | null diff --git a/packages/runtime-core/__tests__/helpers/scopeId.spec.ts b/packages/runtime-core/__tests__/helpers/scopeId.spec.ts new file mode 100644 index 00000000..08f7b1e7 --- /dev/null +++ b/packages/runtime-core/__tests__/helpers/scopeId.spec.ts @@ -0,0 +1,47 @@ +import { withScopeId } from '../../src/helpers/scopeId' +import { h, render, nodeOps, serializeInner } from '@vue/runtime-test' + +describe('scopeId runtime support', () => { + const withParentId = withScopeId('parent') + const withChildId = withScopeId('child') + + test('should attach scopeId', () => { + const App = { + __scopeId: 'parent', + render: withParentId(() => { + return h('div', [h('div')]) + }) + } + const root = nodeOps.createElement('div') + render(h(App), root) + expect(serializeInner(root)).toBe(`
`) + }) + + test('should work on slots', () => { + const Child = { + __scopeId: 'child', + render: withChildId(function(this: any) { + return h('div', this.$slots.default()) + }) + } + const App = { + __scopeId: 'parent', + render: withParentId(() => { + return h( + Child, + withParentId(() => { + return h('div') + }) + ) + }) + } + const root = nodeOps.createElement('div') + render(h(App), root) + // slot content should have: + // - scopeId from parent + // - slotted scopeId (with `-s` postfix) from child (the tree owner) + expect(serializeInner(root)).toBe( + `
` + ) + }) +}) diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index aea2b61e..e40ec89e 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -36,7 +36,6 @@ function compileToFunction( const { code } = compile(template, { hoistStatic: true, - cacheHandlers: true, onError(err: CompilerError) { if (__DEV__) { const message = `Template compilation error: ${err.message}`