}"
`;
+exports[`compile > gen unique helper alias > should avoid conflicts with existing variable names 1`] = `
+"import { txt as _txt2, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div> </div>", true)
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+ const n0 = t0()
+ const x0 = _txt2(n0)
+ _renderEffect(() => _setText(x0, _toDisplayString(_ctx.foo)))
+ return n0
+}"
+`;
+
+exports[`compile > gen unique node variables > should avoid binding conflicts for node vars (n*/x*) 1`] = `
+"import { txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div> </div>")
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+ const n1 = t0()
+ const n3 = t0()
+ const x1 = _txt(n1)
+ const x3 = _txt(n3)
+ _renderEffect(() => {
+ const _foo = _ctx.foo
+ _setText(x1, _toDisplayString(_foo))
+ _setText(x3, _toDisplayString(_foo))
+ })
+ return [n1, n3]
+}"
+`;
+
+exports[`compile > gen unique node variables > should bump old ref var (r*) on conflict 1`] = `
+"import { createTemplateRefSetter as _createTemplateRefSetter, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>")
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+ const _setTemplateRef = _createTemplateRefSetter()
+ const n1 = t0()
+ const n3 = t0()
+ const n4 = t0()
+ let r1
+ let r3
+ let r4
+ _renderEffect(() => {
+ const _bar = _ctx.bar
+ r1 = _setTemplateRef(n1, _bar, r1)
+ r3 = _setTemplateRef(n3, _bar, r3)
+ r4 = _setTemplateRef(n4, _bar, r4)
+ })
+ return [n1, n3, n4]
+}"
+`;
+
+exports[`compile > gen unique node variables > should bump placeholder var (p*) on conflict 1`] = `
+"import { child as _child, setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div><div><div><span></span></div></div></div>", true)
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+ const n1 = t0()
+ const p1 = _child(n1, 0)
+ const p3 = _child(p1, 0)
+ const n0 = _child(p3, 0)
+ _renderEffect(() => _setProp(n0, "id", _ctx.foo))
+ return n1
+}"
+`;
+
+exports[`compile > gen unique node variables > should bump template var (t*) on conflict 1`] = `
+"import { template as _template } from 'vue';
+const t1 = _template("<div></div>")
+const t3 = _template("<span></span>")
+const t4 = _template("<p></p>")
+
+export function render(_ctx, $props, $emit, $attrs, $slots) {
+ const n0 = t1()
+ const n1 = t3()
+ const n2 = t4()
+ return [n0, n1, n2]
+}"
+`;
+
exports[`compile > static + dynamic root 1`] = `
"import { toDisplayString as _toDisplayString, setText as _setText, template as _template } from 'vue';
const t0 = _template(" ")
expect(code).matchSnapshot()
})
})
+
+ describe('gen unique helper alias', () => {
+ test('should avoid conflicts with existing variable names', () => {
+ const code = compile(`<div>{{ foo }}</div>`, {
+ bindingMetadata: {
+ _txt: BindingTypes.LITERAL_CONST,
+ _txt1: BindingTypes.SETUP_REF,
+ },
+ })
+ expect(code).matchSnapshot()
+ expect(code).contains('txt as _txt2')
+ expect(code).contains('const x0 = _txt2(n0)')
+ })
+ })
+
+ describe('gen unique node variables', () => {
+ test('should avoid binding conflicts for node vars (n*/x*)', () => {
+ const code = compile(`<div>{{ foo }}</div><div>{{ foo }}</div>`, {
+ bindingMetadata: {
+ n0: BindingTypes.SETUP_REACTIVE_CONST,
+ x0: BindingTypes.SETUP_MAYBE_REF,
+ n2: BindingTypes.SETUP_REACTIVE_CONST,
+ x2: BindingTypes.SETUP_MAYBE_REF,
+ },
+ })
+
+ expect(code).matchSnapshot()
+ expect(code).not.contains('const n0')
+ expect(code).not.contains('const x0')
+ expect(code).not.contains('const n2')
+ expect(code).not.contains('const x2')
+ expect(code).contains('const n1 = t0()')
+ expect(code).contains('const n3 = t0()')
+ expect(code).contains('const x1 = _txt(n1)')
+ expect(code).contains('const x3 = _txt(n3)')
+ })
+
+ test('should bump old ref var (r*) on conflict', () => {
+ const code = compile(
+ `<div :ref="bar" /><div :ref="bar" /><div :ref="bar" />`,
+ {
+ bindingMetadata: {
+ r0: BindingTypes.SETUP_REF,
+ r2: BindingTypes.SETUP_REF,
+ bar: BindingTypes.SETUP_REF,
+ },
+ },
+ )
+
+ expect(code).matchSnapshot()
+ expect(code).not.contains('let r0')
+ expect(code).not.contains('let r2')
+ expect(code).contains('let r1')
+ expect(code).contains('let r3')
+ expect(code).contains('let r4')
+ expect(code).contains('r1 = _setTemplateRef(n1, _bar, r1)')
+ expect(code).contains('r3 = _setTemplateRef(n3, _bar, r3)')
+ expect(code).contains('r4 = _setTemplateRef(n4, _bar, r4)')
+ })
+
+ test('should bump template var (t*) on conflict', () => {
+ const code = compile(`<div/><span/><p/>`, {
+ bindingMetadata: {
+ t0: BindingTypes.SETUP_REF,
+ t2: BindingTypes.SETUP_REF,
+ },
+ })
+
+ expect(code).matchSnapshot()
+ expect(code).not.contains('const t0 =')
+ expect(code).not.contains('const t2 =')
+ expect(code).contains('const t1 = _template("<div></div>")')
+ expect(code).contains('const t3 = _template("<span></span>")')
+ expect(code).contains('const t4 = _template("<p></p>")')
+ })
+
+ test('should bump placeholder var (p*) on conflict', () => {
+ const code = compile(
+ `<div><div><div><span :id="foo" /></div></div></div>`,
+ {
+ bindingMetadata: {
+ p0: BindingTypes.SETUP_REF,
+ p2: BindingTypes.SETUP_REF,
+ foo: BindingTypes.SETUP_REF,
+ },
+ },
+ )
+
+ expect(code).matchSnapshot()
+ expect(code).not.contains('const p0 = ')
+ expect(code).not.contains('const p2 = ')
+ expect(code).contains('const p1 = ')
+ expect(code).contains('const p3 = ')
+ })
+ })
})
genCall,
} from './generators/utils'
import { setTemplateRefIdent } from './generators/templateRef'
+import { buildNextIdMap, getNextId } from './transform'
export type CodegenOptions = Omit<BaseCodegenOptions, 'optimizeImports'>
+const idWithTrailingDigitsRE = /^([A-Za-z_$][\w$]*)(\d+)$/
+
export class CodegenContext {
options: Required<CodegenOptions>
- helpers: Set<string> = new Set<string>([])
+ bindingNames: Set<string> = new Set<string>()
+
+ helpers: Map<string, string> = new Map()
- helper = (name: CoreHelper | VaporHelper) => {
- this.helpers.add(name)
- return `_${name}`
+ helper = (name: CoreHelper | VaporHelper): string => {
+ if (this.helpers.has(name)) {
+ return this.helpers.get(name)!
+ }
+
+ const base = `_${name}`
+ if (this.bindingNames.size === 0 || !this.bindingNames.has(base)) {
+ this.helpers.set(name, base)
+ return base
+ }
+
+ const map = this.nextIdMap.get(base)
+ // start from 1 because "base" (no suffix) is already taken.
+ const alias = `${base}${getNextId(map, 1)}`
+ this.helpers.set(name, alias)
+ return alias
}
delegates: Set<string> = new Set<string>()
return [this.scopeLevel++, () => this.scopeLevel--] as const
}
+ private templateVars: Map<number, string> = new Map()
+ private nextIdMap: Map<string, Map<number, number>> = new Map()
+ private lastIdMap: Map<string, number> = new Map()
+ private lastTIndex: number = -1
+ private initNextIdMap(): void {
+ if (this.bindingNames.size === 0) return
+
+ // build a map of binding names to their occupied ids
+ const map = new Map<string, Set<number>>()
+ for (const name of this.bindingNames) {
+ const m = idWithTrailingDigitsRE.exec(name)
+ if (!m) continue
+
+ const prefix = m[1]
+ const num = Number(m[2])
+ let set = map.get(prefix)
+ if (!set) map.set(prefix, (set = new Set<number>()))
+ set.add(num)
+ }
+
+ for (const [prefix, nums] of map) {
+ this.nextIdMap.set(prefix, buildNextIdMap(nums))
+ }
+ }
+
+ tName(i: number): string {
+ let name = this.templateVars.get(i)
+ if (name) return name
+
+ const map = this.nextIdMap.get('t')
+ let lastId = this.lastIdMap.get('t') || -1
+ for (let j = this.lastTIndex + 1; j <= i; j++) {
+ this.templateVars.set(
+ j,
+ (name = `t${(lastId = getNextId(map, Math.max(j, lastId + 1)))}`),
+ )
+ }
+ this.lastIdMap.set('t', lastId)
+ this.lastTIndex = i
+ return name!
+ }
+
+ pName(i: number): string {
+ const map = this.nextIdMap.get('p')
+ let lastId = this.lastIdMap.get('p') || -1
+ this.lastIdMap.set('p', (lastId = getNextId(map, Math.max(i, lastId + 1))))
+ return `p${lastId}`
+ }
+
constructor(
public ir: RootIRNode,
options: CodegenOptions,
}
this.options = extend(defaultOptions, options)
this.block = ir.block
+ this.bindingNames = new Set<string>(
+ this.options.bindingMetadata
+ ? Object.keys(this.options.bindingMetadata)
+ : [],
+ )
+ this.initNextIdMap()
}
}
): VaporCodegenResult {
const [frag, push] = buildCodeFragment()
const context = new CodegenContext(ir, options)
- const { helpers } = context
const { inline, bindingMetadata } = options
const functionName = 'render'
ast: ir,
preamble,
map: map && map.toJSON(),
- helpers,
+ helpers: new Set<string>(Array.from(context.helpers.keys())),
}
}
: ''
}
-function genHelperImports({ helpers, helper, options }: CodegenContext) {
+function genHelperImports({ helpers, options }: CodegenContext) {
let imports = ''
if (helpers.size) {
- imports += `import { ${[...helpers]
- .map(h => `${h} as _${h}`)
+ imports += `import { ${Array.from(helpers)
+ .map(([h, alias]) => `${h} as ${alias}`)
.join(', ')} } from '${options.runtimeModuleName}';\n`
}
return imports
export function genTemplates(
templates: string[],
rootIndex: number | undefined,
- { helper }: CodegenContext,
+ context: CodegenContext,
): string {
return templates
.map(
(template, i) =>
- `const t${i} = ${helper('template')}(${JSON.stringify(
+ `const ${context.tName(i)} = ${context.helper('template')}(${JSON.stringify(
template,
)}${i === rootIndex ? ', true' : ''})\n`,
)
const { id, template, operation, hasDynamicChild } = dynamic
if (id !== undefined && template !== undefined) {
- push(NEWLINE, `const n${id} = t${template}()`)
+ push(NEWLINE, `const n${id} = ${context.tName(template)}()`)
push(...genDirectivesForElement(id, context))
}
const logicalIndex = elementIndex - ifBranchCount + prependCount
// p for "placeholder" variables that are meant for possible reuse by
// other access paths
- const variable = id === undefined ? `p${context.block.tempId++}` : `n${id}`
+ const variable =
+ id === undefined ? context.pName(context.block.tempId++) : `n${id}`
pushBlock(NEWLINE, `const ${variable} = `)
if (prev) {
export type TransformOptions = HackOptions<BaseTransformOptions>
+const generatedVarRE = /^[nxr](\d+)$/
+
export class TransformContext<T extends AllNode = AllNode> {
selfName: string | null = null
parent: TransformContext<RootNode | ElementNode> | null = null
slots: IRSlots[] = []
private globalId = 0
+ private nextIdMap: Map<number, number> | null = null
constructor(
public ir: RootIRNode,
this.options = extend({}, defaultOptions, options)
this.root = this as TransformContext<RootNode>
if (options.filename) this.selfName = getSelfName(options.filename)
+ this.initNextIdMap()
}
enterBlock(ir: BlockIRNode, isVFor: boolean = false): () => void {
}
}
- increaseId = (): number => this.globalId++
+ increaseId = (): number => {
+ // allocate an id that won't conflict with user-defined bindings when used
+ // as generated identifiers with n/x/r prefixes (e.g., n1, x1, r1).
+ const id = getNextId(this.nextIdMap, this.globalId)
+ // advance next
+ this.globalId = getNextId(this.nextIdMap, id + 1)
+ return id
+ }
+
+ private initNextIdMap(): void {
+ const binding = this.root.options.bindingMetadata
+ if (!binding) return
+
+ const keys = Object.keys(binding)
+ if (keys.length === 0) return
+
+ // extract numbers for specific literal prefixes
+ const numbers = new Set<number>()
+ for (const name of keys) {
+ const m = generatedVarRE.exec(name)
+ if (m) numbers.add(Number(m[1]))
+ }
+ if (numbers.size === 0) return
+
+ this.globalId = getNextId((this.nextIdMap = buildNextIdMap(numbers)), 0)
+ }
reference(): number {
if (this.dynamic.id !== undefined) return this.dynamic.id
this.dynamic.flags |= DynamicFlag.REFERENCED
}
}
}
+
+/**
+ * Build a "next-id" map from an occupied number set.
+ * For each consecutive range [start..end], map every v in the range to end + 1.
+ * Example: input [0, 1, 2, 4] => { 0: 3, 1: 3, 2: 3, 4: 5 }.
+ */
+export function buildNextIdMap(nums: Iterable<number>): Map<number, number> {
+ const map: Map<number, number> = new Map()
+ const arr = Array.from(new Set(nums)).sort((a, b) => a - b)
+ if (arr.length === 0) return map
+
+ for (let i = 0; i < arr.length; i++) {
+ let start = arr[i]
+ let end = start
+ while (i + 1 < arr.length && arr[i + 1] === end + 1) {
+ i++
+ end = arr[i]
+ }
+ for (let v = start; v <= end; v++) map.set(v, end + 1)
+ }
+ return map
+}
+
+/**
+ * Return the available id for n using a map built by buildNextIdMap:
+ * - If n is not occupied, return n.
+ * - If n is occupied, return the mapped value
+ */
+export function getNextId(
+ map: Map<number, number> | null | undefined,
+ n: number,
+): number {
+ if (map && map.has(n)) return map.get(n)!
+ return n
+}