@reckona/mreact-router 0.0.65 → 0.0.67

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 (59) hide show
  1. package/package.json +13 -12
  2. package/src/actions.ts +1130 -0
  3. package/src/adapters/aws-lambda.ts +993 -0
  4. package/src/adapters/cloudflare.ts +1286 -0
  5. package/src/adapters/devtools.ts +5 -0
  6. package/src/adapters/edge.ts +70 -0
  7. package/src/adapters/node.ts +126 -0
  8. package/src/adapters/static.ts +61 -0
  9. package/src/app-router-globals.ts +19 -0
  10. package/src/assets.ts +113 -0
  11. package/src/build.ts +2948 -0
  12. package/src/bundle-pipeline.ts +496 -0
  13. package/src/cache-config.ts +35 -0
  14. package/src/cache-stats.ts +54 -0
  15. package/src/cache.ts +418 -0
  16. package/src/cli-options.ts +296 -0
  17. package/src/cli.ts +94 -0
  18. package/src/client.ts +3398 -0
  19. package/src/config.ts +146 -0
  20. package/src/cookies.ts +113 -0
  21. package/src/csp.ts +103 -0
  22. package/src/csrf.ts +132 -0
  23. package/src/deferred.ts +52 -0
  24. package/src/dev-server.ts +262 -0
  25. package/src/file-conventions.ts +88 -0
  26. package/src/http.ts +128 -0
  27. package/src/i18n.ts +98 -0
  28. package/src/import-policy.ts +261 -0
  29. package/src/index.ts +221 -0
  30. package/src/link.ts +47 -0
  31. package/src/logger.ts +149 -0
  32. package/src/module-runner.ts +554 -0
  33. package/src/multipart.ts +577 -0
  34. package/src/native-escape.ts +53 -0
  35. package/src/native-route-matcher.ts +173 -0
  36. package/src/navigation-state.ts +98 -0
  37. package/src/navigation.ts +215 -0
  38. package/src/prerender-store.ts +233 -0
  39. package/src/render.ts +5187 -0
  40. package/src/route-path.ts +5 -0
  41. package/src/route-shells.ts +47 -0
  42. package/src/route-source.ts +224 -0
  43. package/src/route-styles.ts +91 -0
  44. package/src/routes.ts +362 -0
  45. package/src/runtime-cache.ts +8 -0
  46. package/src/runtime-state.ts +37 -0
  47. package/src/security-headers.ts +134 -0
  48. package/src/serve.ts +1265 -0
  49. package/src/session.ts +238 -0
  50. package/src/source-jsx.ts +30 -0
  51. package/src/source-modules.ts +69 -0
  52. package/src/stream-list.ts +45 -0
  53. package/src/trace.ts +114 -0
  54. package/src/types.ts +155 -0
  55. package/src/upgrade.ts +8 -0
  56. package/src/vite-config.ts +63 -0
  57. package/src/vite-plugin-cache-key.ts +53 -0
  58. package/src/vite.ts +690 -0
  59. package/src/workspace-packages.ts +67 -0
@@ -0,0 +1,577 @@
1
+ export interface MultipartStreamParseOptions {
2
+ fields?: Readonly<Record<string, MultipartStreamFieldOptions>>;
3
+ maxBytes?: number;
4
+ }
5
+
6
+ export interface MultipartStreamFieldOptions {
7
+ type?: "stream" | "text";
8
+ maxBytes?: number;
9
+ }
10
+
11
+ export interface MultipartStreamPart {
12
+ name: string;
13
+ filename?: string;
14
+ contentType?: string;
15
+ headers: Headers;
16
+ body: ReadableStream<Uint8Array<ArrayBufferLike>>;
17
+ text(): Promise<string>;
18
+ arrayBuffer(): Promise<ArrayBuffer>;
19
+ fixedLengthStream(length?: number | bigint): MultipartFixedLengthStream;
20
+ }
21
+
22
+ export interface MultipartFixedLengthStream {
23
+ readable: ReadableStream<Uint8Array<ArrayBufferLike>>;
24
+ done: Promise<void>;
25
+ }
26
+
27
+ interface PartWriter {
28
+ name: string;
29
+ maxBytes: number | undefined;
30
+ bytes: number;
31
+ writer: WritableStreamDefaultWriter<Uint8Array<ArrayBufferLike>>;
32
+ }
33
+
34
+ const crlf = "\r\n";
35
+ const headerSeparator = "\r\n\r\n";
36
+ const maxHeaderBytes = 64 * 1024;
37
+
38
+ export function parseMultipartStream(
39
+ request: Request,
40
+ options: MultipartStreamParseOptions = {},
41
+ ): AsyncIterable<MultipartStreamPart> {
42
+ return {
43
+ [Symbol.asyncIterator]() {
44
+ const queue = new AsyncQueue<MultipartStreamPart>();
45
+ void parseMultipartRequest(request, options, queue);
46
+ return queue;
47
+ },
48
+ };
49
+ }
50
+
51
+ async function parseMultipartRequest(
52
+ request: Request,
53
+ options: MultipartStreamParseOptions,
54
+ queue: AsyncQueue<MultipartStreamPart>,
55
+ ): Promise<void> {
56
+ const boundary = parseMultipartBoundary(request.headers.get("content-type"));
57
+
58
+ if (boundary === undefined) {
59
+ queue.fail(new Error("Multipart request content-type must include a boundary."));
60
+ return;
61
+ }
62
+
63
+ if (request.body === null) {
64
+ queue.fail(new Error("Multipart request body is not readable."));
65
+ return;
66
+ }
67
+
68
+ const reader = request.body.getReader();
69
+ const delimiter = encodeAscii(`${crlf}--${boundary}`);
70
+ const firstBoundary = encodeAscii(`--${boundary}`);
71
+ const closeSuffix = encodeAscii("--");
72
+ const lineSuffix = encodeAscii(crlf);
73
+ const keepTailBytes = delimiter.length + 4;
74
+ let buffer: Uint8Array<ArrayBufferLike> = new Uint8Array();
75
+ let totalBytes = 0;
76
+ let started = false;
77
+ let currentPart: PartWriter | undefined;
78
+
79
+ queue.setCancel(() => {
80
+ void currentPart?.writer.abort(new Error("Multipart stream iteration was cancelled."));
81
+ void reader.cancel();
82
+ });
83
+
84
+ try {
85
+ while (true) {
86
+ const next = await reader.read();
87
+
88
+ if (!next.done) {
89
+ totalBytes += next.value.byteLength;
90
+ enforceByteLimit("multipart request", totalBytes, options.maxBytes);
91
+ buffer = concatBytes(buffer, next.value);
92
+ }
93
+
94
+ while (true) {
95
+ if (!started) {
96
+ const firstBoundaryIndex = indexOfBytes(buffer, firstBoundary);
97
+
98
+ if (firstBoundaryIndex < 0) {
99
+ if (next.done) {
100
+ throw new Error("Multipart request body does not contain the opening boundary.");
101
+ }
102
+
103
+ trimBufferToTail(0);
104
+ break;
105
+ }
106
+
107
+ buffer = buffer.slice(firstBoundaryIndex + firstBoundary.length);
108
+
109
+ if (startsWithBytes(buffer, closeSuffix)) {
110
+ queue.close();
111
+ return;
112
+ }
113
+
114
+ if (!startsWithBytes(buffer, lineSuffix)) {
115
+ if (next.done) {
116
+ throw new Error("Malformed multipart opening boundary.");
117
+ }
118
+ break;
119
+ }
120
+
121
+ buffer = buffer.slice(lineSuffix.length);
122
+ started = true;
123
+ }
124
+
125
+ if (currentPart === undefined) {
126
+ const headerEnd = indexOfAscii(buffer, headerSeparator);
127
+
128
+ if (headerEnd < 0) {
129
+ if (next.done) {
130
+ throw new Error("Multipart part ended before headers completed.");
131
+ }
132
+
133
+ if (buffer.length > maxHeaderBytes) {
134
+ throw new Error(`Multipart part headers exceeded ${maxHeaderBytes} bytes.`);
135
+ }
136
+ break;
137
+ }
138
+
139
+ currentPart = createPartWriter(
140
+ decodeAscii(buffer.slice(0, headerEnd)),
141
+ options,
142
+ queue,
143
+ );
144
+ buffer = buffer.slice(headerEnd + headerSeparator.length);
145
+ }
146
+
147
+ const delimiterIndex = indexOfBytes(buffer, delimiter);
148
+
149
+ if (delimiterIndex < 0) {
150
+ if (next.done) {
151
+ throw new Error(`Multipart part "${currentPart.name}" ended before the closing boundary.`);
152
+ }
153
+
154
+ const writableLength = Math.max(0, buffer.length - keepTailBytes);
155
+
156
+ if (writableLength === 0) {
157
+ break;
158
+ }
159
+
160
+ await writePartChunk(currentPart, buffer.slice(0, writableLength));
161
+ buffer = buffer.slice(writableLength);
162
+ continue;
163
+ }
164
+
165
+ await writePartChunk(currentPart, buffer.slice(0, delimiterIndex));
166
+ await currentPart.writer.close();
167
+ buffer = buffer.slice(delimiterIndex + delimiter.length);
168
+ currentPart = undefined;
169
+
170
+ if (startsWithBytes(buffer, closeSuffix)) {
171
+ queue.close();
172
+ return;
173
+ }
174
+
175
+ if (startsWithBytes(buffer, lineSuffix)) {
176
+ buffer = buffer.slice(lineSuffix.length);
177
+ continue;
178
+ }
179
+
180
+ if (next.done) {
181
+ throw new Error("Malformed multipart boundary delimiter.");
182
+ }
183
+
184
+ break;
185
+ }
186
+
187
+ if (next.done) {
188
+ break;
189
+ }
190
+ }
191
+
192
+ queue.close();
193
+ } catch (error) {
194
+ await currentPart?.writer.abort(error);
195
+ queue.fail(error);
196
+ } finally {
197
+ queue.setCancel(undefined);
198
+ reader.releaseLock();
199
+ }
200
+
201
+ function trimBufferToTail(prefixLength: number): void {
202
+ if (buffer.length <= keepTailBytes + prefixLength) {
203
+ return;
204
+ }
205
+
206
+ buffer = buffer.slice(buffer.length - keepTailBytes - prefixLength);
207
+ }
208
+ }
209
+
210
+ function createPartWriter(
211
+ rawHeaders: string,
212
+ options: MultipartStreamParseOptions,
213
+ queue: AsyncQueue<MultipartStreamPart>,
214
+ ): PartWriter {
215
+ const headers = parsePartHeaders(rawHeaders);
216
+ const disposition = parseContentDisposition(headers.get("content-disposition"));
217
+
218
+ if (disposition.name === undefined) {
219
+ throw new Error("Multipart part is missing a form-data name.");
220
+ }
221
+
222
+ const stream = new TransformStream<Uint8Array, Uint8Array>();
223
+ const part = createPart({
224
+ name: disposition.name,
225
+ filename: disposition.filename,
226
+ headers,
227
+ body: stream.readable,
228
+ });
229
+
230
+ queue.push(part);
231
+
232
+ return {
233
+ name: disposition.name,
234
+ maxBytes: options.fields?.[disposition.name]?.maxBytes,
235
+ bytes: 0,
236
+ writer: stream.writable.getWriter(),
237
+ };
238
+ }
239
+
240
+ function createPart(options: {
241
+ name: string;
242
+ filename: string | undefined;
243
+ headers: Headers;
244
+ body: ReadableStream<Uint8Array>;
245
+ }): MultipartStreamPart {
246
+ const part: MultipartStreamPart = {
247
+ name: options.name,
248
+ headers: options.headers,
249
+ body: options.body,
250
+ text() {
251
+ return new Response(options.body).text();
252
+ },
253
+ arrayBuffer() {
254
+ return new Response(options.body).arrayBuffer();
255
+ },
256
+ fixedLengthStream(length) {
257
+ const expectedLength = length ?? contentLengthFromHeaders(options.headers);
258
+ const stream = createFixedLengthStream(expectedLength);
259
+
260
+ return {
261
+ readable: stream.readable,
262
+ done: options.body.pipeTo(stream.writable),
263
+ };
264
+ },
265
+ };
266
+
267
+ if (options.filename !== undefined) {
268
+ part.filename = options.filename;
269
+ }
270
+
271
+ const contentType = options.headers.get("content-type");
272
+
273
+ if (contentType !== null) {
274
+ part.contentType = contentType;
275
+ }
276
+
277
+ return part;
278
+ }
279
+
280
+ function contentLengthFromHeaders(headers: Headers): number | bigint {
281
+ const raw = headers.get("content-length");
282
+
283
+ if (raw === null || raw.trim() === "") {
284
+ throw new Error(
285
+ "Multipart part fixedLengthStream() requires a byte length. Pass an explicit length or include a Content-Length part header.",
286
+ );
287
+ }
288
+
289
+ const parsed = Number(raw);
290
+
291
+ if (!Number.isSafeInteger(parsed) || parsed < 0) {
292
+ throw new Error(`Multipart part Content-Length must be a non-negative safe integer. Received ${JSON.stringify(raw)}.`);
293
+ }
294
+
295
+ return parsed;
296
+ }
297
+
298
+ function createFixedLengthStream(length: number | bigint): TransformStream<Uint8Array<ArrayBufferLike>, Uint8Array<ArrayBufferLike>> {
299
+ const FixedLengthStream = (globalThis as {
300
+ FixedLengthStream?: new (
301
+ length: number | bigint,
302
+ ) => TransformStream<Uint8Array<ArrayBufferLike>, Uint8Array<ArrayBufferLike>>;
303
+ }).FixedLengthStream;
304
+
305
+ if (FixedLengthStream === undefined) {
306
+ throw new Error(
307
+ "FixedLengthStream is not available in this runtime. Use arrayBuffer() or pass the part body to a destination that accepts unknown-length streams.",
308
+ );
309
+ }
310
+
311
+ return new FixedLengthStream(length);
312
+ }
313
+
314
+ async function writePartChunk(
315
+ part: PartWriter,
316
+ chunk: Uint8Array<ArrayBufferLike>,
317
+ ): Promise<void> {
318
+ if (chunk.byteLength === 0) {
319
+ return;
320
+ }
321
+
322
+ part.bytes += chunk.byteLength;
323
+ enforceByteLimit(`multipart field "${part.name}"`, part.bytes, part.maxBytes);
324
+ await part.writer.write(chunk);
325
+ }
326
+
327
+ function enforceByteLimit(label: string, bytes: number, maxBytes: number | undefined): void {
328
+ if (maxBytes !== undefined && bytes > maxBytes) {
329
+ throw new Error(`${label} exceeded ${maxBytes} bytes.`);
330
+ }
331
+ }
332
+
333
+ function parseMultipartBoundary(contentType: string | null): string | undefined {
334
+ if (contentType === null) {
335
+ return undefined;
336
+ }
337
+
338
+ const [mediaType, ...parameters] = contentType.split(";").map((part) => part.trim());
339
+
340
+ if (mediaType?.toLowerCase() !== "multipart/form-data") {
341
+ return undefined;
342
+ }
343
+
344
+ for (const parameter of parameters) {
345
+ const [rawName, ...rawValue] = parameter.split("=");
346
+
347
+ if (rawName?.trim().toLowerCase() !== "boundary") {
348
+ continue;
349
+ }
350
+
351
+ const value = rawValue.join("=").trim();
352
+ const unquoted = value.startsWith("\"") && value.endsWith("\"")
353
+ ? value.slice(1, -1)
354
+ : value;
355
+
356
+ return unquoted === "" ? undefined : unquoted;
357
+ }
358
+
359
+ return undefined;
360
+ }
361
+
362
+ function parsePartHeaders(rawHeaders: string): Headers {
363
+ const headers = new Headers();
364
+
365
+ for (const line of rawHeaders.split(crlf)) {
366
+ if (line.trim() === "") {
367
+ continue;
368
+ }
369
+
370
+ const separator = line.indexOf(":");
371
+
372
+ if (separator < 0) {
373
+ throw new Error(`Malformed multipart header: ${line}`);
374
+ }
375
+
376
+ headers.append(
377
+ line.slice(0, separator).trim().toLowerCase(),
378
+ line.slice(separator + 1).trim(),
379
+ );
380
+ }
381
+
382
+ return headers;
383
+ }
384
+
385
+ function parseContentDisposition(value: string | null): {
386
+ name: string | undefined;
387
+ filename: string | undefined;
388
+ } {
389
+ if (value === null) {
390
+ return { name: undefined, filename: undefined };
391
+ }
392
+
393
+ const parameters = value.split(";").map((part) => part.trim());
394
+ const disposition = parameters.shift()?.toLowerCase();
395
+
396
+ if (disposition !== "form-data") {
397
+ return { name: undefined, filename: undefined };
398
+ }
399
+
400
+ let name: string | undefined;
401
+ let filename: string | undefined;
402
+
403
+ for (const parameter of parameters) {
404
+ const separator = parameter.indexOf("=");
405
+
406
+ if (separator < 0) {
407
+ continue;
408
+ }
409
+
410
+ const key = parameter.slice(0, separator).trim().toLowerCase();
411
+ const value = unquoteHeaderParameter(parameter.slice(separator + 1).trim());
412
+
413
+ if (key === "name") {
414
+ name = value;
415
+ } else if (key === "filename") {
416
+ filename = value;
417
+ }
418
+ }
419
+
420
+ return { name, filename };
421
+ }
422
+
423
+ function unquoteHeaderParameter(value: string): string {
424
+ if (!value.startsWith("\"") || !value.endsWith("\"")) {
425
+ return value;
426
+ }
427
+
428
+ return value.slice(1, -1).replaceAll(/\\"/g, "\"").replaceAll(/\\\\/g, "\\");
429
+ }
430
+
431
+ function concatBytes(
432
+ left: Uint8Array<ArrayBufferLike>,
433
+ right: Uint8Array<ArrayBufferLike>,
434
+ ): Uint8Array<ArrayBufferLike> {
435
+ if (left.length === 0) {
436
+ return right;
437
+ }
438
+
439
+ const next = new Uint8Array(left.length + right.length);
440
+ next.set(left);
441
+ next.set(right, left.length);
442
+ return next;
443
+ }
444
+
445
+ function indexOfAscii(bytes: Uint8Array<ArrayBufferLike>, needle: string): number {
446
+ return indexOfBytes(bytes, encodeAscii(needle));
447
+ }
448
+
449
+ function indexOfBytes(
450
+ bytes: Uint8Array<ArrayBufferLike>,
451
+ needle: Uint8Array<ArrayBufferLike>,
452
+ ): number {
453
+ if (needle.length === 0 || bytes.length < needle.length) {
454
+ return -1;
455
+ }
456
+
457
+ const first = needle[0];
458
+
459
+ for (let index = 0; index <= bytes.length - needle.length; index += 1) {
460
+ if (bytes[index] !== first) {
461
+ continue;
462
+ }
463
+
464
+ let matched = true;
465
+
466
+ for (let offset = 1; offset < needle.length; offset += 1) {
467
+ if (bytes[index + offset] !== needle[offset]) {
468
+ matched = false;
469
+ break;
470
+ }
471
+ }
472
+
473
+ if (matched) {
474
+ return index;
475
+ }
476
+ }
477
+
478
+ return -1;
479
+ }
480
+
481
+ function startsWithBytes(
482
+ bytes: Uint8Array<ArrayBufferLike>,
483
+ prefix: Uint8Array<ArrayBufferLike>,
484
+ ): boolean {
485
+ if (bytes.length < prefix.length) {
486
+ return false;
487
+ }
488
+
489
+ for (let index = 0; index < prefix.length; index += 1) {
490
+ if (bytes[index] !== prefix[index]) {
491
+ return false;
492
+ }
493
+ }
494
+
495
+ return true;
496
+ }
497
+
498
+ function encodeAscii(value: string): Uint8Array<ArrayBufferLike> {
499
+ return new TextEncoder().encode(value);
500
+ }
501
+
502
+ function decodeAscii(value: Uint8Array<ArrayBufferLike>): string {
503
+ return new TextDecoder("utf-8", { fatal: false }).decode(value);
504
+ }
505
+
506
+ class AsyncQueue<T> implements AsyncIterator<T> {
507
+ private readonly values: T[] = [];
508
+ private readonly waits: Array<{
509
+ resolve(result: IteratorResult<T>): void;
510
+ reject(error: unknown): void;
511
+ }> = [];
512
+ private done = false;
513
+ private error: unknown;
514
+ private onCancel: (() => void) | undefined;
515
+
516
+ setCancel(onCancel: (() => void) | undefined): void {
517
+ this.onCancel = onCancel;
518
+ }
519
+
520
+ push(value: T): void {
521
+ const wait = this.waits.shift();
522
+
523
+ if (wait !== undefined) {
524
+ wait.resolve({ done: false, value });
525
+ return;
526
+ }
527
+
528
+ this.values.push(value);
529
+ }
530
+
531
+ close(): void {
532
+ this.done = true;
533
+
534
+ for (const wait of this.waits.splice(0)) {
535
+ wait.resolve({ done: true, value: undefined });
536
+ }
537
+ }
538
+
539
+ fail(error: unknown): void {
540
+ this.error = error;
541
+ this.done = true;
542
+
543
+ for (const wait of this.waits.splice(0)) {
544
+ wait.reject(error);
545
+ }
546
+ }
547
+
548
+ next(): Promise<IteratorResult<T>> {
549
+ if (this.values.length > 0) {
550
+ return Promise.resolve({ done: false, value: this.values.shift() as T });
551
+ }
552
+
553
+ if (this.error !== undefined) {
554
+ return Promise.reject(this.error);
555
+ }
556
+
557
+ if (this.done) {
558
+ return Promise.resolve({ done: true, value: undefined });
559
+ }
560
+
561
+ return new Promise((resolve, reject) => {
562
+ this.waits.push({ resolve, reject });
563
+ });
564
+ }
565
+
566
+ return(): Promise<IteratorResult<T>> {
567
+ this.done = true;
568
+ this.values.splice(0);
569
+ this.onCancel?.();
570
+
571
+ for (const wait of this.waits.splice(0)) {
572
+ wait.resolve({ done: true, value: undefined });
573
+ }
574
+
575
+ return Promise.resolve({ done: true, value: undefined });
576
+ }
577
+ }
@@ -0,0 +1,53 @@
1
+ import { createRequire } from "node:module";
2
+ import { nativeModulePackageCandidates } from "./native-route-matcher.js";
3
+ import { escapeHtmlAttribute as escapeHtml } from "@reckona/mreact-shared/html-escape";
4
+
5
+ interface NativeEscapeModule {
6
+ escapeHtmlBatch?: (values: string[]) => string[];
7
+ }
8
+
9
+ let nativeModule: NativeEscapeModule | false | undefined;
10
+ let nativeRequire: ReturnType<typeof createRequire> | false | undefined;
11
+
12
+ export function escapeHtmlBatch(values: readonly unknown[]): string[] {
13
+ const strings = values.map((value) => String(value ?? ""));
14
+ const native = loadNativeEscapeModule();
15
+
16
+ return native?.escapeHtmlBatch?.(strings) ?? strings.map(escapeHtml);
17
+ }
18
+
19
+ function loadNativeEscapeModule(): NativeEscapeModule | undefined {
20
+ if (nativeModule === undefined) {
21
+ const require = nativePackageRequire();
22
+
23
+ if (require === undefined) {
24
+ nativeModule = false;
25
+ return undefined;
26
+ }
27
+
28
+ for (const candidate of nativeModulePackageCandidates(process.platform, process.arch)) {
29
+ try {
30
+ nativeModule = require(candidate) as NativeEscapeModule;
31
+ break;
32
+ } catch {
33
+ // Native package is optional. JS escaping remains the portable fallback.
34
+ }
35
+ }
36
+ nativeModule ??= false;
37
+ }
38
+
39
+ return nativeModule === false ? undefined : nativeModule;
40
+ }
41
+
42
+ function nativePackageRequire(): ReturnType<typeof createRequire> | undefined {
43
+ if (nativeRequire === undefined) {
44
+ try {
45
+ nativeRequire =
46
+ new URL(import.meta.url).protocol === "file:" ? createRequire(import.meta.url) : false;
47
+ } catch {
48
+ nativeRequire = false;
49
+ }
50
+ }
51
+
52
+ return nativeRequire === false ? undefined : nativeRequire;
53
+ }