@taicode/common-server 3.8.0 → 4.0.0-beta.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.
Files changed (145) hide show
  1. package/output/app/auth.test.d.ts +2 -0
  2. package/output/app/auth.test.d.ts.map +1 -0
  3. package/output/app/auth.test.js +148 -0
  4. package/output/app/boot.d.ts +48 -0
  5. package/output/app/boot.d.ts.map +1 -0
  6. package/output/app/boot.js +123 -0
  7. package/output/app/config.test.d.ts +2 -0
  8. package/output/app/config.test.d.ts.map +1 -0
  9. package/output/app/config.test.js +132 -0
  10. package/output/app/context.d.ts +68 -0
  11. package/output/app/context.d.ts.map +1 -0
  12. package/output/app/context.js +73 -0
  13. package/output/app/core/cookie.d.ts +25 -0
  14. package/output/app/core/cookie.d.ts.map +1 -0
  15. package/output/app/core/cookie.js +22 -0
  16. package/output/app/core/identity.d.ts +12 -0
  17. package/output/app/core/identity.d.ts.map +1 -0
  18. package/output/app/core/identity.js +14 -0
  19. package/output/app/core/index.d.ts +44 -0
  20. package/output/app/core/index.d.ts.map +1 -0
  21. package/output/app/core/index.js +57 -0
  22. package/output/app/core/logging.d.ts +13 -0
  23. package/output/app/core/logging.d.ts.map +1 -0
  24. package/output/app/core/logging.js +29 -0
  25. package/output/app/core/openapi.d.ts +17 -0
  26. package/output/app/core/openapi.d.ts.map +1 -0
  27. package/output/app/core/openapi.js +42 -0
  28. package/output/app/core/request-id.d.ts +36 -0
  29. package/output/app/core/request-id.d.ts.map +1 -0
  30. package/output/app/core/request-id.js +66 -0
  31. package/output/app/core/response.d.ts +41 -0
  32. package/output/app/core/response.d.ts.map +1 -0
  33. package/output/app/core/response.js +89 -0
  34. package/output/app/define-server.negative-cases.d.ts +32 -0
  35. package/output/app/define-server.negative-cases.d.ts.map +1 -0
  36. package/output/app/define-server.negative-cases.js +209 -0
  37. package/output/app/define-server.test.js +86 -288
  38. package/output/app/entries.d.ts +29 -0
  39. package/output/app/entries.d.ts.map +1 -0
  40. package/output/app/entries.js +64 -0
  41. package/output/app/env.d.ts +39 -4
  42. package/output/app/env.d.ts.map +1 -1
  43. package/output/app/env.js +32 -4
  44. package/output/app/env.test.js +67 -21
  45. package/output/app/error-contract.test.d.ts +2 -0
  46. package/output/app/error-contract.test.d.ts.map +1 -0
  47. package/output/app/error-contract.test.js +125 -0
  48. package/output/app/errors.d.ts +20 -0
  49. package/output/app/errors.d.ts.map +1 -0
  50. package/output/app/errors.js +51 -0
  51. package/output/app/generate-openapi.test.d.ts +2 -0
  52. package/output/app/generate-openapi.test.d.ts.map +1 -0
  53. package/output/app/generate-openapi.test.js +123 -0
  54. package/output/app/index.d.ts +15 -15
  55. package/output/app/index.d.ts.map +1 -1
  56. package/output/app/index.js +18 -9
  57. package/output/app/lifecycle-logging.test.d.ts +2 -0
  58. package/output/app/lifecycle-logging.test.d.ts.map +1 -0
  59. package/output/app/lifecycle-logging.test.js +155 -0
  60. package/output/app/lifecycle.d.ts +70 -0
  61. package/output/app/lifecycle.d.ts.map +1 -0
  62. package/output/app/lifecycle.js +99 -0
  63. package/output/app/lifecycle.test.d.ts +2 -0
  64. package/output/app/lifecycle.test.d.ts.map +1 -0
  65. package/output/app/lifecycle.test.js +152 -0
  66. package/output/app/middleware/auth.d.ts +71 -0
  67. package/output/app/middleware/auth.d.ts.map +1 -0
  68. package/output/app/middleware/auth.js +190 -0
  69. package/output/app/middleware/context.d.ts +13 -0
  70. package/output/app/middleware/context.d.ts.map +1 -0
  71. package/output/app/middleware/context.js +15 -0
  72. package/output/app/middleware/cors.d.ts +14 -0
  73. package/output/app/middleware/cors.d.ts.map +1 -0
  74. package/output/app/middleware/cors.js +30 -0
  75. package/output/app/middleware/index.d.ts +17 -0
  76. package/output/app/middleware/index.d.ts.map +1 -0
  77. package/output/app/middleware/index.js +16 -0
  78. package/output/app/middleware/rate-limit.d.ts +24 -0
  79. package/output/app/middleware/rate-limit.d.ts.map +1 -0
  80. package/output/app/middleware/rate-limit.js +46 -0
  81. package/output/app/middleware-order.test.d.ts +2 -0
  82. package/output/app/middleware-order.test.d.ts.map +1 -0
  83. package/output/app/middleware-order.test.js +148 -0
  84. package/output/app/permission-operations.d.ts +48 -0
  85. package/output/app/permission-operations.d.ts.map +1 -0
  86. package/output/app/permission-operations.js +77 -0
  87. package/output/app/permission-operations.test.d.ts +2 -0
  88. package/output/app/permission-operations.test.d.ts.map +1 -0
  89. package/output/app/permission-operations.test.js +72 -0
  90. package/output/app/permission-registration.test.d.ts +2 -0
  91. package/output/app/permission-registration.test.d.ts.map +1 -0
  92. package/output/app/permission-registration.test.js +138 -0
  93. package/output/app/redis.d.ts +45 -0
  94. package/output/app/redis.d.ts.map +1 -0
  95. package/output/app/redis.js +61 -0
  96. package/output/app/registered-operations.test.d.ts +2 -0
  97. package/output/app/registered-operations.test.d.ts.map +1 -0
  98. package/output/app/registered-operations.test.js +57 -0
  99. package/output/app/request-id.test.d.ts +2 -0
  100. package/output/app/request-id.test.d.ts.map +1 -0
  101. package/output/app/request-id.test.js +130 -0
  102. package/output/app/resources.test.d.ts +2 -0
  103. package/output/app/resources.test.d.ts.map +1 -0
  104. package/output/app/resources.test.js +197 -0
  105. package/output/app/shutdown.d.ts +17 -4
  106. package/output/app/shutdown.d.ts.map +1 -1
  107. package/output/app/shutdown.js +27 -12
  108. package/output/app/shutdown.test.js +78 -124
  109. package/output/app/signal.d.ts +4 -4
  110. package/output/app/signal.d.ts.map +1 -1
  111. package/output/app/signal.js +3 -3
  112. package/output/app/smoke.test.d.ts +2 -0
  113. package/output/app/smoke.test.d.ts.map +1 -0
  114. package/output/app/smoke.test.js +210 -0
  115. package/output/app/test-support.d.ts +21 -0
  116. package/output/app/test-support.d.ts.map +1 -0
  117. package/output/app/test-support.js +24 -0
  118. package/output/app/type-augment.d.ts +34 -0
  119. package/output/app/type-augment.d.ts.map +1 -0
  120. package/output/app/type-augment.js +7 -0
  121. package/output/app/types.d.ts +185 -123
  122. package/output/app/types.d.ts.map +1 -1
  123. package/output/app/types.js +2 -1
  124. package/package.json +10 -6
  125. package/output/app/assemble.d.ts +0 -16
  126. package/output/app/assemble.d.ts.map +0 -1
  127. package/output/app/assemble.js +0 -124
  128. package/output/app/database.d.ts +0 -36
  129. package/output/app/database.d.ts.map +0 -1
  130. package/output/app/database.js +0 -40
  131. package/output/app/database.test.d.ts +0 -2
  132. package/output/app/database.test.d.ts.map +0 -1
  133. package/output/app/database.test.js +0 -66
  134. package/output/app/default-modules.d.ts +0 -23
  135. package/output/app/default-modules.d.ts.map +0 -1
  136. package/output/app/default-modules.js +0 -40
  137. package/output/app/define-server.d.ts +0 -12
  138. package/output/app/define-server.d.ts.map +0 -1
  139. package/output/app/define-server.js +0 -289
  140. package/output/app/module.d.ts +0 -88
  141. package/output/app/module.d.ts.map +0 -1
  142. package/output/app/module.js +0 -34
  143. package/output/app/steps.d.ts +0 -17
  144. package/output/app/steps.d.ts.map +0 -1
  145. package/output/app/steps.js +0 -41
@@ -0,0 +1,64 @@
1
+ import { bootServer, BOOT_PHASE_OPENAPI, BOOT_PHASE_START } from './boot.js';
2
+ import { loadRuntimeEnvFiles, resolveConfigEnv } from './env.js';
3
+ import { SERVER_BRAND } from './types.js';
4
+ export { SERVER_BRAND } from './types.js';
5
+ /**
6
+ * `defineServer()`(N1 §1)——**纯定义**:产出一个 `Server` 对象,不建连、不起服务。
7
+ *
8
+ * 启动是另一个函数(`startServer`)的事——同一份定义可同时喂给「启动」与「出 spec」
9
+ * (`generateOpenApi`)。调用方**不写泛型**:`R` / `C` / `S` 全部从工厂返回类型推导。
10
+ */
11
+ export function defineServer(config) {
12
+ const server = {
13
+ ...config,
14
+ __brand: SERVER_BRAND,
15
+ };
16
+ return server;
17
+ }
18
+ /** 供内部/调试使用:判断是否为 `defineServer()` 产物 */
19
+ export function isServer(value) {
20
+ return (typeof value === 'object' &&
21
+ value !== null &&
22
+ value.__brand === SERVER_BRAND);
23
+ }
24
+ /**
25
+ * `startServer()`——真正跑起来:构造资源、加载配置、建服务、装中间件、注册路由、listen、装信号。
26
+ *
27
+ * - 最早阶段**固定加载 env**(`.env.dev < .env < .env.local`;`ENV=test` 追加 `.env.test.local`)。
28
+ * - 返回**原生 `FastifyInstance`**:不包 `fastify-plugin`、不返回包装对象。
29
+ * - `listen` 默认 `NODE_ENV !== 'test'`(测试不监听)。
30
+ * - 硬性要求 `Server`(品牌)产物:传裸 `ServerConfig` 是编译错误。
31
+ */
32
+ export async function startServer(server, options = {}) {
33
+ const env = loadRuntimeEnvFiles();
34
+ const isTest = process.env.NODE_ENV === 'test';
35
+ const booted = await bootServer({
36
+ server,
37
+ env,
38
+ phase: BOOT_PHASE_START,
39
+ listen: options.listen ?? !isTest,
40
+ shutdown: options.shutdown,
41
+ });
42
+ return booted.app;
43
+ }
44
+ /**
45
+ * `generateOpenApi()`——只出 spec,跑到能拿到 schema 就停。
46
+ *
47
+ * **红线**:全程**不得要求配置有效** —— 不跑 `config` 槽位、不要求 `DATABASE_URL` / `REDIS_URL`
48
+ * 有效;`resources`(只构造不建连)/ `services` / 中间件注册 / 路由注册照常执行。
49
+ */
50
+ export async function generateOpenApi(server) {
51
+ // 不加载 .env 文件、不读配置
52
+ const env = resolveConfigEnv();
53
+ const booted = await bootServer({
54
+ server,
55
+ env,
56
+ phase: BOOT_PHASE_OPENAPI,
57
+ listen: false,
58
+ });
59
+ const app = booted.app;
60
+ if (typeof app.swagger !== 'function') {
61
+ throw new Error('[generateOpenApi] @fastify/swagger is not registered');
62
+ }
63
+ return app.swagger();
64
+ }
@@ -9,16 +9,51 @@
9
9
  * helm、`docker run -e` 等显式注入始终生效,也避免测试探针
10
10
  * (`check-test-database-isolation`)注入的连接串被仓库里的默认值顶掉。
11
11
  *
12
- * 解析器与 account / expense / notice / release 四家现状保持一致,统一使用 `dotenv`,
13
- * 以保证多行值、引号/转义、`export` 前缀等细节语义完全等价(design §6:与现状逐字一致)。
12
+ * 解析器与四家现状保持一致,统一使用 `dotenv`,以保证多行值、引号/转义、`export` 前缀等
13
+ * 细节语义完全等价(设计 §3.1 ①:与现状逐字一致)。
14
14
  */
15
15
  export declare const ENV_FILE_LAYERS: readonly [".env.dev", ".env", ".env.local"];
16
+ /** `ENV=test` 时追加的最高层(设计 §3.1 ①) */
17
+ export declare const TEST_ENV_FILE_LAYER = ".env.test.local";
18
+ /** `LOG_LEVEL` 未设置时的默认级别(设计 §3.1 ① / §5.1 `logging`) */
19
+ export declare const DEFAULT_LOG_LEVEL = "info";
16
20
  /**
17
- * 按分层顺序加载 env 文件(design 决策 #5:独立导出,不塞进 `defineServer`)。
21
+ * 按分层顺序加载 env 文件(设计 §3.1 ①)。
18
22
  *
19
- * 必须在任何 `config` 读取之前调用;测试 / 脚本同样复用本函数。
23
+ * `startServer` 内部会**固定调用**本函数;工具进程(`vitest.config.ts` / `scripts/*.ts`)不在
24
+ * `startServer` 覆盖范围内,须自行显式调用本函数。
20
25
  *
21
26
  * @param extraLayers 追加在最高优先级的额外层,例如测试配置传 `['.env.test.local']`
22
27
  */
23
28
  export declare function loadEnvFiles(extraLayers?: readonly string[]): void;
29
+ /**
30
+ * 框架在配置前固定加载的 env(N2 §2.2)。
31
+ *
32
+ * 只读固定约定值:`ENV` / `API_PORT` / `LOG_LEVEL` / `DATABASE_URL` / `REDIS_URL`;
33
+ * `debug` 由 `ENV === 'development'` 派生;**绝不回退 `PORT`**(`PORT` 属 nginx)。
34
+ *
35
+ * **不给索引签名**:访问 `env.XXX` 拼错即编译错误。各仓若有额外 env,不扩展这个类型 ——
36
+ * 走 `process.env.XXX`(在 `resources(ctx)` 里读),框架不背书。
37
+ */
38
+ export interface ConfigEnv {
39
+ /** 运行环境标记;`debug` 由 `ENV === 'development'` 派生 */
40
+ ENV: string;
41
+ /** 监听端口;**只读 `API_PORT`,绝不回退 `PORT`** */
42
+ API_PORT: string;
43
+ /** 日志级别;默认 `info` */
44
+ LOG_LEVEL: string;
45
+ /** 数据层连接串(`resources` 只读它构造数据层客户端) */
46
+ DATABASE_URL: string;
47
+ /** Redis 连接串;缺省 → redis 资源为 `undefined` */
48
+ REDIS_URL?: string;
49
+ }
50
+ /** 读取固定 env(不加载文件);`startServer` 会先 `loadRuntimeEnvFiles()` 再调用本函数 */
51
+ export declare function resolveConfigEnv(): ConfigEnv;
52
+ /**
53
+ * `startServer` 最早阶段的固定 env 加载(设计 §3.1 ①):
54
+ * `.env.dev < .env < .env.local`(外部注入最高),`ENV=test` 时再追加 `.env.test.local`。
55
+ *
56
+ * 注意 `generateOpenApi` **不**调用本函数(它不要求配置有效,设计 §3.2 规则 A/B)。
57
+ */
58
+ export declare function loadRuntimeEnvFiles(): ConfigEnv;
24
59
  //# sourceMappingURL=env.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../source/app/env.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,eAAe,6CAA8C,CAAA;AAU1E;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,WAAW,GAAE,SAAS,MAAM,EAAO,GAAG,IAAI,CAUtE"}
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../source/app/env.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,eAAe,6CAA8C,CAAA;AAE1E,oCAAoC;AACpC,eAAO,MAAM,mBAAmB,oBAAoB,CAAA;AAEpD,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,SAAS,CAAA;AAUvC;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,WAAW,GAAE,SAAS,MAAM,EAAO,GAAG,IAAI,CAUtE;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,SAAS;IACxB,kDAAkD;IAClD,GAAG,EAAE,MAAM,CAAA;IACX,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAA;IAChB,qBAAqB;IACrB,SAAS,EAAE,MAAM,CAAA;IACjB,sCAAsC;IACtC,YAAY,EAAE,MAAM,CAAA;IACpB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,sEAAsE;AACtE,wBAAgB,gBAAgB,IAAI,SAAS,CAQ5C;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,IAAI,SAAS,CAM/C"}
package/output/app/env.js CHANGED
@@ -10,19 +10,24 @@ import { config } from 'dotenv';
10
10
  * helm、`docker run -e` 等显式注入始终生效,也避免测试探针
11
11
  * (`check-test-database-isolation`)注入的连接串被仓库里的默认值顶掉。
12
12
  *
13
- * 解析器与 account / expense / notice / release 四家现状保持一致,统一使用 `dotenv`,
14
- * 以保证多行值、引号/转义、`export` 前缀等细节语义完全等价(design §6:与现状逐字一致)。
13
+ * 解析器与四家现状保持一致,统一使用 `dotenv`,以保证多行值、引号/转义、`export` 前缀等
14
+ * 细节语义完全等价(设计 §3.1 ①:与现状逐字一致)。
15
15
  */
16
16
  export const ENV_FILE_LAYERS = ['.env.dev', '.env', '.env.local'];
17
+ /** `ENV=test` 时追加的最高层(设计 §3.1 ①) */
18
+ export const TEST_ENV_FILE_LAYER = '.env.test.local';
19
+ /** `LOG_LEVEL` 未设置时的默认级别(设计 §3.1 ① / §5.1 `logging`) */
20
+ export const DEFAULT_LOG_LEVEL = 'info';
17
21
  /**
18
22
  * 首次 import 时的真实环境变量快照(此时还没有任何 .env 文件被加载)。
19
23
  * 加载结束后原样恢复,实现「外部注入 > .env.local > .env > .env.dev」。
20
24
  */
21
25
  const EXTERNAL_ENV = new Map(Object.entries(process.env).filter((entry) => entry[1] !== undefined));
22
26
  /**
23
- * 按分层顺序加载 env 文件(design 决策 #5:独立导出,不塞进 `defineServer`)。
27
+ * 按分层顺序加载 env 文件(设计 §3.1 ①)。
24
28
  *
25
- * 必须在任何 `config` 读取之前调用;测试 / 脚本同样复用本函数。
29
+ * `startServer` 内部会**固定调用**本函数;工具进程(`vitest.config.ts` / `scripts/*.ts`)不在
30
+ * `startServer` 覆盖范围内,须自行显式调用本函数。
26
31
  *
27
32
  * @param extraLayers 追加在最高优先级的额外层,例如测试配置传 `['.env.test.local']`
28
33
  */
@@ -36,3 +41,26 @@ export function loadEnvFiles(extraLayers = []) {
36
41
  process.env[key] = value;
37
42
  }
38
43
  }
44
+ /** 读取固定 env(不加载文件);`startServer` 会先 `loadRuntimeEnvFiles()` 再调用本函数 */
45
+ export function resolveConfigEnv() {
46
+ return {
47
+ ENV: process.env.ENV ?? '',
48
+ API_PORT: process.env.API_PORT ?? '',
49
+ LOG_LEVEL: process.env.LOG_LEVEL ?? DEFAULT_LOG_LEVEL,
50
+ DATABASE_URL: process.env.DATABASE_URL ?? '',
51
+ REDIS_URL: process.env.REDIS_URL,
52
+ };
53
+ }
54
+ /**
55
+ * `startServer` 最早阶段的固定 env 加载(设计 §3.1 ①):
56
+ * `.env.dev < .env < .env.local`(外部注入最高),`ENV=test` 时再追加 `.env.test.local`。
57
+ *
58
+ * 注意 `generateOpenApi` **不**调用本函数(它不要求配置有效,设计 §3.2 规则 A/B)。
59
+ */
60
+ export function loadRuntimeEnvFiles() {
61
+ loadEnvFiles();
62
+ if ((process.env.ENV ?? '') === 'test') {
63
+ loadEnvFiles([TEST_ENV_FILE_LAYER]);
64
+ }
65
+ return resolveConfigEnv();
66
+ }
@@ -2,6 +2,12 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
2
  import { tmpdir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
+ /**
6
+ * env 加载契约(设计 §3.1 ①)。
7
+ *
8
+ * 固定约定:`.env.dev` < `.env` < `.env.local`(外部注入最高),`ENV=test` 追加 `.env.test.local`;
9
+ * 固定读取 `ENV` / `API_PORT` / `LOG_LEVEL`,端口**只读 `API_PORT`,绝不回退 `PORT`**。
10
+ */
5
11
  const ENV_KEYS = [
6
12
  'LAYER',
7
13
  'OVERRIDE',
@@ -9,12 +15,27 @@ const ENV_KEYS = [
9
15
  'ONLY_ENV',
10
16
  'ONLY_LOCAL',
11
17
  'EXTERNAL_ONLY',
18
+ 'ONLY_TEST',
12
19
  'EXPORTED',
13
20
  'QUOTED',
14
21
  'MULTI',
15
22
  'HASHED',
23
+ 'ENV',
24
+ 'API_PORT',
25
+ 'PORT',
26
+ 'LOG_LEVEL',
16
27
  ];
17
- describe('loadEnvFiles', () => {
28
+ function clearEnvKeys() {
29
+ for (const key of ENV_KEYS) {
30
+ delete process.env[key];
31
+ }
32
+ }
33
+ /** 重新求值 `env.js`(`EXTERNAL_ENV` 快照在模块首次求值时抓取) */
34
+ async function importEnvModule() {
35
+ vi.resetModules();
36
+ return import('./env.js');
37
+ }
38
+ describe('env 加载(设计 §3.1 ①)', () => {
18
39
  let previousCwd;
19
40
  let directory;
20
41
  beforeEach(() => {
@@ -24,20 +45,18 @@ describe('loadEnvFiles', () => {
24
45
  writeFileSync(join(directory, '.env'), 'LAYER=env\nONLY_ENV=env\nOVERRIDE=env\n');
25
46
  writeFileSync(join(directory, '.env.local'), 'LAYER=local\nONLY_LOCAL=local\nOVERRIDE=local\n');
26
47
  process.chdir(directory);
48
+ clearEnvKeys();
27
49
  });
28
50
  afterEach(() => {
29
51
  process.chdir(previousCwd);
30
52
  rmSync(directory, { recursive: true, force: true });
31
- for (const key of ENV_KEYS) {
32
- delete process.env[key];
33
- }
53
+ clearEnvKeys();
34
54
  vi.resetModules();
35
55
  });
36
- it('优先级:外部注入 > .env.local > .env > .env.dev', async () => {
56
+ it('分层优先级:外部注入 > .env.local > .env > .env.dev', async () => {
37
57
  process.env.LAYER = 'external';
38
58
  process.env.EXTERNAL_ONLY = 'external';
39
- vi.resetModules();
40
- const { loadEnvFiles } = await import('./env.js');
59
+ const { loadEnvFiles } = await importEnvModule();
41
60
  loadEnvFiles();
42
61
  expect(process.env.LAYER).toBe('external');
43
62
  expect(process.env.EXTERNAL_ONLY).toBe('external');
@@ -47,32 +66,59 @@ describe('loadEnvFiles', () => {
47
66
  expect(process.env.ONLY_LOCAL).toBe('local');
48
67
  });
49
68
  it('追加层(如 .env.test.local)优先级最高', async () => {
50
- writeFileSync(join(directory, '.env.test.local'), 'OVERRIDE=test-local\n');
51
- vi.resetModules();
52
- const { loadEnvFiles } = await import('./env.js');
69
+ writeFileSync(join(directory, '.env.test.local'), 'OVERRIDE=test-local\nONLY_TEST=1\n');
70
+ const { loadEnvFiles } = await importEnvModule();
53
71
  loadEnvFiles(['.env.test.local']);
54
72
  expect(process.env.OVERRIDE).toBe('test-local');
73
+ expect(process.env.ONLY_TEST).toBe('1');
55
74
  });
56
- it('缺失文件不报错', async () => {
75
+ it('缺失的 env 文件不报错,已有层照常生效', async () => {
57
76
  rmSync(join(directory, '.env.local'));
58
- vi.resetModules();
59
- const { loadEnvFiles } = await import('./env.js');
77
+ const { loadEnvFiles } = await importEnvModule();
60
78
  expect(() => loadEnvFiles()).not.toThrow();
61
79
  expect(process.env.ONLY_DEV).toBe('dev');
80
+ expect(process.env.OVERRIDE).toBe('env');
62
81
  });
63
82
  it('dotenv 语义:export 前缀 / 引号 / 转义 / 多行 / 行尾注释(与四家现状逐字一致)', async () => {
64
- writeFileSync(join(directory, '.env.local'), [
65
- 'export EXPORTED=1',
66
- 'QUOTED="a b"',
67
- 'MULTI="line1\\nline2"',
68
- 'HASHED=value # 行尾注释',
69
- ].join('\n'));
70
- vi.resetModules();
71
- const { loadEnvFiles } = await import('./env.js');
83
+ writeFileSync(join(directory, '.env.local'), ['export EXPORTED=1', 'QUOTED="a b"', 'MULTI="line1\\nline2"', 'HASHED=value # 行尾注释'].join('\n'));
84
+ const { loadEnvFiles } = await importEnvModule();
72
85
  loadEnvFiles();
73
86
  expect(process.env.EXPORTED).toBe('1');
74
87
  expect(process.env.QUOTED).toBe('a b');
75
88
  expect(process.env.MULTI).toBe('line1\nline2');
76
89
  expect(process.env.HASHED).toBe('value');
77
90
  });
91
+ it('loadRuntimeEnvFiles:ENV=test 时追加 .env.test.local', async () => {
92
+ writeFileSync(join(directory, '.env.dev'), 'LAYER=dev\nOVERRIDE=dev\nENV=test\nAPI_PORT=4321\n');
93
+ writeFileSync(join(directory, '.env.test.local'), 'LOG_LEVEL=debug\nONLY_TEST=1\n');
94
+ const { loadRuntimeEnvFiles } = await importEnvModule();
95
+ const env = loadRuntimeEnvFiles();
96
+ expect(env.ENV).toBe('test');
97
+ expect(env.API_PORT).toBe('4321');
98
+ expect(env.LOG_LEVEL).toBe('debug');
99
+ expect(process.env.ONLY_TEST).toBe('1');
100
+ });
101
+ it('loadRuntimeEnvFiles:ENV 非 test 时不加载 .env.test.local', async () => {
102
+ writeFileSync(join(directory, '.env.local'), 'ENV=development\n');
103
+ writeFileSync(join(directory, '.env.test.local'), 'LOG_LEVEL=debug\nONLY_TEST=1\n');
104
+ const { loadRuntimeEnvFiles } = await importEnvModule();
105
+ const env = loadRuntimeEnvFiles();
106
+ expect(env.ENV).toBe('development');
107
+ expect(env.LOG_LEVEL).toBe('info');
108
+ expect(process.env.ONLY_TEST).toBeUndefined();
109
+ });
110
+ it('固定读取:只认 API_PORT(绝不回退 PORT),LOG_LEVEL 默认 info,ENV 缺省空串', async () => {
111
+ process.env.PORT = '9999';
112
+ const { resolveConfigEnv } = await importEnvModule();
113
+ const env = resolveConfigEnv();
114
+ expect(env.API_PORT).toBe('');
115
+ expect(env.LOG_LEVEL).toBe('info');
116
+ expect(env.ENV).toBe('');
117
+ });
118
+ it('固有常量:分层顺序 / 测试层 / 默认日志级别', async () => {
119
+ const { ENV_FILE_LAYERS, TEST_ENV_FILE_LAYER, DEFAULT_LOG_LEVEL } = await importEnvModule();
120
+ expect(ENV_FILE_LAYERS).toEqual(['.env.dev', '.env', '.env.local']);
121
+ expect(TEST_ENV_FILE_LAYER).toBe('.env.test.local');
122
+ expect(DEFAULT_LOG_LEVEL).toBe('info');
123
+ });
78
124
  });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=error-contract.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-contract.test.d.ts","sourceRoot":"","sources":["../../source/app/error-contract.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,125 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { z } from 'zod';
3
+ import { defineServer, startServer } from './entries.js';
4
+ import { SystemError, UserError } from './errors.js';
5
+ /**
6
+ * 错误契约——**强制、不可配置**。
7
+ *
8
+ * 响应信封 `{ status, message, data }`(错误时 `data: null`,`status` 放错误码字符串);
9
+ * 映射固定:`UserError`→400(`error.status` 字面量)/ `SystemError`→500 / zod→400 / 未知→500 /
10
+ * 未匹配路由→404。错误只给稳定**码**,文案不归框架。
11
+ */
12
+ /** zod validator 由 core 的 `schema` 统一装配,运行时按 zod 校验(类型上仍是 FastifySchema) */
13
+ const validatedQuerySchema = {
14
+ querystring: z.object({ count: z.coerce.number().int().min(1) }),
15
+ };
16
+ function createErrorContractServer() {
17
+ return defineServer({
18
+ app: { service: 'error-contract', title: 'Error Contract', version: '1.0.0' },
19
+ resources: () => ({}),
20
+ config: () => ({}),
21
+ services: () => ({}),
22
+ middlewares: () => [],
23
+ routers: (ctx) => {
24
+ ctx.app.get('/ok', () => ({ hello: 'world' }));
25
+ ctx.app.get('/envelope', () => ({ status: 'CUSTOM', message: 'm', data: 1 }));
26
+ ctx.app.get('/user-error', () => {
27
+ throw new UserError('INVALID_TOKEN', undefined, 'token 无效');
28
+ });
29
+ ctx.app.get('/system-error', () => {
30
+ throw new SystemError('DATABASE_UNAVAILABLE', undefined, '数据库不可用');
31
+ });
32
+ ctx.app.get('/unknown-error', () => {
33
+ throw new Error('boom');
34
+ });
35
+ ctx.app.get('/validated', { schema: validatedQuerySchema }, (request) => ({
36
+ count: request.query.count,
37
+ }));
38
+ },
39
+ });
40
+ }
41
+ describe('错误契约', () => {
42
+ it('成功信封:{ status: SUCCESS, message: ok, data }', async () => {
43
+ const app = await startServer(createErrorContractServer(), { listen: false });
44
+ const response = await app.inject({ method: 'GET', url: '/ok' });
45
+ expect(response.statusCode).toBe(200);
46
+ expect(response.json()).toEqual({ status: 'SUCCESS', message: 'ok', data: { hello: 'world' } });
47
+ await app.close();
48
+ });
49
+ it('已是信封的载荷不被二次包裹', async () => {
50
+ const app = await startServer(createErrorContractServer(), { listen: false });
51
+ const response = await app.inject({ method: 'GET', url: '/envelope' });
52
+ expect(response.statusCode).toBe(200);
53
+ expect(response.json()).toEqual({ status: 'CUSTOM', message: 'm', data: 1 });
54
+ await app.close();
55
+ });
56
+ it('UserError → 400(取 `error.status` 字面量),status 放错误码、data 为 null', async () => {
57
+ const app = await startServer(createErrorContractServer(), { listen: false });
58
+ const response = await app.inject({ method: 'GET', url: '/user-error' });
59
+ expect(response.statusCode).toBe(400);
60
+ expect(response.json()).toEqual({ status: 'INVALID_TOKEN', message: 'token 无效', data: null });
61
+ await app.close();
62
+ });
63
+ it('SystemError → 500(取 `error.status` 字面量)', async () => {
64
+ const app = await startServer(createErrorContractServer(), { listen: false });
65
+ const response = await app.inject({ method: 'GET', url: '/system-error' });
66
+ expect(response.statusCode).toBe(500);
67
+ expect(response.json()).toEqual({ status: 'DATABASE_UNAVAILABLE', message: '数据库不可用', data: null });
68
+ await app.close();
69
+ });
70
+ it('未知错误 → 500 + UNKNOWN_ERROR', async () => {
71
+ const app = await startServer(createErrorContractServer(), { listen: false });
72
+ const response = await app.inject({ method: 'GET', url: '/unknown-error' });
73
+ expect(response.statusCode).toBe(500);
74
+ expect(response.json()).toEqual({ status: 'UNKNOWN_ERROR', message: 'boom', data: null });
75
+ await app.close();
76
+ });
77
+ it('zod 校验失败 → 400(INVALID_INPUT);通过校验 → 200', async () => {
78
+ const app = await startServer(createErrorContractServer(), { listen: false });
79
+ const invalid = await app.inject({ method: 'GET', url: '/validated?count=0' });
80
+ expect(invalid.statusCode).toBe(400);
81
+ const invalidBody = invalid.json();
82
+ expect(invalidBody.status).toBe('INVALID_INPUT');
83
+ expect(invalidBody.data).toBeNull();
84
+ expect(invalidBody.message).toContain('count');
85
+ const valid = await app.inject({ method: 'GET', url: '/validated?count=3' });
86
+ expect(valid.statusCode).toBe(200);
87
+ expect(valid.json()).toEqual({ status: 'SUCCESS', message: 'ok', data: { count: 3 } });
88
+ await app.close();
89
+ });
90
+ it('未匹配路由 → 404 + NOT_IMPLEMENTED', async () => {
91
+ const app = await startServer(createErrorContractServer(), { listen: false });
92
+ const response = await app.inject({ method: 'GET', url: '/missing' });
93
+ expect(response.statusCode).toBe(404);
94
+ const body = response.json();
95
+ expect(body.status).toBe('NOT_IMPLEMENTED');
96
+ expect(body.data).toBeNull();
97
+ expect(body.message).toContain('GET /missing');
98
+ await app.close();
99
+ });
100
+ it('资源包里的 `classifyError` 被用作数据层错误分类(→500)', async () => {
101
+ const server = defineServer({
102
+ app: { service: 'error-contract-database', title: 'Error Contract Database', version: '1.0.0' },
103
+ resources: () => ({
104
+ database: {
105
+ classifyError: (error) => error instanceof Error && error.message === 'pg down'
106
+ ? new SystemError('DATABASE_ERROR', { operation: 'query' }, 'pg down')
107
+ : null,
108
+ },
109
+ }),
110
+ config: () => ({}),
111
+ services: () => ({}),
112
+ middlewares: () => [],
113
+ routers: (ctx) => {
114
+ ctx.app.get('/database-down', () => {
115
+ throw new Error('pg down');
116
+ });
117
+ },
118
+ });
119
+ const app = await startServer(server, { listen: false });
120
+ const response = await app.inject({ method: 'GET', url: '/database-down' });
121
+ expect(response.statusCode).toBe(500);
122
+ expect(response.json()).toEqual({ status: 'DATABASE_ERROR', message: 'pg down', data: null });
123
+ await app.close();
124
+ });
125
+ });
@@ -0,0 +1,20 @@
1
+ import type { ErrorContext } from '@taicode/common-base';
2
+ export declare class UserError extends Error {
3
+ readonly type: string;
4
+ readonly context?: ErrorContext | undefined;
5
+ /** 类型守卫(`Symbol.for` 品牌,不用 `instanceof`) */
6
+ static is(value: unknown): value is UserError;
7
+ /** 固定 HTTP 状态(字面量类型) */
8
+ readonly status: 400;
9
+ constructor(type: string, context?: ErrorContext | undefined, message?: string);
10
+ }
11
+ export declare class SystemError extends Error {
12
+ readonly type: string;
13
+ readonly context?: ErrorContext | undefined;
14
+ /** 类型守卫(`Symbol.for` 品牌,不用 `instanceof`) */
15
+ static is(value: unknown): value is SystemError;
16
+ /** 固定 HTTP 状态(字面量类型) */
17
+ readonly status: 500;
18
+ constructor(type: string, context?: ErrorContext | undefined, message?: string);
19
+ }
20
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../source/app/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAsBxD,qBAAa,SAAU,SAAQ,KAAK;aASC,IAAI,EAAE,MAAM;aAAkB,OAAO,CAAC,EAAE,YAAY;IARvF,4CAA4C;IAC5C,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,SAAS;IAI7C,wBAAwB;IACxB,QAAQ,CAAC,MAAM,EAAG,GAAG,CAAS;gBAEK,IAAI,EAAE,MAAM,EAAkB,OAAO,CAAC,EAAE,YAAY,YAAA,EAAE,OAAO,SAAK;CAKtG;AAED,qBAAa,WAAY,SAAQ,KAAK;aASD,IAAI,EAAE,MAAM;aAAkB,OAAO,CAAC,EAAE,YAAY;IARvF,4CAA4C;IAC5C,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW;IAI/C,wBAAwB;IACxB,QAAQ,CAAC,MAAM,EAAG,GAAG,CAAS;gBAEK,IAAI,EAAE,MAAM,EAAkB,OAAO,CAAC,EAAE,YAAY,YAAA,EAAE,OAAO,SAAK;CAKtG"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 错误契约(N2 §2.12)。
3
+ *
4
+ * 框架直接提供并导出 `UserError` / `SystemError`:
5
+ * - `status` 是**字面量**(`400` / `500`)—— 映射表写成
6
+ * `Record<UserError['status'] | SystemError['status'], number>` 时若改错直接编译失败;
7
+ * - `is()` 是**类型守卫**,走 `Symbol.for` 品牌(**不用 `instanceof`**:跨包 / 编译后 / 原型链丢失
8
+ * 都仍然可靠);品牌与 `@taicode/common-base` 同名,故基座抛出的同类错误同样被识别;
9
+ * - 业务只抛这两个错误,状态码映射由框架固定(`UserError`→400 / `SystemError`→500)。
10
+ *
11
+ * 构造签名保持 `(code, context?, message?)`。
12
+ */
13
+ const USER_ERROR_BRAND = Symbol.for('@taicode/common/UserError');
14
+ const SYSTEM_ERROR_BRAND = Symbol.for('@taicode/common/SystemError');
15
+ function hasBrand(value, brand) {
16
+ return typeof value === 'object' && value !== null && value[brand] === true;
17
+ }
18
+ export class UserError extends Error {
19
+ type;
20
+ context;
21
+ /** 类型守卫(`Symbol.for` 品牌,不用 `instanceof`) */
22
+ static is(value) {
23
+ return hasBrand(value, USER_ERROR_BRAND);
24
+ }
25
+ /** 固定 HTTP 状态(字面量类型) */
26
+ status = 400;
27
+ constructor(type, context, message = '') {
28
+ super(message, context);
29
+ this.type = type;
30
+ this.context = context;
31
+ this.name = 'UserError';
32
+ Object.defineProperty(this, USER_ERROR_BRAND, { value: true });
33
+ }
34
+ }
35
+ export class SystemError extends Error {
36
+ type;
37
+ context;
38
+ /** 类型守卫(`Symbol.for` 品牌,不用 `instanceof`) */
39
+ static is(value) {
40
+ return hasBrand(value, SYSTEM_ERROR_BRAND);
41
+ }
42
+ /** 固定 HTTP 状态(字面量类型) */
43
+ status = 500;
44
+ constructor(type, context, message = '') {
45
+ super(message, context);
46
+ this.type = type;
47
+ this.context = context;
48
+ this.name = 'SystemError';
49
+ Object.defineProperty(this, SYSTEM_ERROR_BRAND, { value: true });
50
+ }
51
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=generate-openapi.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-openapi.test.d.ts","sourceRoot":"","sources":["../../source/app/generate-openapi.test.ts"],"names":[],"mappings":""}