(html: string, options?: LinkifyOptions)
| 168 | * @param options - Dependencies map and optional relative import resolver |
| 169 | */ |
| 170 | export function linkifyModuleSpecifiers(html: string, options?: LinkifyOptions): string { |
| 171 | const { dependencies, resolveRelative } = options ?? {} |
| 172 | |
| 173 | const getHref = (moduleSpecifier: string): string | null => { |
| 174 | const cleanSpec = moduleSpecifier.replace(/^['"]|['"]$/g, '').trim() |
| 175 | |
| 176 | // Try relative import resolution first |
| 177 | if (cleanSpec.startsWith('.') && resolveRelative) { |
| 178 | return resolveRelative(moduleSpecifier) |
| 179 | } |
| 180 | |
| 181 | // Not a relative import - check if it's an npm package |
| 182 | if (!isNpmPackage(moduleSpecifier)) { |
| 183 | return null |
| 184 | } |
| 185 | |
| 186 | const packageName = getPackageName(moduleSpecifier) |
| 187 | const dep = dependencies?.[packageName] |
| 188 | if (dep) { |
| 189 | // Link to code browser with resolved version |
| 190 | return `/package-code/${packageName}/v/${dep.version}` |
| 191 | } |
| 192 | // Fall back to package page if not a known dependency |
| 193 | return `/package/${packageName}` |
| 194 | } |
| 195 | |
| 196 | // Match: from keyword span followed by string span containing module specifier |
| 197 | // Pattern: <span style="...">from</span><span style="..."> 'module'</span> |
| 198 | let result = html.replace( |
| 199 | /(<span[^>]*> ?from<\/span>)(<span[^>]*>) (['"][^'"]+['"])<\/span>/g, |
| 200 | (match, fromSpan, stringSpanOpen, moduleSpecifier) => { |
| 201 | const href = getHref(moduleSpecifier) |
| 202 | if (!href) return match |
| 203 | return `${fromSpan}${stringSpanOpen} <a href="${href}" class="import-link">${moduleSpecifier}</a></span>` |
| 204 | }, |
| 205 | ) |
| 206 | |
| 207 | // Match: side-effect imports like `import 'package'` |
| 208 | // Pattern: <span>import</span><span> 'module'</span> |
| 209 | // But NOT: import ... from, import(, or import { |
| 210 | result = result.replace( |
| 211 | /(<span[^>]*>import<\/span>)(<span[^>]*>) (['"][^'"]+['"])<\/span>/g, |
| 212 | (match, importSpan, stringSpanOpen, moduleSpecifier) => { |
| 213 | const href = getHref(moduleSpecifier) |
| 214 | if (!href) return match |
| 215 | return `${importSpan}${stringSpanOpen} <a href="${href}" class="import-link">${moduleSpecifier}</a></span>` |
| 216 | }, |
| 217 | ) |
| 218 | |
| 219 | // Match: require( or import( followed by string |
| 220 | // Pattern: <span> require</span><span>(</span><span>'module'</span> |
| 221 | // or: <span>import</span><span>(</span><span>'module'</span> |
| 222 | // Note: require often has a leading space in the span from Shiki |
| 223 | result = result.replace( |
| 224 | /(<span[^>]*>)(\s*)(require|import)(<\/span>)(<span[^>]*>\(<\/span>)(<span[^>]*>)(['"][^'"]+['"])<\/span>/g, |
| 225 | ( |
| 226 | match, |
| 227 | spanOpen, |
no test coverage detected