@chidchanun/bcp 0.2.13 → 0.2.15
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/README.md +224 -99
- package/docs/README.md +40 -35
- package/docs/api-manifest.json +26 -10
- package/docs/api-reference.md +173 -29
- package/docs/docs-web-manifest.json +8 -4
- package/docs/platform-manifest.json +33 -4
- package/docs/plugin-module-platform.md +391 -0
- package/docs/releases/0.2.14.md +241 -0
- package/docs/releases/0.2.15.md +118 -0
- package/docs/testing-platform.md +602 -0
- package/package.json +11 -1
- package/packages/bundler/src/client-boundary.ts +2 -0
- package/packages/client/src/plugins.mjs +811 -0
- package/packages/client/src/plugins.ts +26 -0
- package/packages/client/src/realtime.mjs +67 -30
- package/packages/client/src/testing.mjs +2357 -0
- package/packages/client/src/testing.ts +52 -0
- package/packages/server/src/plugins.ts +1225 -0
- package/packages/server/src/realtime.ts +95 -41
- package/packages/server/src/testing-page.ts +274 -0
- package/packages/server/src/testing.ts +1884 -0
|
@@ -1139,43 +1139,88 @@ export function createRealtimeSseResponse(
|
|
|
1139
1139
|
options.keepAliveMs ?? 15_000,
|
|
1140
1140
|
"SSE keepAliveMs"
|
|
1141
1141
|
);
|
|
1142
|
+
let closed = false;
|
|
1143
|
+
let controller:
|
|
1144
|
+
ReadableStreamDefaultController<Uint8Array> |
|
|
1145
|
+
undefined;
|
|
1146
|
+
let timer:
|
|
1147
|
+
ReturnType<typeof setTimeout> |
|
|
1148
|
+
undefined;
|
|
1149
|
+
let unsubscribe:
|
|
1150
|
+
() => void =
|
|
1151
|
+
() => undefined;
|
|
1152
|
+
let abortListener:
|
|
1153
|
+
(() => void) |
|
|
1154
|
+
undefined;
|
|
1155
|
+
|
|
1156
|
+
const cleanup = () => {
|
|
1157
|
+
if (closed) {
|
|
1158
|
+
return false;
|
|
1159
|
+
}
|
|
1160
|
+
closed = true;
|
|
1161
|
+
if (timer) {
|
|
1162
|
+
clearTimeout(timer);
|
|
1163
|
+
timer = undefined;
|
|
1164
|
+
}
|
|
1165
|
+
unsubscribe();
|
|
1166
|
+
unsubscribe =
|
|
1167
|
+
() => undefined;
|
|
1168
|
+
if (
|
|
1169
|
+
options.signal &&
|
|
1170
|
+
abortListener
|
|
1171
|
+
) {
|
|
1172
|
+
options.signal.removeEventListener(
|
|
1173
|
+
"abort",
|
|
1174
|
+
abortListener
|
|
1175
|
+
);
|
|
1176
|
+
abortListener = undefined;
|
|
1177
|
+
}
|
|
1178
|
+
return true;
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
const close = () => {
|
|
1182
|
+
if (!cleanup()) {
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
const currentController =
|
|
1186
|
+
controller;
|
|
1187
|
+
controller = undefined;
|
|
1188
|
+
if (!currentController) {
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
try {
|
|
1192
|
+
currentController.close();
|
|
1193
|
+
} catch {
|
|
1194
|
+
// Stream may already be closed or cancelled by the runtime.
|
|
1195
|
+
}
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
const write = (
|
|
1199
|
+
value: string
|
|
1200
|
+
) => {
|
|
1201
|
+
const currentController =
|
|
1202
|
+
controller;
|
|
1203
|
+
if (
|
|
1204
|
+
closed ||
|
|
1205
|
+
!currentController
|
|
1206
|
+
) {
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
try {
|
|
1210
|
+
currentController.enqueue(
|
|
1211
|
+
encoder.encode(value)
|
|
1212
|
+
);
|
|
1213
|
+
} catch {
|
|
1214
|
+
cleanup();
|
|
1215
|
+
controller = undefined;
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1142
1218
|
|
|
1143
1219
|
const stream =
|
|
1144
1220
|
new ReadableStream<Uint8Array>({
|
|
1145
|
-
start(
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
ReturnType<typeof setTimeout> |
|
|
1149
|
-
undefined;
|
|
1150
|
-
let unsubscribe:
|
|
1151
|
-
() => void =
|
|
1152
|
-
() => undefined;
|
|
1153
|
-
|
|
1154
|
-
const close = () => {
|
|
1155
|
-
if (closed) {
|
|
1156
|
-
return;
|
|
1157
|
-
}
|
|
1158
|
-
closed = true;
|
|
1159
|
-
if (timer) {
|
|
1160
|
-
clearTimeout(timer);
|
|
1161
|
-
}
|
|
1162
|
-
unsubscribe();
|
|
1163
|
-
try {
|
|
1164
|
-
controller.close();
|
|
1165
|
-
} catch {
|
|
1166
|
-
// Stream may already be closed by the runtime.
|
|
1167
|
-
}
|
|
1168
|
-
};
|
|
1169
|
-
|
|
1170
|
-
const write = (
|
|
1171
|
-
value: string
|
|
1172
|
-
) => {
|
|
1173
|
-
if (!closed) {
|
|
1174
|
-
controller.enqueue(
|
|
1175
|
-
encoder.encode(value)
|
|
1176
|
-
);
|
|
1177
|
-
}
|
|
1178
|
-
};
|
|
1221
|
+
start(streamController) {
|
|
1222
|
+
controller =
|
|
1223
|
+
streamController;
|
|
1179
1224
|
|
|
1180
1225
|
if (retryMs !== undefined) {
|
|
1181
1226
|
write(
|
|
@@ -1211,11 +1256,13 @@ export function createRealtimeSseResponse(
|
|
|
1211
1256
|
write(
|
|
1212
1257
|
`: keep-alive ${Date.now()}\n\n`
|
|
1213
1258
|
);
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1259
|
+
if (!closed) {
|
|
1260
|
+
timer =
|
|
1261
|
+
setTimeout(
|
|
1262
|
+
keepAlive,
|
|
1263
|
+
keepAliveMs
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1219
1266
|
};
|
|
1220
1267
|
timer =
|
|
1221
1268
|
setTimeout(
|
|
@@ -1224,19 +1271,26 @@ export function createRealtimeSseResponse(
|
|
|
1224
1271
|
);
|
|
1225
1272
|
|
|
1226
1273
|
if (options.signal) {
|
|
1274
|
+
abortListener =
|
|
1275
|
+
close;
|
|
1227
1276
|
if (options.signal.aborted) {
|
|
1228
1277
|
close();
|
|
1229
1278
|
return;
|
|
1230
1279
|
}
|
|
1231
1280
|
options.signal.addEventListener(
|
|
1232
1281
|
"abort",
|
|
1233
|
-
|
|
1282
|
+
abortListener,
|
|
1234
1283
|
{
|
|
1235
1284
|
once: true,
|
|
1236
1285
|
}
|
|
1237
1286
|
);
|
|
1238
1287
|
}
|
|
1239
1288
|
},
|
|
1289
|
+
|
|
1290
|
+
cancel() {
|
|
1291
|
+
cleanup();
|
|
1292
|
+
controller = undefined;
|
|
1293
|
+
},
|
|
1240
1294
|
});
|
|
1241
1295
|
|
|
1242
1296
|
const headers =
|
|
@@ -1461,4 +1515,4 @@ function formatError(
|
|
|
1461
1515
|
} catch {
|
|
1462
1516
|
return String(error);
|
|
1463
1517
|
}
|
|
1464
|
-
}
|
|
1518
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import {
|
|
2
|
+
executePageActionFunction,
|
|
3
|
+
normalizePageActionMethod,
|
|
4
|
+
type PageAction,
|
|
5
|
+
type PageActionExecution,
|
|
6
|
+
type PageActionMethod,
|
|
7
|
+
} from "./form-action.js";
|
|
8
|
+
import {
|
|
9
|
+
executePageGuardFunctions,
|
|
10
|
+
type PageGuard,
|
|
11
|
+
type PageGuardData,
|
|
12
|
+
type PageGuardDefinition,
|
|
13
|
+
type PageGuardExecution,
|
|
14
|
+
} from "./page-guard.js";
|
|
15
|
+
import {
|
|
16
|
+
executePageLoaderFunction,
|
|
17
|
+
type PageLoader,
|
|
18
|
+
type PageLoaderExecution,
|
|
19
|
+
} from "./page-loader.js";
|
|
20
|
+
|
|
21
|
+
export interface TestPageContextOptions {
|
|
22
|
+
params?: Record<string, any>;
|
|
23
|
+
url?: string | URL;
|
|
24
|
+
guardData?: PageGuardData;
|
|
25
|
+
hasGuard?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TestPageGuardOptions
|
|
29
|
+
extends Omit<
|
|
30
|
+
TestPageContextOptions,
|
|
31
|
+
"guardData" | "hasGuard"
|
|
32
|
+
> {
|
|
33
|
+
label?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface TestPageActionOptions
|
|
37
|
+
extends TestPageContextOptions {
|
|
38
|
+
actionName?: string;
|
|
39
|
+
method?: PageActionMethod | string;
|
|
40
|
+
formData?: FormData;
|
|
41
|
+
fields?: Record<
|
|
42
|
+
string,
|
|
43
|
+
string | Blob | Array<string | Blob>
|
|
44
|
+
>;
|
|
45
|
+
label?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function runTestPageLoader(
|
|
49
|
+
loader: PageLoader,
|
|
50
|
+
options: TestPageContextOptions = {}
|
|
51
|
+
): Promise<PageLoaderExecution> {
|
|
52
|
+
assertFunction(
|
|
53
|
+
loader,
|
|
54
|
+
"page loader"
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
return executePageLoaderFunction(
|
|
58
|
+
loader,
|
|
59
|
+
{
|
|
60
|
+
...(options.params ?? {}),
|
|
61
|
+
},
|
|
62
|
+
resolveTestPageUrl(
|
|
63
|
+
options.url
|
|
64
|
+
),
|
|
65
|
+
"bcp/testing loader",
|
|
66
|
+
{
|
|
67
|
+
...(options.guardData ?? {}),
|
|
68
|
+
},
|
|
69
|
+
options.hasGuard ??
|
|
70
|
+
Object.keys(
|
|
71
|
+
options.guardData ?? {}
|
|
72
|
+
).length > 0
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function runTestPageGuards(
|
|
77
|
+
guards:
|
|
78
|
+
| PageGuard
|
|
79
|
+
| PageGuard[]
|
|
80
|
+
| PageGuardDefinition[],
|
|
81
|
+
options: TestPageGuardOptions = {}
|
|
82
|
+
): Promise<PageGuardExecution> {
|
|
83
|
+
const list =
|
|
84
|
+
Array.isArray(guards)
|
|
85
|
+
? guards
|
|
86
|
+
: [guards];
|
|
87
|
+
const definitions:
|
|
88
|
+
PageGuardDefinition[] =
|
|
89
|
+
list.map(
|
|
90
|
+
(candidate, index) => {
|
|
91
|
+
if (
|
|
92
|
+
typeof candidate === "object" &&
|
|
93
|
+
candidate !== null &&
|
|
94
|
+
"guard" in candidate
|
|
95
|
+
) {
|
|
96
|
+
const definition =
|
|
97
|
+
candidate as PageGuardDefinition;
|
|
98
|
+
assertFunction(
|
|
99
|
+
definition.guard,
|
|
100
|
+
`page guard ${index + 1}`
|
|
101
|
+
);
|
|
102
|
+
return {
|
|
103
|
+
filePath:
|
|
104
|
+
definition.filePath ||
|
|
105
|
+
`${options.label ?? "bcp/testing guard"}#${index + 1}`,
|
|
106
|
+
guard:
|
|
107
|
+
definition.guard,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
assertFunction(
|
|
112
|
+
candidate,
|
|
113
|
+
`page guard ${index + 1}`
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
filePath:
|
|
118
|
+
`${options.label ?? "bcp/testing guard"}#${index + 1}`,
|
|
119
|
+
guard:
|
|
120
|
+
candidate as PageGuard,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
return executePageGuardFunctions(
|
|
126
|
+
definitions,
|
|
127
|
+
{
|
|
128
|
+
...(options.params ?? {}),
|
|
129
|
+
},
|
|
130
|
+
resolveTestPageUrl(
|
|
131
|
+
options.url
|
|
132
|
+
)
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function runTestPageAction(
|
|
137
|
+
action: PageAction,
|
|
138
|
+
options: TestPageActionOptions = {}
|
|
139
|
+
): Promise<PageActionExecution> {
|
|
140
|
+
assertFunction(
|
|
141
|
+
action,
|
|
142
|
+
"page action"
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
if (
|
|
146
|
+
options.formData &&
|
|
147
|
+
options.fields
|
|
148
|
+
) {
|
|
149
|
+
throw new TypeError(
|
|
150
|
+
"BCP Testing: page action test cannot specify both formData and fields."
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const actionName =
|
|
155
|
+
normalizeText(
|
|
156
|
+
options.actionName ?? "action",
|
|
157
|
+
"page action name"
|
|
158
|
+
);
|
|
159
|
+
const method =
|
|
160
|
+
normalizePageActionMethod(
|
|
161
|
+
options.method ?? "POST"
|
|
162
|
+
);
|
|
163
|
+
const formData =
|
|
164
|
+
options.formData ??
|
|
165
|
+
createTestFormData(
|
|
166
|
+
options.fields ?? {}
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
return executePageActionFunction(
|
|
170
|
+
action,
|
|
171
|
+
actionName,
|
|
172
|
+
method,
|
|
173
|
+
formData,
|
|
174
|
+
{
|
|
175
|
+
...(options.params ?? {}),
|
|
176
|
+
},
|
|
177
|
+
resolveTestPageUrl(
|
|
178
|
+
options.url
|
|
179
|
+
),
|
|
180
|
+
{
|
|
181
|
+
...(options.guardData ?? {}),
|
|
182
|
+
},
|
|
183
|
+
options.hasGuard ??
|
|
184
|
+
Object.keys(
|
|
185
|
+
options.guardData ?? {}
|
|
186
|
+
).length > 0,
|
|
187
|
+
options.label ??
|
|
188
|
+
"bcp/testing actions"
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function createTestFormData(
|
|
193
|
+
fields: Record<
|
|
194
|
+
string,
|
|
195
|
+
string | Blob | Array<string | Blob>
|
|
196
|
+
> = {}
|
|
197
|
+
): FormData {
|
|
198
|
+
const formData =
|
|
199
|
+
new FormData();
|
|
200
|
+
|
|
201
|
+
for (
|
|
202
|
+
const [name, rawValue]
|
|
203
|
+
of Object.entries(fields)
|
|
204
|
+
) {
|
|
205
|
+
const values =
|
|
206
|
+
Array.isArray(rawValue)
|
|
207
|
+
? rawValue
|
|
208
|
+
: [rawValue];
|
|
209
|
+
|
|
210
|
+
for (const value of values) {
|
|
211
|
+
formData.append(
|
|
212
|
+
name,
|
|
213
|
+
value
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return formData;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function resolveTestPageUrl(
|
|
222
|
+
value?: string | URL
|
|
223
|
+
): URL {
|
|
224
|
+
if (value instanceof URL) {
|
|
225
|
+
return new URL(
|
|
226
|
+
value.href
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const candidate =
|
|
231
|
+
value ??
|
|
232
|
+
"http://bcp.test/";
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
return new URL(
|
|
236
|
+
candidate
|
|
237
|
+
);
|
|
238
|
+
} catch {
|
|
239
|
+
return new URL(
|
|
240
|
+
String(candidate).replace(
|
|
241
|
+
/^\//,
|
|
242
|
+
""
|
|
243
|
+
),
|
|
244
|
+
"http://bcp.test/"
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function assertFunction(
|
|
250
|
+
value: unknown,
|
|
251
|
+
label: string
|
|
252
|
+
): asserts value is (...args: any[]) => unknown {
|
|
253
|
+
if (typeof value !== "function") {
|
|
254
|
+
throw new TypeError(
|
|
255
|
+
`BCP Testing: ${label} must be a function.`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function normalizeText(
|
|
261
|
+
value: unknown,
|
|
262
|
+
label: string
|
|
263
|
+
): string {
|
|
264
|
+
const text =
|
|
265
|
+
String(value ?? "").trim();
|
|
266
|
+
|
|
267
|
+
if (!text) {
|
|
268
|
+
throw new TypeError(
|
|
269
|
+
`BCP Testing: ${label} must be a non-empty string.`
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return text;
|
|
274
|
+
}
|