Files
wehub-resource-sync c48612c494
CI / E2E Tests (push) Has been cancelled
CI / Lint, Typecheck & Unit Tests (push) Has been cancelled
Docs Build / Build docs site (push) Has been cancelled
Publish @openmaic packages / Build, validate & publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:23 +08:00

105 lines
4.4 KiB
TypeScript

/**
* ComfyUI Workflow Discovery
*
* Single source of truth for "which workflow JSON files exist and are
* selectable". Both `/api/comfyui-workflows` (lists workflows for the
* Settings UI) and the ComfyUI image adapter (validates a client-supplied
* workflow id before reading it from disk) import this module so the two
* can never drift apart — a filename the UI offers is always a filename
* the adapter will accept, and vice versa.
*
* Workflow files live flat in the Next.js `public/` directory and are
* recognised by filename: must end in `.json` and either start with
* "comfyui" or contain "workflow" (case-insensitive).
*
* IMPORTANT: this module is reachable from client bundles (imported by
* the adapter, which is imported by image-providers.ts, which is imported
* by the client-side settings store). `fs`/`path` are therefore imported
* dynamically inside each function rather than statically at the top of
* the file — a top-level `import ... from 'fs'` makes the bundler try to
* resolve `fs` for the browser build too, which fails ("Module not found:
* Can't resolve 'fs'"). The functions below are still only ever *called*
* server-side; the dynamic import just keeps the module import-safe.
*/
export interface ComfyuiWorkflowEntry {
id: string;
name: string;
}
/** Convert a workflow filename to a human-readable display name. */
export function filenameToDisplayName(filename: string): string {
return (
filename
.replace(/\.json$/i, '') // strip extension
.replace(/^comfyui[-_]?/i, '') // strip leading "comfyui-" or "comfyui_"
.replace(/[-_]+/g, ' ') // hyphens/underscores → spaces
.trim()
.replace(/\b\w/g, (c) => c.toUpperCase()) || // title-case
'Default Workflow'
); // fallback if name becomes empty
}
/**
* Whether a filename looks like a ComfyUI workflow file, per the naming
* convention documented above. Also rejects anything that isn't a bare
* basename (no path separators, no "..") so this doubles as the safety
* check for client-supplied workflow ids.
*/
export function isComfyuiWorkflowFilename(filename: string): boolean {
if (filename.includes('/') || filename.includes('\\') || filename.includes('..')) {
return false;
}
const lower = filename.toLowerCase();
return lower.endsWith('.json') && (lower.startsWith('comfyui') || lower.includes('workflow'));
}
/**
* List the workflow JSON files actually present in `public/`, with display
* names. Returns `[]` if `public/` doesn't exist, on any read error, or if
* called from the browser (this is server-only — see file-level note).
*
* Async because fs/path are loaded dynamically — callers must `await` this.
*
* The `typeof window === 'undefined'` guard below isn't just a runtime
* safety check: Turbopack/webpack can statically eliminate code behind it
* for the client bundle, which is what lets the dynamic `import('fs')`
* resolve at all. Without the guard, the bundler can't prove this branch
* is server-only and tries to resolve 'fs' for the browser build too,
* which fails ("Module not found: Can't resolve 'fs'"). This matches the
* existing pattern in comfyui-image-adapter.ts's loadWorkflow().
*/
export async function listComfyuiWorkflows(): Promise<ComfyuiWorkflowEntry[]> {
// Server-side only — see file-level note. The `typeof window === 'undefined'`
// guard must wrap the dynamic import directly (not an early-return above
// it) to match the shape Turbopack already dead-code-eliminates correctly
// in comfyui-image-adapter.ts's loadWorkflow().
if (typeof window === 'undefined') {
try {
const fs = await import('fs');
const path = await import('path');
const publicDir = path.join(process.cwd(), 'public');
if (!fs.existsSync(publicDir)) return [];
return fs
.readdirSync(publicDir)
.filter(
(f) => isComfyuiWorkflowFilename(f) && fs.statSync(path.join(publicDir, f)).isFile(),
)
.map((filename) => ({ id: filename, name: filenameToDisplayName(filename) }))
.sort((a, b) => a.name.localeCompare(b.name));
} catch (err) {
console.error('[ComfyUI Workflows] Failed to list workflows:', err);
return [];
}
}
return [];
}
/** Just the filenames, for allowlist membership checks. */
export async function listComfyuiWorkflowFilenames(): Promise<string[]> {
const workflows = await listComfyuiWorkflows();
return workflows.map((w) => w.id);
}