From 16d800e1187b6d14cc22b505bcb757995a20e7f1 Mon Sep 17 00:00:00 2001 From: localhost41 Date: Tue, 21 Jul 2026 09:18:58 -0700 Subject: [PATCH] fix[notask]: emit NodeNext-compatible declaration specifiers --- packages/sdk/scripts/resolve-aliases.mjs | 26 ++-- .../sdk/test/unit/resolve-aliases.test.ts | 142 ++++++++++++++++++ 2 files changed, 154 insertions(+), 14 deletions(-) create mode 100644 packages/sdk/test/unit/resolve-aliases.test.ts diff --git a/packages/sdk/scripts/resolve-aliases.mjs b/packages/sdk/scripts/resolve-aliases.mjs index 92b26b9953..ed223d8d0c 100644 --- a/packages/sdk/scripts/resolve-aliases.mjs +++ b/packages/sdk/scripts/resolve-aliases.mjs @@ -118,9 +118,10 @@ function resolveAliasToRelative(specifier, fromFile, cfg) { const candidates = mapped.map((m) => toJsCandidate(distRoot, m, targetExtension)) const target = candidates.find((c) => fileExists(c)) || candidates[0] let rel = path.relative(path.dirname(fromFile), target) - // For .d.ts files, remove the .d.ts extension from the relative path + // Declaration files use runtime .js specifiers; TypeScript resolves them + // back to the sibling .d.ts files under NodeNext resolution. if (fromFile.endsWith('.d.ts') && rel.endsWith('.d.ts')) { - rel = rel.slice(0, -5) // Remove ".d.ts" + rel = `${rel.slice(0, -5)}.js` } return ensureDotSlash(normalizeToPosix(rel)) } @@ -130,9 +131,10 @@ function resolveAliasToRelative(specifier, fromFile, cfg) { const stripped = specifier.replace(/^@\//, '') const target = toJsCandidate(distRoot, `./${stripped}`, targetExtension) let rel = path.relative(path.dirname(fromFile), target) - // For .d.ts files, remove the .d.ts extension from the relative path + // Declaration files use runtime .js specifiers; TypeScript resolves them + // back to the sibling .d.ts files under NodeNext resolution. if (fromFile.endsWith('.d.ts') && rel.endsWith('.d.ts')) { - rel = rel.slice(0, -5) // Remove ".d.ts" + rel = `${rel.slice(0, -5)}.js` } return ensureDotSlash(normalizeToPosix(rel)) } @@ -144,23 +146,21 @@ function fixDirectoryImports(code, filePath, cfg) { // Fix static imports that don't have .js extension let updated = code.replace( - /(from\s*["'])(\.\/[^"']*?)(? { const resolvedPath = path.resolve(path.dirname(filePath), importPath) const indexFile = targetExtension === '.d.ts' ? 'index.d.ts' : 'index.js' // Check if this is a directory with an index file if (dirExists(resolvedPath) && fileExists(path.join(resolvedPath, indexFile))) { - const extension = targetExtension === '.d.ts' ? '' : '.js' - return `${prefix}${importPath}/index${extension}${suffix}` + return `${prefix}${importPath}/index.js${suffix}` } // Check if this is a file that needs .js extension const fileWithJs = `${resolvedPath}.js` const fileWithDts = `${resolvedPath}.d.ts` if (fileExists(fileWithJs) || fileExists(fileWithDts)) { - const extension = targetExtension === '.d.ts' ? '' : '.js' - return `${prefix}${importPath}${extension}${suffix}` + return `${prefix}${importPath}.js${suffix}` } return match @@ -179,23 +179,21 @@ function fixDirectoryImports(code, filePath, cfg) { // Fix dynamic imports that don't have .js extension updated = updated.replace( - /(import\s*\(\s*["'])(\.\/[^"']*?)(? { const resolvedPath = path.resolve(path.dirname(filePath), importPath) const indexFile = targetExtension === '.d.ts' ? 'index.d.ts' : 'index.js' // Check if this is a directory with an index file if (dirExists(resolvedPath) && fileExists(path.join(resolvedPath, indexFile))) { - const extension = targetExtension === '.d.ts' ? '' : '.js' - return `${prefix}${importPath}/index${extension}${suffix}` + return `${prefix}${importPath}/index.js${suffix}` } // Check if this is a file that needs .js extension const fileWithJs = `${resolvedPath}.js` const fileWithDts = `${resolvedPath}.d.ts` if (fileExists(fileWithJs) || fileExists(fileWithDts)) { - const extension = targetExtension === '.d.ts' ? '' : '.js' - return `${prefix}${importPath}${extension}${suffix}` + return `${prefix}${importPath}.js${suffix}` } return match diff --git a/packages/sdk/test/unit/resolve-aliases.test.ts b/packages/sdk/test/unit/resolve-aliases.test.ts new file mode 100644 index 0000000000..7ba30ffa64 --- /dev/null +++ b/packages/sdk/test/unit/resolve-aliases.test.ts @@ -0,0 +1,142 @@ +import assert from 'node:assert/strict' +import { spawnSync } from 'node:child_process' +import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join, resolve } from 'node:path' +import { test } from 'node:test' +import { fileURLToPath } from 'node:url' + +const sdkRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..') +const resolver = join(sdkRoot, 'scripts', 'resolve-aliases.mjs') +const tsc = join(sdkRoot, 'node_modules', 'typescript', 'bin', 'tsc') + +await test('writes NodeNext-compatible .js specifiers in declarations', () => { + const fixture = mkdtempSync(join(tmpdir(), 'qvac-resolve-aliases-')) + const packageRoot = join(fixture, 'node_modules', '@qvac', 'sdk') + + try { + mkdirSync(join(packageRoot, 'dist', 'models', 'registry'), { recursive: true }) + writeFileSync( + join(packageRoot, 'tsconfig.json'), + JSON.stringify({ + compilerOptions: { + baseUrl: '.', + outDir: 'dist', + paths: { '@/*': ['./*'] } + } + }) + ) + writeFileSync( + join(packageRoot, 'package.json'), + JSON.stringify({ + name: '@qvac/sdk', + type: 'module', + exports: { + '.': { types: './dist/index.d.ts', import: './dist/index.js' }, + './models': { + types: './dist/models/registry/index.d.ts', + import: './dist/models/registry/index.js' + } + } + }) + ) + writeFileSync( + join(packageRoot, 'dist', 'models', 'registry', 'models.d.ts'), + [ + 'export declare const LLAMA_3_2_1B_INST_Q4_0: string', + 'export declare const GTE_LARGE_FP16: string', + 'export declare const BCI_EMBEDDER: string' + ].join('\n') + ) + writeFileSync( + join(packageRoot, 'dist', 'models', 'registry', 'index.d.ts'), + "export * from './models'\n" + ) + writeFileSync(join(packageRoot, 'dist', 'index.d.ts'), "export * from '@/models/registry'\n") + writeFileSync( + join(packageRoot, 'dist', 'external-check.d.ts'), + "export type { External } from '@scope/external/subpath'\n" + ) + mkdirSync(join(packageRoot, 'dist', 'nested')) + writeFileSync( + join(packageRoot, 'dist', 'nested', 'types.d.ts'), + [ + "export type { BCI_EMBEDDER } from '../models/registry/models'", + "export type Model = typeof import('../models/registry/models').BCI_EMBEDDER" + ].join('\n') + ) + writeFileSync(join(packageRoot, 'dist', 'index.js'), "export * from './models/registry'\n") + writeFileSync( + join(packageRoot, 'dist', 'models', 'registry', 'index.js'), + "export * from './models'\n" + ) + writeFileSync( + join(packageRoot, 'dist', 'models', 'registry', 'models.js'), + [ + "export const LLAMA_3_2_1B_INST_Q4_0 = 'llama'", + "export const GTE_LARGE_FP16 = 'gte'", + "export const BCI_EMBEDDER = 'bci'" + ].join('\n') + ) + + const result = spawnSync(process.execPath, [resolver], { + cwd: packageRoot, + encoding: 'utf8' + }) + + assert.equal(result.status, 0, result.stderr) + assert.equal( + readFileSync(join(packageRoot, 'dist', 'index.d.ts'), 'utf8'), + "export * from './models/registry/index.js'\n" + ) + assert.equal( + readFileSync(join(packageRoot, 'dist', 'models', 'registry', 'index.d.ts'), 'utf8'), + "export * from './models.js'\n" + ) + assert.equal( + readFileSync(join(packageRoot, 'dist', 'external-check.d.ts'), 'utf8'), + "export type { External } from '@scope/external/subpath'\n" + ) + assert.equal( + readFileSync(join(packageRoot, 'dist', 'nested', 'types.d.ts'), 'utf8'), + [ + "export type { BCI_EMBEDDER } from '../models/registry/models.js'", + "export type Model = typeof import('../models/registry/models.js').BCI_EMBEDDER" + ].join('\n') + ) + assert.equal( + readFileSync(join(packageRoot, 'dist', 'index.js'), 'utf8'), + "export * from './models/registry/index.js'\n" + ) + + writeFileSync(join(fixture, 'package.json'), JSON.stringify({ type: 'module' })) + writeFileSync( + join(fixture, 'consumer.ts'), + [ + "import { BCI_EMBEDDER } from '@qvac/sdk'", + "import { GTE_LARGE_FP16, LLAMA_3_2_1B_INST_Q4_0 } from '@qvac/sdk/models'", + 'void [BCI_EMBEDDER, GTE_LARGE_FP16, LLAMA_3_2_1B_INST_Q4_0]' + ].join('\n') + ) + writeFileSync( + join(fixture, 'tsconfig.json'), + JSON.stringify({ + compilerOptions: { + module: 'NodeNext', + moduleResolution: 'NodeNext', + noEmit: true, + strict: true + }, + include: ['consumer.ts'] + }) + ) + + const typecheck = spawnSync(process.execPath, [tsc, '-p', 'tsconfig.json'], { + cwd: fixture, + encoding: 'utf8' + }) + assert.equal(typecheck.status, 0, typecheck.stderr || typecheck.stdout) + } finally { + rmSync(fixture, { recursive: true, force: true }) + } +})