@bliztek/mdx-utils 1.0.0 → 2.0.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/README.md +318 -2
- package/dist/index-is67XHX5.d.cts +178 -0
- package/dist/index-is67XHX5.d.ts +178 -0
- package/dist/index.d.cts +1 -42
- package/dist/index.d.ts +1 -42
- package/dist/node.cjs +332 -6
- package/dist/node.d.cts +76 -3
- package/dist/node.d.ts +76 -3
- package/dist/node.js +328 -4
- package/package.json +7 -5
package/dist/node.js
CHANGED
|
@@ -5,13 +5,333 @@ import {
|
|
|
5
5
|
} from "./chunk-2WOYQBZW.js";
|
|
6
6
|
|
|
7
7
|
// src/node.ts
|
|
8
|
+
import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
|
|
9
|
+
import path2 from "path";
|
|
10
|
+
|
|
11
|
+
// src/collection.ts
|
|
8
12
|
import { readdir, readFile } from "fs/promises";
|
|
9
13
|
import path from "path";
|
|
14
|
+
|
|
15
|
+
// src/frontmatter.ts
|
|
16
|
+
function defaultParseFrontmatter(raw) {
|
|
17
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
18
|
+
if (!match) return {};
|
|
19
|
+
const lines = match[1].split(/\r?\n/);
|
|
20
|
+
const result = {};
|
|
21
|
+
let i = 0;
|
|
22
|
+
while (i < lines.length) {
|
|
23
|
+
const line = lines[i];
|
|
24
|
+
if (/^\s*(#|$)/.test(line)) {
|
|
25
|
+
i++;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const kv = line.match(/^(\s*)([A-Za-z_][\w-]*)\s*:\s*(.*)$/);
|
|
29
|
+
if (!kv) {
|
|
30
|
+
i++;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const key = kv[2];
|
|
34
|
+
const rawValue = kv[3].trim();
|
|
35
|
+
if (rawValue === "") {
|
|
36
|
+
const items = [];
|
|
37
|
+
let j = i + 1;
|
|
38
|
+
while (j < lines.length) {
|
|
39
|
+
const next = lines[j];
|
|
40
|
+
if (/^\s*$/.test(next)) {
|
|
41
|
+
j++;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const blockItem = next.match(/^\s+-\s+(.*)$/);
|
|
45
|
+
if (!blockItem) break;
|
|
46
|
+
items.push(parseScalar(blockItem[1].trim()));
|
|
47
|
+
j++;
|
|
48
|
+
}
|
|
49
|
+
if (items.length > 0) {
|
|
50
|
+
result[key] = items;
|
|
51
|
+
i = j;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
result[key] = "";
|
|
55
|
+
i++;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (rawValue.startsWith("[") && rawValue.endsWith("]")) {
|
|
59
|
+
const inner = rawValue.slice(1, -1).trim();
|
|
60
|
+
result[key] = inner === "" ? [] : splitInlineArray(inner).map(parseScalar);
|
|
61
|
+
i++;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
result[key] = parseScalar(rawValue);
|
|
65
|
+
i++;
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
function parseScalar(value) {
|
|
70
|
+
if (value.length >= 2) {
|
|
71
|
+
const first = value[0];
|
|
72
|
+
const last = value[value.length - 1];
|
|
73
|
+
if (first === '"' && last === '"' || first === "'" && last === "'") {
|
|
74
|
+
return value.slice(1, -1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
function splitInlineArray(inner) {
|
|
80
|
+
const out = [];
|
|
81
|
+
let buf = "";
|
|
82
|
+
let quote = null;
|
|
83
|
+
for (let i = 0; i < inner.length; i++) {
|
|
84
|
+
const ch = inner[i];
|
|
85
|
+
if (quote) {
|
|
86
|
+
buf += ch;
|
|
87
|
+
if (ch === quote) quote = null;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (ch === '"' || ch === "'") {
|
|
91
|
+
quote = ch;
|
|
92
|
+
buf += ch;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (ch === ",") {
|
|
96
|
+
out.push(buf.trim());
|
|
97
|
+
buf = "";
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
buf += ch;
|
|
101
|
+
}
|
|
102
|
+
if (buf.trim() !== "") out.push(buf.trim());
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/errors.ts
|
|
107
|
+
var MAX_INLINE_ISSUES = 10;
|
|
108
|
+
function formatMessage(issues) {
|
|
109
|
+
if (issues.length === 0) {
|
|
110
|
+
return "MdxValidationError: no issues (this is a bug \u2014 do not throw with an empty list)";
|
|
111
|
+
}
|
|
112
|
+
const head = `MdxValidationError: ${issues.length} issue${issues.length === 1 ? "" : "s"} found in frontmatter`;
|
|
113
|
+
const shown = issues.slice(0, MAX_INLINE_ISSUES);
|
|
114
|
+
const lines = shown.map(
|
|
115
|
+
(issue) => ` - ${issue.filePath} @ ${issue.path}: ${issue.message}`
|
|
116
|
+
);
|
|
117
|
+
if (issues.length > shown.length) {
|
|
118
|
+
lines.push(` ... and ${issues.length - shown.length} more`);
|
|
119
|
+
}
|
|
120
|
+
return [head, ...lines].join("\n");
|
|
121
|
+
}
|
|
122
|
+
var MdxValidationError = class extends Error {
|
|
123
|
+
constructor(issues) {
|
|
124
|
+
super(formatMessage(issues));
|
|
125
|
+
this.name = "MdxValidationError";
|
|
126
|
+
this.issues = issues;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// src/collection.ts
|
|
131
|
+
async function walkCollection(rootAbs, namespaceDepth) {
|
|
132
|
+
const dirents = await readdir(rootAbs, {
|
|
133
|
+
withFileTypes: true,
|
|
134
|
+
recursive: true
|
|
135
|
+
});
|
|
136
|
+
const files = [];
|
|
137
|
+
for (const dirent of dirents) {
|
|
138
|
+
if (dirent.isDirectory()) continue;
|
|
139
|
+
if (!dirent.name.endsWith(".mdx")) continue;
|
|
140
|
+
const parentPath = dirent.parentPath ?? rootAbs;
|
|
141
|
+
const relativeDir = path.relative(rootAbs, parentPath);
|
|
142
|
+
const segments = relativeDir === "" ? [] : relativeDir.split(path.sep);
|
|
143
|
+
if (segments.length !== namespaceDepth) {
|
|
144
|
+
const filePath = path.join(parentPath, dirent.name);
|
|
145
|
+
throw new Error(
|
|
146
|
+
`[mdx-utils] File ${filePath} is at depth ${segments.length} but the collection declares namespaceDepth=${namespaceDepth}. Move the file or adjust namespaceDepth.`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
files.push({
|
|
150
|
+
namespace: segments.join("/"),
|
|
151
|
+
slug: dirent.name.slice(0, -".mdx".length),
|
|
152
|
+
filePath: path.join(parentPath, dirent.name)
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return files;
|
|
156
|
+
}
|
|
157
|
+
function stripFrontmatterBlock(raw) {
|
|
158
|
+
const match = raw.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
|
159
|
+
return match ? raw.slice(match[0].length) : raw;
|
|
160
|
+
}
|
|
161
|
+
function sortEntries(entries) {
|
|
162
|
+
const withDate = [];
|
|
163
|
+
const withoutDate = [];
|
|
164
|
+
for (const entry of entries) {
|
|
165
|
+
const meta = entry.metadata;
|
|
166
|
+
const publishedAt = meta?.publishedAt;
|
|
167
|
+
if (typeof publishedAt === "string") {
|
|
168
|
+
const ts = new Date(publishedAt).getTime();
|
|
169
|
+
if (!Number.isNaN(ts)) {
|
|
170
|
+
withDate.push({ entry, ts });
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
withoutDate.push(entry);
|
|
175
|
+
}
|
|
176
|
+
withDate.sort((a, b) => b.ts - a.ts);
|
|
177
|
+
return [...withDate.map((x) => x.entry), ...withoutDate];
|
|
178
|
+
}
|
|
179
|
+
function extractIssues(filePath, error) {
|
|
180
|
+
if (error && typeof error === "object" && "issues" in error && Array.isArray(error.issues)) {
|
|
181
|
+
const raw = error.issues;
|
|
182
|
+
const mapped = [];
|
|
183
|
+
for (const entry of raw) {
|
|
184
|
+
const pathValue = entry.path;
|
|
185
|
+
const dotted = Array.isArray(pathValue) ? pathValue.join(".") : typeof pathValue === "string" ? pathValue : "(root)";
|
|
186
|
+
const message2 = typeof entry.message === "string" ? entry.message : String(entry);
|
|
187
|
+
mapped.push({ filePath, path: dotted || "(root)", message: message2 });
|
|
188
|
+
}
|
|
189
|
+
if (mapped.length > 0) return mapped;
|
|
190
|
+
}
|
|
191
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
192
|
+
return [{ filePath, path: "(root)", message }];
|
|
193
|
+
}
|
|
194
|
+
function createMdxCollection(options) {
|
|
195
|
+
const {
|
|
196
|
+
root,
|
|
197
|
+
namespaceDepth = 0,
|
|
198
|
+
frontmatterSchema,
|
|
199
|
+
parseFrontmatter = defaultParseFrontmatter,
|
|
200
|
+
wordsPerMinute
|
|
201
|
+
} = options;
|
|
202
|
+
const rootAbs = path.resolve(root);
|
|
203
|
+
let entriesPromise = null;
|
|
204
|
+
async function loadEntries() {
|
|
205
|
+
const files = await walkCollection(rootAbs, namespaceDepth);
|
|
206
|
+
const loaded = await Promise.all(
|
|
207
|
+
files.map(async (file) => {
|
|
208
|
+
const raw = await readFile(file.filePath, "utf-8");
|
|
209
|
+
const metadata = parseFrontmatter(raw);
|
|
210
|
+
const body = stripFrontmatterBlock(raw);
|
|
211
|
+
const { minutes } = calculateReadTime(body, { wordsPerMinute });
|
|
212
|
+
return {
|
|
213
|
+
namespace: file.namespace,
|
|
214
|
+
slug: file.slug,
|
|
215
|
+
metadata,
|
|
216
|
+
readTime: minutes,
|
|
217
|
+
tableOfContents: void 0,
|
|
218
|
+
filePath: file.filePath
|
|
219
|
+
};
|
|
220
|
+
})
|
|
221
|
+
);
|
|
222
|
+
return sortEntries(loaded);
|
|
223
|
+
}
|
|
224
|
+
function getAll() {
|
|
225
|
+
if (!entriesPromise) {
|
|
226
|
+
entriesPromise = loadEntries().catch((err) => {
|
|
227
|
+
entriesPromise = null;
|
|
228
|
+
throw err;
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
return entriesPromise;
|
|
232
|
+
}
|
|
233
|
+
async function getAllByNamespace(namespace) {
|
|
234
|
+
const all = await getAll();
|
|
235
|
+
return all.filter((entry) => entry.namespace === namespace);
|
|
236
|
+
}
|
|
237
|
+
async function get(args) {
|
|
238
|
+
const targetNamespace = args.namespace ?? "";
|
|
239
|
+
const all = await getAll();
|
|
240
|
+
return all.find(
|
|
241
|
+
(entry) => entry.namespace === targetNamespace && entry.slug === args.slug
|
|
242
|
+
) ?? null;
|
|
243
|
+
}
|
|
244
|
+
async function resolveRelated(namespace, slugs) {
|
|
245
|
+
const all = await getAll();
|
|
246
|
+
const inNamespace = new Map(
|
|
247
|
+
all.filter((entry) => entry.namespace === namespace).map((entry) => [entry.slug, entry])
|
|
248
|
+
);
|
|
249
|
+
const resolved = [];
|
|
250
|
+
for (const slug of slugs) {
|
|
251
|
+
const entry = inNamespace.get(slug);
|
|
252
|
+
if (entry) {
|
|
253
|
+
resolved.push(entry);
|
|
254
|
+
} else {
|
|
255
|
+
console.warn(
|
|
256
|
+
`[mdx-utils] resolveRelated: no entry for "${slug}" in namespace "${namespace}"`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return resolved;
|
|
261
|
+
}
|
|
262
|
+
async function validate() {
|
|
263
|
+
if (!frontmatterSchema) return;
|
|
264
|
+
const all = await getAll();
|
|
265
|
+
const issues = [];
|
|
266
|
+
for (const entry of all) {
|
|
267
|
+
try {
|
|
268
|
+
frontmatterSchema.parse(entry.metadata);
|
|
269
|
+
} catch (err) {
|
|
270
|
+
issues.push(...extractIssues(entry.filePath, err));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (issues.length > 0) {
|
|
274
|
+
throw new MdxValidationError(issues);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function invalidate() {
|
|
278
|
+
entriesPromise = null;
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
getAll,
|
|
282
|
+
getAllByNamespace,
|
|
283
|
+
get,
|
|
284
|
+
resolveRelated,
|
|
285
|
+
validate,
|
|
286
|
+
invalidate
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/redirects.ts
|
|
291
|
+
function applyTokens(template, namespace, slug) {
|
|
292
|
+
return template.replace(/\{namespace\}/g, namespace).replace(/\{slug\}/g, slug);
|
|
293
|
+
}
|
|
294
|
+
async function collectRedirects(options) {
|
|
295
|
+
const {
|
|
296
|
+
root,
|
|
297
|
+
namespaceDepth = 0,
|
|
298
|
+
basePath,
|
|
299
|
+
permanent = true,
|
|
300
|
+
aliasField = "aliases"
|
|
301
|
+
} = options;
|
|
302
|
+
const collection = createMdxCollection({ root, namespaceDepth });
|
|
303
|
+
const entries = await collection.getAll();
|
|
304
|
+
const redirects = [];
|
|
305
|
+
const sourceOwners = /* @__PURE__ */ new Map();
|
|
306
|
+
for (const entry of entries) {
|
|
307
|
+
const meta = entry.metadata;
|
|
308
|
+
const rawAliases = meta[aliasField];
|
|
309
|
+
if (!Array.isArray(rawAliases)) continue;
|
|
310
|
+
const destination = applyTokens(basePath, entry.namespace, entry.slug);
|
|
311
|
+
for (const alias of rawAliases) {
|
|
312
|
+
if (typeof alias !== "string" || alias === "") continue;
|
|
313
|
+
const source = applyTokens(basePath, entry.namespace, alias);
|
|
314
|
+
if (source === destination) continue;
|
|
315
|
+
const existing = sourceOwners.get(source);
|
|
316
|
+
if (existing && existing !== entry.filePath) {
|
|
317
|
+
console.warn(
|
|
318
|
+
`[mdx-utils] collectRedirects: duplicate alias "${alias}" defined in both ${existing} and ${entry.filePath}. Next.js will pick the first match.`
|
|
319
|
+
);
|
|
320
|
+
} else {
|
|
321
|
+
sourceOwners.set(source, entry.filePath);
|
|
322
|
+
}
|
|
323
|
+
redirects.push({ source, destination, permanent });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return redirects;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/node.ts
|
|
10
330
|
var DEFAULT_EXTENSIONS = [".mdx"];
|
|
11
331
|
async function getContentSlugs(dirPath, options) {
|
|
12
332
|
const extensions = options?.extensions ?? DEFAULT_EXTENSIONS;
|
|
13
333
|
const recursive = options?.recursive ?? false;
|
|
14
|
-
const dirents = await
|
|
334
|
+
const dirents = await readdir2(dirPath, {
|
|
15
335
|
withFileTypes: true,
|
|
16
336
|
recursive
|
|
17
337
|
});
|
|
@@ -20,15 +340,19 @@ async function getContentSlugs(dirPath, options) {
|
|
|
20
340
|
).map((dirent) => {
|
|
21
341
|
const slug = dirent.name.substring(0, dirent.name.lastIndexOf("."));
|
|
22
342
|
if (!recursive || !dirent.parentPath) return slug;
|
|
23
|
-
const relativePath =
|
|
24
|
-
return relativePath ?
|
|
343
|
+
const relativePath = path2.relative(dirPath, dirent.parentPath);
|
|
344
|
+
return relativePath ? path2.join(relativePath, slug) : slug;
|
|
25
345
|
});
|
|
26
346
|
}
|
|
27
347
|
async function readMdxFile(filePath) {
|
|
28
|
-
return
|
|
348
|
+
return readFile2(filePath, "utf-8");
|
|
29
349
|
}
|
|
30
350
|
export {
|
|
351
|
+
MdxValidationError,
|
|
31
352
|
calculateReadTime,
|
|
353
|
+
collectRedirects,
|
|
354
|
+
createMdxCollection,
|
|
355
|
+
defaultParseFrontmatter,
|
|
32
356
|
getContentSlugs,
|
|
33
357
|
readMdxFile,
|
|
34
358
|
sortByDateDescending,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bliztek/mdx-utils",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Zero-dependency MDX content utilities for read time calculation and file discovery",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"scripts": {
|
|
28
28
|
"build": "tsup",
|
|
29
29
|
"dev": "tsup --watch",
|
|
30
|
-
"
|
|
30
|
+
"test": "node --import tsx --test src/**/*.test.ts",
|
|
31
|
+
"prepublishOnly": "pnpm run build"
|
|
31
32
|
},
|
|
32
33
|
"keywords": [
|
|
33
34
|
"mdx",
|
|
@@ -45,11 +46,12 @@
|
|
|
45
46
|
},
|
|
46
47
|
"repository": {
|
|
47
48
|
"type": "git",
|
|
48
|
-
"url": "git+https://
|
|
49
|
+
"url": "git+https://gitlab.com/bliztek/mdx-utils.git"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
|
-
"@types/node": "^
|
|
52
|
+
"@types/node": "^25.6.0",
|
|
52
53
|
"tsup": "^8.4.0",
|
|
53
|
-
"
|
|
54
|
+
"tsx": "^4.21.0",
|
|
55
|
+
"typescript": "^6.0.2"
|
|
54
56
|
}
|
|
55
57
|
}
|