singularity-forge/src/extension-discovery.ts
TÂCHES 89ee5e439a fix: unify extension discovery logic (#995)
* 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>
2026-03-17 17:14:04 -06:00

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
}