mdorigin 0.1.8 → 0.2.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 +12 -0
- package/dist/adapters/cloudflare.d.ts +40 -4
- package/dist/adapters/cloudflare.js +163 -7
- package/dist/adapters/node.js +2 -2
- package/dist/cli/build-cloudflare.js +27 -2
- package/dist/cli/help.js +5 -4
- package/dist/cli/init-cloudflare.js +7 -1
- package/dist/cli/main.js +5 -0
- package/dist/cli/sync-cloudflare-r2.d.ts +1 -0
- package/dist/cli/sync-cloudflare-r2.js +47 -0
- package/dist/cloudflare-runtime.d.ts +1 -1
- package/dist/cloudflare.d.ts +42 -6
- package/dist/cloudflare.js +246 -10
- package/dist/core/content-store.d.ts +1 -0
- package/dist/core/content-store.js +8 -1
- package/dist/core/extensions.d.ts +5 -9
- package/dist/core/markdown.js +6 -2
- package/dist/core/request-handler.js +46 -60
- package/dist/core/site-config.d.ts +4 -10
- package/dist/core/site-config.js +18 -10
- package/dist/html/template.d.ts +5 -9
- package/dist/html/template.js +25 -25
- package/dist/html/theme.d.ts +1 -2
- package/dist/html/theme.js +2 -642
- package/dist/index-builder.js +5 -4
- package/dist/search.js +3 -2
- package/package.json +4 -2
- package/dist/html/template-kind.d.ts +0 -1
- package/dist/html/template-kind.js +0 -1
package/dist/cloudflare.js
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { createReadStream } from 'node:fs';
|
|
4
|
+
import { copyFile, mkdir, readFile, readdir, realpath, rm, stat, writeFile, } from 'node:fs/promises';
|
|
2
5
|
import path from 'node:path';
|
|
3
6
|
import { createCloudflareWorker } from './adapters/cloudflare.js';
|
|
4
|
-
import { getMediaTypeForPath, isLikelyTextPath, normalizeContentPath, } from './core/content-store.js';
|
|
7
|
+
import { getMediaTypeForPath, isIgnoredContentName, isLikelyTextPath, normalizeContentPath, } from './core/content-store.js';
|
|
5
8
|
export { createCloudflareWorker };
|
|
9
|
+
const DEFAULT_ASSETS_MAX_BYTES = 25 * 1024 * 1024;
|
|
10
|
+
const DEFAULT_ASSETS_BINDING = 'ASSETS';
|
|
11
|
+
const DEFAULT_R2_BINDING = 'MDORIGIN_R2';
|
|
12
|
+
const BUNDLE_FILE_NAME = 'bundle.json';
|
|
13
|
+
const R2_STATE_FILE_NAME = 'r2-sync-state.json';
|
|
6
14
|
export async function buildCloudflareManifest(options) {
|
|
7
15
|
const rootDir = path.resolve(options.rootDir);
|
|
8
16
|
const files = await listFiles(rootDir);
|
|
9
17
|
const entries = [];
|
|
18
|
+
const binaryMode = options.binaryMode ?? 'inline';
|
|
19
|
+
const assetsMaxBytes = options.assetsMaxBytes ?? DEFAULT_ASSETS_MAX_BYTES;
|
|
20
|
+
const r2Binding = options.r2Binding ?? DEFAULT_R2_BINDING;
|
|
10
21
|
for (const filePath of files) {
|
|
11
22
|
const relativePath = path.relative(rootDir, filePath).replaceAll(path.sep, '/');
|
|
12
23
|
const normalizedPath = normalizeContentPath(relativePath);
|
|
@@ -23,12 +34,21 @@ export async function buildCloudflareManifest(options) {
|
|
|
23
34
|
});
|
|
24
35
|
continue;
|
|
25
36
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
if (binaryMode === 'inline') {
|
|
38
|
+
const bytes = await readFile(filePath);
|
|
39
|
+
entries.push({
|
|
40
|
+
path: normalizedPath,
|
|
41
|
+
kind: 'binary',
|
|
42
|
+
mediaType,
|
|
43
|
+
base64: bytes.toString('base64'),
|
|
44
|
+
});
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const fileStats = await stat(filePath);
|
|
48
|
+
entries.push(await buildExternalBinaryEntry(filePath, normalizedPath, mediaType, fileStats.size, {
|
|
49
|
+
assetsMaxBytes,
|
|
50
|
+
r2Binding,
|
|
51
|
+
}));
|
|
32
52
|
}
|
|
33
53
|
const searchEntries = options.searchDir
|
|
34
54
|
? await readBundleEntries(path.resolve(options.searchDir))
|
|
@@ -38,17 +58,32 @@ export async function buildCloudflareManifest(options) {
|
|
|
38
58
|
entries,
|
|
39
59
|
siteConfig: options.siteConfig,
|
|
40
60
|
searchEntries,
|
|
61
|
+
runtime: binaryMode === 'external'
|
|
62
|
+
? {
|
|
63
|
+
binaryMode,
|
|
64
|
+
r2Binding,
|
|
65
|
+
}
|
|
66
|
+
: {
|
|
67
|
+
binaryMode,
|
|
68
|
+
},
|
|
41
69
|
};
|
|
42
70
|
}
|
|
43
71
|
export async function writeCloudflareBundle(options) {
|
|
44
72
|
const outDir = path.resolve(options.outDir);
|
|
73
|
+
const binaryMode = options.binaryMode ?? 'inline';
|
|
74
|
+
const assetsMaxBytes = options.assetsMaxBytes ?? DEFAULT_ASSETS_MAX_BYTES;
|
|
75
|
+
const r2Binding = options.r2Binding ?? DEFAULT_R2_BINDING;
|
|
45
76
|
const manifest = await buildCloudflareManifest({
|
|
46
77
|
rootDir: options.rootDir,
|
|
47
78
|
siteConfig: options.siteConfig,
|
|
48
79
|
searchDir: options.searchDir,
|
|
80
|
+
binaryMode,
|
|
81
|
+
assetsMaxBytes,
|
|
82
|
+
r2Binding,
|
|
49
83
|
});
|
|
50
84
|
const packageImport = options.packageImport ?? 'mdorigin/cloudflare-runtime';
|
|
51
85
|
const workerFile = path.join(outDir, 'worker.mjs');
|
|
86
|
+
const bundleFile = path.join(outDir, BUNDLE_FILE_NAME);
|
|
52
87
|
const configImportPath = options.configModulePath
|
|
53
88
|
? toPosixPath(path.relative(outDir, options.configModulePath))
|
|
54
89
|
: null;
|
|
@@ -83,10 +118,18 @@ export async function writeCloudflareBundle(options) {
|
|
|
83
118
|
'',
|
|
84
119
|
].join('\n');
|
|
85
120
|
await mkdir(outDir, { recursive: true });
|
|
121
|
+
const metadata = await writeExternalBinaryStaging(path.resolve(options.rootDir), outDir, manifest, {
|
|
122
|
+
binaryMode,
|
|
123
|
+
assetsMaxBytes,
|
|
124
|
+
r2Binding,
|
|
125
|
+
siteTitle: options.siteConfig.siteTitle,
|
|
126
|
+
});
|
|
86
127
|
await writeFile(workerFile, workerSource, 'utf8');
|
|
128
|
+
await writeFile(bundleFile, JSON.stringify(metadata, null, 2), 'utf8');
|
|
87
129
|
return {
|
|
88
130
|
manifest,
|
|
89
131
|
workerFile,
|
|
132
|
+
bundleFile,
|
|
90
133
|
};
|
|
91
134
|
}
|
|
92
135
|
export async function initCloudflareProject(options) {
|
|
@@ -96,8 +139,14 @@ export async function initCloudflareProject(options) {
|
|
|
96
139
|
if (existing && !options.force) {
|
|
97
140
|
throw new Error(`Refusing to overwrite ${configFile}. Re-run with --force to replace it.`);
|
|
98
141
|
}
|
|
142
|
+
const bundleMetadata = await readCloudflareBundleMetadata(options.workerEntry);
|
|
143
|
+
if (bundleMetadata?.binaryMode === 'external' &&
|
|
144
|
+
bundleMetadata.r2Objects.length > 0 &&
|
|
145
|
+
!options.r2Bucket) {
|
|
146
|
+
throw new Error('Cloudflare bundle contains R2-backed binaries. Re-run init cloudflare with --r2-bucket <bucket-name>.');
|
|
147
|
+
}
|
|
99
148
|
const workerName = options.workerName ??
|
|
100
|
-
slugifyWorkerName(options.siteTitle) ??
|
|
149
|
+
slugifyWorkerName(bundleMetadata?.siteTitle ?? options.siteTitle) ??
|
|
101
150
|
'mdorigin-site';
|
|
102
151
|
const compatibilityDate = options.compatibilityDate ?? '2026-03-20';
|
|
103
152
|
const wranglerConfig = [
|
|
@@ -107,13 +156,197 @@ export async function initCloudflareProject(options) {
|
|
|
107
156
|
` "main": ${JSON.stringify(toPosixPath(path.relative(projectDir, options.workerEntry)))},`,
|
|
108
157
|
` "compatibility_date": ${JSON.stringify(compatibilityDate)},`,
|
|
109
158
|
' "compatibility_flags": ["nodejs_compat"]',
|
|
159
|
+
bundleMetadata?.binaryMode === 'external' && bundleMetadata.assetsDir
|
|
160
|
+
? [
|
|
161
|
+
',',
|
|
162
|
+
' "assets": {',
|
|
163
|
+
` "directory": ${JSON.stringify(toPosixPath(path.relative(projectDir, path.join(path.dirname(options.workerEntry), bundleMetadata.assetsDir))))},`,
|
|
164
|
+
` "binding": ${JSON.stringify(DEFAULT_ASSETS_BINDING)},`,
|
|
165
|
+
' "run_worker_first": true',
|
|
166
|
+
' }',
|
|
167
|
+
].join('\n')
|
|
168
|
+
: '',
|
|
169
|
+
bundleMetadata?.binaryMode === 'external' &&
|
|
170
|
+
bundleMetadata.r2Objects.length > 0 &&
|
|
171
|
+
bundleMetadata.r2Binding &&
|
|
172
|
+
options.r2Bucket
|
|
173
|
+
? [
|
|
174
|
+
',',
|
|
175
|
+
' "r2_buckets": [',
|
|
176
|
+
' {',
|
|
177
|
+
` "binding": ${JSON.stringify(bundleMetadata.r2Binding)},`,
|
|
178
|
+
` "bucket_name": ${JSON.stringify(options.r2Bucket)}`,
|
|
179
|
+
' }',
|
|
180
|
+
' ]',
|
|
181
|
+
].join('\n')
|
|
182
|
+
: '',
|
|
110
183
|
'}',
|
|
111
184
|
'',
|
|
112
|
-
]
|
|
185
|
+
]
|
|
186
|
+
.filter(Boolean)
|
|
187
|
+
.join('\n');
|
|
113
188
|
await mkdir(projectDir, { recursive: true });
|
|
114
189
|
await writeFile(configFile, wranglerConfig, 'utf8');
|
|
115
190
|
return { configFile };
|
|
116
191
|
}
|
|
192
|
+
export async function syncCloudflareR2(options) {
|
|
193
|
+
const outDir = path.resolve(options.dir);
|
|
194
|
+
const bundleFile = path.join(outDir, BUNDLE_FILE_NAME);
|
|
195
|
+
const metadata = await readBundleMetadataFile(bundleFile);
|
|
196
|
+
if (metadata.binaryMode !== 'external' || metadata.r2Objects.length === 0) {
|
|
197
|
+
throw new Error(`No R2-backed binaries found in ${bundleFile}.`);
|
|
198
|
+
}
|
|
199
|
+
const stateFile = path.join(outDir, R2_STATE_FILE_NAME);
|
|
200
|
+
const state = await readR2SyncState(stateFile);
|
|
201
|
+
const runCommand = options.runCommand ?? runWranglerCommand;
|
|
202
|
+
let uploadedCount = 0;
|
|
203
|
+
let skippedCount = 0;
|
|
204
|
+
for (const object of metadata.r2Objects) {
|
|
205
|
+
const stateKey = `${options.bucketName}:${object.storageKey}`;
|
|
206
|
+
if (!options.force && state.uploaded[stateKey]) {
|
|
207
|
+
skippedCount += 1;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const filePath = path.join(outDir, object.file);
|
|
211
|
+
const result = runCommand('wrangler', [
|
|
212
|
+
'r2',
|
|
213
|
+
'object',
|
|
214
|
+
'put',
|
|
215
|
+
`${options.bucketName}/${object.storageKey}`,
|
|
216
|
+
'--file',
|
|
217
|
+
filePath,
|
|
218
|
+
'--content-type',
|
|
219
|
+
object.mediaType,
|
|
220
|
+
'--remote',
|
|
221
|
+
]);
|
|
222
|
+
if (result.status !== 0) {
|
|
223
|
+
throw new Error(result.stderr || `Failed to upload R2 object ${object.storageKey}.`);
|
|
224
|
+
}
|
|
225
|
+
state.uploaded[stateKey] = {
|
|
226
|
+
syncedAt: new Date().toISOString(),
|
|
227
|
+
};
|
|
228
|
+
uploadedCount += 1;
|
|
229
|
+
}
|
|
230
|
+
await writeFile(stateFile, JSON.stringify(state, null, 2), 'utf8');
|
|
231
|
+
return {
|
|
232
|
+
uploadedCount,
|
|
233
|
+
skippedCount,
|
|
234
|
+
stateFile,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
async function buildExternalBinaryEntry(filePath, normalizedPath, mediaType, byteSize, options) {
|
|
238
|
+
if (byteSize <= options.assetsMaxBytes) {
|
|
239
|
+
return {
|
|
240
|
+
path: normalizedPath,
|
|
241
|
+
kind: 'binary',
|
|
242
|
+
mediaType,
|
|
243
|
+
storageKind: 'assets',
|
|
244
|
+
storageKey: normalizedPath,
|
|
245
|
+
byteSize,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
path: normalizedPath,
|
|
250
|
+
kind: 'binary',
|
|
251
|
+
mediaType,
|
|
252
|
+
storageKind: 'r2',
|
|
253
|
+
storageKey: await buildR2StorageKey(filePath, normalizedPath),
|
|
254
|
+
byteSize,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
async function writeExternalBinaryStaging(rootDir, outDir, manifest, options) {
|
|
258
|
+
const assetsDir = path.join(outDir, 'assets');
|
|
259
|
+
const r2Dir = path.join(outDir, 'r2');
|
|
260
|
+
await rm(assetsDir, { recursive: true, force: true });
|
|
261
|
+
await rm(r2Dir, { recursive: true, force: true });
|
|
262
|
+
const r2Objects = new Map();
|
|
263
|
+
if (options.binaryMode === 'external') {
|
|
264
|
+
for (const entry of manifest.entries) {
|
|
265
|
+
if (entry.kind !== 'binary' || !('storageKind' in entry)) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const sourceFile = path.resolve(rootDir, entry.path);
|
|
269
|
+
if (entry.storageKind === 'assets') {
|
|
270
|
+
const targetFile = path.join(assetsDir, entry.storageKey);
|
|
271
|
+
await mkdir(path.dirname(targetFile), { recursive: true });
|
|
272
|
+
await copyFile(sourceFile, targetFile);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const relativeFile = toPosixPath(path.join('r2', entry.storageKey));
|
|
276
|
+
const targetFile = path.join(outDir, relativeFile);
|
|
277
|
+
if (!r2Objects.has(entry.storageKey)) {
|
|
278
|
+
await mkdir(path.dirname(targetFile), { recursive: true });
|
|
279
|
+
await copyFile(sourceFile, targetFile);
|
|
280
|
+
r2Objects.set(entry.storageKey, {
|
|
281
|
+
path: entry.path,
|
|
282
|
+
mediaType: entry.mediaType,
|
|
283
|
+
storageKey: entry.storageKey,
|
|
284
|
+
file: relativeFile,
|
|
285
|
+
byteSize: entry.byteSize,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
version: 1,
|
|
292
|
+
workerEntry: 'worker.mjs',
|
|
293
|
+
binaryMode: options.binaryMode,
|
|
294
|
+
assetsMaxBytes: options.binaryMode === 'external' ? options.assetsMaxBytes : undefined,
|
|
295
|
+
assetsDir: options.binaryMode === 'external' ? 'assets' : undefined,
|
|
296
|
+
r2Dir: options.binaryMode === 'external' ? 'r2' : undefined,
|
|
297
|
+
r2Binding: options.binaryMode === 'external' && r2Objects.size > 0
|
|
298
|
+
? options.r2Binding
|
|
299
|
+
: undefined,
|
|
300
|
+
siteTitle: options.siteTitle,
|
|
301
|
+
r2Objects: Array.from(r2Objects.values()).sort((left, right) => left.storageKey.localeCompare(right.storageKey)),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
async function readCloudflareBundleMetadata(workerEntry) {
|
|
305
|
+
const bundleFile = path.join(path.dirname(workerEntry), BUNDLE_FILE_NAME);
|
|
306
|
+
if (!(await pathExists(bundleFile))) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
return readBundleMetadataFile(bundleFile);
|
|
310
|
+
}
|
|
311
|
+
async function readBundleMetadataFile(bundleFile) {
|
|
312
|
+
return JSON.parse(await readFile(bundleFile, 'utf8'));
|
|
313
|
+
}
|
|
314
|
+
async function readR2SyncState(stateFile) {
|
|
315
|
+
if (!(await pathExists(stateFile))) {
|
|
316
|
+
return { version: 1, uploaded: {} };
|
|
317
|
+
}
|
|
318
|
+
return JSON.parse(await readFile(stateFile, 'utf8'));
|
|
319
|
+
}
|
|
320
|
+
async function buildR2StorageKey(filePath, normalizedPath) {
|
|
321
|
+
const extension = path.posix.extname(normalizedPath).toLowerCase();
|
|
322
|
+
const hash = await hashFile(filePath);
|
|
323
|
+
return extension ? `binary/${hash}${extension}` : `binary/${hash}`;
|
|
324
|
+
}
|
|
325
|
+
async function hashFile(filePath) {
|
|
326
|
+
const hash = createHash('sha256');
|
|
327
|
+
await new Promise((resolve, reject) => {
|
|
328
|
+
const stream = createReadStream(filePath);
|
|
329
|
+
stream.on('data', (chunk) => {
|
|
330
|
+
hash.update(chunk);
|
|
331
|
+
});
|
|
332
|
+
stream.on('end', () => resolve());
|
|
333
|
+
stream.on('error', reject);
|
|
334
|
+
});
|
|
335
|
+
return hash.digest('hex');
|
|
336
|
+
}
|
|
337
|
+
function runWranglerCommand(command, args) {
|
|
338
|
+
const result = spawnSync(command, args, {
|
|
339
|
+
encoding: 'utf8',
|
|
340
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
341
|
+
});
|
|
342
|
+
if (result.error) {
|
|
343
|
+
throw result.error;
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
status: result.status,
|
|
347
|
+
stderr: result.stderr ?? '',
|
|
348
|
+
};
|
|
349
|
+
}
|
|
117
350
|
async function listFiles(directory, visitedRealDirectories = new Set()) {
|
|
118
351
|
const directoryRealPath = await realpath(directory);
|
|
119
352
|
if (visitedRealDirectories.has(directoryRealPath)) {
|
|
@@ -123,6 +356,9 @@ async function listFiles(directory, visitedRealDirectories = new Set()) {
|
|
|
123
356
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
124
357
|
const files = [];
|
|
125
358
|
for (const entry of entries) {
|
|
359
|
+
if (isIgnoredContentName(entry.name)) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
126
362
|
const fullPath = path.join(directory, entry.name);
|
|
127
363
|
const entryStats = await stat(fullPath);
|
|
128
364
|
if (entryStats.isDirectory()) {
|
|
@@ -15,6 +15,7 @@ export interface ContentStore {
|
|
|
15
15
|
get(contentPath: string): Promise<ContentEntry | null>;
|
|
16
16
|
listDirectory(contentPath: string): Promise<ContentDirectoryEntry[] | null>;
|
|
17
17
|
}
|
|
18
|
+
export declare function isIgnoredContentName(name: string): boolean;
|
|
18
19
|
export declare class MemoryContentStore implements ContentStore {
|
|
19
20
|
private readonly entries;
|
|
20
21
|
constructor(entries: Iterable<ContentEntry>);
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
export function isIgnoredContentName(name) {
|
|
3
|
+
return name.startsWith('.');
|
|
4
|
+
}
|
|
2
5
|
export class MemoryContentStore {
|
|
3
6
|
entries;
|
|
4
7
|
constructor(entries) {
|
|
@@ -23,7 +26,7 @@ export class MemoryContentStore {
|
|
|
23
26
|
continue;
|
|
24
27
|
}
|
|
25
28
|
const [firstSegment, ...rest] = remainder.split('/');
|
|
26
|
-
if (firstSegment
|
|
29
|
+
if (isIgnoredContentName(firstSegment)) {
|
|
27
30
|
continue;
|
|
28
31
|
}
|
|
29
32
|
if (rest.length === 0) {
|
|
@@ -53,6 +56,7 @@ const MEDIA_TYPES = new Map([
|
|
|
53
56
|
['.js', 'text/javascript; charset=utf-8'],
|
|
54
57
|
['.json', 'application/json; charset=utf-8'],
|
|
55
58
|
['.md', 'text/markdown; charset=utf-8'],
|
|
59
|
+
['.mp4', 'video/mp4'],
|
|
56
60
|
['.pdf', 'application/pdf'],
|
|
57
61
|
['.py', 'text/plain; charset=utf-8'],
|
|
58
62
|
['.png', 'image/png'],
|
|
@@ -84,6 +88,9 @@ export function normalizeContentPath(inputPath) {
|
|
|
84
88
|
resolved.includes('/../')) {
|
|
85
89
|
return null;
|
|
86
90
|
}
|
|
91
|
+
if (resolved.split('/').some(isIgnoredContentName)) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
87
94
|
return resolved;
|
|
88
95
|
}
|
|
89
96
|
export function normalizeDirectoryPath(inputPath) {
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { ManagedIndexEntry, ParsedDocumentMeta } from './markdown.js';
|
|
2
2
|
import type { EditLinkConfig, ResolvedSiteConfig, SiteLogo, SiteNavItem, SiteSocialLink } from './site-config.js';
|
|
3
|
-
import type { TemplateName } from '../html/template-kind.js';
|
|
4
|
-
import type { BuiltInThemeName } from '../html/theme.js';
|
|
5
3
|
type MaybePromise<T> = T | Promise<T>;
|
|
6
4
|
export interface IndexTransformContext {
|
|
7
5
|
mode: 'build' | 'render';
|
|
@@ -11,7 +9,7 @@ export interface IndexTransformContext {
|
|
|
11
9
|
siteConfig?: ResolvedSiteConfig;
|
|
12
10
|
}
|
|
13
11
|
export interface PageRenderModel {
|
|
14
|
-
kind: '
|
|
12
|
+
kind: 'page' | 'listing';
|
|
15
13
|
requestPath: string;
|
|
16
14
|
sourcePath: string;
|
|
17
15
|
siteTitle: string;
|
|
@@ -27,8 +25,6 @@ export interface PageRenderModel {
|
|
|
27
25
|
date?: string;
|
|
28
26
|
showSummary: boolean;
|
|
29
27
|
showDate: boolean;
|
|
30
|
-
theme: BuiltInThemeName;
|
|
31
|
-
template: TemplateName;
|
|
32
28
|
topNav: SiteNavItem[];
|
|
33
29
|
footerNav: SiteNavItem[];
|
|
34
30
|
footerText?: string;
|
|
@@ -38,10 +34,10 @@ export interface PageRenderModel {
|
|
|
38
34
|
stylesheetContent?: string;
|
|
39
35
|
canonicalPath?: string;
|
|
40
36
|
alternateMarkdownPath?: string;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
37
|
+
listingEntries: ManagedIndexEntry[];
|
|
38
|
+
listingRequestPath: string;
|
|
39
|
+
listingInitialPostCount: number;
|
|
40
|
+
listingLoadMoreStep: number;
|
|
45
41
|
searchEnabled: boolean;
|
|
46
42
|
}
|
|
47
43
|
export interface RenderHookContext {
|
package/dist/core/markdown.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import matter from 'gray-matter';
|
|
2
2
|
import { remark } from 'remark';
|
|
3
3
|
import remarkGfm from 'remark-gfm';
|
|
4
|
-
import
|
|
4
|
+
import remarkRehype from 'remark-rehype';
|
|
5
|
+
import rehypeRaw from 'rehype-raw';
|
|
6
|
+
import rehypeStringify from 'rehype-stringify';
|
|
5
7
|
export function getDocumentTitle(meta, body, fallback) {
|
|
6
8
|
return (firstNonEmptyString(meta.title, meta.name) ??
|
|
7
9
|
extractFirstHeading(body) ??
|
|
@@ -24,7 +26,9 @@ export async function parseMarkdownDocument(sourcePath, markdown) {
|
|
|
24
26
|
export async function renderMarkdown(markdown) {
|
|
25
27
|
const output = await remark()
|
|
26
28
|
.use(remarkGfm)
|
|
27
|
-
.use(
|
|
29
|
+
.use(remarkRehype, { allowDangerousHtml: true })
|
|
30
|
+
.use(rehypeRaw)
|
|
31
|
+
.use(rehypeStringify, { allowDangerousHtml: true })
|
|
28
32
|
.process(markdown);
|
|
29
33
|
return String(output);
|
|
30
34
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { isIgnoredContentName } from './content-store.js';
|
|
2
3
|
import { inferDirectoryContentType } from './content-type.js';
|
|
3
4
|
import { getDirectoryIndexCandidates } from './directory-index.js';
|
|
4
5
|
import { extractManagedIndexEntries, getDocumentSummary, getDocumentTitle as getParsedDocumentTitle, parseMarkdownDocument, stripManagedIndexBlock, stripManagedIndexLinks, } from './markdown.js';
|
|
5
6
|
import { applyIndexTransforms, renderFooterOverride, renderHeaderOverride, renderPageWithPlugins, transformHtmlWithPlugins, } from './extensions.js';
|
|
6
7
|
import { handleApiRoute } from './api.js';
|
|
7
8
|
import { normalizeRequestPath, resolveRequest } from './router.js';
|
|
8
|
-
import { escapeHtml,
|
|
9
|
+
import { escapeHtml, renderListingArticleItems, renderDocument, } from '../html/template.js';
|
|
9
10
|
export async function handleSiteRequest(store, pathname, options) {
|
|
10
11
|
const plugins = options.plugins ?? [];
|
|
11
12
|
const searchEnabled = options.searchApi !== undefined;
|
|
@@ -21,7 +22,7 @@ export async function handleSiteRequest(store, pathname, options) {
|
|
|
21
22
|
return renderSitemap(store, options);
|
|
22
23
|
}
|
|
23
24
|
const resolved = resolveRequest(pathname);
|
|
24
|
-
const
|
|
25
|
+
const listingFragmentRequest = getListingFragmentRequest(options.searchParams);
|
|
25
26
|
const negotiatedMarkdown = shouldServeMarkdownForRequest(resolved, options.acceptHeader);
|
|
26
27
|
if (resolved.kind === 'not-found' || !resolved.sourcePath) {
|
|
27
28
|
const aliasRedirect = await tryRedirectAlias(store, pathname, options);
|
|
@@ -82,21 +83,16 @@ export async function handleSiteRequest(store, pathname, options) {
|
|
|
82
83
|
: isRootHomeRequest(resolved.requestPath) && navigation.items.length > 0
|
|
83
84
|
? stripManagedIndexLinks(entry.text, new Set(navigation.items.map((item) => item.href)))
|
|
84
85
|
: entry.text;
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return renderCatalogPostsFragment(catalogEntries, catalogFragmentRequest);
|
|
96
|
-
}
|
|
97
|
-
const documentBody = options.siteConfig.template === 'catalog'
|
|
98
|
-
? stripManagedIndexBlock(renderedBody)
|
|
99
|
-
: renderedBody;
|
|
86
|
+
const listingEntries = await applyIndexTransforms(extractManagedIndexEntries(renderedBody), plugins, {
|
|
87
|
+
mode: 'render',
|
|
88
|
+
requestPath: resolved.requestPath,
|
|
89
|
+
sourcePath: resolved.sourcePath,
|
|
90
|
+
siteConfig: options.siteConfig,
|
|
91
|
+
});
|
|
92
|
+
if (listingFragmentRequest !== null && listingEntries.length > 0) {
|
|
93
|
+
return renderListingPostsFragment(listingEntries, listingFragmentRequest);
|
|
94
|
+
}
|
|
95
|
+
const documentBody = listingEntries.length > 0 ? stripManagedIndexBlock(renderedBody) : renderedBody;
|
|
100
96
|
const renderedParsed = documentBody === entry.text
|
|
101
97
|
? parsed
|
|
102
98
|
: await parseMarkdownDocument(resolved.sourcePath, documentBody);
|
|
@@ -107,18 +103,19 @@ export async function handleSiteRequest(store, pathname, options) {
|
|
|
107
103
|
renderedParsed,
|
|
108
104
|
siteConfig: options.siteConfig,
|
|
109
105
|
topNav: navigation.items,
|
|
110
|
-
|
|
106
|
+
listingEntries,
|
|
111
107
|
searchEnabled,
|
|
112
108
|
plugins,
|
|
113
109
|
varyOnAccept: shouldVaryOnAccept(resolved),
|
|
114
110
|
});
|
|
115
111
|
}
|
|
116
|
-
function
|
|
117
|
-
|
|
112
|
+
function getListingFragmentRequest(searchParams) {
|
|
113
|
+
const format = searchParams?.get('listing-format') ?? searchParams?.get('catalog-format');
|
|
114
|
+
if (format !== 'posts') {
|
|
118
115
|
return null;
|
|
119
116
|
}
|
|
120
|
-
const offset = normalizeNonNegativeInteger(searchParams
|
|
121
|
-
const limit = normalizePositiveInteger(searchParams
|
|
117
|
+
const offset = normalizeNonNegativeInteger(searchParams?.get('listing-offset') ?? searchParams?.get('catalog-offset') ?? null);
|
|
118
|
+
const limit = normalizePositiveInteger(searchParams?.get('listing-limit') ?? searchParams?.get('catalog-limit') ?? null);
|
|
122
119
|
if (offset === null || limit === null) {
|
|
123
120
|
return null;
|
|
124
121
|
}
|
|
@@ -140,7 +137,7 @@ function normalizePositiveInteger(value) {
|
|
|
140
137
|
}
|
|
141
138
|
function buildPageRenderModel(options) {
|
|
142
139
|
return {
|
|
143
|
-
kind: options.
|
|
140
|
+
kind: options.listingEntries.length > 0 ? 'listing' : 'page',
|
|
144
141
|
requestPath: options.resolvedRequestPath,
|
|
145
142
|
sourcePath: options.sourcePath,
|
|
146
143
|
siteTitle: options.siteConfig.siteTitle,
|
|
@@ -158,8 +155,6 @@ function buildPageRenderModel(options) {
|
|
|
158
155
|
date: options.siteConfig.showDate === false ? undefined : options.parsed.meta.date,
|
|
159
156
|
showSummary: options.siteConfig.showSummary,
|
|
160
157
|
showDate: options.siteConfig.showDate,
|
|
161
|
-
theme: options.siteConfig.theme,
|
|
162
|
-
template: options.siteConfig.template,
|
|
163
158
|
topNav: options.topNav,
|
|
164
159
|
footerNav: options.siteConfig.footerNav,
|
|
165
160
|
footerText: options.siteConfig.footerText,
|
|
@@ -169,10 +164,10 @@ function buildPageRenderModel(options) {
|
|
|
169
164
|
stylesheetContent: options.siteConfig.stylesheetContent,
|
|
170
165
|
canonicalPath: getCanonicalHtmlPathForContentPath(options.sourcePath),
|
|
171
166
|
alternateMarkdownPath: getMarkdownRequestPathForContentPath(options.sourcePath),
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
167
|
+
listingEntries: options.listingEntries,
|
|
168
|
+
listingRequestPath: options.resolvedRequestPath,
|
|
169
|
+
listingInitialPostCount: options.siteConfig.listingInitialPostCount,
|
|
170
|
+
listingLoadMoreStep: options.siteConfig.listingLoadMoreStep,
|
|
176
171
|
searchEnabled: options.searchEnabled,
|
|
177
172
|
};
|
|
178
173
|
}
|
|
@@ -184,7 +179,7 @@ async function renderStructuredPage(options) {
|
|
|
184
179
|
parsed: options.parsed,
|
|
185
180
|
siteConfig: options.siteConfig,
|
|
186
181
|
topNav: options.topNav,
|
|
187
|
-
|
|
182
|
+
listingEntries: options.listingEntries,
|
|
188
183
|
searchEnabled: options.searchEnabled,
|
|
189
184
|
});
|
|
190
185
|
const renderContext = {
|
|
@@ -198,7 +193,7 @@ async function renderStructuredPage(options) {
|
|
|
198
193
|
};
|
|
199
194
|
const headerHtml = await renderHeaderOverride(options.plugins, currentContext);
|
|
200
195
|
const footerHtml = await renderFooterOverride(options.plugins, currentContext);
|
|
201
|
-
return
|
|
196
|
+
return renderDocument({
|
|
202
197
|
siteTitle: currentPage.siteTitle,
|
|
203
198
|
siteDescription: currentPage.siteDescription,
|
|
204
199
|
siteUrl: currentPage.siteUrl,
|
|
@@ -211,8 +206,6 @@ async function renderStructuredPage(options) {
|
|
|
211
206
|
date: currentPage.date,
|
|
212
207
|
showSummary: currentPage.showSummary,
|
|
213
208
|
showDate: currentPage.showDate,
|
|
214
|
-
theme: currentPage.theme,
|
|
215
|
-
template: currentPage.template,
|
|
216
209
|
topNav: currentPage.topNav,
|
|
217
210
|
footerNav: currentPage.footerNav,
|
|
218
211
|
footerText: currentPage.footerText,
|
|
@@ -221,14 +214,14 @@ async function renderStructuredPage(options) {
|
|
|
221
214
|
stylesheetContent: currentPage.stylesheetContent,
|
|
222
215
|
canonicalPath: currentPage.canonicalPath,
|
|
223
216
|
alternateMarkdownPath: currentPage.alternateMarkdownPath,
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
217
|
+
listingEntries: currentPage.listingEntries,
|
|
218
|
+
listingRequestPath: currentPage.listingRequestPath,
|
|
219
|
+
listingInitialPostCount: currentPage.listingInitialPostCount,
|
|
220
|
+
listingLoadMoreStep: currentPage.listingLoadMoreStep,
|
|
228
221
|
searchEnabled: currentPage.searchEnabled,
|
|
229
222
|
headerHtml,
|
|
230
223
|
footerHtml,
|
|
231
|
-
})
|
|
224
|
+
});
|
|
232
225
|
});
|
|
233
226
|
const finalHtml = await transformHtmlWithPlugins(renderedPage.html, options.plugins, {
|
|
234
227
|
page: renderedPage.page,
|
|
@@ -242,7 +235,7 @@ async function renderStructuredPage(options) {
|
|
|
242
235
|
body: finalHtml,
|
|
243
236
|
};
|
|
244
237
|
}
|
|
245
|
-
function
|
|
238
|
+
function renderListingPostsFragment(entries, request) {
|
|
246
239
|
const articles = entries.filter((entry) => entry.kind === 'article');
|
|
247
240
|
const visibleArticles = articles.slice(request.offset, request.offset + request.limit);
|
|
248
241
|
const nextOffset = request.offset + visibleArticles.length;
|
|
@@ -252,7 +245,7 @@ function renderCatalogPostsFragment(entries, request) {
|
|
|
252
245
|
'content-type': 'application/json; charset=utf-8',
|
|
253
246
|
},
|
|
254
247
|
body: JSON.stringify({
|
|
255
|
-
itemsHtml:
|
|
248
|
+
itemsHtml: renderListingArticleItems(visibleArticles),
|
|
256
249
|
hasMore: nextOffset < articles.length,
|
|
257
250
|
nextOffset,
|
|
258
251
|
}),
|
|
@@ -424,8 +417,6 @@ async function renderDirectoryListing(store, requestPath, siteConfig, searchEnab
|
|
|
424
417
|
body,
|
|
425
418
|
showSummary: false,
|
|
426
419
|
showDate: false,
|
|
427
|
-
theme: siteConfig.theme,
|
|
428
|
-
template: siteConfig.template,
|
|
429
420
|
topNav: navigation.items,
|
|
430
421
|
footerNav: siteConfig.footerNav,
|
|
431
422
|
footerText: siteConfig.footerText,
|
|
@@ -484,22 +475,17 @@ async function tryRenderAlternateDirectoryIndex(store, requestPath, options) {
|
|
|
484
475
|
: isRootHomeRequest(requestPath) && navigation.items.length > 0
|
|
485
476
|
? stripManagedIndexLinks(entry.text, new Set(navigation.items.map((item) => item.href)))
|
|
486
477
|
: entry.text;
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
return renderCatalogPostsFragment(catalogEntries, catalogFragmentRequest);
|
|
499
|
-
}
|
|
500
|
-
const documentBody = options.siteConfig.template === 'catalog'
|
|
501
|
-
? stripManagedIndexBlock(renderedBody)
|
|
502
|
-
: renderedBody;
|
|
478
|
+
const listingEntries = await applyIndexTransforms(extractManagedIndexEntries(renderedBody), plugins, {
|
|
479
|
+
mode: 'render',
|
|
480
|
+
requestPath,
|
|
481
|
+
sourcePath: candidatePath,
|
|
482
|
+
siteConfig: options.siteConfig,
|
|
483
|
+
});
|
|
484
|
+
const listingFragmentRequest = getListingFragmentRequest(options.searchParams);
|
|
485
|
+
if (listingFragmentRequest !== null && listingEntries.length > 0) {
|
|
486
|
+
return renderListingPostsFragment(listingEntries, listingFragmentRequest);
|
|
487
|
+
}
|
|
488
|
+
const documentBody = listingEntries.length > 0 ? stripManagedIndexBlock(renderedBody) : renderedBody;
|
|
503
489
|
const renderedParsed = documentBody === entry.text
|
|
504
490
|
? parsed
|
|
505
491
|
: await parseMarkdownDocument(candidatePath, documentBody);
|
|
@@ -510,7 +496,7 @@ async function tryRenderAlternateDirectoryIndex(store, requestPath, options) {
|
|
|
510
496
|
renderedParsed,
|
|
511
497
|
siteConfig: options.siteConfig,
|
|
512
498
|
topNav: navigation.items,
|
|
513
|
-
|
|
499
|
+
listingEntries,
|
|
514
500
|
searchEnabled: options.searchApi !== undefined,
|
|
515
501
|
plugins,
|
|
516
502
|
});
|
|
@@ -750,7 +736,7 @@ async function inspectDirectoryShape(store, directoryPath) {
|
|
|
750
736
|
let hasExtraMarkdownFiles = false;
|
|
751
737
|
let hasAssetFiles = false;
|
|
752
738
|
for (const entry of entries) {
|
|
753
|
-
if (entry.name
|
|
739
|
+
if (isIgnoredContentName(entry.name)) {
|
|
754
740
|
continue;
|
|
755
741
|
}
|
|
756
742
|
if (entry.kind === 'directory') {
|