@dogsbay/corpus 0.0.0-stage → 0.2.0-beta.133
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/README.md +25 -2
- package/dist/folder.d.ts +14 -0
- package/dist/folder.js +121 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +45 -0
- package/package.json +39 -3
package/README.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @dogsbay/corpus
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A documentation repository as the Dogsbay importers read it: its file paths (posix, relative
|
|
4
|
+
to the corpus root) and its text on demand. The same importer then runs in the CLI and in a
|
|
5
|
+
browser.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { corpusFromMap } from "@dogsbay/corpus"; // browser: a map of files
|
|
9
|
+
import { readFolder } from "@dogsbay/corpus/folder"; // Node: a folder
|
|
10
|
+
|
|
11
|
+
const corpus = corpusFromMap(new Map([["docs/index.md", "# Home\n"]]));
|
|
12
|
+
corpus.paths; // ["docs/index.md"]
|
|
13
|
+
corpus.text("docs/index.md"); // "# Home\n"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
A corpus is someone else's repository:
|
|
17
|
+
|
|
18
|
+
- only plain paths enter it: no `..`, `.`, empty segment, leading `/`, backslash or control
|
|
19
|
+
character (`isPlainPath` from `@dogsbay/detect`);
|
|
20
|
+
- a folder corpus keeps to its folder once symlinks resolve, lists each real directory once,
|
|
21
|
+
skips `node_modules` and `.git`, and answers `contains(path, dir)`: does `path` still lie
|
|
22
|
+
in `dir` once links resolve? An importer uses it to keep pages and assets in the docs root.
|
|
23
|
+
|
|
24
|
+
## License
|
|
25
|
+
|
|
26
|
+
MIT
|
package/dist/folder.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Corpus } from "./index.js";
|
|
2
|
+
export interface FolderCorpus extends Corpus {
|
|
3
|
+
/** The folder's absolute path. */
|
|
4
|
+
root: string;
|
|
5
|
+
/** A corpus path as an absolute path on disk. */
|
|
6
|
+
absolute(path: string): string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* List `dir` (skipping `node_modules` and `.git`) and read files from it on demand. A
|
|
10
|
+
* directory reached by two names is listed under the first one walked, so `first` names the
|
|
11
|
+
* folders to walk before the rest (the docs roots): a `docs -> content-src` link must list the
|
|
12
|
+
* pages under `docs/`, where `root: ./docs/` looks for them.
|
|
13
|
+
*/
|
|
14
|
+
export declare function readFolder(dir: string, maxDepth?: number, first?: string[]): FolderCorpus;
|
package/dist/folder.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node only: a folder as a {@link Corpus}. Everything else in an importer reads the corpus, so
|
|
3
|
+
* the CLI and the browser import run the same code.
|
|
4
|
+
*
|
|
5
|
+
* The walk keeps to the folder once links resolve, and visits each directory once: a symlink
|
|
6
|
+
* out of the repo read pages from elsewhere, and two links to `.` made the walk branch twice
|
|
7
|
+
* per level until ELOOP (about 2^40 visits). Text is read only when asked for.
|
|
8
|
+
*/
|
|
9
|
+
import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
10
|
+
import { join, sep } from "node:path";
|
|
11
|
+
import { isPlainPath } from "@dogsbay/detect";
|
|
12
|
+
/**
|
|
13
|
+
* List `dir` (skipping `node_modules` and `.git`) and read files from it on demand. A
|
|
14
|
+
* directory reached by two names is listed under the first one walked, so `first` names the
|
|
15
|
+
* folders to walk before the rest (the docs roots): a `docs -> content-src` link must list the
|
|
16
|
+
* pages under `docs/`, where `root: ./docs/` looks for them.
|
|
17
|
+
*/
|
|
18
|
+
export function readFolder(dir, maxDepth = Infinity, first = []) {
|
|
19
|
+
const paths = [];
|
|
20
|
+
/** Each listed path's real path, for `contains`. */
|
|
21
|
+
const reals = new Map();
|
|
22
|
+
let realRoot;
|
|
23
|
+
try {
|
|
24
|
+
realRoot = realpathSync(dir);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
realRoot = "";
|
|
28
|
+
}
|
|
29
|
+
if (realRoot) {
|
|
30
|
+
const seen = new Set([realRoot]);
|
|
31
|
+
const walk = (abs, rel, depth) => {
|
|
32
|
+
let entries;
|
|
33
|
+
try {
|
|
34
|
+
entries = readdirSync(abs).sort();
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
for (const name of entries) {
|
|
40
|
+
if (name === "node_modules" || name === ".git")
|
|
41
|
+
continue;
|
|
42
|
+
const full = join(abs, name);
|
|
43
|
+
const path = rel ? `${rel}/${name}` : name;
|
|
44
|
+
if (!isPlainPath(path))
|
|
45
|
+
continue;
|
|
46
|
+
let real;
|
|
47
|
+
let st;
|
|
48
|
+
try {
|
|
49
|
+
real = realpathSync(full);
|
|
50
|
+
if (real !== realRoot && !real.startsWith(realRoot + sep))
|
|
51
|
+
continue;
|
|
52
|
+
st = statSync(real);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (st.isDirectory()) {
|
|
58
|
+
if (seen.has(real) || depth + 1 > maxDepth)
|
|
59
|
+
continue;
|
|
60
|
+
seen.add(real);
|
|
61
|
+
walk(full, path, depth + 1);
|
|
62
|
+
}
|
|
63
|
+
else if (st.isFile()) {
|
|
64
|
+
paths.push(path);
|
|
65
|
+
reals.set(path, real);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
for (const d of first) {
|
|
70
|
+
if (!d || !isPlainPath(d))
|
|
71
|
+
continue;
|
|
72
|
+
let real;
|
|
73
|
+
try {
|
|
74
|
+
real = realpathSync(join(dir, d));
|
|
75
|
+
if (!real.startsWith(realRoot + sep) || seen.has(real) || !statSync(real).isDirectory())
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
seen.add(real);
|
|
82
|
+
walk(join(dir, d), d, d.split("/").length);
|
|
83
|
+
}
|
|
84
|
+
walk(dir, "", 0);
|
|
85
|
+
}
|
|
86
|
+
const listed = new Set(paths);
|
|
87
|
+
const realDirs = new Map();
|
|
88
|
+
const realDir = (d) => {
|
|
89
|
+
if (!realDirs.has(d)) {
|
|
90
|
+
let r;
|
|
91
|
+
try {
|
|
92
|
+
r = realpathSync(join(dir, d));
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
r = undefined;
|
|
96
|
+
}
|
|
97
|
+
realDirs.set(d, r);
|
|
98
|
+
}
|
|
99
|
+
return realDirs.get(d);
|
|
100
|
+
};
|
|
101
|
+
return {
|
|
102
|
+
root: dir,
|
|
103
|
+
paths,
|
|
104
|
+
absolute: (path) => join(dir, path),
|
|
105
|
+
contains(path, d) {
|
|
106
|
+
const real = reals.get(path);
|
|
107
|
+
const root = realDir(d);
|
|
108
|
+
return !!real && !!root && (real === root || real.startsWith(root + sep));
|
|
109
|
+
},
|
|
110
|
+
text(path) {
|
|
111
|
+
if (!listed.has(path))
|
|
112
|
+
return undefined;
|
|
113
|
+
try {
|
|
114
|
+
return readFileSync(join(dir, path), "utf-8");
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface Corpus {
|
|
2
|
+
/** Every file, posix, relative to the corpus root; plain paths only (`isPlainPath`). */
|
|
3
|
+
paths: string[];
|
|
4
|
+
/** The file's text, or undefined when it is not in the corpus. */
|
|
5
|
+
text(path: string): string | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* Does `path` still lie inside the folder `dir` once links resolve? A folder corpus answers
|
|
8
|
+
* (a symlink in the docs root must not bring in the repo's `.env` or `.git`); a map of files
|
|
9
|
+
* has no links and leaves it out.
|
|
10
|
+
*/
|
|
11
|
+
contains?(path: string, dir: string): boolean;
|
|
12
|
+
}
|
|
13
|
+
export type CorpusFiles = Map<string, string | Uint8Array>;
|
|
14
|
+
export interface MapCorpusOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Every path of the repo (relative to the same root as `files`), when the host knows more
|
|
17
|
+
* files than it has fetched — the playground lists a GitHub tree and fetches as it goes.
|
|
18
|
+
* The corpus lists them all, so the import sees the repo as it is; reading one that `files`
|
|
19
|
+
* does not hold returns undefined and is recorded (`missing`).
|
|
20
|
+
*/
|
|
21
|
+
paths?: Iterable<string>;
|
|
22
|
+
}
|
|
23
|
+
export interface MapCorpus extends Corpus {
|
|
24
|
+
/** Listed paths the import read but `files` did not hold, sorted: fetch them and run it again. */
|
|
25
|
+
missing(): string[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A corpus over a map of files; bytes are read as UTF-8. A key that is not a plain path
|
|
29
|
+
* (`../x`, `/etc/x`, `a/./b`, a backslash) is left out: the keys come from someone else's repo.
|
|
30
|
+
*/
|
|
31
|
+
export declare function corpusFromMap(files: CorpusFiles, opts?: MapCorpusOptions): MapCorpus;
|
|
32
|
+
/**
|
|
33
|
+
* What a browser conversion still needs, when the host listed more paths than it fetched:
|
|
34
|
+
* `read` the import read (fetch them and convert again: the result changes), `copy` the
|
|
35
|
+
* output copies as they are (fetch them for the finished files; nothing is converted again).
|
|
36
|
+
* Both empty: the result is complete. Each round can find one more level of an include or
|
|
37
|
+
* `require` chain, so a host caps the rounds; a result with `read` not empty is not a
|
|
38
|
+
* conversion (its `files` may be empty: the import could not go on without them).
|
|
39
|
+
*/
|
|
40
|
+
export interface Unfetched {
|
|
41
|
+
read: string[];
|
|
42
|
+
copy: string[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The ledger message for a conversion that still needs files (undefined when none), with the
|
|
46
|
+
* error the import stopped on, when it stopped.
|
|
47
|
+
*/
|
|
48
|
+
export declare function unfetchedMessage(read: string[], stoppedOn?: unknown): string | undefined;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @dogsbay/corpus — a corpus as an importer sees it: its file paths (posix, relative to the
|
|
3
|
+
* corpus root) and a way to read one as text. The CLI builds one from a folder
|
|
4
|
+
* (`@dogsbay/corpus/folder`, which also keeps symlinks inside it); the browser builds one from
|
|
5
|
+
* a map of files (`corpusFromMap`). Assets are never read through it: a project lists them as
|
|
6
|
+
* copies, and each host copies the bytes its own way. No Node here.
|
|
7
|
+
*
|
|
8
|
+
* A corpus is someone else's repository: only plain paths enter it (`isPlainPath`).
|
|
9
|
+
*/
|
|
10
|
+
import { isPlainPath } from "@dogsbay/detect";
|
|
11
|
+
/**
|
|
12
|
+
* A corpus over a map of files; bytes are read as UTF-8. A key that is not a plain path
|
|
13
|
+
* (`../x`, `/etc/x`, `a/./b`, a backslash) is left out: the keys come from someone else's repo.
|
|
14
|
+
*/
|
|
15
|
+
export function corpusFromMap(files, opts = {}) {
|
|
16
|
+
const decoder = new TextDecoder();
|
|
17
|
+
const paths = [...new Set([...files.keys(), ...(opts.paths ?? [])])].filter(isPlainPath).sort();
|
|
18
|
+
const listed = new Set(paths);
|
|
19
|
+
const missing = new Set();
|
|
20
|
+
return {
|
|
21
|
+
paths,
|
|
22
|
+
text(path) {
|
|
23
|
+
if (!listed.has(path))
|
|
24
|
+
return undefined;
|
|
25
|
+
const v = files.get(path);
|
|
26
|
+
if (v === undefined) {
|
|
27
|
+
missing.add(path);
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
return typeof v === "string" ? v : decoder.decode(v);
|
|
31
|
+
},
|
|
32
|
+
missing: () => [...missing].sort(),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The ledger message for a conversion that still needs files (undefined when none), with the
|
|
37
|
+
* error the import stopped on, when it stopped.
|
|
38
|
+
*/
|
|
39
|
+
export function unfetchedMessage(read, stoppedOn) {
|
|
40
|
+
if (!read.length)
|
|
41
|
+
return undefined;
|
|
42
|
+
const names = `${read.slice(0, 3).join(", ")}${read.length > 3 ? ", …" : ""}`;
|
|
43
|
+
const stopped = stoppedOn instanceof Error ? ` It stopped on: ${stoppedOn.message}` : "";
|
|
44
|
+
return `${read.length} file${read.length === 1 ? "" : "s"} the conversion reads ${read.length === 1 ? "was" : "were"} not given (${names}): fetch them and convert again.${stopped}`;
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dogsbay/corpus",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0-beta.133",
|
|
4
|
+
"description": "A documentation repository as its importers read it, in the browser or in Node: plain paths plus text on demand, from a map of files or from a folder whose symlinks are kept inside it",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./folder": {
|
|
14
|
+
"types": "./dist/folder.d.ts",
|
|
15
|
+
"import": "./dist/folder.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@dogsbay/detect": "0.2.0-beta.133"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^22.20.1",
|
|
27
|
+
"esbuild": "^0.25.0",
|
|
28
|
+
"typescript": "^5.9.3",
|
|
29
|
+
"vitest": "^4.1.10"
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/dogsbay/dogsbay.git",
|
|
35
|
+
"directory": "packages/corpus"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"test:watch": "vitest"
|
|
41
|
+
}
|
|
6
42
|
}
|