Skip to content

Commit 8f4da02

Browse files
committed
fix(cli): ensure store directory exists in createFileWithStore (#4286)
1 parent 0d3e54d commit 8f4da02

3 files changed

Lines changed: 115 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"trigger.dev": patch
3+
---
4+
5+
Ensure store directory exists before writing in createFileWithStore to prevent ENOENT crashes during dev session handover.
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { existsSync } from "node:fs";
2+
import { mkdtemp, readFile, rm, stat } from "node:fs/promises";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
5+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
6+
import { createFile, createFileWithStore, sanitizeHashForFilename } from "./fileSystem.js";
7+
8+
describe("fileSystem", () => {
9+
let testDir: string;
10+
11+
beforeEach(async () => {
12+
testDir = await mkdtemp(join(tmpdir(), "trigger-fs-test-"));
13+
});
14+
15+
afterEach(async () => {
16+
await rm(testDir, { recursive: true, force: true });
17+
});
18+
19+
describe("sanitizeHashForFilename", () => {
20+
it("replaces forward slashes with underscores and pluses with hyphens", () => {
21+
expect(sanitizeHashForFilename("abc/def+ghi")).toBe("abc_def-ghi");
22+
expect(sanitizeHashForFilename("a/b/c+d+e")).toBe("a_b_c-d-e");
23+
expect(sanitizeHashForFilename("clean-hash_123")).toBe("clean-hash_123");
24+
});
25+
});
26+
27+
describe("createFileWithStore", () => {
28+
it("succeeds when storeDir does not exist yet", async () => {
29+
const storeDir = join(testDir, "store");
30+
const buildDir = join(testDir, "build");
31+
const filePath = join(buildDir, "index.js");
32+
const content = "console.log('hello world');";
33+
const hash = "hash123/abc+def";
34+
35+
expect(existsSync(storeDir)).toBe(false);
36+
37+
const result = await createFileWithStore(filePath, content, storeDir, hash);
38+
39+
expect(result).toBe(filePath);
40+
expect(existsSync(storeDir)).toBe(true);
41+
expect(existsSync(filePath)).toBe(true);
42+
expect(await readFile(filePath, "utf8")).toBe(content);
43+
44+
const storeFile = join(storeDir, sanitizeHashForFilename(hash));
45+
expect(existsSync(storeFile)).toBe(true);
46+
expect(await readFile(storeFile, "utf8")).toBe(content);
47+
});
48+
49+
it("uses content-addressable caching when storeDir and file already exist", async () => {
50+
const storeDir = join(testDir, "store");
51+
const buildDir = join(testDir, "build");
52+
const filePath1 = join(buildDir, "first.js");
53+
const filePath2 = join(buildDir, "second.js");
54+
const content = "export const answer = 42;";
55+
const hash = "shared-hash-xyz";
56+
57+
// First run: writes to store and destination
58+
await createFileWithStore(filePath1, content, storeDir, hash);
59+
expect(existsSync(filePath1)).toBe(true);
60+
61+
const storeFile = join(storeDir, sanitizeHashForFilename(hash));
62+
const storeStatBefore = await stat(storeFile);
63+
64+
// Second run: re-running with existing store uses cached storePath (hardlink or copy)
65+
await createFileWithStore(filePath2, content, storeDir, hash);
66+
expect(existsSync(filePath2)).toBe(true);
67+
expect(await readFile(filePath2, "utf8")).toBe(content);
68+
69+
const storeStatAfter = await stat(storeFile);
70+
// Store file modified time should not have been updated because it was not rewritten
71+
expect(storeStatAfter.mtimeMs).toBe(storeStatBefore.mtimeMs);
72+
});
73+
74+
it("replaces existing file at destination path if already present", async () => {
75+
const storeDir = join(testDir, "store");
76+
const buildDir = join(testDir, "build");
77+
const filePath = join(buildDir, "output.js");
78+
const oldContent = "old content";
79+
const newContent = "new content";
80+
81+
// Write initial file at destination
82+
await createFile(filePath, oldContent);
83+
expect(await readFile(filePath, "utf8")).toBe(oldContent);
84+
85+
// Now create with store replacing it
86+
await createFileWithStore(filePath, newContent, storeDir, "new-hash");
87+
88+
expect(await readFile(filePath, "utf8")).toBe(newContent);
89+
});
90+
91+
it("handles deeply nested non-existent store and build paths", async () => {
92+
const storeDir = join(testDir, "deeply", "nested", "custom", "store");
93+
const buildDir = join(testDir, "another", "nested", "out");
94+
const filePath = join(buildDir, "chunk.js");
95+
const content = "export default 1;";
96+
const hash = "deep/hash+test";
97+
98+
expect(existsSync(storeDir)).toBe(false);
99+
expect(existsSync(buildDir)).toBe(false);
100+
101+
await createFileWithStore(filePath, content, storeDir, hash);
102+
103+
expect(existsSync(filePath)).toBe(true);
104+
expect(existsSync(storeDir)).toBe(true);
105+
expect(await readFile(filePath, "utf8")).toBe(content);
106+
});
107+
});
108+
});

‎packages/cli-v3/src/utilities/fileSystem.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ export async function createFileWithStore(
4747
// Store files by their content hash for true content-addressable storage
4848
const storePath = pathModule.join(storeDir, safeHash);
4949

50-
// Ensure build directory exists
50+
// Ensure build directory and store directory exist
51+
await fsModule.mkdir(storeDir, { recursive: true });
5152
await fsModule.mkdir(pathModule.dirname(filePath), { recursive: true });
5253

5354
// Remove existing file at destination if it exists (hardlinks fail on existing files)

0 commit comments

Comments
 (0)