@faapi/faapi 6.32.0 → 6.33.0
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/dist/cli/index.js +92 -87
- package/dist/cli/index.js.map +1 -1
- package/dist/{contextTypes-WKsf3cCE.d.ts → contextTypes-D1ORxB1P.d.ts} +87 -38
- package/dist/index.d.ts +2 -2
- package/dist/index.js +201 -197
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +27 -36
- package/dist/testing.js.map +1 -1
- package/dist/workerEntry.d.ts +7 -7
- package/dist/workerEntry.js +19 -17
- package/dist/workerEntry.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2126,19 +2126,6 @@ var TaskCancelledError = class extends Error {
|
|
|
2126
2126
|
this.name = "TaskCancelledError";
|
|
2127
2127
|
}
|
|
2128
2128
|
};
|
|
2129
|
-
function safeConfig(config) {
|
|
2130
|
-
if (config === void 0 || config === null) return config;
|
|
2131
|
-
try {
|
|
2132
|
-
structuredClone(config);
|
|
2133
|
-
return config;
|
|
2134
|
-
} catch {
|
|
2135
|
-
}
|
|
2136
|
-
try {
|
|
2137
|
-
return JSON.parse(JSON.stringify(config));
|
|
2138
|
-
} catch {
|
|
2139
|
-
return void 0;
|
|
2140
|
-
}
|
|
2141
|
-
}
|
|
2142
2129
|
function reviveError(payload) {
|
|
2143
2130
|
const err = new Error(payload?.message ?? "task worker error");
|
|
2144
2131
|
if (payload?.name !== void 0) err.name = payload.name;
|
|
@@ -2253,7 +2240,6 @@ async function runTaskInWorker(options) {
|
|
|
2253
2240
|
type: "run",
|
|
2254
2241
|
payload,
|
|
2255
2242
|
taskCtx: {
|
|
2256
|
-
config: safeConfig(taskCtx.config),
|
|
2257
2243
|
job: taskCtx.job
|
|
2258
2244
|
},
|
|
2259
2245
|
registries: options.registries,
|
|
@@ -2262,14 +2248,122 @@ async function runTaskInWorker(options) {
|
|
|
2262
2248
|
});
|
|
2263
2249
|
} catch (err) {
|
|
2264
2250
|
finish(
|
|
2265
|
-
() => reject(
|
|
2266
|
-
new Error(`Task "${taskCtx.job.name}" payload/config is not cloneable: ${String(err)}`)
|
|
2267
|
-
)
|
|
2251
|
+
() => reject(new Error(`Task "${taskCtx.job.name}" payload is not cloneable: ${String(err)}`))
|
|
2268
2252
|
);
|
|
2269
2253
|
}
|
|
2270
2254
|
});
|
|
2271
2255
|
}
|
|
2272
2256
|
|
|
2257
|
+
// src/utils/isPlainObject.ts
|
|
2258
|
+
function isPlainObject(value) {
|
|
2259
|
+
if (value === null || typeof value !== "object") {
|
|
2260
|
+
return false;
|
|
2261
|
+
}
|
|
2262
|
+
if (Array.isArray(value)) {
|
|
2263
|
+
return false;
|
|
2264
|
+
}
|
|
2265
|
+
const proto = Object.getPrototypeOf(value);
|
|
2266
|
+
return proto === null || proto === Object.prototype;
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
// src/utils/stringifyJson.ts
|
|
2270
|
+
function stringifyJson(value) {
|
|
2271
|
+
if (!needsConversion(value, /* @__PURE__ */ new Set())) {
|
|
2272
|
+
return JSON.stringify(value);
|
|
2273
|
+
}
|
|
2274
|
+
return JSON.stringify(convertValue(value, /* @__PURE__ */ new Set()));
|
|
2275
|
+
}
|
|
2276
|
+
function needsConversion(value, ancestors) {
|
|
2277
|
+
if (value === null) return false;
|
|
2278
|
+
const type = typeof value;
|
|
2279
|
+
if (type === "bigint") return true;
|
|
2280
|
+
if (type === "number") return !Number.isFinite(value);
|
|
2281
|
+
if (type !== "object") return false;
|
|
2282
|
+
const obj = value;
|
|
2283
|
+
if (ancestors.has(obj)) {
|
|
2284
|
+
throw new TypeError("[faapi] Converting circular structure to JSON");
|
|
2285
|
+
}
|
|
2286
|
+
if (obj instanceof Date || obj instanceof Map || obj instanceof Set || obj instanceof RegExp) {
|
|
2287
|
+
return true;
|
|
2288
|
+
}
|
|
2289
|
+
if (typeof obj.toJSON === "function") return true;
|
|
2290
|
+
if (Array.isArray(obj)) {
|
|
2291
|
+
ancestors.add(obj);
|
|
2292
|
+
for (let i = 0; i < obj.length; i++) {
|
|
2293
|
+
if (needsConversion(obj[i], ancestors)) {
|
|
2294
|
+
ancestors.delete(obj);
|
|
2295
|
+
return true;
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
ancestors.delete(obj);
|
|
2299
|
+
return false;
|
|
2300
|
+
}
|
|
2301
|
+
if (isPlainObject(obj)) {
|
|
2302
|
+
ancestors.add(obj);
|
|
2303
|
+
for (const key in obj) {
|
|
2304
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
|
|
2305
|
+
if (needsConversion(obj[key], ancestors)) {
|
|
2306
|
+
ancestors.delete(obj);
|
|
2307
|
+
return true;
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
ancestors.delete(obj);
|
|
2311
|
+
return false;
|
|
2312
|
+
}
|
|
2313
|
+
return false;
|
|
2314
|
+
}
|
|
2315
|
+
function convertValue(value, ancestors) {
|
|
2316
|
+
if (value === null) return value;
|
|
2317
|
+
const type = typeof value;
|
|
2318
|
+
if (type === "bigint") return value.toString();
|
|
2319
|
+
if (type === "number") return Number.isFinite(value) ? value : String(value);
|
|
2320
|
+
if (type !== "object") return value;
|
|
2321
|
+
const obj = value;
|
|
2322
|
+
if (ancestors.has(obj)) {
|
|
2323
|
+
throw new TypeError("[faapi] Converting circular structure to JSON");
|
|
2324
|
+
}
|
|
2325
|
+
if (obj instanceof Date) return obj.getTime();
|
|
2326
|
+
if (obj instanceof Map) {
|
|
2327
|
+
ancestors.add(obj);
|
|
2328
|
+
const entries = Array.from(obj, ([k, v]) => [
|
|
2329
|
+
convertValue(k, ancestors),
|
|
2330
|
+
convertValue(v, ancestors)
|
|
2331
|
+
]);
|
|
2332
|
+
ancestors.delete(obj);
|
|
2333
|
+
return entries;
|
|
2334
|
+
}
|
|
2335
|
+
if (obj instanceof Set) {
|
|
2336
|
+
ancestors.add(obj);
|
|
2337
|
+
const values = Array.from(obj, (v) => convertValue(v, ancestors));
|
|
2338
|
+
ancestors.delete(obj);
|
|
2339
|
+
return values;
|
|
2340
|
+
}
|
|
2341
|
+
if (obj instanceof RegExp) return obj.toString();
|
|
2342
|
+
const toJSON = obj.toJSON;
|
|
2343
|
+
if (typeof toJSON === "function") {
|
|
2344
|
+
ancestors.add(obj);
|
|
2345
|
+
const result = convertValue(toJSON.call(obj), ancestors);
|
|
2346
|
+
ancestors.delete(obj);
|
|
2347
|
+
return result;
|
|
2348
|
+
}
|
|
2349
|
+
if (Array.isArray(obj)) {
|
|
2350
|
+
ancestors.add(obj);
|
|
2351
|
+
const arr = obj.map((item) => convertValue(item, ancestors));
|
|
2352
|
+
ancestors.delete(obj);
|
|
2353
|
+
return arr;
|
|
2354
|
+
}
|
|
2355
|
+
if (isPlainObject(obj)) {
|
|
2356
|
+
ancestors.add(obj);
|
|
2357
|
+
const out = {};
|
|
2358
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
2359
|
+
out[key] = convertValue(val, ancestors);
|
|
2360
|
+
}
|
|
2361
|
+
ancestors.delete(obj);
|
|
2362
|
+
return out;
|
|
2363
|
+
}
|
|
2364
|
+
return obj;
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2273
2367
|
// src/logger/formatEntry.ts
|
|
2274
2368
|
function serializeFieldValue(value) {
|
|
2275
2369
|
if (value instanceof Error) {
|
|
@@ -2285,11 +2379,7 @@ function formatEntry(entry) {
|
|
|
2285
2379
|
const plain = Object.fromEntries(
|
|
2286
2380
|
Object.entries(entry.fields).map(([k, v]) => [k, serializeFieldValue(v)])
|
|
2287
2381
|
);
|
|
2288
|
-
|
|
2289
|
-
line += ` ${JSON.stringify(plain)}`;
|
|
2290
|
-
} catch (err) {
|
|
2291
|
-
line += ` [unserializable fields: ${err instanceof Error ? err.message : String(err)}]`;
|
|
2292
|
-
}
|
|
2382
|
+
line += ` ${stringifyJson(plain)}`;
|
|
2293
2383
|
}
|
|
2294
2384
|
return line;
|
|
2295
2385
|
}
|
|
@@ -2307,7 +2397,14 @@ function createFileLogSink(options) {
|
|
|
2307
2397
|
};
|
|
2308
2398
|
const openStream = (name) => {
|
|
2309
2399
|
const stream = fs2.createWriteStream(path3.join(options.dir, `${name}.log`), { flags: "a" });
|
|
2310
|
-
|
|
2400
|
+
let streamErrorLogged = false;
|
|
2401
|
+
stream.on("error", (err) => {
|
|
2402
|
+
if (streamErrorLogged) return;
|
|
2403
|
+
streamErrorLogged = true;
|
|
2404
|
+
console.error(
|
|
2405
|
+
`[faapi] log file stream "${name}.log" failed \u2014 file logging on this stream stopped (subsequent errors suppressed). Check disk space / permissions for ${options.dir}:`,
|
|
2406
|
+
err
|
|
2407
|
+
);
|
|
2311
2408
|
});
|
|
2312
2409
|
streams.set(name, stream);
|
|
2313
2410
|
};
|
|
@@ -2539,13 +2636,14 @@ function createTaskQueue(deps) {
|
|
|
2539
2636
|
const loadTaskModule = deps.loadTaskModule ?? (async (filePath) => await import(filePath));
|
|
2540
2637
|
const loadPayloadSchema = deps.loadPayloadSchema ?? (async (filePath) => {
|
|
2541
2638
|
const zodPath = path4.join(path4.dirname(filePath), "zod.js");
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2639
|
+
const mod = await import(zodPath);
|
|
2640
|
+
const schemaKey = Object.keys(mod).find((k) => k.endsWith("Schema"));
|
|
2641
|
+
if (!schemaKey) {
|
|
2642
|
+
throw new Error(
|
|
2643
|
+
`[faapi] Task payload schema artifact has no *Schema export: ${zodPath} \u2014 re-run "faapi dev" / "faapi build" to regenerate task artifacts.`
|
|
2644
|
+
);
|
|
2548
2645
|
}
|
|
2646
|
+
return mod[schemaKey];
|
|
2549
2647
|
});
|
|
2550
2648
|
function assertKnownTask(name) {
|
|
2551
2649
|
const meta = registry.get(name);
|
|
@@ -2564,14 +2662,13 @@ function createTaskQueue(deps) {
|
|
|
2564
2662
|
async function validatePayload(name, payload) {
|
|
2565
2663
|
if (!schemaCache.has(name)) {
|
|
2566
2664
|
const meta = registry.get(name);
|
|
2567
|
-
schemaCache.set(
|
|
2568
|
-
name,
|
|
2569
|
-
await loadPayloadSchema(path4.resolve(rootDir, meta.filePath)).catch(() => void 0)
|
|
2570
|
-
);
|
|
2665
|
+
schemaCache.set(name, await loadPayloadSchema(path4.resolve(rootDir, meta.filePath)));
|
|
2571
2666
|
}
|
|
2572
2667
|
const schema = schemaCache.get(name);
|
|
2573
2668
|
if (!schema || typeof schema.safeParse !== "function") {
|
|
2574
|
-
|
|
2669
|
+
throw new Error(
|
|
2670
|
+
`[faapi] Task "${name}" has no valid payload schema (zod.js missing or no *Schema export). Payload declarations are mandatory: re-run "faapi dev" / "faapi build" to regenerate task artifacts.`
|
|
2671
|
+
);
|
|
2575
2672
|
}
|
|
2576
2673
|
const result = schema.safeParse(payload);
|
|
2577
2674
|
if (!result.success) {
|
|
@@ -2605,7 +2702,8 @@ function createTaskQueue(deps) {
|
|
|
2605
2702
|
taskModulePath: path4.resolve(rootDir, meta.filePath),
|
|
2606
2703
|
payload: job.payload,
|
|
2607
2704
|
taskCtx: {
|
|
2608
|
-
config
|
|
2705
|
+
// 隔离任务不传 config:worker 线程不接收进程配置(config 含函数字段
|
|
2706
|
+
// 不可结构化克隆,框架不做降级传递)——任务数据经 payload 显式传入
|
|
2609
2707
|
resourcesDir: deps.resourcesDir,
|
|
2610
2708
|
job: { id: job.id, name: job.name, attempt: job.attempt }
|
|
2611
2709
|
},
|
|
@@ -2672,7 +2770,8 @@ function createTaskQueue(deps) {
|
|
|
2672
2770
|
cancelled,
|
|
2673
2771
|
error: record.error
|
|
2674
2772
|
})
|
|
2675
|
-
).catch(() => {
|
|
2773
|
+
).catch((hookErr) => {
|
|
2774
|
+
console.error(`[faapi] task onFailed hook threw for "${job.name}":`, hookErr);
|
|
2676
2775
|
});
|
|
2677
2776
|
}
|
|
2678
2777
|
throw err;
|
|
@@ -2895,25 +2994,27 @@ function detectIgnoredMetaFields(source, strictHit) {
|
|
|
2895
2994
|
for (const line of source.split("\n")) {
|
|
2896
2995
|
const trimmed = line.trim();
|
|
2897
2996
|
if (trimmed.startsWith("//")) continue;
|
|
2997
|
+
if (trimmed.startsWith("*") || trimmed.startsWith("/*") || trimmed.endsWith("*/")) continue;
|
|
2898
2998
|
for (const field of ALL_META_FIELDS) {
|
|
2899
2999
|
if (new RegExp(`\\b${field}\\s*:`).test(trimmed)) declared.add(field);
|
|
2900
3000
|
}
|
|
2901
3001
|
}
|
|
2902
3002
|
return ALL_META_FIELDS.filter((field) => declared.has(field) && !strictHit[field]);
|
|
2903
3003
|
}
|
|
2904
|
-
function
|
|
2905
|
-
|
|
3004
|
+
function throwIgnoredMetaFields(name, fields) {
|
|
3005
|
+
if (fields.length === 0) return;
|
|
3006
|
+
const details = fields.map((field) => {
|
|
2906
3007
|
if (STRING_META_FIELDS.includes(field)) {
|
|
2907
|
-
|
|
2908
|
-
`[faapi] Task "${name}" declares "${field}" but its value is not a string literal \u2014 the declaration was ignored and the task will not be cron-scheduled. Meta values are extracted from source code without importing it, so only literal values are recognized: use a quoted literal (cron: '0 3 * * *'), not an expression or a dynamic value.`
|
|
2909
|
-
);
|
|
2910
|
-
} else {
|
|
2911
|
-
const consequence = field === "timeoutMs" ? " and the task will run in-process with no timeout" : "";
|
|
2912
|
-
console.warn(
|
|
2913
|
-
`[faapi] Task "${name}" declares "${field}" but its value is not a numeric literal \u2014 the declaration was ignored${consequence}. Meta values are extracted from source code without importing it, so only literal values are recognized (underscores allowed, e.g. 1_800_000); expressions like 30 * 60_000 or dynamic values are not supported.`
|
|
2914
|
-
);
|
|
3008
|
+
return `"${field}": value is not a string literal \u2014 the declaration would be ignored and the task would not be cron-scheduled. Use a quoted literal (cron: '0 3 * * *'), not an expression or a dynamic value.`;
|
|
2915
3009
|
}
|
|
2916
|
-
|
|
3010
|
+
const consequence = field === "timeoutMs" ? " \u2014 the declaration would be ignored and the task would silently run in-process with no timeout" : " \u2014 the declaration would be ignored";
|
|
3011
|
+
return `"${field}": value is not a numeric literal${consequence}. Only literal values are recognized (underscores allowed, e.g. 1_800_000); expressions like 30 * 60_000 or dynamic values are not supported.`;
|
|
3012
|
+
});
|
|
3013
|
+
throw new Error(
|
|
3014
|
+
`[faapi] Task "${name}" declares meta field(s) with non-literal values:
|
|
3015
|
+
${details.join("\n ")}
|
|
3016
|
+
Meta values are extracted from source code without importing it, so only literals are recognized. If a matched line is inside a block comment (e.g. a single-line /* ... */ comment containing a meta-like declaration), rewrite the comment so it does not look like a meta declaration.`
|
|
3017
|
+
);
|
|
2917
3018
|
}
|
|
2918
3019
|
function parseNumericLiteral(raw) {
|
|
2919
3020
|
return Number(raw.replace(/_/g, ""));
|
|
@@ -2983,7 +3084,7 @@ async function scanTasks(rootDir, patterns) {
|
|
|
2983
3084
|
timeoutMs: timeoutMs !== void 0,
|
|
2984
3085
|
graceMs: graceMs !== void 0
|
|
2985
3086
|
};
|
|
2986
|
-
|
|
3087
|
+
throwIgnoredMetaFields(name, detectIgnoredMetaFields(source, strictHit));
|
|
2987
3088
|
tasks.push(manifest);
|
|
2988
3089
|
}
|
|
2989
3090
|
return tasks.sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -3583,11 +3684,7 @@ async function ensureMiddlewaresCompiled(middlewarePaths, rootDir) {
|
|
|
3583
3684
|
if (!dist) return;
|
|
3584
3685
|
for (const mwPath of middlewarePaths) {
|
|
3585
3686
|
const sourcePath = prodPathToSourcePath(mwPath, rootDir, dist);
|
|
3586
|
-
|
|
3587
|
-
await ensureCompiled(sourcePath, rootDir, dist);
|
|
3588
|
-
} catch (err) {
|
|
3589
|
-
console.error(`[faapi] Failed to compile middleware source ${sourcePath}:`, err);
|
|
3590
|
-
}
|
|
3687
|
+
await ensureCompiled(sourcePath, rootDir, dist);
|
|
3591
3688
|
}
|
|
3592
3689
|
}
|
|
3593
3690
|
function prodSourcePathToProductPath(sourceAbsPath, rootDir, dist) {
|
|
@@ -4591,116 +4688,6 @@ function hasBufferedBody(response) {
|
|
|
4591
4688
|
return bufferedBodyResponses.has(response);
|
|
4592
4689
|
}
|
|
4593
4690
|
|
|
4594
|
-
// src/utils/isPlainObject.ts
|
|
4595
|
-
function isPlainObject(value) {
|
|
4596
|
-
if (value === null || typeof value !== "object") {
|
|
4597
|
-
return false;
|
|
4598
|
-
}
|
|
4599
|
-
if (Array.isArray(value)) {
|
|
4600
|
-
return false;
|
|
4601
|
-
}
|
|
4602
|
-
const proto = Object.getPrototypeOf(value);
|
|
4603
|
-
return proto === null || proto === Object.prototype;
|
|
4604
|
-
}
|
|
4605
|
-
|
|
4606
|
-
// src/utils/stringifyJson.ts
|
|
4607
|
-
function stringifyJson(value) {
|
|
4608
|
-
if (!needsConversion(value, /* @__PURE__ */ new Set())) {
|
|
4609
|
-
return JSON.stringify(value);
|
|
4610
|
-
}
|
|
4611
|
-
return JSON.stringify(convertValue(value, /* @__PURE__ */ new Set()));
|
|
4612
|
-
}
|
|
4613
|
-
function needsConversion(value, ancestors) {
|
|
4614
|
-
if (value === null) return false;
|
|
4615
|
-
const type = typeof value;
|
|
4616
|
-
if (type === "bigint") return true;
|
|
4617
|
-
if (type === "number") return !Number.isFinite(value);
|
|
4618
|
-
if (type !== "object") return false;
|
|
4619
|
-
const obj = value;
|
|
4620
|
-
if (ancestors.has(obj)) {
|
|
4621
|
-
throw new TypeError("[faapi] Converting circular structure to JSON");
|
|
4622
|
-
}
|
|
4623
|
-
if (obj instanceof Date || obj instanceof Map || obj instanceof Set || obj instanceof RegExp) {
|
|
4624
|
-
return true;
|
|
4625
|
-
}
|
|
4626
|
-
if (typeof obj.toJSON === "function") return true;
|
|
4627
|
-
if (Array.isArray(obj)) {
|
|
4628
|
-
ancestors.add(obj);
|
|
4629
|
-
for (let i = 0; i < obj.length; i++) {
|
|
4630
|
-
if (needsConversion(obj[i], ancestors)) {
|
|
4631
|
-
ancestors.delete(obj);
|
|
4632
|
-
return true;
|
|
4633
|
-
}
|
|
4634
|
-
}
|
|
4635
|
-
ancestors.delete(obj);
|
|
4636
|
-
return false;
|
|
4637
|
-
}
|
|
4638
|
-
if (isPlainObject(obj)) {
|
|
4639
|
-
ancestors.add(obj);
|
|
4640
|
-
for (const key in obj) {
|
|
4641
|
-
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
|
|
4642
|
-
if (needsConversion(obj[key], ancestors)) {
|
|
4643
|
-
ancestors.delete(obj);
|
|
4644
|
-
return true;
|
|
4645
|
-
}
|
|
4646
|
-
}
|
|
4647
|
-
ancestors.delete(obj);
|
|
4648
|
-
return false;
|
|
4649
|
-
}
|
|
4650
|
-
return false;
|
|
4651
|
-
}
|
|
4652
|
-
function convertValue(value, ancestors) {
|
|
4653
|
-
if (value === null) return value;
|
|
4654
|
-
const type = typeof value;
|
|
4655
|
-
if (type === "bigint") return value.toString();
|
|
4656
|
-
if (type === "number") return Number.isFinite(value) ? value : String(value);
|
|
4657
|
-
if (type !== "object") return value;
|
|
4658
|
-
const obj = value;
|
|
4659
|
-
if (ancestors.has(obj)) {
|
|
4660
|
-
throw new TypeError("[faapi] Converting circular structure to JSON");
|
|
4661
|
-
}
|
|
4662
|
-
if (obj instanceof Date) return obj.getTime();
|
|
4663
|
-
if (obj instanceof Map) {
|
|
4664
|
-
ancestors.add(obj);
|
|
4665
|
-
const entries = Array.from(obj, ([k, v]) => [
|
|
4666
|
-
convertValue(k, ancestors),
|
|
4667
|
-
convertValue(v, ancestors)
|
|
4668
|
-
]);
|
|
4669
|
-
ancestors.delete(obj);
|
|
4670
|
-
return entries;
|
|
4671
|
-
}
|
|
4672
|
-
if (obj instanceof Set) {
|
|
4673
|
-
ancestors.add(obj);
|
|
4674
|
-
const values = Array.from(obj, (v) => convertValue(v, ancestors));
|
|
4675
|
-
ancestors.delete(obj);
|
|
4676
|
-
return values;
|
|
4677
|
-
}
|
|
4678
|
-
if (obj instanceof RegExp) return obj.toString();
|
|
4679
|
-
const toJSON = obj.toJSON;
|
|
4680
|
-
if (typeof toJSON === "function") {
|
|
4681
|
-
ancestors.add(obj);
|
|
4682
|
-
const result = convertValue(toJSON.call(obj), ancestors);
|
|
4683
|
-
ancestors.delete(obj);
|
|
4684
|
-
return result;
|
|
4685
|
-
}
|
|
4686
|
-
if (Array.isArray(obj)) {
|
|
4687
|
-
ancestors.add(obj);
|
|
4688
|
-
const arr = obj.map((item) => convertValue(item, ancestors));
|
|
4689
|
-
ancestors.delete(obj);
|
|
4690
|
-
return arr;
|
|
4691
|
-
}
|
|
4692
|
-
if (isPlainObject(obj)) {
|
|
4693
|
-
ancestors.add(obj);
|
|
4694
|
-
const out = {};
|
|
4695
|
-
for (const [key, val] of Object.entries(obj)) {
|
|
4696
|
-
out[key] = convertValue(val, ancestors);
|
|
4697
|
-
}
|
|
4698
|
-
ancestors.delete(obj);
|
|
4699
|
-
return out;
|
|
4700
|
-
}
|
|
4701
|
-
return obj;
|
|
4702
|
-
}
|
|
4703
|
-
|
|
4704
4691
|
// src/runtime/sse.ts
|
|
4705
4692
|
function encodeSseEvent(event) {
|
|
4706
4693
|
let out = "";
|
|
@@ -5898,38 +5885,30 @@ function setCachedMiddlewares(absPath, bundle) {
|
|
|
5898
5885
|
middlewareCache.set(absPath, bundle);
|
|
5899
5886
|
}
|
|
5900
5887
|
async function loadMiddlewaresFile(filePath) {
|
|
5901
|
-
|
|
5902
|
-
try {
|
|
5903
|
-
module = await importWithCacheBust(filePath);
|
|
5904
|
-
} catch (err) {
|
|
5905
|
-
console.error(
|
|
5906
|
-
`[faapi] Failed to load middlewares from ${filePath}:`,
|
|
5907
|
-
err instanceof Error ? err.stack ?? err.message : err
|
|
5908
|
-
);
|
|
5909
|
-
return { middlewares: [], injectors: {} };
|
|
5910
|
-
}
|
|
5888
|
+
const module = await importWithCacheBust(filePath);
|
|
5911
5889
|
const middlewares = module.default ?? module.middlewares ?? [];
|
|
5912
5890
|
if (!Array.isArray(middlewares)) {
|
|
5913
|
-
|
|
5914
|
-
|
|
5891
|
+
throw new TypeError(
|
|
5892
|
+
`[faapi] middlewares.ts must export an array (default or named "middlewares"): ${filePath}`
|
|
5893
|
+
);
|
|
5915
5894
|
}
|
|
5916
|
-
const validMiddlewares =
|
|
5895
|
+
const validMiddlewares = [];
|
|
5896
|
+
for (const m of middlewares) {
|
|
5917
5897
|
if (typeof m !== "function") {
|
|
5918
|
-
|
|
5919
|
-
|
|
5898
|
+
throw new TypeError(
|
|
5899
|
+
`[faapi] every middleware must be a function, got ${typeof m}: ${filePath}`
|
|
5900
|
+
);
|
|
5920
5901
|
}
|
|
5921
|
-
|
|
5922
|
-
}
|
|
5902
|
+
validMiddlewares.push(m);
|
|
5903
|
+
}
|
|
5923
5904
|
const injectors = module.injectors ?? {};
|
|
5924
5905
|
if (typeof injectors !== "object" || injectors === null) {
|
|
5925
|
-
|
|
5926
|
-
return { middlewares: validMiddlewares, injectors: {} };
|
|
5906
|
+
throw new TypeError(`[faapi] "injectors" export must be an object: ${filePath}`);
|
|
5927
5907
|
}
|
|
5928
5908
|
const validInjectors = {};
|
|
5929
5909
|
for (const [name, injector] of Object.entries(injectors)) {
|
|
5930
5910
|
if (typeof injector !== "function") {
|
|
5931
|
-
|
|
5932
|
-
continue;
|
|
5911
|
+
throw new TypeError(`[faapi] injector "${name}" must be a function: ${filePath}`);
|
|
5933
5912
|
}
|
|
5934
5913
|
validInjectors[name] = injector;
|
|
5935
5914
|
}
|
|
@@ -5944,10 +5923,16 @@ async function loadMergedMiddlewares(middlewarePaths) {
|
|
|
5944
5923
|
if (bundle === void 0) {
|
|
5945
5924
|
let loading = inFlight.get(absMwPath);
|
|
5946
5925
|
if (!loading) {
|
|
5947
|
-
loading = loadMiddlewaresFile(absMwPath).then(
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5926
|
+
loading = loadMiddlewaresFile(absMwPath).then(
|
|
5927
|
+
(result) => {
|
|
5928
|
+
setCachedMiddlewares(absMwPath, result);
|
|
5929
|
+
return result;
|
|
5930
|
+
},
|
|
5931
|
+
(err) => {
|
|
5932
|
+
inFlight.delete(absMwPath);
|
|
5933
|
+
throw err;
|
|
5934
|
+
}
|
|
5935
|
+
);
|
|
5951
5936
|
inFlight.set(absMwPath, loading);
|
|
5952
5937
|
}
|
|
5953
5938
|
bundle = await loading;
|
|
@@ -6542,7 +6527,8 @@ async function sendErrorResponse(err, meta, res, onError, ctx) {
|
|
|
6542
6527
|
if (onError && ctx) {
|
|
6543
6528
|
try {
|
|
6544
6529
|
await onError(err, ctx);
|
|
6545
|
-
} catch {
|
|
6530
|
+
} catch (hookErr) {
|
|
6531
|
+
console.error("[faapi] lifecycle onError hook threw:", hookErr);
|
|
6546
6532
|
}
|
|
6547
6533
|
}
|
|
6548
6534
|
}
|
|
@@ -6735,7 +6721,7 @@ async function loadPlugins(declarations, ctx, rootDir, dist) {
|
|
|
6735
6721
|
const upgradeWrappers = [];
|
|
6736
6722
|
const failures = [];
|
|
6737
6723
|
if (!declarations || declarations.length === 0) {
|
|
6738
|
-
return { handlerWrappers, upgradeWrappers
|
|
6724
|
+
return { handlerWrappers, upgradeWrappers };
|
|
6739
6725
|
}
|
|
6740
6726
|
const fullCtx = {
|
|
6741
6727
|
...ctx,
|
|
@@ -6762,7 +6748,7 @@ async function loadPlugins(declarations, ctx, rootDir, dist) {
|
|
|
6762
6748
|
}
|
|
6763
6749
|
if (enable === false) continue;
|
|
6764
6750
|
if (loaded.has(specifier)) {
|
|
6765
|
-
|
|
6751
|
+
failures.push({ specifier, reason: "duplicate plugin declaration" });
|
|
6766
6752
|
continue;
|
|
6767
6753
|
}
|
|
6768
6754
|
loaded.add(specifier);
|
|
@@ -6783,12 +6769,12 @@ async function loadPlugins(declarations, ctx, rootDir, dist) {
|
|
|
6783
6769
|
}
|
|
6784
6770
|
}
|
|
6785
6771
|
if (failures.length > 0) {
|
|
6786
|
-
|
|
6772
|
+
throw new Error(
|
|
6787
6773
|
`[faapi] ${failures.length} plugin(s) failed to load:
|
|
6788
6774
|
` + failures.map((f) => ` - ${f.specifier}: ${f.reason}`).join("\n")
|
|
6789
6775
|
);
|
|
6790
6776
|
}
|
|
6791
|
-
return { handlerWrappers, upgradeWrappers
|
|
6777
|
+
return { handlerWrappers, upgradeWrappers };
|
|
6792
6778
|
}
|
|
6793
6779
|
var TS_EXTS = /\.(ts|tsx|mts|cts)$/;
|
|
6794
6780
|
var JS_EXTS = /\.(js|mjs|cjs|jsx)$/;
|
|
@@ -7389,6 +7375,7 @@ function generateTaskSchemaFileSource(sources, resolveType, helpersImportPath) {
|
|
|
7389
7375
|
}
|
|
7390
7376
|
async function generateTaskArtifacts(manifests, rootDir, dist) {
|
|
7391
7377
|
const metadata = [];
|
|
7378
|
+
const declarationErrors = [];
|
|
7392
7379
|
if (manifests.length > 0) {
|
|
7393
7380
|
const programByFile = createPrograms(manifests.map((m) => path25.resolve(rootDir, m.filePath)));
|
|
7394
7381
|
for (const manifest of manifests) {
|
|
@@ -7397,11 +7384,28 @@ async function generateTaskArtifacts(manifests, rootDir, dist) {
|
|
|
7397
7384
|
name: manifest.name,
|
|
7398
7385
|
filePath: manifest.filePath
|
|
7399
7386
|
});
|
|
7400
|
-
if (result) {
|
|
7401
|
-
|
|
7387
|
+
if (!result) {
|
|
7388
|
+
declarationErrors.push(
|
|
7389
|
+
`"${manifest.name}" (${manifest.filePath}): no exported run function found`
|
|
7390
|
+
);
|
|
7391
|
+
continue;
|
|
7402
7392
|
}
|
|
7393
|
+
if (!result.inputTypeName) {
|
|
7394
|
+
declarationErrors.push(
|
|
7395
|
+
`"${manifest.name}" (${manifest.filePath}): run's first parameter has no type name \u2014 declare a Payload type (an empty interface for parameterless tasks), or explicitly opt out of validation with "type Payload = unknown"`
|
|
7396
|
+
);
|
|
7397
|
+
continue;
|
|
7398
|
+
}
|
|
7399
|
+
metadata.push({ ...result, taskName: manifest.name });
|
|
7403
7400
|
}
|
|
7404
7401
|
}
|
|
7402
|
+
if (declarationErrors.length > 0) {
|
|
7403
|
+
throw new Error(
|
|
7404
|
+
`[faapi] ${declarationErrors.length} task(s) missing a Payload type declaration \u2014 payload validation must not be silently skipped:
|
|
7405
|
+
${declarationErrors.join("\n ")}
|
|
7406
|
+
Every task must declare a payload type on run's first parameter; "type Payload = unknown" generates an always-passing schema (explicit no-validation opt-out).`
|
|
7407
|
+
);
|
|
7408
|
+
}
|
|
7405
7409
|
const serialized = serializeTasks(manifests, dist);
|
|
7406
7410
|
const tasksPath = path25.resolve(rootDir, dist, TASKS_FILE);
|
|
7407
7411
|
await writeTasksModule(serialized, tasksPath);
|
|
@@ -7679,7 +7683,7 @@ async function createAppBase(options) {
|
|
|
7679
7683
|
const taskQueue = createTaskQueue({
|
|
7680
7684
|
registry: registries.task,
|
|
7681
7685
|
rootDir,
|
|
7682
|
-
config,
|
|
7686
|
+
config: config ?? {},
|
|
7683
7687
|
resourcesDir,
|
|
7684
7688
|
registries: createTaskRegistriesView(registries),
|
|
7685
7689
|
// 轻量补全通道:进程内路径经 store 惰性读取(插件注册后的活引用);
|
|
@@ -7710,7 +7714,7 @@ async function createAppBase(options) {
|
|
|
7710
7714
|
dist,
|
|
7711
7715
|
cors: config?.cors ?? true,
|
|
7712
7716
|
onError: config?.lifecycle?.onError,
|
|
7713
|
-
config
|
|
7717
|
+
config,
|
|
7714
7718
|
wsRoutes,
|
|
7715
7719
|
middlewares: config?.middlewares,
|
|
7716
7720
|
injectors: config?.injectors,
|