Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 12 additions & 14 deletions packages/sdk/scripts/resolve-aliases.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand All @@ -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))
}
Expand All @@ -144,23 +146,21 @@ function fixDirectoryImports(code, filePath, cfg) {

// Fix static imports that don't have .js extension
let updated = code.replace(
/(from\s*["'])(\.\/[^"']*?)(?<!\.js|\.d\.ts)(["'])/g,
/(from\s*["'])(\.{1,2}\/[^"']*?)(?<!\.js|\.d\.ts)(["'])/g,
(match, prefix, importPath, suffix) => {
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
Expand All @@ -179,23 +179,21 @@ function fixDirectoryImports(code, filePath, cfg) {

// Fix dynamic imports that don't have .js extension
updated = updated.replace(
/(import\s*\(\s*["'])(\.\/[^"']*?)(?<!\.js|\.d\.ts)(["']\s*\))/g,
/(import\s*\(\s*["'])(\.{1,2}\/[^"']*?)(?<!\.js|\.d\.ts)(["']\s*\))/g,
(match, prefix, importPath, suffix) => {
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
Expand Down
142 changes: 142 additions & 0 deletions packages/sdk/test/unit/resolve-aliases.test.ts
Original file line number Diff line number Diff line change
@@ -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 })
}
})
Loading