nitropage
Version:
A free and open source, extensible visual page builder based on SolidStart.
201 lines (168 loc) • 5.19 kB
text/typescript
import { Blueprint, BlueprintFn, Config } from "#../types";
import { baseFilename } from "#utils/string/baseFilename";
import { titleCase } from "#utils/string/titleCase";
import { once } from "es-toolkit";
import {
Accessor,
createEffect,
createSignal,
runWithOwner,
Setter,
startTransition,
untrack,
} from "solid-js";
import { produce, reconcile, SetStoreFunction } from "solid-js/store";
import { isServer } from "solid-js/web";
import { lazy } from "solid-lazy-plus";
import type { ViteHotContext } from "vite/types/hot.js";
import textData from "../../features/blueprintData/text";
const UNMOUNT = "$$unmount";
export const initBlueprint = async (
filePath: string,
config: Config,
setConfig: SetStoreFunction<Config>,
) => {
if (!config.getBlueprints) return;
const existingPromise = untrack(() => config.initPromises[filePath]);
if (existingPromise) {
await existingPromise;
return;
}
const promise = (async () => {
const blueprints = config.getBlueprints!();
if (!blueprints[filePath]) {
console.error("Invalid blueprint, doesn't exist", filePath);
return;
}
const m = await blueprints[filePath]();
let id: string | undefined = m.id;
if (!m.id || !m.default || !m.blueprint) {
console.error(
"Invalid blueprint, doesn't provide necessary exports (default, id, blueprint)",
filePath,
);
return;
}
let lastVersion: number = undefined!;
const updateBlueprint = function () {
let blueprintFn: BlueprintFn | typeof UNMOUNT = undefined!;
let blueprint: Blueprint;
try {
blueprintFn = m.blueprint();
if (blueprintFn === UNMOUNT) {
if (!id) return;
console.info("Unmounting blueprint", id);
// Another initBlueprint changed the version
// Ergo the blueprint was moved to a different path
// So we must skip the deletion
const isRename = untrack(
() =>
!!(
config.blueprints[id!] &&
lastVersion !== config.blueprints[id!].version
),
);
setConfig(
produce((d) => {
if (lastVersion != null && !isRename) delete d.blueprints[id!];
delete d.initPromises[filePath];
}),
);
return;
}
} catch (err) {
console.error(`Blueprint ${filePath} failed to run.`);
console.error(err);
return;
}
return (async function () {
try {
blueprint = (await blueprintFn({
admin: false, // ctx.admin,
})) as Blueprint;
} catch (err) {
console.error(`Blueprint ${filePath} failed to run.`);
console.error(err);
return;
}
blueprint.component = import.meta.env.PROD
? lazy(async () => m)
: m.default;
blueprint.title =
blueprint.title ||
once(() => {
const fileName = baseFilename(filePath);
const title =
fileName !== "index"
? fileName
: filePath.split("/").splice(-2)[0];
return titleCase(title);
});
if (!blueprint.category || blueprint.category === "all") {
blueprint.category = "misc";
}
// blueprintFn._id -> id after hot module replace
// m.id -> first exported id
blueprint.id =
blueprintFn._id || m.id || baseFilename(filePath).toLowerCase();
id = blueprint.id;
blueprint.slots = blueprint.slots || {};
blueprint.data["id"] = textData({
sanitizeSlug: true,
title() {
return "ID #";
},
});
blueprint.version = Math.random();
lastVersion = blueprint.version;
startTransition(() =>
setConfig(
"blueprints",
blueprint.id,
reconcile(blueprint, { key: null, merge: true }),
),
);
})();
};
if (isServer || import.meta.env.PROD) {
return await updateBlueprint();
} else {
runWithOwner(config.owner(), function () {
createEffect(updateBlueprint);
});
}
})();
setConfig(produce((d) => (d.initPromises[filePath] = promise)));
await promise;
};
export const createBlueprint = function <T extends BlueprintFn>(
fn: T,
hot?: ViteHotContext,
): Accessor<T> {
if (!hot) {
return () => fn;
}
const signal: [state: Accessor<T>, setState: Setter<T>] =
hot.data["npBlueprint"] || createSignal(fn);
hot.data["npBlueprintFn"] = fn;
hot.data["npBlueprint"] = signal;
// Catch invalidate triggered by solid refresh
hot.invalidate = () => {};
let hasError = false;
hot.on("vite:error", function () {
hasError = true;
});
hot.accept(function (mod: any) {
if (hasError) {
hasError = false;
return;
}
if (!mod?.blueprint) {
signal[1](() => UNMOUNT as any);
return;
}
hot.data["npBlueprintFn"]._id = mod.id;
signal[1](() => hot.data["npBlueprintFn"]);
});
return signal[0];
};