@taicode/common-server 3.8.0 → 4.0.0-beta.10
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/output/app/index.d.ts +18 -15
- package/output/app/index.d.ts.map +1 -1
- package/output/app/index.js +330 -9
- package/output/auth/index.d.ts +73 -0
- package/output/auth/index.d.ts.map +1 -0
- package/output/auth/index.js +196 -0
- package/output/core/assemble.d.ts +64 -0
- package/output/core/assemble.d.ts.map +1 -0
- package/output/core/assemble.js +88 -0
- package/output/core/boot/context.d.ts +109 -0
- package/output/core/boot/context.d.ts.map +1 -0
- package/output/core/boot/context.js +145 -0
- package/output/core/boot/lifecycle.d.ts +206 -0
- package/output/core/boot/lifecycle.d.ts.map +1 -0
- package/output/core/boot/lifecycle.js +255 -0
- package/output/core/boot/shutdown.d.ts +34 -0
- package/output/core/boot/shutdown.d.ts.map +1 -0
- package/output/core/boot/shutdown.js +50 -0
- package/output/core/boot/signal.d.ts +13 -0
- package/output/core/boot/signal.d.ts.map +1 -0
- package/output/{app → core/boot}/signal.js +5 -4
- package/output/core/boot/steps.d.ts +138 -0
- package/output/core/boot/steps.d.ts.map +1 -0
- package/output/core/boot/steps.js +259 -0
- package/output/core/entries.d.ts +151 -0
- package/output/core/entries.d.ts.map +1 -0
- package/output/core/entries.js +242 -0
- package/output/core/entries.test.d.ts +2 -0
- package/output/core/entries.test.d.ts.map +1 -0
- package/output/core/entries.test.js +458 -0
- package/output/core/env.d.ts +83 -0
- package/output/core/env.d.ts.map +1 -0
- package/output/core/env.js +124 -0
- package/output/{app → core}/env.test.d.ts.map +1 -1
- package/output/core/env.test.js +105 -0
- package/output/core/modules/auth-provider/index.d.ts +10 -0
- package/output/core/modules/auth-provider/index.d.ts.map +1 -0
- package/output/core/modules/auth-provider/index.js +9 -0
- package/output/core/modules/auth-provider/types.d.ts +71 -0
- package/output/core/modules/auth-provider/types.d.ts.map +1 -0
- package/output/core/modules/context/index.d.ts +18 -0
- package/output/core/modules/context/index.d.ts.map +1 -0
- package/output/core/modules/context/index.js +23 -0
- package/output/core/modules/context/index.test.d.ts.map +1 -0
- package/output/core/modules/cookie/index.d.ts +21 -0
- package/output/core/modules/cookie/index.d.ts.map +1 -0
- package/output/core/modules/cookie/index.js +17 -0
- package/output/core/modules/identity/index.d.ts +4 -0
- package/output/core/modules/identity/index.d.ts.map +1 -0
- package/output/core/modules/identity/index.js +14 -0
- package/output/core/modules/logging/index.d.ts +5 -0
- package/output/core/modules/logging/index.d.ts.map +1 -0
- package/output/core/modules/logging/index.js +29 -0
- package/output/core/modules/openapi/index.d.ts +7 -0
- package/output/core/modules/openapi/index.d.ts.map +1 -0
- package/output/core/modules/openapi/index.js +41 -0
- package/output/core/modules/permission-operations/index.d.ts +26 -0
- package/output/core/modules/permission-operations/index.d.ts.map +1 -0
- package/output/core/modules/permission-operations/index.js +36 -0
- package/output/core/modules/permission-operations/index.test.d.ts +2 -0
- package/output/core/modules/permission-operations/index.test.d.ts.map +1 -0
- package/output/core/modules/permission-operations/index.test.js +45 -0
- package/output/core/modules/request-id/index.d.ts +33 -0
- package/output/core/modules/request-id/index.d.ts.map +1 -0
- package/output/core/modules/request-id/index.js +66 -0
- package/output/core/modules/response/index.d.ts +50 -0
- package/output/core/modules/response/index.d.ts.map +1 -0
- package/output/core/modules/response/index.js +102 -0
- package/output/{schema/index.d.ts → core/modules/schema/builders.d.ts} +8 -1
- package/output/core/modules/schema/builders.d.ts.map +1 -0
- package/output/{schema/index.js → core/modules/schema/builders.js} +7 -4
- package/output/core/modules/schema/index.d.ts +34 -0
- package/output/core/modules/schema/index.d.ts.map +1 -0
- package/output/core/modules/schema/index.js +62 -0
- package/output/core/modules/schema/index.test.d.ts +2 -0
- package/output/core/modules/schema/index.test.d.ts.map +1 -0
- package/output/core/modules/schema/index.test.js +55 -0
- package/output/core/modules/system-routes/index.d.ts +15 -0
- package/output/core/modules/system-routes/index.d.ts.map +1 -0
- package/output/core/modules/system-routes/index.js +205 -0
- package/output/core/resource.d.ts +56 -0
- package/output/core/resource.d.ts.map +1 -0
- package/output/core/resource.js +56 -0
- package/output/core/test-support/database.d.ts +89 -0
- package/output/core/test-support/database.d.ts.map +1 -0
- package/output/core/test-support/database.js +131 -0
- package/output/core/test-support/database.test.d.ts.map +1 -0
- package/output/core/test-support/database.test.js +111 -0
- package/output/core/test-support/environment.d.ts +38 -0
- package/output/core/test-support/environment.d.ts.map +1 -0
- package/output/core/test-support/environment.js +45 -0
- package/output/core/test-support/service-context.d.ts +64 -0
- package/output/core/test-support/service-context.d.ts.map +1 -0
- package/output/core/test-support/service-context.js +63 -0
- package/output/core/test-support/service-context.test.d.ts +2 -0
- package/output/core/test-support/service-context.test.d.ts.map +1 -0
- package/output/core/test-support/service-context.test.js +55 -0
- package/output/core/test-support/test-server.d.ts +154 -0
- package/output/core/test-support/test-server.d.ts.map +1 -0
- package/output/core/test-support/test-server.js +114 -0
- package/output/core/test-support/test-server.test.d.ts +2 -0
- package/output/core/test-support/test-server.test.d.ts.map +1 -0
- package/output/core/test-support/test-server.test.js +296 -0
- package/output/core/test-support/testdata.d.ts +255 -0
- package/output/core/test-support/testdata.d.ts.map +1 -0
- package/output/core/test-support/testdata.js +271 -0
- package/output/core/test-support/testdata.test.d.ts +2 -0
- package/output/core/test-support/testdata.test.d.ts.map +1 -0
- package/output/core/test-support/testdata.test.js +334 -0
- package/output/core/type-augment.d.ts +34 -0
- package/output/core/type-augment.d.ts.map +1 -0
- package/output/core/type-augment.js +7 -0
- package/output/core/types.d.ts +582 -0
- package/output/core/types.d.ts.map +1 -0
- package/output/core/types.js +2 -0
- package/output/cors/index.d.ts +3 -0
- package/output/cors/index.d.ts.map +1 -0
- package/output/cors/index.js +28 -0
- package/output/index.d.ts +10 -4
- package/output/index.d.ts.map +1 -1
- package/output/index.js +25 -5
- package/output/rate-limit/index.d.ts +23 -0
- package/output/rate-limit/index.d.ts.map +1 -0
- package/output/rate-limit/index.js +43 -0
- package/output/{rate-limiter/rate-limiter.d.ts → rate-limit/limiter.d.ts} +1 -1
- package/output/rate-limit/limiter.d.ts.map +1 -0
- package/output/{rate-limiter → rate-limit}/memory-store.d.ts +6 -0
- package/output/rate-limit/memory-store.d.ts.map +1 -0
- package/output/{rate-limiter → rate-limit}/memory-store.js +8 -0
- package/output/{rate-limiter → rate-limit}/redis-store.d.ts +18 -0
- package/output/rate-limit/redis-store.d.ts.map +1 -0
- package/output/{rate-limiter → rate-limit}/redis-store.js +35 -0
- package/output/rate-limit/redis-store.test.d.ts.map +1 -0
- package/output/{rate-limiter → rate-limit}/redis-store.test.js +2 -2
- package/output/rate-limit/types.d.ts +45 -0
- package/output/rate-limit/types.d.ts.map +1 -0
- package/output/{redis-cache/redis-cache.d.ts → redis/cache.d.ts} +1 -1
- package/output/redis/cache.d.ts.map +1 -0
- package/output/redis/cache.test.d.ts +2 -0
- package/output/redis/cache.test.d.ts.map +1 -0
- package/output/{redis-cache/redis-cache.test.js → redis/cache.test.js} +1 -1
- package/output/redis/index.d.ts +50 -0
- package/output/redis/index.d.ts.map +1 -0
- package/output/redis/index.js +71 -0
- package/output/redis/index.test.d.ts +2 -0
- package/output/{context → redis}/index.test.d.ts.map +1 -1
- package/output/redis/index.test.js +55 -0
- package/output/redis-queue/index.js +1 -1
- package/output/redis-queue/test-helpers.d.ts +1 -1
- package/output/redis-queue/test-helpers.js +1 -1
- package/output/redis-queue/types.d.ts +3 -12
- package/output/redis-queue/types.d.ts.map +1 -1
- package/package.json +11 -7
- package/output/app/assemble.d.ts +0 -16
- package/output/app/assemble.d.ts.map +0 -1
- package/output/app/assemble.js +0 -124
- package/output/app/database.d.ts +0 -36
- package/output/app/database.d.ts.map +0 -1
- package/output/app/database.js +0 -40
- package/output/app/database.test.d.ts.map +0 -1
- package/output/app/database.test.js +0 -66
- package/output/app/default-modules.d.ts +0 -23
- package/output/app/default-modules.d.ts.map +0 -1
- package/output/app/default-modules.js +0 -40
- package/output/app/define-server.d.ts +0 -12
- package/output/app/define-server.d.ts.map +0 -1
- package/output/app/define-server.js +0 -289
- package/output/app/define-server.test.d.ts +0 -2
- package/output/app/define-server.test.d.ts.map +0 -1
- package/output/app/define-server.test.js +0 -299
- package/output/app/env.d.ts +0 -24
- package/output/app/env.d.ts.map +0 -1
- package/output/app/env.js +0 -38
- package/output/app/env.test.js +0 -78
- package/output/app/module.d.ts +0 -88
- package/output/app/module.d.ts.map +0 -1
- package/output/app/module.js +0 -34
- package/output/app/shutdown.d.ts +0 -9
- package/output/app/shutdown.d.ts.map +0 -1
- package/output/app/shutdown.js +0 -15
- package/output/app/shutdown.test.d.ts +0 -2
- package/output/app/shutdown.test.d.ts.map +0 -1
- package/output/app/shutdown.test.js +0 -149
- package/output/app/signal.d.ts +0 -12
- package/output/app/signal.d.ts.map +0 -1
- package/output/app/steps.d.ts +0 -17
- package/output/app/steps.d.ts.map +0 -1
- package/output/app/steps.js +0 -41
- package/output/app/types.d.ts +0 -149
- package/output/app/types.d.ts.map +0 -1
- package/output/context/index.d.ts +0 -6
- package/output/context/index.d.ts.map +0 -1
- package/output/context/index.js +0 -14
- package/output/rate-limiter/index.d.ts +0 -5
- package/output/rate-limiter/index.d.ts.map +0 -1
- package/output/rate-limiter/index.js +0 -4
- package/output/rate-limiter/memory-store.d.ts.map +0 -1
- package/output/rate-limiter/rate-limiter.d.ts.map +0 -1
- package/output/rate-limiter/redis-store.d.ts.map +0 -1
- package/output/rate-limiter/redis-store.test.d.ts.map +0 -1
- package/output/rate-limiter/types.d.ts +0 -27
- package/output/rate-limiter/types.d.ts.map +0 -1
- package/output/redis-cache/index.d.ts +0 -4
- package/output/redis-cache/index.d.ts.map +0 -1
- package/output/redis-cache/index.js +0 -1
- package/output/redis-cache/redis-cache.d.ts.map +0 -1
- package/output/redis-cache/redis-cache.test.d.ts +0 -2
- package/output/redis-cache/redis-cache.test.d.ts.map +0 -1
- package/output/schema/index.d.ts.map +0 -1
- /package/output/{app → core}/env.test.d.ts +0 -0
- /package/output/{app → core/modules/auth-provider}/types.js +0 -0
- /package/output/{context → core/modules/context}/index.test.d.ts +0 -0
- /package/output/{context → core/modules/context}/index.test.js +0 -0
- /package/output/{app → core/test-support}/database.test.d.ts +0 -0
- /package/output/{rate-limiter/rate-limiter.js → rate-limit/limiter.js} +0 -0
- /package/output/{rate-limiter → rate-limit}/redis-store.test.d.ts +0 -0
- /package/output/{rate-limiter → rate-limit}/types.js +0 -0
- /package/output/{redis-cache/redis-cache.js → redis/cache.js} +0 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { createAuthMiddleware } from '../auth/index.js';
|
|
3
|
+
import { defineServer, generateOpenApi, startServer } from './entries.js';
|
|
4
|
+
import { routerSchema } from './modules/schema/builders.js';
|
|
5
|
+
import { defineResource } from './resource.js';
|
|
6
|
+
const IDENTITY = { service: 'probe', title: 'Probe', version: '0.0.0' };
|
|
7
|
+
const ROUTE_SCHEMA = routerSchema({
|
|
8
|
+
operationId: 'probeGet',
|
|
9
|
+
summary: '探测',
|
|
10
|
+
description: '探测路由',
|
|
11
|
+
tags: ['探测'],
|
|
12
|
+
});
|
|
13
|
+
/**
|
|
14
|
+
* 出 spec 用的定义:`config` 工厂**故意抛错**、`middlewares` 工厂记调用次数。
|
|
15
|
+
*
|
|
16
|
+
* 两者都能跑通就证明:出 spec 路径**既不需要配置有效**,**也不装配任何中间件**
|
|
17
|
+
* (它的文档只取决于路由声明 + zod 转换)。
|
|
18
|
+
*/
|
|
19
|
+
function buildServer(registerPermissionOperations, onMiddlewares = vi.fn()) {
|
|
20
|
+
return defineServer({
|
|
21
|
+
app: IDENTITY,
|
|
22
|
+
resources: () => ({}),
|
|
23
|
+
config: () => {
|
|
24
|
+
throw new Error('config 不应在出 spec 路径被调用');
|
|
25
|
+
},
|
|
26
|
+
clients: () => ({}),
|
|
27
|
+
services: () => ({}),
|
|
28
|
+
middlewares: (ctx) => {
|
|
29
|
+
onMiddlewares();
|
|
30
|
+
return [
|
|
31
|
+
createAuthMiddleware(ctx, {
|
|
32
|
+
client: { verify: async () => ({ user: {}, token: 't' }), registerPermissionOperations },
|
|
33
|
+
}),
|
|
34
|
+
];
|
|
35
|
+
},
|
|
36
|
+
routers: ({ app }) => {
|
|
37
|
+
app.get('/probe', { schema: ROUTE_SCHEMA }, async () => ({ ok: true }));
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/** 启动用的定义:`config` 正常返回值(`startServer` 会跑它)。 */
|
|
42
|
+
function buildStartableServer(registerPermissionOperations) {
|
|
43
|
+
return defineServer({
|
|
44
|
+
app: IDENTITY,
|
|
45
|
+
resources: () => ({}),
|
|
46
|
+
config: () => ({ server: { cookieNames: ['token'] } }),
|
|
47
|
+
clients: () => ({}),
|
|
48
|
+
services: () => ({}),
|
|
49
|
+
middlewares: (ctx) => [
|
|
50
|
+
createAuthMiddleware(ctx, {
|
|
51
|
+
client: { verify: async () => ({ user: {}, token: 't' }), registerPermissionOperations },
|
|
52
|
+
}),
|
|
53
|
+
],
|
|
54
|
+
routers: ({ app }) => {
|
|
55
|
+
app.get('/probe', { schema: ROUTE_SCHEMA }, async () => ({ ok: true }));
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
describe('generateOpenApi', () => {
|
|
60
|
+
it('产出完整文档(含框架系统路由与业务路由)', async () => {
|
|
61
|
+
const spec = await generateOpenApi(buildServer(async () => { }));
|
|
62
|
+
expect(Object.keys(spec.paths ?? {})).toEqual(expect.arrayContaining(['/', '/health', '/system/openapi', '/system/info', '/probe']));
|
|
63
|
+
expect(spec.info).toEqual({ title: 'Probe', version: '0.0.0' });
|
|
64
|
+
expect(spec.openapi).toBe('3.0.3');
|
|
65
|
+
});
|
|
66
|
+
it('不向鉴权提供方提交权限目录(构建期不产生写操作)', async () => {
|
|
67
|
+
const registerPermissionOperations = vi.fn(async () => { });
|
|
68
|
+
await generateOpenApi(buildServer(registerPermissionOperations));
|
|
69
|
+
expect(registerPermissionOperations).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
it('不装配中间件(文档只取决于路由声明,副作用随之消失)', async () => {
|
|
72
|
+
const onMiddlewares = vi.fn();
|
|
73
|
+
await generateOpenApi(buildServer(async () => { }, onMiddlewares));
|
|
74
|
+
// `auth` 单元在出文档路径里压根不会被构造——这是「不向鉴权方写数据」的根因,
|
|
75
|
+
// 而不是靠某个入口标记把它关掉。
|
|
76
|
+
expect(onMiddlewares).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
it('不构造 resources / config / clients / services(文档不来自它们,构造它们只为了跑服务)', async () => {
|
|
79
|
+
const onResources = vi.fn();
|
|
80
|
+
const onServices = vi.fn();
|
|
81
|
+
const onClients = vi.fn();
|
|
82
|
+
const server = defineServer({
|
|
83
|
+
app: IDENTITY,
|
|
84
|
+
resources: () => {
|
|
85
|
+
onResources();
|
|
86
|
+
return {};
|
|
87
|
+
},
|
|
88
|
+
config: () => {
|
|
89
|
+
throw new Error('config 不应在出 spec 路径被调用');
|
|
90
|
+
},
|
|
91
|
+
clients: () => {
|
|
92
|
+
onClients();
|
|
93
|
+
return {};
|
|
94
|
+
},
|
|
95
|
+
services: () => {
|
|
96
|
+
onServices();
|
|
97
|
+
return {};
|
|
98
|
+
},
|
|
99
|
+
middlewares: () => [],
|
|
100
|
+
routers: ({ app }) => {
|
|
101
|
+
app.get('/probe', { schema: ROUTE_SCHEMA }, async () => ({ ok: true }));
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
const spec = await generateOpenApi(server);
|
|
105
|
+
expect(onResources).not.toHaveBeenCalled();
|
|
106
|
+
expect(onClients).not.toHaveBeenCalled();
|
|
107
|
+
expect(onServices).not.toHaveBeenCalled();
|
|
108
|
+
// 传的是空 services 占位,路由仍能正常注册并进入文档
|
|
109
|
+
expect(Object.keys(spec.paths ?? {})).toContain('/probe');
|
|
110
|
+
});
|
|
111
|
+
it('不跑 config 槽位(出 spec 不要求配置有效)', async () => {
|
|
112
|
+
// `config` 工厂本身就会抛错,能出 spec 即证明它没被调用
|
|
113
|
+
await expect(generateOpenApi(buildServer(async () => { }))).resolves.toBeDefined();
|
|
114
|
+
});
|
|
115
|
+
it('不监听端口(不占用 API_PORT)', async () => {
|
|
116
|
+
const spec = await generateOpenApi(buildServer(async () => { }));
|
|
117
|
+
expect(spec.servers).toEqual([{ url: '/' }]);
|
|
118
|
+
});
|
|
119
|
+
it('业务路由违反 schema 契约时出 spec 直接失败(不给漏声明留活路)', async () => {
|
|
120
|
+
const server = defineServer({
|
|
121
|
+
app: IDENTITY,
|
|
122
|
+
resources: () => ({}),
|
|
123
|
+
config: () => ({}),
|
|
124
|
+
clients: () => ({}),
|
|
125
|
+
services: () => ({}),
|
|
126
|
+
middlewares: () => [],
|
|
127
|
+
routers: ({ app }) => {
|
|
128
|
+
// 故意不声明 schema,验证运行期守卫
|
|
129
|
+
app.get('/bad', async () => ({ ok: true }));
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
await expect(generateOpenApi(server)).rejects.toThrow(/GET \/bad 缺少 schema/);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
describe('startServer', () => {
|
|
136
|
+
// 入口一定 listen,所以这里要一个合法端口:`0` 让内核分配空闲端口,测试不抢固定端口
|
|
137
|
+
beforeAll(() => {
|
|
138
|
+
process.env.API_PORT = '0';
|
|
139
|
+
});
|
|
140
|
+
it('运行期照常提交权限目录(与出 spec 入口相反)', async () => {
|
|
141
|
+
const registerPermissionOperations = vi.fn(async () => { });
|
|
142
|
+
const app = await startServer(buildStartableServer(registerPermissionOperations));
|
|
143
|
+
// `startServer` 的契约就是「一定 listen」:端口交给内核分配(`API_PORT=0`),不靠开关
|
|
144
|
+
expect(app.server.listening).toBe(true);
|
|
145
|
+
// startServer 内部已 `ready()`,钩子应在返回前执行完
|
|
146
|
+
expect(registerPermissionOperations).toHaveBeenCalledTimes(1);
|
|
147
|
+
const [payload] = registerPermissionOperations.mock.calls[0];
|
|
148
|
+
expect(payload.service).toBe('probe');
|
|
149
|
+
expect(payload.version).toBe('0.0.0');
|
|
150
|
+
expect(payload.operations.map((item) => item.operation)).toEqual(expect.arrayContaining(['probeGet', 'getOpenApiDocument']));
|
|
151
|
+
await app.close();
|
|
152
|
+
});
|
|
153
|
+
it('clients 在 config 之后、services 之前构造,并被 services / middlewares / routers 共享同一实例', async () => {
|
|
154
|
+
const order = [];
|
|
155
|
+
const seen = {};
|
|
156
|
+
const server = defineServer({
|
|
157
|
+
app: IDENTITY,
|
|
158
|
+
resources: () => ({}),
|
|
159
|
+
config: () => {
|
|
160
|
+
order.push('config');
|
|
161
|
+
return { account: { serviceUrl: 'https://account.invalid' } };
|
|
162
|
+
},
|
|
163
|
+
clients: (ctx) => {
|
|
164
|
+
order.push('clients');
|
|
165
|
+
// 客户端地址来自业务配置(不是 env)——这正是它不能放进 resources 的原因
|
|
166
|
+
return { account: { baseUrl: ctx.config.account.serviceUrl } };
|
|
167
|
+
},
|
|
168
|
+
services: (ctx) => {
|
|
169
|
+
order.push('services');
|
|
170
|
+
// 两个 service 拿到的是**同一份**客户端实例:services 之间禁止互依,只能靠这一层共享
|
|
171
|
+
seen.serviceFirst = ctx.clients.account;
|
|
172
|
+
seen.serviceSecond = ctx.clients.account;
|
|
173
|
+
return {};
|
|
174
|
+
},
|
|
175
|
+
middlewares: (ctx) => {
|
|
176
|
+
seen.middleware = ctx.clients.account;
|
|
177
|
+
return [];
|
|
178
|
+
},
|
|
179
|
+
routers: (ctx) => {
|
|
180
|
+
order.push('routers');
|
|
181
|
+
seen.router = ctx.clients.account;
|
|
182
|
+
ctx.app.get('/probe', { schema: ROUTE_SCHEMA }, async () => ({ ok: true }));
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
const app = await startServer(server);
|
|
186
|
+
expect(order).toEqual(['config', 'clients', 'services', 'routers']);
|
|
187
|
+
expect(seen.serviceFirst).toEqual({ baseUrl: 'https://account.invalid' });
|
|
188
|
+
expect(seen.serviceSecond).toBe(seen.serviceFirst);
|
|
189
|
+
expect(seen.middleware).toBe(seen.serviceFirst);
|
|
190
|
+
expect(seen.router).toBe(seen.serviceFirst);
|
|
191
|
+
await app.close();
|
|
192
|
+
});
|
|
193
|
+
it('clients 阶段在 services 之后关闭(服务 dispose 时外部客户端仍可用)', async () => {
|
|
194
|
+
const order = [];
|
|
195
|
+
const server = defineServer({
|
|
196
|
+
app: IDENTITY,
|
|
197
|
+
resources: () => ({}),
|
|
198
|
+
config: () => ({}),
|
|
199
|
+
clients: (ctx) => {
|
|
200
|
+
ctx.disposer.add(() => {
|
|
201
|
+
order.push('clients');
|
|
202
|
+
});
|
|
203
|
+
return {};
|
|
204
|
+
},
|
|
205
|
+
services: (ctx) => {
|
|
206
|
+
ctx.disposer.add(() => {
|
|
207
|
+
order.push('services');
|
|
208
|
+
});
|
|
209
|
+
return {};
|
|
210
|
+
},
|
|
211
|
+
middlewares: () => [],
|
|
212
|
+
routers: () => { },
|
|
213
|
+
});
|
|
214
|
+
const app = await startServer(server);
|
|
215
|
+
await app.close();
|
|
216
|
+
expect(order).toEqual(['services', 'clients']);
|
|
217
|
+
});
|
|
218
|
+
it('middlewares 按**数组顺序**注册(顺序即书写顺序,不受键名影响)', async () => {
|
|
219
|
+
const order = [];
|
|
220
|
+
const unit = (name) => ({ name, register: () => { order.push(name); } });
|
|
221
|
+
const server = defineServer({
|
|
222
|
+
app: IDENTITY,
|
|
223
|
+
resources: () => ({}),
|
|
224
|
+
config: () => ({}),
|
|
225
|
+
services: () => ({}),
|
|
226
|
+
// 故意用「纯数字会被对象重排」的反例命名,证明数组顺序不被引擎重排
|
|
227
|
+
middlewares: () => [unit('2-first'), unit('1-second'), unit('cors')],
|
|
228
|
+
routers: ({ app }) => {
|
|
229
|
+
app.get('/probe', { schema: ROUTE_SCHEMA }, async () => ({ ok: true }));
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
const app = await startServer(server);
|
|
233
|
+
expect(order).toEqual(['2-first', '1-second', 'cors']);
|
|
234
|
+
await app.close();
|
|
235
|
+
});
|
|
236
|
+
it('裸注册函数也能进数组(框架补占位名)', async () => {
|
|
237
|
+
let called = false;
|
|
238
|
+
const server = defineServer({
|
|
239
|
+
app: IDENTITY,
|
|
240
|
+
resources: () => ({}),
|
|
241
|
+
config: () => ({}),
|
|
242
|
+
services: () => ({}),
|
|
243
|
+
middlewares: () => [() => { called = true; }],
|
|
244
|
+
routers: ({ app }) => {
|
|
245
|
+
app.get('/probe', { schema: ROUTE_SCHEMA }, async () => ({ ok: true }));
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
const app = await startServer(server);
|
|
249
|
+
expect(called).toBe(true);
|
|
250
|
+
await app.close();
|
|
251
|
+
});
|
|
252
|
+
it('clients 槽位可省略(CL 退化为空,services/middlewares/routers 照常)', async () => {
|
|
253
|
+
const server = defineServer({
|
|
254
|
+
app: IDENTITY,
|
|
255
|
+
resources: () => ({}),
|
|
256
|
+
config: () => ({}),
|
|
257
|
+
// 不写 clients
|
|
258
|
+
services: () => ({}),
|
|
259
|
+
middlewares: () => [],
|
|
260
|
+
routers: ({ app }) => {
|
|
261
|
+
app.get('/probe', { schema: ROUTE_SCHEMA }, async () => ({ ok: true }));
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
const app = await startServer(server);
|
|
265
|
+
const res = await app.inject({ method: 'GET', url: '/probe' });
|
|
266
|
+
expect(res.statusCode).toBe(200);
|
|
267
|
+
await app.close();
|
|
268
|
+
});
|
|
269
|
+
it('env 扩展:手写首位泛型 Env 即可(defineServer<Env>({...})),值取内置加载后的 process.env(框架不提供自定义加载)', async () => {
|
|
270
|
+
const seen = {};
|
|
271
|
+
// 内置加载逻辑把它读进来;应用只在泛型上声明类型,不参与加载
|
|
272
|
+
process.env.SERVICE_TOKEN = 'from-process-env';
|
|
273
|
+
// Env 排泛型首位 → 可单独手写;ctx.env 随之带上 SERVICE_TOKEN 的类型
|
|
274
|
+
const server = defineServer({
|
|
275
|
+
app: IDENTITY,
|
|
276
|
+
resources: (ctx) => {
|
|
277
|
+
seen.fromResources = ctx.env.SERVICE_TOKEN;
|
|
278
|
+
return {};
|
|
279
|
+
},
|
|
280
|
+
config: (ctx) => {
|
|
281
|
+
// config 的 ctx.env 同样带上了扩展类型
|
|
282
|
+
seen.fromConfig = ctx.env.SERVICE_TOKEN;
|
|
283
|
+
return {};
|
|
284
|
+
},
|
|
285
|
+
services: () => ({}),
|
|
286
|
+
middlewares: () => [],
|
|
287
|
+
routers: ({ app }) => {
|
|
288
|
+
app.get('/probe', { schema: ROUTE_SCHEMA }, async () => ({ ok: true }));
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
const app = await startServer(server);
|
|
292
|
+
expect(seen.fromResources).toBe('from-process-env');
|
|
293
|
+
expect(seen.fromConfig).toBe(seen.fromResources);
|
|
294
|
+
delete process.env.SERVICE_TOKEN;
|
|
295
|
+
await app.close();
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
describe('resources 单元生命周期(setup / dispose)', () => {
|
|
299
|
+
/** 造一个「记录调用」的资源单元 */
|
|
300
|
+
function probeResource(order, name, lifecycle = {}) {
|
|
301
|
+
const { setup = true, dispose = true } = lifecycle;
|
|
302
|
+
const client = { name };
|
|
303
|
+
return defineResource(client, {
|
|
304
|
+
...(setup ? { setup: () => { order.push(`setup:${name}`); } } : {}),
|
|
305
|
+
...(dispose ? { dispose: () => { order.push(`dispose:${name}`); } } : {}),
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
it('setup 在 resources 之后、config 之前,逐项按声明序执行', async () => {
|
|
309
|
+
const order = [];
|
|
310
|
+
const server = defineServer({
|
|
311
|
+
app: IDENTITY,
|
|
312
|
+
resources: () => {
|
|
313
|
+
order.push('resources');
|
|
314
|
+
return { first: probeResource(order, 'first'), second: probeResource(order, 'second') };
|
|
315
|
+
},
|
|
316
|
+
config: () => {
|
|
317
|
+
order.push('config');
|
|
318
|
+
return {};
|
|
319
|
+
},
|
|
320
|
+
services: () => ({}),
|
|
321
|
+
middlewares: () => [],
|
|
322
|
+
routers: () => { },
|
|
323
|
+
});
|
|
324
|
+
const app = await startServer(server);
|
|
325
|
+
expect(order).toEqual(['resources', 'setup:first', 'setup:second', 'config']);
|
|
326
|
+
await app.close();
|
|
327
|
+
});
|
|
328
|
+
it('消费方拿到的是解包后的值,不是单元外壳', async () => {
|
|
329
|
+
const order = [];
|
|
330
|
+
let seen;
|
|
331
|
+
const server = defineServer({
|
|
332
|
+
app: IDENTITY,
|
|
333
|
+
resources: () => ({ database: probeResource(order, 'database') }),
|
|
334
|
+
config: (ctx) => {
|
|
335
|
+
seen = ctx.resources.database;
|
|
336
|
+
return {};
|
|
337
|
+
},
|
|
338
|
+
services: () => ({}),
|
|
339
|
+
middlewares: () => [],
|
|
340
|
+
routers: () => { },
|
|
341
|
+
});
|
|
342
|
+
const app = await startServer(server);
|
|
343
|
+
// 看到的是裸 client(`{ name: 'database' }`),没有 `value` / `setup` 外壳
|
|
344
|
+
expect(seen).toEqual({ name: 'database' });
|
|
345
|
+
await app.close();
|
|
346
|
+
});
|
|
347
|
+
it('裸对象(无单元外壳)照旧可用,不产生 setup / dispose 调用', async () => {
|
|
348
|
+
const order = [];
|
|
349
|
+
const server = defineServer({
|
|
350
|
+
app: IDENTITY,
|
|
351
|
+
resources: () => ({ bare: { name: 'bare' } }),
|
|
352
|
+
config: () => ({ picked: true }),
|
|
353
|
+
services: () => ({}),
|
|
354
|
+
middlewares: () => [],
|
|
355
|
+
routers: () => { },
|
|
356
|
+
});
|
|
357
|
+
const app = await startServer(server);
|
|
358
|
+
await app.close();
|
|
359
|
+
expect(order).toEqual([]);
|
|
360
|
+
});
|
|
361
|
+
it('setup 在每次装配时都跑(不做一次性记忆,迁移本身靠状态判定)', async () => {
|
|
362
|
+
const order = [];
|
|
363
|
+
const server = defineServer({
|
|
364
|
+
app: IDENTITY,
|
|
365
|
+
resources: () => ({ database: probeResource(order, 'database', { dispose: false }) }),
|
|
366
|
+
config: () => ({}),
|
|
367
|
+
services: () => ({}),
|
|
368
|
+
middlewares: () => [],
|
|
369
|
+
routers: () => { },
|
|
370
|
+
});
|
|
371
|
+
const first = await startServer(server);
|
|
372
|
+
await first.close();
|
|
373
|
+
const second = await startServer(server);
|
|
374
|
+
await second.close();
|
|
375
|
+
expect(order).toEqual(['setup:database', 'setup:database']);
|
|
376
|
+
});
|
|
377
|
+
it('setup 抛错时装配中断(config 不跑),错误冒泡给入口', async () => {
|
|
378
|
+
const order = [];
|
|
379
|
+
const server = defineServer({
|
|
380
|
+
app: IDENTITY,
|
|
381
|
+
resources: () => ({
|
|
382
|
+
database: defineResource({ name: 'database' }, {
|
|
383
|
+
setup: () => {
|
|
384
|
+
throw new Error('migrate failed');
|
|
385
|
+
},
|
|
386
|
+
}),
|
|
387
|
+
}),
|
|
388
|
+
config: () => {
|
|
389
|
+
order.push('config');
|
|
390
|
+
return {};
|
|
391
|
+
},
|
|
392
|
+
services: () => ({}),
|
|
393
|
+
middlewares: () => [],
|
|
394
|
+
routers: () => { },
|
|
395
|
+
});
|
|
396
|
+
await expect(startServer(server)).rejects.toThrow(/migrate failed/);
|
|
397
|
+
expect(order).toEqual([]);
|
|
398
|
+
});
|
|
399
|
+
it('dispose 按声明序**逆序**执行,排在 flushResources(disposer)之前', async () => {
|
|
400
|
+
const order = [];
|
|
401
|
+
const server = defineServer({
|
|
402
|
+
app: IDENTITY,
|
|
403
|
+
resources: (ctx) => {
|
|
404
|
+
ctx.disposer.add(() => {
|
|
405
|
+
order.push('disposer:resources');
|
|
406
|
+
});
|
|
407
|
+
return { first: probeResource(order, 'first'), second: probeResource(order, 'second') };
|
|
408
|
+
},
|
|
409
|
+
config: () => ({}),
|
|
410
|
+
services: () => ({}),
|
|
411
|
+
middlewares: () => [],
|
|
412
|
+
routers: () => { },
|
|
413
|
+
});
|
|
414
|
+
const app = await startServer(server);
|
|
415
|
+
order.length = 0;
|
|
416
|
+
await app.close();
|
|
417
|
+
expect(order).toEqual(['dispose:second', 'dispose:first', 'disposer:resources']);
|
|
418
|
+
});
|
|
419
|
+
it('单个资源 dispose 失败不阻断其他资源(同阶段语义)', async () => {
|
|
420
|
+
const order = [];
|
|
421
|
+
const server = defineServer({
|
|
422
|
+
app: IDENTITY,
|
|
423
|
+
resources: () => ({
|
|
424
|
+
broken: defineResource({ name: 'broken' }, {
|
|
425
|
+
dispose: () => {
|
|
426
|
+
throw new Error('disconnect failed');
|
|
427
|
+
},
|
|
428
|
+
}),
|
|
429
|
+
healthy: probeResource(order, 'healthy', { setup: false }),
|
|
430
|
+
}),
|
|
431
|
+
config: () => ({}),
|
|
432
|
+
services: () => ({}),
|
|
433
|
+
middlewares: () => [],
|
|
434
|
+
routers: () => { },
|
|
435
|
+
});
|
|
436
|
+
const app = await startServer(server);
|
|
437
|
+
await expect(app.close()).resolves.toBeUndefined();
|
|
438
|
+
// 逆序:healthy 先于 broken;broken 抛错被吞,healthy 已经跑过
|
|
439
|
+
expect(order).toEqual(['dispose:healthy']);
|
|
440
|
+
});
|
|
441
|
+
it('出 spec 路径既不构造 resources,也不跑 setup', async () => {
|
|
442
|
+
const onSetup = vi.fn();
|
|
443
|
+
const server = defineServer({
|
|
444
|
+
app: IDENTITY,
|
|
445
|
+
resources: () => ({ database: defineResource({ name: 'database' }, { setup: onSetup }) }),
|
|
446
|
+
config: () => {
|
|
447
|
+
throw new Error('config 不应在出 spec 路径被调用');
|
|
448
|
+
},
|
|
449
|
+
services: () => ({}),
|
|
450
|
+
middlewares: () => [],
|
|
451
|
+
routers: ({ app }) => {
|
|
452
|
+
app.get('/probe', { schema: ROUTE_SCHEMA }, async () => ({ ok: true }));
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
await generateOpenApi(server);
|
|
456
|
+
expect(onSetup).not.toHaveBeenCalled();
|
|
457
|
+
});
|
|
458
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** `LOG_LEVEL` 未设置时的默认级别(设计 §3.1 ① / §5.1 `logging`) */
|
|
2
|
+
export declare const DEFAULT_LOG_LEVEL = "info";
|
|
3
|
+
/**
|
|
4
|
+
* 允许执行**破坏性 / 不可逆**操作的环境(`ENV` 取值)。
|
|
5
|
+
*
|
|
6
|
+
* 覆盖两类动作,两者共用同一份判定:
|
|
7
|
+
*
|
|
8
|
+
* - 测试数据库清理(`assertTestDatabaseSchema` 那道闸门的同伴);
|
|
9
|
+
* - `testdata/` 脚本的 `load()` / `cleanup()`(`test-support/testdata.ts`)。
|
|
10
|
+
*
|
|
11
|
+
* `development` 是本地开发,`test` 是 vitest 入口显式声明的环境开关。除此之外一律拒绝——
|
|
12
|
+
* 只看 `NODE_ENV`、CI 标志或变量名都不构成证明:`NODE_ENV=test` 完全可能连着生产库。
|
|
13
|
+
*/
|
|
14
|
+
export declare const CLEANABLE_ENVS: readonly string[];
|
|
15
|
+
/**
|
|
16
|
+
* 环境闸门:非可清理环境立即失败。
|
|
17
|
+
*
|
|
18
|
+
* **不在分支里「跳过清理」**——把破坏性动作降级成静默 no-op,会让同一份代码在生产和测试里
|
|
19
|
+
* 走两条不同的路径,测试结果随即失去意义。要禁就整体失败。
|
|
20
|
+
*
|
|
21
|
+
* 报错必须带上当前值:漏配 `ENV` 时的症状否则会变成「莫名其妙不许跑」。
|
|
22
|
+
*/
|
|
23
|
+
export declare function assertCleanableEnvironment(env: string): void;
|
|
24
|
+
/**
|
|
25
|
+
* 框架在配置前固定加载的 env(N2 §2.2)。
|
|
26
|
+
*
|
|
27
|
+
* 只读固定约定值:`ENV` / `API_PORT` / `LOG_LEVEL` / `DATABASE_URL` / `REDIS_URL`;
|
|
28
|
+
* `debug` 由 `ENV === 'development'` 派生;**绝不回退 `PORT`**(`PORT` 属 nginx)。
|
|
29
|
+
*
|
|
30
|
+
* **不给索引签名**:访问 `env.XXX` 拼错即编译错误。各仓若有额外 env,不扩展这个类型 ——
|
|
31
|
+
* 走 `process.env.XXX`(在 `resources(ctx)` 里读),框架不背书。
|
|
32
|
+
*/
|
|
33
|
+
export interface ConfigEnv {
|
|
34
|
+
/** 运行环境标记;`debug` 由 `ENV === 'development'` 派生 */
|
|
35
|
+
ENV: string;
|
|
36
|
+
/** 监听端口;**只读 `API_PORT`,绝不回退 `PORT`** */
|
|
37
|
+
API_PORT: string;
|
|
38
|
+
/** 日志级别;默认 `info` */
|
|
39
|
+
LOG_LEVEL: string;
|
|
40
|
+
/** 数据层连接串(`resources` 只读它构造数据层客户端) */
|
|
41
|
+
DATABASE_URL: string;
|
|
42
|
+
/** Redis 连接串;缺省 → redis 资源为 `undefined` */
|
|
43
|
+
REDIS_URL?: string;
|
|
44
|
+
}
|
|
45
|
+
/** 读取固定 env(不加载文件);入口会先 `loadEnvironment()` 再调用本函数 */
|
|
46
|
+
export declare function resolveConfigEnv(): ConfigEnv;
|
|
47
|
+
/**
|
|
48
|
+
* env 加载模式:
|
|
49
|
+
*
|
|
50
|
+
* - `runtime`(默认)`startServer` 与工具进程用:三层开发分层,`ENV=test` 时再叠测试层。
|
|
51
|
+
* - `test` 测试入口用:**无条件**叠测试层,且三/五层都没给出 `ENV` 时兜底成 `test`。
|
|
52
|
+
*/
|
|
53
|
+
export type EnvironmentMode = 'runtime' | 'test';
|
|
54
|
+
/** `loadEnvironment()` 的选项 */
|
|
55
|
+
export interface LoadEnvironmentOptions {
|
|
56
|
+
mode?: EnvironmentMode;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* 加载 env 文件并在加载后返回固定约定值。
|
|
60
|
+
*
|
|
61
|
+
* **这是 env 的唯一出口**:层名、顺序、覆盖规则全在框架内部,业务与工具进程只表达
|
|
62
|
+
* 「我要运行期 env」还是「我要测试 env」(`mode: 'test'`),不接触 `.env.dev` /
|
|
63
|
+
* `.env.test.local` 这些文件名。
|
|
64
|
+
*
|
|
65
|
+
* 三种调用方:
|
|
66
|
+
*
|
|
67
|
+
* - `startServer`:`loadEnvironment()`(最早阶段固定调用);
|
|
68
|
+
* - `createTestServer`:经 `prepareTestDatabase()` 间接调用(再跑两道闸门);
|
|
69
|
+
* - 工具进程(`vitest.config.ts` / `prisma.config.ts` / `scripts/*.ts`):调
|
|
70
|
+
* `loadRuntimeEnvironment()`(它只是本函数的具名包装)—— 它们不被任何入口覆盖。
|
|
71
|
+
*
|
|
72
|
+
* `mode: 'test'` 的语义顺序:
|
|
73
|
+
*
|
|
74
|
+
* 1. 先加载三层开发分层,再叠测试层(两套 .env.test 逐层覆盖);
|
|
75
|
+
* 2. `loadLayers` 末尾把**外部注入**(进程 / 容器 / CI)原样恢复 —— 所以线上注入的
|
|
76
|
+
* `ENV=production` 一定压过仓库里的 `.env.test`,随后的环境闸门照旧拒绕启动;
|
|
77
|
+
* 3. 前面都没给出 `ENV` 时才兜底成 `test`:测试入口的默认身份就是测试,不该要求调用方
|
|
78
|
+
* 额外声明一次(否则漏写就变成「环境必须是 development/test」的莫名报错)。
|
|
79
|
+
*
|
|
80
|
+
* `generateOpenApi` **不**调用本函数(它不要求配置有效,设计 §3.2 规则 A/B)。
|
|
81
|
+
*/
|
|
82
|
+
export declare function loadEnvironment(options?: LoadEnvironmentOptions): ConfigEnv;
|
|
83
|
+
//# sourceMappingURL=env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../source/core/env.ts"],"names":[],"mappings":"AAgCA,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,SAAS,CAAA;AAEvC;;;;;;;;;;GAUG;AACH,eAAO,MAAM,cAAc,EAAE,SAAS,MAAM,EAA4B,CAAA;AAExE;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAU5D;AA2BD;;;;;;;;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,sDAAsD;AACtD,wBAAgB,gBAAgB,IAAI,SAAS,CAQ5C;AAED;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,MAAM,CAAA;AAEhD,8BAA8B;AAC9B,MAAM,WAAW,sBAAsB;IACrC,IAAI,CAAC,EAAE,eAAe,CAAA;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,eAAe,CAAC,OAAO,GAAE,sBAA2B,GAAG,SAAS,CAY/E"}
|
|
@@ -0,0 +1,124 @@
|
|
|
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
|
+
* **分层是框架的内部实现**:不导出、不参数化。调用方只表达「我要运行期 env」还是「我要测试
|
|
17
|
+
* env」(`mode`),层名与顺序不进入业务代码。
|
|
18
|
+
*/
|
|
19
|
+
const ENV_FILE_LAYERS = ['.env.dev', '.env', '.env.local'];
|
|
20
|
+
/**
|
|
21
|
+
* 测试层(叠在 `ENV_FILE_LAYERS` 之上,低 → 高):
|
|
22
|
+
*
|
|
23
|
+
* .env.test 测试环境默认值(可提交:`ENV=test`、测试库连接串)
|
|
24
|
+
* .env.test.local 本机覆盖(已被 gitignore)
|
|
25
|
+
*
|
|
26
|
+
* 测试入口**无条件**加载这两层(不等 `ENV === 'test'`):测试入口的存在本身就意味着「这是
|
|
27
|
+
* 测试」,让调用方再声明一次是多余且容易漏的。
|
|
28
|
+
*/
|
|
29
|
+
const TEST_ENV_FILE_LAYERS = ['.env.test', '.env.test.local'];
|
|
30
|
+
/** `LOG_LEVEL` 未设置时的默认级别(设计 §3.1 ① / §5.1 `logging`) */
|
|
31
|
+
export const DEFAULT_LOG_LEVEL = 'info';
|
|
32
|
+
/**
|
|
33
|
+
* 允许执行**破坏性 / 不可逆**操作的环境(`ENV` 取值)。
|
|
34
|
+
*
|
|
35
|
+
* 覆盖两类动作,两者共用同一份判定:
|
|
36
|
+
*
|
|
37
|
+
* - 测试数据库清理(`assertTestDatabaseSchema` 那道闸门的同伴);
|
|
38
|
+
* - `testdata/` 脚本的 `load()` / `cleanup()`(`test-support/testdata.ts`)。
|
|
39
|
+
*
|
|
40
|
+
* `development` 是本地开发,`test` 是 vitest 入口显式声明的环境开关。除此之外一律拒绝——
|
|
41
|
+
* 只看 `NODE_ENV`、CI 标志或变量名都不构成证明:`NODE_ENV=test` 完全可能连着生产库。
|
|
42
|
+
*/
|
|
43
|
+
export const CLEANABLE_ENVS = ['development', 'test'];
|
|
44
|
+
/**
|
|
45
|
+
* 环境闸门:非可清理环境立即失败。
|
|
46
|
+
*
|
|
47
|
+
* **不在分支里「跳过清理」**——把破坏性动作降级成静默 no-op,会让同一份代码在生产和测试里
|
|
48
|
+
* 走两条不同的路径,测试结果随即失去意义。要禁就整体失败。
|
|
49
|
+
*
|
|
50
|
+
* 报错必须带上当前值:漏配 `ENV` 时的症状否则会变成「莫名其妙不许跑」。
|
|
51
|
+
*/
|
|
52
|
+
export function assertCleanableEnvironment(env) {
|
|
53
|
+
if (CLEANABLE_ENVS.includes(env)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
throw new Error(`[env-gate] 拒绝在 ENV="${env || '<未设置>'}" 下执行会清理数据的操作:` +
|
|
57
|
+
`只接受 ${CLEANABLE_ENVS.join(' / ')}。` +
|
|
58
|
+
'会写入 / 清理 / truncate / reset 数据库的操作绝不能指向非开发环境。');
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 首次 import 时的真实环境变量快照(此时还没有任何 .env 文件被加载)。
|
|
62
|
+
* 加载结束后原样恢复,实现「外部注入 > .env.local > .env > .env.dev」。
|
|
63
|
+
*/
|
|
64
|
+
const EXTERNAL_ENV = new Map(Object.entries(process.env).filter((entry) => entry[1] !== undefined));
|
|
65
|
+
/**
|
|
66
|
+
* 按分层顺序加载 env 文件(内部实现,见上方「分层不导出」的约定)。
|
|
67
|
+
*
|
|
68
|
+
* @param extraLayers 追加在最高优先级的额外层
|
|
69
|
+
*/
|
|
70
|
+
function loadLayers(extraLayers = []) {
|
|
71
|
+
const layers = [...ENV_FILE_LAYERS, ...extraLayers];
|
|
72
|
+
layers.forEach((path, index) => {
|
|
73
|
+
// 第一层不覆盖已有值,后续层逐层覆盖前一层
|
|
74
|
+
config({ path, override: index > 0 });
|
|
75
|
+
});
|
|
76
|
+
for (const [key, value] of EXTERNAL_ENV) {
|
|
77
|
+
process.env[key] = value;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** 读取固定 env(不加载文件);入口会先 `loadEnvironment()` 再调用本函数 */
|
|
81
|
+
export function resolveConfigEnv() {
|
|
82
|
+
return {
|
|
83
|
+
ENV: process.env.ENV ?? '',
|
|
84
|
+
API_PORT: process.env.API_PORT ?? '',
|
|
85
|
+
LOG_LEVEL: process.env.LOG_LEVEL ?? DEFAULT_LOG_LEVEL,
|
|
86
|
+
DATABASE_URL: process.env.DATABASE_URL ?? '',
|
|
87
|
+
REDIS_URL: process.env.REDIS_URL,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* 加载 env 文件并在加载后返回固定约定值。
|
|
92
|
+
*
|
|
93
|
+
* **这是 env 的唯一出口**:层名、顺序、覆盖规则全在框架内部,业务与工具进程只表达
|
|
94
|
+
* 「我要运行期 env」还是「我要测试 env」(`mode: 'test'`),不接触 `.env.dev` /
|
|
95
|
+
* `.env.test.local` 这些文件名。
|
|
96
|
+
*
|
|
97
|
+
* 三种调用方:
|
|
98
|
+
*
|
|
99
|
+
* - `startServer`:`loadEnvironment()`(最早阶段固定调用);
|
|
100
|
+
* - `createTestServer`:经 `prepareTestDatabase()` 间接调用(再跑两道闸门);
|
|
101
|
+
* - 工具进程(`vitest.config.ts` / `prisma.config.ts` / `scripts/*.ts`):调
|
|
102
|
+
* `loadRuntimeEnvironment()`(它只是本函数的具名包装)—— 它们不被任何入口覆盖。
|
|
103
|
+
*
|
|
104
|
+
* `mode: 'test'` 的语义顺序:
|
|
105
|
+
*
|
|
106
|
+
* 1. 先加载三层开发分层,再叠测试层(两套 .env.test 逐层覆盖);
|
|
107
|
+
* 2. `loadLayers` 末尾把**外部注入**(进程 / 容器 / CI)原样恢复 —— 所以线上注入的
|
|
108
|
+
* `ENV=production` 一定压过仓库里的 `.env.test`,随后的环境闸门照旧拒绕启动;
|
|
109
|
+
* 3. 前面都没给出 `ENV` 时才兜底成 `test`:测试入口的默认身份就是测试,不该要求调用方
|
|
110
|
+
* 额外声明一次(否则漏写就变成「环境必须是 development/test」的莫名报错)。
|
|
111
|
+
*
|
|
112
|
+
* `generateOpenApi` **不**调用本函数(它不要求配置有效,设计 §3.2 规则 A/B)。
|
|
113
|
+
*/
|
|
114
|
+
export function loadEnvironment(options = {}) {
|
|
115
|
+
const test = options.mode === 'test';
|
|
116
|
+
loadLayers(test ? TEST_ENV_FILE_LAYERS : undefined);
|
|
117
|
+
if (!test && (process.env.ENV ?? '') === 'test') {
|
|
118
|
+
loadLayers(TEST_ENV_FILE_LAYERS);
|
|
119
|
+
}
|
|
120
|
+
if (test && (process.env.ENV ?? '') === '') {
|
|
121
|
+
process.env.ENV = 'test';
|
|
122
|
+
}
|
|
123
|
+
return resolveConfigEnv();
|
|
124
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.test.d.ts","sourceRoot":"","sources":["../../source/
|
|
1
|
+
{"version":3,"file":"env.test.d.ts","sourceRoot":"","sources":["../../source/core/env.test.ts"],"names":[],"mappings":""}
|