@valancex/common 0.12.0 → 0.14.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 +21 -0
- package/dist/app.d.ts +11 -4
- package/dist/app.js +177 -32
- package/dist/client.d.ts +4 -1
- package/dist/client.js +20 -4
- package/dist/color-scheme.d.ts +12 -0
- package/dist/color-scheme.js +15 -0
- package/dist/desktop.d.ts +24 -0
- package/dist/desktop.js +72 -0
- package/dist/early-replay.d.ts +57 -0
- package/dist/early-replay.js +75 -0
- package/dist/early.d.ts +8 -0
- package/dist/early.js +30 -0
- package/dist/form.d.ts +165 -0
- package/dist/form.js +388 -0
- package/dist/http.d.ts +4 -1
- package/dist/http.js +7 -1
- package/dist/index.d.ts +167 -4
- package/dist/index.js +68 -0
- package/dist/internal.d.ts +7 -0
- package/dist/internal.js +7 -0
- package/dist/navigation.d.ts +6 -0
- package/dist/navigation.js +5 -0
- package/dist/open-system.d.ts +12 -0
- package/dist/open-system.js +31 -0
- package/dist/open.d.ts +23 -0
- package/dist/open.js +47 -0
- package/dist/platform.d.ts +5 -0
- package/dist/platform.js +4 -0
- package/dist/primitives.d.ts +21 -0
- package/dist/primitives.js +792 -31
- package/dist/record.d.ts +4 -0
- package/dist/record.js +14 -0
- package/dist/scope.d.ts +11 -2
- package/dist/scope.js +10 -1
- package/dist/server.js +4 -2
- package/dist/store-file.d.ts +4 -0
- package/dist/store-file.js +49 -0
- package/dist/store.d.ts +40 -0
- package/dist/store.js +60 -0
- package/dist/targets.d.ts +8 -0
- package/dist/targets.js +22 -0
- package/dist/vocabulary.d.ts +47 -2
- package/dist/vocabulary.js +24 -2
- package/dist/web-app.d.ts +6 -0
- package/dist/web-app.js +20 -0
- package/package.json +14 -5
package/dist/index.d.ts
CHANGED
|
@@ -7,10 +7,22 @@ export interface PageState {
|
|
|
7
7
|
readonly page: string;
|
|
8
8
|
/** How much room the page has: `compact` (under 600 px wide: a phone), `medium` (600 to 839) or `expanded` (840 and up). The framework keeps it current as the window changes; a layout reads it as `viewport`. */
|
|
9
9
|
readonly viewport: Viewport;
|
|
10
|
+
/** The pages visited, in order, for `back` and `forward`: `go` adds the page it goes to after the one shown (and drops any that were ahead). */
|
|
11
|
+
readonly trail: ReadonlyArray<string>;
|
|
12
|
+
/** Where in `trail` the page shown is. */
|
|
13
|
+
readonly cursor: number;
|
|
14
|
+
/** The colors the person's system asks for: `light` or `dark`. The framework keeps it current (the page's `prefers-color-scheme`, the window's system theme); a layout reads it as `colorScheme`. The server renders `light`; the page corrects it as it starts. */
|
|
15
|
+
readonly colorScheme: ColorScheme;
|
|
10
16
|
}
|
|
17
|
+
/** The system's color preference. */
|
|
18
|
+
export type ColorScheme = "light" | "dark";
|
|
19
|
+
/** What an application asks its window to look like: a scheme, or `system` for the person's own. */
|
|
20
|
+
export type Appearance = ColorScheme | "system";
|
|
11
21
|
/** Window size classes, the ones Material Design and iOS size classes agree on closely enough to design for. */
|
|
12
22
|
export type Viewport = "compact" | "medium" | "expanded";
|
|
13
23
|
export declare const FIELD: unique symbol;
|
|
24
|
+
/** The brand of a state field that holds a form (`Form.field()`): the application then takes the person to the first field in error after a failed submit, with nothing written. */
|
|
25
|
+
export declare const FORM: unique symbol;
|
|
14
26
|
/** A state field: its type and its default, declared together. */
|
|
15
27
|
export interface Field<A = unknown> {
|
|
16
28
|
readonly [FIELD]: true;
|
|
@@ -21,6 +33,15 @@ export interface Field<A = unknown> {
|
|
|
21
33
|
* Declares one field of the application's state with its type and its default in one place. The framework builds the state's schema and its initial value from the fields; the default is
|
|
22
34
|
* checked against the type (by TypeScript, and again when the application is built).
|
|
23
35
|
*/
|
|
36
|
+
export declare const DERIVED: unique symbol;
|
|
37
|
+
/** A value worked out from the state, and the type it has. A template reads it by name, like a state field, and no command changes it. */
|
|
38
|
+
export interface Derived<A = unknown> {
|
|
39
|
+
readonly [DERIVED]: true;
|
|
40
|
+
readonly schema: Schema.Schema<A, any, never>;
|
|
41
|
+
readonly from: (state: any) => A;
|
|
42
|
+
}
|
|
43
|
+
/** What `derived` is given: `derive(type, (state) => value)`. The state is the application's, as a command sees it. */
|
|
44
|
+
export type DeriveMaker<S> = <A, I = A>(schema: Schema.Schema<A, I, never>, from: (state: S) => A) => Derived<A>;
|
|
24
45
|
export declare const field: <A, I = A>(schema: Schema.Schema<A, I, never>, initial: NoInfer<A>) => Field<A>;
|
|
25
46
|
/** Fields declared one by one. */
|
|
26
47
|
export type Fields = Readonly<Record<string, Field<any>>>;
|
|
@@ -49,27 +70,105 @@ export interface Component {
|
|
|
49
70
|
readonly [COMPONENT]: true;
|
|
50
71
|
readonly template: Template;
|
|
51
72
|
/** Present when the component declares its props (an omitted side of a declared interface is none). Absent: the contract is inferred from the template and its uses. */
|
|
52
|
-
readonly props?: Readonly<Record<string,
|
|
73
|
+
readonly props?: Readonly<Record<string, PropDeclaration>>;
|
|
53
74
|
readonly events?: Readonly<Record<string, Schema.Schema<any, any, never>>>;
|
|
75
|
+
/** The prop that is the component's accessible name, when its template passes the name of whoever uses it through to a control (see `Contract.name`). */
|
|
76
|
+
readonly name?: string;
|
|
77
|
+
/** The state and commands of one occurrence of the component, when it has any (see `unit`). */
|
|
78
|
+
readonly unit?: Valance.Unit;
|
|
54
79
|
}
|
|
80
|
+
export declare const SNIPPET: unique symbol;
|
|
81
|
+
/** A prop that is a piece of template: what an occurrence writes between its tags (`children`), or as a `{#snippet}` of that name among them. */
|
|
82
|
+
export interface SnippetDeclaration {
|
|
83
|
+
readonly [SNIPPET]: true;
|
|
84
|
+
/** The types of the snippet's parameters, in order; `{@render name(a, b)}` passes exactly these. */
|
|
85
|
+
readonly params: ReadonlyArray<Schema.Schema<any, any, never>>;
|
|
86
|
+
/** Whether an occurrence may leave it out. Rendering it then needs `{#if has(name)}` around the render (`possibly-absent-snippet`). */
|
|
87
|
+
readonly isOptional: boolean;
|
|
88
|
+
/** The same snippet, which an occurrence may leave out: `snippet(Item).optional()`. */
|
|
89
|
+
optional(): SnippetDeclaration;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Declares a prop whose value is a snippet, with the types of its parameters: `children: snippet()` is the content written between the component's tags; `row: snippet(Item, Schema.Number)` is a
|
|
93
|
+
* `{#snippet row(item, index)}` an occurrence passes and the template renders with `{@render row(item, index)}`. The prop named `children` is reserved for the content between the tags and takes no
|
|
94
|
+
* parameters. A snippet is not data: it has no default, is not a field of a record or an event's payload, and is declared on a component that has a template. `snippet(...).optional()` is one an occurrence may leave out (an absent one draws nothing), and `children: snippet().optional()` is content an occurrence may leave out.
|
|
95
|
+
*/
|
|
96
|
+
export declare const snippet: (...params: ReadonlyArray<Schema.Schema<any, any, never>>) => SnippetDeclaration;
|
|
97
|
+
/** Whether a declaration is a snippet's. */
|
|
98
|
+
export declare const isSnippet: (value: unknown) => value is SnippetDeclaration;
|
|
99
|
+
/** A prop is declared by its schema, by `field(schema, default)` when an occurrence that leaves it out should get a value, or by `snippet(…)` when it is a piece of template. */
|
|
100
|
+
export type PropDeclaration = Schema.Schema<any, any, never> | Field<any> | SnippetDeclaration;
|
|
101
|
+
/** Whether a prop is declared with a default. */
|
|
102
|
+
export declare const isFormField: (value: unknown) => boolean;
|
|
103
|
+
export declare const isField: (value: unknown) => value is Field;
|
|
55
104
|
export interface Contract<P extends string, E extends string> {
|
|
56
105
|
/**
|
|
57
|
-
* The inputs the template reads, each with its type. Optional when the schema admits `undefined`.
|
|
58
|
-
*
|
|
106
|
+
* The inputs the template reads, each with its type. Optional when the schema admits `undefined`. `field(schema, default)` gives a prop a default: an occurrence of the component that leaves
|
|
107
|
+
* the prop out gets the default (for a component with a template; a primitive ignores it), and the prop may then be left out. A string-literal union (`Schema.Literal("a", "b")`) is a closed set:
|
|
108
|
+
* the model checks every literal written for it. With neither `props` nor `events` the contract is inferred; with either, the interface is exactly what is written (an omitted side is none).
|
|
59
109
|
*/
|
|
60
110
|
readonly props?: {
|
|
61
|
-
readonly [K in P]:
|
|
111
|
+
readonly [K in P]: PropDeclaration;
|
|
62
112
|
};
|
|
63
113
|
/** The events the template raises, each with its payload's schema; `Schema.Void` is no payload. */
|
|
64
114
|
readonly events?: {
|
|
65
115
|
readonly [K in E]: Schema.Schema<any, any, never>;
|
|
66
116
|
};
|
|
117
|
+
/**
|
|
118
|
+
* The prop that is the component's accessible name: a component whose template draws a control and takes its name from whoever uses it (a labelled checkbox of the design system) says which
|
|
119
|
+
* prop that is, and the toolchain holds both sides to it. The prop must be a string every use writes (the model reports a use that omits it, where it is written), and the template must read it;
|
|
120
|
+
* a control in the template that has no name of its own (no `label`, not inside a `<label>`) is then not reported by `unnamed-control`, because the name arrives from the use.
|
|
121
|
+
*/
|
|
122
|
+
readonly name?: P;
|
|
123
|
+
/**
|
|
124
|
+
* State that belongs to one occurrence of the component, such as whether a disclosure is open or which tab is chosen: `unit({ state, commands })`. The template reads its fields and runs its commands
|
|
125
|
+
* by name, like any others; the application never sees them. A component with a unit states its interface (`props` and `events`, empty if it has none).
|
|
126
|
+
*/
|
|
127
|
+
readonly unit?: Valance.Unit;
|
|
67
128
|
}
|
|
68
129
|
/**
|
|
69
130
|
* Defines a component: its template and an optional contract. Register it once, in `defineApp`'s `components`; the templates that write its tag use it, and nothing about its interface is
|
|
70
131
|
* written again there. Where the toolchain has generated the template's type, a contract that disagrees with the template's keys is a compile-time error.
|
|
71
132
|
*/
|
|
72
133
|
export declare function component<P extends string = string, E extends string = string>(template: Template<P, E>, contract?: Contract<NoInfer<P>, NoInfer<E>>): Component;
|
|
134
|
+
/** What a unit's commands change: some of its fields. */
|
|
135
|
+
type Changes<S> = Partial<S>;
|
|
136
|
+
/** One command of a unit: a function of the state as the person saw it that returns the fields it changes, or the same with parameters and the events it announces. */
|
|
137
|
+
export type UnitCommandSpec<S, P extends Schema.Struct.Fields = Schema.Struct.Fields> = ((state: S) => Changes<S>) | {
|
|
138
|
+
/** The command's parameters, in order: `{ n: Schema.Number }`. Each is a parameter of the command in the model. */
|
|
139
|
+
readonly params?: P;
|
|
140
|
+
readonly run: (state: S, args: Schema.Struct.Type<P>) => Changes<S>;
|
|
141
|
+
/** Events of the component the command announces to its caller, each with the payload it carries, worked out from the state the command leaves: `{ change: (next) => next.at }`. */
|
|
142
|
+
readonly raises?: Readonly<Record<string, (next: S, args: Schema.Struct.Type<P>) => unknown>>;
|
|
143
|
+
};
|
|
144
|
+
/** A command with parameters, typed: the parameters' types reach `run` and `raises`. */
|
|
145
|
+
export declare const unitCommand: <S, P extends Schema.Struct.Fields>(spec: {
|
|
146
|
+
readonly params: P;
|
|
147
|
+
readonly run: (state: S, args: Schema.Struct.Type<P>) => Changes<S>;
|
|
148
|
+
readonly raises?: Readonly<Record<string, (next: S, args: Schema.Struct.Type<P>) => unknown>>;
|
|
149
|
+
}) => UnitCommandSpec<S, P>;
|
|
150
|
+
/**
|
|
151
|
+
* Declares the state and commands of one occurrence of a component (Tier 2; a component with no unit needs none of it). The state is typed with defaults, as application state is; a command is a
|
|
152
|
+
* pure function of the state as the person saw it that returns the fields it changes:
|
|
153
|
+
*
|
|
154
|
+
* export default component(template, {
|
|
155
|
+
* props: { label: Schema.String },
|
|
156
|
+
* events: {},
|
|
157
|
+
* unit: unit({ state: { open: field(Schema.Boolean, false) }, commands: { toggle: (state) => ({ open: !state.open }) } }),
|
|
158
|
+
* });
|
|
159
|
+
*
|
|
160
|
+
* The template reads `open` and runs `toggle()`; the application holds neither. The state belongs to the occurrence: it follows its row when a keyed list is re-sorted, and starts again from the defaults
|
|
161
|
+
* when the occurrence leaves the page and returns. State that must outlive that (or that another part of the application reads) is application state, not a unit. A field named in `controllable` is
|
|
162
|
+
* owned by the caller when it writes a prop of the same name (the prop must be declared, optional and of the same type): the prop is then the value, the unit's change to it is not applied, and
|
|
163
|
+
* `raises` tells the caller what the person chose.
|
|
164
|
+
*/
|
|
165
|
+
export declare const unit: <F extends Fields>(definition: {
|
|
166
|
+
readonly state: F;
|
|
167
|
+
readonly commands: {
|
|
168
|
+
readonly [name: string]: UnitCommandSpec<StateOf<F>, any>;
|
|
169
|
+
};
|
|
170
|
+
readonly controllable?: ReadonlyArray<Extract<keyof F, string>>;
|
|
171
|
+
}) => Valance.Unit<StateOf<F>>;
|
|
73
172
|
/**
|
|
74
173
|
* What the document's title is made of. `page` is the page's own part (default: its title, from the Markdown front matter), `app` the application's name (default: the site's name), `status` what is
|
|
75
174
|
* going on right now and is not part of any page (an unread count, "Saving…"): it is written first, in parentheses. The project's format in \`valance.json\` (`"title": "{page} · {site}"`) puts them
|
|
@@ -81,7 +180,22 @@ export interface TitleParts {
|
|
|
81
180
|
readonly app?: string;
|
|
82
181
|
readonly status?: string;
|
|
83
182
|
}
|
|
183
|
+
/** What differs between targets: the templates. Everything else in the definition (state, commands, title, http) is the application's, and the same on every target. */
|
|
184
|
+
export interface TargetParts {
|
|
185
|
+
readonly layout?: Component;
|
|
186
|
+
readonly components?: ReadonlyArray<Component>;
|
|
187
|
+
readonly override?: ReadonlyArray<Component>;
|
|
188
|
+
}
|
|
84
189
|
export interface AppDefinition<S extends StateInput = {}> {
|
|
190
|
+
/**
|
|
191
|
+
* The templates of one target, for a project with more than one (`valance.web.json` and `valance.desktop.json`): `targets: { web: { layout, components }, desktop: { layout } }`. A target's
|
|
192
|
+
* `layout` replaces the top-level one, and its `components` and `override` are added to it. A target's templates are compiled against that target's vocabulary only; the state, the commands and
|
|
193
|
+
* the content are the one application's.
|
|
194
|
+
*/
|
|
195
|
+
readonly targets?: {
|
|
196
|
+
readonly web?: TargetParts;
|
|
197
|
+
readonly desktop?: TargetParts;
|
|
198
|
+
};
|
|
85
199
|
/** The page's shell. Omit it to use the built-in layout. Its template must be named `layout.mprx`, and it has no props or events: its inputs are the state and the commands. */
|
|
86
200
|
readonly layout?: Component;
|
|
87
201
|
/** Every component of the application besides the layout and the overrides. A template uses one by writing its tag; one that no template writes yet is not part of the program. */
|
|
@@ -109,13 +223,62 @@ export interface AppDefinition<S extends StateInput = {}> {
|
|
|
109
223
|
readonly http?: {
|
|
110
224
|
readonly interceptors?: ReadonlyArray<Interceptor>;
|
|
111
225
|
};
|
|
226
|
+
/**
|
|
227
|
+
* The appearance this application asks of its window, for a state: `appearance: (state) => state.theme` (`"light"`, `"dark"` or `"system"`). A page draws its own colors, so it is the desktop
|
|
228
|
+
* window's: the window follows the system's theme unless this says otherwise. Absent: the system's.
|
|
229
|
+
*/
|
|
230
|
+
readonly appearance?: (state: PageState & StateOf<S>) => Appearance;
|
|
231
|
+
/**
|
|
232
|
+
* The state fields that outlive a run: `persist: ["items", "theme"]`. They are loaded when the application starts and saved when they change, through the platform's `Store` (the page's
|
|
233
|
+
* storage on the web, a file in the user's data directory on the desktop), under the application's name. What was stored and no longer fits a field's type is ignored, not loaded. Where a
|
|
234
|
+
* platform has no store the application runs as it would without this. A field not listed is not kept (a `navigation` flag on one target should not follow the person to another).
|
|
235
|
+
*/
|
|
236
|
+
readonly persist?: ReadonlyArray<Extract<keyof StateOf<S>, string> | "navigation">;
|
|
237
|
+
/**
|
|
238
|
+
* A guard for moving between pages by `go`, `back` and `forward`: the router's `Guard`, asked before the move. It is told where the application is going from and to (a route named for the
|
|
239
|
+
* page, with no parameters), the state, and why: `kind` is `"programmatic"` for `go` and `"history"` for `back` and `forward`. `true` lets it through, `false` stops it (nothing changes),
|
|
240
|
+
* a `redirect(...)` sends it to another page. A guard that throws or fails stops the move. Without one every move is allowed. A page's own Back and Forward are the browser's: the router
|
|
241
|
+
* guards those as `"popstate"`.
|
|
242
|
+
*/
|
|
243
|
+
readonly guard?: NavigationGuard;
|
|
244
|
+
/**
|
|
245
|
+
* Values worked out from the state, for a template to read: `derived: (derive) => ({ open: derive(Schema.Number, (state) => state.items.filter((item) => !item.done).length) })`. Each has its
|
|
246
|
+
* type, as a state field does. They are computed from the state each time it changes, on every target, and a command cannot change one (change the state they come from). The function must
|
|
247
|
+
* be pure and must not throw.
|
|
248
|
+
*/
|
|
249
|
+
readonly derived?: (derive: DeriveMaker<PageState & StateOf<S>>) => {
|
|
250
|
+
readonly [name: string]: Derived<any>;
|
|
251
|
+
};
|
|
112
252
|
/** The commands a layout can run. `command` makes one: `command((state) => nextState)` or `command(InputSchema, (input, state) => nextState)`. */
|
|
113
253
|
readonly commands?: (command: Valance.CommandMaker<PageState & StateOf<S>>) => Valance.CommandTable<any, Valance.Ambient>;
|
|
114
254
|
}
|
|
255
|
+
/** What a navigation guard is told; the router's `GuardContext`, spelled out so the author's entry needs nothing but VALANCE and effect. */
|
|
256
|
+
export interface NavigationContext {
|
|
257
|
+
readonly from: {
|
|
258
|
+
readonly name: string;
|
|
259
|
+
readonly params: Readonly<Record<string, unknown>>;
|
|
260
|
+
readonly query: Readonly<Record<string, unknown>>;
|
|
261
|
+
} | undefined;
|
|
262
|
+
readonly to: {
|
|
263
|
+
readonly name: string;
|
|
264
|
+
readonly params: Readonly<Record<string, unknown>>;
|
|
265
|
+
readonly query: Readonly<Record<string, unknown>>;
|
|
266
|
+
};
|
|
267
|
+
readonly state: unknown;
|
|
268
|
+
readonly kind: "link" | "popstate" | "programmatic" | "history" | "initial" | "close";
|
|
269
|
+
readonly data: Readonly<Record<string, unknown>>;
|
|
270
|
+
}
|
|
271
|
+
/** `true` allows, `false` stops, and `{ _tag: "Redirect", name }` (the router's `redirect(page)`) goes to another page; it may answer now, later or as an Effect. */
|
|
272
|
+
export type NavigationGuard = (context: NavigationContext) => unknown;
|
|
115
273
|
/** The brand that says a value came from `defineApp`. */
|
|
116
274
|
export declare const APP: unique symbol;
|
|
117
275
|
export interface DefinedApp extends AppDefinition<any> {
|
|
118
276
|
readonly [APP]: true;
|
|
119
277
|
}
|
|
120
278
|
/** Declares the application's component tree, state and commands. It starts nothing; the framework runs it. */
|
|
279
|
+
/** What `derived` makes, for the toolchain: the function is called with the one `derive`. */
|
|
280
|
+
export declare const derivedOf: (author: {
|
|
281
|
+
readonly derived?: AppDefinition<any>["derived"];
|
|
282
|
+
} | undefined) => Readonly<Record<string, Derived<any>>>;
|
|
121
283
|
export declare const defineApp: <S extends StateInput = {}>(definition: AppDefinition<S>) => DefinedApp;
|
|
284
|
+
export * as Form from "./form.js";
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,35 @@
|
|
|
1
|
+
import { UNIT } from "@valancex/valance";
|
|
2
|
+
import { Schema } from "effect";
|
|
1
3
|
// ---- state -------------------------------------------------------------------------------------------------------------------------------------------------
|
|
2
4
|
export const FIELD = Symbol.for("valancex.common.field");
|
|
5
|
+
/** The brand of a state field that holds a form (`Form.field()`): the application then takes the person to the first field in error after a failed submit, with nothing written. */
|
|
6
|
+
export const FORM = Symbol.for("valancex.common.form");
|
|
3
7
|
/**
|
|
4
8
|
* Declares one field of the application's state with its type and its default in one place. The framework builds the state's schema and its initial value from the fields; the default is
|
|
5
9
|
* checked against the type (by TypeScript, and again when the application is built).
|
|
6
10
|
*/
|
|
11
|
+
export const DERIVED = Symbol.for("valancex.common.derived");
|
|
12
|
+
const derive = (schema, from) => ({ [DERIVED]: true, schema: schema, from });
|
|
7
13
|
export const field = (schema, initial) => ({ [FIELD]: true, schema: schema, initial });
|
|
8
14
|
export const COMPONENT = Symbol.for("valancex.common.component");
|
|
15
|
+
export const SNIPPET = Symbol.for("valancex.common.snippet");
|
|
16
|
+
/**
|
|
17
|
+
* Declares a prop whose value is a snippet, with the types of its parameters: `children: snippet()` is the content written between the component's tags; `row: snippet(Item, Schema.Number)` is a
|
|
18
|
+
* `{#snippet row(item, index)}` an occurrence passes and the template renders with `{@render row(item, index)}`. The prop named `children` is reserved for the content between the tags and takes no
|
|
19
|
+
* parameters. A snippet is not data: it has no default, is not a field of a record or an event's payload, and is declared on a component that has a template. `snippet(...).optional()` is one an occurrence may leave out (an absent one draws nothing), and `children: snippet().optional()` is content an occurrence may leave out.
|
|
20
|
+
*/
|
|
21
|
+
export const snippet = (...params) => declare(params, false);
|
|
22
|
+
const declare = (params, isOptional) => ({
|
|
23
|
+
[SNIPPET]: true,
|
|
24
|
+
params,
|
|
25
|
+
isOptional,
|
|
26
|
+
optional: () => declare(params, true),
|
|
27
|
+
});
|
|
28
|
+
/** Whether a declaration is a snippet's. */
|
|
29
|
+
export const isSnippet = (value) => typeof value === "object" && value !== null && SNIPPET in value;
|
|
30
|
+
/** Whether a prop is declared with a default. */
|
|
31
|
+
export const isFormField = (value) => typeof value === "object" && value !== null && FORM in value;
|
|
32
|
+
export const isField = (value) => typeof value === "object" && value !== null && FIELD in value;
|
|
9
33
|
/**
|
|
10
34
|
* Defines a component: its template and an optional contract. Register it once, in `defineApp`'s `components`; the templates that write its tag use it, and nothing about its interface is
|
|
11
35
|
* written again there. Where the toolchain has generated the template's type, a contract that disagrees with the template's keys is a compile-time error.
|
|
@@ -16,9 +40,53 @@ export function component(template, contract = {}) {
|
|
|
16
40
|
template,
|
|
17
41
|
...(contract.props === undefined ? {} : { props: contract.props }),
|
|
18
42
|
...(contract.events === undefined ? {} : { events: contract.events }),
|
|
43
|
+
...(contract.name === undefined ? {} : { name: contract.name }),
|
|
44
|
+
...(contract.unit === undefined ? {} : { unit: contract.unit }),
|
|
19
45
|
};
|
|
20
46
|
}
|
|
47
|
+
/** A command with parameters, typed: the parameters' types reach `run` and `raises`. */
|
|
48
|
+
export const unitCommand = (spec) => spec;
|
|
49
|
+
/**
|
|
50
|
+
* Declares the state and commands of one occurrence of a component (Tier 2; a component with no unit needs none of it). The state is typed with defaults, as application state is; a command is a
|
|
51
|
+
* pure function of the state as the person saw it that returns the fields it changes:
|
|
52
|
+
*
|
|
53
|
+
* export default component(template, {
|
|
54
|
+
* props: { label: Schema.String },
|
|
55
|
+
* events: {},
|
|
56
|
+
* unit: unit({ state: { open: field(Schema.Boolean, false) }, commands: { toggle: (state) => ({ open: !state.open }) } }),
|
|
57
|
+
* });
|
|
58
|
+
*
|
|
59
|
+
* The template reads `open` and runs `toggle()`; the application holds neither. The state belongs to the occurrence: it follows its row when a keyed list is re-sorted, and starts again from the defaults
|
|
60
|
+
* when the occurrence leaves the page and returns. State that must outlive that (or that another part of the application reads) is application state, not a unit. A field named in `controllable` is
|
|
61
|
+
* owned by the caller when it writes a prop of the same name (the prop must be declared, optional and of the same type): the prop is then the value, the unit's change to it is not applied, and
|
|
62
|
+
* `raises` tells the caller what the person chose.
|
|
63
|
+
*/
|
|
64
|
+
export const unit = (definition) => {
|
|
65
|
+
const entries = Object.entries(definition.state);
|
|
66
|
+
const fields = Object.fromEntries(entries.map(([name, each]) => [name, { schema: each.schema, initial: each.initial }]));
|
|
67
|
+
const schema = Schema.Struct(Object.fromEntries(entries.map(([name, each]) => [name, each.schema])));
|
|
68
|
+
return {
|
|
69
|
+
[UNIT]: true,
|
|
70
|
+
schema: schema,
|
|
71
|
+
fields,
|
|
72
|
+
controllable: definition.controllable ?? [],
|
|
73
|
+
commands: Object.fromEntries(Object.entries(definition.commands).map(([name, command]) => [
|
|
74
|
+
name,
|
|
75
|
+
typeof command === "function"
|
|
76
|
+
? { run: command }
|
|
77
|
+
: {
|
|
78
|
+
...(command.params === undefined ? {} : { input: Schema.Struct(command.params) }),
|
|
79
|
+
run: command.run,
|
|
80
|
+
...(command.raises === undefined ? {} : { raises: command.raises }),
|
|
81
|
+
},
|
|
82
|
+
])),
|
|
83
|
+
};
|
|
84
|
+
};
|
|
21
85
|
/** The brand that says a value came from `defineApp`. */
|
|
22
86
|
export const APP = Symbol.for("valancex.common");
|
|
23
87
|
/** Declares the application's component tree, state and commands. It starts nothing; the framework runs it. */
|
|
88
|
+
/** What `derived` makes, for the toolchain: the function is called with the one `derive`. */
|
|
89
|
+
export const derivedOf = (author) => author?.derived?.(derive) ?? {};
|
|
24
90
|
export const defineApp = (definition) => ({ ...definition, [APP]: true });
|
|
91
|
+
// ---- forms ---------------------------------------------------------------------------------------------------------------------------------------------------
|
|
92
|
+
export * as Form from "./form.js";
|
package/dist/internal.d.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
export * from "./app.js";
|
|
2
|
+
export * from "./web-app.js";
|
|
2
3
|
export * from "./model.js";
|
|
3
4
|
export * from "./primitives.js";
|
|
4
5
|
export * from "./routes.js";
|
|
5
6
|
export * from "./scope.js";
|
|
6
7
|
export * from "./state.js";
|
|
7
8
|
export * from "./vocabulary.js";
|
|
9
|
+
export * from "./targets.js";
|
|
10
|
+
export * from "./store.js";
|
|
11
|
+
export * from "./open.js";
|
|
12
|
+
export * from "./navigation.js";
|
|
13
|
+
export * from "./platform.js";
|
|
14
|
+
export { earlyHead, earlyShell, earlyShellLimit } from "./early.js";
|
package/dist/internal.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
// What the build tools share with the runtime: the content model, the tag table, the scope a layout reads, the state and the application they make. `@valancex/cli` builds with these and the generated
|
|
2
2
|
// entries run them, so there is one definition of each. Not for applications: an application's code is `@valancex/common` (the entry point).
|
|
3
3
|
export * from "./app.js";
|
|
4
|
+
export * from "./web-app.js";
|
|
4
5
|
export * from "./model.js";
|
|
5
6
|
export * from "./primitives.js";
|
|
6
7
|
export * from "./routes.js";
|
|
7
8
|
export * from "./scope.js";
|
|
8
9
|
export * from "./state.js";
|
|
9
10
|
export * from "./vocabulary.js";
|
|
11
|
+
export * from "./targets.js";
|
|
12
|
+
export * from "./store.js";
|
|
13
|
+
export * from "./open.js";
|
|
14
|
+
export * from "./navigation.js";
|
|
15
|
+
export * from "./platform.js";
|
|
16
|
+
export { earlyHead, earlyShell, earlyShellLimit } from "./early.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import * as Nexus from "@valancex/nexus";
|
|
2
|
+
/** The platform has no history of its own: the engine keeps the list (`trail` and `cursor`). */
|
|
3
|
+
export interface NavigationOwner {
|
|
4
|
+
readonly owner: "engine";
|
|
5
|
+
}
|
|
6
|
+
export declare const Navigation: Nexus.Capability.Capability<NavigationOwner>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Who keeps the list of places the person has been (docs/application-model/NAVIGATION.md). A page has the browser's: its session history holds the list, restores it, and moves along it with the
|
|
2
|
+
// person's Back and Forward. A window has none, so the engine keeps the list in its state; a platform that supplies this capability is one of those, and only there does `persist: ["navigation"]`
|
|
3
|
+
// do anything (restoring a stored page on the web would argue with the address, which is the truth there).
|
|
4
|
+
import * as Nexus from "@valancex/nexus";
|
|
5
|
+
export const Navigation = Nexus.Capability.define("valance/navigation");
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { OpenClient } from "./open.js";
|
|
2
|
+
/** The program and arguments that open `address` on `platform`. */
|
|
3
|
+
export declare const openerFor: (address: string, platform?: NodeJS.Platform) => {
|
|
4
|
+
readonly program: string;
|
|
5
|
+
readonly args: ReadonlyArray<string>;
|
|
6
|
+
};
|
|
7
|
+
type Spawn = (program: string, args: ReadonlyArray<string>) => {
|
|
8
|
+
readonly on: (event: "error", listener: (error: Error) => void) => unknown;
|
|
9
|
+
readonly unref: () => void;
|
|
10
|
+
};
|
|
11
|
+
export declare const systemOpen: (spawn?: Spawn, platform?: NodeJS.Platform) => OpenClient;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// The desktop's opener: the system's own, given the address as an argument (never through a shell, so the address is data and nothing else). Node only.
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
4
|
+
import { OpenError, refusal } from "./open.js";
|
|
5
|
+
/** The program and arguments that open `address` on `platform`. */
|
|
6
|
+
export const openerFor = (address, platform = process.platform) => platform === "win32"
|
|
7
|
+
? { program: "rundll32", args: ["url.dll,FileProtocolHandler", address] }
|
|
8
|
+
: platform === "darwin"
|
|
9
|
+
? { program: "open", args: [address] }
|
|
10
|
+
: { program: "xdg-open", args: [address] };
|
|
11
|
+
export const systemOpen = (spawn = (program, args) => nodeSpawn(program, [...args], { detached: true, stdio: "ignore" }), platform = process.platform) => ({
|
|
12
|
+
open: (address) => {
|
|
13
|
+
const reason = refusal(address);
|
|
14
|
+
if (reason !== undefined) {
|
|
15
|
+
return Effect.fail(new OpenError("Refused", reason));
|
|
16
|
+
}
|
|
17
|
+
const { program, args } = openerFor(address, platform);
|
|
18
|
+
return Effect.async((resume) => {
|
|
19
|
+
try {
|
|
20
|
+
const child = spawn(program, args);
|
|
21
|
+
child.on("error", () => resume(Effect.fail(new OpenError("Unavailable", `${program} could not be started`))));
|
|
22
|
+
child.unref();
|
|
23
|
+
// handed over: the opener is the system's, and does not report back to a program that asked
|
|
24
|
+
setTimeout(() => resume(Effect.void), 0);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
resume(Effect.fail(new OpenError("Unavailable", `${program} could not be started`)));
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
});
|
package/dist/open.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as Nexus from "@valancex/nexus";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
export declare class OpenError extends Error {
|
|
4
|
+
readonly kind: "Refused" | "Unavailable";
|
|
5
|
+
readonly _tag = "OpenError";
|
|
6
|
+
constructor(kind: "Refused" | "Unavailable", message: string);
|
|
7
|
+
}
|
|
8
|
+
export interface OpenClient {
|
|
9
|
+
/** Opens `address` in the program the platform keeps for it. Resolves when it was handed over, not when anything finished. */
|
|
10
|
+
readonly open: (address: string) => Effect.Effect<void, OpenError>;
|
|
11
|
+
}
|
|
12
|
+
/** The capability: ask for it in a command (`Open.open`), and the platform says whether it has one. */
|
|
13
|
+
export declare const Open: Nexus.Capability.Capability<OpenClient>;
|
|
14
|
+
/** The schemes an address may have to be opened. */
|
|
15
|
+
export declare const OPENABLE: readonly ["https:", "http:", "mailto:"];
|
|
16
|
+
/** Why `address` may not be opened, or `undefined` when it may: an absolute address of an openable scheme, without a control character, of a sensible length. */
|
|
17
|
+
export declare const refusal: (address: string) => string | undefined;
|
|
18
|
+
/** Opens `address` through the platform's opener. Requires the `Open` capability. */
|
|
19
|
+
export declare const open: (address: string) => Effect.Effect<void, OpenError | Nexus.Capability.CapabilityUnavailableError, Nexus.Capability.EnvironmentShape>;
|
|
20
|
+
/** The page's opener: a new tab, with no link back to this page. */
|
|
21
|
+
export declare const webOpen: (win?: {
|
|
22
|
+
readonly open: (url: string, target: string, features: string) => unknown;
|
|
23
|
+
}) => OpenClient;
|
package/dist/open.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Sending a person to an address outside the application: a web page in their browser, a mail in their mail program. NEXUS and VALANCE hold no such thing (their rule): it is a capability the
|
|
2
|
+
// platform supplies. The web's is a new tab; the desktop's is the system's own opener (`./open-system`). Only the addresses a person expects a link to open are opened: a script, a file or a
|
|
3
|
+
// program's own scheme is refused, whatever text asked.
|
|
4
|
+
import * as Nexus from "@valancex/nexus";
|
|
5
|
+
import { Effect } from "effect";
|
|
6
|
+
export class OpenError extends Error {
|
|
7
|
+
kind;
|
|
8
|
+
_tag = "OpenError";
|
|
9
|
+
constructor(kind, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.kind = kind;
|
|
12
|
+
this.name = "OpenError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** The capability: ask for it in a command (`Open.open`), and the platform says whether it has one. */
|
|
16
|
+
export const Open = Nexus.Capability.define("valance/open");
|
|
17
|
+
/** The schemes an address may have to be opened. */
|
|
18
|
+
export const OPENABLE = ["https:", "http:", "mailto:"];
|
|
19
|
+
/** Why `address` may not be opened, or `undefined` when it may: an absolute address of an openable scheme, without a control character, of a sensible length. */
|
|
20
|
+
export const refusal = (address) => {
|
|
21
|
+
// oxlint-disable-next-line no-control-regex -- a control character in an address is how one is smuggled into a command line
|
|
22
|
+
if (address.length === 0 || address.length > 2048 || /[\u0000-\u001f\u007f\s]/.test(address)) {
|
|
23
|
+
return "an address is one line of text, at most 2048 characters";
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const url = new URL(address);
|
|
27
|
+
return OPENABLE.includes(url.protocol) ? undefined : `a ${url.protocol} address is not opened (only ${OPENABLE.join(", ")})`;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return "not an absolute address";
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
/** Opens `address` through the platform's opener. Requires the `Open` capability. */
|
|
34
|
+
export const open = (address) => Effect.flatMap(Nexus.Capability.require(Open), (client) => client.open(address));
|
|
35
|
+
/** The page's opener: a new tab, with no link back to this page. */
|
|
36
|
+
export const webOpen = (win) => ({
|
|
37
|
+
open: (address) => {
|
|
38
|
+
const reason = refusal(address);
|
|
39
|
+
if (reason !== undefined) {
|
|
40
|
+
return Effect.fail(new OpenError("Refused", reason));
|
|
41
|
+
}
|
|
42
|
+
const target = win ?? globalThis.window;
|
|
43
|
+
return target === undefined
|
|
44
|
+
? Effect.fail(new OpenError("Unavailable", "this page cannot open a tab"))
|
|
45
|
+
: Effect.try({ try: () => void target.open(address, "_blank", "noopener,noreferrer"), catch: () => new OpenError("Unavailable", "the page's tab was blocked") });
|
|
46
|
+
},
|
|
47
|
+
});
|
package/dist/platform.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// What a platform supplies the application: its capabilities, by contract. `Web.run` and `Native.run` each take one; the web's is `webPlatform` (`./http`), the desktop's is made in `./desktop`.
|
|
2
|
+
import * as Nexus from "@valancex/nexus";
|
|
3
|
+
/** A platform that supplies each `[capability, implementation]` given. */
|
|
4
|
+
export const platformOf = (capabilities) => Nexus.Capability.EnvironmentLive(new Map(capabilities.map(([capability, implementation]) => [capability.id, { _tag: "Available", implementation }])));
|
package/dist/primitives.d.ts
CHANGED
|
@@ -1,2 +1,23 @@
|
|
|
1
1
|
import * as Web from "@valancex/valance/web";
|
|
2
2
|
export declare const primitives: Web.WebPrimitives;
|
|
3
|
+
/** What each tag's props and events are, as the specification states them (the derivation from the table alone cannot say whether a prop is required, nor type one realized as text). */
|
|
4
|
+
export declare const declared: Readonly<Record<string, {
|
|
5
|
+
readonly props: Readonly<Record<string, {
|
|
6
|
+
readonly type: Readonly<Record<string, unknown>>;
|
|
7
|
+
readonly required: boolean;
|
|
8
|
+
}>>;
|
|
9
|
+
readonly events: Readonly<Record<string, {
|
|
10
|
+
readonly payload?: Readonly<Record<string, unknown>>;
|
|
11
|
+
}>>;
|
|
12
|
+
}>>;
|
|
13
|
+
/** The tags that were renamed (decision P7), old to new: a template that writes the old one is told the new. */
|
|
14
|
+
export declare const renamed: Readonly<Record<string, string>>;
|
|
15
|
+
/** The props that were renamed (decision I29), by tag, old to new: a template that writes the old one is told the new. */
|
|
16
|
+
export declare const renamedProps: Readonly<Record<string, Readonly<Record<string, string>>>>;
|
|
17
|
+
/** What a use of a tag must satisfy besides its props (`vlx` checks it): the tags that need a name (a `label` prop, or a `label` around them), the tags a tag may contain only, and the tags a tag must be inside. */
|
|
18
|
+
export declare const rules: {
|
|
19
|
+
readonly named: ReadonlyArray<string>;
|
|
20
|
+
readonly modal: ReadonlyArray<string>;
|
|
21
|
+
readonly only: Readonly<Record<string, ReadonlyArray<string>>>;
|
|
22
|
+
readonly within: Readonly<Record<string, ReadonlyArray<string>>>;
|
|
23
|
+
};
|