@cloudcrane/app-spec 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/page.d.ts +35 -0
- package/dist/src/page.js +72 -0
- package/dist/src/schema.d.ts +6 -0
- package/dist/src/schema.js +2 -0
- package/dist/src/validate.js +2 -1
- package/package.json +1 -1
- package/schema/app-spec.v1.json +4 -0
package/dist/src/index.d.ts
CHANGED
|
@@ -8,3 +8,4 @@ export { leakProblems } from "./leak.js";
|
|
|
8
8
|
export { CLOUDCRANE_CATALOG, CLOUDCRANE_CATALOG_ID, catalogFor, cloudcraneCatalog } from "./catalog.js";
|
|
9
9
|
export { deriveBlueprint, type BlueprintV1 } from "./blueprint.js";
|
|
10
10
|
export { USES, USE_NAMES, NEEDS_ORIGIN, featuresOf, type Use } from "./uses.js";
|
|
11
|
+
export { pageOf, pageProblems, wrapPage, type PageApp } from "./page.js";
|
package/dist/src/index.js
CHANGED
|
@@ -10,3 +10,4 @@ export { leakProblems } from "./leak.js";
|
|
|
10
10
|
export { CLOUDCRANE_CATALOG, CLOUDCRANE_CATALOG_ID, catalogFor, cloudcraneCatalog } from "./catalog.js";
|
|
11
11
|
export { deriveBlueprint } from "./blueprint.js";
|
|
12
12
|
export { USES, USE_NAMES, NEEDS_ORIGIN, featuresOf } from "./uses.js";
|
|
13
|
+
export { pageOf, pageProblems, wrapPage } from "./page.js";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Spec } from "./schema.js";
|
|
2
|
+
/** The spec as the page it is, or null when it isn't one. */
|
|
3
|
+
export declare function pageOf(spec: Spec): string | null;
|
|
4
|
+
type Problem = {
|
|
5
|
+
at: string;
|
|
6
|
+
problem: string;
|
|
7
|
+
};
|
|
8
|
+
/** What a spec that says it's a page may hold: the page, full screen, and no logic of its own. */
|
|
9
|
+
export declare function pageProblems(spec: Spec): Problem[];
|
|
10
|
+
/** What an HTML app says about itself, to make its spec from. */
|
|
11
|
+
export type PageApp = {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
purpose: string;
|
|
15
|
+
html: string;
|
|
16
|
+
/** What made the spec, like "cloudcrane, from an HTML app" (the leak scan refuses one shaped like a call). */
|
|
17
|
+
generator: string;
|
|
18
|
+
/** The runtime version the spec is for; a page needs none, but the spec names the compiler's. */
|
|
19
|
+
runtime?: string;
|
|
20
|
+
data: {
|
|
21
|
+
version: number;
|
|
22
|
+
key: string;
|
|
23
|
+
};
|
|
24
|
+
journeys: Spec["journeys"];
|
|
25
|
+
promises?: Spec["promises"];
|
|
26
|
+
network?: Spec["network"];
|
|
27
|
+
uses?: Spec["uses"];
|
|
28
|
+
tests?: Spec["tests"];
|
|
29
|
+
icon?: string;
|
|
30
|
+
category?: string;
|
|
31
|
+
tags?: string[];
|
|
32
|
+
};
|
|
33
|
+
/** An HTML app as a spec whose one component is the page, full screen. Check it with validateSpec. */
|
|
34
|
+
export declare function wrapPage(app: PageApp): Record<string, unknown>;
|
|
35
|
+
export {};
|
package/dist/src/page.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { CLOUDCRANE_CATALOG_ID } from "./catalog.js";
|
|
2
|
+
// An app written as one HTML page, as a spec. The page is its one page-kit
|
|
3
|
+
// component, and `page` names it: the spec is then the page, and the compiler
|
|
4
|
+
// gives back its HTML as written, with no runtime. It runs, saves and is
|
|
5
|
+
// tested exactly as it would on its own (the same storage keys, the same
|
|
6
|
+
// address, the same tests), while it's described, versioned and checked like
|
|
7
|
+
// any spec. Without `page`, the same component runs in a frame among the
|
|
8
|
+
// spec's own screens.
|
|
9
|
+
/** The spec as the page it is, or null when it isn't one. */
|
|
10
|
+
export function pageOf(spec) {
|
|
11
|
+
if (!spec.page)
|
|
12
|
+
return null;
|
|
13
|
+
const c = spec.components[spec.page];
|
|
14
|
+
return c?.kit === "page" ? c.code : null;
|
|
15
|
+
}
|
|
16
|
+
/** What a spec that says it's a page may hold: the page, full screen, and no logic of its own. */
|
|
17
|
+
export function pageProblems(spec) {
|
|
18
|
+
if (!spec.page)
|
|
19
|
+
return [];
|
|
20
|
+
const c = spec.components[spec.page];
|
|
21
|
+
if (c?.kit !== "page")
|
|
22
|
+
return [{ at: "page", problem: "names no page-kit component" }];
|
|
23
|
+
const problems = [];
|
|
24
|
+
if (Object.keys(spec.components).length !== 1)
|
|
25
|
+
problems.push({ at: "page", problem: "the page is the spec's only component" });
|
|
26
|
+
if (Object.keys(c.inputs).length || Object.keys(c.events).length)
|
|
27
|
+
problems.push({ at: "page", problem: "the page takes no inputs and sends no events" });
|
|
28
|
+
const placed = spec.surface.messages.flatMap((m) => (m["updateComponents"]?.components ?? []));
|
|
29
|
+
if (placed.length !== 1 || placed[0]["component"] !== "Coded" || placed[0]["use"] !== spec.page)
|
|
30
|
+
problems.push({ at: "page", problem: "the screen can hold nothing but the page" });
|
|
31
|
+
const { functions, computed, vectors } = spec.logic;
|
|
32
|
+
if (Object.keys(functions).length || Object.keys(computed).length || vectors.length || spec.data.views.length || spec.data.migrations.length)
|
|
33
|
+
problems.push({ at: "page", problem: "a page has no logic, views or migrations of the spec's own: its code keeps its data" });
|
|
34
|
+
return problems;
|
|
35
|
+
}
|
|
36
|
+
// A page draws itself, so these only satisfy the spec's shape; they're never shown.
|
|
37
|
+
const TOKENS = {
|
|
38
|
+
color: { $type: "color", background: { $value: "#ffffff" }, "on-background": { $value: "#111111" }, primary: { $value: "#0f766e" } },
|
|
39
|
+
};
|
|
40
|
+
/** An HTML app as a spec whose one component is the page, full screen. Check it with validateSpec. */
|
|
41
|
+
export function wrapPage(app) {
|
|
42
|
+
return {
|
|
43
|
+
v: 1,
|
|
44
|
+
manifest: { runtime: app.runtime ?? "0.0.0", generator: app.generator, source: { fingerprint: `sha256:${"0".repeat(64)}` } },
|
|
45
|
+
module: {
|
|
46
|
+
id: app.id,
|
|
47
|
+
name: app.name,
|
|
48
|
+
purpose: app.purpose,
|
|
49
|
+
...(app.icon ? { icon: app.icon } : {}),
|
|
50
|
+
...(app.category ? { category: app.category } : {}),
|
|
51
|
+
...(app.tags?.length ? { tags: app.tags } : {}),
|
|
52
|
+
},
|
|
53
|
+
tokens: TOKENS,
|
|
54
|
+
surface: {
|
|
55
|
+
catalog: CLOUDCRANE_CATALOG_ID,
|
|
56
|
+
messages: [
|
|
57
|
+
{ version: "v0.9.1", createSurface: { surfaceId: app.id, catalogId: CLOUDCRANE_CATALOG_ID } },
|
|
58
|
+
{ version: "v0.9.1", updateComponents: { surfaceId: app.id, components: [{ id: "root", component: "Coded", use: "page", label: app.name }] } },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
data: { version: app.data.version, key: app.data.key, persist: "/saved", views: [], schema: { type: "object" }, initial: {}, migrations: [] },
|
|
62
|
+
logic: { consts: {}, functions: {}, computed: {}, vectors: [] },
|
|
63
|
+
network: app.network ?? [],
|
|
64
|
+
uses: app.uses ?? [],
|
|
65
|
+
journeys: app.journeys,
|
|
66
|
+
promises: app.promises ?? [],
|
|
67
|
+
tests: app.tests ?? [],
|
|
68
|
+
components: { page: { kind: "coded", kit: "page", inputs: {}, events: {}, code: app.html } },
|
|
69
|
+
page: "page",
|
|
70
|
+
mods: { open: [], locked: [] },
|
|
71
|
+
};
|
|
72
|
+
}
|
package/dist/src/schema.d.ts
CHANGED
|
@@ -377,6 +377,8 @@ export declare const SpecSchema: z.ZodEffects<z.ZodObject<{
|
|
|
377
377
|
inputs?: Record<string, "string" | "number" | "boolean" | "map" | "list"> | undefined;
|
|
378
378
|
events?: Record<string, Record<string, "string" | "number" | "boolean" | "map" | "list">> | undefined;
|
|
379
379
|
}>>>;
|
|
380
|
+
/** The page-kit component that is the whole app: the spec is then that page, compiled to its HTML as written (see page.ts). */
|
|
381
|
+
page: z.ZodOptional<z.ZodString>;
|
|
380
382
|
/** What a remix may change and what stays as the partner made it. */
|
|
381
383
|
mods: z.ZodDefault<z.ZodObject<{
|
|
382
384
|
open: z.ZodArray<z.ZodString, "many">;
|
|
@@ -473,6 +475,7 @@ export declare const SpecSchema: z.ZodEffects<z.ZodObject<{
|
|
|
473
475
|
open: string[];
|
|
474
476
|
locked: string[];
|
|
475
477
|
};
|
|
478
|
+
page?: string | undefined;
|
|
476
479
|
}, {
|
|
477
480
|
surface: {
|
|
478
481
|
catalog: string;
|
|
@@ -540,6 +543,7 @@ export declare const SpecSchema: z.ZodEffects<z.ZodObject<{
|
|
|
540
543
|
inputs?: Record<string, "string" | "number" | "boolean" | "map" | "list"> | undefined;
|
|
541
544
|
events?: Record<string, Record<string, "string" | "number" | "boolean" | "map" | "list">> | undefined;
|
|
542
545
|
}> | undefined;
|
|
546
|
+
page?: string | undefined;
|
|
543
547
|
network?: {
|
|
544
548
|
site: string;
|
|
545
549
|
why: string;
|
|
@@ -643,6 +647,7 @@ export declare const SpecSchema: z.ZodEffects<z.ZodObject<{
|
|
|
643
647
|
open: string[];
|
|
644
648
|
locked: string[];
|
|
645
649
|
};
|
|
650
|
+
page?: string | undefined;
|
|
646
651
|
}, {
|
|
647
652
|
surface: {
|
|
648
653
|
catalog: string;
|
|
@@ -710,6 +715,7 @@ export declare const SpecSchema: z.ZodEffects<z.ZodObject<{
|
|
|
710
715
|
inputs?: Record<string, "string" | "number" | "boolean" | "map" | "list"> | undefined;
|
|
711
716
|
events?: Record<string, Record<string, "string" | "number" | "boolean" | "map" | "list">> | undefined;
|
|
712
717
|
}> | undefined;
|
|
718
|
+
page?: string | undefined;
|
|
713
719
|
network?: {
|
|
714
720
|
site: string;
|
|
715
721
|
why: string;
|
package/dist/src/schema.js
CHANGED
|
@@ -176,6 +176,8 @@ export const SpecSchema = z
|
|
|
176
176
|
.record(Id, CodedComponent)
|
|
177
177
|
.refine((c) => Object.keys(c).length <= 5, "at most 5 components")
|
|
178
178
|
.default({}),
|
|
179
|
+
/** The page-kit component that is the whole app: the spec is then that page, compiled to its HTML as written (see page.ts). */
|
|
180
|
+
page: Id.optional(),
|
|
179
181
|
/** What a remix may change and what stays as the partner made it. */
|
|
180
182
|
mods: z
|
|
181
183
|
.object({ open: z.array(PointerPattern), locked: z.array(PointerPattern) })
|
package/dist/src/validate.js
CHANGED
|
@@ -3,6 +3,7 @@ import { catalogFor } from "./catalog.js";
|
|
|
3
3
|
import { leakProblems } from "./leak.js";
|
|
4
4
|
import { logicProblems } from "./logic.js";
|
|
5
5
|
import { SpecSchema } from "./schema.js";
|
|
6
|
+
import { pageProblems } from "./page.js";
|
|
6
7
|
// Everything wrong with a spec, collected rather than stopping at the first: the
|
|
7
8
|
// envelope's shape, then the A2UI surface, then references between parts.
|
|
8
9
|
const pathOf = (path) => path
|
|
@@ -46,7 +47,7 @@ export function validateSpec(input, catalog) {
|
|
|
46
47
|
for (const m of spec.data.migrations)
|
|
47
48
|
if (m.to !== m.from + 1 || m.to > spec.data.version)
|
|
48
49
|
problems.push({ at: `data.migrations ${m.from}`, problem: "steps go one version at a time, up to data.version" });
|
|
49
|
-
problems.push(...codedProblems(spec), ...logicProblems(spec), ...leakProblems(spec));
|
|
50
|
+
problems.push(...codedProblems(spec), ...pageProblems(spec), ...logicProblems(spec), ...leakProblems(spec));
|
|
50
51
|
for (const locked of spec.mods.locked)
|
|
51
52
|
if (spec.mods.open.includes(locked))
|
|
52
53
|
problems.push({ at: `mods ${locked}`, problem: "both open and locked" });
|
package/package.json
CHANGED