a plugin can be more than one screen
a plugin was one screen because the two things it declares — a mount prefix and a frontend — happened to be the same string. caldav is where that stops holding: one sidecar, one permission, one install, and two screens that have nothing to do with each other on screen. calendar and contacts are not sections of one page, and forcing them into one would be a worse product than the core screens they replace. nothing else moved. all eighteen plugins with a frontend use flat web/panels.ts, which is now "one screen, at the mount prefix, labelled by the manifest" — the same file, the same route, the same persisted layout key. the nested form is opt-in and caldav is its only user. the split of who declares what follows the rule this file already had. the DIRECTORY declares that a screen exists (web/<segment>/panels.ts); the MANIFEST says what to call it and which glyph to draw, because a directory listing cannot say either and those are a human's choice. the two are cross-checked and a mismatch is refused rather than reconciled: a segment with no directory renders nothing, a directory with no entry has no tile and no label, and both are the quiet kind of broken. the part that needed the most care is that a segment is a URL the plugin CHOSE — /calendar, not /caldav/calendar. mountPrefix is what makes it impossible for a plugin to shadow a core route, and a chosen segment steps straight out of it. so this is first-party only, refused at discovery, and tested: without that check this field is a way for any installed plugin to take over /files. screenId, not appName, is now the layout key and the panel allow-list's owner. two screens sharing one would persist over each other's sizes and each would accept the other's panels. existing plugins get screenId === appName, so no stored layout is orphaned — verified by regenerating.
This commit is contained in:
@@ -62,23 +62,43 @@ export function generatePluginsModule(states: PluginState[]): string {
|
||||
const imports: string[] = [];
|
||||
const entries: string[] = [];
|
||||
|
||||
withWeb.forEach((state, i) => {
|
||||
const { plugin } = state;
|
||||
const alias = `Plugin${i}`;
|
||||
imports.push(`import { appRegistryMetas as ${alias}Panels } from '${specifier(plugin.web!.panels)}';`);
|
||||
imports.push(`import { defaultLayout as ${alias}Layout } from '${specifier(plugin.web!.layout)}';`);
|
||||
entries.push(
|
||||
` { appName: '${plugin.appName}', route: '${mountPrefix(plugin)}', panels: ${alias}Panels, layout: ${alias}Layout },`,
|
||||
);
|
||||
});
|
||||
// One entry per SCREEN, not per plugin. Almost every plugin has exactly one and this reads the same as
|
||||
// it always did; caldav has two, so it emits two routes, two layout keys and two dock tiles from one
|
||||
// install. The alias counter runs over screens for that reason — two screens of one plugin must not
|
||||
// share an import name.
|
||||
let i = 0;
|
||||
for (const { plugin } of withWeb) {
|
||||
for (const screen of plugin.web!.screens) {
|
||||
const alias = `Plugin${i}`;
|
||||
i += 1;
|
||||
imports.push(`import { appRegistryMetas as ${alias}Panels } from '${specifier(screen.panels)}';`);
|
||||
imports.push(`import { defaultLayout as ${alias}Layout } from '${specifier(screen.layout)}';`);
|
||||
// The screen id is the LAYOUT KEY (`screens/plugin/<id>`) and the panel allow-list's owner, so two
|
||||
// screens of one plugin must not share it — otherwise they persist over each other's sizes and each
|
||||
// would accept the other's panels.
|
||||
const screenId = screen.segment ? `${plugin.appName}/${screen.segment}` : plugin.appName;
|
||||
const route = screen.segment ? `/${screen.segment}` : mountPrefix(plugin);
|
||||
entries.push(
|
||||
` { appName: '${plugin.appName}', screenId: '${screenId}', route: '${route}', panels: ${alias}Panels, layout: ${alias}Layout },`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const body = `${HEADER}
|
||||
import type { AppRegistryMeta, LayoutNode } from 'officerdev';
|
||||
${imports.join('\n')}
|
||||
|
||||
export type GeneratedPlugin = {
|
||||
/** The plugin this screen belongs to. Two screens of one plugin share it. */
|
||||
appName: string;
|
||||
/** Where it mounts, from \`mountPrefix\` — \`/offscale\` for ours, \`/p/<publisher>/<name>\` for others. */
|
||||
/**
|
||||
* This SCREEN's id — \`<appName>\` for a single-screen plugin, \`<appName>/<segment>\` for one of several.
|
||||
*
|
||||
* It is the layout persistence key and the panel allow-list's owner, so it has to be unique per screen
|
||||
* rather than per plugin: sharing it would have two screens persisting over each other's sizes.
|
||||
*/
|
||||
screenId: string;
|
||||
/** Where it mounts — \`mountPrefix\` normally, or a first-party plugin's chosen segment. */
|
||||
route: string;
|
||||
/** The panels it contributes. At least one, or discovery refuses the plugin. */
|
||||
panels: AppRegistryMeta[];
|
||||
|
||||
Reference in New Issue
Block a user