@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 CHANGED
@@ -767,38 +767,30 @@ function setCachedMiddlewares(absPath, bundle) {
767
767
  middlewareCache.set(absPath, bundle);
768
768
  }
769
769
  async function loadMiddlewaresFile(filePath) {
770
- let module;
771
- try {
772
- module = await importWithCacheBust(filePath);
773
- } catch (err) {
774
- console.error(
775
- `[faapi] Failed to load middlewares from ${filePath}:`,
776
- err instanceof Error ? err.stack ?? err.message : err
777
- );
778
- return { middlewares: [], injectors: {} };
779
- }
770
+ const module = await importWithCacheBust(filePath);
780
771
  const middlewares = module.default ?? module.middlewares ?? [];
781
772
  if (!Array.isArray(middlewares)) {
782
- console.warn(`[faapi] middlewares.ts \u5E94\u5BFC\u51FA\u6570\u7EC4\uFF0C\u5DF2\u5FFD\u7565: ${filePath}`);
783
- return { middlewares: [], injectors: {} };
773
+ throw new TypeError(
774
+ `[faapi] middlewares.ts must export an array (default or named "middlewares"): ${filePath}`
775
+ );
784
776
  }
785
- const validMiddlewares = middlewares.filter((m) => {
777
+ const validMiddlewares = [];
778
+ for (const m of middlewares) {
786
779
  if (typeof m !== "function") {
787
- console.warn(`[faapi] \u65E0\u6548\u7684\u4E2D\u95F4\u4EF6\u9879\uFF08\u5E94\u4E3A\u51FD\u6570\uFF09\uFF0C\u5DF2\u5FFD\u7565: ${typeof m}`);
788
- return false;
780
+ throw new TypeError(
781
+ `[faapi] every middleware must be a function, got ${typeof m}: ${filePath}`
782
+ );
789
783
  }
790
- return true;
791
- });
784
+ validMiddlewares.push(m);
785
+ }
792
786
  const injectors = module.injectors ?? {};
793
787
  if (typeof injectors !== "object" || injectors === null) {
794
- console.warn(`[faapi] injectors \u5E94\u5BFC\u51FA\u5BF9\u8C61\uFF0C\u5DF2\u5FFD\u7565: ${filePath}`);
795
- return { middlewares: validMiddlewares, injectors: {} };
788
+ throw new TypeError(`[faapi] "injectors" export must be an object: ${filePath}`);
796
789
  }
797
790
  const validInjectors = {};
798
791
  for (const [name, injector] of Object.entries(injectors)) {
799
792
  if (typeof injector !== "function") {
800
- console.warn(`[faapi] \u6CE8\u5165\u5668 ${name} \u5E94\u4E3A\u51FD\u6570\uFF0C\u5DF2\u5FFD\u7565`);
801
- continue;
793
+ throw new TypeError(`[faapi] injector "${name}" must be a function: ${filePath}`);
802
794
  }
803
795
  validInjectors[name] = injector;
804
796
  }
@@ -813,10 +805,16 @@ async function loadMergedMiddlewares(middlewarePaths) {
813
805
  if (bundle === void 0) {
814
806
  let loading = inFlight.get(absMwPath);
815
807
  if (!loading) {
816
- loading = loadMiddlewaresFile(absMwPath).then((result) => {
817
- setCachedMiddlewares(absMwPath, result);
818
- return result;
819
- });
808
+ loading = loadMiddlewaresFile(absMwPath).then(
809
+ (result) => {
810
+ setCachedMiddlewares(absMwPath, result);
811
+ return result;
812
+ },
813
+ (err) => {
814
+ inFlight.delete(absMwPath);
815
+ throw err;
816
+ }
817
+ );
820
818
  inFlight.set(absMwPath, loading);
821
819
  }
822
820
  bundle = await loading;
@@ -3629,25 +3627,27 @@ function detectIgnoredMetaFields(source, strictHit) {
3629
3627
  for (const line of source.split("\n")) {
3630
3628
  const trimmed = line.trim();
3631
3629
  if (trimmed.startsWith("//")) continue;
3630
+ if (trimmed.startsWith("*") || trimmed.startsWith("/*") || trimmed.endsWith("*/")) continue;
3632
3631
  for (const field of ALL_META_FIELDS) {
3633
3632
  if (new RegExp(`\\b${field}\\s*:`).test(trimmed)) declared.add(field);
3634
3633
  }
3635
3634
  }
3636
3635
  return ALL_META_FIELDS.filter((field) => declared.has(field) && !strictHit[field]);
3637
3636
  }
3638
- function warnIgnoredMetaFields(name, fields) {
3639
- for (const field of fields) {
3637
+ function throwIgnoredMetaFields(name, fields) {
3638
+ if (fields.length === 0) return;
3639
+ const details = fields.map((field) => {
3640
3640
  if (STRING_META_FIELDS.includes(field)) {
3641
- console.warn(
3642
- `[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.`
3643
- );
3644
- } else {
3645
- const consequence = field === "timeoutMs" ? " and the task will run in-process with no timeout" : "";
3646
- console.warn(
3647
- `[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.`
3648
- );
3641
+ 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.`;
3649
3642
  }
3650
- }
3643
+ 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";
3644
+ 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.`;
3645
+ });
3646
+ throw new Error(
3647
+ `[faapi] Task "${name}" declares meta field(s) with non-literal values:
3648
+ ${details.join("\n ")}
3649
+ 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.`
3650
+ );
3651
3651
  }
3652
3652
  function parseNumericLiteral(raw) {
3653
3653
  return Number(raw.replace(/_/g, ""));
@@ -3715,7 +3715,7 @@ async function scanTasks(rootDir, patterns) {
3715
3715
  timeoutMs: timeoutMs !== void 0,
3716
3716
  graceMs: graceMs !== void 0
3717
3717
  };
3718
- warnIgnoredMetaFields(name, detectIgnoredMetaFields(source, strictHit));
3718
+ throwIgnoredMetaFields(name, detectIgnoredMetaFields(source, strictHit));
3719
3719
  tasks.push(manifest);
3720
3720
  }
3721
3721
  return tasks.sort((a, b) => a.name.localeCompare(b.name));
@@ -3830,6 +3830,7 @@ function generateTaskSchemaFileSource(sources, resolveType, helpersImportPath) {
3830
3830
  }
3831
3831
  async function generateTaskArtifacts(manifests, rootDir, dist) {
3832
3832
  const metadata = [];
3833
+ const declarationErrors = [];
3833
3834
  if (manifests.length > 0) {
3834
3835
  const programByFile = createPrograms(manifests.map((m) => path17.resolve(rootDir, m.filePath)));
3835
3836
  for (const manifest of manifests) {
@@ -3838,11 +3839,28 @@ async function generateTaskArtifacts(manifests, rootDir, dist) {
3838
3839
  name: manifest.name,
3839
3840
  filePath: manifest.filePath
3840
3841
  });
3841
- if (result) {
3842
- metadata.push({ ...result, taskName: manifest.name });
3842
+ if (!result) {
3843
+ declarationErrors.push(
3844
+ `"${manifest.name}" (${manifest.filePath}): no exported run function found`
3845
+ );
3846
+ continue;
3847
+ }
3848
+ if (!result.inputTypeName) {
3849
+ declarationErrors.push(
3850
+ `"${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"`
3851
+ );
3852
+ continue;
3843
3853
  }
3854
+ metadata.push({ ...result, taskName: manifest.name });
3844
3855
  }
3845
3856
  }
3857
+ if (declarationErrors.length > 0) {
3858
+ throw new Error(
3859
+ `[faapi] ${declarationErrors.length} task(s) missing a Payload type declaration \u2014 payload validation must not be silently skipped:
3860
+ ${declarationErrors.join("\n ")}
3861
+ 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).`
3862
+ );
3863
+ }
3846
3864
  const serialized = serializeTasks(manifests, dist);
3847
3865
  const tasksPath = path17.resolve(rootDir, dist, TASKS_FILE);
3848
3866
  await writeTasksModule(serialized, tasksPath);
@@ -4023,11 +4041,7 @@ async function ensureMiddlewaresCompiled(middlewarePaths, rootDir) {
4023
4041
  if (!dist) return;
4024
4042
  for (const mwPath of middlewarePaths) {
4025
4043
  const sourcePath = prodPathToSourcePath(mwPath, rootDir, dist);
4026
- try {
4027
- await ensureCompiled(sourcePath, rootDir, dist);
4028
- } catch (err) {
4029
- console.error(`[faapi] Failed to compile middleware source ${sourcePath}:`, err);
4030
- }
4044
+ await ensureCompiled(sourcePath, rootDir, dist);
4031
4045
  }
4032
4046
  }
4033
4047
  function prodSourcePathToProductPath(sourceAbsPath, rootDir, dist) {
@@ -6944,17 +6958,14 @@ function formatEntry(entry) {
6944
6958
  const plain = Object.fromEntries(
6945
6959
  Object.entries(entry.fields).map(([k, v]) => [k, serializeFieldValue(v)])
6946
6960
  );
6947
- try {
6948
- line += ` ${JSON.stringify(plain)}`;
6949
- } catch (err) {
6950
- line += ` [unserializable fields: ${err instanceof Error ? err.message : String(err)}]`;
6951
- }
6961
+ line += ` ${stringifyJson(plain)}`;
6952
6962
  }
6953
6963
  return line;
6954
6964
  }
6955
6965
  var init_formatEntry = __esm({
6956
6966
  "src/logger/formatEntry.ts"() {
6957
6967
  "use strict";
6968
+ init_stringifyJson();
6958
6969
  }
6959
6970
  });
6960
6971
 
@@ -6970,7 +6981,14 @@ function createFileLogSink(options) {
6970
6981
  };
6971
6982
  const openStream = (name) => {
6972
6983
  const stream = fs18.createWriteStream(path24.join(options.dir, `${name}.log`), { flags: "a" });
6973
- stream.on("error", () => {
6984
+ let streamErrorLogged = false;
6985
+ stream.on("error", (err) => {
6986
+ if (streamErrorLogged) return;
6987
+ streamErrorLogged = true;
6988
+ console.error(
6989
+ `[faapi] log file stream "${name}.log" failed \u2014 file logging on this stream stopped (subsequent errors suppressed). Check disk space / permissions for ${options.dir}:`,
6990
+ err
6991
+ );
6974
6992
  });
6975
6993
  streams.set(name, stream);
6976
6994
  };
@@ -9317,7 +9335,8 @@ async function sendErrorResponse(err, meta, res, onError, ctx) {
9317
9335
  if (onError && ctx) {
9318
9336
  try {
9319
9337
  await onError(err, ctx);
9320
- } catch {
9338
+ } catch (hookErr) {
9339
+ console.error("[faapi] lifecycle onError hook threw:", hookErr);
9321
9340
  }
9322
9341
  }
9323
9342
  }
@@ -9461,19 +9480,6 @@ function resolveWorkerEntryUrl() {
9461
9480
  workerEntryUrl = existsSync3(fileURLToPath(js)) ? js : new URL("./workerEntry.ts", import.meta.url);
9462
9481
  return workerEntryUrl;
9463
9482
  }
9464
- function safeConfig(config) {
9465
- if (config === void 0 || config === null) return config;
9466
- try {
9467
- structuredClone(config);
9468
- return config;
9469
- } catch {
9470
- }
9471
- try {
9472
- return JSON.parse(JSON.stringify(config));
9473
- } catch {
9474
- return void 0;
9475
- }
9476
- }
9477
9483
  function reviveError(payload) {
9478
9484
  const err = new Error(payload?.message ?? "task worker error");
9479
9485
  if (payload?.name !== void 0) err.name = payload.name;
@@ -9588,7 +9594,6 @@ async function runTaskInWorker(options) {
9588
9594
  type: "run",
9589
9595
  payload,
9590
9596
  taskCtx: {
9591
- config: safeConfig(taskCtx.config),
9592
9597
  job: taskCtx.job
9593
9598
  },
9594
9599
  registries: options.registries,
@@ -9597,9 +9602,7 @@ async function runTaskInWorker(options) {
9597
9602
  });
9598
9603
  } catch (err) {
9599
9604
  finish(
9600
- () => reject(
9601
- new Error(`Task "${taskCtx.job.name}" payload/config is not cloneable: ${String(err)}`)
9602
- )
9605
+ () => reject(new Error(`Task "${taskCtx.job.name}" payload is not cloneable: ${String(err)}`))
9603
9606
  );
9604
9607
  }
9605
9608
  });
@@ -9650,13 +9653,14 @@ function createTaskQueue(deps) {
9650
9653
  const loadTaskModule = deps.loadTaskModule ?? (async (filePath) => await import(filePath));
9651
9654
  const loadPayloadSchema = deps.loadPayloadSchema ?? (async (filePath) => {
9652
9655
  const zodPath = path28.join(path28.dirname(filePath), "zod.js");
9653
- try {
9654
- const mod = await import(zodPath);
9655
- const schemaKey = Object.keys(mod).find((k) => k.endsWith("Schema"));
9656
- return schemaKey ? mod[schemaKey] : void 0;
9657
- } catch {
9658
- return void 0;
9656
+ const mod = await import(zodPath);
9657
+ const schemaKey = Object.keys(mod).find((k) => k.endsWith("Schema"));
9658
+ if (!schemaKey) {
9659
+ throw new Error(
9660
+ `[faapi] Task payload schema artifact has no *Schema export: ${zodPath} \u2014 re-run "faapi dev" / "faapi build" to regenerate task artifacts.`
9661
+ );
9659
9662
  }
9663
+ return mod[schemaKey];
9660
9664
  });
9661
9665
  function assertKnownTask(name) {
9662
9666
  const meta = registry.get(name);
@@ -9675,14 +9679,13 @@ function createTaskQueue(deps) {
9675
9679
  async function validatePayload(name, payload) {
9676
9680
  if (!schemaCache.has(name)) {
9677
9681
  const meta = registry.get(name);
9678
- schemaCache.set(
9679
- name,
9680
- await loadPayloadSchema(path28.resolve(rootDir, meta.filePath)).catch(() => void 0)
9681
- );
9682
+ schemaCache.set(name, await loadPayloadSchema(path28.resolve(rootDir, meta.filePath)));
9682
9683
  }
9683
9684
  const schema = schemaCache.get(name);
9684
9685
  if (!schema || typeof schema.safeParse !== "function") {
9685
- return payload;
9686
+ throw new Error(
9687
+ `[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.`
9688
+ );
9686
9689
  }
9687
9690
  const result = schema.safeParse(payload);
9688
9691
  if (!result.success) {
@@ -9716,7 +9719,8 @@ function createTaskQueue(deps) {
9716
9719
  taskModulePath: path28.resolve(rootDir, meta.filePath),
9717
9720
  payload: job.payload,
9718
9721
  taskCtx: {
9719
- config: deps.config,
9722
+ // 隔离任务不传 config:worker 线程不接收进程配置(config 含函数字段
9723
+ // 不可结构化克隆,框架不做降级传递)——任务数据经 payload 显式传入
9720
9724
  resourcesDir: deps.resourcesDir,
9721
9725
  job: { id: job.id, name: job.name, attempt: job.attempt }
9722
9726
  },
@@ -9783,7 +9787,8 @@ function createTaskQueue(deps) {
9783
9787
  cancelled,
9784
9788
  error: record.error
9785
9789
  })
9786
- ).catch(() => {
9790
+ ).catch((hookErr) => {
9791
+ console.error(`[faapi] task onFailed hook threw for "${job.name}":`, hookErr);
9787
9792
  });
9788
9793
  }
9789
9794
  throw err;
@@ -10041,7 +10046,7 @@ async function loadPlugins(declarations, ctx, rootDir, dist) {
10041
10046
  const upgradeWrappers = [];
10042
10047
  const failures = [];
10043
10048
  if (!declarations || declarations.length === 0) {
10044
- return { handlerWrappers, upgradeWrappers, failures };
10049
+ return { handlerWrappers, upgradeWrappers };
10045
10050
  }
10046
10051
  const fullCtx = {
10047
10052
  ...ctx,
@@ -10068,7 +10073,7 @@ async function loadPlugins(declarations, ctx, rootDir, dist) {
10068
10073
  }
10069
10074
  if (enable === false) continue;
10070
10075
  if (loaded.has(specifier)) {
10071
- console.warn(`! Plugin already loaded: ${specifier}, skipping`);
10076
+ failures.push({ specifier, reason: "duplicate plugin declaration" });
10072
10077
  continue;
10073
10078
  }
10074
10079
  loaded.add(specifier);
@@ -10089,12 +10094,12 @@ async function loadPlugins(declarations, ctx, rootDir, dist) {
10089
10094
  }
10090
10095
  }
10091
10096
  if (failures.length > 0) {
10092
- console.error(
10097
+ throw new Error(
10093
10098
  `[faapi] ${failures.length} plugin(s) failed to load:
10094
10099
  ` + failures.map((f) => ` - ${f.specifier}: ${f.reason}`).join("\n")
10095
10100
  );
10096
10101
  }
10097
- return { handlerWrappers, upgradeWrappers, failures };
10102
+ return { handlerWrappers, upgradeWrappers };
10098
10103
  }
10099
10104
  function resolveLocalPluginSource(specifier, baseDir) {
10100
10105
  const base = path29.isAbsolute(specifier) ? specifier : path29.resolve(baseDir, specifier);
@@ -10428,7 +10433,7 @@ async function createAppBase(options) {
10428
10433
  const taskQueue = createTaskQueue({
10429
10434
  registry: registries.task,
10430
10435
  rootDir,
10431
- config,
10436
+ config: config ?? {},
10432
10437
  resourcesDir,
10433
10438
  registries: createTaskRegistriesView(registries),
10434
10439
  // 轻量补全通道:进程内路径经 store 惰性读取(插件注册后的活引用);
@@ -10459,7 +10464,7 @@ async function createAppBase(options) {
10459
10464
  dist,
10460
10465
  cors: config?.cors ?? true,
10461
10466
  onError: config?.lifecycle?.onError,
10462
- config: config ?? void 0,
10467
+ config,
10463
10468
  wsRoutes,
10464
10469
  middlewares: config?.middlewares,
10465
10470
  injectors: config?.injectors,