@vesk/adapter 0.2.67 → 0.2.69

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.
@@ -0,0 +1,422 @@
1
+ /**
2
+ * Transport-level response pipeline: conditional GET (ETag → 304) and
3
+ * content-encoding negotiation (brotli / gzip).
4
+ *
5
+ * Both live in one wrapper because they share the same constraint: the decision
6
+ * can only be made once the whole body is known. `res.writeHead` sends headers
7
+ * to the socket immediately, so a wrapper that compressed or tagged at
8
+ * `writeHead` time would either have to buffer every response in the server (a
9
+ * second copy of every page) or get the answer wrong for streamed bodies.
10
+ * Instead this captures `writeHead` and defers it until the body is either
11
+ * complete (`end`) or discovered to be a stream (`write`).
12
+ *
13
+ * Consequences worth knowing:
14
+ *
15
+ * - **One install covers every call site.** The prod server writes headers in
16
+ * ~15 places; wrapping `res` once means a new one cannot be forgotten. A
17
+ * static file, an SSR page, a JSON API body and a middleware short-circuit
18
+ * all pass through the same path.
19
+ * - **The first `write()` opts the response out of everything.** A stream
20
+ * (SSE, a chunked `renderPageStream`) cannot be re-encoded after the first
21
+ * bytes are out, so `write()` flushes the captured headers verbatim and every
22
+ * later `write`/`end` is a passthrough. No `Content-Encoding` is ever
23
+ * announced for a body that was not encoded.
24
+ * - **The wrapper composes with header decorators.** Installing *after* another
25
+ * `writeHead` wrapper means this layer's captured reference IS that wrapper,
26
+ * so security headers and per-request overrides are still applied — at flush
27
+ * time, exactly once.
28
+ *
29
+ * Deliberately not here: range requests, `Last-Modified`/`If-Modified-Since`
30
+ * (the ETag covers the same revalidation need and cannot disagree with itself),
31
+ * and compression of already-encoded bodies.
32
+ */
33
+ import { createHash } from 'node:crypto';
34
+ import { brotliCompress, constants as zlibConstants, createBrotliCompress, createGzip, gzip, } from 'node:zlib';
35
+ import { promisify } from 'node:util';
36
+ const brotliCompressAsync = promisify(brotliCompress);
37
+ const gzipAsync = promisify(gzip);
38
+ /**
39
+ * Pick a content coding from `Accept-Encoding`.
40
+ *
41
+ * Quality values are honoured because a client is entitled to say "I accept
42
+ * gzip but not brotli" (`br;q=0`) or "gzip only at half priority". `*` applies
43
+ * to codings the header does not name. Ties go to brotli: every browser that
44
+ * sends both lists them equally, and brotli is smaller.
45
+ */
46
+ export function negotiateEncoding(header) {
47
+ if (!header)
48
+ return null;
49
+ // `header` is `string | string[]` per Node's typings, but a duplicated
50
+ // header arrives joined by ", " in practice; both shapes fold to one list.
51
+ const value = Array.isArray(header) ? header.join(',') : header;
52
+ const q = {};
53
+ for (const part of value.split(',')) {
54
+ const seg = part.trim();
55
+ if (!seg)
56
+ continue;
57
+ const semi = seg.indexOf(';');
58
+ const token = (semi === -1 ? seg : seg.slice(0, semi)).trim().toLowerCase();
59
+ if (!token)
60
+ continue;
61
+ let weight = 1;
62
+ if (semi !== -1) {
63
+ for (const param of seg.slice(semi + 1).split(';')) {
64
+ const eq = param.indexOf('=');
65
+ if (eq === -1)
66
+ continue;
67
+ if (param.slice(0, eq).trim().toLowerCase() !== 'q')
68
+ continue;
69
+ const parsed = Number.parseFloat(param.slice(eq + 1).trim());
70
+ if (Number.isFinite(parsed))
71
+ weight = parsed;
72
+ }
73
+ }
74
+ // First occurrence wins, per RFC 9110 — a client cannot raise its own
75
+ // preference by listing a coding twice.
76
+ if (!(token in q))
77
+ q[token] = weight;
78
+ }
79
+ const star = q['*'];
80
+ const br = q['br'] !== undefined ? q['br'] : star;
81
+ const gz = q['gzip'] !== undefined ? q['gzip'] : star;
82
+ if (br !== undefined && br > 0 && (gz === undefined || gz <= 0 || br >= gz))
83
+ return 'br';
84
+ if (gz !== undefined && gz > 0)
85
+ return 'gzip';
86
+ return null;
87
+ }
88
+ /**
89
+ * Would compressing this content type actually pay?
90
+ *
91
+ * The rule is "text-like, not already compressed". Images, fonts, video and
92
+ * archives are already compressed — a second pass costs CPU and usually makes
93
+ * the body *larger*, which is why the size check in `installResponsePipeline`
94
+ * is kept as a second line of defence rather than the only one.
95
+ */
96
+ export function isCompressibleType(contentType) {
97
+ if (!contentType)
98
+ return false;
99
+ const type = (contentType.split(';')[0] || '').trim().toLowerCase();
100
+ if (!type)
101
+ return false;
102
+ if (type.startsWith('text/'))
103
+ return true;
104
+ if (type === 'image/svg+xml' || type === 'image/x-icon' || type === 'image/bmp')
105
+ return true;
106
+ if (type.endsWith('+json') || type.endsWith('+xml'))
107
+ return true;
108
+ switch (type) {
109
+ case 'application/javascript':
110
+ case 'application/x-javascript':
111
+ case 'application/ecmascript':
112
+ case 'application/json':
113
+ case 'application/manifest+json':
114
+ case 'application/ld+json':
115
+ case 'application/graphql':
116
+ case 'application/x-ndjson':
117
+ case 'application/xml':
118
+ case 'application/xhtml+xml':
119
+ case 'application/rss+xml':
120
+ case 'application/atom+xml':
121
+ return true;
122
+ default:
123
+ return false;
124
+ }
125
+ }
126
+ /**
127
+ * A validator for these exact bytes.
128
+ *
129
+ * sha1 rather than sha256 because this is a cache validator, not a security
130
+ * primitive (RFC 9110 requires "a function of the representation", and nginx
131
+ * ships md5 here), and every request hashes its body. Truncated to 20 bytes of
132
+ * base64 — 27 characters — which is far beyond collision range for a validator.
133
+ */
134
+ export function etagFor(body) {
135
+ return `"${createHash('sha1').update(body).digest('base64').slice(0, 27)}"`;
136
+ }
137
+ /**
138
+ * `If-None-Match` satisfaction, weak comparison as RFC 9110 §13.1.2 requires
139
+ * for GET. Weak comparison ignores the `W/` prefix on either side, which is what
140
+ * makes a `W/"x"` from a proxy match our strong `"x"`.
141
+ */
142
+ export function ifNoneMatchSatisfied(header, etag) {
143
+ if (!header)
144
+ return false;
145
+ const value = Array.isArray(header) ? header.join(',') : header;
146
+ const candidates = value.split(',').map(s => s.trim()).filter(Boolean);
147
+ if (candidates.includes('*'))
148
+ return true;
149
+ const strip = (s) => (s.startsWith('W/') ? s.slice(2) : s).trim();
150
+ const target = strip(etag);
151
+ for (const candidate of candidates) {
152
+ if (strip(candidate) === target)
153
+ return true;
154
+ }
155
+ return false;
156
+ }
157
+ function getHeader(headers, name) {
158
+ const wanted = name.toLowerCase();
159
+ for (const key of Object.keys(headers)) {
160
+ if (key.toLowerCase() === wanted) {
161
+ const v = headers[key];
162
+ if (v === undefined)
163
+ return undefined;
164
+ return Array.isArray(v) ? v.join(', ') : String(v);
165
+ }
166
+ }
167
+ return undefined;
168
+ }
169
+ function hasNoStore(cacheControl) {
170
+ if (!cacheControl)
171
+ return false;
172
+ return cacheControl.split(',').some(part => part.trim().toLowerCase() === 'no-store');
173
+ }
174
+ /** Add `value` to `Vary` without duplicating an entry already there. */
175
+ function withVary(current, value) {
176
+ if (!current)
177
+ return value;
178
+ const existing = current.split(',').map(s => s.trim().toLowerCase());
179
+ if (existing.includes(value.toLowerCase()))
180
+ return current;
181
+ if (existing.includes('*'))
182
+ return current;
183
+ return `${current}, ${value}`;
184
+ }
185
+ /**
186
+ * Wrap `res` so every buffered response is tagged with an ETag, revalidated
187
+ * against `If-None-Match`, and encoded — in that order, because a 304 must be
188
+ * decided on the *unencoded* bytes.
189
+ */
190
+ export function installResponsePipeline(res, req, options = {}) {
191
+ const compressEnabled = options.compress !== false;
192
+ const etagEnabled = options.etag !== false;
193
+ const threshold = options.compressThreshold ?? 1024;
194
+ const brotliQuality = options.brotliQuality ?? 5;
195
+ const gzipLevel = options.gzipLevel ?? 6;
196
+ const realWriteHead = res.writeHead.bind(res);
197
+ const realWrite = res.write.bind(res);
198
+ const realEnd = res.end.bind(res);
199
+ const realFlushHeaders = res.flushHeaders.bind(res);
200
+ let pendingStatus = 200;
201
+ let pendingHeaders = {};
202
+ let flushed = false;
203
+ let streaming = false;
204
+ let streamCompressor = null;
205
+ /** Emit the captured status+headers exactly once, merging in `extra`. */
206
+ function flush(extra) {
207
+ if (flushed)
208
+ return;
209
+ flushed = true;
210
+ const headers = {};
211
+ for (const [key, value] of Object.entries(pendingHeaders)) {
212
+ if (value !== undefined)
213
+ headers[key] = value;
214
+ }
215
+ for (const [key, value] of Object.entries(extra || {})) {
216
+ if (value === undefined)
217
+ delete headers[key];
218
+ else
219
+ headers[key] = value;
220
+ }
221
+ realWriteHead(pendingStatus, headers);
222
+ }
223
+ res.writeHead = ((statusCode, headers) => {
224
+ if (flushed || streaming)
225
+ return realWriteHead(statusCode, headers);
226
+ pendingStatus = statusCode;
227
+ pendingHeaders = { ...(headers || {}) };
228
+ return res;
229
+ });
230
+ /**
231
+ * Would this response's bytes differ if the client sent a different
232
+ * `Accept-Encoding`? If yes it must be announced in `Vary` whether or not
233
+ * an encoding was actually applied — a cache that stored the identity copy
234
+ * without seeing `Vary` would then serve it to a client that can decode br.
235
+ */
236
+ function encodingIsVariable() {
237
+ return compressEnabled
238
+ && !getHeader(pendingHeaders, 'content-encoding')
239
+ && isCompressibleType(getHeader(pendingHeaders, 'content-type'));
240
+ }
241
+ function startStreamCompression(encoding) {
242
+ const compressor = encoding === 'br'
243
+ ? createBrotliCompress({ params: { [zlibConstants.BROTLI_PARAM_QUALITY]: brotliQuality } })
244
+ : createGzip({ level: gzipLevel });
245
+ // The compressor owns the socket from here: its output is what the client
246
+ // receives, and its `end` is what actually finishes the response. This is
247
+ // what lets a *streamed* body still be encoded — buffering it to compute a
248
+ // validator would defeat the streaming in the first place.
249
+ compressor.on('data', (chunk) => { realWrite(chunk); });
250
+ compressor.on('end', () => { realEnd(); });
251
+ compressor.on('error', () => {
252
+ // A compressor failure must still produce a complete response object;
253
+ // the bytes already written are the unencoded prefix, which is why the
254
+ // socket has to be closed rather than left open.
255
+ try {
256
+ realEnd();
257
+ }
258
+ catch { /* already finished */ }
259
+ });
260
+ streamCompressor = compressor;
261
+ }
262
+ res.write = ((chunk, encoding) => {
263
+ if (!flushed && !streaming) {
264
+ streaming = true;
265
+ const vary = withVary(getHeader(pendingHeaders, 'vary'), 'Accept-Encoding');
266
+ const encodingChosen = encodingIsVariable() ? negotiateEncoding(req.headers['accept-encoding']) : null;
267
+ if (encodingChosen) {
268
+ startStreamCompression(encodingChosen);
269
+ // Content-Length cannot survive: the encoded stream's length is
270
+ // unknown until it ends, and a wrong one truncates the response.
271
+ flush({ 'Content-Encoding': encodingChosen, Vary: vary, 'Content-Length': undefined });
272
+ }
273
+ else {
274
+ flush({ Vary: encodingIsVariable() ? vary : undefined });
275
+ }
276
+ }
277
+ if (streamCompressor) {
278
+ streamCompressor.write(toBuffer(chunk, encoding) || Buffer.alloc(0));
279
+ return true;
280
+ }
281
+ return typeof encoding === 'undefined'
282
+ ? realWrite(chunk)
283
+ : realWrite(chunk, encoding);
284
+ });
285
+ res.flushHeaders = (() => {
286
+ if (!flushed) {
287
+ streaming = true;
288
+ flush();
289
+ }
290
+ realFlushHeaders.call(res);
291
+ });
292
+ function toBuffer(chunk, encoding) {
293
+ if (chunk === undefined || chunk === null)
294
+ return null;
295
+ if (Buffer.isBuffer(chunk))
296
+ return chunk;
297
+ if (typeof chunk === 'string') {
298
+ return Buffer.from(chunk, typeof encoding === 'string' ? encoding : 'utf8');
299
+ }
300
+ if (chunk instanceof Uint8Array)
301
+ return Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
302
+ return null;
303
+ }
304
+ function finish(body, endArgs) {
305
+ const method = (req.method || 'GET').toUpperCase();
306
+ // Nothing to compress, tag or revalidate. Empty bodies include 204 and
307
+ // 304 responses, which must not carry a Content-Length anyway.
308
+ if (!body || body.length === 0 || streaming || flushed) {
309
+ flush();
310
+ realEnd(...endArgs);
311
+ return;
312
+ }
313
+ const cacheable = method === 'GET' || method === 'HEAD';
314
+ const existingEtag = getHeader(pendingHeaders, 'etag');
315
+ // `no-store` means the bytes are not reusable, so a validator would only
316
+ // produce a 304 that hands the client back a body the server has already
317
+ // consumed. This is load-bearing for the single-use `/ssr-data.js` and
318
+ // `/store-init.js` payloads, which are deleted as they are read.
319
+ const canValidate = cacheable && pendingStatus === 200 && !hasNoStore(getHeader(pendingHeaders, 'cache-control'));
320
+ if (!canValidate) {
321
+ maybeCompress(body, endArgs);
322
+ return;
323
+ }
324
+ const etag = existingEtag || (etagEnabled ? etagFor(body) : undefined);
325
+ if (etag && ifNoneMatchSatisfied(req.headers['if-none-match'], etag)) {
326
+ // A 304 carries the validator and the caching directives and nothing
327
+ // else — no body, and no Content-Type/Content-Length describing one.
328
+ const headers304 = { ETag: etag };
329
+ const cc = getHeader(pendingHeaders, 'cache-control');
330
+ if (cc)
331
+ headers304['Cache-Control'] = cc;
332
+ headers304.Vary = withVary(getHeader(pendingHeaders, 'vary'), 'Accept-Encoding');
333
+ flushed = true;
334
+ realWriteHead(304, headers304);
335
+ realEnd();
336
+ return;
337
+ }
338
+ if (etag && !existingEtag) {
339
+ pendingHeaders = { ...pendingHeaders, ETag: etag };
340
+ }
341
+ maybeCompress(body, endArgs);
342
+ }
343
+ function maybeCompress(body, endArgs) {
344
+ // Below the threshold the bytes are identical whatever the client asks
345
+ // for, so no `Vary` is owed — announcing one would fragment caches for
346
+ // nothing. At or above it, the representation does vary by encoding and
347
+ // `Vary` is announced whether or not a coding was chosen.
348
+ if (compressEnabled
349
+ && body.length < threshold
350
+ || !encodingIsVariable()) {
351
+ flush();
352
+ realEnd(...endArgs);
353
+ return;
354
+ }
355
+ const encoding = negotiateEncoding(req.headers['accept-encoding']);
356
+ if (!encoding) {
357
+ flush({ Vary: withVary(getHeader(pendingHeaders, 'vary'), 'Accept-Encoding') });
358
+ realEnd(...endArgs);
359
+ return;
360
+ }
361
+ const compress = encoding === 'br' ? brotliCompressAsync : gzipAsync;
362
+ const params = encoding === 'br'
363
+ ? { params: { [zlibConstants.BROTLI_PARAM_QUALITY]: brotliQuality } }
364
+ : { level: gzipLevel };
365
+ void Promise.resolve()
366
+ .then(() => compress(body, params))
367
+ .then((encoded) => {
368
+ // A body that did not get smaller is not worth the encoding: it
369
+ // costs the client CPU on every fetch for nothing.
370
+ if (flushed || streaming) {
371
+ flush();
372
+ realEnd(...endArgs);
373
+ return;
374
+ }
375
+ if (encoded.length >= body.length) {
376
+ flush({ Vary: withVary(getHeader(pendingHeaders, 'vary'), 'Accept-Encoding') });
377
+ realEnd(...endArgs);
378
+ return;
379
+ }
380
+ flush({
381
+ 'Content-Encoding': encoding,
382
+ Vary: withVary(getHeader(pendingHeaders, 'vary'), 'Accept-Encoding'),
383
+ 'Content-Length': String(encoded.length),
384
+ });
385
+ realEnd(encoded);
386
+ })
387
+ .catch(() => {
388
+ // Compression must never be the reason a response is lost.
389
+ flush({ Vary: withVary(getHeader(pendingHeaders, 'vary'), 'Accept-Encoding') });
390
+ realEnd(...endArgs);
391
+ });
392
+ }
393
+ res.end = ((chunk, encoding, callback) => {
394
+ if (streamCompressor) {
395
+ // Ending the compressor is what ends the response — it flushes its
396
+ // own buffered tail first, so the client gets a complete stream.
397
+ if (typeof callback === 'function')
398
+ res.once('finish', callback);
399
+ const tail = toBuffer(chunk, encoding);
400
+ if (tail)
401
+ streamCompressor.end(tail);
402
+ else
403
+ streamCompressor.end();
404
+ return res;
405
+ }
406
+ if (streaming || flushed) {
407
+ return realEnd(chunk, encoding, callback);
408
+ }
409
+ // Node's end() has four shapes — (), (cb), (data), (data, enc),
410
+ // (data, cb), (data, enc, cb). Forward only the arguments that were
411
+ // actually supplied: `end(body, undefined, undefined)` is NOT the same
412
+ // as `end(body)` for a callback-taking overload.
413
+ const endArgs = chunk === undefined
414
+ ? (callback === undefined ? [] : [callback])
415
+ : encoding === undefined
416
+ ? (callback === undefined ? [chunk] : [chunk, callback])
417
+ : (callback === undefined ? [chunk, encoding] : [chunk, encoding, callback]);
418
+ const body = toBuffer(chunk, encoding);
419
+ finish(body, endArgs);
420
+ return res;
421
+ });
422
+ }
@@ -0,0 +1,77 @@
1
+ export interface ServerOnlyHit {
2
+ specifier: string;
3
+ binding: string;
4
+ importer: string;
5
+ }
6
+ export interface ServerOnlyPluginOptions {
7
+ /** One call per (specifier, binding) replaced by a throwing stub. */
8
+ onStub?: (hit: ServerOnlyHit) => void;
9
+ /** One call per TCP driver seen while bundling. Never stubbed. */
10
+ onDriver?: (hit: {
11
+ specifier: string;
12
+ importer: string;
13
+ }) => void;
14
+ /**
15
+ * One call per binding used at module-evaluation position, where a stub would
16
+ * throw while the chunk loads rather than at a call site. Reported, never
17
+ * stubbed — this is a build error.
18
+ */
19
+ onEager?: (hit: {
20
+ specifier: string;
21
+ bindings: string[];
22
+ importer: string;
23
+ }) => void;
24
+ }
25
+ /** The slice of esbuild's plugin API this uses, declared so the module needs no esbuild types. */
26
+ export interface EsbuildPluginLike {
27
+ name: string;
28
+ setup: (build: {
29
+ onResolve: (opts: {
30
+ filter: RegExp;
31
+ namespace?: string;
32
+ }, cb: (args: {
33
+ path: string;
34
+ importer: string;
35
+ }) => unknown) => void;
36
+ onLoad: (opts: {
37
+ filter: RegExp;
38
+ namespace: string;
39
+ }, cb: (args: {
40
+ path: string;
41
+ }) => {
42
+ contents: string;
43
+ loader: string;
44
+ }) => void;
45
+ }) => void;
46
+ }
47
+ /**
48
+ * The export names one file asks for from `specifier`.
49
+ *
50
+ * Read from the importer's own source rather than guessed: a stub declaring the
51
+ * wrong set of names is either an unresolvable-import build failure or, worse,
52
+ * a silently undefined binding.
53
+ */
54
+ export declare function importedNamesFor(code: string, specifier: string): string[];
55
+ /** The shared throwing-proxy factory, emitted once per stubbed package. */
56
+ export declare function stubFactorySource(specifier: string): string;
57
+ /**
58
+ * Is `name` used at module-evaluation position — i.e. NOT inside a function?
59
+ *
60
+ * This is the difference between a stub that is fine and one that is a trap.
61
+ *
62
+ * export function allRows() { return orm.getAll(); } // never runs in the
63
+ * // browser → stub is fine
64
+ * export const orm = createORM({}); // runs the moment the
65
+ * // chunk evaluates
66
+ *
67
+ * The second form cannot be rescued by any amount of stubbing: the browser
68
+ * evaluates it while loading the chunk, so the page dies before a single
69
+ * component exists. A build error is the honest answer there, and it is
70
+ * decidable from the source alone — no types, no dataflow.
71
+ *
72
+ * Non-computed member keys (`obj.orm`) and object/class property keys are not
73
+ * references, for the same reason the reactivity diagnostics skip them.
74
+ */
75
+ export declare function referencedAtTopLevel(code: string, name: string): boolean;
76
+ export declare function serverOnlyPlugin(options?: ServerOnlyPluginOptions): EsbuildPluginLike;
77
+ //# sourceMappingURL=server-only-plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-only-plugin.d.ts","sourceRoot":"","sources":["../src/server-only-plugin.ts"],"names":[],"mappings":"AA0BA,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,uBAAuB;IACtC,qEAAqE;IACrE,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,IAAI,CAAC;IACtC,kEAAkE;IAClE,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAClE;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACtF;AAED,kGAAkG;AAClG,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,CAAC,KAAK,EAAE;QACb,SAAS,EAAE,CACT,IAAI,EAAE;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,SAAS,CAAC,EAAE,MAAM,CAAA;SAAE,EAC5C,EAAE,EAAE,CAAC,IAAI,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAA;SAAE,KAAK,OAAO,KACtD,IAAI,CAAC;QACV,MAAM,EAAE,CACN,IAAI,EAAE;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,SAAS,EAAE,MAAM,CAAA;SAAE,EAC3C,EAAE,EAAE,CAAC,IAAI,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,KAAK;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,KACjE,IAAI,CAAC;KACX,KAAK,IAAI,CAAC;CACZ;AAMD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CA+C1E;AAED,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAmB3D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAoDxE;AAED,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,uBAA4B,GAAG,iBAAiB,CAoDzF"}