}),
)
})
+
+ test('used on const binding', () => {
+ const onError = vi.fn()
+ parseWithVModel('<div v-model="c" />', {
+ onError,
+ bindingMetadata: {
+ c: BindingTypes.LITERAL_CONST,
+ },
+ })
+
+ expect(onError).toHaveBeenCalledTimes(1)
+ expect(onError).toHaveBeenCalledWith(
+ expect.objectContaining({
+ code: ErrorCodes.X_V_MODEL_ON_CONST,
+ }),
+ )
+ })
})
})
X_V_MODEL_MALFORMED_EXPRESSION,
X_V_MODEL_ON_SCOPE_VARIABLE,
X_V_MODEL_ON_PROPS,
+ X_V_MODEL_ON_CONST,
X_INVALID_EXPRESSION,
X_KEEP_ALIVE_INVALID_CHILDREN,
[ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION]: `v-model value must be a valid JavaScript member expression.`,
[ErrorCodes.X_V_MODEL_ON_SCOPE_VARIABLE]: `v-model cannot be used on v-for or v-slot scope variables because they are not writable.`,
[ErrorCodes.X_V_MODEL_ON_PROPS]: `v-model cannot be used on a prop, because local prop bindings are not writable.\nUse a v-bind binding combined with a v-on listener that emits update:x event instead.`,
+ [ErrorCodes.X_V_MODEL_ON_CONST]: `v-model cannot be used on a const binding because it is not writable.`,
[ErrorCodes.X_INVALID_EXPRESSION]: `Error parsing JavaScript expression: `,
[ErrorCodes.X_KEEP_ALIVE_INVALID_CHILDREN]: `<KeepAlive> expects exactly one child component.`,
[ErrorCodes.X_VNODE_HOOKS]: `@vnode-* hooks in templates are no longer supported. Use the vue: prefix instead. For example, @vnode-mounted should be changed to @vue:mounted. @vnode-* hooks support has been removed in 3.4.`,
return createTransformProps()
}
+ // const bindings are not writable.
+ if (
+ bindingType === BindingTypes.LITERAL_CONST ||
+ bindingType === BindingTypes.SETUP_CONST
+ ) {
+ context.onError(createCompilerError(ErrorCodes.X_V_MODEL_ON_CONST, exp.loc))
+ return createTransformProps()
+ }
+
const maybeRef =
!__BROWSER__ &&
context.inline &&
}
export enum DOMErrorCodes {
- X_V_HTML_NO_EXPRESSION = 53 /* ErrorCodes.__EXTEND_POINT__ */,
+ X_V_HTML_NO_EXPRESSION = 54 /* ErrorCodes.__EXTEND_POINT__ */,
X_V_HTML_WITH_CHILDREN,
X_V_TEXT_NO_EXPRESSION,
X_V_TEXT_WITH_CHILDREN,
}"
`;
+exports[`SFC compile <script setup> > demote const reactive binding to let when used in v-model (inlineTemplate) 1`] = `
+"import { unref as _unref, resolveComponent as _resolveComponent, isRef as _isRef, openBlock as _openBlock, createBlock as _createBlock } from "vue"
+
+import { reactive } from 'vue'
+
+export default {
+ setup(__props) {
+
+ let name = reactive({ first: 'john', last: 'doe' })
+
+return (_ctx, _cache) => {
+ const _component_MyComponent = _resolveComponent("MyComponent")
+
+ return (_openBlock(), _createBlock(_component_MyComponent, {
+ modelValue: _unref(name),
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = $event => (_isRef(name) ? (name).value = $event : name = $event))
+ }, null, 8 /* PROPS */, ["modelValue"]))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > demote const reactive binding to let when used in v-model 1`] = `
+"import { reactive } from 'vue'
+
+export default {
+ setup(__props, { expose: __expose }) {
+ __expose();
+
+ let name = reactive({ first: 'john', last: 'doe' })
+
+return { get name() { return name }, set name(v) { name = v }, reactive }
+}
+
+}"
+`;
+
exports[`SFC compile <script setup> > errors > should allow defineProps/Emit() referencing imported binding 1`] = `
"import { bar } from './bar'
+import { vi } from 'vitest'
import { BindingTypes } from '@vue/compiler-core'
import {
assertCode,
} from './utils'
import { type RawSourceMap, SourceMapConsumer } from 'source-map-js'
+vi.mock('../src/warn', () => ({
+ warn: vi.fn(),
+ warnOnce: vi.fn(),
+}))
+
+import { warnOnce } from '../src/warn'
+
+const warnOnceMock = vi.mocked(warnOnce)
+
describe('SFC compile <script setup>', () => {
test('should compile JS syntax', () => {
const { content } = compile(`
assertCode(content)
})
+ test('demote const reactive binding to let when used in v-model', () => {
+ warnOnceMock.mockClear()
+ const { content, bindings } = compile(`
+ <script setup>
+ import { reactive } from 'vue'
+ const name = reactive({ first: 'john', last: 'doe' })
+ </script>
+
+ <template>
+ <MyComponent v-model="name" />
+ </template>
+ `)
+
+ expect(content).toMatch(
+ `let name = reactive({ first: 'john', last: 'doe' })`,
+ )
+ expect(bindings!.name).toBe(BindingTypes.SETUP_LET)
+ expect(warnOnceMock).toHaveBeenCalledTimes(1)
+ expect(warnOnceMock).toHaveBeenCalledWith(
+ expect.stringContaining(
+ '`v-model` cannot update a `const` reactive binding',
+ ),
+ )
+ assertCode(content)
+ })
+
+ test('demote const reactive binding to let when used in v-model (inlineTemplate)', () => {
+ warnOnceMock.mockClear()
+ const { content, bindings } = compile(
+ `
+ <script setup>
+ import { reactive } from 'vue'
+ const name = reactive({ first: 'john', last: 'doe' })
+ </script>
+
+ <template>
+ <MyComponent v-model="name" />
+ </template>
+ `,
+ { inlineTemplate: true },
+ )
+
+ expect(content).toMatch(
+ `let name = reactive({ first: 'john', last: 'doe' })`,
+ )
+ expect(bindings!.name).toBe(BindingTypes.SETUP_LET)
+ expect(warnOnceMock).toHaveBeenCalledTimes(1)
+ expect(warnOnceMock).toHaveBeenCalledWith(
+ expect.stringContaining(
+ '`v-model` cannot update a `const` reactive binding',
+ ),
+ )
+ assertCode(content)
+ })
+
+ test('v-model should error on literal const bindings', () => {
+ expect(() =>
+ compile(
+ `
+ <script setup>
+ const foo = 1
+ </script>
+ <template>
+ <input v-model="foo" />
+ </template>
+ `,
+ { inlineTemplate: true },
+ ),
+ ).toThrow('v-model cannot be used on a const binding')
+ })
+
describe('<script> and <script setup> co-usage', () => {
test('script first', () => {
const { content } = compile(`
isTS,
} from './script/utils'
import { analyzeScriptBindings } from './script/analyzeScriptBindings'
-import { isImportUsed } from './script/importUsageCheck'
+import {
+ isImportUsed,
+ resolveTemplateVModelIdentifiers,
+} from './script/importUsageCheck'
import { processAwait } from './script/topLevelAwait'
export interface SFCScriptCompileOptions {
ctx.bindingMetadata[key] = setupBindings[key]
}
+ // #11265, https://github.com/vitejs/rolldown-vite/issues/432
+ // 6.1 demote `const foo = reactive()` to `let` when used as v-model target.
+ // In non-inline template compilation, v-model assigns via `$setup.foo = $event`,
+ // which requires a SETUP_LET binding (getter + setter) to keep script state in sync.
+ // In inline mode, it generates `foo = $event`, which also requires `let`.
+ if (sfc.template && !sfc.template.src && sfc.template.ast) {
+ const vModelIds = resolveTemplateVModelIdentifiers(sfc)
+ if (vModelIds.size) {
+ const toDemote = new Set<string>()
+ for (const id of vModelIds) {
+ if (setupBindings[id] === BindingTypes.SETUP_REACTIVE_CONST) {
+ toDemote.add(id)
+ }
+ }
+
+ if (toDemote.size) {
+ for (const node of scriptSetupAst.body) {
+ if (
+ node.type === 'VariableDeclaration' &&
+ node.kind === 'const' &&
+ !node.declare
+ ) {
+ const demotedInDecl: string[] = []
+ for (const decl of node.declarations) {
+ if (decl.id.type === 'Identifier' && toDemote.has(decl.id.name)) {
+ demotedInDecl.push(decl.id.name)
+ }
+ }
+ if (demotedInDecl.length) {
+ ctx.s.overwrite(
+ node.start! + startOffset,
+ node.start! + startOffset + 'const'.length,
+ 'let',
+ )
+ for (const id of demotedInDecl) {
+ setupBindings[id] = BindingTypes.SETUP_LET
+ ctx.bindingMetadata[id] = BindingTypes.SETUP_LET
+ warnOnce(
+ `\`v-model\` cannot update a \`const\` reactive binding \`${id}\`. ` +
+ `The compiler has transformed it to \`let\` to make the update work.`,
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
// 7. inject `useCssVars` calls
if (
sfc.cssVars.length &&
NodeTypes,
type SimpleExpressionNode,
type TemplateChildNode,
+ isSimpleIdentifier,
parserOptions,
walkIdentifiers,
} from '@vue/compiler-dom'
return resolveTemplateUsedIdentifiers(sfc).has(local)
}
-const templateUsageCheckCache = createCache<Set<string>>()
+const templateAnalysisCache = createCache<{
+ usedIds?: Set<string>
+ vModelIds: Set<string>
+}>()
+
+export function resolveTemplateVModelIdentifiers(
+ sfc: SFCDescriptor,
+): Set<string> {
+ return resolveTemplateAnalysisResult(sfc, false).vModelIds
+}
function resolveTemplateUsedIdentifiers(sfc: SFCDescriptor): Set<string> {
+ return resolveTemplateAnalysisResult(sfc).usedIds!
+}
+
+function resolveTemplateAnalysisResult(
+ sfc: SFCDescriptor,
+ collectUsedIds = true,
+): {
+ usedIds?: Set<string>
+ vModelIds: Set<string>
+} {
const { content, ast } = sfc.template!
- const cached = templateUsageCheckCache.get(content)
- if (cached) {
+ const cached = templateAnalysisCache.get(content)
+ if (cached && (!collectUsedIds || cached.usedIds)) {
return cached
}
- const ids = new Set<string>()
+ // When `collectUsedIds` is false we skip the expensive identifier extraction
+ // and only collect `vModelIds`.
+ const ids = collectUsedIds ? new Set<string>() : undefined
+ const vModelIds = new Set<string>()
ast!.children.forEach(walk)
!parserOptions.isNativeTag!(tag) &&
!parserOptions.isBuiltInComponent!(tag)
) {
- ids.add(camelize(tag))
- ids.add(capitalize(camelize(tag)))
+ if (ids) {
+ ids.add(camelize(tag))
+ ids.add(capitalize(camelize(tag)))
+ }
}
for (let i = 0; i < node.props.length; i++) {
const prop = node.props[i]
if (prop.type === NodeTypes.DIRECTIVE) {
- if (!isBuiltInDirective(prop.name)) {
- ids.add(`v${capitalize(camelize(prop.name))}`)
+ if (ids) {
+ if (!isBuiltInDirective(prop.name)) {
+ ids.add(`v${capitalize(camelize(prop.name))}`)
+ }
+ }
+
+ // collect v-model target identifiers (simple identifiers only)
+ if (prop.name === 'model') {
+ const exp = prop.exp
+ if (exp && exp.type === NodeTypes.SIMPLE_EXPRESSION) {
+ const expString = exp.content.trim()
+ if (
+ isSimpleIdentifier(expString) &&
+ expString !== 'undefined'
+ ) {
+ vModelIds.add(expString)
+ }
+ }
}
// process dynamic directive arguments
- if (prop.arg && !(prop.arg as SimpleExpressionNode).isStatic) {
+ if (
+ ids &&
+ prop.arg &&
+ !(prop.arg as SimpleExpressionNode).isStatic
+ ) {
extractIdentifiers(ids, prop.arg)
}
- if (prop.name === 'for') {
- extractIdentifiers(ids, prop.forParseResult!.source)
- } else if (prop.exp) {
- extractIdentifiers(ids, prop.exp)
- } else if (prop.name === 'bind' && !prop.exp) {
- // v-bind shorthand name as identifier
- ids.add(camelize((prop.arg as SimpleExpressionNode).content))
+ if (ids) {
+ if (prop.name === 'for') {
+ extractIdentifiers(ids, prop.forParseResult!.source)
+ } else if (prop.exp) {
+ extractIdentifiers(ids, prop.exp)
+ } else if (prop.name === 'bind' && !prop.exp) {
+ // v-bind shorthand name as identifier
+ ids.add(camelize((prop.arg as SimpleExpressionNode).content))
+ }
}
}
if (
+ ids &&
prop.type === NodeTypes.ATTRIBUTE &&
prop.name === 'ref' &&
prop.value?.content
node.children.forEach(walk)
break
case NodeTypes.INTERPOLATION:
- extractIdentifiers(ids, node.content)
+ if (ids) extractIdentifiers(ids, node.content)
break
}
}
- templateUsageCheckCache.set(content, ids)
- return ids
+ const result = { usedIds: ids, vModelIds }
+ templateAnalysisCache.set(content, result)
+ return result
}
function extractIdentifiers(ids: Set<string>, node: ExpressionNode) {