@czap/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.
Files changed (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +19 -0
  3. package/dist/Satellite.d.ts +42 -0
  4. package/dist/Satellite.d.ts.map +1 -0
  5. package/dist/Satellite.js +55 -0
  6. package/dist/Satellite.js.map +1 -0
  7. package/dist/client-directives/gpu.d.ts +3 -0
  8. package/dist/client-directives/gpu.d.ts.map +1 -0
  9. package/dist/client-directives/gpu.js +5 -0
  10. package/dist/client-directives/gpu.js.map +1 -0
  11. package/dist/client-directives/llm.d.ts +3 -0
  12. package/dist/client-directives/llm.d.ts.map +1 -0
  13. package/dist/client-directives/llm.js +5 -0
  14. package/dist/client-directives/llm.js.map +1 -0
  15. package/dist/client-directives/satellite.d.ts +3 -0
  16. package/dist/client-directives/satellite.d.ts.map +1 -0
  17. package/dist/client-directives/satellite.js +5 -0
  18. package/dist/client-directives/satellite.js.map +1 -0
  19. package/dist/client-directives/stream.d.ts +3 -0
  20. package/dist/client-directives/stream.d.ts.map +1 -0
  21. package/dist/client-directives/stream.js +5 -0
  22. package/dist/client-directives/stream.js.map +1 -0
  23. package/dist/client-directives/wasm.d.ts +3 -0
  24. package/dist/client-directives/wasm.d.ts.map +1 -0
  25. package/dist/client-directives/wasm.js +6 -0
  26. package/dist/client-directives/wasm.js.map +1 -0
  27. package/dist/client-directives/worker.d.ts +3 -0
  28. package/dist/client-directives/worker.d.ts.map +1 -0
  29. package/dist/client-directives/worker.js +5 -0
  30. package/dist/client-directives/worker.js.map +1 -0
  31. package/dist/detect-upgrade.d.ts +16 -0
  32. package/dist/detect-upgrade.d.ts.map +1 -0
  33. package/dist/detect-upgrade.js +105 -0
  34. package/dist/detect-upgrade.js.map +1 -0
  35. package/dist/headers.d.ts +45 -0
  36. package/dist/headers.d.ts.map +1 -0
  37. package/dist/headers.js +64 -0
  38. package/dist/headers.js.map +1 -0
  39. package/dist/index.d.ts +30 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +26 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/integration.d.ts +76 -0
  44. package/dist/integration.d.ts.map +1 -0
  45. package/dist/integration.js +240 -0
  46. package/dist/integration.js.map +1 -0
  47. package/dist/middleware.d.ts +69 -0
  48. package/dist/middleware.d.ts.map +1 -0
  49. package/dist/middleware.js +75 -0
  50. package/dist/middleware.js.map +1 -0
  51. package/dist/quantize.d.ts +50 -0
  52. package/dist/quantize.d.ts.map +1 -0
  53. package/dist/quantize.js +122 -0
  54. package/dist/quantize.js.map +1 -0
  55. package/dist/runtime/boundary.d.ts +123 -0
  56. package/dist/runtime/boundary.d.ts.map +1 -0
  57. package/dist/runtime/boundary.js +164 -0
  58. package/dist/runtime/boundary.js.map +1 -0
  59. package/dist/runtime/globals.d.ts +32 -0
  60. package/dist/runtime/globals.d.ts.map +1 -0
  61. package/dist/runtime/globals.js +45 -0
  62. package/dist/runtime/globals.js.map +1 -0
  63. package/dist/runtime/gpu.d.ts +15 -0
  64. package/dist/runtime/gpu.d.ts.map +1 -0
  65. package/dist/runtime/gpu.js +266 -0
  66. package/dist/runtime/gpu.js.map +1 -0
  67. package/dist/runtime/index.d.ts +7 -0
  68. package/dist/runtime/index.d.ts.map +1 -0
  69. package/dist/runtime/index.js +5 -0
  70. package/dist/runtime/index.js.map +1 -0
  71. package/dist/runtime/llm-receipt-tracker.d.ts +21 -0
  72. package/dist/runtime/llm-receipt-tracker.d.ts.map +1 -0
  73. package/dist/runtime/llm-receipt-tracker.js +60 -0
  74. package/dist/runtime/llm-receipt-tracker.js.map +1 -0
  75. package/dist/runtime/llm-render-pipeline.d.ts +89 -0
  76. package/dist/runtime/llm-render-pipeline.d.ts.map +1 -0
  77. package/dist/runtime/llm-render-pipeline.js +241 -0
  78. package/dist/runtime/llm-render-pipeline.js.map +1 -0
  79. package/dist/runtime/llm-session.d.ts +126 -0
  80. package/dist/runtime/llm-session.d.ts.map +1 -0
  81. package/dist/runtime/llm-session.js +385 -0
  82. package/dist/runtime/llm-session.js.map +1 -0
  83. package/dist/runtime/llm.d.ts +16 -0
  84. package/dist/runtime/llm.d.ts.map +1 -0
  85. package/dist/runtime/llm.js +273 -0
  86. package/dist/runtime/llm.js.map +1 -0
  87. package/dist/runtime/policy.d.ts +100 -0
  88. package/dist/runtime/policy.d.ts.map +1 -0
  89. package/dist/runtime/policy.js +147 -0
  90. package/dist/runtime/policy.js.map +1 -0
  91. package/dist/runtime/receipt-chain.d.ts +22 -0
  92. package/dist/runtime/receipt-chain.d.ts.map +1 -0
  93. package/dist/runtime/receipt-chain.js +80 -0
  94. package/dist/runtime/receipt-chain.js.map +1 -0
  95. package/dist/runtime/runtime-session.d.ts +34 -0
  96. package/dist/runtime/runtime-session.d.ts.map +1 -0
  97. package/dist/runtime/runtime-session.js +102 -0
  98. package/dist/runtime/runtime-session.js.map +1 -0
  99. package/dist/runtime/satellite.d.ts +13 -0
  100. package/dist/runtime/satellite.d.ts.map +1 -0
  101. package/dist/runtime/satellite.js +59 -0
  102. package/dist/runtime/satellite.js.map +1 -0
  103. package/dist/runtime/slots.d.ts +34 -0
  104. package/dist/runtime/slots.d.ts.map +1 -0
  105. package/dist/runtime/slots.js +108 -0
  106. package/dist/runtime/slots.js.map +1 -0
  107. package/dist/runtime/stream-session.d.ts +47 -0
  108. package/dist/runtime/stream-session.d.ts.map +1 -0
  109. package/dist/runtime/stream-session.js +82 -0
  110. package/dist/runtime/stream-session.js.map +1 -0
  111. package/dist/runtime/stream.d.ts +9 -0
  112. package/dist/runtime/stream.d.ts.map +1 -0
  113. package/dist/runtime/stream.js +308 -0
  114. package/dist/runtime/stream.js.map +1 -0
  115. package/dist/runtime/url-policy.d.ts +28 -0
  116. package/dist/runtime/url-policy.d.ts.map +1 -0
  117. package/dist/runtime/url-policy.js +87 -0
  118. package/dist/runtime/url-policy.js.map +1 -0
  119. package/dist/runtime/wasm.d.ts +20 -0
  120. package/dist/runtime/wasm.d.ts.map +1 -0
  121. package/dist/runtime/wasm.js +70 -0
  122. package/dist/runtime/wasm.js.map +1 -0
  123. package/dist/runtime/worker.d.ts +11 -0
  124. package/dist/runtime/worker.d.ts.map +1 -0
  125. package/dist/runtime/worker.js +249 -0
  126. package/dist/runtime/worker.js.map +1 -0
  127. package/package.json +106 -0
  128. package/src/Satellite.astro +39 -0
  129. package/src/Satellite.ts +84 -0
  130. package/src/client-directives/gpu.ts +5 -0
  131. package/src/client-directives/llm.ts +5 -0
  132. package/src/client-directives/satellite.ts +5 -0
  133. package/src/client-directives/stream.ts +5 -0
  134. package/src/client-directives/wasm.ts +6 -0
  135. package/src/client-directives/worker.ts +5 -0
  136. package/src/detect-upgrade.ts +105 -0
  137. package/src/headers.ts +84 -0
  138. package/src/index.ts +30 -0
  139. package/src/integration.ts +309 -0
  140. package/src/middleware.ts +133 -0
  141. package/src/quantize.ts +173 -0
  142. package/src/runtime/boundary.ts +263 -0
  143. package/src/runtime/globals.ts +57 -0
  144. package/src/runtime/gpu.ts +291 -0
  145. package/src/runtime/index.ts +12 -0
  146. package/src/runtime/llm-receipt-tracker.ts +88 -0
  147. package/src/runtime/llm-render-pipeline.ts +366 -0
  148. package/src/runtime/llm-session.ts +548 -0
  149. package/src/runtime/llm.ts +344 -0
  150. package/src/runtime/policy.ts +229 -0
  151. package/src/runtime/receipt-chain.ts +106 -0
  152. package/src/runtime/runtime-session.ts +139 -0
  153. package/src/runtime/satellite.ts +80 -0
  154. package/src/runtime/slots.ts +136 -0
  155. package/src/runtime/stream-session.ts +125 -0
  156. package/src/runtime/stream.ts +407 -0
  157. package/src/runtime/url-policy.ts +107 -0
  158. package/src/runtime/wasm.ts +85 -0
  159. package/src/runtime/worker.ts +307 -0
package/src/headers.ts ADDED
@@ -0,0 +1,84 @@
1
+ /**
2
+ * HTTP header helpers shipped by `@czap/astro`.
3
+ *
4
+ * Encodes the two concerns the integration cares about: asking browsers
5
+ * for the Client Hints czap uses (tier detection), and shipping the
6
+ * COOP/COEP pair required by `SharedArrayBuffer`-backed workers.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ /**
12
+ * Default `Accept-CH` / `Critical-CH` response headers czap requests
13
+ * so the browser sends viewport width, device memory, motion
14
+ * preference, and DPR on the next navigation.
15
+ */
16
+ export const CLIENT_HINTS_HEADERS: Record<string, string> = {
17
+ 'Accept-CH': 'Sec-CH-Viewport-Width, Sec-CH-Device-Memory, Sec-CH-Prefers-Reduced-Motion, Sec-CH-DPR',
18
+ 'Critical-CH': 'Sec-CH-Viewport-Width',
19
+ };
20
+
21
+ /**
22
+ * COOP/COEP header pair required for `SharedArrayBuffer` (used by
23
+ * `@czap/worker`'s SPSC ring). Applied only when the integration is
24
+ * configured with `workers: { enabled: true }`.
25
+ */
26
+ export const CROSS_ORIGIN_HEADERS: Record<string, string> = {
27
+ 'Cross-Origin-Opener-Policy': 'same-origin',
28
+ 'Cross-Origin-Embedder-Policy': 'require-corp',
29
+ };
30
+
31
+ /**
32
+ * Build the `[header, value]` entries czap wants to emit for a given
33
+ * feature toggle set. Used by dev-server middleware and edge adapters
34
+ * that prefer tuple iteration over the `Headers` API.
35
+ */
36
+ export function getCzapHeaderEntries(options: {
37
+ readonly detectEnabled: boolean;
38
+ readonly workersEnabled: boolean;
39
+ readonly acceptCH?: string;
40
+ readonly criticalCH?: string;
41
+ }): Array<readonly [string, string]> {
42
+ const entries: Array<readonly [string, string]> = [];
43
+
44
+ if (options.detectEnabled) {
45
+ const acceptCH = options.acceptCH ?? CLIENT_HINTS_HEADERS['Accept-CH'];
46
+ const criticalCH = options.criticalCH ?? CLIENT_HINTS_HEADERS['Critical-CH'];
47
+ if (acceptCH) {
48
+ entries.push(['Accept-CH', acceptCH]);
49
+ }
50
+ if (criticalCH) {
51
+ entries.push(['Critical-CH', criticalCH]);
52
+ }
53
+ }
54
+
55
+ if (options.workersEnabled) {
56
+ for (const [header, value] of Object.entries(CROSS_ORIGIN_HEADERS)) {
57
+ entries.push([header, value]);
58
+ }
59
+ }
60
+
61
+ return entries;
62
+ }
63
+
64
+ /**
65
+ * Apply the czap header set to an existing {@link Headers} bag and
66
+ * return it (for chaining). Convenience wrapper over
67
+ * {@link getCzapHeaderEntries} for middleware that already has a
68
+ * `Headers` object in hand.
69
+ */
70
+ export function applyCzapHeaders(
71
+ headers: Headers,
72
+ options: {
73
+ readonly detectEnabled: boolean;
74
+ readonly workersEnabled: boolean;
75
+ readonly acceptCH?: string;
76
+ readonly criticalCH?: string;
77
+ },
78
+ ): Headers {
79
+ for (const [header, value] of getCzapHeaderEntries(options)) {
80
+ headers.set(header, value);
81
+ }
82
+
83
+ return headers;
84
+ }
package/src/index.ts ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * `@czap/astro` — **LiteShip** on Astro 6: constraint-shaped adaptive
3
+ * projection hosted as islands and directives.
4
+ *
5
+ * Provides the Astro `Integration` that registers `@czap/vite`,
6
+ * injects client tier detection, **rigs** the `client:satellite` directive,
7
+ * and exposes `Satellite` for shells with server-resolved bearings.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * // astro.config.mjs
12
+ * import { defineConfig } from 'astro/config';
13
+ * import { integration as czap } from '@czap/astro';
14
+ *
15
+ * const config = defineConfig({
16
+ * integrations: [czap({ themes: ['./themes/default.ts'] })],
17
+ * });
18
+ * ```
19
+ *
20
+ * @module
21
+ */
22
+
23
+ export type { IntegrationConfig } from './integration.js';
24
+ export { integration } from './integration.js';
25
+ export type { ServerIslandContext, QuantizeProps } from './quantize.js';
26
+ export { resolveInitialState } from './quantize.js';
27
+ export { satelliteAttrs, resolveInitialStateFallback } from './Satellite.js';
28
+ export type { SatelliteProps } from './Satellite.js';
29
+ export { czapMiddleware } from './middleware.js';
30
+ export type { CzapLocals, CzapMiddlewareConfig } from './middleware.js';
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Astro 6 `AstroIntegration` for czap.
3
+ *
4
+ * Registers the `@czap/vite` plugin, injects the detect/boot scripts,
5
+ * registers every client directive (`client:satellite`,
6
+ * `client:stream`, `client:llm`, `client:worker`, `client:gpu`,
7
+ * `client:wasm`) that the host opts into, and turns on Astro's
8
+ * `serverIslands` experimental flag when requested.
9
+ *
10
+ * @module
11
+ */
12
+
13
+ import type { AstroIntegration } from 'astro';
14
+ import { plugin } from '@czap/vite';
15
+ import type { PluginConfig } from '@czap/vite';
16
+ import { DETECT_UPGRADE_SCRIPT } from './detect-upgrade.js';
17
+ import { getCzapHeaderEntries } from './headers.js';
18
+ import type { RuntimeEndpointPolicy } from '@czap/web';
19
+ import {
20
+ normalizeRuntimeSecurityPolicy,
21
+ type RuntimeHtmlPolicy,
22
+ type RuntimeSecurityPolicy,
23
+ } from './runtime/policy.js';
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Types
27
+ // ---------------------------------------------------------------------------
28
+
29
+ /**
30
+ * Options passed to {@link integration} from `astro.config.mjs`. Every
31
+ * field is optional; omitted features fall back to conservative
32
+ * defaults (detect enabled, stream/llm/gpu enabled, workers/wasm/server
33
+ * islands opt-in).
34
+ */
35
+ export interface IntegrationConfig {
36
+ /** Overrides passed through to `@czap/vite`'s plugin. */
37
+ readonly vite?: PluginConfig;
38
+ /** Enable the inline detect script (default `true`). */
39
+ readonly detect?: boolean;
40
+ /** Turn on Astro's experimental server-islands flag (default `false`). */
41
+ readonly serverIslands?: boolean;
42
+ /** WASM runtime configuration. */
43
+ readonly wasm?: { readonly enabled?: boolean; readonly path?: string };
44
+ /** GPU runtime configuration. */
45
+ readonly gpu?: { readonly enabled?: boolean; readonly preferWebGPU?: boolean };
46
+ /** Off-thread worker runtime configuration. */
47
+ readonly workers?: { readonly enabled?: boolean };
48
+ /** SSE streaming runtime configuration. */
49
+ readonly stream?: { readonly enabled?: boolean };
50
+ /** LLM streaming runtime configuration. */
51
+ readonly llm?: { readonly enabled?: boolean };
52
+ /** Security policies applied to runtime fetch/HTML boundaries. */
53
+ readonly security?: {
54
+ readonly endpointPolicy?: RuntimeEndpointPolicy;
55
+ readonly htmlPolicy?: RuntimeHtmlPolicy;
56
+ };
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Detect Script
61
+ // ---------------------------------------------------------------------------
62
+
63
+ /**
64
+ * Inline script that runs device detection on page load and stores
65
+ * the result as CSS custom properties on the <html> element and
66
+ * as a global for runtime access.
67
+ */
68
+ const DETECT_INLINE_SCRIPT = `
69
+ (function(){
70
+ function writeDetectState(next) {
71
+ var safe = Object.freeze(Object.assign({}, next));
72
+ try {
73
+ Object.defineProperty(window, '__CZAP_DETECT__', {
74
+ value: safe,
75
+ configurable: true,
76
+ enumerable: false,
77
+ writable: false
78
+ });
79
+ } catch (_) {
80
+ try {
81
+ window.__CZAP_DETECT__ = safe;
82
+ } catch (_) {}
83
+ }
84
+ }
85
+
86
+ try {
87
+ var h = document.documentElement;
88
+ var w = window.innerWidth || 0;
89
+ var cores = navigator.hardwareConcurrency || 2;
90
+ var mem = navigator.deviceMemory || 4;
91
+ var touch = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
92
+ var motion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
93
+ var dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
94
+ var dpr = window.devicePixelRatio || 1;
95
+
96
+ h.style.setProperty('--czap-vw', w + 'px');
97
+ h.style.setProperty('--czap-cores', String(cores));
98
+ h.style.setProperty('--czap-dpr', String(dpr));
99
+ h.setAttribute('data-czap-touch', String(touch));
100
+ h.setAttribute('data-czap-motion', motion ? 'reduce' : 'no-preference');
101
+ h.setAttribute('data-czap-scheme', dark ? 'dark' : 'light');
102
+
103
+ // Provisional tier -- conservative, no GPU probe available inline.
104
+ // Full detect package overrides on hydration via data-czap-tier attribute.
105
+ var tier = 'reactive';
106
+ if (motion) tier = 'static';
107
+ else if (cores <= 2 || mem <= 2) tier = 'styled';
108
+ h.setAttribute('data-czap-tier', tier);
109
+ h.setAttribute('data-czap-tier-provisional', 'true');
110
+
111
+ writeDetectState({
112
+ tier: tier,
113
+ provisional: true
114
+ });
115
+ } catch(e) {}
116
+ })();
117
+ `.trim();
118
+
119
+ function serializeInlineRuntimePolicy(policy: RuntimeSecurityPolicy): string {
120
+ return JSON.stringify(policy).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/&/g, '\\u0026');
121
+ }
122
+
123
+ function runtimeBootstrapScript(policy: RuntimeSecurityPolicy): string {
124
+ return `
125
+ import { bootstrapSlots, configureRuntimePolicy, installSwapReinit } from '@czap/astro/runtime';
126
+
127
+ configureRuntimePolicy(${serializeInlineRuntimePolicy(policy)});
128
+ bootstrapSlots();
129
+ installSwapReinit();
130
+ `.trim();
131
+ }
132
+
133
+ const WASM_RUNTIME_SCRIPT = `
134
+ import { wasmUrl } from 'virtual:czap/wasm-url';
135
+ import { configureWasmRuntime } from '@czap/astro/runtime';
136
+
137
+ configureWasmRuntime(wasmUrl);
138
+ `.trim();
139
+
140
+ /**
141
+ * Build an `updateConfig` payload that toggles a single experimental flag
142
+ * not yet present in Astro's declared `experimental` shape.
143
+ *
144
+ * The `experimental` field on `AstroConfig` is strictly keyed, so adding
145
+ * an unknown flag requires a widening bridge. Containing that bridge
146
+ * here keeps the cast off individual call sites.
147
+ */
148
+ function withExperimentalFlag(flag: string, value: boolean): { experimental: Record<string, unknown> } {
149
+ return { experimental: { [flag]: value } };
150
+ }
151
+
152
+ // ---------------------------------------------------------------------------
153
+ // Integration
154
+ // ---------------------------------------------------------------------------
155
+
156
+ /**
157
+ * Build the czap `AstroIntegration`.
158
+ *
159
+ * Plug the returned object into `astro.config.mjs`'s `integrations`
160
+ * array. The integration wires Astro's `astro:config:setup`,
161
+ * `astro:config:done`, `astro:server:setup`, and `astro:build:done`
162
+ * hooks.
163
+ *
164
+ * @example
165
+ * ```ts
166
+ * // astro.config.mjs
167
+ * import { integration as czap } from '@czap/astro';
168
+ *
169
+ * const config = defineConfig({
170
+ * integrations: [czap({ detect: true, workers: { enabled: true } })],
171
+ * });
172
+ * ```
173
+ */
174
+ export function integration(config?: IntegrationConfig): AstroIntegration {
175
+ const detectEnabled = config?.detect !== false;
176
+ const serverIslandsEnabled = config?.serverIslands === true;
177
+ const workersEnabled = config?.workers?.enabled === true;
178
+ const gpuEnabled = config?.gpu?.enabled !== false;
179
+ const streamEnabled = config?.stream?.enabled !== false;
180
+ const llmEnabled = config?.llm?.enabled !== false;
181
+ const wasmEnabled = config?.wasm?.enabled === true;
182
+ const runtimePolicy = normalizeRuntimeSecurityPolicy({
183
+ endpointPolicy: config?.security?.endpointPolicy,
184
+ htmlPolicy: config?.security?.htmlPolicy,
185
+ });
186
+
187
+ return {
188
+ name: '@czap/astro',
189
+
190
+ hooks: {
191
+ 'astro:config:setup': ({ updateConfig, addClientDirective, injectScript, logger }) => {
192
+ type AstroViteConfig = Parameters<typeof updateConfig>[0]['vite'];
193
+ logger.info('Setting up @czap integration');
194
+
195
+ // Astro may carry a different Vite type graph than @czap/vite. The plugin
196
+ // runtime contract is still compatible, so the host integration owns the
197
+ // version bridge here instead of leaking duplicate plugin shapes downstream.
198
+ const astroViteConfig = {
199
+ plugins: [
200
+ plugin({
201
+ ...(config?.vite ?? {}),
202
+ ...(wasmEnabled ? { wasm: { enabled: true, path: config?.wasm?.path } } : {}),
203
+ }),
204
+ ],
205
+ } as AstroViteConfig;
206
+
207
+ updateConfig({
208
+ vite: astroViteConfig,
209
+ });
210
+
211
+ // Register client directives
212
+ addClientDirective({
213
+ name: 'satellite',
214
+ entrypoint: '@czap/astro/client-directives/satellite',
215
+ });
216
+ logger.info('Registered satellite client directive');
217
+
218
+ if (streamEnabled) {
219
+ addClientDirective({
220
+ name: 'stream',
221
+ entrypoint: '@czap/astro/client-directives/stream',
222
+ });
223
+ logger.info('Registered stream client directive');
224
+ }
225
+
226
+ if (llmEnabled) {
227
+ addClientDirective({
228
+ name: 'llm',
229
+ entrypoint: '@czap/astro/client-directives/llm',
230
+ });
231
+ logger.info('Registered llm client directive');
232
+ }
233
+
234
+ if (workersEnabled) {
235
+ addClientDirective({
236
+ name: 'worker',
237
+ entrypoint: '@czap/astro/client-directives/worker',
238
+ });
239
+ logger.info('Registered worker client directive');
240
+ }
241
+
242
+ if (gpuEnabled) {
243
+ addClientDirective({
244
+ name: 'gpu',
245
+ entrypoint: '@czap/astro/client-directives/gpu',
246
+ });
247
+ logger.info('Registered gpu client directive');
248
+ }
249
+
250
+ if (wasmEnabled) {
251
+ addClientDirective({
252
+ name: 'wasm',
253
+ entrypoint: '@czap/astro/client-directives/wasm',
254
+ });
255
+ logger.info('Registered wasm client directive');
256
+ }
257
+
258
+ // Inject detect script for client-side capability detection
259
+ if (detectEnabled) {
260
+ injectScript('head-inline', DETECT_INLINE_SCRIPT);
261
+ logger.info('Injected detect script');
262
+
263
+ // Inject GPU probe upgrade (deferred, non-blocking)
264
+ if (gpuEnabled) {
265
+ injectScript('page', DETECT_UPGRADE_SCRIPT);
266
+ logger.info('Injected GPU probe upgrade');
267
+ }
268
+ }
269
+
270
+ injectScript('page', runtimeBootstrapScript(runtimePolicy));
271
+
272
+ if (wasmEnabled) {
273
+ injectScript('page', WASM_RUNTIME_SCRIPT);
274
+ logger.info('Injected wasm runtime bootstrap');
275
+ }
276
+
277
+ // Configure server islands if enabled.
278
+ // `serverIslands` is not in Astro 6's declared experimental keys yet;
279
+ // enablement is delegated via a named bridge so only the one call site
280
+ // opts out of the declared-config shape.
281
+ if (serverIslandsEnabled) {
282
+ updateConfig(withExperimentalFlag('serverIslands', true));
283
+ logger.info('Enabled server islands');
284
+ }
285
+ },
286
+
287
+ 'astro:config:done': ({ config: astroConfig, logger }) => {
288
+ logger.info(`@czap configured for ${astroConfig.output} output`);
289
+ },
290
+
291
+ 'astro:server:setup': ({ server, logger }) => {
292
+ logger.info('@czap dev server middleware active');
293
+
294
+ if (detectEnabled || workersEnabled) {
295
+ server.middlewares.use((_req: unknown, res: { setHeader(k: string, v: string): void }, next: () => void) => {
296
+ for (const [header, value] of getCzapHeaderEntries({ detectEnabled, workersEnabled })) {
297
+ res.setHeader(header, value);
298
+ }
299
+ next();
300
+ });
301
+ }
302
+ },
303
+
304
+ 'astro:build:done': ({ logger }) => {
305
+ logger.info('@czap build integration complete');
306
+ },
307
+ },
308
+ };
309
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Edge middleware -- Client Hints parsing, tier detection, response headers.
3
+ *
4
+ * Framework-agnostic handler compatible with Astro middleware,
5
+ * Cloudflare Workers, and Express/Vite dev server.
6
+ *
7
+ * @module
8
+ */
9
+
10
+ import { ClientHints, createEdgeHostAdapter, EdgeTier } from '@czap/edge';
11
+ import type { CompiledOutputs, EdgeHostAdapterConfig, ThemeCompileResult } from '@czap/edge';
12
+ import type { ExtendedDeviceCapabilities } from '@czap/detect';
13
+ import { applyCzapHeaders } from './headers.js';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Types
17
+ // ---------------------------------------------------------------------------
18
+
19
+ /**
20
+ * Shape of `context.locals.czap` injected by {@link czapMiddleware}.
21
+ * Astro components (and downstream middleware) read this to drive
22
+ * adaptive rendering decisions.
23
+ */
24
+ export interface CzapLocals {
25
+ /** Resolved tiers (capability, motion, design). */
26
+ readonly tier: {
27
+ readonly cap: string;
28
+ readonly motion: string;
29
+ readonly design: string;
30
+ };
31
+ /** Parsed device capabilities. */
32
+ readonly capabilities: ExtendedDeviceCapabilities;
33
+ /** Edge-host resolution result, present when an edge adapter is configured. */
34
+ readonly edge?: {
35
+ readonly theme?: ThemeCompileResult;
36
+ readonly compiledOutputs?: CompiledOutputs;
37
+ readonly htmlAttributes: string;
38
+ readonly cacheStatus: 'disabled' | 'hit' | 'miss';
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Options accepted by {@link czapMiddleware}.
44
+ *
45
+ * Omit `edge` to run in pure Client-Hints mode. Pass `edge` when you
46
+ * have an `@czap/edge` host adapter (KV cache, theme compilation).
47
+ */
48
+ export interface CzapMiddlewareConfig {
49
+ /** Edge host adapter configuration (KV cache, theme compilation). */
50
+ readonly edge?: EdgeHostAdapterConfig;
51
+ /** Whether to include the Client Hints request headers (default `true`). */
52
+ readonly detect?: boolean;
53
+ /** Whether to emit COOP/COEP headers for worker features. */
54
+ readonly workers?: { readonly enabled?: boolean };
55
+ }
56
+
57
+ interface MiddlewareContext {
58
+ readonly request: Request;
59
+ locals: Record<string, unknown>;
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Middleware
64
+ // ---------------------------------------------------------------------------
65
+
66
+ /**
67
+ * Create the czap edge middleware.
68
+ *
69
+ * Parses Client Hints from request headers, computes tier detection,
70
+ * injects results into `context.locals.czap`, and sets Client Hints
71
+ * response headers (`Accept-CH`, `Critical-CH`).
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * // Astro middleware (src/middleware.ts)
76
+ * import { czapMiddleware } from '@czap/astro';
77
+ * export const onRequest = czapMiddleware();
78
+ * ```
79
+ */
80
+ export function czapMiddleware(
81
+ config?: CzapMiddlewareConfig,
82
+ ): (context: MiddlewareContext, next: () => Promise<Response>) => Promise<Response> {
83
+ const edgeConfig = config?.edge;
84
+ let edgeAdapter: ReturnType<typeof createEdgeHostAdapter> | null = null;
85
+ if (edgeConfig) {
86
+ edgeAdapter = createEdgeHostAdapter(edgeConfig);
87
+ }
88
+ const detectEnabled = config?.detect !== false;
89
+ const workersEnabled = config?.workers?.enabled === true;
90
+
91
+ return async (context: MiddlewareContext, next: () => Promise<Response>): Promise<Response> => {
92
+ const edgeResolution = edgeAdapter ? await edgeAdapter.resolve(context.request.headers) : null;
93
+ const capabilities = edgeResolution?.capabilities ?? ClientHints.parseClientHints(context.request.headers);
94
+ const tier = edgeResolution?.tier ?? EdgeTier.detectTier(context.request.headers);
95
+
96
+ // Inject into locals for component access
97
+ context.locals.czap = {
98
+ tier: {
99
+ cap: tier.capLevel,
100
+ motion: tier.motionTier,
101
+ design: tier.designTier,
102
+ },
103
+ capabilities,
104
+ ...(edgeResolution
105
+ ? {
106
+ edge: {
107
+ theme: edgeResolution.theme,
108
+ compiledOutputs: edgeResolution.compiledOutputs,
109
+ htmlAttributes: edgeResolution.htmlAttributes,
110
+ cacheStatus: edgeResolution.cacheStatus,
111
+ },
112
+ }
113
+ : {}),
114
+ } satisfies CzapLocals;
115
+
116
+ // Continue to the route handler
117
+ const response = await next();
118
+
119
+ // Add Client Hints request headers to the response
120
+ const headers = applyCzapHeaders(new Headers(response.headers), {
121
+ detectEnabled,
122
+ workersEnabled,
123
+ acceptCH: edgeResolution?.responseHeaders.acceptCH ?? ClientHints.acceptCHHeader(),
124
+ criticalCH: edgeResolution?.responseHeaders.criticalCH ?? ClientHints.criticalCHHeader(),
125
+ });
126
+
127
+ return new Response(response.body, {
128
+ status: response.status,
129
+ statusText: response.statusText,
130
+ headers,
131
+ });
132
+ };
133
+ }