mdorigin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +29 -0
  2. package/dist/adapters/cloudflare.d.ts +17 -0
  3. package/dist/adapters/cloudflare.js +53 -0
  4. package/dist/adapters/node.d.ts +11 -0
  5. package/dist/adapters/node.js +115 -0
  6. package/dist/cli/build-cloudflare.d.ts +1 -0
  7. package/dist/cli/build-cloudflare.js +48 -0
  8. package/dist/cli/build-index.d.ts +1 -0
  9. package/dist/cli/build-index.js +35 -0
  10. package/dist/cli/dev.d.ts +1 -0
  11. package/dist/cli/dev.js +53 -0
  12. package/dist/cli/init-cloudflare.d.ts +1 -0
  13. package/dist/cli/init-cloudflare.js +59 -0
  14. package/dist/cli/main.d.ts +1 -0
  15. package/dist/cli/main.js +38 -0
  16. package/dist/cloudflare-runtime.d.ts +2 -0
  17. package/dist/cloudflare-runtime.js +1 -0
  18. package/dist/cloudflare.d.ts +31 -0
  19. package/dist/cloudflare.js +130 -0
  20. package/dist/core/content-store.d.ts +27 -0
  21. package/dist/core/content-store.js +95 -0
  22. package/dist/core/content-type.d.ts +9 -0
  23. package/dist/core/content-type.js +19 -0
  24. package/dist/core/directory-index.d.ts +2 -0
  25. package/dist/core/directory-index.js +5 -0
  26. package/dist/core/markdown.d.ts +20 -0
  27. package/dist/core/markdown.js +135 -0
  28. package/dist/core/request-handler.d.ts +12 -0
  29. package/dist/core/request-handler.js +322 -0
  30. package/dist/core/router.d.ts +7 -0
  31. package/dist/core/router.js +82 -0
  32. package/dist/core/site-config.d.ts +38 -0
  33. package/dist/core/site-config.js +123 -0
  34. package/dist/html/template-kind.d.ts +1 -0
  35. package/dist/html/template-kind.js +1 -0
  36. package/dist/html/template.d.ts +19 -0
  37. package/dist/html/template.js +67 -0
  38. package/dist/html/theme.d.ts +2 -0
  39. package/dist/html/theme.js +608 -0
  40. package/dist/index-builder.d.ts +13 -0
  41. package/dist/index-builder.js +299 -0
  42. package/package.json +66 -0
package/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # mdorigin
2
+
3
+ `mdorigin` is a markdown-first publishing engine.
4
+
5
+ It treats markdown as the only source of truth, serves raw `.md` directly for agents, and renders `.html` views for humans from the same directory tree.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install mdorigin
11
+ ```
12
+
13
+ ## Development
14
+
15
+ ```bash
16
+ npm install
17
+ npm run check
18
+ npm run dev -- --root docs/site
19
+ npm run build:index -- --root docs/site
20
+ ```
21
+
22
+ ## Docs
23
+
24
+ - Getting started: [`docs/site/guides/getting-started.md`](docs/site/guides/getting-started.md)
25
+ - Routing model: [`docs/site/concepts/routing.md`](docs/site/concepts/routing.md)
26
+ - Directory indexes: [`docs/site/concepts/directory-indexes.md`](docs/site/concepts/directory-indexes.md)
27
+ - Configuration: [`docs/site/reference/configuration.md`](docs/site/reference/configuration.md)
28
+ - CLI: [`docs/site/reference/cli.md`](docs/site/reference/cli.md)
29
+ - Cloudflare deployment: [`docs/site/guides/cloudflare.md`](docs/site/guides/cloudflare.md)
@@ -0,0 +1,17 @@
1
+ import { type ContentEntryKind } from '../core/content-store.js';
2
+ import type { ResolvedSiteConfig } from '../core/site-config.js';
3
+ export interface CloudflareManifestEntry {
4
+ path: string;
5
+ kind: ContentEntryKind;
6
+ mediaType: string;
7
+ text?: string;
8
+ base64?: string;
9
+ }
10
+ export interface CloudflareManifest {
11
+ entries: CloudflareManifestEntry[];
12
+ siteConfig?: ResolvedSiteConfig;
13
+ }
14
+ export interface ExportedHandlerLike {
15
+ fetch(request: Request): Promise<Response>;
16
+ }
17
+ export declare function createCloudflareWorker(manifest: CloudflareManifest): ExportedHandlerLike;
@@ -0,0 +1,53 @@
1
+ import { MemoryContentStore, } from '../core/content-store.js';
2
+ import { handleSiteRequest } from '../core/request-handler.js';
3
+ export function createCloudflareWorker(manifest) {
4
+ const store = new MemoryContentStore(manifest.entries.map((entry) => {
5
+ if (entry.kind === 'text') {
6
+ return {
7
+ path: entry.path,
8
+ kind: 'text',
9
+ mediaType: entry.mediaType,
10
+ text: entry.text ?? '',
11
+ };
12
+ }
13
+ return {
14
+ path: entry.path,
15
+ kind: 'binary',
16
+ mediaType: entry.mediaType,
17
+ bytes: decodeBase64(entry.base64 ?? ''),
18
+ };
19
+ }));
20
+ return {
21
+ async fetch(request) {
22
+ const url = new URL(request.url);
23
+ const siteResponse = await handleSiteRequest(store, url.pathname, {
24
+ draftMode: 'exclude',
25
+ siteConfig: manifest.siteConfig ?? {
26
+ siteTitle: 'mdorigin',
27
+ showDate: true,
28
+ showSummary: true,
29
+ theme: 'paper',
30
+ template: 'document',
31
+ topNav: [],
32
+ showHomeIndex: true,
33
+ siteTitleConfigured: false,
34
+ siteDescriptionConfigured: false,
35
+ },
36
+ });
37
+ const headers = new Headers(siteResponse.headers);
38
+ const body = siteResponse.body instanceof Uint8Array
39
+ ? new Blob([Uint8Array.from(siteResponse.body)], {
40
+ type: siteResponse.headers['content-type'],
41
+ })
42
+ : siteResponse.body ?? '';
43
+ return new Response(body, {
44
+ status: siteResponse.status,
45
+ headers,
46
+ });
47
+ },
48
+ };
49
+ }
50
+ function decodeBase64(value) {
51
+ const decoded = atob(value);
52
+ return Uint8Array.from(decoded, (character) => character.charCodeAt(0));
53
+ }
@@ -0,0 +1,11 @@
1
+ import { type IncomingMessage, type ServerResponse } from 'node:http';
2
+ import type { ContentStore } from '../core/content-store.js';
3
+ import type { ResolvedSiteConfig } from '../core/site-config.js';
4
+ export interface NodeAdapterOptions {
5
+ rootDir: string;
6
+ draftMode: 'include' | 'exclude';
7
+ siteConfig: ResolvedSiteConfig;
8
+ }
9
+ export declare function createFileSystemContentStore(rootDir: string): ContentStore;
10
+ export declare function createNodeRequestListener(options: NodeAdapterOptions): (request: IncomingMessage, response: ServerResponse) => Promise<void>;
11
+ export declare function createNodeServer(options: NodeAdapterOptions): import("http").Server<typeof IncomingMessage, typeof ServerResponse>;
@@ -0,0 +1,115 @@
1
+ import { createServer } from 'node:http';
2
+ import { readFile, readdir } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { getMediaTypeForPath, isLikelyTextPath, normalizeContentPath, normalizeDirectoryPath, } from '../core/content-store.js';
5
+ import { handleSiteRequest } from '../core/request-handler.js';
6
+ export function createFileSystemContentStore(rootDir) {
7
+ const resolvedRootDir = path.resolve(rootDir);
8
+ return {
9
+ async get(contentPath) {
10
+ const normalizedPath = normalizeContentPath(contentPath);
11
+ if (normalizedPath === null) {
12
+ return null;
13
+ }
14
+ const filePath = path.resolve(resolvedRootDir, normalizedPath);
15
+ if (!filePath.startsWith(`${resolvedRootDir}${path.sep}`) && filePath !== resolvedRootDir) {
16
+ return null;
17
+ }
18
+ try {
19
+ const mediaType = getMediaTypeForPath(normalizedPath);
20
+ if (isLikelyTextPath(normalizedPath)) {
21
+ const text = await readFile(filePath, 'utf8');
22
+ return {
23
+ path: normalizedPath,
24
+ kind: 'text',
25
+ mediaType,
26
+ text,
27
+ };
28
+ }
29
+ const bytes = new Uint8Array(await readFile(filePath));
30
+ return {
31
+ path: normalizedPath,
32
+ kind: 'binary',
33
+ mediaType,
34
+ bytes,
35
+ };
36
+ }
37
+ catch (error) {
38
+ if (isNodeNotFound(error)) {
39
+ return null;
40
+ }
41
+ throw error;
42
+ }
43
+ },
44
+ async listDirectory(contentPath) {
45
+ const normalizedPath = normalizeDirectoryPath(contentPath);
46
+ if (normalizedPath === null) {
47
+ return null;
48
+ }
49
+ const directoryPath = path.resolve(resolvedRootDir, normalizedPath);
50
+ if (!directoryPath.startsWith(`${resolvedRootDir}${path.sep}`) &&
51
+ directoryPath !== resolvedRootDir) {
52
+ return null;
53
+ }
54
+ try {
55
+ const entries = await readdir(directoryPath, { withFileTypes: true });
56
+ return entries
57
+ .filter((entry) => !entry.name.startsWith('.'))
58
+ .map((entry) => ({
59
+ name: entry.name,
60
+ path: normalizedPath === ''
61
+ ? entry.name
62
+ : `${normalizedPath}/${entry.name}`,
63
+ kind: entry.isDirectory() ? 'directory' : 'file',
64
+ }))
65
+ .sort((left, right) => {
66
+ if (left.kind !== right.kind) {
67
+ return left.kind === 'directory' ? -1 : 1;
68
+ }
69
+ return left.name.localeCompare(right.name);
70
+ });
71
+ }
72
+ catch (error) {
73
+ if (isNodeNotFound(error)) {
74
+ return null;
75
+ }
76
+ throw error;
77
+ }
78
+ },
79
+ };
80
+ }
81
+ export function createNodeRequestListener(options) {
82
+ const store = createFileSystemContentStore(options.rootDir);
83
+ return async function onRequest(request, response) {
84
+ try {
85
+ const url = new URL(request.url ?? '/', 'http://localhost');
86
+ const siteResponse = await handleSiteRequest(store, url.pathname, {
87
+ draftMode: options.draftMode,
88
+ siteConfig: options.siteConfig,
89
+ });
90
+ response.statusCode = siteResponse.status;
91
+ for (const [headerName, headerValue] of Object.entries(siteResponse.headers)) {
92
+ response.setHeader(headerName, headerValue);
93
+ }
94
+ if (siteResponse.body instanceof Uint8Array) {
95
+ response.end(Buffer.from(siteResponse.body));
96
+ return;
97
+ }
98
+ response.end(siteResponse.body ?? '');
99
+ }
100
+ catch (error) {
101
+ response.statusCode = 500;
102
+ response.setHeader('content-type', 'text/plain; charset=utf-8');
103
+ response.end(error instanceof Error ? error.message : 'Unexpected server error');
104
+ }
105
+ };
106
+ }
107
+ export function createNodeServer(options) {
108
+ return createServer(createNodeRequestListener(options));
109
+ }
110
+ function isNodeNotFound(error) {
111
+ return (typeof error === 'object' &&
112
+ error !== null &&
113
+ 'code' in error &&
114
+ error.code === 'ENOENT');
115
+ }
@@ -0,0 +1 @@
1
+ export declare function runBuildCloudflareCommand(argv: string[]): Promise<void>;
@@ -0,0 +1,48 @@
1
+ import path from 'node:path';
2
+ import { createFileSystemContentStore } from '../adapters/node.js';
3
+ import { writeCloudflareBundle } from '../cloudflare.js';
4
+ import { applySiteConfigFrontmatterDefaults, loadSiteConfig, } from '../core/site-config.js';
5
+ export async function runBuildCloudflareCommand(argv) {
6
+ const args = parseArgs(argv);
7
+ if (!args.root) {
8
+ console.error('Usage: mdorigin build cloudflare --root <content-dir> [--out ./dist/cloudflare] [--config mdorigin.config.json]');
9
+ process.exitCode = 1;
10
+ return;
11
+ }
12
+ const rootDir = path.resolve(args.root);
13
+ const loadedSiteConfig = await loadSiteConfig({
14
+ cwd: process.cwd(),
15
+ rootDir,
16
+ configPath: args.config,
17
+ });
18
+ const store = createFileSystemContentStore(rootDir);
19
+ const siteConfig = await applySiteConfigFrontmatterDefaults(store, loadedSiteConfig);
20
+ const result = await writeCloudflareBundle({
21
+ rootDir,
22
+ outDir: path.resolve(args.out ?? 'dist/cloudflare'),
23
+ siteConfig,
24
+ });
25
+ console.log(`cloudflare worker written to ${result.workerFile}`);
26
+ }
27
+ function parseArgs(argv) {
28
+ const result = {};
29
+ for (let index = 0; index < argv.length; index += 1) {
30
+ const argument = argv[index];
31
+ const nextValue = argv[index + 1];
32
+ if (argument === '--root' && nextValue) {
33
+ result.root = nextValue;
34
+ index += 1;
35
+ continue;
36
+ }
37
+ if (argument === '--out' && nextValue) {
38
+ result.out = nextValue;
39
+ index += 1;
40
+ continue;
41
+ }
42
+ if (argument === '--config' && nextValue) {
43
+ result.config = nextValue;
44
+ index += 1;
45
+ }
46
+ }
47
+ return result;
48
+ }
@@ -0,0 +1 @@
1
+ export declare function runBuildIndexCommand(argv: string[]): Promise<void>;
@@ -0,0 +1,35 @@
1
+ import path from 'node:path';
2
+ import { buildDirectoryIndexes } from '../index-builder.js';
3
+ export async function runBuildIndexCommand(argv) {
4
+ const args = parseArgs(argv);
5
+ if (!args.root && !args.dir) {
6
+ console.error('Usage: mdorigin build index (--root <content-dir> | --dir <content-dir>)');
7
+ process.exitCode = 1;
8
+ return;
9
+ }
10
+ const result = await buildDirectoryIndexes({
11
+ rootDir: args.root ? path.resolve(args.root) : undefined,
12
+ dir: args.dir ? path.resolve(args.dir) : undefined,
13
+ });
14
+ console.log(`updated ${result.updatedFiles.length} index file(s)`);
15
+ if (result.skippedDirectories.length > 0) {
16
+ console.log(`skipped ${result.skippedDirectories.length} director${result.skippedDirectories.length === 1 ? 'y' : 'ies'} without index.md`);
17
+ }
18
+ }
19
+ function parseArgs(argv) {
20
+ const result = {};
21
+ for (let index = 0; index < argv.length; index += 1) {
22
+ const argument = argv[index];
23
+ const nextValue = argv[index + 1];
24
+ if (argument === '--root' && nextValue) {
25
+ result.root = nextValue;
26
+ index += 1;
27
+ continue;
28
+ }
29
+ if (argument === '--dir' && nextValue) {
30
+ result.dir = nextValue;
31
+ index += 1;
32
+ }
33
+ }
34
+ return result;
35
+ }
@@ -0,0 +1 @@
1
+ export declare function runDevCommand(argv: string[]): Promise<void>;
@@ -0,0 +1,53 @@
1
+ import path from 'node:path';
2
+ import { createFileSystemContentStore, createNodeServer } from '../adapters/node.js';
3
+ import { applySiteConfigFrontmatterDefaults, loadSiteConfig, } from '../core/site-config.js';
4
+ export async function runDevCommand(argv) {
5
+ const args = parseArgs(argv);
6
+ if (!args.root) {
7
+ console.error('Usage: mdorigin dev --root <content-dir> [--port 3000] [--config mdorigin.config.json]');
8
+ process.exitCode = 1;
9
+ return;
10
+ }
11
+ const rootDir = path.resolve(args.root);
12
+ const port = args.port ?? 3000;
13
+ const loadedSiteConfig = await loadSiteConfig({
14
+ cwd: process.cwd(),
15
+ rootDir,
16
+ configPath: args.config,
17
+ });
18
+ const store = createFileSystemContentStore(rootDir);
19
+ const siteConfig = await applySiteConfigFrontmatterDefaults(store, loadedSiteConfig);
20
+ const server = createNodeServer({
21
+ rootDir,
22
+ draftMode: 'include',
23
+ siteConfig,
24
+ });
25
+ await new Promise((resolve, reject) => {
26
+ server.once('error', reject);
27
+ server.listen(port, () => resolve());
28
+ });
29
+ console.log(`mdorigin dev server listening on http://localhost:${port}`);
30
+ console.log(`content root: ${rootDir}`);
31
+ }
32
+ function parseArgs(argv) {
33
+ const result = {};
34
+ for (let index = 0; index < argv.length; index += 1) {
35
+ const argument = argv[index];
36
+ const nextValue = argv[index + 1];
37
+ if (argument === '--root' && nextValue) {
38
+ result.root = nextValue;
39
+ index += 1;
40
+ continue;
41
+ }
42
+ if (argument === '--port' && nextValue) {
43
+ result.port = Number.parseInt(nextValue, 10);
44
+ index += 1;
45
+ continue;
46
+ }
47
+ if (argument === '--config' && nextValue) {
48
+ result.config = nextValue;
49
+ index += 1;
50
+ }
51
+ }
52
+ return result;
53
+ }
@@ -0,0 +1 @@
1
+ export declare function runInitCloudflareCommand(argv: string[]): Promise<void>;
@@ -0,0 +1,59 @@
1
+ import path from 'node:path';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { initCloudflareProject } from '../cloudflare.js';
4
+ export async function runInitCloudflareCommand(argv) {
5
+ const args = parseArgs(argv);
6
+ const projectDir = path.resolve(args.dir ?? '.');
7
+ const workerEntry = path.resolve(projectDir, args.entry ?? 'dist/cloudflare/worker.mjs');
8
+ const siteTitle = args.name ? undefined : await inferSiteTitleFromWorkerEntry(workerEntry);
9
+ const result = await initCloudflareProject({
10
+ projectDir,
11
+ workerEntry,
12
+ workerName: args.name,
13
+ siteTitle,
14
+ compatibilityDate: args.compatibilityDate,
15
+ force: args.force,
16
+ });
17
+ console.log(`wrangler config written to ${result.configFile}`);
18
+ }
19
+ function parseArgs(argv) {
20
+ const result = {};
21
+ for (let index = 0; index < argv.length; index += 1) {
22
+ const argument = argv[index];
23
+ const nextValue = argv[index + 1];
24
+ if (argument === '--dir' && nextValue) {
25
+ result.dir = nextValue;
26
+ index += 1;
27
+ continue;
28
+ }
29
+ if (argument === '--entry' && nextValue) {
30
+ result.entry = nextValue;
31
+ index += 1;
32
+ continue;
33
+ }
34
+ if (argument === '--name' && nextValue) {
35
+ result.name = nextValue;
36
+ index += 1;
37
+ continue;
38
+ }
39
+ if (argument === '--compatibility-date' && nextValue) {
40
+ result.compatibilityDate = nextValue;
41
+ index += 1;
42
+ continue;
43
+ }
44
+ if (argument === '--force') {
45
+ result.force = true;
46
+ }
47
+ }
48
+ return result;
49
+ }
50
+ async function inferSiteTitleFromWorkerEntry(workerEntry) {
51
+ try {
52
+ const source = await readFile(workerEntry, 'utf8');
53
+ const match = source.match(/"siteTitle":\s*"([^"]+)"/);
54
+ return match?.[1];
55
+ }
56
+ catch {
57
+ return undefined;
58
+ }
59
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,38 @@
1
+ import { runBuildIndexCommand } from './build-index.js';
2
+ import { runBuildCloudflareCommand } from './build-cloudflare.js';
3
+ import { runDevCommand } from './dev.js';
4
+ import { runInitCloudflareCommand } from './init-cloudflare.js';
5
+ async function main() {
6
+ const [command, subcommand, ...rest] = process.argv.slice(2);
7
+ if (command === 'dev') {
8
+ await runDevCommand([subcommand, ...rest].filter(isDefined));
9
+ return;
10
+ }
11
+ if (command === 'build' && subcommand === 'cloudflare') {
12
+ await runBuildCloudflareCommand(rest);
13
+ return;
14
+ }
15
+ if (command === 'build' && subcommand === 'index') {
16
+ await runBuildIndexCommand(rest);
17
+ return;
18
+ }
19
+ if (command === 'init' && subcommand === 'cloudflare') {
20
+ await runInitCloudflareCommand(rest);
21
+ return;
22
+ }
23
+ console.error([
24
+ 'Usage:',
25
+ ' mdorigin dev --root <content-dir> [--port 3000] [--config mdorigin.config.json]',
26
+ ' mdorigin build index (--root <content-dir> | --dir <content-dir>)',
27
+ ' mdorigin build cloudflare --root <content-dir> [--out ./dist/cloudflare] [--config mdorigin.config.json]',
28
+ ' mdorigin init cloudflare [--dir .] [--entry ./dist/cloudflare/worker.mjs] [--name <worker-name>] [--compatibility-date 2026-03-20] [--force]',
29
+ ].join('\n'));
30
+ process.exitCode = 1;
31
+ }
32
+ void main().catch((error) => {
33
+ console.error(error instanceof Error ? error.message : String(error));
34
+ process.exitCode = 1;
35
+ });
36
+ function isDefined(value) {
37
+ return value !== undefined;
38
+ }
@@ -0,0 +1,2 @@
1
+ export { createCloudflareWorker } from './adapters/cloudflare.js';
2
+ export type { CloudflareManifest, CloudflareManifestEntry, ExportedHandlerLike, } from './adapters/cloudflare.js';
@@ -0,0 +1 @@
1
+ export { createCloudflareWorker } from './adapters/cloudflare.js';
@@ -0,0 +1,31 @@
1
+ import type { CloudflareManifest, CloudflareManifestEntry } from './adapters/cloudflare.js';
2
+ import { createCloudflareWorker } from './adapters/cloudflare.js';
3
+ import type { ResolvedSiteConfig } from './core/site-config.js';
4
+ export { createCloudflareWorker };
5
+ export type { CloudflareManifest, CloudflareManifestEntry };
6
+ export interface BuildCloudflareManifestOptions {
7
+ rootDir: string;
8
+ siteConfig: ResolvedSiteConfig;
9
+ }
10
+ export interface WriteCloudflareBundleOptions {
11
+ rootDir: string;
12
+ outDir: string;
13
+ siteConfig: ResolvedSiteConfig;
14
+ packageImport?: string;
15
+ }
16
+ export interface InitCloudflareProjectOptions {
17
+ projectDir: string;
18
+ workerEntry: string;
19
+ workerName?: string;
20
+ siteTitle?: string;
21
+ compatibilityDate?: string;
22
+ force?: boolean;
23
+ }
24
+ export declare function buildCloudflareManifest(options: BuildCloudflareManifestOptions): Promise<CloudflareManifest>;
25
+ export declare function writeCloudflareBundle(options: WriteCloudflareBundleOptions): Promise<{
26
+ manifest: CloudflareManifest;
27
+ workerFile: string;
28
+ }>;
29
+ export declare function initCloudflareProject(options: InitCloudflareProjectOptions): Promise<{
30
+ configFile: string;
31
+ }>;
@@ -0,0 +1,130 @@
1
+ import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { createCloudflareWorker } from './adapters/cloudflare.js';
4
+ import { getMediaTypeForPath, isLikelyTextPath, normalizeContentPath, } from './core/content-store.js';
5
+ export { createCloudflareWorker };
6
+ export async function buildCloudflareManifest(options) {
7
+ const rootDir = path.resolve(options.rootDir);
8
+ const files = await listFiles(rootDir);
9
+ const entries = [];
10
+ for (const filePath of files) {
11
+ const relativePath = path.relative(rootDir, filePath).replaceAll(path.sep, '/');
12
+ const normalizedPath = normalizeContentPath(relativePath);
13
+ if (normalizedPath === null) {
14
+ continue;
15
+ }
16
+ const mediaType = getMediaTypeForPath(normalizedPath);
17
+ if (isLikelyTextPath(normalizedPath)) {
18
+ entries.push({
19
+ path: normalizedPath,
20
+ kind: 'text',
21
+ mediaType,
22
+ text: await readFile(filePath, 'utf8'),
23
+ });
24
+ continue;
25
+ }
26
+ entries.push({
27
+ path: normalizedPath,
28
+ kind: 'binary',
29
+ mediaType,
30
+ base64: (await readFile(filePath)).toString('base64'),
31
+ });
32
+ }
33
+ entries.sort((left, right) => left.path.localeCompare(right.path));
34
+ return {
35
+ entries,
36
+ siteConfig: options.siteConfig,
37
+ };
38
+ }
39
+ export async function writeCloudflareBundle(options) {
40
+ const outDir = path.resolve(options.outDir);
41
+ const manifest = await buildCloudflareManifest({
42
+ rootDir: options.rootDir,
43
+ siteConfig: options.siteConfig,
44
+ });
45
+ const packageImport = options.packageImport ?? 'mdorigin/cloudflare-runtime';
46
+ const workerFile = path.join(outDir, 'worker.mjs');
47
+ const workerSource = [
48
+ `import { createCloudflareWorker } from '${packageImport}';`,
49
+ '',
50
+ `const manifest = ${JSON.stringify(manifest, null, 2)};`,
51
+ '',
52
+ 'export default createCloudflareWorker(manifest);',
53
+ '',
54
+ ].join('\n');
55
+ await mkdir(outDir, { recursive: true });
56
+ await writeFile(workerFile, workerSource, 'utf8');
57
+ return {
58
+ manifest,
59
+ workerFile,
60
+ };
61
+ }
62
+ export async function initCloudflareProject(options) {
63
+ const projectDir = path.resolve(options.projectDir);
64
+ const configFile = path.join(projectDir, 'wrangler.jsonc');
65
+ const existing = await pathExists(configFile);
66
+ if (existing && !options.force) {
67
+ throw new Error(`Refusing to overwrite ${configFile}. Re-run with --force to replace it.`);
68
+ }
69
+ const workerName = options.workerName ??
70
+ slugifyWorkerName(options.siteTitle) ??
71
+ 'mdorigin-site';
72
+ const compatibilityDate = options.compatibilityDate ?? '2026-03-20';
73
+ const wranglerConfig = [
74
+ '{',
75
+ ' "$schema": "./node_modules/wrangler/config-schema.json",',
76
+ ` "name": ${JSON.stringify(workerName)},`,
77
+ ` "main": ${JSON.stringify(toPosixPath(path.relative(projectDir, options.workerEntry)))},`,
78
+ ` "compatibility_date": ${JSON.stringify(compatibilityDate)},`,
79
+ ' "compatibility_flags": ["nodejs_compat"]',
80
+ '}',
81
+ '',
82
+ ].join('\n');
83
+ await mkdir(projectDir, { recursive: true });
84
+ await writeFile(configFile, wranglerConfig, 'utf8');
85
+ return { configFile };
86
+ }
87
+ async function listFiles(directory) {
88
+ const entries = await readdir(directory, { withFileTypes: true });
89
+ const files = [];
90
+ for (const entry of entries) {
91
+ const fullPath = path.join(directory, entry.name);
92
+ if (entry.isDirectory()) {
93
+ files.push(...(await listFiles(fullPath)));
94
+ continue;
95
+ }
96
+ if (entry.isFile()) {
97
+ files.push(fullPath);
98
+ }
99
+ }
100
+ return files;
101
+ }
102
+ async function pathExists(filePath) {
103
+ try {
104
+ await stat(filePath);
105
+ return true;
106
+ }
107
+ catch (error) {
108
+ if (typeof error === 'object' &&
109
+ error !== null &&
110
+ 'code' in error &&
111
+ error.code === 'ENOENT') {
112
+ return false;
113
+ }
114
+ throw error;
115
+ }
116
+ }
117
+ function toPosixPath(filePath) {
118
+ return filePath.replaceAll(path.sep, '/');
119
+ }
120
+ function slugifyWorkerName(value) {
121
+ if (!value) {
122
+ return null;
123
+ }
124
+ const slug = value
125
+ .toLowerCase()
126
+ .replace(/[^a-z0-9]+/g, '-')
127
+ .replace(/^-+|-+$/g, '')
128
+ .replace(/-{2,}/g, '-');
129
+ return slug === '' ? null : slug;
130
+ }