@tsed/cli-testing 6.6.2 → 7.0.0-alpha.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.
@@ -1,13 +1,22 @@
1
1
  import "@tsed/logger-std";
2
2
  import { CliCore, CliExeca, CliFs, CliHttpClient, CliService, configuration, createInjector, DITest, Env, getCommandMetadata, injector, InjectorService, logger, ProjectPackageJson, resolveConfiguration } from "@tsed/cli-core";
3
3
  import { Type } from "@tsed/core";
4
- import { DIContext, runInContext } from "@tsed/di";
4
+ import { DIContext, inject, runInContext } from "@tsed/di";
5
5
  import { $asyncEmit } from "@tsed/hooks";
6
6
  import { v4 } from "uuid";
7
7
  import { FakeCliExeca } from "./FakeCliExeca.js";
8
8
  import { FakeCliFs } from "./FakeCliFs.js";
9
9
  import { FakeCliHttpClient } from "./FakeCliHttpClient.js";
10
10
  export class CliPlatformTest extends DITest {
11
+ static async reset() {
12
+ // Explicitly clear FakeCliFs static collections to prevent memory leaks
13
+ FakeCliFs.files.clear();
14
+ FakeCliFs.directories.clear();
15
+ FakeCliExeca.entries.clear();
16
+ FakeCliHttpClient.entries.clear();
17
+ // Call parent reset method
18
+ return super.reset();
19
+ }
11
20
  static async bootstrap(options = {}) {
12
21
  options = resolveConfiguration({
13
22
  name: "tsed",
@@ -17,6 +26,7 @@ export class CliPlatformTest extends DITest {
17
26
  scriptsDir: "scripts",
18
27
  ...(options.project || {})
19
28
  },
29
+ disableReadUpPkg: true,
20
30
  ...options
21
31
  });
22
32
  CliPlatformTest.createInjector(options);
@@ -36,10 +46,30 @@ export class CliPlatformTest extends DITest {
36
46
  await $asyncEmit("$loadPackageJson");
37
47
  CliPlatformTest.get(CliService).load();
38
48
  }
49
+ static async initProject(options) {
50
+ CliPlatformTest.setPackageJson({
51
+ name: "",
52
+ version: "1.0.0",
53
+ description: "",
54
+ scripts: {},
55
+ dependencies: {},
56
+ devDependencies: {}
57
+ });
58
+ return CliPlatformTest.exec("init", {
59
+ platform: "express",
60
+ rootDir: "./project-data",
61
+ projectName: "project-data",
62
+ tsedVersion: "5.58.1",
63
+ packageManager: "yarn",
64
+ runtime: "node",
65
+ ...options
66
+ });
67
+ }
39
68
  static async create(options = {}, rootModule = CliCore) {
40
69
  options = resolveConfiguration({
41
70
  name: "tsed",
42
- ...options
71
+ ...options,
72
+ disableReadUpPkg: true
43
73
  });
44
74
  CliPlatformTest.createInjector(options);
45
75
  injector().addProvider(CliCore, {
@@ -70,13 +100,14 @@ export class CliPlatformTest extends DITest {
70
100
  static setPackageJson(pkg) {
71
101
  const projectPackageJson = CliPlatformTest.get(ProjectPackageJson);
72
102
  projectPackageJson.setRaw(pkg);
103
+ inject(CliFs).writeJsonSync(projectPackageJson.path, pkg);
73
104
  }
74
105
  /**
75
106
  * Invoke command with a new context without running prompts
76
107
  * @param cmdName
77
108
  * @param initialData
78
109
  */
79
- static exec(cmdName, initialData) {
110
+ static async exec(cmdName, initialData) {
80
111
  const $ctx = new DIContext({
81
112
  id: v4(),
82
113
  injector: injector(),
@@ -86,8 +117,12 @@ export class CliPlatformTest extends DITest {
86
117
  .get("commands")
87
118
  .map((token) => getCommandMetadata(token))
88
119
  .find((commandOpts) => cmdName === commandOpts.name);
120
+ if (cmdName !== "init") {
121
+ initialData.platform ||= "express";
122
+ initialData = inject(ProjectPackageJson).fillWithPreferences(initialData);
123
+ }
89
124
  $ctx.set("data", initialData);
90
125
  $ctx.set("command", metadata);
91
- return runInContext($ctx, () => this.injector.get(CliService).exec(cmdName, initialData, $ctx));
126
+ await runInContext($ctx, () => this.injector.get(CliService).exec(cmdName, initialData, $ctx));
92
127
  }
93
128
  }
@@ -1,10 +1,16 @@
1
1
  import { CliExeca } from "@tsed/cli-core";
2
+ import { $emit } from "@tsed/hooks";
2
3
  import { Observable } from "rxjs";
3
4
  export class FakeCliExeca extends CliExeca {
4
5
  static { this.entries = new Map(); }
5
6
  run(cmd, args, opts) {
6
- const result = FakeCliExeca.entries.get(cmd + " " + args.join(" "));
7
+ const key = cmd + " " + args.join(" ");
8
+ const result = FakeCliExeca.entries.get(key);
9
+ if (!result) {
10
+ FakeCliExeca.entries.set(key, "executed");
11
+ }
7
12
  return new Observable((observer) => {
13
+ $emit(key);
8
14
  observer.next(result);
9
15
  observer.complete();
10
16
  });
@@ -17,7 +23,9 @@ export class FakeCliExeca extends CliExeca {
17
23
  }
18
24
  }));
19
25
  }
20
- return Promise.resolve(FakeCliExeca.entries.get(cmd + " " + args.join(" ")));
26
+ const key = cmd + " " + args.join(" ");
27
+ $emit(key);
28
+ return Promise.resolve(FakeCliExeca.entries.get(key));
21
29
  }
22
30
  runSync(cmd, args, opts) {
23
31
  return {
@@ -1,29 +1,52 @@
1
1
  import * as fs from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { errors, matchGlobs } from "@ts-morph/common";
2
4
  import { isString } from "@tsed/core";
5
+ import {} from "fs-extra";
3
6
  import { normalizePath } from "./normalizePath.js";
4
7
  export class FakeCliFs {
5
- static { this.entries = new Map(); }
8
+ static { this.files = new Map(); }
9
+ static { this.directories = new Set(); }
6
10
  static getKeys() {
7
- return normalizePath(Array.from(FakeCliFs.entries.keys()).sort((a, b) => (a < b ? -1 : 1)));
11
+ return normalizePath(Array.from(FakeCliFs.files.keys()).sort((a, b) => (a < b ? -1 : 1)));
8
12
  }
9
13
  // @ts-ignore
10
14
  findUpFile() {
11
15
  return null;
12
16
  }
13
17
  exists(path) {
14
- return FakeCliFs.entries.has(normalizePath(path));
18
+ path = this.normalizePath(path);
19
+ if (path.includes("templates")) {
20
+ return fs.existsSync(join("/", path));
21
+ }
22
+ return FakeCliFs.files.has(path) || FakeCliFs.files.has(path);
15
23
  }
16
24
  readFile(file, encoding) {
17
- return Promise.resolve(FakeCliFs.entries.get(normalizePath(file)));
25
+ return Promise.resolve(this.readFileSync(file, encoding));
18
26
  }
19
27
  readFileSync(file, encoding) {
20
28
  try {
21
- if (isString(file) && file.match(/_partials/)) {
22
- return fs.readFileSync(file, encoding);
29
+ if (isString(file)) {
30
+ if (file.includes("templates")) {
31
+ return fs.readFileSync(join("/", file), encoding);
32
+ }
33
+ if (file.match(/_partials/) || file.includes("packages/cli")) {
34
+ return fs.readFileSync(file, encoding);
35
+ }
23
36
  }
24
37
  }
25
38
  catch (er) { }
26
- return FakeCliFs.entries.get(normalizePath(file));
39
+ const standardizedFilePath = this.normalizePath(file);
40
+ const parentDir = this.normalizePath(dirname(standardizedFilePath));
41
+ const hasParentDir = FakeCliFs.directories.has(parentDir);
42
+ if (!hasParentDir) {
43
+ throw new errors.FileNotFoundError(standardizedFilePath);
44
+ }
45
+ const fileText = FakeCliFs.files.get(standardizedFilePath);
46
+ if (fileText === undefined) {
47
+ throw new errors.FileNotFoundError(standardizedFilePath);
48
+ }
49
+ return fileText;
27
50
  }
28
51
  async readJson(file, encoding) {
29
52
  const content = await this.readFile(file, encoding);
@@ -33,20 +56,188 @@ export class FakeCliFs {
33
56
  const content = this.readFileSync(file, encoding);
34
57
  return content ? JSON.parse(content) : {};
35
58
  }
59
+ async writeJson(file, data, options) {
60
+ await this.writeFile(file, JSON.stringify(data, null, 2), options || { encoding: "utf8" });
61
+ }
62
+ writeJsonSync(file, data, options) {
63
+ this.writeFileSync(file, JSON.stringify(data, null, 2), options || { encoding: "utf8" });
64
+ }
36
65
  writeFileSync(path, data, options) {
37
- FakeCliFs.entries.set(normalizePath(path), data);
66
+ path = this.normalizePath(path);
67
+ this.mkdirSync(dirname(path));
68
+ FakeCliFs.files.set(path, data);
38
69
  }
39
70
  writeFile(file, data, options) {
40
- FakeCliFs.entries.set(normalizePath(file), data);
71
+ this.writeFileSync(file, data, options);
72
+ return Promise.resolve();
41
73
  }
42
74
  ensureDir(path, options) {
43
- FakeCliFs.entries.set(normalizePath(path), path);
75
+ this.ensureDirSync(path, options);
44
76
  return Promise.resolve();
45
77
  }
46
78
  ensureDirSync(path, options) {
47
- FakeCliFs.entries.set(normalizePath(path), path);
79
+ this.mkdirSync(this.normalizePath(path));
80
+ FakeCliFs.files.set(this.normalizePath(path), path);
81
+ }
82
+ isCaseSensitive() {
83
+ return true;
84
+ }
85
+ delete(path) {
86
+ this.deleteSync(path);
87
+ return Promise.resolve();
88
+ }
89
+ /** @inheritdoc */
90
+ deleteSync(path) {
91
+ FakeCliFs.files.delete(this.normalizePath(path));
92
+ }
93
+ /** @inheritdoc */
94
+ readDirSync(dirPath) {
95
+ const standardizedDirPath = this.normalizePath(dirPath);
96
+ const dir = FakeCliFs.directories.has(standardizedDirPath);
97
+ if (!dir) {
98
+ throw new errors.DirectoryNotFoundError(standardizedDirPath);
99
+ }
100
+ const files = [
101
+ ...[...FakeCliFs.directories.keys()]
102
+ .filter((file) => this.normalizePath(file).startsWith(standardizedDirPath + "/"))
103
+ .map((file) => ({
104
+ name: `/${this.normalizePath(file)}`,
105
+ isFile: false,
106
+ isDirectory: true,
107
+ isSymlink: false
108
+ })),
109
+ ...[...FakeCliFs.files.keys()]
110
+ .filter((file) => !FakeCliFs.directories.has(this.normalizePath(file)))
111
+ .filter((file) => {
112
+ return (this.normalizePath(file).startsWith(standardizedDirPath + "/") &&
113
+ this.normalizePath(file)
114
+ .replace(standardizedDirPath + "/", "")
115
+ .split("/").length == 1);
116
+ })
117
+ .map((file) => {
118
+ return {
119
+ name: `/${this.normalizePath(file)}`,
120
+ isFile: true,
121
+ isDirectory: false,
122
+ isSymlink: false
123
+ };
124
+ })
125
+ ];
126
+ return files;
127
+ }
128
+ mkdir(dirPath) {
129
+ this.mkdirSync(dirPath);
130
+ return Promise.resolve();
131
+ }
132
+ // #writeFileSync(filePath: string, fileText: string) {
133
+ // // private method to avoid calling a method in the constructor that could be overwritten (virtual method)
134
+ // const standardizedFilePath = FileUtils.getStandardizedAbsolutePath(this, filePath);
135
+ // const dirPath = FileUtils.getDirPath(standardizedFilePath);
136
+ // this.#getOrCreateDir(dirPath).files.set(standardizedFilePath, fileText);
137
+ // }
138
+ mkdirSync(dirPath) {
139
+ this.normalizePath(dirPath)
140
+ .split("/")
141
+ .reduce((currentPath, dirName) => {
142
+ const nextPath = join(currentPath, dirName);
143
+ FakeCliFs.directories.add(nextPath);
144
+ return nextPath;
145
+ }, "");
146
+ }
147
+ move(srcPath, destPath) {
148
+ this.moveSync(srcPath, destPath);
149
+ return Promise.resolve();
150
+ }
151
+ moveSync(srcPath, destPath) {
152
+ const standardizedSrcPath = this.normalizePath(srcPath);
153
+ const standardizedDestPath = this.normalizePath(destPath);
154
+ if (this.fileExistsSync(standardizedSrcPath)) {
155
+ const fileText = this.readFileSync(standardizedSrcPath);
156
+ this.deleteSync(standardizedSrcPath);
157
+ this.writeFileSync(standardizedDestPath, fileText);
158
+ }
159
+ else if (FakeCliFs.directories.has(standardizedSrcPath)) {
160
+ const impactedDirs = [...FakeCliFs.directories.keys()].filter((dir) => {
161
+ return dir.startsWith(standardizedSrcPath);
162
+ });
163
+ const impactedFiles = [...FakeCliFs.files.entries()].filter(([file]) => {
164
+ return file.startsWith(standardizedSrcPath);
165
+ });
166
+ // delete all impacted dirs
167
+ for (const dir of impactedDirs) {
168
+ FakeCliFs.directories.delete(dir);
169
+ }
170
+ this.mkdirSync(standardizedDestPath);
171
+ // delete all impacted files
172
+ for (const [filePath, fileText] of impactedFiles) {
173
+ FakeCliFs.files.delete(filePath);
174
+ FakeCliFs.files.set(filePath.replace(standardizedSrcPath, standardizedDestPath), fileText);
175
+ }
176
+ }
177
+ else {
178
+ throw new errors.PathNotFoundError(standardizedSrcPath);
179
+ }
180
+ }
181
+ copy(srcPath, destPath) {
182
+ this.copySync(srcPath, destPath);
183
+ return Promise.resolve();
184
+ }
185
+ copySync(srcPath, destPath) {
186
+ const standardizedSrcPath = this.normalizePath(srcPath);
187
+ const standardizedDestPath = this.normalizePath(destPath);
188
+ if (this.fileExistsSync(standardizedSrcPath)) {
189
+ this.writeFileSync(standardizedDestPath, this.readFileSync(standardizedSrcPath));
190
+ }
191
+ else if (FakeCliFs.directories.has(standardizedSrcPath)) {
192
+ const impactedFiles = [...FakeCliFs.files.entries()].filter(([file]) => {
193
+ return file.startsWith(standardizedSrcPath);
194
+ });
195
+ this.mkdirSync(standardizedDestPath);
196
+ // delete all impacted files
197
+ for (const [filePath, fileText] of impactedFiles) {
198
+ FakeCliFs.files.set(filePath.replace(standardizedSrcPath, standardizedDestPath), fileText);
199
+ }
200
+ }
201
+ else {
202
+ throw new errors.PathNotFoundError(standardizedSrcPath);
203
+ }
204
+ }
205
+ fileExists(filePath) {
206
+ return Promise.resolve(this.fileExistsSync(filePath));
207
+ }
208
+ fileExistsSync(filePath) {
209
+ return this.exists(filePath);
210
+ }
211
+ directoryExists(dirPath) {
212
+ return Promise.resolve(this.directoryExistsSync(dirPath));
213
+ }
214
+ directoryExistsSync(dirPath) {
215
+ return FakeCliFs.directories.has(this.normalizePath(dirPath));
216
+ }
217
+ realpathSync(path) {
218
+ return path;
219
+ }
220
+ getCurrentDirectory() {
221
+ return "/";
222
+ }
223
+ glob(patterns) {
224
+ try {
225
+ return Promise.resolve(this.globSync(patterns));
226
+ }
227
+ catch (err) {
228
+ return Promise.reject(err);
229
+ }
230
+ }
231
+ /** @inheritdoc */
232
+ globSync(patterns) {
233
+ const allFilePaths = [...FakeCliFs.directories.keys(), ...FakeCliFs.files.keys()]
234
+ .map((file) => {
235
+ return "/" + this.normalizePath(file);
236
+ })
237
+ .sort((a, b) => a.localeCompare(b));
238
+ return matchGlobs(allFilePaths, patterns, this.getCurrentDirectory());
48
239
  }
49
- $onDestroy() {
50
- FakeCliFs.entries.clear();
240
+ normalizePath(path) {
241
+ return normalizePath(path).replace(/^\//, "");
51
242
  }
52
243
  }
@@ -15,7 +15,4 @@ export class FakeCliHttpClient extends CliHttpClient {
15
15
  }
16
16
  return FakeCliHttpClient.entries.get(key)?.(endpoint, options);
17
17
  }
18
- $onDestroy() {
19
- FakeCliHttpClient.entries.clear();
20
- }
21
18
  }
package/lib/esm/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./CliPlatformTest.js";
2
+ export * from "./FakeCliExeca.js";
2
3
  export * from "./FakeCliFs.js";
3
4
  export * from "./FakeCliHttpClient.js";
4
5
  export * from "./normalizePath.js";
@@ -2,7 +2,9 @@ import "@tsed/logger-std";
2
2
  import { DITest, InjectorService } from "@tsed/cli-core";
3
3
  import { Type } from "@tsed/core";
4
4
  export declare class CliPlatformTest extends DITest {
5
+ static reset(): Promise<void>;
5
6
  static bootstrap(options?: Partial<TsED.Configuration>): Promise<void>;
7
+ static initProject(options?: any): Promise<void>;
6
8
  static create(options?: Partial<TsED.Configuration>, rootModule?: Type): Promise<void>;
7
9
  /**
8
10
  * Create a new injector with the right default services
@@ -14,5 +16,5 @@ export declare class CliPlatformTest extends DITest {
14
16
  * @param cmdName
15
17
  * @param initialData
16
18
  */
17
- static exec(cmdName: string, initialData: any): Promise<Promise<any>>;
19
+ static exec(cmdName: string, initialData: any): Promise<void>;
18
20
  }
@@ -1,7 +1,9 @@
1
1
  import type { PathLike } from "node:fs";
2
- import type { EnsureDirOptions, WriteFileOptions } from "fs-extra";
3
- export declare class FakeCliFs {
4
- static entries: Map<any, string>;
2
+ import { type FileSystemHost, type RuntimeDirEntry } from "@ts-morph/common";
3
+ import { type EnsureDirOptions, type WriteFileOptions } from "fs-extra";
4
+ export declare class FakeCliFs implements FileSystemHost {
5
+ static files: Map<any, string>;
6
+ static directories: Set<string>;
5
7
  static getKeys(): any;
6
8
  findUpFile(): null;
7
9
  exists(path: string): boolean;
@@ -9,9 +11,32 @@ export declare class FakeCliFs {
9
11
  readFileSync(file: string | Buffer | number, encoding?: any): string;
10
12
  readJson(file: string | Buffer | number, encoding?: any): Promise<string>;
11
13
  readJsonSync(file: string | Buffer | number, encoding?: any): Promise<string>;
14
+ writeJson(file: string | Buffer | number, data: any, options?: WriteFileOptions | string): Promise<any>;
15
+ writeJsonSync(file: string | Buffer | number, data: any, options?: WriteFileOptions | string): void;
12
16
  writeFileSync(path: PathLike | number, data: any, options?: WriteFileOptions): void;
13
- writeFile(file: string | Buffer | number, data: any, options?: WriteFileOptions | string): void;
17
+ writeFile(file: string | Buffer | number, data: any, options?: WriteFileOptions | string): Promise<void>;
14
18
  ensureDir(path: string, options?: EnsureDirOptions | number): Promise<void>;
15
19
  ensureDirSync(path: string, options?: EnsureDirOptions | number): void;
16
- $onDestroy(): void;
20
+ isCaseSensitive(): boolean;
21
+ delete(path: string): Promise<void>;
22
+ /** @inheritdoc */
23
+ deleteSync(path: string): void;
24
+ /** @inheritdoc */
25
+ readDirSync(dirPath: string): RuntimeDirEntry[];
26
+ mkdir(dirPath: string): Promise<void>;
27
+ mkdirSync(dirPath: string): void;
28
+ move(srcPath: string, destPath: string): Promise<void>;
29
+ moveSync(srcPath: string, destPath: string): void;
30
+ copy(srcPath: string, destPath: string): Promise<void>;
31
+ copySync(srcPath: string, destPath: string): void;
32
+ fileExists(filePath: string): Promise<boolean>;
33
+ fileExistsSync(filePath: string): boolean;
34
+ directoryExists(dirPath: string): Promise<boolean>;
35
+ directoryExistsSync(dirPath: string): boolean;
36
+ realpathSync(path: string): string;
37
+ getCurrentDirectory(): string;
38
+ glob(patterns: ReadonlyArray<string>): Promise<string[]>;
39
+ /** @inheritdoc */
40
+ globSync(patterns: ReadonlyArray<string>): string[];
41
+ private normalizePath;
17
42
  }
@@ -1,7 +1,5 @@
1
1
  import { CliHttpClient, type CliHttpClientOptions } from "@tsed/cli-core";
2
- import type { OnDestroy } from "@tsed/di";
3
- export declare class FakeCliHttpClient extends CliHttpClient implements OnDestroy {
4
- static entries: Map<string, any>;
2
+ export declare class FakeCliHttpClient extends CliHttpClient {
3
+ static entries: Map<string, (endpoint: string, options: CliHttpClientOptions) => any>;
5
4
  get(endpoint: string, options?: CliHttpClientOptions): Promise<any>;
6
- $onDestroy(): void;
7
5
  }
@@ -1,4 +1,5 @@
1
1
  export * from "./CliPlatformTest.js";
2
+ export * from "./FakeCliExeca.js";
2
3
  export * from "./FakeCliFs.js";
3
4
  export * from "./FakeCliHttpClient.js";
4
5
  export * from "./normalizePath.js";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tsed/cli-testing",
3
3
  "description": "Utils to test you CLI based on Ts.ED CLI",
4
- "version": "6.6.2",
4
+ "version": "7.0.0-alpha.1",
5
5
  "type": "module",
6
6
  "main": "./lib/esm/index.js",
7
7
  "source": "./src/index.ts",
@@ -30,11 +30,11 @@
30
30
  "decorators"
31
31
  ],
32
32
  "dependencies": {
33
- "@tsed/cli-core": "6.6.2",
33
+ "@tsed/cli-core": "7.0.0-alpha.1",
34
34
  "tslib": "2.7.0"
35
35
  },
36
36
  "devDependencies": {
37
- "@tsed/typescript": "6.6.2",
37
+ "@tsed/typescript": "7.0.0-alpha.1",
38
38
  "cross-env": "7.0.3",
39
39
  "typescript": "5.6.2",
40
40
  "vitest": "3.2.4"
@@ -46,5 +46,8 @@
46
46
  },
47
47
  "homepage": "https://github.com/tsedio/tsed-cli/tree/master/packages/cli-testing",
48
48
  "author": "Romain Lenzotti",
49
- "license": "MIT"
49
+ "license": "MIT",
50
+ "publishConfig": {
51
+ "tag": "alpha"
52
+ }
50
53
  }