@hydraharness/harness-atomic-write 0.1.1-rc.10
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/LICENSE +21 -0
- package/README.md +46 -0
- package/lib/index.js +149 -0
- package/lib/invariant.js +23 -0
- package/lib/types/index.d.ts +75 -0
- package/lib/types/invariant.d.ts +16 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @hydraharness/harness-atomic-write
|
|
2
|
+
|
|
3
|
+
Zero-dependency atomic file replacement shared by file-backed stores that must never leave partial, symlink-hijacked, or wider-than-intended content on disk — the user-settings document (`@hydraharness/harness-settings-file`) and the credentials store (`@hydraharness/harness-credentials-local`).
|
|
4
|
+
|
|
5
|
+
## Surface
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { withFileLock, writeFileAtomic } from '@hydraharness/harness-atomic-write'
|
|
9
|
+
|
|
10
|
+
declare const text: string
|
|
11
|
+
declare const render: (previous: string) => string
|
|
12
|
+
|
|
13
|
+
await writeFileAtomic('/home/u/.hydra/settings.yaml', text, { mode: 0o600 })
|
|
14
|
+
|
|
15
|
+
// Read-modify-write against the same file from several processes.
|
|
16
|
+
await withFileLock('/home/u/.hydra/settings.yaml', async () => {
|
|
17
|
+
await writeFileAtomic('/home/u/.hydra/settings.yaml', render(text), { mode: 0o600 })
|
|
18
|
+
})
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`writeFileAtomic` commits one already-rendered string. The contract, in the order failures would exploit it:
|
|
22
|
+
|
|
23
|
+
- **Exclusive-create temp** (`wx`, random suffix): the open refuses to follow a symlink planted at a guessable temp path.
|
|
24
|
+
- **The fresh inode carries `mode` through the rename**: replacing a wider-permission file narrows it without a chmod race. `mode` is required so the permission decision stays visible at every call site (subject to the process umask, like every fresh inode).
|
|
25
|
+
- **`rename` replaces a symlinked target itself**, never writing through to its referent.
|
|
26
|
+
- **Windows replacement retries transient `EPERM`** for a bounded interval, covering unrelated readers that are closing the target; other failures remain immediate.
|
|
27
|
+
- **Same-directory sibling** keeps the rename on one filesystem, so the swap stays atomic.
|
|
28
|
+
- Parent directories are created; on any failure the temp is removed and the failure rethrown; readers observe either the old or the new complete content.
|
|
29
|
+
|
|
30
|
+
`withFileLock` serializes the writers of one file across processes, for the read-render-commit cycles a bare atomic commit cannot make safe on its own. The lock is a `wx`-created `<filename>.lock` sibling, so readers never contend; waiters back off exponentially and fail with a timeout rather than block forever. `EEXIST` identifies contention directly; `EPERM` does so when a fresh `lstat` confirms that the lock path exists, and one unconfirmed Windows `EPERM` is retried because the holder may have released the lock before that check. A contender never removes the existing lock: age cannot distinguish a crashed owner from a paused live writer.
|
|
31
|
+
|
|
32
|
+
How long a contender waits is a property of the operation the holder runs, so it is stated per call through `waitMs`. The default is sized for file work alone; a holder whose cycle includes a network round trip — a credential mutation that refreshes an expired token — states a longer one, because leaving the default would fail every other writer of that file for the duration. The retry cadence stays fixed: it governs how often a contender asks, which no caller has a reason to vary.
|
|
33
|
+
|
|
34
|
+
## Model Experience
|
|
35
|
+
|
|
36
|
+
None, as this is a pure filesystem primitive; nothing here reaches a model request.
|
|
37
|
+
|
|
38
|
+
#### KV Cache effect
|
|
39
|
+
|
|
40
|
+
None; nothing here enters a request prefix.
|
|
41
|
+
|
|
42
|
+
## Known Limitations and Deferred Work
|
|
43
|
+
|
|
44
|
+
- **Atomic, not durable** — no `fsync` of the file or its directory, so after a crash the rename may be observed unwound. The file-backed stores here re-read and republish on boot, keeping durability the caller's policy.
|
|
45
|
+
- **String content only** — no `Buffer` or stream form until a consumer needs one.
|
|
46
|
+
- **Orphaned locks require operator recovery** — a process that exits while holding the lock can leave the sibling behind. Later writers time out without deleting it; an operator removes it only after verifying that no writer still owns it. File age alone is not safe evidence of abandonment.
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { lstat, mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
//#region lib/types/index.js
|
|
5
|
+
/**
|
|
6
|
+
* Zero-dependency atomic file replacement and writer coordination.
|
|
7
|
+
* `writeFileAtomic` writes a random-suffix sibling with exclusive create and
|
|
8
|
+
* the caller's permission bits, then renames it over the target, so readers
|
|
9
|
+
* observe either the old or the new complete content and a replaced file ends
|
|
10
|
+
* up with exactly the stated mode. `withFileLock` serializes cross-process
|
|
11
|
+
* writers of one file through a `wx`-created `<file>.lock` sibling, so a
|
|
12
|
+
* read-modify-write cycle can never resurrect a state another writer just
|
|
13
|
+
* replaced; readers stay lock-free because the rename commit is atomic.
|
|
14
|
+
* @module @hydraharness/harness-atomic-write
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Replace `filename` with `content` in one atomic step, creating parent
|
|
18
|
+
* directories. The content is first written to a random-suffix sibling opened
|
|
19
|
+
* with exclusive create (`wx`): the open refuses to follow a symlink planted
|
|
20
|
+
* at the temp path, and the fresh inode carries `options.mode` through the
|
|
21
|
+
* rename, so replacing a wider-permission file narrows it without a chmod
|
|
22
|
+
* race. The rename also replaces a symlinked target itself instead of writing
|
|
23
|
+
* through to its referent, and the same-directory sibling keeps the rename on
|
|
24
|
+
* one filesystem. On any failure the temp file is removed and the failure
|
|
25
|
+
* rethrown. Crash durability (fsync) is out of scope.
|
|
26
|
+
* @param filename - final path receiving the content.
|
|
27
|
+
* @param content - complete next file content.
|
|
28
|
+
* @param options - permission bits for the replacement inode.
|
|
29
|
+
*/
|
|
30
|
+
async function writeFileAtomic(filename, content, options) {
|
|
31
|
+
await mkdir(dirname(filename), {
|
|
32
|
+
recursive: true,
|
|
33
|
+
...options.dirMode === void 0 ? {} : { mode: options.dirMode }
|
|
34
|
+
});
|
|
35
|
+
const temp = `${filename}.${randomBytes(6).toString("hex")}.tmp`;
|
|
36
|
+
try {
|
|
37
|
+
await writeFile(temp, content, {
|
|
38
|
+
mode: options.mode,
|
|
39
|
+
flag: "wx"
|
|
40
|
+
});
|
|
41
|
+
await renameWithRetry(temp, filename);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
await rm(temp, { force: true });
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Whether an exclusive create found an existing lock. */
|
|
48
|
+
async function isLockContention(error, lockPath) {
|
|
49
|
+
const code = error?.code;
|
|
50
|
+
if (code === "EEXIST") return true;
|
|
51
|
+
if (code !== "EPERM") return false;
|
|
52
|
+
try {
|
|
53
|
+
await lstat(lockPath);
|
|
54
|
+
return true;
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Retry cadence for a contended lock. These stay robustness invariants of the
|
|
61
|
+
* cross-process write protocol rather than deployment tunables: they govern how
|
|
62
|
+
* often a contender asks, which no caller has a reason to vary.
|
|
63
|
+
*/
|
|
64
|
+
const LOCK_RETRY_INITIAL_MS = 20;
|
|
65
|
+
const LOCK_RETRY_MAX_MS = 200;
|
|
66
|
+
/**
|
|
67
|
+
* Windows can briefly deny replacing a file while an unrelated reader closes
|
|
68
|
+
* its handle. Keep the retry local to the publish step; the writer lock still
|
|
69
|
+
* owns application-level coordination.
|
|
70
|
+
*/
|
|
71
|
+
const RENAME_RETRY_INITIAL_MS = 20;
|
|
72
|
+
const RENAME_RETRY_MAX_MS = 200;
|
|
73
|
+
const RENAME_RETRY_WAIT_MS = 2e3;
|
|
74
|
+
/** Replace a file, absorbing only a transient Windows replacement denial. */
|
|
75
|
+
async function renameWithRetry(source, target) {
|
|
76
|
+
const retryable = process.platform === "win32";
|
|
77
|
+
const deadline = Date.now() + RENAME_RETRY_WAIT_MS;
|
|
78
|
+
let delay = RENAME_RETRY_INITIAL_MS;
|
|
79
|
+
for (;;) {
|
|
80
|
+
try {
|
|
81
|
+
await rename(source, target);
|
|
82
|
+
return;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
const code = error?.code;
|
|
85
|
+
if (!retryable || code !== "EPERM" || Date.now() >= deadline) throw error;
|
|
86
|
+
}
|
|
87
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
88
|
+
delay = Math.min(delay * 2, RENAME_RETRY_MAX_MS);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* How long a contender waits when the caller states no limit — sized for the
|
|
93
|
+
* render-and-rename cycle every call site had when this package was written.
|
|
94
|
+
* Expiry fails the contender rather than guessing whether the existing lock
|
|
95
|
+
* still has an owner. How long is *worth* waiting is a property of the
|
|
96
|
+
* operation the lock holder runs, which is why {@link FileLockOptions.waitMs}
|
|
97
|
+
* exists; the value here is the floor for an operation that does file work
|
|
98
|
+
* alone.
|
|
99
|
+
*/
|
|
100
|
+
const DEFAULT_LOCK_WAIT_MS = 2e3;
|
|
101
|
+
/**
|
|
102
|
+
* Hold the cross-process writer lock for `filename` around one operation. The
|
|
103
|
+
* lock is a `wx`-created sibling (`<filename>.lock`); paired with the
|
|
104
|
+
* rename-based commit of {@link writeFileAtomic}, readers stay lock-free and
|
|
105
|
+
* only writers contend. `EEXIST` is contention directly; an `EPERM` is
|
|
106
|
+
* contention only when a fresh `lstat` confirms the lock path exists, covering
|
|
107
|
+
* Windows exclusive-create behavior without hiding an unrelated permission
|
|
108
|
+
* failure. One unconfirmed Windows `EPERM` gets one retry because a contender
|
|
109
|
+
* can observe the lock after its owner has already released it. Contention
|
|
110
|
+
* backs off exponentially and fails with a timed-out error after the deadline.
|
|
111
|
+
* The contender never removes an existing lock because file age cannot prove
|
|
112
|
+
* that its owner stopped; orphan recovery is an operator action. The parent
|
|
113
|
+
* directory must exist.
|
|
114
|
+
* @param filename - the file whose writers this lock serializes.
|
|
115
|
+
* @param operation - the read-render-commit cycle to run while holding the lock.
|
|
116
|
+
* @param options - acquisition options; omitted waits {@link DEFAULT_LOCK_WAIT_MS}.
|
|
117
|
+
* @returns the operation's result; the lock releases on both outcomes.
|
|
118
|
+
*/
|
|
119
|
+
async function withFileLock(filename, operation, options) {
|
|
120
|
+
const lockPath = `${filename}.lock`;
|
|
121
|
+
const deadline = Date.now() + (options?.waitMs ?? DEFAULT_LOCK_WAIT_MS);
|
|
122
|
+
let delay = LOCK_RETRY_INITIAL_MS;
|
|
123
|
+
let unconfirmedWindowsEpermRetried = false;
|
|
124
|
+
for (;;) {
|
|
125
|
+
try {
|
|
126
|
+
await writeFile(lockPath, `${process.pid}\n`, {
|
|
127
|
+
mode: 384,
|
|
128
|
+
flag: "wx"
|
|
129
|
+
});
|
|
130
|
+
break;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (!await isLockContention(error, lockPath)) {
|
|
133
|
+
const code = error?.code;
|
|
134
|
+
if (process.platform !== "win32" || code !== "EPERM" || unconfirmedWindowsEpermRetried) throw error;
|
|
135
|
+
unconfirmedWindowsEpermRetried = true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (Date.now() >= deadline) throw new Error(`atomic-write: timed out waiting for the writer lock at ${lockPath}`);
|
|
139
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
140
|
+
delay = Math.min(delay * 2, LOCK_RETRY_MAX_MS);
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
return await operation();
|
|
144
|
+
} finally {
|
|
145
|
+
await rm(lockPath, { force: true });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
//#endregion
|
|
149
|
+
export { withFileLock, writeFileAtomic };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@hydraharness/harness-atomic-write`.
|
|
4
|
+
* @module @hydraharness/harness-atomic-write/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@hydraharness/harness-atomic-write";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "atomic-write-invariant";
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/**
|
|
12
|
+
* No runtime invariant: this pure filesystem primitive owns no event stream or mutable runtime
|
|
13
|
+
* data; its replacement contract is enforced by unit tests.
|
|
14
|
+
*/
|
|
15
|
+
const install = () => {};
|
|
16
|
+
/**
|
|
17
|
+
* Register this package's invariant companion.
|
|
18
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
19
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
20
|
+
*/
|
|
21
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
22
|
+
//#endregion
|
|
23
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-dependency atomic file replacement and writer coordination.
|
|
3
|
+
* `writeFileAtomic` writes a random-suffix sibling with exclusive create and
|
|
4
|
+
* the caller's permission bits, then renames it over the target, so readers
|
|
5
|
+
* observe either the old or the new complete content and a replaced file ends
|
|
6
|
+
* up with exactly the stated mode. `withFileLock` serializes cross-process
|
|
7
|
+
* writers of one file through a `wx`-created `<file>.lock` sibling, so a
|
|
8
|
+
* read-modify-write cycle can never resurrect a state another writer just
|
|
9
|
+
* replaced; readers stay lock-free because the rename commit is atomic.
|
|
10
|
+
* @module @hydraharness/harness-atomic-write
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Filesystem options for {@link writeFileAtomic}; `mode` is required so the
|
|
14
|
+
* permission decision stays visible at every call site.
|
|
15
|
+
*/
|
|
16
|
+
export interface WriteFileAtomicOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Permission bits stamped on the fresh temp inode and carried through the
|
|
19
|
+
* rename (subject to the process umask, like every fresh inode).
|
|
20
|
+
*/
|
|
21
|
+
mode: number;
|
|
22
|
+
/**
|
|
23
|
+
* Permission bits for parent directories this call creates (subject to the
|
|
24
|
+
* umask; existing directories keep their mode). Omission uses the mkdir
|
|
25
|
+
* default — pass `0o700` when the tree holds user-private data.
|
|
26
|
+
*/
|
|
27
|
+
dirMode?: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Replace `filename` with `content` in one atomic step, creating parent
|
|
31
|
+
* directories. The content is first written to a random-suffix sibling opened
|
|
32
|
+
* with exclusive create (`wx`): the open refuses to follow a symlink planted
|
|
33
|
+
* at the temp path, and the fresh inode carries `options.mode` through the
|
|
34
|
+
* rename, so replacing a wider-permission file narrows it without a chmod
|
|
35
|
+
* race. The rename also replaces a symlinked target itself instead of writing
|
|
36
|
+
* through to its referent, and the same-directory sibling keeps the rename on
|
|
37
|
+
* one filesystem. On any failure the temp file is removed and the failure
|
|
38
|
+
* rethrown. Crash durability (fsync) is out of scope.
|
|
39
|
+
* @param filename - final path receiving the content.
|
|
40
|
+
* @param content - complete next file content.
|
|
41
|
+
* @param options - permission bits for the replacement inode.
|
|
42
|
+
*/
|
|
43
|
+
export declare function writeFileAtomic(filename: string, content: string, options: WriteFileAtomicOptions): Promise<void>;
|
|
44
|
+
/** Options for one {@link withFileLock} acquisition. */
|
|
45
|
+
export interface FileLockOptions {
|
|
46
|
+
/**
|
|
47
|
+
* Maximum time to wait for the lock, in milliseconds. State one when the
|
|
48
|
+
* holder's operation legitimately runs longer than file work — a credential
|
|
49
|
+
* mutation that refreshes a token performs a network round trip while
|
|
50
|
+
* holding the lock, and leaving the default in place would fail every other
|
|
51
|
+
* writer of the same file for the duration. Waiting is productive: a
|
|
52
|
+
* contender that acquires the lock afterwards re-reads the committed state.
|
|
53
|
+
*/
|
|
54
|
+
waitMs?: number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Hold the cross-process writer lock for `filename` around one operation. The
|
|
58
|
+
* lock is a `wx`-created sibling (`<filename>.lock`); paired with the
|
|
59
|
+
* rename-based commit of {@link writeFileAtomic}, readers stay lock-free and
|
|
60
|
+
* only writers contend. `EEXIST` is contention directly; an `EPERM` is
|
|
61
|
+
* contention only when a fresh `lstat` confirms the lock path exists, covering
|
|
62
|
+
* Windows exclusive-create behavior without hiding an unrelated permission
|
|
63
|
+
* failure. One unconfirmed Windows `EPERM` gets one retry because a contender
|
|
64
|
+
* can observe the lock after its owner has already released it. Contention
|
|
65
|
+
* backs off exponentially and fails with a timed-out error after the deadline.
|
|
66
|
+
* The contender never removes an existing lock because file age cannot prove
|
|
67
|
+
* that its owner stopped; orphan recovery is an operator action. The parent
|
|
68
|
+
* directory must exist.
|
|
69
|
+
* @param filename - the file whose writers this lock serializes.
|
|
70
|
+
* @param operation - the read-render-commit cycle to run while holding the lock.
|
|
71
|
+
* @param options - acquisition options; omitted waits {@link DEFAULT_LOCK_WAIT_MS}.
|
|
72
|
+
* @returns the operation's result; the lock releases on both outcomes.
|
|
73
|
+
*/
|
|
74
|
+
export declare function withFileLock<T>(filename: string, operation: () => Promise<T>, options?: FileLockOptions): Promise<T>;
|
|
75
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@hydraharness/harness-atomic-write`.
|
|
3
|
+
* @module @hydraharness/harness-atomic-write/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@hydraharness/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "atomic-write-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hydraharness/harness-atomic-write",
|
|
3
|
+
"description": "Zero-dependency atomic file replacement: exclusive-create random-suffix temp + rename carrying the caller-stated permissions (writeFileAtomic)",
|
|
4
|
+
"version": "0.1.1-rc.10",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/MaiHongPhong2010/Hydra-Harness.git",
|
|
11
|
+
"directory": "packages/util/atomic-write"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./invariant": {
|
|
22
|
+
"types": "./lib/types/invariant.d.ts",
|
|
23
|
+
"default": "./lib/invariant.js"
|
|
24
|
+
},
|
|
25
|
+
"./src/*": "./src/*",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"lib/index.js",
|
|
30
|
+
"lib/invariant.js",
|
|
31
|
+
"lib/types/**/*.d.ts"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@hydraharness/harness-invariants": "^0.1.1-rc.10",
|
|
36
|
+
"@hydraharness/cordis": "^4.0.2"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@hydraharness/harness-invariants": "^0.1.1-rc.10",
|
|
40
|
+
"@hydraharness/cordis": "^4.0.2"
|
|
41
|
+
}
|
|
42
|
+
}
|