seemore 1.10.9 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -2
- package/dist/cli/index.js +642 -212
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +24 -1
- package/package.json +1 -1
- package/src/app/entry.client.tsx +13 -0
- package/src/app/layout/Header.tsx +9 -1
- package/src/app/lib/auth.ts +31 -0
- package/src/app/lib/chunkReload.ts +23 -0
- package/src/app/mdx/Pdf.tsx +38 -2
- package/src/app/styles/globals.css +6 -4
- package/src/app/virtual.d.ts +5 -0
- package/src/shared/auth/crypto.ts +190 -0
- package/src/shared/auth/files.ts +37 -0
- package/src/shared/auth/lock.ts +152 -0
- package/src/shared/auth/store.ts +54 -0
- package/src/shared/auth/sw.ts +57 -0
- package/src/shared/auth/worker.ts +408 -0
package/dist/cli/index.js
CHANGED
|
@@ -19,43 +19,43 @@ __export(paths_exports, {
|
|
|
19
19
|
packageRoot: () => packageRoot,
|
|
20
20
|
resolveContentRoot: () => resolveContentRoot
|
|
21
21
|
});
|
|
22
|
-
import { existsSync
|
|
23
|
-
import { createHash
|
|
22
|
+
import { existsSync, readFileSync, realpathSync } from "fs";
|
|
23
|
+
import { createHash } from "crypto";
|
|
24
24
|
import { tmpdir } from "os";
|
|
25
|
-
import { dirname as
|
|
25
|
+
import { dirname as dirname2, join as join3, resolve } from "path";
|
|
26
26
|
import { fileURLToPath } from "url";
|
|
27
27
|
function packageRoot() {
|
|
28
|
-
let dir =
|
|
28
|
+
let dir = dirname2(fileURLToPath(import.meta.url));
|
|
29
29
|
for (let depth = 0; depth < 10; depth++) {
|
|
30
|
-
if (
|
|
31
|
-
const parent =
|
|
30
|
+
if (existsSync(join3(dir, "package.json"))) return dir;
|
|
31
|
+
const parent = dirname2(dir);
|
|
32
32
|
if (parent === dir) break;
|
|
33
33
|
dir = parent;
|
|
34
34
|
}
|
|
35
35
|
throw new Error("seemore: could not locate its own package root.");
|
|
36
36
|
}
|
|
37
37
|
function appRoot() {
|
|
38
|
-
return
|
|
38
|
+
return join3(packageRoot(), "src", "app");
|
|
39
39
|
}
|
|
40
40
|
function packageDirOf(name, fromFile) {
|
|
41
|
-
let dir =
|
|
41
|
+
let dir = dirname2(fromFile);
|
|
42
42
|
for (let depth = 0; depth < 10; depth++) {
|
|
43
|
-
const manifest =
|
|
44
|
-
if (
|
|
43
|
+
const manifest = join3(dir, "package.json");
|
|
44
|
+
if (existsSync(manifest) && JSON.parse(readFileSync(manifest, "utf8")).name === name) {
|
|
45
45
|
return dir;
|
|
46
46
|
}
|
|
47
|
-
const parent =
|
|
47
|
+
const parent = dirname2(dir);
|
|
48
48
|
if (parent === dir) break;
|
|
49
49
|
dir = parent;
|
|
50
50
|
}
|
|
51
51
|
throw new Error(`seemore: could not locate the "${name}" package directory.`);
|
|
52
52
|
}
|
|
53
53
|
function cacheDir(contentRoot) {
|
|
54
|
-
const key =
|
|
55
|
-
return
|
|
54
|
+
const key = createHash("sha256").update(resolve(contentRoot)).digest("hex").slice(0, 12);
|
|
55
|
+
return join3(tmpdir(), "seemore", key);
|
|
56
56
|
}
|
|
57
57
|
function resolveContentRoot(cwd, explicit) {
|
|
58
|
-
return explicit !== void 0 ? canonicalise(
|
|
58
|
+
return explicit !== void 0 ? canonicalise(resolve(cwd, explicit)) : canonicalise(cwd);
|
|
59
59
|
}
|
|
60
60
|
function canonicalise(dir) {
|
|
61
61
|
try {
|
|
@@ -75,18 +75,102 @@ import { parseArgs } from "util";
|
|
|
75
75
|
import pc5 from "picocolors";
|
|
76
76
|
|
|
77
77
|
// src/cli/build.ts
|
|
78
|
-
import { mkdirSync as mkdirSync3, mkdtempSync, readFileSync as
|
|
79
|
-
import { createRequire as
|
|
80
|
-
import { tmpdir as
|
|
81
|
-
import { isAbsolute as isAbsolute2, join as
|
|
78
|
+
import { mkdirSync as mkdirSync3, mkdtempSync, readFileSync as readFileSync7, rmSync, writeFileSync as writeFileSync7 } from "fs";
|
|
79
|
+
import { createRequire as createRequire4 } from "module";
|
|
80
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
81
|
+
import { isAbsolute as isAbsolute2, join as join12, relative as relative2, resolve as resolve5 } from "path";
|
|
82
82
|
import pc2 from "picocolors";
|
|
83
83
|
import { build as viteBuild } from "vite";
|
|
84
84
|
|
|
85
|
-
// src/node/
|
|
86
|
-
import {
|
|
87
|
-
import {
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
// src/node/auth/build.ts
|
|
86
|
+
import { writeFileSync as writeFileSync4 } from "fs";
|
|
87
|
+
import { join as join7 } from "path";
|
|
88
|
+
|
|
89
|
+
// src/shared/auth/crypto.ts
|
|
90
|
+
var KDF_ITERATIONS = 6e5;
|
|
91
|
+
var MAGIC = new Uint8Array([83, 77, 80, 49]);
|
|
92
|
+
var IV_BYTES = 12;
|
|
93
|
+
var HEADER_BYTES = MAGIC.length + IV_BYTES;
|
|
94
|
+
var SALT_PREFIX = "seemore-auth-v1\0";
|
|
95
|
+
var encoder = new TextEncoder();
|
|
96
|
+
function subtle() {
|
|
97
|
+
return globalThis.crypto.subtle;
|
|
98
|
+
}
|
|
99
|
+
function normalisePassword(password) {
|
|
100
|
+
return password.normalize("NFC");
|
|
101
|
+
}
|
|
102
|
+
async function deriveSalt(id) {
|
|
103
|
+
return new Uint8Array(await subtle().digest("SHA-256", encoder.encode(SALT_PREFIX + id)));
|
|
104
|
+
}
|
|
105
|
+
async function deriveKek(password, salt, iterations = KDF_ITERATIONS) {
|
|
106
|
+
const material = await subtle().importKey("raw", encoder.encode(normalisePassword(password)), "PBKDF2", false, [
|
|
107
|
+
"deriveKey"
|
|
108
|
+
]);
|
|
109
|
+
return await subtle().deriveKey(
|
|
110
|
+
{ name: "PBKDF2", hash: "SHA-256", salt, iterations },
|
|
111
|
+
material,
|
|
112
|
+
{ name: "AES-KW", length: 256 },
|
|
113
|
+
false,
|
|
114
|
+
["wrapKey", "unwrapKey"]
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
async function generateContentKey() {
|
|
118
|
+
return await subtle().generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]);
|
|
119
|
+
}
|
|
120
|
+
async function wrapContentKey(contentKey, kek) {
|
|
121
|
+
return new Uint8Array(await subtle().wrapKey("raw", contentKey, kek, "AES-KW"));
|
|
122
|
+
}
|
|
123
|
+
async function encryptFile(key, path, plaintext) {
|
|
124
|
+
const iv = globalThis.crypto.getRandomValues(new Uint8Array(IV_BYTES));
|
|
125
|
+
const sealed = await subtle().encrypt(
|
|
126
|
+
{ name: "AES-GCM", iv, additionalData: encoder.encode(path), tagLength: 128 },
|
|
127
|
+
key,
|
|
128
|
+
plaintext
|
|
129
|
+
);
|
|
130
|
+
const out = new Uint8Array(HEADER_BYTES + sealed.byteLength);
|
|
131
|
+
out.set(MAGIC, 0);
|
|
132
|
+
out.set(iv, MAGIC.length);
|
|
133
|
+
out.set(new Uint8Array(sealed), HEADER_BYTES);
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
function isEncrypted(bytes) {
|
|
137
|
+
return bytes.length > HEADER_BYTES && MAGIC.every((byte, index) => bytes[index] === byte);
|
|
138
|
+
}
|
|
139
|
+
async function createManifest(options) {
|
|
140
|
+
const iterations = options.iterations ?? KDF_ITERATIONS;
|
|
141
|
+
const salt = await deriveSalt(options.id);
|
|
142
|
+
const kek = await deriveKek(options.password, salt, iterations);
|
|
143
|
+
const contentKey = await generateContentKey();
|
|
144
|
+
return {
|
|
145
|
+
contentKey,
|
|
146
|
+
manifest: {
|
|
147
|
+
v: 1,
|
|
148
|
+
kdf: { name: "PBKDF2", hash: "SHA-256", iterations, salt: encodeBase64(salt) },
|
|
149
|
+
key: encodeBase64(await wrapContentKey(contentKey, kek)),
|
|
150
|
+
remember: options.remember
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function encodeBase64(bytes) {
|
|
155
|
+
let binary = "";
|
|
156
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
157
|
+
return btoa(binary);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/shared/auth/files.ts
|
|
161
|
+
var PUBLIC_FILES = [
|
|
162
|
+
"index.html",
|
|
163
|
+
"404.html",
|
|
164
|
+
"200.html",
|
|
165
|
+
"_redirects",
|
|
166
|
+
".nojekyll",
|
|
167
|
+
"_headers",
|
|
168
|
+
"sw.js",
|
|
169
|
+
"auth.json"
|
|
170
|
+
];
|
|
171
|
+
var MANIFEST_FILE = "auth.json";
|
|
172
|
+
var WORKER_FILE = "sw.js";
|
|
173
|
+
var APP_FILE = "app.html";
|
|
90
174
|
|
|
91
175
|
// src/shared/base.ts
|
|
92
176
|
var EXTERNAL = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
|
|
@@ -114,6 +198,386 @@ function withBase(base, href) {
|
|
|
114
198
|
return b + href.replace(/^\/+/, "");
|
|
115
199
|
}
|
|
116
200
|
|
|
201
|
+
// src/node/content/slug.ts
|
|
202
|
+
import { slug as slugify } from "github-slugger";
|
|
203
|
+
var INDEX_NAMES = /* @__PURE__ */ new Set(["index", "readme"]);
|
|
204
|
+
var CONTENT_EXT = /\.mdx?$/i;
|
|
205
|
+
function toPosix(file) {
|
|
206
|
+
return file.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
207
|
+
}
|
|
208
|
+
function slugifySegment(segment) {
|
|
209
|
+
const slugged = slugify(segment);
|
|
210
|
+
if (slugged !== "") return slugged;
|
|
211
|
+
const fallback = segment.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
|
|
212
|
+
return fallback === "" ? "untitled" : fallback;
|
|
213
|
+
}
|
|
214
|
+
function toRoute(file) {
|
|
215
|
+
const posix = toPosix(file);
|
|
216
|
+
const segments = posix.split("/");
|
|
217
|
+
const basename3 = segments.pop() ?? "";
|
|
218
|
+
const stem = basename3.replace(CONTENT_EXT, "");
|
|
219
|
+
const isIndex = INDEX_NAMES.has(stem.toLowerCase());
|
|
220
|
+
const slugs = segments.map(slugifySegment);
|
|
221
|
+
if (!isIndex) slugs.push(slugifySegment(stem));
|
|
222
|
+
return {
|
|
223
|
+
file: posix,
|
|
224
|
+
url: slugs.length === 0 ? "/" : `/${slugs.join("/")}`,
|
|
225
|
+
slugs,
|
|
226
|
+
output: [...slugs, "index.html"].join("/"),
|
|
227
|
+
isIndex
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function resolveRoutes(files) {
|
|
231
|
+
const sorted = [...files].map(toPosix).sort();
|
|
232
|
+
const byUrl = /* @__PURE__ */ new Map();
|
|
233
|
+
const warnings = [];
|
|
234
|
+
const errors = [];
|
|
235
|
+
for (const file of sorted) {
|
|
236
|
+
const route = toRoute(file);
|
|
237
|
+
const bucket = byUrl.get(route.url);
|
|
238
|
+
if (bucket) bucket.push(route);
|
|
239
|
+
else byUrl.set(route.url, [route]);
|
|
240
|
+
}
|
|
241
|
+
const routes = [];
|
|
242
|
+
for (const [url, candidates] of [...byUrl.entries()].sort(([a], [b]) => a < b ? -1 : 1)) {
|
|
243
|
+
if (candidates.length === 1) {
|
|
244
|
+
routes.push(candidates[0]);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const indexes = candidates.filter((c) => c.isIndex);
|
|
248
|
+
if (indexes.length === candidates.length) {
|
|
249
|
+
const winner = indexes.find((c) => c.file.split("/").pop()?.toLowerCase().startsWith("index")) ?? indexes[0];
|
|
250
|
+
const losers = indexes.filter((c) => c !== winner);
|
|
251
|
+
warnings.push(
|
|
252
|
+
`${url} has more than one index file: using ${winner.file}, ignoring ${losers.map((l) => l.file).join(", ")}.`
|
|
253
|
+
);
|
|
254
|
+
routes.push(winner);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
errors.push(
|
|
258
|
+
`Duplicate route ${url} produced by ${candidates.length} files:
|
|
259
|
+
` + candidates.map((c) => ` - ${c.file}`).join("\n") + `
|
|
260
|
+
Rename one of them, or exclude it with \`exclude\` in seemore.config.ts.`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
return { routes, errors, warnings };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/node/prerender/deploy.ts
|
|
267
|
+
import { writeFileSync } from "fs";
|
|
268
|
+
import { join } from "path";
|
|
269
|
+
function writeDeployArtifacts(outDir, base, shell, options = {}) {
|
|
270
|
+
const prefix = base === "/" ? "" : base.replace(/\/+$/, "");
|
|
271
|
+
writeFileSync(join(outDir, "_redirects"), `${prefix}/* ${prefix}/index.html 200
|
|
272
|
+
`, "utf8");
|
|
273
|
+
writeFileSync(join(outDir, "200.html"), shell, "utf8");
|
|
274
|
+
writeFileSync(join(outDir, ".nojekyll"), "", "utf8");
|
|
275
|
+
if (options.auth === true) {
|
|
276
|
+
writeFileSync(join(outDir, "_headers"), "/*\n X-Robots-Tag: noindex, nofollow\n", "utf8");
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/node/prerender/emit.ts
|
|
281
|
+
import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
282
|
+
import { dirname, join as join2 } from "path";
|
|
283
|
+
function outputPathFor(url) {
|
|
284
|
+
const clean = url.replace(/^\/+|\/+$/g, "");
|
|
285
|
+
return clean === "" ? "index.html" : join2(clean, "index.html");
|
|
286
|
+
}
|
|
287
|
+
function writeHtml(outDir, relativePath, html) {
|
|
288
|
+
const target = join2(outDir, relativePath);
|
|
289
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
290
|
+
writeFileSync2(target, html, "utf8");
|
|
291
|
+
}
|
|
292
|
+
function applyTemplate(template, { html, head }) {
|
|
293
|
+
return template.replace("<!--seemore-head-->", () => head).replace("<!--seemore-app-->", () => html);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/node/auth/bundle.ts
|
|
297
|
+
init_paths();
|
|
298
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
299
|
+
import { join as join4 } from "path";
|
|
300
|
+
import { build } from "vite";
|
|
301
|
+
var bundles = /* @__PURE__ */ new Map();
|
|
302
|
+
function bundleAuthScript(entry) {
|
|
303
|
+
let bundle = bundles.get(entry);
|
|
304
|
+
if (bundle === void 0) {
|
|
305
|
+
bundle = compile(entry);
|
|
306
|
+
bundles.set(entry, bundle);
|
|
307
|
+
bundle.catch(() => bundles.delete(entry));
|
|
308
|
+
}
|
|
309
|
+
return bundle;
|
|
310
|
+
}
|
|
311
|
+
async function compile(entry) {
|
|
312
|
+
const result = await build({
|
|
313
|
+
root: packageRoot(),
|
|
314
|
+
configFile: false,
|
|
315
|
+
envDir: false,
|
|
316
|
+
publicDir: false,
|
|
317
|
+
clearScreen: false,
|
|
318
|
+
logLevel: "warn",
|
|
319
|
+
cacheDir: join4(tmpdir2(), "seemore", "auth-scripts"),
|
|
320
|
+
build: {
|
|
321
|
+
write: false,
|
|
322
|
+
minify: true,
|
|
323
|
+
target: "es2020",
|
|
324
|
+
copyPublicDir: false,
|
|
325
|
+
reportCompressedSize: false,
|
|
326
|
+
lib: {
|
|
327
|
+
entry: join4(packageRoot(), "src", "shared", "auth", `${entry}.ts`),
|
|
328
|
+
formats: ["iife"],
|
|
329
|
+
name: "seemoreAuth",
|
|
330
|
+
fileName: () => `${entry}.js`
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
const outputs = Array.isArray(result) ? result : [result];
|
|
335
|
+
const chunk = outputs.flatMap((output) => output.output ?? []).find((file) => file.type === "chunk");
|
|
336
|
+
if (chunk?.code === void 0) throw new Error(`seemore: bundling the ${entry} script produced no code.`);
|
|
337
|
+
return chunk.code;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// src/node/auth/output.ts
|
|
341
|
+
import { readdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
342
|
+
import { join as join5 } from "path";
|
|
343
|
+
function publicFiles(favicon) {
|
|
344
|
+
return favicon === void 0 ? [...PUBLIC_FILES] : [...PUBLIC_FILES, toPosix(favicon)];
|
|
345
|
+
}
|
|
346
|
+
async function encryptOutput(outDir, contentKey, open) {
|
|
347
|
+
const isPublic = new Set(open);
|
|
348
|
+
let encrypted = 0;
|
|
349
|
+
for (const path of listFiles(outDir)) {
|
|
350
|
+
if (isPublic.has(path)) continue;
|
|
351
|
+
const file = join5(outDir, path);
|
|
352
|
+
writeFileSync3(file, await encryptFile(contentKey, path, new Uint8Array(readFileSync2(file))));
|
|
353
|
+
encrypted++;
|
|
354
|
+
}
|
|
355
|
+
return encrypted;
|
|
356
|
+
}
|
|
357
|
+
function assertOutputEncrypted(outDir, open) {
|
|
358
|
+
const isPublic = new Set(open);
|
|
359
|
+
const plaintext = listFiles(outDir).filter((path) => !isPublic.has(path) && !isEncrypted(readFileSync2(join5(outDir, path))));
|
|
360
|
+
if (plaintext.length === 0) return;
|
|
361
|
+
throw new Error(
|
|
362
|
+
`seemore stopped the build: ${plaintext.length === 1 ? "this file was" : "these files were"} about to ship unencrypted, and \`auth\` only allows its public files in plain text:
|
|
363
|
+
${plaintext.map((path) => ` - ${path}`).join("\n")}
|
|
364
|
+
|
|
365
|
+
This is a bug in seemore. Please report it at https://github.com/arifszn/seemore/issues.`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
function listFiles(dir, prefix = "") {
|
|
369
|
+
const files = [];
|
|
370
|
+
for (const entry of readdirSync(join5(dir, prefix), { withFileTypes: true })) {
|
|
371
|
+
const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
372
|
+
if (entry.isDirectory()) files.push(...listFiles(dir, path));
|
|
373
|
+
else files.push(path);
|
|
374
|
+
}
|
|
375
|
+
return files.sort();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/node/auth/shell.ts
|
|
379
|
+
function renderLockShell(options) {
|
|
380
|
+
const title = escapeHtml(options.title);
|
|
381
|
+
const icon = options.favicon === void 0 ? void 0 : escapeHtml(options.favicon);
|
|
382
|
+
const config = JSON.stringify({ base: options.base }).replace(/</g, "\\u003c");
|
|
383
|
+
return [
|
|
384
|
+
"<!doctype html>",
|
|
385
|
+
'<html lang="en">',
|
|
386
|
+
"<head>",
|
|
387
|
+
'<meta charset="utf-8" />',
|
|
388
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
389
|
+
// Search engines: nothing past this screen is readable to them anyway.
|
|
390
|
+
'<meta name="robots" content="noindex, nofollow" />',
|
|
391
|
+
`<title>${title}</title>`,
|
|
392
|
+
options.description === void 0 ? "" : `<meta name="description" content="${escapeHtml(options.description)}" />`,
|
|
393
|
+
icon === void 0 ? "" : `<link rel="icon" href="${icon}" />`,
|
|
394
|
+
`<style>${styles(options.colours)}</style>`,
|
|
395
|
+
`<script>${inlineScript(THEME_SCRIPT)}</script>`,
|
|
396
|
+
"</head>",
|
|
397
|
+
"<body>",
|
|
398
|
+
'<main class="lock">',
|
|
399
|
+
icon === void 0 ? "" : `<img class="lock-icon" src="${icon}" alt="" width="44" height="44" />`,
|
|
400
|
+
`<h1>${title}</h1>`,
|
|
401
|
+
options.description === void 0 ? "" : `<p class="lock-desc">${escapeHtml(options.description)}</p>`,
|
|
402
|
+
'<form id="seemore-auth-form" hidden>',
|
|
403
|
+
'<label class="lock-label" for="seemore-auth-password">Password</label>',
|
|
404
|
+
'<input id="seemore-auth-password" name="password" type="password" placeholder="Password" autocomplete="current-password" required />',
|
|
405
|
+
'<button id="seemore-auth-submit" type="submit">Unlock</button>',
|
|
406
|
+
'<p id="seemore-auth-status" class="lock-status" role="alert"></p>',
|
|
407
|
+
"</form>",
|
|
408
|
+
`<p id="seemore-auth-unsupported" class="lock-note" hidden>This browser can't unlock this site \u2014 it needs service workers. Try a regular (not private) window or another browser.</p>`,
|
|
409
|
+
'<noscript><p class="lock-note">This site needs JavaScript to unlock.</p></noscript>',
|
|
410
|
+
"</main>",
|
|
411
|
+
`<script type="application/json" id="seemore-auth-config">${config}</script>`,
|
|
412
|
+
`<script>${inlineScript(options.script)}</script>`,
|
|
413
|
+
"</body>",
|
|
414
|
+
"</html>",
|
|
415
|
+
""
|
|
416
|
+
].filter((line) => line !== "").join("\n");
|
|
417
|
+
}
|
|
418
|
+
var THEME_SCRIPT = "try{var t=localStorage.getItem('theme');if(t==='dark'||(t!=='light'&&matchMedia('(prefers-color-scheme: dark)').matches))document.documentElement.classList.add('dark')}catch(e){}";
|
|
419
|
+
function styles(colours) {
|
|
420
|
+
const vars = (tokens) => Object.entries(tokens).map(([name, value]) => `--${name}:${value}`).join(";");
|
|
421
|
+
return [
|
|
422
|
+
`:root{color-scheme:light;${vars(colours.light)}}`,
|
|
423
|
+
`:root.dark{color-scheme:dark;${vars(colours.dark)}}`,
|
|
424
|
+
"*{box-sizing:border-box}",
|
|
425
|
+
"[hidden]{display:none!important}",
|
|
426
|
+
"html,body{min-height:100%}",
|
|
427
|
+
'body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px;background:var(--background);color:var(--foreground);font:15px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}',
|
|
428
|
+
".lock{width:100%;max-width:360px;padding:32px;border:1px solid color-mix(in srgb,var(--foreground) 12%,transparent);border-radius:14px;background:color-mix(in srgb,var(--background) 94%,var(--foreground));box-shadow:0 12px 32px color-mix(in srgb,var(--foreground) 8%,transparent);text-align:center;animation:lock-in .24s ease-out both}",
|
|
429
|
+
":root:not(.dark) .lock{background:color-mix(in srgb,var(--background) 92%,white)}",
|
|
430
|
+
".lock-icon{width:40px;height:40px;margin-bottom:20px}",
|
|
431
|
+
"h1{margin:0;font-size:22px;line-height:1.2;font-weight:650;letter-spacing:-.015em}",
|
|
432
|
+
".lock-site{margin:7px 0 0;color:var(--muted-foreground);font-size:14px}",
|
|
433
|
+
".lock-desc{margin:6px 0 0;font-size:14px;color:var(--muted-foreground)}",
|
|
434
|
+
"form{width:100%;margin-top:24px;text-align:left}",
|
|
435
|
+
".lock-label{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}",
|
|
436
|
+
"input{display:block;width:100%;min-width:0;font:inherit;padding:10px 12px;border-radius:8px;border:1px solid var(--border);background:var(--background);color:inherit}",
|
|
437
|
+
"input:focus{outline:2px solid var(--ring);outline-offset:2px}",
|
|
438
|
+
"button{display:block;width:100%;margin-top:18px;font:inherit;font-weight:600;padding:10px 16px;border:0;border-radius:8px;background:var(--primary);color:var(--primary-foreground);cursor:pointer}",
|
|
439
|
+
"button:disabled{opacity:.6;cursor:progress}",
|
|
440
|
+
".lock-status{margin:10px 0 0;min-height:1.5em;font-size:14px;color:var(--error)}",
|
|
441
|
+
".lock-note{margin:14px 0 0;font-size:14px;color:var(--muted-foreground);max-width:34ch}",
|
|
442
|
+
".lock-shake{animation:lock-shake .3s ease-in-out}",
|
|
443
|
+
"@keyframes lock-in{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}",
|
|
444
|
+
"@keyframes lock-shake{20%,60%{transform:translateX(-5px)}40%,80%{transform:translateX(5px)}}",
|
|
445
|
+
"@media (max-width:400px){.lock{padding:24px}}",
|
|
446
|
+
"@media (prefers-reduced-motion:reduce){.lock,.lock-shake{animation:none}}"
|
|
447
|
+
].join("\n");
|
|
448
|
+
}
|
|
449
|
+
function inlineScript(code) {
|
|
450
|
+
return code.replace(/<\/(script)/gi, "<\\/$1");
|
|
451
|
+
}
|
|
452
|
+
function escapeHtml(value) {
|
|
453
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// src/node/auth/theme.ts
|
|
457
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
458
|
+
import { createRequire } from "module";
|
|
459
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
460
|
+
var TOKENS = [
|
|
461
|
+
"background",
|
|
462
|
+
"foreground",
|
|
463
|
+
"muted-foreground",
|
|
464
|
+
"card",
|
|
465
|
+
"border",
|
|
466
|
+
"primary",
|
|
467
|
+
"primary-foreground",
|
|
468
|
+
"ring",
|
|
469
|
+
"error"
|
|
470
|
+
];
|
|
471
|
+
var FALLBACK = {
|
|
472
|
+
background: "hsl(0, 0%, 96%)",
|
|
473
|
+
foreground: "hsl(0, 0%, 3.9%)",
|
|
474
|
+
"muted-foreground": "hsl(0, 0%, 45.1%)",
|
|
475
|
+
card: "hsl(0, 0%, 94.7%)",
|
|
476
|
+
border: "hsla(0, 0%, 80%, 50%)",
|
|
477
|
+
primary: "hsl(0, 0%, 9%)",
|
|
478
|
+
"primary-foreground": "hsl(0, 0%, 98%)",
|
|
479
|
+
ring: "hsl(0, 0%, 63.9%)",
|
|
480
|
+
error: "oklch(63.7% 0.237 25.331)"
|
|
481
|
+
};
|
|
482
|
+
var require_ = createRequire(import.meta.url);
|
|
483
|
+
function shellColours(theme) {
|
|
484
|
+
const light = {};
|
|
485
|
+
const dark = {};
|
|
486
|
+
let dir;
|
|
487
|
+
try {
|
|
488
|
+
dir = join6(dirname3(require_.resolve("fumadocs-ui/package.json")), "css");
|
|
489
|
+
} catch {
|
|
490
|
+
dir = void 0;
|
|
491
|
+
}
|
|
492
|
+
for (const file of dir === void 0 ? [] : [join6(dir, "lib", "default-colors.css"), join6(dir, `${theme}.css`)]) {
|
|
493
|
+
let css;
|
|
494
|
+
try {
|
|
495
|
+
css = readFileSync3(file, "utf8").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
496
|
+
} catch {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
for (const [, prelude = "", block = ""] of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
|
|
500
|
+
const selectors = (prelude.split(";").pop() ?? "").split(",").map((selector) => selector.trim());
|
|
501
|
+
const targets = [];
|
|
502
|
+
if (selectors.some((selector) => selector.startsWith("@theme") || selector === ":root")) targets.push(light);
|
|
503
|
+
if (selectors.includes(".dark")) targets.push(dark);
|
|
504
|
+
for (const [, name = "", value = ""] of block.matchAll(/--color-fd-([\w-]+)\s*:\s*([^;]+);/g)) {
|
|
505
|
+
if (value.includes("var(")) continue;
|
|
506
|
+
for (const target of targets) target[name] = value.trim();
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
const pick = (tokens, fallback) => Object.fromEntries(TOKENS.map((token) => [token, tokens[token] ?? fallback[token] ?? FALLBACK[token]]));
|
|
511
|
+
return { light: pick(light, {}), dark: pick(dark, light) };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/node/auth/build.ts
|
|
515
|
+
async function sealSite(ctx, outDir, template, password) {
|
|
516
|
+
const { config } = ctx;
|
|
517
|
+
const auth = config.auth;
|
|
518
|
+
if (auth === void 0) throw new Error("seemore: sealSite needs `auth` in the config.");
|
|
519
|
+
writeHtml(outDir, APP_FILE, applyTemplate(template, { html: "", head: appHead(config, template) }));
|
|
520
|
+
const shell = renderLockShell({
|
|
521
|
+
title: config.title,
|
|
522
|
+
description: config.description,
|
|
523
|
+
base: config.base,
|
|
524
|
+
favicon: faviconHref(config, template),
|
|
525
|
+
colours: shellColours(config.theme),
|
|
526
|
+
script: await bundleAuthScript("lock")
|
|
527
|
+
});
|
|
528
|
+
writeHtml(outDir, "index.html", shell);
|
|
529
|
+
writeHtml(outDir, "404.html", shell);
|
|
530
|
+
writeDeployArtifacts(outDir, config.base, shell, { auth: true });
|
|
531
|
+
const { manifest, contentKey } = await createManifest({ password, id: auth.id, remember: auth.remember });
|
|
532
|
+
writeFileSync4(join7(outDir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
|
|
533
|
+
`, "utf8");
|
|
534
|
+
const open = publicFiles(config.favicon);
|
|
535
|
+
writeFileSync4(
|
|
536
|
+
join7(outDir, WORKER_FILE),
|
|
537
|
+
`var SEEMORE_AUTH=${JSON.stringify({ publicFiles: open })};
|
|
538
|
+
${await bundleAuthScript("sw")}`,
|
|
539
|
+
"utf8"
|
|
540
|
+
);
|
|
541
|
+
const encrypted = await encryptOutput(outDir, contentKey, open);
|
|
542
|
+
assertOutputEncrypted(outDir, open);
|
|
543
|
+
return encrypted;
|
|
544
|
+
}
|
|
545
|
+
function faviconHref(config, template) {
|
|
546
|
+
if (config.favicon !== void 0) return withBase(config.base, `/${toPosix(config.favicon)}`);
|
|
547
|
+
return /<link rel="icon" href="(data:[^"]+)"/.exec(template)?.[1];
|
|
548
|
+
}
|
|
549
|
+
function appHead(config, template) {
|
|
550
|
+
const tags = [`<title>${escapeHtml(config.title)}</title>`, '<meta name="robots" content="noindex, nofollow" />'];
|
|
551
|
+
if (config.description !== void 0) {
|
|
552
|
+
tags.push(`<meta name="description" content="${escapeHtml(config.description)}" />`);
|
|
553
|
+
}
|
|
554
|
+
const favicon = faviconHref(config, template);
|
|
555
|
+
if (config.favicon !== void 0 && favicon !== void 0) tags.push(`<link rel="icon" href="${escapeHtml(favicon)}" />`);
|
|
556
|
+
return tags.join("\n ");
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// src/node/auth/password.ts
|
|
560
|
+
var PASSWORD_ENV = "SEEMORE_PASSWORD";
|
|
561
|
+
function readPassword(command, env = process.env) {
|
|
562
|
+
const raw = env[PASSWORD_ENV];
|
|
563
|
+
if (raw === void 0 || raw === "") {
|
|
564
|
+
throw new Error(
|
|
565
|
+
`\`auth\` is on, but ${PASSWORD_ENV} is not set. Pass the password through the environment \u2014 in CI, from a secret:
|
|
566
|
+
|
|
567
|
+
${PASSWORD_ENV}='a-long-passphrase' ${command}
|
|
568
|
+
|
|
569
|
+
Never put it in seemore.config.ts.`
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
return normalisePassword(raw);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/node/config/load.ts
|
|
576
|
+
import { existsSync as existsSync2 } from "fs";
|
|
577
|
+
import { dirname as dirname4, isAbsolute, resolve as resolve2 } from "path";
|
|
578
|
+
import { createJiti } from "jiti";
|
|
579
|
+
import "zod";
|
|
580
|
+
|
|
117
581
|
// src/shared/types.ts
|
|
118
582
|
var FEATURES = [
|
|
119
583
|
"navigation.instant.prefetch",
|
|
@@ -251,6 +715,14 @@ var searchSchema = z.union([
|
|
|
251
715
|
})
|
|
252
716
|
]);
|
|
253
717
|
var pageActionsSchema = z.array(z.enum(ACTION_IDS)).default(["copy-markdown", "export-html"]);
|
|
718
|
+
var REMEMBER_FORMS = "expected a number of hours or days, like '12h' or '7d'.";
|
|
719
|
+
var authSchema = z.preprocess(
|
|
720
|
+
(input) => input === true ? {} : input === false ? void 0 : input,
|
|
721
|
+
z.strictObject({
|
|
722
|
+
id: z.string().trim().min(1, { error: "must not be empty. Leave `id` out to use the site title, or give the site a stable name." }).optional(),
|
|
723
|
+
remember: z.string({ error: REMEMBER_FORMS }).regex(/^[1-9]\d*[hd]$/, { error: REMEMBER_FORMS }).optional()
|
|
724
|
+
}).optional()
|
|
725
|
+
);
|
|
254
726
|
var configSchema = z.object({
|
|
255
727
|
/**
|
|
256
728
|
* Optional here, but required whenever a config file exists — load.ts enforces that,
|
|
@@ -277,7 +749,8 @@ var configSchema = z.object({
|
|
|
277
749
|
}).optional(),
|
|
278
750
|
search: searchSchema.default("static"),
|
|
279
751
|
pageActions: pageActionsSchema,
|
|
280
|
-
exclude: z.array(z.string()).default([])
|
|
752
|
+
exclude: z.array(z.string()).default([]),
|
|
753
|
+
auth: authSchema
|
|
281
754
|
});
|
|
282
755
|
|
|
283
756
|
// src/node/config/load.ts
|
|
@@ -289,9 +762,11 @@ function resolveConfig(input, options) {
|
|
|
289
762
|
// Nothing to link to without an edit base, so the flag follows the option.
|
|
290
763
|
"content.action.edit": parsed.editLink !== void 0
|
|
291
764
|
});
|
|
765
|
+
const title = parsed.title ?? "Docs";
|
|
766
|
+
const auth = parsed.auth === void 0 ? void 0 : { id: parsed.auth.id ?? title, remember: rememberSeconds(parsed.auth.remember) };
|
|
767
|
+
if (auth !== void 0) assertAuthCompatible(features, search);
|
|
292
768
|
return {
|
|
293
|
-
|
|
294
|
-
title: parsed.title ?? "Docs",
|
|
769
|
+
title,
|
|
295
770
|
description: parsed.description,
|
|
296
771
|
favicon: parsed.favicon,
|
|
297
772
|
base: normaliseBase(parsed.base),
|
|
@@ -304,10 +779,32 @@ function resolveConfig(input, options) {
|
|
|
304
779
|
search,
|
|
305
780
|
pageActions: parsed.pageActions,
|
|
306
781
|
exclude: parsed.exclude,
|
|
782
|
+
auth,
|
|
307
783
|
root: options.root,
|
|
308
784
|
configFile: options.configFile
|
|
309
785
|
};
|
|
310
786
|
}
|
|
787
|
+
var DAY_SECONDS = 86400;
|
|
788
|
+
function rememberSeconds(remember) {
|
|
789
|
+
if (remember === void 0) return DAY_SECONDS;
|
|
790
|
+
const amount = Number(remember.slice(0, -1));
|
|
791
|
+
return remember.endsWith("h") ? amount * 3600 : amount * DAY_SECONDS;
|
|
792
|
+
}
|
|
793
|
+
function assertAuthCompatible(features, search) {
|
|
794
|
+
const problems = [];
|
|
795
|
+
if (features["social.cards"]) {
|
|
796
|
+
problems.push("`auth` cannot be combined with `social.cards`: the cards would publish page titles as public images. Remove one of them.");
|
|
797
|
+
}
|
|
798
|
+
if (search.provider !== "static") {
|
|
799
|
+
problems.push(
|
|
800
|
+
`\`auth\` cannot be combined with \`search.provider: '${search.provider}'\`: the search index would go to a third party in plaintext. Use \`search: 'static'\`, or remove \`auth\`.`
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
if (problems.length > 0) {
|
|
804
|
+
throw new Error(`Incompatible options in seemore config:
|
|
805
|
+
${problems.map((problem) => ` - ${problem}`).join("\n")}`);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
311
808
|
async function loadConfig(options) {
|
|
312
809
|
const file = findConfigFile(options);
|
|
313
810
|
if (file === void 0) {
|
|
@@ -327,21 +824,21 @@ ${error instanceof Error ? error.message : String(error)}`, {
|
|
|
327
824
|
throw new Error(`${file} must export a config object as its default export, got ${typeof loaded}.`);
|
|
328
825
|
}
|
|
329
826
|
return {
|
|
330
|
-
config: resolveConfig(loaded, { root:
|
|
827
|
+
config: resolveConfig(loaded, { root: dirname4(file), configFile: file }),
|
|
331
828
|
file
|
|
332
829
|
};
|
|
333
830
|
}
|
|
334
831
|
function findConfigFile({ root, configPath }) {
|
|
335
832
|
if (configPath !== void 0) {
|
|
336
833
|
const absolute = resolveFrom(root, configPath);
|
|
337
|
-
if (!
|
|
834
|
+
if (!existsSync2(absolute)) {
|
|
338
835
|
throw new Error(`Config file not found: ${absolute}`);
|
|
339
836
|
}
|
|
340
837
|
return absolute;
|
|
341
838
|
}
|
|
342
839
|
for (const name of CONFIG_NAMES) {
|
|
343
|
-
const candidate =
|
|
344
|
-
if (
|
|
840
|
+
const candidate = resolve2(root, name);
|
|
841
|
+
if (existsSync2(candidate)) return candidate;
|
|
345
842
|
}
|
|
346
843
|
return void 0;
|
|
347
844
|
}
|
|
@@ -349,7 +846,7 @@ function resolveConfigPath(options) {
|
|
|
349
846
|
return options.configPath === void 0 ? void 0 : resolveFrom(options.cwd, options.configPath);
|
|
350
847
|
}
|
|
351
848
|
function resolveFrom(root, path) {
|
|
352
|
-
return isAbsolute(path) ? path :
|
|
849
|
+
return isAbsolute(path) ? path : resolve2(root, path);
|
|
353
850
|
}
|
|
354
851
|
function parseOrThrow(input, file) {
|
|
355
852
|
const result = configSchema.safeParse(input);
|
|
@@ -383,73 +880,6 @@ function explain(issue) {
|
|
|
383
880
|
|
|
384
881
|
// src/node/content/links.ts
|
|
385
882
|
import { slug as slugify2 } from "github-slugger";
|
|
386
|
-
|
|
387
|
-
// src/node/content/slug.ts
|
|
388
|
-
import { slug as slugify } from "github-slugger";
|
|
389
|
-
var INDEX_NAMES = /* @__PURE__ */ new Set(["index", "readme"]);
|
|
390
|
-
var CONTENT_EXT = /\.mdx?$/i;
|
|
391
|
-
function toPosix(file) {
|
|
392
|
-
return file.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
393
|
-
}
|
|
394
|
-
function slugifySegment(segment) {
|
|
395
|
-
const slugged = slugify(segment);
|
|
396
|
-
if (slugged !== "") return slugged;
|
|
397
|
-
const fallback = segment.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
|
|
398
|
-
return fallback === "" ? "untitled" : fallback;
|
|
399
|
-
}
|
|
400
|
-
function toRoute(file) {
|
|
401
|
-
const posix = toPosix(file);
|
|
402
|
-
const segments = posix.split("/");
|
|
403
|
-
const basename3 = segments.pop() ?? "";
|
|
404
|
-
const stem = basename3.replace(CONTENT_EXT, "");
|
|
405
|
-
const isIndex = INDEX_NAMES.has(stem.toLowerCase());
|
|
406
|
-
const slugs = segments.map(slugifySegment);
|
|
407
|
-
if (!isIndex) slugs.push(slugifySegment(stem));
|
|
408
|
-
return {
|
|
409
|
-
file: posix,
|
|
410
|
-
url: slugs.length === 0 ? "/" : `/${slugs.join("/")}`,
|
|
411
|
-
slugs,
|
|
412
|
-
output: [...slugs, "index.html"].join("/"),
|
|
413
|
-
isIndex
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
function resolveRoutes(files) {
|
|
417
|
-
const sorted = [...files].map(toPosix).sort();
|
|
418
|
-
const byUrl = /* @__PURE__ */ new Map();
|
|
419
|
-
const warnings = [];
|
|
420
|
-
const errors = [];
|
|
421
|
-
for (const file of sorted) {
|
|
422
|
-
const route = toRoute(file);
|
|
423
|
-
const bucket = byUrl.get(route.url);
|
|
424
|
-
if (bucket) bucket.push(route);
|
|
425
|
-
else byUrl.set(route.url, [route]);
|
|
426
|
-
}
|
|
427
|
-
const routes = [];
|
|
428
|
-
for (const [url, candidates] of [...byUrl.entries()].sort(([a], [b]) => a < b ? -1 : 1)) {
|
|
429
|
-
if (candidates.length === 1) {
|
|
430
|
-
routes.push(candidates[0]);
|
|
431
|
-
continue;
|
|
432
|
-
}
|
|
433
|
-
const indexes = candidates.filter((c) => c.isIndex);
|
|
434
|
-
if (indexes.length === candidates.length) {
|
|
435
|
-
const winner = indexes.find((c) => c.file.split("/").pop()?.toLowerCase().startsWith("index")) ?? indexes[0];
|
|
436
|
-
const losers = indexes.filter((c) => c !== winner);
|
|
437
|
-
warnings.push(
|
|
438
|
-
`${url} has more than one index file: using ${winner.file}, ignoring ${losers.map((l) => l.file).join(", ")}.`
|
|
439
|
-
);
|
|
440
|
-
routes.push(winner);
|
|
441
|
-
continue;
|
|
442
|
-
}
|
|
443
|
-
errors.push(
|
|
444
|
-
`Duplicate route ${url} produced by ${candidates.length} files:
|
|
445
|
-
` + candidates.map((c) => ` - ${c.file}`).join("\n") + `
|
|
446
|
-
Rename one of them, or exclude it with \`exclude\` in seemore.config.ts.`
|
|
447
|
-
);
|
|
448
|
-
}
|
|
449
|
-
return { routes, errors, warnings };
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
// src/node/content/links.ts
|
|
453
883
|
var CONTENT_EXT2 = /\.mdx?$/i;
|
|
454
884
|
function createLinkResolver(pages, base) {
|
|
455
885
|
const byPath = /* @__PURE__ */ new Map();
|
|
@@ -543,9 +973,9 @@ import { dynamicLoader } from "fumadocs-core/source";
|
|
|
543
973
|
import { lucideIconsPlugin } from "fumadocs-core/source/plugins/lucide-icons";
|
|
544
974
|
|
|
545
975
|
// src/node/content/scan.ts
|
|
546
|
-
import { readFileSync } from "fs";
|
|
547
|
-
import { createHash } from "crypto";
|
|
548
|
-
import { basename, dirname as
|
|
976
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
977
|
+
import { createHash as createHash2 } from "crypto";
|
|
978
|
+
import { basename, dirname as dirname5, join as join8, resolve as resolve3 } from "path";
|
|
549
979
|
import { globSync } from "tinyglobby";
|
|
550
980
|
import { z as z4 } from "zod";
|
|
551
981
|
|
|
@@ -615,7 +1045,7 @@ var metaSchema = z4.object({
|
|
|
615
1045
|
description: z4.string().optional()
|
|
616
1046
|
}).loose();
|
|
617
1047
|
function scan(options) {
|
|
618
|
-
const contentRoot =
|
|
1048
|
+
const contentRoot = resolve3(options.contentRoot);
|
|
619
1049
|
const ignore = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
|
|
620
1050
|
const contentFiles = globSync(["**/*.md", "**/*.mdx"], {
|
|
621
1051
|
cwd: contentRoot,
|
|
@@ -627,13 +1057,13 @@ function scan(options) {
|
|
|
627
1057
|
const { routes, errors, warnings } = resolveRoutes(contentFiles);
|
|
628
1058
|
const pages = [];
|
|
629
1059
|
for (const route of routes) {
|
|
630
|
-
const absPath =
|
|
1060
|
+
const absPath = join8(contentRoot, route.file);
|
|
631
1061
|
let data;
|
|
632
1062
|
let version;
|
|
633
1063
|
try {
|
|
634
|
-
const text =
|
|
1064
|
+
const text = readFileSync4(absPath, "utf8");
|
|
635
1065
|
data = parseFrontmatter(text, route.file).data;
|
|
636
|
-
version =
|
|
1066
|
+
version = createHash2("sha256").update(text).digest("hex").slice(0, 12);
|
|
637
1067
|
} catch (error) {
|
|
638
1068
|
errors.push(error instanceof Error ? error.message : String(error));
|
|
639
1069
|
continue;
|
|
@@ -651,9 +1081,9 @@ function scan(options) {
|
|
|
651
1081
|
}));
|
|
652
1082
|
const metaDirs = /* @__PURE__ */ new Set();
|
|
653
1083
|
for (const file of metaFiles) {
|
|
654
|
-
const absPath =
|
|
1084
|
+
const absPath = join8(contentRoot, file);
|
|
655
1085
|
try {
|
|
656
|
-
const parsed = metaSchema.safeParse(JSON.parse(
|
|
1086
|
+
const parsed = metaSchema.safeParse(JSON.parse(readFileSync4(absPath, "utf8")));
|
|
657
1087
|
if (!parsed.success) {
|
|
658
1088
|
errors.push(
|
|
659
1089
|
`Invalid ${file}:
|
|
@@ -661,7 +1091,7 @@ ${parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.messa
|
|
|
661
1091
|
);
|
|
662
1092
|
continue;
|
|
663
1093
|
}
|
|
664
|
-
metaDirs.add(
|
|
1094
|
+
metaDirs.add(dirname5(file));
|
|
665
1095
|
files.push({ type: "meta", path: file, absolutePath: absPath, data: parsed.data });
|
|
666
1096
|
} catch (error) {
|
|
667
1097
|
errors.push(`Invalid ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -673,7 +1103,7 @@ ${parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.messa
|
|
|
673
1103
|
function synthesiseOrderMeta(pages, metaDirs) {
|
|
674
1104
|
const byDir = /* @__PURE__ */ new Map();
|
|
675
1105
|
for (const page of pages) {
|
|
676
|
-
const dir =
|
|
1106
|
+
const dir = dirname5(page.file);
|
|
677
1107
|
const bucket = byDir.get(dir);
|
|
678
1108
|
if (bucket) bucket.push(page);
|
|
679
1109
|
else byDir.set(dir, [page]);
|
|
@@ -795,43 +1225,16 @@ function createContext(options) {
|
|
|
795
1225
|
// src/cli/build.ts
|
|
796
1226
|
init_paths();
|
|
797
1227
|
|
|
798
|
-
// src/node/prerender/emit.ts
|
|
799
|
-
import { mkdirSync, writeFileSync } from "fs";
|
|
800
|
-
import { dirname as dirname4, join as join3 } from "path";
|
|
801
|
-
function outputPathFor(url) {
|
|
802
|
-
const clean = url.replace(/^\/+|\/+$/g, "");
|
|
803
|
-
return clean === "" ? "index.html" : join3(clean, "index.html");
|
|
804
|
-
}
|
|
805
|
-
function writeHtml(outDir, relativePath, html) {
|
|
806
|
-
const target = join3(outDir, relativePath);
|
|
807
|
-
mkdirSync(dirname4(target), { recursive: true });
|
|
808
|
-
writeFileSync(target, html, "utf8");
|
|
809
|
-
}
|
|
810
|
-
function applyTemplate(template, { html, head }) {
|
|
811
|
-
return template.replace("<!--seemore-head-->", () => head).replace("<!--seemore-app-->", () => html);
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
// src/node/prerender/deploy.ts
|
|
815
|
-
import { writeFileSync as writeFileSync2 } from "fs";
|
|
816
|
-
import { join as join4 } from "path";
|
|
817
|
-
function writeDeployArtifacts(outDir, base, shell) {
|
|
818
|
-
const prefix = base === "/" ? "" : base.replace(/\/+$/, "");
|
|
819
|
-
writeFileSync2(join4(outDir, "_redirects"), `${prefix}/* ${prefix}/index.html 200
|
|
820
|
-
`, "utf8");
|
|
821
|
-
writeFileSync2(join4(outDir, "200.html"), shell, "utf8");
|
|
822
|
-
writeFileSync2(join4(outDir, ".nojekyll"), "", "utf8");
|
|
823
|
-
}
|
|
824
|
-
|
|
825
1228
|
// src/node/prerender/render.ts
|
|
826
1229
|
import { pathToFileURL } from "url";
|
|
827
|
-
import { join as
|
|
828
|
-
import { build } from "vite";
|
|
1230
|
+
import { join as join10 } from "path";
|
|
1231
|
+
import { build as build2 } from "vite";
|
|
829
1232
|
|
|
830
1233
|
// src/node/vite/config.ts
|
|
831
1234
|
init_paths();
|
|
832
1235
|
import { realpathSync as realpathSync2 } from "fs";
|
|
833
|
-
import { createRequire as
|
|
834
|
-
import { join as
|
|
1236
|
+
import { createRequire as createRequire3 } from "module";
|
|
1237
|
+
import { join as join9 } from "path";
|
|
835
1238
|
import react from "@vitejs/plugin-react";
|
|
836
1239
|
import tailwindcss from "@tailwindcss/vite";
|
|
837
1240
|
import mdx from "@mdx-js/rollup";
|
|
@@ -853,7 +1256,7 @@ import {
|
|
|
853
1256
|
|
|
854
1257
|
// src/node/vite/remark.ts
|
|
855
1258
|
import { existsSync as existsSync3 } from "fs";
|
|
856
|
-
import { dirname as
|
|
1259
|
+
import { dirname as dirname6, relative, resolve as resolve4 } from "path";
|
|
857
1260
|
import { visit } from "unist-util-visit";
|
|
858
1261
|
var WIKILINK = /\[\[([^\]\n]+)\]\]/g;
|
|
859
1262
|
var ALERTS = {
|
|
@@ -956,7 +1359,7 @@ function remarkSeemoreD2() {
|
|
|
956
1359
|
function remarkSeemoreAssets(options) {
|
|
957
1360
|
return (tree, file) => {
|
|
958
1361
|
if (typeof file.path !== "string" || file.path === "") return;
|
|
959
|
-
const dir =
|
|
1362
|
+
const dir = dirname6(file.path);
|
|
960
1363
|
const from = virtualPath(options.contentRoot, file);
|
|
961
1364
|
visit(tree, "image", (node, index, parent) => {
|
|
962
1365
|
if (parent === void 0 || index === void 0) return;
|
|
@@ -1093,12 +1496,12 @@ function createRehypePlugins(options = {}) {
|
|
|
1093
1496
|
}
|
|
1094
1497
|
|
|
1095
1498
|
// src/node/vite/plugin.ts
|
|
1096
|
-
import { readFileSync as
|
|
1097
|
-
import { createRequire } from "module";
|
|
1098
|
-
import { dirname as
|
|
1499
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
1500
|
+
import { createRequire as createRequire2 } from "module";
|
|
1501
|
+
import { dirname as dirname7 } from "path";
|
|
1099
1502
|
|
|
1100
1503
|
// src/node/search/build.ts
|
|
1101
|
-
import { readFileSync as
|
|
1504
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
1102
1505
|
import { gzipSync } from "zlib";
|
|
1103
1506
|
import { createFromSource } from "fumadocs-core/search/server";
|
|
1104
1507
|
import { structure } from "fumadocs-core/mdx-plugins";
|
|
@@ -1114,7 +1517,7 @@ async function buildSearchIndex(ctx) {
|
|
|
1114
1517
|
const bodies = /* @__PURE__ */ new Map();
|
|
1115
1518
|
for (const page of ctx.pages()) {
|
|
1116
1519
|
try {
|
|
1117
|
-
bodies.set(page.url, parseFrontmatter(
|
|
1520
|
+
bodies.set(page.url, parseFrontmatter(readFileSync5(page.absPath, "utf8"), page.file).content);
|
|
1118
1521
|
} catch {
|
|
1119
1522
|
}
|
|
1120
1523
|
}
|
|
@@ -1187,11 +1590,11 @@ var PAGE_PREFIX = "seemore-page:";
|
|
|
1187
1590
|
var resolvedId = (id) => `\0${id}`;
|
|
1188
1591
|
var IMPORTS_MARKER = /\/\* seemore:imports[\s\S]*?\*\//;
|
|
1189
1592
|
var USER_CSS_MARKER = /\/\* seemore:user-css[\s\S]*?\*\//;
|
|
1190
|
-
var
|
|
1593
|
+
var require_2 = createRequire2(import.meta.url);
|
|
1191
1594
|
function styleImports(ctx) {
|
|
1192
1595
|
const lines = [`@import 'fumadocs-ui/css/${ctx.config.theme}.css';`];
|
|
1193
1596
|
try {
|
|
1194
|
-
lines.push(`@source '${
|
|
1597
|
+
lines.push(`@source '${dirname7(require_2.resolve("fumadocs-ui/package.json"))}/dist';`);
|
|
1195
1598
|
} catch {
|
|
1196
1599
|
}
|
|
1197
1600
|
return lines.join("\n");
|
|
@@ -1322,7 +1725,7 @@ function handleSourceRead(ctx, req, res) {
|
|
|
1322
1725
|
if (page === void 0) return send(res, 404, { error: "That file is not part of this site." });
|
|
1323
1726
|
const start = Number(query.get("start"));
|
|
1324
1727
|
const end = Number(query.get("end"));
|
|
1325
|
-
const content =
|
|
1728
|
+
const content = readFileSync6(page.absPath, "utf8");
|
|
1326
1729
|
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end > content.length) {
|
|
1327
1730
|
return send(res, 400, { error: "The requested range is not inside this file." });
|
|
1328
1731
|
}
|
|
@@ -1340,7 +1743,7 @@ async function handleSourceWrite(ctx, req, res) {
|
|
|
1340
1743
|
if (typeof body.expected !== "string" || typeof body.text !== "string") {
|
|
1341
1744
|
return send(res, 400, { error: "Both `expected` and `text` are required." });
|
|
1342
1745
|
}
|
|
1343
|
-
const content =
|
|
1746
|
+
const content = readFileSync6(page.absPath, "utf8");
|
|
1344
1747
|
const result = spliceSource(content, {
|
|
1345
1748
|
start: body.start,
|
|
1346
1749
|
end: body.end,
|
|
@@ -1348,7 +1751,7 @@ async function handleSourceWrite(ctx, req, res) {
|
|
|
1348
1751
|
text: body.text
|
|
1349
1752
|
});
|
|
1350
1753
|
if (!result.ok) return send(res, result.status, { error: result.error });
|
|
1351
|
-
|
|
1754
|
+
writeFileSync5(page.absPath, result.content, "utf8");
|
|
1352
1755
|
return send(res, 200, { ok: true });
|
|
1353
1756
|
}
|
|
1354
1757
|
function resolvePage(ctx, file) {
|
|
@@ -1431,7 +1834,7 @@ function json(value) {
|
|
|
1431
1834
|
}
|
|
1432
1835
|
function readIfExists(path) {
|
|
1433
1836
|
try {
|
|
1434
|
-
return
|
|
1837
|
+
return readFileSync6(path, "utf8");
|
|
1435
1838
|
} catch {
|
|
1436
1839
|
return void 0;
|
|
1437
1840
|
}
|
|
@@ -1514,12 +1917,17 @@ async function reloadById(server, id) {
|
|
|
1514
1917
|
}
|
|
1515
1918
|
|
|
1516
1919
|
// src/node/vite/config.ts
|
|
1517
|
-
var
|
|
1920
|
+
var require_3 = createRequire3(import.meta.url);
|
|
1518
1921
|
function d2BrowserEntry() {
|
|
1519
|
-
const entry =
|
|
1520
|
-
return
|
|
1922
|
+
const entry = require_3.resolve("@terrastruct/d2");
|
|
1923
|
+
return join9(packageDirOf("@terrastruct/d2", entry), "dist", "browser", "index.js");
|
|
1521
1924
|
}
|
|
1522
|
-
|
|
1925
|
+
var HASHED_NAMES = {
|
|
1926
|
+
entryFileNames: "assets/[hash].js",
|
|
1927
|
+
chunkFileNames: "assets/[hash].js",
|
|
1928
|
+
assetFileNames: "assets/[hash][extname]"
|
|
1929
|
+
};
|
|
1930
|
+
function createViteConfig({ ctx, mode, outDir, ssrOutDir, auth = false }) {
|
|
1523
1931
|
const root = appRoot();
|
|
1524
1932
|
const isSsr = ssrOutDir !== void 0;
|
|
1525
1933
|
const mdxOptions = {
|
|
@@ -1557,11 +1965,17 @@ function createViteConfig({ ctx, mode, outDir, ssrOutDir }) {
|
|
|
1557
1965
|
tailwindcss(),
|
|
1558
1966
|
...mode === "dev" ? [seemoreWatcherPlugin(ctx)] : []
|
|
1559
1967
|
],
|
|
1968
|
+
// A compile-time constant rather than a field of `virtual:seemore/config`, which carries
|
|
1969
|
+
// nothing about protection. Defined either way, so an unprotected bundle drops the auth code.
|
|
1970
|
+
define: { "import.meta.env.SEEMORE_AUTH": JSON.stringify(auth) },
|
|
1560
1971
|
// Vite bundles workers with the browser export condition, but a worker has no `document`.
|
|
1561
1972
|
// `decode-named-character-reference` — pulled in through fumadocs' search client, via
|
|
1562
1973
|
// remark — calls `document.createElement` at module scope in its browser build, so the
|
|
1563
1974
|
// search worker threw on load. The package ships a DOM-free `worker` entry; use it.
|
|
1564
|
-
worker: {
|
|
1975
|
+
worker: {
|
|
1976
|
+
plugins: () => [workerConditionPlugin()],
|
|
1977
|
+
rollupOptions: auth ? { output: HASHED_NAMES } : void 0
|
|
1978
|
+
},
|
|
1565
1979
|
resolve: {
|
|
1566
1980
|
// The app is compiled from seemore's own sources, so its dependencies must resolve
|
|
1567
1981
|
// from seemore's directory rather than from the user's project.
|
|
@@ -1596,7 +2010,7 @@ function createViteConfig({ ctx, mode, outDir, ssrOutDir }) {
|
|
|
1596
2010
|
}
|
|
1597
2011
|
},
|
|
1598
2012
|
build: isSsr ? {
|
|
1599
|
-
ssr:
|
|
2013
|
+
ssr: join9(root, "entry.prerender.tsx"),
|
|
1600
2014
|
outDir: ssrOutDir,
|
|
1601
2015
|
emptyOutDir: true,
|
|
1602
2016
|
copyPublicDir: false,
|
|
@@ -1605,7 +2019,7 @@ function createViteConfig({ ctx, mode, outDir, ssrOutDir }) {
|
|
|
1605
2019
|
} : {
|
|
1606
2020
|
outDir,
|
|
1607
2021
|
emptyOutDir: true,
|
|
1608
|
-
rollupOptions: { input:
|
|
2022
|
+
rollupOptions: { input: join9(root, "index.html"), output: auth ? HASHED_NAMES : void 0 },
|
|
1609
2023
|
// The app bundle is seemore's own, not the user's; warning them about a size they
|
|
1610
2024
|
// cannot act on is noise.
|
|
1611
2025
|
chunkSizeWarningLimit: 2e3
|
|
@@ -1656,8 +2070,8 @@ var WORKER_SAFE_ENTRIES = /* @__PURE__ */ new Set(["decode-named-character-refer
|
|
|
1656
2070
|
|
|
1657
2071
|
// src/node/prerender/render.ts
|
|
1658
2072
|
async function loadPrerenderModule(ctx, ssrOutDir) {
|
|
1659
|
-
await
|
|
1660
|
-
const entry =
|
|
2073
|
+
await build2(createViteConfig({ ctx, mode: "build", ssrOutDir }));
|
|
2074
|
+
const entry = join10(ssrOutDir, "entry.prerender.js");
|
|
1661
2075
|
const loaded = await import(pathToFileURL(entry).href);
|
|
1662
2076
|
if (typeof loaded.render !== "function" || typeof loaded.renderArticle !== "function" || typeof loaded.listRoutes !== "function") {
|
|
1663
2077
|
throw new Error(`seemore: the prerender build at ${entry} did not export \`render\`, \`renderArticle\` and \`listRoutes\`.`);
|
|
@@ -1666,8 +2080,8 @@ async function loadPrerenderModule(ctx, ssrOutDir) {
|
|
|
1666
2080
|
}
|
|
1667
2081
|
|
|
1668
2082
|
// src/node/social/cards.ts
|
|
1669
|
-
import { mkdirSync as mkdirSync2, writeFileSync as
|
|
1670
|
-
import { dirname as
|
|
2083
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
2084
|
+
import { dirname as dirname8, join as join11 } from "path";
|
|
1671
2085
|
|
|
1672
2086
|
// src/shared/og.ts
|
|
1673
2087
|
function ogImagePath(url) {
|
|
@@ -1690,9 +2104,9 @@ async function generateSocialCards(ctx, outDir) {
|
|
|
1690
2104
|
for (const page of ctx.pages()) {
|
|
1691
2105
|
const png = await renderCard(takumi, ctx.config.title, page.data.title, page.data.description);
|
|
1692
2106
|
if (png === void 0) continue;
|
|
1693
|
-
const target =
|
|
1694
|
-
mkdirSync2(
|
|
1695
|
-
|
|
2107
|
+
const target = join11(outDir, ogImagePath(page.url));
|
|
2108
|
+
mkdirSync2(dirname8(target), { recursive: true });
|
|
2109
|
+
writeFileSync6(target, png);
|
|
1696
2110
|
written++;
|
|
1697
2111
|
}
|
|
1698
2112
|
return written;
|
|
@@ -1734,6 +2148,7 @@ async function runBuild(options) {
|
|
|
1734
2148
|
const ctx = createContext({ config, contentRoot });
|
|
1735
2149
|
const outDir = resolve5(options.cwd, options.outDir ?? "dist");
|
|
1736
2150
|
assertSafeOutDir(outDir, options.cwd, contentRoot);
|
|
2151
|
+
const password = config.auth === void 0 ? void 0 : readPassword("npx seemore build");
|
|
1737
2152
|
const scan2 = ctx.source.current();
|
|
1738
2153
|
failOnErrors(ctx.errors(), contentRoot);
|
|
1739
2154
|
if (scan2.pages.length === 0) {
|
|
@@ -1741,9 +2156,29 @@ async function runBuild(options) {
|
|
|
1741
2156
|
}
|
|
1742
2157
|
for (const warning of scan2.warnings) ctx.warnings.add(warning);
|
|
1743
2158
|
console.log(pc2.dim(`seemore ${scan2.pages.length} pages from ${relative2(options.cwd, contentRoot) || "."}`));
|
|
1744
|
-
await viteBuild(createViteConfig({ ctx, mode: "build", outDir }));
|
|
1745
|
-
const template =
|
|
1746
|
-
const
|
|
2159
|
+
await viteBuild(createViteConfig({ ctx, mode: "build", outDir, auth: password !== void 0 }));
|
|
2160
|
+
const template = readFileSync7(join12(outDir, "index.html"), "utf8");
|
|
2161
|
+
const routes = password === void 0 ? await prerenderPages(ctx, outDir, template) : countRoutes(ctx);
|
|
2162
|
+
if (config.search.provider === "static") {
|
|
2163
|
+
const index = await buildSearchIndex(ctx);
|
|
2164
|
+
mkdirSync3(join12(outDir, "api"), { recursive: true });
|
|
2165
|
+
writeFileSync7(join12(outDir, "api", "search.json"), index, "utf8");
|
|
2166
|
+
const size = measureIndex(index);
|
|
2167
|
+
console.log(pc2.dim(`seemore search index ${formatBytes(size.gzipped)} gzipped`));
|
|
2168
|
+
if (size.warning !== void 0) ctx.warnings.add(size.warning);
|
|
2169
|
+
}
|
|
2170
|
+
if (config.search.provider !== "static") await warnIfSearchSdkMissing(ctx, config.search.provider);
|
|
2171
|
+
if (config.features["social.cards"]) await generateSocialCards(ctx, outDir);
|
|
2172
|
+
if (password !== void 0) {
|
|
2173
|
+
const encrypted = await sealSite(ctx, outDir, template, password);
|
|
2174
|
+
console.log(pc2.dim(`seemore ${encrypted} files encrypted; visitors unlock them with the password`));
|
|
2175
|
+
}
|
|
2176
|
+
ctx.warnings.flush();
|
|
2177
|
+
console.log(pc2.green(`seemore ${routes} pages written to ${relative2(options.cwd, outDir) || outDir}`));
|
|
2178
|
+
return { outDir, routes };
|
|
2179
|
+
}
|
|
2180
|
+
async function prerenderPages(ctx, outDir, template) {
|
|
2181
|
+
const ssrOutDir = mkdtempSync(join12(tmpdir3(), "seemore-ssr-"));
|
|
1747
2182
|
try {
|
|
1748
2183
|
const prerender = await loadPrerenderModule(ctx, ssrOutDir);
|
|
1749
2184
|
const urls = prerender.listRoutes();
|
|
@@ -1756,24 +2191,16 @@ async function runBuild(options) {
|
|
|
1756
2191
|
}
|
|
1757
2192
|
const notFound = applyTemplate(template, await prerender.render("/__seemore_not_found"));
|
|
1758
2193
|
writeHtml(outDir, "404.html", notFound);
|
|
1759
|
-
writeDeployArtifacts(outDir, config.base, notFound);
|
|
1760
|
-
|
|
1761
|
-
const index = await buildSearchIndex(ctx);
|
|
1762
|
-
mkdirSync3(join8(outDir, "api"), { recursive: true });
|
|
1763
|
-
writeFileSync5(join8(outDir, "api", "search.json"), index, "utf8");
|
|
1764
|
-
const size = measureIndex(index);
|
|
1765
|
-
console.log(pc2.dim(`seemore search index ${formatBytes(size.gzipped)} gzipped`));
|
|
1766
|
-
if (size.warning !== void 0) ctx.warnings.add(size.warning);
|
|
1767
|
-
}
|
|
1768
|
-
if (config.search.provider !== "static") await warnIfSearchSdkMissing(ctx, config.search.provider);
|
|
1769
|
-
if (config.features["social.cards"]) await generateSocialCards(ctx, outDir);
|
|
1770
|
-
ctx.warnings.flush();
|
|
1771
|
-
console.log(pc2.green(`seemore ${routes.length} pages written to ${relative2(options.cwd, outDir) || outDir}`));
|
|
1772
|
-
return { outDir, routes: routes.length };
|
|
2194
|
+
writeDeployArtifacts(outDir, ctx.config.base, notFound);
|
|
2195
|
+
return routes.length;
|
|
1773
2196
|
} finally {
|
|
1774
2197
|
rmSync(ssrOutDir, { recursive: true, force: true });
|
|
1775
2198
|
}
|
|
1776
2199
|
}
|
|
2200
|
+
function countRoutes(ctx) {
|
|
2201
|
+
const pages = ctx.pages();
|
|
2202
|
+
return pages.some((page) => page.url === "/") ? pages.length : pages.length + 1;
|
|
2203
|
+
}
|
|
1777
2204
|
function assertSafeOutDir(outDir, cwd, contentRoot) {
|
|
1778
2205
|
const contains = (parent, child) => {
|
|
1779
2206
|
const rel = relative2(parent, child);
|
|
@@ -1793,7 +2220,7 @@ function assertSafeOutDir(outDir, cwd, contentRoot) {
|
|
|
1793
2220
|
async function warnIfSearchSdkMissing(ctx, provider) {
|
|
1794
2221
|
const packageName = provider === "algolia" ? "algoliasearch" : "@orama/core";
|
|
1795
2222
|
try {
|
|
1796
|
-
|
|
2223
|
+
createRequire4(join12(ctx.config.root, "noop.js")).resolve(packageName);
|
|
1797
2224
|
} catch {
|
|
1798
2225
|
ctx.warnings.add(
|
|
1799
2226
|
`\`search.provider\` is '${provider}', which needs ${packageName}. Run \`npm install ${packageName}\` or search will find nothing.`
|
|
@@ -1839,6 +2266,9 @@ async function runDev(options) {
|
|
|
1839
2266
|
if (scan2.pages.length === 0) {
|
|
1840
2267
|
ctx.warnings.add(`No Markdown files found under ${contentRoot}. seemore will serve an empty site until there are.`);
|
|
1841
2268
|
}
|
|
2269
|
+
if (config.auth !== void 0 && options.json !== true) {
|
|
2270
|
+
console.log(pc3.dim("seemore auth is build-only; the dev server is not password-protected"));
|
|
2271
|
+
}
|
|
1842
2272
|
const base = createViteConfig({ ctx, mode: "dev" });
|
|
1843
2273
|
const server = await createServer({
|
|
1844
2274
|
...base,
|
|
@@ -1873,9 +2303,9 @@ async function runDev(options) {
|
|
|
1873
2303
|
}
|
|
1874
2304
|
|
|
1875
2305
|
// src/cli/export.ts
|
|
1876
|
-
import { existsSync as existsSync4, mkdtempSync as mkdtempSync2, readFileSync as
|
|
1877
|
-
import { basename as basename2, dirname as
|
|
1878
|
-
import { tmpdir as
|
|
2306
|
+
import { existsSync as existsSync4, mkdtempSync as mkdtempSync2, readFileSync as readFileSync8, readdirSync as readdirSync2, rmSync as rmSync2, statSync, writeFileSync as writeFileSync8, mkdirSync as mkdirSync4 } from "fs";
|
|
2307
|
+
import { basename as basename2, dirname as dirname9, extname, join as join13, relative as relative3, resolve as resolve6 } from "path";
|
|
2308
|
+
import { tmpdir as tmpdir4 } from "os";
|
|
1879
2309
|
import pc4 from "picocolors";
|
|
1880
2310
|
import { build as viteBuild2 } from "vite";
|
|
1881
2311
|
init_paths();
|
|
@@ -1913,7 +2343,7 @@ async function runExport(options) {
|
|
|
1913
2343
|
if (!existsSync4(target) || !statSync(target).isFile()) {
|
|
1914
2344
|
throw new Error(`No such file: ${options.file}`);
|
|
1915
2345
|
}
|
|
1916
|
-
const contentRoot = resolveContentRoot(options.cwd,
|
|
2346
|
+
const contentRoot = resolveContentRoot(options.cwd, dirname9(target));
|
|
1917
2347
|
const loaded = await loadConfig({ root: contentRoot, configPath: resolveConfigPath(options) });
|
|
1918
2348
|
const config = {
|
|
1919
2349
|
...loaded.config,
|
|
@@ -1935,23 +2365,23 @@ ${errors.join("\n\n")}`);
|
|
|
1935
2365
|
if (page === void 0) {
|
|
1936
2366
|
throw new Error(`${options.file} is not part of this site \u2014 excluded in the config, or outside ${relative3(options.cwd, contentRoot) || "."}.`);
|
|
1937
2367
|
}
|
|
1938
|
-
const outDir = mkdtempSync2(
|
|
1939
|
-
const ssrOutDir = mkdtempSync2(
|
|
2368
|
+
const outDir = mkdtempSync2(join13(tmpdir4(), "seemore-export-"));
|
|
2369
|
+
const ssrOutDir = mkdtempSync2(join13(tmpdir4(), "seemore-export-ssr-"));
|
|
1940
2370
|
try {
|
|
1941
2371
|
await viteBuild2(createViteConfig({ ctx, mode: "build", outDir }));
|
|
1942
2372
|
const css = readBuiltCss(outDir);
|
|
1943
|
-
const template =
|
|
2373
|
+
const template = readFileSync8(join13(outDir, "index.html"), "utf8");
|
|
1944
2374
|
const prerender = await loadPrerenderModule(ctx, ssrOutDir);
|
|
1945
2375
|
const article = await prerender.renderArticle(page.url);
|
|
1946
2376
|
const runtime = await bundleRuntime();
|
|
1947
2377
|
const html = assemble({ article, css, runtime, config, outDir, contentRoot, template });
|
|
1948
2378
|
const filename = `${basename2(target).replace(/\.(?:md|mdx)$/i, "")}.html`;
|
|
1949
|
-
const targetPath = options.out === void 0 ?
|
|
2379
|
+
const targetPath = options.out === void 0 ? join13(dirname9(target), filename) : join13(resolve6(options.cwd, options.out), filename);
|
|
1950
2380
|
if (existsSync4(targetPath)) {
|
|
1951
2381
|
console.log(pc4.yellow(`seemore replacing existing ${relative3(options.cwd, targetPath) || targetPath}`));
|
|
1952
2382
|
}
|
|
1953
|
-
mkdirSync4(
|
|
1954
|
-
|
|
2383
|
+
mkdirSync4(dirname9(targetPath), { recursive: true });
|
|
2384
|
+
writeFileSync8(targetPath, html, "utf8");
|
|
1955
2385
|
ctx.warnings.flush();
|
|
1956
2386
|
console.log(pc4.green(`seemore wrote ${relative3(options.cwd, targetPath) || targetPath} (${formatBytes2(html.length)})`));
|
|
1957
2387
|
} finally {
|
|
@@ -1960,13 +2390,13 @@ ${errors.join("\n\n")}`);
|
|
|
1960
2390
|
}
|
|
1961
2391
|
}
|
|
1962
2392
|
function readBuiltCss(outDir) {
|
|
1963
|
-
const assets =
|
|
1964
|
-
const files = existsSync4(assets) ?
|
|
2393
|
+
const assets = join13(outDir, "assets");
|
|
2394
|
+
const files = existsSync4(assets) ? readdirSync2(assets).filter((file) => file.endsWith(".css")) : [];
|
|
1965
2395
|
if (files.length === 0) throw new Error("The build produced no stylesheet \u2014 the export would be unstyled.");
|
|
1966
|
-
return files.map((file) =>
|
|
2396
|
+
return files.map((file) => readFileSync8(join13(assets, file), "utf8")).join("\n");
|
|
1967
2397
|
}
|
|
1968
2398
|
async function bundleRuntime() {
|
|
1969
|
-
const dir = mkdtempSync2(
|
|
2399
|
+
const dir = mkdtempSync2(join13(tmpdir4(), "seemore-export-runtime-"));
|
|
1970
2400
|
try {
|
|
1971
2401
|
await viteBuild2({
|
|
1972
2402
|
configFile: false,
|
|
@@ -1977,14 +2407,14 @@ async function bundleRuntime() {
|
|
|
1977
2407
|
minify: true,
|
|
1978
2408
|
target: "es2018",
|
|
1979
2409
|
lib: {
|
|
1980
|
-
entry:
|
|
2410
|
+
entry: join13(appRoot(), "export", "standalone.ts"),
|
|
1981
2411
|
name: "seemoreExport",
|
|
1982
2412
|
formats: ["iife"],
|
|
1983
2413
|
fileName: () => "runtime.js"
|
|
1984
2414
|
}
|
|
1985
2415
|
}
|
|
1986
2416
|
});
|
|
1987
|
-
return
|
|
2417
|
+
return readFileSync8(join13(dir, "runtime.js"), "utf8");
|
|
1988
2418
|
} finally {
|
|
1989
2419
|
rmSync2(dir, { recursive: true, force: true });
|
|
1990
2420
|
}
|
|
@@ -2001,8 +2431,8 @@ function assemble(input) {
|
|
|
2001
2431
|
"<head>",
|
|
2002
2432
|
'<meta charset="utf-8">',
|
|
2003
2433
|
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
2004
|
-
`<title>${
|
|
2005
|
-
description === void 0 ? "" : `<meta name="description" content="${
|
|
2434
|
+
`<title>${escapeHtml2(title)}</title>`,
|
|
2435
|
+
description === void 0 ? "" : `<meta name="description" content="${escapeHtml2(description)}">`,
|
|
2006
2436
|
'<meta name="generator" content="seemore">',
|
|
2007
2437
|
THEME_INIT,
|
|
2008
2438
|
favicon,
|
|
@@ -2051,7 +2481,7 @@ function resolveAsset(src, base, outDir, contentRoot) {
|
|
|
2051
2481
|
if (base !== "/" && path.startsWith(base)) path = path.slice(base.length - 1);
|
|
2052
2482
|
const suffix = path.replace(/^\/+/, "");
|
|
2053
2483
|
for (const root of [outDir, contentRoot]) {
|
|
2054
|
-
const candidate =
|
|
2484
|
+
const candidate = join13(root, suffix);
|
|
2055
2485
|
if (existsSync4(candidate) && statSync(candidate).isFile()) return candidate;
|
|
2056
2486
|
}
|
|
2057
2487
|
return void 0;
|
|
@@ -2075,11 +2505,11 @@ var MIME = {
|
|
|
2075
2505
|
};
|
|
2076
2506
|
function dataUriFor(file) {
|
|
2077
2507
|
const mime = MIME[extname(file).toLowerCase()] ?? "application/octet-stream";
|
|
2078
|
-
return `data:${mime};base64,${
|
|
2508
|
+
return `data:${mime};base64,${readFileSync8(file).toString("base64")}`;
|
|
2079
2509
|
}
|
|
2080
2510
|
function faviconLink(config, contentRoot, template) {
|
|
2081
2511
|
if (config.favicon !== void 0) {
|
|
2082
|
-
const file =
|
|
2512
|
+
const file = join13(contentRoot, config.favicon);
|
|
2083
2513
|
if (existsSync4(file)) {
|
|
2084
2514
|
try {
|
|
2085
2515
|
return `<link rel="icon" href="${dataUriFor(file)}" />`;
|
|
@@ -2108,9 +2538,9 @@ function withExportToc(html) {
|
|
|
2108
2538
|
}
|
|
2109
2539
|
}
|
|
2110
2540
|
const items = sections.map((section) => {
|
|
2111
|
-
const self = `<a href="#${section.id}">${
|
|
2541
|
+
const self = `<a href="#${section.id}">${escapeHtml2(section.text)}</a>`;
|
|
2112
2542
|
if (section.children.length === 0) return `<li>${self}</li>`;
|
|
2113
|
-
const kids = section.children.map((child) => `<li><a href="#${child.id}">${
|
|
2543
|
+
const kids = section.children.map((child) => `<li><a href="#${child.id}">${escapeHtml2(child.text)}</a></li>`).join("");
|
|
2114
2544
|
return `<li>${self}<ul>${kids}</ul></li>`;
|
|
2115
2545
|
}).join("");
|
|
2116
2546
|
const toc = `<nav class="seemore-export-toc"><details><summary>On this page</summary><ul>${items}</ul></details></nav>`;
|
|
@@ -2120,7 +2550,7 @@ function withExportToc(html) {
|
|
|
2120
2550
|
function decodeEntities(text) {
|
|
2121
2551
|
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll(""", '"').replaceAll("'", "'");
|
|
2122
2552
|
}
|
|
2123
|
-
function
|
|
2553
|
+
function escapeHtml2(value) {
|
|
2124
2554
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
2125
2555
|
}
|
|
2126
2556
|
function formatBytes2(size) {
|
|
@@ -2177,10 +2607,10 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
2177
2607
|
return;
|
|
2178
2608
|
}
|
|
2179
2609
|
if (values.version === true) {
|
|
2180
|
-
const { readFileSync:
|
|
2181
|
-
const { join:
|
|
2610
|
+
const { readFileSync: readFileSync9 } = await import("fs");
|
|
2611
|
+
const { join: join14 } = await import("path");
|
|
2182
2612
|
const { packageRoot: packageRoot2 } = await Promise.resolve().then(() => (init_paths(), paths_exports));
|
|
2183
|
-
const pkg = JSON.parse(
|
|
2613
|
+
const pkg = JSON.parse(readFileSync9(join14(packageRoot2(), "package.json"), "utf8"));
|
|
2184
2614
|
console.log(pkg.version);
|
|
2185
2615
|
return;
|
|
2186
2616
|
}
|