@faapi/next 6.17.0 → 6.18.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.
@@ -39,6 +39,14 @@ async function apiCall(input, init) {
39
39
  }
40
40
  throw new ApiError("NON_JSON_RESPONSE", res.status, statusMessage(res.status));
41
41
  }
42
+ if (body === null || typeof body !== "object") {
43
+ console.error("[apiCall] \u975E JSON \u54CD\u5E94", {
44
+ status: res.status,
45
+ url: res.url,
46
+ body: text.slice(0, 200)
47
+ });
48
+ throw new ApiError("NON_JSON_RESPONSE", res.status, statusMessage(res.status));
49
+ }
42
50
  if (!res.ok || body.error) {
43
51
  throw new ApiError(
44
52
  body.error?.code ?? "HTTP_ERROR",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/client/apiError.ts","../../src/client/apiCall.ts"],"sourcesContent":["/**\n * 客户端统一错误类型与响应信封类型。\n *\n * 零依赖约束:本模块属于浏览器端代码(@faapi/next/client 子路径),禁止\n * import @faapi/faapi 及任何 Node 模块——主包含 fs/child_process 等服务端\n * 依赖,传递性引入会把服务端代码拉进浏览器 bundle,导致 Next.js 客户端\n * 打包失败。类型独立声明、结构镜像主包,详见 apiError.md。\n */\n\n/**\n * 字段级校验详情,镜像主包 ValidationIssue(path/code/expected/received/message)。\n *\n * 服务端 JSON 的形状即客户端类型;主包结构变更时此处需手动同步。\n */\nexport interface ApiValidationIssue {\n /** 字段路径,如 'user.address.city' */\n path: string;\n /** 错误码,机器可读的契约,如 'TYPE_MISMATCH' */\n code: string;\n /** 期望类型/值,如 'number' */\n expected: string;\n /** 实际类型/值,如 'string' */\n received: string;\n /** 人类可读的字段级错误描述 */\n message: string;\n}\n\n/** 失败信封:{ error: { message, ...code?, ...issues? } }(主包 defaultFail 中 code 省略时不存在) */\nexport interface ApiErrorBody {\n error: {\n code?: string;\n message: string;\n issues?: ApiValidationIssue[];\n };\n}\n\n/**\n * faapi 默认响应信封:成功 { data } / 失败 { error }。\n * 与主包 config.response.ok/fail 的默认实现一致;业务方自定义信封时\n * 不适用(见 apiCall.md 已知限制)。\n */\nexport type ApiEnvelope<T> = Partial<{ data: T }> & Partial<ApiErrorBody>;\n\n/**\n * API 错误:携带 code/status/issues,调用方可按 code 分支处理。\n *\n * 继承 Error,现有 `e instanceof Error ? e.message : String(e)` 的\n * toast 消费代码不受影响。\n */\nexport class ApiError extends Error {\n /** 字符串业务错误码,如 'VALIDATION_ERROR';非信封错误为框架侧兜底码 */\n readonly code: string;\n /** HTTP 状态码 */\n readonly status: number;\n /** VALIDATION_ERROR 时的字段级校验详情,供表单回显 */\n readonly issues?: readonly ApiValidationIssue[];\n\n constructor(\n code: string,\n status: number,\n message: string,\n issues?: readonly ApiValidationIssue[],\n ) {\n super(message);\n this.name = 'ApiError';\n this.code = code;\n this.status = status;\n this.issues = issues;\n }\n}\n","/**\n * 浏览器端统一请求入口:fetch 封装 + 非 JSON 响应守卫 + faapi 信封解包。\n *\n * 守卫动机:/api/* 的错误响应恒为 JSON 信封,但链路其他层会返回 HTML——\n * 反代/网关错误页(502/504)、Next.js 404 页、SSO 登录守卫 302(fetch 跟随\n * 重定向拿到登录页)。直接 res.json() 会把裸 SyntaxError 原文抛进界面;\n * 这里检测 + 转译为结构化 ApiError,现场留在 console。行为规格见 apiCall.md。\n */\nimport { ApiError, type ApiEnvelope } from './apiError';\n\n/**\n * 非 JSON 响应(反代错误页/空 body)按状态映射的可行动中文文案。\n *\n * 导出供业务方 fork 自定义文案(如英文产品)时组合使用。\n */\nexport function statusMessage(status: number): string {\n if (status === 504) return '服务响应超时,请稍后重试';\n if (status === 502 || status === 503) return '服务暂时不可用,请稍后重试';\n if (status === 401) return '登录已过期,请刷新页面重新登录';\n return `请求失败: ${status}`;\n}\n\n/**\n * 发起 API 请求并解包 faapi 默认信封,失败一律抛 ApiError。\n *\n * 成功返回 body.data;非 JSON/信封错误/空响应转译为结构化错误,\n * 现场经 console.error 保留(status/url/body 前 200 字符)。\n * 仅支持主包 config.response 默认信封,自定义信封时业务方自行包装。\n */\nexport async function apiCall<T>(input: string, init?: RequestInit): Promise<T> {\n const res = await fetch(input, init);\n const text = await res.text();\n\n let body: ApiEnvelope<T>;\n try {\n body = JSON.parse(text) as ApiEnvelope<T>;\n } catch {\n // 非 JSON 响应:现场留给排障,错误转译为可行动的文案。\n console.error('[apiCall] 非 JSON 响应', {\n status: res.status,\n url: res.url,\n body: text.slice(0, 200),\n });\n if (res.redirected) {\n // fetch 跟随重定向后拿到非 JSON(典型:SSO 登录守卫 302 → 登录页)\n throw new ApiError('REDIRECTED', res.status, '登录状态已失效,请刷新页面重新登录');\n }\n throw new ApiError('NON_JSON_RESPONSE', res.status, statusMessage(res.status));\n }\n\n if (!res.ok || body.error) {\n throw new ApiError(\n body.error?.code ?? 'HTTP_ERROR',\n res.status,\n body.error?.message || statusMessage(res.status),\n body.error?.issues,\n );\n }\n\n if (body.data === undefined) {\n // {data: null} 合法返回 null;无 data 字段({})视为空响应错误\n throw new ApiError('EMPTY_RESPONSE', res.status, `请求失败: ${res.status}`);\n }\n\n return body.data;\n}\n"],"mappings":";AAiDO,IAAM,WAAN,cAAuB,MAAM;AAAA;AAAA,EAEzB;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA,EAET,YACE,MACA,QACA,SACA,QACA;AACA,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,SAAS;AAAA,EAChB;AACF;;;ACtDO,SAAS,cAAc,QAAwB;AACpD,MAAI,WAAW,IAAK,QAAO;AAC3B,MAAI,WAAW,OAAO,WAAW,IAAK,QAAO;AAC7C,MAAI,WAAW,IAAK,QAAO;AAC3B,SAAO,6BAAS,MAAM;AACxB;AASA,eAAsB,QAAW,OAAe,MAAgC;AAC9E,QAAM,MAAM,MAAM,MAAM,OAAO,IAAI;AACnC,QAAM,OAAO,MAAM,IAAI,KAAK;AAE5B,MAAI;AACJ,MAAI;AACF,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB,QAAQ;AAEN,YAAQ,MAAM,sCAAuB;AAAA,MACnC,QAAQ,IAAI;AAAA,MACZ,KAAK,IAAI;AAAA,MACT,MAAM,KAAK,MAAM,GAAG,GAAG;AAAA,IACzB,CAAC;AACD,QAAI,IAAI,YAAY;AAElB,YAAM,IAAI,SAAS,cAAc,IAAI,QAAQ,wGAAmB;AAAA,IAClE;AACA,UAAM,IAAI,SAAS,qBAAqB,IAAI,QAAQ,cAAc,IAAI,MAAM,CAAC;AAAA,EAC/E;AAEA,MAAI,CAAC,IAAI,MAAM,KAAK,OAAO;AACzB,UAAM,IAAI;AAAA,MACR,KAAK,OAAO,QAAQ;AAAA,MACpB,IAAI;AAAA,MACJ,KAAK,OAAO,WAAW,cAAc,IAAI,MAAM;AAAA,MAC/C,KAAK,OAAO;AAAA,IACd;AAAA,EACF;AAEA,MAAI,KAAK,SAAS,QAAW;AAE3B,UAAM,IAAI,SAAS,kBAAkB,IAAI,QAAQ,6BAAS,IAAI,MAAM,EAAE;AAAA,EACxE;AAEA,SAAO,KAAK;AACd;","names":[]}
1
+ {"version":3,"sources":["../../src/client/apiError.ts","../../src/client/apiCall.ts"],"sourcesContent":["/**\n * 客户端统一错误类型与响应信封类型。\n *\n * 零依赖约束:本模块属于浏览器端代码(@faapi/next/client 子路径),禁止\n * import @faapi/faapi 及任何 Node 模块——主包含 fs/child_process 等服务端\n * 依赖,传递性引入会把服务端代码拉进浏览器 bundle,导致 Next.js 客户端\n * 打包失败。类型独立声明、结构镜像主包,详见 apiError.md。\n */\n\n/**\n * 字段级校验详情,镜像主包 ValidationIssue(path/code/expected/received/message)。\n *\n * 服务端 JSON 的形状即客户端类型;主包结构变更时此处需手动同步。\n */\nexport interface ApiValidationIssue {\n /** 字段路径,如 'user.address.city' */\n path: string;\n /** 错误码,机器可读的契约,如 'TYPE_MISMATCH' */\n code: string;\n /** 期望类型/值,如 'number' */\n expected: string;\n /** 实际类型/值,如 'string' */\n received: string;\n /** 人类可读的字段级错误描述 */\n message: string;\n}\n\n/** 失败信封:{ error: { message, ...code?, ...issues? } }(主包 defaultFail 中 code 省略时不存在) */\nexport interface ApiErrorBody {\n error: {\n code?: string;\n message: string;\n issues?: ApiValidationIssue[];\n };\n}\n\n/**\n * faapi 默认响应信封:成功 { data } / 失败 { error }。\n * 与主包 config.response.ok/fail 的默认实现一致;业务方自定义信封时\n * 不适用(见 apiCall.md 已知限制)。\n */\nexport type ApiEnvelope<T> = Partial<{ data: T }> & Partial<ApiErrorBody>;\n\n/**\n * API 错误:携带 code/status/issues,调用方可按 code 分支处理。\n *\n * 继承 Error,现有 `e instanceof Error ? e.message : String(e)` 的\n * toast 消费代码不受影响。\n */\nexport class ApiError extends Error {\n /** 字符串业务错误码,如 'VALIDATION_ERROR';非信封错误为框架侧兜底码 */\n readonly code: string;\n /** HTTP 状态码 */\n readonly status: number;\n /** VALIDATION_ERROR 时的字段级校验详情,供表单回显 */\n readonly issues?: readonly ApiValidationIssue[];\n\n constructor(\n code: string,\n status: number,\n message: string,\n issues?: readonly ApiValidationIssue[],\n ) {\n super(message);\n this.name = 'ApiError';\n this.code = code;\n this.status = status;\n this.issues = issues;\n }\n}\n","/**\n * 浏览器端统一请求入口:fetch 封装 + 非 JSON 响应守卫 + faapi 信封解包。\n *\n * 守卫动机:/api/* 的错误响应恒为 JSON 信封,但链路其他层会返回 HTML——\n * 反代/网关错误页(502/504)、Next.js 404 页、SSO 登录守卫 302(fetch 跟随\n * 重定向拿到登录页)。直接 res.json() 会把裸 SyntaxError 原文抛进界面;\n * 这里检测 + 转译为结构化 ApiError,现场留在 console。行为规格见 apiCall.md。\n */\nimport { ApiError, type ApiEnvelope } from './apiError';\n\n/**\n * 非 JSON 响应(反代错误页/空 body)按状态映射的可行动中文文案。\n *\n * 导出供业务方 fork 自定义文案(如英文产品)时组合使用。\n */\nexport function statusMessage(status: number): string {\n if (status === 504) return '服务响应超时,请稍后重试';\n if (status === 502 || status === 503) return '服务暂时不可用,请稍后重试';\n if (status === 401) return '登录已过期,请刷新页面重新登录';\n return `请求失败: ${status}`;\n}\n\n/**\n * 发起 API 请求并解包 faapi 默认信封,失败一律抛 ApiError。\n *\n * 成功返回 body.data;非 JSON/信封错误/空响应转译为结构化错误,\n * 现场经 console.error 保留(status/url/body 前 200 字符)。\n * 仅支持主包 config.response 默认信封,自定义信封时业务方自行包装。\n */\nexport async function apiCall<T>(input: string, init?: RequestInit): Promise<T> {\n const res = await fetch(input, init);\n const text = await res.text();\n\n let body: ApiEnvelope<T>;\n try {\n body = JSON.parse(text) as ApiEnvelope<T>;\n } catch {\n // 非 JSON 响应:现场留给排障,错误转译为可行动的文案。\n console.error('[apiCall] 非 JSON 响应', {\n status: res.status,\n url: res.url,\n body: text.slice(0, 200),\n });\n if (res.redirected) {\n // fetch 跟随重定向后拿到非 JSON(典型:SSO 登录守卫 302 → 登录页)\n throw new ApiError('REDIRECTED', res.status, '登录状态已失效,请刷新页面重新登录');\n }\n throw new ApiError('NON_JSON_RESPONSE', res.status, statusMessage(res.status));\n }\n\n // JSON.parse(\"null\") === null 等非对象形态:合法 JSON 但不是信封,归入非 JSON 分支\n // (否则 body.error 访问抛裸 TypeError,击穿「失败一律转译为 ApiError」的模块承诺)\n if (body === null || typeof body !== 'object') {\n console.error('[apiCall] 非 JSON 响应', {\n status: res.status,\n url: res.url,\n body: text.slice(0, 200),\n });\n throw new ApiError('NON_JSON_RESPONSE', res.status, statusMessage(res.status));\n }\n\n if (!res.ok || body.error) {\n throw new ApiError(\n body.error?.code ?? 'HTTP_ERROR',\n res.status,\n body.error?.message || statusMessage(res.status),\n body.error?.issues,\n );\n }\n\n if (body.data === undefined) {\n // {data: null} 合法返回 null;无 data 字段({})视为空响应错误\n throw new ApiError('EMPTY_RESPONSE', res.status, `请求失败: ${res.status}`);\n }\n\n return body.data;\n}\n"],"mappings":";AAiDO,IAAM,WAAN,cAAuB,MAAM;AAAA;AAAA,EAEzB;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA,EAET,YACE,MACA,QACA,SACA,QACA;AACA,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,SAAS;AAAA,EAChB;AACF;;;ACtDO,SAAS,cAAc,QAAwB;AACpD,MAAI,WAAW,IAAK,QAAO;AAC3B,MAAI,WAAW,OAAO,WAAW,IAAK,QAAO;AAC7C,MAAI,WAAW,IAAK,QAAO;AAC3B,SAAO,6BAAS,MAAM;AACxB;AASA,eAAsB,QAAW,OAAe,MAAgC;AAC9E,QAAM,MAAM,MAAM,MAAM,OAAO,IAAI;AACnC,QAAM,OAAO,MAAM,IAAI,KAAK;AAE5B,MAAI;AACJ,MAAI;AACF,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB,QAAQ;AAEN,YAAQ,MAAM,sCAAuB;AAAA,MACnC,QAAQ,IAAI;AAAA,MACZ,KAAK,IAAI;AAAA,MACT,MAAM,KAAK,MAAM,GAAG,GAAG;AAAA,IACzB,CAAC;AACD,QAAI,IAAI,YAAY;AAElB,YAAM,IAAI,SAAS,cAAc,IAAI,QAAQ,wGAAmB;AAAA,IAClE;AACA,UAAM,IAAI,SAAS,qBAAqB,IAAI,QAAQ,cAAc,IAAI,MAAM,CAAC;AAAA,EAC/E;AAIA,MAAI,SAAS,QAAQ,OAAO,SAAS,UAAU;AAC7C,YAAQ,MAAM,sCAAuB;AAAA,MACnC,QAAQ,IAAI;AAAA,MACZ,KAAK,IAAI;AAAA,MACT,MAAM,KAAK,MAAM,GAAG,GAAG;AAAA,IACzB,CAAC;AACD,UAAM,IAAI,SAAS,qBAAqB,IAAI,QAAQ,cAAc,IAAI,MAAM,CAAC;AAAA,EAC/E;AAEA,MAAI,CAAC,IAAI,MAAM,KAAK,OAAO;AACzB,UAAM,IAAI;AAAA,MACR,KAAK,OAAO,QAAQ;AAAA,MACpB,IAAI;AAAA,MACJ,KAAK,OAAO,WAAW,cAAc,IAAI,MAAM;AAAA,MAC/C,KAAK,OAAO;AAAA,IACd;AAAA,EACF;AAEA,MAAI,KAAK,SAAS,QAAW;AAE3B,UAAM,IAAI,SAAS,kBAAkB,IAAI,QAAQ,6BAAS,IAAI,MAAM,EAAE;AAAA,EACxE;AAEA,SAAO,KAAK;AACd;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faapi/next",
3
- "version": "6.17.0",
3
+ "version": "6.18.0",
4
4
  "description": "Next.js integration for faapi — serve faapi APIs and Next.js pages from a single server",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -35,11 +35,11 @@
35
35
  "vitest": "^4.1.11",
36
36
  "ws": "^8.21.0",
37
37
  "zod": "^4.4.3",
38
- "@faapi/faapi": "6.17.0"
38
+ "@faapi/faapi": "6.18.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "next": ">=13.0.0",
42
- "@faapi/faapi": "^6.17.0"
42
+ "@faapi/faapi": "^6.18.0"
43
43
  },
44
44
  "peerDependenciesMeta": {
45
45
  "next": {
@@ -56,6 +56,7 @@
56
56
  "access": "public",
57
57
  "provenance": true
58
58
  },
59
+ "sideEffects": false,
59
60
  "scripts": {
60
61
  "build": "tsup",
61
62
  "test": "vitest run --passWithNoTests",