@osdlc/tiered-dirs 0.0.0-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.
@@ -0,0 +1,311 @@
1
+ /**
2
+ * The horizontal axis: how durable a piece of state is. The vertical axis —
3
+ * who shares it — is the tier (see `tiered-dirs.ts`).
4
+ *
5
+ * A caller names both: `suite('notes').cache()`.
6
+ *
7
+ * Exhaustive on purpose, as in the Rust twin: a new home changes every
8
+ * caller's layout, so adding one is a breaking (0.x minor) release.
9
+ *
10
+ * Stable in 0.x.
11
+ */
12
+ export type Home = 'durable' | 'config' | 'cache' | 'state' | 'logs' | 'runtime';
13
+ /**
14
+ * Every {@link Home}, in table order.
15
+ *
16
+ * Stable in 0.x.
17
+ */
18
+ export declare const HOMES: readonly Home[];
19
+ /**
20
+ * Which per-OS table the homes come from when no override moves them. The two
21
+ * differ on macOS only: `native` is Apple's places, `xdg` is the XDG Base
22
+ * Directory rules. Linux uses XDG under both, and on Windows `xdg` means
23
+ * `native`. A CLI or developer tool uses `xdg`; a GUI app uses `native`. The
24
+ * Rust twin's `Convention` has the same two.
25
+ *
26
+ * A minor release may add a convention, as the Rust `Convention` is
27
+ * `#[non_exhaustive]`. A `switch` over it needs a `default` arm.
28
+ *
29
+ * Stable in 0.x.
30
+ */
31
+ export type Convention = 'native' | 'xdg';
32
+ /**
33
+ * Every {@link Convention}. The first is the default.
34
+ *
35
+ * Stable in 0.x.
36
+ */
37
+ export declare const CONVENTIONS: readonly Convention[];
38
+ /**
39
+ * Parse a {@link Convention}: exactly `native` or `xdg`, in lower case,
40
+ * untrimmed. The Rust twin's `Convention::from_str` is the same rule.
41
+ *
42
+ * Throws {@link TieredDirsError} with code `INVALID_CONVENTION` for any other
43
+ * value. {@link EnvVars.convention} is read differently: it is trimmed first.
44
+ *
45
+ * Stable in 0.x.
46
+ */
47
+ export declare function parseConvention(value: string): Convention;
48
+ /**
49
+ * The per-OS roots the whole package is built on.
50
+ *
51
+ * This is the injectable seam. Nothing below it reads the environment: a
52
+ * `TieredDirs` resolves its roots once, at construction, and every path after that
53
+ * is pure string work. That is what lets a server that cannot read the
54
+ * environment hand in roots from its own configuration, while a command-line
55
+ * program builds them from the environment.
56
+ *
57
+ * Every field is `readonly`: build a new value to change a root, as
58
+ * {@link rootedBaseDirs} and the `overrides` option do.
59
+ *
60
+ * Stable in 0.x.
61
+ */
62
+ export interface BaseDirs {
63
+ /** Long-lived state that must survive a cache purge. */
64
+ readonly durable: string;
65
+ /** User-editable configuration. */
66
+ readonly config: string;
67
+ /** Regenerable data. The OS may delete this at any time. */
68
+ readonly cache: string;
69
+ /** State that should outlive a restart but is not user data: history, trust markers. */
70
+ readonly state: string;
71
+ /** Log files. Never folded into `cache` — the OS purges cache. */
72
+ readonly logs: string;
73
+ /** Pidfiles, sockets, locks. Never under `cache`; see {@link systemBaseDirs}. */
74
+ readonly runtime: string;
75
+ /**
76
+ * The platform temp directory. Not a {@link Home}: nothing here outlives the
77
+ * call that wrote it, so it is untiered and reached through `TieredDirs.scratch()`
78
+ * rather than through a tier.
79
+ */
80
+ readonly scratch: string;
81
+ }
82
+ /**
83
+ * The names of the environment variables that move base directories. Build one
84
+ * with {@link envVars}, which derives every name from a prefix.
85
+ *
86
+ * A minor release may add a name, as the Rust `EnvVars` is
87
+ * `#[non_exhaustive]`. An object built by hand would then miss it, so build
88
+ * one with {@link envVars}. `dirs[home]` is the Rust `EnvVars::dir`.
89
+ *
90
+ * Stable in 0.x.
91
+ */
92
+ export interface EnvVars {
93
+ /**
94
+ * Moves every base directory not moved by its own variable into the
95
+ * {@link rootedBaseDirs} layout under the named root.
96
+ */
97
+ readonly root: string;
98
+ /** The variable that moves one base directory. */
99
+ readonly dirs: Readonly<Record<keyof BaseDirs, string>>;
100
+ /**
101
+ * When set (non-empty), {@link systemBaseDirs} throws instead of falling back
102
+ * to an OS home. A test runner sets it, and a child process inherits it.
103
+ */
104
+ readonly requireOverride: string;
105
+ /**
106
+ * Picks the {@link Convention}, over the one the caller passed: `native` or
107
+ * `xdg`. Moves no directory itself.
108
+ */
109
+ readonly convention: string;
110
+ }
111
+ /**
112
+ * {@link EnvVars} named from one prefix: `<P>_ROOT`, `<P>_DURABLE_DIR`,
113
+ * `<P>_CONFIG_DIR`, `<P>_CACHE_DIR`, `<P>_STATE_DIR`, `<P>_LOGS_DIR`,
114
+ * `<P>_RUNTIME_DIR`, `<P>_SCRATCH_DIR`, `<P>_REQUIRE_OVERRIDE` and
115
+ * `<P>_CONVENTION`. The Rust twin's `env_vars!` derives the same names.
116
+ *
117
+ * Stable in 0.x.
118
+ */
119
+ export declare function envVars(prefix: string): EnvVars;
120
+ /**
121
+ * Every variable that moves a base directory: the root and the seven
122
+ * per-directory names. Excludes {@link EnvVars.requireOverride} and
123
+ * {@link EnvVars.convention}, which move nothing.
124
+ *
125
+ * Stable in 0.x.
126
+ */
127
+ export declare function overrideVars(vars: EnvVars): readonly string[];
128
+ /**
129
+ * Whether `env` sets any of {@link overrideVars} to a non-empty value. The
130
+ * Rust twin's `EnvVars::is_overridden` is the same test.
131
+ *
132
+ * Stable in 0.x.
133
+ */
134
+ export declare function isOverridden(vars: EnvVars, env?: Record<string, string | undefined>): boolean;
135
+ /**
136
+ * Every base directory under one stub root: `<root>/durable`, `<root>/config`,
137
+ * `<root>/cache`, `<root>/state`, `<root>/logs`, `<root>/scratch`, and runtime
138
+ * at `<root>/durable/run`.
139
+ *
140
+ * The test and parity-fixture seam, and the layout {@link EnvVars.root} selects. The
141
+ * Rust twin's `BaseDirs::rooted` has the same layout, so a fixture row asserts
142
+ * identical absolute paths on both arms.
143
+ *
144
+ * Stable in 0.x.
145
+ */
146
+ export declare function rootedBaseDirs(root: string): BaseDirs;
147
+ /**
148
+ * How {@link systemBaseDirs} and `TieredDirs.fromEnv` resolve. Every input is
149
+ * optional and injectable. The Rust twin's `ResolveOptions` has `convention`
150
+ * and `testRoot`; `platform`, `env`, `home` and `overrides` are this twin's
151
+ * own seams.
152
+ *
153
+ * Stable in 0.x.
154
+ */
155
+ export interface ResolveOptions {
156
+ /** Defaults to `process.platform`. */
157
+ platform?: string;
158
+ /**
159
+ * The per-OS table to use. Defaults to `native`. {@link EnvVars.convention}
160
+ * beats it when set.
161
+ */
162
+ convention?: Convention;
163
+ /**
164
+ * Defaults to `process.env`. Injecting this means the supplied environment
165
+ * is the whole story: the host's real home directory is not consulted. The
166
+ * override variables are read from it too.
167
+ */
168
+ env?: Record<string, string | undefined>;
169
+ /** Home directory. Overrides `HOME` / `USERPROFILE` when given. */
170
+ home?: string;
171
+ /**
172
+ * Replace individual roots, ahead of the environment. An overridden root is
173
+ * taken as-is and needs no home directory. It is not a base for the others:
174
+ * overriding `durable` here leaves `runtime` and `logs` where the
175
+ * environment and the per-OS table put them.
176
+ */
177
+ overrides?: Partial<BaseDirs>;
178
+ /**
179
+ * Whether an unset root variable reads as {@link testRoot} in a test runner
180
+ * process. Defaults to `true`. With `false`, resolution runs as it would
181
+ * outside a test runner: {@link EnvVars.root} and
182
+ * {@link EnvVars.requireOverride} still apply. Has no effect when `env` or
183
+ * `home` is injected, since neither test rule runs then.
184
+ */
185
+ testRoot?: boolean;
186
+ }
187
+ /**
188
+ * Resolve the base directories. Each takes the first answer in this order:
189
+ *
190
+ * 1. `opts.overrides`.
191
+ * 2. The root's own variable, `vars.dirs[key]` ({@link EnvVars.dirs}).
192
+ * 3. `vars.root` ({@link EnvVars.root}) — the {@link rootedBaseDirs} layout.
193
+ * 4. The per-OS table below, for the convention: {@link EnvVars.convention}
194
+ * when set, else `opts.convention`, else `native`. The variable is
195
+ * trimmed, then must be exactly `native` or `xdg`; anything else throws.
196
+ *
197
+ * Runtime follows durable: when the runtime variable is unset but durable is
198
+ * moved by step 2 or 3, runtime is `<durable>/run`, never the OS runtime
199
+ * directory. An empty variable counts as unset; a relative one throws. The
200
+ * Rust twin's `BaseDirs::from_env` resolves steps 2–4 the same way, held to it
201
+ * by the parity fixture's `env_cases`.
202
+ *
203
+ * The per-OS table this package owns so no caller has to. Linux, and macOS
204
+ * under `xdg`, use the XDG column; Windows uses its column under both.
205
+ *
206
+ * | home | macOS, `native` | XDG (Linux/other; macOS `xdg`) | Windows |
207
+ * |---------|--------------------------------|---------------------------------|----------------------|
208
+ * | durable | `~/Library/Application Support`| `$XDG_DATA_HOME`, `~/.local/share` | `%APPDATA%`, `<home>/AppData/Roaming` |
209
+ * | config | `~/Library/Application Support`| `$XDG_CONFIG_HOME`, `~/.config` | `%APPDATA%`, `<home>/AppData/Roaming` |
210
+ * | cache | `~/Library/Caches` | `$XDG_CACHE_HOME`, `~/.cache` | `%LOCALAPPDATA%`, `<home>/AppData/Local` |
211
+ * | state | `~/Library/Application Support`| `$XDG_STATE_HOME`, `~/.local/state` | `<local>/state`, where `<local>` is the cache cell |
212
+ * | logs | `~/Library/Logs` | `$XDG_STATE_HOME`, `~/.local/state` | `<durable>/logs` |
213
+ * | runtime | `<durable>/run` | `$XDG_RUNTIME_DIR`, `<durable>/run` | `<durable>/run` |
214
+ * | scratch | `$TMPDIR`, `/tmp` | `$TMPDIR`, `/tmp` | `%TEMP%`, `%TMP%`, `<home>/AppData/Local/Temp` |
215
+ *
216
+ * A relative `XDG_*` value is ignored, as the XDG spec says, and `~` is not
217
+ * expanded. Under XDG, logs and state share one directory.
218
+ *
219
+ * Two of those cells need care:
220
+ *
221
+ * - `logs` has no cross-platform library equivalent. macOS wants
222
+ * `~/Library/Logs`; where the OS offers nothing, logs land at
223
+ * `<durable>/logs`. They are never folded into `cache`.
224
+ * - `runtime` is real only under the XDG table (Linux, and macOS under
225
+ * `xdg`), and only when `XDG_RUNTIME_DIR` is set. Everywhere else it
226
+ * resolves under `<durable>/run` — deliberately not under cache, because the
227
+ * OS may purge cache while the process a pidfile describes is still alive.
228
+ * State is never cache either: on Windows it is `<local>/state`, beside
229
+ * each tier's cache folder. The reserved segment names keep them apart.
230
+ *
231
+ * `scratch` is resolved from the injected environment rather than by calling
232
+ * `os.tmpdir()`, which reads the real `process.env` and would make an injected
233
+ * environment stop being the whole story. The Rust twin asks its own platform
234
+ * the same question; the two roots need not match, because parity pins only
235
+ * what sits below a root.
236
+ *
237
+ * Throws {@link TieredDirsError}. Its `code` is `MISSING_BASE_DIR` when a
238
+ * root falls back to the home directory and none can be found,
239
+ * `RELATIVE_OVERRIDE` when an override variable is not absolute,
240
+ * `INVALID_CONVENTION` when {@link EnvVars.convention} names no convention,
241
+ * and `OVERRIDE_REQUIRED` as the second test rule below says. Headless CI and
242
+ * sandboxes do not always have a home directory.
243
+ *
244
+ * Two test rules run first, and only when neither `env` nor `home` is
245
+ * injected (an injected environment is the whole story):
246
+ *
247
+ * - In a test runner process with the root variable unset and `HOME` not
248
+ * moved, the root reads as {@link testRoot}:
249
+ * `<tmp>/tiered-dirs-test-<pid>-<start ms>`, unique per process and
250
+ * cached per module instance (per test file under Vitest's default isolation).
251
+ * `opts.testRoot: false` turns this rule off; the next one still applies.
252
+ * A test runner process has a test file as its entry point and runs
253
+ * under `NODE_ENV=test`, `NODE_TEST_CONTEXT` (set by `node --test`), Bun, or
254
+ * a Vitest worker. Bun counts on its own because `bun test` keeps a
255
+ * `NODE_ENV` that is already set. A Vitest worker has `VITEST_WORKER_ID` or
256
+ * `VITEST_POOL_ID` set, and its entry point is a file in the `dist/workers`
257
+ * folder of the `vitest` package (Vitest 4 and later) or tinypool's
258
+ * `dist/entry/process.js` or `dist/entry/worker.js` (Vitest 3 and earlier),
259
+ * not the test file, so a worker counts by that entry point instead of the
260
+ * test-file check. A child a test spawns
261
+ * inherits the Vitest variables but has a different entry point, so it gets
262
+ * no default. A test that points `HOME` at a fixture, in-process or for a child,
263
+ * keeps it.
264
+ * - With {@link EnvVars.requireOverride} set, a root that would fall back to
265
+ * the per-OS table throws a {@link TieredDirsError} naming the fix.
266
+ *
267
+ * The Rust twin's `BaseDirs::from_env` applies the same two rules, with two
268
+ * differences. It detects a test by its cargo test binary rather than by the
269
+ * environment. And the "`HOME` not moved" condition exists only here: a
270
+ * TypeScript test can point `HOME` at a fixture to redirect the home, which a
271
+ * Rust test cannot do through `dirs`, so the TypeScript rule yields to it.
272
+ * The cost is that a CI job whose `HOME` differs from the passwd home, such as
273
+ * a GitHub Actions container job (`HOME=/github/home`, passwd home `/root`),
274
+ * gets no test-root default. Set `<P>_ROOT` there.
275
+ *
276
+ * Stable in 0.x.
277
+ */
278
+ export declare function systemBaseDirs(vars: EnvVars, opts?: ResolveOptions): BaseDirs;
279
+ /**
280
+ * `<tmp>/tiered-dirs-test-<pid>-<start ms>` when this process is a test runner
281
+ * process, else `undefined`. Resolved once per module instance: once per test
282
+ * file under Vitest's default isolation, and once per installed copy of the
283
+ * package.
284
+ *
285
+ * A test runner process is the one {@link systemBaseDirs} describes: a test
286
+ * file as its entry point under `NODE_ENV=test`, `NODE_TEST_CONTEXT` or Bun,
287
+ * or a Vitest worker. A child a test spawns is not one.
288
+ *
289
+ * The start time keeps a later process that reuses the pid off this one's
290
+ * leftover state. Callers that read the environment themselves, such as a
291
+ * product-tier layer, use this to apply the same default. The Rust twin's
292
+ * `test_root()` is the same per-process root, detected by its cargo test
293
+ * binary.
294
+ *
295
+ * Stable in 0.x.
296
+ */
297
+ export declare function testRoot(): string | undefined;
298
+ /** Whether this process is a test runner running a test file (see {@link systemBaseDirs}). */
299
+ export declare function isTestRunnerProcess(env: Record<string, string | undefined>, entry?: string): boolean;
300
+ /**
301
+ * The XDG layout under `home`, with the `XDG_*` variables read from `env`: the
302
+ * `xdg` table, on any host. Each home is its `XDG_*` variable when that holds
303
+ * an absolute path, else its default under `home`. Logs share the state home.
304
+ * Scratch is `$TMPDIR`, else `/tmp`.
305
+ *
306
+ * The Rust twin's `BaseDirs::xdg` builds the same layout, held to it by the
307
+ * parity fixture's `convention_cases`.
308
+ *
309
+ * Stable in 0.x.
310
+ */
311
+ export declare function xdgBaseDirs(home: string, env: Record<string, string | undefined>): BaseDirs;