@chidchanun/bcp 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -0
- package/README.md +218 -7
- package/docs/application-modules.md +63 -3
- package/docs/releasing.md +34 -19
- package/docs/route-guards.md +240 -0
- package/docs/server-data-loaders.md +240 -0
- package/docs/updating.md +130 -0
- package/package.json +1 -1
- package/packages/bundler/src/server-production-guards.ts +497 -0
- package/packages/bundler/src/server-production-middleware.ts +33 -8
- package/packages/bundler/src/server-production.ts +25 -0
- package/packages/cli/src/args.ts +58 -0
- package/packages/cli/src/index.ts +41 -13
- package/packages/cli/src/update.ts +860 -0
- package/packages/client/src/index.tsx +5 -0
- package/packages/client/src/loader-data.tsx +229 -0
- package/packages/client/src/router-v2.tsx +254 -29
- package/packages/server/src/dev-navigation-target.ts +188 -0
- package/packages/server/src/index.ts +209 -119
- package/packages/server/src/navigation-payload.ts +24 -1
- package/packages/server/src/navigation-response.ts +284 -0
- package/packages/server/src/page-guard.ts +529 -0
- package/packages/server/src/page-loader.ts +643 -0
- package/packages/server/src/standalone-production-runtime-v2-guard.ts +815 -0
- package/packages/server/src/standalone-production-runtime-v2-navigation.ts +785 -0
- package/packages/server/src/standalone-production-runtime-v2.ts +131 -19
- package/packages/server/src/standalone-production-runtime-v3.ts +1 -1
- package/packages/server/src/standalone-production-runtime-v4.ts +42 -2
- package/packages/server/src/static-dev-server.ts +21 -17
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
pathToFileURL,
|
|
5
|
+
} from "node:url";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
executePageGuards,
|
|
9
|
+
type PageGuardData,
|
|
10
|
+
} from "./page-guard.js";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
headers,
|
|
14
|
+
} from "./request-context.js";
|
|
15
|
+
|
|
16
|
+
export const LOADER_DATA_PARAM =
|
|
17
|
+
"__bcp_loader_data";
|
|
18
|
+
export const GUARD_DATA_PARAM =
|
|
19
|
+
"__bcp_guard_data";
|
|
20
|
+
export const GUARD_DATA_HEADER =
|
|
21
|
+
"x-bcp-guard-data";
|
|
22
|
+
|
|
23
|
+
const SERVER_DATA_MARKER =
|
|
24
|
+
"__bcp_server_data";
|
|
25
|
+
|
|
26
|
+
export interface PageLoaderContext {
|
|
27
|
+
params: Record<string, any>;
|
|
28
|
+
searchParams: URLSearchParams;
|
|
29
|
+
guardData: Readonly<PageGuardData>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type PageLoader = (
|
|
33
|
+
context: PageLoaderContext
|
|
34
|
+
) =>
|
|
35
|
+
| unknown
|
|
36
|
+
| Promise<unknown>;
|
|
37
|
+
|
|
38
|
+
export interface PageServerDataEnvelope {
|
|
39
|
+
__bcp_server_data: true;
|
|
40
|
+
hasPageLoader: boolean;
|
|
41
|
+
hasGuard: boolean;
|
|
42
|
+
loaderData: unknown;
|
|
43
|
+
guardData: PageGuardData;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface PageLoaderExecution {
|
|
47
|
+
/*
|
|
48
|
+
* hasLoader remains the SSR-provider switch for backward compatibility.
|
|
49
|
+
* It is true when either loader or guard data must be made available to
|
|
50
|
+
* React during server rendering.
|
|
51
|
+
*/
|
|
52
|
+
hasLoader: boolean;
|
|
53
|
+
hasPageLoader?: boolean;
|
|
54
|
+
hasGuard?: boolean;
|
|
55
|
+
data: unknown;
|
|
56
|
+
loaderData?: unknown;
|
|
57
|
+
guardData?: PageGuardData;
|
|
58
|
+
response: Response | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function findPageLoaderFile(
|
|
62
|
+
pageFilePath: string
|
|
63
|
+
): string | null {
|
|
64
|
+
const directory =
|
|
65
|
+
path.dirname(
|
|
66
|
+
pageFilePath
|
|
67
|
+
);
|
|
68
|
+
const tsFile =
|
|
69
|
+
path.join(
|
|
70
|
+
directory,
|
|
71
|
+
"loader.ts"
|
|
72
|
+
);
|
|
73
|
+
const tsxFile =
|
|
74
|
+
path.join(
|
|
75
|
+
directory,
|
|
76
|
+
"loader.tsx"
|
|
77
|
+
);
|
|
78
|
+
const hasTs =
|
|
79
|
+
fs.existsSync(
|
|
80
|
+
tsFile
|
|
81
|
+
);
|
|
82
|
+
const hasTsx =
|
|
83
|
+
fs.existsSync(
|
|
84
|
+
tsxFile
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (hasTs && hasTsx) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`BCP Framework: both loader.ts and loader.tsx exist in "${directory}".`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (hasTs) {
|
|
94
|
+
return tsFile;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (hasTsx) {
|
|
98
|
+
return tsxFile;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function routeHasPageLoader(
|
|
105
|
+
pageFilePath: string
|
|
106
|
+
): boolean {
|
|
107
|
+
return findPageLoaderFile(
|
|
108
|
+
pageFilePath
|
|
109
|
+
) !== null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function executePageLoader(
|
|
113
|
+
pageFilePath: string,
|
|
114
|
+
params: Record<string, any>,
|
|
115
|
+
requestUrl: URL,
|
|
116
|
+
moduleVersion = 0
|
|
117
|
+
): Promise<PageLoaderExecution> {
|
|
118
|
+
const appDirectory =
|
|
119
|
+
findAppDirectory(
|
|
120
|
+
pageFilePath
|
|
121
|
+
);
|
|
122
|
+
const guardExecution =
|
|
123
|
+
await executePageGuards(
|
|
124
|
+
appDirectory,
|
|
125
|
+
pageFilePath,
|
|
126
|
+
params,
|
|
127
|
+
requestUrl,
|
|
128
|
+
moduleVersion
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (
|
|
132
|
+
guardExecution.response
|
|
133
|
+
) {
|
|
134
|
+
return createExecution({
|
|
135
|
+
hasPageLoader:
|
|
136
|
+
false,
|
|
137
|
+
hasGuard:
|
|
138
|
+
guardExecution.hasGuard,
|
|
139
|
+
loaderData:
|
|
140
|
+
undefined,
|
|
141
|
+
guardData:
|
|
142
|
+
guardExecution.data,
|
|
143
|
+
response:
|
|
144
|
+
guardExecution.response,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const loaderFile =
|
|
149
|
+
findPageLoaderFile(
|
|
150
|
+
pageFilePath
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
if (!loaderFile) {
|
|
154
|
+
return createExecution({
|
|
155
|
+
hasPageLoader:
|
|
156
|
+
false,
|
|
157
|
+
hasGuard:
|
|
158
|
+
guardExecution.hasGuard,
|
|
159
|
+
loaderData:
|
|
160
|
+
undefined,
|
|
161
|
+
guardData:
|
|
162
|
+
guardExecution.data,
|
|
163
|
+
response:
|
|
164
|
+
null,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const moduleUrl =
|
|
169
|
+
pathToFileURL(
|
|
170
|
+
loaderFile
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
moduleUrl.searchParams.set(
|
|
174
|
+
"bcp-loader",
|
|
175
|
+
String(
|
|
176
|
+
moduleVersion
|
|
177
|
+
)
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const loaderModule =
|
|
181
|
+
await import(
|
|
182
|
+
moduleUrl.href
|
|
183
|
+
) as {
|
|
184
|
+
loader?: unknown;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
if (
|
|
188
|
+
typeof loaderModule.loader !==
|
|
189
|
+
"function"
|
|
190
|
+
) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`BCP Framework: loader "${loaderFile}" must export a named loader() function.`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return executePageLoaderFunction(
|
|
197
|
+
loaderModule.loader as
|
|
198
|
+
PageLoader,
|
|
199
|
+
params,
|
|
200
|
+
requestUrl,
|
|
201
|
+
loaderFile,
|
|
202
|
+
guardExecution.data,
|
|
203
|
+
guardExecution.hasGuard
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function executePageLoaderFunction(
|
|
208
|
+
loader: PageLoader,
|
|
209
|
+
params: Record<string, any>,
|
|
210
|
+
requestUrl: URL,
|
|
211
|
+
label = "loader",
|
|
212
|
+
explicitGuardData?: PageGuardData,
|
|
213
|
+
explicitHasGuard?: boolean
|
|
214
|
+
): Promise<PageLoaderExecution> {
|
|
215
|
+
const headerGuardData =
|
|
216
|
+
explicitGuardData ===
|
|
217
|
+
undefined
|
|
218
|
+
? await readGuardDataHeader()
|
|
219
|
+
: null;
|
|
220
|
+
const guardData =
|
|
221
|
+
explicitGuardData ??
|
|
222
|
+
headerGuardData?.data ??
|
|
223
|
+
{};
|
|
224
|
+
const hasGuard =
|
|
225
|
+
explicitHasGuard ??
|
|
226
|
+
headerGuardData?.hasGuard ??
|
|
227
|
+
false;
|
|
228
|
+
|
|
229
|
+
const result =
|
|
230
|
+
await loader({
|
|
231
|
+
params: {
|
|
232
|
+
...params,
|
|
233
|
+
},
|
|
234
|
+
searchParams:
|
|
235
|
+
new URLSearchParams(
|
|
236
|
+
requestUrl.searchParams
|
|
237
|
+
),
|
|
238
|
+
guardData: {
|
|
239
|
+
...guardData,
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
if (
|
|
244
|
+
result instanceof
|
|
245
|
+
Response
|
|
246
|
+
) {
|
|
247
|
+
return createExecution({
|
|
248
|
+
hasPageLoader:
|
|
249
|
+
true,
|
|
250
|
+
hasGuard,
|
|
251
|
+
loaderData:
|
|
252
|
+
undefined,
|
|
253
|
+
guardData,
|
|
254
|
+
response:
|
|
255
|
+
result,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
assertLoaderData(
|
|
260
|
+
result,
|
|
261
|
+
label
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
return createExecution({
|
|
265
|
+
hasPageLoader:
|
|
266
|
+
true,
|
|
267
|
+
hasGuard,
|
|
268
|
+
loaderData:
|
|
269
|
+
result,
|
|
270
|
+
guardData,
|
|
271
|
+
response:
|
|
272
|
+
null,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function attachLoaderDataToParams(
|
|
277
|
+
params: Record<string, any>,
|
|
278
|
+
execution: PageLoaderExecution
|
|
279
|
+
): Record<string, any> {
|
|
280
|
+
let result = {
|
|
281
|
+
...params,
|
|
282
|
+
};
|
|
283
|
+
const hasGuard =
|
|
284
|
+
execution.hasGuard ===
|
|
285
|
+
true;
|
|
286
|
+
const guardData =
|
|
287
|
+
execution.guardData ??
|
|
288
|
+
{};
|
|
289
|
+
const hasPageLoader =
|
|
290
|
+
execution.hasPageLoader ??
|
|
291
|
+
execution.hasLoader;
|
|
292
|
+
const loaderData =
|
|
293
|
+
execution.hasPageLoader ===
|
|
294
|
+
undefined
|
|
295
|
+
? execution.data
|
|
296
|
+
: execution.loaderData;
|
|
297
|
+
|
|
298
|
+
if (hasGuard) {
|
|
299
|
+
result = {
|
|
300
|
+
...result,
|
|
301
|
+
[GUARD_DATA_PARAM]:
|
|
302
|
+
guardData,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (hasPageLoader) {
|
|
307
|
+
result = {
|
|
308
|
+
...result,
|
|
309
|
+
[LOADER_DATA_PARAM]:
|
|
310
|
+
loaderData,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return result;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function isPageServerDataEnvelope(
|
|
318
|
+
value: unknown
|
|
319
|
+
): value is PageServerDataEnvelope {
|
|
320
|
+
return (
|
|
321
|
+
value !== null &&
|
|
322
|
+
typeof value ===
|
|
323
|
+
"object" &&
|
|
324
|
+
(
|
|
325
|
+
value as
|
|
326
|
+
Record<string, unknown>
|
|
327
|
+
)[SERVER_DATA_MARKER] ===
|
|
328
|
+
true
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function createExecution(
|
|
333
|
+
input: {
|
|
334
|
+
hasPageLoader: boolean;
|
|
335
|
+
hasGuard: boolean;
|
|
336
|
+
loaderData: unknown;
|
|
337
|
+
guardData: PageGuardData;
|
|
338
|
+
response: Response | null;
|
|
339
|
+
}
|
|
340
|
+
): PageLoaderExecution {
|
|
341
|
+
const data:
|
|
342
|
+
PageServerDataEnvelope = {
|
|
343
|
+
__bcp_server_data:
|
|
344
|
+
true,
|
|
345
|
+
hasPageLoader:
|
|
346
|
+
input.hasPageLoader,
|
|
347
|
+
hasGuard:
|
|
348
|
+
input.hasGuard,
|
|
349
|
+
loaderData:
|
|
350
|
+
input.loaderData,
|
|
351
|
+
guardData: {
|
|
352
|
+
...input.guardData,
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
hasLoader:
|
|
358
|
+
input.hasPageLoader ||
|
|
359
|
+
input.hasGuard,
|
|
360
|
+
hasPageLoader:
|
|
361
|
+
input.hasPageLoader,
|
|
362
|
+
hasGuard:
|
|
363
|
+
input.hasGuard,
|
|
364
|
+
data,
|
|
365
|
+
loaderData:
|
|
366
|
+
input.loaderData,
|
|
367
|
+
guardData:
|
|
368
|
+
data.guardData,
|
|
369
|
+
response:
|
|
370
|
+
input.response,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function readGuardDataHeader():
|
|
375
|
+
Promise<{
|
|
376
|
+
hasGuard: true;
|
|
377
|
+
data: PageGuardData;
|
|
378
|
+
} | null> {
|
|
379
|
+
let requestHeaders:
|
|
380
|
+
Headers;
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
requestHeaders =
|
|
384
|
+
await headers();
|
|
385
|
+
} catch {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const encoded =
|
|
390
|
+
requestHeaders.get(
|
|
391
|
+
GUARD_DATA_HEADER
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
if (!encoded) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
let parsed:
|
|
399
|
+
unknown;
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
parsed =
|
|
403
|
+
JSON.parse(
|
|
404
|
+
Buffer.from(
|
|
405
|
+
encoded,
|
|
406
|
+
"base64url"
|
|
407
|
+
).toString(
|
|
408
|
+
"utf8"
|
|
409
|
+
)
|
|
410
|
+
);
|
|
411
|
+
} catch {
|
|
412
|
+
throw new Error(
|
|
413
|
+
"BCP Framework: invalid internal guard data header."
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
assertGuardHeaderData(
|
|
418
|
+
parsed
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
hasGuard:
|
|
423
|
+
true,
|
|
424
|
+
data:
|
|
425
|
+
parsed,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function assertGuardHeaderData(
|
|
430
|
+
value: unknown
|
|
431
|
+
): asserts value is PageGuardData {
|
|
432
|
+
if (
|
|
433
|
+
value === null ||
|
|
434
|
+
Array.isArray(
|
|
435
|
+
value
|
|
436
|
+
) ||
|
|
437
|
+
typeof value !==
|
|
438
|
+
"object"
|
|
439
|
+
) {
|
|
440
|
+
throw new Error(
|
|
441
|
+
"BCP Framework: invalid internal guard data header."
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const prototype =
|
|
446
|
+
Object.getPrototypeOf(
|
|
447
|
+
value
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
if (
|
|
451
|
+
prototype !==
|
|
452
|
+
Object.prototype &&
|
|
453
|
+
prototype !==
|
|
454
|
+
null
|
|
455
|
+
) {
|
|
456
|
+
throw new Error(
|
|
457
|
+
"BCP Framework: invalid internal guard data header."
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function findAppDirectory(
|
|
463
|
+
pageFilePath: string
|
|
464
|
+
): string {
|
|
465
|
+
let current =
|
|
466
|
+
path.dirname(
|
|
467
|
+
path.resolve(
|
|
468
|
+
pageFilePath
|
|
469
|
+
)
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
while (true) {
|
|
473
|
+
if (
|
|
474
|
+
path.basename(
|
|
475
|
+
current
|
|
476
|
+
) === "app"
|
|
477
|
+
) {
|
|
478
|
+
return current;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const parent =
|
|
482
|
+
path.dirname(
|
|
483
|
+
current
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
if (parent === current) {
|
|
487
|
+
throw new Error(
|
|
488
|
+
`BCP Framework: could not resolve app directory for page "${pageFilePath}".`
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
current =
|
|
493
|
+
parent;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function assertLoaderData(
|
|
498
|
+
value: unknown,
|
|
499
|
+
loaderLabel: string
|
|
500
|
+
): void {
|
|
501
|
+
const seen =
|
|
502
|
+
new Set<object>();
|
|
503
|
+
|
|
504
|
+
validateJsonValue(
|
|
505
|
+
value,
|
|
506
|
+
"$",
|
|
507
|
+
seen,
|
|
508
|
+
loaderLabel
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function validateJsonValue(
|
|
513
|
+
value: unknown,
|
|
514
|
+
location: string,
|
|
515
|
+
seen: Set<object>,
|
|
516
|
+
loaderLabel: string
|
|
517
|
+
): void {
|
|
518
|
+
if (
|
|
519
|
+
value === null ||
|
|
520
|
+
typeof value ===
|
|
521
|
+
"string" ||
|
|
522
|
+
typeof value ===
|
|
523
|
+
"boolean"
|
|
524
|
+
) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (
|
|
529
|
+
typeof value ===
|
|
530
|
+
"number"
|
|
531
|
+
) {
|
|
532
|
+
if (
|
|
533
|
+
Number.isFinite(
|
|
534
|
+
value
|
|
535
|
+
)
|
|
536
|
+
) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
throwInvalidLoaderData(
|
|
541
|
+
loaderLabel,
|
|
542
|
+
location,
|
|
543
|
+
"numbers must be finite"
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (
|
|
548
|
+
typeof value !==
|
|
549
|
+
"object"
|
|
550
|
+
) {
|
|
551
|
+
throwInvalidLoaderData(
|
|
552
|
+
loaderLabel,
|
|
553
|
+
location,
|
|
554
|
+
`unsupported ${typeof value} value`
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const objectValue =
|
|
559
|
+
value as object;
|
|
560
|
+
|
|
561
|
+
if (seen.has(objectValue)) {
|
|
562
|
+
throwInvalidLoaderData(
|
|
563
|
+
loaderLabel,
|
|
564
|
+
location,
|
|
565
|
+
"circular references are not supported"
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
seen.add(
|
|
570
|
+
objectValue
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
try {
|
|
574
|
+
if (Array.isArray(value)) {
|
|
575
|
+
value.forEach(
|
|
576
|
+
(
|
|
577
|
+
item,
|
|
578
|
+
index
|
|
579
|
+
) => {
|
|
580
|
+
validateJsonValue(
|
|
581
|
+
item,
|
|
582
|
+
`${location}[${index}]`,
|
|
583
|
+
seen,
|
|
584
|
+
loaderLabel
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const prototype =
|
|
592
|
+
Object.getPrototypeOf(
|
|
593
|
+
value
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
if (
|
|
597
|
+
prototype !==
|
|
598
|
+
Object.prototype &&
|
|
599
|
+
prototype !==
|
|
600
|
+
null
|
|
601
|
+
) {
|
|
602
|
+
throwInvalidLoaderData(
|
|
603
|
+
loaderLabel,
|
|
604
|
+
location,
|
|
605
|
+
"only plain objects and arrays are supported"
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
for (
|
|
610
|
+
const [
|
|
611
|
+
key,
|
|
612
|
+
item,
|
|
613
|
+
]
|
|
614
|
+
of Object.entries(
|
|
615
|
+
value as Record<
|
|
616
|
+
string,
|
|
617
|
+
unknown
|
|
618
|
+
>
|
|
619
|
+
)
|
|
620
|
+
) {
|
|
621
|
+
validateJsonValue(
|
|
622
|
+
item,
|
|
623
|
+
`${location}.${key}`,
|
|
624
|
+
seen,
|
|
625
|
+
loaderLabel
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
} finally {
|
|
629
|
+
seen.delete(
|
|
630
|
+
objectValue
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function throwInvalidLoaderData(
|
|
636
|
+
loaderLabel: string,
|
|
637
|
+
location: string,
|
|
638
|
+
reason: string
|
|
639
|
+
): never {
|
|
640
|
+
throw new Error(
|
|
641
|
+
`BCP Framework: loader "${loaderLabel}" returned non-serializable data at ${location}: ${reason}.`
|
|
642
|
+
);
|
|
643
|
+
}
|