Stability fixes: plugin-scoped contexts for owned pages, absent-href guard, checked locale mounts
CI / full-gate (push) Successful in 2m37s
CI / full-gate (push) Successful in 2m37s
This commit is contained in:
@@ -24,18 +24,22 @@ test("runBootHooks runs each onBoot in order, skips plugins without one, and a t
|
||||
|
||||
test("runRequestHooks short-circuits on the first RouteResult (with its plugin); later hooks skipped", async () => {
|
||||
const calls: string[] = [];
|
||||
const scoped: string[] = []; // each hook is handed a context built for its own plugin
|
||||
const contextFor = (pluginId: string) => { scoped.push(pluginId); return ctx; };
|
||||
const short = await runRequestHooks([
|
||||
plugin("a", { onRequest: () => void calls.push("a") }), // returns void → continue
|
||||
plugin("b", { onRequest: () => { calls.push("b"); return { html: "stop" }; } }),
|
||||
plugin("c", { onRequest: () => void calls.push("c") }), // never reached
|
||||
], ctx);
|
||||
], contextFor);
|
||||
|
||||
assert.deepEqual(short?.result, { html: "stop" });
|
||||
assert.equal(short?.plugin.id, "b"); // the owning plugin (so a `view` result resolves correctly)
|
||||
assert.equal(short?.ctx, ctx); // …and the context it ran on, for rendering its view
|
||||
assert.deepEqual(calls, ["a", "b"]);
|
||||
assert.deepEqual(scoped, ["a", "b"]); // a plugin without the hook never builds a context
|
||||
|
||||
// No hook short-circuits → null (proceed with normal routing).
|
||||
assert.equal(await runRequestHooks([plugin("a", { onRequest: () => {} })], ctx), null);
|
||||
assert.equal(await runRequestHooks([plugin("a", { onRequest: () => {} })], contextFor), null);
|
||||
});
|
||||
|
||||
test("runResponseHooks runs every onResponse as an observer with the result; a throw fails", async () => {
|
||||
|
||||
@@ -13,11 +13,17 @@ export async function runBootHooks(plugins: Plugin[]): Promise<void> {
|
||||
|
||||
// Before route matching. The first hook to return a RouteResult short-circuits the request — its
|
||||
// result becomes the response and later hooks + the route handler are skipped. Returns that result
|
||||
// with its owning plugin (so a `view` result resolves against that plugin's views), or null to proceed.
|
||||
export async function runRequestHooks(plugins: Plugin[], ctx: RequestContext): Promise<{ plugin: Plugin; result: RouteResult } | null> {
|
||||
// with its owning plugin (so a `view` result resolves against that plugin's views), or null to
|
||||
// proceed. Each hook gets a context scoped to its own plugin, so `ctx.t` reads that plugin's catalog.
|
||||
export async function runRequestHooks(
|
||||
plugins: Plugin[],
|
||||
contextFor: (pluginId: string) => RequestContext,
|
||||
): Promise<{ ctx: RequestContext; plugin: Plugin; result: RouteResult } | null> {
|
||||
for (const plugin of plugins) {
|
||||
const result = await plugin.hooks?.onRequest?.(ctx);
|
||||
if (result != null) return { plugin, result };
|
||||
if (!plugin.hooks?.onRequest) continue;
|
||||
const ctx = contextFor(plugin.id);
|
||||
const result = await plugin.hooks.onRequest(ctx);
|
||||
if (result != null) return { ctx, plugin, result };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user