@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/dist/mdx.mjs
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
//#region src/mdx.ts
|
|
2
|
+
/**
|
|
3
|
+
* The imports in one top-level ESM block, and what is left after removing them.
|
|
4
|
+
*
|
|
5
|
+
* The remainder matters twice over. A block holds whatever the author wrote, so
|
|
6
|
+
* an `export const` can sit beside an import and the document's body may use
|
|
7
|
+
* it; dropping the whole block would take the export with the import. And what
|
|
8
|
+
* is left is spliced back into the document, so it has to stay recognisable as
|
|
9
|
+
* ESM: a comment surviving on its own line starts a paragraph, which swallows
|
|
10
|
+
* the export that follows it.
|
|
11
|
+
*
|
|
12
|
+
* Statement boundaries come from `es-module-lexer`, the lexer Vite reads the
|
|
13
|
+
* same imports with. Deciding them here instead means reimplementing JavaScript
|
|
14
|
+
* tokenization: a quote inside a regex literal is not a string, a line starting
|
|
15
|
+
* with `import` inside a template is not a statement, `{ import: "x" }` is a
|
|
16
|
+
* property, and each of those was wrong before the lexer answered it.
|
|
17
|
+
*
|
|
18
|
+
* Anything import-shaped the lexer reports and this cannot read throws rather
|
|
19
|
+
* than being skipped: a skipped import is a component that silently renders as
|
|
20
|
+
* nothing, which is the failure this whole path exists to remove.
|
|
21
|
+
*/
|
|
22
|
+
async function readEsm(block, where) {
|
|
23
|
+
let { init, parse } = await import("es-module-lexer");
|
|
24
|
+
await init;
|
|
25
|
+
let lexable = readable(block, parse, where);
|
|
26
|
+
let comments = commentSpans(lexable);
|
|
27
|
+
let statements = [];
|
|
28
|
+
let imports = [];
|
|
29
|
+
for (let found of parse(lexable)[0]) {
|
|
30
|
+
if (found.t === 3) throw importMeta(where);
|
|
31
|
+
if (found.t !== 1 || !block.startsWith("import", found.ss)) continue;
|
|
32
|
+
let tail = endOfStatement(block, found.e + 1, comments);
|
|
33
|
+
statements.push({
|
|
34
|
+
start: found.ss,
|
|
35
|
+
end: tail.end
|
|
36
|
+
});
|
|
37
|
+
let read = readImport(block, found, tail, comments, where);
|
|
38
|
+
if (read) imports.push(read);
|
|
39
|
+
}
|
|
40
|
+
let outside = comments.filter((comment) => !statements.some((held) => covers(held, comment)));
|
|
41
|
+
return {
|
|
42
|
+
imports,
|
|
43
|
+
remainder: blank(block, [...statements, ...outside])
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function covers(outer, inner) {
|
|
47
|
+
return inner.start >= outer.start && inner.end <= outer.end;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The block in a form the lexer can read.
|
|
51
|
+
*
|
|
52
|
+
* An MDX document defines a component by writing JSX in its ESM block, and the
|
|
53
|
+
* lexer is a JavaScript lexer: `() => <em>n</em>` is a parse error to it. So
|
|
54
|
+
* each line is masked from the first JSX tag it holds, which leaves every
|
|
55
|
+
* import intact -- an import statement cannot contain a `<` -- and keeps every
|
|
56
|
+
* offset, because the mask is the same width as what it covers.
|
|
57
|
+
*/
|
|
58
|
+
function readable(block, parse, where) {
|
|
59
|
+
if (lexes(block, parse)) return block;
|
|
60
|
+
let masked = block.split("\n").map((line) => {
|
|
61
|
+
let at = line.search(/<(?=[A-Za-z_$>/])/);
|
|
62
|
+
return at === -1 ? line : line.slice(0, at) + " ".repeat(line.length - at);
|
|
63
|
+
}).join("\n");
|
|
64
|
+
if (lexes(masked, parse)) return masked;
|
|
65
|
+
throw new Error(`Could not read the imports of "${where}": its \`import\` and \`export\` block is not valid JavaScript. Add contentLayer() from @pitlane/content/vite so the build compiles this collection.`);
|
|
66
|
+
}
|
|
67
|
+
function lexes(source, parse) {
|
|
68
|
+
try {
|
|
69
|
+
parse(source);
|
|
70
|
+
return true;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* What follows the specifier: an optional attributes clause, then an optional
|
|
77
|
+
* semicolon, with comments allowed in both gaps.
|
|
78
|
+
*
|
|
79
|
+
* The lexer reports neither reliably. Its statement end stops at the specifier,
|
|
80
|
+
* leaving a `;` that becomes a paragraph reading `;` in the rendered page, and
|
|
81
|
+
* it declines to report a clause written with a trailing comma, with no
|
|
82
|
+
* attributes at all, or on the line below -- all of which are valid, and all of
|
|
83
|
+
* which the host still demands before it will load the module.
|
|
84
|
+
*/
|
|
85
|
+
function endOfStatement(block, after, comments) {
|
|
86
|
+
let end = skipBlanks(block, after, comments);
|
|
87
|
+
let attributes;
|
|
88
|
+
let phrase = /^(?:with|assert)\b/.exec(block.slice(end));
|
|
89
|
+
if (phrase) {
|
|
90
|
+
let open = skipBlanks(block, end + phrase[0].length, comments);
|
|
91
|
+
if (block[open] === "{") {
|
|
92
|
+
let close = balanced(block, open);
|
|
93
|
+
attributes = {
|
|
94
|
+
start: open,
|
|
95
|
+
end: close
|
|
96
|
+
};
|
|
97
|
+
end = close;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
let semicolon = skipBlanks(block, end, comments);
|
|
101
|
+
return {
|
|
102
|
+
end: block[semicolon] === ";" ? semicolon + 1 : end,
|
|
103
|
+
attributes
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** Past whitespace and comments, which may sit anywhere in a statement. */
|
|
107
|
+
function skipBlanks(block, from, comments) {
|
|
108
|
+
let index = from;
|
|
109
|
+
for (;;) {
|
|
110
|
+
while (index < block.length && /\s/.test(block[index])) index += 1;
|
|
111
|
+
let comment = comments.find((held) => held.start === index);
|
|
112
|
+
if (!comment) return index;
|
|
113
|
+
index = comment.end;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** The index just past the `}` closing the `{` at `open`. */
|
|
117
|
+
function balanced(block, open) {
|
|
118
|
+
let depth = 0;
|
|
119
|
+
for (let index = open; index < block.length; index += 1) if (block[index] === "{") depth += 1;
|
|
120
|
+
else if (block[index] === "}") {
|
|
121
|
+
depth -= 1;
|
|
122
|
+
if (depth === 0) return index + 1;
|
|
123
|
+
}
|
|
124
|
+
return block.length;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Replaces each span with its own newlines.
|
|
128
|
+
*
|
|
129
|
+
* Blanked rather than deleted so every offset after it still lines up with the
|
|
130
|
+
* source the author wrote, which is what keeps a later error's line number
|
|
131
|
+
* pointing at the right line.
|
|
132
|
+
*/
|
|
133
|
+
function blank(block, spans) {
|
|
134
|
+
let ordered = [...spans].sort((a, b) => b.start - a.start);
|
|
135
|
+
let out = block;
|
|
136
|
+
for (let { start, end } of ordered) {
|
|
137
|
+
let removed = out.slice(start, end);
|
|
138
|
+
out = out.slice(0, start) + "\n".repeat((removed.match(/\n/g) ?? []).length) + out.slice(end);
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
/** Reads one statement, or nothing when it is type-only. */
|
|
143
|
+
function readImport(block, found, tail, comments, where) {
|
|
144
|
+
let statement = block.slice(found.ss, tail.end);
|
|
145
|
+
let specifier = found.n;
|
|
146
|
+
if (specifier === void 0) throw unreadable(statement, where);
|
|
147
|
+
let clause = erase(block, {
|
|
148
|
+
start: found.ss + 6,
|
|
149
|
+
end: found.s - 1
|
|
150
|
+
}, comments).trim().replace(/\bfrom$/, "").trim();
|
|
151
|
+
if (/^type\b/.test(clause) && clause.slice(4).trim().length > 0) return void 0;
|
|
152
|
+
let attributes = tail.attributes && readAttributes(erase(block, tail.attributes, comments));
|
|
153
|
+
return {
|
|
154
|
+
specifier,
|
|
155
|
+
bindings: readBindings(clause, statement, where),
|
|
156
|
+
attributes
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/** A slice of the block with any comment in it replaced by blanks. */
|
|
160
|
+
function erase(block, span, comments) {
|
|
161
|
+
let out = block.slice(span.start, span.end);
|
|
162
|
+
for (let comment of comments) {
|
|
163
|
+
if (!covers(span, comment)) continue;
|
|
164
|
+
let start = comment.start - span.start;
|
|
165
|
+
let width = comment.end - comment.start;
|
|
166
|
+
out = out.slice(0, start) + " ".repeat(width) + out.slice(start + width);
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
function readBindings(clause, statement, where) {
|
|
171
|
+
let bindings = /* @__PURE__ */ new Map();
|
|
172
|
+
let named = /\{([\s\S]*)\}/.exec(clause);
|
|
173
|
+
let head = (named ? clause.slice(0, named.index) : clause).replace(/,\s*$/, "").trim();
|
|
174
|
+
if (head.startsWith("* as ")) throw namespaceImport(head.slice(5).trim(), statement, where);
|
|
175
|
+
if (head.length > 0) bindings.set(identifier(head, statement, where), "default");
|
|
176
|
+
for (let part of named?.[1]?.split(",") ?? []) {
|
|
177
|
+
let entry = part.trim();
|
|
178
|
+
if (entry.length === 0) continue;
|
|
179
|
+
let words = entry.split(/\s+/);
|
|
180
|
+
if (words[0] === "type" && words.length > 1 && words[1] !== "as") continue;
|
|
181
|
+
let renamed = /^(\S+)\s+as\s+(\S+)$/.exec(entry);
|
|
182
|
+
if (renamed) bindings.set(identifier(renamed[2], statement, where), renamed[1]);
|
|
183
|
+
else bindings.set(identifier(entry, statement, where), entry);
|
|
184
|
+
}
|
|
185
|
+
return bindings;
|
|
186
|
+
}
|
|
187
|
+
const ATTRIBUTE_RE = /(?:([A-Za-z_$][\w$]*)|"([^"]*)"|'([^']*)')\s*:\s*(?:"([^"]*)"|'([^']*)')/g;
|
|
188
|
+
/**
|
|
189
|
+
* An attributes clause has to reach the `import()` this path performs. Dropping
|
|
190
|
+
* it leaves `import data from "./x.json" with { type: "json" }` importing the
|
|
191
|
+
* file with no attribute, which Node refuses outright.
|
|
192
|
+
*/
|
|
193
|
+
function readAttributes(clause) {
|
|
194
|
+
let attributes = {};
|
|
195
|
+
for (let [, name, quoted, single, value, singleValue] of clause.matchAll(ATTRIBUTE_RE)) attributes[name ?? quoted ?? single] = value ?? singleValue;
|
|
196
|
+
return attributes;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* The comments in a block of ESM source.
|
|
200
|
+
*
|
|
201
|
+
* Only comments: the lexer answers for imports, so a mistake here leaves a
|
|
202
|
+
* comment in the remainder rather than losing a component. Regex literals are
|
|
203
|
+
* tracked all the same, because `/"/` holds a quote that would otherwise open a
|
|
204
|
+
* string running to the end of the next real one.
|
|
205
|
+
*/
|
|
206
|
+
function commentSpans(block) {
|
|
207
|
+
let comments = [];
|
|
208
|
+
let nesting = [];
|
|
209
|
+
let previous = "";
|
|
210
|
+
let index = 0;
|
|
211
|
+
while (index < block.length) {
|
|
212
|
+
let char = block[index];
|
|
213
|
+
if (nesting.at(-1) === "template") {
|
|
214
|
+
if (char === "\\") index += 2;
|
|
215
|
+
else if (char === "`") {
|
|
216
|
+
nesting.pop();
|
|
217
|
+
previous = "`";
|
|
218
|
+
index += 1;
|
|
219
|
+
} else if (char === "$" && block[index + 1] === "{") {
|
|
220
|
+
nesting.push("interpolation");
|
|
221
|
+
previous = "";
|
|
222
|
+
index += 2;
|
|
223
|
+
} else index += 1;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (char === "/" && block[index + 1] === "/") {
|
|
227
|
+
let line = block.indexOf("\n", index);
|
|
228
|
+
let end = line === -1 ? block.length : line;
|
|
229
|
+
comments.push({
|
|
230
|
+
start: index,
|
|
231
|
+
end
|
|
232
|
+
});
|
|
233
|
+
index = end;
|
|
234
|
+
} else if (char === "/" && block[index + 1] === "*") {
|
|
235
|
+
let close = block.indexOf("*/", index + 2);
|
|
236
|
+
let end = close === -1 ? block.length : close + 2;
|
|
237
|
+
comments.push({
|
|
238
|
+
start: index,
|
|
239
|
+
end
|
|
240
|
+
});
|
|
241
|
+
index = end;
|
|
242
|
+
} else if (char === "/" && startsRegex(previous)) {
|
|
243
|
+
index = endOfRegex(block, index);
|
|
244
|
+
previous = "/";
|
|
245
|
+
} else if (char === "\"" || char === "'") {
|
|
246
|
+
index = endOfString(block, index, char);
|
|
247
|
+
previous = char;
|
|
248
|
+
} else if (char === "`") {
|
|
249
|
+
nesting.push("template");
|
|
250
|
+
index += 1;
|
|
251
|
+
} else if (char === "{") {
|
|
252
|
+
nesting.push("brace");
|
|
253
|
+
previous = char;
|
|
254
|
+
index += 1;
|
|
255
|
+
} else if (char === "}") {
|
|
256
|
+
if (nesting.length > 0) nesting.pop();
|
|
257
|
+
previous = char;
|
|
258
|
+
index += 1;
|
|
259
|
+
} else if (isWordStart(char)) {
|
|
260
|
+
let end = index;
|
|
261
|
+
while (end < block.length && isWordPart(block[end])) end += 1;
|
|
262
|
+
previous = block.slice(index, end);
|
|
263
|
+
index = end;
|
|
264
|
+
} else if (/\s/.test(char)) index += 1;
|
|
265
|
+
else {
|
|
266
|
+
previous = char;
|
|
267
|
+
index += 1;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return comments;
|
|
271
|
+
}
|
|
272
|
+
/** Where a `/` can only begin a regex, never divide. */
|
|
273
|
+
const BEFORE_REGEX = /* @__PURE__ */ new Set([
|
|
274
|
+
"",
|
|
275
|
+
"=",
|
|
276
|
+
"(",
|
|
277
|
+
",",
|
|
278
|
+
":",
|
|
279
|
+
"[",
|
|
280
|
+
"!",
|
|
281
|
+
"&",
|
|
282
|
+
"|",
|
|
283
|
+
"?",
|
|
284
|
+
";",
|
|
285
|
+
"{",
|
|
286
|
+
"}",
|
|
287
|
+
"+",
|
|
288
|
+
"-",
|
|
289
|
+
"*",
|
|
290
|
+
"%",
|
|
291
|
+
"~",
|
|
292
|
+
"^",
|
|
293
|
+
"<",
|
|
294
|
+
">",
|
|
295
|
+
"return",
|
|
296
|
+
"typeof",
|
|
297
|
+
"instanceof",
|
|
298
|
+
"in",
|
|
299
|
+
"of",
|
|
300
|
+
"new",
|
|
301
|
+
"delete",
|
|
302
|
+
"void",
|
|
303
|
+
"case",
|
|
304
|
+
"do",
|
|
305
|
+
"else",
|
|
306
|
+
"yield",
|
|
307
|
+
"await",
|
|
308
|
+
"throw"
|
|
309
|
+
]);
|
|
310
|
+
function startsRegex(previous) {
|
|
311
|
+
return BEFORE_REGEX.has(previous);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* The end of a regex literal, or the `/` itself when it turns out to divide.
|
|
315
|
+
*
|
|
316
|
+
* A literal never spans a line, so an unclosed one by the end of the line is
|
|
317
|
+
* division after all, whatever the token before it suggested.
|
|
318
|
+
*/
|
|
319
|
+
function endOfRegex(block, start) {
|
|
320
|
+
let index = start + 1;
|
|
321
|
+
let inClass = false;
|
|
322
|
+
while (index < block.length) {
|
|
323
|
+
let char = block[index];
|
|
324
|
+
if (char === "\n") return start + 1;
|
|
325
|
+
if (char === "\\") index += 2;
|
|
326
|
+
else if (char === "[") {
|
|
327
|
+
inClass = true;
|
|
328
|
+
index += 1;
|
|
329
|
+
} else if (char === "]") {
|
|
330
|
+
inClass = false;
|
|
331
|
+
index += 1;
|
|
332
|
+
} else if (char === "/" && !inClass) {
|
|
333
|
+
index += 1;
|
|
334
|
+
while (index < block.length && /[a-z]/.test(block[index])) index += 1;
|
|
335
|
+
return index;
|
|
336
|
+
} else index += 1;
|
|
337
|
+
}
|
|
338
|
+
return start + 1;
|
|
339
|
+
}
|
|
340
|
+
function endOfString(block, start, quote) {
|
|
341
|
+
let index = start + 1;
|
|
342
|
+
while (index < block.length) {
|
|
343
|
+
let char = block[index];
|
|
344
|
+
if (char === "\\") index += 2;
|
|
345
|
+
else if (char === quote) return index + 1;
|
|
346
|
+
else index += 1;
|
|
347
|
+
}
|
|
348
|
+
return block.length;
|
|
349
|
+
}
|
|
350
|
+
function isWordStart(char) {
|
|
351
|
+
return /[A-Za-z_$]/.test(char);
|
|
352
|
+
}
|
|
353
|
+
function isWordPart(char) {
|
|
354
|
+
return /[\w$]/.test(char);
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* A bundler compiles the document to a module, where `import.meta` is
|
|
358
|
+
* ordinary. Here the body is a function body, so the engine refuses it with
|
|
359
|
+
* `SyntaxError: Cannot use 'import.meta' outside a module` thrown from source
|
|
360
|
+
* the author never wrote, naming neither the document nor the reason.
|
|
361
|
+
*/
|
|
362
|
+
function importMeta(where) {
|
|
363
|
+
return /* @__PURE__ */ new Error(`"${where}" uses \`import.meta\`, which cannot be evaluated outside a bundler: the document is compiled to a function body rather than a module. Add contentLayer() from @pitlane/content/vite so the build compiles this collection.`);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* A local name becomes a parameter of the function the compiled body is
|
|
367
|
+
* evaluated as, so anything that is not an identifier has to be refused here
|
|
368
|
+
* rather than producing a syntax error in generated source.
|
|
369
|
+
*/
|
|
370
|
+
function identifier(name, statement, where) {
|
|
371
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(name)) throw unreadable(statement, where);
|
|
372
|
+
return name;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Sätteri compiles `import * as ui from "./x.tsx"` to `const {} = arguments[0]`
|
|
376
|
+
* in `function-body` mode: the local name is never bound, so the document
|
|
377
|
+
* throws `ReferenceError: ui is not defined` from inside compiled source the
|
|
378
|
+
* author never wrote. Refused here, where the file and the statement are both
|
|
379
|
+
* still in hand. A bundler compiles the same document to a module, where the
|
|
380
|
+
* namespace import is ordinary and works.
|
|
381
|
+
*/
|
|
382
|
+
function namespaceImport(local, statement, where) {
|
|
383
|
+
return /* @__PURE__ */ new Error(`"${where}" imports \`* as ${local}\` in \`${statement}\`, which cannot be resolved outside a bundler. Import the components by name instead, or add contentLayer() from @pitlane/content/vite so the build compiles this collection.`);
|
|
384
|
+
}
|
|
385
|
+
function unreadable(statement, where) {
|
|
386
|
+
return /* @__PURE__ */ new Error(`Could not read the import \`${statement}\` in "${where}". Rendering an MDX entry outside a bundler resolves its imports directly, which needs an ordinary import statement.`);
|
|
387
|
+
}
|
|
388
|
+
//#endregion
|
|
389
|
+
export { readEsm };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
//#region src/parse.ts
|
|
2
|
+
/**
|
|
3
|
+
* An error already framed with the collection that produced it.
|
|
4
|
+
*
|
|
5
|
+
* The marker is a type rather than a substring of the message: matching on
|
|
6
|
+
* wording couples every thrower to `annotate`'s idea of what a framed message
|
|
7
|
+
* looks like, and rewording one of them would double-wrap or skip silently.
|
|
8
|
+
*/
|
|
9
|
+
var ContentError = class extends Error {
|
|
10
|
+
collection;
|
|
11
|
+
constructor(collection, message, options) {
|
|
12
|
+
super(message, options);
|
|
13
|
+
this.name = "ContentError";
|
|
14
|
+
this.collection = collection;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Validates one entry's data against its collection's schema.
|
|
19
|
+
*
|
|
20
|
+
* The thrown message names everything needed to find the file and fix it: the
|
|
21
|
+
* entry, the collection, the path when there is one, and one line per issue.
|
|
22
|
+
* This is the earliest point the data exists, so it is the earliest point the
|
|
23
|
+
* mistake can be reported.
|
|
24
|
+
*/
|
|
25
|
+
async function parseEntryData(schema, target, data) {
|
|
26
|
+
let result = await schema["~standard"].validate(data);
|
|
27
|
+
if (result.issues) throw new ContentError(target.collection, parseFailure(target, result.issues));
|
|
28
|
+
return result.value;
|
|
29
|
+
}
|
|
30
|
+
function parseFailure(target, issues) {
|
|
31
|
+
let where = target.filePath ? ` (${target.filePath})` : "";
|
|
32
|
+
let lines = issues.map((issue) => ` - ${issuePath(issue)}${issue.message}`);
|
|
33
|
+
return [`Failed to parse entry "${target.id}" in collection "${target.collection}"${where}:`, ...lines].join("\n");
|
|
34
|
+
}
|
|
35
|
+
function issuePath(issue) {
|
|
36
|
+
if (!issue.path || issue.path.length === 0) return "";
|
|
37
|
+
return `${issue.path.map((segment) => typeof segment === "object" ? String(segment.key) : String(segment)).join(".")}: `;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Frames the one import failure `render()` can produce on its own.
|
|
41
|
+
*
|
|
42
|
+
* The renderer is loaded on demand so that reading a collection needs no
|
|
43
|
+
* framework, which means an application that never renders never installs
|
|
44
|
+
* one. When it does render, Node reports a resolver path and a specifier and
|
|
45
|
+
* nothing about content. Anything else that fails while the renderer loads is
|
|
46
|
+
* left alone, so a real error inside it is not disguised as a missing install.
|
|
47
|
+
*
|
|
48
|
+
* @param where The entry being rendered, for the message.
|
|
49
|
+
* @param cause The failure the dynamic import threw.
|
|
50
|
+
* @returns A framed error, or `undefined` when this was some other failure.
|
|
51
|
+
*/
|
|
52
|
+
function missingRenderer(where, cause) {
|
|
53
|
+
if (!(cause instanceof Error && cause.code === "ERR_MODULE_NOT_FOUND" && /'remix(?:\/[^']*)?'/.test(cause.message))) return void 0;
|
|
54
|
+
return new Error(`Rendering "${where}" needs the peer dependency "remix"; install it, or read the entry's data without calling render().`, { cause });
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
export { missingRenderer as n, parseEntryData as r, ContentError as t };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { g as PrebuiltCollections } from "./types-xSR1WTBq.mjs";
|
|
2
|
+
//#region src/prebuild.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The channel between `contentLayer()` and `createContent`.
|
|
5
|
+
*
|
|
6
|
+
* The plugin runs the application's content module through Vite's module
|
|
7
|
+
* runner, which evaluates it in a realm of its own. A module-scoped variable
|
|
8
|
+
* would therefore be a different variable on each side, so the handshake lives
|
|
9
|
+
* on globals whose names `symbols.ts` owns.
|
|
10
|
+
*/
|
|
11
|
+
interface Channel {
|
|
12
|
+
collections: Map<string, unknown[]>;
|
|
13
|
+
watched: Set<string>;
|
|
14
|
+
/** Collections whose loader configured runtime rendering options. */
|
|
15
|
+
configuredSatteri: Set<string>;
|
|
16
|
+
tasks: (() => Promise<void>)[];
|
|
17
|
+
root: string;
|
|
18
|
+
}
|
|
19
|
+
/** Opens prebuild mode. Called by `contentLayer()` before it runs the entry. */
|
|
20
|
+
declare function openPrebuild(root: string): Channel;
|
|
21
|
+
/** Closes prebuild mode, so a later `createContent` in this process is normal. */
|
|
22
|
+
declare function closePrebuild(): void;
|
|
23
|
+
/** Whether a build is collecting declarations and prebuilding their entries. */
|
|
24
|
+
declare function isPrebuilding(): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* The root loaders resolve relative paths against.
|
|
27
|
+
*
|
|
28
|
+
* A prebuilt collection never asks, so the fallback only runs where a loader
|
|
29
|
+
* is about to read the filesystem anyway. `process` is reached defensively all
|
|
30
|
+
* the same: a host without it should hear about the missing filesystem from
|
|
31
|
+
* the loader, which names the collection, rather than about a missing global.
|
|
32
|
+
*/
|
|
33
|
+
declare function contentRoot(): string;
|
|
34
|
+
/** Defers loading until the plugin finishes evaluating the declaration module. */
|
|
35
|
+
declare function registerPrebuild(populate: () => Promise<void>): void;
|
|
36
|
+
/** Records one collection's entries for `contentLayer()` to read back. */
|
|
37
|
+
declare function recordPrebuilt(collection: string, entries: unknown[]): void;
|
|
38
|
+
/**
|
|
39
|
+
* Records the paths a loader says it reads, so `contentLayer()` can watch them.
|
|
40
|
+
*
|
|
41
|
+
* This comes from the loader rather than from the entries it produced: a
|
|
42
|
+
* collection that currently matches nothing has no file paths to infer from,
|
|
43
|
+
* and it is exactly the collection whose first file needs to be noticed.
|
|
44
|
+
*/
|
|
45
|
+
declare function recordWatched(paths: readonly string[]): void;
|
|
46
|
+
/**
|
|
47
|
+
* Records that a prebuilt collection's loader configured `options.satteri`.
|
|
48
|
+
*
|
|
49
|
+
* Those options only take effect when the collection renders at runtime, so a
|
|
50
|
+
* prebuilt collection carrying them has a plugin list that silently applies on
|
|
51
|
+
* some hosts and not others. `contentLayer()` warns rather than let that pass.
|
|
52
|
+
*/
|
|
53
|
+
declare function recordConfiguredSatteri(collection: string): void;
|
|
54
|
+
/**
|
|
55
|
+
* The manifest `contentLayer()` emitted, or `null` when nothing prebuilt anything.
|
|
56
|
+
*
|
|
57
|
+
* A prebuild reads no manifest, because it is producing one. Without that rule
|
|
58
|
+
* a process that has already imported a prebuilt bundle — two builds in one
|
|
59
|
+
* test run, a build after a preview — would hand the next prebuild the previous
|
|
60
|
+
* manifest, the loaders would look as though they had already run, and the
|
|
61
|
+
* collection would be emitted empty.
|
|
62
|
+
*/
|
|
63
|
+
declare function prebuiltManifest(): PrebuiltCollections | null;
|
|
64
|
+
//#endregion
|
|
65
|
+
export { closePrebuild, contentRoot, isPrebuilding, openPrebuild, prebuiltManifest, recordConfiguredSatteri, recordPrebuilt, recordWatched, registerPrebuild };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { n as PREBUILD_CHANNEL, r as PREBUILT_MANIFEST } from "./symbols-DmXlrDbX.mjs";
|
|
2
|
+
import "./manifest.mjs";
|
|
3
|
+
//#region src/prebuild.ts
|
|
4
|
+
/** Opens prebuild mode. Called by `contentLayer()` before it runs the entry. */
|
|
5
|
+
function openPrebuild(root) {
|
|
6
|
+
let channel = {
|
|
7
|
+
collections: /* @__PURE__ */ new Map(),
|
|
8
|
+
watched: /* @__PURE__ */ new Set(),
|
|
9
|
+
configuredSatteri: /* @__PURE__ */ new Set(),
|
|
10
|
+
tasks: [],
|
|
11
|
+
root
|
|
12
|
+
};
|
|
13
|
+
globalThis[PREBUILD_CHANNEL] = channel;
|
|
14
|
+
return channel;
|
|
15
|
+
}
|
|
16
|
+
/** Closes prebuild mode, so a later `createContent` in this process is normal. */
|
|
17
|
+
function closePrebuild() {
|
|
18
|
+
delete globalThis[PREBUILD_CHANNEL];
|
|
19
|
+
}
|
|
20
|
+
/** Whether a build is collecting declarations and prebuilding their entries. */
|
|
21
|
+
function isPrebuilding() {
|
|
22
|
+
return globalThis[PREBUILD_CHANNEL] !== void 0;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The root loaders resolve relative paths against.
|
|
26
|
+
*
|
|
27
|
+
* A prebuilt collection never asks, so the fallback only runs where a loader
|
|
28
|
+
* is about to read the filesystem anyway. `process` is reached defensively all
|
|
29
|
+
* the same: a host without it should hear about the missing filesystem from
|
|
30
|
+
* the loader, which names the collection, rather than about a missing global.
|
|
31
|
+
*/
|
|
32
|
+
function contentRoot() {
|
|
33
|
+
let root = globalThis[PREBUILD_CHANNEL]?.root;
|
|
34
|
+
if (root !== void 0) return root;
|
|
35
|
+
return typeof process === "undefined" ? "/" : process.cwd();
|
|
36
|
+
}
|
|
37
|
+
/** Defers loading until the plugin finishes evaluating the declaration module. */
|
|
38
|
+
function registerPrebuild(populate) {
|
|
39
|
+
globalThis[PREBUILD_CHANNEL]?.tasks.push(populate);
|
|
40
|
+
}
|
|
41
|
+
/** Records one collection's entries for `contentLayer()` to read back. */
|
|
42
|
+
function recordPrebuilt(collection, entries) {
|
|
43
|
+
globalThis[PREBUILD_CHANNEL]?.collections.set(collection, entries);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Records the paths a loader says it reads, so `contentLayer()` can watch them.
|
|
47
|
+
*
|
|
48
|
+
* This comes from the loader rather than from the entries it produced: a
|
|
49
|
+
* collection that currently matches nothing has no file paths to infer from,
|
|
50
|
+
* and it is exactly the collection whose first file needs to be noticed.
|
|
51
|
+
*/
|
|
52
|
+
function recordWatched(paths) {
|
|
53
|
+
let channel = globalThis[PREBUILD_CHANNEL];
|
|
54
|
+
if (!channel) return;
|
|
55
|
+
for (let path of paths) channel.watched.add(path);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Records that a prebuilt collection's loader configured `options.satteri`.
|
|
59
|
+
*
|
|
60
|
+
* Those options only take effect when the collection renders at runtime, so a
|
|
61
|
+
* prebuilt collection carrying them has a plugin list that silently applies on
|
|
62
|
+
* some hosts and not others. `contentLayer()` warns rather than let that pass.
|
|
63
|
+
*/
|
|
64
|
+
function recordConfiguredSatteri(collection) {
|
|
65
|
+
globalThis[PREBUILD_CHANNEL]?.configuredSatteri.add(collection);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The manifest `contentLayer()` emitted, or `null` when nothing prebuilt anything.
|
|
69
|
+
*
|
|
70
|
+
* A prebuild reads no manifest, because it is producing one. Without that rule
|
|
71
|
+
* a process that has already imported a prebuilt bundle — two builds in one
|
|
72
|
+
* test run, a build after a preview — would hand the next prebuild the previous
|
|
73
|
+
* manifest, the loaders would look as though they had already run, and the
|
|
74
|
+
* collection would be emitted empty.
|
|
75
|
+
*/
|
|
76
|
+
function prebuiltManifest() {
|
|
77
|
+
if (isPrebuilding()) return null;
|
|
78
|
+
return globalThis[PREBUILT_MANIFEST] ?? null;
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
export { closePrebuild, contentRoot, isPrebuilding, openPrebuild, prebuiltManifest, recordConfiguredSatteri, recordPrebuilt, recordWatched, registerPrebuild };
|