plugins: a splash is part of a plugin, like its icon
assets/splash.png is discovered by presence, published by the same copy that already took the whole assets directory, and carried on DiscoveredPlugin beside icon. The platform draws no splash — a plugin has no launch. It is carried because the plugin repository is where a product's artwork lives now, and the phone app that fronts the same product needs somewhere to read it from that is not a designer's folder. The pair is the point: the icon is a reduction of the splash's world, and keeping them together is what stops them drifting. Publishing no longer gates on the icon alone, which left a plugin shipping only a splash serving nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,9 @@ export const assetPublicDir = (sidecarId: string): string => join(process.cwd(),
|
|||||||
/** The URL a manifest should use for a shipped icon. */
|
/** The URL a manifest should use for a shipped icon. */
|
||||||
export const iconUrl = (sidecarId: string): string => `/plugins/${sidecarId}/icon.png`;
|
export const iconUrl = (sidecarId: string): string => `/plugins/${sidecarId}/icon.png`;
|
||||||
|
|
||||||
|
/** The URL for a shipped splash. Published by the same copy — `publishAssets` takes the whole dir. */
|
||||||
|
export const splashUrl = (sidecarId: string): string => `/plugins/${sidecarId}/splash.png`;
|
||||||
|
|
||||||
const exists = async (path: string): Promise<boolean> => {
|
const exists = async (path: string): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
await stat(path);
|
await stat(path);
|
||||||
|
|||||||
@@ -106,6 +106,9 @@ export async function loadPlugin(dir: string, appName: string): Promise<Discover
|
|||||||
// could only ever name a glyph the platform had already bundled, which is a ceiling a plugin
|
// could only ever name a glyph the platform had already bundled, which is a ceiling a plugin
|
||||||
// from a marketplace has no way to see coming.
|
// from a marketplace has no way to see coming.
|
||||||
icon: fileOrNull(join(dir, 'assets', 'icon.png')),
|
icon: fileOrNull(join(dir, 'assets', 'icon.png')),
|
||||||
|
// The launch scene, carried for whoever fronts this plugin on a phone. Same rule as the icon:
|
||||||
|
// the file's presence is the declaration, and there is no manifest field to keep in step.
|
||||||
|
splash: fileOrNull(join(dir, 'assets', 'splash.png')),
|
||||||
api: fileOrNull(join(dir, 'api', 'router.ts')),
|
api: fileOrNull(join(dir, 'api', 'router.ts')),
|
||||||
schema: fileOrNull(join(dir, 'db', 'schema.ts')),
|
schema: fileOrNull(join(dir, 'db', 'schema.ts')),
|
||||||
sidecar: findSidecar(dir),
|
sidecar: findSidecar(dir),
|
||||||
|
|||||||
@@ -320,7 +320,9 @@ export async function installPlugin(
|
|||||||
// obvious: the `/plugins/*` route is dynamic precisely so a first install does not show a broken
|
// obvious: the `/plugins/*` route is dynamic precisely so a first install does not show a broken
|
||||||
// image until the next restart, and uninstall is allowed to delete these because they are copies of
|
// image until the next restart, and uninstall is allowed to delete these because they are copies of
|
||||||
// files that still exist in the plugin's source.
|
// files that still exist in the plugin's source.
|
||||||
if (plugin.icon) {
|
// Either asset is reason enough to publish — the copy takes the whole directory, and gating on
|
||||||
|
// the icon alone left a plugin that ships only a splash serving nothing.
|
||||||
|
if (plugin.icon || plugin.splash) {
|
||||||
await publishAssets(appName, plugin.dir);
|
await publishAssets(appName, plugin.dir);
|
||||||
await step(steps, onStep, `assets: published to /plugins/${appName}/`);
|
await step(steps, onStep, `assets: published to /plugins/${appName}/`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -240,6 +240,18 @@ export type DiscoveredPlugin = {
|
|||||||
* naming one outside that set silently renders a neutral box.
|
* naming one outside that set silently renders a neutral box.
|
||||||
*/
|
*/
|
||||||
icon: string | null;
|
icon: string | null;
|
||||||
|
/**
|
||||||
|
* `assets/splash.png` — the app's launch scene, published beside the icon.
|
||||||
|
*
|
||||||
|
* The platform itself draws no splash; a plugin has no launch. It is carried because the plugin
|
||||||
|
* repository is where a product's artwork lives now, and the mobile app that fronts the same
|
||||||
|
* product needs somewhere to read it from that is not a designer's folder. Presence is the
|
||||||
|
* declaration, exactly as with the icon.
|
||||||
|
*
|
||||||
|
* The pair is deliberate: `assets/icon.png` is a reduction of `assets/splash.png`'s world, and
|
||||||
|
* keeping them in one place is what stops the two drifting apart.
|
||||||
|
*/
|
||||||
|
splash: string | null;
|
||||||
/** `api/router.ts` — a backend router, mounted at `mountPrefix`. */
|
/** `api/router.ts` — a backend router, mounted at `mountPrefix`. */
|
||||||
api: string | null;
|
api: string | null;
|
||||||
/** `db/schema.ts` — tables, pushed on install. Every name must be prefixed `<appName>_`. */
|
/** `db/schema.ts` — tables, pushed on install. Every name must be prefixed `<appName>_`. */
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const plugin = (appName: string, unauthenticatedPaths?: string[], publisher = 'o
|
|||||||
...(unauthenticatedPaths ? { unauthenticatedPaths } : {}),
|
...(unauthenticatedPaths ? { unauthenticatedPaths } : {}),
|
||||||
},
|
},
|
||||||
icon: null,
|
icon: null,
|
||||||
|
splash: null,
|
||||||
api: null,
|
api: null,
|
||||||
schema: null,
|
schema: null,
|
||||||
sidecar: null,
|
sidecar: null,
|
||||||
|
|||||||
Reference in New Issue
Block a user