@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/record.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type * as Valance from "@valancex/valance";
|
|
2
|
+
import type { Redaction, SessionRecorder } from "@valancex/valance/replay";
|
|
3
|
+
/** A recorder for the page of `program`, posting to the development server's `/__vlx/record`; it writes the last tree when the page is hidden. */
|
|
4
|
+
export declare const recording: (program: Valance.Program, redact?: Redaction) => SessionRecorder;
|
package/dist/record.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { postTo, recorder } from "@valancex/valance/replay";
|
|
2
|
+
/** A recorder for the page of `program`, posting to the development server's `/__vlx/record`; it writes the last tree when the page is hidden. */
|
|
3
|
+
export const recording = (program, redact) => {
|
|
4
|
+
const model = program.model;
|
|
5
|
+
const { root, templates } = program;
|
|
6
|
+
const record = recorder({
|
|
7
|
+
write: postTo("/__vlx/record"),
|
|
8
|
+
warn: (message) => console.warn(`vlx dev --record: ${message}`),
|
|
9
|
+
...(redact === undefined ? {} : { redact }),
|
|
10
|
+
programs: () => ({ root, templates, model: typeof model === "string" ? model : JSON.stringify(model) }),
|
|
11
|
+
});
|
|
12
|
+
addEventListener("pagehide", () => void record.flush());
|
|
13
|
+
return record;
|
|
14
|
+
};
|
package/dist/scope.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
|
-
import type { TitleParts, Viewport } from "./index.js";
|
|
2
|
+
import type { ColorScheme, TitleParts, Viewport } from "./index.js";
|
|
3
3
|
import type { Content, Page } from "./model.js";
|
|
4
4
|
export declare const FrameScope: Schema.Struct<{
|
|
5
5
|
/** The application's name (`valance.json`). */
|
|
@@ -32,6 +32,12 @@ export declare const FrameScope: Schema.Struct<{
|
|
|
32
32
|
}>>;
|
|
33
33
|
/** How much room the page has: `"compact"`, `"medium"` or `"expanded"` (see `Viewport`). `{#if viewport == "compact"}` chooses a layout for a phone. The server renders `expanded`; the browser corrects it after it starts. */
|
|
34
34
|
viewport: Schema.Literal<["compact", "medium", "expanded"]>;
|
|
35
|
+
/** The colors the person's system asks for: `"light"` or `"dark"`. `theme={theme == "system" ? colorScheme : theme}` lets an application offer "follow the system" beside its own choices. The server renders `light`; the page corrects it after it starts. */
|
|
36
|
+
colorScheme: Schema.Literal<["light", "dark"]>;
|
|
37
|
+
/** Whether there is a page before this one in the trail (`back()` goes to it). `disabled={!canGoBack}` greys a Back button. */
|
|
38
|
+
canGoBack: typeof Schema.Boolean;
|
|
39
|
+
/** Whether there is a page after this one in the trail (`forward()` goes to it), as there is after a `back()`. */
|
|
40
|
+
canGoForward: typeof Schema.Boolean;
|
|
35
41
|
/** The page's body. Draw it with `<blocks items={blocks} />`, or draw each block yourself. */
|
|
36
42
|
blocks: Schema.Array$<Schema.Struct<{
|
|
37
43
|
id: typeof Schema.String;
|
|
@@ -80,4 +86,7 @@ export declare const pageAt: (content: Content, path: string) => Page | undefine
|
|
|
80
86
|
* (the home page, usually) is that name alone. The author's `title` function may replace the parts; the caller cleans the result (control and direction-override characters out, length limited).
|
|
81
87
|
*/
|
|
82
88
|
export declare const documentTitle: (content: Content, page: Page | undefined, parts?: TitleParts) => string;
|
|
83
|
-
export declare const frame: (content: Content, path: string, viewport?: Viewport
|
|
89
|
+
export declare const frame: (content: Content, path: string, viewport?: Viewport, colorScheme?: ColorScheme, trail?: {
|
|
90
|
+
readonly canGoBack: boolean;
|
|
91
|
+
readonly canGoForward: boolean;
|
|
92
|
+
}) => typeof FrameScope.Type;
|
package/dist/scope.js
CHANGED
|
@@ -38,6 +38,12 @@ export const FrameScope = Schema.Struct({
|
|
|
38
38
|
toc: Schema.Array(Schema.Struct({ id: Schema.String, href: Schema.String, label: Schema.String, level: Schema.Number })),
|
|
39
39
|
/** How much room the page has: `"compact"`, `"medium"` or `"expanded"` (see `Viewport`). `{#if viewport == "compact"}` chooses a layout for a phone. The server renders `expanded`; the browser corrects it after it starts. */
|
|
40
40
|
viewport: Schema.Literal("compact", "medium", "expanded"),
|
|
41
|
+
/** The colors the person's system asks for: `"light"` or `"dark"`. `theme={theme == "system" ? colorScheme : theme}` lets an application offer "follow the system" beside its own choices. The server renders `light`; the page corrects it after it starts. */
|
|
42
|
+
colorScheme: Schema.Literal("light", "dark"),
|
|
43
|
+
/** Whether there is a page before this one in the trail (`back()` goes to it). `disabled={!canGoBack}` greys a Back button. */
|
|
44
|
+
canGoBack: Schema.Boolean,
|
|
45
|
+
/** Whether there is a page after this one in the trail (`forward()` goes to it), as there is after a `back()`. */
|
|
46
|
+
canGoForward: Schema.Boolean,
|
|
41
47
|
/** The page's body. Draw it with `<blocks items={blocks} />`, or draw each block yourself. */
|
|
42
48
|
blocks: Schema.Array(BlockView),
|
|
43
49
|
/** Neighbours in navigation order; `href` is empty when there is none. */
|
|
@@ -103,7 +109,7 @@ export const documentTitle = (content, page, parts = {}) => {
|
|
|
103
109
|
const body = shown === app ? shown : content.site.title.replaceAll("{page}", shown).replaceAll("{site}", app);
|
|
104
110
|
return parts.status === undefined || parts.status === "" ? body : `(${parts.status}) ${body}`;
|
|
105
111
|
};
|
|
106
|
-
export const frame = (content, path, viewport = "expanded") => {
|
|
112
|
+
export const frame = (content, path, viewport = "expanded", colorScheme = "light", trail = { canGoBack: false, canGoForward: false }) => {
|
|
107
113
|
const page = pageAt(content, path);
|
|
108
114
|
const shown = page ?? notFoundPage(content);
|
|
109
115
|
const at = page === undefined ? -1 : content.pages.indexOf(page);
|
|
@@ -120,6 +126,9 @@ export const frame = (content, path, viewport = "expanded") => {
|
|
|
120
126
|
return {
|
|
121
127
|
site: content.site.name,
|
|
122
128
|
viewport,
|
|
129
|
+
colorScheme,
|
|
130
|
+
canGoBack: trail.canGoBack,
|
|
131
|
+
canGoForward: trail.canGoForward,
|
|
123
132
|
home: withBase(content.site.base, "/"),
|
|
124
133
|
title: shown.title,
|
|
125
134
|
description: shown.description,
|
package/dist/server.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { renderHead } from "@valancex/valance/web/plugin";
|
|
2
2
|
import { renderToHtml } from "@valancex/valance/web/server";
|
|
3
3
|
import { Effect } from "effect";
|
|
4
|
-
import { application,
|
|
4
|
+
import { application, stateFor } from "./app.js";
|
|
5
|
+
import { earlyHead } from "./early.js";
|
|
6
|
+
import { plugin } from "./web-app.js";
|
|
5
7
|
import { routes } from "./routes.js";
|
|
6
8
|
export { routes };
|
|
7
9
|
const safe = (json) => json.replaceAll("<", "\\u003c").replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029");
|
|
@@ -14,7 +16,7 @@ export const createRenderer = ({ content, program, author }) => {
|
|
|
14
16
|
const boot = safe(JSON.stringify({ state: served.state, built }));
|
|
15
17
|
return {
|
|
16
18
|
status: table.known(pathname) ? 200 : 404,
|
|
17
|
-
html: `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">${renderHead(served.head)}</head><body><div id="app">${served.html}</div><script id="valance-boot" type="application/json">${boot}</script><script type="module" src="${built.script}"></script></body></html>`,
|
|
19
|
+
html: `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">${built.wasm === undefined ? "" : `<link rel="modulepreload" href="${built.script}">`}${earlyHead(built)}${renderHead(served.head)}</head><body><div id="app" data-vlx="starting">${served.html}</div><script id="valance-boot" type="application/json">${boot}</script><script type="module" src="${built.script}"></script></body></html>`,
|
|
18
20
|
};
|
|
19
21
|
};
|
|
20
22
|
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { StoreClient } from "./store.js";
|
|
2
|
+
/** Where an application keeps its data on this machine: `$XDG_DATA_HOME` (else `~/.local/share`) on Linux, `%APPDATA%` on Windows, `~/Library/Application Support` on macOS, then the application's name. */
|
|
3
|
+
export declare const userDataDir: (name: string, env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform) => string;
|
|
4
|
+
export declare const fileStore: (dir: string) => StoreClient;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// The desktop's store: one JSON file per key in the user's data directory for the application. Node only (the page never loads this). A write goes to a temporary file and is renamed over the
|
|
2
|
+
// real one, so a crash leaves the old value or the new one, never half of one.
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { checkKey, decodeStored, encodeStored, MAX_STORED, StoreError } from "./store.js";
|
|
8
|
+
/** Where an application keeps its data on this machine: `$XDG_DATA_HOME` (else `~/.local/share`) on Linux, `%APPDATA%` on Windows, `~/Library/Application Support` on macOS, then the application's name. */
|
|
9
|
+
export const userDataDir = (name, env = process.env, platform = process.platform) => {
|
|
10
|
+
const app = name.replace(/[^\w.-]+/g, "-").replace(/^[.-]+/, "") || "app";
|
|
11
|
+
const base = platform === "win32"
|
|
12
|
+
? (env["APPDATA"] ?? join(homedir(), "AppData", "Roaming"))
|
|
13
|
+
: platform === "darwin"
|
|
14
|
+
? join(homedir(), "Library", "Application Support")
|
|
15
|
+
: (env["XDG_DATA_HOME"] ?? join(homedir(), ".local", "share"));
|
|
16
|
+
return join(base, app);
|
|
17
|
+
};
|
|
18
|
+
const unavailable = () => new StoreError("Unavailable", "the data directory refused");
|
|
19
|
+
export const fileStore = (dir) => {
|
|
20
|
+
const file = (key) => join(dir, `${key}.json`);
|
|
21
|
+
return {
|
|
22
|
+
get: (key, schema) => Effect.flatMap(checkKey(key), (safe) => Effect.flatMap(Effect.try({
|
|
23
|
+
try: () => {
|
|
24
|
+
try {
|
|
25
|
+
return readFileSync(file(safe), "utf8");
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error.code === "ENOENT") {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
catch: unavailable,
|
|
35
|
+
}), (text) => (text === null ? Effect.succeed(undefined) : decodeStored(text, schema)))),
|
|
36
|
+
set: (key, schema, value) => Effect.flatMap(checkKey(key), (safe) => Effect.flatMap(encodeStored(value, schema), (text) => text.length > MAX_STORED
|
|
37
|
+
? Effect.fail(new StoreError("Full", "the value is larger than a store holds"))
|
|
38
|
+
: Effect.try({
|
|
39
|
+
try: () => {
|
|
40
|
+
mkdirSync(dir, { recursive: true });
|
|
41
|
+
const temporary = `${file(safe)}.${process.pid}.tmp`;
|
|
42
|
+
writeFileSync(temporary, text);
|
|
43
|
+
renameSync(temporary, file(safe));
|
|
44
|
+
},
|
|
45
|
+
catch: unavailable,
|
|
46
|
+
}))),
|
|
47
|
+
remove: (key) => Effect.flatMap(checkKey(key), (safe) => Effect.try({ try: () => rmSync(file(safe), { force: true }), catch: unavailable })),
|
|
48
|
+
};
|
|
49
|
+
};
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as Nexus from "@valancex/nexus";
|
|
2
|
+
import { Effect, Schema } from "effect";
|
|
3
|
+
/** Why a store did not do what was asked, by kind. */
|
|
4
|
+
export declare class StoreError extends Error {
|
|
5
|
+
readonly kind: "Unavailable" | "Full" | "Decode" | "Unsafe";
|
|
6
|
+
readonly _tag = "StoreError";
|
|
7
|
+
constructor(kind: "Unavailable" | "Full" | "Decode" | "Unsafe", message: string);
|
|
8
|
+
}
|
|
9
|
+
export interface StoreClient {
|
|
10
|
+
/** The value stored under `key`, decoded by `schema`; `undefined` when nothing is stored. */
|
|
11
|
+
readonly get: <A>(key: string, schema: Schema.Schema<A, any, never>) => Effect.Effect<A | undefined, StoreError>;
|
|
12
|
+
/** Stores `value` under `key`, encoded by `schema`. */
|
|
13
|
+
readonly set: <A>(key: string, schema: Schema.Schema<A, any, never>, value: A) => Effect.Effect<void, StoreError>;
|
|
14
|
+
/** Forgets `key`. */
|
|
15
|
+
readonly remove: (key: string) => Effect.Effect<void, StoreError>;
|
|
16
|
+
}
|
|
17
|
+
/** The capability: ask for it in a command (`Store.get`), and the platform says whether it has one. */
|
|
18
|
+
export declare const Store: Nexus.Capability.Capability<StoreClient>;
|
|
19
|
+
type Needs = Nexus.Capability.EnvironmentShape;
|
|
20
|
+
type Failure = StoreError | Nexus.Capability.CapabilityUnavailableError;
|
|
21
|
+
export declare const get: <A>(key: string, schema: Schema.Schema<A, any, never>) => Effect.Effect<A | undefined, Failure, Needs>;
|
|
22
|
+
export declare const set: <A>(key: string, schema: Schema.Schema<A, any, never>, value: A) => Effect.Effect<void, Failure, Needs>;
|
|
23
|
+
export declare const remove: (key: string) => Effect.Effect<void, Failure, Needs>;
|
|
24
|
+
export declare const checkKey: (key: string) => Effect.Effect<string, StoreError>;
|
|
25
|
+
/** Decodes stored text with `schema`: text that is not JSON, or does not fit, is `Decode`. */
|
|
26
|
+
export declare const decodeStored: <A>(text: string, schema: Schema.Schema<A, any, never>) => Effect.Effect<A, StoreError>;
|
|
27
|
+
export declare const encodeStored: <A>(value: A, schema: Schema.Schema<A, any, never>) => Effect.Effect<string, StoreError>;
|
|
28
|
+
/** The part of `Storage` the web's store uses (so a test supplies its own). */
|
|
29
|
+
export interface TextStorage {
|
|
30
|
+
readonly getItem: (key: string) => string | null;
|
|
31
|
+
readonly setItem: (key: string, value: string) => void;
|
|
32
|
+
readonly removeItem: (key: string) => void;
|
|
33
|
+
}
|
|
34
|
+
/** The longest value written, in characters: a page's storage is small and shared. */
|
|
35
|
+
export declare const MAX_STORED = 1000000;
|
|
36
|
+
/** The web's store: the page's `localStorage`, under `valance:<namespace>:<key>`. Where storage is off (a private window, blocked site data) every call is `Unavailable`, and the application runs without it. */
|
|
37
|
+
export declare const webStore: (namespace: string, storage?: TextStorage) => StoreClient;
|
|
38
|
+
/** The platform that supplies `store` as the `Store` capability. */
|
|
39
|
+
export declare const storePlatform: (store: StoreClient) => Nexus.Application.Platform;
|
|
40
|
+
export {};
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// What an application keeps between runs. NEXUS and VALANCE hold no storage (their rule): what an application needs from where it runs is a *capability*, a typed contract the application states
|
|
2
|
+
// and the platform supplies. This is that contract for a small keyed store, and the web's implementation of it (the page's `localStorage`). The desktop's, a file in the user's data directory,
|
|
3
|
+
// is `./store-file` (it needs Node, so the page never loads it).
|
|
4
|
+
//
|
|
5
|
+
// const saved = yield* Store.get("items", Items); // Effect<readonly Item[] | undefined, StoreError, the Store capability>
|
|
6
|
+
// yield* Store.set("items", Items, next);
|
|
7
|
+
//
|
|
8
|
+
// A value goes in as JSON and comes out *decoded by the Schema the caller states*: what is stored by an older version of the application, or by anything else, and no longer fits, is not a
|
|
9
|
+
// value: it is `Decode`, and nothing wrong reaches state. A failure says what kind it was and never what it carried.
|
|
10
|
+
import * as Nexus from "@valancex/nexus";
|
|
11
|
+
import { Effect, Schema } from "effect";
|
|
12
|
+
/** Why a store did not do what was asked, by kind. */
|
|
13
|
+
export class StoreError extends Error {
|
|
14
|
+
kind;
|
|
15
|
+
_tag = "StoreError";
|
|
16
|
+
constructor(kind, message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.kind = kind;
|
|
19
|
+
this.name = "StoreError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** The capability: ask for it in a command (`Store.get`), and the platform says whether it has one. */
|
|
23
|
+
export const Store = Nexus.Capability.define("valance/store");
|
|
24
|
+
const client = Nexus.Capability.require(Store);
|
|
25
|
+
export const get = (key, schema) => Effect.flatMap(client, (store) => store.get(key, schema));
|
|
26
|
+
export const set = (key, schema, value) => Effect.flatMap(client, (store) => store.set(key, schema, value));
|
|
27
|
+
export const remove = (key) => Effect.flatMap(client, (store) => store.remove(key));
|
|
28
|
+
/** A key is a word: letters, digits, `.`, `_` and `-`, so it can never name anything but the entry it is. */
|
|
29
|
+
const SAFE_KEY = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
|
|
30
|
+
export const checkKey = (key) => SAFE_KEY.test(key) ? Effect.succeed(key) : Effect.fail(new StoreError("Unsafe", `"${key}" cannot be a store key: use letters, digits, . _ - (at most 80 characters)`));
|
|
31
|
+
/** Decodes stored text with `schema`: text that is not JSON, or does not fit, is `Decode`. */
|
|
32
|
+
export const decodeStored = (text, schema) => Effect.try({ try: () => JSON.parse(text), catch: () => new StoreError("Decode", "what is stored is not JSON") }).pipe(Effect.flatMap((json) => Schema.decodeUnknown(schema)(json).pipe(Effect.mapError(() => new StoreError("Decode", "what is stored no longer fits its type")))));
|
|
33
|
+
export const encodeStored = (value, schema) => Schema.encode(schema)(value).pipe(Effect.mapError(() => new StoreError("Decode", "the value does not fit its type")), Effect.map((encoded) => JSON.stringify(encoded)));
|
|
34
|
+
/** The longest value written, in characters: a page's storage is small and shared. */
|
|
35
|
+
export const MAX_STORED = 1_000_000;
|
|
36
|
+
/** The web's store: the page's `localStorage`, under `valance:<namespace>:<key>`. Where storage is off (a private window, blocked site data) every call is `Unavailable`, and the application runs without it. */
|
|
37
|
+
export const webStore = (namespace, storage) => {
|
|
38
|
+
const prefix = `valance:${namespace}:`;
|
|
39
|
+
const area = () => Effect.try({
|
|
40
|
+
try: () => {
|
|
41
|
+
const found = storage ?? globalThis.localStorage;
|
|
42
|
+
if (found === undefined) {
|
|
43
|
+
throw new Error("no storage");
|
|
44
|
+
}
|
|
45
|
+
return found;
|
|
46
|
+
},
|
|
47
|
+
catch: () => new StoreError("Unavailable", "this page has no storage"),
|
|
48
|
+
});
|
|
49
|
+
const guarded = (run) => Effect.flatMap(area(), (found) => Effect.try({
|
|
50
|
+
try: () => run(found),
|
|
51
|
+
catch: (error) => new StoreError(error instanceof Error && error.name === "QuotaExceededError" ? "Full" : "Unavailable", "the page's storage refused"),
|
|
52
|
+
}));
|
|
53
|
+
return {
|
|
54
|
+
get: (key, schema) => Effect.flatMap(checkKey(key), (safe) => Effect.flatMap(guarded((found) => found.getItem(`${prefix}${safe}`)), (text) => (text === null ? Effect.succeed(undefined) : decodeStored(text, schema)))),
|
|
55
|
+
set: (key, schema, value) => Effect.flatMap(checkKey(key), (safe) => Effect.flatMap(encodeStored(value, schema), (text) => text.length > MAX_STORED ? Effect.fail(new StoreError("Full", "the value is larger than a store holds")) : guarded((found) => found.setItem(`${prefix}${safe}`, text)))),
|
|
56
|
+
remove: (key) => Effect.flatMap(checkKey(key), (safe) => guarded((found) => found.removeItem(`${prefix}${safe}`))),
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
/** The platform that supplies `store` as the `Store` capability. */
|
|
60
|
+
export const storePlatform = (store) => Nexus.Capability.EnvironmentLive(new Map([[Store.id, { _tag: "Available", implementation: store }]]));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DefinedApp } from "./index.js";
|
|
2
|
+
export type TargetName = "web" | "desktop";
|
|
3
|
+
/** On a definition made by `forTarget`: the definition it was made from, so the toolchain can still see every target's templates. */
|
|
4
|
+
export declare const WHOLE: unique symbol;
|
|
5
|
+
/** The definition as `target` sees it: its own layout in place of the top-level one, its components and overrides added. */
|
|
6
|
+
export declare const forTarget: (author: DefinedApp | undefined, target: TargetName) => DefinedApp | undefined;
|
|
7
|
+
/** Every target the definition has templates for. */
|
|
8
|
+
export declare const targetsOf: (author: DefinedApp | undefined) => ReadonlyArray<TargetName>;
|
package/dist/targets.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** On a definition made by `forTarget`: the definition it was made from, so the toolchain can still see every target's templates. */
|
|
2
|
+
export const WHOLE = Symbol.for("valancex.common.whole");
|
|
3
|
+
/** The definition as `target` sees it: its own layout in place of the top-level one, its components and overrides added. */
|
|
4
|
+
export const forTarget = (author, target) => {
|
|
5
|
+
if (author === undefined) {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
const parts = author.targets?.[target];
|
|
9
|
+
if (parts === undefined) {
|
|
10
|
+
return author;
|
|
11
|
+
}
|
|
12
|
+
const layout = parts.layout ?? author.layout;
|
|
13
|
+
return {
|
|
14
|
+
...author,
|
|
15
|
+
...{ [WHOLE]: author },
|
|
16
|
+
...(layout === undefined ? {} : { layout }),
|
|
17
|
+
components: [...(author.components ?? []), ...(parts.components ?? [])],
|
|
18
|
+
override: [...(author.override ?? []), ...(parts.override ?? [])],
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
/** Every target the definition has templates for. */
|
|
22
|
+
export const targetsOf = (author) => Object.keys(author?.targets ?? {});
|
package/dist/vocabulary.d.ts
CHANGED
|
@@ -1,9 +1,54 @@
|
|
|
1
1
|
import type { WebPrimitives } from "@valancex/port-web";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* A target's vocabulary has one of two forms. The web's is a table of PORT Web's realizations, from which the manifest is derived (`tags`). Any other target's is a MESH manifest, as data
|
|
4
|
+
* (`components`): the PORT ships it (the Desktop PORT's `vocabulary.json` and `content.json`), and the build reads it as it is.
|
|
5
|
+
*/
|
|
6
|
+
export interface WebVocabulary {
|
|
3
7
|
/** The target's id (`web`). */
|
|
4
8
|
readonly target: string;
|
|
5
9
|
/** The tags, each with its props and events. */
|
|
6
10
|
readonly tags: WebPrimitives;
|
|
11
|
+
/**
|
|
12
|
+
* The props a template may leave out, by tag. PORT Web's table says how a prop is realized, not whether it is required, so the manifest derived from it makes every prop required but a boolean
|
|
13
|
+
* one; this is the author-facing rule beside it. An absent prop is not in the render tree, and PORT Web realizes an absent prop as no attribute (or the element's own initial value).
|
|
14
|
+
*/
|
|
15
|
+
readonly optional?: Readonly<Record<string, ReadonlyArray<string>>>;
|
|
16
|
+
/**
|
|
17
|
+
* What each tag's props and events are, as a specification states them: the type of a prop and whether a use must write it, which the table alone cannot say. Where present it settles the manifest
|
|
18
|
+
* entry's props and events (the table still says how they are realized); `optional` then applies to what it does not cover.
|
|
19
|
+
*/
|
|
20
|
+
/** Tags that were renamed, old to new: a template that writes the old one is told the new one. */
|
|
21
|
+
readonly renamed?: Readonly<Record<string, string>>;
|
|
22
|
+
/** Props that were renamed, by tag, old to new: a template that writes the old one is told the new one. */
|
|
23
|
+
readonly renamedProps?: Readonly<Record<string, Readonly<Record<string, string>>>>;
|
|
24
|
+
readonly rules?: {
|
|
25
|
+
readonly named: ReadonlyArray<string>;
|
|
26
|
+
/** The tags that are a modal layer: none is inside another (`nested-modal`). */
|
|
27
|
+
readonly modal?: ReadonlyArray<string>;
|
|
28
|
+
readonly only: Readonly<Record<string, ReadonlyArray<string>>>;
|
|
29
|
+
readonly within: Readonly<Record<string, ReadonlyArray<string>>>;
|
|
30
|
+
};
|
|
31
|
+
readonly declared?: Readonly<Record<string, {
|
|
32
|
+
readonly props: Readonly<Record<string, {
|
|
33
|
+
readonly type: Readonly<Record<string, unknown>>;
|
|
34
|
+
readonly required: boolean;
|
|
35
|
+
}>>;
|
|
36
|
+
readonly events: Readonly<Record<string, {
|
|
37
|
+
readonly payload?: Readonly<Record<string, unknown>>;
|
|
38
|
+
}>>;
|
|
39
|
+
}>>;
|
|
7
40
|
}
|
|
41
|
+
export interface ManifestVocabulary {
|
|
42
|
+
readonly target: string;
|
|
43
|
+
/** MESH manifest components: each with its props and events. */
|
|
44
|
+
readonly components: Readonly<Record<string, unknown>>;
|
|
45
|
+
}
|
|
46
|
+
export type Vocabulary = WebVocabulary | ManifestVocabulary;
|
|
47
|
+
/** The names a template may write as a primitive in this vocabulary. */
|
|
48
|
+
export declare const namesOf: (vocabulary: Vocabulary) => ReadonlyArray<string>;
|
|
49
|
+
/** A vocabulary that is a MESH manifest (or several: their components are merged, and a name may not be in two). */
|
|
50
|
+
export declare const fromManifests: (target: string, ...manifests: ReadonlyArray<{
|
|
51
|
+
readonly components: Readonly<Record<string, unknown>>;
|
|
52
|
+
}>) => ManifestVocabulary;
|
|
8
53
|
/** The web's vocabulary: HTML elements, with `class`, `theme`, `id` and `aria-*` as styling and accessibility hooks. */
|
|
9
|
-
export declare const webVocabulary:
|
|
54
|
+
export declare const webVocabulary: WebVocabulary;
|
package/dist/vocabulary.js
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
-
import { primitives } from "./primitives.js";
|
|
1
|
+
import { declared, primitives, renamed, renamedProps, rules } from "./primitives.js";
|
|
2
|
+
/** The names a template may write as a primitive in this vocabulary. */
|
|
3
|
+
export const namesOf = (vocabulary) => Object.keys("tags" in vocabulary ? vocabulary.tags : vocabulary.components);
|
|
4
|
+
/** A vocabulary that is a MESH manifest (or several: their components are merged, and a name may not be in two). */
|
|
5
|
+
export const fromManifests = (target, ...manifests) => {
|
|
6
|
+
const components = {};
|
|
7
|
+
for (const manifest of manifests) {
|
|
8
|
+
for (const [name, each] of Object.entries(manifest.components)) {
|
|
9
|
+
if (name in components) {
|
|
10
|
+
throw new Error(`the ${target} vocabulary declares "${name}" twice`);
|
|
11
|
+
}
|
|
12
|
+
components[name] = each;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return { target, components };
|
|
16
|
+
};
|
|
2
17
|
/** The web's vocabulary: HTML elements, with `class`, `theme`, `id` and `aria-*` as styling and accessibility hooks. */
|
|
3
|
-
export const webVocabulary = {
|
|
18
|
+
export const webVocabulary = {
|
|
19
|
+
target: "web",
|
|
20
|
+
tags: primitives,
|
|
21
|
+
declared,
|
|
22
|
+
rules,
|
|
23
|
+
renamed,
|
|
24
|
+
renamedProps,
|
|
25
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Plugin } from "@valancex/valance/web/plugin";
|
|
2
|
+
import type { AppState, Built } from "./app.js";
|
|
3
|
+
import type { DefinedApp } from "./index.js";
|
|
4
|
+
import type { Content } from "./model.js";
|
|
5
|
+
/** The plugin that makes the application a website: its tags, its URL policy, and its document head. */
|
|
6
|
+
export declare const plugin: (content: Content, built: Built, author?: DefinedApp) => Plugin<AppState>;
|
package/dist/web-app.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { titleFor } from "./app.js";
|
|
2
|
+
import { pageAt } from "./scope.js";
|
|
3
|
+
import { primitives } from "./primitives.js";
|
|
4
|
+
import { routes } from "./routes.js";
|
|
5
|
+
/** The plugin that makes the application a website: its tags, its URL policy, and its document head. */
|
|
6
|
+
export const plugin = (content, built, author) => ({
|
|
7
|
+
name: "valance-app",
|
|
8
|
+
primitives,
|
|
9
|
+
routes: routes(content),
|
|
10
|
+
head: (state) => {
|
|
11
|
+
const page = pageAt(content, state.page);
|
|
12
|
+
const shown = page ?? { description: "" };
|
|
13
|
+
const canonical = content.site.url === "" || page === undefined ? [] : [{ rel: "canonical", href: `${content.site.url}${content.site.base}${page.path.replace(/^\//, "")}` }];
|
|
14
|
+
return {
|
|
15
|
+
title: titleFor(content, page, state, author),
|
|
16
|
+
meta: shown.description === "" ? [] : [{ name: "description", content: shown.description }],
|
|
17
|
+
links: [...built.styles.map((href) => ({ rel: "stylesheet", href })), ...canonical],
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@valancex/common",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "The author's side of a VALANCE application (defineApp, component, field) and the runtime the vlx build runs: the browser and server entries. What src/main.ts imports; @valancex/cli builds what it describes.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"valance",
|
|
@@ -32,6 +32,14 @@
|
|
|
32
32
|
"types": "./dist/client.d.ts",
|
|
33
33
|
"default": "./dist/client.js"
|
|
34
34
|
},
|
|
35
|
+
"./record": {
|
|
36
|
+
"types": "./dist/record.d.ts",
|
|
37
|
+
"default": "./dist/record.js"
|
|
38
|
+
},
|
|
39
|
+
"./desktop": {
|
|
40
|
+
"types": "./dist/desktop.d.ts",
|
|
41
|
+
"default": "./dist/desktop.js"
|
|
42
|
+
},
|
|
35
43
|
"./http": {
|
|
36
44
|
"types": "./dist/http.d.ts",
|
|
37
45
|
"default": "./dist/http.js"
|
|
@@ -56,10 +64,11 @@
|
|
|
56
64
|
"provenance": true
|
|
57
65
|
},
|
|
58
66
|
"dependencies": {
|
|
59
|
-
"@valancex/mesh-runtime": "^0.
|
|
60
|
-
"@valancex/nexus": "^0.
|
|
61
|
-
"@valancex/port-web": "^0.
|
|
62
|
-
"@valancex/
|
|
67
|
+
"@valancex/mesh-runtime": "^0.15.0 || ^0.16.0 || ^0.17.0",
|
|
68
|
+
"@valancex/nexus": "^0.14.0 || ^0.15.0 || ^0.16.0",
|
|
69
|
+
"@valancex/port-web": "^0.12.0 || ^0.13.0 || ^0.14.0",
|
|
70
|
+
"@valancex/router": "^0.14.0",
|
|
71
|
+
"@valancex/valance": "^0.14.0"
|
|
63
72
|
},
|
|
64
73
|
"peerDependencies": {
|
|
65
74
|
"effect": "^3.10.0"
|