@pitlane/content 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/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/codegen.d.mts +40 -0
- package/dist/codegen.mjs +131 -0
- package/dist/hot.d.mts +52 -0
- package/dist/hot.mjs +65 -0
- package/dist/index.d.mts +14 -0
- package/dist/index.mjs +313 -0
- package/dist/loaders.d.mts +38 -0
- package/dist/loaders.mjs +255 -0
- package/dist/manifest.d.mts +1 -0
- package/dist/manifest.mjs +4 -0
- package/dist/mdx.d.mts +35 -0
- package/dist/mdx.mjs +389 -0
- package/dist/parse-DoKe2tNa.mjs +57 -0
- package/dist/prebuild.d.mts +65 -0
- package/dist/prebuild.mjs +81 -0
- package/dist/render-U9dXN6f0.mjs +273 -0
- package/dist/satteri.d.mts +41 -0
- package/dist/satteri.mjs +119 -0
- package/dist/symbols-DmXlrDbX.mjs +29 -0
- package/dist/types-xSR1WTBq.d.mts +238 -0
- package/dist/vite.d.mts +18 -0
- package/dist/vite.mjs +262 -0
- package/package.json +104 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# @pitlane/content
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- `createContent` returns typed collection handles synchronously without loading
|
|
8
|
+
entries. Reads and rendering remain asynchronous; declaration errors throw
|
|
9
|
+
synchronously.
|
|
10
|
+
- `getCollection`, `getCollection(filter)`, and `getEntry` over entries sorted
|
|
11
|
+
by id; `c.reference(collection)` for typed pointers between collections.
|
|
12
|
+
- `render()` resolves an entry's Markdown or MDX to a Remix component and its
|
|
13
|
+
heading list, parsing nothing until it is called.
|
|
14
|
+
- Two loader interfaces: `ContentLoader` resolves a whole collection in one
|
|
15
|
+
execution and can be prebuilt, `LiveLoader` answers one query at a time and
|
|
16
|
+
runs on every read. `loaders.glob` and `loaders.file` implement the first.
|
|
17
|
+
- `contentLayer()` from `@pitlane/content/vite` loads `ContentLoader` collections
|
|
18
|
+
after evaluating their declarations and inlines them into the bundle. It waits
|
|
19
|
+
for loading and validation before emitting, and watches the loaders' sources
|
|
20
|
+
in dev.
|
|
21
|
+
- `headings()` from `@pitlane/content/satteri` produces the heading list on both
|
|
22
|
+
rendering paths.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mark Malstrom
|
|
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
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @pitlane/content
|
|
2
|
+
|
|
3
|
+
Schema-validated, cross-referenced content collections for [Remix 3](https://remix.run).
|
|
4
|
+
|
|
5
|
+
Reads Markdown, MDX, JSON, and YAML into collections a controller queries like a
|
|
6
|
+
database. Frontmatter is validated against a schema, one entry can reference
|
|
7
|
+
another, and the types come from the schema rather than from generated code.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @pitlane/content
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { createContent } from "@pitlane/content";
|
|
15
|
+
import * as loaders from "@pitlane/content/loaders";
|
|
16
|
+
import * as s from "remix/data-schema";
|
|
17
|
+
import * as coerce from "remix/data-schema/coerce";
|
|
18
|
+
|
|
19
|
+
export let content = createContent(c => ({
|
|
20
|
+
blog: c.collection({
|
|
21
|
+
loader: loaders.glob({ pattern: "**/*.mdx", base: "app/content/blog" }),
|
|
22
|
+
schema: s.object({
|
|
23
|
+
title: s.string(),
|
|
24
|
+
publishedOn: coerce.date(),
|
|
25
|
+
author: c.reference("authors"),
|
|
26
|
+
}),
|
|
27
|
+
}),
|
|
28
|
+
authors: c.collection({
|
|
29
|
+
loader: loaders.file("app/content/authors.json"),
|
|
30
|
+
schema: s.object({ name: s.string() }),
|
|
31
|
+
}),
|
|
32
|
+
}));
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
let posts = await content.blog.getCollection();
|
|
37
|
+
let post = await content.blog.getEntry(params.slug);
|
|
38
|
+
let { Content, headings } = await post.render();
|
|
39
|
+
let author = await content.authors.getEntry(post.data.author);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`createContent()` returns synchronously without loading entries. Import the
|
|
43
|
+
returned object wherever you need it; reads and rendering stay asynchronous.
|
|
44
|
+
|
|
45
|
+
The loaders are ordinary runtime code, which covers Node, Bun, Deno, and
|
|
46
|
+
container hosts. For a host with no filesystem, add `contentLayer()` from
|
|
47
|
+
`@pitlane/content/vite` and the build resolves the collections ahead of time,
|
|
48
|
+
inlining entry data and compiling Markdown bodies into the bundle. The
|
|
49
|
+
collection declarations do not change.
|
|
50
|
+
|
|
51
|
+
## Entry points
|
|
52
|
+
|
|
53
|
+
| Entry point | Exports |
|
|
54
|
+
| -------------------------- | ------------------------------------------- |
|
|
55
|
+
| `@pitlane/content` | `createContent` and the collection types |
|
|
56
|
+
| `@pitlane/content/loaders` | `glob`, `file` |
|
|
57
|
+
| `@pitlane/content/satteri` | `headings` and `rawStyles`, Sätteri plugins |
|
|
58
|
+
| `@pitlane/content/vite` | `contentLayer`, the build-time plugin |
|
|
59
|
+
|
|
60
|
+
## Without Remix
|
|
61
|
+
|
|
62
|
+
Every peer dependency is optional. The data path, meaning the loaders, schema
|
|
63
|
+
validation, and both query methods, has no static dependency on `remix` and
|
|
64
|
+
works with any [Standard Schema](https://standardschema.dev) validator. Only
|
|
65
|
+
`render()` needs Remix, because it resolves to a Remix component, and it says
|
|
66
|
+
so if you call it without one.
|
|
67
|
+
|
|
68
|
+
## Documentation
|
|
69
|
+
|
|
70
|
+
- [Content](https://pitlane.tools/guides/content), whose toggle also serves
|
|
71
|
+
[an application that runs without a build](https://pitlane.tools/guides/content-no-build)
|
|
72
|
+
- [Creating a content loader](https://pitlane.tools/guides/content-loaders)
|
|
73
|
+
- [API reference](https://pitlane.tools/package/content/)
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { p as LoadedEntry } from "./types-xSR1WTBq.mjs";
|
|
2
|
+
//#region src/codegen.d.ts
|
|
3
|
+
/** The prefix of the virtual modules that carry each entry's raw body. */
|
|
4
|
+
declare const BODY_PREFIX = "\0pitlane-content/entry/";
|
|
5
|
+
/** One entry's raw body, and the file it was read from. */
|
|
6
|
+
interface Body {
|
|
7
|
+
source: string;
|
|
8
|
+
format: "md" | "mdx";
|
|
9
|
+
/** Absent for a loader that produced the body without a file. */
|
|
10
|
+
filePath?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The manifest module `contentLayer()` emits, as JavaScript source.
|
|
14
|
+
*
|
|
15
|
+
* Every value is a literal so the bundler can see it, and each body is a static
|
|
16
|
+
* import of a virtual module, which is the step no runtime cleverness replaces:
|
|
17
|
+
* a component is code, and only the bundler turns source into code.
|
|
18
|
+
*
|
|
19
|
+
* `bodies` is filled with the sources those virtual modules resolve to, each
|
|
20
|
+
* with the path of the entry it came from. A body module has no directory of
|
|
21
|
+
* its own, so a relative import inside it can only be resolved against that.
|
|
22
|
+
*
|
|
23
|
+
* `headings` holds the list the build measured for each Markdown entry, keyed
|
|
24
|
+
* the way `bodies` is. Markdown compiles to an HTML string, which carries no
|
|
25
|
+
* heading list, so without this a prebuilt page's table of contents is empty
|
|
26
|
+
* while the same file renders one at runtime.
|
|
27
|
+
*/
|
|
28
|
+
declare function manifestModule(collections: Record<string, LoadedEntry[]>, bodies: Map<string, Body>, headings?: ReadonlyMap<string, unknown>): string;
|
|
29
|
+
/**
|
|
30
|
+
* Writes a value as JavaScript source, preserving what JSON would flatten.
|
|
31
|
+
*
|
|
32
|
+
* Refuses anything it cannot write exactly. The alternative is worse than an
|
|
33
|
+
* error: a `Map` would arrive as `{}` and a `NaN` as `null`, and the page built
|
|
34
|
+
* from it would be wrong with nothing to read in the build log. `where` names
|
|
35
|
+
* the entry in that message, because a build failure without one sends the
|
|
36
|
+
* reader through every file in the collection.
|
|
37
|
+
*/
|
|
38
|
+
declare function literal(value: unknown, where?: string, seen?: Set<object>): string;
|
|
39
|
+
//#endregion
|
|
40
|
+
export { BODY_PREFIX, Body, literal, manifestModule };
|
package/dist/codegen.mjs
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { i as PREBUILT_MANIFEST_KEY } from "./symbols-DmXlrDbX.mjs";
|
|
2
|
+
//#region src/codegen.ts
|
|
3
|
+
/** The prefix of the virtual modules that carry each entry's raw body. */
|
|
4
|
+
const BODY_PREFIX = "\0pitlane-content/entry/";
|
|
5
|
+
/**
|
|
6
|
+
* The manifest module `contentLayer()` emits, as JavaScript source.
|
|
7
|
+
*
|
|
8
|
+
* Every value is a literal so the bundler can see it, and each body is a static
|
|
9
|
+
* import of a virtual module, which is the step no runtime cleverness replaces:
|
|
10
|
+
* a component is code, and only the bundler turns source into code.
|
|
11
|
+
*
|
|
12
|
+
* `bodies` is filled with the sources those virtual modules resolve to, each
|
|
13
|
+
* with the path of the entry it came from. A body module has no directory of
|
|
14
|
+
* its own, so a relative import inside it can only be resolved against that.
|
|
15
|
+
*
|
|
16
|
+
* `headings` holds the list the build measured for each Markdown entry, keyed
|
|
17
|
+
* the way `bodies` is. Markdown compiles to an HTML string, which carries no
|
|
18
|
+
* heading list, so without this a prebuilt page's table of contents is empty
|
|
19
|
+
* while the same file renders one at runtime.
|
|
20
|
+
*/
|
|
21
|
+
function manifestModule(collections, bodies, headings = /* @__PURE__ */ new Map()) {
|
|
22
|
+
let imports = [];
|
|
23
|
+
bodies.clear();
|
|
24
|
+
let entries = Object.entries(collections).map(([collection, loaded]) => {
|
|
25
|
+
let items = loaded.map((entry) => {
|
|
26
|
+
let where = `${collection}/${entry.id}`;
|
|
27
|
+
let fields = [`id: ${literal(entry.id, where)}`, `data: ${literal(entry.data, where)}`];
|
|
28
|
+
if (entry.filePath) fields.push(`filePath: ${literal(entry.filePath, where)}`);
|
|
29
|
+
if (entry.body) {
|
|
30
|
+
let binding = `body${bodies.size}`;
|
|
31
|
+
let id = `${BODY_PREFIX}${collection}/${entry.id}.${entry.body.format}`;
|
|
32
|
+
bodies.set(id, {
|
|
33
|
+
source: entry.body.source,
|
|
34
|
+
format: entry.body.format,
|
|
35
|
+
filePath: entry.filePath
|
|
36
|
+
});
|
|
37
|
+
imports.push(`import * as ${binding} from ${literal(id, where)};`);
|
|
38
|
+
fields.push(`body: ${bodyExpression(entry.body.format, binding, headings.get(id), where)}`);
|
|
39
|
+
}
|
|
40
|
+
return `{ ${fields.join(", ")} }`;
|
|
41
|
+
});
|
|
42
|
+
return ` [${literal(collection)}]: [${items.join(", ")}]`;
|
|
43
|
+
});
|
|
44
|
+
return [
|
|
45
|
+
...imports,
|
|
46
|
+
`globalThis[Symbol.for(${literal(PREBUILT_MANIFEST_KEY)})] = {`,
|
|
47
|
+
entries.join(",\n"),
|
|
48
|
+
"};",
|
|
49
|
+
""
|
|
50
|
+
].join("\n");
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* MDX compiles to a module carrying a component and a heading list. Markdown
|
|
54
|
+
* compiles to an HTML string, which `vite-plugin-satteri` exports as `html`,
|
|
55
|
+
* so its heading list is written beside it.
|
|
56
|
+
*/
|
|
57
|
+
function bodyExpression(format, binding, headings, where) {
|
|
58
|
+
if (format === "mdx") return `{ format: "mdx", module: ${binding} }`;
|
|
59
|
+
return `{ format: "md", html: ${binding}.html ?? ${binding}.default${headings === void 0 ? "" : `, headings: ${literal(headings, where)}`} }`;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Writes a value as JavaScript source, preserving what JSON would flatten.
|
|
63
|
+
*
|
|
64
|
+
* Refuses anything it cannot write exactly. The alternative is worse than an
|
|
65
|
+
* error: a `Map` would arrive as `{}` and a `NaN` as `null`, and the page built
|
|
66
|
+
* from it would be wrong with nothing to read in the build log. `where` names
|
|
67
|
+
* the entry in that message, because a build failure without one sends the
|
|
68
|
+
* reader through every file in the collection.
|
|
69
|
+
*/
|
|
70
|
+
function literal(value, where = "an entry", seen = /* @__PURE__ */ new Set()) {
|
|
71
|
+
if (value === void 0) return "undefined";
|
|
72
|
+
if (value === null) return "null";
|
|
73
|
+
if (typeof value === "string") return quote(value);
|
|
74
|
+
if (typeof value === "boolean") return String(value);
|
|
75
|
+
if (typeof value === "bigint") return `${value}n`;
|
|
76
|
+
if (typeof value === "number") {
|
|
77
|
+
if (!Number.isFinite(value)) throw unwritable(where, value);
|
|
78
|
+
return Object.is(value, -0) ? "-0" : String(value);
|
|
79
|
+
}
|
|
80
|
+
if (typeof value !== "object") throw unwritable(where, value);
|
|
81
|
+
if (seen.has(value)) throw new Error(`${where} contains a cycle, which cannot be written into the manifest.`);
|
|
82
|
+
let nested = new Set(seen).add(value);
|
|
83
|
+
if (value instanceof Date) {
|
|
84
|
+
if (Number.isNaN(value.getTime())) throw unwritable(where, value);
|
|
85
|
+
return `new Date(${quote(value.toISOString())})`;
|
|
86
|
+
}
|
|
87
|
+
if (Object.getPrototypeOf(value) !== (Array.isArray(value) ? Array.prototype : Object.prototype)) throw unwritable(where, value);
|
|
88
|
+
let keys = Reflect.ownKeys(value);
|
|
89
|
+
if (Array.isArray(value)) {
|
|
90
|
+
let extra = keys.filter((key) => key !== "length" && !isIndex(key, value.length));
|
|
91
|
+
if (extra.length > 0) throw unwritableKey(where, extra[0]);
|
|
92
|
+
return `[${value.map((item) => literal(item, where, nested)).join(", ")}]`;
|
|
93
|
+
}
|
|
94
|
+
return `{ ${keys.map((key) => {
|
|
95
|
+
if (typeof key === "symbol") throw unwritableKey(where, key);
|
|
96
|
+
let descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
97
|
+
if (!descriptor?.enumerable || !("value" in descriptor)) throw unwritableKey(where, key);
|
|
98
|
+
if (key === "__proto__") throw unwritableKey(where, key);
|
|
99
|
+
return `${quote(key)}: ${literal(descriptor.value, where, nested)}`;
|
|
100
|
+
}).join(", ")} }`;
|
|
101
|
+
}
|
|
102
|
+
function isIndex(key, length) {
|
|
103
|
+
if (typeof key === "symbol") return false;
|
|
104
|
+
let index = Number(key);
|
|
105
|
+
return Number.isInteger(index) && index >= 0 && index < length;
|
|
106
|
+
}
|
|
107
|
+
function unwritableKey(where, key) {
|
|
108
|
+
let name = typeof key === "symbol" ? key.toString() : `"${key}"`;
|
|
109
|
+
return /* @__PURE__ */ new Error(`${where} has a key ${name} that cannot be written into the manifest.`);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* A quoted string, with the two separators `JSON.stringify` leaves raw.
|
|
113
|
+
*
|
|
114
|
+
* U+2028 and U+2029 are line terminators in JavaScript but not in JSON, so a
|
|
115
|
+
* frontmatter value containing one would end the statement it sits in.
|
|
116
|
+
*/
|
|
117
|
+
function quote(text) {
|
|
118
|
+
return JSON.stringify(text).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
119
|
+
}
|
|
120
|
+
function unwritable(where, value) {
|
|
121
|
+
return /* @__PURE__ */ new Error(`${where} produced ${describe(value)}, which cannot be written into the manifest.`);
|
|
122
|
+
}
|
|
123
|
+
function describe(value) {
|
|
124
|
+
if (typeof value === "number") return `the number ${String(value)}`;
|
|
125
|
+
if (typeof value === "function") return "a function";
|
|
126
|
+
if (typeof value === "symbol") return "a symbol";
|
|
127
|
+
if (typeof value === "object" && value !== null) return `a ${value.constructor?.name ?? "non-plain object"}`;
|
|
128
|
+
return `a ${typeof value}`;
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
131
|
+
export { BODY_PREFIX, literal, manifestModule };
|
package/dist/hot.d.mts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//#region src/hot.d.ts
|
|
2
|
+
/** A file change the supervisor's watcher observed. */
|
|
3
|
+
interface FileEvent {
|
|
4
|
+
event: "add" | "change" | "unlink";
|
|
5
|
+
filePath: string;
|
|
6
|
+
}
|
|
7
|
+
/** An event for the browser HMR client. A reload is the only one content sends. */
|
|
8
|
+
interface BrowserEvent {
|
|
9
|
+
type: "reload";
|
|
10
|
+
files?: string[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The part of `remix/node-hmr`'s `BrowserHmrChannel` this module uses.
|
|
14
|
+
*
|
|
15
|
+
* Declared structurally rather than imported, because importing the module
|
|
16
|
+
* that declares it would pull a Node-only dependency into every bundle this
|
|
17
|
+
* package reaches. {@link hotContent} holds the only import of it, loaded on
|
|
18
|
+
* demand behind its guard.
|
|
19
|
+
*/
|
|
20
|
+
interface BrowserHmrChannel {
|
|
21
|
+
readonly url: string;
|
|
22
|
+
close(): void;
|
|
23
|
+
onFileEvents(handler: (events: readonly FileEvent[]) => Promise<readonly BrowserEvent[]>): () => void;
|
|
24
|
+
updateWatchedFiles(delta: {
|
|
25
|
+
add: readonly string[];
|
|
26
|
+
remove: readonly string[];
|
|
27
|
+
}): void;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Reloads the browser when a file behind a collection changes.
|
|
31
|
+
*
|
|
32
|
+
* Call it once, beside `createContent`, and leave it in for production: it
|
|
33
|
+
* does nothing unless `remix/node-hmr` is supervising the process, which is
|
|
34
|
+
* what a `dev` command does and a production server does not. That is also why
|
|
35
|
+
* the import below is loaded on demand. `remix/node-hmr/runtime` is Node-only
|
|
36
|
+
* and can only be imported by a supervised child, so a static import would
|
|
37
|
+
* follow this module into a Worker bundle.
|
|
38
|
+
*
|
|
39
|
+
* @param content The object `createContent` returned.
|
|
40
|
+
*/
|
|
41
|
+
declare function hotContent(content: Record<string, unknown>): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* The watching itself, over a channel someone else opened.
|
|
44
|
+
*
|
|
45
|
+
* Separated from {@link hotContent} so it can be driven by a channel a test
|
|
46
|
+
* controls: the real one exists only inside a process `remix/node-hmr` started.
|
|
47
|
+
*
|
|
48
|
+
* @internal
|
|
49
|
+
*/
|
|
50
|
+
declare function watchCollections(content: Record<string, unknown>, channel: BrowserHmrChannel, root: string): Promise<void>;
|
|
51
|
+
//#endregion
|
|
52
|
+
export { BrowserEvent, BrowserHmrChannel, FileEvent, hotContent, watchCollections };
|
package/dist/hot.mjs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { t as HOT_COLLECTION } from "./symbols-DmXlrDbX.mjs";
|
|
2
|
+
import { contentRoot } from "./prebuild.mjs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
//#region src/hot.ts
|
|
5
|
+
/**
|
|
6
|
+
* Reloads the browser when a file behind a collection changes.
|
|
7
|
+
*
|
|
8
|
+
* Call it once, beside `createContent`, and leave it in for production: it
|
|
9
|
+
* does nothing unless `remix/node-hmr` is supervising the process, which is
|
|
10
|
+
* what a `dev` command does and a production server does not. That is also why
|
|
11
|
+
* the import below is loaded on demand. `remix/node-hmr/runtime` is Node-only
|
|
12
|
+
* and can only be imported by a supervised child, so a static import would
|
|
13
|
+
* follow this module into a Worker bundle.
|
|
14
|
+
*
|
|
15
|
+
* @param content The object `createContent` returned.
|
|
16
|
+
*/
|
|
17
|
+
async function hotContent(content) {
|
|
18
|
+
if (!process.env.REMIX_NODE_HMR) return;
|
|
19
|
+
let { createBrowserHmrChannel } = await import("remix/node-hmr/runtime");
|
|
20
|
+
await watchCollections(content, await createBrowserHmrChannel(), contentRoot());
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The watching itself, over a channel someone else opened.
|
|
24
|
+
*
|
|
25
|
+
* Separated from {@link hotContent} so it can be driven by a channel a test
|
|
26
|
+
* controls: the real one exists only inside a process `remix/node-hmr` started.
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
async function watchCollections(content, channel, root) {
|
|
31
|
+
/** Which collection each watched file belongs to, so an event finds its owner. */
|
|
32
|
+
let owners = /* @__PURE__ */ new Map();
|
|
33
|
+
for (let collection of Object.values(content)) {
|
|
34
|
+
let hot = handle(collection);
|
|
35
|
+
if (!hot) continue;
|
|
36
|
+
let watched = [];
|
|
37
|
+
hot.onPopulated((files) => {
|
|
38
|
+
let absolute = files.map((file) => path.resolve(root, file));
|
|
39
|
+
channel.updateWatchedFiles({
|
|
40
|
+
add: absolute,
|
|
41
|
+
remove: watched
|
|
42
|
+
});
|
|
43
|
+
for (let file of watched) owners.get(file)?.delete(hot);
|
|
44
|
+
for (let file of absolute) {
|
|
45
|
+
let set = owners.get(file) ?? /* @__PURE__ */ new Set();
|
|
46
|
+
set.add(hot);
|
|
47
|
+
owners.set(file, set);
|
|
48
|
+
}
|
|
49
|
+
watched = absolute;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
channel.onFileEvents(async (events) => {
|
|
53
|
+
let stale = /* @__PURE__ */ new Set();
|
|
54
|
+
for (let { filePath } of events) for (let owner of owners.get(path.resolve(root, filePath)) ?? []) stale.add(owner);
|
|
55
|
+
if (stale.size === 0) return [];
|
|
56
|
+
for (let collection of stale) collection.invalidate();
|
|
57
|
+
return [{ type: "reload" }];
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function handle(collection) {
|
|
61
|
+
if (typeof collection !== "object" || collection === null) return void 0;
|
|
62
|
+
return collection[HOT_COLLECTION];
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
export { hotContent, watchCollections };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { _ as Reference, a as ContentBuilder, c as EntryBody, d as LiveEntry, f as LiveLoader, h as LoaderContext, i as Content, l as GenerateIdOptions, m as Loader, n as CollectionDefinition, o as ContentLoader, p as LoadedEntry, r as CollectionEntry, s as Entry, t as Collection, u as Heading, v as RenderedEntry } from "./types-xSR1WTBq.mjs";
|
|
2
|
+
//#region src/content.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Declares a set of content collections.
|
|
5
|
+
*
|
|
6
|
+
* Returns the collection object synchronously without loading entries.
|
|
7
|
+
* Reads and rendering remain asynchronous. Declaration errors throw here.
|
|
8
|
+
*
|
|
9
|
+
* Under `contentLayer()`, registers deferred population work. The plugin
|
|
10
|
+
* awaits it after evaluating the declarations and before emitting the bundle.
|
|
11
|
+
*/
|
|
12
|
+
declare function createContent<T extends Record<string, CollectionDefinition>>(build: (c: ContentBuilder) => T): Content<T>;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { type Collection, type CollectionDefinition, type CollectionEntry, type ContentBuilder, type ContentLoader, type Entry, type EntryBody, type GenerateIdOptions, type Heading, type LiveEntry, type LiveLoader, type LoadedEntry, type Loader, type LoaderContext, type Reference, type RenderedEntry, createContent };
|