a plugin screen can declare its routes
Every screen got `/<route>` and `/<route>/:section` and nothing deeper, so a section had to live in the query string. A screen may now name its routes in the manifest, and give one its own arrangement of the screen's panels. Absent, the old pair is unchanged — which is every plugin but fofos-admin. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { existsSync, readdirSync, statSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { join, sep } from 'node:path';
|
||||
import { PLATFORM_DIR } from '../data-path';
|
||||
import {
|
||||
manifestProblems,
|
||||
@@ -93,10 +93,14 @@ function findWeb(
|
||||
`Every plugin route renders a Workspace: contribute panels and a layout, not a screen.`,
|
||||
};
|
||||
}
|
||||
const problems: string[] = [];
|
||||
const routes = routesOf(manifest?.routes, webDir, layout, problems);
|
||||
if (problems.length) return { web: null, error: `${appName}: ${problems.join('; ')}.` };
|
||||
|
||||
return {
|
||||
web: {
|
||||
screens: [
|
||||
{ segment: null, label: manifest?.label ?? appName, icon: manifest?.icon, art: null, panels, layout },
|
||||
{ segment: null, label: manifest?.label ?? appName, icon: manifest?.icon, art: null, panels, layout, routes },
|
||||
],
|
||||
// Optional and unannounced — presence is the declaration, as it is for panels and layout.
|
||||
fileActions: fileOrNull(join(webDir, 'file-actions.ts')),
|
||||
@@ -145,6 +149,8 @@ function findWeb(
|
||||
art: fileOrNull(join(dir, 'assets', `${screen.segment}.png`)),
|
||||
panels,
|
||||
layout,
|
||||
// On the screen entry, not the manifest root: a route belongs to the screen it renders inside.
|
||||
routes: routesOf(screen.routes, join(webDir, screen.segment), layout, problems),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -162,6 +168,52 @@ function findWeb(
|
||||
return { web: { screens, fileActions: fileOrNull(join(webDir, 'file-actions.ts')) } };
|
||||
}
|
||||
|
||||
/**
|
||||
* A screen's route table, resolved against the plugin directory.
|
||||
*
|
||||
* ── Every layout is checked HERE ──
|
||||
*
|
||||
* A route naming a file that is not there would be a build error in generated code — a stack trace
|
||||
* pointing at `Plugins.gen.tsx`, which nobody wrote. Refused at discovery it is a sentence naming the
|
||||
* plugin and the missing file, which is the same trade every other check in this file makes.
|
||||
*
|
||||
* A path is not validated beyond being a string: it is react-router's syntax, not ours, and a
|
||||
* half-understood copy of their grammar would refuse patterns that work.
|
||||
*/
|
||||
function routesOf(
|
||||
declared: { path: string; layout?: string }[] | undefined,
|
||||
webDir: string,
|
||||
fallbackLayout: string,
|
||||
problems: string[],
|
||||
): { path: string; layout: string }[] {
|
||||
const seen = new Set<string>();
|
||||
const routes: { path: string; layout: string }[] = [];
|
||||
|
||||
for (const route of declared ?? []) {
|
||||
// Two entries on one path is one of them never matching, silently — react-router takes the first.
|
||||
if (seen.has(route.path)) {
|
||||
problems.push(`route ${JSON.stringify(route.path)} is declared twice`);
|
||||
continue;
|
||||
}
|
||||
seen.add(route.path);
|
||||
|
||||
if (!route.layout) {
|
||||
routes.push({ path: route.path, layout: fallbackLayout });
|
||||
continue;
|
||||
}
|
||||
// Under web/, like everything else a screen declares. `join` on a traversal would leave the plugin
|
||||
// directory, so the resolved path is checked to still be inside it.
|
||||
const resolved = join(webDir, route.layout.replace(/^web\//, ''));
|
||||
if (!resolved.startsWith(webDir + sep) || !existsSync(resolved)) {
|
||||
problems.push(`route ${JSON.stringify(route.path)} names layout ${JSON.stringify(route.layout)}, which is not in web/`);
|
||||
continue;
|
||||
}
|
||||
routes.push({ path: route.path, layout: resolved });
|
||||
}
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
const fileOrNull = (path: string): string | null => (existsSync(path) ? path : null);
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user