-
-
Notifications
You must be signed in to change notification settings - Fork 343
fix: correct code link for alias #2056
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c25cafc
83503cc
12ee36d
9ac82cc
5099d3b
e6a8d16
a2859c1
cfdc81f
91bfb34
a6ad3f9
8c29a0e
dd546ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -101,6 +101,37 @@ function getExtensionPriority(sourceFile: string): string[][] { | |
| return [[], ['.ts', '.js'], ['.d.ts'], ['.json']] | ||
| } | ||
|
|
||
| /** | ||
| * Resolve an alias specifier to the directory path within a file path. | ||
| * Supports #, ~, and @ prefixes (e.g. #app, ~/app, @/app). | ||
| * The alias must match a path segment exactly (no partial matches). | ||
| */ | ||
| export function resolveAliasToDir(aliasSpec: string, filePath?: string | null): string | null { | ||
| if ( | ||
| (!aliasSpec.startsWith('#') && !aliasSpec.startsWith('~') && !aliasSpec.startsWith('@')) || | ||
| !filePath | ||
| ) { | ||
| return null | ||
| } | ||
|
|
||
| // Support #app, #/app, ~app, ~/app, @app, @/app | ||
| const alias = aliasSpec.replace(/^[#~@]\/?/, '') | ||
| const segments = filePath.split('/') | ||
|
|
||
| let lastMatchIndex = -1 | ||
| for (let i = 0; i < segments.length; i++) { | ||
| if (segments[i] === alias) { | ||
| lastMatchIndex = i | ||
| } | ||
| } | ||
|
|
||
| if (lastMatchIndex === -1) { | ||
| return null | ||
| } | ||
|
|
||
| return segments.slice(0, lastMatchIndex + 1).join('/') | ||
| } | ||
|
|
||
| /** | ||
| * Get index file extensions to try for directory imports. | ||
| */ | ||
|
|
@@ -131,6 +162,10 @@ export interface ResolvedImport { | |
| path: string | ||
| } | ||
|
|
||
| export type InternalImportTarget = string | { default?: string; import?: string } | null | undefined | ||
|
|
||
| export type InternalImportsMap = Record<string, InternalImportTarget> | ||
|
|
||
| /** | ||
| * Resolve a relative import specifier to an actual file path. | ||
| * | ||
|
|
@@ -198,6 +233,129 @@ export function resolveRelativeImport( | |
| return null | ||
| } | ||
|
|
||
| function normalizeInternalImportTarget(target: InternalImportTarget): string | null { | ||
| if (typeof target === 'string') { | ||
| return target | ||
| } | ||
|
|
||
| if (target && typeof target === 'object') { | ||
| if (typeof target.import === 'string') { | ||
| return target.import | ||
| } | ||
|
|
||
| if (typeof target.default === 'string') { | ||
| return target.default | ||
| } | ||
| } | ||
|
|
||
| return null | ||
| } | ||
|
|
||
| function normalizeAliasPrefix(value: string): string { | ||
| return value.replace(/^([#~@])\//, '$1') | ||
| } | ||
|
|
||
| function guessInternalImportTarget( | ||
| imports: InternalImportsMap, | ||
| specifier: string, | ||
| files: FileSet, | ||
| currentFile: string, | ||
| ): string | null { | ||
| const normalizedSpecifier = normalizeAliasPrefix(specifier) | ||
|
|
||
| for (const [key, value] of Object.entries(imports)) { | ||
| const normalizedKey = normalizeAliasPrefix(key) | ||
| if ( | ||
| normalizedSpecifier === normalizedKey || | ||
| normalizedSpecifier.startsWith(`${normalizedKey}/`) | ||
| ) { | ||
| const basePath = resolveAliasToDir(key, normalizeInternalImportTarget(value)) | ||
| if (!basePath) continue | ||
|
|
||
| const suffix = normalizedSpecifier.slice(normalizedKey.length).replace(/^\//, '') | ||
| const pathWithoutExt = suffix ? `${basePath}/${suffix}` : basePath | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const toCheckPath = (p: string) => files.has(normalizePath(p)) || files.has(p) | ||
|
|
||
| // Path already has an extension-like suffix on the last segment - return as is if exists | ||
| const filename = pathWithoutExt.split('/').pop() ?? '' | ||
| if (filename.includes('.') && !filename.endsWith('.')) { | ||
| if (toCheckPath(pathWithoutExt)) { | ||
| return pathWithoutExt.startsWith('./') ? pathWithoutExt : `./${pathWithoutExt}` | ||
| } | ||
| return null | ||
| } | ||
|
|
||
| // Try adding extensions based on currentFile type | ||
| const extensionGroups = getExtensionPriority(currentFile) | ||
| for (const extensions of extensionGroups) { | ||
| if (extensions.length === 0) { | ||
| if (toCheckPath(pathWithoutExt)) { | ||
| return pathWithoutExt.startsWith('./') ? pathWithoutExt : `./${pathWithoutExt}` | ||
| } | ||
| } else { | ||
| for (const ext of extensions) { | ||
| const pathWithExt = pathWithoutExt + ext | ||
| if (toCheckPath(pathWithExt)) { | ||
| return pathWithExt.startsWith('./') ? pathWithExt : `./${pathWithExt}` | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Try as directory with index file | ||
| for (const indexFile of getIndexExtensions(currentFile)) { | ||
| const indexPath = `${pathWithoutExt}/${indexFile}` | ||
| if (toCheckPath(indexPath)) { | ||
| return indexPath.startsWith('./') ? indexPath : `./${indexPath}` | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return null | ||
| } | ||
|
|
||
| /** | ||
| * import ... from '#components/Button.vue' | ||
| * import ... from '#/components/Button.vue' | ||
| * import ... from '~/components/Button.vue' | ||
| * import ... from '~components/Button.vue' | ||
| */ | ||
| export function resolveInternalImport( | ||
| specifier: string, | ||
| currentFile: string, | ||
| imports: InternalImportsMap | undefined, | ||
| files: FileSet, | ||
| ): ResolvedImport | null { | ||
| const cleanSpecifier = specifier.replace(/^['"]|['"]$/g, '').trim() | ||
|
|
||
| if ( | ||
| (!cleanSpecifier.startsWith('#') && | ||
| !cleanSpecifier.startsWith('~') && | ||
| !cleanSpecifier.startsWith('@')) || | ||
| !imports | ||
| ) { | ||
| return null | ||
| } | ||
|
|
||
| const importTarget = normalizeInternalImportTarget(imports[cleanSpecifier]) | ||
| const target = | ||
| importTarget != null | ||
| ? importTarget | ||
| : guessInternalImportTarget(imports, cleanSpecifier, files, currentFile) | ||
|
|
||
| if (!target || !target.startsWith('./')) { | ||
| return null | ||
| } | ||
|
|
||
| const path = normalizePath(target) | ||
| if (!path || path.startsWith('..') || !files.has(path)) { | ||
| return null | ||
|
Comment on lines
+341
to
+353
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add extension/index fallback for exact internal targets. Exact Proposed fix const path = normalizePath(target)
- if (!path || path.startsWith('..') || !files.has(path)) {
+ if (!path || path.startsWith('..')) {
return null
}
- return { path }
+ if (files.has(path)) {
+ return { path }
+ }
+
+ for (const extensions of getExtensionPriority(currentFile)) {
+ for (const ext of extensions) {
+ const candidate = `${path}${ext}`
+ if (files.has(candidate)) {
+ return { path: candidate }
+ }
+ }
+ }
+
+ for (const indexFile of getIndexExtensions(currentFile)) {
+ const candidate = `${path}/${indexFile}`
+ if (files.has(candidate)) {
+ return { path: candidate }
+ }
+ }
+
+ return null
} |
||
| } | ||
|
|
||
| return { path } | ||
| } | ||
|
|
||
| /** | ||
| * Create a resolver function bound to a specific file tree and current file. | ||
| */ | ||
|
|
@@ -206,9 +364,13 @@ export function createImportResolver( | |
| currentFile: string, | ||
| packageName: string, | ||
| version: string, | ||
| internalImports?: InternalImportsMap, | ||
| ): (specifier: string) => string | null { | ||
| return (specifier: string) => { | ||
| const resolved = resolveRelativeImport(specifier, currentFile, files) | ||
| const relativeResolved = resolveRelativeImport(specifier, currentFile, files) | ||
| const internalResolved = resolveInternalImport(specifier, currentFile, internalImports, files) | ||
| const resolved = relativeResolved != null ? relativeResolved : internalResolved | ||
|
|
||
| if (resolved) { | ||
| return `/package-code/${packageName}/v/${version}/${resolved.path}` | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle empty alias roots (
@,~,#) explicitly.After normalisation, aliases like
@/~/become empty, and this segment-based lookup can returnnullfor valid targets such as./src(no empty segment), so aliased links still fail in common setups.Proposed fix
export function resolveAliasToDir(aliasSpec: string, filePath?: string | null): string | null { if ( (!aliasSpec.startsWith('#') && !aliasSpec.startsWith('~') && !aliasSpec.startsWith('@')) || !filePath ) { return null } // Support `#app`, `#/app`, ~app, ~/app, `@app`, `@/app` const alias = aliasSpec.replace(/^[#~@]\/?/, '') + const normalizedFilePath = filePath.replace(/\/+$/, '') + if (!normalizedFilePath) { + return null + } + if (alias === '') { + return normalizedFilePath + } - const segments = filePath.split('/') + const segments = normalizedFilePath.split('/')