@dogsbay/corpus 0.0.0-stage → 0.2.0-beta.132

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 CHANGED
@@ -1,3 +1,26 @@
1
- # Temporary Holding Version
1
+ # @dogsbay/corpus
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
@@ -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
+ }
@@ -0,0 +1,18 @@
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
+ /**
15
+ * A corpus over a map of files; bytes are read as UTF-8. A key that is not a plain path
16
+ * (`../x`, `/etc/x`, `a/./b`, a backslash) is left out: the keys come from someone else's repo.
17
+ */
18
+ export declare function corpusFromMap(files: CorpusFiles): Corpus;
package/dist/index.js ADDED
@@ -0,0 +1,28 @@
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) {
16
+ const decoder = new TextDecoder();
17
+ const paths = [...files.keys()].filter(isPlainPath).sort();
18
+ const listed = new Set(paths);
19
+ return {
20
+ paths,
21
+ text(path) {
22
+ if (!listed.has(path))
23
+ return undefined;
24
+ const v = files.get(path);
25
+ return v === undefined ? undefined : typeof v === "string" ? v : decoder.decode(v);
26
+ },
27
+ };
28
+ }
package/package.json CHANGED
@@ -1,6 +1,42 @@
1
1
  {
2
2
  "name": "@dogsbay/corpus",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0-beta.132",
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.132"
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
  }