@gyral/core 0.0.0 → 0.2.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/CHANGELOG.md +32 -0
- package/LICENSE +1 -1
- package/NOTICE +11 -0
- package/README.md +45 -5
- package/dist/accessible-name.d.ts +19 -0
- package/dist/accessible-name.js +56 -0
- package/dist/accessible-name.js.map +1 -0
- package/dist/children.d.ts +37 -0
- package/dist/children.js +49 -0
- package/dist/children.js.map +1 -0
- package/dist/command.d.ts +63 -0
- package/dist/command.js +26 -0
- package/dist/command.js.map +1 -0
- package/dist/define.d.ts +9 -0
- package/dist/define.js +338 -0
- package/dist/define.js.map +1 -0
- package/dist/devtools-events.d.ts +51 -0
- package/dist/devtools-events.js +3 -0
- package/dist/devtools-events.js.map +1 -0
- package/dist/devtools-off.d.ts +9 -0
- package/dist/devtools-off.js +10 -0
- package/dist/devtools-off.js.map +1 -0
- package/dist/devtools.d.ts +13 -0
- package/dist/devtools.js +68 -0
- package/dist/devtools.js.map +1 -0
- package/dist/drivers-scope.d.ts +15 -0
- package/dist/drivers-scope.js +48 -0
- package/dist/drivers-scope.js.map +1 -0
- package/dist/element-directive.d.ts +20 -0
- package/dist/element-directive.js +31 -0
- package/dist/element-directive.js.map +1 -0
- package/dist/element-types.d.ts +34 -0
- package/dist/element-types.js +2 -0
- package/dist/element-types.js.map +1 -0
- package/dist/focus.d.ts +39 -0
- package/dist/focus.js +42 -0
- package/dist/focus.js.map +1 -0
- package/dist/forms.d.ts +59 -0
- package/dist/forms.js +138 -0
- package/dist/forms.js.map +1 -0
- package/dist/hydration.d.ts +36 -0
- package/dist/hydration.js +72 -0
- package/dist/hydration.js.map +1 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +7 -0
- package/dist/init.js +10 -0
- package/dist/init.js.map +1 -0
- package/dist/intent.d.ts +25 -0
- package/dist/intent.js +157 -0
- package/dist/intent.js.map +1 -0
- package/dist/internal/interpreter.d.ts +8 -0
- package/dist/internal/interpreter.js +166 -0
- package/dist/internal/interpreter.js.map +1 -0
- package/dist/invalid.d.ts +21 -0
- package/dist/invalid.js +70 -0
- package/dist/invalid.js.map +1 -0
- package/dist/invokers.d.ts +15 -0
- package/dist/invokers.js +64 -0
- package/dist/invokers.js.map +1 -0
- package/dist/islands.d.ts +11 -0
- package/dist/islands.js +63 -0
- package/dist/islands.js.map +1 -0
- package/dist/json-safety.d.ts +8 -0
- package/dist/json-safety.js +68 -0
- package/dist/json-safety.js.map +1 -0
- package/dist/light-dom.d.ts +61 -0
- package/dist/light-dom.js +109 -0
- package/dist/light-dom.js.map +1 -0
- package/dist/lit-html-check.d.ts +6 -0
- package/dist/lit-html-check.js +48 -0
- package/dist/lit-html-check.js.map +1 -0
- package/dist/live-boolean.d.ts +22 -0
- package/dist/live-boolean.js +34 -0
- package/dist/live-boolean.js.map +1 -0
- package/dist/props.d.ts +26 -0
- package/dist/props.js +44 -0
- package/dist/props.js.map +1 -0
- package/dist/random.d.ts +13 -0
- package/dist/random.js +19 -0
- package/dist/random.js.map +1 -0
- package/dist/states.d.ts +7 -0
- package/dist/states.js +23 -0
- package/dist/states.js.map +1 -0
- package/dist/store-binding.d.ts +14 -0
- package/dist/store-binding.js +63 -0
- package/dist/store-binding.js.map +1 -0
- package/dist/store-scope.d.ts +40 -0
- package/dist/store-scope.js +146 -0
- package/dist/store-scope.js.map +1 -0
- package/dist/store.d.ts +114 -0
- package/dist/store.js +129 -0
- package/dist/store.js.map +1 -0
- package/dist/stores-provider.d.ts +6 -0
- package/dist/stores-provider.js +57 -0
- package/dist/stores-provider.js.map +1 -0
- package/dist/styles.d.ts +12 -0
- package/dist/styles.js +11 -0
- package/dist/styles.js.map +1 -0
- package/dist/template-whitespace.d.ts +7 -0
- package/dist/template-whitespace.js +192 -0
- package/dist/template-whitespace.js.map +1 -0
- package/dist/templates.d.ts +5 -0
- package/dist/templates.js +15 -0
- package/dist/templates.js.map +1 -0
- package/dist/textarea.d.ts +26 -0
- package/dist/textarea.js +85 -0
- package/dist/textarea.js.map +1 -0
- package/dist/transitions.d.ts +9 -0
- package/dist/transitions.js +26 -0
- package/dist/transitions.js.map +1 -0
- package/dist/types.d.ts +186 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/vite.d.ts +25 -0
- package/dist/vite.js +37 -0
- package/dist/vite.js.map +1 -0
- package/package.json +41 -13
package/dist/store.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { isServer } from 'lit';
|
|
2
|
+
import { DEVTOOLS_ENABLED, devCommands, devStore } from '#devtools';
|
|
3
|
+
import { splitNext, } from './command.js';
|
|
4
|
+
import { makeInterpreter } from './internal/interpreter.js';
|
|
5
|
+
const onServer = isServer;
|
|
6
|
+
/** Defines a store. `name` keys it in registries, seeds and overrides, so keep it unique. */
|
|
7
|
+
export function defineStore(name, spec) {
|
|
8
|
+
const store = {
|
|
9
|
+
name,
|
|
10
|
+
spec,
|
|
11
|
+
instance: (initial) => createInstance(store, initial),
|
|
12
|
+
checkSeed: (seed) => checkSeed(spec.schema, seed),
|
|
13
|
+
};
|
|
14
|
+
return store;
|
|
15
|
+
}
|
|
16
|
+
const issuePath = (issue) => (issue.path ?? []).map((s) => String(typeof s === 'object' ? s.key : s)).join('.');
|
|
17
|
+
function checkSeed(schema, seed) {
|
|
18
|
+
// Sound without a schema: the seed is this store's state, serialized by the server.
|
|
19
|
+
if (schema === undefined)
|
|
20
|
+
return { ok: true, state: seed };
|
|
21
|
+
const result = schema['~standard'].validate(seed);
|
|
22
|
+
if (result instanceof Promise) {
|
|
23
|
+
return { ok: false, issues: ['the store schema is async; seeds need a synchronous schema'] };
|
|
24
|
+
}
|
|
25
|
+
if (result.issues === undefined)
|
|
26
|
+
return { ok: true, state: result.value };
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
issues: result.issues.map((i) => `${issuePath(i) || '(root)'}: ${i.message}`),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function createInstance(store, initial) {
|
|
33
|
+
// Sound: send() only calls the reducer whose key equals msg._tag.
|
|
34
|
+
const reducers = store.spec.update;
|
|
35
|
+
const listeners = new Set();
|
|
36
|
+
let interpreter;
|
|
37
|
+
let disposed = false;
|
|
38
|
+
const [initState, initCommands] = splitNext(store.spec.init());
|
|
39
|
+
let state = initial === undefined ? initState : initial;
|
|
40
|
+
const resolve = (driver) => instance.drivers[driver.name] ?? store.spec.drivers?.[driver.name] ?? driver;
|
|
41
|
+
let scope;
|
|
42
|
+
// Store-to-store writes: delivered synchronously to the other store's instance in scope.
|
|
43
|
+
const deliver = ({ store: target, msg }) => {
|
|
44
|
+
if (scope === undefined) {
|
|
45
|
+
console.warn(`store "${store.name}" sends to "${target.name}" but is not held by a store scope ` +
|
|
46
|
+
'(a registry, <gyral-stores> or the page default), so the message is dropped.');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
scope(target).send(msg);
|
|
50
|
+
};
|
|
51
|
+
const run = (commands) => {
|
|
52
|
+
if (onServer || disposed || commands.length === 0)
|
|
53
|
+
return; // never on the server (ADR 0012)
|
|
54
|
+
for (const cmd of commands) {
|
|
55
|
+
if (cmd.driver === STORE_SEND) {
|
|
56
|
+
deliver(cmd.input);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
interpreter ??= makeInterpreter(resolve, (msg) => {
|
|
60
|
+
// Stores have no IntentRejected reducer; only their own messages apply.
|
|
61
|
+
if (msg._tag !== 'IntentRejected')
|
|
62
|
+
instance.send(msg);
|
|
63
|
+
}, DEVTOOLS_ENABLED ? devCommands(() => `store:${store.name}`) : undefined);
|
|
64
|
+
interpreter.run(cmd);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const instance = {
|
|
68
|
+
store,
|
|
69
|
+
get state() {
|
|
70
|
+
return state;
|
|
71
|
+
},
|
|
72
|
+
drivers: {},
|
|
73
|
+
send(msg) {
|
|
74
|
+
const reducer = reducers[msg._tag];
|
|
75
|
+
if (reducer === undefined) {
|
|
76
|
+
console.warn(`store "${store.name}" has no update for message "${msg._tag}".`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const prev = state;
|
|
80
|
+
const [next, commands] = splitNext(reducer(prev, msg));
|
|
81
|
+
state = next;
|
|
82
|
+
if (DEVTOOLS_ENABLED)
|
|
83
|
+
devStore(store.name, msg, prev, next);
|
|
84
|
+
if (!Object.is(next, prev))
|
|
85
|
+
for (const listener of [...listeners])
|
|
86
|
+
listener(next, prev);
|
|
87
|
+
run(commands);
|
|
88
|
+
},
|
|
89
|
+
subscribe(listener) {
|
|
90
|
+
listeners.add(listener);
|
|
91
|
+
return () => listeners.delete(listener);
|
|
92
|
+
},
|
|
93
|
+
dispose() {
|
|
94
|
+
disposed = true;
|
|
95
|
+
interpreter?.dispose();
|
|
96
|
+
listeners.clear();
|
|
97
|
+
},
|
|
98
|
+
bindScope(resolver) {
|
|
99
|
+
scope = resolver;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
// A seeded instance (hydration) starts init's commands after the hydrating renders flushed,
|
|
103
|
+
// so a fast driver can't change state under them (same reason as ADR 0012 for components).
|
|
104
|
+
if (initial === undefined)
|
|
105
|
+
run(initCommands);
|
|
106
|
+
else if (initCommands.length > 0)
|
|
107
|
+
setTimeout(() => {
|
|
108
|
+
run(initCommands);
|
|
109
|
+
}, 0);
|
|
110
|
+
return instance;
|
|
111
|
+
}
|
|
112
|
+
/** Marker driver for `send()`: `define()` delivers these to the resolved store instance. */
|
|
113
|
+
export const STORE_SEND = {
|
|
114
|
+
name: '@gyral/store-send',
|
|
115
|
+
run: () => undefined,
|
|
116
|
+
};
|
|
117
|
+
/** A command that sends `msg` to the component's instance of `store`. Writes are commands. */
|
|
118
|
+
// NoInfer: the store decides the message type, so a literal (`{ _tag: 'Track', … }`) is checked
|
|
119
|
+
// against it instead of widening `_tag` to string (store-to-store sends have no other context).
|
|
120
|
+
export function send(store, msg) {
|
|
121
|
+
const input = { store, msg };
|
|
122
|
+
return { driver: STORE_SEND, input, onSuccess: () => undefined };
|
|
123
|
+
}
|
|
124
|
+
/** Narrows a `StoreChanged` message to one store's typed state, or `undefined`. */
|
|
125
|
+
export function changed(store, msg) {
|
|
126
|
+
// Sound: the message was built from this store's instance (matched by unique name).
|
|
127
|
+
return msg.store === store.name ? { state: msg.state, prev: msg.prev } : undefined;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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","sourcesContent":["// Shared state: stores as \"props from the side\" (docs/design-docs/0013-shared-state.md).\n// A store is MVI without a view: init + pure update, commands run by its own interpreter.\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport { isServer } from 'lit';\nimport { DEVTOOLS_ENABLED, devCommands, devStore } from '#devtools';\nimport {\n splitNext,\n type AnyDriver,\n type Command,\n type DriverOverrides,\n type Next,\n} from './command.js';\nimport { makeInterpreter, type Interpreter } from './internal/interpreter.js';\nimport type { IntentRejected, Tagged } from './types.js';\n\nconst onServer: boolean = isServer;\n\ntype Variant<M extends Tagged, K extends M['_tag']> = Extract<M, { readonly _tag: K }>;\n\n/** One pure reducer per message tag, like a component's `update` but without context. */\nexport type StoreUpdate<S, M extends Tagged> = {\n readonly [K in M['_tag']]: (state: S, msg: Variant<M, K>) => Next<S, M>;\n};\n\nexport interface StoreSpec<S, M extends Tagged> {\n /** Initial state (and optional commands, e.g. load a saved cart). */\n readonly init: () => Next<S, M>;\n readonly update: StoreUpdate<S, M>;\n /** Driver substitutions by name for this store's commands. */\n readonly drivers?: DriverOverrides;\n /**\n * Validates the server's page seed before the client uses it (any synchronous Standard\n * Schema). An invalid seed is reported with console.error and the store starts from `init`.\n */\n readonly schema?: StandardSchemaV1<unknown, S>;\n}\n\n/** The result of checking a seed against a store's schema. */\nexport type SeedCheck<S> =\n | { readonly ok: true; readonly state: S }\n | { readonly ok: false; readonly issues: readonly string[] };\n\n/** What `ctx.read()` accepts: anything that names a store and carries its state type. */\nexport interface StoreRef<S> {\n readonly name: string;\n /** Type-only marker for the state type. Never set at runtime. */\n readonly stateType?: (state: S) => S;\n}\n\n/** A live store: one per page on the client, one per request on the server, one per test. */\nexport interface StoreInstance<S, M extends Tagged> {\n readonly store: Store<S, M>;\n readonly state: S;\n /** Feeds a message through the store's `update` and notifies subscribers on change. */\n send(msg: M): void;\n /** Called after every state change. Returns an unsubscribe function. */\n subscribe(listener: (state: S, prev: S) => void): () => void;\n /** Per-instance driver substitutions (test fakes). Checked before the spec's. */\n drivers: DriverOverrides;\n /** Stops running commands and drops subscribers. */\n dispose(): void;\n /** Set by the registry that holds the instance (store-to-store `send`). */\n bindScope(resolve: StoreResolver): void;\n}\n\nexport interface Store<S, M extends Tagged> extends StoreRef<S> {\n readonly spec: StoreSpec<S, M>;\n /**\n * A fresh, independent instance. `initial` skips `init`'s state (a server seed, a test\n * fixture); `init`'s commands still run on the client.\n */\n instance(initial?: S): StoreInstance<S, M>;\n /** Checks a seed with `spec.schema` (ok as-is without one). */\n checkSeed(seed: unknown): SeedCheck<S>;\n /** Type-only marker for the message type. Never set at runtime. */\n readonly messageType?: (msg: M) => M;\n}\n\n/** Any store, with its types erased (for declarations and registries). */\nexport interface AnyStore {\n readonly name: string;\n instance(initial?: never): AnyStoreInstance;\n checkSeed(seed: unknown): SeedCheck<unknown>;\n}\n\n/** Any store instance, with its types erased. */\nexport interface AnyStoreInstance {\n readonly store: AnyStore;\n readonly state: unknown;\n send(msg: never): void;\n subscribe(listener: (state: unknown, prev: unknown) => void): () => void;\n dispose(): void;\n /**\n * Called by the registry that holds this instance, so the store's `send(other, msg)`\n * commands reach `other`'s instance in the same scope (gyral-czi.20).\n */\n bindScope?(resolve: StoreResolver): void;\n}\n\n/** Finds a store's instance in a scope (a registry). */\nexport type StoreResolver = (store: AnyStore) => AnyStoreInstance;\n\n/** Store instances substituted by store name (tests, islands). */\nexport type StoreOverrides = Readonly<Record<string, AnyStoreInstance>>;\n\n/** Defines a store. `name` keys it in registries, seeds and overrides, so keep it unique. */\nexport function defineStore<S, M extends Tagged>(name: string, spec: StoreSpec<S, M>): Store<S, M> {\n const store: Store<S, M> = {\n name,\n spec,\n instance: (initial?: S) => createInstance(store, initial),\n checkSeed: (seed) => checkSeed(spec.schema, seed),\n };\n return store;\n}\n\nconst issuePath = (issue: StandardSchemaV1.Issue): string =>\n (issue.path ?? []).map((s) => String(typeof s === 'object' ? s.key : s)).join('.');\n\nfunction checkSeed<S>(\n schema: StandardSchemaV1<unknown, S> | undefined,\n seed: unknown,\n): SeedCheck<S> {\n // Sound without a schema: the seed is this store's state, serialized by the server.\n if (schema === undefined) return { ok: true, state: seed as S };\n const result = schema['~standard'].validate(seed);\n if (result instanceof Promise) {\n return { ok: false, issues: ['the store schema is async; seeds need a synchronous schema'] };\n }\n if (result.issues === undefined) return { ok: true, state: result.value };\n return {\n ok: false,\n issues: result.issues.map((i) => `${issuePath(i) || '(root)'}: ${i.message}`),\n };\n}\n\nfunction createInstance<S, M extends Tagged>(\n store: Store<S, M>,\n initial: S | undefined,\n): StoreInstance<S, M> {\n // Sound: send() only calls the reducer whose key equals msg._tag.\n const reducers = store.spec.update as unknown as Readonly<\n Record<string, ((state: S, msg: M) => Next<S, M>) | undefined>\n >;\n const listeners = new Set<(state: S, prev: S) => void>();\n let interpreter: Interpreter<M | IntentRejected> | undefined;\n let disposed = false;\n\n const [initState, initCommands] = splitNext(store.spec.init());\n let state = initial === undefined ? initState : initial;\n\n const resolve = (driver: AnyDriver): AnyDriver =>\n instance.drivers[driver.name] ?? store.spec.drivers?.[driver.name] ?? driver;\n\n let scope: StoreResolver | undefined;\n\n // Store-to-store writes: delivered synchronously to the other store's instance in scope.\n const deliver = ({ store: target, msg }: StoreSendInput): void => {\n if (scope === undefined) {\n console.warn(\n `store \"${store.name}\" sends to \"${target.name}\" but is not held by a store scope ` +\n '(a registry, <gyral-stores> or the page default), so the message is dropped.',\n );\n return;\n }\n (scope(target) as { send(msg: unknown): void }).send(msg);\n };\n\n const run = (commands: ReadonlyArray<Command<M | IntentRejected>>): void => {\n if (onServer || disposed || commands.length === 0) return; // never on the server (ADR 0012)\n for (const cmd of commands) {\n if (cmd.driver === STORE_SEND) {\n deliver(cmd.input as StoreSendInput);\n continue;\n }\n interpreter ??= makeInterpreter<M | IntentRejected>(\n resolve,\n (msg) => {\n // Stores have no IntentRejected reducer; only their own messages apply.\n if (msg._tag !== 'IntentRejected') instance.send(msg as M);\n },\n DEVTOOLS_ENABLED ? devCommands(() => `store:${store.name}`) : undefined,\n );\n interpreter.run(cmd);\n }\n };\n\n const instance: StoreInstance<S, M> = {\n store,\n get state() {\n return state;\n },\n drivers: {},\n send(msg) {\n const reducer = reducers[msg._tag];\n if (reducer === undefined) {\n console.warn(`store \"${store.name}\" has no update for message \"${msg._tag}\".`);\n return;\n }\n const prev = state;\n const [next, commands] = splitNext(reducer(prev, msg));\n state = next;\n if (DEVTOOLS_ENABLED) devStore(store.name, msg, prev, next);\n if (!Object.is(next, prev)) for (const listener of [...listeners]) listener(next, prev);\n run(commands);\n },\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n dispose() {\n disposed = true;\n interpreter?.dispose();\n listeners.clear();\n },\n bindScope(resolver) {\n scope = resolver;\n },\n };\n\n // A seeded instance (hydration) starts init's commands after the hydrating renders flushed,\n // so a fast driver can't change state under them (same reason as ADR 0012 for components).\n if (initial === undefined) run(initCommands);\n else if (initCommands.length > 0)\n setTimeout(() => {\n run(initCommands);\n }, 0);\n return instance;\n}\n\n/** Marker driver for `send()`: `define()` delivers these to the resolved store instance. */\nexport const STORE_SEND = {\n name: '@gyral/store-send',\n run: () => undefined,\n} as const;\n\n/** The input of a `send()` command. */\nexport interface StoreSendInput {\n readonly store: AnyStore;\n readonly msg: Tagged;\n}\n\n/** A command that sends `msg` to the component's instance of `store`. Writes are commands. */\n// NoInfer: the store decides the message type, so a literal (`{ _tag: 'Track', … }`) is checked\n// against it instead of widening `_tag` to string (store-to-store sends have no other context).\nexport function send<S, M extends Tagged>(store: Store<S, M>, msg: NoInfer<M>): Command<never> {\n const input: StoreSendInput = { store, msg };\n return { driver: STORE_SEND, input, onSuccess: () => undefined };\n}\n\n/** Framework message: a store this component reads changed (ADR 0013). Optional reducer. */\nexport interface StoreChanged {\n readonly _tag: 'StoreChanged';\n /** The store's name. Narrow with `changed(store, msg)`. */\n readonly store: string;\n readonly state: unknown;\n readonly prev: unknown;\n}\n\n/** Narrows a `StoreChanged` message to one store's typed state, or `undefined`. */\nexport function changed<S>(\n store: StoreRef<S>,\n msg: StoreChanged,\n): { readonly state: S; readonly prev: S } | undefined {\n // Sound: the message was built from this store's instance (matched by unique name).\n return msg.store === store.name ? { state: msg.state as S, prev: msg.prev as S } : undefined;\n}\n"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registers `<gyral-stores>` for server renders; `@gyral/ssr` calls it. A no-op in the browser,
|
|
3
|
+
* where a plain `<gyral-stores>` element already scopes stores and the server's shadow root
|
|
4
|
+
* (a `<slot>`) needs no JavaScript. Idempotent.
|
|
5
|
+
*/
|
|
6
|
+
export declare function defineStoresProvider(): void;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// `<gyral-stores>` as a real element for server rendering (gyral-czi.20, ADR 0013 addendum).
|
|
2
|
+
// On the client a provider is a plain element (scopeFor walks the DOM). On the server it must
|
|
3
|
+
// be a registered element: components find it through Lit's SSR event path, and its instances'
|
|
4
|
+
// states are written to `data-gyral-stores` for the client to restore.
|
|
5
|
+
import { css, html, isServer, LitElement } from 'lit';
|
|
6
|
+
import { warnJsonHazard } from './json-safety.js';
|
|
7
|
+
import { providerScope, STORE_SEED_ATTRIBUTE, STORES_ELEMENT, STORES_REQUEST, } from './store-scope.js';
|
|
8
|
+
const onServer = isServer;
|
|
9
|
+
/** Server-only element: renders `<slot>` in Declarative Shadow DOM, so children show as-is. */
|
|
10
|
+
class GyralStores extends LitElement {
|
|
11
|
+
static { this.styles = css `
|
|
12
|
+
:host {
|
|
13
|
+
display: contents;
|
|
14
|
+
}
|
|
15
|
+
`; }
|
|
16
|
+
#instances;
|
|
17
|
+
/** This subtree's store instances; stores not listed start from their seed or `init`. */
|
|
18
|
+
get instances() {
|
|
19
|
+
return this.#instances;
|
|
20
|
+
}
|
|
21
|
+
set instances(value) {
|
|
22
|
+
this.#instances = value;
|
|
23
|
+
// Written before the attributes render; the client restores it (providerScope()).
|
|
24
|
+
if (value !== undefined && value.length > 0) {
|
|
25
|
+
const seed = Object.fromEntries(value.map((i) => [i.store.name, i.state]));
|
|
26
|
+
for (const [name, state] of Object.entries(seed)) {
|
|
27
|
+
warnJsonHazard(`store "${name}" in <${STORES_ELEMENT}>`, state, 'state');
|
|
28
|
+
}
|
|
29
|
+
this.setAttribute(STORE_SEED_ATTRIBUTE, JSON.stringify(seed));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
constructor() {
|
|
33
|
+
super();
|
|
34
|
+
// Lit's SSR routes a slotted child's events slot → getRootNode() → host, and the root is
|
|
35
|
+
// only this element's shadow root if one exists; the renderer never calls connectedCallback
|
|
36
|
+
// (which would create it), so attach it here.
|
|
37
|
+
this.attachShadow({ mode: 'open' });
|
|
38
|
+
this.addEventListener(STORES_REQUEST, (event) => {
|
|
39
|
+
event.stopPropagation();
|
|
40
|
+
event.detail.registry = providerScope(this);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
render() {
|
|
44
|
+
return html `<slot></slot>`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Registers `<gyral-stores>` for server renders; `@gyral/ssr` calls it. A no-op in the browser,
|
|
49
|
+
* where a plain `<gyral-stores>` element already scopes stores and the server's shadow root
|
|
50
|
+
* (a `<slot>`) needs no JavaScript. Idempotent.
|
|
51
|
+
*/
|
|
52
|
+
export function defineStoresProvider() {
|
|
53
|
+
if (onServer && customElements.get(STORES_ELEMENT) === undefined) {
|
|
54
|
+
customElements.define(STORES_ELEMENT, GyralStores);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=stores-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stores-provider.js","sourceRoot":"","sources":["../src/stores-provider.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,8FAA8F;AAC9F,+FAA+F;AAC/F,uEAAuE;AACvE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,cAAc,EACd,cAAc,GAEf,MAAM,kBAAkB,CAAC;AAE1B,MAAM,QAAQ,GAAY,QAAQ,CAAC;AAEnC,+FAA+F;AAC/F,MAAM,WAAY,SAAQ,UAAU;aAClB,WAAM,GAAG,GAAG,CAAA;;;;GAI3B,CAAC;IAEF,UAAU,CAA0C;IAEpD,yFAAyF;IACzF,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IAED,IAAI,SAAS,CAAC,KAA8C;QAC1D,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;QACxB,kFAAkF;QAClF,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC5C,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAC3E,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;gBACjD,cAAc,CAAC,UAAU,IAAI,SAAS,cAAc,GAAG,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;YAC3E,CAAC;YACD,IAAI,CAAC,YAAY,CAAC,oBAAoB,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;QAChE,CAAC;IACH,CAAC;IAED;QACE,KAAK,EAAE,CAAC;QACR,yFAAyF;QACzF,4FAA4F;QAC5F,8CAA8C;QAC9C,IAAI,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;QACpC,IAAI,CAAC,gBAAgB,CAAC,cAAc,EAAE,CAAC,KAAK,EAAE,EAAE;YAC9C,KAAK,CAAC,eAAe,EAAE,CAAC;YACvB,KAAoC,CAAC,MAAM,CAAC,QAAQ,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;QAC9E,CAAC,CAAC,CAAC;IACL,CAAC;IAEkB,MAAM;QACvB,OAAO,IAAI,CAAA,eAAe,CAAC;IAC7B,CAAC;;AAGH;;;;GAIG;AACH,MAAM,UAAU,oBAAoB;IAClC,IAAI,QAAQ,IAAI,cAAc,CAAC,GAAG,CAAC,cAAc,CAAC,KAAK,SAAS,EAAE,CAAC;QACjE,cAAc,CAAC,MAAM,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;IACrD,CAAC;AACH,CAAC","sourcesContent":["// `<gyral-stores>` as a real element for server rendering (gyral-czi.20, ADR 0013 addendum).\n// On the client a provider is a plain element (scopeFor walks the DOM). On the server it must\n// be a registered element: components find it through Lit's SSR event path, and its instances'\n// states are written to `data-gyral-stores` for the client to restore.\nimport { css, html, isServer, LitElement } from 'lit';\nimport { warnJsonHazard } from './json-safety.js';\nimport type { AnyStoreInstance } from './store.js';\nimport {\n providerScope,\n STORE_SEED_ATTRIBUTE,\n STORES_ELEMENT,\n STORES_REQUEST,\n type StoresRequest,\n} from './store-scope.js';\n\nconst onServer: boolean = isServer;\n\n/** Server-only element: renders `<slot>` in Declarative Shadow DOM, so children show as-is. */\nclass GyralStores extends LitElement {\n static override styles = css`\n :host {\n display: contents;\n }\n `;\n\n #instances: readonly AnyStoreInstance[] | undefined;\n\n /** This subtree's store instances; stores not listed start from their seed or `init`. */\n get instances(): readonly AnyStoreInstance[] | undefined {\n return this.#instances;\n }\n\n set instances(value: readonly AnyStoreInstance[] | undefined) {\n this.#instances = value;\n // Written before the attributes render; the client restores it (providerScope()).\n if (value !== undefined && value.length > 0) {\n const seed = Object.fromEntries(value.map((i) => [i.store.name, i.state]));\n for (const [name, state] of Object.entries(seed)) {\n warnJsonHazard(`store \"${name}\" in <${STORES_ELEMENT}>`, state, 'state');\n }\n this.setAttribute(STORE_SEED_ATTRIBUTE, JSON.stringify(seed));\n }\n }\n\n constructor() {\n super();\n // Lit's SSR routes a slotted child's events slot → getRootNode() → host, and the root is\n // only this element's shadow root if one exists; the renderer never calls connectedCallback\n // (which would create it), so attach it here.\n this.attachShadow({ mode: 'open' });\n this.addEventListener(STORES_REQUEST, (event) => {\n event.stopPropagation();\n (event as CustomEvent<StoresRequest>).detail.registry = providerScope(this);\n });\n }\n\n protected override render(): unknown {\n return html`<slot></slot>`;\n }\n}\n\n/**\n * Registers `<gyral-stores>` for server renders; `@gyral/ssr` calls it. A no-op in the browser,\n * where a plain `<gyral-stores>` element already scopes stores and the server's shadow root\n * (a `<slot>`) needs no JavaScript. Idempotent.\n */\nexport function defineStoresProvider(): void {\n if (onServer && customElements.get(STORES_ELEMENT) === undefined) {\n customElements.define(STORES_ELEMENT, GyralStores);\n }\n}\n"]}
|
package/dist/styles.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type CSSResultGroup, type CSSResultOrNative } from 'lit';
|
|
2
|
+
/**
|
|
3
|
+
* What `spec.styles` accepts:
|
|
4
|
+
* - `css` tagged templates (Lit's `CSSResult`);
|
|
5
|
+
* - plain strings, e.g. a stylesheet module shared with the document (`import base from
|
|
6
|
+
* './base.css?raw'`). Strings are trusted CSS from your own code, never user input;
|
|
7
|
+
* - `CSSStyleSheet` instances (constructable stylesheets, shared across components);
|
|
8
|
+
* - arrays of any of these, nested freely.
|
|
9
|
+
*/
|
|
10
|
+
export type Styles = CSSResultOrNative | string | readonly Styles[];
|
|
11
|
+
/** Normalizes `Styles` into what Lit's `static styles` expects. */
|
|
12
|
+
export declare function toCssResultGroup(styles: Styles): CSSResultGroup;
|
package/dist/styles.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Component styles (gyral-czi.24): `css` templates, plain strings or constructed sheets.
|
|
2
|
+
import { unsafeCSS } from 'lit';
|
|
3
|
+
/** Normalizes `Styles` into what Lit's `static styles` expects. */
|
|
4
|
+
export function toCssResultGroup(styles) {
|
|
5
|
+
if (typeof styles === 'string')
|
|
6
|
+
return unsafeCSS(styles);
|
|
7
|
+
if (Array.isArray(styles))
|
|
8
|
+
return styles.map(toCssResultGroup);
|
|
9
|
+
return styles;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=styles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"styles.js","sourceRoot":"","sources":["../src/styles.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,OAAO,EAAE,SAAS,EAA+C,MAAM,KAAK,CAAC;AAY7E,mEAAmE;AACnE,MAAM,UAAU,gBAAgB,CAAC,MAAc;IAC7C,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC,MAAM,CAAC,CAAC;IACzD,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAAE,OAAQ,MAA4B,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IACtF,OAAO,MAA2B,CAAC;AACrC,CAAC","sourcesContent":["// Component styles (gyral-czi.24): `css` templates, plain strings or constructed sheets.\nimport { unsafeCSS, type CSSResultGroup, type CSSResultOrNative } from 'lit';\n\n/**\n * What `spec.styles` accepts:\n * - `css` tagged templates (Lit's `CSSResult`);\n * - plain strings, e.g. a stylesheet module shared with the document (`import base from\n * './base.css?raw'`). Strings are trusted CSS from your own code, never user input;\n * - `CSSStyleSheet` instances (constructable stylesheets, shared across components);\n * - arrays of any of these, nested freely.\n */\nexport type Styles = CSSResultOrNative | string | readonly Styles[];\n\n/** Normalizes `Styles` into what Lit's `static styles` expects. */\nexport function toCssResultGroup(styles: Styles): CSSResultGroup {\n if (typeof styles === 'string') return unsafeCSS(styles);\n if (Array.isArray(styles)) return (styles as readonly Styles[]).map(toCssResultGroup);\n return styles as CSSResultOrNative;\n}\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Minified copy of template strings (same length, bindings unmoved). Idempotent. */
|
|
2
|
+
export declare function minifyStrings(strings: readonly string[]): string[];
|
|
3
|
+
/**
|
|
4
|
+
* The minified TemplateStringsArray for a call site. Cached by identity, so Lit's own template
|
|
5
|
+
* cache (keyed by the strings array) still hits once per call site.
|
|
6
|
+
*/
|
|
7
|
+
export declare function minifyTemplate(strings: TemplateStringsArray): TemplateStringsArray;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// Template whitespace minification (gyral-9rf, ADR 0016 addendum).
|
|
2
|
+
//
|
|
3
|
+
// lit-html keeps every newline and indent between tags as a DOM text node; compilers (Svelte,
|
|
4
|
+
// Solid, JSX) drop them. In a benchmark table row that is 12 extra nodes out of 25, and removing
|
|
5
|
+
// them made create/replace/clear 11-24% faster (gyral-1kq). Gyral's `html` and `svg` tags run the
|
|
6
|
+
// template strings through `minifyTemplate()` once per call site (cached by strings identity),
|
|
7
|
+
// so the server and the browser always build the same template: hydration digests match in any
|
|
8
|
+
// toolchain (Vite, tsx, plain Node), with no build step.
|
|
9
|
+
//
|
|
10
|
+
// Rules (the same text everywhere, documented in docs/references/consumer-setup.md):
|
|
11
|
+
// - Whitespace-only text that contains a newline is removed when it sits next to a template
|
|
12
|
+
// edge, a block-level tag, or the inside edge of a <button>/<select> (CSS never renders it
|
|
13
|
+
// there). Between two inline neighbours (phrasing elements, custom elements, bindings,
|
|
14
|
+
// comments) it collapses to one space.
|
|
15
|
+
// - Other runs of whitespace in text collapse to one space; a leading or trailing run with a
|
|
16
|
+
// newline next to one of those edges is removed.
|
|
17
|
+
// - <pre>, <textarea>, <script>, <style> and <title> contents, tags and attribute values,
|
|
18
|
+
// and comments are copied unchanged. Binding positions never move: the result has the same
|
|
19
|
+
// number of strings, so Lit parts and SSR hydration markers line up.
|
|
20
|
+
// The function is idempotent: minifying minified strings changes nothing.
|
|
21
|
+
/** Elements whose box is block-level (or never laid out inline) by default. */
|
|
22
|
+
const BLOCK = new Set(('address article aside blockquote body caption col colgroup datalist dd details dialog div ' +
|
|
23
|
+
'dl dt fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header hgroup hr html ' +
|
|
24
|
+
'legend li main menu nav ol optgroup option p pre search section select summary table tbody ' +
|
|
25
|
+
'td tfoot th thead tr ul ' +
|
|
26
|
+
// SVG containers and shapes: whitespace between them is never rendered.
|
|
27
|
+
'circle clippath defs ellipse g line lineargradient marker mask path pattern polygon ' +
|
|
28
|
+
'polyline radialgradient rect stop symbol use').split(' '));
|
|
29
|
+
/** Elements whose text content is copied verbatim (whitespace is meaningful or raw text). */
|
|
30
|
+
const RAW_TEXT = new Set(['script', 'style', 'textarea', 'title']);
|
|
31
|
+
const PRESERVE = 'pre';
|
|
32
|
+
/**
|
|
33
|
+
* Inline-block controls: they sit inline among their neighbours, but whitespace at the start or
|
|
34
|
+
* end of their own content is never rendered (it is at the edge of their own line box).
|
|
35
|
+
*/
|
|
36
|
+
const INLINE_BOX = new Set(['button', 'select']);
|
|
37
|
+
/**
|
|
38
|
+
* The edge a tag makes for the text next to it. `inside` is true when the text is inside the
|
|
39
|
+
* element (after its start tag, or before its end tag).
|
|
40
|
+
*/
|
|
41
|
+
function edgeOfTag(name, inside) {
|
|
42
|
+
const lower = name.toLowerCase();
|
|
43
|
+
if (BLOCK.has(lower))
|
|
44
|
+
return 'block';
|
|
45
|
+
return inside && INLINE_BOX.has(lower) ? 'block' : 'inline';
|
|
46
|
+
}
|
|
47
|
+
const WS = /^[ \t\n\r\f]*$/;
|
|
48
|
+
const RUN = /[ \t\n\r\f]+/g;
|
|
49
|
+
function minifyChunk(text, left, right) {
|
|
50
|
+
if (text === '')
|
|
51
|
+
return text;
|
|
52
|
+
if (WS.test(text)) {
|
|
53
|
+
const removable = /[\n\r]/.test(text) &&
|
|
54
|
+
(left === 'template' || right === 'template' || left === 'block' || right === 'block');
|
|
55
|
+
return removable ? '' : ' ';
|
|
56
|
+
}
|
|
57
|
+
// Mixed text: a leading or trailing run with a newline next to a removable edge goes too.
|
|
58
|
+
const dropStart = left !== 'inline' && /^[ \t\f]*[\n\r]/.test(text);
|
|
59
|
+
const dropEnd = right !== 'inline' && /[\n\r][ \t\n\r\f]*$/.test(text);
|
|
60
|
+
let body = text.replace(RUN, ' ');
|
|
61
|
+
if (dropStart)
|
|
62
|
+
body = body.replace(/^ /, '');
|
|
63
|
+
if (dropEnd)
|
|
64
|
+
body = body.replace(/ $/, '');
|
|
65
|
+
return body;
|
|
66
|
+
}
|
|
67
|
+
const Mode = { Text: 0, Tag: 1, Comment: 2, RawText: 3 };
|
|
68
|
+
const TAG_START = /^<(\/?)([A-Za-z!?][^\s/>]*)/;
|
|
69
|
+
/** Minify one segment, carrying the HTML scan state across bindings. */
|
|
70
|
+
function minifySegment(segment, state, last) {
|
|
71
|
+
let out = '';
|
|
72
|
+
let chunk = '';
|
|
73
|
+
const flushText = (right) => {
|
|
74
|
+
out += state.preDepth > 0 ? chunk : minifyChunk(chunk, state.left, right);
|
|
75
|
+
chunk = '';
|
|
76
|
+
};
|
|
77
|
+
let i = 0;
|
|
78
|
+
while (i < segment.length) {
|
|
79
|
+
const ch = segment.charAt(i);
|
|
80
|
+
if (state.mode === Mode.Comment) {
|
|
81
|
+
if (segment.startsWith('-->', i)) {
|
|
82
|
+
out += '-->';
|
|
83
|
+
i += 3;
|
|
84
|
+
state.mode = Mode.Text;
|
|
85
|
+
state.left = 'inline';
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
out += ch;
|
|
89
|
+
i += 1;
|
|
90
|
+
}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (state.mode === Mode.RawText) {
|
|
94
|
+
const close = `</${state.rawName}`;
|
|
95
|
+
if (segment.slice(i, i + close.length).toLowerCase() === close) {
|
|
96
|
+
state.mode = Mode.Text;
|
|
97
|
+
state.left = 'inline';
|
|
98
|
+
continue; // the closing tag is read in Text mode
|
|
99
|
+
}
|
|
100
|
+
out += ch;
|
|
101
|
+
i += 1;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (state.mode === Mode.Tag) {
|
|
105
|
+
out += ch;
|
|
106
|
+
i += 1;
|
|
107
|
+
if (state.quote !== '') {
|
|
108
|
+
if (ch === state.quote)
|
|
109
|
+
state.quote = '';
|
|
110
|
+
}
|
|
111
|
+
else if (ch === '"' || ch === "'") {
|
|
112
|
+
state.quote = ch;
|
|
113
|
+
}
|
|
114
|
+
else if (ch === '>') {
|
|
115
|
+
endTag(state, out.endsWith('/>'));
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
// Text mode.
|
|
120
|
+
if (ch === '<' && segment.startsWith('<!--', i)) {
|
|
121
|
+
flushText('inline');
|
|
122
|
+
state.mode = Mode.Comment;
|
|
123
|
+
out += '<!--';
|
|
124
|
+
i += 4;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const tag = ch === '<' ? TAG_START.exec(segment.slice(i)) : null;
|
|
128
|
+
if (tag !== null) {
|
|
129
|
+
const name = (tag[2] ?? '').toLowerCase();
|
|
130
|
+
const closing = tag[1] === '/';
|
|
131
|
+
flushText(edgeOfTag(name, closing));
|
|
132
|
+
state.closing = closing;
|
|
133
|
+
state.tagName = name;
|
|
134
|
+
state.mode = Mode.Tag;
|
|
135
|
+
out += tag[0];
|
|
136
|
+
i += tag[0].length;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
chunk += ch;
|
|
140
|
+
i += 1;
|
|
141
|
+
}
|
|
142
|
+
if (state.mode === Mode.Text) {
|
|
143
|
+
flushText(last ? 'template' : 'inline');
|
|
144
|
+
state.left = 'inline'; // the next segment starts after a binding
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
function endTag(state, selfClosing) {
|
|
149
|
+
const name = state.tagName;
|
|
150
|
+
state.mode = Mode.Text;
|
|
151
|
+
state.left = edgeOfTag(name, !state.closing && !selfClosing);
|
|
152
|
+
if (name === PRESERVE) {
|
|
153
|
+
if (state.closing)
|
|
154
|
+
state.preDepth = Math.max(0, state.preDepth - 1);
|
|
155
|
+
else if (!selfClosing)
|
|
156
|
+
state.preDepth += 1;
|
|
157
|
+
}
|
|
158
|
+
if (!state.closing && !selfClosing && RAW_TEXT.has(name)) {
|
|
159
|
+
state.mode = Mode.RawText;
|
|
160
|
+
state.rawName = name;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Minified copy of template strings (same length, bindings unmoved). Idempotent. */
|
|
164
|
+
export function minifyStrings(strings) {
|
|
165
|
+
const state = {
|
|
166
|
+
mode: Mode.Text,
|
|
167
|
+
quote: '',
|
|
168
|
+
tagName: '',
|
|
169
|
+
closing: false,
|
|
170
|
+
rawName: '',
|
|
171
|
+
preDepth: 0,
|
|
172
|
+
left: 'template',
|
|
173
|
+
};
|
|
174
|
+
return strings.map((segment, index) => minifySegment(segment, state, index === strings.length - 1));
|
|
175
|
+
}
|
|
176
|
+
const cache = new WeakMap();
|
|
177
|
+
/**
|
|
178
|
+
* The minified TemplateStringsArray for a call site. Cached by identity, so Lit's own template
|
|
179
|
+
* cache (keyed by the strings array) still hits once per call site.
|
|
180
|
+
*/
|
|
181
|
+
export function minifyTemplate(strings) {
|
|
182
|
+
const cached = cache.get(strings);
|
|
183
|
+
if (cached !== undefined)
|
|
184
|
+
return cached;
|
|
185
|
+
const minified = minifyStrings(strings);
|
|
186
|
+
// Lit accepts only arrays with an own `raw` field (its anti-spoofing check), as lit/static-html
|
|
187
|
+
// builds them. Cooked strings are what Lit reads; `raw` mirrors them.
|
|
188
|
+
const result = Object.freeze(Object.assign(minified, { raw: Object.freeze([...minified]) }));
|
|
189
|
+
cache.set(strings, result);
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=template-whitespace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template-whitespace.js","sourceRoot":"","sources":["../src/template-whitespace.ts"],"names":[],"mappings":"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","sourcesContent":["// Template whitespace minification (gyral-9rf, ADR 0016 addendum).\n//\n// lit-html keeps every newline and indent between tags as a DOM text node; compilers (Svelte,\n// Solid, JSX) drop them. In a benchmark table row that is 12 extra nodes out of 25, and removing\n// them made create/replace/clear 11-24% faster (gyral-1kq). Gyral's `html` and `svg` tags run the\n// template strings through `minifyTemplate()` once per call site (cached by strings identity),\n// so the server and the browser always build the same template: hydration digests match in any\n// toolchain (Vite, tsx, plain Node), with no build step.\n//\n// Rules (the same text everywhere, documented in docs/references/consumer-setup.md):\n// - Whitespace-only text that contains a newline is removed when it sits next to a template\n// edge, a block-level tag, or the inside edge of a <button>/<select> (CSS never renders it\n// there). Between two inline neighbours (phrasing elements, custom elements, bindings,\n// comments) it collapses to one space.\n// - Other runs of whitespace in text collapse to one space; a leading or trailing run with a\n// newline next to one of those edges is removed.\n// - <pre>, <textarea>, <script>, <style> and <title> contents, tags and attribute values,\n// and comments are copied unchanged. Binding positions never move: the result has the same\n// number of strings, so Lit parts and SSR hydration markers line up.\n// The function is idempotent: minifying minified strings changes nothing.\n\n/** Elements whose box is block-level (or never laid out inline) by default. */\nconst BLOCK = new Set(\n (\n 'address article aside blockquote body caption col colgroup datalist dd details dialog div ' +\n 'dl dt fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header hgroup hr html ' +\n 'legend li main menu nav ol optgroup option p pre search section select summary table tbody ' +\n 'td tfoot th thead tr ul ' +\n // SVG containers and shapes: whitespace between them is never rendered.\n 'circle clippath defs ellipse g line lineargradient marker mask path pattern polygon ' +\n 'polyline radialgradient rect stop symbol use'\n ).split(' '),\n);\n\n/** Elements whose text content is copied verbatim (whitespace is meaningful or raw text). */\nconst RAW_TEXT = new Set(['script', 'style', 'textarea', 'title']);\nconst PRESERVE = 'pre';\n\n/** What sits on one side of a text chunk. */\ntype Edge = 'template' | 'inline' | 'block';\n\n/**\n * Inline-block controls: they sit inline among their neighbours, but whitespace at the start or\n * end of their own content is never rendered (it is at the edge of their own line box).\n */\nconst INLINE_BOX = new Set(['button', 'select']);\n\n/**\n * The edge a tag makes for the text next to it. `inside` is true when the text is inside the\n * element (after its start tag, or before its end tag).\n */\nfunction edgeOfTag(name: string, inside: boolean): Edge {\n const lower = name.toLowerCase();\n if (BLOCK.has(lower)) return 'block';\n return inside && INLINE_BOX.has(lower) ? 'block' : 'inline';\n}\n\nconst WS = /^[ \\t\\n\\r\\f]*$/;\nconst RUN = /[ \\t\\n\\r\\f]+/g;\n\nfunction minifyChunk(text: string, left: Edge, right: Edge): string {\n if (text === '') return text;\n if (WS.test(text)) {\n const removable =\n /[\\n\\r]/.test(text) &&\n (left === 'template' || right === 'template' || left === 'block' || right === 'block');\n return removable ? '' : ' ';\n }\n // Mixed text: a leading or trailing run with a newline next to a removable edge goes too.\n const dropStart = left !== 'inline' && /^[ \\t\\f]*[\\n\\r]/.test(text);\n const dropEnd = right !== 'inline' && /[\\n\\r][ \\t\\n\\r\\f]*$/.test(text);\n let body = text.replace(RUN, ' ');\n if (dropStart) body = body.replace(/^ /, '');\n if (dropEnd) body = body.replace(/ $/, '');\n return body;\n}\n\nconst Mode = { Text: 0, Tag: 1, Comment: 2, RawText: 3 } as const;\ntype Mode = (typeof Mode)[keyof typeof Mode];\n\ninterface Scan {\n mode: Mode;\n /** Quote char while inside an attribute value in a tag, else ''. */\n quote: string;\n /** Name of the tag being read (lower case), and whether it is a closing tag. */\n tagName: string;\n closing: boolean;\n /** Raw-text element we are inside (its contents are copied verbatim). */\n rawName: string;\n /** Depth of open <pre> elements: text is copied verbatim while > 0. */\n preDepth: number;\n /** Edge type of the boundary before the current text chunk. */\n left: Edge;\n}\n\nconst TAG_START = /^<(\\/?)([A-Za-z!?][^\\s/>]*)/;\n\n/** Minify one segment, carrying the HTML scan state across bindings. */\nfunction minifySegment(segment: string, state: Scan, last: boolean): string {\n let out = '';\n let chunk = '';\n const flushText = (right: Edge): void => {\n out += state.preDepth > 0 ? chunk : minifyChunk(chunk, state.left, right);\n chunk = '';\n };\n let i = 0;\n while (i < segment.length) {\n const ch = segment.charAt(i);\n if (state.mode === Mode.Comment) {\n if (segment.startsWith('-->', i)) {\n out += '-->';\n i += 3;\n state.mode = Mode.Text;\n state.left = 'inline';\n } else {\n out += ch;\n i += 1;\n }\n continue;\n }\n if (state.mode === Mode.RawText) {\n const close = `</${state.rawName}`;\n if (segment.slice(i, i + close.length).toLowerCase() === close) {\n state.mode = Mode.Text;\n state.left = 'inline';\n continue; // the closing tag is read in Text mode\n }\n out += ch;\n i += 1;\n continue;\n }\n if (state.mode === Mode.Tag) {\n out += ch;\n i += 1;\n if (state.quote !== '') {\n if (ch === state.quote) state.quote = '';\n } else if (ch === '\"' || ch === \"'\") {\n state.quote = ch;\n } else if (ch === '>') {\n endTag(state, out.endsWith('/>'));\n }\n continue;\n }\n // Text mode.\n if (ch === '<' && segment.startsWith('<!--', i)) {\n flushText('inline');\n state.mode = Mode.Comment;\n out += '<!--';\n i += 4;\n continue;\n }\n const tag = ch === '<' ? TAG_START.exec(segment.slice(i)) : null;\n if (tag !== null) {\n const name = (tag[2] ?? '').toLowerCase();\n const closing = tag[1] === '/';\n flushText(edgeOfTag(name, closing));\n state.closing = closing;\n state.tagName = name;\n state.mode = Mode.Tag;\n out += tag[0];\n i += tag[0].length;\n continue;\n }\n chunk += ch;\n i += 1;\n }\n if (state.mode === Mode.Text) {\n flushText(last ? 'template' : 'inline');\n state.left = 'inline'; // the next segment starts after a binding\n }\n return out;\n}\n\nfunction endTag(state: Scan, selfClosing: boolean): void {\n const name = state.tagName;\n state.mode = Mode.Text;\n state.left = edgeOfTag(name, !state.closing && !selfClosing);\n if (name === PRESERVE) {\n if (state.closing) state.preDepth = Math.max(0, state.preDepth - 1);\n else if (!selfClosing) state.preDepth += 1;\n }\n if (!state.closing && !selfClosing && RAW_TEXT.has(name)) {\n state.mode = Mode.RawText;\n state.rawName = name;\n }\n}\n\n/** Minified copy of template strings (same length, bindings unmoved). Idempotent. */\nexport function minifyStrings(strings: readonly string[]): string[] {\n const state: Scan = {\n mode: Mode.Text,\n quote: '',\n tagName: '',\n closing: false,\n rawName: '',\n preDepth: 0,\n left: 'template',\n };\n return strings.map((segment, index) =>\n minifySegment(segment, state, index === strings.length - 1),\n );\n}\n\nconst cache = new WeakMap<TemplateStringsArray, TemplateStringsArray>();\n\n/**\n * The minified TemplateStringsArray for a call site. Cached by identity, so Lit's own template\n * cache (keyed by the strings array) still hits once per call site.\n */\nexport function minifyTemplate(strings: TemplateStringsArray): TemplateStringsArray {\n const cached = cache.get(strings);\n if (cached !== undefined) return cached;\n const minified = minifyStrings(strings);\n // Lit accepts only arrays with an own `raw` field (its anti-spoofing check), as lit/static-html\n // builds them. Cooked strings are what Lit reads; `raw` mirrors them.\n const result = Object.freeze(\n Object.assign(minified, { raw: Object.freeze([...minified]) }),\n ) as unknown as TemplateStringsArray;\n cache.set(strings, result);\n return result;\n}\n"]}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type TemplateResult } from 'lit';
|
|
2
|
+
/** Lit's `html`, with indentation whitespace between tags removed. */
|
|
3
|
+
export declare function html(strings: TemplateStringsArray, ...values: unknown[]): TemplateResult<1>;
|
|
4
|
+
/** Lit's `svg`, with indentation whitespace between elements removed. */
|
|
5
|
+
export declare function svg(strings: TemplateStringsArray, ...values: unknown[]): TemplateResult<2>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Gyral's `html` and `svg` template tags: Lit's, with template whitespace minified once per call
|
|
2
|
+
// site (template-whitespace.ts, gyral-9rf). Server and browser run the same code, so the
|
|
3
|
+
// templates (and their hydration digests) are identical in every toolchain. For exact
|
|
4
|
+
// whitespace outside <pre>/<textarea>, import `html` from 'lit' instead.
|
|
5
|
+
import { html as litHtml, svg as litSvg } from 'lit';
|
|
6
|
+
import { minifyTemplate } from './template-whitespace.js';
|
|
7
|
+
/** Lit's `html`, with indentation whitespace between tags removed. */
|
|
8
|
+
export function html(strings, ...values) {
|
|
9
|
+
return litHtml(minifyTemplate(strings), ...values);
|
|
10
|
+
}
|
|
11
|
+
/** Lit's `svg`, with indentation whitespace between elements removed. */
|
|
12
|
+
export function svg(strings, ...values) {
|
|
13
|
+
return litSvg(minifyTemplate(strings), ...values);
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=templates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"templates.js","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,yFAAyF;AACzF,sFAAsF;AACtF,yEAAyE;AACzE,OAAO,EAAE,IAAI,IAAI,OAAO,EAAE,GAAG,IAAI,MAAM,EAAuB,MAAM,KAAK,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE1D,sEAAsE;AACtE,MAAM,UAAU,IAAI,CAAC,OAA6B,EAAE,GAAG,MAAiB;IACtE,OAAO,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,EAAE,GAAG,MAAM,CAAC,CAAC;AACrD,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,GAAG,CAAC,OAA6B,EAAE,GAAG,MAAiB;IACrE,OAAO,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,EAAE,GAAG,MAAM,CAAC,CAAC;AACpD,CAAC","sourcesContent":["// Gyral's `html` and `svg` template tags: Lit's, with template whitespace minified once per call\n// site (template-whitespace.ts, gyral-9rf). Server and browser run the same code, so the\n// templates (and their hydration digests) are identical in every toolchain. For exact\n// whitespace outside <pre>/<textarea>, import `html` from 'lit' instead.\nimport { html as litHtml, svg as litSvg, type TemplateResult } from 'lit';\nimport { minifyTemplate } from './template-whitespace.js';\n\n/** Lit's `html`, with indentation whitespace between tags removed. */\nexport function html(strings: TemplateStringsArray, ...values: unknown[]): TemplateResult<1> {\n return litHtml(minifyTemplate(strings), ...values);\n}\n\n/** Lit's `svg`, with indentation whitespace between elements removed. */\nexport function svg(strings: TemplateStringsArray, ...values: unknown[]): TemplateResult<2> {\n return litSvg(minifyTemplate(strings), ...values);\n}\n"]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Directive, type ChildPart, type PartInfo } from 'lit/directive.js';
|
|
2
|
+
export type TextareaAttribute = string | number | boolean | null | undefined;
|
|
3
|
+
export interface TextareaOptions {
|
|
4
|
+
/** The model's value. Applied to the live element only when the model changes. */
|
|
5
|
+
readonly value: string;
|
|
6
|
+
/** Attributes such as name, id, rows, required, maxlength, data-intent, aria-invalid. */
|
|
7
|
+
readonly attrs?: Readonly<Record<string, TextareaAttribute>>;
|
|
8
|
+
}
|
|
9
|
+
/** The markup the server writes, and the client's first render: escaped, no bindings inside. */
|
|
10
|
+
export declare function textareaMarkup({ value, attrs }: TextareaOptions): string;
|
|
11
|
+
declare class TextareaDirective extends Directive {
|
|
12
|
+
#private;
|
|
13
|
+
constructor(info: PartInfo);
|
|
14
|
+
render(options: TextareaOptions): unknown;
|
|
15
|
+
update(part: ChildPart, [options]: [TextareaOptions]): unknown;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A `<textarea>` whose content comes from the model, safe for SSR and hydration:
|
|
19
|
+
*
|
|
20
|
+
* ${textarea({ value: s.message, attrs: { name: 'message', rows: 5, required: true,
|
|
21
|
+
* 'data-intent': i.Message } })}
|
|
22
|
+
*
|
|
23
|
+
* Text typed before or after hydration is kept until the model's value changes.
|
|
24
|
+
*/
|
|
25
|
+
export declare const textarea: (options: TextareaOptions) => import("lit/directive.js").DirectiveResult<typeof TextareaDirective>;
|
|
26
|
+
export {};
|