mdorigin 0.1.1 → 0.1.3
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 +97 -9
- package/dist/adapters/cloudflare.d.ts +7 -1
- package/dist/adapters/cloudflare.js +11 -1
- package/dist/adapters/node.d.ts +4 -0
- package/dist/adapters/node.js +51 -11
- package/dist/cli/build-cloudflare.js +11 -4
- package/dist/cli/build-index.js +17 -3
- package/dist/cli/build-search.d.ts +1 -0
- package/dist/cli/build-search.js +45 -0
- package/dist/cli/dev.js +14 -4
- package/dist/cli/main.js +15 -3
- package/dist/cli/search.d.ts +1 -0
- package/dist/cli/search.js +36 -0
- package/dist/cloudflare.d.ts +3 -0
- package/dist/cloudflare.js +67 -6
- 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/extensions.d.ts +66 -0
- package/dist/core/extensions.js +86 -0
- package/dist/core/markdown.d.ts +4 -0
- package/dist/core/markdown.js +53 -0
- package/dist/core/request-handler.d.ts +6 -0
- package/dist/core/request-handler.js +211 -68
- package/dist/core/site-config.d.ts +18 -0
- package/dist/core/site-config.js +88 -16
- package/dist/html/template.d.ts +8 -0
- package/dist/html/template.js +228 -12
- package/dist/html/theme.js +254 -9
- package/dist/index-builder.d.ts +3 -1
- package/dist/index-builder.js +82 -45
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/search.d.ts +59 -0
- package/dist/search.js +370 -0
- package/package.json +20 -5
package/README.md
CHANGED
|
@@ -2,34 +2,122 @@
|
|
|
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, renders
|
|
5
|
+
It treats markdown as the only source of truth, serves raw `.md` directly for agents, renders HTML for humans from the same directory tree, and can expose the same content to both browsers and tools through stable routes.
|
|
6
|
+
|
|
7
|
+
## Core Principle
|
|
8
|
+
|
|
9
|
+
`mdorigin` is not meant to become a template system.
|
|
10
|
+
|
|
11
|
+
Its core is:
|
|
12
|
+
|
|
13
|
+
- routing rules
|
|
14
|
+
- markdown tree normalization
|
|
15
|
+
- document, index, asset, search, and site semantics
|
|
16
|
+
- stable page models derived from the same content tree
|
|
17
|
+
|
|
18
|
+
That means `mdorigin` should own content semantics, while page rendering remains extensible. In practice, this is the direction for advanced customization: users should be able to replace page-level rendering with code, without replacing the routing and content kernel itself.
|
|
19
|
+
|
|
20
|
+
## Why mdorigin
|
|
21
|
+
|
|
22
|
+
- markdown stays directly accessible at `.md` routes
|
|
23
|
+
- extensionless routes render human-friendly HTML from the same files
|
|
24
|
+
- `README.md`, `index.md`, and `SKILL.md` all fit into one routing model
|
|
25
|
+
- the same core works in local preview and Cloudflare Workers
|
|
26
|
+
- optional search is powered by [`indexbind`](https://github.com/jolestar/indexbind)
|
|
6
27
|
|
|
7
28
|
## Install
|
|
8
29
|
|
|
9
30
|
```bash
|
|
10
|
-
npm install
|
|
31
|
+
npm install -g mdorigin
|
|
11
32
|
```
|
|
12
33
|
|
|
13
|
-
Then run it
|
|
34
|
+
Then run it directly:
|
|
14
35
|
|
|
15
36
|
```bash
|
|
16
|
-
|
|
37
|
+
mdorigin dev --root docs/site
|
|
17
38
|
```
|
|
18
39
|
|
|
19
|
-
|
|
40
|
+
If you prefer a project-local install instead, use `npm install --save-dev mdorigin` and run it with `npx --no-install mdorigin ...`.
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
20
43
|
|
|
21
44
|
```bash
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
npm run build:index -- --root docs/site
|
|
45
|
+
mdorigin dev --root docs/site
|
|
46
|
+
mdorigin build index --root docs/site
|
|
47
|
+
mdorigin build cloudflare --root docs/site
|
|
26
48
|
```
|
|
27
49
|
|
|
50
|
+
That is enough to preview a site locally, keep directory indexes up to date, and generate a Cloudflare Worker bundle.
|
|
51
|
+
|
|
52
|
+
## Code-Based Extensions
|
|
53
|
+
|
|
54
|
+
`mdorigin` now supports a code config alongside `mdorigin.config.json`.
|
|
55
|
+
|
|
56
|
+
Use `mdorigin.config.ts` when you want to customize rendering or indexing with code instead of asking for a template system:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
export default {
|
|
60
|
+
siteTitle: "My Site",
|
|
61
|
+
plugins: [
|
|
62
|
+
{
|
|
63
|
+
name: "custom-footer",
|
|
64
|
+
renderFooter() {
|
|
65
|
+
return '<footer class="custom-footer">Built with mdorigin</footer>';
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Current stable hooks are:
|
|
73
|
+
|
|
74
|
+
- `transformIndex(entries, context)`
|
|
75
|
+
- `renderHeader(context)`
|
|
76
|
+
- `renderFooter(context)`
|
|
77
|
+
- `renderPage(page, context, next)`
|
|
78
|
+
- `transformHtml(html, context)`
|
|
79
|
+
|
|
80
|
+
The intended boundary is:
|
|
81
|
+
|
|
82
|
+
- `mdorigin` owns routing and normalized content semantics
|
|
83
|
+
- plugins may replace page rendering
|
|
84
|
+
- plugins do not replace the request kernel itself
|
|
85
|
+
|
|
86
|
+
## Optional Search
|
|
87
|
+
|
|
88
|
+
`mdorigin` can build a local retrieval bundle through the optional [`indexbind`](https://github.com/jolestar/indexbind) package. For the retrieval engine itself, see the `indexbind` docs: <https://indexbind.jolestar.workers.dev>.
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npm install indexbind
|
|
92
|
+
mdorigin build search --root docs/site
|
|
93
|
+
mdorigin search --index dist/search "cloudflare deploy"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`build search` now defaults to the higher-quality `model2vec` backend. If you need a smaller or faster fallback, you can opt back into hashing:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
mdorigin build search --root docs/site --embedding-backend hashing
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
To expose the same search bundle from the site runtime:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
mdorigin dev --root docs/site --search dist/search
|
|
106
|
+
mdorigin build cloudflare --root docs/site --search dist/search
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Runtime endpoints:
|
|
110
|
+
|
|
111
|
+
- `/api/search?q=cloudflare+deploy`
|
|
112
|
+
- `/api/openapi.json`
|
|
113
|
+
|
|
28
114
|
## Docs
|
|
29
115
|
|
|
116
|
+
- Docs site: <https://mdorigin.jolestar.workers.dev>
|
|
30
117
|
- Getting started: [`docs/site/guides/getting-started.md`](docs/site/guides/getting-started.md)
|
|
31
118
|
- Routing model: [`docs/site/concepts/routing.md`](docs/site/concepts/routing.md)
|
|
32
119
|
- Directory indexes: [`docs/site/concepts/directory-indexes.md`](docs/site/concepts/directory-indexes.md)
|
|
33
120
|
- Configuration: [`docs/site/reference/configuration.md`](docs/site/reference/configuration.md)
|
|
34
121
|
- CLI: [`docs/site/reference/cli.md`](docs/site/reference/cli.md)
|
|
122
|
+
- Search setup: [`docs/site/guides/getting-started.md`](docs/site/guides/getting-started.md#quick-start)
|
|
35
123
|
- Cloudflare deployment: [`docs/site/guides/cloudflare.md`](docs/site/guides/cloudflare.md)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type ContentEntryKind } from '../core/content-store.js';
|
|
2
|
+
import type { MdoPlugin } from '../core/extensions.js';
|
|
2
3
|
import type { ResolvedSiteConfig } from '../core/site-config.js';
|
|
4
|
+
import { type SearchBundleEntry } from '../search.js';
|
|
3
5
|
export interface CloudflareManifestEntry {
|
|
4
6
|
path: string;
|
|
5
7
|
kind: ContentEntryKind;
|
|
@@ -10,8 +12,12 @@ export interface CloudflareManifestEntry {
|
|
|
10
12
|
export interface CloudflareManifest {
|
|
11
13
|
entries: CloudflareManifestEntry[];
|
|
12
14
|
siteConfig?: ResolvedSiteConfig;
|
|
15
|
+
searchEntries?: SearchBundleEntry[];
|
|
13
16
|
}
|
|
14
17
|
export interface ExportedHandlerLike {
|
|
15
18
|
fetch(request: Request): Promise<Response>;
|
|
16
19
|
}
|
|
17
|
-
export
|
|
20
|
+
export interface CreateCloudflareWorkerOptions {
|
|
21
|
+
plugins?: MdoPlugin[];
|
|
22
|
+
}
|
|
23
|
+
export declare function createCloudflareWorker(manifest: CloudflareManifest, options?: CreateCloudflareWorkerOptions): ExportedHandlerLike;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { MemoryContentStore, } from '../core/content-store.js';
|
|
2
2
|
import { handleSiteRequest } from '../core/request-handler.js';
|
|
3
|
-
|
|
3
|
+
import { createSearchApiFromBundle } from '../search.js';
|
|
4
|
+
export function createCloudflareWorker(manifest, options = {}) {
|
|
4
5
|
const store = new MemoryContentStore(manifest.entries.map((entry) => {
|
|
5
6
|
if (entry.kind === 'text') {
|
|
6
7
|
return {
|
|
@@ -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);
|
|
@@ -37,10 +41,16 @@ export function createCloudflareWorker(manifest) {
|
|
|
37
41
|
socialLinks: [],
|
|
38
42
|
editLink: undefined,
|
|
39
43
|
showHomeIndex: true,
|
|
44
|
+
catalogInitialPostCount: 10,
|
|
45
|
+
catalogLoadMoreStep: 10,
|
|
40
46
|
siteTitleConfigured: false,
|
|
41
47
|
siteDescriptionConfigured: false,
|
|
42
48
|
},
|
|
43
49
|
acceptHeader: request.headers.get('accept') ?? undefined,
|
|
50
|
+
searchParams: url.searchParams,
|
|
51
|
+
requestUrl: request.url,
|
|
52
|
+
searchApi,
|
|
53
|
+
plugins: options.plugins,
|
|
44
54
|
});
|
|
45
55
|
const headers = new Headers(siteResponse.headers);
|
|
46
56
|
const body = siteResponse.body instanceof Uint8Array
|
package/dist/adapters/node.d.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { type IncomingMessage, type ServerResponse } from 'node:http';
|
|
2
2
|
import type { ContentStore } from '../core/content-store.js';
|
|
3
|
+
import type { MdoPlugin } from '../core/extensions.js';
|
|
3
4
|
import type { ResolvedSiteConfig } from '../core/site-config.js';
|
|
5
|
+
import type { SearchApi } from '../search.js';
|
|
4
6
|
export interface NodeAdapterOptions {
|
|
5
7
|
rootDir: string;
|
|
6
8
|
draftMode: 'include' | 'exclude';
|
|
7
9
|
siteConfig: ResolvedSiteConfig;
|
|
10
|
+
searchApi?: SearchApi;
|
|
11
|
+
plugins?: MdoPlugin[];
|
|
8
12
|
}
|
|
9
13
|
export declare function createFileSystemContentStore(rootDir: string): ContentStore;
|
|
10
14
|
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) {
|
|
@@ -87,6 +123,10 @@ export function createNodeRequestListener(options) {
|
|
|
87
123
|
draftMode: options.draftMode,
|
|
88
124
|
siteConfig: options.siteConfig,
|
|
89
125
|
acceptHeader: request.headers.accept,
|
|
126
|
+
searchParams: url.searchParams,
|
|
127
|
+
requestUrl: url.toString(),
|
|
128
|
+
searchApi: options.searchApi,
|
|
129
|
+
plugins: options.plugins,
|
|
90
130
|
});
|
|
91
131
|
response.statusCode = siteResponse.status;
|
|
92
132
|
for (const [headerName, headerValue] of Object.entries(siteResponse.headers)) {
|
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { createFileSystemContentStore } from '../adapters/node.js';
|
|
3
3
|
import { writeCloudflareBundle } from '../cloudflare.js';
|
|
4
|
-
import { applySiteConfigFrontmatterDefaults,
|
|
4
|
+
import { applySiteConfigFrontmatterDefaults, loadUserSiteConfig, } from '../core/site-config.js';
|
|
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
|
|
8
|
+
console.error('Usage: mdorigin build cloudflare --root <content-dir> [--out ./dist/cloudflare] [--config <config-file>] [--search ./dist/search]');
|
|
9
9
|
process.exitCode = 1;
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
12
12
|
const rootDir = path.resolve(args.root);
|
|
13
|
-
const
|
|
13
|
+
const loadedConfig = await loadUserSiteConfig({
|
|
14
14
|
cwd: process.cwd(),
|
|
15
15
|
rootDir,
|
|
16
16
|
configPath: args.config,
|
|
17
17
|
});
|
|
18
18
|
const store = createFileSystemContentStore(rootDir);
|
|
19
|
-
const siteConfig = await applySiteConfigFrontmatterDefaults(store,
|
|
19
|
+
const siteConfig = await applySiteConfigFrontmatterDefaults(store, loadedConfig.siteConfig);
|
|
20
20
|
const result = await writeCloudflareBundle({
|
|
21
21
|
rootDir,
|
|
22
22
|
outDir: path.resolve(args.out ?? 'dist/cloudflare'),
|
|
23
23
|
siteConfig,
|
|
24
|
+
searchDir: args.search ? path.resolve(args.search) : undefined,
|
|
25
|
+
configModulePath: loadedConfig.configModulePath,
|
|
24
26
|
});
|
|
25
27
|
console.log(`cloudflare worker written to ${result.workerFile}`);
|
|
26
28
|
}
|
|
@@ -42,6 +44,11 @@ function parseArgs(argv) {
|
|
|
42
44
|
if (argument === '--config' && nextValue) {
|
|
43
45
|
result.config = nextValue;
|
|
44
46
|
index += 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (argument === '--search' && nextValue) {
|
|
50
|
+
result.search = nextValue;
|
|
51
|
+
index += 1;
|
|
45
52
|
}
|
|
46
53
|
}
|
|
47
54
|
return result;
|
package/dist/cli/build-index.js
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { buildDirectoryIndexes } from '../index-builder.js';
|
|
3
|
+
import { loadUserSiteConfig } from '../core/site-config.js';
|
|
3
4
|
export async function runBuildIndexCommand(argv) {
|
|
4
5
|
const args = parseArgs(argv);
|
|
5
6
|
if (!args.root && !args.dir) {
|
|
6
|
-
console.error('Usage: mdorigin build index (--root <content-dir> | --dir <content-dir>)');
|
|
7
|
+
console.error('Usage: mdorigin build index (--root <content-dir> | --dir <content-dir>) [--config <config-file>]');
|
|
7
8
|
process.exitCode = 1;
|
|
8
9
|
return;
|
|
9
10
|
}
|
|
11
|
+
const rootDir = args.root ? path.resolve(args.root) : undefined;
|
|
12
|
+
const dir = args.dir ? path.resolve(args.dir) : undefined;
|
|
13
|
+
const loadedConfig = await loadUserSiteConfig({
|
|
14
|
+
cwd: process.cwd(),
|
|
15
|
+
rootDir: rootDir ?? dir,
|
|
16
|
+
configPath: args.config,
|
|
17
|
+
});
|
|
10
18
|
const result = await buildDirectoryIndexes({
|
|
11
|
-
rootDir
|
|
12
|
-
dir
|
|
19
|
+
rootDir,
|
|
20
|
+
dir,
|
|
21
|
+
plugins: loadedConfig.plugins,
|
|
13
22
|
});
|
|
14
23
|
console.log(`updated ${result.updatedFiles.length} index file(s)`);
|
|
15
24
|
if (result.skippedDirectories.length > 0) {
|
|
@@ -29,6 +38,11 @@ function parseArgs(argv) {
|
|
|
29
38
|
if (argument === '--dir' && nextValue) {
|
|
30
39
|
result.dir = nextValue;
|
|
31
40
|
index += 1;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (argument === '--config' && nextValue) {
|
|
44
|
+
result.config = nextValue;
|
|
45
|
+
index += 1;
|
|
32
46
|
}
|
|
33
47
|
}
|
|
34
48
|
return result;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runBuildSearchCommand(rawArgs: string[]): Promise<void>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { buildSearchBundle } from '../search.js';
|
|
3
|
+
import { applySiteConfigFrontmatterDefaults, loadUserSiteConfig, } 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 <config-file>]');
|
|
9
|
+
}
|
|
10
|
+
const rootDir = path.resolve(args.root);
|
|
11
|
+
const store = createFileSystemContentStore(rootDir);
|
|
12
|
+
const loadedConfig = await loadUserSiteConfig({
|
|
13
|
+
rootDir,
|
|
14
|
+
configPath: args.config,
|
|
15
|
+
});
|
|
16
|
+
const siteConfig = await applySiteConfigFrontmatterDefaults(store, loadedConfig.siteConfig);
|
|
17
|
+
const result = await buildSearchBundle({
|
|
18
|
+
rootDir,
|
|
19
|
+
outDir: path.resolve(args.out ?? 'dist/search'),
|
|
20
|
+
siteConfig,
|
|
21
|
+
embeddingBackend: args.embeddingBackend ?? 'model2vec',
|
|
22
|
+
model: args.model,
|
|
23
|
+
});
|
|
24
|
+
console.log(`search bundle written to ${result.outputDir} (${result.documentCount} documents, ${result.chunkCount} chunks)`);
|
|
25
|
+
}
|
|
26
|
+
function parseArgs(rawArgs) {
|
|
27
|
+
const parsed = {};
|
|
28
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
29
|
+
const arg = rawArgs[index];
|
|
30
|
+
if (arg.startsWith('--')) {
|
|
31
|
+
const value = rawArgs[index + 1];
|
|
32
|
+
if (value && !value.startsWith('--')) {
|
|
33
|
+
parsed[arg.slice(2)] = value;
|
|
34
|
+
index += 1;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
root: parsed.root,
|
|
40
|
+
out: parsed.out,
|
|
41
|
+
embeddingBackend: parsed['embedding-backend'],
|
|
42
|
+
model: parsed.model,
|
|
43
|
+
config: parsed.config,
|
|
44
|
+
};
|
|
45
|
+
}
|
package/dist/cli/dev.js
CHANGED
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { createFileSystemContentStore, createNodeServer } from '../adapters/node.js';
|
|
3
|
-
import { applySiteConfigFrontmatterDefaults,
|
|
3
|
+
import { applySiteConfigFrontmatterDefaults, loadUserSiteConfig, } 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
|
|
8
|
+
console.error('Usage: mdorigin dev --root <content-dir> [--port 3000] [--config <config-file>] [--search ./dist/search]');
|
|
8
9
|
process.exitCode = 1;
|
|
9
10
|
return;
|
|
10
11
|
}
|
|
11
12
|
const rootDir = path.resolve(args.root);
|
|
12
13
|
const port = args.port ?? 3000;
|
|
13
|
-
const
|
|
14
|
+
const loadedConfig = await loadUserSiteConfig({
|
|
14
15
|
cwd: process.cwd(),
|
|
15
16
|
rootDir,
|
|
16
17
|
configPath: args.config,
|
|
17
18
|
});
|
|
18
19
|
const store = createFileSystemContentStore(rootDir);
|
|
19
|
-
const siteConfig = await applySiteConfigFrontmatterDefaults(store,
|
|
20
|
+
const siteConfig = await applySiteConfigFrontmatterDefaults(store, loadedConfig.siteConfig);
|
|
20
21
|
const server = createNodeServer({
|
|
21
22
|
rootDir,
|
|
22
23
|
draftMode: 'include',
|
|
23
24
|
siteConfig,
|
|
25
|
+
searchApi: args.search
|
|
26
|
+
? await createSearchApiFromDirectory(path.resolve(args.search))
|
|
27
|
+
: undefined,
|
|
28
|
+
plugins: loadedConfig.plugins,
|
|
24
29
|
});
|
|
25
30
|
await new Promise((resolve, reject) => {
|
|
26
31
|
server.once('error', reject);
|
|
@@ -47,6 +52,11 @@ function parseArgs(argv) {
|
|
|
47
52
|
if (argument === '--config' && nextValue) {
|
|
48
53
|
result.config = nextValue;
|
|
49
54
|
index += 1;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (argument === '--search' && nextValue) {
|
|
58
|
+
result.search = nextValue;
|
|
59
|
+
index += 1;
|
|
50
60
|
}
|
|
51
61
|
}
|
|
52
62
|
return result;
|
package/dist/cli/main.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { runBuildIndexCommand } from './build-index.js';
|
|
3
3
|
import { runBuildCloudflareCommand } from './build-cloudflare.js';
|
|
4
|
+
import { runBuildSearchCommand } from './build-search.js';
|
|
4
5
|
import { runDevCommand } from './dev.js';
|
|
5
6
|
import { runInitCloudflareCommand } from './init-cloudflare.js';
|
|
7
|
+
import { runSearchCommand } from './search.js';
|
|
6
8
|
async function main() {
|
|
7
9
|
const [command, subcommand, ...rest] = process.argv.slice(2);
|
|
8
10
|
if (command === 'dev') {
|
|
@@ -17,16 +19,26 @@ async function main() {
|
|
|
17
19
|
await runBuildIndexCommand(rest);
|
|
18
20
|
return;
|
|
19
21
|
}
|
|
22
|
+
if (command === 'build' && subcommand === 'search') {
|
|
23
|
+
await runBuildSearchCommand(rest);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
20
26
|
if (command === 'init' && subcommand === 'cloudflare') {
|
|
21
27
|
await runInitCloudflareCommand(rest);
|
|
22
28
|
return;
|
|
23
29
|
}
|
|
30
|
+
if (command === 'search') {
|
|
31
|
+
await runSearchCommand([subcommand, ...rest].filter(isDefined));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
24
34
|
console.error([
|
|
25
35
|
'Usage:',
|
|
26
|
-
' mdorigin dev --root <content-dir> [--port 3000] [--config
|
|
27
|
-
' mdorigin build index (--root <content-dir> | --dir <content-dir>)',
|
|
28
|
-
' mdorigin build
|
|
36
|
+
' mdorigin dev --root <content-dir> [--port 3000] [--config <config-file>] [--search ./dist/search]',
|
|
37
|
+
' mdorigin build index (--root <content-dir> | --dir <content-dir>) [--config <config-file>]',
|
|
38
|
+
' mdorigin build search --root <content-dir> [--out ./dist/search] [--embedding-backend model2vec|hashing] [--model sentence-transformers/all-MiniLM-L6-v2] [--config <config-file>]',
|
|
39
|
+
' mdorigin build cloudflare --root <content-dir> [--out ./dist/cloudflare] [--config <config-file>] [--search ./dist/search]',
|
|
29
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>',
|
|
30
42
|
].join('\n'));
|
|
31
43
|
process.exitCode = 1;
|
|
32
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,15 @@ 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;
|
|
17
|
+
configModulePath?: string;
|
|
15
18
|
}
|
|
16
19
|
export interface InitCloudflareProjectOptions {
|
|
17
20
|
projectDir: string;
|