102 lines
2.7 KiB
TypeScript
102 lines
2.7 KiB
TypeScript
|
|
/**
|
||
|
|
* App loader utility for CLI commands.
|
||
|
|
* Handles various app export patterns and module systems.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { resolve } from 'node:path'
|
||
|
|
import { pathToFileURL } from 'node:url'
|
||
|
|
|
||
|
|
export interface LoadedApp {
|
||
|
|
fastify: unknown
|
||
|
|
source: 'default' | 'named' | 'commonjs'
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Load a Fastify app from app.js in the given directory.
|
||
|
|
* Supports:
|
||
|
|
* - ESM default export: export default fastifyInstance
|
||
|
|
* - ESM named export: export const createApp = () => fastifyInstance
|
||
|
|
* - CommonJS: module.exports = fastifyInstance
|
||
|
|
* - CommonJS named: exports.createApp = () => fastifyInstance
|
||
|
|
*/
|
||
|
|
export async function loadApp(cwd: string): Promise<LoadedApp> {
|
||
|
|
const appPath = resolve(cwd, 'app.js')
|
||
|
|
const appUrl = pathToFileURL(appPath).href + '?t=' + Date.now()
|
||
|
|
|
||
|
|
let appModule: Record<string, unknown>
|
||
|
|
try {
|
||
|
|
appModule = await import(appUrl) as Record<string, unknown>
|
||
|
|
} catch (err) {
|
||
|
|
throw new AppLoadError(
|
||
|
|
`Cannot load app.js: ${err instanceof Error ? err.message : String(err)}`,
|
||
|
|
'import_failed',
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Try default export first
|
||
|
|
if (appModule.default && isFastifyInstance(appModule.default)) {
|
||
|
|
return { fastify: appModule.default, source: 'default' }
|
||
|
|
}
|
||
|
|
|
||
|
|
// Try named exports that look like Fastify instances or factory functions
|
||
|
|
for (const [key, value] of Object.entries(appModule)) {
|
||
|
|
if (key === 'default') continue
|
||
|
|
|
||
|
|
if (isFastifyInstance(value)) {
|
||
|
|
return { fastify: value, source: 'named' }
|
||
|
|
}
|
||
|
|
|
||
|
|
// Try calling factory functions
|
||
|
|
if (typeof value === 'function' && !isClass(value)) {
|
||
|
|
try {
|
||
|
|
const result = await value()
|
||
|
|
if (isFastifyInstance(result)) {
|
||
|
|
return { fastify: result, source: 'named' }
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
// Factory function failed, try next
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// If module itself is a Fastify instance (CommonJS)
|
||
|
|
if (isFastifyInstance(appModule)) {
|
||
|
|
return { fastify: appModule, source: 'commonjs' }
|
||
|
|
}
|
||
|
|
|
||
|
|
throw new AppLoadError(
|
||
|
|
'No Fastify instance found in app.js. Ensure app.js exports a Fastify instance or a factory function.',
|
||
|
|
'no_fastify',
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if a value looks like a Fastify instance.
|
||
|
|
*/
|
||
|
|
function isFastifyInstance(value: unknown): boolean {
|
||
|
|
return value !== null &&
|
||
|
|
typeof value === 'object' &&
|
||
|
|
typeof (value as Record<string, unknown>).ready === 'function'
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if a function is a class constructor.
|
||
|
|
*/
|
||
|
|
function isClass(fn: unknown): boolean {
|
||
|
|
return typeof fn === 'function' &&
|
||
|
|
fn.toString().startsWith('class ')
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Error type for app loading failures.
|
||
|
|
*/
|
||
|
|
export class AppLoadError extends Error {
|
||
|
|
constructor(
|
||
|
|
message: string,
|
||
|
|
public readonly code: 'import_failed' | 'no_fastify',
|
||
|
|
) {
|
||
|
|
super(message)
|
||
|
|
this.name = 'AppLoadError'
|
||
|
|
}
|
||
|
|
}
|