@sigmx/astro 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +26 -0
- package/dist/client.d.ts +4 -0
- package/dist/client.js +7 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +41 -0
- package/dist/server.d.ts +106 -0
- package/dist/server.js +181 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Callum Bonnyman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @sigmx/astro
|
|
2
|
+
|
|
3
|
+
Astro integration and server helpers for [sigmx](../../README.md).
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
// astro.config.mjs
|
|
7
|
+
import { defineConfig } from 'astro/config'
|
|
8
|
+
import sigmx from '@sigmx/astro'
|
|
9
|
+
|
|
10
|
+
export default defineConfig({ integrations: [sigmx()] })
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
// src/pages/api/hello.ts
|
|
15
|
+
import { readSignals, sseStream } from '@sigmx/astro/server'
|
|
16
|
+
export const prerender = false
|
|
17
|
+
export const GET = async ({ request }) => {
|
|
18
|
+
const { name } = await readSignals(request)
|
|
19
|
+
return sseStream(async (s) => {
|
|
20
|
+
s.patchSignals({ greeting: `hello ${name}` })
|
|
21
|
+
s.patchElements(`<p id="out">hello ${name}</p>`)
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Full guide: the sigmx documentation site in `website/` of https://github.com/wrux/sigmx, page "SDKs → Astro".
|
package/dist/client.d.ts
ADDED
package/dist/client.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Convenience re-exports for app-local entrypoints:
|
|
2
|
+
// import { createSigmx, all, plugins } from '@sigmx/astro/client'
|
|
3
|
+
// createSigmx({ plugins: [plugins.text, plugins.on] })
|
|
4
|
+
export * from 'sigmx';
|
|
5
|
+
export * as plugins from 'sigmx/plugins';
|
|
6
|
+
export { all } from 'sigmx/presets/all';
|
|
7
|
+
export { minimal } from 'sigmx/presets/minimal';
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { AstroIntegration } from 'astro';
|
|
2
|
+
import { type AutoOptions, type PrecompileOptions as CorePrecompileOptions } from 'sigmx/vite';
|
|
3
|
+
export interface PrecompileOptions extends Omit<CorePrecompileOptions, 'root' | 'prefixes'> {
|
|
4
|
+
/**
|
|
5
|
+
* Keep the runtime compiler as a fallback for expressions the scan cannot see (markup built from
|
|
6
|
+
* strings at render time). Default true. Set false to drop the compiler from the bundle entirely;
|
|
7
|
+
* unseen expressions then throw.
|
|
8
|
+
*/
|
|
9
|
+
fallback?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface SigmxIntegrationOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Inject the client on every page (default true). Set false and import `@sigmx/astro/client`
|
|
14
|
+
* from a `<script>` on the pages that need it.
|
|
15
|
+
*/
|
|
16
|
+
inject?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Module specifier of an app-local entrypoint that creates the instance itself, e.g.
|
|
19
|
+
* `/src/sigmx.ts`. When set, the other client options are ignored.
|
|
20
|
+
*/
|
|
21
|
+
entrypoint?: string;
|
|
22
|
+
/**
|
|
23
|
+
* `'all'` (default), `'minimal'`, `'auto'` (scan `src/` and register only what is used), or a
|
|
24
|
+
* list of plugin export names from `sigmx/plugins`.
|
|
25
|
+
*/
|
|
26
|
+
plugins?: 'all' | 'minimal' | 'auto' | string[];
|
|
27
|
+
/** Options for `plugins: 'auto'`: `always`, `custom`, `include`, `extensions`, `log`. */
|
|
28
|
+
auto?: Omit<AutoOptions, 'root' | 'prefixes'>;
|
|
29
|
+
/** Attribute prefix(es) to scan. Default `data-`. */
|
|
30
|
+
prefix?: string | string[];
|
|
31
|
+
/** Server event-name prefixes to accept. Default `sigmx-`. */
|
|
32
|
+
eventPrefix?: string | string[];
|
|
33
|
+
/** Name of the global the instance is exposed as (default `sigmx`); false for none. */
|
|
34
|
+
expose?: string | false;
|
|
35
|
+
/**
|
|
36
|
+
* Compile attribute expressions at build time into a function table, so the browser never calls
|
|
37
|
+
* `new Function`. `true` for defaults, or an options object.
|
|
38
|
+
*/
|
|
39
|
+
precompile?: boolean | PrecompileOptions;
|
|
40
|
+
}
|
|
41
|
+
/** Source of the injected client module. Exported for tests and custom entrypoints. */
|
|
42
|
+
export declare const bootScript: (o?: SigmxIntegrationOptions) => string;
|
|
43
|
+
export default function sigmx(options?: SigmxIntegrationOptions): AstroIntegration;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { sigmxAuto, sigmxPrecompile } from 'sigmx/vite';
|
|
2
|
+
const AUTO = 'virtual:sigmx-plugins';
|
|
3
|
+
const EXPRESSIONS = 'virtual:sigmx-expressions';
|
|
4
|
+
/** Source of the injected client module. Exported for tests and custom entrypoints. */
|
|
5
|
+
export const bootScript = (o = {}) => {
|
|
6
|
+
const options = JSON.stringify({ prefix: o.prefix, eventPrefix: o.eventPrefix });
|
|
7
|
+
const imports = Array.isArray(o.plugins)
|
|
8
|
+
? `import { ${o.plugins.join(', ')} } from 'sigmx/plugins';\nconst plugins = [${o.plugins.join(', ')}];`
|
|
9
|
+
: o.plugins === 'auto'
|
|
10
|
+
? `import { plugins } from '${AUTO}';`
|
|
11
|
+
: `import { ${o.plugins ?? 'all'} as plugins } from 'sigmx/presets/${o.plugins ?? 'all'}';`;
|
|
12
|
+
const expose = o.expose === false ? '' : `\nwindow[${JSON.stringify(o.expose ?? 'sigmx')}] = app;`;
|
|
13
|
+
if (!o.precompile)
|
|
14
|
+
return `import { createSigmx } from 'sigmx';\n${imports}\nconst app = createSigmx({ plugins, ...${options} });${expose}\n`;
|
|
15
|
+
const fallback = typeof o.precompile === 'object' && o.precompile.fallback === false ? 'undefined' : 'runtimeExpressions(functionCompiler)';
|
|
16
|
+
return (`import { createRuntime, precompiled, runtimeExpressions, functionCompiler } from 'sigmx';\nimport { table } from '${EXPRESSIONS}';\n${imports}\n` +
|
|
17
|
+
`const app = createRuntime({ plugins, expressions: precompiled(table, ${fallback}), ...${options} });${expose}\n`);
|
|
18
|
+
};
|
|
19
|
+
export default function sigmx(options = {}) {
|
|
20
|
+
return {
|
|
21
|
+
name: '@sigmx/astro',
|
|
22
|
+
hooks: {
|
|
23
|
+
'astro:config:setup': ({ injectScript, updateConfig, config }) => {
|
|
24
|
+
const root = new URL(config.root).pathname;
|
|
25
|
+
const prefixes = [].concat(options.prefix ?? 'data-');
|
|
26
|
+
const plugins = [];
|
|
27
|
+
if (options.plugins === 'auto')
|
|
28
|
+
plugins.push(sigmxAuto({ ...options.auto, root, prefixes }));
|
|
29
|
+
if (options.precompile) {
|
|
30
|
+
const po = typeof options.precompile === 'object' ? options.precompile : {};
|
|
31
|
+
plugins.push(sigmxPrecompile({ include: po.include, extensions: po.extensions, custom: po.custom ?? options.auto?.custom, root, prefixes }));
|
|
32
|
+
}
|
|
33
|
+
if (plugins.length)
|
|
34
|
+
updateConfig({ vite: { plugins } });
|
|
35
|
+
if (options.inject === false)
|
|
36
|
+
return;
|
|
37
|
+
injectScript('page', options.entrypoint ? `import ${JSON.stringify(options.entrypoint)};` : bootScript(options));
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/** Query-string key that carries signals on GET/DELETE requests. */
|
|
2
|
+
export declare const SIGNALS_KEY = "sigmx";
|
|
3
|
+
export declare const EVENT_PATCH_ELEMENTS = "sigmx-patch-elements";
|
|
4
|
+
export declare const EVENT_PATCH_SIGNALS = "sigmx-patch-signals";
|
|
5
|
+
export declare const SSE_HEADERS: {
|
|
6
|
+
readonly 'content-type': "text/event-stream";
|
|
7
|
+
readonly 'cache-control': "no-cache";
|
|
8
|
+
readonly connection: "keep-alive";
|
|
9
|
+
};
|
|
10
|
+
export type PatchMode = 'outer' | 'inner' | 'replace' | 'prepend' | 'append' | 'before' | 'after' | 'remove';
|
|
11
|
+
export interface ServerEvent {
|
|
12
|
+
event: string;
|
|
13
|
+
/** Data lines, each `key value`. */
|
|
14
|
+
lines: string[];
|
|
15
|
+
id?: string;
|
|
16
|
+
retry?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface EventOptions {
|
|
19
|
+
id?: string;
|
|
20
|
+
retry?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface PatchElementsOptions extends EventOptions {
|
|
23
|
+
selector?: string;
|
|
24
|
+
mode?: PatchMode;
|
|
25
|
+
useViewTransition?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface PatchSignalsOptions extends EventOptions {
|
|
28
|
+
onlyIfMissing?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface ExecuteScriptOptions extends EventOptions {
|
|
31
|
+
/** Remove the script element after it runs (default true). */
|
|
32
|
+
autoRemove?: boolean;
|
|
33
|
+
attributes?: Record<string, string>;
|
|
34
|
+
}
|
|
35
|
+
/** Patch HTML into the page. Without a selector, top-level elements are morphed by id. */
|
|
36
|
+
export declare const patchElements: (html: string, o?: PatchElementsOptions) => ServerEvent;
|
|
37
|
+
export declare const removeElements: (selector: string, o?: EventOptions) => ServerEvent;
|
|
38
|
+
/** Merge signals (JSON merge-patch: null removes). Accepts an object or pre-serialised JSON. */
|
|
39
|
+
export declare const patchSignals: (signals: Record<string, unknown> | string, o?: PatchSignalsOptions) => ServerEvent;
|
|
40
|
+
/** Remove signals by dotted path. */
|
|
41
|
+
export declare const removeSignals: (paths: string[], o?: EventOptions) => ServerEvent;
|
|
42
|
+
/** Run JavaScript in the page by appending a script element to the body. */
|
|
43
|
+
export declare const executeScript: (script: string, o?: ExecuteScriptOptions) => ServerEvent;
|
|
44
|
+
/** Serialise one event in `text/event-stream` format. */
|
|
45
|
+
export declare const formatEvent: (e: ServerEvent) => string;
|
|
46
|
+
/** A streaming event-source response you write to over time. */
|
|
47
|
+
export declare class SigmxStream {
|
|
48
|
+
readonly response: Response;
|
|
49
|
+
private controller;
|
|
50
|
+
private readonly encoder;
|
|
51
|
+
private open;
|
|
52
|
+
constructor(init?: ResponseInit);
|
|
53
|
+
get closed(): boolean;
|
|
54
|
+
send(event: ServerEvent): this;
|
|
55
|
+
patchElements(html: string, o?: PatchElementsOptions): this;
|
|
56
|
+
removeElements(selector: string, o?: EventOptions): this;
|
|
57
|
+
patchSignals(signals: Record<string, unknown> | string, o?: PatchSignalsOptions): this;
|
|
58
|
+
removeSignals(paths: string[], o?: EventOptions): this;
|
|
59
|
+
executeScript(script: string, o?: ExecuteScriptOptions): this;
|
|
60
|
+
close(): void;
|
|
61
|
+
}
|
|
62
|
+
/** A complete event-source response from a fixed list of events. */
|
|
63
|
+
export declare const sse: (...events: ServerEvent[]) => Response;
|
|
64
|
+
/** Stream events while `fn` runs; the response closes when it resolves. */
|
|
65
|
+
export declare const sseStream: (fn: (stream: SigmxStream) => Promise<void> | void, init?: ResponseInit) => Response;
|
|
66
|
+
/** A plain HTML response; the client morphs it by id, or honours the selector and mode headers. */
|
|
67
|
+
export declare const html: (body: string, o?: PatchElementsOptions & {
|
|
68
|
+
status?: number;
|
|
69
|
+
}) => Response;
|
|
70
|
+
/** A plain JSON response; the client merges it as signals. */
|
|
71
|
+
export declare const json: (signals: Record<string, unknown>, o?: PatchSignalsOptions & {
|
|
72
|
+
status?: number;
|
|
73
|
+
}) => Response;
|
|
74
|
+
/** Minimal Standard Schema (standardschema.dev) so any compliant validator works without a dependency. */
|
|
75
|
+
export interface StandardSchema<Output = unknown> {
|
|
76
|
+
readonly '~standard': {
|
|
77
|
+
readonly validate: (value: unknown) => SchemaResult<Output> | Promise<SchemaResult<Output>>;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export type SchemaIssue = {
|
|
81
|
+
readonly message: string;
|
|
82
|
+
readonly path?: ReadonlyArray<PropertyKey | {
|
|
83
|
+
readonly key: PropertyKey;
|
|
84
|
+
}>;
|
|
85
|
+
};
|
|
86
|
+
export type SchemaResult<T> = {
|
|
87
|
+
readonly value: T;
|
|
88
|
+
readonly issues?: undefined;
|
|
89
|
+
} | {
|
|
90
|
+
readonly issues: ReadonlyArray<SchemaIssue>;
|
|
91
|
+
};
|
|
92
|
+
export declare class SignalsError extends Error {
|
|
93
|
+
readonly issues: ReadonlyArray<SchemaIssue>;
|
|
94
|
+
readonly status: number;
|
|
95
|
+
constructor(message: string, issues?: ReadonlyArray<SchemaIssue>, status?: number);
|
|
96
|
+
/** A ready-made error response for endpoints that want to bail out. */
|
|
97
|
+
response(): Response;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Read the signals a sigmx request carries: the `sigmx` query parameter on GET/DELETE, a JSON
|
|
101
|
+
* body otherwise, or form fields for `contentType: 'form'` requests. Validates with `schema` if given.
|
|
102
|
+
*/
|
|
103
|
+
export declare function readSignals(request: Request): Promise<Record<string, any>>;
|
|
104
|
+
export declare function readSignals<T>(request: Request, schema: StandardSchema<T>): Promise<T>;
|
|
105
|
+
/** True when the request was made by the sigmx client (has the `Sigmx-Request` header). */
|
|
106
|
+
export declare const isSigmxRequest: (request: Request) => boolean;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// Server helpers for answering sigmx requests from Astro endpoints (or any Fetch-API runtime).
|
|
2
|
+
/** Query-string key that carries signals on GET/DELETE requests. */
|
|
3
|
+
export const SIGNALS_KEY = 'sigmx';
|
|
4
|
+
export const EVENT_PATCH_ELEMENTS = 'sigmx-patch-elements';
|
|
5
|
+
export const EVENT_PATCH_SIGNALS = 'sigmx-patch-signals';
|
|
6
|
+
export const SSE_HEADERS = {
|
|
7
|
+
'content-type': 'text/event-stream',
|
|
8
|
+
'cache-control': 'no-cache',
|
|
9
|
+
connection: 'keep-alive',
|
|
10
|
+
};
|
|
11
|
+
// ---- event builders -------------------------------------------------------
|
|
12
|
+
/** Patch HTML into the page. Without a selector, top-level elements are morphed by id. */
|
|
13
|
+
export const patchElements = (html, o = {}) => {
|
|
14
|
+
const lines = [];
|
|
15
|
+
if (o.selector)
|
|
16
|
+
lines.push(`selector ${o.selector}`);
|
|
17
|
+
if (o.mode && o.mode !== 'outer')
|
|
18
|
+
lines.push(`mode ${o.mode}`);
|
|
19
|
+
if (o.useViewTransition)
|
|
20
|
+
lines.push('useViewTransition true');
|
|
21
|
+
for (const l of html.trim().split('\n'))
|
|
22
|
+
lines.push(`elements ${l}`);
|
|
23
|
+
return { event: EVENT_PATCH_ELEMENTS, lines, id: o.id, retry: o.retry };
|
|
24
|
+
};
|
|
25
|
+
export const removeElements = (selector, o = {}) => ({
|
|
26
|
+
event: EVENT_PATCH_ELEMENTS,
|
|
27
|
+
lines: [`selector ${selector}`, 'mode remove'],
|
|
28
|
+
...o,
|
|
29
|
+
});
|
|
30
|
+
/** Merge signals (JSON merge-patch: null removes). Accepts an object or pre-serialised JSON. */
|
|
31
|
+
export const patchSignals = (signals, o = {}) => ({
|
|
32
|
+
event: EVENT_PATCH_SIGNALS,
|
|
33
|
+
lines: [...(o.onlyIfMissing ? ['onlyIfMissing true'] : []), `signals ${typeof signals === 'string' ? signals : JSON.stringify(signals)}`],
|
|
34
|
+
id: o.id,
|
|
35
|
+
retry: o.retry,
|
|
36
|
+
});
|
|
37
|
+
/** Remove signals by dotted path. */
|
|
38
|
+
export const removeSignals = (paths, o = {}) => {
|
|
39
|
+
const patch = {};
|
|
40
|
+
for (const p of paths) {
|
|
41
|
+
const keys = p.split('.');
|
|
42
|
+
const last = keys.pop();
|
|
43
|
+
let cur = patch;
|
|
44
|
+
for (const k of keys)
|
|
45
|
+
cur = cur[k] ??= {};
|
|
46
|
+
cur[last] = null;
|
|
47
|
+
}
|
|
48
|
+
return patchSignals(patch, o);
|
|
49
|
+
};
|
|
50
|
+
/** Run JavaScript in the page by appending a script element to the body. */
|
|
51
|
+
export const executeScript = (script, o = {}) => {
|
|
52
|
+
const attrs = { ...(o.autoRemove ?? true ? { 'data-init': 'el.remove()' } : {}), ...o.attributes };
|
|
53
|
+
const attrText = Object.entries(attrs)
|
|
54
|
+
.map(([k, v]) => ` ${k}="${v.replace(/"/g, '"')}"`)
|
|
55
|
+
.join('');
|
|
56
|
+
return patchElements(`<script${attrText}>${script}</script>`, { selector: 'body', mode: 'append', id: o.id, retry: o.retry });
|
|
57
|
+
};
|
|
58
|
+
/** Serialise one event in `text/event-stream` format. */
|
|
59
|
+
export const formatEvent = (e) => [e.id !== undefined && `id: ${e.id}`, e.retry !== undefined && `retry: ${e.retry}`, `event: ${e.event}`, ...e.lines.map((l) => `data: ${l}`)]
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.join('\n') + '\n\n';
|
|
62
|
+
// ---- responses ------------------------------------------------------------
|
|
63
|
+
/** A streaming event-source response you write to over time. */
|
|
64
|
+
export class SigmxStream {
|
|
65
|
+
response;
|
|
66
|
+
controller;
|
|
67
|
+
encoder = new TextEncoder();
|
|
68
|
+
open = true;
|
|
69
|
+
constructor(init = {}) {
|
|
70
|
+
const body = new ReadableStream({
|
|
71
|
+
start: (c) => {
|
|
72
|
+
this.controller = c;
|
|
73
|
+
},
|
|
74
|
+
cancel: () => {
|
|
75
|
+
this.open = false;
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
this.response = new Response(body, { ...init, headers: { ...SSE_HEADERS, ...init.headers } });
|
|
79
|
+
}
|
|
80
|
+
get closed() {
|
|
81
|
+
return !this.open;
|
|
82
|
+
}
|
|
83
|
+
send(event) {
|
|
84
|
+
if (this.open)
|
|
85
|
+
this.controller.enqueue(this.encoder.encode(formatEvent(event)));
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
patchElements(html, o) {
|
|
89
|
+
return this.send(patchElements(html, o));
|
|
90
|
+
}
|
|
91
|
+
removeElements(selector, o) {
|
|
92
|
+
return this.send(removeElements(selector, o));
|
|
93
|
+
}
|
|
94
|
+
patchSignals(signals, o) {
|
|
95
|
+
return this.send(patchSignals(signals, o));
|
|
96
|
+
}
|
|
97
|
+
removeSignals(paths, o) {
|
|
98
|
+
return this.send(removeSignals(paths, o));
|
|
99
|
+
}
|
|
100
|
+
executeScript(script, o) {
|
|
101
|
+
return this.send(executeScript(script, o));
|
|
102
|
+
}
|
|
103
|
+
close() {
|
|
104
|
+
if (this.open) {
|
|
105
|
+
this.open = false;
|
|
106
|
+
this.controller.close();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** A complete event-source response from a fixed list of events. */
|
|
111
|
+
export const sse = (...events) => new Response(events.map(formatEvent).join(''), { headers: SSE_HEADERS });
|
|
112
|
+
/** Stream events while `fn` runs; the response closes when it resolves. */
|
|
113
|
+
export const sseStream = (fn, init) => {
|
|
114
|
+
const stream = new SigmxStream(init);
|
|
115
|
+
Promise.resolve()
|
|
116
|
+
.then(() => fn(stream))
|
|
117
|
+
.catch((e) => console.error('sigmx sseStream:', e))
|
|
118
|
+
.finally(() => stream.close());
|
|
119
|
+
return stream.response;
|
|
120
|
+
};
|
|
121
|
+
/** A plain HTML response; the client morphs it by id, or honours the selector and mode headers. */
|
|
122
|
+
export const html = (body, o = {}) => new Response(body, {
|
|
123
|
+
status: o.status ?? 200,
|
|
124
|
+
headers: {
|
|
125
|
+
'content-type': 'text/html; charset=utf-8',
|
|
126
|
+
...(o.selector ? { 'sigmx-selector': o.selector } : {}),
|
|
127
|
+
...(o.mode ? { 'sigmx-mode': o.mode } : {}),
|
|
128
|
+
...(o.useViewTransition ? { 'sigmx-use-view-transition': 'true' } : {}),
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
/** A plain JSON response; the client merges it as signals. */
|
|
132
|
+
export const json = (signals, o = {}) => new Response(JSON.stringify(signals), {
|
|
133
|
+
status: o.status ?? 200,
|
|
134
|
+
headers: { 'content-type': 'application/json', ...(o.onlyIfMissing ? { 'sigmx-only-if-missing': 'true' } : {}) },
|
|
135
|
+
});
|
|
136
|
+
export class SignalsError extends Error {
|
|
137
|
+
issues;
|
|
138
|
+
status;
|
|
139
|
+
constructor(message, issues = [], status = 422) {
|
|
140
|
+
super(message);
|
|
141
|
+
this.issues = issues;
|
|
142
|
+
this.status = status;
|
|
143
|
+
this.name = 'SignalsError';
|
|
144
|
+
}
|
|
145
|
+
/** A ready-made error response for endpoints that want to bail out. */
|
|
146
|
+
response() {
|
|
147
|
+
return Response.json({ error: this.message, issues: this.issues }, { status: this.status });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export async function readSignals(request, schema) {
|
|
151
|
+
let raw;
|
|
152
|
+
const method = request.method.toUpperCase();
|
|
153
|
+
const type = request.headers.get('content-type') ?? '';
|
|
154
|
+
try {
|
|
155
|
+
if (method === 'GET' || method === 'DELETE') {
|
|
156
|
+
const q = new URL(request.url).searchParams.get(SIGNALS_KEY);
|
|
157
|
+
raw = q ? JSON.parse(q) : {};
|
|
158
|
+
}
|
|
159
|
+
else if (type.includes('application/json')) {
|
|
160
|
+
const text = await request.text();
|
|
161
|
+
raw = text ? JSON.parse(text) : {};
|
|
162
|
+
}
|
|
163
|
+
else if (type.includes('form')) {
|
|
164
|
+
raw = Object.fromEntries((await request.formData()).entries());
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
raw = {};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
throw new SignalsError(`could not parse signals: ${e.message}`, [], 400);
|
|
172
|
+
}
|
|
173
|
+
if (!schema)
|
|
174
|
+
return raw;
|
|
175
|
+
const result = await schema['~standard'].validate(raw);
|
|
176
|
+
if (result.issues)
|
|
177
|
+
throw new SignalsError(`invalid signals: ${result.issues.map((i) => i.message).join('; ')}`, result.issues);
|
|
178
|
+
return result.value;
|
|
179
|
+
}
|
|
180
|
+
/** True when the request was made by the sigmx client (has the `Sigmx-Request` header). */
|
|
181
|
+
export const isSigmxRequest = (request) => request.headers.get('sigmx-request') === 'true';
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sigmx/astro",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Astro integration and server helpers for sigmx: inject the client, stream patches from endpoints, read signals from requests.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"astro",
|
|
7
|
+
"astro-integration",
|
|
8
|
+
"sigmx",
|
|
9
|
+
"hypermedia",
|
|
10
|
+
"sse"
|
|
11
|
+
],
|
|
12
|
+
"author": "Callum Bonnyman",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/wrux/sigmx.git",
|
|
17
|
+
"directory": "sdks/astro"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/wrux/sigmx#readme",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/wrux/sigmx/issues"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"default": "./dist/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./client": {
|
|
35
|
+
"types": "./dist/client.d.ts",
|
|
36
|
+
"default": "./dist/client.js"
|
|
37
|
+
},
|
|
38
|
+
"./server": {
|
|
39
|
+
"types": "./dist/server.d.ts",
|
|
40
|
+
"default": "./dist/server.js"
|
|
41
|
+
},
|
|
42
|
+
"./package.json": "./package.json"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=20"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsc -p tsconfig.json",
|
|
52
|
+
"test": "npm run build && node --test tests/*.test.mjs",
|
|
53
|
+
"prepublishOnly": "npm test",
|
|
54
|
+
"prepack": "npm run build"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/node": "^22.20.1",
|
|
58
|
+
"astro": "^7.3.1",
|
|
59
|
+
"sigmx": "file:../..",
|
|
60
|
+
"typescript": "^5.9.0"
|
|
61
|
+
},
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"astro": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
64
|
+
"sigmx": "^0.1.0"
|
|
65
|
+
}
|
|
66
|
+
}
|