@saeeol/core 7.3.2 → 7.3.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.
@@ -1,338 +0,0 @@
1
- import { describe, test, expect } from "bun:test"
2
- import { Effect, Layer, FileSystem } from "effect"
3
- import { NodeFileSystem } from "@effect/platform-node"
4
- import { AppFileSystem } from "@saeeol/core/filesystem"
5
- import { testEffect } from "../lib/effect"
6
- import path from "path"
7
-
8
- const live = AppFileSystem.layer.pipe(Layer.provideMerge(NodeFileSystem.layer))
9
- const { effect: it } = testEffect(live)
10
-
11
- describe("AppFileSystem", () => {
12
- describe("isDir", () => {
13
- it(
14
- "returns true for directories",
15
- Effect.gen(function* () {
16
- const fs = yield* AppFileSystem.Service
17
- const filesys = yield* FileSystem.FileSystem
18
- const tmp = yield* filesys.makeTempDirectoryScoped()
19
- expect(yield* fs.isDir(tmp)).toBe(true)
20
- }),
21
- )
22
-
23
- it(
24
- "returns false for files",
25
- Effect.gen(function* () {
26
- const fs = yield* AppFileSystem.Service
27
- const filesys = yield* FileSystem.FileSystem
28
- const tmp = yield* filesys.makeTempDirectoryScoped()
29
- const file = path.join(tmp, "test.txt")
30
- yield* filesys.writeFileString(file, "hello")
31
- expect(yield* fs.isDir(file)).toBe(false)
32
- }),
33
- )
34
-
35
- it(
36
- "returns false for non-existent paths",
37
- Effect.gen(function* () {
38
- const fs = yield* AppFileSystem.Service
39
- expect(yield* fs.isDir("/tmp/nonexistent-" + Math.random())).toBe(false)
40
- }),
41
- )
42
- })
43
-
44
- describe("isFile", () => {
45
- it(
46
- "returns true for files",
47
- Effect.gen(function* () {
48
- const fs = yield* AppFileSystem.Service
49
- const filesys = yield* FileSystem.FileSystem
50
- const tmp = yield* filesys.makeTempDirectoryScoped()
51
- const file = path.join(tmp, "test.txt")
52
- yield* filesys.writeFileString(file, "hello")
53
- expect(yield* fs.isFile(file)).toBe(true)
54
- }),
55
- )
56
-
57
- it(
58
- "returns false for directories",
59
- Effect.gen(function* () {
60
- const fs = yield* AppFileSystem.Service
61
- const filesys = yield* FileSystem.FileSystem
62
- const tmp = yield* filesys.makeTempDirectoryScoped()
63
- expect(yield* fs.isFile(tmp)).toBe(false)
64
- }),
65
- )
66
- })
67
-
68
- describe("readJson / writeJson", () => {
69
- it(
70
- "round-trips JSON data",
71
- Effect.gen(function* () {
72
- const fs = yield* AppFileSystem.Service
73
- const filesys = yield* FileSystem.FileSystem
74
- const tmp = yield* filesys.makeTempDirectoryScoped()
75
- const file = path.join(tmp, "data.json")
76
- const data = { name: "test", count: 42, nested: { ok: true } }
77
-
78
- yield* fs.writeJson(file, data)
79
- const result = yield* fs.readJson(file)
80
-
81
- expect(result).toEqual(data)
82
- }),
83
- )
84
- })
85
-
86
- describe("ensureDir", () => {
87
- it(
88
- "creates nested directories",
89
- Effect.gen(function* () {
90
- const fs = yield* AppFileSystem.Service
91
- const filesys = yield* FileSystem.FileSystem
92
- const tmp = yield* filesys.makeTempDirectoryScoped()
93
- const nested = path.join(tmp, "a", "b", "c")
94
-
95
- yield* fs.ensureDir(nested)
96
-
97
- const info = yield* filesys.stat(nested)
98
- expect(info.type).toBe("Directory")
99
- }),
100
- )
101
-
102
- it(
103
- "is idempotent",
104
- Effect.gen(function* () {
105
- const fs = yield* AppFileSystem.Service
106
- const filesys = yield* FileSystem.FileSystem
107
- const tmp = yield* filesys.makeTempDirectoryScoped()
108
- const dir = path.join(tmp, "existing")
109
- yield* filesys.makeDirectory(dir)
110
-
111
- yield* fs.ensureDir(dir)
112
-
113
- const info = yield* filesys.stat(dir)
114
- expect(info.type).toBe("Directory")
115
- }),
116
- )
117
- })
118
-
119
- describe("writeWithDirs", () => {
120
- it(
121
- "creates parent directories if missing",
122
- Effect.gen(function* () {
123
- const fs = yield* AppFileSystem.Service
124
- const filesys = yield* FileSystem.FileSystem
125
- const tmp = yield* filesys.makeTempDirectoryScoped()
126
- const file = path.join(tmp, "deep", "nested", "file.txt")
127
-
128
- yield* fs.writeWithDirs(file, "hello")
129
-
130
- expect(yield* filesys.readFileString(file)).toBe("hello")
131
- }),
132
- )
133
-
134
- it(
135
- "writes directly when parent exists",
136
- Effect.gen(function* () {
137
- const fs = yield* AppFileSystem.Service
138
- const filesys = yield* FileSystem.FileSystem
139
- const tmp = yield* filesys.makeTempDirectoryScoped()
140
- const file = path.join(tmp, "direct.txt")
141
-
142
- yield* fs.writeWithDirs(file, "world")
143
-
144
- expect(yield* filesys.readFileString(file)).toBe("world")
145
- }),
146
- )
147
-
148
- it(
149
- "writes Uint8Array content",
150
- Effect.gen(function* () {
151
- const fs = yield* AppFileSystem.Service
152
- const filesys = yield* FileSystem.FileSystem
153
- const tmp = yield* filesys.makeTempDirectoryScoped()
154
- const file = path.join(tmp, "binary.bin")
155
- const content = new Uint8Array([0x00, 0x01, 0x02, 0x03])
156
-
157
- yield* fs.writeWithDirs(file, content)
158
-
159
- const result = yield* filesys.readFile(file)
160
- expect(new Uint8Array(result)).toEqual(content)
161
- }),
162
- )
163
- })
164
-
165
- describe("findUp", () => {
166
- it(
167
- "finds target in start directory",
168
- Effect.gen(function* () {
169
- const fs = yield* AppFileSystem.Service
170
- const filesys = yield* FileSystem.FileSystem
171
- const tmp = yield* filesys.makeTempDirectoryScoped()
172
- yield* filesys.writeFileString(path.join(tmp, "target.txt"), "found")
173
-
174
- const result = yield* fs.findUp("target.txt", tmp)
175
- expect(result).toEqual([path.join(tmp, "target.txt")])
176
- }),
177
- )
178
-
179
- it(
180
- "finds target in parent directories",
181
- Effect.gen(function* () {
182
- const fs = yield* AppFileSystem.Service
183
- const filesys = yield* FileSystem.FileSystem
184
- const tmp = yield* filesys.makeTempDirectoryScoped()
185
- yield* filesys.writeFileString(path.join(tmp, "marker"), "root")
186
- const child = path.join(tmp, "a", "b")
187
- yield* filesys.makeDirectory(child, { recursive: true })
188
-
189
- const result = yield* fs.findUp("marker", child, tmp)
190
- expect(result).toEqual([path.join(tmp, "marker")])
191
- }),
192
- )
193
-
194
- it(
195
- "returns empty array when not found",
196
- Effect.gen(function* () {
197
- const fs = yield* AppFileSystem.Service
198
- const filesys = yield* FileSystem.FileSystem
199
- const tmp = yield* filesys.makeTempDirectoryScoped()
200
- const result = yield* fs.findUp("nonexistent", tmp, tmp)
201
- expect(result).toEqual([])
202
- }),
203
- )
204
- })
205
-
206
- describe("up", () => {
207
- it(
208
- "finds multiple targets walking up",
209
- Effect.gen(function* () {
210
- const fs = yield* AppFileSystem.Service
211
- const filesys = yield* FileSystem.FileSystem
212
- const tmp = yield* filesys.makeTempDirectoryScoped()
213
- yield* filesys.writeFileString(path.join(tmp, "a.txt"), "a")
214
- yield* filesys.writeFileString(path.join(tmp, "b.txt"), "b")
215
- const child = path.join(tmp, "sub")
216
- yield* filesys.makeDirectory(child)
217
- yield* filesys.writeFileString(path.join(child, "a.txt"), "a-child")
218
-
219
- const result = yield* fs.up({ targets: ["a.txt", "b.txt"], start: child, stop: tmp })
220
-
221
- expect(result).toContain(path.join(child, "a.txt"))
222
- expect(result).toContain(path.join(tmp, "a.txt"))
223
- expect(result).toContain(path.join(tmp, "b.txt"))
224
- }),
225
- )
226
- })
227
-
228
- describe("glob", () => {
229
- it(
230
- "finds files matching pattern",
231
- Effect.gen(function* () {
232
- const fs = yield* AppFileSystem.Service
233
- const filesys = yield* FileSystem.FileSystem
234
- const tmp = yield* filesys.makeTempDirectoryScoped()
235
- yield* filesys.writeFileString(path.join(tmp, "a.ts"), "a")
236
- yield* filesys.writeFileString(path.join(tmp, "b.ts"), "b")
237
- yield* filesys.writeFileString(path.join(tmp, "c.json"), "c")
238
-
239
- const result = yield* fs.glob("*.ts", { cwd: tmp })
240
- expect(result.sort()).toEqual(["a.ts", "b.ts"])
241
- }),
242
- )
243
-
244
- it(
245
- "supports absolute paths",
246
- Effect.gen(function* () {
247
- const fs = yield* AppFileSystem.Service
248
- const filesys = yield* FileSystem.FileSystem
249
- const tmp = yield* filesys.makeTempDirectoryScoped()
250
- yield* filesys.writeFileString(path.join(tmp, "file.txt"), "hello")
251
-
252
- const result = yield* fs.glob("*.txt", { cwd: tmp, absolute: true })
253
- expect(result).toEqual([path.join(tmp, "file.txt")])
254
- }),
255
- )
256
- })
257
-
258
- describe("globMatch", () => {
259
- it(
260
- "matches patterns",
261
- Effect.gen(function* () {
262
- const fs = yield* AppFileSystem.Service
263
- expect(fs.globMatch("*.ts", "foo.ts")).toBe(true)
264
- expect(fs.globMatch("*.ts", "foo.json")).toBe(false)
265
- expect(fs.globMatch("src/**", "src/a/b.ts")).toBe(true)
266
- }),
267
- )
268
- })
269
-
270
- describe("globUp", () => {
271
- it(
272
- "finds files walking up directories",
273
- Effect.gen(function* () {
274
- const fs = yield* AppFileSystem.Service
275
- const filesys = yield* FileSystem.FileSystem
276
- const tmp = yield* filesys.makeTempDirectoryScoped()
277
- yield* filesys.writeFileString(path.join(tmp, "root.md"), "root")
278
- const child = path.join(tmp, "a", "b")
279
- yield* filesys.makeDirectory(child, { recursive: true })
280
- yield* filesys.writeFileString(path.join(child, "leaf.md"), "leaf")
281
-
282
- const result = yield* fs.globUp("*.md", child, tmp)
283
- expect(result).toContain(path.join(child, "leaf.md"))
284
- expect(result).toContain(path.join(tmp, "root.md"))
285
- }),
286
- )
287
- })
288
-
289
- describe("built-in passthrough", () => {
290
- it(
291
- "exists works",
292
- Effect.gen(function* () {
293
- yield* AppFileSystem.Service
294
- const filesys = yield* FileSystem.FileSystem
295
- const tmp = yield* filesys.makeTempDirectoryScoped()
296
- const file = path.join(tmp, "exists.txt")
297
- yield* filesys.writeFileString(file, "yes")
298
-
299
- expect(yield* filesys.exists(file)).toBe(true)
300
- expect(yield* filesys.exists(file + ".nope")).toBe(false)
301
- }),
302
- )
303
-
304
- it(
305
- "remove works",
306
- Effect.gen(function* () {
307
- yield* AppFileSystem.Service
308
- const filesys = yield* FileSystem.FileSystem
309
- const tmp = yield* filesys.makeTempDirectoryScoped()
310
- const file = path.join(tmp, "delete-me.txt")
311
- yield* filesys.writeFileString(file, "bye")
312
-
313
- yield* filesys.remove(file)
314
-
315
- expect(yield* filesys.exists(file)).toBe(false)
316
- }),
317
- )
318
- })
319
-
320
- describe("pure helpers", () => {
321
- test("mimeType returns correct types", () => {
322
- expect(AppFileSystem.mimeType("file.json")).toBe("application/json")
323
- expect(AppFileSystem.mimeType("image.png")).toBe("image/png")
324
- expect(AppFileSystem.mimeType("unknown.qzx")).toBe("application/octet-stream")
325
- })
326
-
327
- test("contains checks path containment", () => {
328
- expect(AppFileSystem.contains("/a/b", "/a/b/c")).toBe(true)
329
- expect(AppFileSystem.contains("/a/b", "/a/c")).toBe(false)
330
- })
331
-
332
- test("overlaps detects overlapping paths", () => {
333
- expect(AppFileSystem.overlaps("/a/b", "/a/b/c")).toBe(true)
334
- expect(AppFileSystem.overlaps("/a/b/c", "/a/b")).toBe(true)
335
- expect(AppFileSystem.overlaps("/a", "/b")).toBe(false)
336
- })
337
- })
338
- })
@@ -1,60 +0,0 @@
1
- import fs from "fs/promises"
2
- import os from "os"
3
- import { Effect, Layer } from "effect"
4
- import { AppFileSystem } from "@saeeol/core/filesystem"
5
- import { EffectFlock } from "@saeeol/core/util/effect-flock"
6
- import { Global } from "@saeeol/core/global"
7
-
8
- type Msg = {
9
- key: string
10
- dir: string
11
- holdMs?: number
12
- ready?: string
13
- active?: string
14
- done?: string
15
- }
16
-
17
- function sleep(ms: number) {
18
- return new Promise<void>((resolve) => setTimeout(resolve, ms))
19
- }
20
-
21
- const msg: Msg = JSON.parse(process.argv[2])
22
-
23
- const testGlobal = Global.layerWith({
24
- home: os.homedir(),
25
- data: os.tmpdir(),
26
- cache: os.tmpdir(),
27
- config: os.tmpdir(),
28
- state: os.tmpdir(),
29
- bin: os.tmpdir(),
30
- log: os.tmpdir(),
31
- })
32
-
33
- const testLayer = EffectFlock.layer.pipe(Layer.provide(testGlobal), Layer.provide(AppFileSystem.defaultLayer))
34
-
35
- async function job() {
36
- if (msg.ready) await fs.writeFile(msg.ready, String(process.pid))
37
- if (msg.active) await fs.writeFile(msg.active, String(process.pid), { flag: "wx" })
38
-
39
- try {
40
- if (msg.holdMs && msg.holdMs > 0) await sleep(msg.holdMs)
41
- if (msg.done) await fs.appendFile(msg.done, "1\n")
42
- } finally {
43
- if (msg.active) await fs.rm(msg.active, { force: true })
44
- }
45
- }
46
-
47
- await Effect.runPromise(
48
- Effect.gen(function* () {
49
- const flock = yield* EffectFlock.Service
50
- yield* flock.withLock(
51
- Effect.promise(() => job()),
52
- msg.key,
53
- msg.dir,
54
- )
55
- }).pipe(Effect.provide(testLayer)),
56
- ).catch((err) => {
57
- const text = err instanceof Error ? (err.stack ?? err.message) : String(err)
58
- process.stderr.write(text)
59
- process.exit(1)
60
- })
@@ -1,72 +0,0 @@
1
- import fs from "fs/promises"
2
- import { Flock } from "@saeeol/core/util/flock"
3
-
4
- type Msg = {
5
- key: string
6
- dir: string
7
- staleMs?: number
8
- timeoutMs?: number
9
- baseDelayMs?: number
10
- maxDelayMs?: number
11
- holdMs?: number
12
- ready?: string
13
- active?: string
14
- done?: string
15
- }
16
-
17
- function sleep(ms: number) {
18
- return new Promise<void>((resolve) => {
19
- setTimeout(resolve, ms)
20
- })
21
- }
22
-
23
- function input() {
24
- const raw = process.argv[2]
25
- if (!raw) {
26
- throw new Error("Missing flock worker input")
27
- }
28
-
29
- return JSON.parse(raw) as Msg
30
- }
31
-
32
- async function job(input: Msg) {
33
- if (input.ready) {
34
- await fs.writeFile(input.ready, String(process.pid))
35
- }
36
-
37
- if (input.active) {
38
- await fs.writeFile(input.active, String(process.pid), { flag: "wx" })
39
- }
40
-
41
- try {
42
- if (input.holdMs && input.holdMs > 0) {
43
- await sleep(input.holdMs)
44
- }
45
-
46
- if (input.done) {
47
- await fs.appendFile(input.done, "1\n")
48
- }
49
- } finally {
50
- if (input.active) {
51
- await fs.rm(input.active, { force: true })
52
- }
53
- }
54
- }
55
-
56
- async function main() {
57
- const msg = input()
58
-
59
- await Flock.withLock(msg.key, () => job(msg), {
60
- dir: msg.dir,
61
- staleMs: msg.staleMs,
62
- timeoutMs: msg.timeoutMs,
63
- baseDelayMs: msg.baseDelayMs,
64
- maxDelayMs: msg.maxDelayMs,
65
- })
66
- }
67
-
68
- await main().catch((err) => {
69
- const text = err instanceof Error ? (err.stack ?? err.message) : String(err)
70
- process.stderr.write(text)
71
- process.exit(1)
72
- })
@@ -1,13 +0,0 @@
1
- import fs from "fs/promises"
2
- import { tmpdir as osTmpdir } from "os"
3
- import path from "path"
4
-
5
- export const tmpdir = async () => {
6
- const dir = await fs.mkdtemp(path.join(osTmpdir(), "saeeol-core-test-"))
7
- return {
8
- path: dir,
9
- async [Symbol.asyncDispose]() {
10
- await fs.rm(dir, { recursive: true, force: true })
11
- },
12
- }
13
- }
@@ -1,16 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import fs from "fs/promises"
3
- import os from "os"
4
- import path from "path"
5
- import { Global } from "@saeeol/core/global"
6
-
7
- describe("global paths", () => {
8
- test("tmp path is under the system temp directory", () => {
9
- expect(Global.Path.tmp).toBe(path.join(os.tmpdir(), "saeeol"))
10
- expect(Global.make().tmp).toBe(Global.Path.tmp)
11
- })
12
-
13
- test("tmp path is created on module load", async () => {
14
- expect((await fs.stat(Global.Path.tmp)).isDirectory()).toBe(true)
15
- })
16
- })
@@ -1,53 +0,0 @@
1
- import { test, type TestOptions } from "bun:test"
2
- import { Cause, Effect, Exit, Layer } from "effect"
3
- import type * as Scope from "effect/Scope"
4
- import * as TestClock from "effect/testing/TestClock"
5
- import * as TestConsole from "effect/testing/TestConsole"
6
-
7
- type Body<A, E, R> = Effect.Effect<A, E, R> | (() => Effect.Effect<A, E, R>)
8
-
9
- const body = <A, E, R>(value: Body<A, E, R>) => Effect.suspend(() => (typeof value === "function" ? value() : value))
10
-
11
- const run = <A, E, R, E2>(value: Body<A, E, R | Scope.Scope>, layer: Layer.Layer<R, E2>) =>
12
- Effect.gen(function* () {
13
- const exit = yield* body(value).pipe(Effect.scoped, Effect.provide(layer), Effect.exit)
14
- if (Exit.isFailure(exit)) {
15
- for (const err of Cause.prettyErrors(exit.cause)) {
16
- yield* Effect.logError(err)
17
- }
18
- }
19
- return yield* exit
20
- }).pipe(Effect.runPromise)
21
-
22
- const make = <R, E>(testLayer: Layer.Layer<R, E>, liveLayer: Layer.Layer<R, E>) => {
23
- const effect = <A, E2>(name: string, value: Body<A, E2, R | Scope.Scope>, opts?: number | TestOptions) =>
24
- test(name, () => run(value, testLayer), opts)
25
-
26
- effect.only = <A, E2>(name: string, value: Body<A, E2, R | Scope.Scope>, opts?: number | TestOptions) =>
27
- test.only(name, () => run(value, testLayer), opts)
28
-
29
- effect.skip = <A, E2>(name: string, value: Body<A, E2, R | Scope.Scope>, opts?: number | TestOptions) =>
30
- test.skip(name, () => run(value, testLayer), opts)
31
-
32
- const live = <A, E2>(name: string, value: Body<A, E2, R | Scope.Scope>, opts?: number | TestOptions) =>
33
- test(name, () => run(value, liveLayer), opts)
34
-
35
- live.only = <A, E2>(name: string, value: Body<A, E2, R | Scope.Scope>, opts?: number | TestOptions) =>
36
- test.only(name, () => run(value, liveLayer), opts)
37
-
38
- live.skip = <A, E2>(name: string, value: Body<A, E2, R | Scope.Scope>, opts?: number | TestOptions) =>
39
- test.skip(name, () => run(value, liveLayer), opts)
40
-
41
- return { effect, live }
42
- }
43
-
44
- // Test environment with TestClock and TestConsole
45
- const testEnv = Layer.mergeAll(TestConsole.layer, TestClock.layer())
46
-
47
- // Live environment - uses real clock, but keeps TestConsole for output capture
48
- const liveEnv = TestConsole.layer
49
-
50
- export const it = make(testEnv, liveEnv)
51
-
52
- export const testEffect = <R, E>(layer: Layer.Layer<R, E>) =>
53
- make(Layer.provideMerge(layer, testEnv), Layer.provideMerge(layer, liveEnv))
@@ -1,51 +0,0 @@
1
- import path from "path"
2
- import { describe, expect, test } from "bun:test"
3
- import { Effect } from "effect"
4
- import { NpmConfig } from "@saeeol/core/npm-config"
5
- import { tmpdir } from "./fixture/tmpdir"
6
-
7
- describe("NpmConfig.load", () => {
8
- test("reads registry from project .npmrc", async () => {
9
- await using tmp = await tmpdir()
10
- await Bun.write(path.join(tmp.path, ".npmrc"), "registry=https://registry.example.test/\n")
11
-
12
- const config = await Effect.runPromise(NpmConfig.load(tmp.path))
13
-
14
- expect(config.registry).toBe("https://registry.example.test/")
15
- })
16
-
17
- test("reads scoped registries from project .npmrc", async () => {
18
- await using tmp = await tmpdir()
19
- await Bun.write(path.join(tmp.path, ".npmrc"), "@acme:registry=https://npm.acme.test/\n")
20
-
21
- const config = await Effect.runPromise(NpmConfig.load(tmp.path))
22
-
23
- expect(config["@acme:registry"]).toBe("https://npm.acme.test/")
24
- })
25
-
26
- test("flattens boolean and list options", async () => {
27
- await using tmp = await tmpdir()
28
- await Bun.write(path.join(tmp.path, ".npmrc"), "ignore-scripts=true\nomit[]=dev\nomit[]=optional\n")
29
-
30
- const config = await Effect.runPromise(NpmConfig.load(tmp.path))
31
-
32
- expect(config.ignoreScripts).toBe(true)
33
- expect(config.omit).toEqual(["dev", "optional"])
34
- })
35
- })
36
-
37
- describe("NpmConfig.registry", () => {
38
- test("normalizes configured registry without trailing slash", async () => {
39
- await using tmp = await tmpdir()
40
- await Bun.write(path.join(tmp.path, ".npmrc"), "registry=https://registry.example.test/\n")
41
-
42
- await expect(Effect.runPromise(NpmConfig.registry(tmp.path))).resolves.toBe("https://registry.example.test")
43
- })
44
-
45
- test("leaves configured registry without trailing slash unchanged", async () => {
46
- await using tmp = await tmpdir()
47
- await Bun.write(path.join(tmp.path, ".npmrc"), "registry=https://registry.example.test\n")
48
-
49
- await expect(Effect.runPromise(NpmConfig.registry(tmp.path))).resolves.toBe("https://registry.example.test")
50
- })
51
- })