@contentrail/reader 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +83 -2
- package/dist/errors.d.ts +16 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +27 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/load.d.ts +16 -0
- package/dist/load.d.ts.map +1 -0
- package/dist/load.js +153 -0
- package/dist/load.js.map +1 -0
- package/dist/paths.d.ts +12 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +84 -0
- package/dist/paths.js.map +1 -0
- package/dist/source.d.ts +15 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +19 -0
- package/dist/source.js.map +1 -0
- package/dist/types.d.ts +27 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +47 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ContentRail contributors
|
|
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
CHANGED
|
@@ -1,3 +1,84 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@contentrail/reader`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Node.js / build-time reader for local JSON content. Validates entries with `@contentrail/core`.
|
|
4
|
+
|
|
5
|
+
**Not browser-safe.** Do not import this package from client bundles. Use it in Astro frontmatter, Vite plugins, or other build-time Node code.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Node.js **≥ 22**
|
|
10
|
+
- `@contentrail/core` (and Zod via core)
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
> **Not on the public npm registry yet.** See `docs/PUBLISHING.md`.
|
|
15
|
+
|
|
16
|
+
### Monorepo (current)
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
pnpm install
|
|
20
|
+
pnpm --filter @contentrail/reader build
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Intended after first public `0.1.0` release
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
pnpm add @contentrail/core @contentrail/reader zod
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Layout
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
{rootDir}/
|
|
33
|
+
{collection.name}/
|
|
34
|
+
{entryId}.json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Example: `content/articles/welcome.json`
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { defineCollection } from "@contentrail/core";
|
|
43
|
+
import {
|
|
44
|
+
createLocalJsonSource,
|
|
45
|
+
loadCollection,
|
|
46
|
+
loadEntry,
|
|
47
|
+
} from "@contentrail/reader";
|
|
48
|
+
import { z } from "zod";
|
|
49
|
+
|
|
50
|
+
const articles = defineCollection({
|
|
51
|
+
name: "articles",
|
|
52
|
+
schema: z.object({
|
|
53
|
+
title: z.string().min(1),
|
|
54
|
+
slug: z.string().min(1),
|
|
55
|
+
published: z.boolean(),
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const source = await createLocalJsonSource({ rootDir: "./content" });
|
|
60
|
+
const all = await loadCollection(source, articles);
|
|
61
|
+
const one = await loadEntry(source, articles, "welcome");
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Behaviour
|
|
65
|
+
|
|
66
|
+
| Case | Result |
|
|
67
|
+
| ---------------------------- | ---------------------------------------------------------------------- |
|
|
68
|
+
| Valid files | Returned as `{ id, sourcePath, data }` |
|
|
69
|
+
| Empty collection directory | `[]` |
|
|
70
|
+
| Missing collection directory | `ContentReadError` `MISSING_COLLECTION` |
|
|
71
|
+
| Missing entry file | `ContentReadError` `MISSING_ENTRY` |
|
|
72
|
+
| Malformed JSON | `ContentReadError` `INVALID_JSON` |
|
|
73
|
+
| Schema failure | `ContentReadError` `INVALID_ENTRY` (cause is `ContentValidationError`) |
|
|
74
|
+
| Path escape / unsafe segment | `ContentReadError` `PATH_ESCAPE` |
|
|
75
|
+
| Duplicate file ids | `ContentReadError` `DUPLICATE_ID` |
|
|
76
|
+
| Duplicate `slug` values | `ContentReadError` `DUPLICATE_SLUG` (disable with `uniqueBy: null`) |
|
|
77
|
+
|
|
78
|
+
Default sort: entry `id` ascending. Invalid content always fails the load (no silent skip).
|
|
79
|
+
|
|
80
|
+
## Compatibility
|
|
81
|
+
|
|
82
|
+
- Node.js 22+
|
|
83
|
+
- ESM only
|
|
84
|
+
- Depends on `@contentrail/core` (Zod schemas)
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ContentValidationError } from "@contentrail/core";
|
|
2
|
+
export type ContentReadErrorCode = "INVALID_ROOT" | "PATH_ESCAPE" | "MISSING_COLLECTION" | "MISSING_ENTRY" | "INVALID_JSON" | "INVALID_ENTRY" | "DUPLICATE_ID" | "DUPLICATE_SLUG" | "EMPTY_ID";
|
|
3
|
+
export declare class ContentReadError extends Error {
|
|
4
|
+
readonly code: ContentReadErrorCode;
|
|
5
|
+
readonly collection?: string;
|
|
6
|
+
readonly entryId?: string;
|
|
7
|
+
readonly path?: string;
|
|
8
|
+
constructor(code: ContentReadErrorCode, message: string, options?: {
|
|
9
|
+
collection?: string;
|
|
10
|
+
entryId?: string;
|
|
11
|
+
path?: string;
|
|
12
|
+
cause?: unknown;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export declare function isContentValidationError(error: unknown): error is ContentValidationError;
|
|
16
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAEhE,MAAM,MAAM,oBAAoB,GAC5B,cAAc,GACd,aAAa,GACb,oBAAoB,GACpB,eAAe,GACf,cAAc,GACd,eAAe,GACf,cAAc,GACd,gBAAgB,GAChB,UAAU,CAAC;AAEf,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;gBAGrB,IAAI,EAAE,oBAAoB,EAC1B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;QACP,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,OAAO,CAAC;KACZ;CAkBT;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,sBAAsB,CAOjC"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export class ContentReadError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
collection;
|
|
4
|
+
entryId;
|
|
5
|
+
path;
|
|
6
|
+
constructor(code, message, options = {}) {
|
|
7
|
+
super(message, options.cause !== undefined ? { cause: options.cause } : undefined);
|
|
8
|
+
this.name = "ContentReadError";
|
|
9
|
+
this.code = code;
|
|
10
|
+
if (options.collection !== undefined) {
|
|
11
|
+
this.collection = options.collection;
|
|
12
|
+
}
|
|
13
|
+
if (options.entryId !== undefined) {
|
|
14
|
+
this.entryId = options.entryId;
|
|
15
|
+
}
|
|
16
|
+
if (options.path !== undefined) {
|
|
17
|
+
this.path = options.path;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function isContentValidationError(error) {
|
|
22
|
+
return (typeof error === "object" &&
|
|
23
|
+
error !== null &&
|
|
24
|
+
"code" in error &&
|
|
25
|
+
error.code === "CONTENT_VALIDATION_ERROR");
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAaA,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAChC,IAAI,CAAuB;IAC3B,UAAU,CAAU;IACpB,OAAO,CAAU;IACjB,IAAI,CAAU;IAEvB,YACE,IAA0B,EAC1B,OAAe,EACf,UAKI,EAAE;QAEN,KAAK,CACH,OAAO,EACP,OAAO,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CACnE,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;QAC/B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,OAAO,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;YACrC,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;QACvC,CAAC;QACD,IAAI,OAAO,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;YAClC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QACjC,CAAC;QACD,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC/B,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QAC3B,CAAC;IACH,CAAC;CACF;AAED,MAAM,UAAU,wBAAwB,CACtC,KAAc;IAEd,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,MAAM,IAAI,KAAK;QACd,KAA2B,CAAC,IAAI,KAAK,0BAA0B,CACjE,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { ContentReadError, type ContentReadErrorCode } from "./errors.js";
|
|
2
|
+
export { createLocalJsonSource } from "./source.js";
|
|
3
|
+
export { loadCollection, loadEntry } from "./load.js";
|
|
4
|
+
export type { ContentRailEntry, LoadCollectionOptions, LocalJsonSource, } from "./types.js";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAC1E,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtD,YAAY,EACV,gBAAgB,EAChB,qBAAqB,EACrB,eAAe,GAChB,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAA6B,MAAM,aAAa,CAAC;AAC1E,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC"}
|
package/dist/load.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ContentCollection } from "@contentrail/core";
|
|
2
|
+
import type { ContentRailEntry, LoadCollectionOptions, LocalJsonSource } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Loads and validates a single entry from `{root}/{collection}/{id}.json`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function loadEntry<TCollection extends ContentCollection>(source: LocalJsonSource, collection: TCollection, id: string): Promise<ContentRailEntry<TCollection>>;
|
|
7
|
+
/**
|
|
8
|
+
* Loads every `*.json` file in `{root}/{collection}/`, validates each entry,
|
|
9
|
+
* and returns them in deterministic order (by `id` ascending by default).
|
|
10
|
+
*
|
|
11
|
+
* An existing empty collection directory yields an empty array.
|
|
12
|
+
* A missing collection directory fails with `MISSING_COLLECTION`.
|
|
13
|
+
* Invalid JSON or schema failures fail the whole load (no silent skip).
|
|
14
|
+
*/
|
|
15
|
+
export declare function loadCollection<TCollection extends ContentCollection>(source: LocalJsonSource, collection: TCollection, options?: LoadCollectionOptions): Promise<Array<ContentRailEntry<TCollection>>>;
|
|
16
|
+
//# sourceMappingURL=load.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../src/load.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,iBAAiB,EAAiB,MAAM,mBAAmB,CAAC;AAO1E,OAAO,KAAK,EACV,gBAAgB,EAChB,qBAAqB,EACrB,eAAe,EAChB,MAAM,YAAY,CAAC;AAkEpB;;GAEG;AACH,wBAAsB,SAAS,CAAC,WAAW,SAAS,iBAAiB,EACnE,MAAM,EAAE,eAAe,EACvB,UAAU,EAAE,WAAW,EACvB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,gBAAgB,CAAC,WAAW,CAAC,CAAC,CAqCxC;AAED;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAAC,WAAW,SAAS,iBAAiB,EACxE,MAAM,EAAE,eAAe,EACvB,UAAU,EAAE,WAAW,EACvB,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,KAAK,CAAC,gBAAgB,CAAC,WAAW,CAAC,CAAC,CAAC,CA+D/C"}
|
package/dist/load.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { validateEntry } from "@contentrail/core";
|
|
3
|
+
import { ContentReadError } from "./errors.js";
|
|
4
|
+
import { assertSafeSegment, entryIdFromFileName, resolveUnderRoot, } from "./paths.js";
|
|
5
|
+
function assertSource(source) {
|
|
6
|
+
if (source === null ||
|
|
7
|
+
typeof source !== "object" ||
|
|
8
|
+
source.kind !== "local-json" ||
|
|
9
|
+
typeof source.rootDir !== "string") {
|
|
10
|
+
throw new TypeError("Expected a LocalJsonSource from createLocalJsonSource().");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
async function readJsonFile(filePath) {
|
|
14
|
+
let raw;
|
|
15
|
+
try {
|
|
16
|
+
raw = await fs.readFile(filePath, "utf8");
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
throw new ContentReadError("MISSING_ENTRY", `Unable to read content file: ${filePath}`, { path: filePath, cause: error });
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(raw);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
throw new ContentReadError("INVALID_JSON", `Malformed JSON in content file: ${filePath}`, { path: filePath, cause: error });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function loadValidatedEntry(source, collection, id, filePath) {
|
|
29
|
+
const json = await readJsonFile(filePath);
|
|
30
|
+
const result = validateEntry(collection, json);
|
|
31
|
+
if (!result.success) {
|
|
32
|
+
throw new ContentReadError("INVALID_ENTRY", `Invalid content for collection "${collection.name}" entry "${id}": ${result.error.message}`, {
|
|
33
|
+
collection: collection.name,
|
|
34
|
+
entryId: id,
|
|
35
|
+
path: filePath,
|
|
36
|
+
cause: result.error,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return Object.freeze({
|
|
40
|
+
id,
|
|
41
|
+
sourcePath: filePath,
|
|
42
|
+
data: result.data,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Loads and validates a single entry from `{root}/{collection}/{id}.json`.
|
|
47
|
+
*/
|
|
48
|
+
export async function loadEntry(source, collection, id) {
|
|
49
|
+
assertSource(source);
|
|
50
|
+
assertSafeSegment(collection.name, "collection name");
|
|
51
|
+
assertSafeSegment(id, "entry id");
|
|
52
|
+
let filePath;
|
|
53
|
+
try {
|
|
54
|
+
filePath = await resolveUnderRoot(source.rootDir, [collection.name, `${id}.json`], { mustExist: true });
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (error instanceof ContentReadError && error.code === "PATH_ESCAPE") {
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
throw new ContentReadError("MISSING_ENTRY", `Content entry not found: ${collection.name}/${id}.json`, {
|
|
61
|
+
collection: collection.name,
|
|
62
|
+
entryId: id,
|
|
63
|
+
cause: error,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const stat = await fs.stat(filePath);
|
|
67
|
+
if (!stat.isFile()) {
|
|
68
|
+
throw new ContentReadError("MISSING_ENTRY", `Content entry path is not a file: ${filePath}`, { collection: collection.name, entryId: id, path: filePath });
|
|
69
|
+
}
|
|
70
|
+
return loadValidatedEntry(source, collection, id, filePath);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Loads every `*.json` file in `{root}/{collection}/`, validates each entry,
|
|
74
|
+
* and returns them in deterministic order (by `id` ascending by default).
|
|
75
|
+
*
|
|
76
|
+
* An existing empty collection directory yields an empty array.
|
|
77
|
+
* A missing collection directory fails with `MISSING_COLLECTION`.
|
|
78
|
+
* Invalid JSON or schema failures fail the whole load (no silent skip).
|
|
79
|
+
*/
|
|
80
|
+
export async function loadCollection(source, collection, options = {}) {
|
|
81
|
+
assertSource(source);
|
|
82
|
+
assertSafeSegment(collection.name, "collection name");
|
|
83
|
+
let collectionDir;
|
|
84
|
+
try {
|
|
85
|
+
collectionDir = await resolveUnderRoot(source.rootDir, [collection.name], {
|
|
86
|
+
mustExist: true,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (error instanceof ContentReadError && error.code === "PATH_ESCAPE") {
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
throw new ContentReadError("MISSING_COLLECTION", `Content collection directory not found: ${collection.name}`, { collection: collection.name, cause: error });
|
|
94
|
+
}
|
|
95
|
+
const dirStat = await fs.stat(collectionDir);
|
|
96
|
+
if (!dirStat.isDirectory()) {
|
|
97
|
+
throw new ContentReadError("MISSING_COLLECTION", `Content collection path is not a directory: ${collectionDir}`, { collection: collection.name, path: collectionDir });
|
|
98
|
+
}
|
|
99
|
+
const dirents = await fs.readdir(collectionDir, { withFileTypes: true });
|
|
100
|
+
const jsonFiles = dirents
|
|
101
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
102
|
+
.map((entry) => entry.name)
|
|
103
|
+
.sort((a, b) => a.localeCompare(b));
|
|
104
|
+
const entries = [];
|
|
105
|
+
const seenIds = new Set();
|
|
106
|
+
for (const fileName of jsonFiles) {
|
|
107
|
+
const id = entryIdFromFileName(fileName);
|
|
108
|
+
if (seenIds.has(id)) {
|
|
109
|
+
throw new ContentReadError("DUPLICATE_ID", `Duplicate entry id "${id}" in collection "${collection.name}".`, { collection: collection.name, entryId: id });
|
|
110
|
+
}
|
|
111
|
+
seenIds.add(id);
|
|
112
|
+
const filePath = await resolveUnderRoot(source.rootDir, [collection.name, fileName], { mustExist: true });
|
|
113
|
+
entries.push(await loadValidatedEntry(source, collection, id, filePath));
|
|
114
|
+
}
|
|
115
|
+
const uniqueBy = options.uniqueBy === undefined ? "slug" : options.uniqueBy;
|
|
116
|
+
if (uniqueBy !== null) {
|
|
117
|
+
assertUniqueField(collection.name, entries, uniqueBy);
|
|
118
|
+
}
|
|
119
|
+
return sortEntries(entries, options);
|
|
120
|
+
}
|
|
121
|
+
function assertUniqueField(collectionName, entries, field) {
|
|
122
|
+
const seen = new Map();
|
|
123
|
+
for (const entry of entries) {
|
|
124
|
+
const value = entry.data[field];
|
|
125
|
+
if (value === undefined) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (typeof value !== "string") {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const previous = seen.get(value);
|
|
132
|
+
if (previous !== undefined) {
|
|
133
|
+
throw new ContentReadError("DUPLICATE_SLUG", `Duplicate "${field}" value "${value}" in collection "${collectionName}" (entries "${previous}" and "${entry.id}").`, { collection: collectionName, entryId: entry.id });
|
|
134
|
+
}
|
|
135
|
+
seen.set(value, entry.id);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function sortEntries(entries, options) {
|
|
139
|
+
const sortBy = options.sortBy ?? "id";
|
|
140
|
+
const sortOrder = options.sortOrder ?? "asc";
|
|
141
|
+
const direction = sortOrder === "desc" ? -1 : 1;
|
|
142
|
+
const sorted = [...entries].sort((left, right) => {
|
|
143
|
+
const leftValue = sortBy === "id"
|
|
144
|
+
? left.id
|
|
145
|
+
: String(left.data[sortBy] ?? "");
|
|
146
|
+
const rightValue = sortBy === "id"
|
|
147
|
+
? right.id
|
|
148
|
+
: String(right.data[sortBy] ?? "");
|
|
149
|
+
return leftValue.localeCompare(rightValue) * direction;
|
|
150
|
+
});
|
|
151
|
+
return sorted;
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=load.js.map
|
package/dist/load.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load.js","sourceRoot":"","sources":["../src/load.ts"],"names":[],"mappings":"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"}
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function resolveRootDir(rootDir: string): Promise<string>;
|
|
2
|
+
export declare function assertSafeSegment(segment: string, label: string): void;
|
|
3
|
+
/**
|
|
4
|
+
* Joins segments under root and ensures the normalized path stays inside root.
|
|
5
|
+
* When the path exists, resolves symlinks via realpath and re-checks containment.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveUnderRoot(rootDir: string, segments: readonly string[], options?: {
|
|
8
|
+
mustExist?: boolean;
|
|
9
|
+
}): Promise<string>;
|
|
10
|
+
export declare function isPathInsideRoot(rootDir: string, candidate: string): boolean;
|
|
11
|
+
export declare function entryIdFromFileName(fileName: string): string;
|
|
12
|
+
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAMA,wBAAsB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA8BrE;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAetE;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAO,GACpC,OAAO,CAAC,MAAM,CAAC,CAsCjB;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAQ5E;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAU5D"}
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ContentReadError } from "./errors.js";
|
|
4
|
+
const SEGMENT_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
5
|
+
export async function resolveRootDir(rootDir) {
|
|
6
|
+
if (typeof rootDir !== "string" || rootDir.trim() === "") {
|
|
7
|
+
throw new ContentReadError("INVALID_ROOT", 'Local JSON source requires a non-empty "rootDir" string.');
|
|
8
|
+
}
|
|
9
|
+
const absolute = path.resolve(rootDir);
|
|
10
|
+
try {
|
|
11
|
+
const real = await fs.realpath(absolute);
|
|
12
|
+
const stat = await fs.stat(real);
|
|
13
|
+
if (!stat.isDirectory()) {
|
|
14
|
+
throw new ContentReadError("INVALID_ROOT", `Content root is not a directory: ${real}`, { path: real });
|
|
15
|
+
}
|
|
16
|
+
return real;
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error instanceof ContentReadError) {
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
throw new ContentReadError("INVALID_ROOT", `Content root does not exist or is not readable: ${absolute}`, { path: absolute, cause: error });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function assertSafeSegment(segment, label) {
|
|
26
|
+
if (segment === "" ||
|
|
27
|
+
segment === "." ||
|
|
28
|
+
segment === ".." ||
|
|
29
|
+
segment.includes("/") ||
|
|
30
|
+
segment.includes("\\") ||
|
|
31
|
+
segment.includes("\0") ||
|
|
32
|
+
!SEGMENT_PATTERN.test(segment)) {
|
|
33
|
+
throw new ContentReadError("PATH_ESCAPE", `Unsafe ${label} segment rejected: ${JSON.stringify(segment)}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Joins segments under root and ensures the normalized path stays inside root.
|
|
38
|
+
* When the path exists, resolves symlinks via realpath and re-checks containment.
|
|
39
|
+
*/
|
|
40
|
+
export async function resolveUnderRoot(rootDir, segments, options = {}) {
|
|
41
|
+
for (const segment of segments) {
|
|
42
|
+
assertSafeSegment(segment, "path");
|
|
43
|
+
}
|
|
44
|
+
const joined = path.resolve(rootDir, ...segments);
|
|
45
|
+
if (!isPathInsideRoot(rootDir, joined)) {
|
|
46
|
+
throw new ContentReadError("PATH_ESCAPE", `Resolved path escapes content root: ${joined}`, { path: joined });
|
|
47
|
+
}
|
|
48
|
+
if (options.mustExist === false) {
|
|
49
|
+
return joined;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const real = await fs.realpath(joined);
|
|
53
|
+
if (!isPathInsideRoot(rootDir, real)) {
|
|
54
|
+
throw new ContentReadError("PATH_ESCAPE", `Resolved path escapes content root after symlink resolution: ${real}`, { path: real });
|
|
55
|
+
}
|
|
56
|
+
return real;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (error instanceof ContentReadError) {
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
if (options.mustExist === true) {
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
// Path does not exist yet — return the verified joined path.
|
|
66
|
+
return joined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function isPathInsideRoot(rootDir, candidate) {
|
|
70
|
+
const relative = path.relative(rootDir, candidate);
|
|
71
|
+
return (relative === "" ||
|
|
72
|
+
(!relative.startsWith(`..${path.sep}`) &&
|
|
73
|
+
relative !== ".." &&
|
|
74
|
+
!path.isAbsolute(relative)));
|
|
75
|
+
}
|
|
76
|
+
export function entryIdFromFileName(fileName) {
|
|
77
|
+
if (!fileName.endsWith(".json")) {
|
|
78
|
+
throw new ContentReadError("EMPTY_ID", `Content files must use a .json extension: ${fileName}`);
|
|
79
|
+
}
|
|
80
|
+
const id = fileName.slice(0, -".json".length);
|
|
81
|
+
assertSafeSegment(id, "entry id");
|
|
82
|
+
return id;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"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"}
|
package/dist/source.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { LocalJsonSource } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Creates a local JSON content source rooted at `rootDir`.
|
|
4
|
+
*
|
|
5
|
+
* Layout:
|
|
6
|
+
* ```text
|
|
7
|
+
* {rootDir}/{collection.name}/{entryId}.json
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* This API is Node.js / build-time only. Do not import it from browser bundles.
|
|
11
|
+
*/
|
|
12
|
+
export declare function createLocalJsonSource(options: {
|
|
13
|
+
rootDir: string;
|
|
14
|
+
}): Promise<LocalJsonSource>;
|
|
15
|
+
//# sourceMappingURL=source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source.d.ts","sourceRoot":"","sources":["../src/source.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;;;;;;;;;GASG;AACH,wBAAsB,qBAAqB,CAAC,OAAO,EAAE;IACnD,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,eAAe,CAAC,CAM3B"}
|
package/dist/source.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { resolveRootDir } from "./paths.js";
|
|
2
|
+
/**
|
|
3
|
+
* Creates a local JSON content source rooted at `rootDir`.
|
|
4
|
+
*
|
|
5
|
+
* Layout:
|
|
6
|
+
* ```text
|
|
7
|
+
* {rootDir}/{collection.name}/{entryId}.json
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* This API is Node.js / build-time only. Do not import it from browser bundles.
|
|
11
|
+
*/
|
|
12
|
+
export async function createLocalJsonSource(options) {
|
|
13
|
+
const rootDir = await resolveRootDir(options.rootDir);
|
|
14
|
+
return Object.freeze({
|
|
15
|
+
kind: "local-json",
|
|
16
|
+
rootDir,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source.js","sourceRoot":"","sources":["../src/source.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAG5C;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,OAE3C;IACC,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACtD,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,IAAI,EAAE,YAAqB;QAC3B,OAAO;KACR,CAAC,CAAC;AACL,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ContentCollection, InferEntry } from "@contentrail/core";
|
|
2
|
+
export interface LocalJsonSource {
|
|
3
|
+
readonly kind: "local-json";
|
|
4
|
+
readonly rootDir: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ContentRailEntry<TCollection extends ContentCollection> {
|
|
7
|
+
/** File basename without `.json`. */
|
|
8
|
+
readonly id: string;
|
|
9
|
+
/** Absolute path of the source file (build-time only; do not ship to browsers). */
|
|
10
|
+
readonly sourcePath: string;
|
|
11
|
+
readonly data: InferEntry<TCollection>;
|
|
12
|
+
}
|
|
13
|
+
export interface LoadCollectionOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Sort key. `"id"` (default) sorts by entry id.
|
|
16
|
+
* Any other string sorts by that top-level data field as a string.
|
|
17
|
+
*/
|
|
18
|
+
readonly sortBy?: "id" | (string & {});
|
|
19
|
+
/** Default: `"asc"`. */
|
|
20
|
+
readonly sortOrder?: "asc" | "desc";
|
|
21
|
+
/**
|
|
22
|
+
* Field used to detect duplicate public identifiers across entries.
|
|
23
|
+
* Defaults to `"slug"` when present on entries; set `null` to disable.
|
|
24
|
+
*/
|
|
25
|
+
readonly uniqueBy?: string | null;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEvE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB,CAAC,WAAW,SAAS,iBAAiB;IACrE,qCAAqC;IACrC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,mFAAmF;IACnF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC,WAAW,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;IACvC,wBAAwB;IACxB,QAAQ,CAAC,SAAS,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IACpC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentrail/reader",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Build-time ContentRail local JSON content reader (Node.js).",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=22"
|
|
10
|
+
},
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"node": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@contentrail/core": "0.1.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22.20.5",
|
|
30
|
+
"typescript": "5.9.3",
|
|
31
|
+
"zod": "3.25.76"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/Hridhin-k/contentrail.git",
|
|
36
|
+
"directory": "packages/reader"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/Hridhin-k/contentrail/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/Hridhin-k/contentrail#readme",
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc -p tsconfig.json",
|
|
47
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
48
|
+
"test": "pnpm run build && tsc -p tsconfig.test.json && node --test test-dist/**/*.test.js"
|
|
49
|
+
}
|
|
6
50
|
}
|