@osdlc/tiered-dirs 0.0.1-stage → 0.3.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/CHANGELOG.md +99 -0
- package/CONTRIBUTING.md +66 -0
- package/LICENSE-APACHE +202 -0
- package/LICENSE-MIT +21 -0
- package/README.md +421 -3
- package/SECURITY.md +23 -0
- package/dist/base-dirs.d.ts +311 -0
- package/dist/base-dirs.js +477 -0
- package/dist/errors.d.ts +36 -0
- package/dist/errors.js +20 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +24 -0
- package/dist/segment.d.ts +17 -0
- package/dist/segment.js +40 -0
- package/dist/tiered-dirs.d.ts +190 -0
- package/dist/tiered-dirs.js +237 -0
- package/package.json +58 -5
package/README.md
CHANGED
|
@@ -1,5 +1,423 @@
|
|
|
1
1
|
# @osdlc/tiered-dirs
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Machine-local directories for every product a vendor ships, on Linux, macOS
|
|
4
|
+
and Windows.
|
|
5
|
+
|
|
6
|
+
## The problem
|
|
7
|
+
|
|
8
|
+
A program that keeps state on disk needs a directory for it, and the right one
|
|
9
|
+
differs by operating system and by kind of state. A vendor that ships several
|
|
10
|
+
programs also has to decide which state they share and which they keep apart.
|
|
11
|
+
And a test suite must never write into the developer's real home directory.
|
|
12
|
+
|
|
13
|
+
`@osdlc/tiered-dirs` answers all three. A caller names a directory on two axes:
|
|
14
|
+
|
|
15
|
+
- **tier**: who shares the state. `vendor()` is `<vendor>`, shared by
|
|
16
|
+
everything the vendor ships. `suite(key)` is `<vendor>.<key>`, shared by the
|
|
17
|
+
programs of one suite. `standalone(tool)` is the bare `<tool>`, for a general
|
|
18
|
+
tool that is not part of the vendor's suite.
|
|
19
|
+
- **home**: how long the state must last. `durable`, `config`, `cache`,
|
|
20
|
+
`state`, `logs` or `runtime`.
|
|
21
|
+
|
|
22
|
+
`scratch()` sits outside both axes: the platform temp directory.
|
|
23
|
+
|
|
24
|
+
A **convention** picks the per-OS table the homes come from: `native` (the
|
|
25
|
+
default) or `xdg`. They differ on macOS only.
|
|
26
|
+
|
|
27
|
+
It is for a vendor with more than one program, or one program with tests that
|
|
28
|
+
must not touch the real home. It computes paths only: it creates, reads and
|
|
29
|
+
deletes nothing on disk. It has no dependencies. The
|
|
30
|
+
[Rust crate `tiered-dirs`](https://crates.io/crates/tiered-dirs) is its twin
|
|
31
|
+
and gives the same relative paths.
|
|
32
|
+
|
|
33
|
+
## When not to use it
|
|
34
|
+
|
|
35
|
+
- One program with one config file. `env-paths` or `xdg-app-paths` is enough.
|
|
36
|
+
- You want the app framework's own directories, for example Tauri's. Use the
|
|
37
|
+
framework's.
|
|
38
|
+
- You need the directories created. This package does not create them.
|
|
39
|
+
|
|
40
|
+
## How it compares
|
|
41
|
+
|
|
42
|
+
| Package | What it gives |
|
|
43
|
+
|---|---|
|
|
44
|
+
| [env-paths](https://github.com/sindresorhus/env-paths) | One tool's data, config, cache, log and temp directories per OS. |
|
|
45
|
+
| [xdg-basedir](https://github.com/sindresorhus/xdg-basedir) | The XDG variables and their defaults, with no app name. |
|
|
46
|
+
| [xdg-app-paths](https://github.com/rivy/js.xdg-app-paths) | One tool's XDG directories, with a legacy fallback. |
|
|
47
|
+
| [os-paths](https://github.com/rivy/js.os-paths) | The OS home, temp and XDG directories. |
|
|
48
|
+
| [appdirsjs](https://github.com/codingjerk/appdirs-js) | One tool's cache, config and data directories. |
|
|
49
|
+
|
|
50
|
+
They give per-app or XDG directories for one tool. None of them has shared
|
|
51
|
+
vendor and suite tiers, separate state and runtime homes that are never the
|
|
52
|
+
cache, one override variable per root, a scratch root for tests, or a Rust
|
|
53
|
+
twin held to the same paths.
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
npm install @osdlc/tiered-dirs
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Quick start
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { TieredDirs, envVars } from '@osdlc/tiered-dirs'
|
|
65
|
+
|
|
66
|
+
const dirs = TieredDirs.fromEnv('com.example', envVars('EXAMPLE'), {
|
|
67
|
+
platform: 'linux',
|
|
68
|
+
env: { HOME: '/home/ada' },
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
dirs.vendor().config().path
|
|
72
|
+
// /home/ada/.config/com.example
|
|
73
|
+
|
|
74
|
+
dirs.suite('notes').cache().scoped('vaults', 'v7').app('search').path
|
|
75
|
+
// /home/ada/.cache/com.example.notes/vaults/v7/search
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
A real program leaves out `platform` and `env`; they are there so the paths
|
|
79
|
+
above do not depend on the machine. `examples/usage.test.ts` runs every export
|
|
80
|
+
of this package.
|
|
81
|
+
|
|
82
|
+
## Tiers
|
|
83
|
+
|
|
84
|
+
| tier | segment | shared by |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| vendor | `<vendor>` | every product line the vendor ships |
|
|
87
|
+
| suite | `<vendor>.<suiteKey>` | the products that share user data |
|
|
88
|
+
| standalone | `<tool>` | one general tool, shipped outside the suite |
|
|
89
|
+
|
|
90
|
+
The vendor is the one segment the caller fixes up front. The suite segment is
|
|
91
|
+
composed from it. `suiteKey` is a caller-supplied constant, not derived from a
|
|
92
|
+
bundle identifier. `Tier.kind` reports which method produced a tier (`vendor`,
|
|
93
|
+
`suite` or `standalone`).
|
|
94
|
+
|
|
95
|
+
`Tier` and `Dir` are exported as types only. Their constructors are not part
|
|
96
|
+
of the package, so a value always comes from a checked method:
|
|
97
|
+
`TieredDirs.vendor()`, `suite()`, `standalone()` and `scratch()`, `Tier.home()`
|
|
98
|
+
and the home methods, and `Dir.scoped()` and `Dir.app()`.
|
|
99
|
+
|
|
100
|
+
The product tier, one product's own directory keyed by bundle identifier, is
|
|
101
|
+
not here. An app framework usually resolves it, and this package takes no
|
|
102
|
+
framework dependency. `TieredDirs.isVendorBundleId(bundleId)` lets a layer
|
|
103
|
+
above check that a product's bundle id sits under the vendor.
|
|
104
|
+
`isVendorBundleId(vendor, bundleId)` is the same check for a caller with no
|
|
105
|
+
`TieredDirs`. Both are a bare prefix test, as in the Rust twin.
|
|
106
|
+
|
|
107
|
+
Tiers are distinct addresses and there is no merge. `vendor().durable()`,
|
|
108
|
+
`suite(k).durable()` and `standalone(t).durable()` are different locations.
|
|
109
|
+
This package never reads two and merges them. A given piece of state lives at
|
|
110
|
+
exactly one tier and the caller names the tier it means.
|
|
111
|
+
|
|
112
|
+
### A general tool supplies its own root
|
|
113
|
+
|
|
114
|
+
The vendor and suite tiers govern a suite: products that share one owner. A
|
|
115
|
+
general tool the vendor ships into other people's repositories is not in that
|
|
116
|
+
suite, and `suite('tool')` would write the vendor's namespace onto a stranger's
|
|
117
|
+
machine.
|
|
118
|
+
|
|
119
|
+
`standalone(tool)` is the tier for that tool. Its segment is the bare tool
|
|
120
|
+
name, with no vendor composition. Its config directory is `~/.config/<tool>` on
|
|
121
|
+
Linux, and on macOS under `xdg`. Under `native` on macOS it is
|
|
122
|
+
`~/Library/Application Support/<tool>`.
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { TieredDirs, envVars } from '@osdlc/tiered-dirs'
|
|
126
|
+
|
|
127
|
+
const cli = TieredDirs.fromEnv('com.example', envVars('TOOL'), {
|
|
128
|
+
convention: 'xdg',
|
|
129
|
+
platform: 'darwin',
|
|
130
|
+
env: { HOME: '/Users/ada' },
|
|
131
|
+
})
|
|
132
|
+
cli.standalone('tool').config().path
|
|
133
|
+
// /Users/ada/.config/tool
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Homes
|
|
137
|
+
|
|
138
|
+
| home | Linux (both) | macOS, `native` | macOS, `xdg` | Windows (both) |
|
|
139
|
+
|---|---|---|---|---|
|
|
140
|
+
| durable | `$XDG_DATA_HOME`, `~/.local/share` | `~/Library/Application Support` | `$XDG_DATA_HOME`, `~/.local/share` | `%APPDATA%` |
|
|
141
|
+
| config | `$XDG_CONFIG_HOME`, `~/.config` | `~/Library/Application Support` | `$XDG_CONFIG_HOME`, `~/.config` | `%APPDATA%` |
|
|
142
|
+
| cache | `$XDG_CACHE_HOME`, `~/.cache` | `~/Library/Caches` | `$XDG_CACHE_HOME`, `~/.cache` | `%LOCALAPPDATA%` |
|
|
143
|
+
| state | `$XDG_STATE_HOME`, `~/.local/state` | `~/Library/Application Support` | `$XDG_STATE_HOME`, `~/.local/state` | `%LOCALAPPDATA%\state` |
|
|
144
|
+
| logs | `$XDG_STATE_HOME`, `~/.local/state` | `~/Library/Logs` | `$XDG_STATE_HOME`, `~/.local/state` | `<durable>/logs` |
|
|
145
|
+
| runtime | `$XDG_RUNTIME_DIR`, `<durable>/run` | `<durable>/run` | `$XDG_RUNTIME_DIR`, `<durable>/run` | `<durable>/run` |
|
|
146
|
+
|
|
147
|
+
`logs` and `state` never fall back to `cache`, because the OS purges cache. On
|
|
148
|
+
Windows `state` is `%LOCALAPPDATA%\state`: under the same root as each tier's
|
|
149
|
+
cache folder and beside it, never the same folder. `state`, `logs` and `run`
|
|
150
|
+
are reserved segment names, in any case, so no tier can land on another home's
|
|
151
|
+
root: `standalone('state')` throws `TieredDirsError`. `runtime` sits under
|
|
152
|
+
`durable` for the same reason: the OS may purge cache while the process a
|
|
153
|
+
pidfile describes is still alive. Under XDG, logs and state share one
|
|
154
|
+
directory, so keep their file names apart. A relative `XDG_*` value is
|
|
155
|
+
ignored, as the XDG spec says, and `~` is not expanded. On Windows, `xdg`
|
|
156
|
+
means `native`.
|
|
157
|
+
|
|
158
|
+
## Choosing a convention
|
|
159
|
+
|
|
160
|
+
- A CLI or developer tool uses `xdg`. Its users look for `~/.config/<tool>` and
|
|
161
|
+
keep it in their dotfiles.
|
|
162
|
+
- A GUI app uses `native`. macOS users and tools expect
|
|
163
|
+
`~/Library/Application Support`.
|
|
164
|
+
- A file that two programs read is resolved in one place, by the program that
|
|
165
|
+
owns it, under that program's convention.
|
|
166
|
+
|
|
167
|
+
Pass `{ convention: 'xdg' }` to `systemBaseDirs` or `TieredDirs.fromEnv`.
|
|
168
|
+
`<P>_CONVENTION=native` or `xdg` overrides the caller's choice; any other value
|
|
169
|
+
throws `TieredDirsError`. The value is trimmed, then must match exactly, in
|
|
170
|
+
lower case. `parseConvention(value)` parses a convention from anywhere else,
|
|
171
|
+
such as a flag: exactly `native` or `xdg`, untrimmed, like the Rust twin's
|
|
172
|
+
`FromStr`. `xdgBaseDirs(home, env)` builds the XDG table under any home, on
|
|
173
|
+
any host.
|
|
174
|
+
|
|
175
|
+
## Overrides and scratch roots
|
|
176
|
+
|
|
177
|
+
Every base directory can be moved by environment variable, so a test or harness
|
|
178
|
+
can run a whole app inside one scratch root. The caller picks the names:
|
|
179
|
+
`envVars('EXAMPLE')` reads `EXAMPLE_ROOT`, `EXAMPLE_<HOME>_DIR`,
|
|
180
|
+
`EXAMPLE_SCRATCH_DIR`, `EXAMPLE_REQUIRE_OVERRIDE` and `EXAMPLE_CONVENTION`.
|
|
181
|
+
`overrideVars(vars)` lists the ones that move a directory, and
|
|
182
|
+
`isOverridden(vars, env)` says whether any of them is set to a non-empty value
|
|
183
|
+
(`env` defaults to `process.env`). Per base directory,
|
|
184
|
+
the first answer wins:
|
|
185
|
+
|
|
186
|
+
| order | source | effect |
|
|
187
|
+
|---|---|---|
|
|
188
|
+
| 1 | `<P>_<HOME>_DIR` | moves one: `<P>_DURABLE_DIR`, `<P>_CONFIG_DIR`, `<P>_CACHE_DIR`, `<P>_STATE_DIR`, `<P>_LOGS_DIR`, `<P>_RUNTIME_DIR`, `<P>_SCRATCH_DIR` |
|
|
189
|
+
| 2 | `<P>_ROOT` | the rooted layout: `<root>/durable`, `<root>/config`, `<root>/cache`, `<root>/state`, `<root>/logs`, `<root>/scratch`, runtime at `<root>/durable/run` |
|
|
190
|
+
| 3 | the OS default | the table above, for `<P>_CONVENTION` when set, else `opts.convention`, else `native` |
|
|
191
|
+
|
|
192
|
+
Runtime follows durable: when `<P>_RUNTIME_DIR` is unset but durable is moved by
|
|
193
|
+
step 1 or 2, runtime is `<durable>/run`. An empty variable counts as unset; a
|
|
194
|
+
relative one is an error. `opts.overrides` on `systemBaseDirs` sits ahead of all
|
|
195
|
+
three. `rootedBaseDirs(root)` builds the layout of step 2 directly.
|
|
196
|
+
|
|
197
|
+
### Tests never resolve a real home
|
|
198
|
+
|
|
199
|
+
When neither `env` nor `home` is injected, `systemBaseDirs` applies two rules
|
|
200
|
+
first:
|
|
201
|
+
|
|
202
|
+
- In a test runner process where `<P>_ROOT` is unset and `HOME` is not moved,
|
|
203
|
+
`<P>_ROOT` reads as `<tmp>/tiered-dirs-test-<pid>-<start ms>`. "HOME moved"
|
|
204
|
+
means `HOME` differs from the passwd home. The Rust twin has no such
|
|
205
|
+
condition. It is kept so a test that points `HOME` at a fixture keeps its
|
|
206
|
+
paths. `{ testRoot: false }` turns the test-root default off explicitly. A CI
|
|
207
|
+
job that sets `HOME` itself, such as a GitHub Actions container job
|
|
208
|
+
(`HOME=/github/home`, passwd home `/root`), therefore gets no test-root
|
|
209
|
+
default: set `<P>_ROOT` there. A test runner
|
|
210
|
+
process is one with a test file as its entry point that runs with
|
|
211
|
+
`NODE_ENV=test`, `NODE_TEST_CONTEXT` (set by `node --test`) or under Bun.
|
|
212
|
+
A Vitest worker is detected too. A test that points `HOME` at a fixture
|
|
213
|
+
keeps it. A child process a test spawns gets no default, because its entry
|
|
214
|
+
point is neither a test file nor a Vitest worker.
|
|
215
|
+
- With `<P>_REQUIRE_OVERRIDE` set, a root that would fall back to the per-OS
|
|
216
|
+
table throws a `TieredDirsError` naming the fix.
|
|
217
|
+
|
|
218
|
+
`testRoot()` returns that root, cached per module instance (per test file under
|
|
219
|
+
Vitest's default isolation), in a test runner process, and
|
|
220
|
+
`undefined` elsewhere. A layer that reads its own variables, such as a
|
|
221
|
+
product tier, uses it to apply the same default. Pass `{ testRoot: false }`
|
|
222
|
+
to `systemBaseDirs` or `TieredDirs.fromEnv` to skip the first rule only.
|
|
223
|
+
`<P>_ROOT` and `<P>_REQUIRE_OVERRIDE` still apply.
|
|
224
|
+
|
|
225
|
+
## Scratch
|
|
226
|
+
|
|
227
|
+
`scratch()` returns the platform temp directory: `$TMPDIR` or `/tmp`, and
|
|
228
|
+
`%TEMP%` / `%TMP%` on Windows. It adds nothing to the platform answer, so "no
|
|
229
|
+
direct OS path call outside this package" can be checked with a search.
|
|
230
|
+
|
|
231
|
+
Scratch is not a `Home` and not a tier. It holds state that does not outlive a
|
|
232
|
+
single call. The `runtime` home holds state that does and describes a live
|
|
233
|
+
process: a pidfile, a socket, a lock.
|
|
234
|
+
|
|
235
|
+
## Injectable roots
|
|
236
|
+
|
|
237
|
+
Nothing below `systemBaseDirs` reads the environment. A `TieredDirs` resolves
|
|
238
|
+
its roots once, at construction, and every path after that is pure string work.
|
|
239
|
+
A server that cannot read the environment hands in roots from its own config:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
import { TieredDirs, type BaseDirs } from '@osdlc/tiered-dirs'
|
|
243
|
+
|
|
244
|
+
const bases: BaseDirs = {
|
|
245
|
+
durable: '/srv/data',
|
|
246
|
+
config: '/srv/config',
|
|
247
|
+
cache: '/srv/cache',
|
|
248
|
+
state: '/srv/state',
|
|
249
|
+
logs: '/srv/logs',
|
|
250
|
+
runtime: '/srv/run',
|
|
251
|
+
scratch: '/srv/tmp',
|
|
252
|
+
}
|
|
253
|
+
const dirs = new TieredDirs('com.example', bases)
|
|
254
|
+
dirs.vendor().cache().path
|
|
255
|
+
// /srv/cache/com.example
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Every `BaseDirs` field is `readonly`. To change a root, build a new object, for
|
|
259
|
+
example `{ ...bases, cache: '/srv/other' }`.
|
|
260
|
+
|
|
261
|
+
Tests use the same seam to assert exact paths without touching a real home
|
|
262
|
+
directory.
|
|
263
|
+
|
|
264
|
+
## Failure
|
|
265
|
+
|
|
266
|
+
Every base directory can fail to resolve; headless CI and sandboxes do not
|
|
267
|
+
always have a home. `systemBaseDirs` and `TieredDirs.fromEnv` throw
|
|
268
|
+
`TieredDirsError` rather than assert, and `overrides` rescues an environment
|
|
269
|
+
with no home at all. `vendor`, `suiteKey`, `tool`, `kind`, `key` and `app` are
|
|
270
|
+
each checked as a single path segment: non-empty, no separator, no `..`,
|
|
271
|
+
matching `/^[a-z0-9][a-z0-9._-]*$/i`, and not `state`, `logs` or `run`.
|
|
272
|
+
`isValidSegment(key)` is that rule. An invalid one throws `TieredDirsError`.
|
|
273
|
+
|
|
274
|
+
`TieredDirsError.code` says which failure it is, one code per variant of the
|
|
275
|
+
Rust twin's `Error`. Its type is `TieredDirsErrorCode`:
|
|
276
|
+
|
|
277
|
+
| code | when |
|
|
278
|
+
|---|---|
|
|
279
|
+
| `MISSING_BASE_DIR` | a root needs a home directory and none can be found |
|
|
280
|
+
| `RELATIVE_OVERRIDE` | an override variable holds a relative path |
|
|
281
|
+
| `OVERRIDE_REQUIRED` | `<P>_REQUIRE_OVERRIDE` is set and a root would fall back to the per-OS table |
|
|
282
|
+
| `INVALID_CONVENTION` | `<P>_CONVENTION` or `parseConvention` got a value other than `native` or `xdg` |
|
|
283
|
+
| `INVALID_KEY` | a segment fails `isValidSegment` |
|
|
284
|
+
|
|
285
|
+
The message gives the details, such as the variable or the key.
|
|
286
|
+
|
|
287
|
+
## API
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
function envVars(prefix: string): EnvVars
|
|
291
|
+
function overrideVars(vars: EnvVars): readonly string[]
|
|
292
|
+
function isOverridden(vars: EnvVars, env?: Record<string, string | undefined>): boolean
|
|
293
|
+
interface EnvVars {
|
|
294
|
+
root: string
|
|
295
|
+
dirs: Record<keyof BaseDirs, string>
|
|
296
|
+
requireOverride: string
|
|
297
|
+
convention: string
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function isValidSegment(key: string): boolean
|
|
301
|
+
|
|
302
|
+
type Home = 'durable' | 'config' | 'cache' | 'state' | 'logs' | 'runtime'
|
|
303
|
+
const HOMES: readonly Home[]
|
|
304
|
+
type Convention = 'native' | 'xdg'
|
|
305
|
+
const CONVENTIONS: readonly Convention[]
|
|
306
|
+
function parseConvention(value: string): Convention
|
|
307
|
+
type TierKind = 'vendor' | 'suite' | 'standalone'
|
|
308
|
+
const TIER_KINDS: readonly TierKind[]
|
|
309
|
+
interface BaseDirs {
|
|
310
|
+
readonly durable: string
|
|
311
|
+
readonly config: string
|
|
312
|
+
readonly cache: string
|
|
313
|
+
readonly state: string
|
|
314
|
+
readonly logs: string
|
|
315
|
+
readonly runtime: string
|
|
316
|
+
readonly scratch: string
|
|
317
|
+
}
|
|
318
|
+
interface ResolveOptions {
|
|
319
|
+
platform?: string
|
|
320
|
+
convention?: Convention
|
|
321
|
+
env?: Record<string, string | undefined>
|
|
322
|
+
home?: string
|
|
323
|
+
overrides?: Partial<BaseDirs>
|
|
324
|
+
testRoot?: boolean // default true
|
|
325
|
+
}
|
|
326
|
+
function systemBaseDirs(vars: EnvVars, opts?: ResolveOptions): BaseDirs
|
|
327
|
+
function testRoot(): string | undefined
|
|
328
|
+
function rootedBaseDirs(root: string): BaseDirs
|
|
329
|
+
function xdgBaseDirs(home: string, env: Record<string, string | undefined>): BaseDirs
|
|
330
|
+
|
|
331
|
+
class TieredDirs {
|
|
332
|
+
constructor(vendor: string, bases: BaseDirs)
|
|
333
|
+
static fromEnv(vendor: string, vars: EnvVars, opts?: ResolveOptions): TieredDirs
|
|
334
|
+
readonly vendorName: string
|
|
335
|
+
readonly bases: Readonly<BaseDirs>
|
|
336
|
+
vendor(): Tier
|
|
337
|
+
suite(suiteKey: string): Tier
|
|
338
|
+
standalone(tool: string): Tier
|
|
339
|
+
scratch(): Dir
|
|
340
|
+
isVendorBundleId(bundleId: string): boolean
|
|
341
|
+
}
|
|
342
|
+
function isVendorBundleId(vendor: string, bundleId: string): boolean
|
|
343
|
+
|
|
344
|
+
// Tier and Dir are exported as types only. Get one from the methods above.
|
|
345
|
+
class Tier {
|
|
346
|
+
readonly segment: string
|
|
347
|
+
readonly kind: TierKind
|
|
348
|
+
durable(): Dir
|
|
349
|
+
config(): Dir
|
|
350
|
+
cache(): Dir
|
|
351
|
+
state(): Dir
|
|
352
|
+
logs(): Dir
|
|
353
|
+
runtime(): Dir
|
|
354
|
+
home(home: Home): Dir
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
class Dir {
|
|
358
|
+
readonly base: string
|
|
359
|
+
readonly segments: readonly string[]
|
|
360
|
+
get path(): string // absolute, host separator
|
|
361
|
+
get relative(): string // below the base, always "/"-separated
|
|
362
|
+
scoped(kind: string, key: string): Dir
|
|
363
|
+
app(app: string): Dir
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
type TieredDirsErrorCode =
|
|
367
|
+
| 'MISSING_BASE_DIR'
|
|
368
|
+
| 'RELATIVE_OVERRIDE'
|
|
369
|
+
| 'OVERRIDE_REQUIRED'
|
|
370
|
+
| 'INVALID_CONVENTION'
|
|
371
|
+
| 'INVALID_KEY'
|
|
372
|
+
class TieredDirsError extends Error {
|
|
373
|
+
readonly code: TieredDirsErrorCode
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## Parity
|
|
378
|
+
|
|
379
|
+
This package is the twin of the Rust `tiered-dirs` crate.
|
|
380
|
+
`packages/rust/tiered-dirs/tests/fixtures/parity.json` in the repository is
|
|
381
|
+
the contract between them: a table of (tier, home, scope, app) to expected
|
|
382
|
+
relative path, a `scratch_cases` list for the scratch root, `env_cases` for the
|
|
383
|
+
override order, `convention_cases` for the XDG layout under a stub home and
|
|
384
|
+
`rejected_conventions`. Both suites read that one file with base directories
|
|
385
|
+
stubbed: `rootedBaseDirs('/stub')` here, `BaseDirs::rooted("/stub")` there.
|
|
386
|
+
Neither twin may add a path shape the other lacks.
|
|
387
|
+
|
|
388
|
+
## Stability
|
|
389
|
+
|
|
390
|
+
Every export is stable in 0.x, and its TSDoc says so: `TieredDirs`,
|
|
391
|
+
`TieredDirsError`, `envVars`, `overrideVars`, `isOverridden`,
|
|
392
|
+
`systemBaseDirs`, `testRoot`, `rootedBaseDirs`, `xdgBaseDirs`,
|
|
393
|
+
`parseConvention`, `isValidSegment`, `isVendorBundleId`, `HOMES`,
|
|
394
|
+
`CONVENTIONS`, `TIER_KINDS` and the types `Tier`, `Dir`, `BaseDirs`, `EnvVars`, `Home`,
|
|
395
|
+
`Convention`, `TierKind`, `ResolveOptions` and `TieredDirsErrorCode`. A
|
|
396
|
+
breaking change to one of them ships only in a new 0.x minor release, with a
|
|
397
|
+
CHANGELOG entry that starts with `Breaking:`.
|
|
398
|
+
|
|
399
|
+
Some of them may grow in a minor release, as their Rust twins are
|
|
400
|
+
`#[non_exhaustive]`: `TierKind` and `Convention` may gain a member,
|
|
401
|
+
`TieredDirsErrorCode` a code, and `EnvVars` a name. A `switch` over one of the
|
|
402
|
+
unions needs a `default` arm. Build `EnvVars` with `envVars()`, not by hand,
|
|
403
|
+
so a new name is filled in. `Home` is exhaustive on purpose: a new home
|
|
404
|
+
changes every caller's layout, so adding one is a breaking release.
|
|
405
|
+
|
|
406
|
+
## Runtimes and platforms
|
|
407
|
+
|
|
408
|
+
Node.js 22 or later, ESM only. The built package is tested on Node.js 22 and
|
|
409
|
+
24, on Linux and macOS. The code uses `node:os` and `node:path` only. The path
|
|
410
|
+
tables above cover Windows, and the tests check all three tables by injecting
|
|
411
|
+
the platform. The tests compare paths without depending on the separator, but
|
|
412
|
+
they do not run on a Windows host in CI yet. Bun is not tested, though the
|
|
413
|
+
test-root rule recognises it.
|
|
414
|
+
|
|
415
|
+
## Security
|
|
416
|
+
|
|
417
|
+
The package builds path strings. It runs no commands, writes no files and
|
|
418
|
+
creates no temporary files. Every caller-supplied segment is checked, and an
|
|
419
|
+
override variable must be an absolute path. See [SECURITY.md](./SECURITY.md).
|
|
420
|
+
|
|
421
|
+
## License
|
|
422
|
+
|
|
423
|
+
`MIT OR Apache-2.0`, at your option.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
Please report vulnerabilities privately. Do not open a public issue.
|
|
6
|
+
|
|
7
|
+
Use GitHub private vulnerability reporting on the repository: open the
|
|
8
|
+
[Security tab](https://github.com/osdlc/osdlc/security) and choose
|
|
9
|
+
"Report a vulnerability".
|
|
10
|
+
|
|
11
|
+
Include the version, a short description, and steps to reproduce. A minimal
|
|
12
|
+
code sample helps.
|
|
13
|
+
|
|
14
|
+
## Supported versions
|
|
15
|
+
|
|
16
|
+
Only the latest release gets security fixes. While the version is 0.x, that
|
|
17
|
+
is the latest minor release. Upgrade to it.
|
|
18
|
+
|
|
19
|
+
## What to expect
|
|
20
|
+
|
|
21
|
+
This is a small project. We aim to reply within a week. We will tell you
|
|
22
|
+
whether we accept the report and keep you updated until a fix is released.
|
|
23
|
+
We will credit you in the release notes if you want that.
|