@usegraft/compiler 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.js +290 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anderson Joseph
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { AnyCollection } from '@usegraft/core';
|
|
2
|
+
import { Database, ChangeSet } from '@usegraft/db';
|
|
3
|
+
|
|
4
|
+
/** One asset reference found in a document's frontmatter. */
|
|
5
|
+
interface DocAsset {
|
|
6
|
+
/** The field the reference came from. */
|
|
7
|
+
field: string;
|
|
8
|
+
/** Key of the binary in the asset store. */
|
|
9
|
+
key: string;
|
|
10
|
+
alt?: string;
|
|
11
|
+
}
|
|
12
|
+
interface ProjectedDoc {
|
|
13
|
+
collection: string;
|
|
14
|
+
slug: string;
|
|
15
|
+
data: Record<string, unknown>;
|
|
16
|
+
body: string;
|
|
17
|
+
contentHash: string;
|
|
18
|
+
sourcePath: string;
|
|
19
|
+
/** Asset references indexed from `asset` fields (validated by the schema). */
|
|
20
|
+
assets: DocAsset[];
|
|
21
|
+
}
|
|
22
|
+
declare function parseDocument(raw: string, collection: AnyCollection, sourcePath: string): ProjectedDoc;
|
|
23
|
+
|
|
24
|
+
interface CompileOptions {
|
|
25
|
+
contentDir: string;
|
|
26
|
+
collections: Record<string, AnyCollection>;
|
|
27
|
+
db: Database;
|
|
28
|
+
branchId?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Git commit recorded with this compilation. Omit to auto-resolve from
|
|
31
|
+
* `contentDir` (tolerant: records null outside a git repo); pass null to skip.
|
|
32
|
+
*/
|
|
33
|
+
gitSha?: string | null;
|
|
34
|
+
/**
|
|
35
|
+
* Also remove index rows in collections this schema doesn't know. Off by
|
|
36
|
+
* default: purging unknown collections is the signature of two projects
|
|
37
|
+
* sharing one DATABASE_URL, so projection aborts with INDEX_OWNERSHIP
|
|
38
|
+
* unless this is set (legitimate after a collection rename/delete).
|
|
39
|
+
*/
|
|
40
|
+
pruneUnknown?: boolean;
|
|
41
|
+
}
|
|
42
|
+
interface CompileResult {
|
|
43
|
+
count: number;
|
|
44
|
+
docs: ProjectedDoc[];
|
|
45
|
+
/** What this run did to the index: added / changed / removed keys + unchanged count. */
|
|
46
|
+
changes: ChangeSet;
|
|
47
|
+
/** The commit the content tree was compiled from, when resolvable. */
|
|
48
|
+
gitSha: string | null;
|
|
49
|
+
}
|
|
50
|
+
/** Read + validate the content tree into normalized docs. Pure of the database. */
|
|
51
|
+
declare function readDocs(contentDir: string, collections: Record<string, AnyCollection>): ProjectedDoc[];
|
|
52
|
+
declare function compile(options: CompileOptions): Promise<CompileResult>;
|
|
53
|
+
interface CompileStaticOptions {
|
|
54
|
+
contentDir: string;
|
|
55
|
+
collections: Record<string, AnyCollection>;
|
|
56
|
+
/** Artifact path, e.g. <project>/.graft/index.db. */
|
|
57
|
+
indexPath: string;
|
|
58
|
+
/** As in CompileOptions: omit to auto-resolve, null to skip. */
|
|
59
|
+
gitSha?: string | null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The static-mode sibling of `compile`: same validate step (readDocs), but the
|
|
63
|
+
* projection target is the SQLite artifact — no database, no env. Branching is
|
|
64
|
+
* git's job here, so there is no branchId; the artifact records "main".
|
|
65
|
+
*/
|
|
66
|
+
declare function compileStatic(options: CompileStaticOptions): Promise<CompileResult>;
|
|
67
|
+
/** HEAD commit of the repo containing `cwd`, or null (no git, no repo — never throws). */
|
|
68
|
+
declare function resolveGitSha(cwd: string): string | null;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Write a document's bytes, translating a read-only filesystem into an error
|
|
72
|
+
* that explains itself. Serverless platforms deploy an immutable filesystem, so
|
|
73
|
+
* a Studio or MCP write served from one fails deep inside fs with an opaque
|
|
74
|
+
* errno; authored content being files is the whole model, so the fix is to run
|
|
75
|
+
* the writing surface where the checkout is writable.
|
|
76
|
+
*/
|
|
77
|
+
declare function writeDocumentFile(fullPath: string, raw: string): void;
|
|
78
|
+
/**
|
|
79
|
+
* The bytes to write for a document.
|
|
80
|
+
*
|
|
81
|
+
* @param existingRaw The file's current contents, or undefined for a new file.
|
|
82
|
+
* @returns the existing frontmatter with only genuinely changed keys rewritten,
|
|
83
|
+
* plus the new body. Falls back to a full `matter.stringify` when the block
|
|
84
|
+
* cannot be patched safely.
|
|
85
|
+
*/
|
|
86
|
+
declare function composeDocument(existingRaw: string | undefined, data: Record<string, unknown>, body: string): string;
|
|
87
|
+
|
|
88
|
+
export { type CompileOptions, type CompileResult, type CompileStaticOptions, type DocAsset, type ProjectedDoc, compile, compileStatic, composeDocument, parseDocument, readDocs, resolveGitSha, writeDocumentFile };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// src/parse.ts
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
import { basename } from "path";
|
|
4
|
+
import { GraftError } from "@usegraft/contracts";
|
|
5
|
+
import matter from "gray-matter";
|
|
6
|
+
var SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
7
|
+
function parseDocument(raw, collection, sourcePath) {
|
|
8
|
+
const { data: frontmatter, content } = matter(raw);
|
|
9
|
+
const result = collection.schema.safeParse(frontmatter);
|
|
10
|
+
if (!result.success) {
|
|
11
|
+
const issues = result.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
12
|
+
throw new GraftError({
|
|
13
|
+
code: "SCHEMA_VALIDATION_FAILED",
|
|
14
|
+
message: `${sourcePath} does not match collection "${collection.name}"`,
|
|
15
|
+
fix: `Update the frontmatter to satisfy the "${collection.name}" schema \u2014 ${issues}`,
|
|
16
|
+
details: { sourcePath, issues: result.error.issues }
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
const slugValue = frontmatter.slug;
|
|
20
|
+
const slug = typeof slugValue === "string" && slugValue.length > 0 ? slugValue : basename(sourcePath).replace(/\.mdx?$/, "");
|
|
21
|
+
if (!SLUG_RE.test(slug)) {
|
|
22
|
+
throw new GraftError({
|
|
23
|
+
code: "INVALID_SLUG",
|
|
24
|
+
message: `Slug "${slug}" (${sourcePath}) is not URL-safe`,
|
|
25
|
+
fix: `Slugs must be kebab-case: lowercase letters, digits, and single hyphens (e.g. "getting-started"). Set a valid \`slug\` in frontmatter or rename the file.`,
|
|
26
|
+
details: { slug, sourcePath, pattern: SLUG_RE.source }
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
const data = result.data;
|
|
30
|
+
return {
|
|
31
|
+
collection: collection.name,
|
|
32
|
+
slug,
|
|
33
|
+
data,
|
|
34
|
+
body: content.trim(),
|
|
35
|
+
contentHash: createHash("sha256").update(raw).digest("hex"),
|
|
36
|
+
sourcePath,
|
|
37
|
+
assets: collectAssets(collection, data)
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function collectAssets(collection, data) {
|
|
41
|
+
const assets = [];
|
|
42
|
+
for (const [name, definition] of Object.entries(collection.fields)) {
|
|
43
|
+
if (definition.type !== "asset") continue;
|
|
44
|
+
const value = data[name];
|
|
45
|
+
if (value === void 0 || value === null) continue;
|
|
46
|
+
const ref = value;
|
|
47
|
+
assets.push({ field: name, key: ref.key, ...ref.alt !== void 0 && { alt: ref.alt } });
|
|
48
|
+
}
|
|
49
|
+
return assets;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/compile.ts
|
|
53
|
+
import { execFileSync } from "child_process";
|
|
54
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
55
|
+
import { join, relative, sep } from "path";
|
|
56
|
+
import { GraftError as GraftError2 } from "@usegraft/contracts";
|
|
57
|
+
import {
|
|
58
|
+
projectBranchContent,
|
|
59
|
+
projectStaticContent
|
|
60
|
+
} from "@usegraft/db";
|
|
61
|
+
function readDocs(contentDir, collections) {
|
|
62
|
+
if (!existsSync(contentDir) || !statSync(contentDir).isDirectory()) {
|
|
63
|
+
throw new GraftError2({
|
|
64
|
+
code: "CONTENT_DIR_NOT_FOUND",
|
|
65
|
+
message: `Content directory not found: ${contentDir}`,
|
|
66
|
+
fix: `Create it (documents live at <contentDir>/<collection>/<slug>.mdx, e.g. content/pages/home.mdx) or pass the correct \`contentDir\` to compile().`,
|
|
67
|
+
details: { contentDir, collections: Object.keys(collections) }
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const docs = [];
|
|
71
|
+
const seen = /* @__PURE__ */ new Map();
|
|
72
|
+
for (const file of walk(contentDir)) {
|
|
73
|
+
const sourcePath = relative(contentDir, file).split(sep).join("/");
|
|
74
|
+
const segments = sourcePath.split("/");
|
|
75
|
+
const collectionName = segments.length > 1 ? segments[0] ?? "" : "";
|
|
76
|
+
const collection = collections[collectionName];
|
|
77
|
+
if (!collection) {
|
|
78
|
+
const known = Object.keys(collections).join(", ") || "(none registered)";
|
|
79
|
+
throw new GraftError2({
|
|
80
|
+
code: "COLLECTION_NOT_FOUND",
|
|
81
|
+
message: segments.length > 1 ? `No collection registered for "${collectionName}" (${sourcePath})` : `${sourcePath} sits at the content root; documents must live in a collection folder`,
|
|
82
|
+
fix: segments.length > 1 ? `Register it \u2014 defineCollection({ name: "${collectionName}", fields: { \u2026 } }) and pass it to compile() \u2014 or move the file into one of: ${known}.` : `Move it to <contentDir>/<collection>/${sourcePath}. Registered collections: ${known}.`,
|
|
83
|
+
details: { sourcePath, collection: collectionName, registered: Object.keys(collections) }
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (collection.authority === "db-authoritative") {
|
|
87
|
+
throw new GraftError2({
|
|
88
|
+
code: "AUTHORITY_MISMATCH",
|
|
89
|
+
message: `${sourcePath} is a file, but collection "${collectionName}" is db-authoritative \u2014 its records live in Postgres, not in the content tree.`,
|
|
90
|
+
fix: `Remove the file and write the data through the collection's functions instead (insertRecord via a defineFunction mutation). Files are only for file-authoritative collections.`,
|
|
91
|
+
details: { sourcePath, collection: collectionName, authority: collection.authority }
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const doc = parseDocument(readFileSync(file, "utf8"), collection, sourcePath);
|
|
95
|
+
const key = `${doc.collection}/${doc.slug}`;
|
|
96
|
+
const existing = seen.get(key);
|
|
97
|
+
if (existing) {
|
|
98
|
+
throw new GraftError2({
|
|
99
|
+
code: "SLUG_NOT_UNIQUE",
|
|
100
|
+
message: `Slug "${doc.slug}" in collection "${doc.collection}" is used by both ${existing} and ${sourcePath}`,
|
|
101
|
+
fix: `Give one a unique \`slug\` in frontmatter, or rename the file.`,
|
|
102
|
+
details: { slug: doc.slug, collection: doc.collection, files: [existing, sourcePath] }
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
seen.set(key, sourcePath);
|
|
106
|
+
docs.push(doc);
|
|
107
|
+
}
|
|
108
|
+
return docs.sort(
|
|
109
|
+
(a, b) => a.collection === b.collection ? a.slug.localeCompare(b.slug) : a.collection.localeCompare(b.collection)
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
async function compile(options) {
|
|
113
|
+
const docs = readDocs(options.contentDir, options.collections);
|
|
114
|
+
const gitSha = options.gitSha === void 0 ? resolveGitSha(options.contentDir) : options.gitSha;
|
|
115
|
+
const changes = await projectBranchContent(
|
|
116
|
+
options.db,
|
|
117
|
+
docs.map((doc) => ({
|
|
118
|
+
collection: doc.collection,
|
|
119
|
+
slug: doc.slug,
|
|
120
|
+
data: doc.data,
|
|
121
|
+
body: doc.body,
|
|
122
|
+
contentHash: doc.contentHash,
|
|
123
|
+
sourcePath: doc.sourcePath
|
|
124
|
+
})),
|
|
125
|
+
{
|
|
126
|
+
branchId: options.branchId ?? "main",
|
|
127
|
+
gitSha,
|
|
128
|
+
knownCollections: Object.keys(options.collections),
|
|
129
|
+
pruneUnknown: options.pruneUnknown
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
return { count: docs.length, docs, changes, gitSha };
|
|
133
|
+
}
|
|
134
|
+
async function compileStatic(options) {
|
|
135
|
+
const docs = readDocs(options.contentDir, options.collections);
|
|
136
|
+
const gitSha = options.gitSha === void 0 ? resolveGitSha(options.contentDir) : options.gitSha;
|
|
137
|
+
const changes = await projectStaticContent(
|
|
138
|
+
docs.map((doc) => ({
|
|
139
|
+
collection: doc.collection,
|
|
140
|
+
slug: doc.slug,
|
|
141
|
+
data: doc.data,
|
|
142
|
+
body: doc.body,
|
|
143
|
+
contentHash: doc.contentHash,
|
|
144
|
+
sourcePath: doc.sourcePath
|
|
145
|
+
})),
|
|
146
|
+
{ path: options.indexPath, gitSha }
|
|
147
|
+
);
|
|
148
|
+
return { count: docs.length, docs, changes, gitSha };
|
|
149
|
+
}
|
|
150
|
+
function resolveGitSha(cwd) {
|
|
151
|
+
try {
|
|
152
|
+
return execFileSync("git", ["rev-parse", "HEAD"], {
|
|
153
|
+
cwd,
|
|
154
|
+
encoding: "utf8",
|
|
155
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
156
|
+
}).trim();
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function walk(dir) {
|
|
162
|
+
const out = [];
|
|
163
|
+
for (const name of readdirSync(dir)) {
|
|
164
|
+
const full = join(dir, name);
|
|
165
|
+
if (statSync(full).isDirectory()) out.push(...walk(full));
|
|
166
|
+
else if (/\.mdx?$/.test(name)) out.push(full);
|
|
167
|
+
}
|
|
168
|
+
return out.sort();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/serialize.ts
|
|
172
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
173
|
+
import { dirname } from "path";
|
|
174
|
+
import { GraftError as GraftError3 } from "@usegraft/contracts";
|
|
175
|
+
import matter2 from "gray-matter";
|
|
176
|
+
var READ_ONLY_CODES = /* @__PURE__ */ new Set(["EROFS", "EACCES", "EPERM"]);
|
|
177
|
+
function writeDocumentFile(fullPath, raw) {
|
|
178
|
+
try {
|
|
179
|
+
mkdirSync(dirname(fullPath), { recursive: true });
|
|
180
|
+
writeFileSync(fullPath, raw);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
const code = error.code ?? "";
|
|
183
|
+
if (!READ_ONLY_CODES.has(code)) throw error;
|
|
184
|
+
throw new GraftError3({
|
|
185
|
+
code: "CONTENT_TREE_READ_ONLY",
|
|
186
|
+
message: `Cannot write ${fullPath}: the filesystem refused it (${code}).`,
|
|
187
|
+
fix: "Authored content lives in files, so writing needs a writable checkout. Run this surface locally (`graft studio` / `graft mcp`) or in a container with the project mounted read-write; a serverless deployment's filesystem is read-only and should serve reads only.",
|
|
188
|
+
details: { path: fullPath, errno: code }
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
var FRONTMATTER_BLOCK = /^(?---\r?\n[\s\S]*?\r?\n---[ \t]*\r?\n)([\s\S]*)$/;
|
|
193
|
+
function sameData(a, b) {
|
|
194
|
+
if (a === b) return true;
|
|
195
|
+
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
|
|
196
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
197
|
+
return a.length === b.length && a.every((item, i) => sameData(item, b[i]));
|
|
198
|
+
}
|
|
199
|
+
if (typeof a === "object" && typeof b === "object" && a !== null && b !== null) {
|
|
200
|
+
const aKeys = Object.keys(a);
|
|
201
|
+
const bKeys = Object.keys(b);
|
|
202
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
203
|
+
return aKeys.every(
|
|
204
|
+
(key) => Object.hasOwn(b, key) && sameData(a[key], b[key])
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
var BLOCK_PARTS = /^(?---[ \t]*\r?\n)([\s\S]*?)(\r?\n---[ \t]*\r?\n)$/;
|
|
210
|
+
var TOP_LEVEL_KEY = /^([A-Za-z0-9_$-]+|"(?:[^"\\]|\\.)*"|'(?:[^']|'')*'):(?:\s|$)/;
|
|
211
|
+
function unquoteKey(key) {
|
|
212
|
+
if (key.startsWith('"') && key.endsWith('"')) return JSON.parse(key);
|
|
213
|
+
if (key.startsWith("'") && key.endsWith("'")) return key.slice(1, -1).replace(/''/g, "'");
|
|
214
|
+
return key;
|
|
215
|
+
}
|
|
216
|
+
function scanKeys(yaml) {
|
|
217
|
+
const lines = yaml.split(/\r?\n/);
|
|
218
|
+
const out = [];
|
|
219
|
+
for (const line of lines) {
|
|
220
|
+
const match = TOP_LEVEL_KEY.exec(line);
|
|
221
|
+
if (match?.[1]) {
|
|
222
|
+
out.push({ key: unquoteKey(match[1]), lines: [line] });
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (out.length === 0) {
|
|
226
|
+
if (line.trim() === "") continue;
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
out[out.length - 1]?.lines.push(line);
|
|
230
|
+
}
|
|
231
|
+
return out.length > 0 ? out : null;
|
|
232
|
+
}
|
|
233
|
+
function renderKey(key, value) {
|
|
234
|
+
const dumped = matter2.stringify("", { [key]: value });
|
|
235
|
+
const block = FRONTMATTER_BLOCK.exec(dumped)?.[1];
|
|
236
|
+
if (!block) return null;
|
|
237
|
+
const inner = BLOCK_PARTS.exec(block)?.[2];
|
|
238
|
+
if (inner === void 0) return null;
|
|
239
|
+
return inner.split(/\r?\n/);
|
|
240
|
+
}
|
|
241
|
+
function patchFrontmatter(block, original, data, newline) {
|
|
242
|
+
const parts = BLOCK_PARTS.exec(block);
|
|
243
|
+
if (!parts) return null;
|
|
244
|
+
const [, open = "", yaml = "", close = ""] = parts;
|
|
245
|
+
const scanned = scanKeys(yaml);
|
|
246
|
+
if (!scanned) return null;
|
|
247
|
+
const out = [];
|
|
248
|
+
const kept = /* @__PURE__ */ new Set();
|
|
249
|
+
for (const entry of scanned) {
|
|
250
|
+
if (!Object.hasOwn(data, entry.key)) continue;
|
|
251
|
+
kept.add(entry.key);
|
|
252
|
+
if (Object.hasOwn(original, entry.key) && sameData(original[entry.key], data[entry.key])) {
|
|
253
|
+
out.push(...entry.lines);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const rendered = renderKey(entry.key, data[entry.key]);
|
|
257
|
+
if (!rendered) return null;
|
|
258
|
+
out.push(...rendered);
|
|
259
|
+
}
|
|
260
|
+
for (const key of Object.keys(data)) {
|
|
261
|
+
if (kept.has(key)) continue;
|
|
262
|
+
const rendered = renderKey(key, data[key]);
|
|
263
|
+
if (!rendered) return null;
|
|
264
|
+
out.push(...rendered);
|
|
265
|
+
}
|
|
266
|
+
if (out.length === 0) return null;
|
|
267
|
+
return open + out.join(newline) + close;
|
|
268
|
+
}
|
|
269
|
+
function composeDocument(existingRaw, data, body) {
|
|
270
|
+
if (existingRaw === void 0) return matter2.stringify(body, data);
|
|
271
|
+
const match = FRONTMATTER_BLOCK.exec(existingRaw);
|
|
272
|
+
if (!match) return matter2.stringify(body, data);
|
|
273
|
+
const [, block = "", rest = ""] = match;
|
|
274
|
+
const newline = /\r\n/.test(block) ? "\r\n" : "\n";
|
|
275
|
+
const original = matter2(existingRaw).data;
|
|
276
|
+
const frontmatter = sameData(original, data) ? block : patchFrontmatter(block, original, data, newline);
|
|
277
|
+
if (frontmatter === null) return matter2.stringify(body, data);
|
|
278
|
+
const separator = /^\r?\n/.exec(rest)?.[0] ?? "";
|
|
279
|
+
const needsNewline = !body.endsWith("\n") && existingRaw.endsWith("\n");
|
|
280
|
+
return frontmatter + separator + body + (needsNewline ? newline : "");
|
|
281
|
+
}
|
|
282
|
+
export {
|
|
283
|
+
compile,
|
|
284
|
+
compileStatic,
|
|
285
|
+
composeDocument,
|
|
286
|
+
parseDocument,
|
|
287
|
+
readDocs,
|
|
288
|
+
resolveGitSha,
|
|
289
|
+
writeDocumentFile
|
|
290
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@usegraft/compiler",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/AndersonDesign1/graft.git",
|
|
8
|
+
"directory": "packages/compiler"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"module": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"gray-matter": "^4.0.3",
|
|
28
|
+
"@usegraft/contracts": "0.1.0",
|
|
29
|
+
"@usegraft/core": "0.1.0",
|
|
30
|
+
"@usegraft/db": "0.1.0"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=22.16"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
37
|
+
"dev": "tsup src/index.ts --format esm --watch",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"test": "vitest run --passWithNoTests",
|
|
40
|
+
"lint": "oxlint ."
|
|
41
|
+
}
|
|
42
|
+
}
|