mdorigin 0.1.0 → 0.1.2

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 CHANGED
@@ -2,15 +2,51 @@
2
2
 
3
3
  `mdorigin` is a markdown-first publishing engine.
4
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.
5
+ It treats markdown as the only source of truth, serves raw `.md` directly for agents, renders `.html` views for humans from the same directory tree, can return markdown from extensionless routes when clients send `Accept: text/markdown`, supports frontmatter aliases for old URL redirects, and can publish skill-style bundles built around `SKILL.md`.
6
6
 
7
7
  ## Install
8
8
 
9
9
  ```bash
10
- npm install mdorigin
10
+ npm install -g mdorigin
11
11
  ```
12
12
 
13
- ## Development
13
+ Then run it directly:
14
+
15
+ ```bash
16
+ mdorigin dev --root docs/site
17
+ ```
18
+
19
+ If you prefer a project-local install instead, use `npm install --save-dev mdorigin` and run it with `npx --no-install mdorigin ...`.
20
+
21
+ ## Optional Search
22
+
23
+ `mdorigin` can build a local retrieval bundle through the optional [`indexbind`](https://github.com/jolestar/indexbind) package:
24
+
25
+ ```bash
26
+ npm install indexbind
27
+ mdorigin build search --root docs/site
28
+ mdorigin search --index dist/search "cloudflare deploy"
29
+ ```
30
+
31
+ `build search` now defaults to the higher-quality `model2vec` backend. If you need a smaller or faster fallback, you can opt back into hashing:
32
+
33
+ ```bash
34
+ mdorigin build search --root docs/site --embedding-backend hashing
35
+ ```
36
+
37
+ To expose the same search bundle from the site runtime:
38
+
39
+ ```bash
40
+ mdorigin dev --root docs/site --search dist/search
41
+ mdorigin build cloudflare --root docs/site --search dist/search
42
+ ```
43
+
44
+ Runtime endpoints:
45
+
46
+ - `/api/search?q=cloudflare+deploy`
47
+ - `/api/openapi.json`
48
+
49
+ ## Repo Development
14
50
 
15
51
  ```bash
16
52
  npm install
@@ -19,6 +55,19 @@ npm run dev -- --root docs/site
19
55
  npm run build:index -- --root docs/site
20
56
  ```
21
57
 
58
+ ## Release
59
+
60
+ Publishing is handled by GitHub Actions through npm trusted publishing.
61
+
62
+ - workflow: `.github/workflows/release.yml`
63
+ - trigger: push a tag like `v0.1.2`, or run the workflow manually
64
+
65
+ The npm package settings still need a one-time trusted publisher entry for:
66
+
67
+ - owner: `jolestar`
68
+ - repository: `mdorigin`
69
+ - workflow file: `release.yml`
70
+
22
71
  ## Docs
23
72
 
24
73
  - Getting started: [`docs/site/guides/getting-started.md`](docs/site/guides/getting-started.md)
@@ -1,5 +1,6 @@
1
1
  import { type ContentEntryKind } from '../core/content-store.js';
2
2
  import type { ResolvedSiteConfig } from '../core/site-config.js';
3
+ import { type SearchBundleEntry } from '../search.js';
3
4
  export interface CloudflareManifestEntry {
4
5
  path: string;
5
6
  kind: ContentEntryKind;
@@ -10,6 +11,7 @@ export interface CloudflareManifestEntry {
10
11
  export interface CloudflareManifest {
11
12
  entries: CloudflareManifestEntry[];
12
13
  siteConfig?: ResolvedSiteConfig;
14
+ searchEntries?: SearchBundleEntry[];
13
15
  }
14
16
  export interface ExportedHandlerLike {
15
17
  fetch(request: Request): Promise<Response>;
@@ -1,5 +1,6 @@
1
1
  import { MemoryContentStore, } from '../core/content-store.js';
2
2
  import { handleSiteRequest } from '../core/request-handler.js';
3
+ import { createSearchApiFromBundle } from '../search.js';
3
4
  export function createCloudflareWorker(manifest) {
4
5
  const store = new MemoryContentStore(manifest.entries.map((entry) => {
5
6
  if (entry.kind === 'text') {
@@ -17,6 +18,9 @@ export function createCloudflareWorker(manifest) {
17
18
  bytes: decodeBase64(entry.base64 ?? ''),
18
19
  };
19
20
  }));
21
+ const searchApi = manifest.searchEntries && manifest.searchEntries.length > 0
22
+ ? createSearchApiFromBundle(manifest.searchEntries)
23
+ : undefined;
20
24
  return {
21
25
  async fetch(request) {
22
26
  const url = new URL(request.url);
@@ -24,15 +28,28 @@ export function createCloudflareWorker(manifest) {
24
28
  draftMode: 'exclude',
25
29
  siteConfig: manifest.siteConfig ?? {
26
30
  siteTitle: 'mdorigin',
31
+ siteUrl: undefined,
32
+ favicon: undefined,
33
+ logo: undefined,
27
34
  showDate: true,
28
35
  showSummary: true,
29
36
  theme: 'paper',
30
37
  template: 'document',
31
38
  topNav: [],
39
+ footerNav: [],
40
+ footerText: undefined,
41
+ socialLinks: [],
42
+ editLink: undefined,
32
43
  showHomeIndex: true,
44
+ catalogInitialPostCount: 10,
45
+ catalogLoadMoreStep: 10,
33
46
  siteTitleConfigured: false,
34
47
  siteDescriptionConfigured: false,
35
48
  },
49
+ acceptHeader: request.headers.get('accept') ?? undefined,
50
+ searchParams: url.searchParams,
51
+ requestUrl: request.url,
52
+ searchApi,
36
53
  });
37
54
  const headers = new Headers(siteResponse.headers);
38
55
  const body = siteResponse.body instanceof Uint8Array
@@ -1,10 +1,12 @@
1
1
  import { type IncomingMessage, type ServerResponse } from 'node:http';
2
2
  import type { ContentStore } from '../core/content-store.js';
3
3
  import type { ResolvedSiteConfig } from '../core/site-config.js';
4
+ import type { SearchApi } from '../search.js';
4
5
  export interface NodeAdapterOptions {
5
6
  rootDir: string;
6
7
  draftMode: 'include' | 'exclude';
7
8
  siteConfig: ResolvedSiteConfig;
9
+ searchApi?: SearchApi;
8
10
  }
9
11
  export declare function createFileSystemContentStore(rootDir: string): ContentStore;
10
12
  export declare function createNodeRequestListener(options: NodeAdapterOptions): (request: IncomingMessage, response: ServerResponse) => Promise<void>;
@@ -1,5 +1,5 @@
1
1
  import { createServer } from 'node:http';
2
- import { readFile, readdir } from 'node:fs/promises';
2
+ import { readFile, readdir, stat } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { getMediaTypeForPath, isLikelyTextPath, normalizeContentPath, normalizeDirectoryPath, } from '../core/content-store.js';
5
5
  import { handleSiteRequest } from '../core/request-handler.js';
@@ -12,10 +12,14 @@ export function createFileSystemContentStore(rootDir) {
12
12
  return null;
13
13
  }
14
14
  const filePath = path.resolve(resolvedRootDir, normalizedPath);
15
- if (!filePath.startsWith(`${resolvedRootDir}${path.sep}`) && filePath !== resolvedRootDir) {
15
+ if (!isVisiblePathWithinRoot(resolvedRootDir, filePath)) {
16
16
  return null;
17
17
  }
18
18
  try {
19
+ const fileStats = await stat(filePath);
20
+ if (!fileStats.isFile()) {
21
+ return null;
22
+ }
19
23
  const mediaType = getMediaTypeForPath(normalizedPath);
20
24
  if (isLikelyTextPath(normalizedPath)) {
21
25
  const text = await readFile(filePath, 'utf8');
@@ -47,21 +51,49 @@ export function createFileSystemContentStore(rootDir) {
47
51
  return null;
48
52
  }
49
53
  const directoryPath = path.resolve(resolvedRootDir, normalizedPath);
50
- if (!directoryPath.startsWith(`${resolvedRootDir}${path.sep}`) &&
51
- directoryPath !== resolvedRootDir) {
54
+ if (!isVisiblePathWithinRoot(resolvedRootDir, directoryPath)) {
52
55
  return null;
53
56
  }
54
57
  try {
58
+ const directoryStats = await stat(directoryPath);
59
+ if (!directoryStats.isDirectory()) {
60
+ return null;
61
+ }
55
62
  const entries = await readdir(directoryPath, { withFileTypes: true });
56
- return entries
63
+ const resolvedEntries = await Promise.all(entries
57
64
  .filter((entry) => !entry.name.startsWith('.'))
58
- .map((entry) => ({
59
- name: entry.name,
60
- path: normalizedPath === ''
65
+ .map(async (entry) => {
66
+ const childVisiblePath = normalizedPath === ''
61
67
  ? entry.name
62
- : `${normalizedPath}/${entry.name}`,
63
- kind: entry.isDirectory() ? 'directory' : 'file',
64
- }))
68
+ : `${normalizedPath}/${entry.name}`;
69
+ const childFilePath = path.resolve(resolvedRootDir, childVisiblePath);
70
+ try {
71
+ const childStats = await stat(childFilePath);
72
+ if (childStats.isDirectory()) {
73
+ return {
74
+ name: entry.name,
75
+ path: childVisiblePath,
76
+ kind: 'directory',
77
+ };
78
+ }
79
+ if (childStats.isFile()) {
80
+ return {
81
+ name: entry.name,
82
+ path: childVisiblePath,
83
+ kind: 'file',
84
+ };
85
+ }
86
+ }
87
+ catch (error) {
88
+ if (isNodeNotFound(error)) {
89
+ return null;
90
+ }
91
+ throw error;
92
+ }
93
+ return null;
94
+ }));
95
+ return resolvedEntries
96
+ .filter((entry) => entry !== null)
65
97
  .sort((left, right) => {
66
98
  if (left.kind !== right.kind) {
67
99
  return left.kind === 'directory' ? -1 : 1;
@@ -78,6 +110,10 @@ export function createFileSystemContentStore(rootDir) {
78
110
  },
79
111
  };
80
112
  }
113
+ function isVisiblePathWithinRoot(rootDir, candidatePath) {
114
+ return (candidatePath === rootDir ||
115
+ candidatePath.startsWith(`${rootDir}${path.sep}`));
116
+ }
81
117
  export function createNodeRequestListener(options) {
82
118
  const store = createFileSystemContentStore(options.rootDir);
83
119
  return async function onRequest(request, response) {
@@ -86,6 +122,10 @@ export function createNodeRequestListener(options) {
86
122
  const siteResponse = await handleSiteRequest(store, url.pathname, {
87
123
  draftMode: options.draftMode,
88
124
  siteConfig: options.siteConfig,
125
+ acceptHeader: request.headers.accept,
126
+ searchParams: url.searchParams,
127
+ requestUrl: url.toString(),
128
+ searchApi: options.searchApi,
89
129
  });
90
130
  response.statusCode = siteResponse.status;
91
131
  for (const [headerName, headerValue] of Object.entries(siteResponse.headers)) {
@@ -5,7 +5,7 @@ import { applySiteConfigFrontmatterDefaults, loadSiteConfig, } from '../core/sit
5
5
  export async function runBuildCloudflareCommand(argv) {
6
6
  const args = parseArgs(argv);
7
7
  if (!args.root) {
8
- console.error('Usage: mdorigin build cloudflare --root <content-dir> [--out ./dist/cloudflare] [--config mdorigin.config.json]');
8
+ console.error('Usage: mdorigin build cloudflare --root <content-dir> [--out ./dist/cloudflare] [--config mdorigin.config.json] [--search ./dist/search]');
9
9
  process.exitCode = 1;
10
10
  return;
11
11
  }
@@ -21,6 +21,7 @@ export async function runBuildCloudflareCommand(argv) {
21
21
  rootDir,
22
22
  outDir: path.resolve(args.out ?? 'dist/cloudflare'),
23
23
  siteConfig,
24
+ searchDir: args.search ? path.resolve(args.search) : undefined,
24
25
  });
25
26
  console.log(`cloudflare worker written to ${result.workerFile}`);
26
27
  }
@@ -42,6 +43,11 @@ function parseArgs(argv) {
42
43
  if (argument === '--config' && nextValue) {
43
44
  result.config = nextValue;
44
45
  index += 1;
46
+ continue;
47
+ }
48
+ if (argument === '--search' && nextValue) {
49
+ result.search = nextValue;
50
+ index += 1;
45
51
  }
46
52
  }
47
53
  return result;
@@ -0,0 +1 @@
1
+ export declare function runBuildSearchCommand(rawArgs: string[]): Promise<void>;
@@ -0,0 +1,44 @@
1
+ import path from 'node:path';
2
+ import { buildSearchBundle } from '../search.js';
3
+ import { applySiteConfigFrontmatterDefaults, loadSiteConfig } from '../core/site-config.js';
4
+ import { createFileSystemContentStore } from '../adapters/node.js';
5
+ export async function runBuildSearchCommand(rawArgs) {
6
+ const args = parseArgs(rawArgs);
7
+ if (!args.root) {
8
+ throw new Error('Usage: mdorigin build search --root <content-dir> [--out ./dist/search] [--embedding-backend model2vec|hashing] [--model sentence-transformers/all-MiniLM-L6-v2] [--config mdorigin.config.json]');
9
+ }
10
+ const rootDir = path.resolve(args.root);
11
+ const store = createFileSystemContentStore(rootDir);
12
+ const siteConfig = await applySiteConfigFrontmatterDefaults(store, await loadSiteConfig({
13
+ rootDir,
14
+ configPath: args.config,
15
+ }));
16
+ const result = await buildSearchBundle({
17
+ rootDir,
18
+ outDir: path.resolve(args.out ?? 'dist/search'),
19
+ siteConfig,
20
+ embeddingBackend: args.embeddingBackend ?? 'model2vec',
21
+ model: args.model,
22
+ });
23
+ console.log(`search bundle written to ${result.outputDir} (${result.documentCount} documents, ${result.chunkCount} chunks)`);
24
+ }
25
+ function parseArgs(rawArgs) {
26
+ const parsed = {};
27
+ for (let index = 0; index < rawArgs.length; index += 1) {
28
+ const arg = rawArgs[index];
29
+ if (arg.startsWith('--')) {
30
+ const value = rawArgs[index + 1];
31
+ if (value && !value.startsWith('--')) {
32
+ parsed[arg.slice(2)] = value;
33
+ index += 1;
34
+ }
35
+ }
36
+ }
37
+ return {
38
+ root: parsed.root,
39
+ out: parsed.out,
40
+ embeddingBackend: parsed['embedding-backend'],
41
+ model: parsed.model,
42
+ config: parsed.config,
43
+ };
44
+ }
package/dist/cli/dev.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import path from 'node:path';
2
2
  import { createFileSystemContentStore, createNodeServer } from '../adapters/node.js';
3
3
  import { applySiteConfigFrontmatterDefaults, loadSiteConfig, } from '../core/site-config.js';
4
+ import { createSearchApiFromDirectory } from '../search.js';
4
5
  export async function runDevCommand(argv) {
5
6
  const args = parseArgs(argv);
6
7
  if (!args.root) {
7
- console.error('Usage: mdorigin dev --root <content-dir> [--port 3000] [--config mdorigin.config.json]');
8
+ console.error('Usage: mdorigin dev --root <content-dir> [--port 3000] [--config mdorigin.config.json] [--search ./dist/search]');
8
9
  process.exitCode = 1;
9
10
  return;
10
11
  }
@@ -21,6 +22,9 @@ export async function runDevCommand(argv) {
21
22
  rootDir,
22
23
  draftMode: 'include',
23
24
  siteConfig,
25
+ searchApi: args.search
26
+ ? await createSearchApiFromDirectory(path.resolve(args.search))
27
+ : undefined,
24
28
  });
25
29
  await new Promise((resolve, reject) => {
26
30
  server.once('error', reject);
@@ -47,6 +51,11 @@ function parseArgs(argv) {
47
51
  if (argument === '--config' && nextValue) {
48
52
  result.config = nextValue;
49
53
  index += 1;
54
+ continue;
55
+ }
56
+ if (argument === '--search' && nextValue) {
57
+ result.search = nextValue;
58
+ index += 1;
50
59
  }
51
60
  }
52
61
  return result;
@@ -1 +1,2 @@
1
+ #!/usr/bin/env node
1
2
  export {};
package/dist/cli/main.js CHANGED
@@ -1,7 +1,10 @@
1
+ #!/usr/bin/env node
1
2
  import { runBuildIndexCommand } from './build-index.js';
2
3
  import { runBuildCloudflareCommand } from './build-cloudflare.js';
4
+ import { runBuildSearchCommand } from './build-search.js';
3
5
  import { runDevCommand } from './dev.js';
4
6
  import { runInitCloudflareCommand } from './init-cloudflare.js';
7
+ import { runSearchCommand } from './search.js';
5
8
  async function main() {
6
9
  const [command, subcommand, ...rest] = process.argv.slice(2);
7
10
  if (command === 'dev') {
@@ -16,16 +19,26 @@ async function main() {
16
19
  await runBuildIndexCommand(rest);
17
20
  return;
18
21
  }
22
+ if (command === 'build' && subcommand === 'search') {
23
+ await runBuildSearchCommand(rest);
24
+ return;
25
+ }
19
26
  if (command === 'init' && subcommand === 'cloudflare') {
20
27
  await runInitCloudflareCommand(rest);
21
28
  return;
22
29
  }
30
+ if (command === 'search') {
31
+ await runSearchCommand([subcommand, ...rest].filter(isDefined));
32
+ return;
33
+ }
23
34
  console.error([
24
35
  'Usage:',
25
- ' mdorigin dev --root <content-dir> [--port 3000] [--config mdorigin.config.json]',
36
+ ' mdorigin dev --root <content-dir> [--port 3000] [--config mdorigin.config.json] [--search ./dist/search]',
26
37
  ' mdorigin build index (--root <content-dir> | --dir <content-dir>)',
27
- ' mdorigin build cloudflare --root <content-dir> [--out ./dist/cloudflare] [--config mdorigin.config.json]',
38
+ ' mdorigin build search --root <content-dir> [--out ./dist/search] [--embedding-backend model2vec|hashing] [--model sentence-transformers/all-MiniLM-L6-v2] [--config mdorigin.config.json]',
39
+ ' mdorigin build cloudflare --root <content-dir> [--out ./dist/cloudflare] [--config mdorigin.config.json] [--search ./dist/search]',
28
40
  ' mdorigin init cloudflare [--dir .] [--entry ./dist/cloudflare/worker.mjs] [--name <worker-name>] [--compatibility-date 2026-03-20] [--force]',
41
+ ' mdorigin search --index <search-dir> [--top-k 10] <query>',
29
42
  ].join('\n'));
30
43
  process.exitCode = 1;
31
44
  }
@@ -0,0 +1 @@
1
+ export declare function runSearchCommand(rawArgs: string[]): Promise<void>;
@@ -0,0 +1,36 @@
1
+ import path from 'node:path';
2
+ import { searchBundle } from '../search.js';
3
+ export async function runSearchCommand(rawArgs) {
4
+ const args = parseArgs(rawArgs);
5
+ if (!args.indexDir || !args.query) {
6
+ throw new Error('Usage: mdorigin search --index <search-dir> [--top-k 10] <query>');
7
+ }
8
+ const hits = await searchBundle({
9
+ indexDir: path.resolve(args.indexDir),
10
+ query: args.query,
11
+ topK: args.topK,
12
+ });
13
+ console.log(JSON.stringify(hits, null, 2));
14
+ }
15
+ function parseArgs(rawArgs) {
16
+ const flags = {};
17
+ const positionals = [];
18
+ for (let index = 0; index < rawArgs.length; index += 1) {
19
+ const arg = rawArgs[index];
20
+ if (arg.startsWith('--')) {
21
+ const value = rawArgs[index + 1];
22
+ if (value && !value.startsWith('--')) {
23
+ flags[arg.slice(2)] = value;
24
+ index += 1;
25
+ }
26
+ continue;
27
+ }
28
+ positionals.push(arg);
29
+ }
30
+ const topK = flags['top-k'] ? Number.parseInt(flags['top-k'], 10) : undefined;
31
+ return {
32
+ indexDir: flags.index,
33
+ topK: Number.isInteger(topK) && topK > 0 ? topK : undefined,
34
+ query: positionals.join(' ').trim(),
35
+ };
36
+ }
@@ -6,12 +6,14 @@ export type { CloudflareManifest, CloudflareManifestEntry };
6
6
  export interface BuildCloudflareManifestOptions {
7
7
  rootDir: string;
8
8
  siteConfig: ResolvedSiteConfig;
9
+ searchDir?: string;
9
10
  }
10
11
  export interface WriteCloudflareBundleOptions {
11
12
  rootDir: string;
12
13
  outDir: string;
13
14
  siteConfig: ResolvedSiteConfig;
14
15
  packageImport?: string;
16
+ searchDir?: string;
15
17
  }
16
18
  export interface InitCloudflareProjectOptions {
17
19
  projectDir: string;
@@ -1,4 +1,4 @@
1
- import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
1
+ import { mkdir, readdir, readFile, realpath, stat, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { createCloudflareWorker } from './adapters/cloudflare.js';
4
4
  import { getMediaTypeForPath, isLikelyTextPath, normalizeContentPath, } from './core/content-store.js';
@@ -30,10 +30,14 @@ export async function buildCloudflareManifest(options) {
30
30
  base64: (await readFile(filePath)).toString('base64'),
31
31
  });
32
32
  }
33
+ const searchEntries = options.searchDir
34
+ ? await readBundleEntries(path.resolve(options.searchDir))
35
+ : undefined;
33
36
  entries.sort((left, right) => left.path.localeCompare(right.path));
34
37
  return {
35
38
  entries,
36
39
  siteConfig: options.siteConfig,
40
+ searchEntries,
37
41
  };
38
42
  }
39
43
  export async function writeCloudflareBundle(options) {
@@ -41,6 +45,7 @@ export async function writeCloudflareBundle(options) {
41
45
  const manifest = await buildCloudflareManifest({
42
46
  rootDir: options.rootDir,
43
47
  siteConfig: options.siteConfig,
48
+ searchDir: options.searchDir,
44
49
  });
45
50
  const packageImport = options.packageImport ?? 'mdorigin/cloudflare-runtime';
46
51
  const workerFile = path.join(outDir, 'worker.mjs');
@@ -84,21 +89,52 @@ export async function initCloudflareProject(options) {
84
89
  await writeFile(configFile, wranglerConfig, 'utf8');
85
90
  return { configFile };
86
91
  }
87
- async function listFiles(directory) {
92
+ async function listFiles(directory, visitedRealDirectories = new Set()) {
93
+ const directoryRealPath = await realpath(directory);
94
+ if (visitedRealDirectories.has(directoryRealPath)) {
95
+ return [];
96
+ }
97
+ visitedRealDirectories.add(directoryRealPath);
88
98
  const entries = await readdir(directory, { withFileTypes: true });
89
99
  const files = [];
90
100
  for (const entry of entries) {
91
101
  const fullPath = path.join(directory, entry.name);
92
- if (entry.isDirectory()) {
93
- files.push(...(await listFiles(fullPath)));
102
+ const entryStats = await stat(fullPath);
103
+ if (entryStats.isDirectory()) {
104
+ files.push(...(await listFiles(fullPath, visitedRealDirectories)));
94
105
  continue;
95
106
  }
96
- if (entry.isFile()) {
107
+ if (entryStats.isFile()) {
97
108
  files.push(fullPath);
98
109
  }
99
110
  }
100
111
  return files;
101
112
  }
113
+ async function readBundleEntries(directory) {
114
+ const files = await listFiles(directory);
115
+ const entries = [];
116
+ for (const filePath of files) {
117
+ const relativePath = path.relative(directory, filePath).replaceAll(path.sep, '/');
118
+ const mediaType = getMediaTypeForPath(relativePath);
119
+ if (isLikelyTextPath(relativePath) || relativePath.endsWith('.json')) {
120
+ entries.push({
121
+ path: relativePath,
122
+ kind: 'text',
123
+ mediaType,
124
+ text: await readFile(filePath, 'utf8'),
125
+ });
126
+ continue;
127
+ }
128
+ entries.push({
129
+ path: relativePath,
130
+ kind: 'binary',
131
+ mediaType,
132
+ base64: (await readFile(filePath)).toString('base64'),
133
+ });
134
+ }
135
+ entries.sort((left, right) => left.path.localeCompare(right.path));
136
+ return entries;
137
+ }
102
138
  async function pathExists(filePath) {
103
139
  try {
104
140
  await stat(filePath);
@@ -0,0 +1,13 @@
1
+ import type { ResolvedSiteConfig } from './site-config.js';
2
+ import type { SearchApi } from '../search.js';
3
+ export interface ApiRouteOptions {
4
+ searchApi?: SearchApi;
5
+ siteConfig: ResolvedSiteConfig;
6
+ requestUrl?: string;
7
+ }
8
+ export interface ApiRouteResponse {
9
+ status: number;
10
+ headers: Record<string, string>;
11
+ body: string;
12
+ }
13
+ export declare function handleApiRoute(pathname: string, searchParams: URLSearchParams | undefined, options: ApiRouteOptions): Promise<ApiRouteResponse | null>;