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
This commit is contained in:
Jakub Zych
2026-11-04 19:28:38 +01:00
parent 1080a3f0c4
commit c6ffa6ca13
2 changed files with 67 additions and 2 deletions

View File

@@ -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

67
tests/autoImports.test.ts Normal file
View File

@@ -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<string, string[]>()
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<string, string[]>()
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')
})