* fix: unify extension discovery between loader.ts and resource-loader.ts Extract shared extension discovery logic (resolveExtensionEntries, discoverExtensionEntryPaths) into extension-discovery.ts. Both loader.ts and resource-loader.ts now use the same algorithm, which supports package.json pi.extensions declarations in addition to index.ts/index.js fallback. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update test to match refactored extension discovery imports Test checked for readdirSync which was replaced by discoverExtensionEntryPaths. Updated import path from resource-loader.ts to extension-discovery.ts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
75 lines
2.3 KiB
TypeScript
75 lines
2.3 KiB
TypeScript
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
import { join, resolve } from 'node:path'
|
|
|
|
function isExtensionFile(name: string): boolean {
|
|
return name.endsWith('.ts') || name.endsWith('.js')
|
|
}
|
|
|
|
/**
|
|
* Resolves the entry-point file(s) for a single extension directory.
|
|
*
|
|
* 1. If the directory contains a package.json with a `pi.extensions` array,
|
|
* each entry is resolved relative to the directory and returned (if it exists).
|
|
* 2. Otherwise falls back to `index.ts` → `index.js`.
|
|
*/
|
|
export function resolveExtensionEntries(dir: string): string[] {
|
|
const packageJsonPath = join(dir, 'package.json')
|
|
if (existsSync(packageJsonPath)) {
|
|
try {
|
|
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
|
|
const declared = pkg?.pi?.extensions
|
|
if (Array.isArray(declared)) {
|
|
const resolved = declared
|
|
.filter((entry: unknown): entry is string => typeof entry === 'string')
|
|
.map((entry: string) => resolve(dir, entry))
|
|
.filter((entry: string) => existsSync(entry))
|
|
if (resolved.length > 0) {
|
|
return resolved
|
|
}
|
|
}
|
|
} catch {
|
|
// Ignore malformed manifests and fall back to index.ts/index.js discovery.
|
|
}
|
|
}
|
|
|
|
const indexTs = join(dir, 'index.ts')
|
|
if (existsSync(indexTs)) {
|
|
return [indexTs]
|
|
}
|
|
|
|
const indexJs = join(dir, 'index.js')
|
|
if (existsSync(indexJs)) {
|
|
return [indexJs]
|
|
}
|
|
|
|
return []
|
|
}
|
|
|
|
/**
|
|
* Discovers all extension entry-point paths under an extensions directory.
|
|
*
|
|
* - Top-level .ts/.js files are treated as standalone extension entry points.
|
|
* - Subdirectories are resolved via `resolveExtensionEntries()` (package.json →
|
|
* pi.extensions, then index.ts/index.js fallback).
|
|
*/
|
|
export function discoverExtensionEntryPaths(extensionsDir: string): string[] {
|
|
if (!existsSync(extensionsDir)) {
|
|
return []
|
|
}
|
|
|
|
const discovered: string[] = []
|
|
for (const entry of readdirSync(extensionsDir, { withFileTypes: true })) {
|
|
const entryPath = join(extensionsDir, entry.name)
|
|
|
|
if ((entry.isFile() || entry.isSymbolicLink()) && isExtensionFile(entry.name)) {
|
|
discovered.push(entryPath)
|
|
continue
|
|
}
|
|
|
|
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
discovered.push(...resolveExtensionEntries(entryPath))
|
|
}
|
|
}
|
|
|
|
return discovered
|
|
}
|