refactor: replace temp package with node:fs equivalents#437
Merged
shd101wyy merged 2 commits intoMay 24, 2026
Conversation
The `temp` package was last released in 2020 and depends on
`rimraf@~2.6.2` → `glob@7` → `inflight`, all of which npm flags as
deprecated on every install. `inflight` in particular is flagged as
leaking memory.
Crossnote only uses three temp APIs (`open`, `mkdirSync`, `track`),
all of which can be expressed in ~80 lines on top of `node:fs`,
`node:os`, and `node:crypto`. Add `src/lib/temp.ts` as a drop-in
replacement that preserves the same return shapes (`{path, fd}`),
the same on-disk name pattern (prefix + random hex + suffix in the
system temp dir), and the same opt-in cleanup semantics
(`track()` registers a `process.on('exit', ...)` handler that
removes tracked files and directories).
Then swap the three `import * as temp from 'temp'` sites
(`src/utility.ts`, `src/tools/pdf.ts`,
`src/markdown-engine/transformer.ts`) to import from
`./lib/temp` and drop both `temp` and `@types/temp` from
package.json.
After this change, `temp`, `rimraf@2`, `mkdirp@0.5`, and `inflight`
are all gone from crossnote's production dependency tree. `npm
install crossnote` emits no more `inflight`/`rimraf`/`glob@7`
deprecation warnings.
A new `test/lib/temp.test.ts` exercises directory creation, file
creation with prefix/suffix, uniqueness, and the `track()` toggle.
The full Jest suite (`pnpm test`) shows 398 passed / 6 failed, with
the 6 failures pre-existing on `develop` (`colon-fence`, `tag`,
`wikilink-embed`) and untouched by this change.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR removes the unmaintained temp dependency (and its deprecated transitive chain) by introducing an internal src/lib/temp.ts wrapper that provides the small subset of the temp API Crossnote uses, and updates call sites accordingly.
Changes:
- Replaced
tempimports withsrc/lib/tempin the few direct call sites. - Added
src/lib/temp.tsimplementingopen,mkdirSync, andtrackon top of Node built-ins. - Removed
temp/@types/tempfrom dependencies and added Jest coverage for the new wrapper.
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/lib/temp.ts |
New Node-builtins-based replacement for the subset of temp used by Crossnote. |
src/utility.ts |
Switches tempOpen to delegate to the new wrapper and keeps track() enablement. |
src/tools/pdf.ts |
Updates temp import to the new wrapper for temp directory creation. |
src/markdown-engine/transformer.ts |
Updates temp import to the new wrapper for downloads temp dir creation. |
test/lib/temp.test.ts |
Adds tests for mkdirSync, open, and track behavior. |
package.json |
Removes temp and @types/temp dependencies. |
pnpm-lock.yaml |
Locks updated to reflect removed dependencies and transitive cleanup. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (2)
test/lib/temp.test.ts:46
- This test creates a real temp file but never deletes it after reading it back. Please remove the temp file as part of the test cleanup to avoid leaving artifacts in the system temp directory across test runs.
it('creates a writable temp file with prefix + suffix in the path', async () => {
const info = await temp.open({
prefix: 'crossnote-test',
suffix: '.svg',
});
try {
expect(existsSync(info.path)).toBe(true);
expect(info.path.endsWith('.svg')).toBe(true);
expect(path.basename(info.path)).toMatch(/^crossnote-test-f-/);
const payload = Buffer.from('<svg></svg>');
writeSync(info.fd, payload, 0, payload.length, 0);
} finally {
closeSync(info.fd);
}
expect(readFileSync(info.path, 'utf8')).toBe('<svg></svg>');
});
test/lib/temp.test.ts:59
- This test creates two temp files and closes their descriptors, but it does not remove the files afterwards. Please delete the created files during test cleanup to prevent accumulating temp artifacts in CI and local runs.
it('returns distinct paths for repeated calls', async () => {
const [a, b] = await Promise.all([
temp.open({ prefix: 'crossnote' }),
temp.open({ prefix: 'crossnote' }),
]);
try {
expect(a.path).not.toBe(b.path);
} finally {
closeSync(a.fd);
closeSync(b.fd);
}
});
Comment on lines
+29
to
+46
| const trackedFiles = new Map<string, number>(); | ||
| const trackedDirs = new Set<string>(); | ||
| let tracking = false; | ||
| let exitHandlerRegistered = false; | ||
|
|
||
| function registerExitHandler() { | ||
| if (exitHandlerRegistered) return; | ||
| exitHandlerRegistered = true; | ||
| process.on('exit', () => { | ||
| if (!tracking) return; | ||
| for (const [filePath, fd] of trackedFiles) { | ||
| try { | ||
| closeSync(fd); | ||
| } catch { | ||
| // fd may already be closed | ||
| } | ||
| try { | ||
| rmSync(filePath, { force: true }); |
Comment on lines
+8
to
+25
| it('creates a temp directory under os.tmpdir() with the given prefix', () => { | ||
| const dirPath = temp.mkdirSync('crossnote-test'); | ||
| expect(existsSync(dirPath)).toBe(true); | ||
| expect(dirPath.startsWith(tmpdir())).toBe(true); | ||
| expect(path.basename(dirPath)).toMatch(/^crossnote-test-d-/); | ||
| }); | ||
|
|
||
| it('produces a unique path on each call', () => { | ||
| const a = temp.mkdirSync('crossnote-uniq'); | ||
| const b = temp.mkdirSync('crossnote-uniq'); | ||
| expect(a).not.toBe(b); | ||
| }); | ||
|
|
||
| it('honours a custom dir option', () => { | ||
| const parent = temp.mkdirSync('crossnote-parent'); | ||
| const child = temp.mkdirSync({ prefix: 'crossnote-child', dir: parent }); | ||
| expect(child.startsWith(parent + path.sep)).toBe(true); | ||
| }); |
…facts Address Copilot review on shd101wyy#437: tests in test/lib/temp.test.ts created real files via temp.open() and real dirs via temp.mkdirSync() but never removed them, accumulating artifacts in os.tmpdir() across CI runs and local invocations. Collect every path returned by the temp helpers under test into a shared 'created' array, then rmSync(..., { recursive: true, force: true }) each entry in afterEach. Cleanup runs whether the test passes or fails, so a failing assertion no longer leaks the partial artifact. Same fix applied to the mkdirSync tests (which Copilot didn't flag but have the same shape — they also created directories that survived). Tests still pass (6/6, ~1.8s).
Contributor
Author
|
Thanks for the review @copilot-pull-request-reviewer — fixed in b69e674. Both |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the
tempdependency with a small (~80-line) wrapper aroundnode:fs/node:os/node:crypto.tempwas last published in 2020 and pinsrimraf@~2.6.2→glob@7→inflight, so every `npm install crossnote` currently emits four deprecation warnings:```
npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory…
npm warn deprecated rimraf@2.6.3: Rimraf versions prior to v4 are no longer supported
npm warn deprecated glob@7.2.3: Old versions of glob are not supported…
npm warn deprecated mkdirp@0.5.6: Legacy versions of mkdirp are no longer supported…
```
After this PR, all four are gone from the production tree.
What's used vs. what's replaced
Crossnote only consumes three pieces of `temp`'s API surface:
The new `src/lib/temp.ts` re-implements those three (plus the `AffixOptions` / `OpenFile` types) on top of:
Return shapes, on-disk filename pattern (`-f-` in `os.tmpdir()`), and opt-in cleanup semantics are preserved, so every call site in `utility.ts`, `tools/pdf.ts`, `tools/wavedrom.ts`, `tools/magick.ts`, `tools/mermaid.ts`, `markdown-engine/index.ts`, and `markdown-engine/transformer.ts` continues to work without further edits. The only signature simplification is in `utility.tempOpen`, which used to wrap `temp.open`'s callback API in a `new Promise` — `lib/temp.open` is already promise-returning, so it can simply delegate.
Verification
A new `test/lib/temp.test.ts` exercises directory creation under `os.tmpdir()`, file creation with prefix/suffix, path uniqueness across calls, and the `track()` toggle.
Context
Filed alongside #436 (the `bit-field` ESM extension fix). Both came up while running crossnote inside an MCP server where every install emitted the deprecation chain to the user.