@faapi/next 4.2.0 → 4.2.1

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/index.js CHANGED
@@ -61,13 +61,16 @@ var nextPlugin = {
61
61
  };
62
62
  async function loadUserConfigAndMergeTrustHostHeader(dir, dev) {
63
63
  try {
64
- const { PHASE_DEVELOPMENT_SERVER, PHASE_PRODUCTION_SERVER } = await import("next/constants");
65
- const configModule = await import("next/dist/server/config");
66
- const loadConfig = configModule.default ?? configModule ?? void 0;
67
- if (typeof loadConfig !== "function") {
64
+ const constantsModule = await importNextInternalModule(["next/constants.js", "next/constants"]);
65
+ const configModule = await importNextInternalModule([
66
+ "next/dist/server/config.js",
67
+ "next/dist/esm/server/config.js"
68
+ ]);
69
+ const loadConfig = resolveLoadConfigFunction(configModule);
70
+ if (!loadConfig) {
68
71
  throw new Error("loadConfig is not a function");
69
72
  }
70
- const phase = dev ? PHASE_DEVELOPMENT_SERVER : PHASE_PRODUCTION_SERVER;
73
+ const phase = dev ? resolvePhaseConstant(constantsModule, "PHASE_DEVELOPMENT_SERVER") ?? "phase-development-server" : resolvePhaseConstant(constantsModule, "PHASE_PRODUCTION_SERVER") ?? "phase-production-server";
71
74
  const userConfig = await loadConfig(phase, dir, { silent: true });
72
75
  if (!userConfig || typeof userConfig !== "object") {
73
76
  return void 0;
@@ -85,11 +88,42 @@ async function loadUserConfigAndMergeTrustHostHeader(dir, dev) {
85
88
  };
86
89
  } catch {
87
90
  console.warn(
88
- "[faapi-next] \u81EA\u52A8\u5F00\u542F experimental.trustHostHeader \u5931\u8D25\uFF08\u65E0\u6CD5\u52A0\u8F7D next.config.ts\uFF09\u3002\n \u82E5\u5904\u4E8E\u53CD\u5411\u4EE3\u7406\uFF08Nginx/Caddy \u7B49\uFF09\u573A\u666F\uFF0C\u8BF7\u624B\u52A8\u5728 next.config.ts \u4E2D\u914D\u7F6E\uFF1A\n experimental: { trustHostHeader: true }"
91
+ '[faapi-next] \u81EA\u52A8\u5F00\u542F experimental.trustHostHeader \u5931\u8D25\uFF08\u65E0\u6CD5\u52A0\u8F7D Next.js \u5185\u90E8 config \u6A21\u5757\uFF0Cconf \u672A\u6CE8\u5165\uFF09\u3002\n \u8BF7\u52FF\u5728 next.config.ts \u624B\u5199 experimental.trustHostHeader \u66FF\u4EE3\u2014\u2014Next 16+ \u7684 config\n schema \u4E0D\u63A5\u53D7\u8BE5\u5B57\u6BB5\uFF0C\u624B\u5199\u4F1A\u89E6\u53D1 "Unrecognized key(s)" \u544A\u8B66\u3002\n \u975E\u53CD\u5411\u4EE3\u7406\u573A\u666F\u53EF\u5FFD\u7565\u672C\u63D0\u793A\uFF1B\u5982\u9700\u5173\u95ED\u672C\u63D0\u793A\uFF0C\u8BBE\u63D2\u4EF6\u9009\u9879 trustHostHeader: false\u3002'
89
92
  );
90
93
  return void 0;
91
94
  }
92
95
  }
96
+ async function importNextInternalModule(specifiers) {
97
+ for (const specifier of specifiers) {
98
+ try {
99
+ return await import(specifier);
100
+ } catch {
101
+ }
102
+ }
103
+ return void 0;
104
+ }
105
+ function resolveLoadConfigFunction(configModule) {
106
+ const direct = configModule?.default;
107
+ if (typeof direct === "function") {
108
+ return direct;
109
+ }
110
+ const nested = direct?.default;
111
+ if (typeof nested === "function") {
112
+ return nested;
113
+ }
114
+ if (typeof configModule === "function") {
115
+ return configModule;
116
+ }
117
+ return void 0;
118
+ }
119
+ function resolvePhaseConstant(constantsModule, key) {
120
+ const topLevel = constantsModule?.[key];
121
+ if (typeof topLevel === "string") {
122
+ return topLevel;
123
+ }
124
+ const fromDefault = constantsModule?.default?.[key];
125
+ return typeof fromDefault === "string" ? fromDefault : void 0;
126
+ }
93
127
  var createNextServer_default = nextPlugin;
94
128
  export {
95
129
  createNextServer_default as default
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/createNextServer.ts"],"sourcesContent":["/**\n * @faapi/next — Next.js + faapi 集成插件\n *\n * 通过 faapi.config.ts 的 plugins 字段加载,在 server.listen 之前包装 handler:\n * - /api/* 走 faapi handler\n * - 其余走 Next.js getRequestHandler\n *\n * WS upgrade 同步分流:faapi WS 路由走原始 upgrade handler,其余走 Next.js HMR。\n *\n * @see createNextServer.md 设计要点与使用场景\n */\nimport path from 'node:path';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { FaapiPlugin, PluginContext, RequestHandler, UpgradeHandler } from '@faapi/faapi';\n\n/**\n * Next.js 应用最小类型定义\n *\n * next 作为 optional peerDependency,不强制安装;这里只声明插件用到的字段,\n * 避免直接依赖 next 包的类型。\n */\ninterface NextApp {\n getRequestHandler(): (req: IncomingMessage, res: ServerResponse) => Promise<unknown>;\n prepare(): Promise<void>;\n getUpgradeHandler?(): (req: IncomingMessage, socket: unknown, head: Buffer) => void;\n}\n\n/** 插件选项(来自 faapi.config.ts plugins 声明的第二个元素或 options 字段) */\nexport interface NextPluginOptions {\n /** 开发模式,默认 NODE_ENV !== 'production' */\n dev?: boolean;\n /** Next.js 项目目录,默认 '.' */\n dir?: string;\n /** faapi API 路径前缀,默认 '/api';匹配此前缀的请求走 faapi,其余走 Next.js */\n apiPrefix?: string;\n /**\n * 是否自动开启 Next.js 的 experimental.trustHostHeader,默认 true\n *\n * 反向代理(Nginx/Caddy 等)场景下,Next.js 默认用固定的 hostname:port 构造\n * initURL(如 https://localhost:3000/path),忽略代理透传的 Host 头,导致\n * SSR 重定向/链接指向错误域名。开启 trustHostHeader 后,Next.js 直接用\n * req.headers.host 构造 URL,正确反映客户端原始请求。\n *\n * 实现方式:插件用 Next.js 的 loadConfig 加载用户 next.config.ts,合并\n * experimental.trustHostHeader=true,再通过 next() 的 conf 选项传入。\n * 用户在 next.config.ts 中的其他配置会被保留。若加载失败则退回不传 conf,\n * 此时需手动在 next.config.ts 中配置。\n *\n * 设为 false 可禁用此行为(如非反向代理场景,或用户想手动控制)。\n */\n trustHostHeader?: boolean;\n}\n\n/** 默认 API 前缀 */\nconst DEFAULT_API_PREFIX = '/api';\n\n/**\n * 判断 pathname 是否匹配 apiPrefix\n *\n * apiPrefix '/api' 匹配 '/api' 和 '/api/*',不匹配 '/api2'\n */\nfunction isApiPath(pathname: string, apiPrefix: string): boolean {\n return pathname === apiPrefix || pathname.startsWith(apiPrefix + '/');\n}\n\n/**\n * Next.js + faapi 集成插件\n *\n * 通过 faapi.config.ts 的 plugins 字段加载:\n *\n * ```ts\n * export default {\n * plugins: [\n * ['@faapi/next', { dir: '.' }] // 带选项的元组\n * ],\n * } satisfies FaapiConfig;\n * ```\n *\n * 启动时用 `faapi` 命令,自动集成 Next.js,无需写 custom server 代码。\n */\nconst nextPlugin: FaapiPlugin = {\n name: '@faapi/next',\n\n async setup(ctx: PluginContext): Promise<void> {\n const options = (ctx.options as NextPluginOptions) ?? {};\n const dev = options.dev ?? process.env.NODE_ENV !== 'production';\n // dir 相对于项目根目录解析(CLI 形态下 rootDir === process.cwd(),行为不变;\n // 库 API 形态下 rootDir 可能不同,按 rootDir 解析更合理)\n const dir = path.resolve(ctx.rootDir, options.dir ?? '.');\n const apiPrefix = options.apiPrefix ?? DEFAULT_API_PREFIX;\n const trustHostHeader = options.trustHostHeader ?? true;\n\n // 1. 动态 import next(peerDependency,未安装时报错)\n let nextFactory: (opts: { dev: boolean; dir: string; conf?: unknown }) => NextApp;\n try {\n nextFactory = (await import('next')).default as typeof nextFactory;\n } catch {\n throw new Error('[faapi-next] next 包未安装。请运行 `pnpm add next` 安装 Next.js。');\n }\n\n // 2. 构造 conf:若开启 trustHostHeader,加载用户 next.config.ts 并合并\n // experimental.trustHostHeader=true。loadConfig 返回完整配置(已 normalize\n // + 填充默认值),展开后覆盖 experimental 字段,再通过 conf 传给 next()。\n // 注意:传 conf 会让 Next.js 跳过读取 next.config.ts,所以这里必须加载\n // 用户配置以保留 next.config.ts 中的其他设置(images/rewrites 等)。\n let conf: Record<string, unknown> | undefined;\n if (trustHostHeader) {\n conf = await loadUserConfigAndMergeTrustHostHeader(dir, dev);\n }\n\n // 3. 启动 Next.js\n const nextApp = nextFactory(conf ? { dev, dir, conf } : { dev, dir });\n const nextHandle = nextApp.getRequestHandler();\n await nextApp.prepare();\n\n // 4. Next.js upgrade handler(dev 模式 HMR)\n const nextUpgradeHandler =\n typeof nextApp.getUpgradeHandler === 'function' ? nextApp.getUpgradeHandler() : null;\n\n // 5. 包装 HTTP handler:/api/* 走 faapi,其余走 Next.js\n ctx.wrapHandler?.((original: RequestHandler): RequestHandler => {\n return (req, res) => {\n const { pathname } = new URL(req.url ?? '/', 'http://localhost');\n if (isApiPath(pathname, apiPrefix)) {\n original(req, res);\n } else {\n // 不传 parsedUrl,让 Next.js 内部解析 req.url\n // NextUrlWithParsedQuery 是 url.parse() 返回的结构,不是 URL 对象\n // 用 Promise.resolve 包装,兼容 handler 返回 undefined 的情况\n Promise.resolve(nextHandle(req, res)).catch((err: unknown) => {\n console.error('[faapi-next] Next.js handler error:', err);\n if (!res.headersSent) {\n res.statusCode = 500;\n res.end('Next.js handler error');\n }\n });\n }\n };\n });\n\n // 6. 包装 WS upgrade handler:/api/* 走 faapi,其余走 Next.js HMR\n ctx.wrapUpgradeHandler?.((original: UpgradeHandler | undefined): UpgradeHandler => {\n return (req, socket, head) => {\n const { pathname } = new URL(req.url ?? '/', 'http://localhost');\n if (isApiPath(pathname, apiPrefix) && original) {\n original(req, socket, head);\n } else if (nextUpgradeHandler) {\n nextUpgradeHandler(req, socket, head);\n } else {\n socket.destroy();\n }\n };\n });\n\n console.log(\n `- Next.js integration: ${dev ? 'dev' : 'prod'} mode, dir=${dir}, apiPrefix=${apiPrefix}${conf ? ', trustHostHeader=auto' : ''}`,\n );\n },\n};\n\n/**\n * 加载用户 next.config.{js,ts,mjs},合并 experimental.trustHostHeader=true,\n * 返回可传给 next() conf 选项的配置对象。\n *\n * 用 Next.js 内部的 loadConfig(next/dist/server/config)加载完整配置(已 normalize\n * + 填充默认值),展开后覆盖 experimental.trustHostHeader。这样用户在 next.config.ts\n * 中的其他配置(images/rewrites/redirects 等)都会被保留。\n *\n * 失败时返回 undefined(退回不传 conf,Next.js 自己加载 next.config.ts,\n * trustHostHeader 不自动开启),并打印警告提示用户手动配置。\n */\nasync function loadUserConfigAndMergeTrustHostHeader(\n dir: string,\n dev: boolean,\n): Promise<Record<string, unknown> | undefined> {\n try {\n const { PHASE_DEVELOPMENT_SERVER, PHASE_PRODUCTION_SERVER } = await import('next/constants');\n const configModule = (await import('next/dist/server/config')) as {\n default?: (phase: string, dir: string, opts?: unknown) => Promise<Record<string, unknown>>;\n } & Record<string, unknown>;\n // 优先取 default 导出,否则取模块本身(兼容 CJS/ESM 互操作差异)\n const loadConfig =\n configModule.default ??\n (configModule as unknown as (\n phase: string,\n dir: string,\n opts?: unknown,\n ) => Promise<Record<string, unknown>>) ??\n undefined;\n if (typeof loadConfig !== 'function') {\n throw new Error('loadConfig is not a function');\n }\n const phase = dev ? PHASE_DEVELOPMENT_SERVER : PHASE_PRODUCTION_SERVER;\n const userConfig = await loadConfig(phase, dir, { silent: true });\n if (!userConfig || typeof userConfig !== 'object') {\n return undefined;\n }\n const experimental = (userConfig.experimental as Record<string, unknown> | undefined) ?? {};\n // 用户已在 next.config.ts 显式开启则不重复设置\n // 想关闭用插件选项 trustHostHeader: false(跳过整个 loadConfig 分支)\n if (experimental.trustHostHeader === true) {\n return { ...userConfig };\n }\n return {\n ...userConfig,\n experimental: {\n ...experimental,\n trustHostHeader: true,\n },\n };\n } catch {\n console.warn(\n '[faapi-next] 自动开启 experimental.trustHostHeader 失败(无法加载 next.config.ts)。\\n' +\n ' 若处于反向代理(Nginx/Caddy 等)场景,请手动在 next.config.ts 中配置:\\n' +\n ' experimental: { trustHostHeader: true }',\n );\n return undefined;\n }\n}\n\nexport default nextPlugin;\nexport type { FaapiPlugin, PluginContext, RequestHandler, UpgradeHandler } from '@faapi/faapi';\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/createNextServer.ts"],"sourcesContent":["/**\n * @faapi/next — Next.js + faapi 集成插件\n *\n * 通过 faapi.config.ts 的 plugins 字段加载,在 server.listen 之前包装 handler:\n * - /api/* 走 faapi handler\n * - 其余走 Next.js getRequestHandler\n *\n * WS upgrade 同步分流:faapi WS 路由走原始 upgrade handler,其余走 Next.js HMR。\n *\n * @see createNextServer.md 设计要点与使用场景\n */\nimport path from 'node:path';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { FaapiPlugin, PluginContext, RequestHandler, UpgradeHandler } from '@faapi/faapi';\n\n/**\n * Next.js 应用最小类型定义\n *\n * next 作为 optional peerDependency,不强制安装;这里只声明插件用到的字段,\n * 避免直接依赖 next 包的类型。\n */\ninterface NextApp {\n getRequestHandler(): (req: IncomingMessage, res: ServerResponse) => Promise<unknown>;\n prepare(): Promise<void>;\n getUpgradeHandler?(): (req: IncomingMessage, socket: unknown, head: Buffer) => void;\n}\n\n/** 插件选项(来自 faapi.config.ts plugins 声明的第二个元素或 options 字段) */\nexport interface NextPluginOptions {\n /** 开发模式,默认 NODE_ENV !== 'production' */\n dev?: boolean;\n /** Next.js 项目目录,默认 '.' */\n dir?: string;\n /** faapi API 路径前缀,默认 '/api';匹配此前缀的请求走 faapi,其余走 Next.js */\n apiPrefix?: string;\n /**\n * 是否自动开启 Next.js 的 experimental.trustHostHeader,默认 true\n *\n * 反向代理(Nginx/Caddy 等)场景下,Next.js 默认用固定的 hostname:port 构造\n * initURL(如 https://localhost:3000/path),忽略代理透传的 Host 头,导致\n * SSR 重定向/链接指向错误域名。开启 trustHostHeader 后,Next.js 直接用\n * req.headers.host 构造 URL,正确反映客户端原始请求。\n *\n * 实现方式:插件用 Next.js 的 loadConfig 加载用户 next.config.ts,合并\n * experimental.trustHostHeader=true,再通过 next() 的 conf 选项传入。\n * 用户在 next.config.ts 中的其他配置会被保留。若加载失败则退回不传 conf,\n * 此时需手动在 next.config.ts 中配置。\n *\n * 设为 false 可禁用此行为(如非反向代理场景,或用户想手动控制)。\n */\n trustHostHeader?: boolean;\n}\n\n/** 默认 API 前缀 */\nconst DEFAULT_API_PREFIX = '/api';\n\n/**\n * 判断 pathname 是否匹配 apiPrefix\n *\n * apiPrefix '/api' 匹配 '/api' 和 '/api/*',不匹配 '/api2'\n */\nfunction isApiPath(pathname: string, apiPrefix: string): boolean {\n return pathname === apiPrefix || pathname.startsWith(apiPrefix + '/');\n}\n\n/**\n * Next.js + faapi 集成插件\n *\n * 通过 faapi.config.ts 的 plugins 字段加载:\n *\n * ```ts\n * export default {\n * plugins: [\n * ['@faapi/next', { dir: '.' }] // 带选项的元组\n * ],\n * } satisfies FaapiConfig;\n * ```\n *\n * 启动时用 `faapi` 命令,自动集成 Next.js,无需写 custom server 代码。\n */\nconst nextPlugin: FaapiPlugin = {\n name: '@faapi/next',\n\n async setup(ctx: PluginContext): Promise<void> {\n const options = (ctx.options as NextPluginOptions) ?? {};\n const dev = options.dev ?? process.env.NODE_ENV !== 'production';\n // dir 相对于项目根目录解析(CLI 形态下 rootDir === process.cwd(),行为不变;\n // 库 API 形态下 rootDir 可能不同,按 rootDir 解析更合理)\n const dir = path.resolve(ctx.rootDir, options.dir ?? '.');\n const apiPrefix = options.apiPrefix ?? DEFAULT_API_PREFIX;\n const trustHostHeader = options.trustHostHeader ?? true;\n\n // 1. 动态 import next(peerDependency,未安装时报错)\n let nextFactory: (opts: { dev: boolean; dir: string; conf?: unknown }) => NextApp;\n try {\n nextFactory = (await import('next')).default as typeof nextFactory;\n } catch {\n throw new Error('[faapi-next] next 包未安装。请运行 `pnpm add next` 安装 Next.js。');\n }\n\n // 2. 构造 conf:若开启 trustHostHeader,加载用户 next.config.ts 并合并\n // experimental.trustHostHeader=true。loadConfig 返回完整配置(已 normalize\n // + 填充默认值),展开后覆盖 experimental 字段,再通过 conf 传给 next()。\n // 注意:传 conf 会让 Next.js 跳过读取 next.config.ts,所以这里必须加载\n // 用户配置以保留 next.config.ts 中的其他设置(images/rewrites 等)。\n let conf: Record<string, unknown> | undefined;\n if (trustHostHeader) {\n conf = await loadUserConfigAndMergeTrustHostHeader(dir, dev);\n }\n\n // 3. 启动 Next.js\n const nextApp = nextFactory(conf ? { dev, dir, conf } : { dev, dir });\n const nextHandle = nextApp.getRequestHandler();\n await nextApp.prepare();\n\n // 4. Next.js upgrade handler(dev 模式 HMR)\n const nextUpgradeHandler =\n typeof nextApp.getUpgradeHandler === 'function' ? nextApp.getUpgradeHandler() : null;\n\n // 5. 包装 HTTP handler:/api/* 走 faapi,其余走 Next.js\n ctx.wrapHandler?.((original: RequestHandler): RequestHandler => {\n return (req, res) => {\n const { pathname } = new URL(req.url ?? '/', 'http://localhost');\n if (isApiPath(pathname, apiPrefix)) {\n original(req, res);\n } else {\n // 不传 parsedUrl,让 Next.js 内部解析 req.url\n // NextUrlWithParsedQuery 是 url.parse() 返回的结构,不是 URL 对象\n // 用 Promise.resolve 包装,兼容 handler 返回 undefined 的情况\n Promise.resolve(nextHandle(req, res)).catch((err: unknown) => {\n console.error('[faapi-next] Next.js handler error:', err);\n if (!res.headersSent) {\n res.statusCode = 500;\n res.end('Next.js handler error');\n }\n });\n }\n };\n });\n\n // 6. 包装 WS upgrade handler:/api/* 走 faapi,其余走 Next.js HMR\n ctx.wrapUpgradeHandler?.((original: UpgradeHandler | undefined): UpgradeHandler => {\n return (req, socket, head) => {\n const { pathname } = new URL(req.url ?? '/', 'http://localhost');\n if (isApiPath(pathname, apiPrefix) && original) {\n original(req, socket, head);\n } else if (nextUpgradeHandler) {\n nextUpgradeHandler(req, socket, head);\n } else {\n socket.destroy();\n }\n };\n });\n\n console.log(\n `- Next.js integration: ${dev ? 'dev' : 'prod'} mode, dir=${dir}, apiPrefix=${apiPrefix}${conf ? ', trustHostHeader=auto' : ''}`,\n );\n },\n};\n\n/**\n * 加载用户 next.config.{js,ts,mjs},合并 experimental.trustHostHeader=true,\n * 返回可传给 next() conf 选项的配置对象。\n *\n * 用 Next.js 内部的 loadConfig 加载完整配置(已 normalize + 填充默认值),展开后\n * 覆盖 experimental.trustHostHeader。这样用户在 next.config.ts 中的其他配置\n * (images/rewrites/redirects 等)都会被保留。\n *\n * next() 收到 conf 后走 loadConfig 的 customConfig 分支(不读文件、不做 schema\n * 校验——校验只发生在从文件加载的分支),因此注入 trustHostHeader 不会触发\n * \"Unrecognized key(s)\" 告警;而从文件加载(用户手写该字段)会被 Next 16+ 的\n * config schema 拒绝。\n *\n * 失败时返回 undefined(退回不传 conf,Next.js 自己加载 next.config.ts,\n * trustHostHeader 不自动开启),并打印警告。警告不引导手写该字段(Next 16+\n * schema 校验会告警,形成死循环)。\n */\nasync function loadUserConfigAndMergeTrustHostHeader(\n dir: string,\n dev: boolean,\n): Promise<Record<string, unknown> | undefined> {\n try {\n // 插件是 ESM:Node 的 ESM 解析器不补全扩展名,specifier 必须带 .js;\n // Next 各版本 dist 布局不同(部分环境仅有 esm 布局),按候选顺序回退\n const constantsModule = await importNextInternalModule(['next/constants.js', 'next/constants']);\n const configModule = await importNextInternalModule([\n 'next/dist/server/config.js',\n 'next/dist/esm/server/config.js',\n ]);\n const loadConfig = resolveLoadConfigFunction(configModule);\n if (!loadConfig) {\n throw new Error('loadConfig is not a function');\n }\n const phase = dev\n ? (resolvePhaseConstant(constantsModule, 'PHASE_DEVELOPMENT_SERVER') ??\n 'phase-development-server')\n : (resolvePhaseConstant(constantsModule, 'PHASE_PRODUCTION_SERVER') ??\n 'phase-production-server');\n const userConfig = await loadConfig(phase, dir, { silent: true });\n if (!userConfig || typeof userConfig !== 'object') {\n return undefined;\n }\n const experimental = (userConfig.experimental as Record<string, unknown> | undefined) ?? {};\n // 用户已在 next.config.ts 显式开启则不重复设置\n // 想关闭用插件选项 trustHostHeader: false(跳过整个 loadConfig 分支)\n if (experimental.trustHostHeader === true) {\n return { ...userConfig };\n }\n return {\n ...userConfig,\n experimental: {\n ...experimental,\n trustHostHeader: true,\n },\n };\n } catch {\n console.warn(\n '[faapi-next] 自动开启 experimental.trustHostHeader 失败(无法加载 Next.js 内部 config 模块,conf 未注入)。\\n' +\n ' 请勿在 next.config.ts 手写 experimental.trustHostHeader 替代——Next 16+ 的 config\\n' +\n ' schema 不接受该字段,手写会触发 \"Unrecognized key(s)\" 告警。\\n' +\n ' 非反向代理场景可忽略本提示;如需关闭本提示,设插件选项 trustHostHeader: false。',\n );\n return undefined;\n }\n}\n\n/**\n * 按候选顺序依次 import,返回第一个成功的模块。\n *\n * 为什么不用单个 specifier:Next 内部模块路径属于非公开实现,各版本 dist 布局\n * 存在差异;全部失败返回 undefined,由调用方决定降级行为。\n */\nasync function importNextInternalModule(\n specifiers: string[],\n): Promise<Record<string, unknown> | undefined> {\n for (const specifier of specifiers) {\n try {\n return (await import(specifier)) as Record<string, unknown>;\n } catch {\n // 该布局在当前 next 版本不存在,尝试下一个候选\n }\n }\n return undefined;\n}\n\n/** loadConfig 函数签名(Next.js 内部 API:phase + dir + options) */\ntype LoadConfigFn = (\n phase: string,\n dir: string,\n opts?: unknown,\n) => Promise<Record<string, unknown>>;\n\n/**\n * 从 config 模块命名空间解析 loadConfig 函数。\n *\n * Next 16 的 config.js 是 CJS 但带 __esModule 编译标记(module.exports.default =\n * loadConfig),ESM 互操作后 namespace.default 是对象,函数在 namespace.default.default;\n * 常规 CJS(module.exports = fn)互操作后 namespace.default 即函数;ESM 原生同理。\n */\nfunction resolveLoadConfigFunction(\n configModule: Record<string, unknown> | undefined,\n): LoadConfigFn | undefined {\n const direct = configModule?.default;\n if (typeof direct === 'function') {\n return direct as LoadConfigFn;\n }\n const nested = (direct as Record<string, unknown> | undefined)?.default;\n if (typeof nested === 'function') {\n return nested as LoadConfigFn;\n }\n if (typeof configModule === 'function') {\n return configModule as unknown as LoadConfigFn;\n }\n return undefined;\n}\n\n/**\n * 从 constants 模块命名空间解析 phase 常量(顶层命名导出或 module.exports 内)。\n */\nfunction resolvePhaseConstant(\n constantsModule: Record<string, unknown> | undefined,\n key: string,\n): string | undefined {\n const topLevel = constantsModule?.[key];\n if (typeof topLevel === 'string') {\n return topLevel;\n }\n const fromDefault = (constantsModule?.default as Record<string, unknown> | undefined)?.[key];\n return typeof fromDefault === 'string' ? fromDefault : undefined;\n}\n\nexport default nextPlugin;\nexport type { FaapiPlugin, PluginContext, RequestHandler, UpgradeHandler } from '@faapi/faapi';\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faapi/next",
3
- "version": "4.2.0",
3
+ "version": "4.2.1",
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",
@@ -31,11 +31,11 @@
31
31
  "vitest": "^4.1.11",
32
32
  "ws": "^8.21.0",
33
33
  "zod": "^4.4.3",
34
- "@faapi/faapi": "4.2.0"
34
+ "@faapi/faapi": "4.2.1"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "next": ">=13.0.0",
38
- "@faapi/faapi": "^4.2.0"
38
+ "@faapi/faapi": "^4.2.1"
39
39
  },
40
40
  "peerDependenciesMeta": {
41
41
  "next": {