Skip to content

refactor: replace temp package with node:fs equivalents#437

Merged
shd101wyy merged 2 commits into
shd101wyy:developfrom
dougborg:refactor/replace-temp-with-node-fs
May 24, 2026
Merged

refactor: replace temp package with node:fs equivalents#437
shd101wyy merged 2 commits into
shd101wyy:developfrom
dougborg:refactor/replace-temp-with-node-fs

Conversation

@dougborg
Copy link
Copy Markdown
Contributor

Summary

Replaces the temp dependency with a small (~80-line) wrapper around node:fs / node:os / node:crypto. temp was last published in 2020 and pins rimraf@~2.6.2glob@7inflight, 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:

Where Call Purpose
`src/utility.ts:18` `temp.track()` Opt-in cleanup of tracked tempfiles/dirs on process exit
`src/utility.ts:22` (via `tempOpen`) `temp.open(opts, cb)` Create a tempfile, return `{path, fd}`
`src/tools/pdf.ts:32`, `src/markdown-engine/transformer.ts:128` `temp.mkdirSync(prefix)` Create a tempdir, return its path

The new `src/lib/temp.ts` re-implements those three (plus the `AffixOptions` / `OpenFile` types) on top of:

  • `fs.openSync` with the `wx+` flag for the file path generator
  • `fs.mkdtempSync` for directories
  • `crypto.randomBytes` for the unique-name nonce
  • `process.on('exit', …)` + `fs.rmSync({recursive: true, force: true})` for the `track()` cleanup hook

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

  • `pnpm install` no longer emits `inflight`, `rimraf`, `glob@7`, or `mkdirp@0.5` warnings.
  • `pnpm why rimraf` and `pnpm why inflight` confirm those packages are absent from production deps. `glob@7` only remains under `babel-jest` / `test-exclude` (devDependencies; doesn't affect consumers).
  • `pnpm check` (eslint + prettier + tsc) passes.
  • `pnpm build` produces `out/esm/index.mjs` and `out/cjs/index.cjs` with zero `temp` references (`grep -c "'temp'"` returns 0 in both).
  • `pnpm test`: 398 passed, 6 failed (404 total). The same 6 failures pre-exist on `develop` in `colon-fence.test.ts`, `tag.test.ts`, and `wikilink-embed.test.ts` — they are not touched by this change. Confirmed via `git stash && git checkout develop && jest `.

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.

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.
Copilot AI review requested due to automatic review settings May 20, 2026 19:27
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 temp imports with src/lib/temp in the few direct call sites.
  • Added src/lib/temp.ts implementing open, mkdirSync, and track on top of Node built-ins.
  • Removed temp / @types/temp from 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 thread src/lib/temp.ts
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 thread test/lib/temp.test.ts
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).
@dougborg
Copy link
Copy Markdown
Contributor Author

Thanks for the review @copilot-pull-request-reviewer — fixed in b69e674. Both open() and mkdirSync() test helpers now collect every created path into an array and rmSync them in afterEach (the latter wasn't flagged but has the same shape, so cleaned up too). Tests still pass 6/6.

@shd101wyy shd101wyy self-requested a review May 24, 2026 08:41
Copy link
Copy Markdown
Owner

@shd101wyy shd101wyy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thank you!

@shd101wyy shd101wyy merged commit d14254b into shd101wyy:develop May 24, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants