@piqit/resolvers 0.0.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/dist/edge.d.ts +15 -0
- package/dist/edge.d.ts.map +1 -0
- package/dist/edge.js +15 -0
- package/dist/edge.js.map +1 -0
- package/dist/file-markdown.d.ts +73 -0
- package/dist/file-markdown.d.ts.map +1 -0
- package/dist/file-markdown.js +201 -0
- package/dist/file-markdown.js.map +1 -0
- package/dist/frontmatter.d.ts +58 -0
- package/dist/frontmatter.d.ts.map +1 -0
- package/dist/frontmatter.js +254 -0
- package/dist/frontmatter.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/markdown.d.ts +95 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +174 -0
- package/dist/markdown.js.map +1 -0
- package/dist/path-pattern.d.ts +96 -0
- package/dist/path-pattern.d.ts.map +1 -0
- package/dist/path-pattern.js +130 -0
- package/dist/path-pattern.js.map +1 -0
- package/dist/static.d.ts +65 -0
- package/dist/static.d.ts.map +1 -0
- package/dist/static.js +194 -0
- package/dist/static.js.map +1 -0
- package/package.json +31 -0
- package/src/edge.ts +15 -0
- package/src/file-markdown.ts +352 -0
- package/src/frontmatter.ts +269 -0
- package/src/index.ts +65 -0
- package/src/markdown.ts +262 -0
- package/src/path-pattern.ts +226 -0
- package/src/static.ts +227 -0
package/dist/static.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static Content Resolver
|
|
3
|
+
*
|
|
4
|
+
* A resolver for querying pre-compiled content in edge environments
|
|
5
|
+
* like Cloudflare Workers where filesystem access is not available.
|
|
6
|
+
*
|
|
7
|
+
* Content is compiled at build time and bundled as a static module.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // 1. Build script compiles content:
|
|
11
|
+
* // build-content.ts
|
|
12
|
+
* import { fileMarkdown } from "@piqit/resolvers";
|
|
13
|
+
* const posts = fileMarkdown({ ... });
|
|
14
|
+
* const allPosts = await posts.resolve({ scan: {}, filter: {}, select: ["params.*", "frontmatter.*", "body.*"] });
|
|
15
|
+
* await Bun.write("src/generated/content.ts", `export const posts = ${JSON.stringify(allPosts)};`);
|
|
16
|
+
*
|
|
17
|
+
* // 2. Worker imports and uses static resolver:
|
|
18
|
+
* // worker.ts
|
|
19
|
+
* import { posts } from "./generated/content";
|
|
20
|
+
* import { staticContent } from "@piqit/resolvers";
|
|
21
|
+
* import { piq } from "piqit";
|
|
22
|
+
*
|
|
23
|
+
* const postsResolver = staticContent(posts);
|
|
24
|
+
*
|
|
25
|
+
* const results = await piq.from(postsResolver)
|
|
26
|
+
* .filter({ author: "John" })
|
|
27
|
+
* .select("params.slug", "frontmatter.title")
|
|
28
|
+
* .exec();
|
|
29
|
+
*/
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// Filter Helpers
|
|
32
|
+
// =============================================================================
|
|
33
|
+
/**
|
|
34
|
+
* Get a nested value from an object using dot-path notation.
|
|
35
|
+
*/
|
|
36
|
+
function getByPath(obj, path) {
|
|
37
|
+
const parts = path.split(".");
|
|
38
|
+
let current = obj;
|
|
39
|
+
for (const part of parts) {
|
|
40
|
+
if (current == null || typeof current !== "object") {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
current = current[part];
|
|
44
|
+
}
|
|
45
|
+
return current;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Default filter implementation - checks equality on frontmatter fields.
|
|
49
|
+
*/
|
|
50
|
+
function defaultFilter(item, filter) {
|
|
51
|
+
const frontmatter = item.frontmatter;
|
|
52
|
+
if (!frontmatter) {
|
|
53
|
+
return Object.keys(filter).length === 0;
|
|
54
|
+
}
|
|
55
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
56
|
+
if (frontmatter[key] !== value) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Check if scan constraints match params.
|
|
64
|
+
*/
|
|
65
|
+
function matchesScan(item, scan) {
|
|
66
|
+
const params = item.params;
|
|
67
|
+
if (!params) {
|
|
68
|
+
return Object.keys(scan).length === 0;
|
|
69
|
+
}
|
|
70
|
+
for (const [key, value] of Object.entries(scan)) {
|
|
71
|
+
if (value !== undefined && params[key] !== value) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Select specific fields from an item based on select paths.
|
|
79
|
+
*/
|
|
80
|
+
function selectFields(item, selectPaths) {
|
|
81
|
+
const result = {};
|
|
82
|
+
for (const path of selectPaths) {
|
|
83
|
+
// Handle wildcards like "params.*"
|
|
84
|
+
if (path.endsWith(".*")) {
|
|
85
|
+
const namespace = path.slice(0, -2);
|
|
86
|
+
const nsValue = getByPath(item, namespace);
|
|
87
|
+
if (nsValue && typeof nsValue === "object") {
|
|
88
|
+
result[namespace] = { ...nsValue };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
// Regular path like "params.slug" or "frontmatter.title"
|
|
93
|
+
const parts = path.split(".");
|
|
94
|
+
const namespace = parts[0];
|
|
95
|
+
// Ensure namespace exists in result
|
|
96
|
+
if (!result[namespace]) {
|
|
97
|
+
result[namespace] = {};
|
|
98
|
+
}
|
|
99
|
+
// Set the value
|
|
100
|
+
const value = getByPath(item, path);
|
|
101
|
+
if (parts.length === 2) {
|
|
102
|
+
result[namespace][parts[1]] = value;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// Deeper path - just copy the value
|
|
106
|
+
result[namespace] = value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
// =============================================================================
|
|
113
|
+
// Schema Factories
|
|
114
|
+
// =============================================================================
|
|
115
|
+
/**
|
|
116
|
+
* Create a passthrough schema that accepts any value.
|
|
117
|
+
* Used for static content where validation happened at build time.
|
|
118
|
+
*/
|
|
119
|
+
function createPassthroughSchema() {
|
|
120
|
+
return {
|
|
121
|
+
"~standard": {
|
|
122
|
+
version: 1,
|
|
123
|
+
vendor: "piqit/resolvers/static",
|
|
124
|
+
validate(value) {
|
|
125
|
+
return { value: value };
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// =============================================================================
|
|
131
|
+
// Resolver Factory
|
|
132
|
+
// =============================================================================
|
|
133
|
+
/**
|
|
134
|
+
* Create a static content resolver from pre-compiled data.
|
|
135
|
+
*
|
|
136
|
+
* This resolver is designed for edge environments like Cloudflare Workers
|
|
137
|
+
* where filesystem access is not available. Content is compiled at build
|
|
138
|
+
* time and bundled as a static module.
|
|
139
|
+
*
|
|
140
|
+
* @param data - Array of pre-compiled content items
|
|
141
|
+
* @returns A resolver that queries the static data
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* // In your worker:
|
|
145
|
+
* import { posts } from "./generated/content";
|
|
146
|
+
* import { staticContent } from "@piqit/resolvers";
|
|
147
|
+
* import { piq } from "piqit";
|
|
148
|
+
*
|
|
149
|
+
* const postsResolver = staticContent(posts);
|
|
150
|
+
*
|
|
151
|
+
* export default {
|
|
152
|
+
* async fetch(request: Request) {
|
|
153
|
+
* const results = await piq.from(postsResolver)
|
|
154
|
+
* .scan({ year: "2024" })
|
|
155
|
+
* .select("params.slug", "frontmatter.title")
|
|
156
|
+
* .exec();
|
|
157
|
+
*
|
|
158
|
+
* return Response.json(results);
|
|
159
|
+
* }
|
|
160
|
+
* };
|
|
161
|
+
*/
|
|
162
|
+
export function staticContent(data) {
|
|
163
|
+
const scanSchema = createPassthroughSchema();
|
|
164
|
+
const filterSchema = createPassthroughSchema();
|
|
165
|
+
const resultSchema = createPassthroughSchema();
|
|
166
|
+
return {
|
|
167
|
+
schema: {
|
|
168
|
+
scanParams: scanSchema,
|
|
169
|
+
filterParams: filterSchema,
|
|
170
|
+
result: resultSchema,
|
|
171
|
+
},
|
|
172
|
+
async resolve(spec) {
|
|
173
|
+
let results = [...data];
|
|
174
|
+
// Apply scan constraints (filter by params)
|
|
175
|
+
if (spec.scan && Object.keys(spec.scan).length > 0) {
|
|
176
|
+
results = results.filter((item) => matchesScan(item, spec.scan));
|
|
177
|
+
}
|
|
178
|
+
// Apply filter constraints (filter by frontmatter)
|
|
179
|
+
if (spec.filter && Object.keys(spec.filter).length > 0) {
|
|
180
|
+
results = results.filter((item) => defaultFilter(item, spec.filter));
|
|
181
|
+
}
|
|
182
|
+
// Apply select to return only requested fields
|
|
183
|
+
if (spec.select && spec.select.length > 0) {
|
|
184
|
+
return results.map((item) => selectFields(item, spec.select));
|
|
185
|
+
}
|
|
186
|
+
return results;
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Alias for staticContent - provides a more descriptive name for the use case.
|
|
192
|
+
*/
|
|
193
|
+
export const staticResolver = staticContent;
|
|
194
|
+
//# sourceMappingURL=static.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"static.js","sourceRoot":"","sources":["../src/static.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@piqit/resolvers",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./src/index.ts",
|
|
9
|
+
"import": "./src/index.ts"
|
|
10
|
+
},
|
|
11
|
+
"./edge": {
|
|
12
|
+
"types": "./src/edge.ts",
|
|
13
|
+
"import": "./src/edge.ts"
|
|
14
|
+
},
|
|
15
|
+
"./static": {
|
|
16
|
+
"types": "./src/static.ts",
|
|
17
|
+
"import": "./src/static.ts"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"src",
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"piqit": "0.0.2"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/bun": "latest",
|
|
29
|
+
"typescript": "^5.8.3"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/edge.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @piqit/resolvers/edge - Edge-compatible resolvers
|
|
3
|
+
*
|
|
4
|
+
* This entry point only exports resolvers that work in edge environments
|
|
5
|
+
* like Cloudflare Workers, where dynamic code generation is not allowed.
|
|
6
|
+
*
|
|
7
|
+
* Use this instead of '@piqit/resolvers' in your Worker:
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* import { staticContent } from "@piqit/resolvers/edge";
|
|
11
|
+
*
|
|
12
|
+
* @packageDocumentation
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export { staticContent, staticResolver } from "./static"
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem Markdown Resolver
|
|
3
|
+
*
|
|
4
|
+
* A resolver for querying markdown files from the filesystem.
|
|
5
|
+
* Optimized for reading only what's needed based on the query.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Resolver, StandardSchema, Infer } from "piqit"
|
|
9
|
+
import { compilePattern, createParamsSchema, type PathParams } from "./path-pattern"
|
|
10
|
+
import { parseFrontmatter } from "./frontmatter"
|
|
11
|
+
import { parseMarkdownBody, type BodyOptions, type BodyResult, type Heading } from "./markdown"
|
|
12
|
+
import path from "node:path"
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Types
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Options for creating a file markdown resolver.
|
|
20
|
+
*/
|
|
21
|
+
export interface FileMarkdownOptions<
|
|
22
|
+
TPath extends string,
|
|
23
|
+
TFrontmatter extends StandardSchema,
|
|
24
|
+
TBody extends BodyOptions
|
|
25
|
+
> {
|
|
26
|
+
/**
|
|
27
|
+
* Base directory for finding files.
|
|
28
|
+
* Can be absolute or relative to cwd.
|
|
29
|
+
*/
|
|
30
|
+
base: string
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Path pattern with {param} placeholders.
|
|
34
|
+
* @example '{year}/{slug}.md'
|
|
35
|
+
*/
|
|
36
|
+
path: TPath
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Schema for validating frontmatter.
|
|
40
|
+
* The schema's inferred type defines filter parameters.
|
|
41
|
+
*/
|
|
42
|
+
frontmatter: TFrontmatter
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Body parsing options.
|
|
46
|
+
* @default { raw: false, html: false, headings: false }
|
|
47
|
+
*/
|
|
48
|
+
body?: TBody
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The shape of results from a file markdown resolver.
|
|
53
|
+
*/
|
|
54
|
+
export interface FileMarkdownResult<
|
|
55
|
+
TParams,
|
|
56
|
+
TFrontmatter,
|
|
57
|
+
TBody extends BodyResult
|
|
58
|
+
> {
|
|
59
|
+
params: TParams
|
|
60
|
+
frontmatter: TFrontmatter
|
|
61
|
+
body: TBody
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Type for body shape based on options.
|
|
66
|
+
*/
|
|
67
|
+
type ComputedBodyShape<T extends BodyOptions | undefined> = T extends BodyOptions
|
|
68
|
+
? {
|
|
69
|
+
raw: T["raw"] extends true ? string : never
|
|
70
|
+
html: T["html"] extends true ? string : never
|
|
71
|
+
headings: T["headings"] extends true ? Heading[] : never
|
|
72
|
+
}
|
|
73
|
+
: Record<string, never>
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Clean up body shape to remove never types.
|
|
77
|
+
*/
|
|
78
|
+
type CleanBodyShape<T> = {
|
|
79
|
+
[K in keyof T as T[K] extends never ? never : K]: T[K]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// =============================================================================
|
|
83
|
+
// Result Schema Factory
|
|
84
|
+
// =============================================================================
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create a result schema for the resolver.
|
|
88
|
+
* This schema validates the namespaced result shape.
|
|
89
|
+
*/
|
|
90
|
+
function createResultSchema<TFrontmatter, TBody extends BodyResult>(
|
|
91
|
+
_paramNames: string[],
|
|
92
|
+
frontmatterSchema: StandardSchema<TFrontmatter>,
|
|
93
|
+
bodyOptions: BodyOptions
|
|
94
|
+
): StandardSchema<FileMarkdownResult<Record<string, string>, TFrontmatter, TBody>> {
|
|
95
|
+
return {
|
|
96
|
+
"~standard": {
|
|
97
|
+
version: 1,
|
|
98
|
+
vendor: "piqit/resolvers",
|
|
99
|
+
validate(value: unknown) {
|
|
100
|
+
if (value === null || typeof value !== "object") {
|
|
101
|
+
return { issues: [{ message: "Expected object" }] }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const obj = value as Record<string, unknown>
|
|
105
|
+
|
|
106
|
+
// Validate params
|
|
107
|
+
if (obj.params == null || typeof obj.params !== "object") {
|
|
108
|
+
return { issues: [{ message: "Missing params", path: ["params"] }] }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Validate frontmatter using the provided schema
|
|
112
|
+
const fmResult = frontmatterSchema["~standard"].validate(obj.frontmatter)
|
|
113
|
+
if (fmResult.issues) {
|
|
114
|
+
return {
|
|
115
|
+
issues: fmResult.issues.map((issue) => ({
|
|
116
|
+
...issue,
|
|
117
|
+
path: ["frontmatter", ...(issue.path || [])],
|
|
118
|
+
})),
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Validate body shape
|
|
123
|
+
if (bodyOptions.raw || bodyOptions.html || bodyOptions.headings) {
|
|
124
|
+
if (obj.body == null || typeof obj.body !== "object") {
|
|
125
|
+
return { issues: [{ message: "Missing body", path: ["body"] }] }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { value: obj as unknown as FileMarkdownResult<Record<string, string>, TFrontmatter, TBody> }
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// =============================================================================
|
|
136
|
+
// Helper Functions
|
|
137
|
+
// =============================================================================
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check if any select paths require frontmatter data.
|
|
141
|
+
*/
|
|
142
|
+
function needsFrontmatter(selectPaths: string[]): boolean {
|
|
143
|
+
return selectPaths.some((p) => p.startsWith("frontmatter.") || p === "frontmatter.*")
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Check if any select paths require body data.
|
|
148
|
+
*/
|
|
149
|
+
function needsBody(selectPaths: string[]): boolean {
|
|
150
|
+
return selectPaths.some((p) => p.startsWith("body.") || p === "body.*")
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Check if any select paths require params.
|
|
155
|
+
*/
|
|
156
|
+
function needsParams(selectPaths: string[]): boolean {
|
|
157
|
+
return selectPaths.some((p) => p.startsWith("params.") || p === "params.*")
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get which body parts are needed based on select paths.
|
|
162
|
+
*/
|
|
163
|
+
function getNeededBodyParts(selectPaths: string[]): BodyOptions {
|
|
164
|
+
const result: BodyOptions = {}
|
|
165
|
+
|
|
166
|
+
for (const path of selectPaths) {
|
|
167
|
+
if (path === "body.*") {
|
|
168
|
+
// Need all body parts
|
|
169
|
+
return { raw: true, html: true, headings: true }
|
|
170
|
+
}
|
|
171
|
+
if (path === "body.raw") result.raw = true
|
|
172
|
+
if (path === "body.html") result.html = true
|
|
173
|
+
if (path === "body.headings") result.headings = true
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return result
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Check if filter constraints match frontmatter.
|
|
181
|
+
* Simple equality check for now.
|
|
182
|
+
*/
|
|
183
|
+
function matchesFilter(
|
|
184
|
+
frontmatter: Record<string, unknown>,
|
|
185
|
+
filter: Record<string, unknown>
|
|
186
|
+
): boolean {
|
|
187
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
188
|
+
if (frontmatter[key] !== value) {
|
|
189
|
+
return false
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return true
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// =============================================================================
|
|
196
|
+
// Resolver Factory
|
|
197
|
+
// =============================================================================
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Create a filesystem markdown resolver.
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* const postsResolver = fileMarkdown({
|
|
204
|
+
* base: 'content/posts',
|
|
205
|
+
* path: '{year}/{slug}.md',
|
|
206
|
+
* frontmatter: z.object({
|
|
207
|
+
* title: z.string(),
|
|
208
|
+
* status: z.enum(['draft', 'published']),
|
|
209
|
+
* }),
|
|
210
|
+
* body: { html: true, headings: true }
|
|
211
|
+
* })
|
|
212
|
+
*/
|
|
213
|
+
export function fileMarkdown<
|
|
214
|
+
TPath extends string,
|
|
215
|
+
TFrontmatter extends StandardSchema,
|
|
216
|
+
TBody extends BodyOptions = Record<string, never>
|
|
217
|
+
>(
|
|
218
|
+
options: FileMarkdownOptions<TPath, TFrontmatter, TBody>
|
|
219
|
+
): Resolver<
|
|
220
|
+
StandardSchema<Partial<PathParams<TPath>>>,
|
|
221
|
+
TFrontmatter,
|
|
222
|
+
StandardSchema<
|
|
223
|
+
FileMarkdownResult<
|
|
224
|
+
PathParams<TPath>,
|
|
225
|
+
Infer<TFrontmatter>,
|
|
226
|
+
CleanBodyShape<ComputedBodyShape<TBody>>
|
|
227
|
+
>
|
|
228
|
+
>
|
|
229
|
+
> {
|
|
230
|
+
const pattern = compilePattern(options.path)
|
|
231
|
+
const basePath = path.isAbsolute(options.base)
|
|
232
|
+
? options.base
|
|
233
|
+
: path.join(process.cwd(), options.base)
|
|
234
|
+
|
|
235
|
+
const bodyOptions: BodyOptions = options.body || {}
|
|
236
|
+
|
|
237
|
+
// Create schemas
|
|
238
|
+
const scanSchema = createParamsSchema(pattern) as StandardSchema<Partial<PathParams<TPath>>>
|
|
239
|
+
const resultSchema = createResultSchema(
|
|
240
|
+
pattern.paramNames,
|
|
241
|
+
options.frontmatter,
|
|
242
|
+
bodyOptions
|
|
243
|
+
) as StandardSchema<
|
|
244
|
+
FileMarkdownResult<
|
|
245
|
+
PathParams<TPath>,
|
|
246
|
+
Infer<TFrontmatter>,
|
|
247
|
+
CleanBodyShape<ComputedBodyShape<TBody>>
|
|
248
|
+
>
|
|
249
|
+
>
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
schema: {
|
|
253
|
+
scanParams: scanSchema,
|
|
254
|
+
filterParams: options.frontmatter,
|
|
255
|
+
result: resultSchema,
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
async resolve(spec) {
|
|
259
|
+
const results: Array<
|
|
260
|
+
Partial<
|
|
261
|
+
FileMarkdownResult<
|
|
262
|
+
PathParams<TPath>,
|
|
263
|
+
Infer<TFrontmatter>,
|
|
264
|
+
CleanBodyShape<ComputedBodyShape<TBody>>
|
|
265
|
+
>
|
|
266
|
+
>
|
|
267
|
+
> = []
|
|
268
|
+
|
|
269
|
+
// 1. Generate glob pattern from scan constraints
|
|
270
|
+
const globPattern = pattern.toGlob(spec.scan as Record<string, unknown>)
|
|
271
|
+
|
|
272
|
+
// 2. Find matching files using Bun.Glob
|
|
273
|
+
const glob = new Bun.Glob(globPattern)
|
|
274
|
+
const files: string[] = []
|
|
275
|
+
|
|
276
|
+
for await (const file of glob.scan({ cwd: basePath, absolute: false })) {
|
|
277
|
+
files.push(file)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// 3. Determine what we need to read
|
|
281
|
+
const wantParams = needsParams(spec.select)
|
|
282
|
+
const wantFrontmatter = needsFrontmatter(spec.select)
|
|
283
|
+
const wantBody = needsBody(spec.select)
|
|
284
|
+
const hasFilter = spec.filter && Object.keys(spec.filter).length > 0
|
|
285
|
+
const neededBodyParts = wantBody ? getNeededBodyParts(spec.select) : {}
|
|
286
|
+
|
|
287
|
+
// 4. Process each file
|
|
288
|
+
for (const relativePath of files) {
|
|
289
|
+
// Extract params from path
|
|
290
|
+
const params = pattern.match(relativePath)
|
|
291
|
+
if (!params) continue
|
|
292
|
+
|
|
293
|
+
const fullPath = path.join(basePath, relativePath)
|
|
294
|
+
|
|
295
|
+
// Read file content only if needed
|
|
296
|
+
let content: string | null = null
|
|
297
|
+
let frontmatter: Record<string, unknown> | null = null
|
|
298
|
+
let body: BodyResult | null = null
|
|
299
|
+
|
|
300
|
+
// If filtering or selecting frontmatter, we need to read it
|
|
301
|
+
if (hasFilter || wantFrontmatter) {
|
|
302
|
+
content = await Bun.file(fullPath).text()
|
|
303
|
+
frontmatter = parseFrontmatter(content)
|
|
304
|
+
|
|
305
|
+
if (!frontmatter) {
|
|
306
|
+
frontmatter = {}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Check filter constraints
|
|
310
|
+
if (hasFilter && !matchesFilter(frontmatter, spec.filter as Record<string, unknown>)) {
|
|
311
|
+
continue
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// If selecting body, parse it
|
|
316
|
+
if (wantBody) {
|
|
317
|
+
if (!content) {
|
|
318
|
+
content = await Bun.file(fullPath).text()
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Only parse the body parts that are needed
|
|
322
|
+
body = parseMarkdownBody(content, neededBodyParts)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Build result with only requested fields
|
|
326
|
+
const result: Partial<
|
|
327
|
+
FileMarkdownResult<
|
|
328
|
+
PathParams<TPath>,
|
|
329
|
+
Infer<TFrontmatter>,
|
|
330
|
+
CleanBodyShape<ComputedBodyShape<TBody>>
|
|
331
|
+
>
|
|
332
|
+
> = {}
|
|
333
|
+
|
|
334
|
+
if (wantParams) {
|
|
335
|
+
result.params = params as PathParams<TPath>
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (wantFrontmatter) {
|
|
339
|
+
result.frontmatter = frontmatter as Infer<TFrontmatter>
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (wantBody && body) {
|
|
343
|
+
result.body = body as CleanBodyShape<ComputedBodyShape<TBody>>
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
results.push(result)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return results
|
|
350
|
+
},
|
|
351
|
+
}
|
|
352
|
+
}
|