feat: add display kiosk host
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
isVersionCompatible,
|
||||
type ModuleCapabilityDeclaration,
|
||||
type ModuleManifest,
|
||||
type PermissionId,
|
||||
safeParseModuleManifest,
|
||||
semanticVersionSchema,
|
||||
} from "@ekklesia/contracts";
|
||||
@@ -61,10 +62,16 @@ export interface ResolvedModuleEntrypoints {
|
||||
readonly schema?: string;
|
||||
readonly migrations?: string;
|
||||
readonly jobs?: string;
|
||||
/** A module's declared presentation artifact (ADR 0021), resolved and validated exactly like the
|
||||
* other entrypoints. Discovery only proves the file exists, is compiled ESM, and stays inside the
|
||||
* module package — nothing in this repository imports, evaluates, mounts, or renders it yet. */
|
||||
/** A module's primary presentation artifact (ADR 0021), resolved and validated exactly like the
|
||||
* other executable entrypoints. Discovery never imports or evaluates browser artifacts. */
|
||||
readonly ui?: string;
|
||||
/** The independently mountable full-viewport artifact and its statically declared access policy.
|
||||
* `artifactPath` is canonical and package-contained; `permission` already matched one of this
|
||||
* module's own manifest declarations during manifest parsing. */
|
||||
readonly display?: {
|
||||
readonly artifactPath: string;
|
||||
readonly permission: PermissionId;
|
||||
};
|
||||
}
|
||||
|
||||
// Branded primitives (`PermissionId`, `ModuleId`, ...) are `string & {brand}` intersection types,
|
||||
@@ -258,11 +265,49 @@ async function resolveEntrypoints(
|
||||
): Promise<ResolvedModuleEntrypoints | undefined> {
|
||||
const resolved: Partial<Record<keyof ModuleManifest["entrypoints"], string>> = {};
|
||||
const canonicalModuleDirectory = await realpath(moduleDirectory);
|
||||
const { display, ...ordinaryEntrypoints } = manifest.entrypoints;
|
||||
const ordinaryConfiguredEntrypoints = Object.entries(ordinaryEntrypoints)
|
||||
.filter((entry): entry is [string, string] => entry[1] !== undefined)
|
||||
.map(([entrypointName, relativeEntrypoint]) => ({
|
||||
entrypointName,
|
||||
issuePath: `entrypoints.${entrypointName}`,
|
||||
relativeEntrypoint,
|
||||
}));
|
||||
const configuredEntrypoints = [
|
||||
...ordinaryConfiguredEntrypoints,
|
||||
...(display === undefined
|
||||
? []
|
||||
: [
|
||||
{
|
||||
entrypointName: "display",
|
||||
issuePath: "entrypoints.display.artifact",
|
||||
relativeEntrypoint: display.artifact,
|
||||
},
|
||||
]),
|
||||
];
|
||||
let entrypointCollision = false;
|
||||
|
||||
for (const [entrypointName, relativeEntrypoint] of Object.entries(manifest.entrypoints)) {
|
||||
if (relativeEntrypoint === undefined) {
|
||||
continue;
|
||||
if (display !== undefined) {
|
||||
const configuredDisplayPath = path.resolve(moduleDirectory, display.artifact);
|
||||
for (const ordinary of ordinaryConfiguredEntrypoints) {
|
||||
if (path.resolve(moduleDirectory, ordinary.relativeEntrypoint) === configuredDisplayPath) {
|
||||
entrypointCollision = true;
|
||||
issues.push({
|
||||
code: "MODULE_ENTRYPOINT_INVALID",
|
||||
message: `display must not resolve to the same configured file as ${ordinary.entrypointName}.`,
|
||||
moduleDirectory: directoryName,
|
||||
moduleId: manifest.id,
|
||||
path: "entrypoints.display.artifact",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const {
|
||||
entrypointName,
|
||||
issuePath: entrypointIssuePath,
|
||||
relativeEntrypoint,
|
||||
} of configuredEntrypoints) {
|
||||
if (
|
||||
isCompiledEntrypoint(entrypointName) &&
|
||||
![".js", ".mjs"].includes(path.extname(relativeEntrypoint).toLowerCase())
|
||||
@@ -272,7 +317,7 @@ async function resolveEntrypoints(
|
||||
message: `${entrypointName} must reference compiled ESM JavaScript with a .js or .mjs extension.`,
|
||||
moduleDirectory: directoryName,
|
||||
moduleId: manifest.id,
|
||||
path: `entrypoints.${entrypointName}`,
|
||||
path: entrypointIssuePath,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -283,7 +328,7 @@ async function resolveEntrypoints(
|
||||
message: `${entrypointName} resolves outside its module directory.`,
|
||||
moduleDirectory: directoryName,
|
||||
moduleId: manifest.id,
|
||||
path: `entrypoints.${entrypointName}`,
|
||||
path: entrypointIssuePath,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -296,7 +341,7 @@ async function resolveEntrypoints(
|
||||
message: `${entrypointName} resolves through a link outside its module directory.`,
|
||||
moduleDirectory: directoryName,
|
||||
moduleId: manifest.id,
|
||||
path: `entrypoints.${entrypointName}`,
|
||||
path: entrypointIssuePath,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -312,7 +357,7 @@ async function resolveEntrypoints(
|
||||
: `${entrypointName} must resolve to a file.`,
|
||||
moduleDirectory: directoryName,
|
||||
moduleId: manifest.id,
|
||||
path: `entrypoints.${entrypointName}`,
|
||||
path: entrypointIssuePath,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -329,12 +374,27 @@ async function resolveEntrypoints(
|
||||
}.`,
|
||||
moduleDirectory: directoryName,
|
||||
moduleId: manifest.id,
|
||||
path: `entrypoints.${entrypointName}`,
|
||||
path: entrypointIssuePath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolved.server) {
|
||||
if (resolved.display !== undefined) {
|
||||
for (const [entrypointName, canonicalEntrypoint] of Object.entries(resolved)) {
|
||||
if (entrypointName !== "display" && canonicalEntrypoint === resolved.display) {
|
||||
entrypointCollision = true;
|
||||
issues.push({
|
||||
code: "MODULE_ENTRYPOINT_INVALID",
|
||||
message: `display must not resolve through a link to the same file as ${entrypointName}.`,
|
||||
moduleDirectory: directoryName,
|
||||
moduleId: manifest.id,
|
||||
path: "entrypoints.display.artifact",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolved.server || entrypointCollision) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -344,6 +404,14 @@ async function resolveEntrypoints(
|
||||
...(resolved.migrations ? { migrations: resolved.migrations } : {}),
|
||||
...(resolved.jobs ? { jobs: resolved.jobs } : {}),
|
||||
...(resolved.ui ? { ui: resolved.ui } : {}),
|
||||
...(resolved.display && display
|
||||
? {
|
||||
display: {
|
||||
artifactPath: resolved.display,
|
||||
permission: display.permission,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user