@taicode/common-server 3.7.13 → 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 (130) 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.d.ts +2 -0
  38. package/output/app/define-server.test.d.ts.map +1 -0
  39. package/output/app/define-server.test.js +97 -0
  40. package/output/app/entries.d.ts +29 -0
  41. package/output/app/entries.d.ts.map +1 -0
  42. package/output/app/entries.js +64 -0
  43. package/output/app/env.d.ts +59 -0
  44. package/output/app/env.d.ts.map +1 -0
  45. package/output/app/env.js +66 -0
  46. package/output/app/env.test.d.ts +2 -0
  47. package/output/app/env.test.d.ts.map +1 -0
  48. package/output/app/env.test.js +124 -0
  49. package/output/app/error-contract.test.d.ts +2 -0
  50. package/output/app/error-contract.test.d.ts.map +1 -0
  51. package/output/app/error-contract.test.js +125 -0
  52. package/output/app/errors.d.ts +20 -0
  53. package/output/app/errors.d.ts.map +1 -0
  54. package/output/app/errors.js +51 -0
  55. package/output/app/generate-openapi.test.d.ts +2 -0
  56. package/output/app/generate-openapi.test.d.ts.map +1 -0
  57. package/output/app/generate-openapi.test.js +123 -0
  58. package/output/app/index.d.ts +16 -0
  59. package/output/app/index.d.ts.map +1 -0
  60. package/output/app/index.js +18 -0
  61. package/output/app/lifecycle-logging.test.d.ts +2 -0
  62. package/output/app/lifecycle-logging.test.d.ts.map +1 -0
  63. package/output/app/lifecycle-logging.test.js +155 -0
  64. package/output/app/lifecycle.d.ts +70 -0
  65. package/output/app/lifecycle.d.ts.map +1 -0
  66. package/output/app/lifecycle.js +99 -0
  67. package/output/app/lifecycle.test.d.ts +2 -0
  68. package/output/app/lifecycle.test.d.ts.map +1 -0
  69. package/output/app/lifecycle.test.js +152 -0
  70. package/output/app/middleware/auth.d.ts +71 -0
  71. package/output/app/middleware/auth.d.ts.map +1 -0
  72. package/output/app/middleware/auth.js +190 -0
  73. package/output/app/middleware/context.d.ts +13 -0
  74. package/output/app/middleware/context.d.ts.map +1 -0
  75. package/output/app/middleware/context.js +15 -0
  76. package/output/app/middleware/cors.d.ts +14 -0
  77. package/output/app/middleware/cors.d.ts.map +1 -0
  78. package/output/app/middleware/cors.js +30 -0
  79. package/output/app/middleware/index.d.ts +17 -0
  80. package/output/app/middleware/index.d.ts.map +1 -0
  81. package/output/app/middleware/index.js +16 -0
  82. package/output/app/middleware/rate-limit.d.ts +24 -0
  83. package/output/app/middleware/rate-limit.d.ts.map +1 -0
  84. package/output/app/middleware/rate-limit.js +46 -0
  85. package/output/app/middleware-order.test.d.ts +2 -0
  86. package/output/app/middleware-order.test.d.ts.map +1 -0
  87. package/output/app/middleware-order.test.js +148 -0
  88. package/output/app/permission-operations.d.ts +48 -0
  89. package/output/app/permission-operations.d.ts.map +1 -0
  90. package/output/app/permission-operations.js +77 -0
  91. package/output/app/permission-operations.test.d.ts +2 -0
  92. package/output/app/permission-operations.test.d.ts.map +1 -0
  93. package/output/app/permission-operations.test.js +72 -0
  94. package/output/app/permission-registration.test.d.ts +2 -0
  95. package/output/app/permission-registration.test.d.ts.map +1 -0
  96. package/output/app/permission-registration.test.js +138 -0
  97. package/output/app/redis.d.ts +45 -0
  98. package/output/app/redis.d.ts.map +1 -0
  99. package/output/app/redis.js +61 -0
  100. package/output/app/registered-operations.test.d.ts +2 -0
  101. package/output/app/registered-operations.test.d.ts.map +1 -0
  102. package/output/app/registered-operations.test.js +57 -0
  103. package/output/app/request-id.test.d.ts +2 -0
  104. package/output/app/request-id.test.d.ts.map +1 -0
  105. package/output/app/request-id.test.js +130 -0
  106. package/output/app/resources.test.d.ts +2 -0
  107. package/output/app/resources.test.d.ts.map +1 -0
  108. package/output/app/resources.test.js +197 -0
  109. package/output/app/shutdown.d.ts +22 -0
  110. package/output/app/shutdown.d.ts.map +1 -0
  111. package/output/app/shutdown.js +30 -0
  112. package/output/app/shutdown.test.d.ts +2 -0
  113. package/output/app/shutdown.test.d.ts.map +1 -0
  114. package/output/app/shutdown.test.js +103 -0
  115. package/output/app/signal.d.ts +12 -0
  116. package/output/app/signal.d.ts.map +1 -0
  117. package/output/app/signal.js +45 -0
  118. package/output/app/smoke.test.d.ts +2 -0
  119. package/output/app/smoke.test.d.ts.map +1 -0
  120. package/output/app/smoke.test.js +210 -0
  121. package/output/app/test-support.d.ts +21 -0
  122. package/output/app/test-support.d.ts.map +1 -0
  123. package/output/app/test-support.js +24 -0
  124. package/output/app/type-augment.d.ts +34 -0
  125. package/output/app/type-augment.d.ts.map +1 -0
  126. package/output/app/type-augment.js +7 -0
  127. package/output/app/types.d.ts +211 -0
  128. package/output/app/types.d.ts.map +1 -0
  129. package/output/app/types.js +2 -0
  130. package/package.json +12 -5
@@ -0,0 +1,209 @@
1
+ import { defineServer, startServer } from './entries.js';
2
+ /**
3
+ * N2 §4 的 **15 条负向用例**:这些写法**必须编译失败**,全部由 `tsc --noEmit` 覆盖
4
+ * (**不进**运行期 vitest 断言)。
5
+ *
6
+ * 文件名刻意**不带** `.test.ts`:它由 `tsc --noEmit` 校验,不被 vitest 当作测试文件执行。
7
+ * 每个用例的 `@ts-expect-error` 必须**确有错误**,否则 tsc 会报「未使用」而失败。
8
+ *
9
+ * 说明:`defineServer` 的调用结果一律不用 —— 这些函数只作编译期证据。
10
+ */
11
+ const identity = { service: 'negative-cases', title: 'Negative Cases', version: '1.0.0' };
12
+ /** #1 `resources` 看不到 `config` */
13
+ export function resourcesCannotSeeConfig() {
14
+ defineServer({
15
+ app: identity,
16
+ // @ts-expect-error `config` 不存在于 ResourcesContext
17
+ resources: ({ config }) => ({ config }),
18
+ config: () => ({}),
19
+ services: () => ({}),
20
+ middlewares: () => [],
21
+ routers: () => undefined,
22
+ });
23
+ }
24
+ /** #2 `resources` 看不到 `services` */
25
+ export function resourcesCannotSeeServices() {
26
+ defineServer({
27
+ app: identity,
28
+ // @ts-expect-error `services` 不存在于 ResourcesContext
29
+ resources: ({ services }) => ({ services }),
30
+ config: () => ({}),
31
+ services: () => ({}),
32
+ middlewares: () => [],
33
+ routers: () => undefined,
34
+ });
35
+ }
36
+ /** #3 `config` 看不到 `services` */
37
+ export function configCannotSeeServices() {
38
+ defineServer({
39
+ app: identity,
40
+ resources: () => ({}),
41
+ config: (ctx) => {
42
+ // @ts-expect-error `services` 不存在于 ConfigContext
43
+ void ctx.services;
44
+ return {};
45
+ },
46
+ services: () => ({}),
47
+ middlewares: () => [],
48
+ routers: () => undefined,
49
+ });
50
+ }
51
+ /** #4 `services` 看不到 `services`(禁 service 互依) */
52
+ export function servicesCannotSeeServices() {
53
+ defineServer({
54
+ app: identity,
55
+ resources: () => ({}),
56
+ config: () => ({}),
57
+ services: (ctx) => {
58
+ // @ts-expect-error `services` 不存在于 ServiceContext(禁互依)
59
+ void ctx.services;
60
+ return {};
61
+ },
62
+ middlewares: () => [],
63
+ routers: () => undefined,
64
+ });
65
+ }
66
+ /** #5 `routers` 看不到 `resources`(禁绕层直连数据层) */
67
+ export function routersCannotSeeResources() {
68
+ defineServer({
69
+ app: identity,
70
+ resources: () => ({ database: {} }),
71
+ config: () => ({}),
72
+ services: () => ({}),
73
+ middlewares: () => [],
74
+ routers: (ctx) => {
75
+ // @ts-expect-error `resources` 不存在于 RouterContext
76
+ void ctx.resources.database;
77
+ },
78
+ });
79
+ }
80
+ /** #6 `routers` 看不到 `config` */
81
+ export function routersCannotSeeConfig() {
82
+ defineServer({
83
+ app: identity,
84
+ resources: () => ({}),
85
+ config: () => ({}),
86
+ services: () => ({}),
87
+ middlewares: () => [],
88
+ routers: (ctx) => {
89
+ // @ts-expect-error `config` 不存在于 RouterContext
90
+ void ctx.config;
91
+ },
92
+ });
93
+ }
94
+ /** #7 service 值声明 `dependsOn` → 该值塌成 `never` */
95
+ export function serviceValueCannotDeclareDependsOn() {
96
+ defineServer({
97
+ app: identity,
98
+ resources: () => ({}),
99
+ config: () => ({}),
100
+ // @ts-expect-error `dependsOn` 声明使该值塌成 never
101
+ services: () => ({ a: { dependsOn: ['b'] } }),
102
+ middlewares: () => [],
103
+ routers: () => undefined,
104
+ });
105
+ }
106
+ /** #8 service 值是原始类型 → 被拒(防把配置塞进 services) */
107
+ export function serviceValueCannotBePrimitive() {
108
+ defineServer({
109
+ app: identity,
110
+ resources: () => ({}),
111
+ config: () => ({}),
112
+ // @ts-expect-error 原始类型值被 ServicesShape 拒绝
113
+ services: () => ({ count: 1 }),
114
+ middlewares: () => [],
115
+ routers: () => undefined,
116
+ });
117
+ }
118
+ /** #9 `startServer` 只接受品牌产物,裸 `ServerConfig` 编译失败 */
119
+ export function startServerRejectsBareConfig(bare) {
120
+ // @ts-expect-error 缺 `__brand`,不是 Server
121
+ void startServer(bare);
122
+ }
123
+ /** #10 `app.version` 必填 */
124
+ export function appVersionIsRequired() {
125
+ defineServer({
126
+ // @ts-expect-error `AppIdentity.version` 必填
127
+ app: { service: 'no-version', title: 'No Version' },
128
+ resources: () => ({}),
129
+ config: () => ({}),
130
+ services: () => ({}),
131
+ middlewares: () => [],
132
+ routers: () => undefined,
133
+ });
134
+ }
135
+ /** #11 `cache: false` 静态关闭:键被 `Live<>` 从资源视图剔除 */
136
+ export function staticallyDisabledResourceIsRemoved() {
137
+ defineServer({
138
+ app: identity,
139
+ resources: () => ({ cache: false, database: { query: () => '' } }),
140
+ config: (ctx) => {
141
+ // @ts-expect-error `cache: false` 的键已被 `Live<>` 剔除
142
+ void ctx.cache;
143
+ return {};
144
+ },
145
+ services: () => ({}),
146
+ middlewares: () => [],
147
+ routers: () => undefined,
148
+ });
149
+ }
150
+ /** #12 `config` 看不到 `app` */
151
+ export function configCannotSeeApp() {
152
+ defineServer({
153
+ app: identity,
154
+ resources: () => ({}),
155
+ config: (ctx) => {
156
+ // @ts-expect-error `app` 不存在于 ConfigContext
157
+ void ctx.app;
158
+ return {};
159
+ },
160
+ services: () => ({}),
161
+ middlewares: () => [],
162
+ routers: () => undefined,
163
+ });
164
+ }
165
+ /** #13 `middlewares` 看不到 `resources`(中间件禁直连数据层) */
166
+ export function middlewaresCannotSeeResources() {
167
+ defineServer({
168
+ app: identity,
169
+ resources: () => ({ database: {} }),
170
+ config: () => ({}),
171
+ services: () => ({}),
172
+ middlewares: (ctx) => {
173
+ // @ts-expect-error `resources` 不存在于 MiddlewareContext
174
+ void ctx.resources.database;
175
+ return [];
176
+ },
177
+ routers: () => undefined,
178
+ });
179
+ }
180
+ /** #14 `middlewares` 看不到 `env` */
181
+ export function middlewaresCannotSeeEnv() {
182
+ defineServer({
183
+ app: identity,
184
+ resources: () => ({}),
185
+ config: () => ({}),
186
+ services: () => ({}),
187
+ middlewares: (ctx) => {
188
+ // @ts-expect-error `env` 不存在于 MiddlewareContext
189
+ void ctx.env;
190
+ return [];
191
+ },
192
+ routers: () => undefined,
193
+ });
194
+ }
195
+ /** #15 `services` 看不到 `env`(服务层禁读 env) */
196
+ export function servicesCannotSeeEnv() {
197
+ defineServer({
198
+ app: identity,
199
+ resources: () => ({}),
200
+ config: () => ({}),
201
+ services: (ctx) => {
202
+ // @ts-expect-error `env` 不存在于 ServiceContext
203
+ void ctx.env.DATABASE_URL;
204
+ return {};
205
+ },
206
+ middlewares: () => [],
207
+ routers: () => undefined,
208
+ });
209
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=define-server.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-server.test.d.ts","sourceRoot":"","sources":["../../source/app/define-server.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,97 @@
1
+ import { describe, expect, expectTypeOf, it } from 'vitest';
2
+ import { defineServer, isServer, SERVER_BRAND, generateOpenApi } from './entries.js';
3
+ /**
4
+ * `defineServer()`(N1 §1)——**纯定义**:产出一个 `Server` 对象,不建连、不起服务。
5
+ *
6
+ * 同一份定义可同时喂给「启动」与「出 spec」,这也是 `mode` 消失的原因。
7
+ */
8
+ describe('defineServer 纯定义(N1 §1)', () => {
9
+ it('返回带品牌字段的 Server,并原样保留六个槽位', () => {
10
+ let resourcesCalls = 0;
11
+ const config = {
12
+ app: { service: 'define-server-test', title: 'Define Server Test', version: '1.0.0' },
13
+ resources: () => {
14
+ resourcesCalls += 1;
15
+ return {};
16
+ },
17
+ config: () => ({ enabled: true }),
18
+ services: () => ({}),
19
+ middlewares: () => [],
20
+ routers: () => undefined,
21
+ };
22
+ const server = defineServer(config);
23
+ expect(server.__brand).toBe(SERVER_BRAND);
24
+ expect(server.app).toEqual({ service: 'define-server-test', title: 'Define Server Test', version: '1.0.0' });
25
+ expect(server.services).toBe(config.services);
26
+ expect(server.routers).toBe(config.routers);
27
+ // 纯定义:不跑任何工厂
28
+ expect(resourcesCalls).toBe(0);
29
+ });
30
+ it('isServer 只识别 defineServer 产物', () => {
31
+ const server = defineServer({
32
+ app: { service: 'is-server-test', title: 'Is Server Test', version: '1.0.0' },
33
+ resources: () => ({}),
34
+ config: () => ({}),
35
+ services: () => ({}),
36
+ middlewares: () => [],
37
+ routers: () => undefined,
38
+ });
39
+ expect(isServer(server)).toBe(true);
40
+ expect(isServer({ __brand: 'other' })).toBe(false);
41
+ expect(isServer(null)).toBe(false);
42
+ expect(isServer('taicode.server')).toBe(false);
43
+ });
44
+ it('定义产物三入口共用:generateOpenApi 只读定义,不跑 config 槽位', async () => {
45
+ let configCalls = 0;
46
+ const server = defineServer({
47
+ app: { service: 'define-shared', title: 'Define Shared', version: '1.0.0' },
48
+ resources: () => ({}),
49
+ config: () => {
50
+ configCalls += 1;
51
+ return {};
52
+ },
53
+ services: () => ({}),
54
+ middlewares: () => [],
55
+ routers: () => undefined,
56
+ });
57
+ await generateOpenApi(server);
58
+ expect(configCalls).toBe(0);
59
+ });
60
+ it('app.version 必填:缺 version 时 TypeScript 编译失败(无默认值)', () => {
61
+ // 纯类型约束:这里只靠 `tsc --noEmit` 校验。若 `version` 又变回可选,
62
+ // `@ts-expect-error` 会因「未使用」而报错,测试反而变红。
63
+ const build = () => ({
64
+ // @ts-expect-error 缺少必填 app.version
65
+ app: { service: 'missing-version', title: 'Missing Version' },
66
+ resources: () => ({}),
67
+ config: () => ({}),
68
+ services: () => ({}),
69
+ middlewares: () => [],
70
+ routers: () => undefined,
71
+ });
72
+ expect(build).toBeTypeOf('function');
73
+ });
74
+ it('R / C / S 全推导:middlewares 与 routers 的 ctx.services 是强类型实体(M5)', () => {
75
+ defineServer({
76
+ app: { service: 'infer', title: 'Infer', version: '1.0.0' },
77
+ resources: () => ({ database: { query: (sql) => sql } }),
78
+ config: ({ resources }) => ({ domain: resources.database.query('domain') }),
79
+ services: ({ resources, config }) => ({
80
+ user: {
81
+ find: (id) => `${config.domain}:${resources.database.query(id)}`,
82
+ },
83
+ }),
84
+ middlewares: (ctx) => {
85
+ // 类型自证:ctx.services 是 services 的返回类型(不是 unknown)
86
+ expectTypeOf(ctx.services.user.find).toEqualTypeOf();
87
+ // 类型自证:ctx.config 是 config 的返回类型
88
+ expectTypeOf(ctx.config.domain).toEqualTypeOf();
89
+ return [];
90
+ },
91
+ routers: (ctx) => {
92
+ expectTypeOf(ctx.services.user.find).toEqualTypeOf();
93
+ ctx.app.get('/user', () => ({ name: ctx.services.user.find('1') }));
94
+ },
95
+ });
96
+ });
97
+ });
@@ -0,0 +1,29 @@
1
+ import type { OpenApiDocument, Server, ServerConfig, StartServerOptions } from './types.js';
2
+ import type { FastifyInstance } from 'fastify';
3
+ export { SERVER_BRAND } from './types.js';
4
+ /**
5
+ * `defineServer()`(N1 §1)——**纯定义**:产出一个 `Server` 对象,不建连、不起服务。
6
+ *
7
+ * 启动是另一个函数(`startServer`)的事——同一份定义可同时喂给「启动」与「出 spec」
8
+ * (`generateOpenApi`)。调用方**不写泛型**:`R` / `C` / `S` 全部从工厂返回类型推导。
9
+ */
10
+ export declare function defineServer<R, C, S>(config: ServerConfig<R, C, S>): Server<R, C, S>;
11
+ /** 供内部/调试使用:判断是否为 `defineServer()` 产物 */
12
+ export declare function isServer(value: unknown): value is Server<unknown, unknown, unknown>;
13
+ /**
14
+ * `startServer()`——真正跑起来:构造资源、加载配置、建服务、装中间件、注册路由、listen、装信号。
15
+ *
16
+ * - 最早阶段**固定加载 env**(`.env.dev < .env < .env.local`;`ENV=test` 追加 `.env.test.local`)。
17
+ * - 返回**原生 `FastifyInstance`**:不包 `fastify-plugin`、不返回包装对象。
18
+ * - `listen` 默认 `NODE_ENV !== 'test'`(测试不监听)。
19
+ * - 硬性要求 `Server`(品牌)产物:传裸 `ServerConfig` 是编译错误。
20
+ */
21
+ export declare function startServer<R, C, S>(server: Server<R, C, S>, options?: StartServerOptions<R, C, S>): Promise<FastifyInstance>;
22
+ /**
23
+ * `generateOpenApi()`——只出 spec,跑到能拿到 schema 就停。
24
+ *
25
+ * **红线**:全程**不得要求配置有效** —— 不跑 `config` 槽位、不要求 `DATABASE_URL` / `REDIS_URL`
26
+ * 有效;`resources`(只构造不建连)/ `services` / 中间件注册 / 路由注册照常执行。
27
+ */
28
+ export declare function generateOpenApi<R, C, S>(server: Server<R, C, S>): Promise<OpenApiDocument>;
29
+ //# sourceMappingURL=entries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entries.d.ts","sourceRoot":"","sources":["../../source/app/entries.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAC3F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAE9C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAEzC;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAMpF;AAED,yCAAyC;AACzC,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAMnF;AAED;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,GAAE,kBAAkB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAavI;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,OAAO,CAAC,eAAe,CAAC,CAiBhG"}
@@ -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
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * 默认 env 文件分层(低 → 高优先级,后者覆盖前者):
3
+ *
4
+ * .env.dev 已提交进仓库的 dev 默认值(克隆后 `pnpm run dev` 开箱即用)
5
+ * .env 本机私有覆盖(已被 gitignore)
6
+ * .env.local 本机更高优先级覆盖(已被 gitignore)
7
+ *
8
+ * 真实环境变量(进程/容器注入)优先级最高,任何 .env 文件都不会覆盖它们 —— 这条保证让 CI、
9
+ * helm、`docker run -e` 等显式注入始终生效,也避免测试探针
10
+ * (`check-test-database-isolation`)注入的连接串被仓库里的默认值顶掉。
11
+ *
12
+ * 解析器与四家现状保持一致,统一使用 `dotenv`,以保证多行值、引号/转义、`export` 前缀等
13
+ * 细节语义完全等价(设计 §3.1 ①:与现状逐字一致)。
14
+ */
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";
20
+ /**
21
+ * 按分层顺序加载 env 文件(设计 §3.1 ①)。
22
+ *
23
+ * `startServer` 内部会**固定调用**本函数;工具进程(`vitest.config.ts` / `scripts/*.ts`)不在
24
+ * `startServer` 覆盖范围内,须自行显式调用本函数。
25
+ *
26
+ * @param extraLayers 追加在最高优先级的额外层,例如测试配置传 `['.env.test.local']`
27
+ */
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;
59
+ //# sourceMappingURL=env.d.ts.map
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,66 @@
1
+ import { config } from 'dotenv';
2
+ /**
3
+ * 默认 env 文件分层(低 → 高优先级,后者覆盖前者):
4
+ *
5
+ * .env.dev 已提交进仓库的 dev 默认值(克隆后 `pnpm run dev` 开箱即用)
6
+ * .env 本机私有覆盖(已被 gitignore)
7
+ * .env.local 本机更高优先级覆盖(已被 gitignore)
8
+ *
9
+ * 真实环境变量(进程/容器注入)优先级最高,任何 .env 文件都不会覆盖它们 —— 这条保证让 CI、
10
+ * helm、`docker run -e` 等显式注入始终生效,也避免测试探针
11
+ * (`check-test-database-isolation`)注入的连接串被仓库里的默认值顶掉。
12
+ *
13
+ * 解析器与四家现状保持一致,统一使用 `dotenv`,以保证多行值、引号/转义、`export` 前缀等
14
+ * 细节语义完全等价(设计 §3.1 ①:与现状逐字一致)。
15
+ */
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';
21
+ /**
22
+ * 首次 import 时的真实环境变量快照(此时还没有任何 .env 文件被加载)。
23
+ * 加载结束后原样恢复,实现「外部注入 > .env.local > .env > .env.dev」。
24
+ */
25
+ const EXTERNAL_ENV = new Map(Object.entries(process.env).filter((entry) => entry[1] !== undefined));
26
+ /**
27
+ * 按分层顺序加载 env 文件(设计 §3.1 ①)。
28
+ *
29
+ * `startServer` 内部会**固定调用**本函数;工具进程(`vitest.config.ts` / `scripts/*.ts`)不在
30
+ * `startServer` 覆盖范围内,须自行显式调用本函数。
31
+ *
32
+ * @param extraLayers 追加在最高优先级的额外层,例如测试配置传 `['.env.test.local']`
33
+ */
34
+ export function loadEnvFiles(extraLayers = []) {
35
+ const layers = [...ENV_FILE_LAYERS, ...extraLayers];
36
+ layers.forEach((path, index) => {
37
+ // 第一层不覆盖已有值,后续层逐层覆盖前一层
38
+ config({ path, override: index > 0 });
39
+ });
40
+ for (const [key, value] of EXTERNAL_ENV) {
41
+ process.env[key] = value;
42
+ }
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
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=env.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.test.d.ts","sourceRoot":"","sources":["../../source/app/env.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,124 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
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
+ */
11
+ const ENV_KEYS = [
12
+ 'LAYER',
13
+ 'OVERRIDE',
14
+ 'ONLY_DEV',
15
+ 'ONLY_ENV',
16
+ 'ONLY_LOCAL',
17
+ 'EXTERNAL_ONLY',
18
+ 'ONLY_TEST',
19
+ 'EXPORTED',
20
+ 'QUOTED',
21
+ 'MULTI',
22
+ 'HASHED',
23
+ 'ENV',
24
+ 'API_PORT',
25
+ 'PORT',
26
+ 'LOG_LEVEL',
27
+ ];
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 ①)', () => {
39
+ let previousCwd;
40
+ let directory;
41
+ beforeEach(() => {
42
+ previousCwd = process.cwd();
43
+ directory = mkdtempSync(join(tmpdir(), 'common-app-env-'));
44
+ writeFileSync(join(directory, '.env.dev'), 'LAYER=dev\nONLY_DEV=dev\nOVERRIDE=dev\n');
45
+ writeFileSync(join(directory, '.env'), 'LAYER=env\nONLY_ENV=env\nOVERRIDE=env\n');
46
+ writeFileSync(join(directory, '.env.local'), 'LAYER=local\nONLY_LOCAL=local\nOVERRIDE=local\n');
47
+ process.chdir(directory);
48
+ clearEnvKeys();
49
+ });
50
+ afterEach(() => {
51
+ process.chdir(previousCwd);
52
+ rmSync(directory, { recursive: true, force: true });
53
+ clearEnvKeys();
54
+ vi.resetModules();
55
+ });
56
+ it('分层优先级:外部注入 > .env.local > .env > .env.dev', async () => {
57
+ process.env.LAYER = 'external';
58
+ process.env.EXTERNAL_ONLY = 'external';
59
+ const { loadEnvFiles } = await importEnvModule();
60
+ loadEnvFiles();
61
+ expect(process.env.LAYER).toBe('external');
62
+ expect(process.env.EXTERNAL_ONLY).toBe('external');
63
+ expect(process.env.OVERRIDE).toBe('local');
64
+ expect(process.env.ONLY_DEV).toBe('dev');
65
+ expect(process.env.ONLY_ENV).toBe('env');
66
+ expect(process.env.ONLY_LOCAL).toBe('local');
67
+ });
68
+ it('追加层(如 .env.test.local)优先级最高', async () => {
69
+ writeFileSync(join(directory, '.env.test.local'), 'OVERRIDE=test-local\nONLY_TEST=1\n');
70
+ const { loadEnvFiles } = await importEnvModule();
71
+ loadEnvFiles(['.env.test.local']);
72
+ expect(process.env.OVERRIDE).toBe('test-local');
73
+ expect(process.env.ONLY_TEST).toBe('1');
74
+ });
75
+ it('缺失的 env 文件不报错,已有层照常生效', async () => {
76
+ rmSync(join(directory, '.env.local'));
77
+ const { loadEnvFiles } = await importEnvModule();
78
+ expect(() => loadEnvFiles()).not.toThrow();
79
+ expect(process.env.ONLY_DEV).toBe('dev');
80
+ expect(process.env.OVERRIDE).toBe('env');
81
+ });
82
+ it('dotenv 语义:export 前缀 / 引号 / 转义 / 多行 / 行尾注释(与四家现状逐字一致)', async () => {
83
+ writeFileSync(join(directory, '.env.local'), ['export EXPORTED=1', 'QUOTED="a b"', 'MULTI="line1\\nline2"', 'HASHED=value # 行尾注释'].join('\n'));
84
+ const { loadEnvFiles } = await importEnvModule();
85
+ loadEnvFiles();
86
+ expect(process.env.EXPORTED).toBe('1');
87
+ expect(process.env.QUOTED).toBe('a b');
88
+ expect(process.env.MULTI).toBe('line1\nline2');
89
+ expect(process.env.HASHED).toBe('value');
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
+ });
124
+ });