@jigs.design/host 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tom Bates
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,52 @@
1
- # Temporary Holding Version
1
+ # @jigs.design/host
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Defines a host: the contract between your product and the model. A host lists the components a spec may draw, the recipes built from them, the procedures it may call, the image hosts it may load from, and the design guidance and situations the model designs by. It holds schemas, not the code that runs a procedure.
4
+
5
+ Import it from server code and config. No browser package imports it; the browser imports `ActionEvent` as a type at most.
6
+
7
+ ```ts
8
+ import { catalog } from "@jigs.design/catalog";
9
+ import { defineHost, mutation, query } from "@jigs.design/host";
10
+ import { z } from "zod";
11
+
12
+ export const host = defineHost({
13
+ components: catalog,
14
+ procedures: {
15
+ orders: query({
16
+ input: z.object({ status: z.enum(["open", "archived"]) }),
17
+ output: z.array(z.object({ id: z.string(), total: z.number() })),
18
+ description: "Lists the caller's orders with the given status.",
19
+ }),
20
+ archiveOrder: mutation({
21
+ input: z.object({ id: z.string() }),
22
+ invalidates: ["orders"],
23
+ description: "Archives one of the caller's orders.",
24
+ }),
25
+ },
26
+ });
27
+ ```
28
+
29
+ ## Exports
30
+
31
+ | Export | What it is |
32
+ | ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
33
+ | `defineHost` | Builds a host from `components`, `procedures`, `recipes`, `imageHosts`, `guidance` and `situations` |
34
+ | `query`, `mutation`, `action` | The three kinds of procedure. An action is handled by the embedding page |
35
+ | `recipe` | A compound component written in Pickle from catalog components |
36
+ | `situation` | Design guidance for when the reader has a given job, with a checked example |
37
+ | `defineCatalog`, `defineComponent` | Build your own catalog |
38
+ | `string`, `number`, `boolean`, `oneOf`, `handler`, `image`, `link`, `list`, `block`, `inline`, `none` | Prop and children builders for components |
39
+ | `contentSecurityPolicy` | The page's `frame-src` and `img-src` directives, merged into yours |
40
+ | `CatalogProps`, `PropsOf`, `ActionEvent` | Types for a registry and for a page's `onAction` |
41
+
42
+ ## Source
43
+
44
+ - `src/host.ts`: `defineHost`, `contentSecurityPolicy` and `ActionEvent`.
45
+ - `src/catalog.ts`: catalogs, components and the prop builders.
46
+ - `src/procedures.ts`: `query`, `mutation`, `action` and invalidation scopes.
47
+ - `src/recipes.ts`: `recipe`, compiled against the host's catalog.
48
+ - `src/guidance.ts`: `situation` and the design guidance types; the host's replace the catalog's.
49
+
50
+ ## Docs
51
+
52
+ [Host](../../apps/docs/docs/host/index.mdx), [Catalog](../../apps/docs/docs/host/catalog.mdx), [Recipes](../../apps/docs/docs/host/recipes.mdx), [Design guidance](../../apps/docs/docs/host/guidance.mdx), [Queries](../../apps/docs/docs/host/queries.mdx), [Mutations](../../apps/docs/docs/host/mutations.mdx), [Actions](../../apps/docs/docs/host/actions.mdx).
@@ -0,0 +1,143 @@
1
+ import { DesignGuidance, GuidanceInput, SituationDefinition } from "./guidance.js";
2
+ import { ChildrenKind, ComponentSchema, PropSchema, Row } from "@jigs.design/runtime";
3
+ //#region src/catalog.d.ts
4
+ /**
5
+ * One prop of a catalog component. `schema` is what the browser checks;
6
+ * `value` exists only in types, so `PropsOf` can type a registry component.
7
+ */
8
+ export interface PropDefinition<TValue> {
9
+ /** For a handler: the props the component calls it with, in order, which the prompt prints as its signature. */
10
+ args?: ReadonlyArray<PropDefinition<unknown>>;
11
+ /** Pairs a value prop with its change handler, e.g. `value` with `onChange`. */
12
+ controlled?: string;
13
+ default?: boolean | number | string;
14
+ description?: string;
15
+ schema: PropSchema;
16
+ /**
17
+ * What the prompt calls an enum's values when it declares them once
18
+ * under Types, e.g. `ChartColor`. Without it, the prompt names them.
19
+ */
20
+ typeName?: string;
21
+ readonly value?: TValue;
22
+ }
23
+ interface PropOptions<TDefault> {
24
+ default?: TDefault;
25
+ description?: string;
26
+ }
27
+ export declare function string(options?: PropOptions<string> & {
28
+ controlled?: string;
29
+ }): PropDefinition<string>;
30
+ export declare function number(options?: PropOptions<number> & {
31
+ controlled?: string;
32
+ max?: number;
33
+ min?: number;
34
+ }): PropDefinition<number>;
35
+ export declare function boolean(options?: PropOptions<boolean> & {
36
+ controlled?: string;
37
+ }): PropDefinition<boolean>;
38
+ export declare function oneOf<const TValues extends ReadonlyArray<string>>(values: TValues, options?: PropOptions<TValues[number]> & {
39
+ controlled?: string;
40
+ typeName?: string;
41
+ }): PropDefinition<TValues[number]>;
42
+ /** An `https` URL on one of the host's image hosts. */
43
+ export declare function image(options?: PropOptions<string>): PropDefinition<string>;
44
+ /** An `http` or `https` URL that loads nothing until the user clicks it. */
45
+ export declare function link(options?: PropOptions<string>): PropDefinition<string>;
46
+ /** A list of flat objects, such as chart rows or select options. */
47
+ export declare function list(options?: PropOptions<never>): PropDefinition<ReadonlyArray<Row>>;
48
+ type ValueOfDefinition<D> = D extends PropDefinition<infer V> ? V : never;
49
+ type HandlerArgs<TArgs extends ReadonlyArray<PropDefinition<unknown>>> = { [K in keyof TArgs]: ValueOfDefinition<TArgs[K]>; };
50
+ /**
51
+ * A function prop. `args` are what the component passes back to the
52
+ * program: they type the registry component's prop, and the prompt prints
53
+ * them as the handler's signature, `(value: number) => void`.
54
+ */
55
+ export declare function handler<const TArgs extends ReadonlyArray<PropDefinition<unknown>> = []>(args?: TArgs, options?: PropOptions<never>): PropDefinition<(...values: HandlerArgs<TArgs>) => void>;
56
+ export declare const none: () => ChildrenKind;
57
+ export declare const inline: () => ChildrenKind;
58
+ export declare const block: () => ChildrenKind;
59
+ export type PropDefinitions = Readonly<Record<string, PropDefinition<unknown>>>;
60
+ /**
61
+ * What a component is to the server's design checks. Only the catalog knows
62
+ * which of its components is a card or a button, and which props make a
63
+ * button the primary one, so it says so here.
64
+ */
65
+ export type ComponentDesign = {
66
+ /**
67
+ * The prop values that make it the primary action, with the props'
68
+ * defaults filled in, e.g. `{ variant: "solid", color: "primary" }`.
69
+ * A view has at most one.
70
+ */
71
+ primary?: Readonly<Record<string, boolean | number | string>>;
72
+ /**
73
+ * A control the reader acts with. Each counts toward a chat block's
74
+ * limit of two; one with no children needs its `ariaLabel`.
75
+ */
76
+ role: "action";
77
+ } | {
78
+ /**
79
+ * `chart` draws data and needs its `ariaLabel`; `field` takes input
80
+ * and needs a `label` or an `ariaLabel`; `surface` is a card, and
81
+ * one never sits inside another.
82
+ */
83
+ role: "chart" | "field" | "surface";
84
+ };
85
+ export interface ComponentDefinition<TName extends string = string, TProps extends PropDefinitions = PropDefinitions> {
86
+ /** Plausible wrong uses, and the concrete thing to reach for instead. */
87
+ avoid: ReadonlyArray<{
88
+ useInstead: string;
89
+ when: string;
90
+ }>;
91
+ children: ChildrenKind;
92
+ description: string;
93
+ /** What the design checks take it for; a component without one is none of those. */
94
+ design?: ComponentDesign;
95
+ name: TName;
96
+ props: TProps;
97
+ }
98
+ export declare function defineComponent<const TName extends string, const TProps extends PropDefinitions>(name: TName, definition: {
99
+ avoid?: ReadonlyArray<{
100
+ useInstead: string;
101
+ when: string;
102
+ }>;
103
+ children: ChildrenKind;
104
+ description: string;
105
+ design?: ComponentDesign;
106
+ props: TProps;
107
+ }): ComponentDefinition<TName, TProps>;
108
+ export interface Catalog<TComponents extends ReadonlyArray<ComponentDefinition> = ReadonlyArray<ComponentDefinition>> {
109
+ components: TComponents;
110
+ /**
111
+ * Design guidance for every UI drawn with this catalog. Not part of
112
+ * `version`: it changes no compiled program.
113
+ */
114
+ guidance: DesignGuidance;
115
+ /** Guidance for what the reader will do with a UI; the model picks by `when`. */
116
+ situations: ReadonlyArray<SituationDefinition>;
117
+ /** Raise it when a component or a prop changes. */
118
+ version: number;
119
+ }
120
+ /**
121
+ * A catalog. Throws on two components with one name, a version that is not
122
+ * a non-negative integer, or a situation name that is not kebab-case or is
123
+ * listed twice.
124
+ */
125
+ export declare function defineCatalog<const TComponents extends ReadonlyArray<ComponentDefinition>>(definition: {
126
+ components: TComponents;
127
+ /** A string for every placement, or `{ shared, chat, document }`. */
128
+ guidance?: GuidanceInput;
129
+ situations?: ReadonlyArray<SituationDefinition>;
130
+ version: number;
131
+ }): Catalog<TComponents>;
132
+ /** The props a registry component receives for a catalog component. */
133
+ export type PropsOf<C extends ComponentDefinition> = { [K in keyof C["props"]]?: ValueOfDefinition<C["props"][K]>; };
134
+ /** The props of every component in a catalog, by tag name. Type a registry with it. */
135
+ export type CatalogProps<TCatalog extends Catalog> = { [K in TCatalog["components"][number]["name"]]: PropsOf<ComponentNamed<TCatalog, K>>; };
136
+ /** A catalog component by name. */
137
+ export type ComponentNamed<TCatalog extends Catalog, TName extends string> = Extract<TCatalog["components"][number], {
138
+ name: TName;
139
+ }>;
140
+ /** What the browser checks for a component. */
141
+ export declare function componentSchema(component: ComponentDefinition): ComponentSchema;
142
+ //#endregion
143
+ export {};
@@ -0,0 +1,99 @@
1
+ import { checkSituations, normalizeGuidance } from "./guidance.js";
2
+ //#region src/catalog.ts
3
+ function prop(schema, options) {
4
+ const definition = { schema };
5
+ if (options.controlled !== void 0) definition.controlled = options.controlled;
6
+ if (options.default !== void 0) definition.default = options.default;
7
+ if (options.description !== void 0) definition.description = options.description;
8
+ if (options.typeName !== void 0) definition.typeName = options.typeName;
9
+ return definition;
10
+ }
11
+ function string(options = {}) {
12
+ return prop({ kind: "string" }, options);
13
+ }
14
+ function number(options = {}) {
15
+ const schema = { kind: "number" };
16
+ if (options.min !== void 0) schema.min = options.min;
17
+ if (options.max !== void 0) schema.max = options.max;
18
+ return prop(schema, options);
19
+ }
20
+ function boolean(options = {}) {
21
+ return prop({ kind: "boolean" }, options);
22
+ }
23
+ function oneOf(values, options = {}) {
24
+ return prop({
25
+ kind: "enum",
26
+ values
27
+ }, options);
28
+ }
29
+ /** An `https` URL on one of the host's image hosts. */
30
+ function image(options = {}) {
31
+ return prop({ kind: "image" }, options);
32
+ }
33
+ /** An `http` or `https` URL that loads nothing until the user clicks it. */
34
+ function link(options = {}) {
35
+ return prop({ kind: "link" }, options);
36
+ }
37
+ /** A list of flat objects, such as chart rows or select options. */
38
+ function list(options = {}) {
39
+ return prop({ kind: "list" }, options);
40
+ }
41
+ /**
42
+ * A function prop. `args` are what the component passes back to the
43
+ * program: they type the registry component's prop, and the prompt prints
44
+ * them as the handler's signature, `(value: number) => void`.
45
+ */
46
+ function handler(args, options = {}) {
47
+ const definition = prop({ kind: "handler" }, options);
48
+ return args === void 0 || args.length === 0 ? definition : {
49
+ ...definition,
50
+ args
51
+ };
52
+ }
53
+ const none = () => "none";
54
+ const inline = () => "inline";
55
+ const block = () => "block";
56
+ function defineComponent(name, definition) {
57
+ if (!/^[a-z][A-Za-z0-9]*$/.test(name)) throw new Error(`Component name "${name}" must start with a lowercase letter and use letters and digits only`);
58
+ const component = {
59
+ avoid: definition.avoid ?? [],
60
+ children: definition.children,
61
+ description: definition.description,
62
+ name,
63
+ props: definition.props
64
+ };
65
+ if (definition.design !== void 0) component.design = definition.design;
66
+ return component;
67
+ }
68
+ /**
69
+ * A catalog. Throws on two components with one name, a version that is not
70
+ * a non-negative integer, or a situation name that is not kebab-case or is
71
+ * listed twice.
72
+ */
73
+ function defineCatalog(definition) {
74
+ if (!Number.isInteger(definition.version) || definition.version < 0) throw new Error("Catalog version must be a non-negative integer");
75
+ const seen = /* @__PURE__ */ new Set();
76
+ for (const component of definition.components) {
77
+ if (seen.has(component.name)) throw new Error(`Catalog lists <${component.name}> twice`);
78
+ seen.add(component.name);
79
+ }
80
+ const situations = definition.situations ?? [];
81
+ checkSituations(situations);
82
+ return {
83
+ components: definition.components,
84
+ guidance: normalizeGuidance(definition.guidance),
85
+ situations,
86
+ version: definition.version
87
+ };
88
+ }
89
+ /** What the browser checks for a component. */
90
+ function componentSchema(component) {
91
+ const props = {};
92
+ for (const [name, definition] of Object.entries(component.props)) props[name] = definition.schema;
93
+ return {
94
+ children: component.children,
95
+ props
96
+ };
97
+ }
98
+ //#endregion
99
+ export { block, boolean, componentSchema, defineCatalog, defineComponent, handler, image, inline, link, list, none, number, oneOf, string };
@@ -0,0 +1,44 @@
1
+ //#region src/guidance.d.ts
2
+ /**
3
+ * Design guidance: how to design a good UI with a catalog, not only a valid
4
+ * one. Foundations apply to every UI; a situation applies when its `when`
5
+ * matches what the reader will do with the UI. See
6
+ * `docs/design-guidance/rfc.md`.
7
+ */
8
+ /** Guidance for every placement (`shared`), and for one prompt mode each. */
9
+ export interface DesignGuidance {
10
+ /** A UI in a chat reply. */
11
+ chat?: string;
12
+ /** A UI that is the whole reply. */
13
+ document?: string;
14
+ /** Every UI, in either mode. */
15
+ shared?: string;
16
+ }
17
+ /** What `guidance` takes: a string, for every placement, or the three parts. */
18
+ export type GuidanceInput = DesignGuidance | string;
19
+ /**
20
+ * Conditional design guidance, keyed on what the reader does with the UI.
21
+ * The prompt lists every situation; the model follows the one whose `when`
22
+ * matches.
23
+ */
24
+ export interface SituationDefinition {
25
+ /** Pickle the server compiles against the host at startup. */
26
+ example?: string;
27
+ /** How to design for this situation, stating where it differs from the foundations. */
28
+ guidance: string;
29
+ /** Kebab-case label for apps, logs and evals. The model selects on `when`. */
30
+ name: string;
31
+ /** What the reader will do with the UI, as a sentence. */
32
+ when: string;
33
+ }
34
+ /**
35
+ * A situation. Throws when `name` is not kebab-case, or `when` or `guidance`
36
+ * is empty. The server compiles `example` at startup.
37
+ */
38
+ export declare function situation(definition: {
39
+ example?: string;
40
+ guidance: string;
41
+ name: string;
42
+ when: string;
43
+ }): SituationDefinition;
44
+ //#endregion
@@ -0,0 +1,52 @@
1
+ //#region src/guidance.ts
2
+ const SITUATION_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
3
+ /** Throws unless `name` is kebab-case: lower-case letters and digits, single hyphens. */
4
+ function checkSituationName(name) {
5
+ if (!SITUATION_NAME.test(name)) throw new Error(`Situation name "${name}" must be kebab-case: lower-case letters and digits, joined by single hyphens`);
6
+ }
7
+ function isSharedOnly(input) {
8
+ return typeof input === "string";
9
+ }
10
+ /** Guidance with each part trimmed and empty parts left out. */
11
+ function normalizeGuidance(input) {
12
+ const given = input ?? {};
13
+ const parts = isSharedOnly(given) ? { shared: given } : given;
14
+ const out = {};
15
+ const chat = parts.chat?.trim() ?? "";
16
+ const document = parts.document?.trim() ?? "";
17
+ const shared = parts.shared?.trim() ?? "";
18
+ if (chat !== "") out.chat = chat;
19
+ if (document !== "") out.document = document;
20
+ if (shared !== "") out.shared = shared;
21
+ return out;
22
+ }
23
+ /**
24
+ * A situation. Throws when `name` is not kebab-case, or `when` or `guidance`
25
+ * is empty. The server compiles `example` at startup.
26
+ */
27
+ function situation(definition) {
28
+ checkSituationName(definition.name);
29
+ const when = definition.when.trim();
30
+ const guidance = definition.guidance.trim();
31
+ if (when === "") throw new Error(`Situation "${definition.name}" needs a \`when\``);
32
+ if (guidance === "") throw new Error(`Situation "${definition.name}" needs \`guidance\``);
33
+ const built = {
34
+ guidance,
35
+ name: definition.name,
36
+ when
37
+ };
38
+ const example = definition.example?.trim() ?? "";
39
+ if (example !== "") built.example = example;
40
+ return built;
41
+ }
42
+ /** Throws on a situation name that is not kebab-case or is listed twice. */
43
+ function checkSituations(situations) {
44
+ const seen = /* @__PURE__ */ new Set();
45
+ for (const entry of situations) {
46
+ checkSituationName(entry.name);
47
+ if (seen.has(entry.name)) throw new Error(`Situation "${entry.name}" is listed twice`);
48
+ seen.add(entry.name);
49
+ }
50
+ }
51
+ //#endregion
52
+ export { checkSituations, normalizeGuidance, situation };
package/dist/host.d.ts ADDED
@@ -0,0 +1,88 @@
1
+ import { DesignGuidance, GuidanceInput, SituationDefinition } from "./guidance.js";
2
+ import { Catalog } from "./catalog.js";
3
+ import { ActionDefinition, MutationDefinition, Parsed, Procedures, QueryDefinition } from "./procedures.js";
4
+ import { RecipeDefinition } from "./recipes.js";
5
+ import { z } from "zod";
6
+ //#region src/host.d.ts
7
+ export interface Host<TProcedures extends Procedures = Procedures, TCatalog extends Catalog = Catalog> {
8
+ components: TCatalog;
9
+ /** The host's design guidance, or the catalog's when the host sets none. */
10
+ guidance: DesignGuidance;
11
+ imageHosts: ReadonlyArray<string>;
12
+ procedures: TProcedures;
13
+ /** Compound components written in Pickle; the server checks them at startup. */
14
+ recipes: ReadonlyArray<RecipeDefinition>;
15
+ /** The host's situations, or the catalog's when the host sets none. */
16
+ situations: ReadonlyArray<SituationDefinition>;
17
+ }
18
+ /**
19
+ * What a page's `onAction` receives for a host: one member per action, with
20
+ * the input as its schema parsed it. Import it as a type in the browser:
21
+ * `onAction={(action: ActionEvent<typeof host>) => …}`.
22
+ */
23
+ export type ActionEvent<THost extends {
24
+ procedures: Procedures;
25
+ }> = { [K in keyof THost["procedures"] & string]: THost["procedures"][K] extends {
26
+ input: infer I extends z.ZodType | undefined;
27
+ kind: ActionDefinition["kind"];
28
+ } ? {
29
+ input: Parsed<I>;
30
+ name: K;
31
+ } : never; }[keyof THost["procedures"] & string];
32
+ /**
33
+ * What a scope may return for query `Q`: an input as the view would send it
34
+ * (the view's queries whose input contains it refresh), an array of those,
35
+ * or `true`.
36
+ */
37
+ type ScopeResult<Q extends QueryDefinition> = true | QueryInput<Q> | ReadonlyArray<QueryInput<Q>>;
38
+ type QueryInput<Q extends QueryDefinition> = Q["input"] extends z.ZodType ? z.input<Q["input"]> : undefined;
39
+ type CheckedScopes<TProcedures extends Procedures, I extends z.ZodType | undefined, O extends z.ZodType | undefined, S> = { readonly [Q in keyof S]: Q extends keyof TProcedures ? TProcedures[Q] extends QueryDefinition ? true | ((input: Parsed<I>, output: Parsed<O>) => ScopeResult<TProcedures[Q]>) : never : never; };
40
+ /** Each mutation's scopes, with returns checked against their query's input. */
41
+ type CheckedProcedures<TProcedures extends Procedures> = { readonly [K in keyof TProcedures]: TProcedures[K] extends MutationDefinition<infer I, infer O, infer S> ? {
42
+ scopes: CheckedScopes<TProcedures, I, O, S>;
43
+ } : TProcedures[K]; };
44
+ /**
45
+ * The product's boundary with the spec. Throws at startup when a schema has
46
+ * no JSON form, `invalidates` names something that is not a query, a
47
+ * procedure name is not an identifier, an image host is malformed, or a
48
+ * situation name is not kebab-case or is listed twice.
49
+ *
50
+ * `guidance` and `situations` replace the catalog's when set; to extend the
51
+ * catalog's, spread them in.
52
+ */
53
+ export declare function defineHost<const TProcedures extends Procedures, const TCatalog extends Catalog>(definition: {
54
+ components: TCatalog;
55
+ /** A string for every placement, or `{ shared, chat, document }`. */
56
+ guidance?: GuidanceInput;
57
+ imageHosts?: ReadonlyArray<string>;
58
+ procedures: TProcedures & CheckedProcedures<TProcedures>;
59
+ recipes?: ReadonlyArray<RecipeDefinition>;
60
+ situations?: ReadonlyArray<SituationDefinition>;
61
+ }): Host<TProcedures, TCatalog>;
62
+ /** A policy as directive names and their sources, e.g. `{ "img-src": ["'self'"] }`. */
63
+ export type PolicyDirectives = Readonly<Record<string, ReadonlyArray<string>>>;
64
+ /** The embedding page's policy, and where it must not be sent. */
65
+ export interface PagePolicy {
66
+ /**
67
+ * False for the endpoint, true for every other URL. The endpoint serves the
68
+ * sandbox frame with its own policy; sending the page's there replaces it,
69
+ * and the frame then refuses to run.
70
+ */
71
+ appliesTo: (url: string | URL) => boolean;
72
+ /** The page's directives with the ones Pickle needs merged in. */
73
+ directives: PolicyDirectives;
74
+ /** `directives` as one `Content-Security-Policy` header value. */
75
+ header: string;
76
+ }
77
+ /**
78
+ * The embedding page's policy: the page's own `directives` plus what Pickle
79
+ * needs, `frame-src` for the sandbox frame and `img-src` for the host's image
80
+ * hosts. Send `header` on every response `appliesTo` accepts; the endpoint's
81
+ * frame carries its own policy, and a page policy sent there replaces it.
82
+ */
83
+ export declare function contentSecurityPolicy(host: Host, options: {
84
+ directives?: PolicyDirectives;
85
+ endpoint: string;
86
+ }): PagePolicy;
87
+ //#endregion
88
+ export {};
package/dist/host.js ADDED
@@ -0,0 +1,104 @@
1
+ import { checkSituations, normalizeGuidance } from "./guidance.js";
2
+ import { isCallPolicy } from "./procedures.js";
3
+ import { isImageHostEntry, normalizeImageHost } from "@jigs.design/runtime";
4
+ import { z } from "zod";
5
+ //#region src/host.ts
6
+ const PROCEDURE_NAME = /^[A-Za-z][A-Za-z0-9_]*$/;
7
+ function checkJsonSchema(label, schema) {
8
+ try {
9
+ z.toJSONSchema(schema);
10
+ } catch (error) {
11
+ throw new Error(`${label} has no JSON Schema form, so it cannot travel as JSON. Use plain values, such as an ISO string for a date.`, { cause: error });
12
+ }
13
+ }
14
+ /**
15
+ * The product's boundary with the spec. Throws at startup when a schema has
16
+ * no JSON form, `invalidates` names something that is not a query, a
17
+ * procedure name is not an identifier, an image host is malformed, or a
18
+ * situation name is not kebab-case or is listed twice.
19
+ *
20
+ * `guidance` and `situations` replace the catalog's when set; to extend the
21
+ * catalog's, spread them in.
22
+ */
23
+ function defineHost(definition) {
24
+ const { procedures } = definition;
25
+ const recipes = definition.recipes ?? [];
26
+ const situations = definition.situations ?? definition.components.situations;
27
+ checkSituations(situations);
28
+ const components = new Set(definition.components.components.map((component) => component.name));
29
+ const recipeNames = /* @__PURE__ */ new Set();
30
+ for (const entry of recipes) {
31
+ if (components.has(entry.name)) throw new Error(`Recipe "${entry.name}" has the name of a catalog component`);
32
+ if (recipeNames.has(entry.name)) throw new Error(`Recipe "${entry.name}" is listed twice`);
33
+ recipeNames.add(entry.name);
34
+ }
35
+ for (const [name, procedure] of Object.entries(procedures)) {
36
+ if (!PROCEDURE_NAME.test(name)) throw new Error(`Procedure name "${name}" must be an identifier: letters, digits and underscores, starting with a letter`);
37
+ if (procedure.input !== void 0) checkJsonSchema(`The input of "${name}"`, procedure.input);
38
+ if (procedure.output !== void 0) checkJsonSchema(`The output of "${name}"`, procedure.output);
39
+ if (procedure.kind !== "action" && procedure.projection !== void 0) checkJsonSchema(`The projection of "${name}"`, procedure.projection.schema);
40
+ if (procedure.kind === "mutation") {
41
+ if (!isCallPolicy(procedure.policy)) throw new Error(`"${name}" has policy ${JSON.stringify(procedure.policy)}: write "allow", "confirm" or "deny"`);
42
+ if (procedure.policy === "confirm" && procedure.confirm === void 0) throw new Error(`"${name}" asks for confirmation but has no confirm message: give it confirm: (input) => text`);
43
+ for (const target of procedure.invalidates) if ((Object.hasOwn(procedures, target) ? procedures[target] : void 0)?.kind !== "query") throw new Error(`"${name}" invalidates "${target}", which is not a query on this host`);
44
+ }
45
+ }
46
+ const imageHosts = (definition.imageHosts ?? []).map((entry) => {
47
+ if (!isImageHostEntry(entry)) throw new Error(`Invalid image host "${entry}"; use a hostname such as "cdn.example.com" or "*.example.com", optionally with a path prefix ending in "/"`);
48
+ return normalizeImageHost(entry);
49
+ });
50
+ return {
51
+ components: definition.components,
52
+ guidance: definition.guidance === void 0 ? definition.components.guidance : normalizeGuidance(definition.guidance),
53
+ imageHosts,
54
+ procedures,
55
+ recipes,
56
+ situations
57
+ };
58
+ }
59
+ /** The source a CSP needs for `endpoint`: `'self'` for a path, the origin for a URL. */
60
+ function endpointSource(endpoint) {
61
+ if (endpoint.startsWith("/")) return "'self'";
62
+ return new URL(endpoint).origin;
63
+ }
64
+ /**
65
+ * Where a directive Pickle adds takes its sources from when the page has not
66
+ * set it: once the directive exists, the browser stops falling back.
67
+ */
68
+ const FALLBACKS = /* @__PURE__ */ new Map([["frame-src", ["child-src", "default-src"]], ["img-src", ["default-src"]]]);
69
+ /** Adds `extra` to `directives`, keeping each source once and dropping a `'none'` that now has company. */
70
+ function mergeDirectives(directives, extra) {
71
+ const merged = new Map(Object.entries(directives).map(([name, sources]) => [name, [...sources]]));
72
+ for (const [name, sources] of Object.entries(extra)) {
73
+ const fallback = (FALLBACKS.get(name) ?? []).map((other) => directives[other]).find((found) => found !== void 0);
74
+ const list = merged.get(name) ?? [...fallback ?? []];
75
+ for (const source of sources) if (!list.includes(source)) list.push(source);
76
+ merged.set(name, list.length > 1 ? list.filter((source) => source !== "'none'") : list);
77
+ }
78
+ return Object.fromEntries(merged);
79
+ }
80
+ /**
81
+ * The embedding page's policy: the page's own `directives` plus what Pickle
82
+ * needs, `frame-src` for the sandbox frame and `img-src` for the host's image
83
+ * hosts. Send `header` on every response `appliesTo` accepts; the endpoint's
84
+ * frame carries its own policy, and a page policy sent there replaces it.
85
+ */
86
+ function contentSecurityPolicy(host, options) {
87
+ const directives = mergeDirectives(options.directives ?? {}, {
88
+ "frame-src": [endpointSource(options.endpoint)],
89
+ "img-src": ["'self'", ...host.imageHosts.map((entry) => `https://${entry}`)]
90
+ });
91
+ const absolute = !options.endpoint.startsWith("/");
92
+ const endpoint = new URL(options.endpoint, "https://page.invalid");
93
+ const path = endpoint.pathname.replace(/\/$/, "");
94
+ return {
95
+ appliesTo: (url) => {
96
+ const target = new URL(url, endpoint);
97
+ return !((!absolute || target.origin === endpoint.origin) && (target.pathname === path || target.pathname.startsWith(`${path}/`)));
98
+ },
99
+ directives,
100
+ header: Object.entries(directives).map(([name, sources]) => [name, ...sources].join(" ")).join("; ")
101
+ };
102
+ }
103
+ //#endregion
104
+ export { contentSecurityPolicy, defineHost };
@@ -0,0 +1,6 @@
1
+ import { DesignGuidance, GuidanceInput, SituationDefinition, situation } from "./guidance.js";
2
+ import { Catalog, CatalogProps, ComponentDefinition, ComponentDesign, ComponentNamed, PropDefinition, PropDefinitions, PropsOf, block, boolean, componentSchema, defineCatalog, defineComponent, handler, image, inline, link, list, none, number, oneOf, string } from "./catalog.js";
3
+ import { ActionDefinition, CallPolicy, InputOf, InvalidationScope, InvalidationScopes, MutationDefinition, OutputOf, ProcedureDefinition, Procedures, Projection, QueryDefinition, action, dataSchema, isCallPolicy, mutation, query } from "./procedures.js";
4
+ import { RecipeDefinition, recipe } from "./recipes.js";
5
+ import { ActionEvent, Host, PagePolicy, PolicyDirectives, contentSecurityPolicy, defineHost } from "./host.js";
6
+ export { type ActionDefinition, type ActionEvent, type CallPolicy, type Catalog, type CatalogProps, type ComponentDefinition, type ComponentDesign, type ComponentNamed, type DesignGuidance, type GuidanceInput, type Host, type InputOf, type InvalidationScope, type InvalidationScopes, type MutationDefinition, type OutputOf, type PagePolicy, type PolicyDirectives, type ProcedureDefinition, type Procedures, type Projection, type PropDefinition, type PropDefinitions, type PropsOf, type QueryDefinition, type RecipeDefinition, type SituationDefinition, action, block, boolean, componentSchema, contentSecurityPolicy, dataSchema, defineCatalog, defineComponent, defineHost, handler, image, inline, isCallPolicy, link, list, mutation, none, number, oneOf, query, recipe, situation, string };
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import { situation } from "./guidance.js";
2
+ import { block, boolean, componentSchema, defineCatalog, defineComponent, handler, image, inline, link, list, none, number, oneOf, string } from "./catalog.js";
3
+ import { action, dataSchema, isCallPolicy, mutation, query } from "./procedures.js";
4
+ import { contentSecurityPolicy, defineHost } from "./host.js";
5
+ import { recipe } from "./recipes.js";
6
+ export { action, block, boolean, componentSchema, contentSecurityPolicy, dataSchema, defineCatalog, defineComponent, defineHost, handler, image, inline, isCallPolicy, link, list, mutation, none, number, oneOf, query, recipe, situation, string };
@@ -0,0 +1,148 @@
1
+ import { JsonValue } from "@jigs.design/runtime";
2
+ import { z } from "zod";
3
+ //#region src/procedures.d.ts
4
+ /**
5
+ * The part of a procedure's output its views read, and how to make it. The
6
+ * server parses the whole output, as the contract with `run`, then calls
7
+ * `project` on it and parses the result with `schema`; that is what the
8
+ * view receives. The prompt shows `schema` in the output's place, so a
9
+ * generated API's large records reach the model as the fields a UI shows.
10
+ */
11
+ export interface Projection<TOutput = unknown> {
12
+ project(output: TOutput): Unparsed;
13
+ schema: z.ZodType;
14
+ }
15
+ /** What a projection's `project` returns, before its `schema` parses it. */
16
+ type Unparsed = z.input<z.ZodType>;
17
+ /** A procedure the spec reads with `useQuery`. */
18
+ export interface QueryDefinition<TInput extends z.ZodType | undefined = z.ZodType | undefined, TOutput extends z.ZodType = z.ZodType> {
19
+ description: string;
20
+ input: TInput;
21
+ kind: "query";
22
+ output: TOutput;
23
+ /** What views read instead of the whole output; see `Projection`. */
24
+ projection?: Projection<z.output<TOutput>>;
25
+ }
26
+ /**
27
+ * Which inputs of one query a mutation refreshes. `true` refreshes every
28
+ * input. A function receives the mutation's parsed input and output, and
29
+ * returns the query input to refresh, an array of them, or `true`, all of
30
+ * which travel as JSON. `defineHost` narrows the return to the query's input.
31
+ */
32
+ export type InvalidationScope<TInput = never, TOutput = never> = true | ((input: TInput, output: TOutput) => JsonValue);
33
+ /** Per query name, which of its inputs a mutation refreshes. */
34
+ export type InvalidationScopes<TInput = never, TOutput = never> = Readonly<Record<string, InvalidationScope<TInput, TOutput>>>;
35
+ /**
36
+ * What the server does with a call to a mutation: run it (`"allow"`), ask
37
+ * the user through the page first (`"confirm"`), or refuse it with
38
+ * `FORBIDDEN` (`"deny"`). A denied mutation stays in the prompt; the refusal
39
+ * happens at call time, never by hiding the procedure.
40
+ */
41
+ export type CallPolicy = "allow" | "confirm" | "deny";
42
+ /**
43
+ * Whether `policy` is one of the three. A plain-JavaScript host, or a server
44
+ * override that returns a Promise, can hand over anything; the server denies
45
+ * a call whose policy is not one of these rather than running it.
46
+ */
47
+ export declare function isCallPolicy(policy: CallPolicy): boolean;
48
+ /** A procedure the spec calls with `useMutation`. */
49
+ export interface MutationDefinition<TInput extends z.ZodType | undefined = z.ZodType | undefined, TOutput extends z.ZodType | undefined = z.ZodType | undefined, TScopes extends InvalidationScopes = InvalidationScopes> {
50
+ /**
51
+ * The text the page shows when it asks the user to confirm a call, from
52
+ * the parsed input. The host writes it, never the model.
53
+ */
54
+ confirm?(input: Parsed<TInput>): string;
55
+ description: string;
56
+ input: TInput;
57
+ /** Every query a success refreshes. */
58
+ invalidates: ReadonlyArray<string>;
59
+ kind: "mutation";
60
+ output: TOutput;
61
+ /** Whether a call runs; the server's `policy` option may override it. */
62
+ policy: CallPolicy;
63
+ /** What views read instead of the whole output; see `Projection`. */
64
+ projection?: Projection<Parsed<TOutput>>;
65
+ /** For each name in `invalidates`, which of that query's inputs refresh. */
66
+ scopes: TScopes;
67
+ }
68
+ /**
69
+ * A procedure the spec calls with `useMutation` that the embedding page
70
+ * handles instead of the server: the server checks the input and the view
71
+ * hands it to `onAction`. It has no output and refreshes nothing.
72
+ */
73
+ export interface ActionDefinition<TInput extends z.ZodType | undefined = z.ZodType | undefined> {
74
+ description: string;
75
+ input: TInput;
76
+ kind: "action";
77
+ output: undefined;
78
+ }
79
+ export type ProcedureDefinition = ActionDefinition | MutationDefinition | QueryDefinition;
80
+ export type Procedures = Readonly<Record<string, ProcedureDefinition>>;
81
+ /**
82
+ * The schema of what a procedure's views read: its projection's when it has
83
+ * one, else its output; `undefined` for an action or a procedure with no
84
+ * output. The prompt shows it, and the member check reads against it.
85
+ */
86
+ export declare function dataSchema(procedure: ProcedureDefinition): z.ZodType | undefined;
87
+ /** What `run` receives for a procedure: the parsed input, or `undefined` when it takes none. */
88
+ export type InputOf<P extends ProcedureDefinition> = P["input"] extends z.ZodType ? z.output<P["input"]> : undefined;
89
+ /** What `run` returns for a procedure, before the server parses it. */
90
+ export type OutputOf<P extends ProcedureDefinition> = P["output"] extends z.ZodType ? z.input<P["output"]> : unknown;
91
+ /** How `query` and `mutation` take a projection: `project` returns what `schema` parses. */
92
+ interface ProjectionOption<TOutput, TProjection extends z.ZodType> {
93
+ project: (output: TOutput) => z.input<TProjection>;
94
+ schema: TProjection;
95
+ }
96
+ export declare function query<TOutput extends z.ZodType, TInput extends z.ZodType | undefined = undefined, TProjection extends z.ZodType = z.ZodType>(definition: {
97
+ description: string;
98
+ input?: TInput;
99
+ output: TOutput;
100
+ /** What views read instead of the whole output; see `Projection`. */
101
+ projection?: ProjectionOption<z.output<TOutput>, TProjection>;
102
+ }): QueryDefinition<TInput, TOutput>;
103
+ /** A schema's parsed value, or `undefined` when there is no schema. */
104
+ export type Parsed<T extends z.ZodType | undefined> = T extends z.ZodType ? z.output<T> : undefined;
105
+ /**
106
+ * The scopes an `invalidates` value means. A list or nothing leaves `TScopes`
107
+ * at its default, with a `string` key, and refreshes every input of each name.
108
+ */
109
+ type ScopesOf<TScopes, TNames extends ReadonlyArray<string>> = string extends keyof TScopes ? { readonly [K in TNames[number]]: true; } : { readonly [K in keyof TScopes]: Extract<TScopes[K], InvalidationScope>; };
110
+ /**
111
+ * Any function, by a member every function has. It has no call signature on
112
+ * purpose: see `mutation`'s `TScopes`.
113
+ */
114
+ interface ScopeFunction {
115
+ readonly name: string;
116
+ }
117
+ /** What `mutation` infers scopes from: `true` or a function, per name. */
118
+ type WrittenScope = ScopeFunction | true;
119
+ type WrittenScopes = Readonly<Record<string, WrittenScope>>;
120
+ export declare function mutation<TInput extends z.ZodType | undefined = undefined, TOutput extends z.ZodType | undefined = undefined, TProjection extends z.ZodType = z.ZodType, const TScopes extends WrittenScopes = WrittenScopes, const TNames extends ReadonlyArray<string> = readonly []>(definition: {
121
+ /**
122
+ * What the page asks the user before a call runs, written from the parsed
123
+ * input. With it, `policy` defaults to `"confirm"`.
124
+ */
125
+ confirm?: (input: Parsed<TInput>) => string;
126
+ description: string;
127
+ input?: TInput;
128
+ /**
129
+ * The queries a success refreshes. A list of names refreshes every input
130
+ * of each; an object keyed by name scopes each one (`InvalidationScope`).
131
+ */
132
+ invalidates?: TNames | { readonly [K in keyof TScopes]: TScopes[K] & InvalidationScope<Parsed<TInput>, Parsed<TOutput>>; };
133
+ output?: TOutput;
134
+ /**
135
+ * Whether a call runs; see `CallPolicy`. Default `"confirm"` with a
136
+ * `confirm` message and `"allow"` without one. `"confirm"` needs the
137
+ * message: the model never writes what the user is asked.
138
+ */
139
+ policy?: CallPolicy;
140
+ /** What views read instead of the whole output; see `Projection`. */
141
+ projection?: ProjectionOption<Parsed<TOutput>, TProjection>;
142
+ }): MutationDefinition<TInput, TOutput, NoInfer<ScopesOf<TScopes, TNames>>>;
143
+ export declare function action<TInput extends z.ZodType | undefined = undefined>(definition: {
144
+ description: string;
145
+ input?: TInput;
146
+ }): ActionDefinition<NoInfer<TInput>>;
147
+ //#endregion
148
+ export {};
@@ -0,0 +1,61 @@
1
+ import "zod";
2
+ //#region src/procedures.ts
3
+ /**
4
+ * Whether `policy` is one of the three. A plain-JavaScript host, or a server
5
+ * override that returns a Promise, can hand over anything; the server denies
6
+ * a call whose policy is not one of these rather than running it.
7
+ */
8
+ function isCallPolicy(policy) {
9
+ return policy === "allow" || policy === "confirm" || policy === "deny";
10
+ }
11
+ /**
12
+ * The schema of what a procedure's views read: its projection's when it has
13
+ * one, else its output; `undefined` for an action or a procedure with no
14
+ * output. The prompt shows it, and the member check reads against it.
15
+ */
16
+ function dataSchema(procedure) {
17
+ return procedure.kind === "action" ? void 0 : procedure.projection?.schema ?? procedure.output;
18
+ }
19
+ function query(definition) {
20
+ const input = definition.input;
21
+ const created = {
22
+ description: definition.description,
23
+ input,
24
+ kind: "query",
25
+ output: definition.output
26
+ };
27
+ if (definition.projection !== void 0) created.projection = definition.projection;
28
+ return created;
29
+ }
30
+ function isNameList(value) {
31
+ return Array.isArray(value);
32
+ }
33
+ function mutation(definition) {
34
+ const input = definition.input;
35
+ const output = definition.output;
36
+ const invalidates = definition.invalidates ?? [];
37
+ const scopes = isNameList(invalidates) ? Object.fromEntries(invalidates.map((name) => [name, true])) : invalidates;
38
+ const created = {
39
+ description: definition.description,
40
+ input,
41
+ invalidates: Object.keys(scopes),
42
+ kind: "mutation",
43
+ output,
44
+ policy: definition.policy ?? (definition.confirm === void 0 ? "allow" : "confirm"),
45
+ scopes
46
+ };
47
+ if (definition.confirm !== void 0) created.confirm = definition.confirm;
48
+ if (definition.projection !== void 0) created.projection = definition.projection;
49
+ return created;
50
+ }
51
+ function action(definition) {
52
+ const input = definition.input;
53
+ return {
54
+ description: definition.description,
55
+ input,
56
+ kind: "action",
57
+ output: void 0
58
+ };
59
+ }
60
+ //#endregion
61
+ export { action, dataSchema, isCallPolicy, mutation, query };
@@ -0,0 +1,24 @@
1
+ //#region src/recipes.d.ts
2
+ /**
3
+ * A recipe: a compound component the product writes in Pickle, from catalog
4
+ * components and other recipes. The model uses it like any tag. Because it is
5
+ * written in catalog tags, it runs on every renderer whose catalog has those
6
+ * tags, and the server checks it against the host at startup.
7
+ */
8
+ export interface RecipeDefinition {
9
+ /** One line: when the model should reach for this recipe. */
10
+ description: string;
11
+ /** A one-line use, shown to the model. */
12
+ example?: string;
13
+ /** The name of the `<define>` in `source`. */
14
+ name: string;
15
+ /** Pickle source with exactly one `<define name="…">`. */
16
+ source: string;
17
+ }
18
+ /** A recipe named by the `<define>` in its source. */
19
+ export declare function recipe(definition: {
20
+ description: string;
21
+ example?: string;
22
+ source: string;
23
+ }): RecipeDefinition;
24
+ //#endregion
@@ -0,0 +1,79 @@
1
+ //#region src/recipes.ts
2
+ const RECIPE_NAME = /^[a-z][A-Za-z0-9]*$/;
3
+ const DEFINE_TAG = /<define(?=[\s/>])/g;
4
+ const ATTR_NAME = /[A-Za-z_][\w-]*/y;
5
+ /** Index just past the `}` matching the `{` at `start`, or -1 if unbalanced. */
6
+ function skipBraced(source, start) {
7
+ let depth = 0;
8
+ let quote = null;
9
+ for (let index = start; index < source.length; index++) {
10
+ const char = source.charAt(index);
11
+ if (quote !== null) {
12
+ if (char === "\\") index++;
13
+ else if (char === quote) quote = null;
14
+ } else if (char === "\"" || char === "'" || char === "`") quote = char;
15
+ else if (char === "{") depth++;
16
+ else if (char === "}") {
17
+ depth--;
18
+ if (depth === 0) return index + 1;
19
+ }
20
+ }
21
+ return -1;
22
+ }
23
+ /** A literal attribute value: `"x"`, `'x'` or `{"x"}`. */
24
+ function literal(value) {
25
+ const quoted = /^\s*\{?\s*(?:"([^"\\]*)"|'([^'\\]*)')\s*\}?\s*$/.exec(value);
26
+ return quoted === null ? null : quoted[1] ?? quoted[2] ?? null;
27
+ }
28
+ /**
29
+ * The `name` attribute of the opening tag at `start`, read the way the
30
+ * compiler reads it. The compiler checks the whole source again at startup.
31
+ */
32
+ function nameAttribute(source, start) {
33
+ let cursor = start + 7;
34
+ while (cursor < source.length) {
35
+ const char = source.charAt(cursor);
36
+ if (/\s/.test(char)) {
37
+ cursor++;
38
+ continue;
39
+ }
40
+ if (char === ">" || char === "/") return null;
41
+ ATTR_NAME.lastIndex = cursor;
42
+ const attr = ATTR_NAME.exec(source)?.[0];
43
+ if (attr === void 0) return null;
44
+ cursor += attr.length;
45
+ if (source.charAt(cursor) !== "=") continue;
46
+ cursor++;
47
+ const opener = source.charAt(cursor);
48
+ let end;
49
+ if (opener === "\"" || opener === "'") end = source.indexOf(opener, cursor + 1) + 1;
50
+ else if (opener === "{") end = skipBraced(source, cursor);
51
+ else return null;
52
+ if (end <= 0) return null;
53
+ if (attr === "name") return literal(source.slice(cursor, end));
54
+ cursor = end;
55
+ }
56
+ return null;
57
+ }
58
+ /** The name of the one `<define>` in a recipe's source. */
59
+ function defineName(source) {
60
+ const starts = [...source.matchAll(DEFINE_TAG)].map((match) => match.index);
61
+ const [start] = starts;
62
+ if (start === void 0 || starts.length > 1) throw new Error(`A recipe's source must contain exactly one <define>; found ${starts.length}`);
63
+ const name = nameAttribute(source, start);
64
+ if (name === null) throw new Error("A recipe's <define> needs a literal name, e.g. <define name=\"orderRow\">");
65
+ if (!RECIPE_NAME.test(name)) throw new Error(`Recipe name "${name}" must start with a lowercase letter and use letters and digits only`);
66
+ return name;
67
+ }
68
+ /** A recipe named by the `<define>` in its source. */
69
+ function recipe(definition) {
70
+ const built = {
71
+ description: definition.description,
72
+ name: defineName(definition.source),
73
+ source: definition.source
74
+ };
75
+ if (definition.example !== void 0) built.example = definition.example;
76
+ return built;
77
+ }
78
+ //#endregion
79
+ export { recipe };
package/package.json CHANGED
@@ -1,6 +1,36 @@
1
1
  {
2
2
  "name": "@jigs.design/host",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Define a host: the catalog a spec may draw, the procedures it may call, and the image hosts.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/yoamomonstruos/pickle.git",
9
+ "directory": "packages/host"
10
+ },
11
+ "type": "module",
12
+ "sideEffects": false,
13
+ "files": [
14
+ "dist",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ }
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "dependencies": {
28
+ "@jigs.design/runtime": "0.1.0"
29
+ },
30
+ "peerDependencies": {
31
+ "zod": "^4.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "zod": "4.6.7"
35
+ }
6
36
  }