Skip to content

Commit 709ef98

Browse files
evantahlerclaude
andcommitted
feat: mode-aware component autoloader for built (.js) vs source (.ts)
Phase 2 of dual-runtime support. globLoader and API.loadLocalConfig now match both TypeScript sources (.ts/.tsx — the dev/source layout) and compiled JavaScript (.js/.mjs/.cjs — the built dist/ layout a plain-Node consumer runs), skip declaration files (.d.ts), and dedupe by basename so a stray compiled .js next to its .ts source can't double-instantiate. In dev only .ts is present, so loading order/behavior is unchanged: full suite (54 files / 777 tests) still passes on Bun. Asset + scaffold-source path resolution (packageRoot/scaffoldSourceRoot) is deferred to the build-step PR, where it can be validated against the real tsup output layout. Part of #488 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 83c66d4 commit 709ef98

2 files changed

Lines changed: 24 additions & 6 deletions

File tree

packages/keryx/classes/API.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,14 @@ export class API {
198198
const configDir = path.join(this.rootDir, "config");
199199
if (!fs.existsSync(configDir)) return;
200200

201-
for (const file of await glob("**/*.ts", configDir)) {
202-
if (file.startsWith(".")) continue;
201+
const seen = new Set<string>();
202+
for (const file of await glob("**/*.{ts,tsx,js,mjs,cjs}", configDir)) {
203+
if (file.startsWith(".") || file.endsWith(".d.ts")) continue;
204+
205+
// Collapse `foo.ts` / `foo.js` to a single load per basename.
206+
const base = file.replace(/\.(ts|tsx|js|mjs|cjs)$/, "");
207+
if (seen.has(base)) continue;
208+
seen.add(base);
203209

204210
const fullPath = path.join(configDir, file);
205211
const mod = await import(fullPath);

packages/keryx/util/glob.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@ import { ErrorType, TypedError } from "../classes/TypedError";
44
import { glob } from "./runtime";
55

66
/**
7-
* Auto-discover and instantiate all exported classes from `.ts`/`.tsx` files in a directory.
8-
* Files prefixed with `.` are skipped. Used to load actions, initializers, and servers.
7+
* Auto-discover and instantiate all exported classes from source modules in a
8+
* directory. Used to load actions, initializers, and servers.
9+
*
10+
* Matches both TypeScript sources (`.ts`/`.tsx`, the dev/source layout under Bun
11+
* or Node + a TS loader) and compiled JavaScript (`.js`/`.mjs`/`.cjs`, the
12+
* built `dist/` layout a plain-Node consumer runs). Declaration files (`.d.ts`)
13+
* and dotfiles are skipped, and at most one module is loaded per basename so a
14+
* stray compiled `.js` sitting next to its `.ts` source can't double-instantiate.
915
*
1016
* @param searchDir - Absolute path or relative path (resolved from `api.rootDir`) to scan.
1117
* @returns Array of instantiated class instances of type `T`.
@@ -17,8 +23,14 @@ export async function globLoader<T>(searchDir: string) {
1723
? searchDir
1824
: path.join(api.rootDir, searchDir);
1925

20-
for (const file of await glob("**/*.{ts,tsx}", dir)) {
21-
if (file.startsWith(".")) continue;
26+
const seen = new Set<string>();
27+
for (const file of await glob("**/*.{ts,tsx,js,mjs,cjs}", dir)) {
28+
if (file.startsWith(".") || file.endsWith(".d.ts")) continue;
29+
30+
// Collapse `foo.ts` / `foo.js` to a single load per basename.
31+
const base = file.replace(/\.(ts|tsx|js|mjs|cjs)$/, "");
32+
if (seen.has(base)) continue;
33+
seen.add(base);
2234

2335
const fullPath = path.join(dir, file);
2436
const modules = (await import(fullPath)) as Record<string, unknown>;

0 commit comments

Comments
 (0)