functionalscript 0.46.1 → 0.47.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/fjs/basen/cbase32/module.f.mjs +18 -10
- package/fjs/basen/cbase32/proof.f.d.mts +3 -0
- package/fjs/basen/cbase32/proof.f.mjs +24 -2
- package/fjs/basen/proof.f.mjs +8 -16
- package/fjs/bnf/ll1/proof.f.d.mts +2 -0
- package/fjs/bnf/ll1/proof.f.mjs +33 -18
- package/fjs/bnf/module.f.d.mts +2 -2
- package/fjs/bnf/module.f.mjs +2 -2
- package/fjs/cas/proof.f.mjs +4 -26
- package/fjs/ci/common/module.f.mjs +6 -0
- package/fjs/ci/config/module.f.d.mts +7 -7
- package/fjs/ci/config/module.f.mjs +7 -7
- package/fjs/cli/module.f.mjs +14 -10
- package/fjs/crypto/vdf/module.f.d.mts +0 -1
- package/fjs/crypto/vdf/module.f.mjs +3 -14
- package/fjs/dev/module.f.mjs +10 -20
- package/fjs/djs/parser/module.f.d.mts +101 -53
- package/fjs/djs/parser/module.f.mjs +732 -525
- package/fjs/djs/parser/proof.f.d.mts +6 -0
- package/fjs/djs/parser/proof.f.mjs +315 -11
- package/fjs/djs/parser/types.d.ts +29 -2
- package/fjs/djs/tokenizer/module.f.mjs +122 -105
- package/fjs/djs/tokenizer/proof.f.mjs +17 -0
- package/fjs/djs/transpiler/module.f.mjs +4 -5
- package/fjs/djs/transpiler/proof.f.d.mts +2 -0
- package/fjs/djs/transpiler/proof.f.mjs +25 -0
- package/fjs/edag/amnesia/module.f.d.mts +20 -0
- package/fjs/edag/amnesia/module.f.mjs +303 -0
- package/fjs/edag/amnesia/proof.f.d.mts +79 -0
- package/fjs/edag/amnesia/proof.f.mjs +573 -0
- package/fjs/edag/amnesia/types.d.ts +15 -0
- package/fjs/edag/module.f.d.mts +410 -0
- package/fjs/edag/module.f.mjs +559 -0
- package/fjs/edag/proof.f.d.mts +116 -0
- package/fjs/edag/proof.f.mjs +686 -0
- package/fjs/edag/types.d.ts +55 -0
- package/fjs/effects/node/memory/module.d.mts +37 -3
- package/fjs/effects/node/memory/module.mjs +38 -4
- package/fjs/effects/node/memory/proof.d.mts +5 -2
- package/fjs/effects/node/memory/proof.mjs +36 -6
- package/fjs/effects/node/module.d.mts +9 -1
- package/fjs/effects/node/module.f.d.mts +39 -0
- package/fjs/effects/node/module.f.mjs +46 -0
- package/fjs/effects/node/module.mjs +234 -32
- package/fjs/effects/node/types.d.ts +28 -2
- package/fjs/effects/node/virtual/module.f.d.mts +19 -10
- package/fjs/effects/node/virtual/module.f.mjs +246 -32
- package/fjs/effects/node/virtual/proof.f.d.mts +17 -2
- package/fjs/effects/node/virtual/proof.f.mjs +232 -8
- package/fjs/effects/node/virtual/types.d.ts +57 -1
- package/fjs/emergent_testing/browser/proof.d.mts +55 -0
- package/fjs/emergent_testing/browser/proof.mjs +352 -0
- package/fjs/emergent_testing/browser/species.proof.d.mts +4 -0
- package/fjs/emergent_testing/browser/species.proof.mjs +45 -0
- package/fjs/emergent_testing/browser.d.mts +79 -0
- package/fjs/emergent_testing/browser.mjs +432 -0
- package/fjs/emergent_testing/proof.f.d.mts +1 -1
- package/fjs/emergent_testing/proof.f.mjs +2 -2
- package/fjs/js/string_escape/module.f.d.mts +47 -0
- package/fjs/js/string_escape/module.f.mjs +95 -0
- package/fjs/js/string_escape/proof.f.d.mts +8 -0
- package/fjs/js/string_escape/proof.f.mjs +68 -0
- package/fjs/js/tokenizer/module.f.mjs +14 -12
- package/fjs/mcp/cas/module.f.d.mts +1 -7
- package/fjs/mcp/cas/module.f.mjs +24 -15
- package/fjs/mcp/evo/module.f.mjs +9 -24
- package/fjs/mcp/module.f.d.mts +3 -1
- package/fjs/mcp/module.f.mjs +4 -2
- package/fjs/media/json/rtti/module.f.d.mts +2 -2
- package/fjs/media/json/rtti/module.f.mjs +1 -1
- package/fjs/media/json/schema/module.f.d.mts +15 -5
- package/fjs/media/json/schema/module.f.mjs +42 -15
- package/fjs/media/json/schema/proof.f.d.mts +10 -1
- package/fjs/media/json/schema/proof.f.mjs +88 -16
- package/fjs/media/json/serializer/module.f.mjs +14 -26
- package/fjs/media/json/types.d.ts +1 -1
- package/fjs/media/lock/module.f.d.mts +8 -3
- package/fjs/media/lock/module.f.mjs +9 -3
- package/fjs/media/lock/proof.f.d.mts +1 -1
- package/fjs/media/lock/proof.f.mjs +2 -2
- package/fjs/media/lock/types.d.ts +1 -3
- package/fjs/media/module.f.d.mts +20 -13
- package/fjs/media/module.f.mjs +29 -16
- package/fjs/media/note/module.f.d.mts +14 -7
- package/fjs/media/note/module.f.mjs +14 -7
- package/fjs/media/note/proof.f.mjs +1 -1
- package/fjs/media/proof.f.mjs +5 -5
- package/fjs/media/revision/module.f.d.mts +8 -2
- package/fjs/media/revision/module.f.mjs +9 -3
- package/fjs/media/revision/proof.f.mjs +1 -1
- package/fjs/media/revision/types.d.ts +1 -1
- package/fjs/media/type/module.f.d.mts +20 -0
- package/fjs/media/type/module.f.mjs +74 -0
- package/fjs/media/type/proof.f.d.mts +5 -0
- package/fjs/media/type/proof.f.mjs +24 -1
- package/fjs/module.f.mjs +6 -0
- package/fjs/path/module.f.d.mts +60 -3
- package/fjs/path/module.f.mjs +138 -14
- package/fjs/path/proof.f.d.mts +3 -0
- package/fjs/path/proof.f.mjs +298 -2
- package/fjs/protocol/json_rpc/module.f.d.mts +65 -28
- package/fjs/protocol/json_rpc/module.f.mjs +49 -13
- package/fjs/protocol/json_rpc/proof.f.d.mts +4 -0
- package/fjs/protocol/json_rpc/proof.f.mjs +29 -0
- package/fjs/protocol/json_rpc/types.d.ts +12 -2
- package/fjs/protocol/mcp/module.f.d.mts +89 -54
- package/fjs/protocol/mcp/module.f.mjs +110 -56
- package/fjs/protocol/mcp/proof.f.d.mts +13 -0
- package/fjs/protocol/mcp/proof.f.mjs +107 -7
- package/fjs/protocol/mcp/stdio/module.f.mjs +3 -4
- package/fjs/protocol/mcp/stdio/proof.f.mjs +2 -2
- package/fjs/protocol/mcp/types.d.ts +14 -2
- package/fjs/text/utf8/module.f.mjs +27 -1
- package/fjs/text/utf8/proof.f.mjs +37 -0
- package/fjs/types/array/module.f.mjs +18 -11
- package/fjs/types/array/proof.f.mjs +27 -1
- package/fjs/types/bigfloat/module.f.d.mts +48 -9
- package/fjs/types/bigfloat/module.f.mjs +147 -59
- package/fjs/types/bigfloat/proof.f.d.mts +2 -0
- package/fjs/types/bigfloat/proof.f.mjs +136 -4
- package/fjs/types/bigfloat/types.d.ts +17 -0
- package/fjs/types/function/module.f.d.mts +11 -2
- package/fjs/types/function/module.f.mjs +20 -2
- package/fjs/types/function/proof.f.mjs +5 -1
- package/fjs/types/phantom/types.d.ts +21 -0
- package/fjs/types/result/module.f.d.mts +4 -4
- package/fjs/types/result/module.f.mjs +2 -2
- package/fjs/types/result/proof.f.d.mts +6 -2
- package/fjs/types/result/proof.f.mjs +17 -1
- package/fjs/types/rtti/common/module.f.d.mts +78 -5
- package/fjs/types/rtti/common/module.f.mjs +168 -3
- package/fjs/types/rtti/common/proof.f.d.mts +14 -0
- package/fjs/types/rtti/common/proof.f.mjs +61 -2
- package/fjs/types/rtti/common/types.d.ts +23 -1
- package/fjs/types/rtti/data/module.f.d.mts +57 -1
- package/fjs/types/rtti/data/module.f.mjs +241 -47
- package/fjs/types/rtti/data/proof.f.d.mts +10 -0
- package/fjs/types/rtti/data/proof.f.mjs +272 -39
- package/fjs/types/rtti/data/types.d.ts +14 -7
- package/fjs/types/rtti/host.proof.d.mts +33 -0
- package/fjs/types/rtti/host.proof.mjs +170 -0
- package/fjs/types/rtti/module.f.d.mts +51 -6
- package/fjs/types/rtti/module.f.mjs +50 -3
- package/fjs/types/rtti/parse/module.f.d.mts +31 -24
- package/fjs/types/rtti/parse/module.f.mjs +197 -59
- package/fjs/types/rtti/parse/proof.f.d.mts +37 -2
- package/fjs/types/rtti/parse/proof.f.mjs +171 -47
- package/fjs/types/rtti/proof.f.d.mts +6 -0
- package/fjs/types/rtti/proof.f.mjs +42 -1
- package/fjs/types/rtti/ts/module.f.d.mts +9 -3
- package/fjs/types/rtti/ts/module.f.mjs +68 -15
- package/fjs/types/rtti/ts/proof.f.d.mts +42 -2
- package/fjs/types/rtti/ts/proof.f.mjs +139 -6
- package/fjs/types/rtti/ts/types.d.ts +164 -16
- package/fjs/types/rtti/types.d.ts +31 -3
- package/fjs/types/rtti/validate/module.f.d.mts +42 -25
- package/fjs/types/rtti/validate/module.f.mjs +187 -53
- package/fjs/types/rtti/validate/proof.f.d.mts +28 -2
- package/fjs/types/rtti/validate/proof.f.mjs +430 -75
- package/fjs/types/ts/types.d.ts +4 -0
- package/fjs/web/module.f.d.mts +115 -0
- package/fjs/web/module.f.mjs +581 -0
- package/fjs/web/proof.f.d.mts +39 -0
- package/fjs/web/proof.f.mjs +413 -0
- package/fjs/web/types.d.ts +39 -0
- package/fjs/website/browser-prepare.d.mts +5 -0
- package/fjs/website/browser-prepare.mjs +115 -0
- package/fjs/website/browser-source.d.mts +37 -0
- package/fjs/website/browser-source.mjs +202 -0
- package/fjs/website/browser-source.proof.d.mts +31 -0
- package/fjs/website/browser-source.proof.mjs +162 -0
- package/fjs/website/browser.d.mts +5 -0
- package/fjs/website/browser.mjs +8 -0
- package/fjs/website/module.f.d.mts +3 -3
- package/fjs/website/module.f.mjs +62 -9
- package/fjs/website/proof.f.d.mts +4 -0
- package/fjs/website/proof.f.mjs +18 -3
- package/package.json +2 -3
|
@@ -91,8 +91,14 @@ export declare const lockField: LockFieldSchema;
|
|
|
91
91
|
* rtti schema for a `revision` BLOB. See the README for the full semantics of
|
|
92
92
|
* each field; `dialect` is the type discriminant, matched here as an exact
|
|
93
93
|
* literal so structural validation alone rejects any other dialect's blob.
|
|
94
|
+
*
|
|
95
|
+
* `open`, and deliberately so: a bare struct is closed, so an older reader
|
|
96
|
+
* would reject a blob a newer writer had added a field to, and this dialect's
|
|
97
|
+
* own versioning rule — additive extension keeps the tag, see `./README.md` —
|
|
98
|
+
* is stated in terms of that older reader accepting it. Do not drop the
|
|
99
|
+
* wrapper.
|
|
94
100
|
*/
|
|
95
|
-
export declare const revisionSchema: {
|
|
101
|
+
export declare const revisionSchema: import("../../types/rtti/types.ts").Rest<{
|
|
96
102
|
readonly dialect: "vnd.fjs.revision";
|
|
97
103
|
readonly subject: import("../../types/rtti/types.ts")._Type0<"string">;
|
|
98
104
|
readonly parents: import("../../types/rtti/types.ts").Type1<"array", import("../../types/rtti/types.ts")._Type0<"string">>;
|
|
@@ -100,7 +106,7 @@ export declare const revisionSchema: {
|
|
|
100
106
|
readonly generation: import("../../types/rtti/types.ts")._Type0<"number">;
|
|
101
107
|
readonly archived: import("../../types/rtti/types.ts").Or<readonly [true, undefined]>;
|
|
102
108
|
readonly lock: import("../../types/rtti/types.ts").Or<readonly [LockFieldSchema, undefined]>;
|
|
103
|
-
}
|
|
109
|
+
}, import("../../types/rtti/types.ts").Unknown>;
|
|
104
110
|
/** Serializes a revision canonically, recursively sorting every object's property names.
|
|
105
111
|
* @type {(revision: Revision) => string}
|
|
106
112
|
*/
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* @import { LockField, LockFieldSchema, LockMap, LockSchema, Revision, RevisionError } from './types.ts'
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { array, number, option, string } from '../../types/rtti/module.f.mjs'
|
|
24
|
+
import { array, number, open, option, string } from '../../types/rtti/module.f.mjs'
|
|
25
25
|
import { parse as rttiParse } from '../../types/rtti/parse/module.f.mjs'
|
|
26
26
|
import { parse as parseJson } from '../json/module.f.mjs'
|
|
27
27
|
import { cBase32ToVec } from '../../basen/cbase32/module.f.mjs'
|
|
@@ -113,8 +113,14 @@ export const lockField = () => ['or', hash, lock]
|
|
|
113
113
|
* rtti schema for a `revision` BLOB. See the README for the full semantics of
|
|
114
114
|
* each field; `dialect` is the type discriminant, matched here as an exact
|
|
115
115
|
* literal so structural validation alone rejects any other dialect's blob.
|
|
116
|
+
*
|
|
117
|
+
* `open`, and deliberately so: a bare struct is closed, so an older reader
|
|
118
|
+
* would reject a blob a newer writer had added a field to, and this dialect's
|
|
119
|
+
* own versioning rule — additive extension keeps the tag, see `./README.md` —
|
|
120
|
+
* is stated in terms of that older reader accepting it. Do not drop the
|
|
121
|
+
* wrapper.
|
|
116
122
|
*/
|
|
117
|
-
export const revisionSchema = /** @type {const} */ ({
|
|
123
|
+
export const revisionSchema = open(/** @type {const} */ ({
|
|
118
124
|
dialect,
|
|
119
125
|
subject: string,
|
|
120
126
|
parents: array(hash),
|
|
@@ -122,7 +128,7 @@ export const revisionSchema = /** @type {const} */ ({
|
|
|
122
128
|
generation: number,
|
|
123
129
|
archived: option(true),
|
|
124
130
|
lock: option(lockField),
|
|
125
|
-
})
|
|
131
|
+
}))
|
|
126
132
|
|
|
127
133
|
/** Serializes a revision canonically, recursively sorting every object's property names.
|
|
128
134
|
* @type {(revision: Revision) => string}
|
|
@@ -228,7 +228,7 @@ export const proof = {
|
|
|
228
228
|
assertEq(t, 'error')
|
|
229
229
|
},
|
|
230
230
|
|
|
231
|
-
//
|
|
231
|
+
// `revisionSchema` says `open`: extra fields don't break validation — the
|
|
232
232
|
// additive forward-compatibility path the versioning rule relies on.
|
|
233
233
|
extraFieldsAccepted: () => {
|
|
234
234
|
const [t] = validate(revisionOf({ future: 'field' }))
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* `LockMap` is written by hand rather than derived, so that the recursion
|
|
7
7
|
* reads directly, and is then pinned against the module's rtti schema with
|
|
8
|
-
* `Assert<
|
|
8
|
+
* `Assert<Check<LockMap, typeof lock>>` — the same arrangement the JSON
|
|
9
9
|
* data model uses in [`../json/types.ts`](../json/types.ts). `LockSchema` is
|
|
10
10
|
* the schema side of the same recursion: `lock` cannot infer its own type
|
|
11
11
|
* (a `const` may not reference itself in its own initializer), so it carries
|
|
@@ -31,6 +31,12 @@
|
|
|
31
31
|
* sits between the `RIFF` and `WEBP` markers, so its pattern carries four
|
|
32
32
|
* wildcard bytes rather than being one contiguous run.
|
|
33
33
|
*
|
|
34
|
+
* Beside the two byte sniffers, `detectPath` answers the same question from the
|
|
35
|
+
* other end — the `Content-Type` a *file name* implies, read off its extension.
|
|
36
|
+
* A sniffer cannot serve that question at all: `text/html`, `text/css` and
|
|
37
|
+
* `text/javascript` are byte-identical UTF-8 text, and only the name tells them
|
|
38
|
+
* apart.
|
|
39
|
+
*
|
|
34
40
|
* See `./types.ts` for the type-level API.
|
|
35
41
|
*
|
|
36
42
|
* @module
|
|
@@ -130,3 +136,17 @@ export declare const detectVec: (bytes: Vec) => DetectMeta;
|
|
|
130
136
|
* @returns {Effect<O, DetectMeta, IoChannel>}
|
|
131
137
|
*/
|
|
132
138
|
export declare const detectStream: <O extends Operation>(stream: List<O, Vec, IoChannel>) => Effect<O, DetectMeta, IoChannel>;
|
|
139
|
+
/**
|
|
140
|
+
* The `Content-Type` a file name implies, from its extension alone — the bytes
|
|
141
|
+
* are never read. Text types carry `; charset=utf-8`, so the result is a
|
|
142
|
+
* complete header value rather than a bare media type; an absent or unknown
|
|
143
|
+
* extension is `application/octet-stream`.
|
|
144
|
+
*
|
|
145
|
+
* This is the counterpart of {@link detect}, not a replacement for it: sniffing
|
|
146
|
+
* answers what unlabelled bytes are, and this answers what a named file claims
|
|
147
|
+
* to be. A server needs the second — the three text formats a browser treats
|
|
148
|
+
* differently are byte-identical to a sniffer.
|
|
149
|
+
*
|
|
150
|
+
* @type {(path: string) => string}
|
|
151
|
+
*/
|
|
152
|
+
export declare const detectPath: (path: string) => string;
|
|
@@ -31,6 +31,12 @@
|
|
|
31
31
|
* sits between the `RIFF` and `WEBP` markers, so its pattern carries four
|
|
32
32
|
* wildcard bytes rather than being one contiguous run.
|
|
33
33
|
*
|
|
34
|
+
* Beside the two byte sniffers, `detectPath` answers the same question from the
|
|
35
|
+
* other end — the `Content-Type` a *file name* implies, read off its extension.
|
|
36
|
+
* A sniffer cannot serve that question at all: `text/html`, `text/css` and
|
|
37
|
+
* `text/javascript` are byte-identical UTF-8 text, and only the name tells them
|
|
38
|
+
* apart.
|
|
39
|
+
*
|
|
34
40
|
* See `./types.ts` for the type-level API.
|
|
35
41
|
*
|
|
36
42
|
* @module
|
|
@@ -49,6 +55,7 @@ import { iterable } from '../../types/list/module.f.mjs'
|
|
|
49
55
|
import { pureOk, step as ioStep } from '../../effects/module.f.mjs'
|
|
50
56
|
import { isValidCodePoint, isTextCodePoint } from '../../text/code_point/module.f.mjs'
|
|
51
57
|
import { utf8ByteToCodePointOp } from '../../text/utf8/module.f.mjs'
|
|
58
|
+
import { at } from '../../types/object/module.f.mjs'
|
|
52
59
|
|
|
53
60
|
// ── Magic-byte signatures ─────────────────────────────────────────────────────────
|
|
54
61
|
//
|
|
@@ -269,3 +276,70 @@ export const detectStream = stream => {
|
|
|
269
276
|
})
|
|
270
277
|
return loop(detectInit)(stream)
|
|
271
278
|
}
|
|
279
|
+
|
|
280
|
+
// ── Extension table ───────────────────────────────────────────────────────────────
|
|
281
|
+
//
|
|
282
|
+
// The same question asked from the other end: not "what do these bytes look like"
|
|
283
|
+
// but "what does this file name claim to be". A server answering `Content-Type`
|
|
284
|
+
// needs the second, because the first cannot tell `text/html` from `text/css` from
|
|
285
|
+
// `text/javascript` — every one of them is plain UTF-8 text, and only the extension
|
|
286
|
+
// distinguishes them.
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Media type by lower-case file-name extension, without the `charset` parameter
|
|
290
|
+
* {@link detectPath} adds. Deliberately small: the types the pages under
|
|
291
|
+
* `fjs/website` are built from, plus the image formats {@link detect} already
|
|
292
|
+
* recognizes.
|
|
293
|
+
*/
|
|
294
|
+
const byExtension = /** @type {const} */ ({
|
|
295
|
+
html: 'text/html',
|
|
296
|
+
css: 'text/css',
|
|
297
|
+
js: 'text/javascript',
|
|
298
|
+
mjs: 'text/javascript',
|
|
299
|
+
json: 'application/json',
|
|
300
|
+
svg: 'image/svg+xml',
|
|
301
|
+
png: 'image/png',
|
|
302
|
+
jpg: 'image/jpeg',
|
|
303
|
+
jpeg: 'image/jpeg',
|
|
304
|
+
gif: 'image/gif',
|
|
305
|
+
webp: 'image/webp',
|
|
306
|
+
wasm: 'application/wasm',
|
|
307
|
+
txt: 'text/plain',
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
/** The answer for a name whose extension is absent or unknown.
|
|
311
|
+
*
|
|
312
|
+
* @type {string}
|
|
313
|
+
*/
|
|
314
|
+
const octetStream = 'application/octet-stream'
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The lower-case extension of the last segment of `path`, or `''` when it has
|
|
318
|
+
* none. A leading dot is a name, not an extension: `.gitignore` has none.
|
|
319
|
+
*
|
|
320
|
+
* @type {(path: string) => string}
|
|
321
|
+
*/
|
|
322
|
+
const extension = path => {
|
|
323
|
+
const name = path.slice(path.lastIndexOf('/') + 1)
|
|
324
|
+
const dot = name.lastIndexOf('.')
|
|
325
|
+
return dot > 0 ? name.slice(dot + 1).toLowerCase() : ''
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* The `Content-Type` a file name implies, from its extension alone — the bytes
|
|
330
|
+
* are never read. Text types carry `; charset=utf-8`, so the result is a
|
|
331
|
+
* complete header value rather than a bare media type; an absent or unknown
|
|
332
|
+
* extension is `application/octet-stream`.
|
|
333
|
+
*
|
|
334
|
+
* This is the counterpart of {@link detect}, not a replacement for it: sniffing
|
|
335
|
+
* answers what unlabelled bytes are, and this answers what a named file claims
|
|
336
|
+
* to be. A server needs the second — the three text formats a browser treats
|
|
337
|
+
* differently are byte-identical to a sniffer.
|
|
338
|
+
*
|
|
339
|
+
* @type {(path: string) => string}
|
|
340
|
+
*/
|
|
341
|
+
export const detectPath = path => {
|
|
342
|
+
const mime = at(extension(path))(byExtension)
|
|
343
|
+
if (mime === null) { return octetStream }
|
|
344
|
+
return mime.startsWith('text/') ? `${mime}; charset=utf-8` : mime
|
|
345
|
+
}
|
|
@@ -12,7 +12,7 @@ import { nonEmpty, empty as emptyList } from '../../effects/list/module.f.mjs'
|
|
|
12
12
|
import { pureError } from '../../effects/module.f.mjs'
|
|
13
13
|
import { error, ok } from '../../types/result/module.f.mjs'
|
|
14
14
|
import { ioError } from '../../effects/node/module.f.mjs'
|
|
15
|
-
import { detect, detectStream, detectVec } from './module.f.mjs'
|
|
15
|
+
import { detect, detectPath, detectStream, detectVec } from './module.f.mjs'
|
|
16
16
|
|
|
17
17
|
// Builds a big-endian `Vec` from a list of byte values — mirrors how the CAS
|
|
18
18
|
// store would hold the leading bytes of a stored blob.
|
|
@@ -342,4 +342,27 @@ export const proof = {
|
|
|
342
342
|
assertEq(m.length, 3n)
|
|
343
343
|
},
|
|
344
344
|
},
|
|
345
|
+
detectPath: {
|
|
346
|
+
// A text type carries the charset, so the answer is a complete header
|
|
347
|
+
// value rather than a bare media type.
|
|
348
|
+
text: () => {
|
|
349
|
+
assertEq(detectPath('index.html'), 'text/html; charset=utf-8')
|
|
350
|
+
assertEq(detectPath('a/b/main.css'), 'text/css; charset=utf-8')
|
|
351
|
+
assertEq(detectPath('module.f.mjs'), 'text/javascript; charset=utf-8')
|
|
352
|
+
},
|
|
353
|
+
// A binary type does not: `charset` says nothing about bytes.
|
|
354
|
+
binary: () => {
|
|
355
|
+
assertEq(detectPath('logo.png'), 'image/png')
|
|
356
|
+
assertEq(detectPath('photo.JPG'), 'image/jpeg')
|
|
357
|
+
assertEq(detectPath('vm.wasm'), 'application/wasm')
|
|
358
|
+
},
|
|
359
|
+
// The names that carry no extension, plus an extension nothing claims.
|
|
360
|
+
fallback: () => {
|
|
361
|
+
assertEq(detectPath('README'), 'application/octet-stream')
|
|
362
|
+
assertEq(detectPath('.gitignore'), 'application/octet-stream')
|
|
363
|
+
assertEq(detectPath('archive.tar.zst'), 'application/octet-stream')
|
|
364
|
+
// A dot in a parent directory is not this file's extension.
|
|
365
|
+
assertEq(detectPath('v1.2/README'), 'application/octet-stream')
|
|
366
|
+
},
|
|
367
|
+
},
|
|
345
368
|
}
|
package/fjs/module.f.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { main as ciMain } from './ci/module.f.mjs'
|
|
|
14
14
|
import { errorExit, errorMessage, exitStep, import_ } from './effects/node/module.f.mjs'
|
|
15
15
|
import { dispatch } from './cli/module.f.mjs'
|
|
16
16
|
import { casMcpServer } from './mcp/module.f.mjs'
|
|
17
|
+
import { main as webMain } from './web/module.f.mjs'
|
|
17
18
|
import { resultStep } from './effects/module.f.mjs'
|
|
18
19
|
|
|
19
20
|
/** @type {Commands<NodeOp>} */
|
|
@@ -53,6 +54,11 @@ const commands = [
|
|
|
53
54
|
description: 'Generate the GitHub Actions CI workflow',
|
|
54
55
|
handler: ciMain,
|
|
55
56
|
},
|
|
57
|
+
{
|
|
58
|
+
names: ['web', 'w'],
|
|
59
|
+
description: 'Serve a directory over HTTP',
|
|
60
|
+
handler: webMain,
|
|
61
|
+
},
|
|
56
62
|
{
|
|
57
63
|
names: ['run', 'r'],
|
|
58
64
|
description: 'Run a FunctionalScript module as a NodeProgram',
|
package/fjs/path/module.f.d.mts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Path parsing and normalization helpers for portable module paths.
|
|
3
3
|
*
|
|
4
|
+
* A path is a {@link root} followed by segments. The root is what makes the
|
|
5
|
+
* path absolute — `/`, `//` for a UNC path, or `C:/` for a Windows drive —
|
|
6
|
+
* and it is not a segment: it survives normalization, and `..` cannot escape
|
|
7
|
+
* it. Everything after it folds by the usual rules, so `a//b` is `a/b` and
|
|
8
|
+
* `a/../b` is `b`.
|
|
9
|
+
*
|
|
4
10
|
* @module
|
|
5
11
|
*
|
|
6
12
|
* @import { Fold, Reduce, Unary } from '../types/function/operator/types.ts'
|
|
@@ -14,17 +20,62 @@ import type { Reduce, Unary } from '../types/function/operator/types.ts';
|
|
|
14
20
|
*/
|
|
15
21
|
export declare const toPosix: (path: string) => string;
|
|
16
22
|
/**
|
|
17
|
-
*
|
|
23
|
+
* The root of a path, carrying its trailing separator: `'/'` for a POSIX
|
|
24
|
+
* absolute path, `'//'` for a UNC one, `'C:/'` for a Windows drive, and `''`
|
|
25
|
+
* for a relative path.
|
|
26
|
+
*
|
|
27
|
+
* `..` cannot escape whatever this answers, so it is also the definition of
|
|
28
|
+
* how far up a path can go. On Windows that is not quite the whole story: the
|
|
29
|
+
* `server/share` of a UNC path is part of its root there and is an ordinary
|
|
30
|
+
* segment here, so `..` can still climb past a share — see
|
|
31
|
+
* {@link split} for why the root stops at `//`.
|
|
32
|
+
*
|
|
33
|
+
* A drive root is the one that folding can *create*. `/` and `//` are prefixes
|
|
34
|
+
* of the text, so no amount of `.`/`..` collapsing produces one; `C:/` is a
|
|
35
|
+
* segment's content, so a relative path whose first segment happens to be a
|
|
36
|
+
* drive letter becomes drive-rooted once folded — `root('./C:/a')` is `''`
|
|
37
|
+
* while `root(normalize('./C:/a'))` is `'C:/'`. On Windows that is the reading
|
|
38
|
+
* that is wanted; on a POSIX host with a directory actually named `C:` it is
|
|
39
|
+
* not, and this module has no way to tell the two apart after folding.
|
|
40
|
+
*
|
|
41
|
+
* @type {(path: string) => string}
|
|
42
|
+
*/
|
|
43
|
+
export declare const root: (path: string) => string;
|
|
44
|
+
/**
|
|
45
|
+
* Splits a path into normalized segments, *without* its root — `parse('/a/b')`
|
|
46
|
+
* and `parse('a/b')` are both `['a', 'b']`. Use {@link root} to ask whether the
|
|
47
|
+
* path was absolute, or {@link normalize} for a string that keeps it.
|
|
18
48
|
*
|
|
19
49
|
* Empty (`""`) and current-directory (`"."`) segments are removed, parent-directory
|
|
20
50
|
* (`".."`) segments collapse the previous segment when possible, and Windows
|
|
21
|
-
* separators are converted to POSIX separators.
|
|
51
|
+
* separators are converted to POSIX separators. A `".."` that would escape a
|
|
52
|
+
* root is dropped rather than kept.
|
|
22
53
|
*
|
|
23
54
|
* @type {(path: string) => readonly string[]}
|
|
24
55
|
*/
|
|
25
56
|
export declare const parse: (path: string) => readonly string[];
|
|
26
57
|
/**
|
|
27
|
-
*
|
|
58
|
+
* Whether a `..` in `path` would climb above its root — what a caller asks when
|
|
59
|
+
* it has to *refuse* a traversal rather than clamp one.
|
|
60
|
+
*
|
|
61
|
+
* {@link parse} cannot answer it: `parse` folds with the root in place, and the
|
|
62
|
+
* escaping `..` is precisely what that removes. Here the root comes off first
|
|
63
|
+
* and the remainder folds as a relative path, where a `..` with nothing left to
|
|
64
|
+
* cancel survives to be counted.
|
|
65
|
+
*
|
|
66
|
+
* Taking the root off is not the same as dropping a leading `/`. The remainder
|
|
67
|
+
* of `///../x` is `//../x` and the remainder of `/C:/../../x` is
|
|
68
|
+
* `C:/../../x` — both read as rooted again, and folding them a second time
|
|
69
|
+
* would clamp the very `..` being looked for. The fold is told there is no
|
|
70
|
+
* root instead of being handed a string that looks like it has none.
|
|
71
|
+
*
|
|
72
|
+
* @type {(path: string) => boolean}
|
|
73
|
+
*/
|
|
74
|
+
export declare const escapes: (path: string) => boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Normalizes a path string by parsing and rejoining it with POSIX separators,
|
|
77
|
+
* keeping the root: `normalize('/a/./b')` is `'/a/b'` and `normalize('/')` is
|
|
78
|
+
* `'/'`.
|
|
28
79
|
*
|
|
29
80
|
* @type {Unary<string, string>}
|
|
30
81
|
*/
|
|
@@ -32,6 +83,12 @@ export declare const normalize: Unary<string, string>;
|
|
|
32
83
|
/**
|
|
33
84
|
* Concatenates two path fragments and returns a normalized path.
|
|
34
85
|
*
|
|
86
|
+
* An absolute `b` names a path on its own, so it replaces `a` rather than being
|
|
87
|
+
* appended to it. Otherwise the root comes from `a` and the segments from the
|
|
88
|
+
* two joined — taking the root from `a` rather than from the joined string is
|
|
89
|
+
* what stops the separator between them from being read as a root of its own,
|
|
90
|
+
* so `concat('')('../x')` is `'../x'` and not `'x'`.
|
|
91
|
+
*
|
|
35
92
|
* @type {Reduce<string>}
|
|
36
93
|
*/
|
|
37
94
|
export declare const concat: Reduce<string>;
|
package/fjs/path/module.f.mjs
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Path parsing and normalization helpers for portable module paths.
|
|
3
3
|
*
|
|
4
|
+
* A path is a {@link root} followed by segments. The root is what makes the
|
|
5
|
+
* path absolute — `/`, `//` for a UNC path, or `C:/` for a Windows drive —
|
|
6
|
+
* and it is not a segment: it survives normalization, and `..` cannot escape
|
|
7
|
+
* it. Everything after it folds by the usual rules, so `a//b` is `a/b` and
|
|
8
|
+
* `a/../b` is `b`.
|
|
9
|
+
*
|
|
4
10
|
* @module
|
|
5
11
|
*
|
|
6
12
|
* @import { Fold, Reduce, Unary } from '../types/function/operator/types.ts'
|
|
@@ -10,13 +16,19 @@
|
|
|
10
16
|
import { fold, last, take, length, concat as listConcat, toArray } from '../types/list/module.f.mjs'
|
|
11
17
|
import { join as listJoin, concat as stringConcat } from '../types/string/module.f.mjs'
|
|
12
18
|
|
|
13
|
-
/**
|
|
14
|
-
|
|
19
|
+
/**
|
|
20
|
+
* `rooted` is the only thing the fold needs to know about the root: whether a
|
|
21
|
+
* leading `..` has anywhere to go. `/a/../..` is `/`, because there is no
|
|
22
|
+
* parent of the root to name, while `a/../..` stays `..`.
|
|
23
|
+
*
|
|
24
|
+
* @type {(rooted: boolean) => Fold<string, List<string>>}
|
|
25
|
+
*/
|
|
26
|
+
const foldNormalizeOp = rooted => input => state => {
|
|
15
27
|
switch(input) {
|
|
16
28
|
case '': case '.': { return state }
|
|
17
29
|
case '..': {
|
|
18
30
|
switch(last(undefined)(state)) {
|
|
19
|
-
case undefined:
|
|
31
|
+
case undefined: { return rooted ? state : listConcat(state)([input]) }
|
|
20
32
|
case '..': { return listConcat(state)([input]) }
|
|
21
33
|
}
|
|
22
34
|
return take(length(state) - 1)(state)
|
|
@@ -32,38 +44,150 @@ const foldNormalizeOp = input => state => {
|
|
|
32
44
|
*/
|
|
33
45
|
export const toPosix = path => path.replaceAll('\\', '/')
|
|
34
46
|
|
|
47
|
+
/** @type {(c: string) => boolean} */
|
|
48
|
+
const isDriveLetter = c => (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A Windows drive root, and only in its absolute spelling: `C:/` roots the
|
|
52
|
+
* path, while a bare `C:` and the drive-relative `C:foo` — which names the
|
|
53
|
+
* current directory *on* drive C — do not, and stay ordinary segments.
|
|
54
|
+
*
|
|
55
|
+
* @type {(p: string) => boolean}
|
|
56
|
+
*/
|
|
57
|
+
const isDriveRoot = p => p.length >= 3 && p[1] === ':' && p[2] === '/' && isDriveLetter(p[0])
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A bare drive, which {@link isDriveRoot} deliberately excludes — and which
|
|
61
|
+
* {@link concat} must therefore not turn into one. The separator `concat`
|
|
62
|
+
* inserts between its two arguments is what would do it: `C:` and `dir` joined
|
|
63
|
+
* with a `/` is the drive root `C:/dir` rather than the drive-relative `C:dir`,
|
|
64
|
+
* a different place on the disk. Joined without one, the answer stays the kind
|
|
65
|
+
* of path it started as.
|
|
66
|
+
*
|
|
67
|
+
* This keeps `concat` from changing a path's kind. It does not make the
|
|
68
|
+
* drive-relative form *resolve* like Windows: `C:dir` is one ordinary segment
|
|
69
|
+
* here, so `concat('C:dir')('../..')` is `'..'` where Windows says `C:..`.
|
|
70
|
+
* Modelling that needs a third kind of path, which this module does not have.
|
|
71
|
+
*
|
|
72
|
+
* @type {(p: string) => boolean}
|
|
73
|
+
*/
|
|
74
|
+
const isBareDrive = p => p.length === 2 && p[1] === ':' && isDriveLetter(p[0])
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Splits an already-POSIX path into its root and everything after it, so that
|
|
78
|
+
* `root + rest` is the path again. The root carries its own trailing separator
|
|
79
|
+
* — `'/'`, `'//'`, `'C:/'`, or `''` — which is what lets {@link rejoin} put a
|
|
80
|
+
* path back together without a separator of its own.
|
|
81
|
+
*
|
|
82
|
+
* A root is a fixed-width prefix here, never a parsed one. `//` marks a UNC
|
|
83
|
+
* path but stops there rather than swallowing `server/share`: those are two
|
|
84
|
+
* arbitrary segments, and a path like `//a/../../etc/passwd` would fold `../`
|
|
85
|
+
* *into* the root, which is precisely what a root must not do. Three or more
|
|
86
|
+
* leading slashes are an ordinary root followed by empty segments, which is
|
|
87
|
+
* what POSIX requires.
|
|
88
|
+
*
|
|
89
|
+
* @type {(p: string) => readonly [root: string, rest: string]}
|
|
90
|
+
*/
|
|
91
|
+
const split = p =>
|
|
92
|
+
p.startsWith('//') && !p.startsWith('///') ? ['//', p.slice(2)]
|
|
93
|
+
: p.startsWith('/') ? ['/', p.slice(1)]
|
|
94
|
+
: isDriveRoot(p) ? [p.slice(0, 3), p.slice(3)]
|
|
95
|
+
: ['', p]
|
|
96
|
+
|
|
97
|
+
/** @type {(rooted: boolean) => (rest: string) => readonly string[]} */
|
|
98
|
+
const posixSegments = rooted => rest => toArray(fold(foldNormalizeOp(rooted))([])(rest.split('/')))
|
|
99
|
+
|
|
100
|
+
/** @type {(s: readonly [string, string]) => string} */
|
|
101
|
+
const rejoin = ([r, rest]) => stringConcat([r, listJoin('/')(posixSegments(r !== '')(rest))])
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The root of a path, carrying its trailing separator: `'/'` for a POSIX
|
|
105
|
+
* absolute path, `'//'` for a UNC one, `'C:/'` for a Windows drive, and `''`
|
|
106
|
+
* for a relative path.
|
|
107
|
+
*
|
|
108
|
+
* `..` cannot escape whatever this answers, so it is also the definition of
|
|
109
|
+
* how far up a path can go. On Windows that is not quite the whole story: the
|
|
110
|
+
* `server/share` of a UNC path is part of its root there and is an ordinary
|
|
111
|
+
* segment here, so `..` can still climb past a share — see
|
|
112
|
+
* {@link split} for why the root stops at `//`.
|
|
113
|
+
*
|
|
114
|
+
* A drive root is the one that folding can *create*. `/` and `//` are prefixes
|
|
115
|
+
* of the text, so no amount of `.`/`..` collapsing produces one; `C:/` is a
|
|
116
|
+
* segment's content, so a relative path whose first segment happens to be a
|
|
117
|
+
* drive letter becomes drive-rooted once folded — `root('./C:/a')` is `''`
|
|
118
|
+
* while `root(normalize('./C:/a'))` is `'C:/'`. On Windows that is the reading
|
|
119
|
+
* that is wanted; on a POSIX host with a directory actually named `C:` it is
|
|
120
|
+
* not, and this module has no way to tell the two apart after folding.
|
|
121
|
+
*
|
|
122
|
+
* @type {(path: string) => string}
|
|
123
|
+
*/
|
|
124
|
+
export const root = path => split(toPosix(path))[0]
|
|
125
|
+
|
|
35
126
|
/**
|
|
36
|
-
* Splits a path into normalized segments
|
|
127
|
+
* Splits a path into normalized segments, *without* its root — `parse('/a/b')`
|
|
128
|
+
* and `parse('a/b')` are both `['a', 'b']`. Use {@link root} to ask whether the
|
|
129
|
+
* path was absolute, or {@link normalize} for a string that keeps it.
|
|
37
130
|
*
|
|
38
131
|
* Empty (`""`) and current-directory (`"."`) segments are removed, parent-directory
|
|
39
132
|
* (`".."`) segments collapse the previous segment when possible, and Windows
|
|
40
|
-
* separators are converted to POSIX separators.
|
|
133
|
+
* separators are converted to POSIX separators. A `".."` that would escape a
|
|
134
|
+
* root is dropped rather than kept.
|
|
41
135
|
*
|
|
42
136
|
* @type {(path: string) => readonly string[]}
|
|
43
137
|
*/
|
|
44
138
|
export const parse = path => {
|
|
45
|
-
const
|
|
46
|
-
return
|
|
139
|
+
const [r, rest] = split(toPosix(path))
|
|
140
|
+
return posixSegments(r !== '')(rest)
|
|
47
141
|
}
|
|
48
142
|
|
|
49
143
|
/**
|
|
50
|
-
*
|
|
144
|
+
* Whether a `..` in `path` would climb above its root — what a caller asks when
|
|
145
|
+
* it has to *refuse* a traversal rather than clamp one.
|
|
146
|
+
*
|
|
147
|
+
* {@link parse} cannot answer it: `parse` folds with the root in place, and the
|
|
148
|
+
* escaping `..` is precisely what that removes. Here the root comes off first
|
|
149
|
+
* and the remainder folds as a relative path, where a `..` with nothing left to
|
|
150
|
+
* cancel survives to be counted.
|
|
151
|
+
*
|
|
152
|
+
* Taking the root off is not the same as dropping a leading `/`. The remainder
|
|
153
|
+
* of `///../x` is `//../x` and the remainder of `/C:/../../x` is
|
|
154
|
+
* `C:/../../x` — both read as rooted again, and folding them a second time
|
|
155
|
+
* would clamp the very `..` being looked for. The fold is told there is no
|
|
156
|
+
* root instead of being handed a string that looks like it has none.
|
|
157
|
+
*
|
|
158
|
+
* @type {(path: string) => boolean}
|
|
159
|
+
*/
|
|
160
|
+
export const escapes = path => posixSegments(false)(split(toPosix(path))[1]).includes('..')
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Normalizes a path string by parsing and rejoining it with POSIX separators,
|
|
164
|
+
* keeping the root: `normalize('/a/./b')` is `'/a/b'` and `normalize('/')` is
|
|
165
|
+
* `'/'`.
|
|
51
166
|
*
|
|
52
167
|
* @type {Unary<string, string>}
|
|
53
168
|
*/
|
|
54
|
-
export const normalize = path =>
|
|
55
|
-
const foldResult = parse(path)
|
|
56
|
-
return listJoin('/')(foldResult)
|
|
57
|
-
}
|
|
169
|
+
export const normalize = path => rejoin(split(toPosix(path)))
|
|
58
170
|
|
|
59
171
|
/**
|
|
60
172
|
* Concatenates two path fragments and returns a normalized path.
|
|
61
173
|
*
|
|
174
|
+
* An absolute `b` names a path on its own, so it replaces `a` rather than being
|
|
175
|
+
* appended to it. Otherwise the root comes from `a` and the segments from the
|
|
176
|
+
* two joined — taking the root from `a` rather than from the joined string is
|
|
177
|
+
* what stops the separator between them from being read as a root of its own,
|
|
178
|
+
* so `concat('')('../x')` is `'../x'` and not `'x'`.
|
|
179
|
+
*
|
|
62
180
|
* @type {Reduce<string>}
|
|
63
181
|
*/
|
|
64
182
|
export const concat = a => b => {
|
|
65
|
-
const
|
|
66
|
-
return
|
|
183
|
+
const [rb, restb] = split(toPosix(b))
|
|
184
|
+
if (rb !== '') { return rejoin([rb, restb]) }
|
|
185
|
+
// `a` is normalized before its root is read, because what `a` *is* decides
|
|
186
|
+
// the join and only the folded form answers that: `./C:` and `x/../C:` are
|
|
187
|
+
// both the bare drive `C:`, and reading them unfolded would insert the
|
|
188
|
+
// separator that makes a drive root out of one.
|
|
189
|
+
const [ra, resta] = split(normalize(a))
|
|
190
|
+
return rejoin([ra, stringConcat([resta, isBareDrive(resta) ? '' : '/', restb])])
|
|
67
191
|
}
|
|
68
192
|
|
|
69
193
|
/**
|
package/fjs/path/proof.f.d.mts
CHANGED