dommief 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/dist/cli.js +2695 -0
- package/dist/index.d.ts +326 -0
- package/dist/index.js +1383 -0
- package/dist/theme-api.d.ts +108 -0
- package/dist/theme-api.js +268 -0
- package/dist/types-zwMRCGy9.d.ts +81 -0
- package/package.json +70 -4
- package/src/client/Api.tsx +73 -0
- package/src/client/AppRoutes.tsx +51 -0
- package/src/client/ClientOnly.tsx +22 -0
- package/src/client/Demo.tsx +70 -0
- package/src/client/DemoRegistry.ts +11 -0
- package/src/client/NotFound.tsx +13 -0
- package/src/client/Tabs.tsx +70 -0
- package/src/client/entry-client.tsx +84 -0
- package/src/client/entry-server.tsx +52 -0
- package/src/client/index.tsx +4 -0
- package/src/client/search-client.ts +91 -0
- package/src/client/search-core.ts +151 -0
- package/src/client/search-worker.ts +45 -0
- package/src/client/virtual.d.ts +16 -0
- package/src/routesModule.ts +61 -0
- package/src/search-config.ts +45 -0
- package/src/theme-api.tsx +242 -0
- package/src/types.ts +71 -0
- package/README.md +0 -3
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2695 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res, err) => function __init() {
|
|
5
|
+
if (err) throw err[0];
|
|
6
|
+
try {
|
|
7
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
+
} catch (e) {
|
|
9
|
+
throw err = [e], e;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/markdown/errors.ts
|
|
18
|
+
function formatError(err) {
|
|
19
|
+
if (err instanceof DommiefError) return err.format();
|
|
20
|
+
if (err instanceof Error) return err.message;
|
|
21
|
+
return String(err);
|
|
22
|
+
}
|
|
23
|
+
var DommiefError;
|
|
24
|
+
var init_errors = __esm({
|
|
25
|
+
"src/markdown/errors.ts"() {
|
|
26
|
+
"use strict";
|
|
27
|
+
DommiefError = class extends Error {
|
|
28
|
+
file;
|
|
29
|
+
line;
|
|
30
|
+
col;
|
|
31
|
+
code;
|
|
32
|
+
layout;
|
|
33
|
+
constructor(message, opts = {}) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "DommiefError";
|
|
36
|
+
this.file = opts.file ?? "";
|
|
37
|
+
this.line = opts.line;
|
|
38
|
+
this.col = opts.col;
|
|
39
|
+
this.code = opts.code;
|
|
40
|
+
this.layout = opts.layout;
|
|
41
|
+
}
|
|
42
|
+
format() {
|
|
43
|
+
const loc = this.line != null ? `:${this.line}${this.col != null ? ":" + this.col : ""}` : "";
|
|
44
|
+
const head = this.file ? `${this.file}${loc}` : loc.slice(1) || "";
|
|
45
|
+
return head ? `${head}: ${this.message}` : this.message;
|
|
46
|
+
}
|
|
47
|
+
toString() {
|
|
48
|
+
return this.format();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// src/plugins.ts
|
|
55
|
+
function assertSync(result, name) {
|
|
56
|
+
if (result && typeof result.then === "function") {
|
|
57
|
+
throw new DommiefError(`plugin "${name}": setup ph\u1EA3i \u0111\u1ED3ng b\u1ED9 \u2014 hook tap sau \`await\` s\u1EBD kh\xF4ng bao gi\u1EDD ch\u1EA1y`, { code: "plugin_setup_async" });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function registerPlugin(host, plugin) {
|
|
61
|
+
if (!plugin) return;
|
|
62
|
+
if (typeof plugin === "function") {
|
|
63
|
+
assertSync(plugin(host), plugin.name || "plugin");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const p = plugin;
|
|
67
|
+
const name = p.name ?? "plugin";
|
|
68
|
+
if (typeof p.setup === "function") assertSync(p.setup(host), name);
|
|
69
|
+
if (typeof p.modifyConfig === "function") host.modifyConfig.tap(name, p.modifyConfig);
|
|
70
|
+
if (typeof p.modifyRoutes === "function") host.modifyRoutes.tap(name, p.modifyRoutes);
|
|
71
|
+
if (typeof p.addRemarkPlugins === "function") host.addRemarkPlugins.tap(name, p.addRemarkPlugins);
|
|
72
|
+
if (typeof p.addRehypePlugins === "function") host.addRehypePlugins.tap(name, p.addRehypePlugins);
|
|
73
|
+
if (typeof p.addHtmlHeadTags === "function") host.addHtmlHeadTags.tap(name, p.addHtmlHeadTags);
|
|
74
|
+
if (typeof p.onBuildEnd === "function") host.onBuildEnd.tap(name, p.onBuildEnd);
|
|
75
|
+
}
|
|
76
|
+
function createPluginHost(plugins = []) {
|
|
77
|
+
const host = new PluginHookHost();
|
|
78
|
+
for (const p of plugins ?? []) registerPlugin(host, p);
|
|
79
|
+
return host;
|
|
80
|
+
}
|
|
81
|
+
var Hook, PluginHookHost;
|
|
82
|
+
var init_plugins = __esm({
|
|
83
|
+
"src/plugins.ts"() {
|
|
84
|
+
"use strict";
|
|
85
|
+
init_errors();
|
|
86
|
+
Hook = class {
|
|
87
|
+
name;
|
|
88
|
+
constructor(name = "") {
|
|
89
|
+
this.name = name;
|
|
90
|
+
}
|
|
91
|
+
taps = [];
|
|
92
|
+
tap(name, fn) {
|
|
93
|
+
this.taps.push({ name, fn });
|
|
94
|
+
}
|
|
95
|
+
call(arg) {
|
|
96
|
+
let value = arg;
|
|
97
|
+
for (const t of this.taps) {
|
|
98
|
+
const r = t.fn(value);
|
|
99
|
+
if (r !== void 0) value = r;
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
async callAsync(arg) {
|
|
104
|
+
let value = arg;
|
|
105
|
+
for (const t of this.taps) {
|
|
106
|
+
const r = await t.fn(value);
|
|
107
|
+
if (r !== void 0) value = r;
|
|
108
|
+
}
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
PluginHookHost = class {
|
|
113
|
+
modifyConfig = new Hook("modifyConfig");
|
|
114
|
+
modifyRoutes = new Hook("modifyRoutes");
|
|
115
|
+
addRemarkPlugins = new Hook("addRemarkPlugins");
|
|
116
|
+
addRehypePlugins = new Hook("addRehypePlugins");
|
|
117
|
+
addHtmlHeadTags = new Hook("addHtmlHeadTags");
|
|
118
|
+
onBuildEnd = new Hook("onBuildEnd");
|
|
119
|
+
applyModifyConfig(config) {
|
|
120
|
+
return this.modifyConfig.call(config);
|
|
121
|
+
}
|
|
122
|
+
applyModifyRoutes(routes) {
|
|
123
|
+
const r = this.modifyRoutes.call(routes);
|
|
124
|
+
return r ?? routes;
|
|
125
|
+
}
|
|
126
|
+
collectRemarkPlugins() {
|
|
127
|
+
const out = [];
|
|
128
|
+
for (const t of this.addRemarkPlugins.taps) {
|
|
129
|
+
const r = t.fn();
|
|
130
|
+
if (Array.isArray(r)) out.push(...r);
|
|
131
|
+
else if (r !== void 0 && r !== null) out.push(r);
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
collectRehypePlugins() {
|
|
136
|
+
const out = [];
|
|
137
|
+
for (const t of this.addRehypePlugins.taps) {
|
|
138
|
+
const r = t.fn();
|
|
139
|
+
if (Array.isArray(r)) out.push(...r);
|
|
140
|
+
else if (r !== void 0 && r !== null) out.push(r);
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
collectHtmlHeadTags() {
|
|
145
|
+
const out = [];
|
|
146
|
+
for (const t of this.addHtmlHeadTags.taps) {
|
|
147
|
+
const r = t.fn();
|
|
148
|
+
if (Array.isArray(r)) out.push(...r.map(String));
|
|
149
|
+
else if (r !== void 0 && r !== null) out.push(String(r));
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
async runBuildEnd(ctx) {
|
|
154
|
+
for (const t of this.onBuildEnd.taps) {
|
|
155
|
+
await t.fn(ctx);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// src/markdown/remark-meta.ts
|
|
163
|
+
import yaml from "yaml";
|
|
164
|
+
import { unified } from "unified";
|
|
165
|
+
import remarkParse from "remark-parse";
|
|
166
|
+
import remarkFrontmatter from "remark-frontmatter";
|
|
167
|
+
import Slugger from "github-slugger";
|
|
168
|
+
import { visit } from "unist-util-visit";
|
|
169
|
+
function textOf(node) {
|
|
170
|
+
let out = "";
|
|
171
|
+
if (node.type === "text") out += node.value || "";
|
|
172
|
+
if (node.children) for (const c of node.children) out += textOf(c);
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
function parseFrontmatterYaml(raw, opts = {}) {
|
|
176
|
+
const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
177
|
+
if (!m || m[1] === void 0) return {};
|
|
178
|
+
try {
|
|
179
|
+
return yaml.parse(m[1]) ?? {};
|
|
180
|
+
} catch (e) {
|
|
181
|
+
opts.onWarn?.(`frontmatter_invalid_yaml: ${opts.file ?? ""}: ${e.message}`);
|
|
182
|
+
return {};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function remarkMeta(options = {}) {
|
|
186
|
+
return (tree, file) => {
|
|
187
|
+
const filename = options.filename ?? file?.path ?? file?.history?.[0] ?? "";
|
|
188
|
+
file.data = file.data || {};
|
|
189
|
+
let frontmatter = {};
|
|
190
|
+
if (tree.children?.[0]?.type === "yaml") {
|
|
191
|
+
frontmatter = parseFrontmatterYaml("---\n" + tree.children[0].value + "\n---", { file: filename });
|
|
192
|
+
tree.children.shift();
|
|
193
|
+
}
|
|
194
|
+
if (frontmatter.title == null) {
|
|
195
|
+
let firstH1 = null;
|
|
196
|
+
visit(tree, "heading", (node) => {
|
|
197
|
+
if (node.depth === 1 && firstH1 === null) firstH1 = textOf(node).trim();
|
|
198
|
+
});
|
|
199
|
+
if (firstH1) frontmatter.title = firstH1;
|
|
200
|
+
}
|
|
201
|
+
file.data.frontmatter = frontmatter;
|
|
202
|
+
file.data.filename = filename;
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function extractPageSummary(raw, file, onWarn) {
|
|
206
|
+
const frontmatter = parseFrontmatterYaml(raw, onWarn ? { file, onWarn } : { file });
|
|
207
|
+
const tree = unified().use(remarkParse).use(remarkFrontmatter, ["yaml"]).parse(raw);
|
|
208
|
+
const toc = [];
|
|
209
|
+
const slugger = new Slugger();
|
|
210
|
+
let firstH1 = null;
|
|
211
|
+
visit(tree, "heading", (node) => {
|
|
212
|
+
const title = textOf(node).trim();
|
|
213
|
+
if (!title) return;
|
|
214
|
+
const id = slugger.slug(title);
|
|
215
|
+
toc.push({ id, depth: node.depth, title });
|
|
216
|
+
if (node.depth === 1 && firstH1 === null) firstH1 = title;
|
|
217
|
+
});
|
|
218
|
+
if (frontmatter.title == null && firstH1) frontmatter.title = firstH1;
|
|
219
|
+
return { frontmatter, toc };
|
|
220
|
+
}
|
|
221
|
+
var init_remark_meta = __esm({
|
|
222
|
+
"src/markdown/remark-meta.ts"() {
|
|
223
|
+
"use strict";
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// src/markdown/rehype-slug.ts
|
|
228
|
+
import { visit as visit2 } from "unist-util-visit";
|
|
229
|
+
import Slugger2 from "github-slugger";
|
|
230
|
+
function textOf2(node) {
|
|
231
|
+
if (node.type === "text") return node.value || "";
|
|
232
|
+
if (node.children) return node.children.map(textOf2).join("");
|
|
233
|
+
return "";
|
|
234
|
+
}
|
|
235
|
+
function rehypeSlug() {
|
|
236
|
+
return (tree, file) => {
|
|
237
|
+
file.data = file.data || {};
|
|
238
|
+
const toc = [];
|
|
239
|
+
const slugger = new Slugger2();
|
|
240
|
+
visit2(tree, "element", (node) => {
|
|
241
|
+
if (/^h[1-6]$/.test(node.tagName)) {
|
|
242
|
+
const text = textOf2(node).trim();
|
|
243
|
+
const depth = Number(node.tagName.slice(1));
|
|
244
|
+
let id = node.properties?.id;
|
|
245
|
+
if (!id) id = slugger.slug(text);
|
|
246
|
+
if (!node.properties) node.properties = {};
|
|
247
|
+
node.properties.id = id;
|
|
248
|
+
toc.push({ id, depth, title: text });
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
file.data.toc = toc;
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function rehypeAutolinkHeadings() {
|
|
255
|
+
return (tree) => {
|
|
256
|
+
visit2(tree, "element", (node) => {
|
|
257
|
+
if (/^h[1-6]$/.test(node.tagName) && node.properties?.id) {
|
|
258
|
+
const anchor = { type: "element", tagName: "a", properties: { href: "#" + node.properties.id, className: ["dommief-heading-anchor"], tabIndex: -1 }, children: [] };
|
|
259
|
+
node.children = [anchor, ...node.children];
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
var init_rehype_slug = __esm({
|
|
265
|
+
"src/markdown/rehype-slug.ts"() {
|
|
266
|
+
"use strict";
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// src/utils/path.ts
|
|
271
|
+
function winPath(p) {
|
|
272
|
+
if (/^\\\\\?\\/.test(p)) return p;
|
|
273
|
+
return p.replace(/\\/g, "/");
|
|
274
|
+
}
|
|
275
|
+
var init_path = __esm({
|
|
276
|
+
"src/utils/path.ts"() {
|
|
277
|
+
"use strict";
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// src/markdown/rehype-link.ts
|
|
282
|
+
import path2 from "path";
|
|
283
|
+
import { visit as visit3 } from "unist-util-visit";
|
|
284
|
+
function rehypeLink(options = {}) {
|
|
285
|
+
return (tree, file) => {
|
|
286
|
+
const from = winPath(String(file?.path ?? ""));
|
|
287
|
+
const prefix = (options.base ?? "/").replace(/\/$/, "");
|
|
288
|
+
const warnings = Array.isArray(file?.data?.warnings) ? file.data.warnings : [];
|
|
289
|
+
visit3(tree, "element", (node) => {
|
|
290
|
+
if (node.tagName !== "a" || !node.properties?.href) return;
|
|
291
|
+
const href = String(node.properties.href);
|
|
292
|
+
if (EXTERNAL.test(href)) return;
|
|
293
|
+
const m = href.match(/^([^#?]*\.md)([?#].*)?$/);
|
|
294
|
+
if (!m || m[1] === void 0) return;
|
|
295
|
+
const tail = m[2] ?? "";
|
|
296
|
+
if (!options.byFile) {
|
|
297
|
+
node.properties.href = m[1].replace(/\.md$/, "") + tail;
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const abs = path2.posix.resolve(path2.posix.dirname(from), m[1]);
|
|
301
|
+
const route = options.byFile[abs];
|
|
302
|
+
if (route) node.properties.href = (route.path === "/" ? prefix + "/" : prefix + route.path) + tail;
|
|
303
|
+
else warnings.push(`link_unresolved: "${href}" trong ${from} kh\xF4ng tr\u1ECF t\u1EDBi file md n\xE0o thu\u1ED9c docDirs`);
|
|
304
|
+
});
|
|
305
|
+
visit3(tree, "element", (node) => {
|
|
306
|
+
if (node.tagName !== "a" || !node.properties?.href || node.properties["data-src-line"]) return;
|
|
307
|
+
const href = String(node.properties.href);
|
|
308
|
+
if (EXTERNAL.test(href) && !href.startsWith("/")) return;
|
|
309
|
+
const line = node.position?.start?.line;
|
|
310
|
+
if (line) node.properties["data-src-line"] = String(line);
|
|
311
|
+
});
|
|
312
|
+
if (file?.data) file.data.warnings = warnings;
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
var EXTERNAL;
|
|
316
|
+
var init_rehype_link = __esm({
|
|
317
|
+
"src/markdown/rehype-link.ts"() {
|
|
318
|
+
"use strict";
|
|
319
|
+
init_path();
|
|
320
|
+
EXTERNAL = /^([a-z][a-z0-9+.-]*:|\/\/|#|\/)/i;
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// src/markdown/rehype-img.ts
|
|
325
|
+
import path3 from "path";
|
|
326
|
+
import { visit as visit4 } from "unist-util-visit";
|
|
327
|
+
function rehypeImg() {
|
|
328
|
+
return (tree, file) => {
|
|
329
|
+
const images = [];
|
|
330
|
+
visit4(tree, "element", (node) => {
|
|
331
|
+
if (node.tagName !== "img" || !node.properties?.src) return;
|
|
332
|
+
const src = String(node.properties.src);
|
|
333
|
+
if (SKIP.test(src)) return;
|
|
334
|
+
const clean = src.replace(/[?#].*$/, "");
|
|
335
|
+
const specifier = clean.startsWith(".") ? clean : "./" + path3.posix.normalize(clean);
|
|
336
|
+
const name = `${PLACEHOLDER}${images.length}__`;
|
|
337
|
+
images.push({ name, specifier });
|
|
338
|
+
node.properties.src = name;
|
|
339
|
+
});
|
|
340
|
+
visit4(tree, "element", (node) => {
|
|
341
|
+
if (node.tagName !== "img" || !node.properties?.src || node.properties["data-src-line"]) return;
|
|
342
|
+
const line = node.position?.start?.line;
|
|
343
|
+
if (line) node.properties["data-src-line"] = String(line);
|
|
344
|
+
});
|
|
345
|
+
file.data = file.data || {};
|
|
346
|
+
file.data.images = images;
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
var PLACEHOLDER, SKIP;
|
|
350
|
+
var init_rehype_img = __esm({
|
|
351
|
+
"src/markdown/rehype-img.ts"() {
|
|
352
|
+
"use strict";
|
|
353
|
+
PLACEHOLDER = "__DOMMIEF_IMG_";
|
|
354
|
+
SKIP = /^([a-z][a-z0-9+.-]*:|\/\/|#|\/)/i;
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// src/markdown/rehype-desc.ts
|
|
359
|
+
import { visit as visit5 } from "unist-util-visit";
|
|
360
|
+
function rehypeDesc() {
|
|
361
|
+
return (tree, file) => {
|
|
362
|
+
file.data = file.data || {};
|
|
363
|
+
let desc;
|
|
364
|
+
visit5(tree, "element", (node) => {
|
|
365
|
+
if (!desc && node.tagName === "p") {
|
|
366
|
+
const text = (node.children || []).map((c) => c.value || "").join("").trim();
|
|
367
|
+
if (text) desc = text.slice(0, 160);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
if (desc) {
|
|
371
|
+
if (!file.data.frontmatter) file.data.frontmatter = {};
|
|
372
|
+
if (file.data.frontmatter.description == null) file.data.frontmatter.description = desc;
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function rehypeStrip() {
|
|
377
|
+
return (tree) => {
|
|
378
|
+
visit5(tree, "element", (node, index, parent) => {
|
|
379
|
+
if (node.tagName === "script" && parent && index !== void 0) {
|
|
380
|
+
parent.children.splice(index, 1);
|
|
381
|
+
return index;
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
var init_rehype_desc = __esm({
|
|
387
|
+
"src/markdown/rehype-desc.ts"() {
|
|
388
|
+
"use strict";
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// src/markdown/rehype-text.ts
|
|
393
|
+
function rehypeText() {
|
|
394
|
+
return (tree, file) => {
|
|
395
|
+
file.data = file.data || {};
|
|
396
|
+
const toc = file.data.toc || [];
|
|
397
|
+
const texts = [];
|
|
398
|
+
const paraIdOf = /* @__PURE__ */ new WeakMap();
|
|
399
|
+
let nextParaId = 0;
|
|
400
|
+
let tocCursor = -1;
|
|
401
|
+
let currentTocIndex;
|
|
402
|
+
const isContainer = (node) => {
|
|
403
|
+
if (!node || node.type !== "element") return false;
|
|
404
|
+
const tag = node.tagName;
|
|
405
|
+
if (tag === "p" || tag === "ul" || tag === "ol" || tag === "root") return true;
|
|
406
|
+
return typeof tag === "string" && /^[A-Z]/.test(tag);
|
|
407
|
+
};
|
|
408
|
+
const containerOf = (ancestors) => {
|
|
409
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
410
|
+
if (isContainer(ancestors[i])) return ancestors[i];
|
|
411
|
+
}
|
|
412
|
+
return tree;
|
|
413
|
+
};
|
|
414
|
+
const walk = (node, ancestors) => {
|
|
415
|
+
if (node && node.type === "element" && /^h[1-6]$/.test(node.tagName)) {
|
|
416
|
+
tocCursor += 1;
|
|
417
|
+
if (tocCursor < toc.length) currentTocIndex = tocCursor;
|
|
418
|
+
for (const c of node.children || []) walk(c, [...ancestors, node]);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (node && node.type === "text") {
|
|
422
|
+
const parent = ancestors[ancestors.length - 1];
|
|
423
|
+
if (parent && /^h[1-6]$/.test(parent.tagName)) return;
|
|
424
|
+
const v = node.value || "";
|
|
425
|
+
if (!v.trim()) return;
|
|
426
|
+
const container = containerOf(ancestors);
|
|
427
|
+
if (!paraIdOf.has(container)) paraIdOf.set(container, nextParaId++);
|
|
428
|
+
const paraId = paraIdOf.get(container);
|
|
429
|
+
const frag = { type: "content", value: v, paraId };
|
|
430
|
+
if (currentTocIndex !== void 0) frag.tocIndex = currentTocIndex;
|
|
431
|
+
texts.push(frag);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (node && Array.isArray(node.children)) {
|
|
435
|
+
for (const c of node.children) walk(c, [...ancestors, node]);
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
walk(tree, []);
|
|
439
|
+
file.data.texts = texts;
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
var init_rehype_text = __esm({
|
|
443
|
+
"src/markdown/rehype-text.ts"() {
|
|
444
|
+
"use strict";
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// src/markdown/rehype-demo.ts
|
|
449
|
+
import { visit as visit6 } from "unist-util-visit";
|
|
450
|
+
import { createHash } from "crypto";
|
|
451
|
+
function remarkDemoMeta() {
|
|
452
|
+
return (tree) => {
|
|
453
|
+
visit6(tree, "code", (node) => {
|
|
454
|
+
if (/\bdemo\b/.test(String(node.meta ?? ""))) {
|
|
455
|
+
node.data = { ...node.data, hProperties: { ...node.data?.hProperties, "data-demo": "true" } };
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
function demoId(source, file) {
|
|
461
|
+
return createHash("sha256").update(file + "\n" + source).digest("hex").slice(0, 8);
|
|
462
|
+
}
|
|
463
|
+
function codeText(node) {
|
|
464
|
+
let out = "";
|
|
465
|
+
visit6(node, "text", (t) => {
|
|
466
|
+
out += t.value;
|
|
467
|
+
});
|
|
468
|
+
return out;
|
|
469
|
+
}
|
|
470
|
+
function rehypeDemo() {
|
|
471
|
+
return (tree, file) => {
|
|
472
|
+
const demos = Array.isArray(file.data?.demos) ? file.data.demos : [];
|
|
473
|
+
const filePath = String(file.path ?? "");
|
|
474
|
+
visit6(tree, "element", (node, index, parent) => {
|
|
475
|
+
if (!parent || typeof index !== "number") return;
|
|
476
|
+
if (node.tagName === "code" && node.properties?.src) {
|
|
477
|
+
const src = String(node.properties.src);
|
|
478
|
+
const source2 = codeText(node).trim();
|
|
479
|
+
const id2 = demoId(source2 || src, filePath);
|
|
480
|
+
demos.push({ id: id2, source: source2, src });
|
|
481
|
+
parent.children[index] = {
|
|
482
|
+
type: "element",
|
|
483
|
+
tagName: "DomDemo",
|
|
484
|
+
properties: { id: id2, src, source: source2 },
|
|
485
|
+
children: []
|
|
486
|
+
};
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (node.tagName !== "pre") return;
|
|
490
|
+
const codeNode = Array.isArray(node.children) ? node.children.find((c) => c?.tagName === "code") : null;
|
|
491
|
+
if (!codeNode) return;
|
|
492
|
+
if (!codeNode.properties?.dataDemo) return;
|
|
493
|
+
const source = codeText(codeNode).replace(/\n$/, "");
|
|
494
|
+
const id = demoId(source, filePath);
|
|
495
|
+
demos.push({ id, source });
|
|
496
|
+
parent.children[index] = {
|
|
497
|
+
type: "element",
|
|
498
|
+
tagName: "DomDemo",
|
|
499
|
+
properties: { id, source },
|
|
500
|
+
children: []
|
|
501
|
+
};
|
|
502
|
+
});
|
|
503
|
+
file.data.demos = demos;
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
var init_rehype_demo = __esm({
|
|
507
|
+
"src/markdown/rehype-demo.ts"() {
|
|
508
|
+
"use strict";
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// src/markdown/rehype-api.ts
|
|
513
|
+
import { visit as visit7 } from "unist-util-visit";
|
|
514
|
+
import { createHash as createHash2 } from "crypto";
|
|
515
|
+
import fs2 from "fs";
|
|
516
|
+
import path4 from "path";
|
|
517
|
+
import { createRequire } from "module";
|
|
518
|
+
function loadDocgen() {
|
|
519
|
+
if (docgen) return docgen;
|
|
520
|
+
try {
|
|
521
|
+
docgen = createRequire(import.meta.url)("react-docgen-typescript");
|
|
522
|
+
return docgen;
|
|
523
|
+
} catch (e) {
|
|
524
|
+
throw new DommiefError(`<API> c\u1EA7n react-docgen-typescript + typescript 5.x: ${e.message}`, { code: "api_docgen_unavailable" });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
function hashFileContent(content) {
|
|
528
|
+
return createHash2("sha256").update(content).digest("hex");
|
|
529
|
+
}
|
|
530
|
+
function resolveApiSrc(src, filePath) {
|
|
531
|
+
const base = filePath ? path4.dirname(filePath) : process.cwd();
|
|
532
|
+
return path4.isAbsolute(src) ? src : path4.resolve(base, src);
|
|
533
|
+
}
|
|
534
|
+
function buildApiTable(src, filePath = "") {
|
|
535
|
+
const abs = resolveApiSrc(src, filePath);
|
|
536
|
+
let content;
|
|
537
|
+
try {
|
|
538
|
+
content = fs2.readFileSync(abs, "utf8");
|
|
539
|
+
} catch {
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
const hash = hashFileContent(content);
|
|
543
|
+
const cached = apiCache.get(abs);
|
|
544
|
+
if (cached && cached.hash === hash) return cached.entries;
|
|
545
|
+
const parser = loadDocgen().withDefaultConfig({});
|
|
546
|
+
let docs;
|
|
547
|
+
try {
|
|
548
|
+
docs = parser.parse(abs) ?? [];
|
|
549
|
+
} catch {
|
|
550
|
+
docs = [];
|
|
551
|
+
}
|
|
552
|
+
const entries = (docs || []).map((d) => ({
|
|
553
|
+
displayName: d.displayName,
|
|
554
|
+
description: d.description || "",
|
|
555
|
+
src: abs,
|
|
556
|
+
hash,
|
|
557
|
+
props: Object.values(d.props || {}).map((p) => ({
|
|
558
|
+
name: p.name,
|
|
559
|
+
type: p.type?.name ?? p.type?.raw ?? "",
|
|
560
|
+
required: !!p.required,
|
|
561
|
+
description: p.description || "",
|
|
562
|
+
defaultValue: p.defaultValue?.value
|
|
563
|
+
}))
|
|
564
|
+
}));
|
|
565
|
+
apiCache.set(abs, { hash, entries });
|
|
566
|
+
return entries;
|
|
567
|
+
}
|
|
568
|
+
function rehypeApi() {
|
|
569
|
+
return (tree, file) => {
|
|
570
|
+
const apis = Array.isArray(file?.data?.apis) ? file.data.apis : [];
|
|
571
|
+
const watched = Array.isArray(file?.data?.apiSrcFiles) ? file.data.apiSrcFiles : [];
|
|
572
|
+
const filePath = String(file?.path ?? "");
|
|
573
|
+
visit7(tree, "element", (node, index, parent) => {
|
|
574
|
+
if (!parent || typeof index !== "number") return;
|
|
575
|
+
if (String(node.tagName).toLowerCase() !== "api") return;
|
|
576
|
+
const src = String(node.properties?.src ?? "");
|
|
577
|
+
if (!src) return;
|
|
578
|
+
const abs = resolveApiSrc(src, filePath);
|
|
579
|
+
const data = buildApiTable(src, filePath);
|
|
580
|
+
watched.push(abs);
|
|
581
|
+
apis.push({ id: hashFileContent(src + filePath).slice(0, 8), src: abs, data });
|
|
582
|
+
parent.children[index] = {
|
|
583
|
+
type: "element",
|
|
584
|
+
tagName: "DomApi",
|
|
585
|
+
properties: {
|
|
586
|
+
src: abs,
|
|
587
|
+
data: data ? JSON.stringify(data) : void 0
|
|
588
|
+
},
|
|
589
|
+
children: []
|
|
590
|
+
};
|
|
591
|
+
});
|
|
592
|
+
if (file?.data) {
|
|
593
|
+
file.data.apis = apis;
|
|
594
|
+
file.data.apiSrcFiles = watched;
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
var docgen, apiCache;
|
|
599
|
+
var init_rehype_api = __esm({
|
|
600
|
+
"src/markdown/rehype-api.ts"() {
|
|
601
|
+
"use strict";
|
|
602
|
+
init_errors();
|
|
603
|
+
apiCache = /* @__PURE__ */ new Map();
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
// src/markdown/remark-container.ts
|
|
608
|
+
import { visit as visit8 } from "unist-util-visit";
|
|
609
|
+
function restrictToContainerSyntax(extensions) {
|
|
610
|
+
const isDirective = (e) => {
|
|
611
|
+
const f = e?.flow?.[58];
|
|
612
|
+
return !!e?.text?.[58] && (Array.isArray(f) ? f : [f]).some((c) => /Directive/.test(c?.tokenize?.name ?? ""));
|
|
613
|
+
};
|
|
614
|
+
const hit = extensions.filter(isDirective);
|
|
615
|
+
if (hit.length !== 1) throw new Error("remark-directive \u0111\u1ED5i c\u1EA5u tr\xFAc n\u1ED9i b\u1ED9 (kh\xF4ng th\u1EA5y \u0111\xFAng 1 extension directive {text, flow}); c\u1EADp nh\u1EADt remarkContainer");
|
|
616
|
+
const ext = hit[0];
|
|
617
|
+
const flow = ext.flow[58];
|
|
618
|
+
const constructs = Array.isArray(flow) ? flow : [flow];
|
|
619
|
+
const containers = constructs.filter((c) => /Container/.test(c?.tokenize?.name ?? ""));
|
|
620
|
+
if (containers.length !== 1) throw new Error("remark-directive \u0111\u1ED5i c\u1EA5u tr\xFAc n\u1ED9i b\u1ED9 (kh\xF4ng th\u1EA5y \u0111\xFAng 1 container construct); c\u1EADp nh\u1EADt remarkContainer");
|
|
621
|
+
delete ext.text;
|
|
622
|
+
ext.flow = { 58: containers };
|
|
623
|
+
}
|
|
624
|
+
function remarkContainer() {
|
|
625
|
+
const data = this.data();
|
|
626
|
+
restrictToContainerSyntax(data.micromarkExtensions ?? []);
|
|
627
|
+
return (tree, file) => {
|
|
628
|
+
visit8(tree, "containerDirective", (node, index, parent) => {
|
|
629
|
+
if (!parent || typeof index !== "number") return;
|
|
630
|
+
const name = String(node.name);
|
|
631
|
+
if (!CONTAINER_TYPES.includes(name)) {
|
|
632
|
+
const warnings = (file.data ??= {}).warnings ??= [];
|
|
633
|
+
const pos = node.position?.start;
|
|
634
|
+
warnings.push(`${file.path ?? ""}${pos ? `:${pos.line}:${pos.column}` : ""} container ":::${name}" kh\xF4ng \u0111\u01B0\u1EE3c h\u1ED7 tr\u1EE3 (ch\u1EC9 ${CONTAINER_TYPES.join("/")}); render th\xE0nh <div> th\u01B0\u1EDDng`);
|
|
635
|
+
node.data = { hName: "div", hProperties: {} };
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
const type = name;
|
|
639
|
+
const labelIdx = node.children.findIndex((c) => c.data?.directiveLabel);
|
|
640
|
+
const labelNode = labelIdx >= 0 ? node.children.splice(labelIdx, 1)[0] : null;
|
|
641
|
+
const title = node.attributes?.title ?? (labelNode ? textOf3(labelNode) : void 0);
|
|
642
|
+
const children = [...node.children];
|
|
643
|
+
if (title) {
|
|
644
|
+
children.unshift({ type: "paragraph", data: { hName: "p", hProperties: { className: ["dommief-container__title"] } }, children: [{ type: "text", value: title }] });
|
|
645
|
+
}
|
|
646
|
+
node.children = children;
|
|
647
|
+
node.data = {
|
|
648
|
+
hName: "div",
|
|
649
|
+
hProperties: {
|
|
650
|
+
className: ["dommief-container", `dommief-container--${type}`],
|
|
651
|
+
// warning/error là thông tin cần chú ý ngay → role="alert" không đúng (nó ngắt screen reader);
|
|
652
|
+
// dùng role="note" cho mọi loại, kiểu nằm ở class.
|
|
653
|
+
role: "note",
|
|
654
|
+
...title ? {} : { "aria-label": type }
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
});
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
function textOf3(node) {
|
|
661
|
+
let out = "";
|
|
662
|
+
visit8(node, "text", (t) => {
|
|
663
|
+
out += t.value;
|
|
664
|
+
});
|
|
665
|
+
return out.trim();
|
|
666
|
+
}
|
|
667
|
+
var CONTAINER_TYPES;
|
|
668
|
+
var init_remark_container = __esm({
|
|
669
|
+
"src/markdown/remark-container.ts"() {
|
|
670
|
+
"use strict";
|
|
671
|
+
CONTAINER_TYPES = ["info", "warning", "success", "error"];
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
// src/markdown/rehype-code.ts
|
|
676
|
+
import { visit as visit9 } from "unist-util-visit";
|
|
677
|
+
import { bundledLanguages, createHighlighter } from "shiki";
|
|
678
|
+
function parseHighlightLines(meta) {
|
|
679
|
+
const m = meta.match(HIGHLIGHT_RE);
|
|
680
|
+
if (!m) return { lines: [], rest: meta.trim() };
|
|
681
|
+
const set = /* @__PURE__ */ new Set();
|
|
682
|
+
for (const part of m[1].split(",").filter(Boolean)) {
|
|
683
|
+
const [a, b] = part.split("-").map(Number);
|
|
684
|
+
const end = b ?? a;
|
|
685
|
+
for (let n = Math.min(a, end); n <= Math.max(a, end); n++) if (n >= 1) set.add(n);
|
|
686
|
+
}
|
|
687
|
+
return { lines: [...set].sort((x, y) => x - y), rest: meta.replace(HIGHLIGHT_RE, "").trim() };
|
|
688
|
+
}
|
|
689
|
+
function remarkCodeMeta() {
|
|
690
|
+
return (tree) => {
|
|
691
|
+
visit9(tree, "code", (node) => {
|
|
692
|
+
const meta = String(node.meta ?? "");
|
|
693
|
+
if (!meta) return;
|
|
694
|
+
const { lines, rest } = parseHighlightLines(meta);
|
|
695
|
+
const title = rest.match(/\[([^\]]+)\]/)?.[1];
|
|
696
|
+
const props = {};
|
|
697
|
+
if (lines.length) props["data-highlight-lines"] = lines.join(",");
|
|
698
|
+
if (title) props["data-title"] = title;
|
|
699
|
+
if (Object.keys(props).length) node.data = { ...node.data, hProperties: { ...node.data?.hProperties, ...props } };
|
|
700
|
+
node.meta = rest || null;
|
|
701
|
+
});
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
function getHighlighter(opts) {
|
|
705
|
+
const key = `${opts.themes.light}|${opts.themes.dark}`;
|
|
706
|
+
let h = highlighters.get(key);
|
|
707
|
+
if (!h) {
|
|
708
|
+
h = createHighlighter({ themes: [opts.themes.light, opts.themes.dark], langs: [] });
|
|
709
|
+
highlighters.set(key, h);
|
|
710
|
+
}
|
|
711
|
+
return h;
|
|
712
|
+
}
|
|
713
|
+
function textOf4(node) {
|
|
714
|
+
let out = "";
|
|
715
|
+
visit9(node, "text", (t) => {
|
|
716
|
+
out += t.value;
|
|
717
|
+
});
|
|
718
|
+
return out;
|
|
719
|
+
}
|
|
720
|
+
function langOf(code) {
|
|
721
|
+
const cls = code.properties?.className;
|
|
722
|
+
const list = Array.isArray(cls) ? cls.map(String) : typeof cls === "string" ? cls.split(/\s+/) : [];
|
|
723
|
+
return list.map((c) => /^language-(.+)$/.exec(c)?.[1]).find(Boolean);
|
|
724
|
+
}
|
|
725
|
+
function markLines(pre, highlight, lineNumbers) {
|
|
726
|
+
const code = pre.children?.find((c) => c.tagName === "code");
|
|
727
|
+
let n = 0;
|
|
728
|
+
for (const child of code?.children ?? []) {
|
|
729
|
+
if (child.type !== "element" || child.tagName !== "span") continue;
|
|
730
|
+
const cls = String(child.properties?.class ?? child.properties?.className ?? "");
|
|
731
|
+
if (!/\bline\b/.test(cls)) continue;
|
|
732
|
+
n++;
|
|
733
|
+
const classes = cls.split(/\s+/).filter(Boolean);
|
|
734
|
+
if (highlight.has(n)) classes.push("highlighted");
|
|
735
|
+
child.properties = { ...child.properties, class: classes.join(" ") };
|
|
736
|
+
if (lineNumbers) child.properties["data-line"] = String(n);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
function rehypeCode(options = {}) {
|
|
740
|
+
const shiki = options.shiki === void 0 ? DEFAULT_SHIKI : options.shiki;
|
|
741
|
+
const lineNumbers = !!options.lineNumbers;
|
|
742
|
+
return async (tree, file) => {
|
|
743
|
+
const jobs = [];
|
|
744
|
+
visit9(tree, "element", (node, index, parent) => {
|
|
745
|
+
if (node.tagName !== "pre" || !parent || typeof index !== "number") return;
|
|
746
|
+
const kids = (node.children ?? []).filter((c) => !(c.type === "text" && !c.value.trim()));
|
|
747
|
+
if (kids.length !== 1 || kids[0].tagName !== "code") return;
|
|
748
|
+
jobs.push({ parent, index, pre: node, code: kids[0] });
|
|
749
|
+
});
|
|
750
|
+
if (!jobs.length) return;
|
|
751
|
+
const highlighter = shiki ? await getHighlighter(shiki) : null;
|
|
752
|
+
for (const { parent, index, pre, code } of jobs) {
|
|
753
|
+
const lang = langOf(code);
|
|
754
|
+
const title = code.properties?.dataTitle;
|
|
755
|
+
const hl = new Set(String(code.properties?.dataHighlightLines ?? "").split(",").filter(Boolean).map(Number));
|
|
756
|
+
let inner = pre;
|
|
757
|
+
if (highlighter && shiki && lang) {
|
|
758
|
+
if (lang in bundledLanguages) {
|
|
759
|
+
if (!highlighter.getLoadedLanguages().includes(lang)) await highlighter.loadLanguage(lang);
|
|
760
|
+
const source = textOf4(code).replace(/\n$/, "");
|
|
761
|
+
const root = highlighter.codeToHast(source, { lang, themes: { light: shiki.themes.light, dark: shiki.themes.dark }, defaultColor: false });
|
|
762
|
+
inner = root.children[0];
|
|
763
|
+
markLines(inner, hl, lineNumbers);
|
|
764
|
+
} else {
|
|
765
|
+
const warnings = (file.data ??= {}).warnings ??= [];
|
|
766
|
+
const pos = pre.position?.start;
|
|
767
|
+
warnings.push(`${file.path ?? ""}${pos ? `:${pos.line}` : ""} Shiki kh\xF4ng c\xF3 grammar cho "${lang}"; hi\u1EC3n th\u1ECB code th\xF4`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (inner === pre && hl.size) pre.properties = { ...pre.properties, "data-highlight-lines": [...hl].join(",") };
|
|
771
|
+
delete code.properties?.dataHighlightLines;
|
|
772
|
+
delete code.properties?.dataTitle;
|
|
773
|
+
const children = [];
|
|
774
|
+
if (title) children.push({ type: "element", tagName: "div", properties: { className: ["dommief-code__title"] }, children: [{ type: "text", value: title }] });
|
|
775
|
+
children.push(inner);
|
|
776
|
+
parent.children[index] = {
|
|
777
|
+
type: "element",
|
|
778
|
+
tagName: "div",
|
|
779
|
+
properties: { className: ["dommief-code"], ...lang ? { "data-lang": lang } : {} },
|
|
780
|
+
children
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
var DEFAULT_SHIKI, CODE_PIPELINE_VERSION, HIGHLIGHT_RE, highlighters;
|
|
786
|
+
var init_rehype_code = __esm({
|
|
787
|
+
"src/markdown/rehype-code.ts"() {
|
|
788
|
+
"use strict";
|
|
789
|
+
DEFAULT_SHIKI = { themes: { light: "github-light", dark: "github-dark" } };
|
|
790
|
+
CODE_PIPELINE_VERSION = "shiki4-v1";
|
|
791
|
+
HIGHLIGHT_RE = /\{((?:\d+(?:-\d+)?,?)+)\}/;
|
|
792
|
+
highlighters = /* @__PURE__ */ new Map();
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
// src/markdown/to-module.ts
|
|
797
|
+
import { toEstree } from "hast-util-to-estree";
|
|
798
|
+
import { toJs, jsx } from "estree-util-to-js";
|
|
799
|
+
function toModule(hast, meta, images = [], demos = []) {
|
|
800
|
+
const { texts: _texts, ...clientMeta } = meta;
|
|
801
|
+
const estree = toEstree(hast, { elementAttributeNameCase: "react" });
|
|
802
|
+
const { value: code } = toJs(estree, { handlers: jsx });
|
|
803
|
+
let body = code.replace(/;\s*$/, "").trim();
|
|
804
|
+
const imageImports = images.map((im, i) => {
|
|
805
|
+
body = body.split(`src="${im.name}"`).join(`src={__dmImg${i}}`);
|
|
806
|
+
return `import __dmImg${i} from ${JSON.stringify(im.specifier)};
|
|
807
|
+
`;
|
|
808
|
+
}).join("");
|
|
809
|
+
const used = ["DomDemo", "DumiDemo", "DomApi", "DumiApi"].filter((n) => new RegExp(`<${n}[\\s/>]`).test(body));
|
|
810
|
+
const clientNames = [...used];
|
|
811
|
+
if (demos.length) clientNames.push("registerDemo");
|
|
812
|
+
const clientImport = clientNames.length ? `import { ${Array.from(new Set(clientNames)).join(", ")} } from '${CLIENT_MODULE}';
|
|
813
|
+
` : "";
|
|
814
|
+
const demoImports = demos.map((d) => {
|
|
815
|
+
const specifier = d.src ? d.src : `virtual:dommief-demo/${d.id}`;
|
|
816
|
+
return `import Demo_${d.id} from ${JSON.stringify(specifier)};
|
|
817
|
+
`;
|
|
818
|
+
}).join("");
|
|
819
|
+
const demoRegs = demos.map((d) => `registerDemo(${JSON.stringify(d.id)}, Demo_${d.id});
|
|
820
|
+
`).join("");
|
|
821
|
+
return `import React from 'react';
|
|
822
|
+
${imageImports}${clientImport}${demoImports}${demoRegs}export default function Content(){ return (
|
|
823
|
+
${body}
|
|
824
|
+
); }
|
|
825
|
+
export const meta = ${JSON.stringify(clientMeta)};
|
|
826
|
+
`;
|
|
827
|
+
}
|
|
828
|
+
var CLIENT_MODULE;
|
|
829
|
+
var init_to_module = __esm({
|
|
830
|
+
"src/markdown/to-module.ts"() {
|
|
831
|
+
"use strict";
|
|
832
|
+
CLIENT_MODULE = "dommief/client";
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
// src/markdown/index.ts
|
|
837
|
+
var markdown_exports = {};
|
|
838
|
+
__export(markdown_exports, {
|
|
839
|
+
transformMarkdown: () => transformMarkdown
|
|
840
|
+
});
|
|
841
|
+
import { unified as unified2 } from "unified";
|
|
842
|
+
import remarkParse2 from "remark-parse";
|
|
843
|
+
import remarkFrontmatter2 from "remark-frontmatter";
|
|
844
|
+
import remarkGfm from "remark-gfm";
|
|
845
|
+
import remarkDirective from "remark-directive";
|
|
846
|
+
import remarkRehype from "remark-rehype";
|
|
847
|
+
import rehypeRaw from "rehype-raw";
|
|
848
|
+
import "unist-util-visit";
|
|
849
|
+
async function transformMarkdown(source, file, opts = {}) {
|
|
850
|
+
const host = opts.hooks ?? (opts.plugins ? createPluginHost(opts.plugins) : void 0);
|
|
851
|
+
const remarkPlugins = [...opts.remarkPlugins ?? [], ...host ? host.collectRemarkPlugins() : []];
|
|
852
|
+
const rehypePlugins = [...opts.rehypePlugins ?? [], ...host ? host.collectRehypePlugins() : []];
|
|
853
|
+
let processor;
|
|
854
|
+
try {
|
|
855
|
+
processor = unified2().use(remarkParse2).use(remarkFrontmatter2, ["yaml"]).use(remarkGfm).use(remarkDirective).use(remarkContainer).use(remarkMeta, { filename: file }).use(remarkCodeMeta).use(remarkDemoMeta);
|
|
856
|
+
for (const p of remarkPlugins) processor = processor.use(p);
|
|
857
|
+
processor = processor.use(remarkRehype, { allowDangerousHtml: true }).use(rehypeRaw);
|
|
858
|
+
for (const p of rehypePlugins) processor = processor.use(p);
|
|
859
|
+
processor = processor.use(rehypeSlug).use(rehypeAutolinkHeadings).use(rehypeLink, opts.byFile ? { byFile: opts.byFile, base: opts.base ?? "/" } : {}).use(rehypeImg).use(rehypeDesc).use(rehypeStrip).use(rehypeText).use(rehypeDemo).use(rehypeCode, opts.code ?? {}).use(rehypeApi);
|
|
860
|
+
} catch (e) {
|
|
861
|
+
throw new DommiefError("failed to build pipeline", { file });
|
|
862
|
+
}
|
|
863
|
+
const vfile = { data: {}, path: file };
|
|
864
|
+
let hast;
|
|
865
|
+
try {
|
|
866
|
+
const tree = processor.parse(source);
|
|
867
|
+
hast = await processor.run(tree, vfile);
|
|
868
|
+
} catch (e) {
|
|
869
|
+
throw new DommiefError("markdown transform failed: " + e.message, { file });
|
|
870
|
+
}
|
|
871
|
+
const apiSrcFiles = vfile.data?.apiSrcFiles ?? [];
|
|
872
|
+
const meta = {
|
|
873
|
+
frontmatter: vfile.data?.frontmatter ?? {},
|
|
874
|
+
toc: vfile.data?.toc ?? [],
|
|
875
|
+
texts: vfile.data?.texts ?? []
|
|
876
|
+
};
|
|
877
|
+
const demos = vfile.data?.demos ?? [];
|
|
878
|
+
const code = toModule(hast, meta, vfile.data?.images ?? [], demos);
|
|
879
|
+
return { code, meta, warnings: vfile.data?.warnings ?? [], apiSrcFiles, demos };
|
|
880
|
+
}
|
|
881
|
+
var init_markdown = __esm({
|
|
882
|
+
"src/markdown/index.ts"() {
|
|
883
|
+
"use strict";
|
|
884
|
+
init_remark_meta();
|
|
885
|
+
init_rehype_slug();
|
|
886
|
+
init_rehype_link();
|
|
887
|
+
init_rehype_img();
|
|
888
|
+
init_rehype_desc();
|
|
889
|
+
init_rehype_text();
|
|
890
|
+
init_rehype_demo();
|
|
891
|
+
init_rehype_api();
|
|
892
|
+
init_remark_container();
|
|
893
|
+
init_rehype_code();
|
|
894
|
+
init_to_module();
|
|
895
|
+
init_errors();
|
|
896
|
+
init_plugins();
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
// src/search-config.ts
|
|
901
|
+
function createMiniSearchConfig() {
|
|
902
|
+
const stripDiacritics = (s) => s.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
|
903
|
+
const processTerm = (term) => {
|
|
904
|
+
const t = stripDiacritics(term).toLowerCase().replace(/đ/g, "d").normalize("NFC").trim();
|
|
905
|
+
return t.length ? t : null;
|
|
906
|
+
};
|
|
907
|
+
const tokenize = (text) => {
|
|
908
|
+
const out = [];
|
|
909
|
+
for (const raw of text.split(/[\s\p{P}]+/u)) {
|
|
910
|
+
if (!raw) continue;
|
|
911
|
+
let buf = "";
|
|
912
|
+
for (const ch of raw) {
|
|
913
|
+
if (CJK_RANGE.test(ch)) {
|
|
914
|
+
if (buf) {
|
|
915
|
+
out.push(buf);
|
|
916
|
+
buf = "";
|
|
917
|
+
}
|
|
918
|
+
out.push(ch);
|
|
919
|
+
} else {
|
|
920
|
+
buf += ch;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
if (buf) out.push(buf);
|
|
924
|
+
}
|
|
925
|
+
return out;
|
|
926
|
+
};
|
|
927
|
+
return { tokenize, processTerm };
|
|
928
|
+
}
|
|
929
|
+
var CJK_RANGE, SEARCH_FIELDS, SEARCH_STORE_FIELDS;
|
|
930
|
+
var init_search_config = __esm({
|
|
931
|
+
"src/search-config.ts"() {
|
|
932
|
+
"use strict";
|
|
933
|
+
CJK_RANGE = /[\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/;
|
|
934
|
+
SEARCH_FIELDS = ["value", "pageTitle", "sectionTitle"];
|
|
935
|
+
SEARCH_STORE_FIELDS = ["routeId", "path", "pageTitle", "sectionTitle", "paraId", "value", "pageKey", "sectionKey"];
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
// src/search.ts
|
|
940
|
+
var search_exports = {};
|
|
941
|
+
__export(search_exports, {
|
|
942
|
+
INDEX_BUDGET_BYTES_PER_PAGE: () => INDEX_BUDGET_BYTES_PER_PAGE,
|
|
943
|
+
SEARCH_FIELDS: () => SEARCH_FIELDS,
|
|
944
|
+
SEARCH_STORE_FIELDS: () => SEARCH_STORE_FIELDS,
|
|
945
|
+
buildSearchIndex: () => buildSearchIndex,
|
|
946
|
+
createMiniSearchConfig: () => createMiniSearchConfig
|
|
947
|
+
});
|
|
948
|
+
import MiniSearch from "minisearch";
|
|
949
|
+
async function buildSearchIndex(opts) {
|
|
950
|
+
const { routes, loadPageMeta, config } = opts;
|
|
951
|
+
const cfg = createMiniSearchConfig();
|
|
952
|
+
const defaultLocale = config.locales[0]?.id;
|
|
953
|
+
const localesOut = {};
|
|
954
|
+
const stats = {};
|
|
955
|
+
const manifestLocales = {};
|
|
956
|
+
for (const loc of config.locales) {
|
|
957
|
+
const locRoutes = routes.filter((r) => (r.locale ?? defaultLocale) === loc.id);
|
|
958
|
+
if (locRoutes.length === 0) continue;
|
|
959
|
+
const ms = new MiniSearch({
|
|
960
|
+
fields: [...SEARCH_FIELDS],
|
|
961
|
+
storeFields: [...SEARCH_STORE_FIELDS],
|
|
962
|
+
...cfg
|
|
963
|
+
});
|
|
964
|
+
const docs = [];
|
|
965
|
+
for (const route of locRoutes) {
|
|
966
|
+
const meta = await loadPageMeta(route);
|
|
967
|
+
const title = meta.frontmatter?.title || route.id;
|
|
968
|
+
const toc = meta.toc || [];
|
|
969
|
+
const fragments = meta.texts || [];
|
|
970
|
+
for (let i = 0; i < fragments.length; i++) {
|
|
971
|
+
const f = fragments[i];
|
|
972
|
+
const tocIndex = f.tocIndex;
|
|
973
|
+
const sectionTitle = tocIndex !== void 0 && toc[tocIndex] ? toc[tocIndex].title : title;
|
|
974
|
+
const pageKey = route.id;
|
|
975
|
+
const sectionKey = `${route.id}::${tocIndex ?? "orphan"}`;
|
|
976
|
+
docs.push({
|
|
977
|
+
id: `${route.id}#${f.paraId ?? i}#${i}`,
|
|
978
|
+
routeId: route.id,
|
|
979
|
+
path: route.path,
|
|
980
|
+
pageTitle: title,
|
|
981
|
+
sectionTitle,
|
|
982
|
+
paraId: f.paraId ?? 0,
|
|
983
|
+
value: f.value,
|
|
984
|
+
pageKey,
|
|
985
|
+
sectionKey
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
ms.addAll(docs);
|
|
990
|
+
const json = ms.toJSON();
|
|
991
|
+
localesOut[loc.id] = json;
|
|
992
|
+
const bytes = Buffer.byteLength(JSON.stringify(json));
|
|
993
|
+
stats[loc.id] = { docs: docs.length, bytes, pages: locRoutes.length };
|
|
994
|
+
manifestLocales[loc.id] = `/search-index/${loc.id}.json`;
|
|
995
|
+
}
|
|
996
|
+
return { locales: localesOut, stats, manifest: { locales: manifestLocales } };
|
|
997
|
+
}
|
|
998
|
+
var INDEX_BUDGET_BYTES_PER_PAGE;
|
|
999
|
+
var init_search = __esm({
|
|
1000
|
+
"src/search.ts"() {
|
|
1001
|
+
"use strict";
|
|
1002
|
+
init_search_config();
|
|
1003
|
+
INDEX_BUDGET_BYTES_PER_PAGE = 5 * 1024;
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
// src/cli.ts
|
|
1008
|
+
import path14 from "path";
|
|
1009
|
+
import { cac } from "cac";
|
|
1010
|
+
import { createServer } from "vite";
|
|
1011
|
+
|
|
1012
|
+
// src/vite/plugin.ts
|
|
1013
|
+
import fs9 from "fs";
|
|
1014
|
+
import path9 from "path";
|
|
1015
|
+
|
|
1016
|
+
// src/config.ts
|
|
1017
|
+
init_errors();
|
|
1018
|
+
init_plugins();
|
|
1019
|
+
import { z } from "zod";
|
|
1020
|
+
import fs from "fs";
|
|
1021
|
+
import path from "path";
|
|
1022
|
+
import { loadConfigFromFile } from "vite";
|
|
1023
|
+
var LocaleInputSchema = z.object({
|
|
1024
|
+
id: z.string().min(1).refine(
|
|
1025
|
+
(s) => {
|
|
1026
|
+
try {
|
|
1027
|
+
Intl.getCanonicalLocales(s);
|
|
1028
|
+
return true;
|
|
1029
|
+
} catch {
|
|
1030
|
+
return false;
|
|
1031
|
+
}
|
|
1032
|
+
},
|
|
1033
|
+
'ph\u1EA3i l\xE0 m\xE3 locale BCP 47 (vd "en-US", "vi-VN"; kh\xF4ng d\xF9ng "_")'
|
|
1034
|
+
),
|
|
1035
|
+
name: z.string().optional(),
|
|
1036
|
+
base: z.string().startsWith("/", 'ph\u1EA3i b\u1EAFt \u0111\u1EA7u b\u1EB1ng "/"').optional(),
|
|
1037
|
+
suffix: z.string().optional()
|
|
1038
|
+
});
|
|
1039
|
+
var ConfigSchema = z.object({
|
|
1040
|
+
title: z.string().default("Dommief"),
|
|
1041
|
+
description: z.string().optional(),
|
|
1042
|
+
base: z.string().startsWith("/", 'ph\u1EA3i b\u1EAFt \u0111\u1EA7u b\u1EB1ng "/" (URL tuy\u1EC7t \u0111\u1ED1i kh\xF4ng \u0111\u01B0\u1EE3c h\u1ED7 tr\u1EE3)').default("/"),
|
|
1043
|
+
outDir: z.string().default("dist"),
|
|
1044
|
+
srcDir: z.string().default("docs"),
|
|
1045
|
+
/** Khi `library`, tự động quét `src/` để hỗ trợ atom routing cho component library. */
|
|
1046
|
+
preset: z.enum(["docs", "library"]).default("docs"),
|
|
1047
|
+
docDirs: z.array(z.union([z.string(), z.object({ dir: z.string() })])).optional(),
|
|
1048
|
+
forceKebabCase: z.boolean().default(true),
|
|
1049
|
+
locales: z.array(LocaleInputSchema).default([{ id: "en-US", name: "English" }]),
|
|
1050
|
+
themeConfig: z.record(z.string(), z.unknown()).default({}),
|
|
1051
|
+
theme: z.string().default("dommief-theme-default"),
|
|
1052
|
+
ssg: z.object({
|
|
1053
|
+
concurrency: z.number().int().positive().default(4),
|
|
1054
|
+
/** Route không prerender nhưng VẪN phát shell (REVIEW §1 #17). Pattern: path chính xác hoặc `/prefix/*`. */
|
|
1055
|
+
exclude: z.array(z.string()).default([]),
|
|
1056
|
+
/** Route không phát file nào. */
|
|
1057
|
+
noEmit: z.array(z.string()).default([])
|
|
1058
|
+
}).default({ concurrency: 4, exclude: [], noEmit: [] }),
|
|
1059
|
+
/** Cache transform md theo content-hash (DM-406). `--no-cache` tắt. */
|
|
1060
|
+
cache: z.boolean().default(true),
|
|
1061
|
+
/** Kiểm link nội bộ chết lúc build (DM-410). `--no-link-check` tắt. */
|
|
1062
|
+
linkCheck: z.boolean().default(true),
|
|
1063
|
+
dev: z.object({ port: z.number().int().positive().default(5173) }).default({ port: 5173 }),
|
|
1064
|
+
/** sitemap.xml + robots.txt (DM-708, CONFIG §1.5). `false` (mặc định) = không phát; khai
|
|
1065
|
+
* object thì `hostname` BẮT BUỘC (không đoán domain). */
|
|
1066
|
+
sitemap: z.union([z.literal(false), z.object({ hostname: z.string().url(), exclude: z.array(z.string()).default([]) })]).default(false),
|
|
1067
|
+
/** Ghi `robots.txt` trỏ tới sitemap. Mặc định: true nếu có sitemap, ngược lại false. */
|
|
1068
|
+
robots: z.boolean().optional(),
|
|
1069
|
+
/** Shiki lúc build + số dòng (CONFIG §1, DM-706). `shiki: false` = code thô. */
|
|
1070
|
+
markdown: z.object({
|
|
1071
|
+
shiki: z.union([z.literal(false), z.object({ themes: z.object({ light: z.string(), dark: z.string() }) })]).default({ themes: { light: "github-light", dark: "github-dark" } }),
|
|
1072
|
+
lineNumbers: z.boolean().default(false)
|
|
1073
|
+
}).default({ shiki: { themes: { light: "github-light", dark: "github-dark" } }, lineNumbers: false }),
|
|
1074
|
+
/** Search index (DM-801). */
|
|
1075
|
+
search: z.object({ enabled: z.boolean().default(true) }).default({ enabled: true }),
|
|
1076
|
+
plugins: z.array(z.any()).default([])
|
|
1077
|
+
}).strict();
|
|
1078
|
+
var CONFIG_FILES = ["dommief.config.ts", "dommief.config.mts", "dommief.config.js", "dommief.config.mjs"];
|
|
1079
|
+
async function loadConfigFile(opts) {
|
|
1080
|
+
let file;
|
|
1081
|
+
if (opts.configPath) {
|
|
1082
|
+
file = path.resolve(opts.cwd, opts.configPath);
|
|
1083
|
+
if (!fs.existsSync(file)) throw new DommiefError(`kh\xF4ng t\xECm th\u1EA5y file config: ${file}`, { code: "config_not_found", file });
|
|
1084
|
+
} else {
|
|
1085
|
+
file = CONFIG_FILES.map((f) => path.join(opts.cwd, f)).find((p) => fs.existsSync(p));
|
|
1086
|
+
if (!file) return null;
|
|
1087
|
+
}
|
|
1088
|
+
const loaded = await loadConfigFromFile({ command: "serve", mode: "development" }, file, opts.cwd, "silent");
|
|
1089
|
+
if (!loaded) return null;
|
|
1090
|
+
return { path: loaded.path, config: loaded.config };
|
|
1091
|
+
}
|
|
1092
|
+
function normalizeBase(base) {
|
|
1093
|
+
return base.endsWith("/") ? base : base + "/";
|
|
1094
|
+
}
|
|
1095
|
+
function resolveConfig(userConfig, opts) {
|
|
1096
|
+
const fileLabel = opts.configFile ? path.basename(opts.configFile) : "dommief.config";
|
|
1097
|
+
const sources = {};
|
|
1098
|
+
for (const k of Object.keys(userConfig ?? {})) sources[k] = fileLabel;
|
|
1099
|
+
let merged = { ...userConfig };
|
|
1100
|
+
const layer = (key, value, source) => {
|
|
1101
|
+
if (value === void 0 || value === "") return;
|
|
1102
|
+
merged[key] = value;
|
|
1103
|
+
sources[key] = source;
|
|
1104
|
+
};
|
|
1105
|
+
if (opts.cli?.cache === false) {
|
|
1106
|
+
merged.cache = false;
|
|
1107
|
+
sources.cache = "--no-cache";
|
|
1108
|
+
}
|
|
1109
|
+
if (opts.cli?.linkCheck === false) {
|
|
1110
|
+
merged.linkCheck = false;
|
|
1111
|
+
sources.linkCheck = "--no-link-check";
|
|
1112
|
+
}
|
|
1113
|
+
layer("base", opts.cli?.base, "--base");
|
|
1114
|
+
layer("outDir", opts.cli?.outDir, "--out-dir");
|
|
1115
|
+
layer("base", opts.env?.DOMMIEF_BASE, "DOMMIEF_BASE");
|
|
1116
|
+
layer("outDir", opts.env?.DOMMIEF_OUT_DIR, "DOMMIEF_OUT_DIR");
|
|
1117
|
+
const fail = (keyPath, message) => {
|
|
1118
|
+
const top = String(keyPath[0] ?? "");
|
|
1119
|
+
const value = keyPath.reduce((v, k) => v?.[k], merged);
|
|
1120
|
+
const shown = value === void 0 ? "(thi\u1EBFu)" : JSON.stringify(value);
|
|
1121
|
+
throw new DommiefError(
|
|
1122
|
+
`c\u1EA5u h\xECnh kh\xF4ng h\u1EE3p l\u1EC7
|
|
1123
|
+
${formatKeyPath(keyPath)} ${shown}
|
|
1124
|
+
${message}
|
|
1125
|
+
(ngu\u1ED3n: ${sources[top] ?? "default"})`,
|
|
1126
|
+
{ code: "config_invalid" }
|
|
1127
|
+
);
|
|
1128
|
+
};
|
|
1129
|
+
const pluginHost = createPluginHost(Array.isArray(merged.plugins) ? merged.plugins : []);
|
|
1130
|
+
const modified = pluginHost.applyModifyConfig(merged);
|
|
1131
|
+
if (modified && typeof modified === "object") merged = { ...merged, ...modified };
|
|
1132
|
+
const parsed = ConfigSchema.safeParse(merged);
|
|
1133
|
+
if (!parsed.success) {
|
|
1134
|
+
const issue = parsed.error.issues[0];
|
|
1135
|
+
if (issue.code === "unrecognized_keys") {
|
|
1136
|
+
const key = issue.keys[0];
|
|
1137
|
+
const hint = suggestKey(key);
|
|
1138
|
+
fail([key], `key kh\xF4ng \u0111\u01B0\u1EE3c h\u1ED7 tr\u1EE3${hint ? `; b\u1EA1n mu\u1ED1n n\xF3i "${hint}"?` : ""}`);
|
|
1139
|
+
}
|
|
1140
|
+
fail(issue.path, issue.message);
|
|
1141
|
+
}
|
|
1142
|
+
const c = parsed.data;
|
|
1143
|
+
const locales = resolveLocales(c.locales, fail);
|
|
1144
|
+
const config = {
|
|
1145
|
+
root: opts.cwd,
|
|
1146
|
+
title: c.title,
|
|
1147
|
+
base: normalizeBase(c.base),
|
|
1148
|
+
outDir: c.outDir,
|
|
1149
|
+
srcDir: c.srcDir,
|
|
1150
|
+
docDirs: (c.docDirs ?? [c.srcDir]).map((d) => typeof d === "string" ? { dir: d } : { dir: d.dir }),
|
|
1151
|
+
forceKebabCase: c.forceKebabCase,
|
|
1152
|
+
locales,
|
|
1153
|
+
themeConfig: c.themeConfig,
|
|
1154
|
+
theme: c.theme,
|
|
1155
|
+
ssg: c.ssg,
|
|
1156
|
+
cache: c.cache,
|
|
1157
|
+
linkCheck: c.linkCheck,
|
|
1158
|
+
dev: c.dev,
|
|
1159
|
+
sitemap: c.sitemap,
|
|
1160
|
+
robots: c.robots ?? c.sitemap !== false,
|
|
1161
|
+
search: c.search,
|
|
1162
|
+
markdown: c.markdown,
|
|
1163
|
+
plugins: c.plugins ?? []
|
|
1164
|
+
};
|
|
1165
|
+
if (c.description !== void 0) config.description = c.description;
|
|
1166
|
+
return config;
|
|
1167
|
+
}
|
|
1168
|
+
async function loadConfig(root, cli2) {
|
|
1169
|
+
const file = await loadConfigFile({ cwd: root });
|
|
1170
|
+
return resolveConfig(file?.config ?? {}, { cwd: root, env: process.env, configFile: file?.path, ...cli2 ? { cli: cli2 } : {} });
|
|
1171
|
+
}
|
|
1172
|
+
function resolveLocales(input, fail) {
|
|
1173
|
+
const suffixMode = input[0]?.suffix !== void 0;
|
|
1174
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1175
|
+
return input.map((l, i) => {
|
|
1176
|
+
if (seen.has(l.id)) fail(["locales", i, "id"], `id "${l.id}" b\u1ECB tr\xF9ng`);
|
|
1177
|
+
seen.add(l.id);
|
|
1178
|
+
const name = l.name ?? l.id;
|
|
1179
|
+
if (suffixMode) {
|
|
1180
|
+
if (l.base !== void 0 || l.suffix === void 0) {
|
|
1181
|
+
fail(["locales", i], 'kh\xF4ng tr\u1ED9n "base" v\xE0 "suffix": locales[0] d\xF9ng ch\u1EBF \u0111\u1ED9 suffix n\xEAn m\u1ECDi locale ph\u1EA3i khai "suffix" v\xE0 kh\xF4ng khai "base"');
|
|
1182
|
+
}
|
|
1183
|
+
return { id: l.id, name, base: "/", suffix: l.suffix };
|
|
1184
|
+
}
|
|
1185
|
+
if (l.suffix !== void 0) {
|
|
1186
|
+
fail(["locales", i], 'kh\xF4ng tr\u1ED9n "base" v\xE0 "suffix": locales[0] d\xF9ng ch\u1EBF \u0111\u1ED9 base n\xEAn kh\xF4ng locale n\xE0o \u0111\u01B0\u1EE3c khai "suffix"');
|
|
1187
|
+
}
|
|
1188
|
+
const base = normalizeBase(l.base ?? (i === 0 ? "/" : `/${l.id}/`));
|
|
1189
|
+
if (i > 0 && base === "/") {
|
|
1190
|
+
fail(["locales", i, "base"], `ch\u1EC9 locale \u0111\u1EA7u ti\xEAn \u0111\u01B0\u1EE3c d\xF9ng base "/". \u0110\u01B0a "${l.id}" l\xEAn \u0111\u1EA7u m\u1EA3ng ho\u1EB7c \u0111\u1ED5i base.`);
|
|
1191
|
+
}
|
|
1192
|
+
return { id: l.id, name, base, suffix: "" };
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
function formatKeyPath(keyPath) {
|
|
1196
|
+
return keyPath.map((k, i) => typeof k === "number" ? `[${k}]` : i === 0 ? k : `.${k}`).join("");
|
|
1197
|
+
}
|
|
1198
|
+
function suggestKey(key) {
|
|
1199
|
+
let best;
|
|
1200
|
+
let bestDist = 3;
|
|
1201
|
+
for (const known of Object.keys(ConfigSchema.shape)) {
|
|
1202
|
+
const d = editDistance(key, known);
|
|
1203
|
+
if (d < bestDist) [best, bestDist] = [known, d];
|
|
1204
|
+
}
|
|
1205
|
+
return best;
|
|
1206
|
+
}
|
|
1207
|
+
function editDistance(a, b) {
|
|
1208
|
+
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
1209
|
+
for (let i = 1; i <= a.length; i++) {
|
|
1210
|
+
const cur = [i];
|
|
1211
|
+
for (let j = 1; j <= b.length; j++) {
|
|
1212
|
+
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
1213
|
+
}
|
|
1214
|
+
prev = cur;
|
|
1215
|
+
}
|
|
1216
|
+
return prev[b.length];
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// src/vite/plugin.ts
|
|
1220
|
+
init_plugins();
|
|
1221
|
+
init_markdown();
|
|
1222
|
+
|
|
1223
|
+
// src/paths.ts
|
|
1224
|
+
init_errors();
|
|
1225
|
+
import fs3 from "fs";
|
|
1226
|
+
import { fileURLToPath } from "url";
|
|
1227
|
+
function resolveClientFile(name) {
|
|
1228
|
+
const candidates = [`./client/${name}`, `../src/client/${name}`].map((rel) => fileURLToPath(new URL(rel, import.meta.url)));
|
|
1229
|
+
const hit = candidates.find((p) => fs3.existsSync(p));
|
|
1230
|
+
if (!hit) {
|
|
1231
|
+
throw new DommiefError(`kh\xF4ng t\xECm th\u1EA5y client/${name} c\u1EE7a dommief (\u0111\xE3 th\u1EED: ${candidates.join(", ")}). Package c\xE0i thi\u1EBFu th\u01B0 m\u1EE5c "src".`, { code: "client_entry_missing" });
|
|
1232
|
+
}
|
|
1233
|
+
return hit;
|
|
1234
|
+
}
|
|
1235
|
+
function resolveThemeApiFile() {
|
|
1236
|
+
const candidates = ["./theme-api.tsx", "../src/theme-api.tsx"].map((rel) => fileURLToPath(new URL(rel, import.meta.url)));
|
|
1237
|
+
const hit = candidates.find((p) => fs3.existsSync(p));
|
|
1238
|
+
if (!hit) {
|
|
1239
|
+
throw new DommiefError(`kh\xF4ng t\xECm th\u1EA5y theme-api.tsx c\u1EE7a dommief (\u0111\xE3 th\u1EED: ${candidates.join(", ")})`, { code: "client_entry_missing" });
|
|
1240
|
+
}
|
|
1241
|
+
return hit;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// src/routesModule.ts
|
|
1245
|
+
init_path();
|
|
1246
|
+
function createRouteModule(lazy, id, path15, load) {
|
|
1247
|
+
let mod;
|
|
1248
|
+
let promise;
|
|
1249
|
+
const preload = () => {
|
|
1250
|
+
if (!promise) promise = load().then((m) => {
|
|
1251
|
+
mod = m;
|
|
1252
|
+
return m;
|
|
1253
|
+
});
|
|
1254
|
+
return promise;
|
|
1255
|
+
};
|
|
1256
|
+
const component = lazy(() => mod ? { then: (res) => res(mod) } : preload());
|
|
1257
|
+
return { id, path: path15, component, preload };
|
|
1258
|
+
}
|
|
1259
|
+
function createRoutesModule(routes) {
|
|
1260
|
+
const entries = routes.map(
|
|
1261
|
+
(r) => ` createRouteModule(React.lazy, ${JSON.stringify(r.id)}, ${JSON.stringify(r.path)}, () => import(${JSON.stringify(winPath(r.file))})),`
|
|
1262
|
+
);
|
|
1263
|
+
return `import React from "react";
|
|
1264
|
+
const createRouteModule = ${createRouteModule.toString()};
|
|
1265
|
+
export default [
|
|
1266
|
+
${entries.join("\n")}
|
|
1267
|
+
];
|
|
1268
|
+
`;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// src/vite/routesPlugin.ts
|
|
1272
|
+
function dommiefRoutesPlugin(state) {
|
|
1273
|
+
const virtualId = "virtual:dommief/routes";
|
|
1274
|
+
const resolvedId = "\0" + virtualId;
|
|
1275
|
+
let server = null;
|
|
1276
|
+
const invalidate = () => {
|
|
1277
|
+
state.resetTable();
|
|
1278
|
+
const mod = server?.moduleGraph.getModuleById(resolvedId);
|
|
1279
|
+
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
1280
|
+
};
|
|
1281
|
+
return {
|
|
1282
|
+
name: "dommief:routes",
|
|
1283
|
+
enforce: "pre",
|
|
1284
|
+
configureServer(s) {
|
|
1285
|
+
server = s;
|
|
1286
|
+
},
|
|
1287
|
+
resolveId(id) {
|
|
1288
|
+
if (id === virtualId) return resolvedId;
|
|
1289
|
+
return null;
|
|
1290
|
+
},
|
|
1291
|
+
load(id) {
|
|
1292
|
+
if (id !== resolvedId) return null;
|
|
1293
|
+
return { code: createRoutesModule(state.getTable().routes), map: null };
|
|
1294
|
+
},
|
|
1295
|
+
watchChange(id, { event }) {
|
|
1296
|
+
if (!id.endsWith(".md")) return;
|
|
1297
|
+
if (event === "create" || event === "delete") {
|
|
1298
|
+
invalidate();
|
|
1299
|
+
server?.ws.send({ type: "full-reload" });
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// src/vite/sitePlugin.ts
|
|
1306
|
+
import { createHash as createHash3 } from "crypto";
|
|
1307
|
+
import { readFileSync } from "fs";
|
|
1308
|
+
|
|
1309
|
+
// src/siteFeatures.ts
|
|
1310
|
+
init_remark_meta();
|
|
1311
|
+
function fmFor(map, route) {
|
|
1312
|
+
if (!map) return {};
|
|
1313
|
+
return map[route.id] ?? map[route.file] ?? {};
|
|
1314
|
+
}
|
|
1315
|
+
function localeId(route, locales) {
|
|
1316
|
+
if (route.locale) return route.locale;
|
|
1317
|
+
return locales[0]?.id ?? "default";
|
|
1318
|
+
}
|
|
1319
|
+
function titleOf(route, fm) {
|
|
1320
|
+
const t = fm.title ?? fm.navTitle ?? fm.sidebarTitle;
|
|
1321
|
+
if (typeof t === "string" && t.trim()) return t;
|
|
1322
|
+
return route.id.split("/").pop() ?? route.id;
|
|
1323
|
+
}
|
|
1324
|
+
function orderOf(fm) {
|
|
1325
|
+
const v = fm.order ?? fm.navOrder ?? fm.sidebarOrder;
|
|
1326
|
+
return typeof v === "number" ? v : Number(v ?? 0) || 0;
|
|
1327
|
+
}
|
|
1328
|
+
function buildNav(routes, frontmatter, locales = []) {
|
|
1329
|
+
const out = {};
|
|
1330
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1331
|
+
for (const route of routes) {
|
|
1332
|
+
const lid = localeId(route, locales);
|
|
1333
|
+
const fm = fmFor(frontmatter, route);
|
|
1334
|
+
const navFlag = fm.nav;
|
|
1335
|
+
if (navFlag === false) continue;
|
|
1336
|
+
const item = {
|
|
1337
|
+
title: titleOf(route, fm),
|
|
1338
|
+
link: route.path,
|
|
1339
|
+
order: orderOf(fm),
|
|
1340
|
+
activePath: route.path
|
|
1341
|
+
};
|
|
1342
|
+
const arr = groups.get(lid) ?? [];
|
|
1343
|
+
arr.push(item);
|
|
1344
|
+
groups.set(lid, arr);
|
|
1345
|
+
}
|
|
1346
|
+
for (const [lid, items] of groups) {
|
|
1347
|
+
out[lid] = items.sort((a, b) => a.order - b.order || String(a.link).localeCompare(String(b.link)));
|
|
1348
|
+
}
|
|
1349
|
+
for (const l of locales) {
|
|
1350
|
+
if (!out[l.id]) out[l.id] = [];
|
|
1351
|
+
}
|
|
1352
|
+
return out;
|
|
1353
|
+
}
|
|
1354
|
+
function buildSidebar(routes, frontmatter, locales = []) {
|
|
1355
|
+
const out = {};
|
|
1356
|
+
const byLocale = /* @__PURE__ */ new Map();
|
|
1357
|
+
for (const route of routes) {
|
|
1358
|
+
const lid = localeId(route, locales);
|
|
1359
|
+
const fm = fmFor(frontmatter, route);
|
|
1360
|
+
const groupTitle = typeof fm.group === "string" && fm.group.trim() ? fm.group : route.id.split("/")[0] ?? "General";
|
|
1361
|
+
const item = {
|
|
1362
|
+
title: titleOf(route, fm),
|
|
1363
|
+
link: route.path,
|
|
1364
|
+
order: orderOf(fm),
|
|
1365
|
+
...Object.keys(fm).length ? { frontmatter: fm } : {}
|
|
1366
|
+
};
|
|
1367
|
+
let groups = byLocale.get(lid);
|
|
1368
|
+
if (!groups) {
|
|
1369
|
+
groups = /* @__PURE__ */ new Map();
|
|
1370
|
+
byLocale.set(lid, groups);
|
|
1371
|
+
}
|
|
1372
|
+
let g = groups.get(groupTitle);
|
|
1373
|
+
if (!g) {
|
|
1374
|
+
g = { title: groupTitle, order: orderOf(fm), children: [] };
|
|
1375
|
+
groups.set(groupTitle, g);
|
|
1376
|
+
}
|
|
1377
|
+
g.children.push(item);
|
|
1378
|
+
g.order = Math.min(g.order, orderOf(fm));
|
|
1379
|
+
}
|
|
1380
|
+
for (const [lid, groups] of byLocale) {
|
|
1381
|
+
const arr = [...groups.values()].map((g) => ({
|
|
1382
|
+
...g,
|
|
1383
|
+
children: g.children.sort((a, b) => a.order - b.order || a.link.localeCompare(b.link))
|
|
1384
|
+
}));
|
|
1385
|
+
out[lid] = arr.sort((a, b) => a.order - b.order || String(a.title).localeCompare(String(b.title)));
|
|
1386
|
+
}
|
|
1387
|
+
for (const l of locales) {
|
|
1388
|
+
if (!out[l.id]) out[l.id] = [];
|
|
1389
|
+
}
|
|
1390
|
+
return out;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// src/siteModule.ts
|
|
1394
|
+
function createSiteModule({ routes, config, meta }) {
|
|
1395
|
+
const routesById = Object.fromEntries(routes.map((r) => [r.id, r]));
|
|
1396
|
+
const frontmatter = config.frontmatter ?? {};
|
|
1397
|
+
const nav = buildNav(routes, frontmatter, config.locales);
|
|
1398
|
+
const sidebar = buildSidebar(routes, frontmatter, config.locales);
|
|
1399
|
+
const head = {
|
|
1400
|
+
base: config.base,
|
|
1401
|
+
title: config.title,
|
|
1402
|
+
...config.description === void 0 ? {} : { description: config.description },
|
|
1403
|
+
locales: config.locales,
|
|
1404
|
+
themeConfig: config.themeConfig ?? {},
|
|
1405
|
+
routes: routesById,
|
|
1406
|
+
search: config.search ?? { enabled: true }
|
|
1407
|
+
};
|
|
1408
|
+
const json = JSON.stringify(head);
|
|
1409
|
+
return `export default {
|
|
1410
|
+
...${json},
|
|
1411
|
+
meta: ${JSON.stringify(meta)},
|
|
1412
|
+
nav: ${JSON.stringify(nav)},
|
|
1413
|
+
sidebar: ${JSON.stringify(sidebar)},
|
|
1414
|
+
};
|
|
1415
|
+
`;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// src/vite/sitePlugin.ts
|
|
1419
|
+
init_remark_meta();
|
|
1420
|
+
function frontmatterHash(file) {
|
|
1421
|
+
let raw;
|
|
1422
|
+
try {
|
|
1423
|
+
raw = readFileSync(file, "utf8");
|
|
1424
|
+
} catch {
|
|
1425
|
+
return "";
|
|
1426
|
+
}
|
|
1427
|
+
const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
1428
|
+
const block = m?.[1] ?? "";
|
|
1429
|
+
const headings = raw.match(/^#{1,6}\s.*$/gm)?.join("\n") ?? "";
|
|
1430
|
+
return createHash3("sha256").update(block + "\n--\n" + headings).digest("hex").slice(0, 16);
|
|
1431
|
+
}
|
|
1432
|
+
function dommiefSitePlugin(state) {
|
|
1433
|
+
const virtualId = "virtual:dommief/site";
|
|
1434
|
+
const resolvedId = "\0" + virtualId;
|
|
1435
|
+
let frontmatterHashes = /* @__PURE__ */ new Map();
|
|
1436
|
+
let cache2 = null;
|
|
1437
|
+
let server = null;
|
|
1438
|
+
const invalidate = () => {
|
|
1439
|
+
cache2 = null;
|
|
1440
|
+
state.resetTable();
|
|
1441
|
+
const mod = server?.moduleGraph.getModuleById(resolvedId);
|
|
1442
|
+
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
1443
|
+
};
|
|
1444
|
+
return {
|
|
1445
|
+
name: "dommief:site",
|
|
1446
|
+
enforce: "pre",
|
|
1447
|
+
configureServer(s) {
|
|
1448
|
+
server = s;
|
|
1449
|
+
},
|
|
1450
|
+
resolveId(id) {
|
|
1451
|
+
if (id === virtualId) return resolvedId;
|
|
1452
|
+
return null;
|
|
1453
|
+
},
|
|
1454
|
+
load(id) {
|
|
1455
|
+
if (id !== resolvedId) return null;
|
|
1456
|
+
if (cache2) return { code: cache2, map: null };
|
|
1457
|
+
const config = state.config();
|
|
1458
|
+
const t = state.getTable();
|
|
1459
|
+
const summaries = new Map(t.routes.map((r) => {
|
|
1460
|
+
let raw;
|
|
1461
|
+
try {
|
|
1462
|
+
raw = readFileSync(r.file, "utf8");
|
|
1463
|
+
} catch {
|
|
1464
|
+
raw = "";
|
|
1465
|
+
}
|
|
1466
|
+
return [r.id, extractPageSummary(raw, r.file, (m) => this.warn(m))];
|
|
1467
|
+
}));
|
|
1468
|
+
const frontmatter = Object.fromEntries([...summaries].map(([id2, s]) => [id2, s.frontmatter]));
|
|
1469
|
+
const meta = Object.fromEntries([...summaries].map(([id2, s]) => [id2, s]));
|
|
1470
|
+
const code = createSiteModule({
|
|
1471
|
+
routes: t.routes,
|
|
1472
|
+
config: {
|
|
1473
|
+
base: config.base,
|
|
1474
|
+
title: config.title,
|
|
1475
|
+
description: config.description,
|
|
1476
|
+
locales: config.locales,
|
|
1477
|
+
themeConfig: config.themeConfig,
|
|
1478
|
+
frontmatter,
|
|
1479
|
+
search: { enabled: config.search.enabled }
|
|
1480
|
+
},
|
|
1481
|
+
meta
|
|
1482
|
+
});
|
|
1483
|
+
frontmatterHashes = new Map(t.routes.map((r) => [r.file, frontmatterHash(r.file)]));
|
|
1484
|
+
cache2 = code;
|
|
1485
|
+
return { code, map: null };
|
|
1486
|
+
},
|
|
1487
|
+
handleHotUpdate(ctx) {
|
|
1488
|
+
const id = ctx.file;
|
|
1489
|
+
if (!id.endsWith(".md")) return;
|
|
1490
|
+
const oldHash = frontmatterHashes.get(id);
|
|
1491
|
+
const newHash = frontmatterHash(id);
|
|
1492
|
+
if (oldHash !== void 0 && oldHash !== newHash) {
|
|
1493
|
+
frontmatterHashes.set(id, newHash);
|
|
1494
|
+
invalidate();
|
|
1495
|
+
}
|
|
1496
|
+
},
|
|
1497
|
+
watchChange(id, { event }) {
|
|
1498
|
+
if (!id.endsWith(".md")) return;
|
|
1499
|
+
if (event === "create" || event === "delete") {
|
|
1500
|
+
invalidate();
|
|
1501
|
+
server?.ws.send({ type: "full-reload" });
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// src/vite/searchIndexPlugin.ts
|
|
1508
|
+
import fs4 from "fs";
|
|
1509
|
+
var VIRTUAL_ID = "virtual:dommief/search-index";
|
|
1510
|
+
var RESOLVED_ID = "\0" + VIRTUAL_ID;
|
|
1511
|
+
var PREFIX = "/search-index/";
|
|
1512
|
+
function dommiefSearchIndexPlugin(state) {
|
|
1513
|
+
return {
|
|
1514
|
+
name: "dommief:search-index",
|
|
1515
|
+
enforce: "pre",
|
|
1516
|
+
resolveId(id) {
|
|
1517
|
+
if (id === VIRTUAL_ID) return RESOLVED_ID;
|
|
1518
|
+
return null;
|
|
1519
|
+
},
|
|
1520
|
+
load(id) {
|
|
1521
|
+
if (id !== RESOLVED_ID) return null;
|
|
1522
|
+
const config = state.config();
|
|
1523
|
+
if (!config.search.enabled) return { code: "export const manifest = { locales: {} }; export default manifest;\n", map: null };
|
|
1524
|
+
const locales = Object.fromEntries((config.locales ?? []).map((l) => [l.id, `${config.base}search-index/${l.id}.json`]));
|
|
1525
|
+
return { code: `export const manifest = ${JSON.stringify({ locales })}; export default manifest;
|
|
1526
|
+
`, map: null };
|
|
1527
|
+
},
|
|
1528
|
+
configureServer(s) {
|
|
1529
|
+
s.middlewares.use(async (req, res, next) => {
|
|
1530
|
+
const url = req.url?.split("?")[0] ?? "";
|
|
1531
|
+
if (!url.endsWith(".json")) return next();
|
|
1532
|
+
const base = (state.config().base || "/").replace(/\/$/, "") || "";
|
|
1533
|
+
const localPath = base && url.startsWith(base + PREFIX) ? url.slice((base + PREFIX).length) : url.startsWith(PREFIX) ? url.slice(PREFIX.length) : null;
|
|
1534
|
+
if (localPath === null) return next();
|
|
1535
|
+
const localeId2 = localPath.replace(/\.json$/, "");
|
|
1536
|
+
if (!state.config().locales?.some((l) => l.id === localeId2)) return next();
|
|
1537
|
+
if (!state.config().search.enabled) {
|
|
1538
|
+
res.statusCode = 404;
|
|
1539
|
+
return res.end();
|
|
1540
|
+
}
|
|
1541
|
+
try {
|
|
1542
|
+
const { buildSearchIndex: buildSearchIndex2 } = await Promise.resolve().then(() => (init_search(), search_exports));
|
|
1543
|
+
const { transformMarkdown: transformMarkdown2 } = await Promise.resolve().then(() => (init_markdown(), markdown_exports));
|
|
1544
|
+
const t = state.getTable();
|
|
1545
|
+
const routes = t.routes.filter((r) => (r.locale ?? state.config().locales[0]?.id) === localeId2);
|
|
1546
|
+
const resIdx = await buildSearchIndex2({
|
|
1547
|
+
routes,
|
|
1548
|
+
loadPageMeta: async (r) => {
|
|
1549
|
+
let raw;
|
|
1550
|
+
try {
|
|
1551
|
+
raw = fs4.readFileSync(r.file, "utf8");
|
|
1552
|
+
} catch {
|
|
1553
|
+
raw = "";
|
|
1554
|
+
}
|
|
1555
|
+
const tr = await transformMarkdown2(raw, r.file, { hooks: state.host(), byFile: t.byFile, base: state.config().base, code: state.config().markdown });
|
|
1556
|
+
return tr.meta;
|
|
1557
|
+
},
|
|
1558
|
+
config: { locales: state.config().locales }
|
|
1559
|
+
});
|
|
1560
|
+
res.setHeader("Content-Type", "application/json");
|
|
1561
|
+
res.end(JSON.stringify(resIdx.locales[localeId2] ?? {}));
|
|
1562
|
+
} catch (e) {
|
|
1563
|
+
res.statusCode = 500;
|
|
1564
|
+
res.end(JSON.stringify({ error: e instanceof Error ? e.message : "search index build failed" }));
|
|
1565
|
+
}
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// src/routes.ts
|
|
1572
|
+
init_errors();
|
|
1573
|
+
init_path();
|
|
1574
|
+
init_plugins();
|
|
1575
|
+
import fs5 from "fs";
|
|
1576
|
+
import path5 from "path";
|
|
1577
|
+
import { createHash as createHash4 } from "crypto";
|
|
1578
|
+
var INDEX_NAMES = /* @__PURE__ */ new Set(["index", "README"]);
|
|
1579
|
+
var SEGMENT_OK = /^[\p{L}\p{N}_-]+$/u;
|
|
1580
|
+
function listMarkdownFiles(dir) {
|
|
1581
|
+
const out = [];
|
|
1582
|
+
const walk = (d) => {
|
|
1583
|
+
for (const e of fs5.readdirSync(d, { withFileTypes: true })) {
|
|
1584
|
+
if (e.name.startsWith(".") || e.name.startsWith("_") || e.name === "node_modules") continue;
|
|
1585
|
+
const p = path5.join(d, e.name);
|
|
1586
|
+
if (e.isDirectory()) walk(p);
|
|
1587
|
+
else if (e.isFile() && e.name.endsWith(".md")) out.push(winPath(p));
|
|
1588
|
+
}
|
|
1589
|
+
};
|
|
1590
|
+
walk(dir);
|
|
1591
|
+
return out.sort();
|
|
1592
|
+
}
|
|
1593
|
+
function scanRoutes({ cwd, config }) {
|
|
1594
|
+
const routes = [];
|
|
1595
|
+
const warnings = [];
|
|
1596
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1597
|
+
const docDirs = config.preset === "library" && !(config.docDirs || []).some((d) => d.dir === "src") ? [...config.docDirs || [], { dir: "src" }] : config.docDirs || [];
|
|
1598
|
+
for (const { dir } of docDirs) {
|
|
1599
|
+
const absDir = path5.resolve(cwd, dir);
|
|
1600
|
+
if (!fs5.existsSync(absDir)) {
|
|
1601
|
+
warnings.push(`doc_dir_missing: docDirs "${dir}" kh\xF4ng t\u1ED3n t\u1EA1i (${winPath(absDir)})`);
|
|
1602
|
+
continue;
|
|
1603
|
+
}
|
|
1604
|
+
for (const file of listMarkdownFiles(absDir)) {
|
|
1605
|
+
if (seen.has(file)) {
|
|
1606
|
+
warnings.push(`route_duplicate_file: ${file} n\u1EB1m trong nhi\u1EC1u docDirs, gi\u1EEF docDir khai tr\u01B0\u1EDBc`);
|
|
1607
|
+
continue;
|
|
1608
|
+
}
|
|
1609
|
+
seen.add(file);
|
|
1610
|
+
const id = path5.posix.relative(winPath(absDir), file).slice(0, -".md".length);
|
|
1611
|
+
routes.push({ id, path: `/${id}`, file });
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
return { routes, warnings };
|
|
1615
|
+
}
|
|
1616
|
+
function kebab(segment, localeId2) {
|
|
1617
|
+
return segment.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").toLocaleLowerCase(localeId2);
|
|
1618
|
+
}
|
|
1619
|
+
function normalizeRoutes(raw, { cwd, config }) {
|
|
1620
|
+
const warnings = [];
|
|
1621
|
+
const errors = [];
|
|
1622
|
+
const show = (file) => path5.posix.relative(winPath(cwd), file) || file;
|
|
1623
|
+
const defaultLocale = config.locales[0];
|
|
1624
|
+
const items = [];
|
|
1625
|
+
for (const r of raw) {
|
|
1626
|
+
const segs = r.id.split("/");
|
|
1627
|
+
let name = segs.pop();
|
|
1628
|
+
const dir = segs.join("/");
|
|
1629
|
+
let locale = defaultLocale;
|
|
1630
|
+
let suffixStripped = false;
|
|
1631
|
+
const hit = config.locales.find((l) => name.endsWith("." + l.id));
|
|
1632
|
+
if (hit) {
|
|
1633
|
+
locale = hit;
|
|
1634
|
+
suffixStripped = true;
|
|
1635
|
+
name = name.slice(0, -(hit.id.length + 1));
|
|
1636
|
+
} else {
|
|
1637
|
+
const last = name.slice(name.lastIndexOf(".") + 1);
|
|
1638
|
+
const typo = name.includes(".") && config.locales.find((l) => l.id.toLowerCase() === last.toLowerCase());
|
|
1639
|
+
if (typo) warnings.push(`locale_unknown_suffix: ${show(r.file)} c\xF3 h\u1EADu t\u1ED1 ".${last}" g\u1EA7n gi\u1ED1ng locale "${typo.id}" nh\u01B0ng kh\xF4ng kh\u1EDBp; x\u1EED l\xFD nh\u01B0 file th\u01B0\u1EDDng`);
|
|
1640
|
+
}
|
|
1641
|
+
const indexName = INDEX_NAMES.has(name) ? name : void 0;
|
|
1642
|
+
const parts = indexName ? segs : [...segs, name];
|
|
1643
|
+
const split = parts.flatMap((s) => s.split("."));
|
|
1644
|
+
const dotted = split.length !== parts.length;
|
|
1645
|
+
const routePath = split.filter(Boolean).map((s) => config.forceKebabCase ? kebab(s, locale.id) : s).join("/").normalize("NFC");
|
|
1646
|
+
const bad = routePath.split("/").find((s) => s && !SEGMENT_OK.test(s));
|
|
1647
|
+
if (bad) {
|
|
1648
|
+
const ch = [...bad].find((c) => !SEGMENT_OK.test(c));
|
|
1649
|
+
errors.push({
|
|
1650
|
+
code: "route_invalid_path",
|
|
1651
|
+
message: `dommief: routes: k\xFD t\u1EF1 kh\xF4ng h\u1EE3p l\u1EC7 "${ch}" trong \u0111\u01B0\u1EDDng d\u1EABn c\u1EE7a file
|
|
1652
|
+
${show(r.file)}
|
|
1653
|
+
segment ch\u1EC9 \u0111\u01B0\u1EE3c ch\u1EE9a ch\u1EEF, s\u1ED1, "-" v\xE0 "_". \u0110\u1ED5i t\xEAn file.`
|
|
1654
|
+
});
|
|
1655
|
+
} else if (/[^\x00-\x7F]/.test(routePath)) {
|
|
1656
|
+
warnings.push(`route_non_ascii: ${show(r.file)} \u2192 "/${routePath}" c\xF3 k\xFD t\u1EF1 ngo\xE0i ASCII; NFC/NFD v\xE0 hosting t\u0129nh c\xF3 th\u1EC3 l\u1EC7ch, n\xEAn \u0111\u1EB7t t\xEAn file ASCII`);
|
|
1657
|
+
}
|
|
1658
|
+
const joined = locale.suffix ? `/${routePath}${locale.suffix}` : `${locale.base}${routePath}`;
|
|
1659
|
+
const p = joined.replace(/\/{2,}/g, "/").replace(/(.)\/$/, "$1");
|
|
1660
|
+
items.push({
|
|
1661
|
+
// locale mặc định ⇒ không có key (JSON round-trip giữ nguyên object)
|
|
1662
|
+
route: { id: r.id, path: p, file: r.file, ...locale === defaultLocale ? {} : { locale: locale.id } },
|
|
1663
|
+
routePath,
|
|
1664
|
+
locale,
|
|
1665
|
+
suffixStripped,
|
|
1666
|
+
...indexName === void 0 ? {} : { indexName },
|
|
1667
|
+
dir,
|
|
1668
|
+
dotted
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1672
|
+
for (const it of items) byId.set(it.route.id, [...byId.get(it.route.id) ?? [], it]);
|
|
1673
|
+
for (const [id, group] of byId) {
|
|
1674
|
+
if (group.length < 2) continue;
|
|
1675
|
+
errors.push({
|
|
1676
|
+
code: "route_duplicate",
|
|
1677
|
+
message: `dommief: routes: tr\xF9ng route id "${id}" (${group.length} file)
|
|
1678
|
+
${group.map((g) => ` ${show(g.route.file)}`).join("\n")}
|
|
1679
|
+
id l\xE0 \u0111\u01B0\u1EDDng d\u1EABn t\u01B0\u01A1ng \u0111\u1ED1i trong docDir. \u0110\u1ED5i t\xEAn file ho\u1EB7c t\xE1ch docDir th\xE0nh 2 site.`
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
1683
|
+
for (const it of items) {
|
|
1684
|
+
if ((byId.get(it.route.id)?.length ?? 0) > 1) continue;
|
|
1685
|
+
const key = it.route.path.toLowerCase();
|
|
1686
|
+
byPath.set(key, [...byPath.get(key) ?? [], it]);
|
|
1687
|
+
}
|
|
1688
|
+
for (const group of byPath.values()) {
|
|
1689
|
+
if (group.length < 2) continue;
|
|
1690
|
+
errors.push({
|
|
1691
|
+
code: "route_duplicate",
|
|
1692
|
+
message: `dommief: routes: tr\xF9ng \u0111\u01B0\u1EDDng d\u1EABn "${group[0].route.path}" (${group.length} file)
|
|
1693
|
+
${group.map((g) => ` ${show(g.route.file)}`).join("\n")}
|
|
1694
|
+
nguy\xEAn nh\xE2n: ${duplicateCause(group[0], group[1])}`
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
if (errors.length) {
|
|
1698
|
+
throw new DommiefError(errors.map((e) => e.message).join("\n\n"), { code: errors[0].code });
|
|
1699
|
+
}
|
|
1700
|
+
return { routes: items.map((it) => it.route), warnings };
|
|
1701
|
+
}
|
|
1702
|
+
function duplicateCause(a, b) {
|
|
1703
|
+
const base = (n) => path5.posix.basename(n.route.file);
|
|
1704
|
+
const stripped = a.suffixStripped ? a : b.suffixStripped ? b : void 0;
|
|
1705
|
+
if (stripped && a.locale === b.locale) {
|
|
1706
|
+
const other = stripped === a ? b : a;
|
|
1707
|
+
const owner = stripped.route.locale ? `locale "${stripped.locale.id}"` : "locale m\u1EB7c \u0111\u1ECBnh";
|
|
1708
|
+
return `"${base(stripped)}" thu\u1ED9c ${owner} n\xEAn tr\xF9ng "${base(other)}"`;
|
|
1709
|
+
}
|
|
1710
|
+
const dirName = (n) => n.dir || "/";
|
|
1711
|
+
if (a.indexName && b.indexName) return `c\u1EA3 "${a.indexName}.md" v\xE0 "${b.indexName}.md" \u0111\u1EC1u g\u1ED9p v\xE0o "${dirName(a)}"`;
|
|
1712
|
+
const idx = a.indexName ? a : b.indexName ? b : void 0;
|
|
1713
|
+
if (idx) return `"${idx.indexName}.md" g\u1ED9p v\xE0o th\u01B0 m\u1EE5c "${dirName(idx)}"`;
|
|
1714
|
+
if (a.route.id.toLowerCase() === b.route.id.toLowerCase()) return "tr\xF9ng sau khi kebab (macOS/Windows kh\xF4ng ph\xE2n bi\u1EC7t hoa th\u01B0\u1EDDng)";
|
|
1715
|
+
if (a.dotted || b.dotted) return '"." trong t\xEAn file \u0111\u01B0\u1EE3c \u0111\u1ED5i th\xE0nh "/"';
|
|
1716
|
+
return "kebab-case l\xE0m hai file tr\xF9ng nhau";
|
|
1717
|
+
}
|
|
1718
|
+
function routeConfigHash(config) {
|
|
1719
|
+
const { srcDir, docDirs, locales, forceKebabCase, preset } = config;
|
|
1720
|
+
const json = JSON.stringify({
|
|
1721
|
+
srcDir,
|
|
1722
|
+
docDirs: docDirs.map((d) => d.dir),
|
|
1723
|
+
locales: locales.map(({ id, name, base, suffix }) => ({ id, name, base, suffix })),
|
|
1724
|
+
forceKebabCase,
|
|
1725
|
+
preset: preset ?? "docs"
|
|
1726
|
+
});
|
|
1727
|
+
return createHash4("sha256").update(json).digest("hex").slice(0, 8);
|
|
1728
|
+
}
|
|
1729
|
+
function buildRouteTable({ cwd, config, hooks, plugins }) {
|
|
1730
|
+
const host = hooks ?? (plugins ? createPluginHost(plugins) : config?.plugins ? createPluginHost(config.plugins) : void 0);
|
|
1731
|
+
const scan = scanRoutes({ cwd, config });
|
|
1732
|
+
const norm = normalizeRoutes(scan.routes, { cwd, config });
|
|
1733
|
+
const cmp = (x, y) => x < y ? -1 : x > y ? 1 : 0;
|
|
1734
|
+
let routes = norm.routes;
|
|
1735
|
+
if (host) routes = host.applyModifyRoutes(routes);
|
|
1736
|
+
routes = routes.sort((a, b) => cmp(a.path, b.path) || cmp(a.id, b.id));
|
|
1737
|
+
const table = {
|
|
1738
|
+
warnings: [...scan.warnings, ...norm.warnings],
|
|
1739
|
+
routes,
|
|
1740
|
+
byId: {},
|
|
1741
|
+
byPath: {},
|
|
1742
|
+
byFile: {},
|
|
1743
|
+
configHash: routeConfigHash(config)
|
|
1744
|
+
};
|
|
1745
|
+
for (const r of routes) {
|
|
1746
|
+
table.byId[r.id] = r;
|
|
1747
|
+
table.byPath[r.path] = r;
|
|
1748
|
+
table.byFile[r.file] = r;
|
|
1749
|
+
}
|
|
1750
|
+
return table;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// src/vite/state.ts
|
|
1754
|
+
function createState(cwd, get) {
|
|
1755
|
+
let table = null;
|
|
1756
|
+
return {
|
|
1757
|
+
cwd,
|
|
1758
|
+
config: () => get().config,
|
|
1759
|
+
host: () => get().host,
|
|
1760
|
+
getTable: () => table ??= buildRouteTable({ cwd, config: get().config, hooks: get().host }),
|
|
1761
|
+
resetTable: () => {
|
|
1762
|
+
table = null;
|
|
1763
|
+
}
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// src/vite/plugin.ts
|
|
1768
|
+
init_path();
|
|
1769
|
+
|
|
1770
|
+
// src/theme/resolver.ts
|
|
1771
|
+
init_errors();
|
|
1772
|
+
import fs7 from "fs";
|
|
1773
|
+
import path7 from "path";
|
|
1774
|
+
|
|
1775
|
+
// src/theme/loader.ts
|
|
1776
|
+
init_errors();
|
|
1777
|
+
import fs6 from "fs";
|
|
1778
|
+
import path6 from "path";
|
|
1779
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1780
|
+
var EXTS = [".tsx", ".ts", ".jsx", ".js"];
|
|
1781
|
+
var DEFAULT_THEME = "dommief-theme-default";
|
|
1782
|
+
function isComponentName(name) {
|
|
1783
|
+
return /^[A-Z0-9]/.test(name);
|
|
1784
|
+
}
|
|
1785
|
+
function repoPackagesDir() {
|
|
1786
|
+
let cur = path6.dirname(fileURLToPath2(import.meta.url));
|
|
1787
|
+
while (cur !== path6.parse(cur).root) {
|
|
1788
|
+
if (fs6.existsSync(path6.join(cur, "pnpm-workspace.yaml"))) return path6.join(cur, "packages");
|
|
1789
|
+
cur = path6.dirname(cur);
|
|
1790
|
+
}
|
|
1791
|
+
return null;
|
|
1792
|
+
}
|
|
1793
|
+
function assertInside(cwd, target, theme) {
|
|
1794
|
+
const rel = path6.relative(cwd, target);
|
|
1795
|
+
const insideCwd = rel === "" || !rel.startsWith("..") && !path6.isAbsolute(rel);
|
|
1796
|
+
const insideNodeModules = target.split(path6.sep).includes("node_modules");
|
|
1797
|
+
if (!insideCwd && !insideNodeModules) {
|
|
1798
|
+
throw new DommiefError(`\u0111\u01B0\u1EDDng d\u1EABn theme "${theme}" tho\xE1t ra ngo\xE0i th\u01B0 m\u1EE5c d\u1EF1 \xE1n: ${target}`, { code: "theme_path_escape", file: target });
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
function resolveThemeRoot(cwd, theme) {
|
|
1802
|
+
const isPathLike = theme.startsWith(".") || theme.startsWith("/");
|
|
1803
|
+
let root = null;
|
|
1804
|
+
if (isPathLike) {
|
|
1805
|
+
const candidate = path6.resolve(cwd, theme);
|
|
1806
|
+
if (fs6.existsSync(candidate) && fs6.statSync(candidate).isDirectory()) root = candidate;
|
|
1807
|
+
if (!root) throw new DommiefError(`kh\xF4ng t\xECm th\u1EA5y theme t\u1EA1i \u0111\u01B0\u1EDDng d\u1EABn "${theme}"`, { code: "theme_package_not_found" });
|
|
1808
|
+
assertInside(cwd, root, theme);
|
|
1809
|
+
} else {
|
|
1810
|
+
const nmCandidate = path6.resolve(cwd, "node_modules", theme);
|
|
1811
|
+
if (fs6.existsSync(nmCandidate)) root = nmCandidate;
|
|
1812
|
+
else {
|
|
1813
|
+
const packages = repoPackagesDir();
|
|
1814
|
+
const pkgDirName = theme === DEFAULT_THEME ? "theme-default" : theme;
|
|
1815
|
+
const srcCandidate = packages ? path6.join(packages, pkgDirName) : null;
|
|
1816
|
+
if (srcCandidate && fs6.existsSync(srcCandidate)) root = srcCandidate;
|
|
1817
|
+
}
|
|
1818
|
+
if (!root) {
|
|
1819
|
+
throw new DommiefError(`kh\xF4ng t\xECm th\u1EA5y theme package "${theme}" (\u0111\xE3 th\u1EED node_modules/${theme})`, { code: "theme_package_not_found" });
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
if (fs6.existsSync(path6.join(root, "src", "layouts"))) root = path6.join(root, "src");
|
|
1823
|
+
return root;
|
|
1824
|
+
}
|
|
1825
|
+
function pickEntry(dir, name) {
|
|
1826
|
+
for (const ext of EXTS) {
|
|
1827
|
+
const f = path6.join(dir, name + ext);
|
|
1828
|
+
if (fs6.existsSync(f) && fs6.statSync(f).isFile()) return f;
|
|
1829
|
+
}
|
|
1830
|
+
const subIndexDir = path6.join(dir, name);
|
|
1831
|
+
if (fs6.existsSync(subIndexDir) && fs6.statSync(subIndexDir).isDirectory()) {
|
|
1832
|
+
for (const ext of EXTS) {
|
|
1833
|
+
const f = path6.join(subIndexDir, "index" + ext);
|
|
1834
|
+
if (fs6.existsSync(f) && fs6.statSync(f).isFile()) return f;
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
return null;
|
|
1838
|
+
}
|
|
1839
|
+
function scanComponents(dir) {
|
|
1840
|
+
const out = {};
|
|
1841
|
+
if (!fs6.existsSync(dir)) return out;
|
|
1842
|
+
for (const entry of fs6.readdirSync(dir, { withFileTypes: true })) {
|
|
1843
|
+
const name = entry.name;
|
|
1844
|
+
if (!isComponentName(name)) continue;
|
|
1845
|
+
if (entry.isFile()) {
|
|
1846
|
+
const ext = EXTS.find((e) => name.endsWith(e));
|
|
1847
|
+
if (!ext) continue;
|
|
1848
|
+
const specifier = name.slice(0, -ext.length);
|
|
1849
|
+
if (!isComponentName(specifier)) continue;
|
|
1850
|
+
out[specifier] ??= path6.resolve(dir, name);
|
|
1851
|
+
} else if (entry.isDirectory()) {
|
|
1852
|
+
const found = pickEntry(dir, name);
|
|
1853
|
+
if (found) out[name] = path6.resolve(found);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
return out;
|
|
1857
|
+
}
|
|
1858
|
+
function scanLocales(dir) {
|
|
1859
|
+
const out = {};
|
|
1860
|
+
if (!fs6.existsSync(dir)) return out;
|
|
1861
|
+
for (const entry of fs6.readdirSync(dir, { withFileTypes: true })) {
|
|
1862
|
+
if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
1863
|
+
out[entry.name.slice(0, -".json".length)] = path6.resolve(dir, entry.name);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
return out;
|
|
1867
|
+
}
|
|
1868
|
+
function loadThemeManifest(root, name) {
|
|
1869
|
+
const layoutsDir = path6.join(root, "layouts");
|
|
1870
|
+
const layouts = {};
|
|
1871
|
+
const missing = [];
|
|
1872
|
+
for (const layoutName of ["GlobalLayout", "DocLayout", "DemoLayout"]) {
|
|
1873
|
+
const p = pickEntry(layoutsDir, layoutName);
|
|
1874
|
+
if (p) layouts[layoutName] = p;
|
|
1875
|
+
else missing.push(layoutName);
|
|
1876
|
+
}
|
|
1877
|
+
if (missing.length) {
|
|
1878
|
+
throw new DommiefError(`theme "${name}" thi\u1EBFu layout: ${missing.join(", ")}`, { code: "theme_layout_missing", file: layoutsDir, layout: missing.join(",") });
|
|
1879
|
+
}
|
|
1880
|
+
const pluginJs = ["plugin.ts", "plugin.js", path6.join("plugin", "index.ts"), path6.join("plugin", "index.js")].map((f) => path6.join(root, f)).find((f) => fs6.existsSync(f));
|
|
1881
|
+
return {
|
|
1882
|
+
name,
|
|
1883
|
+
root,
|
|
1884
|
+
builtins: scanComponents(path6.join(root, "builtins")),
|
|
1885
|
+
slots: scanComponents(path6.join(root, "slots")),
|
|
1886
|
+
layouts,
|
|
1887
|
+
locales: scanLocales(path6.join(root, "locales")),
|
|
1888
|
+
plugin: pluginJs ?? null
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
function loadTheme(opts) {
|
|
1892
|
+
const cwd = path6.resolve(opts.cwd);
|
|
1893
|
+
const theme = opts.theme ?? DEFAULT_THEME;
|
|
1894
|
+
const root = resolveThemeRoot(cwd, theme);
|
|
1895
|
+
return loadThemeManifest(root, theme);
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// src/theme/resolver.ts
|
|
1899
|
+
function tryLoadLocal(cwd) {
|
|
1900
|
+
const localRoot = path7.resolve(cwd, ".dommief", "theme");
|
|
1901
|
+
if (!fs7.existsSync(localRoot)) return null;
|
|
1902
|
+
const layoutsDir = path7.join(localRoot, "layouts");
|
|
1903
|
+
const layouts = {};
|
|
1904
|
+
for (const name of ["GlobalLayout", "DocLayout", "DemoLayout"]) {
|
|
1905
|
+
const found = scanComponents(layoutsDir)[name];
|
|
1906
|
+
if (found) layouts[name] = found;
|
|
1907
|
+
}
|
|
1908
|
+
return {
|
|
1909
|
+
name: ".dommief/theme",
|
|
1910
|
+
root: localRoot,
|
|
1911
|
+
builtins: scanComponents(path7.join(localRoot, "builtins")),
|
|
1912
|
+
slots: scanComponents(path7.join(localRoot, "slots")),
|
|
1913
|
+
layouts,
|
|
1914
|
+
locales: {},
|
|
1915
|
+
plugin: null
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
function loadThemeChain(cwd, config) {
|
|
1919
|
+
const themeName = config.theme ?? DEFAULT_THEME;
|
|
1920
|
+
const pkg = loadTheme({ cwd, theme: themeName });
|
|
1921
|
+
const fallback = themeName === DEFAULT_THEME ? pkg : loadTheme({ cwd, theme: DEFAULT_THEME });
|
|
1922
|
+
return { local: tryLoadLocal(cwd), pkg, fallback };
|
|
1923
|
+
}
|
|
1924
|
+
function manifestsOf(chain) {
|
|
1925
|
+
const list = [chain.local, chain.pkg, chain.pkg === chain.fallback ? null : chain.fallback].filter((m) => m !== null);
|
|
1926
|
+
return list;
|
|
1927
|
+
}
|
|
1928
|
+
function resolveSlot(name, kind, chain) {
|
|
1929
|
+
for (const m of manifestsOf(chain)) {
|
|
1930
|
+
const hit = kind === "layouts" ? m.layouts[name] : kind === "locales" ? m.locales[name] : m[kind][name];
|
|
1931
|
+
if (hit) return hit;
|
|
1932
|
+
}
|
|
1933
|
+
throw new DommiefError(`kh\xF4ng c\xF3 theme n\xE0o cung c\u1EA5p ${kind}/${name}`, { code: "theme_slot_missing" });
|
|
1934
|
+
}
|
|
1935
|
+
function allNames(chain, kind) {
|
|
1936
|
+
const names = /* @__PURE__ */ new Set();
|
|
1937
|
+
for (const m of manifestsOf(chain)) {
|
|
1938
|
+
const map = kind === "layouts" ? m.layouts : kind === "locales" ? m.locales : m[kind];
|
|
1939
|
+
for (const k of Object.keys(map)) names.add(k);
|
|
1940
|
+
}
|
|
1941
|
+
return [...names];
|
|
1942
|
+
}
|
|
1943
|
+
function buildAliasChain(cwd, config) {
|
|
1944
|
+
const chain = loadThemeChain(cwd, config);
|
|
1945
|
+
const aliases = {};
|
|
1946
|
+
for (const kind of ["slots", "builtins", "layouts"]) {
|
|
1947
|
+
for (const name of allNames(chain, kind)) {
|
|
1948
|
+
aliases[`dommief/theme/${kind}/${name}`] = resolveSlot(name, kind, chain);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
aliases["dommief/theme"] = chain.pkg.root;
|
|
1952
|
+
return aliases;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
// src/cache.ts
|
|
1956
|
+
init_rehype_code();
|
|
1957
|
+
import { createHash as createHash5 } from "crypto";
|
|
1958
|
+
import fs8 from "fs";
|
|
1959
|
+
import path8 from "path";
|
|
1960
|
+
var CACHE_VERSION = 3;
|
|
1961
|
+
function stableStringify(value) {
|
|
1962
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
1963
|
+
if (value && typeof value === "object") {
|
|
1964
|
+
const o = value;
|
|
1965
|
+
return `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(o[k])}`).join(",")}}`;
|
|
1966
|
+
}
|
|
1967
|
+
return JSON.stringify(value) ?? "null";
|
|
1968
|
+
}
|
|
1969
|
+
var sha = (s) => createHash5("sha256").update(s).digest("hex");
|
|
1970
|
+
function hashKey(input) {
|
|
1971
|
+
return sha(stableStringify({ ...input, version: input.version ?? CACHE_VERSION }));
|
|
1972
|
+
}
|
|
1973
|
+
function computeRoutesHash(byFile) {
|
|
1974
|
+
return sha(stableStringify(Object.fromEntries(Object.entries(byFile).map(([f, r]) => [f, r.path]))));
|
|
1975
|
+
}
|
|
1976
|
+
function computePipelineHash(opts) {
|
|
1977
|
+
const plugins = (opts.plugins ?? []).map((p) => typeof p === "function" ? String(p) : stableStringify(p));
|
|
1978
|
+
return sha(stableStringify({ code: opts.code ?? null, plugins, codeVersion: CODE_PIPELINE_VERSION }));
|
|
1979
|
+
}
|
|
1980
|
+
var TransformCache = class {
|
|
1981
|
+
constructor(dir, enabled = true) {
|
|
1982
|
+
this.dir = dir;
|
|
1983
|
+
this.enabled = enabled;
|
|
1984
|
+
if (!enabled) return;
|
|
1985
|
+
try {
|
|
1986
|
+
this.manifest = JSON.parse(fs8.readFileSync(path8.join(dir, "manifest.json"), "utf8"));
|
|
1987
|
+
} catch {
|
|
1988
|
+
this.manifest = {};
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
dir;
|
|
1992
|
+
enabled;
|
|
1993
|
+
stats = { hit: 0, miss: 0, missReasons: {} };
|
|
1994
|
+
counted = /* @__PURE__ */ new Set();
|
|
1995
|
+
used = /* @__PURE__ */ new Set();
|
|
1996
|
+
manifest = {};
|
|
1997
|
+
nextManifest = {};
|
|
1998
|
+
fileOf(key) {
|
|
1999
|
+
return path8.join(this.dir, key.slice(0, 2), `${key}.json`);
|
|
2000
|
+
}
|
|
2001
|
+
read(key) {
|
|
2002
|
+
if (!this.enabled) return null;
|
|
2003
|
+
try {
|
|
2004
|
+
const v = JSON.parse(fs8.readFileSync(this.fileOf(key), "utf8"));
|
|
2005
|
+
return typeof v?.code === "string" && v.meta ? v : null;
|
|
2006
|
+
} catch {
|
|
2007
|
+
return null;
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
/** Ghi nguyên tử: tmp cùng thư mục rồi rename (build bị kill giữa chừng không để lại JSON cụt; cùng ổ nên rename không fail trên Windows). */
|
|
2011
|
+
write(key, payload) {
|
|
2012
|
+
if (!this.enabled) return;
|
|
2013
|
+
const file = this.fileOf(key);
|
|
2014
|
+
fs8.mkdirSync(path8.dirname(file), { recursive: true });
|
|
2015
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
2016
|
+
fs8.writeFileSync(tmp, JSON.stringify(payload));
|
|
2017
|
+
fs8.renameSync(tmp, file);
|
|
2018
|
+
}
|
|
2019
|
+
/** Xoá file cache không còn dùng (trang bị xoá/đổi tên/đổi nội dung). Best-effort, không throw. */
|
|
2020
|
+
gc(keepKeys) {
|
|
2021
|
+
if (!this.enabled) return;
|
|
2022
|
+
try {
|
|
2023
|
+
for (const shard of fs8.readdirSync(this.dir, { withFileTypes: true })) {
|
|
2024
|
+
if (!shard.isDirectory()) continue;
|
|
2025
|
+
for (const f of fs8.readdirSync(path8.join(this.dir, shard.name))) {
|
|
2026
|
+
if (!keepKeys.has(f.replace(/\.json$/, "")) || f.endsWith(".tmp")) fs8.rmSync(path8.join(this.dir, shard.name, f), { force: true });
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
} catch {
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
/**
|
|
2033
|
+
* Lấy từ cache hoặc tính mới. Mỗi file chỉ tính vào `stats` ở lần tra ĐẦU: build chạy plugin 2 lần
|
|
2034
|
+
* (client + SSR), lần 2 luôn hit vì lần 1 vừa ghi — đếm cả hai sẽ làm số liệu vô nghĩa.
|
|
2035
|
+
*/
|
|
2036
|
+
async getOrCompute(file, input, compute, cacheable = true) {
|
|
2037
|
+
const key = hashKey(input);
|
|
2038
|
+
const context = sha(stableStringify({ base: input.base, routesHash: input.routesHash }));
|
|
2039
|
+
const first = !this.counted.has(file);
|
|
2040
|
+
const hit = cacheable ? this.read(key) : null;
|
|
2041
|
+
if (hit) {
|
|
2042
|
+
if (first) this.stats.hit++;
|
|
2043
|
+
this.record(file, key, input, context);
|
|
2044
|
+
return hit;
|
|
2045
|
+
}
|
|
2046
|
+
if (first) {
|
|
2047
|
+
this.stats.miss++;
|
|
2048
|
+
this.stats.missReasons[file] = this.reasonFor(file, input, context);
|
|
2049
|
+
}
|
|
2050
|
+
this.counted.add(file);
|
|
2051
|
+
const payload = await compute();
|
|
2052
|
+
if (cacheable) {
|
|
2053
|
+
this.write(key, payload);
|
|
2054
|
+
this.record(file, key, input, context);
|
|
2055
|
+
}
|
|
2056
|
+
return payload;
|
|
2057
|
+
}
|
|
2058
|
+
record(file, key, input, context) {
|
|
2059
|
+
this.counted.add(file);
|
|
2060
|
+
this.used.add(key);
|
|
2061
|
+
this.nextManifest[file] = { md: sha(input.md), context: `${context}|${input.pipelineHash}`, key };
|
|
2062
|
+
}
|
|
2063
|
+
reasonFor(file, input, context) {
|
|
2064
|
+
const prev = this.manifest[file];
|
|
2065
|
+
if (!prev) return "new";
|
|
2066
|
+
if (prev.md !== sha(input.md)) return "md-changed";
|
|
2067
|
+
const [prevCtx, prevPipe] = [prev.context.split("|")[0], prev.context.split("|")[1]];
|
|
2068
|
+
if (prevPipe !== input.pipelineHash) return "pipeline-changed";
|
|
2069
|
+
return prevCtx !== context ? "config-changed" : "md-changed";
|
|
2070
|
+
}
|
|
2071
|
+
/** Cuối build: ghi manifest + gc theo các key vừa dùng. Chỉ giữ entry của file còn tồn tại. */
|
|
2072
|
+
finish() {
|
|
2073
|
+
if (!this.enabled) return;
|
|
2074
|
+
this.gc(this.used);
|
|
2075
|
+
try {
|
|
2076
|
+
fs8.mkdirSync(this.dir, { recursive: true });
|
|
2077
|
+
fs8.writeFileSync(path8.join(this.dir, "manifest.json"), JSON.stringify(this.nextManifest));
|
|
2078
|
+
} catch {
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
summary() {
|
|
2082
|
+
const total = this.stats.hit + this.stats.miss;
|
|
2083
|
+
return `cache ${this.stats.hit}/${total} hit`;
|
|
2084
|
+
}
|
|
2085
|
+
};
|
|
2086
|
+
|
|
2087
|
+
// src/prefers-color.ts
|
|
2088
|
+
var PREFERS_COLOR_ATTR = "data-prefers-color";
|
|
2089
|
+
var PREFERS_COLOR_STORAGE_KEY = "dommief:prefers-color";
|
|
2090
|
+
var PREFERS_COLOR_BOOT_SCRIPT = `(function(){try{var k=${JSON.stringify(PREFERS_COLOR_STORAGE_KEY)},a=${JSON.stringify(PREFERS_COLOR_ATTR)},v=localStorage.getItem(k);if(v!=='light'&&v!=='dark'&&v!=='auto')return;var c=v==='auto'?(matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'):v;document.documentElement.setAttribute(a,c)}catch(e){}})();`;
|
|
2091
|
+
function resolveConfiguredColor(defaultValue) {
|
|
2092
|
+
if (!defaultValue || defaultValue === "auto") return void 0;
|
|
2093
|
+
return defaultValue;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// src/vite/plugin.ts
|
|
2097
|
+
var demoRegistry = /* @__PURE__ */ new Map();
|
|
2098
|
+
function dommiefPlugin(opts) {
|
|
2099
|
+
let loaded = opts.config ? { config: opts.config, host: opts.host ?? createPluginHost(opts.config.plugins ?? []) } : void 0;
|
|
2100
|
+
const state = createState(opts.root, () => {
|
|
2101
|
+
if (!loaded) throw new Error("dommief: config ch\u01B0a \u0111\u01B0\u1EE3c n\u1EA1p");
|
|
2102
|
+
return loaded;
|
|
2103
|
+
});
|
|
2104
|
+
const markdownPlugin = {
|
|
2105
|
+
name: "dommief:markdown",
|
|
2106
|
+
enforce: "pre",
|
|
2107
|
+
async config() {
|
|
2108
|
+
if (!loaded) {
|
|
2109
|
+
const config = await loadConfig(opts.root);
|
|
2110
|
+
loaded = { config, host: createPluginHost(config.plugins ?? []) };
|
|
2111
|
+
}
|
|
2112
|
+
const aliases = buildAliasChain(opts.root, loaded.config);
|
|
2113
|
+
aliases["dommief/theme-api"] = resolveThemeApiFile();
|
|
2114
|
+
return {
|
|
2115
|
+
base: loaded.config.base,
|
|
2116
|
+
resolve: { dedupe: ["react", "react-dom", "react-router"], alias: aliases },
|
|
2117
|
+
ssr: { noExternal: ["dommief"] }
|
|
2118
|
+
};
|
|
2119
|
+
},
|
|
2120
|
+
resolveId(id, importer) {
|
|
2121
|
+
if (id === "dommief/client") return resolveClientFile("index.tsx");
|
|
2122
|
+
const bare = id.replace(/^\//, "");
|
|
2123
|
+
if (bare === "virtual:dommief/entry") return "\0virtual:dommief/entry";
|
|
2124
|
+
if (bare.startsWith("virtual:dommief-demo/")) return "\0" + bare;
|
|
2125
|
+
if (bare === "virtual:dommief/routes" || bare === "virtual:dommief/site") return null;
|
|
2126
|
+
if (id.endsWith(".md") || id.endsWith(".md?meta")) {
|
|
2127
|
+
const base = importer ? path9.dirname(importer.replace(/\?meta$/, "")) : process.cwd();
|
|
2128
|
+
const q = id.endsWith("?meta") ? "?meta" : "";
|
|
2129
|
+
const clean = id.replace(/\?meta$/, "");
|
|
2130
|
+
return winPath(path9.isAbsolute(clean) ? clean : path9.resolve(base, clean)) + q;
|
|
2131
|
+
}
|
|
2132
|
+
return null;
|
|
2133
|
+
},
|
|
2134
|
+
async load(id) {
|
|
2135
|
+
if (id.startsWith("\0virtual:dommief-demo/")) {
|
|
2136
|
+
const demoId2 = id.slice("\0virtual:dommief-demo/".length);
|
|
2137
|
+
const source = demoRegistry.get(demoId2) ?? "export default function Demo(){ return null; }";
|
|
2138
|
+
return { code: source, map: null, moduleType: "jsx" };
|
|
2139
|
+
}
|
|
2140
|
+
if (id === "\0virtual:dommief/entry") {
|
|
2141
|
+
return { code: `import ${JSON.stringify(resolveClientFile("entry-client.tsx"))};`, map: null, moduleType: "js" };
|
|
2142
|
+
}
|
|
2143
|
+
if (id.endsWith(".md") || id.endsWith(".md?meta")) {
|
|
2144
|
+
const isMeta = id.endsWith("?meta");
|
|
2145
|
+
const file = isMeta ? id.slice(0, -"?meta".length) : id;
|
|
2146
|
+
const code = await fs9.promises.readFile(file, "utf8");
|
|
2147
|
+
const transformOpts = { hooks: state.host(), byFile: state.getTable().byFile, base: state.config().base, code: state.config().markdown };
|
|
2148
|
+
let out;
|
|
2149
|
+
let meta;
|
|
2150
|
+
let warnings;
|
|
2151
|
+
let apiSrcFiles = [];
|
|
2152
|
+
if (opts.cache) {
|
|
2153
|
+
const host = state.host();
|
|
2154
|
+
const input = {
|
|
2155
|
+
md: code,
|
|
2156
|
+
file,
|
|
2157
|
+
base: transformOpts.base,
|
|
2158
|
+
routesHash: computeRoutesHash(transformOpts.byFile),
|
|
2159
|
+
pipelineHash: computePipelineHash({ code: transformOpts.code, plugins: [...host.collectRemarkPlugins(), ...host.collectRehypePlugins()] })
|
|
2160
|
+
};
|
|
2161
|
+
const cacheable = !/<API[\s/>]/.test(code);
|
|
2162
|
+
const hit = await opts.cache.getOrCompute(file, input, async () => {
|
|
2163
|
+
const r = await transformMarkdown(code, file, transformOpts);
|
|
2164
|
+
return { code: r.code, meta: r.meta, warnings: r.warnings, demos: r.demos };
|
|
2165
|
+
}, cacheable);
|
|
2166
|
+
let hitDemos = [];
|
|
2167
|
+
({ code: out, meta, warnings } = hit);
|
|
2168
|
+
hitDemos = hit.demos ?? [];
|
|
2169
|
+
for (const d of hitDemos) {
|
|
2170
|
+
demoRegistry.set(d.id, d.source);
|
|
2171
|
+
if (d.src) this.addWatchFile(path9.resolve(path9.dirname(file), d.src));
|
|
2172
|
+
}
|
|
2173
|
+
} else {
|
|
2174
|
+
const r = await transformMarkdown(code, file, transformOpts);
|
|
2175
|
+
({ code: out, meta, warnings, apiSrcFiles } = r);
|
|
2176
|
+
const nonCacheDemos = r.demos ?? [];
|
|
2177
|
+
for (const d of nonCacheDemos) {
|
|
2178
|
+
demoRegistry.set(d.id, d.source);
|
|
2179
|
+
if (d.src) this.addWatchFile(path9.resolve(path9.dirname(file), d.src));
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
for (const w of warnings) this.warn(w);
|
|
2183
|
+
for (const apiFile of apiSrcFiles) this.addWatchFile(apiFile);
|
|
2184
|
+
if (!isMeta) opts.collectTexts?.(file, meta.texts);
|
|
2185
|
+
if (isMeta) {
|
|
2186
|
+
return { code: `export default ${JSON.stringify({ frontmatter: meta.frontmatter, toc: meta.toc })};`, map: null, moduleType: "js" };
|
|
2187
|
+
}
|
|
2188
|
+
return { code: out, map: null, moduleType: "jsx" };
|
|
2189
|
+
}
|
|
2190
|
+
return null;
|
|
2191
|
+
},
|
|
2192
|
+
// order 'pre': script chèn ở đây phải đi qua bước bundle HTML của Vite, nếu không build không có JS.
|
|
2193
|
+
transformIndexHtml: { order: "pre", handler(html) {
|
|
2194
|
+
const title = state.config().title;
|
|
2195
|
+
let out = html;
|
|
2196
|
+
if (!/<title>/.test(out)) out = out.replace("<head>", `<head><title>${title}</title>`);
|
|
2197
|
+
const tags = state.host().collectHtmlHeadTags();
|
|
2198
|
+
if (tags.length) out = out.replace("</head>", tags.join("\n") + "</head>");
|
|
2199
|
+
const themeConfig = state.config().themeConfig;
|
|
2200
|
+
const configuredColor = resolveConfiguredColor(themeConfig?.prefersColor?.default);
|
|
2201
|
+
if (configuredColor && !/data-prefers-color=/.test(out)) {
|
|
2202
|
+
out = out.replace(/<html(\s|>)/, `<html ${PREFERS_COLOR_ATTR}="${configuredColor}"$1`);
|
|
2203
|
+
}
|
|
2204
|
+
if (!out.includes(PREFERS_COLOR_BOOT_SCRIPT)) {
|
|
2205
|
+
out = out.replace("</head>", `<script>${PREFERS_COLOR_BOOT_SCRIPT}</script></head>`);
|
|
2206
|
+
}
|
|
2207
|
+
if (!out.includes("virtual:dommief/entry")) {
|
|
2208
|
+
out = out.replace("</body>", `<script type="module" src="/virtual:dommief/entry"></script></body>`);
|
|
2209
|
+
}
|
|
2210
|
+
return out;
|
|
2211
|
+
} },
|
|
2212
|
+
configureServer(server) {
|
|
2213
|
+
const localThemeDir = winPath(path9.resolve(opts.root, ".dommief", "theme")) + "/";
|
|
2214
|
+
const update = () => {
|
|
2215
|
+
const aliases = buildAliasChain(opts.root, loaded.config);
|
|
2216
|
+
server.config.resolve.alias = { ...server.config.resolve.alias, ...aliases };
|
|
2217
|
+
server.ws.send({ type: "full-reload" });
|
|
2218
|
+
};
|
|
2219
|
+
server.watcher.on("add", (f) => {
|
|
2220
|
+
if (winPath(f).startsWith(localThemeDir)) update();
|
|
2221
|
+
});
|
|
2222
|
+
server.watcher.on("unlink", (f) => {
|
|
2223
|
+
if (winPath(f).startsWith(localThemeDir)) update();
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
};
|
|
2227
|
+
return [markdownPlugin, dommiefRoutesPlugin(state), dommiefSitePlugin(state), dommiefSearchIndexPlugin(state)];
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
// src/log.ts
|
|
2231
|
+
function createLogger(verbose = false) {
|
|
2232
|
+
return {
|
|
2233
|
+
verbose,
|
|
2234
|
+
info: (m) => console.log(m),
|
|
2235
|
+
debug: (m) => {
|
|
2236
|
+
if (verbose) console.log("[debug]", m);
|
|
2237
|
+
},
|
|
2238
|
+
warn: (m) => console.warn("[warn]", m),
|
|
2239
|
+
error: (m) => console.error("[error]", m)
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
// src/cli.ts
|
|
2244
|
+
init_plugins();
|
|
2245
|
+
|
|
2246
|
+
// src/build.ts
|
|
2247
|
+
import path12 from "path";
|
|
2248
|
+
import fs12 from "fs";
|
|
2249
|
+
import { pathToFileURL } from "url";
|
|
2250
|
+
import { build } from "vite";
|
|
2251
|
+
init_errors();
|
|
2252
|
+
|
|
2253
|
+
// src/link-check.ts
|
|
2254
|
+
import fs10 from "fs";
|
|
2255
|
+
import path10 from "path";
|
|
2256
|
+
var HREF_RE = /<a\b[^>]*\shref="([^"]*)"[^>]*>/g;
|
|
2257
|
+
var ASSET_RE = /<(?:img|link)\b[^>]*\s(?:src|href)="([^"]*)"[^>]*>/g;
|
|
2258
|
+
var LINE_RE = /\sdata-src-line="(\d+)"/;
|
|
2259
|
+
var EXTERNAL2 = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
|
|
2260
|
+
function extractRefs(re, html, routeId, file) {
|
|
2261
|
+
const out = [];
|
|
2262
|
+
let m;
|
|
2263
|
+
re.lastIndex = 0;
|
|
2264
|
+
while (m = re.exec(html)) {
|
|
2265
|
+
const tag = m[0];
|
|
2266
|
+
const href = m[1];
|
|
2267
|
+
if (!href || href.startsWith("#") || EXTERNAL2.test(href)) continue;
|
|
2268
|
+
const line = tag.match(LINE_RE)?.[1] ?? "\u2014";
|
|
2269
|
+
out.push({ href, routeId, file, line });
|
|
2270
|
+
}
|
|
2271
|
+
return out;
|
|
2272
|
+
}
|
|
2273
|
+
function collect(routeId, html, file) {
|
|
2274
|
+
return [...extractRefs(HREF_RE, html, routeId, file), ...extractRefs(ASSET_RE, html, routeId, file)];
|
|
2275
|
+
}
|
|
2276
|
+
function stripBase(p, base) {
|
|
2277
|
+
const b = base.replace(/\/$/, "");
|
|
2278
|
+
if (b && p.startsWith(b + "/")) return p.slice(b.length) || "/";
|
|
2279
|
+
if (b && p === b) return "/";
|
|
2280
|
+
return p;
|
|
2281
|
+
}
|
|
2282
|
+
function check(refs, opts) {
|
|
2283
|
+
const issues = [];
|
|
2284
|
+
for (const ref of refs) {
|
|
2285
|
+
const isAsset = !!ref.href.match(/\.(png|jpe?g|gif|svg|webp|avif|ico|css|woff2?|ttf)(\?.*)?$/i);
|
|
2286
|
+
const [rawPath, hash] = ref.href.split("#");
|
|
2287
|
+
const cleanPath = (rawPath ?? "").split("?")[0];
|
|
2288
|
+
if (isAsset) {
|
|
2289
|
+
const relPath = stripBase(cleanPath, opts.base) || "/";
|
|
2290
|
+
const onDisk = path10.join(opts.outDir, decodeURIComponent(relPath));
|
|
2291
|
+
if (!fs10.existsSync(onDisk)) issues.push({ code: "link_asset_missing", ref });
|
|
2292
|
+
continue;
|
|
2293
|
+
}
|
|
2294
|
+
const routePath = stripBase(cleanPath, opts.base) || "/";
|
|
2295
|
+
const target = opts.byPath[routePath];
|
|
2296
|
+
if (!target) {
|
|
2297
|
+
issues.push({ code: "link_target_missing", ref });
|
|
2298
|
+
continue;
|
|
2299
|
+
}
|
|
2300
|
+
if (hash) {
|
|
2301
|
+
const toc = opts.tocByRoute[target.path] ?? opts.tocByRoute[routePath];
|
|
2302
|
+
if (toc !== void 0 && !toc.some((t) => t.id === hash)) {
|
|
2303
|
+
issues.push({ code: "link_anchor_missing", ref });
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
return issues;
|
|
2308
|
+
}
|
|
2309
|
+
var MAX_LINES = 20;
|
|
2310
|
+
function formatIssues(issues) {
|
|
2311
|
+
const lines = issues.slice(0, MAX_LINES).map((i) => `${i.ref.file}:${i.ref.line} ${i.ref.href} (${i.code})`);
|
|
2312
|
+
const rest = issues.length - lines.length;
|
|
2313
|
+
if (rest > 0) lines.push(`\u2026 v\xE0 ${rest} l\u1ED7i n\u1EEFa`);
|
|
2314
|
+
return lines.join("\n");
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
// src/exclude.ts
|
|
2318
|
+
var cache = /* @__PURE__ */ new Map();
|
|
2319
|
+
function globToRegExp(pattern) {
|
|
2320
|
+
let src = "";
|
|
2321
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
2322
|
+
const c = pattern.charAt(i);
|
|
2323
|
+
if (c === "*" && pattern.charAt(i + 1) === "*") {
|
|
2324
|
+
const slash = pattern.charAt(i + 2) === "/";
|
|
2325
|
+
src += slash ? "(?:.*/)?" : ".*";
|
|
2326
|
+
i += slash ? 2 : 1;
|
|
2327
|
+
} else if (c === "*") {
|
|
2328
|
+
src += "[^/]*";
|
|
2329
|
+
} else if (c === "?") {
|
|
2330
|
+
src += "[^/]";
|
|
2331
|
+
} else {
|
|
2332
|
+
src += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
return new RegExp(`^${src}$`);
|
|
2336
|
+
}
|
|
2337
|
+
function matcher(pattern) {
|
|
2338
|
+
const hit = cache.get(pattern);
|
|
2339
|
+
if (hit) return hit;
|
|
2340
|
+
const re = globToRegExp(pattern);
|
|
2341
|
+
cache.set(pattern, re);
|
|
2342
|
+
return re;
|
|
2343
|
+
}
|
|
2344
|
+
function matchExclude(path15, patterns) {
|
|
2345
|
+
return patterns.some((p) => matcher(p).test(path15));
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
// src/sitemap.ts
|
|
2349
|
+
import fs11 from "fs";
|
|
2350
|
+
import path11 from "path";
|
|
2351
|
+
function canonicalUrl(hostname, base, routePath) {
|
|
2352
|
+
const host = hostname.replace(/\/$/, "");
|
|
2353
|
+
const b = base.replace(/\/$/, "");
|
|
2354
|
+
const p = routePath === "/" ? "/" : routePath;
|
|
2355
|
+
return host + (b + p).replace(/\/{2,}/g, "/").replace(/^(https?:\/)([^/])/, "$1/$2");
|
|
2356
|
+
}
|
|
2357
|
+
function buildSitemap(routes, config, base) {
|
|
2358
|
+
const urls = routes.filter((r) => r.path !== "/404" && !/[:*]/.test(r.path) && !matchExclude(r.path, config.exclude)).map((r) => ` <url><loc>${escapeXml(canonicalUrl(config.hostname, base, r.path))}</loc></url>`).join("\n");
|
|
2359
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2360
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
2361
|
+
${urls}
|
|
2362
|
+
</urlset>
|
|
2363
|
+
`;
|
|
2364
|
+
}
|
|
2365
|
+
function buildRobots(sitemapUrl) {
|
|
2366
|
+
return `User-agent: *
|
|
2367
|
+
Allow: /
|
|
2368
|
+
|
|
2369
|
+
Sitemap: ${sitemapUrl}
|
|
2370
|
+
`;
|
|
2371
|
+
}
|
|
2372
|
+
function escapeXml(s) {
|
|
2373
|
+
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
2374
|
+
}
|
|
2375
|
+
function writeSitemapAndRobots(opts) {
|
|
2376
|
+
if (opts.sitemap === false) return;
|
|
2377
|
+
if (!opts.sitemap.hostname) {
|
|
2378
|
+
opts.log.warn("sitemap.hostname thi\u1EBFu \u2014 b\u1ECF qua sitemap.xml/robots.txt");
|
|
2379
|
+
return;
|
|
2380
|
+
}
|
|
2381
|
+
const xml = buildSitemap(opts.routes, opts.sitemap, opts.base);
|
|
2382
|
+
fs11.writeFileSync(path11.join(opts.outDir, "sitemap.xml"), xml);
|
|
2383
|
+
if (opts.robots) {
|
|
2384
|
+
const sitemapUrl = canonicalUrl(opts.sitemap.hostname, opts.base, "/").replace(/\/$/, "") + "/sitemap.xml";
|
|
2385
|
+
fs11.writeFileSync(path11.join(opts.outDir, "robots.txt"), buildRobots(sitemapUrl));
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
// src/build.ts
|
|
2390
|
+
var ROOT_PLACEHOLDER = /<div id="root"><\/div>/;
|
|
2391
|
+
async function mapLimit(items, limit, fn) {
|
|
2392
|
+
let i = 0;
|
|
2393
|
+
const worker = async () => {
|
|
2394
|
+
while (i < items.length) await fn(items[i++]);
|
|
2395
|
+
};
|
|
2396
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
2397
|
+
}
|
|
2398
|
+
var LinkCheckFailedError = class extends Error {
|
|
2399
|
+
constructor(report) {
|
|
2400
|
+
super("link n\u1ED9i b\u1ED9 ch\u1EBFt; ch\u1EA1y v\u1EDBi --no-link-check \u0111\u1EC3 b\u1ECF qua");
|
|
2401
|
+
this.report = report;
|
|
2402
|
+
}
|
|
2403
|
+
report;
|
|
2404
|
+
};
|
|
2405
|
+
var emitters = [];
|
|
2406
|
+
emitters.push({
|
|
2407
|
+
name: "sitemap",
|
|
2408
|
+
run: ({ config, outDir, routes, log }) => {
|
|
2409
|
+
writeSitemapAndRobots({ outDir, routes, sitemap: config.sitemap, robots: config.robots, base: config.base, log });
|
|
2410
|
+
}
|
|
2411
|
+
});
|
|
2412
|
+
function formatTimings(timings) {
|
|
2413
|
+
const total = timings.reduce((s, t) => s + t.ms, 0);
|
|
2414
|
+
const w = Math.max(...timings.map((t) => t.stage.length), 5);
|
|
2415
|
+
const pct = (ms) => (total > 0 ? `${Math.round(ms / total * 100)}%` : "\u2014").padStart(4);
|
|
2416
|
+
const sec = (ms) => ((ms / 1e3).toFixed(2) + "s").padStart(8);
|
|
2417
|
+
const rows = timings.map((t) => ` ${t.stage.padEnd(w)} ${sec(t.ms)} ${pct(t.ms)}`);
|
|
2418
|
+
return ["build timings:", ...rows, ` ${"total".padEnd(w)} ${sec(total)} ${pct(total)}`].join("\n");
|
|
2419
|
+
}
|
|
2420
|
+
function planRoutes(routes, ssg, log) {
|
|
2421
|
+
const prerender = [];
|
|
2422
|
+
const shell = [];
|
|
2423
|
+
for (const r of routes) {
|
|
2424
|
+
if (matchesAny(r.path, ssg.noEmit)) continue;
|
|
2425
|
+
if (matchesAny(r.path, ssg.exclude)) {
|
|
2426
|
+
shell.push(r);
|
|
2427
|
+
if (log && routes.some((other) => other !== r && other.path.startsWith(r.path === "/" ? "/" : r.path + "/"))) {
|
|
2428
|
+
log.warn(`ssg.exclude \u0111ang lo\u1EA1i route "${r.path}" NH\u01AFNG n\xF3 c\xF3 route con \u2014 \u0111i\u1EC1u h\u01B0\u1EDBng th\u1EB3ng v\xE0o "${r.path}" tr\xEAn host kh\xF4ng SPA fallback s\u1EBD 404`);
|
|
2429
|
+
}
|
|
2430
|
+
continue;
|
|
2431
|
+
}
|
|
2432
|
+
prerender.push(r);
|
|
2433
|
+
}
|
|
2434
|
+
return { prerender, shell };
|
|
2435
|
+
}
|
|
2436
|
+
function matchesAny(p, patterns) {
|
|
2437
|
+
return patterns.some((pat) => pat.endsWith("/*") && p === pat.slice(0, -2) || matchExclude(p, [pat]));
|
|
2438
|
+
}
|
|
2439
|
+
function routesByPath(routes) {
|
|
2440
|
+
return Object.fromEntries(routes.map((r) => [r.path, { path: r.path }]));
|
|
2441
|
+
}
|
|
2442
|
+
async function runBuild({ root, config, host, log }) {
|
|
2443
|
+
const linkCheckEnabled = config.linkCheck !== false;
|
|
2444
|
+
const timings = [];
|
|
2445
|
+
const timed = async (stage, fn) => {
|
|
2446
|
+
const t0 = performance.now();
|
|
2447
|
+
try {
|
|
2448
|
+
return await fn();
|
|
2449
|
+
} finally {
|
|
2450
|
+
timings.push({ stage, ms: performance.now() - t0 });
|
|
2451
|
+
}
|
|
2452
|
+
};
|
|
2453
|
+
const cache2 = new TransformCache(path12.join(root, ".dommief", "cache"), config.cache);
|
|
2454
|
+
const textsByFile = /* @__PURE__ */ new Map();
|
|
2455
|
+
const plugins = () => [dommiefPlugin({ root, config, host, cache: cache2, collectTexts: (file, texts) => textsByFile.set(file, texts) })];
|
|
2456
|
+
const distDir = path12.resolve(root, config.outDir);
|
|
2457
|
+
await timed("client", () => build({ root, plugins: plugins(), base: config.base, build: { outDir: distDir, emptyOutDir: true } }));
|
|
2458
|
+
const ssrDir = path12.join(root, ".dommief", "server");
|
|
2459
|
+
fs12.mkdirSync(ssrDir, { recursive: true });
|
|
2460
|
+
await timed("ssr", () => build({
|
|
2461
|
+
root,
|
|
2462
|
+
plugins: plugins(),
|
|
2463
|
+
base: config.base,
|
|
2464
|
+
build: {
|
|
2465
|
+
outDir: ssrDir,
|
|
2466
|
+
emptyOutDir: true,
|
|
2467
|
+
ssr: resolveClientFile("entry-server.tsx"),
|
|
2468
|
+
rollupOptions: { output: { entryFileNames: "server.mjs", format: "esm" } }
|
|
2469
|
+
}
|
|
2470
|
+
}));
|
|
2471
|
+
const templatePath = path12.join(distDir, "index.html");
|
|
2472
|
+
const rawTemplate = fs12.readFileSync(templatePath, "utf8");
|
|
2473
|
+
const template = /<meta[^>]+name=["']viewport["']/i.test(rawTemplate) ? rawTemplate : rawTemplate.replace(/<\/head>/i, ' <meta name="viewport" content="width=device-width, initial-scale=1">\n</head>');
|
|
2474
|
+
if (!ROOT_PLACEHOLDER.test(template)) {
|
|
2475
|
+
throw new DommiefError('index.html kh\xF4ng c\xF3 <div id="root"></div> \u0111\u1EC3 ch\xE8n n\u1ED9i dung prerender', { file: templatePath, code: "ssg_no_root" });
|
|
2476
|
+
}
|
|
2477
|
+
const serverMod = await import(pathToFileURL(path12.join(ssrDir, "server.mjs")).href);
|
|
2478
|
+
const routes = serverMod.routes ?? serverMod.default?.routes ?? [];
|
|
2479
|
+
const render = serverMod.render ?? serverMod.default?.render;
|
|
2480
|
+
const site = serverMod.site ?? serverMod.default?.site;
|
|
2481
|
+
const fileOf = (routeId) => site?.routes?.[routeId]?.file ?? routeId;
|
|
2482
|
+
const canonicalHostname = config.sitemap === false ? void 0 : config.sitemap.hostname;
|
|
2483
|
+
const escapeAttr = (s) => s.replace(/"/g, """);
|
|
2484
|
+
const wrap = (html, routePath) => {
|
|
2485
|
+
let out = template.replace(ROOT_PLACEHOLDER, () => `<div id="root">${html}</div>`);
|
|
2486
|
+
if (canonicalHostname && routePath !== "/404") {
|
|
2487
|
+
const href = canonicalUrl(canonicalHostname, config.base, routePath);
|
|
2488
|
+
if (!/<link[^>]+rel=["']canonical["']/i.test(out)) {
|
|
2489
|
+
out = out.replace(/<\/head>/i, ` <link rel="canonical" href="${escapeAttr(href)}">
|
|
2490
|
+
</head>`);
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
return out;
|
|
2494
|
+
};
|
|
2495
|
+
const plan = planRoutes(routes, config.ssg, log);
|
|
2496
|
+
const writePage = (r, html) => {
|
|
2497
|
+
const targetDir = r.path === "/" ? distDir : path12.join(distDir, r.path.replace(/^\//, ""));
|
|
2498
|
+
fs12.mkdirSync(targetDir, { recursive: true });
|
|
2499
|
+
fs12.writeFileSync(path12.join(targetDir, "index.html"), html);
|
|
2500
|
+
};
|
|
2501
|
+
const linkRefs = [];
|
|
2502
|
+
await timed("prerender", async () => {
|
|
2503
|
+
await mapLimit(plan.prerender, config.ssg.concurrency, async (r) => {
|
|
2504
|
+
const html = await render(r.path);
|
|
2505
|
+
if (linkCheckEnabled) linkRefs.push(...collect(r.id, html, fileOf(r.id)));
|
|
2506
|
+
writePage(r, wrap(html, r.path));
|
|
2507
|
+
});
|
|
2508
|
+
for (const r of plan.shell) writePage(r, wrap(template, r.path));
|
|
2509
|
+
try {
|
|
2510
|
+
fs12.writeFileSync(path12.join(distDir, "404.html"), wrap(await render("/404"), "/404"));
|
|
2511
|
+
} catch (e) {
|
|
2512
|
+
log.warn("kh\xF4ng render \u0111\u01B0\u1EE3c 404.html: " + formatError(e));
|
|
2513
|
+
}
|
|
2514
|
+
});
|
|
2515
|
+
if (linkCheckEnabled) {
|
|
2516
|
+
await timed("link-check", async () => {
|
|
2517
|
+
const tocByRoute = {};
|
|
2518
|
+
for (const r of plan.prerender) {
|
|
2519
|
+
const meta = site?.meta?.[r.id];
|
|
2520
|
+
if (meta?.toc) tocByRoute[r.path] = meta.toc;
|
|
2521
|
+
}
|
|
2522
|
+
const issues = check(linkRefs, { byPath: routesByPath(routes), tocByRoute, base: config.base, outDir: distDir });
|
|
2523
|
+
if (issues.length) throw new LinkCheckFailedError(formatIssues(issues));
|
|
2524
|
+
});
|
|
2525
|
+
}
|
|
2526
|
+
if (config.search.enabled) {
|
|
2527
|
+
await timed("search-index", async () => {
|
|
2528
|
+
const { buildSearchIndex: buildSearchIndex2, INDEX_BUDGET_BYTES_PER_PAGE: INDEX_BUDGET_BYTES_PER_PAGE2 } = await Promise.resolve().then(() => (init_search(), search_exports));
|
|
2529
|
+
const res = await buildSearchIndex2({
|
|
2530
|
+
routes: plan.prerender,
|
|
2531
|
+
loadPageMeta: (r) => ({
|
|
2532
|
+
frontmatter: site?.meta?.[r.id]?.frontmatter ?? {},
|
|
2533
|
+
toc: site?.meta?.[r.id]?.toc ?? [],
|
|
2534
|
+
texts: textsByFile.get(fileOf(r.id)) ?? []
|
|
2535
|
+
}),
|
|
2536
|
+
config: { locales: config.locales }
|
|
2537
|
+
});
|
|
2538
|
+
const searchDir = path12.join(distDir, "search-index");
|
|
2539
|
+
fs12.mkdirSync(searchDir, { recursive: true });
|
|
2540
|
+
for (const [localeId2, json] of Object.entries(res.locales)) {
|
|
2541
|
+
fs12.writeFileSync(path12.join(searchDir, `${localeId2}.json`), JSON.stringify(json));
|
|
2542
|
+
}
|
|
2543
|
+
for (const [localeId2, s] of Object.entries(res.stats)) {
|
|
2544
|
+
const kbPerPage = s.pages ? (s.bytes / 1024 / s.pages).toFixed(1) : "0";
|
|
2545
|
+
log.info(`search index [${localeId2}]: ${s.docs} docs, ${(s.bytes / 1024).toFixed(1)} KB, ${kbPerPage} KB/trang`);
|
|
2546
|
+
if (s.bytes > INDEX_BUDGET_BYTES_PER_PAGE2 * s.pages) {
|
|
2547
|
+
log.warn(`search index [${localeId2}] v\u01B0\u1EE3t ng\xE2n s\xE1ch ${(INDEX_BUDGET_BYTES_PER_PAGE2 / 1024).toFixed(0)} KB/trang (\u0111ang \u1EDF ${kbPerPage} KB/trang)`);
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
});
|
|
2551
|
+
}
|
|
2552
|
+
for (const e of emitters) await timed(e.name, async () => {
|
|
2553
|
+
await e.run({ root, config, outDir: distDir, routes: plan.prerender, log });
|
|
2554
|
+
});
|
|
2555
|
+
await timed("plugins", () => host.runBuildEnd({ config, outDir: distDir, routes }));
|
|
2556
|
+
cache2.finish();
|
|
2557
|
+
if (config.cache) {
|
|
2558
|
+
log.info(cache2.summary());
|
|
2559
|
+
if (log.verbose) for (const [f, why] of Object.entries(cache2.stats.missReasons)) log.info(` miss ${path12.relative(root, f)} (${why})`);
|
|
2560
|
+
}
|
|
2561
|
+
log.info(formatTimings(timings));
|
|
2562
|
+
log.info("build done");
|
|
2563
|
+
return {
|
|
2564
|
+
timings: Object.fromEntries(timings.map((t) => [t.stage, t.ms])),
|
|
2565
|
+
cacheStats: cache2.stats
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
// src/preview.ts
|
|
2570
|
+
init_errors();
|
|
2571
|
+
import fs13 from "fs";
|
|
2572
|
+
import path13 from "path";
|
|
2573
|
+
import { preview as vitePreview } from "vite";
|
|
2574
|
+
function assertBuildOutput(outDir) {
|
|
2575
|
+
const indexHtml = path13.join(outDir, "index.html");
|
|
2576
|
+
if (!fs13.existsSync(outDir) || !fs13.existsSync(indexHtml)) {
|
|
2577
|
+
throw new DommiefError(`kh\xF4ng c\xF3 build output \u1EDF "${outDir}". Ch\u1EA1y \`dommief build\` tr\u01B0\u1EDBc.`, { code: "no_build_output", file: outDir });
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
async function startPreview(opts) {
|
|
2581
|
+
assertBuildOutput(opts.outDir);
|
|
2582
|
+
const server = await vitePreview({
|
|
2583
|
+
root: opts.root,
|
|
2584
|
+
base: opts.base,
|
|
2585
|
+
preview: {
|
|
2586
|
+
...opts.port !== void 0 ? { port: opts.port } : {},
|
|
2587
|
+
...opts.host !== void 0 ? { host: opts.host } : {},
|
|
2588
|
+
...opts.open !== void 0 ? { open: opts.open } : {}
|
|
2589
|
+
},
|
|
2590
|
+
build: { outDir: opts.outDir },
|
|
2591
|
+
plugins: [spaFallbackPlugin(opts)]
|
|
2592
|
+
});
|
|
2593
|
+
return server;
|
|
2594
|
+
}
|
|
2595
|
+
function spaFallbackPlugin(opts) {
|
|
2596
|
+
const shellPath = path13.join(opts.outDir, "index.html");
|
|
2597
|
+
const base = opts.base.replace(/\/$/, "");
|
|
2598
|
+
const serveHtml = (res, file, status = 200) => {
|
|
2599
|
+
res.statusCode = status;
|
|
2600
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
2601
|
+
fs13.createReadStream(file).pipe(res);
|
|
2602
|
+
};
|
|
2603
|
+
return {
|
|
2604
|
+
name: "dommief:preview-spa-fallback",
|
|
2605
|
+
configurePreviewServer(s) {
|
|
2606
|
+
s.middlewares.use((req, res, next) => {
|
|
2607
|
+
const urlPath = decodeURIComponent((req.url ?? "/").split("?")[0] ?? "/");
|
|
2608
|
+
if (req.method !== "GET") return next();
|
|
2609
|
+
if (base && urlPath !== base && !urlPath.startsWith(base + "/")) {
|
|
2610
|
+
res.statusCode = 404;
|
|
2611
|
+
res.end("Not Found");
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
const rel = base ? urlPath.slice(base.length) || "/" : urlPath;
|
|
2615
|
+
const outDirResolved = path13.resolve(opts.outDir);
|
|
2616
|
+
const safe = (p) => {
|
|
2617
|
+
const r = path13.resolve(p);
|
|
2618
|
+
return r === outDirResolved || r.startsWith(outDirResolved + path13.sep);
|
|
2619
|
+
};
|
|
2620
|
+
const candidates = [
|
|
2621
|
+
path13.join(opts.outDir, rel, "index.html"),
|
|
2622
|
+
path13.join(opts.outDir, rel.endsWith(".html") ? rel : rel.replace(/\/$/, "") + ".html")
|
|
2623
|
+
];
|
|
2624
|
+
const hit = candidates.find((p) => safe(p) && fs13.existsSync(p) && fs13.statSync(p).isFile());
|
|
2625
|
+
if (hit) return serveHtml(res, hit);
|
|
2626
|
+
if (/\.[a-z0-9]+$/i.test(rel.replace(/\.html$/i, ""))) return next();
|
|
2627
|
+
const notFound = path13.join(opts.outDir, "404.html");
|
|
2628
|
+
if (fs13.existsSync(notFound)) {
|
|
2629
|
+
serveHtml(res, notFound, 404);
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2632
|
+
serveHtml(res, shellPath);
|
|
2633
|
+
});
|
|
2634
|
+
}
|
|
2635
|
+
};
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
// src/cli.ts
|
|
2639
|
+
init_errors();
|
|
2640
|
+
var cli = cac("dommief");
|
|
2641
|
+
cli.command("dev", "start dev server").option("--verbose", "verbose logging").option("--base <base>", "public base path (vd /docs/)").option("--port <port>", "dev server port").action(async (opts) => {
|
|
2642
|
+
const log = createLogger(!!opts.verbose);
|
|
2643
|
+
const root = process.cwd();
|
|
2644
|
+
log.debug("root=" + root);
|
|
2645
|
+
const config = await loadConfig(root, { base: opts.base });
|
|
2646
|
+
const host = createPluginHost(config.plugins ?? []);
|
|
2647
|
+
const server = await createServer({
|
|
2648
|
+
root,
|
|
2649
|
+
plugins: [dommiefPlugin({ root, config, host })],
|
|
2650
|
+
server: { port: Number(opts.port ?? config.dev.port) }
|
|
2651
|
+
});
|
|
2652
|
+
await server.listen();
|
|
2653
|
+
server.printUrls();
|
|
2654
|
+
});
|
|
2655
|
+
cli.command("build", "build site").option("--verbose", "verbose logging").option("--base <base>", "public base path (vd /docs/)").option("--out-dir <dir>", "output directory").option("--no-cache", "b\u1ECF qua cache transform").option("--no-link-check", "kh\xF4ng ki\u1EC3m link n\u1ED9i b\u1ED9 ch\u1EBFt").action(async (opts) => {
|
|
2656
|
+
const log = createLogger(!!opts.verbose);
|
|
2657
|
+
const root = process.cwd();
|
|
2658
|
+
log.debug("root=" + root);
|
|
2659
|
+
const config = await loadConfig(root, { base: opts.base, outDir: opts.outDir, cache: opts.cache, linkCheck: opts.linkCheck });
|
|
2660
|
+
const host = createPluginHost(config.plugins ?? []);
|
|
2661
|
+
try {
|
|
2662
|
+
await runBuild({ root, config, host, log });
|
|
2663
|
+
} catch (e) {
|
|
2664
|
+
if (e instanceof LinkCheckFailedError) {
|
|
2665
|
+
log.error("link n\u1ED9i b\u1ED9 ch\u1EBFt:\n" + e.report);
|
|
2666
|
+
process.exitCode = 1;
|
|
2667
|
+
return;
|
|
2668
|
+
}
|
|
2669
|
+
throw e;
|
|
2670
|
+
}
|
|
2671
|
+
});
|
|
2672
|
+
cli.command("preview", "preview built site").option("--base <base>", "public base path (vd /docs/)").option("--out-dir <dir>", "output directory").option("--port <port>", "preview server port").option("--host [host]", "expose ra network").option("--open", "m\u1EDF tr\xECnh duy\u1EC7t").action(async (opts) => {
|
|
2673
|
+
const root = process.cwd();
|
|
2674
|
+
const config = await loadConfig(root, { base: opts.base, outDir: opts.outDir });
|
|
2675
|
+
try {
|
|
2676
|
+
const server = await startPreview({
|
|
2677
|
+
root,
|
|
2678
|
+
base: config.base,
|
|
2679
|
+
outDir: path14.resolve(root, config.outDir),
|
|
2680
|
+
...opts.port !== void 0 ? { port: Number(opts.port) } : {},
|
|
2681
|
+
...opts.host !== void 0 ? { host: opts.host } : {},
|
|
2682
|
+
...opts.open !== void 0 ? { open: opts.open } : {}
|
|
2683
|
+
});
|
|
2684
|
+
server.printUrls();
|
|
2685
|
+
} catch (e) {
|
|
2686
|
+
if (e instanceof DommiefError) {
|
|
2687
|
+
console.error("[error] " + formatError(e));
|
|
2688
|
+
process.exitCode = 1;
|
|
2689
|
+
return;
|
|
2690
|
+
}
|
|
2691
|
+
throw e;
|
|
2692
|
+
}
|
|
2693
|
+
});
|
|
2694
|
+
cli.help();
|
|
2695
|
+
cli.parse();
|