From c6ffa6ca136182cea011a0a98fa2f76f1da9b0ad Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Wed, 4 Nov 2026 19:28:38 +0100 Subject: [PATCH] fix: export JournalImageVariant from one auto-imported file - types.ts re-exported the interface srcset.ts declares, so every build of an app with the layer warned Duplicated imports "JournalImageVariant" - a test checks that every name of app/utils and app/composables has one home --- app/utils/types.ts | 2 -- tests/autoImports.test.ts | 67 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 tests/autoImports.test.ts diff --git a/app/utils/types.ts b/app/utils/types.ts index 41177a0..d046bf4 100644 --- a/app/utils/types.ts +++ b/app/utils/types.ts @@ -4,8 +4,6 @@ import type { JournalImageVariant } from './srcset.ts' -export type { JournalImageVariant } - /** The author object embedded in a post row. `null` when the post has none. */ export interface JournalAuthorRef { id: number diff --git a/tests/autoImports.test.ts b/tests/autoImports.test.ts new file mode 100644 index 0000000..98971ae --- /dev/null +++ b/tests/autoImports.test.ts @@ -0,0 +1,67 @@ +// Nuxt auto-imports every export of app/utils and app/composables. A name +// exported from two of those files is a "Duplicated imports" warning on +// every build of an app with the layer (an interface re-exported next to +// its declaration counts), so each name has one home. + +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { readdirSync, readFileSync } from 'node:fs' +import { join } from 'node:path' + +const root = new URL('..', import.meta.url).pathname + +/** The names a module exports: declarations and export lists. */ +function exportedNames(source: string): string[] { + const names: string[] = [] + const decl = /^export\s+(?:declare\s+)?(?:async\s+)?(?:interface|type|function\*?|const|let|var|class|enum)\s+([A-Za-z_$][\w$]*)/gm + for (const m of source.matchAll(decl)) names.push(m[1]!) + const list = /^export\s+(?:type\s+)?\{([^}]*)\}/gm + for (const m of source.matchAll(list)) { + for (const part of m[1]!.split(',')) { + const name = part.trim().split(/\s+as\s+/).pop()?.replace(/^type\s+/, '').trim() + if (name) names.push(name) + } + } + return names +} + +function autoImportedFiles(): string[] { + const out: string[] = [] + for (const dir of ['app/utils', 'app/composables']) { + for (const f of readdirSync(join(root, dir))) { + if (/\.(ts|mts|js|mjs)$/.test(f) && !f.endsWith('.d.ts')) out.push(join(dir, f)) + } + } + return out +} + +test('exportedNames reads declarations and export lists', () => { + assert.deepEqual( + exportedNames("export interface A {}\nexport type B = 1\nexport function c() {}\nexport const d = 1\nexport type { E }\nexport { f as g, type H }\n"), + ['A', 'B', 'c', 'd', 'E', 'g', 'H'], + ) +}) + +test('a name re-exported next to its declaration is a duplicate', () => { + const files = { + 'app/utils/srcset.ts': 'export interface JournalImageVariant { width: number }', + 'app/utils/types.ts': "import type { JournalImageVariant } from './srcset.ts'\nexport type { JournalImageVariant }", + } + const seen = new Map() + for (const [file, src] of Object.entries(files)) { + for (const name of exportedNames(src)) seen.set(name, [...(seen.get(name) ?? []), file]) + } + assert.deepEqual(seen.get('JournalImageVariant'), ['app/utils/srcset.ts', 'app/utils/types.ts']) +}) + +test('every auto-imported name is exported from one file only', () => { + const homes = new Map() + for (const file of autoImportedFiles()) { + for (const name of exportedNames(readFileSync(join(root, file), 'utf8'))) { + homes.set(name, [...(homes.get(name) ?? []), file]) + } + } + const duplicates = [...homes].filter(([, files]) => files.length > 1).map(([name, files]) => `${name}: ${files.join(', ')}`) + assert.deepEqual(duplicates, []) + assert.ok(homes.has('JournalImageVariant'), 'JournalImageVariant is still auto-imported') +})