@decohere/core 0.0.1-dev.126 → 0.0.1-dev.5
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/package.json +40 -15
- package/src/caching/cache-manager.ts +102 -0
- package/src/caching/cache.decorator.ts +56 -0
- package/src/caching/cache.ts +11 -0
- package/{es6/src/caching/index.js → src/caching/index.ts} +1 -1
- package/src/caching/invalidate-cache.decorator.ts +81 -0
- package/src/caching/memoize.decorator.ts +34 -0
- package/src/caching/node-cache-memoize.adapter.ts +47 -0
- package/src/cli/action.ts +77 -0
- package/src/cli/command.ts +173 -0
- package/src/cli/index.ts +30 -0
- package/src/cli/module.type.ts +48 -0
- package/src/cli/options.ts +81 -0
- package/src/collections/find-duplicates.ts +16 -0
- package/src/collections/merge-arrays.ts +9 -0
- package/src/collections/sum.ts +71 -0
- package/src/collections/unique-objects.ts +23 -0
- package/src/composition/index.ts +48 -0
- package/src/configuration/configuration.types.ts +13 -0
- package/src/diagnostics/index.ts +1 -0
- package/src/diagnostics/trace.ts +97 -0
- package/{es6/src/extensibility/decorator-factory-base.js → src/extensibility/decorator-factory-base.ts} +17 -9
- package/src/extensibility/decorator-factory.ts +44 -0
- package/src/extensibility/extension.ts +34 -0
- package/src/extensibility/mixwith.ts +42 -0
- package/src/extensibility/pipeline.ts +60 -0
- package/{types/src/extensibility/step.d.ts → src/extensibility/step.ts} +6 -4
- package/src/interop/docker/docker.ts +57 -0
- package/src/interop/errors/interop.error.ts +1 -0
- package/src/interop/git/git.ts +98 -0
- package/src/interop/git/git.types.ts +17 -0
- package/src/interop/index.ts +22 -0
- package/src/interop/node/index.ts +1 -0
- package/src/interop/node/node.ts +38 -0
- package/src/interop/npm/npm.ts +168 -0
- package/src/interop/npm/npm.types.ts +28 -0
- package/src/interop/pnpm/pnpm.ts +22 -0
- package/src/interop/pnpm/pnpm.types.ts +3 -0
- package/src/interop/shell/shell.argv.ts +31 -0
- package/src/interop/shell/shell.ts +100 -0
- package/src/interop/shell/shell.types.ts +9 -0
- package/src/interop/types/detect-package-manager.ts +45 -0
- package/src/interop/types/package-manager.ts +7 -0
- package/src/interop/yarn/yarn.ts +28 -0
- package/src/interop/yarn/yarn.types.ts +3 -0
- package/src/io/copy-file.ts +13 -0
- package/src/io/delete-file.ts +4 -0
- package/src/io/directory-exists.ts +9 -0
- package/src/io/file-exists.ts +10 -0
- package/src/io/find-directories.ts +13 -0
- package/src/io/find-file.ts +37 -0
- package/{es6/src/io/io.js → src/io/io.ts} +3 -0
- package/src/io/read-file.ts +25 -0
- package/src/io/remove-dir.ts +11 -0
- package/src/io/types/directory.type.ts +4 -0
- package/src/io/types/file.ts +34 -0
- package/src/io/url-to-path.ts +17 -0
- package/src/io/watch.ts +290 -0
- package/src/io/write-file.ts +21 -0
- package/src/objects/deep-merge.ts +131 -0
- package/{es6/src/reflection/index.js → src/reflection/index.ts} +1 -0
- package/src/reflection/types/reflection.types.ts +28 -0
- package/src/reflection/utilities.ts +92 -0
- package/src/semver/index.ts +1 -0
- package/src/semver/semver.ts +96 -0
- package/src/system/camel-case.ts +3 -0
- package/src/system/debounce.ts +13 -0
- package/src/system/decorator.ts +41 -0
- package/src/system/environment.ts +6 -0
- package/{types/src/system/function.types.d.ts → src/system/function.types.ts} +10 -2
- package/src/system/retry.decorator.ts +43 -0
- package/src/system/sleep.ts +6 -0
- package/src/system/type.utils.ts +5 -0
- package/src/testing/flush-promises.ts +7 -0
- package/src/testing/glob-with-memfs.ts +25 -0
- package/src/testing/index.ts +5 -0
- package/src/testing/memfs.ts +28 -0
- package/src/testing/mock-logger.ts +42 -0
- package/src/testing/spy-on-constructor.ts +33 -0
- package/tests/caching/cache-manager.spec.ts +147 -0
- package/tests/caching/cache-test-class.ts +91 -0
- package/tests/caching/cache.decorator.spec.ts +259 -0
- package/tests/caching/invalidate-cache-test-class.ts +52 -0
- package/tests/caching/invalidate-cache.decorator.spec.ts +120 -0
- package/tests/caching/memoize-test-class.ts +99 -0
- package/tests/caching/memoize.decorator.spec.ts +203 -0
- package/tests/cli/command.model.spec.ts +91 -0
- package/tests/collections/merge-arrays.spec.ts +50 -0
- package/tests/collections/sum.spec.ts +86 -0
- package/tests/collections/unique-objects.spec.ts +233 -0
- package/tests/extensibility/extension.spec.ts +29 -0
- package/tests/extensibility/pipeline.spec.ts +379 -0
- package/tests/interop/detect-package-manager.spec.ts +70 -0
- package/tests/interop/npm.spec.ts +129 -0
- package/tests/interop/pnpm.spec.ts +30 -0
- package/tests/interop/shell.spec.ts +47 -0
- package/tests/interop/yarn.spec.ts +18 -0
- package/tests/io/find-file.spec.ts +188 -0
- package/tests/io/watch.real-fs.spec.ts +76 -0
- package/tests/io/watch.spec.ts +291 -0
- package/tests/mock-prototype.ts +12 -0
- package/tests/semver/semver.spec.ts +51 -0
- package/tests/system/decorator.spec.ts +250 -0
- package/tests/system/retry.decorator.spec.ts +57 -0
- package/LICENSE +0 -15
- package/commonjs/src/caching/cache-manager.js +0 -98
- package/commonjs/src/caching/cache.decorator.js +0 -69
- package/commonjs/src/caching/cache.js +0 -6
- package/commonjs/src/caching/index.js +0 -23
- package/commonjs/src/caching/invalidate-cache.decorator.js +0 -92
- package/commonjs/src/caching/map-cache.js +0 -39
- package/commonjs/src/caching/memoize.decorator.js +0 -36
- package/commonjs/src/collections/collections.js +0 -21
- package/commonjs/src/collections/find-duplicates.js +0 -35
- package/commonjs/src/collections/index.js +0 -52
- package/commonjs/src/collections/merge-arrays.js +0 -34
- package/commonjs/src/collections/sum.js +0 -63
- package/commonjs/src/collections/unique-objects.js +0 -39
- package/commonjs/src/composition/index.js +0 -4
- package/commonjs/src/configuration/configuration.types.js +0 -11
- package/commonjs/src/configuration/index.js +0 -18
- package/commonjs/src/extensibility/decorator-factory-base.js +0 -22
- package/commonjs/src/extensibility/decorator-factory.js +0 -31
- package/commonjs/src/extensibility/extension.js +0 -37
- package/commonjs/src/extensibility/index.js +0 -23
- package/commonjs/src/extensibility/mixwith.js +0 -36
- package/commonjs/src/extensibility/pipeline.js +0 -56
- package/commonjs/src/extensibility/step.js +0 -18
- package/commonjs/src/io/copy-file.js +0 -25
- package/commonjs/src/io/delete-file.js +0 -21
- package/commonjs/src/io/directory-exists.js +0 -24
- package/commonjs/src/io/file-exists.js +0 -19
- package/commonjs/src/io/index.js +0 -66
- package/commonjs/src/io/io.js +0 -25
- package/commonjs/src/io/read-file.js +0 -39
- package/commonjs/src/io/remove-dir.js +0 -22
- package/commonjs/src/io/types/directory.type.js +0 -4
- package/commonjs/src/io/types/file.js +0 -28
- package/commonjs/src/io/types/index.js +0 -19
- package/commonjs/src/io/url-to-path.js +0 -33
- package/commonjs/src/io/write-file.js +0 -30
- package/commonjs/src/objects/deep-merge.js +0 -67
- package/commonjs/src/objects/index.js +0 -11
- package/commonjs/src/reflection/index.js +0 -20
- package/commonjs/src/reflection/types/index.js +0 -18
- package/commonjs/src/reflection/types/reflection.types.js +0 -26
- package/commonjs/src/reflection/utilities.js +0 -118
- package/commonjs/src/system/camel-case.js +0 -13
- package/commonjs/src/system/debounce.js +0 -19
- package/commonjs/src/system/decorator.js +0 -46
- package/commonjs/src/system/environment.js +0 -23
- package/commonjs/src/system/function.types.js +0 -18
- package/commonjs/src/system/index.js +0 -24
- package/commonjs/src/system/retry.decorator.js +0 -40
- package/commonjs/src/system/sleep.js +0 -13
- package/commonjs/src/system/type.utils.js +0 -4
- package/es6/src/caching/cache-manager.js +0 -82
- package/es6/src/caching/cache.decorator.js +0 -51
- package/es6/src/caching/cache.js +0 -3
- package/es6/src/caching/invalidate-cache.decorator.js +0 -71
- package/es6/src/caching/map-cache.js +0 -35
- package/es6/src/caching/memoize.decorator.js +0 -27
- package/es6/src/collections/find-duplicates.js +0 -17
- package/es6/src/collections/merge-arrays.js +0 -16
- package/es6/src/collections/sum.js +0 -45
- package/es6/src/collections/unique-objects.js +0 -21
- package/es6/src/composition/index.js +0 -1
- package/es6/src/configuration/configuration.types.js +0 -1
- package/es6/src/extensibility/decorator-factory.js +0 -21
- package/es6/src/extensibility/extension.js +0 -27
- package/es6/src/extensibility/mixwith.js +0 -26
- package/es6/src/extensibility/pipeline.js +0 -46
- package/es6/src/extensibility/step.js +0 -16
- package/es6/src/io/copy-file.js +0 -10
- package/es6/src/io/delete-file.js +0 -6
- package/es6/src/io/directory-exists.js +0 -9
- package/es6/src/io/file-exists.js +0 -9
- package/es6/src/io/read-file.js +0 -13
- package/es6/src/io/remove-dir.js +0 -7
- package/es6/src/io/types/directory.type.js +0 -1
- package/es6/src/io/types/file.js +0 -18
- package/es6/src/io/url-to-path.js +0 -15
- package/es6/src/io/write-file.js +0 -15
- package/es6/src/objects/deep-merge.js +0 -57
- package/es6/src/reflection/types/reflection.types.js +0 -8
- package/es6/src/reflection/utilities.js +0 -74
- package/es6/src/system/camel-case.js +0 -3
- package/es6/src/system/debounce.js +0 -9
- package/es6/src/system/decorator.js +0 -30
- package/es6/src/system/environment.js +0 -5
- package/es6/src/system/function.types.js +0 -26
- package/es6/src/system/retry.decorator.js +0 -30
- package/es6/src/system/sleep.js +0 -3
- package/es6/src/system/type.utils.js +0 -1
- package/types/src/caching/cache-manager.d.ts +0 -59
- package/types/src/caching/cache.d.ts +0 -11
- package/types/src/caching/cache.decorator.d.ts +0 -18
- package/types/src/caching/index.d.ts +0 -6
- package/types/src/caching/invalidate-cache.decorator.d.ts +0 -17
- package/types/src/caching/map-cache.d.ts +0 -23
- package/types/src/caching/memoize.decorator.d.ts +0 -16
- package/types/src/collections/collections.d.ts +0 -4
- package/types/src/collections/find-duplicates.d.ts +0 -2
- package/types/src/collections/index.d.ts +0 -1
- package/types/src/collections/merge-arrays.d.ts +0 -2
- package/types/src/collections/sum.d.ts +0 -4
- package/types/src/collections/unique-objects.d.ts +0 -2
- package/types/src/composition/index.d.ts +0 -37
- package/types/src/configuration/configuration.types.d.ts +0 -10
- package/types/src/configuration/index.d.ts +0 -1
- package/types/src/extensibility/decorator-factory-base.d.ts +0 -39
- package/types/src/extensibility/decorator-factory.d.ts +0 -19
- package/types/src/extensibility/extension.d.ts +0 -9
- package/types/src/extensibility/index.d.ts +0 -6
- package/types/src/extensibility/mixwith.d.ts +0 -6
- package/types/src/extensibility/pipeline.d.ts +0 -12
- package/types/src/io/copy-file.d.ts +0 -4
- package/types/src/io/delete-file.d.ts +0 -1
- package/types/src/io/directory-exists.d.ts +0 -1
- package/types/src/io/file-exists.d.ts +0 -1
- package/types/src/io/index.d.ts +0 -2
- package/types/src/io/io.d.ts +0 -8
- package/types/src/io/read-file.d.ts +0 -4
- package/types/src/io/remove-dir.d.ts +0 -5
- package/types/src/io/types/directory.type.d.ts +0 -4
- package/types/src/io/types/file.d.ts +0 -20
- package/types/src/io/types/index.d.ts +0 -2
- package/types/src/io/url-to-path.d.ts +0 -2
- package/types/src/io/write-file.d.ts +0 -5
- package/types/src/objects/deep-merge.d.ts +0 -41
- package/types/src/objects/index.d.ts +0 -1
- package/types/src/reflection/index.d.ts +0 -3
- package/types/src/reflection/types/index.d.ts +0 -1
- package/types/src/reflection/types/reflection.types.d.ts +0 -21
- package/types/src/reflection/utilities.d.ts +0 -27
- package/types/src/system/camel-case.d.ts +0 -1
- package/types/src/system/debounce.d.ts +0 -1
- package/types/src/system/decorator.d.ts +0 -15
- package/types/src/system/environment.d.ts +0 -6
- package/types/src/system/index.d.ts +0 -7
- package/types/src/system/retry.decorator.d.ts +0 -6
- package/types/src/system/sleep.d.ts +0 -1
- package/types/src/system/type.utils.d.ts +0 -1
- /package/{es6/src/collections/collections.js → src/collections/collections.ts} +0 -0
- /package/{es6/src/collections/index.js → src/collections/index.ts} +0 -0
- /package/{es6/src/configuration/index.js → src/configuration/index.ts} +0 -0
- /package/{es6/src/extensibility/index.js → src/extensibility/index.ts} +0 -0
- /package/{es6/src/io/index.js → src/io/index.ts} +0 -0
- /package/{es6/src/io/types/index.js → src/io/types/index.ts} +0 -0
- /package/{es6/src/objects/index.js → src/objects/index.ts} +0 -0
- /package/{es6/src/reflection/types/index.js → src/reflection/types/index.ts} +0 -0
- /package/{es6/src/system/index.js → src/system/index.ts} +0 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import * as glob from "glob";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { findFile, findFiles } from "../../src/io/find-file.js";
|
|
4
|
+
|
|
5
|
+
vi.mock("glob");
|
|
6
|
+
|
|
7
|
+
describe("findFile", () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
vi.clearAllMocks();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("should fail fast when glob is not explicitly overridden", async () => {
|
|
13
|
+
await expect(findFile("**/*.ts")).rejects.toThrow(
|
|
14
|
+
'Mocked "glob" was called without an explicit test override.',
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should return file with relativePath when pattern matches", async () => {
|
|
19
|
+
vi.spyOn(glob, "glob").mockResolvedValue(["src/index.ts"]);
|
|
20
|
+
|
|
21
|
+
const result = await findFile("**/*.ts");
|
|
22
|
+
|
|
23
|
+
expect(result).toEqual({
|
|
24
|
+
relativePath: "src/index.ts",
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should return undefined when no files match", async () => {
|
|
29
|
+
vi.spyOn(glob, "glob").mockResolvedValue([]);
|
|
30
|
+
|
|
31
|
+
const result = await findFile("**/*.ts");
|
|
32
|
+
|
|
33
|
+
expect(result).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should return only the first match", async () => {
|
|
37
|
+
vi.spyOn(glob, "glob").mockResolvedValue(["src/index.ts", "src/utils.ts"]);
|
|
38
|
+
|
|
39
|
+
const result = await findFile("**/*.ts");
|
|
40
|
+
|
|
41
|
+
expect(result).toEqual({
|
|
42
|
+
relativePath: "src/index.ts",
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("findFiles", () => {
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
vi.clearAllMocks();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should transform glob Path objects to File objects with relativePath and fullPath", async () => {
|
|
53
|
+
// Mock glob Path objects
|
|
54
|
+
const mockGlobPaths = [
|
|
55
|
+
{
|
|
56
|
+
isFile: () => true,
|
|
57
|
+
relative: () => "src/index.ts",
|
|
58
|
+
fullpath: () => "/test/project/src/index.ts",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
isFile: () => true,
|
|
62
|
+
relative: () => "src/utils.ts",
|
|
63
|
+
fullpath: () => "/test/project/src/utils.ts",
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
vi.spyOn(glob, "glob").mockResolvedValue(mockGlobPaths as unknown as string[]);
|
|
68
|
+
|
|
69
|
+
const result = await findFiles(["**/*.ts"], {
|
|
70
|
+
cwd: "/test/project",
|
|
71
|
+
ignore: [],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(result).toHaveLength(2);
|
|
75
|
+
expect(result[0]).toEqual({
|
|
76
|
+
relativePath: "src/index.ts",
|
|
77
|
+
fullPath: "/test/project/src/index.ts",
|
|
78
|
+
});
|
|
79
|
+
expect(result[1]).toEqual({
|
|
80
|
+
relativePath: "src/utils.ts",
|
|
81
|
+
fullPath: "/test/project/src/utils.ts",
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should use p.relative() for relativePath, not p.name", async () => {
|
|
86
|
+
// This test verifies the bug fix where p.name was incorrectly used
|
|
87
|
+
const mockGlobPaths = [
|
|
88
|
+
{
|
|
89
|
+
isFile: () => true,
|
|
90
|
+
name: "index.ts", // This would be wrong to use
|
|
91
|
+
relative: () => "src/components/index.ts", // This is correct
|
|
92
|
+
fullpath: () => "/test/project/src/components/index.ts",
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
vi.spyOn(glob, "glob").mockResolvedValue(mockGlobPaths as unknown as string[]);
|
|
97
|
+
|
|
98
|
+
const result = await findFiles(["**/*.ts"], {
|
|
99
|
+
cwd: "/test/project",
|
|
100
|
+
ignore: [],
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Assert that we get the full relative path, not just the filename
|
|
104
|
+
expect(result[0].relativePath).toBe("src/components/index.ts");
|
|
105
|
+
expect(result[0].relativePath).not.toBe("index.ts");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should filter out directories and only return files", async () => {
|
|
109
|
+
const mockGlobPaths = [
|
|
110
|
+
{
|
|
111
|
+
isFile: () => true,
|
|
112
|
+
relative: () => "src/index.ts",
|
|
113
|
+
fullpath: () => "/test/project/src/index.ts",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
isFile: () => false, // Directory
|
|
117
|
+
relative: () => "src/components",
|
|
118
|
+
fullpath: () => "/test/project/src/components",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
isFile: () => true,
|
|
122
|
+
relative: () => "src/utils.ts",
|
|
123
|
+
fullpath: () => "/test/project/src/utils.ts",
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
vi.spyOn(glob, "glob").mockResolvedValue(mockGlobPaths as unknown as string[]);
|
|
128
|
+
|
|
129
|
+
const result = await findFiles(["**/*.ts"], {
|
|
130
|
+
cwd: "/test/project",
|
|
131
|
+
ignore: [],
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(result).toHaveLength(2);
|
|
135
|
+
expect(result.map((f) => f.relativePath)).toEqual(["src/index.ts", "src/utils.ts"]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should handle empty results", async () => {
|
|
139
|
+
vi.spyOn(glob, "glob").mockResolvedValue([]);
|
|
140
|
+
|
|
141
|
+
const result = await findFiles(["**/*.ts"], {
|
|
142
|
+
cwd: "/test/project",
|
|
143
|
+
ignore: [],
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(result).toEqual([]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should pass options to glob correctly", async () => {
|
|
150
|
+
const globSpy = vi.spyOn(glob, "glob").mockResolvedValue([]);
|
|
151
|
+
|
|
152
|
+
await findFiles(["**/*.ts", "**/*.js"], {
|
|
153
|
+
cwd: "/test/project",
|
|
154
|
+
ignore: ["**/*.spec.ts"],
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(globSpy).toHaveBeenCalledWith(
|
|
158
|
+
["**/*.ts", "**/*.js"],
|
|
159
|
+
expect.objectContaining({
|
|
160
|
+
cwd: "/test/project",
|
|
161
|
+
ignore: ["**/*.spec.ts"],
|
|
162
|
+
withFileTypes: true,
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("should work with nested directory structures", async () => {
|
|
168
|
+
const mockGlobPaths = [
|
|
169
|
+
{
|
|
170
|
+
isFile: () => true,
|
|
171
|
+
relative: () => "deep/nested/path/to/file.ts",
|
|
172
|
+
fullpath: () => "/test/project/deep/nested/path/to/file.ts",
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
vi.spyOn(glob, "glob").mockResolvedValue(mockGlobPaths as unknown as string[]);
|
|
177
|
+
|
|
178
|
+
const result = await findFiles(["**/*.ts"], {
|
|
179
|
+
cwd: "/test/project",
|
|
180
|
+
ignore: [],
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
expect(result[0]).toEqual({
|
|
184
|
+
relativePath: "deep/nested/path/to/file.ts",
|
|
185
|
+
fullPath: "/test/project/deep/nested/path/to/file.ts",
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
// Atomic saves are a real-filesystem behaviour, so this suite opts out of the memfs mock.
|
|
4
|
+
vi.unmock("fs");
|
|
5
|
+
vi.unmock("node:fs");
|
|
6
|
+
vi.unmock("fs/promises");
|
|
7
|
+
vi.unmock("node:fs/promises");
|
|
8
|
+
|
|
9
|
+
const { mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } = await import("node:fs");
|
|
10
|
+
const { tmpdir } = await import("node:os");
|
|
11
|
+
const { join } = await import("node:path");
|
|
12
|
+
const { watch } = await import("../../src/io/watch.js");
|
|
13
|
+
|
|
14
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
|
+
|
|
16
|
+
/** Wait until `predicate` holds, polling, or fail after `timeoutMs`. */
|
|
17
|
+
async function eventually(predicate: () => boolean, timeoutMs = 2000) {
|
|
18
|
+
const start = Date.now();
|
|
19
|
+
while (!predicate()) {
|
|
20
|
+
if (Date.now() - start > timeoutMs) throw new Error("timed out waiting for watch events");
|
|
21
|
+
await sleep(20);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// #505: many editors (vim, sed -i, "safe write") and AI agents save by writing a temp file
|
|
26
|
+
// and renaming it over the original. With Node's recursive fs.watch on Linux the watcher then
|
|
27
|
+
// went silent for that file, so hot reload stopped reacting to it.
|
|
28
|
+
describe("io.watch on a real filesystem", () => {
|
|
29
|
+
let dir: string | undefined;
|
|
30
|
+
let close: (() => void) | undefined;
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
close?.();
|
|
34
|
+
if (dir) rmSync(dir, { recursive: true, force: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("keeps reporting changes to a file after it is replaced by an atomic save", async () => {
|
|
38
|
+
dir = mkdtempSync(join(tmpdir(), "decohere-watch-"));
|
|
39
|
+
mkdirSync(join(dir, "src"));
|
|
40
|
+
const file = join(dir, "src", "a.ts");
|
|
41
|
+
writeFileSync(file, "v0");
|
|
42
|
+
|
|
43
|
+
const changes: string[] = [];
|
|
44
|
+
const watcher = watch(dir, { ignoreInitial: true });
|
|
45
|
+
close = watcher.close;
|
|
46
|
+
watcher.on("change", (f) => changes.push(f.relativePath));
|
|
47
|
+
await watcher.ready;
|
|
48
|
+
|
|
49
|
+
for (let i = 1; i <= 3; i++) {
|
|
50
|
+
const tmp = join(dir, "src", `.a.ts.tmp${i}`);
|
|
51
|
+
writeFileSync(tmp, `v${i}`);
|
|
52
|
+
renameSync(tmp, file);
|
|
53
|
+
await eventually(() => changes.filter((p) => p === "src/a.ts").length >= i);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
writeFileSync(file, "a plain save after the atomic ones");
|
|
57
|
+
await eventually(() => changes.filter((p) => p === "src/a.ts").length >= 4);
|
|
58
|
+
expect(changes.filter((p) => p === "src/a.ts").length).toBeGreaterThanOrEqual(4);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("reports files in a directory created after the watch started", async () => {
|
|
62
|
+
dir = mkdtempSync(join(tmpdir(), "decohere-watch-"));
|
|
63
|
+
const added: string[] = [];
|
|
64
|
+
const watcher = watch(dir, { ignoreInitial: true });
|
|
65
|
+
close = watcher.close;
|
|
66
|
+
watcher.on("add", (f) => added.push(f.relativePath));
|
|
67
|
+
await watcher.ready;
|
|
68
|
+
|
|
69
|
+
mkdirSync(join(dir, "feature"));
|
|
70
|
+
await sleep(50);
|
|
71
|
+
writeFileSync(join(dir, "feature", "new.ts"), "x");
|
|
72
|
+
|
|
73
|
+
await eventually(() => added.includes("feature/new.ts"));
|
|
74
|
+
expect(added).toContain("feature/new.ts");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fs, vol } from "memfs";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { watch } from "../../src/io/watch.js";
|
|
5
|
+
|
|
6
|
+
vi.mock("fs");
|
|
7
|
+
vi.mock("node:fs");
|
|
8
|
+
vi.mock("fs/promises");
|
|
9
|
+
vi.mock("node:fs/promises");
|
|
10
|
+
|
|
11
|
+
describe("io.watch", () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vol.reset();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
vol.reset();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("emits change for nested subdirectory files", async () => {
|
|
21
|
+
const rootPath = "/workspace/project/src";
|
|
22
|
+
const nestedDirectoryPath = path.join(rootPath, "nested", "deeper");
|
|
23
|
+
const nestedFilePath = path.join(nestedDirectoryPath, "index.ts");
|
|
24
|
+
const expectedRelativePath = path.posix.join("nested", "deeper", "index.ts");
|
|
25
|
+
|
|
26
|
+
fs.mkdirSync(nestedDirectoryPath, { recursive: true });
|
|
27
|
+
fs.writeFileSync(nestedFilePath, "export const v = 1;", { encoding: "utf8" });
|
|
28
|
+
|
|
29
|
+
await new Promise<void>((resolve, reject) => {
|
|
30
|
+
const watcher = watch(rootPath);
|
|
31
|
+
|
|
32
|
+
watcher.on("change", (file) => {
|
|
33
|
+
if (file.relativePath !== expectedRelativePath) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
expect(file.fullPath).toBe(nestedFilePath);
|
|
38
|
+
watcher.close();
|
|
39
|
+
resolve();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
setTimeout(() => {
|
|
43
|
+
fs.writeFileSync(nestedFilePath, "export const v = 2;", { encoding: "utf8" });
|
|
44
|
+
}, 30);
|
|
45
|
+
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
watcher.close();
|
|
48
|
+
reject(new Error("expected change event was not emitted for nested file"));
|
|
49
|
+
}, 800);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("does not emit initial add events when ignoreInitial is true", async () => {
|
|
54
|
+
const rootPath = "/workspace/project/src";
|
|
55
|
+
const existingFilePath = path.join(rootPath, "index.ts");
|
|
56
|
+
|
|
57
|
+
fs.mkdirSync(rootPath, { recursive: true });
|
|
58
|
+
fs.writeFileSync(existingFilePath, "export const v = 1;", { encoding: "utf8" });
|
|
59
|
+
|
|
60
|
+
await new Promise<void>((resolve, reject) => {
|
|
61
|
+
const watcher = watch(rootPath, { ignoreInitial: true });
|
|
62
|
+
|
|
63
|
+
watcher.on("add", () => {
|
|
64
|
+
watcher.close();
|
|
65
|
+
reject(new Error("unexpected initial add event"));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
setTimeout(() => {
|
|
69
|
+
watcher.close();
|
|
70
|
+
resolve();
|
|
71
|
+
}, 100);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("emits add for a new file when ignoreInitial is true", async () => {
|
|
76
|
+
const rootPath = "/workspace/project/src";
|
|
77
|
+
const addedFilePath = path.join(rootPath, "new-file.ts");
|
|
78
|
+
|
|
79
|
+
fs.mkdirSync(rootPath, { recursive: true });
|
|
80
|
+
fs.writeFileSync(path.join(rootPath, "existing.ts"), "export const x = 1;", {
|
|
81
|
+
encoding: "utf8",
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await new Promise<void>((resolve, reject) => {
|
|
85
|
+
const watcher = watch(rootPath, { ignoreInitial: true });
|
|
86
|
+
|
|
87
|
+
watcher.on("add", (file) => {
|
|
88
|
+
if (file.relativePath !== "new-file.ts") {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
expect(file.fullPath).toBe(addedFilePath);
|
|
93
|
+
watcher.close();
|
|
94
|
+
resolve();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
watcher.ready.then(() => {
|
|
98
|
+
fs.writeFileSync(addedFilePath, "export const y = 2;", { encoding: "utf8" });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
setTimeout(() => {
|
|
102
|
+
watcher.close();
|
|
103
|
+
reject(new Error("expected add event was not emitted for new file"));
|
|
104
|
+
}, 800);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("emits unlink when a watched file is deleted", async () => {
|
|
109
|
+
const rootPath = "/workspace/project/src";
|
|
110
|
+
const removedFilePath = path.join(rootPath, "index.ts");
|
|
111
|
+
|
|
112
|
+
fs.mkdirSync(rootPath, { recursive: true });
|
|
113
|
+
fs.writeFileSync(removedFilePath, "export const v = 1;", { encoding: "utf8" });
|
|
114
|
+
|
|
115
|
+
await new Promise<void>((resolve, reject) => {
|
|
116
|
+
const watcher = watch(rootPath, { ignoreInitial: true });
|
|
117
|
+
|
|
118
|
+
watcher.on("unlink", (file) => {
|
|
119
|
+
if (file.relativePath !== "index.ts") {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
expect(file.fullPath).toBe(removedFilePath);
|
|
124
|
+
watcher.close();
|
|
125
|
+
resolve();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
watcher.ready.then(() => {
|
|
129
|
+
fs.unlinkSync(removedFilePath);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
setTimeout(() => {
|
|
133
|
+
watcher.close();
|
|
134
|
+
reject(new Error("expected unlink event was not emitted for removed file"));
|
|
135
|
+
}, 800);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("applies include patterns to emitted events", async () => {
|
|
140
|
+
const rootPath = "/workspace/project/src";
|
|
141
|
+
const ignoredFilePath = path.join(rootPath, "ignored.js");
|
|
142
|
+
const includedFilePath = path.join(rootPath, "included.ts");
|
|
143
|
+
|
|
144
|
+
fs.mkdirSync(rootPath, { recursive: true });
|
|
145
|
+
|
|
146
|
+
await new Promise<void>((resolve, reject) => {
|
|
147
|
+
const watcher = watch(rootPath, { ignoreInitial: true, include: ["**/*.ts"] });
|
|
148
|
+
|
|
149
|
+
watcher.on("add", (file) => {
|
|
150
|
+
if (file.relativePath === "ignored.js") {
|
|
151
|
+
watcher.close();
|
|
152
|
+
reject(new Error("include filter emitted ignored.js unexpectedly"));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (file.relativePath !== "included.ts") {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
expect(file.fullPath).toBe(includedFilePath);
|
|
161
|
+
watcher.close();
|
|
162
|
+
resolve();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
watcher.ready.then(() => {
|
|
166
|
+
fs.writeFileSync(ignoredFilePath, "export const ignored = true;", {
|
|
167
|
+
encoding: "utf8",
|
|
168
|
+
});
|
|
169
|
+
fs.writeFileSync(includedFilePath, "export const included = true;", {
|
|
170
|
+
encoding: "utf8",
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
setTimeout(() => {
|
|
175
|
+
watcher.close();
|
|
176
|
+
reject(new Error("include filter did not emit included.ts"));
|
|
177
|
+
}, 800);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("applies ignore patterns to emitted events", async () => {
|
|
182
|
+
const rootPath = "/workspace/project/src";
|
|
183
|
+
const ignoredFilePath = path.join(rootPath, "index.spec.ts");
|
|
184
|
+
const includedFilePath = path.join(rootPath, "index.ts");
|
|
185
|
+
|
|
186
|
+
fs.mkdirSync(rootPath, { recursive: true });
|
|
187
|
+
|
|
188
|
+
await new Promise<void>((resolve, reject) => {
|
|
189
|
+
const watcher = watch(rootPath, {
|
|
190
|
+
ignoreInitial: true,
|
|
191
|
+
ignore: ["**/*.spec.ts"],
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
watcher.on("add", (file) => {
|
|
195
|
+
if (file.relativePath === "index.spec.ts") {
|
|
196
|
+
watcher.close();
|
|
197
|
+
reject(new Error("ignore filter emitted index.spec.ts unexpectedly"));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (file.relativePath !== "index.ts") {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
expect(file.fullPath).toBe(includedFilePath);
|
|
206
|
+
watcher.close();
|
|
207
|
+
resolve();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
watcher.ready.then(() => {
|
|
211
|
+
fs.writeFileSync(ignoredFilePath, "export const ignored = true;", {
|
|
212
|
+
encoding: "utf8",
|
|
213
|
+
});
|
|
214
|
+
fs.writeFileSync(includedFilePath, "export const included = true;", {
|
|
215
|
+
encoding: "utf8",
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
setTimeout(() => {
|
|
220
|
+
watcher.close();
|
|
221
|
+
reject(new Error("ignore filter did not emit index.ts"));
|
|
222
|
+
}, 800);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("emits ready after watcher initialization", async () => {
|
|
227
|
+
const rootPath = "/workspace/project/src";
|
|
228
|
+
fs.mkdirSync(rootPath, { recursive: true });
|
|
229
|
+
fs.writeFileSync(path.join(rootPath, "index.ts"), "export const v = 1;", {
|
|
230
|
+
encoding: "utf8",
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
await new Promise<void>((resolve, reject) => {
|
|
234
|
+
const watcher = watch(rootPath, { ignoreInitial: true });
|
|
235
|
+
|
|
236
|
+
watcher.on("ready", () => {
|
|
237
|
+
watcher.close();
|
|
238
|
+
resolve();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
setTimeout(() => {
|
|
242
|
+
watcher.close();
|
|
243
|
+
reject(new Error("watcher did not emit ready event"));
|
|
244
|
+
}, 200);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("resolves watcher.ready after initialization", async () => {
|
|
249
|
+
const rootPath = "/workspace/project/src";
|
|
250
|
+
fs.mkdirSync(rootPath, { recursive: true });
|
|
251
|
+
const watcher = watch(rootPath, { ignoreInitial: true });
|
|
252
|
+
|
|
253
|
+
await expect(watcher.ready).resolves.toBeUndefined();
|
|
254
|
+
watcher.close();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("emits change only once for a single write", async () => {
|
|
258
|
+
const rootPath = "/workspace/project/src";
|
|
259
|
+
const filePath = path.join(rootPath, "index.ts");
|
|
260
|
+
|
|
261
|
+
fs.mkdirSync(rootPath, { recursive: true });
|
|
262
|
+
fs.writeFileSync(filePath, "export const v = 1;", { encoding: "utf8" });
|
|
263
|
+
|
|
264
|
+
await new Promise<void>((resolve, reject) => {
|
|
265
|
+
const watcher = watch(rootPath, { ignoreInitial: true });
|
|
266
|
+
let changeCount = 0;
|
|
267
|
+
|
|
268
|
+
watcher.on("change", (file) => {
|
|
269
|
+
if (file.relativePath !== "index.ts") {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
changeCount += 1;
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
watcher.ready.then(() => {
|
|
277
|
+
fs.writeFileSync(filePath, "export const v = 2;", { encoding: "utf8" });
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
setTimeout(() => {
|
|
281
|
+
watcher.close();
|
|
282
|
+
try {
|
|
283
|
+
expect(changeCount).toBeLessThanOrEqual(1);
|
|
284
|
+
resolve();
|
|
285
|
+
} catch (error) {
|
|
286
|
+
reject(error);
|
|
287
|
+
}
|
|
288
|
+
}, 300);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { vi } from "vitest";
|
|
2
|
+
import type { Constructable } from "../src/reflection/index.js";
|
|
3
|
+
|
|
4
|
+
export function mockPrototype<T extends object>(obj: Constructable<T>): T {
|
|
5
|
+
const funcs = Object.entries(Object.getOwnPropertyDescriptors(obj.prototype)).filter(
|
|
6
|
+
([, prop]) => typeof prop.value === "function",
|
|
7
|
+
);
|
|
8
|
+
const mock = Object.fromEntries(funcs.map(([key]) => [key, vi.fn()]));
|
|
9
|
+
const instance = new obj();
|
|
10
|
+
Object.assign(instance, mock);
|
|
11
|
+
return instance;
|
|
12
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { Semver, SemverError } from "../../src/semver/index.js";
|
|
3
|
+
|
|
4
|
+
describe("Semver", () => {
|
|
5
|
+
it("bumps major versions", () => {
|
|
6
|
+
expect(new Semver("1.2.3").increment("major").toString()).toBe("2.0.0");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("bumps minor versions", () => {
|
|
10
|
+
expect(new Semver("1.2.3").increment("minor").toString()).toBe("1.3.0");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("bumps patch versions", () => {
|
|
14
|
+
expect(new Semver("1.2.3").increment("patch").toString()).toBe("1.2.4");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("starts prerelease when none exists", () => {
|
|
18
|
+
expect(new Semver("1.2.3").increment("prerelease", "dev").toString()).toBe("1.2.4-dev.0");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("increments prerelease counter when identifier matches", () => {
|
|
22
|
+
expect(new Semver("1.2.3-dev.4").increment("prerelease", "dev").toString()).toBe(
|
|
23
|
+
"1.2.3-dev.5",
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("resets prerelease counter when identifier differs", () => {
|
|
28
|
+
expect(new Semver("1.2.3-rc.4").increment("prerelease", "dev").toString()).toBe(
|
|
29
|
+
"1.2.3-dev.0",
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("preserves prerelease dotted segments before counter", () => {
|
|
34
|
+
expect(new Semver("1.2.3-dev.feature.7").increment("prerelease", "dev").toString()).toBe(
|
|
35
|
+
"1.2.3-dev.feature.8",
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("allows chained increments", () => {
|
|
40
|
+
expect(
|
|
41
|
+
new Semver("1.2.3").increment("patch").increment("prerelease", "dev").toString(),
|
|
42
|
+
).toBe("1.2.5-dev.0");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("throws SemverError for invalid semantic version", () => {
|
|
46
|
+
expect(() => new Semver("not-a-version")).toThrow(SemverError);
|
|
47
|
+
expect(() => new Semver("not-a-version")).toThrow(
|
|
48
|
+
"Invalid semantic version: not-a-version",
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
});
|