@jintianxiayu/cache-decorator 1.0.0 → 1.0.2
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/CHANGELOG.md +25 -11
- package/README.md +364 -270
- package/dist/core/cache-error.d.ts +75 -0
- package/dist/core/cache-error.d.ts.map +1 -0
- package/dist/core/cache-error.js +168 -0
- package/dist/core/cache-error.js.map +1 -0
- package/dist/core/cache-logger.d.ts +3 -2
- package/dist/core/cache-logger.d.ts.map +1 -1
- package/dist/core/cache-logger.js +2 -0
- package/dist/core/cache-logger.js.map +1 -1
- package/dist/decorators/cache-evict.d.ts.map +1 -1
- package/dist/decorators/cache-evict.js +15 -10
- package/dist/decorators/cache-evict.js.map +1 -1
- package/dist/decorators/cache.d.ts +6 -0
- package/dist/decorators/cache.d.ts.map +1 -1
- package/dist/decorators/cache.js +113 -36
- package/dist/decorators/cache.js.map +1 -1
- package/jest.config.js +11 -11
- package/package.json +1 -1
- package/src/adapters/ioredis-cache-client.ts +89 -89
- package/src/adapters/node-redis-cache-client.ts +95 -95
- package/src/adapters/redis-key-prefix.ts +38 -38
- package/src/core/cache-error.ts +233 -0
- package/src/core/cache-logger.ts +116 -105
- package/src/core/key-builder.ts +33 -33
- package/src/core/native-cache.ts +55 -55
- package/src/core/pending-cache.ts +29 -29
- package/src/core/redis-cache-client.ts +160 -160
- package/src/core/redis-cache.ts +104 -104
- package/src/decorators/cache-evict.ts +137 -129
- package/src/decorators/cache.ts +323 -203
- package/src/index.ts +11 -11
- package/test/cache-evict-logging.test.ts +398 -362
- package/test/cache-logger.integration.test.ts +159 -129
- package/test/cache-logger.test.ts +156 -153
- package/test/cache-logging.test.ts +929 -544
- package/test/cache.test.ts +1017 -255
- package/test/fixtures/cache-logger-child.mjs +108 -108
- package/test/fixtures/cache-provider-failure-child.mjs +70 -0
- package/test/helpers/legacy-redis-cache.ts +41 -22
- package/test/helpers/package-consumer.ts +231 -231
- package/test/helpers/redis-fixture.ts +142 -142
- package/test/ioredis-cache-client.test.ts +143 -143
- package/test/legacy-redis-cache.test.ts +51 -0
- package/test/native-cache.test.ts +77 -77
- package/test/node-redis-cache-client.test.ts +149 -149
- package/test/pending-cache.test.ts +69 -69
- package/test/redis-cache-client-lifecycle.test.ts +112 -112
- package/test/redis-cache-client-types.test.ts +184 -184
- package/test/redis-cache-client.integration.test.ts +355 -327
- package/test/redis-cache-decorator.test.ts +601 -201
- package/test/redis-cache-provider.test.ts +269 -269
- package/test/type-contract/contract.ts +119 -64
- package/test/type-contract/tsconfig.json +12 -12
- package/tsconfig.json +8 -8
|
@@ -1,231 +1,231 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
5
|
-
|
|
6
|
-
export const packageRoot = resolve(__dirname, '../..');
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 从 README 指定小节提取第一个 TypeScript 示例,使文档参与真实消费项目编译。
|
|
10
|
-
* @param section README 的三级标题文本。
|
|
11
|
-
* @param headingLevel 小节使用的二级或三级标题层级。
|
|
12
|
-
* @returns 标题下第一个 TypeScript 代码块。
|
|
13
|
-
* @throws 标题或代码块不存在时抛出。
|
|
14
|
-
*/
|
|
15
|
-
export function readReadmeExample(section: string, headingLevel: 2 | 3 = 3): string {
|
|
16
|
-
const readme = readFileSync(join(packageRoot, 'README.md'), 'utf8').replaceAll('\r\n', '\n');
|
|
17
|
-
const heading = `${'#'.repeat(headingLevel)} ${section}\n`;
|
|
18
|
-
const sectionStart = readme.indexOf(heading);
|
|
19
|
-
if (sectionStart < 0) {
|
|
20
|
-
throw new Error(`Missing README section: ${section}`);
|
|
21
|
-
}
|
|
22
|
-
const sectionEnd = readme.indexOf('\n##', sectionStart + heading.length);
|
|
23
|
-
const content = readme.slice(sectionStart, sectionEnd < 0 ? undefined : sectionEnd);
|
|
24
|
-
const example = /```typescript\n([\s\S]*?)\n```/.exec(content)?.[1];
|
|
25
|
-
if (!example) {
|
|
26
|
-
throw new Error(`Missing TypeScript example in README section: ${section}`);
|
|
27
|
-
}
|
|
28
|
-
return example;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** 临时消费项目的已安装产物;所有路径均位于当前测试拥有的临时目录。 */
|
|
32
|
-
export interface PackageConsumer {
|
|
33
|
-
readonly directory: string;
|
|
34
|
-
readonly installedPackage: string;
|
|
35
|
-
readonly dependencyTree: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* 用结构化参数运行子进程,并清除可能让临时项目继承 workspace 依赖的环境变量。
|
|
40
|
-
* @param request 可执行文件、参数及工作目录。
|
|
41
|
-
* @returns 标准输出。
|
|
42
|
-
* @throws 子进程失败、超时或无法启动时抛出带输出的错误。
|
|
43
|
-
*/
|
|
44
|
-
export function runCommand(request: {
|
|
45
|
-
readonly executable: string;
|
|
46
|
-
readonly args: string[];
|
|
47
|
-
readonly cwd: string;
|
|
48
|
-
}): string {
|
|
49
|
-
const environment = { ...process.env };
|
|
50
|
-
delete environment.NODE_PATH;
|
|
51
|
-
delete environment.INIT_CWD;
|
|
52
|
-
delete environment.PNPM_WORKSPACE_DIR;
|
|
53
|
-
const result = spawnSync(request.executable, request.args, {
|
|
54
|
-
cwd: request.cwd,
|
|
55
|
-
env: environment,
|
|
56
|
-
encoding: 'utf8',
|
|
57
|
-
timeout: 120000,
|
|
58
|
-
windowsHide: true,
|
|
59
|
-
maxBuffer: 8 * 1024 * 1024,
|
|
60
|
-
});
|
|
61
|
-
if (result.error || result.status !== 0) {
|
|
62
|
-
throw new Error(
|
|
63
|
-
`${request.executable} failed: ${result.error?.message ?? result.status}\n${result.stdout}\n${result.stderr}`
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
return result.stdout;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** 使用生命周期提供的 pnpm 路径;Windows 独立 Jest 运行兼容 Node 安装目录中的 pnpm。 */
|
|
70
|
-
function runPnpm(args: string[], cwd: string): string {
|
|
71
|
-
const candidates = [
|
|
72
|
-
process.env.npm_execpath,
|
|
73
|
-
join(dirname(process.execPath), 'node_modules/pnpm/bin/pnpm.mjs'),
|
|
74
|
-
join(dirname(process.execPath), 'node_modules/pnpm/bin/pnpm.cjs'),
|
|
75
|
-
];
|
|
76
|
-
const executable = candidates.find((candidate) => candidate && existsSync(candidate));
|
|
77
|
-
if (!executable) {
|
|
78
|
-
throw new Error('Run package consumer tests with pnpm test');
|
|
79
|
-
}
|
|
80
|
-
const isScript = ['.mjs', '.cjs', '.js'].some((extension) => executable.endsWith(extension));
|
|
81
|
-
return runCommand({
|
|
82
|
-
executable: isScript ? process.execPath : executable,
|
|
83
|
-
args: isScript ? [executable, ...args] : args,
|
|
84
|
-
cwd,
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* 创建与 workspace 不相交的临时测试根目录。
|
|
90
|
-
* @returns 位于系统临时目录且带专用前缀的目录。
|
|
91
|
-
*/
|
|
92
|
-
export function createPackageTestRoot(): string {
|
|
93
|
-
return mkdtempSync(join(tmpdir(), 'cache-consumer-'));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* 构建并打包当前 cache-decorator 包。
|
|
98
|
-
* @param root 本次测试拥有的临时根目录。
|
|
99
|
-
* @returns 这次测试使用的 tarball 绝对路径。
|
|
100
|
-
* @throws 构建或打包失败时抛出。
|
|
101
|
-
*/
|
|
102
|
-
export function packCurrentPackage(root: string): string {
|
|
103
|
-
runCommand({
|
|
104
|
-
executable: process.execPath,
|
|
105
|
-
args: [require.resolve('typescript/bin/tsc'), '-p', join(packageRoot, 'tsconfig.json')],
|
|
106
|
-
cwd: packageRoot,
|
|
107
|
-
});
|
|
108
|
-
const archive = join(root, 'cache-decorator.tgz');
|
|
109
|
-
runPnpm(['pack', '--out', archive, '--json'], packageRoot);
|
|
110
|
-
return archive;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* 构建并打包当前 workspace Logger,供外部消费 fixture 显式满足 required peer。
|
|
115
|
-
* @param root 本次测试拥有的临时根目录。
|
|
116
|
-
* @returns 这次测试使用的 Logger tarball 绝对路径。
|
|
117
|
-
* @throws 构建或打包失败时抛出。
|
|
118
|
-
*/
|
|
119
|
-
export function packLoggerPackage(root: string): string {
|
|
120
|
-
const loggerRoot = resolve(packageRoot, '../logger');
|
|
121
|
-
runCommand({
|
|
122
|
-
executable: process.execPath,
|
|
123
|
-
args: [require.resolve('typescript/bin/tsc'), '-p', join(loggerRoot, 'tsconfig.json')],
|
|
124
|
-
cwd: loggerRoot,
|
|
125
|
-
});
|
|
126
|
-
const archive = join(root, 'logger.tgz');
|
|
127
|
-
runPnpm(['pack', '--out', archive, '--json'], loggerRoot);
|
|
128
|
-
return archive;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** 读取当前 workspace 已安装版本,避免消费测试解析无关的新版本。 */
|
|
132
|
-
function installedVersion(name: string): string {
|
|
133
|
-
const localManifest = join(packageRoot, 'node_modules', name, 'package.json');
|
|
134
|
-
const manifestPath = existsSync(localManifest) ? localManifest : require.resolve(`${name}/package.json`);
|
|
135
|
-
const manifest: { readonly version: string } = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
136
|
-
return manifest.version;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* 安装真实 tarball 和唯一指定的 Redis 客户端,严格编译并运行消费用法。
|
|
141
|
-
* @param request 临时根、归档、客户端名称及消费源码。
|
|
142
|
-
* @returns 可审计的安装路径及生产依赖树。
|
|
143
|
-
* @throws 安装、严格类型检查或运行失败时抛出。
|
|
144
|
-
*/
|
|
145
|
-
export function installConsumer(request: {
|
|
146
|
-
readonly root: string;
|
|
147
|
-
readonly archive: string;
|
|
148
|
-
readonly loggerArchive: string;
|
|
149
|
-
readonly client: 'none' | 'redis' | 'ioredis';
|
|
150
|
-
readonly source: string;
|
|
151
|
-
readonly readmeSource: string;
|
|
152
|
-
readonly quickStartSource: string;
|
|
153
|
-
}): PackageConsumer {
|
|
154
|
-
const directory = join(request.root, request.client);
|
|
155
|
-
mkdirSync(directory);
|
|
156
|
-
const dependencies: Record<string, string> = {
|
|
157
|
-
'@jintianxiayu/cache-decorator': `file:${request.archive.replaceAll('\\', '/')}`,
|
|
158
|
-
'@jintianxiayu/logger': `file:${request.loggerArchive.replaceAll('\\', '/')}`,
|
|
159
|
-
'reflect-metadata': installedVersion('reflect-metadata'),
|
|
160
|
-
};
|
|
161
|
-
if (request.client !== 'none') {
|
|
162
|
-
dependencies[request.client] = installedVersion(request.client);
|
|
163
|
-
}
|
|
164
|
-
writeFileSync(
|
|
165
|
-
join(directory, 'package.json'),
|
|
166
|
-
JSON.stringify({
|
|
167
|
-
name: `cache-consumer-${request.client}`,
|
|
168
|
-
private: true,
|
|
169
|
-
dependencies,
|
|
170
|
-
devDependencies: {
|
|
171
|
-
typescript: installedVersion('typescript'),
|
|
172
|
-
'@types/node': installedVersion('@types/node'),
|
|
173
|
-
},
|
|
174
|
-
})
|
|
175
|
-
);
|
|
176
|
-
writeFileSync(
|
|
177
|
-
join(directory, 'tsconfig.json'),
|
|
178
|
-
JSON.stringify({
|
|
179
|
-
compilerOptions: {
|
|
180
|
-
strict: true,
|
|
181
|
-
skipLibCheck: false,
|
|
182
|
-
target: 'ES2021',
|
|
183
|
-
module: 'NodeNext',
|
|
184
|
-
moduleResolution: 'NodeNext',
|
|
185
|
-
types: ['node'],
|
|
186
|
-
outDir: 'out',
|
|
187
|
-
experimentalDecorators: true,
|
|
188
|
-
emitDecoratorMetadata: true,
|
|
189
|
-
},
|
|
190
|
-
include: ['consumer.ts', 'readme-example.ts', 'quick-start.ts'],
|
|
191
|
-
})
|
|
192
|
-
);
|
|
193
|
-
writeFileSync(join(directory, 'consumer.ts'), request.source);
|
|
194
|
-
writeFileSync(join(directory, 'readme-example.ts'), request.readmeSource);
|
|
195
|
-
writeFileSync(join(directory, 'quick-start.ts'), request.quickStartSource);
|
|
196
|
-
runPnpm(
|
|
197
|
-
[
|
|
198
|
-
'install',
|
|
199
|
-
'--ignore-scripts',
|
|
200
|
-
'--store-dir',
|
|
201
|
-
join(request.root, 'store'),
|
|
202
|
-
'--config.auto-install-peers=false',
|
|
203
|
-
],
|
|
204
|
-
directory
|
|
205
|
-
);
|
|
206
|
-
runCommand({
|
|
207
|
-
executable: process.execPath,
|
|
208
|
-
args: [join(directory, 'node_modules/typescript/bin/tsc'), '-p', directory],
|
|
209
|
-
cwd: directory,
|
|
210
|
-
});
|
|
211
|
-
runCommand({ executable: process.execPath, args: [join(directory, 'out/consumer.js')], cwd: directory });
|
|
212
|
-
return {
|
|
213
|
-
directory,
|
|
214
|
-
installedPackage: join(directory, 'node_modules/@jintianxiayu/cache-decorator'),
|
|
215
|
-
dependencyTree: runPnpm(['list', '--prod', '--depth', '100', '--json'], directory),
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* 仅删除本测试通过 mkdtemp 创建且已确认位于系统临时目录内的目录。
|
|
221
|
-
* @param root 待删除的测试临时根目录。
|
|
222
|
-
* @returns 删除完成后返回。
|
|
223
|
-
* @throws 目标不在系统临时目录或前缀不匹配时拒绝删除。
|
|
224
|
-
*/
|
|
225
|
-
export function removePackageTestRoot(root: string): void {
|
|
226
|
-
const absolute = resolve(root);
|
|
227
|
-
if (!absolute.startsWith(resolve(tmpdir()) + sep) || !basename(absolute).startsWith('cache-consumer-')) {
|
|
228
|
-
throw new Error('Refusing to remove a path outside the package test temporary root');
|
|
229
|
-
}
|
|
230
|
-
rmSync(absolute, { recursive: true, force: true, maxRetries: 3 });
|
|
231
|
-
}
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export const packageRoot = resolve(__dirname, '../..');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 从 README 指定小节提取第一个 TypeScript 示例,使文档参与真实消费项目编译。
|
|
10
|
+
* @param section README 的三级标题文本。
|
|
11
|
+
* @param headingLevel 小节使用的二级或三级标题层级。
|
|
12
|
+
* @returns 标题下第一个 TypeScript 代码块。
|
|
13
|
+
* @throws 标题或代码块不存在时抛出。
|
|
14
|
+
*/
|
|
15
|
+
export function readReadmeExample(section: string, headingLevel: 2 | 3 = 3): string {
|
|
16
|
+
const readme = readFileSync(join(packageRoot, 'README.md'), 'utf8').replaceAll('\r\n', '\n');
|
|
17
|
+
const heading = `${'#'.repeat(headingLevel)} ${section}\n`;
|
|
18
|
+
const sectionStart = readme.indexOf(heading);
|
|
19
|
+
if (sectionStart < 0) {
|
|
20
|
+
throw new Error(`Missing README section: ${section}`);
|
|
21
|
+
}
|
|
22
|
+
const sectionEnd = readme.indexOf('\n##', sectionStart + heading.length);
|
|
23
|
+
const content = readme.slice(sectionStart, sectionEnd < 0 ? undefined : sectionEnd);
|
|
24
|
+
const example = /```typescript\n([\s\S]*?)\n```/.exec(content)?.[1];
|
|
25
|
+
if (!example) {
|
|
26
|
+
throw new Error(`Missing TypeScript example in README section: ${section}`);
|
|
27
|
+
}
|
|
28
|
+
return example;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 临时消费项目的已安装产物;所有路径均位于当前测试拥有的临时目录。 */
|
|
32
|
+
export interface PackageConsumer {
|
|
33
|
+
readonly directory: string;
|
|
34
|
+
readonly installedPackage: string;
|
|
35
|
+
readonly dependencyTree: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 用结构化参数运行子进程,并清除可能让临时项目继承 workspace 依赖的环境变量。
|
|
40
|
+
* @param request 可执行文件、参数及工作目录。
|
|
41
|
+
* @returns 标准输出。
|
|
42
|
+
* @throws 子进程失败、超时或无法启动时抛出带输出的错误。
|
|
43
|
+
*/
|
|
44
|
+
export function runCommand(request: {
|
|
45
|
+
readonly executable: string;
|
|
46
|
+
readonly args: string[];
|
|
47
|
+
readonly cwd: string;
|
|
48
|
+
}): string {
|
|
49
|
+
const environment = { ...process.env };
|
|
50
|
+
delete environment.NODE_PATH;
|
|
51
|
+
delete environment.INIT_CWD;
|
|
52
|
+
delete environment.PNPM_WORKSPACE_DIR;
|
|
53
|
+
const result = spawnSync(request.executable, request.args, {
|
|
54
|
+
cwd: request.cwd,
|
|
55
|
+
env: environment,
|
|
56
|
+
encoding: 'utf8',
|
|
57
|
+
timeout: 120000,
|
|
58
|
+
windowsHide: true,
|
|
59
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
60
|
+
});
|
|
61
|
+
if (result.error || result.status !== 0) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`${request.executable} failed: ${result.error?.message ?? result.status}\n${result.stdout}\n${result.stderr}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return result.stdout;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 使用生命周期提供的 pnpm 路径;Windows 独立 Jest 运行兼容 Node 安装目录中的 pnpm。 */
|
|
70
|
+
function runPnpm(args: string[], cwd: string): string {
|
|
71
|
+
const candidates = [
|
|
72
|
+
process.env.npm_execpath,
|
|
73
|
+
join(dirname(process.execPath), 'node_modules/pnpm/bin/pnpm.mjs'),
|
|
74
|
+
join(dirname(process.execPath), 'node_modules/pnpm/bin/pnpm.cjs'),
|
|
75
|
+
];
|
|
76
|
+
const executable = candidates.find((candidate) => candidate && existsSync(candidate));
|
|
77
|
+
if (!executable) {
|
|
78
|
+
throw new Error('Run package consumer tests with pnpm test');
|
|
79
|
+
}
|
|
80
|
+
const isScript = ['.mjs', '.cjs', '.js'].some((extension) => executable.endsWith(extension));
|
|
81
|
+
return runCommand({
|
|
82
|
+
executable: isScript ? process.execPath : executable,
|
|
83
|
+
args: isScript ? [executable, ...args] : args,
|
|
84
|
+
cwd,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 创建与 workspace 不相交的临时测试根目录。
|
|
90
|
+
* @returns 位于系统临时目录且带专用前缀的目录。
|
|
91
|
+
*/
|
|
92
|
+
export function createPackageTestRoot(): string {
|
|
93
|
+
return mkdtempSync(join(tmpdir(), 'cache-consumer-'));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 构建并打包当前 cache-decorator 包。
|
|
98
|
+
* @param root 本次测试拥有的临时根目录。
|
|
99
|
+
* @returns 这次测试使用的 tarball 绝对路径。
|
|
100
|
+
* @throws 构建或打包失败时抛出。
|
|
101
|
+
*/
|
|
102
|
+
export function packCurrentPackage(root: string): string {
|
|
103
|
+
runCommand({
|
|
104
|
+
executable: process.execPath,
|
|
105
|
+
args: [require.resolve('typescript/bin/tsc'), '-p', join(packageRoot, 'tsconfig.json')],
|
|
106
|
+
cwd: packageRoot,
|
|
107
|
+
});
|
|
108
|
+
const archive = join(root, 'cache-decorator.tgz');
|
|
109
|
+
runPnpm(['pack', '--out', archive, '--json'], packageRoot);
|
|
110
|
+
return archive;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 构建并打包当前 workspace Logger,供外部消费 fixture 显式满足 required peer。
|
|
115
|
+
* @param root 本次测试拥有的临时根目录。
|
|
116
|
+
* @returns 这次测试使用的 Logger tarball 绝对路径。
|
|
117
|
+
* @throws 构建或打包失败时抛出。
|
|
118
|
+
*/
|
|
119
|
+
export function packLoggerPackage(root: string): string {
|
|
120
|
+
const loggerRoot = resolve(packageRoot, '../logger');
|
|
121
|
+
runCommand({
|
|
122
|
+
executable: process.execPath,
|
|
123
|
+
args: [require.resolve('typescript/bin/tsc'), '-p', join(loggerRoot, 'tsconfig.json')],
|
|
124
|
+
cwd: loggerRoot,
|
|
125
|
+
});
|
|
126
|
+
const archive = join(root, 'logger.tgz');
|
|
127
|
+
runPnpm(['pack', '--out', archive, '--json'], loggerRoot);
|
|
128
|
+
return archive;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 读取当前 workspace 已安装版本,避免消费测试解析无关的新版本。 */
|
|
132
|
+
function installedVersion(name: string): string {
|
|
133
|
+
const localManifest = join(packageRoot, 'node_modules', name, 'package.json');
|
|
134
|
+
const manifestPath = existsSync(localManifest) ? localManifest : require.resolve(`${name}/package.json`);
|
|
135
|
+
const manifest: { readonly version: string } = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
136
|
+
return manifest.version;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 安装真实 tarball 和唯一指定的 Redis 客户端,严格编译并运行消费用法。
|
|
141
|
+
* @param request 临时根、归档、客户端名称及消费源码。
|
|
142
|
+
* @returns 可审计的安装路径及生产依赖树。
|
|
143
|
+
* @throws 安装、严格类型检查或运行失败时抛出。
|
|
144
|
+
*/
|
|
145
|
+
export function installConsumer(request: {
|
|
146
|
+
readonly root: string;
|
|
147
|
+
readonly archive: string;
|
|
148
|
+
readonly loggerArchive: string;
|
|
149
|
+
readonly client: 'none' | 'redis' | 'ioredis';
|
|
150
|
+
readonly source: string;
|
|
151
|
+
readonly readmeSource: string;
|
|
152
|
+
readonly quickStartSource: string;
|
|
153
|
+
}): PackageConsumer {
|
|
154
|
+
const directory = join(request.root, request.client);
|
|
155
|
+
mkdirSync(directory);
|
|
156
|
+
const dependencies: Record<string, string> = {
|
|
157
|
+
'@jintianxiayu/cache-decorator': `file:${request.archive.replaceAll('\\', '/')}`,
|
|
158
|
+
'@jintianxiayu/logger': `file:${request.loggerArchive.replaceAll('\\', '/')}`,
|
|
159
|
+
'reflect-metadata': installedVersion('reflect-metadata'),
|
|
160
|
+
};
|
|
161
|
+
if (request.client !== 'none') {
|
|
162
|
+
dependencies[request.client] = installedVersion(request.client);
|
|
163
|
+
}
|
|
164
|
+
writeFileSync(
|
|
165
|
+
join(directory, 'package.json'),
|
|
166
|
+
JSON.stringify({
|
|
167
|
+
name: `cache-consumer-${request.client}`,
|
|
168
|
+
private: true,
|
|
169
|
+
dependencies,
|
|
170
|
+
devDependencies: {
|
|
171
|
+
typescript: installedVersion('typescript'),
|
|
172
|
+
'@types/node': installedVersion('@types/node'),
|
|
173
|
+
},
|
|
174
|
+
})
|
|
175
|
+
);
|
|
176
|
+
writeFileSync(
|
|
177
|
+
join(directory, 'tsconfig.json'),
|
|
178
|
+
JSON.stringify({
|
|
179
|
+
compilerOptions: {
|
|
180
|
+
strict: true,
|
|
181
|
+
skipLibCheck: false,
|
|
182
|
+
target: 'ES2021',
|
|
183
|
+
module: 'NodeNext',
|
|
184
|
+
moduleResolution: 'NodeNext',
|
|
185
|
+
types: ['node'],
|
|
186
|
+
outDir: 'out',
|
|
187
|
+
experimentalDecorators: true,
|
|
188
|
+
emitDecoratorMetadata: true,
|
|
189
|
+
},
|
|
190
|
+
include: ['consumer.ts', 'readme-example.ts', 'quick-start.ts'],
|
|
191
|
+
})
|
|
192
|
+
);
|
|
193
|
+
writeFileSync(join(directory, 'consumer.ts'), request.source);
|
|
194
|
+
writeFileSync(join(directory, 'readme-example.ts'), request.readmeSource);
|
|
195
|
+
writeFileSync(join(directory, 'quick-start.ts'), request.quickStartSource);
|
|
196
|
+
runPnpm(
|
|
197
|
+
[
|
|
198
|
+
'install',
|
|
199
|
+
'--ignore-scripts',
|
|
200
|
+
'--store-dir',
|
|
201
|
+
join(request.root, 'store'),
|
|
202
|
+
'--config.auto-install-peers=false',
|
|
203
|
+
],
|
|
204
|
+
directory
|
|
205
|
+
);
|
|
206
|
+
runCommand({
|
|
207
|
+
executable: process.execPath,
|
|
208
|
+
args: [join(directory, 'node_modules/typescript/bin/tsc'), '-p', directory],
|
|
209
|
+
cwd: directory,
|
|
210
|
+
});
|
|
211
|
+
runCommand({ executable: process.execPath, args: [join(directory, 'out/consumer.js')], cwd: directory });
|
|
212
|
+
return {
|
|
213
|
+
directory,
|
|
214
|
+
installedPackage: join(directory, 'node_modules/@jintianxiayu/cache-decorator'),
|
|
215
|
+
dependencyTree: runPnpm(['list', '--prod', '--depth', '100', '--json'], directory),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* 仅删除本测试通过 mkdtemp 创建且已确认位于系统临时目录内的目录。
|
|
221
|
+
* @param root 待删除的测试临时根目录。
|
|
222
|
+
* @returns 删除完成后返回。
|
|
223
|
+
* @throws 目标不在系统临时目录或前缀不匹配时拒绝删除。
|
|
224
|
+
*/
|
|
225
|
+
export function removePackageTestRoot(root: string): void {
|
|
226
|
+
const absolute = resolve(root);
|
|
227
|
+
if (!absolute.startsWith(resolve(tmpdir()) + sep) || !basename(absolute).startsWith('cache-consumer-')) {
|
|
228
|
+
throw new Error('Refusing to remove a path outside the package test temporary root');
|
|
229
|
+
}
|
|
230
|
+
rmSync(absolute, { recursive: true, force: true, maxRetries: 3 });
|
|
231
|
+
}
|