Skip to content

Commit b725f84

Browse files
committed
feat: add BaseLoader
1 parent 1823548 commit b725f84

File tree

7 files changed

+895
-251
lines changed

7 files changed

+895
-251
lines changed

src/loaders/BaseLoader.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { join, dirname } from 'path';
2+
import { existsSync } from 'fs';
3+
import { logger } from '../core/Logger.js';
4+
import { discoverFilesRecursively, hasValidFiles } from '../utils/fileDiscovery.js';
5+
6+
export interface LoaderConfig {
7+
subdirectory: string;
8+
excludedFiles: string[];
9+
extensions: string[];
10+
}
11+
12+
export abstract class BaseLoader<T> {
13+
protected readonly directory: string;
14+
protected readonly config: LoaderConfig;
15+
16+
constructor(config: LoaderConfig, basePath?: string) {
17+
this.config = config;
18+
this.directory = this.resolveDirectory(config.subdirectory, basePath);
19+
}
20+
21+
private resolveDirectory(subdirectory: string, basePath?: string): string {
22+
if (basePath) {
23+
const dir = join(basePath, subdirectory);
24+
logger.debug(`Using provided base path for ${subdirectory}: ${dir}`);
25+
return dir;
26+
}
27+
28+
const projectRoot = process.cwd();
29+
const distPath = join(projectRoot, 'dist', subdirectory);
30+
31+
if (existsSync(distPath)) {
32+
logger.debug(`Using project's dist/${subdirectory} directory: ${distPath}`);
33+
return distPath;
34+
}
35+
36+
const mainModulePath = process.argv[1];
37+
const moduleDir = dirname(mainModulePath);
38+
39+
const dir = moduleDir.endsWith('dist')
40+
? join(moduleDir, subdirectory)
41+
: join(moduleDir, 'dist', subdirectory);
42+
43+
logger.debug(`Using module path for ${subdirectory}: ${dir}`);
44+
return dir;
45+
}
46+
47+
async hasItems(): Promise<boolean> {
48+
try {
49+
return await hasValidFiles(this.directory, {
50+
extensions: this.config.extensions,
51+
excludePatterns: this.config.excludedFiles,
52+
});
53+
} catch (error) {
54+
logger.debug(`No ${this.config.subdirectory} directory found: ${(error as Error).message}`);
55+
return false;
56+
}
57+
}
58+
59+
protected abstract validateItem(item: any): item is T;
60+
protected abstract createInstance(ItemClass: any): T;
61+
protected abstract getItemName(item: T): string;
62+
63+
async loadItems(): Promise<T[]> {
64+
try {
65+
logger.debug(`Attempting to load ${this.config.subdirectory} from: ${this.directory}`);
66+
67+
const files = await discoverFilesRecursively(this.directory, {
68+
extensions: this.config.extensions,
69+
excludePatterns: this.config.excludedFiles,
70+
});
71+
72+
if (files.length === 0) {
73+
logger.debug(`No ${this.config.subdirectory} files found`);
74+
return [];
75+
}
76+
77+
logger.debug(`Found ${this.config.subdirectory} files: ${files.join(', ')}`);
78+
79+
const items: T[] = [];
80+
81+
for (const file of files) {
82+
try {
83+
const fullPath = join(this.directory, file);
84+
logger.debug(
85+
`Attempting to load ${this.config.subdirectory.slice(0, -1)} from: ${fullPath}`
86+
);
87+
88+
const importPath = `file://${fullPath}`;
89+
const module = await import(importPath);
90+
91+
let ItemClass = null;
92+
93+
if (module.default && typeof module.default === 'function') {
94+
ItemClass = module.default;
95+
} else if (typeof module === 'function') {
96+
ItemClass = module;
97+
} else {
98+
const keys = Object.keys(module).filter((key) => key !== 'default');
99+
for (const key of keys) {
100+
const exportValue = module[key];
101+
if (typeof exportValue === 'function') {
102+
ItemClass = exportValue;
103+
break;
104+
}
105+
}
106+
}
107+
108+
if (!ItemClass) {
109+
logger.warn(`No valid export found in ${file}`);
110+
continue;
111+
}
112+
113+
const item = this.createInstance(ItemClass);
114+
if (this.validateItem(item)) {
115+
items.push(item);
116+
}
117+
} catch (error) {
118+
logger.error(
119+
`Error loading ${this.config.subdirectory.slice(0, -1)} ${file}: ${(error as Error).message}`
120+
);
121+
}
122+
}
123+
124+
logger.debug(
125+
`Successfully loaded ${items.length} ${this.config.subdirectory}: ${items.map(this.getItemName).join(', ')}`
126+
);
127+
return items;
128+
} catch (error) {
129+
logger.error(`Failed to load ${this.config.subdirectory}: ${(error as Error).message}`);
130+
return [];
131+
}
132+
}
133+
}

src/loaders/promptLoader.ts

Lines changed: 20 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,24 @@
11
import { PromptProtocol } from '../prompts/BasePrompt.js';
2-
import { join, dirname } from 'path';
3-
import { promises as fs } from 'fs';
2+
import { BaseLoader } from './BaseLoader.js';
43
import { logger } from '../core/Logger.js';
5-
import { discoverFilesRecursively, hasValidFiles } from '../utils/fileDiscovery.js';
6-
7-
export class PromptLoader {
8-
private readonly PROMPTS_DIR: string;
9-
private readonly EXCLUDED_FILES = ['BasePrompt.js', '*.test.js', '*.spec.js'];
104

5+
export class PromptLoader extends BaseLoader<PromptProtocol> {
116
constructor(basePath?: string) {
12-
if (basePath) {
13-
// If basePath is provided, it should be the directory containing the prompts folder
14-
this.PROMPTS_DIR = join(basePath, 'prompts');
15-
} else {
16-
// For backwards compatibility, use the old behavior with process.argv[1]
17-
const mainModulePath = process.argv[1];
18-
this.PROMPTS_DIR = join(dirname(mainModulePath), 'prompts');
19-
}
20-
logger.debug(`Initialized PromptLoader with directory: ${this.PROMPTS_DIR}`);
7+
super(
8+
{
9+
subdirectory: 'prompts',
10+
excludedFiles: ['BasePrompt.js', '*.test.js', '*.spec.js'],
11+
extensions: ['.js'],
12+
},
13+
basePath
14+
);
2115
}
2216

2317
async hasPrompts(): Promise<boolean> {
24-
try {
25-
return await hasValidFiles(this.PROMPTS_DIR, {
26-
extensions: ['.js'],
27-
excludePatterns: this.EXCLUDED_FILES,
28-
});
29-
} catch (error) {
30-
logger.debug(`No prompts directory found: ${(error as Error).message}`);
31-
return false;
32-
}
18+
return this.hasItems();
3319
}
3420

35-
private validatePrompt(prompt: any): prompt is PromptProtocol {
21+
protected validateItem(prompt: any): prompt is PromptProtocol {
3622
const isValid = Boolean(
3723
prompt &&
3824
typeof prompt.name === 'string' &&
@@ -49,53 +35,15 @@ export class PromptLoader {
4935
return isValid;
5036
}
5137

52-
async loadPrompts(): Promise<PromptProtocol[]> {
53-
try {
54-
logger.debug(`Attempting to load prompts from: ${this.PROMPTS_DIR}`);
55-
56-
const promptFiles = await discoverFilesRecursively(this.PROMPTS_DIR, {
57-
extensions: ['.js'],
58-
excludePatterns: this.EXCLUDED_FILES,
59-
});
60-
61-
if (promptFiles.length === 0) {
62-
logger.debug('No prompt files found');
63-
return [];
64-
}
65-
66-
logger.debug(`Found prompt files: ${promptFiles.join(', ')}`);
67-
68-
const prompts: PromptProtocol[] = [];
69-
70-
for (const file of promptFiles) {
71-
try {
72-
const fullPath = join(this.PROMPTS_DIR, file);
73-
logger.debug(`Attempting to load prompt from: ${fullPath}`);
74-
75-
const importPath = `file://${fullPath}`;
76-
const { default: PromptClass } = await import(importPath);
77-
78-
if (!PromptClass) {
79-
logger.warn(`No default export found in ${file}`);
80-
continue;
81-
}
38+
protected createInstance(PromptClass: any): PromptProtocol {
39+
return new PromptClass();
40+
}
8241

83-
const prompt = new PromptClass();
84-
if (this.validatePrompt(prompt)) {
85-
prompts.push(prompt);
86-
}
87-
} catch (error) {
88-
logger.error(`Error loading prompt ${file}: ${(error as Error).message}`);
89-
}
90-
}
42+
protected getItemName(prompt: PromptProtocol): string {
43+
return prompt.name;
44+
}
9145

92-
logger.debug(
93-
`Successfully loaded ${prompts.length} prompts: ${prompts.map((p) => p.name).join(', ')}`
94-
);
95-
return prompts;
96-
} catch (error) {
97-
logger.error(`Failed to load prompts: ${(error as Error).message}`);
98-
return [];
99-
}
46+
async loadPrompts(): Promise<PromptProtocol[]> {
47+
return this.loadItems();
10048
}
10149
}

src/loaders/resourceLoader.ts

Lines changed: 20 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,24 @@
11
import { ResourceProtocol } from '../resources/BaseResource.js';
2-
import { join, dirname } from 'path';
3-
import { promises as fs } from 'fs';
2+
import { BaseLoader } from './BaseLoader.js';
43
import { logger } from '../core/Logger.js';
5-
import { discoverFilesRecursively, hasValidFiles } from '../utils/fileDiscovery.js';
6-
7-
export class ResourceLoader {
8-
private readonly RESOURCES_DIR: string;
9-
private readonly EXCLUDED_FILES = ['BaseResource.js', '*.test.js', '*.spec.js'];
104

5+
export class ResourceLoader extends BaseLoader<ResourceProtocol> {
116
constructor(basePath?: string) {
12-
if (basePath) {
13-
// If basePath is provided, it should be the directory containing the resources folder
14-
this.RESOURCES_DIR = join(basePath, 'resources');
15-
} else {
16-
// For backwards compatibility, use the old behavior with process.argv[1]
17-
const mainModulePath = process.argv[1];
18-
this.RESOURCES_DIR = join(dirname(mainModulePath), 'resources');
19-
}
20-
logger.debug(`Initialized ResourceLoader with directory: ${this.RESOURCES_DIR}`);
7+
super(
8+
{
9+
subdirectory: 'resources',
10+
excludedFiles: ['BaseResource.js', '*.test.js', '*.spec.js'],
11+
extensions: ['.js'],
12+
},
13+
basePath
14+
);
2115
}
2216

2317
async hasResources(): Promise<boolean> {
24-
try {
25-
return await hasValidFiles(this.RESOURCES_DIR, {
26-
extensions: ['.js'],
27-
excludePatterns: this.EXCLUDED_FILES,
28-
});
29-
} catch (error) {
30-
logger.debug(`No resources directory found: ${(error as Error).message}`);
31-
return false;
32-
}
18+
return this.hasItems();
3319
}
3420

35-
private validateResource(resource: any): resource is ResourceProtocol {
21+
protected validateItem(resource: any): resource is ResourceProtocol {
3622
const isValid = Boolean(
3723
resource &&
3824
typeof resource.uri === 'string' &&
@@ -50,53 +36,15 @@ export class ResourceLoader {
5036
return isValid;
5137
}
5238

53-
async loadResources(): Promise<ResourceProtocol[]> {
54-
try {
55-
logger.debug(`Attempting to load resources from: ${this.RESOURCES_DIR}`);
56-
57-
const resourceFiles = await discoverFilesRecursively(this.RESOURCES_DIR, {
58-
extensions: ['.js'],
59-
excludePatterns: this.EXCLUDED_FILES,
60-
});
61-
62-
if (resourceFiles.length === 0) {
63-
logger.debug('No resource files found');
64-
return [];
65-
}
66-
67-
logger.debug(`Found resource files: ${resourceFiles.join(', ')}`);
68-
69-
const resources: ResourceProtocol[] = [];
70-
71-
for (const file of resourceFiles) {
72-
try {
73-
const fullPath = join(this.RESOURCES_DIR, file);
74-
logger.debug(`Attempting to load resource from: ${fullPath}`);
75-
76-
const importPath = `file://${fullPath}`;
77-
const { default: ResourceClass } = await import(importPath);
78-
79-
if (!ResourceClass) {
80-
logger.warn(`No default export found in ${file}`);
81-
continue;
82-
}
39+
protected createInstance(ResourceClass: any): ResourceProtocol {
40+
return new ResourceClass();
41+
}
8342

84-
const resource = new ResourceClass();
85-
if (this.validateResource(resource)) {
86-
resources.push(resource);
87-
}
88-
} catch (error) {
89-
logger.error(`Error loading resource ${file}: ${(error as Error).message}`);
90-
}
91-
}
43+
protected getItemName(resource: ResourceProtocol): string {
44+
return resource.name;
45+
}
9246

93-
logger.debug(
94-
`Successfully loaded ${resources.length} resources: ${resources.map((r) => r.name).join(', ')}`
95-
);
96-
return resources;
97-
} catch (error) {
98-
logger.error(`Failed to load resources: ${(error as Error).message}`);
99-
return [];
100-
}
47+
async loadResources(): Promise<ResourceProtocol[]> {
48+
return this.loadItems();
10149
}
10250
}

0 commit comments

Comments
 (0)