Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,15 @@ jobs:
strategy:
matrix:
node-version:
- 16.x
- 18.x
- 20.x
- 22.x
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: Get Node Version
run: echo "::set-output name=version::$(node -v)"
run: echo "version=$(node -v)" >> "$GITHUB_OUTPUT"
id: node-version
- name: Cache node_modules
uses: actions/cache@v3
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 20.x
node-version: 22.x
cache: npm
- name: Get Node Version
run: echo "::set-output name=version::$(node -v)"
run: echo "version=$(node -v)" >> "$GITHUB_OUTPUT"
id: node-version
- name: Cache node_modules
uses: actions/cache@v3
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ await usingTemporaryFiles(async ({ path, add, addDirectory, read, remove }) => {
});
```

### Explicit resource management (`using`)

```js copy
import { temporaryFiles } from "using-temporary-files";

using api = temporaryFiles();
await api.add("file.txt", "Hello, world!");
```

`temporaryFiles()` returns an object with the same file operations as `usingTemporaryFiles()`, plus `Symbol.dispose` and `Symbol.asyncDispose` so it can be used with `using` / `await using`.

### Multiple callbacks

`usingTemporaryFiles()` accepts any number of callbacks. They share the same temporary directory and are called in order.
Expand Down
9 changes: 8 additions & 1 deletion dist/using-temporary-files.d.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
interface Operations {
add: (path: string, contents: string) => Promise<void>;
addDirectory: (path: string) => Promise<void>;
path: (relativePaths: string) => string;
path: (...relativePaths: Readonly<string[]>) => string;
read: (path: string) => Promise<string>;
remove: (path: string) => Promise<void>;
}
interface DisposableOperations extends Readonly<Operations> {
[Symbol.asyncDispose]: () => Promise<void>;
[Symbol.dispose]: () => void;
asyncDispose: () => Promise<void>;
dispose: () => void;
}
type Callback = (operations: Readonly<Operations>) => Promise<void>;
export declare function temporaryFiles(): DisposableOperations;
export declare function usingTemporaryFiles(...callbacks: Readonly<Callback[]>): Promise<void>;
export {};
130 changes: 102 additions & 28 deletions dist/using-temporary-files.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable total-functions/no-unsafe-readonly-mutable-assignment */
/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable no-await-in-loop */
import { randomUUID } from "node:crypto";
import { constants as fsConstants } from "node:fs";
import fs from "node:fs/promises";
import os from "node:os";
Expand Down Expand Up @@ -48,42 +49,115 @@ function createReadFunction(basePath) {
return await fs.readFile(fullPath, encoding);
};
}
// eslint-disable-next-line max-statements
export async function usingTemporaryFiles(...callbacks) {
function createTemporaryDirectory() {
const baseDirectory = DEBUG
? nodePath.resolve(process.cwd(), "./")
: os.tmpdir();
const temporaryDirectory = String(await fs.mkdtemp(nodePath.join(baseDirectory, "utf-")));
return nodePath.join(baseDirectory, `utf-${randomUUID()}`);
}
function createOperations(temporaryDirectory, ready) {
return {
async add(filePath, contents) {
await ready;
await createAddFunction(temporaryDirectory)(filePath, contents);
},
async addDirectory(filePath) {
await ready;
await createAddDirectoryFunction(temporaryDirectory)(filePath);
},
path(...relativePaths) {
return nodePath.join(temporaryDirectory, ...relativePaths);
},
async read(filePath) {
await ready;
return await createReadFunction(temporaryDirectory)(filePath);
},
async remove(filePath) {
await ready;
await createRemoveFunction(temporaryDirectory)(filePath);
},
};
}
async function removeTemporaryDirectory(temporaryDirectory) {
let retries = RETRIES;
while (retries > 0) {
try {
await fs.rm(temporaryDirectory, {
recursive: true,
});
break;
}
catch {
// eslint-disable-next-line promise/avoid-new, compat/compat
await new Promise((resolve) => {
setTimeout(resolve, RETRY_TIMEOUT_MILLISECONDS);
});
retries -= 1;
}
}
}
async function removeTemporaryDirectoryWhenReady(ready, temporaryDirectory) {
await ready;
await removeTemporaryDirectory(temporaryDirectory);
}
// eslint-disable-next-line max-statements
function createTemporaryFilesResource() {
const temporaryDirectory = createTemporaryDirectory();
const ready = fs.mkdir(temporaryDirectory, {
recursive: true,
});
let disposed = false;
let cleanupPromise = ready;
let cleanupQueued = false;
const operations = createOperations(temporaryDirectory, ready);
function queueCleanup() {
if (cleanupQueued) {
return;
}
cleanupQueued = true;
cleanupPromise = removeTemporaryDirectoryWhenReady(ready, temporaryDirectory);
}
async function asyncDispose() {
if (disposed) {
await cleanupPromise;
return;
}
disposed = true;
queueCleanup();
await cleanupPromise;
}
function dispose() {
if (disposed) {
return;
}
disposed = true;
queueCleanup();
}
const resource = {
...operations,
asyncDispose,
dispose,
[Symbol.asyncDispose]: asyncDispose,
[Symbol.dispose]: dispose,
};
return {
ready,
resource,
};
}
export function temporaryFiles() {
return createTemporaryFilesResource().resource;
}
export async function usingTemporaryFiles(...callbacks) {
const { ready, resource: operations } = createTemporaryFilesResource();
await ready;
try {
for (const callback of callbacks) {
// eslint-disable-next-line n/callback-return
await callback({
add: createAddFunction(temporaryDirectory),
addDirectory: createAddDirectoryFunction(temporaryDirectory),
path(...relativePaths) {
return nodePath.join(temporaryDirectory, ...relativePaths);
},
read: createReadFunction(temporaryDirectory),
remove: createRemoveFunction(temporaryDirectory),
});
await callback(operations);
}
}
finally {
let retries = RETRIES;
while (retries > 0) {
try {
await fs.rm(temporaryDirectory, {
recursive: true,
});
break;
}
catch {
// eslint-disable-next-line promise/avoid-new, compat/compat
await new Promise((resolve) => {
setTimeout(resolve, RETRY_TIMEOUT_MILLISECONDS);
});
retries -= 1;
}
}
await operations.asyncDispose();
}
}
Loading