/**
* @file
* @brief Loader implementation (browser): Vite import.meta.glob bundles prompts / lists / expansions / presets at build time.
*/
// Browser loader: bundles the prompt data at build time via Vite's
// `import.meta.glob`. The dynamic prompts are already ESM default-export modules,
// so they bundle directly; the lists and expansions are imported as raw text.
// This is what lets the real engine run in the browser with no Node `fs`.
//
// Only used by the Vite SPA. The patterns are relative to THIS file (src/core/):
// the dynamic-prompts and the rest of the prompt content (lists/expansions/
// presets) all live under the repo-root data/ folder (../../data/...).
import {
resolveListLines,
logicalListNames,
allListNames,
autoGroupListDirs,
resolveName,
compareNames,
} from "../listManifest.js";
import compileDpl from "./dpl/dpl.js";
const dpModules = import.meta.glob("../../data/dynamic-prompts/**/*.js", { eager: true });
const dpDplRaw = import.meta.glob("../../data/dynamic-prompts/**/*.dpl", {
query: "?raw",
import: "default",
eager: true,
});
const dpMetaModules = import.meta.glob("../../data/dynamic-prompts/**/*.json", {
eager: true,
import: "default",
});
const dpForcePrefixFiles = import.meta.glob("../../data/dynamic-prompts/**/_force-prefix", {
eager: true,
});
const dpGroupRaw = import.meta.glob("../../data/dynamic-prompts/**/*.group", {
query: "?raw",
import: "default",
eager: true,
});
const dpEnableGroupFiles = import.meta.glob("../../data/dynamic-prompts/**/_enable-group-list", {
eager: true,
});
const dpDisableGroupFiles = import.meta.glob("../../data/dynamic-prompts/**/_disable-group-list", {
eager: true,
});
const listRaw = import.meta.glob("../../data/lists/**/*.txt", {
query: "?raw",
import: "default",
eager: true,
});
const groupRaw = import.meta.glob("../../data/lists/**/*.group", {
query: "?raw",
import: "default",
eager: true,
});
// Marker files are empty (extension-less); an empty file is a valid empty module, so
// the eager glob parses fine. We only use the keys (which folders contain a marker).
const forcePrefixFiles = import.meta.glob("../../data/lists/**/_force-prefix", { eager: true });
const enableGroupFiles = import.meta.glob("../../data/lists/**/_enable-group-list", {
eager: true,
});
const disableGroupFiles = import.meta.glob("../../data/lists/**/_disable-group-list", {
eager: true,
});
const metaModules = import.meta.glob("../../data/lists/**/*.json", {
eager: true,
import: "default",
});
const expansionRaw = import.meta.glob("../../data/expansions-obsolete/**/*.txt", {
query: "?raw",
import: "default",
eager: true,
});
const expansionMetaModules = import.meta.glob("../../data/expansions-obsolete/**/*.json", {
eager: true,
import: "default",
});
const expForcePrefixFiles = import.meta.glob("../../data/expansions-obsolete/**/_force-prefix", {
eager: true,
});
const expGroupRaw = import.meta.glob("../../data/expansions-obsolete/**/*.group", {
query: "?raw",
import: "default",
eager: true,
});
const expEnableGroupFiles = import.meta.glob(
"../../data/expansions-obsolete/**/_enable-group-list",
{
eager: true,
},
);
const expDisableGroupFiles = import.meta.glob(
"../../data/expansions-obsolete/**/_disable-group-list",
{
eager: true,
},
);
const presetModules = import.meta.glob("../../data/presets/*.json", {
eager: true,
import: "default",
});
// ".../dynamic-prompts/v1/castle.js" -> "v1/castle"; ".../lists/keyword.txt" -> "keyword"
function keyFor(path, dir) {
const marker = `/${dir}/`;
const i = path.indexOf(marker);
const rel = i >= 0 ? path.slice(i + marker.length) : path;
return rel.replace(/\.[^./]+$/, "");
}
// Active dynamic-prompt catalog = v3 (`.dpl`, with optional same-name `.js` sidecars) + frozen
// v1 (`.js`). The v2 tree stays on disk as reference but is NOT loaded. A `.js` is a generator
// only when there is no same-name `.dpl` (otherwise it is that `.dpl`'s sidecar). Mirrors nodeLoader.
const dpJsModules = {};
for (const [path, mod] of Object.entries(dpModules)) {
const key = keyFor(path, "dynamic-prompts");
if (!key.split("/").pop().startsWith("_")) dpJsModules[key] = mod;
}
const dpDplText = {};
for (const [path, raw] of Object.entries(dpDplRaw)) {
const key = keyFor(path, "dynamic-prompts");
if (!key.split("/").pop().startsWith("_")) dpDplText[key] = String(raw);
}
// Active generator keys: every `.dpl`, plus `.js` with no same-name `.dpl`.
const dynamicPromptKeys = new Set(Object.keys(dpDplText));
for (const k of Object.keys(dpJsModules)) if (!dpDplText[k]) dynamicPromptKeys.add(k);
// Bridge for a compiled `.dpl`: resolve a JS sidecar (`script:` / `{js:}` / `insert js:`) from the
// bundled module map by joining the sidecar's relative path onto the `.dpl`'s key. Root-absolute
// (`/src/...`) paths aren't in the browser glob, so they resolve to "" (sidecars import src/ internally).
const dplModCache = {};
function browserBridge(dplKey) {
const baseDir = dplKey.includes("/") ? dplKey.slice(0, dplKey.lastIndexOf("/")) : "";
const joinKey = (rel) => {
rel = rel.replace(/\.js$/, "");
if (rel.startsWith("/")) return null;
const parts = baseDir ? baseDir.split("/") : [];
for (const seg of rel.split("/")) {
if (seg === "." || seg === "") continue;
else if (seg === "..") parts.pop();
else parts.push(seg);
}
return parts.join("/");
};
return {
resolveJs(p, ctx) {
const k = joinKey(p);
const mod = k && dpJsModules[k];
const fn = mod && (mod.default || mod);
return typeof fn === "function"
? (fn(ctx.settings, ctx.imageSettings, ctx.upscaleSettings) ?? "")
: "";
},
runPrompt: (name) => `{#${String(name).replace(/^#/, "")}}`,
runList: (name) => `{${name}}`,
expand: (s) => s,
};
}
// Files whose basename starts with `_` are internal/config (markers etc.), not lists.
const isInternal = (key) => key.split("/").pop().startsWith("_");
const listLines = {};
for (const [path, raw] of Object.entries(listRaw)) {
const key = keyFor(path, "lists");
if (!isInternal(key)) listLines[key] = String(raw).split("\n");
}
const groupLines = {};
for (const [path, raw] of Object.entries(groupRaw)) {
const key = keyFor(path, "lists");
if (!isInternal(key)) groupLines[key] = String(raw).split("\n");
}
const expansionText = {};
for (const [path, raw] of Object.entries(expansionRaw)) {
const key = keyFor(path, "expansions-obsolete");
if (!isInternal(key)) expansionText[key] = String(raw);
}
const expansionMetaMap = {};
for (const [path, obj] of Object.entries(expansionMetaModules)) {
const key = keyFor(path, "expansions-obsolete");
if (!isInternal(key)) expansionMetaMap[key] = obj;
}
const presets = {};
for (const [path, obj] of Object.entries(presetModules)) {
presets[keyFor(path, "presets")] = obj;
}
const listMetaMap = {};
for (const [path, obj] of Object.entries(metaModules)) {
const key = keyFor(path, "lists");
if (!isInternal(key)) listMetaMap[key] = obj;
}
const dpMetaMap = {};
for (const [path, obj] of Object.entries(dpMetaModules)) {
const key = keyFor(path, "dynamic-prompts");
if (!isInternal(key)) dpMetaMap[key] = obj;
}
const dpGroupLines = {};
for (const [path, raw] of Object.entries(dpGroupRaw)) {
const key = keyFor(path, "dynamic-prompts");
if (!isInternal(key)) dpGroupLines[key] = String(raw).split("\n");
}
const expGroupLines = {};
for (const [path, raw] of Object.entries(expGroupRaw)) {
const key = keyFor(path, "expansions-obsolete");
if (!isInternal(key)) expGroupLines[key] = String(raw).split("\n");
}
// Folders (relative to data/lists) that contain a `_`-prefixed marker file.
const markerDirs = (files, marker, seg = "lists") =>
Object.keys(files).map((p) => {
const i = p.indexOf(`/${seg}/`);
return p.slice(i + `/${seg}/`.length).replace(new RegExp(`/${marker}$`), "");
});
const forcedDirs = markerDirs(forcePrefixFiles, "_force-prefix");
const expForcedDirs = markerDirs(expForcePrefixFiles, "_force-prefix", "expansions-obsolete");
const dpForcedDirsAll = markerDirs(dpForcePrefixFiles, "_force-prefix", "dynamic-prompts");
const dpForcedDirs = dpForcedDirsAll.filter((d) => d.startsWith("v3/"));
// Implied groups: folders with 2+ direct lists, plus enable/disable marker overrides.
const groupListDirs = autoGroupListDirs(
logicalListNames(Object.keys(listLines)),
markerDirs(enableGroupFiles, "_enable-group-list"),
markerDirs(disableGroupFiles, "_disable-group-list"),
);
// Implied groups for dynamic prompts (v2 folder with 2+ generators) and expansions.
const dpGroupDirs = autoGroupListDirs(
[...dynamicPromptKeys].filter((n) => n.startsWith("v3/")),
markerDirs(dpEnableGroupFiles, "_enable-group-list", "dynamic-prompts").filter((d) =>
d.startsWith("v3/"),
),
markerDirs(dpDisableGroupFiles, "_disable-group-list", "dynamic-prompts").filter((d) =>
d.startsWith("v3/"),
),
);
// All generations (v1/v2/v3) — the engine/UI filter by prefix so each generation is first-class.
const dpGroupDirsAll = autoGroupListDirs(
[...dynamicPromptKeys],
markerDirs(dpEnableGroupFiles, "_enable-group-list", "dynamic-prompts"),
markerDirs(dpDisableGroupFiles, "_disable-group-list", "dynamic-prompts"),
);
const expGroupDirs = autoGroupListDirs(
Object.keys(expansionText),
markerDirs(expEnableGroupFiles, "_enable-group-list", "expansions-obsolete"),
markerDirs(expDisableGroupFiles, "_disable-group-list", "expansions-obsolete"),
);
/**
* Browser data loader for the engine: Vite `import.meta.glob` bundles. Implements
* `readExpansion`, `readListLines`, `listNames`, `expansionNames`, `loadDynamicPrompt`,
* `dynamicPromptNames`, `presetNames`, `loadPreset`.
* @type {object}
*/
export const browserLoader = {
readExpansion(name) {
// Expansions nest into category folders; resolve a bare/partial `<name>` by path
// suffix (same rule as lists) so `<rays>` still finds `lighting/rays`.
return expansionText[resolveName(name, Object.keys(expansionText))] ?? null;
},
readListLines(name, includeAdult = false) {
const names = allListNames([
...logicalListNames([...Object.keys(listLines), ...Object.keys(groupLines)]),
...groupListDirs,
]);
const canonical = resolveName(name, names);
return resolveListLines(
canonical,
{
names,
readListFile: (n) => listLines[n] ?? null,
readGroupFile: (n) => groupLines[n] ?? null,
groupListDirs,
},
includeAdult,
);
},
listNames() {
return allListNames([
...logicalListNames([...Object.keys(listLines), ...Object.keys(groupLines)]),
...groupListDirs,
]);
},
forcedPrefixDirs() {
return forcedDirs;
},
groupListDirs() {
return groupListDirs;
},
readListMeta(name) {
return listMetaMap[name] ?? null;
},
expansionNames() {
return Object.keys(expansionText);
},
readExpansionMeta(name) {
return expansionMetaMap[name] ?? null;
},
expansionForcedPrefixDirs() {
return expForcedDirs;
},
expansionGroupDirs() {
return expGroupDirs;
},
readExpansionGroup(name) {
return expGroupLines[name] ?? null;
},
loadDynamicPrompt(key) {
if (dplModCache[key]) return dplModCache[key];
if (dpDplText[key]) {
const mod = compileDpl(dpDplText[key], browserBridge(key));
dplModCache[key] = mod;
return mod;
}
return dpJsModules[key] ?? null;
},
dynamicPromptNames() {
return [...dynamicPromptKeys].sort(compareNames);
},
readDynPromptMeta(name) {
return dpMetaMap[name] ?? null;
},
dynPromptForcedPrefixDirs() {
return dpForcedDirs;
},
dynPromptForcedPrefixDirsAll() {
return dpForcedDirsAll;
},
dynPromptGroupDirs() {
return dpGroupDirs;
},
dynPromptGroupDirsAll() {
return dpGroupDirsAll;
},
readDynPromptGroup(name) {
return dpGroupLines[name] ?? null;
},
presetNames() {
return Object.keys(presets);
},
loadPreset(name) {
return presets[name] ?? null;
},
};