@williamthorsen/toolbelt.filesystem 0.8.2 → 0.9.0
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.
- package/.readyup/kits/default.js +514 -0
- package/.readyup/manifest.json +100 -0
- package/CHANGELOG.md +97 -13
- package/README.md +100 -151
- package/dist/esm/1-proposed/index.d.ts +0 -1
- package/dist/esm/1-proposed/index.js +0 -1
- package/dist/esm/1-proposed/replaceFileExtension.d.ts +20 -0
- package/dist/esm/3-candidate/index.d.ts +1 -1
- package/dist/esm/3-candidate/index.js +1 -1
- package/dist/esm/3-candidate/writeAtomic.d.ts +24 -0
- package/dist/esm/4-release/directory-chain-matches.d.ts +39 -0
- package/dist/esm/4-release/listDirectoryChain.d.ts +15 -0
- package/dist/esm/4-release/loadConfigCascade.d.ts +36 -0
- package/dist/esm/4-release/reconcileFile.d.ts +22 -0
- package/dist/esm/4-release/reconcileFileFromFile.d.ts +21 -0
- package/package.json +10 -2
- package/dist/esm/1-proposed/writeAtomic.d.ts +0 -1
- package/dist/esm/3-candidate/createTempTree.d.ts +0 -19
- package/dist/esm/3-candidate/createTempTree.js +0 -148
- /package/dist/esm/{1-proposed → 3-candidate}/writeAtomic.js +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,90 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.9.0 — 2026-09-15
|
|
6
|
+
|
|
7
|
+
### 🎉 Features
|
|
8
|
+
|
|
9
|
+
- 🚨 **Breaking:** Move createTempTree from toolbelt.filesystem to toolbelt.testing (#313)
|
|
10
|
+
|
|
11
|
+
- Moves `CreateTempTreeOptions` and the `TempTree` handle to `@williamthorsen/toolbelt.testing/candidate` along with `createTempTree`.
|
|
12
|
+
|
|
13
|
+
Migration: Import `createTempTree`, `CreateTempTreeOptions`, and `TempTree` from `@williamthorsen/toolbelt.testing/candidate`, and declare `@williamthorsen/toolbelt.testing` in the manifest that declared `@williamthorsen/toolbelt.filesystem` for them.
|
|
14
|
+
|
|
15
|
+
- Add a ReadyUp adoption kit to toolbelt.filesystem (#316)
|
|
16
|
+
|
|
17
|
+
- Adds a ReadyUp adoption kit to `@williamthorsen/toolbelt.filesystem` that recommends `writeAtomic` where a project writes a file to a temporary path and renames it into place, and `listDirectoryChain`, `findDirectoryChainMatch`, or `listDirectoryChainMatches` where a loop ascends to the filesystem root by `path.dirname`.
|
|
18
|
+
- Promotes `writeAtomic` to the candidate tier.
|
|
19
|
+
|
|
20
|
+
Migration: Change any import of `writeAtomic` from `@williamthorsen/toolbelt.filesystem/proposed` to `@williamthorsen/toolbelt.filesystem/candidate`.
|
|
21
|
+
|
|
22
|
+
- Read a directory walk's probed name through a binding or constant (#324)
|
|
23
|
+
|
|
24
|
+
- Extends the ReadyUp adoption kit in `@williamthorsen/toolbelt.packaging` to recommend `findPackageRoot` or `resolveSelfVersion` for a hand-rolled `package.json` search that checks for the file through a variable declared once inside its loop and never reassigned there, or through a string constant declared once in the same file.
|
|
25
|
+
- Stops the kit in `@williamthorsen/toolbelt.filesystem` from reporting such a search, which it previously treated as a generic directory walk.
|
|
26
|
+
|
|
27
|
+
- Report hand-rolled dedents in the strings adoption kit (#330)
|
|
28
|
+
|
|
29
|
+
- Adds `no-joined-line-array`, which reports an array of string or template literals that spans several lines and is joined with a newline.
|
|
30
|
+
- Adds `no-layout-breaking-template`, which reports an untagged template literal whose later lines drop below the indentation of the line on which it opens.
|
|
31
|
+
|
|
32
|
+
### 🐛 Bug fixes
|
|
33
|
+
|
|
34
|
+
- Stop reading each segment of a probe's path as a separately probed name (#320)
|
|
35
|
+
|
|
36
|
+
- Fixes an issue in which `toolbelt.filesystem`'s ReadyUp adoption kit treated a loop over parent directories as a search for each directory's own `package.json` when the path checked at each level contained a `'package.json'` literal, as in `path.join(dir, 'node_modules', name, 'package.json')`, and so did not report the loop under `no-hand-rolled-directory-walk`.
|
|
37
|
+
|
|
38
|
+
### ♻️ Refactoring
|
|
39
|
+
|
|
40
|
+
- Move directory-walk recognition from filesystem into packages/adoption (#318)
|
|
41
|
+
|
|
42
|
+
- Adds `listDirectoryAscents` to `packages/adoption`, which reports each directory ascent once with the names probed by its innermost loop, and reduces `filesystem`'s `listChainWalkSites` to a partition of that output, so a `toolbelt.packaging` kit can partition the same ascents without its own copy of the recognition.
|
|
43
|
+
- Renames the hand-off rule `isProjectRootSearch` to `isManifestSearch` and rewrites the text in `packages/adoption` and `filesystem` that credited `findProjectRoot` alone with a `package.json` walk.
|
|
44
|
+
- Stops `filesystem`'s kit from reporting a loop that ascends one binding around an inner loop ascending another binding and probing for `package.json`, which is the only finding changed by the move.
|
|
45
|
+
|
|
46
|
+
### 🧪 Tests
|
|
47
|
+
|
|
48
|
+
- Move the rdy run report reader into the adoption test utilities (#323)
|
|
49
|
+
|
|
50
|
+
- Replaces the `rdy run --json` report reader copied into each of the twelve `pragma-suppression.tool.test.ts` suites with `listKitCheckReports`, a helper added to `@williamthorsen/toolbelt.adoption/test-utils` that runs a package's compiled kit over a fixture repo and returns its check reports, so a change to the shape of readyup's report needs one edit rather than twelve.
|
|
51
|
+
- Fixes the error thrown for a kit that does not load: Each copy discarded the load error recorded by `rdy` on the kit's entry and threw "the run reported no adoption checks", and the helper throws with `rdy`'s own message instead.
|
|
52
|
+
|
|
53
|
+
### ⚙️ Tooling
|
|
54
|
+
|
|
55
|
+
- Remove the stale repo-local cliff.toml and normalize changelog titles (#327)
|
|
56
|
+
|
|
57
|
+
- Stops `release-kit prepare` from printing a "skipped due to grouping error(s)" warning for each releasable workspace by letting it resolve the git-cliff template bundled with release-kit, previously overridden by the root `cliff.toml`.
|
|
58
|
+
- Excludes commits without a ticket prefix from future changelog entries.
|
|
59
|
+
- Renames the section titles in every `packages/*/.meta/changelog.json`, except `Dependency updates`, to the headings of release-kit's work-type taxonomy, such as "🎉 Features" and "🏗️ Internal features", and regenerates each `CHANGELOG.md` so that release-kit orders existing and new sections by the same rule.
|
|
60
|
+
- Causes the next `release-kit prepare` to plan patch releases of `dstructs`, `hof`, and `sets`, which had no other commits since their last release, because the changelog commit touches every workspace.
|
|
61
|
+
|
|
62
|
+
### 📚 Documentation
|
|
63
|
+
|
|
64
|
+
- Align prose with plain-speech doctrine and writing conventions (#306)
|
|
65
|
+
|
|
66
|
+
- Copy-edits prose across the repo: comments, test names, package READMEs, and `AGENTS.md`.
|
|
67
|
+
- Rewrites a few user-facing strings as well, among them `configure-project`'s help text and the errors from `parseProjectSpec`, `securityCommands`, and `hashString`.
|
|
68
|
+
|
|
69
|
+
## 0.8.3 — 2026-09-06
|
|
70
|
+
|
|
71
|
+
### 📚 Documentation
|
|
72
|
+
|
|
73
|
+
- Repair reduced object relatives in passages recurring across files (#262)
|
|
74
|
+
|
|
75
|
+
Repairs the reduced object relative in the prose passages that recur across more than one file, in package READMEs, source comments, test titles, and the ReadyUp kits' check messages.
|
|
76
|
+
|
|
77
|
+
- Repair reduced object relatives in the READMEs and AGENTS.md (#263)
|
|
78
|
+
|
|
79
|
+
Repairs the reduced object relative in `AGENTS.md`, the root `README.md`, and the package READMEs.
|
|
80
|
+
|
|
81
|
+
- Repair reduced object relatives in packages/adoption (#264)
|
|
82
|
+
|
|
83
|
+
Repairs the reduced object relative in `packages/adoption`, in source comments, doc descriptions, and test titles.
|
|
84
|
+
|
|
85
|
+
- Repair the repository's prose and record every rejection's ground (#290)
|
|
86
|
+
|
|
87
|
+
Applies one repo-wide `revise-prose` sweep across the repository's READMEs, `AGENTS.md`, source comments, doc descriptions, and test names.
|
|
88
|
+
|
|
5
89
|
## 0.8.2 — 2026-08-30
|
|
6
90
|
|
|
7
91
|
### Dependency updates
|
|
@@ -10,7 +94,7 @@ All notable changes to this project will be documented in this file.
|
|
|
10
94
|
|
|
11
95
|
## 0.8.1 — 2026-08-28
|
|
12
96
|
|
|
13
|
-
### Refactoring
|
|
97
|
+
### ♻️ Refactoring
|
|
14
98
|
|
|
15
99
|
- Upgrade eslint-config-typescript to v12.0.1 and satisfy its new rules (#236)
|
|
16
100
|
|
|
@@ -18,7 +102,7 @@ All notable changes to this project will be documented in this file.
|
|
|
18
102
|
|
|
19
103
|
## 0.8.0 — 2026-08-24
|
|
20
104
|
|
|
21
|
-
### Features
|
|
105
|
+
### 🎉 Features
|
|
22
106
|
|
|
23
107
|
- Add a recursive listFiles to createTempTree's entry API (#212)
|
|
24
108
|
|
|
@@ -26,7 +110,7 @@ All notable changes to this project will be documented in this file.
|
|
|
26
110
|
|
|
27
111
|
## 0.7.0 — 2026-08-21
|
|
28
112
|
|
|
29
|
-
### Features
|
|
113
|
+
### 🎉 Features
|
|
30
114
|
|
|
31
115
|
- 🚨 **Breaking:** Fix createTempTree's symlink guard and disposal, and complete its entry API (#207)
|
|
32
116
|
|
|
@@ -40,13 +124,13 @@ All notable changes to this project will be documented in this file.
|
|
|
40
124
|
|
|
41
125
|
## 0.6.0 — 2026-08-16
|
|
42
126
|
|
|
43
|
-
### Features
|
|
127
|
+
### 🎉 Features
|
|
44
128
|
|
|
45
129
|
- Add mkdir, symlink, write, and writeJson methods to `TempTree` (#176)
|
|
46
130
|
|
|
47
131
|
Adds four write methods to `TempTree` in `@williamthorsen/toolbelt.filesystem`: `mkdir`, `symlink`, `write`, and `writeJson`. Each takes a tree-relative path, creates the parent directories it needs, resolves through the same containment check `resolve` applies, and returns the absolute path, so a suite writing into a built temporary tree reaches it through the handle rather than through `node:fs`. `symlink` accepts a link path and a target, and picks the link type from the target.
|
|
48
132
|
|
|
49
|
-
### Tests
|
|
133
|
+
### 🧪 Tests
|
|
50
134
|
|
|
51
135
|
- Drop expect-type in favor of expectTypeOf (#175)
|
|
52
136
|
|
|
@@ -54,7 +138,7 @@ All notable changes to this project will be documented in this file.
|
|
|
54
138
|
|
|
55
139
|
## 0.5.0 — 2026-08-15
|
|
56
140
|
|
|
57
|
-
### Features
|
|
141
|
+
### 🎉 Features
|
|
58
142
|
|
|
59
143
|
- 🚨 **Breaking:** Promote createTempTree to the candidate tier with a caller-chosen prefix and binary entries (#148)
|
|
60
144
|
|
|
@@ -68,7 +152,7 @@ All notable changes to this project will be documented in this file.
|
|
|
68
152
|
|
|
69
153
|
## 0.4.1 — 2026-08-13
|
|
70
154
|
|
|
71
|
-
### Tooling
|
|
155
|
+
### ⚙️ Tooling
|
|
72
156
|
|
|
73
157
|
- Remove redundant .gitignore files
|
|
74
158
|
- Populate manifest metadata and adopt a pnpm catalog (#140)
|
|
@@ -77,7 +161,7 @@ All notable changes to this project will be documented in this file.
|
|
|
77
161
|
|
|
78
162
|
## 0.4.0 — 2026-08-12
|
|
79
163
|
|
|
80
|
-
### Features
|
|
164
|
+
### 🎉 Features
|
|
81
165
|
|
|
82
166
|
- 🚨 **Breaking:** Add reconcileFile to toolbelt.filesystem and promote describeError to release tier (#122)
|
|
83
167
|
|
|
@@ -95,7 +179,7 @@ All notable changes to this project will be documented in this file.
|
|
|
95
179
|
|
|
96
180
|
## 0.3.0 — 2026-08-08
|
|
97
181
|
|
|
98
|
-
### Features
|
|
182
|
+
### 🎉 Features
|
|
99
183
|
|
|
100
184
|
- Migrate replaceFileExtension into filesystem package (#74)
|
|
101
185
|
|
|
@@ -121,7 +205,7 @@ All notable changes to this project will be documented in this file.
|
|
|
121
205
|
|
|
122
206
|
`findProjectRoot` moves from `@williamthorsen/toolbelt.filesystem` to `@williamthorsen/toolbelt.packaging`. Callers of `loadConfigCascade` must now state where its upward search stops, rather than relying on a project root the function found for them.
|
|
123
207
|
|
|
124
|
-
### Tooling
|
|
208
|
+
### ⚙️ Tooling
|
|
125
209
|
|
|
126
210
|
- Migrate Vitest configs to the nmr projects model (#73)
|
|
127
211
|
|
|
@@ -133,17 +217,17 @@ All notable changes to this project will be documented in this file.
|
|
|
133
217
|
|
|
134
218
|
## 0.2.1 — 2026-07-27
|
|
135
219
|
|
|
136
|
-
### Tooling
|
|
220
|
+
### ⚙️ Tooling
|
|
137
221
|
|
|
138
222
|
- Normalize Vitest, and lint configs
|
|
139
223
|
|
|
140
|
-
### Documentation
|
|
224
|
+
### 📚 Documentation
|
|
141
225
|
|
|
142
226
|
- Change license to ISC
|
|
143
227
|
|
|
144
228
|
## 0.2.0 — 2026-07-24
|
|
145
229
|
|
|
146
|
-
### Features
|
|
230
|
+
### 🎉 Features
|
|
147
231
|
|
|
148
232
|
- Add the filesystem package with bounded cascading config discovery (#71)
|
|
149
233
|
|
package/README.md
CHANGED
|
@@ -1,8 +1,43 @@
|
|
|
1
|
+
<!-- readme-type: library -->
|
|
2
|
+
|
|
1
3
|
# @williamthorsen/toolbelt.filesystem
|
|
2
4
|
|
|
3
5
|
Filesystem utilities for TypeScript and JavaScript.
|
|
4
6
|
|
|
5
|
-
<!-- section:release-notes
|
|
7
|
+
<!-- section:release-notes -->
|
|
8
|
+
## Release notes — v0.9.0 (2026-09-15)
|
|
9
|
+
|
|
10
|
+
### 🎉 Features
|
|
11
|
+
|
|
12
|
+
- 🚨 **Breaking:** Move createTempTree from toolbelt.filesystem to toolbelt.testing (#313)
|
|
13
|
+
|
|
14
|
+
- Moves `CreateTempTreeOptions` and the `TempTree` handle to `@williamthorsen/toolbelt.testing/candidate` along with `createTempTree`.
|
|
15
|
+
|
|
16
|
+
Migration: Import `createTempTree`, `CreateTempTreeOptions`, and `TempTree` from `@williamthorsen/toolbelt.testing/candidate`, and declare `@williamthorsen/toolbelt.testing` in the manifest that declared `@williamthorsen/toolbelt.filesystem` for them.
|
|
17
|
+
|
|
18
|
+
- Add a ReadyUp adoption kit to toolbelt.filesystem (#316)
|
|
19
|
+
|
|
20
|
+
- Adds a ReadyUp adoption kit to `@williamthorsen/toolbelt.filesystem` that recommends `writeAtomic` where a project writes a file to a temporary path and renames it into place, and `listDirectoryChain`, `findDirectoryChainMatch`, or `listDirectoryChainMatches` where a loop ascends to the filesystem root by `path.dirname`.
|
|
21
|
+
- Promotes `writeAtomic` to the candidate tier.
|
|
22
|
+
|
|
23
|
+
Migration: Change any import of `writeAtomic` from `@williamthorsen/toolbelt.filesystem/proposed` to `@williamthorsen/toolbelt.filesystem/candidate`.
|
|
24
|
+
|
|
25
|
+
- Read a directory walk's probed name through a binding or constant (#324)
|
|
26
|
+
|
|
27
|
+
- Extends the ReadyUp adoption kit in `@williamthorsen/toolbelt.packaging` to recommend `findPackageRoot` or `resolveSelfVersion` for a hand-rolled `package.json` search that checks for the file through a variable declared once inside its loop and never reassigned there, or through a string constant declared once in the same file.
|
|
28
|
+
- Stops the kit in `@williamthorsen/toolbelt.filesystem` from reporting such a search, which it previously treated as a generic directory walk.
|
|
29
|
+
|
|
30
|
+
- Report hand-rolled dedents in the strings adoption kit (#330)
|
|
31
|
+
|
|
32
|
+
- Adds `no-joined-line-array`, which reports an array of string or template literals that spans several lines and is joined with a newline.
|
|
33
|
+
- Adds `no-layout-breaking-template`, which reports an untagged template literal whose later lines drop below the indentation of the line on which it opens.
|
|
34
|
+
|
|
35
|
+
### 🐛 Bug fixes
|
|
36
|
+
|
|
37
|
+
- Stop reading each segment of a probe's path as a separately probed name (#320)
|
|
38
|
+
|
|
39
|
+
- Fixes an issue in which `toolbelt.filesystem`'s ReadyUp adoption kit treated a loop over parent directories as a search for each directory's own `package.json` when the path checked at each level contained a `'package.json'` literal, as in `path.join(dir, 'node_modules', name, 'package.json')`, and so did not report the loop under `no-hand-rolled-directory-walk`.
|
|
40
|
+
<!-- /section:release-notes -->
|
|
6
41
|
|
|
7
42
|
## Installation
|
|
8
43
|
|
|
@@ -12,9 +47,44 @@ pnpm add @williamthorsen/toolbelt.filesystem
|
|
|
12
47
|
|
|
13
48
|
## Runtime requirements
|
|
14
49
|
|
|
15
|
-
`
|
|
50
|
+
`findDirectoryChainMatch`, `listDirectoryChainMatches`, `loadConfigCascade`, `reconcileFile`, `reconcileFileFromFile`, and `writeAtomic` reach the filesystem through `node:` builtins, so they run under Node.js 24 or later, Bun, and Deno. They do not run in browsers, nor in edge runtimes that expose no filesystem. `listDirectoryChain` and `replaceFileExtension` touch no filesystem, so an edge runtime that exposes none runs them; they still import `node:path`, which a browser bundle has to supply.
|
|
51
|
+
|
|
52
|
+
`loadConfigCascade` imports each config through the host runtime, so a `.ts` config is subject to whatever that runtime does with TypeScript. Node strips types rather than compiling them, which admits erasable syntax alone: An `enum`, a `namespace`, or a parameter property in a config file fails to parse. A `.mjs` or `.js` config sidesteps the question.
|
|
53
|
+
|
|
54
|
+
## Adoption checks
|
|
55
|
+
|
|
56
|
+
The package ships a ReadyUp kit, so a project that installs it can ask how far its adoption got:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
rdy run --packages
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The kit reads the project's tracked sources and reports two idioms, each counted against the calls that the project already makes into this package. Both report at `recommend`: They are correct code that a published function expresses better, not defects.
|
|
63
|
+
|
|
64
|
+
`no-hand-rolled-atomic-write` reports a function body that writes a path held in a binding and renames that same binding, naming the function that holds it. A write alone, a rename alone, a `copyFile` followed by a rename, and a rename of a path that the body never wrote report nothing. The site reports at `recommend` however the temp file is staged, because a text scan cannot tell whether two paths share a volume, and one staged under the system temporary directory is the case worth checking by hand: `rename` is atomic only within one filesystem.
|
|
16
65
|
|
|
17
|
-
`
|
|
66
|
+
`no-hand-rolled-directory-walk` reports a loop that ascends by `dirname` until it reaches the filesystem root, naming the line the loop opens on. A loop that only ascends takes [`listDirectoryChain`](#listdirectorychain); one that probes each level for a name takes [`findDirectoryChainMatch`](#finddirectorychainmatch) or [`listDirectoryChainMatches`](#listdirectorychainmatches). The report names the site rather than the shape, so the fix text describes both and the reader chooses from the loop itself. A loop probing for `package.json` is left alone: That site belongs to [`@williamthorsen/toolbelt.packaging`](https://github.com/williamthorsen/toolbelt/tree/main/packages/packaging#readme), whose own kit reports it, and reporting it here would mean seeing one loop twice under conflicting advice.
|
|
67
|
+
|
|
68
|
+
The walk detector under-matches by design. A recursive walk-up function is no loop, an ascent written as `path.resolve(dir, '..')` carries a different anchor, and a loop that computes a parent per item without assigning it back is no ascent. None of the three reports.
|
|
69
|
+
|
|
70
|
+
Bootstrap wrappers under `bin/` are exempt: Such a wrapper imports only builtins so its build-first message survives an incomplete install, and importing this package there would replace that message with a module-resolution failure. Tests are exempt too, since they write these shapes deliberately. A source declared generated or vendored by the project in its own `.gitattributes`, under `linguist-generated` or `linguist-vendored`, is exempt as well: The sweep drops it before the kit sees it, so committed bundler output yields no advice that anyone could act on. The sweep is readyup's, so this holds on readyup 0.35.0 or later.
|
|
71
|
+
|
|
72
|
+
A reviewed site is silenced by an `rdy-ignore` pragma on its own line, or `rdy-ignore-next-line` on the line above. A pragma naming a check's id suppresses that check alone; with no id it covers every check on the line. A failed check prints its id ahead of its fraction, which is the form to write:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
// rdy-ignore-next-line toolbelt.filesystem/no-hand-rolled-directory-walk -- the ascent stops at a ceiling
|
|
76
|
+
while (dir !== stopAtDir) {
|
|
77
|
+
dir = path.dirname(dir);
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Add the package to `.config/readyup.config.ts` to include it in a routine sweep:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
export default defineRdyConfig({
|
|
85
|
+
packages: ['@williamthorsen/toolbelt.filesystem'],
|
|
86
|
+
});
|
|
87
|
+
```
|
|
18
88
|
|
|
19
89
|
## `listDirectoryChain`
|
|
20
90
|
|
|
@@ -36,7 +106,7 @@ listDirectoryChain('/home/dev/app/src', { stopAtDir: '/home/dev' });
|
|
|
36
106
|
|
|
37
107
|
`stopAtDir` bounds the ascent inclusively and is resolved the same way `startDir` is, so a relative ceiling behaves like a relative start. One that is neither the start directory nor an ancestor of it throws, naming both, rather than being ignored and letting the ascent run past the bound. The comparison is exact, so a `stopAtDir` differing from its target only in case is off the chain even on a volume that would open it.
|
|
38
108
|
|
|
39
|
-
The result type records that the chain is never empty, which
|
|
109
|
+
The result type records that the chain is never empty, which spares the nearest directory an undefined check:
|
|
40
110
|
|
|
41
111
|
```ts
|
|
42
112
|
const [nearestDir] = listDirectoryChain(process.cwd()); // string, not string | undefined
|
|
@@ -71,9 +141,9 @@ listDirectoryChainMatches('/home/dev/app/src', ['.git'], { stopAtDir: '/home/dev
|
|
|
71
141
|
// [{ dir: '/home/dev/app', entryName: '.git', entryPath: '/home/dev/app/.git' }]
|
|
72
142
|
```
|
|
73
143
|
|
|
74
|
-
A level yields at most one match, the earliest of `names` found there, and a level holding none contributes nothing, so an empty result is an ordinary outcome rather than an error. A name matches a directory as readily as a file, which
|
|
144
|
+
A level yields at most one match, the earliest of `names` found there, and a level holding none contributes nothing, so an empty result is an ordinary outcome rather than an error. A name matches a directory as readily as a file, which lets `.git` be probed without knowing whether the clone is ordinary or a worktree.
|
|
75
145
|
|
|
76
|
-
Each name is a path relative to the level it is probed
|
|
146
|
+
Each name is a path relative to the level against which it is probed, so a nested location such as `.config/stack.config.mjs` works. A name that would leave its level (an absolute path, or one whose `..` segments escape it) is rejected before any level is probed, so the rejection never depends on what happens to exist on disk.
|
|
77
147
|
|
|
78
148
|
`options` is forwarded to `listDirectoryChain`, so `stopAtDir` bounds the ascent the same way.
|
|
79
149
|
|
|
@@ -98,7 +168,7 @@ findDirectoryChainMatch('/home/dev/app/src', ['.git']);
|
|
|
98
168
|
// { dir: '/home/dev/app', entryName: '.git', entryPath: '/home/dev/app/.git' }
|
|
99
169
|
```
|
|
100
170
|
|
|
101
|
-
Probing stops at the first level that matches, so no level beyond it is touched -- the reason to reach for this rather than read element zero off `listDirectoryChainMatches`, which probes to the ceiling regardless. The nullable return type is the other reason:
|
|
171
|
+
Probing stops at the first level that matches, so no level beyond it is touched -- the reason to reach for this rather than read element zero off `listDirectoryChainMatches`, which probes to the ceiling regardless. The nullable return type is the other reason: A result that may be absent says so, where an array leaves the caller to narrow.
|
|
102
172
|
|
|
103
173
|
## `loadConfigCascade`
|
|
104
174
|
|
|
@@ -113,9 +183,9 @@ loadConfigCascade<TConfig>(options: {
|
|
|
113
183
|
|
|
114
184
|
Loads every config file between `startDir` and `stopAtDir`, nearest first, and reads nothing above that boundary.
|
|
115
185
|
|
|
116
|
-
Discovery is [`listDirectoryChainMatches`](#listdirectorychainmatches) bounded at `stopAtDir`:
|
|
186
|
+
Discovery is [`listDirectoryChainMatches`](#listdirectorychainmatches) bounded at `stopAtDir`: The first of `fileNames` that exists at a level becomes that level's config, a level holding none contributes nothing, and a name that would leave its level is rejected before any file is read. A `stopAtDir` that is neither the start directory nor one of its ancestors throws, on the same terms that `listDirectoryChain` sets out.
|
|
117
187
|
|
|
118
|
-
The boundary is required, and it is the caller's to choose. That
|
|
188
|
+
The boundary is required, and it is the caller's to choose. That keeps this function free of any notion of what marks a project: It never asks whether a directory holds a lockfile or a workspace manifest. Where the boundary should be a project root, [`findProjectRoot`](https://github.com/williamthorsen/toolbelt/tree/main/packages/packaging#findprojectroot) in `@williamthorsen/toolbelt.packaging` resolves one from markers.
|
|
119
189
|
|
|
120
190
|
The matched files are then imported one at a time, and `shouldStopAscent` is consulted after each. Once it returns true, the ascent halts and no farther file is imported at all, rather than being loaded and discarded:
|
|
121
191
|
|
|
@@ -130,7 +200,7 @@ interface ConfigCascade<TConfig> {
|
|
|
130
200
|
}
|
|
131
201
|
```
|
|
132
202
|
|
|
133
|
-
A config is the module's default export. A matched module declaring none is rejected by name; validating what a config contains stays with the caller, which
|
|
203
|
+
A config is the module's default export. A matched module declaring none is rejected by name; validating what a config contains stays with the caller, which lets one mechanism serve schemas sharing no fields.
|
|
134
204
|
|
|
135
205
|
### The `shouldStopAscent` convention
|
|
136
206
|
|
|
@@ -155,7 +225,7 @@ const { entries, stopReason } = await loadConfigCascade<StackConfig>({
|
|
|
155
225
|
});
|
|
156
226
|
```
|
|
157
227
|
|
|
158
|
-
`stopReason` is provenance for the caller to surface, so a user can see whether the predicate ended the cascade or it simply reached the boundary. Which directory bounded it is the `stopAtDir` the caller
|
|
228
|
+
`stopReason` is provenance for the caller to surface, so a user can see whether the predicate ended the cascade or it simply reached the boundary. Which directory bounded it is the `stopAtDir` passed in by the caller.
|
|
159
229
|
|
|
160
230
|
## `reconcileFile`
|
|
161
231
|
|
|
@@ -176,20 +246,20 @@ reconcileFile('.config/tool.config.ts', template);
|
|
|
176
246
|
// { filePath: '.config/tool.config.ts', outcome: 'created' }
|
|
177
247
|
```
|
|
178
248
|
|
|
179
|
-
Missing parent directories are created. `isDryRun` writes nothing and creates no directory, returning the outcome the real call would have produced, which
|
|
249
|
+
Missing parent directories are created. `isDryRun` writes nothing and creates no directory, returning the outcome that the real call would have produced, which lets a `--dry-run` flag print what the run itself would. A write that would fail is the exception: Nothing detects that without attempting it, so a dry run reports the write's intended outcome.
|
|
180
250
|
|
|
181
|
-
`conflictPolicy` decides what becomes of an existing file whose content differs, and decides nothing else:
|
|
251
|
+
`conflictPolicy` decides what becomes of an existing file whose content differs, and decides nothing else: It is consulted in that case alone. The default, `'skip'`, never replaces a file that the user may have edited.
|
|
182
252
|
|
|
183
253
|
| exists | differs | `conflictPolicy` | outcome |
|
|
184
254
|
| ------ | ------- | ---------------- | ------------- |
|
|
185
|
-
| no |
|
|
255
|
+
| no | n/a | n/a | `created` |
|
|
186
256
|
| yes | no | either | `up-to-date` |
|
|
187
257
|
| yes | yes | `replace` | `overwritten` |
|
|
188
258
|
| yes | yes | `skip` | `skipped` |
|
|
189
259
|
|
|
190
|
-
What counts as differing follows the policy, which is the part worth reading twice. `'replace'` promises the file holds exactly `content` afterwards, so only byte-identical content reports `up-to-date`; a file differing from `content` only in trailing whitespace is rewritten, because calling it up to date would leave the caller holding a file that is not what it asked for. `'skip'` modifies nothing either way, so its comparison decides a message alone and ignores trailing whitespace per line and at end of file, which keeps formatter churn from reading as a conflict. `up-to-date` therefore means the same thing under both:
|
|
260
|
+
What counts as differing follows the policy, which is the part worth reading twice. `'replace'` promises the file holds exactly `content` afterwards, so only byte-identical content reports `up-to-date`; a file differing from `content` only in trailing whitespace is rewritten, because calling it up to date would leave the caller holding a file that is not what it asked for. `'skip'` modifies nothing either way, so its comparison decides a message alone and ignores trailing whitespace per line and at end of file, which keeps formatter churn from reading as a conflict. `up-to-date` therefore means the same thing under both: This policy has no work to do.
|
|
191
261
|
|
|
192
|
-
The result discriminates on `outcome`, so a failure always
|
|
262
|
+
The result discriminates on `outcome`, so a failure always names its reason:
|
|
193
263
|
|
|
194
264
|
```ts
|
|
195
265
|
type FileReconciliation =
|
|
@@ -198,13 +268,13 @@ type FileReconciliation =
|
|
|
198
268
|
| { filePath: string; outcome: 'failed'; error: string };
|
|
199
269
|
```
|
|
200
270
|
|
|
201
|
-
An I/O error on the write path reports `failed` rather than throwing, which
|
|
271
|
+
An I/O error on the write path reports `failed` rather than throwing, which lets a command writing several files collect a result for each instead of losing the rest to the first failure.
|
|
202
272
|
|
|
203
273
|
Three behaviors are worth knowing before they surprise you:
|
|
204
274
|
|
|
205
|
-
- A `skipped` result
|
|
206
|
-
- The existence probe follows symlinks. A dangling symlink therefore reports as non-existent:
|
|
207
|
-
- The probe and the write are separate calls, leaving a window in which another process can create or remove the file. That gap is left open deliberately:
|
|
275
|
+
- A `skipped` result with an `error` means the existing file could not be read for comparison. The file was left alone, which is exactly what `'skip'` promises, so this is not a failure and a command exiting non-zero on failures should not count it as one.
|
|
276
|
+
- The existence probe follows symlinks. A dangling symlink therefore reports as non-existent: The outcome is `created`, the result names the link, and the bytes land at the link's target.
|
|
277
|
+
- The probe and the write are separate calls, leaving a window in which another process can create or remove the file. That gap is left open deliberately: The callers that this serves are scaffolding commands with no competing writer, and an exclusive-create flag would close only the create half of it.
|
|
208
278
|
|
|
209
279
|
## `reconcileFileFromFile`
|
|
210
280
|
|
|
@@ -216,7 +286,7 @@ reconcileFileFromFile(
|
|
|
216
286
|
): FileReconciliation;
|
|
217
287
|
```
|
|
218
288
|
|
|
219
|
-
Reconciles `filePath` against the content of `sourcePath`, which
|
|
289
|
+
Reconciles `filePath` against the content of `sourcePath`, for which a command copying a bundled template reaches:
|
|
220
290
|
|
|
221
291
|
```ts
|
|
222
292
|
import { reconcileFileFromFile } from '@williamthorsen/toolbelt.filesystem';
|
|
@@ -225,9 +295,9 @@ reconcileFileFromFile('.config/git-cliff.toml', bundledTemplatePath);
|
|
|
225
295
|
// { filePath: '.config/git-cliff.toml', outcome: 'created' }
|
|
226
296
|
```
|
|
227
297
|
|
|
228
|
-
It is [`reconcileFile`](#reconcilefile) with the read supplied:
|
|
298
|
+
It is [`reconcileFile`](#reconcilefile) with the read supplied: The outcome table, the conflict policy, the created parent directories, and the result type are that function's, unchanged. Three things are this one's own.
|
|
229
299
|
|
|
230
|
-
The source is read as utf8 text, so a binary source is not supported:
|
|
300
|
+
The source is read as utf8 text, so a binary source is not supported: It would be decoded and re-encoded on the way through.
|
|
231
301
|
|
|
232
302
|
A source that cannot be read reports `failed` rather than throwing, and a missing source is not distinguished from an unreadable one. The reason names the source and the cause:
|
|
233
303
|
|
|
@@ -235,134 +305,13 @@ A source that cannot be read reports `failed` rather than throwing, and a missin
|
|
|
235
305
|
Failed to read /pkg/cliff.toml.template: ENOENT: no such file or directory, open '/pkg/cliff.toml.template'
|
|
236
306
|
```
|
|
237
307
|
|
|
238
|
-
The path is interpolated rather than left to the underlying message, which
|
|
308
|
+
The path is interpolated rather than left to the underlying message, which has none of its own at the read stage: Reading a directory yields `EISDIR: illegal operation on a directory, read`. Under `ENOENT` the path therefore reads twice. The result's `filePath` is the destination on this path as on every other, so a caller copying several templates keys its results by destination and still sees which source failed.
|
|
239
309
|
|
|
240
310
|
The source is read even under `isDryRun`, because the outcome depends on comparing its content. A dry run can therefore report `failed` where `reconcileFile`'s cannot, and it still writes nothing.
|
|
241
311
|
|
|
242
|
-
## `createTempTree`
|
|
243
|
-
|
|
244
|
-
Candidate tier: imported from `@williamthorsen/toolbelt.filesystem/candidate` rather than the package root, and subject to change.
|
|
245
|
-
|
|
246
|
-
```ts
|
|
247
|
-
createTempTree(entries: Record<string, string | Uint8Array>, options?: { prefix?: string }): TempTree;
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
Builds a throwaway directory tree and returns a handle that removes it when the binding leaves scope:
|
|
251
|
-
|
|
252
|
-
```ts
|
|
253
|
-
import { createTempTree } from '@williamthorsen/toolbelt.filesystem/candidate';
|
|
254
|
-
|
|
255
|
-
{
|
|
256
|
-
using tree = createTempTree({
|
|
257
|
-
'.git/': '',
|
|
258
|
-
'packages/app/package.json': '{ "name": "app" }',
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
tree.dir; // '/private/var/folders/.../toolbelt-a1b2c3'
|
|
262
|
-
tree.resolve('packages/app'); // '/private/var/folders/.../toolbelt-a1b2c3/packages/app'
|
|
263
|
-
}
|
|
264
|
-
// The tree is gone here.
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
Each key of `entries` is a path relative to the tree root. One ending in `/` becomes a directory; any other becomes a file holding the mapped contents, with its intermediate directories created for it. A key resolving outside the root is rejected, and a call that throws leaves nothing on disk.
|
|
268
|
-
|
|
269
|
-
A value is text or the bytes themselves, so a body no UTF-8 round trip survives is as writable as a string:
|
|
270
|
-
|
|
271
|
-
```ts
|
|
272
|
-
using tree = createTempTree({ 'logo.png': pngBytes });
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
`prefix` names the directory built under the system temporary directory, defaulting to `toolbelt-`. Set it to whatever is doing the building, so a tree outliving a crashed run says what made it:
|
|
276
|
-
|
|
277
|
-
```ts
|
|
278
|
-
using tree = createTempTree({}, { prefix: 'rdy-tsconfig-' });
|
|
279
|
-
tree.dir; // '/private/var/folders/.../rdy-tsconfig-a1b2c3'
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
A prefix that would place the tree anywhere but directly inside the system temporary directory is rejected before anything is created. `mkdtemp` appends its random suffix to the joined path as given, so a prefix holding `/` or `\` targets a nested directory that has to already exist, or, where it ascends, a directory outside the temporary one; and a prefix that normalizes away (`''`, `'.'`, or `'..'`) lands the suffix beside the temporary directory rather than within it.
|
|
283
|
-
|
|
284
|
-
```ts
|
|
285
|
-
interface TempTree extends Disposable {
|
|
286
|
-
readonly dir: string;
|
|
287
|
-
exists(entryPath: string): boolean;
|
|
288
|
-
list(entryPath?: string): string[];
|
|
289
|
-
listFiles(entryPath?: string): string[];
|
|
290
|
-
mkdir(entryPath: string): string;
|
|
291
|
-
read(entryPath: string): string;
|
|
292
|
-
readJson(entryPath: string): unknown;
|
|
293
|
-
resolve(...segments: string[]): string;
|
|
294
|
-
rm(entryPath: string): void;
|
|
295
|
-
symlink(linkPath: string, targetPath: string): string;
|
|
296
|
-
write(entryPath: string, contents: string | Uint8Array): string;
|
|
297
|
-
writeAll(entries: Record<string, string | Uint8Array>): void;
|
|
298
|
-
writeJson(entryPath: string, value: unknown): string;
|
|
299
|
-
}
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
`dir` is realpath-resolved, because `os.tmpdir()` is a symlink on macOS and a caller comparing paths against it would otherwise see a mismatch it did not cause.
|
|
303
|
-
|
|
304
|
-
`resolve` joins `segments` against the root and throws when the result would fall outside it, so a stray `..` fails loudly rather than reaching into the enclosing directory. An absolute segment landing inside the root is returned unchanged. The containment test is lexical, so it does not follow a symlink inside the tree that points out of it.
|
|
305
|
-
|
|
306
|
-
`mkdir`, `symlink`, `write`, `writeAll`, and `writeJson` write into the tree after it is built, for a fixture that varies per test or a file created to trigger a re-read:
|
|
307
|
-
|
|
308
|
-
```ts
|
|
309
|
-
using tree = createTempTree({ 'packages/app/package.json': '{ "name": "app" }' });
|
|
310
|
-
|
|
311
|
-
tree.write('packages/app/src/main.ts', 'export {};\n'); // '/private/var/folders/.../packages/app/src/main.ts'
|
|
312
|
-
tree.writeJson('tsconfig.json', { include: ['src'] });
|
|
313
|
-
tree.mkdir('packages/empty');
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
Each creates the parent directories it needs, resolves through the same containment check as `resolve`, and returns the absolute path of what it wrote. `symlink`'s link path is checked; its target is not, being a string the link holds rather than a location the tree writes to.
|
|
317
|
-
|
|
318
|
-
`writeAll` takes the same map the constructor takes, `/`-suffix convention included, so a fixture built in one call can be added to in one call:
|
|
319
|
-
|
|
320
|
-
```ts
|
|
321
|
-
tree.writeAll({ 'packages/empty/': '', 'packages/app/src/main.ts': 'export {};\n' });
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
It returns nothing, there being no single path to return, and unlike the constructor it is not atomic: a failure part-way leaves the entries already written in place, there being no whole tree to discard.
|
|
325
|
-
|
|
326
|
-
They part company on an entry that already exists: `write` replaces it, `mkdir` leaves it and its contents alone, and `symlink` raises `EEXIST`.
|
|
327
|
-
|
|
328
|
-
`symlink` takes the link first and the target second, inverting `fs.symlinkSync`, so that it reads like the other methods: the path being created leads. The target is stored verbatim, so it may be absolute or relative, name something outside the tree, or dangle until the target appears; a relative one resolves against the link's own directory, as POSIX resolves it. Code under test that reads a link rather than following it therefore sees the string that was passed, which is what a consumer hashing a link's target depends on.
|
|
329
|
-
|
|
330
|
-
```ts
|
|
331
|
-
using tree = createTempTree({ 'store/kit/package.json': '{ "name": "kit" }' });
|
|
332
|
-
|
|
333
|
-
tree.symlink('node_modules/kit', '../store/kit'); // reads back as '../store/kit'
|
|
334
|
-
tree.symlink('node_modules/.bin', tree.resolve('store/kit/bin')); // reads back absolute
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
The link type is chosen from the target, which is where the one portability difference lives. An absolute directory target is linked as a junction, which Windows creates without the elevation a directory symlink needs; a relative directory target is linked as a directory, which needs that elevation, because Node normalizes a junction's target to an absolute path and would discard the relative string. Every other target, one that does not exist included, is linked as a file, matching what Node falls back to when no type is given.
|
|
338
|
-
|
|
339
|
-
`exists`, `list`, `listFiles`, `read`, `readJson`, and `rm` read the tree back and remove from it, each through the same containment check:
|
|
340
|
-
|
|
341
|
-
```ts
|
|
342
|
-
using tree = createTempTree({ 'packages/app/package.json': '{ "name": "app" }', 'packages/app/src/main.ts': 'export {};\n' });
|
|
343
|
-
|
|
344
|
-
tree.list(); // ['packages'], defaulting to the tree root
|
|
345
|
-
tree.list('packages/app'); // ['package.json', 'src'], sorted
|
|
346
|
-
tree.listFiles('packages'); // ['app/package.json', 'app/src/main.ts'], at any depth
|
|
347
|
-
tree.read('packages/app/src/main.ts'); // 'export {};\n'
|
|
348
|
-
tree.readJson('packages/app/package.json'); // unknown, for the caller to narrow
|
|
349
|
-
tree.exists('packages/app/tsconfig.json'); // false
|
|
350
|
-
tree.rm('packages/app');
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
`listFiles` reaches every depth and reports paths relative to the directory it was given, sorted, with `/` as the separator on every platform: a path a test asserts on is a value rather than a location, so `'app/src/main.ts'` should not vary by platform. It parts from `list` twice. A directory that is not there answers `[]` where `list` raises `ENOENT`, which is what lets a suite assert that a build emitted nothing without guarding the call; a path that exists as a file still raises `ENOTDIR`, as `list` does. And a symlink below the directory it was given is neither named nor descended, so every path in the result names a file held inside the tree, where `list` reports a link by name at its own level. The directory given as the argument is the exception, followed as `list`, `read`, and `exists` follow theirs: one naming a link out of the tree lists the target's files.
|
|
354
|
-
|
|
355
|
-
`read` returns UTF-8 text, and a missing entry raises `ENOENT` rather than answering emptily -- `exists` is the check. `readJson` returns `unknown`, so a caller narrows it rather than trusting an asserted type; contents that do not parse raise an error naming the entry, which the parse error alone does not. `exists` follows a symlink, so a dangling one answers `false`. `rm` is recursive and silent on an entry that is not there.
|
|
356
|
-
|
|
357
|
-
`writeJson` writes two-space-indented JSON ending in a newline, so a tree outliving a crashed run reads as a real config file would. A fixture needing exact bytes goes through `write` instead. A value `JSON.stringify` cannot represent -- `undefined`, a function, a symbol -- is refused rather than written, so an optional binding that arrived empty fails at the call that passed it instead of surfacing later as a parse error.
|
|
358
|
-
|
|
359
|
-
Disposal is idempotent, and it removes a tree that has been made unwritable: unlinking an entry needs write permission on the directory containing it, so disposal restores permission across the tree and retries once before giving up. A suite that chmods a directory to exercise a write-failure path therefore needs no wrapper to chmod it back.
|
|
360
|
-
|
|
361
|
-
`Disposable` is declared in `lib.esnext.disposable.d.ts` alone, so consuming this export requires `ESNext.Disposable` in your `lib`.
|
|
362
|
-
|
|
363
312
|
## `replaceFileExtension`
|
|
364
313
|
|
|
365
|
-
Proposed tier:
|
|
314
|
+
Proposed tier: Imported from `@williamthorsen/toolbelt.filesystem/proposed` rather than the package root, and subject to change.
|
|
366
315
|
|
|
367
316
|
```ts
|
|
368
317
|
replaceFileExtension(filePath: string, newExtension: string, options?: { oldExtension?: string }): string;
|
|
@@ -390,7 +339,7 @@ Two inputs throw rather than returning a path that would quietly be wrong: a `fi
|
|
|
390
339
|
|
|
391
340
|
## `writeAtomic`
|
|
392
341
|
|
|
393
|
-
|
|
342
|
+
Candidate tier: Imported from `@williamthorsen/toolbelt.filesystem/candidate` rather than the package root, and subject to change.
|
|
394
343
|
|
|
395
344
|
```ts
|
|
396
345
|
writeAtomic(filePath: string, content: string | Uint8Array): Promise<void>;
|
|
@@ -399,19 +348,19 @@ writeAtomic(filePath: string, content: string | Uint8Array): Promise<void>;
|
|
|
399
348
|
Writes `content` to `filePath` through a temp file and a rename, so a concurrent reader sees either the previous file or the complete new one, never a partial write:
|
|
400
349
|
|
|
401
350
|
```ts
|
|
402
|
-
import { writeAtomic } from '@williamthorsen/toolbelt.filesystem/
|
|
351
|
+
import { writeAtomic } from '@williamthorsen/toolbelt.filesystem/candidate';
|
|
403
352
|
|
|
404
353
|
await writeAtomic('.agents/manifest.json', `${JSON.stringify(manifest, null, 2)}\n`);
|
|
405
354
|
```
|
|
406
355
|
|
|
407
|
-
The temp file is a sibling of the target, which is the part a hand-rolled copy most often gets wrong: `rename` is atomic only within one filesystem, so a temp file staged under the system temporary directory fails with `EXDEV` the moment the target lives on another volume. Its name is dot-prefixed and
|
|
356
|
+
The temp file is a sibling of the target, which is the part that a hand-rolled copy most often gets wrong: `rename` is atomic only within one filesystem, so a temp file staged under the system temporary directory fails with `EXDEV` the moment the target lives on another volume. Its name is dot-prefixed and contains a random component, so it stays out of `*` globs and two processes writing the same target do not collide.
|
|
408
357
|
|
|
409
358
|
Missing parent directories are created, as they are for [`reconcileFile`](#reconcilefile).
|
|
410
359
|
|
|
411
|
-
An existing target's permission bits are
|
|
360
|
+
An existing target's permission bits are copied onto the replacement. A plain `writeFile` truncates the file in place and so preserves its mode, while a rename replaces the inode and would otherwise reset it to the platform default; without this, swapping a plain write for an atomic one would silently widen a `0o600` file to world-readable. A target that does not exist yet gets the platform default, exactly as a plain write would.
|
|
412
361
|
|
|
413
362
|
Three behaviors are worth knowing before they surprise you:
|
|
414
363
|
|
|
415
|
-
- Nothing is fsynced. "Atomic" here means no torn reads, not survives-power-loss:
|
|
364
|
+
- Nothing is fsynced. "Atomic" here means no torn reads, not survives-power-loss: A write from which this function has returned can still be lost to a power failure. A durability option is additive if a caller ever needs one.
|
|
416
365
|
- A symlink at `filePath` is replaced by a regular file rather than written through, because the rename replaces the target's directory entry. The link's former target is left untouched.
|
|
417
366
|
- A failure removes the temp file best-effort and rethrows the error that caused it, never the cleanup's own. Where the cleanup also fails, the temp file survives beside the target under its dot-prefixed name ending in `.tmp`, which is where to look for one.
|
|
@@ -1,4 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns `filePath` with its extension replaced by `newExtension`.
|
|
3
|
+
*
|
|
4
|
+
* The extension is the substring beginning at the final period in the file name, which must come at the very end
|
|
5
|
+
* of the path. A multi-part extension is indivisible to `path.extname` (`.d.ts` reports as `.ts`), so declare one
|
|
6
|
+
* through `oldExtension` to replace it whole.
|
|
7
|
+
*
|
|
8
|
+
* Either extension may be written with or without its leading period, so `'js'` and `'.js'` are equivalent. An
|
|
9
|
+
* empty `newExtension` removes the extension. Throws when `filePath` ends with a separator, and when `filePath`
|
|
10
|
+
* does not end with a declared `oldExtension`.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* replaceFileExtension('src/main.ts', '.js'); // 'src/main.js'
|
|
14
|
+
* replaceFileExtension('src/main.d.ts', '.js', { oldExtension: '.d.ts' }); // 'src/main.js'
|
|
15
|
+
*
|
|
16
|
+
* @category Filesystem
|
|
17
|
+
* @experimental
|
|
18
|
+
* @stage proposed
|
|
19
|
+
*/
|
|
1
20
|
export declare function replaceFileExtension(filePath: string, newExtension: string, options?: ReplaceFileExtensionOptions): string;
|
|
2
21
|
export interface ReplaceFileExtensionOptions {
|
|
22
|
+
/** The extension to replace; defaults to the value returned by Node's `path.extname`. */
|
|
3
23
|
oldExtension?: string | undefined;
|
|
4
24
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { writeAtomic } from './writeAtomic.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { writeAtomic } from "./writeAtomic.js";
|