@chidchanun/bcp 0.1.7 → 0.1.9
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 +71 -0
- package/README.md +190 -4
- package/docs/application-modules.md +63 -3
- package/docs/server-data-loaders.md +240 -0
- package/docs/session-auth.md +186 -0
- package/package.json +1 -1
- package/packages/bundler/src/server-production.ts +25 -0
- package/packages/client/src/index.tsx +4 -0
- package/packages/client/src/loader-data.tsx +99 -0
- package/packages/client/src/router-v2.tsx +254 -29
- package/packages/client/src/server.ts +13 -0
- 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-loader.ts +346 -0
- package/packages/server/src/session.ts +694 -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,346 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
pathToFileURL,
|
|
5
|
+
} from "node:url";
|
|
6
|
+
|
|
7
|
+
export const LOADER_DATA_PARAM =
|
|
8
|
+
"__bcp_loader_data";
|
|
9
|
+
|
|
10
|
+
export interface PageLoaderContext {
|
|
11
|
+
params: Record<string, any>;
|
|
12
|
+
searchParams: URLSearchParams;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type PageLoader = (
|
|
16
|
+
context: PageLoaderContext
|
|
17
|
+
) =>
|
|
18
|
+
| unknown
|
|
19
|
+
| Promise<unknown>;
|
|
20
|
+
|
|
21
|
+
export interface PageLoaderExecution {
|
|
22
|
+
hasLoader: boolean;
|
|
23
|
+
data: unknown;
|
|
24
|
+
response: Response | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function findPageLoaderFile(
|
|
28
|
+
pageFilePath: string
|
|
29
|
+
): string | null {
|
|
30
|
+
const directory =
|
|
31
|
+
path.dirname(
|
|
32
|
+
pageFilePath
|
|
33
|
+
);
|
|
34
|
+
const tsFile =
|
|
35
|
+
path.join(
|
|
36
|
+
directory,
|
|
37
|
+
"loader.ts"
|
|
38
|
+
);
|
|
39
|
+
const tsxFile =
|
|
40
|
+
path.join(
|
|
41
|
+
directory,
|
|
42
|
+
"loader.tsx"
|
|
43
|
+
);
|
|
44
|
+
const hasTs =
|
|
45
|
+
fs.existsSync(
|
|
46
|
+
tsFile
|
|
47
|
+
);
|
|
48
|
+
const hasTsx =
|
|
49
|
+
fs.existsSync(
|
|
50
|
+
tsxFile
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (hasTs && hasTsx) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`BCP Framework: both loader.ts and loader.tsx exist in "${directory}".`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (hasTs) {
|
|
60
|
+
return tsFile;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (hasTsx) {
|
|
64
|
+
return tsxFile;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function routeHasPageLoader(
|
|
71
|
+
pageFilePath: string
|
|
72
|
+
): boolean {
|
|
73
|
+
return findPageLoaderFile(
|
|
74
|
+
pageFilePath
|
|
75
|
+
) !== null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function executePageLoader(
|
|
79
|
+
pageFilePath: string,
|
|
80
|
+
params: Record<string, any>,
|
|
81
|
+
requestUrl: URL,
|
|
82
|
+
moduleVersion = 0
|
|
83
|
+
): Promise<PageLoaderExecution> {
|
|
84
|
+
const loaderFile =
|
|
85
|
+
findPageLoaderFile(
|
|
86
|
+
pageFilePath
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (!loaderFile) {
|
|
90
|
+
return {
|
|
91
|
+
hasLoader:
|
|
92
|
+
false,
|
|
93
|
+
data:
|
|
94
|
+
undefined,
|
|
95
|
+
response:
|
|
96
|
+
null,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const moduleUrl =
|
|
101
|
+
pathToFileURL(
|
|
102
|
+
loaderFile
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
moduleUrl.searchParams.set(
|
|
106
|
+
"bcp-loader",
|
|
107
|
+
String(
|
|
108
|
+
moduleVersion
|
|
109
|
+
)
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const loaderModule =
|
|
113
|
+
await import(
|
|
114
|
+
moduleUrl.href
|
|
115
|
+
) as {
|
|
116
|
+
loader?: unknown;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
if (
|
|
120
|
+
typeof loaderModule.loader !==
|
|
121
|
+
"function"
|
|
122
|
+
) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`BCP Framework: loader "${loaderFile}" must export a named loader() function.`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return executePageLoaderFunction(
|
|
129
|
+
loaderModule.loader as
|
|
130
|
+
PageLoader,
|
|
131
|
+
params,
|
|
132
|
+
requestUrl,
|
|
133
|
+
loaderFile
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function executePageLoaderFunction(
|
|
138
|
+
loader: PageLoader,
|
|
139
|
+
params: Record<string, any>,
|
|
140
|
+
requestUrl: URL,
|
|
141
|
+
label = "loader"
|
|
142
|
+
): Promise<PageLoaderExecution> {
|
|
143
|
+
const result =
|
|
144
|
+
await loader({
|
|
145
|
+
params: {
|
|
146
|
+
...params,
|
|
147
|
+
},
|
|
148
|
+
searchParams:
|
|
149
|
+
new URLSearchParams(
|
|
150
|
+
requestUrl.searchParams
|
|
151
|
+
),
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (
|
|
155
|
+
result instanceof
|
|
156
|
+
Response
|
|
157
|
+
) {
|
|
158
|
+
return {
|
|
159
|
+
hasLoader:
|
|
160
|
+
true,
|
|
161
|
+
data:
|
|
162
|
+
undefined,
|
|
163
|
+
response:
|
|
164
|
+
result,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
assertLoaderData(
|
|
169
|
+
result,
|
|
170
|
+
label
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
hasLoader:
|
|
175
|
+
true,
|
|
176
|
+
data:
|
|
177
|
+
result,
|
|
178
|
+
response:
|
|
179
|
+
null,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function attachLoaderDataToParams(
|
|
184
|
+
params: Record<string, any>,
|
|
185
|
+
execution: PageLoaderExecution
|
|
186
|
+
): Record<string, any> {
|
|
187
|
+
if (!execution.hasLoader) {
|
|
188
|
+
return {
|
|
189
|
+
...params,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
...params,
|
|
195
|
+
[LOADER_DATA_PARAM]:
|
|
196
|
+
execution.data,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function assertLoaderData(
|
|
201
|
+
value: unknown,
|
|
202
|
+
loaderLabel: string
|
|
203
|
+
): void {
|
|
204
|
+
const seen =
|
|
205
|
+
new Set<object>();
|
|
206
|
+
|
|
207
|
+
validateJsonValue(
|
|
208
|
+
value,
|
|
209
|
+
"$",
|
|
210
|
+
seen,
|
|
211
|
+
loaderLabel
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function validateJsonValue(
|
|
216
|
+
value: unknown,
|
|
217
|
+
location: string,
|
|
218
|
+
seen: Set<object>,
|
|
219
|
+
loaderLabel: string
|
|
220
|
+
): void {
|
|
221
|
+
if (
|
|
222
|
+
value === null ||
|
|
223
|
+
typeof value ===
|
|
224
|
+
"string" ||
|
|
225
|
+
typeof value ===
|
|
226
|
+
"boolean"
|
|
227
|
+
) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (
|
|
232
|
+
typeof value ===
|
|
233
|
+
"number"
|
|
234
|
+
) {
|
|
235
|
+
if (
|
|
236
|
+
Number.isFinite(
|
|
237
|
+
value
|
|
238
|
+
)
|
|
239
|
+
) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
throwInvalidLoaderData(
|
|
244
|
+
loaderLabel,
|
|
245
|
+
location,
|
|
246
|
+
"numbers must be finite"
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (
|
|
251
|
+
typeof value !==
|
|
252
|
+
"object"
|
|
253
|
+
) {
|
|
254
|
+
throwInvalidLoaderData(
|
|
255
|
+
loaderLabel,
|
|
256
|
+
location,
|
|
257
|
+
`unsupported ${typeof value} value`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const objectValue =
|
|
262
|
+
value as object;
|
|
263
|
+
|
|
264
|
+
if (seen.has(objectValue)) {
|
|
265
|
+
throwInvalidLoaderData(
|
|
266
|
+
loaderLabel,
|
|
267
|
+
location,
|
|
268
|
+
"circular references are not supported"
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
seen.add(
|
|
273
|
+
objectValue
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
if (Array.isArray(value)) {
|
|
278
|
+
value.forEach(
|
|
279
|
+
(
|
|
280
|
+
item,
|
|
281
|
+
index
|
|
282
|
+
) => {
|
|
283
|
+
validateJsonValue(
|
|
284
|
+
item,
|
|
285
|
+
`${location}[${index}]`,
|
|
286
|
+
seen,
|
|
287
|
+
loaderLabel
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const prototype =
|
|
295
|
+
Object.getPrototypeOf(
|
|
296
|
+
value
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
if (
|
|
300
|
+
prototype !==
|
|
301
|
+
Object.prototype &&
|
|
302
|
+
prototype !==
|
|
303
|
+
null
|
|
304
|
+
) {
|
|
305
|
+
throwInvalidLoaderData(
|
|
306
|
+
loaderLabel,
|
|
307
|
+
location,
|
|
308
|
+
"only plain objects and arrays are supported"
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
for (
|
|
313
|
+
const [
|
|
314
|
+
key,
|
|
315
|
+
item,
|
|
316
|
+
]
|
|
317
|
+
of Object.entries(
|
|
318
|
+
value as Record<
|
|
319
|
+
string,
|
|
320
|
+
unknown
|
|
321
|
+
>
|
|
322
|
+
)
|
|
323
|
+
) {
|
|
324
|
+
validateJsonValue(
|
|
325
|
+
item,
|
|
326
|
+
`${location}.${key}`,
|
|
327
|
+
seen,
|
|
328
|
+
loaderLabel
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
} finally {
|
|
332
|
+
seen.delete(
|
|
333
|
+
objectValue
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function throwInvalidLoaderData(
|
|
339
|
+
loaderLabel: string,
|
|
340
|
+
location: string,
|
|
341
|
+
reason: string
|
|
342
|
+
): never {
|
|
343
|
+
throw new Error(
|
|
344
|
+
`BCP Framework: loader "${loaderLabel}" returned non-serializable data at ${location}: ${reason}.`
|
|
345
|
+
);
|
|
346
|
+
}
|