diff --git a/code/builders/builder-vite/src/codegen-project-annotations.test.ts b/code/builders/builder-vite/src/codegen-project-annotations.test.ts new file mode 100644 index 000000000000..46b9d5d8a39e --- /dev/null +++ b/code/builders/builder-vite/src/codegen-project-annotations.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest'; + +import { generateProjectAnnotationsCodeFromPreviews } from './codegen-project-annotations'; + +describe('generateProjectAnnotationsCodeFromPreviews', () => { + it('keeps generated preview import identifiers unique when aliases collide', () => { + const result = generateProjectAnnotationsCodeFromPreviews({ + // These paths previously produced the same generated `preview_` identifier. + previewAnnotations: ['/virtual/path/0000r/preview.js', '/virtual/path/00020/preview.js'], + projectRoot: '/virtual', + frameworkName: 'frameworkName', + isCsf4: false, + }); + + const importVariables = [...result.matchAll(/import \* as (\w+) from/g)].map( + (match) => match[1] + ); + + expect(importVariables).toHaveLength(2); + expect(new Set(importVariables).size).toBe(importVariables.length); + expect(result).toContain(`hmrPreviewAnnotationModules[0] ?? ${importVariables[0]}`); + expect(result).toContain(`hmrPreviewAnnotationModules[1] ?? ${importVariables[1]}`); + }); + + it('suffixes repeated collisions deterministically across more than two previews', () => { + const result = generateProjectAnnotationsCodeFromPreviews({ + // These paths all collide under the current djb2 hash, so the fallback suffixing stays active. + previewAnnotations: [ + '/virtual/path/0000r/preview.js', + '/virtual/path/00020/preview.js', + '/virtual/path/0001Q/preview.js', + ], + projectRoot: '/virtual', + frameworkName: 'frameworkName', + isCsf4: false, + }); + + const importVariables = [...result.matchAll(/import \* as (\w+) from/g)].map( + (match) => match[1] + ); + + expect(importVariables[0]).toMatch(/^[A-Za-z_$][A-Za-z0-9_$]*$/); + expect(importVariables).toEqual([ + importVariables[0], + `${importVariables[0]}_2`, + `${importVariables[0]}_3`, + ]); + expect(result).toContain(`hmrPreviewAnnotationModules[2] ?? ${importVariables[2]}`); + }); +}); diff --git a/code/builders/builder-vite/src/codegen-project-annotations.ts b/code/builders/builder-vite/src/codegen-project-annotations.ts index 1f5b5192145d..7d2e9a585c9e 100644 --- a/code/builders/builder-vite/src/codegen-project-annotations.ts +++ b/code/builders/builder-vite/src/codegen-project-annotations.ts @@ -44,11 +44,13 @@ export function generateProjectAnnotationsCodeFromPreviews(options: { const variables: string[] = []; const imports: string[] = []; + const usedVariables = new Set(); for (const previewAnnotation of previewAnnotationURLs) { - const variable = + const baseVariable = genSafeVariableName(filename(previewAnnotation)).replace(/_(45|46|47)/g, '_') + '_' + hash(previewAnnotation); + const variable = getUniqueImportVariable(baseVariable, usedVariables); variables.push(variable); imports.push(genImport(previewAnnotation, { name: '*', as: variable })); } @@ -105,7 +107,20 @@ export function generateProjectAnnotationsCodeFromPreviews(options: { `.trim(); } -/** djb2 hash — http://www.cse.yorku.ca/~oz/hash.html */ +function getUniqueImportVariable(baseVariable: string, usedVariables: Set) { + let variable = baseVariable; + let duplicateCount = 2; + + while (usedVariables.has(variable)) { + variable = `${baseVariable}_${duplicateCount}`; + duplicateCount += 1; + } + + usedVariables.add(variable); + return variable; +} + +/** djb2 hash - http://www.cse.yorku.ca/~oz/hash.html */ function hash(value: string) { let acc = 5381; for (let i = 0; i < value.length; i++) {