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 +52 -3
- package/dist/adapters/cloudflare.d.ts +2 -0
- package/dist/adapters/cloudflare.js +17 -0
- package/dist/adapters/node.d.ts +2 -0
- package/dist/adapters/node.js +51 -11
- package/dist/cli/build-cloudflare.js +7 -1
- package/dist/cli/build-search.d.ts +1 -0
- package/dist/cli/build-search.js +44 -0
- package/dist/cli/dev.js +10 -1
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +15 -2
- package/dist/cli/search.d.ts +1 -0
- package/dist/cli/search.js +36 -0
- package/dist/cloudflare.d.ts +2 -0
- package/dist/cloudflare.js +41 -5
- package/dist/core/api.d.ts +13 -0
- package/dist/core/api.js +160 -0
- package/dist/core/content-store.js +5 -0
- package/dist/core/content-type.d.ts +1 -0
- package/dist/core/content-type.js +3 -0
- package/dist/core/directory-index.d.ts +1 -1
- package/dist/core/directory-index.js +5 -1
- package/dist/core/markdown.d.ts +12 -0
- package/dist/core/markdown.js +88 -0
- package/dist/core/request-handler.d.ts +5 -0
- package/dist/core/request-handler.js +412 -24
- package/dist/core/router.d.ts +1 -0
- package/dist/core/router.js +1 -1
- package/dist/core/site-config.d.ts +31 -0
- package/dist/core/site-config.js +98 -2
- package/dist/html/template-kind.d.ts +1 -1
- package/dist/html/template.d.ts +17 -1
- package/dist/html/template.js +282 -21
- package/dist/html/theme.js +542 -100
- package/dist/index-builder.js +62 -29
- package/dist/search.d.ts +59 -0
- package/dist/search.js +370 -0
- package/package.json +12 -3
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,
|
|
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
|
-
|
|
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
|
package/dist/adapters/node.d.ts
CHANGED
|
@@ -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>;
|
package/dist/adapters/node.js
CHANGED
|
@@ -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 (!
|
|
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 (!
|
|
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
|
-
|
|
63
|
+
const resolvedEntries = await Promise.all(entries
|
|
57
64
|
.filter((entry) => !entry.name.startsWith('.'))
|
|
58
|
-
.map((entry) =>
|
|
59
|
-
|
|
60
|
-
path: normalizedPath === ''
|
|
65
|
+
.map(async (entry) => {
|
|
66
|
+
const childVisiblePath = normalizedPath === ''
|
|
61
67
|
? entry.name
|
|
62
|
-
: `${normalizedPath}/${entry.name}
|
|
63
|
-
|
|
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;
|
package/dist/cli/main.d.ts
CHANGED
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
|
|
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
|
+
}
|
package/dist/cloudflare.d.ts
CHANGED
|
@@ -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;
|
package/dist/cloudflare.js
CHANGED
|
@@ -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
|
-
|
|
93
|
-
|
|
102
|
+
const entryStats = await stat(fullPath);
|
|
103
|
+
if (entryStats.isDirectory()) {
|
|
104
|
+
files.push(...(await listFiles(fullPath, visitedRealDirectories)));
|
|
94
105
|
continue;
|
|
95
106
|
}
|
|
96
|
-
if (
|
|
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>;
|