diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts index 31fe534ca78..9e58d751da9 100644 --- a/packages/compiler-core/src/codegen.ts +++ b/packages/compiler-core/src/codegen.ts @@ -60,12 +60,13 @@ type CodegenNode = TemplateChildNode | JSChildNode | SSRCodegenNode export interface CodegenResult { code: string + preamble: string ast: RootNode map?: RawSourceMap } export interface CodegenContext - extends Omit, 'bindingMetadata'> { + extends Omit, 'bindingMetadata' | 'inline'> { source: string code: string line: number @@ -199,12 +200,18 @@ export function generate( const hasHelpers = ast.helpers.length > 0 const useWithBlock = !prefixIdentifiers && mode !== 'module' const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module' + const isSetupInlined = !!options.inline // preambles + // in setup() inline mode, the preamble is generated in a sub context + // and returned separately. + const preambleContext = isSetupInlined + ? createCodegenContext(ast, options) + : context if (!__BROWSER__ && mode === 'module') { - genModulePreamble(ast, context, genScopeId) + genModulePreamble(ast, preambleContext, genScopeId, isSetupInlined) } else { - genFunctionPreamble(ast, context) + genFunctionPreamble(ast, preambleContext) } // binding optimizations @@ -213,10 +220,17 @@ export function generate( : `` // enter render function if (!ssr) { - if (genScopeId) { - push(`const render = ${PURE_ANNOTATION}_withId(`) + if (isSetupInlined) { + if (genScopeId) { + push(`${PURE_ANNOTATION}_withId(`) + } + push(`(_ctx, _cache${optimizeSources}) => {`) + } else { + if (genScopeId) { + push(`const render = ${PURE_ANNOTATION}_withId(`) + } + push(`function render(_ctx, _cache${optimizeSources}) {`) } - push(`function render(_ctx, _cache${optimizeSources}) {`) } else { if (genScopeId) { push(`const ssrRender = ${PURE_ANNOTATION}_withId(`) @@ -290,6 +304,7 @@ export function generate( return { ast, code: context.code, + preamble: isSetupInlined ? preambleContext.code : ``, // SourceMapGenerator does have toJSON() method but it's not in the types map: context.map ? (context.map as any).toJSON() : undefined } @@ -356,7 +371,8 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) { function genModulePreamble( ast: RootNode, context: CodegenContext, - genScopeId: boolean + genScopeId: boolean, + inline?: boolean ) { const { push, @@ -423,7 +439,10 @@ function genModulePreamble( genHoists(ast.hoists, context) newline() - push(`export `) + + if (!inline) { + push(`export `) + } } function genAssets( diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts index 7cc6219c1cb..03388b1f7a8 100644 --- a/packages/compiler-core/src/index.ts +++ b/packages/compiler-core/src/index.ts @@ -7,7 +7,8 @@ export { TransformOptions, CodegenOptions, HoistTransform, - BindingMetadata + BindingMetadata, + BindingTypes } from './options' export { baseParse, TextModes } from './parse' export { diff --git a/packages/compiler-core/src/options.ts b/packages/compiler-core/src/options.ts index 22d8a086b2f..2a6b79c1642 100644 --- a/packages/compiler-core/src/options.ts +++ b/packages/compiler-core/src/options.ts @@ -61,11 +61,47 @@ export type HoistTransform = ( parent: ParentNode ) => void +export const enum BindingTypes { + DATA = 'data', + PROPS = 'props', + SETUP = 'setup', + CONST = 'const', + OPTIONS = 'options' +} + export interface BindingMetadata { - [key: string]: 'data' | 'props' | 'setup' | 'options' + [key: string]: BindingTypes +} + +interface SharedTransformCodegenOptions { + /** + * Transform expressions like {{ foo }} to `_ctx.foo`. + * If this option is false, the generated code will be wrapped in a + * `with (this) { ... }` block. + * - This is force-enabled in module mode, since modules are by default strict + * and cannot use `with` + * @default mode === 'module' + */ + prefixIdentifiers?: boolean + /** + * Generate SSR-optimized render functions instead. + * The resulting function must be attached to the component via the + * `ssrRender` option instead of `render`. + */ + ssr?: boolean + /** + * Optional binding metadata analyzed from script - used to optimize + * binding access when `prefixIdentifiers` is enabled. + */ + bindingMetadata?: BindingMetadata + /** + * Compile the function for inlining inside setup(). + * This allows the function to directly access setup() local bindings. + */ + inline?: boolean } -export interface TransformOptions { +export interface TransformOptions extends SharedTransformCodegenOptions { /** * An array of node transforms to be applied to every AST node. */ @@ -128,26 +164,15 @@ export interface TransformOptions { * SFC scoped styles ID */ scopeId?: string | null - /** - * Generate SSR-optimized render functions instead. - * The resulting function must be attached to the component via the - * `ssrRender` option instead of `render`. - */ - ssr?: boolean /** * SFC `` ).content ) @@ -343,9 +367,7 @@ import b from 'b' describe('async/await detection', () => { function assertAwaitDetection(code: string, shouldAsync = true) { const { content } = compile(``) - expect(content).toMatch( - `export ${shouldAsync ? `async ` : ``}function setup` - ) + expect(content).toMatch(`${shouldAsync ? `async ` : ``}setup()`) } test('expression statement', () => { @@ -356,8 +378,8 @@ import b from 'b' assertAwaitDetection(`const a = 1 + (await foo)`) }) - test('export', () => { - assertAwaitDetection(`export const a = 1 + (await foo)`) + test('ref', () => { + assertAwaitDetection(`ref: a = 1 + (await foo)`) }) test('nested statements', () => { @@ -366,7 +388,7 @@ import b from 'b' test('should ignore await inside functions', () => { // function declaration - assertAwaitDetection(`export async function foo() { await bar }`, false) + assertAwaitDetection(`async function foo() { await bar }`, false) // function expression assertAwaitDetection(`const foo = async () => { await bar }`, false) // object method @@ -379,6 +401,202 @@ import b from 'b' }) }) + describe('ref: syntax sugar', () => { + test('convert ref declarations', () => { + const { content, bindings } = compile(``) + expect(content).toMatch(`import { ref as _ref } from 'vue'`) + expect(content).not.toMatch(`ref: foo`) + expect(content).not.toMatch(`ref: a`) + expect(content).not.toMatch(`ref: b`) + expect(content).toMatch(`const foo = _ref()`) + expect(content).toMatch(`const a = _ref(1)`) + expect(content).toMatch(` + const b = _ref({ + count: 0 + }) + `) + // normal declarations left untouched + expect(content).toMatch(`let c = () => {}`) + expect(content).toMatch(`let d`) + assertCode(content) + expect(bindings).toStrictEqual({ + foo: 'setup', + a: 'setup', + b: 'setup', + c: 'setup', + d: 'setup' + }) + }) + + test('multi ref declarations', () => { + const { content, bindings } = compile(``) + expect(content).toMatch(` + const a = _ref(1), b = _ref(2), c = _ref({ + count: 0 + }) + `) + expect(content).toMatch(`return { a, b, c }`) + assertCode(content) + expect(bindings).toStrictEqual({ + a: 'setup', + b: 'setup', + c: 'setup' + }) + }) + + test('should not convert non ref labels', () => { + const { content } = compile(``) + expect(content).toMatch(`foo: a = 1, b = 2`) + assertCode(content) + }) + + test('accessing ref binding', () => { + const { content } = compile(``) + expect(content).toMatch(`console.log(a.value)`) + expect(content).toMatch(`return a.value + 1`) + assertCode(content) + }) + + test('cases that should not append .value', () => { + const { content } = compile(``) + expect(content).not.toMatch(`a.value`) + }) + + test('mutating ref binding', () => { + const { content } = compile(``) + expect(content).toMatch(`a.value++`) + expect(content).toMatch(`a.value = a.value + 1`) + expect(content).toMatch(`b.value.count++`) + expect(content).toMatch(`b.value.count = b.value.count + 1`) + assertCode(content) + }) + + test('using ref binding in property shorthand', () => { + const { content } = compile(``) + expect(content).toMatch(`const b = { a: a.value }`) + // should not convert destructure + expect(content).toMatch(`const { a } = b`) + assertCode(content) + }) + + test('object destructure', () => { + const { content, bindings } = compile(``) + expect(content).toMatch( + `const n = _ref(1), { a: __a, b: __c, d: __d = 1, e: __f = 2, ...__g } = useFoo()` + ) + expect(content).toMatch(`\nconst a = _ref(__a);`) + expect(content).not.toMatch(`\nconst b = _ref(__b);`) + expect(content).toMatch(`\nconst c = _ref(__c);`) + expect(content).toMatch(`\nconst d = _ref(__d);`) + expect(content).not.toMatch(`\nconst e = _ref(__e);`) + expect(content).toMatch(`\nconst f = _ref(__f);`) + expect(content).toMatch(`\nconst g = _ref(__g);`) + expect(content).toMatch( + `console.log(n.value, a.value, c.value, d.value, f.value, g.value)` + ) + expect(content).toMatch(`return { n, a, c, d, f, g }`) + expect(bindings).toStrictEqual({ + n: 'setup', + a: 'setup', + c: 'setup', + d: 'setup', + f: 'setup', + g: 'setup' + }) + assertCode(content) + }) + + test('array destructure', () => { + const { content, bindings } = compile(``) + expect(content).toMatch( + `const n = _ref(1), [__a, __b = 1, ...__c] = useFoo()` + ) + expect(content).toMatch(`\nconst a = _ref(__a);`) + expect(content).toMatch(`\nconst b = _ref(__b);`) + expect(content).toMatch(`\nconst c = _ref(__c);`) + expect(content).toMatch(`console.log(n.value, a.value, b.value, c.value)`) + expect(content).toMatch(`return { n, a, b, c }`) + expect(bindings).toStrictEqual({ + n: 'setup', + a: 'setup', + b: 'setup', + c: 'setup' + }) + assertCode(content) + }) + + test('nested destructure', () => { + const { content, bindings } = compile(``) + expect(content).toMatch(`const [{ a: { b: __b }}] = useFoo()`) + expect(content).toMatch(`const { c: [__d, __e] } = useBar()`) + expect(content).not.toMatch(`\nconst a = _ref(__a);`) + expect(content).not.toMatch(`\nconst c = _ref(__c);`) + expect(content).toMatch(`\nconst b = _ref(__b);`) + expect(content).toMatch(`\nconst d = _ref(__d);`) + expect(content).toMatch(`\nconst e = _ref(__e);`) + expect(content).toMatch(`return { b, d, e }`) + expect(bindings).toStrictEqual({ + b: 'setup', + d: 'setup', + e: 'setup' + }) + assertCode(content) + }) + }) + describe('errors', () => { test('`) + ).toThrow(moduleErrorMsg) + + expect(() => + compile(``) + ).toThrow(moduleErrorMsg) + expect(() => compile(``) - ).toThrow(`Cannot export locally defined variable as default`) + ).toThrow(moduleErrorMsg) }) - test('export default referencing local var', () => { + test('ref: non-assignment expressions', () => { expect(() => compile(``) + ).toThrow(`ref: statements can only contain assignment expressions`) + }) + + test('defineOptions() w/ both type and non-type args', () => { + expect(() => { + compile(``) + }).toThrow(`cannot accept both type and non-type arguments`) + }) + + test('defineOptions() referencing local var', () => { + expect(() => + compile(``) ).toThrow(`cannot reference locally declared variables`) }) - test('export default referencing exports', () => { + test('defineOptions() referencing ref declarations', () => { expect(() => compile(``) ).toThrow(`cannot reference locally declared variables`) }) - test('should allow export default referencing scope var', () => { + test('should allow defineOptions() referencing scope var', () => { assertCode( compile(``).content ) }) - test('should allow export default referencing imported binding', () => { + test('should allow defineOptions() referencing imported binding', () => { assertCode( compile(``).content ) }) - - test('should allow export default referencing re-exported binding', () => { - assertCode( - compile(``).content - ) - }) - - test('error on duplicated default export', () => { - expect(() => - compile(` - - - `) - ).toThrow(`Default export is already declared`) - - expect(() => - compile(` - - - `) - ).toThrow(`Default export is already declared`) - - expect(() => - compile(` - - - `) - ).toThrow(`Default export is already declared`) - - expect(() => - compile(` - - - `) - ).toThrow(`Default export is already declared`) - - expect(() => - compile(` - - - `) - ).toThrow(`Default export is already declared`) - }) }) }) @@ -725,11 +903,12 @@ describe('SFC analyze `) expect(bindings).toStrictEqual({ diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index 93cc6cc4383..a049d205971 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -1,7 +1,7 @@ import MagicString from 'magic-string' -import { BindingMetadata } from '@vue/compiler-core' +import { BindingMetadata, BindingTypes } from '@vue/compiler-core' import { SFCDescriptor, SFCScriptBlock } from './parse' -import { parse, ParserPlugin } from '@babel/parser' +import { parse as _parse, ParserOptions, ParserPlugin } from '@babel/parser' import { babelParserDefaultPlugins, generateCodeFrame } from '@vue/shared' import { Node, @@ -10,30 +10,59 @@ import { ObjectExpression, ArrayPattern, Identifier, - ExpressionStatement, - ArrowFunctionExpression, ExportSpecifier, Function as FunctionNode, TSType, TSTypeLiteral, TSFunctionType, - TSDeclareFunction, ObjectProperty, ArrayExpression, - Statement + Statement, + Expression, + LabeledStatement, + TSUnionType } from '@babel/types' import { walk } from 'estree-walker' import { RawSourceMap } from 'source-map' -import { genCssVarsCode, injectCssVarsCalls } from './genCssVars' +import { + CSS_VARS_HELPER, + genCssVarsCode, + injectCssVarsCalls +} from './genCssVars' +import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate' + +const DEFINE_OPTIONS = 'defineOptions' export interface SFCScriptCompileOptions { /** * https://babeljs.io/docs/en/babel-parser#plugins */ babelParserPlugins?: ParserPlugin[] + /** + * Enable ref: label sugar + * https://github.com/vuejs/rfcs/pull/228 + * @default true + */ + refSugar?: boolean + /** + * Compile the template and inline the resulting render function + * directly inside setup(). + * - Only affects