@executablemd/runtime 0.10.2 → 0.11.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/esm/config.js +27 -0
- package/esm/files.js +5 -0
- package/esm/host-files.js +100 -1
- package/esm/mod.js +3 -2
- package/package.json +1 -1
- package/types/config.d.ts +20 -0
- package/types/files.d.ts +15 -1
- package/types/host-files.d.ts +1 -1
- package/types/mod.d.ts +3 -2
package/esm/config.js
CHANGED
|
@@ -22,12 +22,21 @@
|
|
|
22
22
|
* Installing at `min` is what lets a nested override win: a block's own
|
|
23
23
|
* `timeout=` outranks the value the command line established for the run.
|
|
24
24
|
* Omitting a field inherits the enclosing value rather than clearing it.
|
|
25
|
+
*
|
|
26
|
+
* `verbose` is the fourth field and is not a timeout. It says whether the
|
|
27
|
+
* scope reading it renders verbose-only content, it is `false` until something
|
|
28
|
+
* says otherwise, and it is installed and overridden exactly the way a timeout
|
|
29
|
+
* is. It bounds nothing, opens nothing and decides nothing about authority: a
|
|
30
|
+
* component reads it to choose between rendering its content and rendering
|
|
31
|
+
* nothing, and the host's own presentation — the journal, the event echo, the
|
|
32
|
+
* testing report — is decided by the command line rather than by this field.
|
|
25
33
|
*/
|
|
26
34
|
import { createApi } from "@effectionx/context-api";
|
|
27
35
|
export const Config = createApi("Config", {
|
|
28
36
|
timeout: undefined,
|
|
29
37
|
timeoutExec: undefined,
|
|
30
38
|
timeoutFetch: undefined,
|
|
39
|
+
verbose: false,
|
|
31
40
|
});
|
|
32
41
|
/**
|
|
33
42
|
* A configured duration is milliseconds or nothing. Anything else fails here,
|
|
@@ -56,3 +65,21 @@ export const timeout = validated("timeout", Config.operations.timeout);
|
|
|
56
65
|
export const timeoutExec = validated("timeoutExec", Config.operations.timeoutExec);
|
|
57
66
|
/** The validated default timeout for a Fetch. */
|
|
58
67
|
export const timeoutFetch = validated("timeoutFetch", Config.operations.timeoutFetch);
|
|
68
|
+
/**
|
|
69
|
+
* The validated verbosity of the reading scope.
|
|
70
|
+
*
|
|
71
|
+
* There is no "absent" verbosity the way there is an absent timeout: a scope
|
|
72
|
+
* either renders verbose-only content or it does not. Anything that is not a
|
|
73
|
+
* boolean — what an untyped JavaScript consumer can still install — fails
|
|
74
|
+
* here, where the reader asked, rather than being read as truthiness by
|
|
75
|
+
* whichever component asked first.
|
|
76
|
+
*/
|
|
77
|
+
export const verbose = {
|
|
78
|
+
*[Symbol.iterator]() {
|
|
79
|
+
const configured = yield* Config.operations.verbose;
|
|
80
|
+
if (typeof configured !== "boolean") {
|
|
81
|
+
throw new Error(`Config verbose must be a boolean, got ${String(configured)}`);
|
|
82
|
+
}
|
|
83
|
+
return configured;
|
|
84
|
+
},
|
|
85
|
+
};
|
package/esm/files.js
CHANGED
|
@@ -83,6 +83,7 @@ const OPERATIONS = [
|
|
|
83
83
|
"check-file-path",
|
|
84
84
|
"read",
|
|
85
85
|
"delete",
|
|
86
|
+
"ensure-directory",
|
|
86
87
|
"glob",
|
|
87
88
|
"temporary-directory",
|
|
88
89
|
];
|
|
@@ -556,6 +557,10 @@ export const Files = createApi("executablemd.runtime.files", {
|
|
|
556
557
|
throw new FilesProviderUnavailableError();
|
|
557
558
|
},
|
|
558
559
|
// deno-lint-ignore require-yield
|
|
560
|
+
*ensureDirectory(_input) {
|
|
561
|
+
throw new FilesProviderUnavailableError();
|
|
562
|
+
},
|
|
563
|
+
// deno-lint-ignore require-yield
|
|
559
564
|
*globFiles(_input) {
|
|
560
565
|
throw new FilesProviderUnavailableError();
|
|
561
566
|
},
|
package/esm/host-files.js
CHANGED
|
@@ -238,6 +238,43 @@ function* removalDestination(input) {
|
|
|
238
238
|
return { reason: reasonOf(error) };
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* The directory a `<Dir>` names, which is the one target that may be absolute.
|
|
243
|
+
*
|
|
244
|
+
* Every other operation here refuses an absolute path outright, because a
|
|
245
|
+
* document that writes one is naming a place outside the work it was given.
|
|
246
|
+
* `<Dir>` is the established exception: an absolute `path` has always been used
|
|
247
|
+
* as written, and this operation exists to serve that component. So an absolute
|
|
248
|
+
* target is taken as it stands and is not measured against the working
|
|
249
|
+
* directory — there is no base it was ever relative to.
|
|
250
|
+
*
|
|
251
|
+
* A relative target keeps the ordinary rules: resolved against `cwd`, with both
|
|
252
|
+
* sides canonical, so a working directory reached through a symlink is not read
|
|
253
|
+
* as an escape and a `..` that genuinely leaves still is.
|
|
254
|
+
*
|
|
255
|
+
* The final segment is resolved along with the rest. A directory that already
|
|
256
|
+
* exists behind a symlink is the directory it points at, and entering it is
|
|
257
|
+
* what the document asked for.
|
|
258
|
+
*/
|
|
259
|
+
function* directoryDestination(input) {
|
|
260
|
+
try {
|
|
261
|
+
if (isAbsolute(input.path)) {
|
|
262
|
+
return { path: yield* resolveExisting(input.path) };
|
|
263
|
+
}
|
|
264
|
+
if (!within(input.cwd, resolve(input.cwd, input.path))) {
|
|
265
|
+
return { reason: "lexical-escape" };
|
|
266
|
+
}
|
|
267
|
+
const base = (yield* API.Fs.operations.realpath(input.cwd)) ?? input.cwd;
|
|
268
|
+
const path = yield* resolveExisting(resolve(input.cwd, input.path));
|
|
269
|
+
if (!within(base, path)) {
|
|
270
|
+
return { reason: "resolved-escape" };
|
|
271
|
+
}
|
|
272
|
+
return { path };
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
return { reason: reasonOf(error) };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
241
278
|
function nonWriteFailure(operation, phase, reason) {
|
|
242
279
|
return Err(filesFailure({ operation, phase, reason }));
|
|
243
280
|
}
|
|
@@ -472,6 +509,57 @@ export function hostFilesHandler(options = {}) {
|
|
|
472
509
|
}
|
|
473
510
|
return Ok(undefined);
|
|
474
511
|
}
|
|
512
|
+
/**
|
|
513
|
+
* Make the named path a directory, creating what is missing.
|
|
514
|
+
*
|
|
515
|
+
* Three answers, and the order between them is the contract. An existing
|
|
516
|
+
* directory is success without touching it: nothing is replaced, cleared or
|
|
517
|
+
* written, because the document asked for the directory to exist and it does.
|
|
518
|
+
* An existing entry that is not a directory is a refusal — a file where a
|
|
519
|
+
* directory was asked for is a mistake to report, never a thing to remove.
|
|
520
|
+
* Anything else is created, recursively, along with every missing parent.
|
|
521
|
+
*
|
|
522
|
+
* The target is classified before creation is attempted so the refusal for a
|
|
523
|
+
* non-directory target is decided here rather than left to whatever the
|
|
524
|
+
* platform's `mkdir -p` happens to say. An intermediate non-directory is the
|
|
525
|
+
* platform's to report, and `ENOTDIR` already carries it into the shared
|
|
526
|
+
* vocabulary — so both refusals arrive as `not-directory` and neither carries
|
|
527
|
+
* a host path or a platform message.
|
|
528
|
+
*
|
|
529
|
+
* Creation is direct and persists. There is no rollback and no teardown
|
|
530
|
+
* removal: a later failure of the content that runs inside this directory
|
|
531
|
+
* says nothing about whether the directory should exist.
|
|
532
|
+
*/
|
|
533
|
+
function* ensureDirectory(input) {
|
|
534
|
+
if (input.path.length === 0) {
|
|
535
|
+
return nonWriteFailure("ensure-directory", "lexical", "empty-path");
|
|
536
|
+
}
|
|
537
|
+
const target = yield* directoryDestination(input);
|
|
538
|
+
if ("reason" in target) {
|
|
539
|
+
return nonWriteFailure("ensure-directory", "resolution", target.reason);
|
|
540
|
+
}
|
|
541
|
+
notify(observe, { operation: "ensure-directory", phase: "target" });
|
|
542
|
+
try {
|
|
543
|
+
const info = yield* API.Fs.operations.stat(target.path);
|
|
544
|
+
if (info.exists && !info.isDirectory) {
|
|
545
|
+
return nonWriteFailure("ensure-directory", "target", "not-directory");
|
|
546
|
+
}
|
|
547
|
+
if (info.exists) {
|
|
548
|
+
return Ok(undefined);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
catch (error) {
|
|
552
|
+
return nonWriteFailure("ensure-directory", "target", reasonOf(error));
|
|
553
|
+
}
|
|
554
|
+
notify(observe, { operation: "ensure-directory", phase: "access" });
|
|
555
|
+
try {
|
|
556
|
+
yield* API.Fs.operations.ensureDir(target.path);
|
|
557
|
+
}
|
|
558
|
+
catch (error) {
|
|
559
|
+
return nonWriteFailure("ensure-directory", "access", reasonOf(error));
|
|
560
|
+
}
|
|
561
|
+
return Ok(undefined);
|
|
562
|
+
}
|
|
475
563
|
/**
|
|
476
564
|
* The regular files under `cwd` that `include` selects and `exclude` does not.
|
|
477
565
|
*
|
|
@@ -550,7 +638,15 @@ export function hostFilesHandler(options = {}) {
|
|
|
550
638
|
yield* provide(Ok(canonical));
|
|
551
639
|
});
|
|
552
640
|
}
|
|
553
|
-
return {
|
|
641
|
+
return {
|
|
642
|
+
checkFilePath,
|
|
643
|
+
readTextFile,
|
|
644
|
+
writeTextFile,
|
|
645
|
+
deleteFile,
|
|
646
|
+
ensureDirectory,
|
|
647
|
+
globFiles,
|
|
648
|
+
temporaryDirectory,
|
|
649
|
+
};
|
|
554
650
|
}
|
|
555
651
|
/**
|
|
556
652
|
* Remove a temporary directory as its scope ends.
|
|
@@ -612,6 +708,9 @@ export function useHostFiles(options = {}) {
|
|
|
612
708
|
*deleteFile([input]) {
|
|
613
709
|
return yield* handler.deleteFile(input);
|
|
614
710
|
},
|
|
711
|
+
*ensureDirectory([input]) {
|
|
712
|
+
return yield* handler.ensureDirectory(input);
|
|
713
|
+
},
|
|
615
714
|
*globFiles([input]) {
|
|
616
715
|
return yield* handler.globFiles(input);
|
|
617
716
|
},
|
package/esm/mod.js
CHANGED
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
* - `API.Service` — scoped attached service startup (`startService`)
|
|
20
20
|
* - `NativeLauncher` — handing one native agent UI the foreground terminal
|
|
21
21
|
* (`reserveTerminal`, `flushOutput`, `nativeLaunch`)
|
|
22
|
-
* - `Config` — shared execution config (`timeout`, `timeoutExec`, `timeoutFetch
|
|
22
|
+
* - `Config` — shared execution config (`timeout`, `timeoutExec`, `timeoutFetch`,
|
|
23
|
+
* `verbose`)
|
|
23
24
|
*
|
|
24
25
|
* See `apis.ts` for architecture rationale.
|
|
25
26
|
* See `@executablemd/runtime/test` for composable test stubs.
|
|
@@ -28,7 +29,7 @@ import "./_dnt.polyfills.js";
|
|
|
28
29
|
export { API } from "./apis.js";
|
|
29
30
|
export { exec, readTextFile, writeTextFile, stat, lstat, readDirectory, glob, realpath, ensureDir, rename, remove, fetch, cwd, env, platform, command, compile, useQuietProcessOutput, } from "./apis.js";
|
|
30
31
|
export { Service, SERVICE_HOSTNAME, SERVICE_READY_PREFIX, ServiceProcessExitBeforeReadyError, ServiceProtocolDuplicateError, ServiceProtocolHostnameMismatchError, ServiceProtocolIncompatibleError, ServiceProtocolMalformedError, ServiceProtocolTokenMismatchError, ServiceProviderError, ServiceStartupTimeoutError, ServiceTeardownError, ServiceUnexpectedExitError, parseServiceReadyRecord, startService, } from "./service.js";
|
|
31
|
-
export { Config, timeout, timeoutExec, timeoutFetch } from "./config.js";
|
|
32
|
+
export { Config, timeout, timeoutExec, timeoutFetch, verbose } from "./config.js";
|
|
32
33
|
export { asDuration, durationError, parseDuration } from "./duration.js";
|
|
33
34
|
export { asFilesFatal, FILES_ERROR, FILES_ERROR_MESSAGE, FILES_FATAL, FILES_INVARIANT_MESSAGE, FILES_OPERATION_DENIED_MESSAGE, FILES_PROVIDER_UNAVAILABLE_MESSAGE, FILES_WRITE_SUCCESS, Files, FilesError, FilesInvariantError, FilesOperationDeniedError, FilesProviderUnavailableError, fileWriteFailure, fileWriteSuccess, filesFailure, isFilesFatal, parseFilesPhase, parseFilesReason, parseFileWriteFailure, parseFileWritePhase, parseFileWriteSuccess, parseFilesFailure, parseFilesFatal, } from "./files.js";
|
|
34
35
|
export { flushOutput, installControlledLauncher, installForegroundLauncher, NATIVE_LAUNCHER_UNAVAILABLE, NativeLauncher, NativeLauncherUnavailableError, nativeLaunch, NO_TERMINAL, reserveTerminal, } from "./launcher.js";
|
package/package.json
CHANGED
package/types/config.d.ts
CHANGED
|
@@ -22,6 +22,14 @@
|
|
|
22
22
|
* Installing at `min` is what lets a nested override win: a block's own
|
|
23
23
|
* `timeout=` outranks the value the command line established for the run.
|
|
24
24
|
* Omitting a field inherits the enclosing value rather than clearing it.
|
|
25
|
+
*
|
|
26
|
+
* `verbose` is the fourth field and is not a timeout. It says whether the
|
|
27
|
+
* scope reading it renders verbose-only content, it is `false` until something
|
|
28
|
+
* says otherwise, and it is installed and overridden exactly the way a timeout
|
|
29
|
+
* is. It bounds nothing, opens nothing and decides nothing about authority: a
|
|
30
|
+
* component reads it to choose between rendering its content and rendering
|
|
31
|
+
* nothing, and the host's own presentation — the journal, the event echo, the
|
|
32
|
+
* testing report — is decided by the command line rather than by this field.
|
|
25
33
|
*/
|
|
26
34
|
import { type Api } from "@effectionx/context-api";
|
|
27
35
|
import type { Operation } from "effection";
|
|
@@ -32,6 +40,8 @@ export interface ConfigApi {
|
|
|
32
40
|
timeoutExec: number | undefined;
|
|
33
41
|
/** Default timeout for each Fetch, in milliseconds; undefined for none. */
|
|
34
42
|
timeoutFetch: number | undefined;
|
|
43
|
+
/** Whether this scope renders verbose-only content; false for none. */
|
|
44
|
+
verbose: boolean;
|
|
35
45
|
}
|
|
36
46
|
export declare const Config: Api<ConfigApi>;
|
|
37
47
|
/** The validated run deadline. Read by the run boundary and nothing else. */
|
|
@@ -40,3 +50,13 @@ export declare const timeout: Operation<number | undefined>;
|
|
|
40
50
|
export declare const timeoutExec: Operation<number | undefined>;
|
|
41
51
|
/** The validated default timeout for a Fetch. */
|
|
42
52
|
export declare const timeoutFetch: Operation<number | undefined>;
|
|
53
|
+
/**
|
|
54
|
+
* The validated verbosity of the reading scope.
|
|
55
|
+
*
|
|
56
|
+
* There is no "absent" verbosity the way there is an absent timeout: a scope
|
|
57
|
+
* either renders verbose-only content or it does not. Anything that is not a
|
|
58
|
+
* boolean — what an untyped JavaScript consumer can still install — fails
|
|
59
|
+
* here, where the reader asked, rather than being read as truthiness by
|
|
60
|
+
* whichever component asked first.
|
|
61
|
+
*/
|
|
62
|
+
export declare const verbose: Operation<boolean>;
|
package/types/files.d.ts
CHANGED
|
@@ -68,7 +68,7 @@ export declare const FILES_WRITE_SUCCESS = "executablemd.runtime.files-write-suc
|
|
|
68
68
|
*/
|
|
69
69
|
export type FilesReason = "empty-path" | "absolute-path" | "lexical-escape" | "resolved-escape" | "missing" | "directory" | "special-file" | "not-directory" | "permission-denied" | "read-only" | "too-many-symlinks" | "path-too-long" | "no-space" | "quota-exhausted" | "cross-device" | "busy" | "too-many-open-files" | "directory-not-empty" | "invalid-pattern" | "operation-failed";
|
|
70
70
|
/** The operations whose failure carries no commit outcome. */
|
|
71
|
-
export type FilesOperation = "check-file-path" | "read" | "delete" | "glob" | "temporary-directory";
|
|
71
|
+
export type FilesOperation = "check-file-path" | "read" | "delete" | "ensure-directory" | "glob" | "temporary-directory";
|
|
72
72
|
/** Where a non-write operation stopped. */
|
|
73
73
|
export type FilesPhase = "lexical" | "resolution" | "target" | "access" | "pattern" | "traversal" | "acquire";
|
|
74
74
|
/** Where a write stopped, which is what decides what may be said about the target. */
|
|
@@ -145,6 +145,20 @@ export interface FilesHandler {
|
|
|
145
145
|
* that same success.
|
|
146
146
|
*/
|
|
147
147
|
deleteFile(input: FilePathInput): Operation<Result<void>>;
|
|
148
|
+
/**
|
|
149
|
+
* Make this path name a directory, and answer with nothing.
|
|
150
|
+
*
|
|
151
|
+
* Recursively creates the target and any missing parent. An existing
|
|
152
|
+
* directory is already the answer, so it succeeds without replacing it,
|
|
153
|
+
* clearing it or touching what is in it; a file or another non-directory, at
|
|
154
|
+
* the target or anywhere on the way to it, is a refusal.
|
|
155
|
+
*
|
|
156
|
+
* Mandatory like the rest, and Unit for the same reason `deleteFile` is: a
|
|
157
|
+
* document that asked for a directory to exist has been answered by its
|
|
158
|
+
* existence. Nothing comes back to branch on — no path, no handle, and no
|
|
159
|
+
* word on whether this call is what created it.
|
|
160
|
+
*/
|
|
161
|
+
ensureDirectory(input: FilePathInput): Operation<Result<void>>;
|
|
148
162
|
/** Sorted, deduplicated, POSIX-separated paths of the regular files that match. */
|
|
149
163
|
globFiles(input: GlobInput): Operation<Result<string[]>>;
|
|
150
164
|
/**
|
package/types/host-files.d.ts
CHANGED
|
@@ -74,7 +74,7 @@ import type { FilesHandler } from "./files.js";
|
|
|
74
74
|
* observable rather than merely stated.
|
|
75
75
|
*/
|
|
76
76
|
export interface HostFilesEvent {
|
|
77
|
-
readonly operation: "read" | "write" | "delete" | "glob";
|
|
77
|
+
readonly operation: "read" | "write" | "delete" | "ensure-directory" | "glob";
|
|
78
78
|
readonly phase: "target" | "access" | "parents" | "temporary" | "commit" | "cleanup" | "read-dir";
|
|
79
79
|
}
|
|
80
80
|
/** Synchronous, so nothing can run between the observation and the call it precedes. */
|
package/types/mod.d.ts
CHANGED
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
* - `API.Service` — scoped attached service startup (`startService`)
|
|
20
20
|
* - `NativeLauncher` — handing one native agent UI the foreground terminal
|
|
21
21
|
* (`reserveTerminal`, `flushOutput`, `nativeLaunch`)
|
|
22
|
-
* - `Config` — shared execution config (`timeout`, `timeoutExec`, `timeoutFetch
|
|
22
|
+
* - `Config` — shared execution config (`timeout`, `timeoutExec`, `timeoutFetch`,
|
|
23
|
+
* `verbose`)
|
|
23
24
|
*
|
|
24
25
|
* See `apis.ts` for architecture rationale.
|
|
25
26
|
* See `@executablemd/runtime/test` for composable test stubs.
|
|
@@ -30,7 +31,7 @@ export { exec, readTextFile, writeTextFile, stat, lstat, readDirectory, glob, re
|
|
|
30
31
|
export type { DirectoryEntry, EvalBlock, FetchInit, FetchOperation, LinkStatResult, ResponseHeaders, RuntimeFetchResponse, StatResult, } from "./apis.js";
|
|
31
32
|
export { Service, SERVICE_HOSTNAME, SERVICE_READY_PREFIX, ServiceProcessExitBeforeReadyError, ServiceProtocolDuplicateError, ServiceProtocolHostnameMismatchError, ServiceProtocolIncompatibleError, ServiceProtocolMalformedError, ServiceProtocolTokenMismatchError, ServiceProviderError, ServiceStartupTimeoutError, ServiceTeardownError, ServiceUnexpectedExitError, parseServiceReadyRecord, startService, } from "./service.js";
|
|
32
33
|
export type { ServiceEndpoint, ServiceHandler, ServiceAttachment, ServiceStartOptions, } from "./service.js";
|
|
33
|
-
export { Config, timeout, timeoutExec, timeoutFetch } from "./config.js";
|
|
34
|
+
export { Config, timeout, timeoutExec, timeoutFetch, verbose } from "./config.js";
|
|
34
35
|
export type { ConfigApi } from "./config.js";
|
|
35
36
|
export { asDuration, durationError, parseDuration } from "./duration.js";
|
|
36
37
|
export type { ProcessExecOptions, ProcessOutcome } from "./apis.js";
|