@aaronellington/vite-plugin-inkwell 0.0.4 → 0.0.6
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 +6 -0
- package/dist/assets.d.ts +5 -0
- package/dist/assets.d.ts.map +1 -0
- package/dist/assets.js +50 -0
- package/dist/assets.js.map +1 -0
- package/dist/content.d.ts +13 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +110 -0
- package/dist/content.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +4 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +195 -0
- package/dist/plugin.js.map +1 -0
- package/dist/types.d.ts +49 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/{content.d.ts → global.d.ts} +1 -1
- package/package.json +13 -5
- package/src/assets.ts +0 -84
- package/src/content.ts +0 -143
- package/src/index.ts +0 -7
- package/src/plugin.ts +0 -248
- package/src/types.ts +0 -53
package/README.md
CHANGED
|
@@ -24,6 +24,12 @@ export default defineConfig({
|
|
|
24
24
|
});
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
For TypeScript support of `inkwell:*` imports, add a reference in a `.d.ts` file (e.g. `env.d.ts`):
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
/// <reference types="@aaronellington/vite-plugin-inkwell/types" />
|
|
31
|
+
```
|
|
32
|
+
|
|
27
33
|
## Usage
|
|
28
34
|
|
|
29
35
|
Import a content directory using the `inkwell:` prefix. The path resolves relative to the importing file:
|
package/dist/assets.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AssetReference } from "./types.js";
|
|
2
|
+
export declare function extractAssetReferences(html: string, mdFilePath: string): AssetReference[];
|
|
3
|
+
export declare function replaceAssetsWithPlaceholders(html: string, assets: AssetReference[]): string;
|
|
4
|
+
export declare function generateSlugModuleCode(html: string, assets: AssetReference[]): string;
|
|
5
|
+
//# sourceMappingURL=assets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assets.d.ts","sourceRoot":"","sources":["../src/assets.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAKhD,wBAAgB,sBAAsB,CACrC,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,GAChB,cAAc,EAAE,CA6BlB;AAED,wBAAgB,6BAA6B,CAC5C,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,cAAc,EAAE,GACtB,MAAM,CAMR;AAED,wBAAgB,sBAAsB,CACrC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,cAAc,EAAE,GACtB,MAAM,CA4BR"}
|
package/dist/assets.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const ASSET_REGEX = /(?:src|href|poster)=["'](?!(?:https?:|data:|#|\/\/|\/))([^"']+)["']/g;
|
|
4
|
+
export function extractAssetReferences(html, mdFilePath) {
|
|
5
|
+
const mdDir = path.dirname(mdFilePath);
|
|
6
|
+
const assets = [];
|
|
7
|
+
const seen = new Set();
|
|
8
|
+
ASSET_REGEX.lastIndex = 0;
|
|
9
|
+
for (let match = ASSET_REGEX.exec(html); match !== null; match = ASSET_REGEX.exec(html)) {
|
|
10
|
+
const originalPath = match[1];
|
|
11
|
+
if (seen.has(originalPath))
|
|
12
|
+
continue;
|
|
13
|
+
seen.add(originalPath);
|
|
14
|
+
const absolutePath = path.resolve(mdDir, originalPath);
|
|
15
|
+
if (!fs.existsSync(absolutePath)) {
|
|
16
|
+
throw new Error(`Missing asset referenced in ${mdFilePath}: "${originalPath}"\n` +
|
|
17
|
+
`Resolved to: ${absolutePath}`);
|
|
18
|
+
}
|
|
19
|
+
const placeholderToken = `__CONTENT_ASSET_${assets.length}__`;
|
|
20
|
+
assets.push({ absolutePath, originalPath, placeholderToken });
|
|
21
|
+
}
|
|
22
|
+
return assets;
|
|
23
|
+
}
|
|
24
|
+
export function replaceAssetsWithPlaceholders(html, assets) {
|
|
25
|
+
let result = html;
|
|
26
|
+
for (const asset of assets) {
|
|
27
|
+
result = result.split(asset.originalPath).join(asset.placeholderToken);
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
export function generateSlugModuleCode(html, assets) {
|
|
32
|
+
if (assets.length === 0) {
|
|
33
|
+
return `export default ${JSON.stringify(html)};`;
|
|
34
|
+
}
|
|
35
|
+
const lines = [];
|
|
36
|
+
for (let i = 0; i < assets.length; i++) {
|
|
37
|
+
const asset = assets[i];
|
|
38
|
+
lines.push(`import __asset_${i}__ from ${JSON.stringify(asset.absolutePath)};`);
|
|
39
|
+
}
|
|
40
|
+
lines.push("");
|
|
41
|
+
lines.push(`let html = ${JSON.stringify(html)};`);
|
|
42
|
+
for (let i = 0; i < assets.length; i++) {
|
|
43
|
+
const asset = assets[i];
|
|
44
|
+
lines.push(`html = html.replaceAll(${JSON.stringify(asset.placeholderToken)}, __asset_${i}__);`);
|
|
45
|
+
}
|
|
46
|
+
lines.push("");
|
|
47
|
+
lines.push("export default html;");
|
|
48
|
+
return lines.join("\n");
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=assets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assets.js","sourceRoot":"","sources":["../src/assets.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { MarkedExtension } from "marked";
|
|
2
|
+
import { Marked } from "marked";
|
|
3
|
+
import type { ParsedContentItem } from "./types.js";
|
|
4
|
+
export declare function scanDirectory(dir: string, recursive: boolean): string[];
|
|
5
|
+
export declare function parseFrontmatter(fileContent: string, filePath: string): {
|
|
6
|
+
data: Record<string, unknown>;
|
|
7
|
+
content: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function computeSlug(frontmatter: Record<string, unknown>, filePath: string): string;
|
|
10
|
+
export declare function checkDuplicateSlugs(items: ParsedContentItem[]): void;
|
|
11
|
+
export declare function createRenderer(extensions: MarkedExtension[]): Marked;
|
|
12
|
+
export declare function parseContentFile(filePath: string, baseDir: string, renderer: Marked, validate?: (frontmatter: Record<string, unknown>, filePath: string) => void): ParsedContentItem;
|
|
13
|
+
//# sourceMappingURL=content.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAE/B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAavE,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,MAAM,EAAE,CAcvE;AAED,wBAAgB,gBAAgB,CAC/B,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,GACd;IAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAQpD;AAED,wBAAgB,WAAW,CAC1B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,QAAQ,EAAE,MAAM,GACd,MAAM,CAQR;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,iBAAiB,EAAE,GAAG,IAAI,CAcpE;AAeD,wBAAgB,cAAc,CAAC,UAAU,EAAE,eAAe,EAAE,GAAG,MAAM,CAOpE;AAED,wBAAgB,gBAAgB,CAC/B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,QAAQ,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,GACzE,iBAAiB,CAsCnB"}
|
package/dist/content.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import matter from "gray-matter";
|
|
4
|
+
import { Marked } from "marked";
|
|
5
|
+
import { parse as parseToml } from "smol-toml";
|
|
6
|
+
const matterOptions = {
|
|
7
|
+
engines: {
|
|
8
|
+
toml: {
|
|
9
|
+
parse: parseToml,
|
|
10
|
+
stringify: () => {
|
|
11
|
+
throw new Error("TOML stringify not supported");
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
export function scanDirectory(dir, recursive) {
|
|
17
|
+
const results = [];
|
|
18
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
const fullPath = path.join(dir, entry.name);
|
|
21
|
+
if (entry.isDirectory() && recursive) {
|
|
22
|
+
results.push(...scanDirectory(fullPath, recursive));
|
|
23
|
+
}
|
|
24
|
+
else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
25
|
+
results.push(fullPath);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return results;
|
|
29
|
+
}
|
|
30
|
+
export function parseFrontmatter(fileContent, filePath) {
|
|
31
|
+
try {
|
|
32
|
+
const result = matter(fileContent, matterOptions);
|
|
33
|
+
return { content: result.content, data: result.data };
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
37
|
+
throw new Error(`Invalid frontmatter in ${filePath}: ${message}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function computeSlug(frontmatter, filePath) {
|
|
41
|
+
if (typeof frontmatter.slug === "string" &&
|
|
42
|
+
frontmatter.slug.trim().length > 0) {
|
|
43
|
+
return frontmatter.slug.trim();
|
|
44
|
+
}
|
|
45
|
+
return path.basename(filePath, ".md");
|
|
46
|
+
}
|
|
47
|
+
export function checkDuplicateSlugs(items) {
|
|
48
|
+
const seen = new Map();
|
|
49
|
+
for (const item of items) {
|
|
50
|
+
const existing = seen.get(item.frontmatter.slug);
|
|
51
|
+
if (existing) {
|
|
52
|
+
throw new Error(`Duplicate slug "${item.frontmatter.slug}" found in:\n` +
|
|
53
|
+
` - ${existing}\n` +
|
|
54
|
+
` - ${item.filePath}\n` +
|
|
55
|
+
`Provide an explicit "slug" in frontmatter to resolve.`);
|
|
56
|
+
}
|
|
57
|
+
seen.set(item.frontmatter.slug, item.filePath);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const FILE_EXT_REGEX = /\.\w{1,10}$/;
|
|
61
|
+
const assetLinkExtension = {
|
|
62
|
+
renderer: {
|
|
63
|
+
link({ href, text }) {
|
|
64
|
+
if (href && FILE_EXT_REGEX.test(href)) {
|
|
65
|
+
return `<a href="${href}" target="_blank" rel="noopener noreferrer">${text}</a>`;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
export function createRenderer(extensions) {
|
|
72
|
+
const marked = new Marked();
|
|
73
|
+
marked.use(assetLinkExtension);
|
|
74
|
+
if (extensions.length > 0) {
|
|
75
|
+
marked.use(...extensions);
|
|
76
|
+
}
|
|
77
|
+
return marked;
|
|
78
|
+
}
|
|
79
|
+
export function parseContentFile(filePath, baseDir, renderer, validate) {
|
|
80
|
+
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
81
|
+
const { data, content } = parseFrontmatter(fileContent, filePath);
|
|
82
|
+
if (validate) {
|
|
83
|
+
validate(data, filePath);
|
|
84
|
+
}
|
|
85
|
+
const slug = computeSlug(data, filePath);
|
|
86
|
+
const directoryPath = path.relative(baseDir, path.dirname(filePath));
|
|
87
|
+
const html = renderer.parse(content);
|
|
88
|
+
if (typeof html !== "string") {
|
|
89
|
+
throw new Error(`Async marked extensions are not supported. File: ${filePath}`);
|
|
90
|
+
}
|
|
91
|
+
const frontmatter = {
|
|
92
|
+
...data,
|
|
93
|
+
date: typeof data.date === "string"
|
|
94
|
+
? data.date
|
|
95
|
+
: data.date instanceof Date
|
|
96
|
+
? data.date.toISOString()
|
|
97
|
+
: "",
|
|
98
|
+
draft: data.draft === true,
|
|
99
|
+
slug,
|
|
100
|
+
title: typeof data.title === "string" ? data.title : "",
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
assets: [],
|
|
104
|
+
directoryPath,
|
|
105
|
+
filePath,
|
|
106
|
+
frontmatter,
|
|
107
|
+
html,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=content.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.js","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,MAAM,MAAM,aAAa,CAAA;AAEhC,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/B,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,WAAW,CAAA;AAG9C,MAAM,aAAa,GAAyC;IAC3D,OAAO,EAAE;QACR,IAAI,EAAE;YACL,KAAK,EAAE,SAAkE;YACzE,SAAS,EAAE,GAAG,EAAE;gBACf,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAA;YAChD,CAAC;SACD;KACD;CACD,CAAA;AAED,MAAM,UAAU,aAAa,CAAC,GAAW,EAAE,SAAkB;IAC5D,MAAM,OAAO,GAAa,EAAE,CAAA;IAC5B,MAAM,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAA;IAE5D,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAA;QAC3C,IAAI,KAAK,CAAC,WAAW,EAAE,IAAI,SAAS,EAAE,CAAC;YACtC,OAAO,CAAC,IAAI,CAAC,GAAG,aAAa,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAA;QACpD,CAAC;aAAM,IAAI,KAAK,CAAC,MAAM,EAAE,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YACzD,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QACvB,CAAC;IACF,CAAC;IAED,OAAO,OAAO,CAAA;AACf,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC/B,WAAmB,EACnB,QAAgB;IAEhB,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,EAAE,aAAa,CAAC,CAAA;QACjD,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAA;IACtD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QACtE,MAAM,IAAI,KAAK,CAAC,0BAA0B,QAAQ,KAAK,OAAO,EAAE,CAAC,CAAA;IAClE,CAAC;AACF,CAAC;AAED,MAAM,UAAU,WAAW,CAC1B,WAAoC,EACpC,QAAgB;IAEhB,IACC,OAAO,WAAW,CAAC,IAAI,KAAK,QAAQ;QACpC,WAAW,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EACjC,CAAC;QACF,OAAO,WAAW,CAAC,IAAI,CAAC,IAAI,EAAE,CAAA;IAC/B,CAAC;IACD,OAAO,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAA;AACtC,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,KAA0B;IAC7D,MAAM,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAA;IACtC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAC1B,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAA;QAChD,IAAI,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CACd,mBAAmB,IAAI,CAAC,WAAW,CAAC,IAAI,eAAe;gBACtD,OAAO,QAAQ,IAAI;gBACnB,OAAO,IAAI,CAAC,QAAQ,IAAI;gBACxB,uDAAuD,CACxD,CAAA;QACF,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAA;IAC/C,CAAC;AACF,CAAC;AAED,MAAM,cAAc,GAAG,aAAa,CAAA;AAEpC,MAAM,kBAAkB,GAAoB;IAC3C,QAAQ,EAAE;QACT,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE;YAClB,IAAI,IAAI,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBACvC,OAAO,YAAY,IAAI,+CAA+C,IAAI,MAAM,CAAA;YACjF,CAAC;YACD,OAAO,KAAK,CAAA;QACb,CAAC;KACD;CACD,CAAA;AAED,MAAM,UAAU,cAAc,CAAC,UAA6B;IAC3D,MAAM,MAAM,GAAG,IAAI,MAAM,EAAE,CAAA;IAC3B,MAAM,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAA;IAC9B,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3B,MAAM,CAAC,GAAG,CAAC,GAAG,UAAU,CAAC,CAAA;IAC1B,CAAC;IACD,OAAO,MAAM,CAAA;AACd,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC/B,QAAgB,EAChB,OAAe,EACf,QAAgB,EAChB,QAA2E;IAE3E,MAAM,WAAW,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;IACtD,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,gBAAgB,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAA;IAEjE,IAAI,QAAQ,EAAE,CAAC;QACd,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;IACzB,CAAC;IAED,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;IACxC,MAAM,aAAa,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAA;IAEpE,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IACpC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CACd,oDAAoD,QAAQ,EAAE,CAC9D,CAAA;IACF,CAAC;IAED,MAAM,WAAW,GAAuB;QACvC,GAAG,IAAI;QACP,IAAI,EACH,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ;YAC5B,CAAC,CAAC,IAAI,CAAC,IAAI;YACX,CAAC,CAAC,IAAI,CAAC,IAAI,YAAY,IAAI;gBAC1B,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE;gBACzB,CAAC,CAAC,EAAE;QACP,KAAK,EAAE,IAAI,CAAC,KAAK,KAAK,IAAI;QAC1B,IAAI;QACJ,KAAK,EAAE,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;KACvD,CAAA;IAED,OAAO;QACN,MAAM,EAAE,EAAE;QACV,aAAa;QACb,QAAQ;QACR,WAAW;QACX,IAAI;KACJ,CAAA;AACF,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AACrC,YAAY,EACX,kBAAkB,EAClB,WAAW,EACX,oBAAoB,EACpB,iBAAiB,GACjB,MAAM,YAAY,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA"}
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAiC,MAAM,MAAM,CAAA;AAYjE,OAAO,KAAK,EAAE,oBAAoB,EAAqB,MAAM,YAAY,CAAA;AAMzE,wBAAgB,OAAO,CAAC,OAAO,CAAC,EAAE,oBAAoB,GAAG,MAAM,CAmO9D"}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { extractAssetReferences, generateSlugModuleCode, replaceAssetsWithPlaceholders, } from "./assets.js";
|
|
4
|
+
import { checkDuplicateSlugs, createRenderer, parseContentFile, scanDirectory, } from "./content.js";
|
|
5
|
+
const CONTENT_PREFIX = "inkwell:";
|
|
6
|
+
const RESOLVED_PREFIX = "\0inkwell:";
|
|
7
|
+
const SLUG_SEPARATOR = "/";
|
|
8
|
+
export function inkwell(options) {
|
|
9
|
+
const opts = options ?? {};
|
|
10
|
+
let config;
|
|
11
|
+
let server;
|
|
12
|
+
let isProduction = false;
|
|
13
|
+
const renderer = createRenderer(opts.markedExtensions ?? []);
|
|
14
|
+
// Map from absolute directory path to its parsed content items
|
|
15
|
+
const collections = new Map();
|
|
16
|
+
// Track which directories are in use for HMR
|
|
17
|
+
const watchedDirs = new Set();
|
|
18
|
+
function buildCollection(absoluteDir) {
|
|
19
|
+
if (!fs.existsSync(absoluteDir)) {
|
|
20
|
+
throw new Error(`Content directory does not exist: ${absoluteDir}`);
|
|
21
|
+
}
|
|
22
|
+
const recursive = opts.recursive !== false;
|
|
23
|
+
const allFiles = scanDirectory(absoluteDir, recursive);
|
|
24
|
+
const items = [];
|
|
25
|
+
for (const filePath of allFiles) {
|
|
26
|
+
const item = parseContentFile(filePath, absoluteDir, renderer, opts.validate);
|
|
27
|
+
const assets = extractAssetReferences(item.html, filePath);
|
|
28
|
+
item.assets = assets;
|
|
29
|
+
item.html = replaceAssetsWithPlaceholders(item.html, assets);
|
|
30
|
+
items.push(item);
|
|
31
|
+
}
|
|
32
|
+
checkDuplicateSlugs(items);
|
|
33
|
+
return items;
|
|
34
|
+
}
|
|
35
|
+
function getVisibleItems(items) {
|
|
36
|
+
if (isProduction && !opts.includeDrafts) {
|
|
37
|
+
return items.filter((item) => !item.frontmatter.draft);
|
|
38
|
+
}
|
|
39
|
+
return items;
|
|
40
|
+
}
|
|
41
|
+
function generateCollectionModule(absoluteDir) {
|
|
42
|
+
const allItems = collections.get(absoluteDir);
|
|
43
|
+
if (!allItems) {
|
|
44
|
+
throw new Error(`No content collection for directory: ${absoluteDir}`);
|
|
45
|
+
}
|
|
46
|
+
const items = getVisibleItems(allItems);
|
|
47
|
+
const slugPrefix = CONTENT_PREFIX + absoluteDir + SLUG_SEPARATOR;
|
|
48
|
+
const entries = items.map((item) => {
|
|
49
|
+
const meta = { ...item.frontmatter };
|
|
50
|
+
delete meta.title;
|
|
51
|
+
delete meta.slug;
|
|
52
|
+
delete meta.date;
|
|
53
|
+
delete meta.draft;
|
|
54
|
+
return [
|
|
55
|
+
" {",
|
|
56
|
+
` title: ${JSON.stringify(item.frontmatter.title)},`,
|
|
57
|
+
` slug: ${JSON.stringify(item.frontmatter.slug)},`,
|
|
58
|
+
` date: ${JSON.stringify(item.frontmatter.date)},`,
|
|
59
|
+
` draft: ${JSON.stringify(item.frontmatter.draft)},`,
|
|
60
|
+
` directory: ${JSON.stringify(item.directoryPath)},`,
|
|
61
|
+
` meta: ${JSON.stringify(meta)},`,
|
|
62
|
+
` getHtml: () => import(${JSON.stringify(slugPrefix + item.frontmatter.slug)}).then(m => m.default),`,
|
|
63
|
+
" }",
|
|
64
|
+
].join("\n");
|
|
65
|
+
});
|
|
66
|
+
return `export default [\n${entries.join(",\n")}\n];\n`;
|
|
67
|
+
}
|
|
68
|
+
function findItemBySlug(absoluteDir, slug) {
|
|
69
|
+
const items = collections.get(absoluteDir);
|
|
70
|
+
return items?.find((i) => i.frontmatter.slug === slug);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
configResolved(resolvedConfig) {
|
|
74
|
+
config = resolvedConfig;
|
|
75
|
+
isProduction = resolvedConfig.command === "build";
|
|
76
|
+
},
|
|
77
|
+
configureServer(devServer) {
|
|
78
|
+
server = devServer;
|
|
79
|
+
},
|
|
80
|
+
enforce: "pre",
|
|
81
|
+
hotUpdate(ctx) {
|
|
82
|
+
const { file } = ctx;
|
|
83
|
+
if (!file.endsWith(".md"))
|
|
84
|
+
return;
|
|
85
|
+
if (!server)
|
|
86
|
+
return;
|
|
87
|
+
// Find which watched directory this file belongs to
|
|
88
|
+
let matchedDir;
|
|
89
|
+
for (const dir of watchedDirs) {
|
|
90
|
+
if (file.startsWith(dir + path.sep) || file.startsWith(`${dir}/`)) {
|
|
91
|
+
matchedDir = dir;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (!matchedDir)
|
|
96
|
+
return;
|
|
97
|
+
try {
|
|
98
|
+
const items = buildCollection(matchedDir);
|
|
99
|
+
collections.set(matchedDir, items);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
103
|
+
server.config.logger.error(message);
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
// Invalidate the collection module
|
|
107
|
+
const collectionId = RESOLVED_PREFIX + matchedDir;
|
|
108
|
+
const mod = this.environment.moduleGraph.getModuleById(collectionId);
|
|
109
|
+
if (mod) {
|
|
110
|
+
this.environment.moduleGraph.invalidateModule(mod);
|
|
111
|
+
}
|
|
112
|
+
// Invalidate the changed file's slug module
|
|
113
|
+
const items = collections.get(matchedDir);
|
|
114
|
+
const changedItem = items?.find((item) => item.filePath === file);
|
|
115
|
+
if (changedItem) {
|
|
116
|
+
const slugId = RESOLVED_PREFIX +
|
|
117
|
+
matchedDir +
|
|
118
|
+
SLUG_SEPARATOR +
|
|
119
|
+
changedItem.frontmatter.slug;
|
|
120
|
+
const slugModule = this.environment.moduleGraph.getModuleById(slugId);
|
|
121
|
+
if (slugModule) {
|
|
122
|
+
this.environment.moduleGraph.invalidateModule(slugModule);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
server.hot.send({ type: "full-reload" });
|
|
126
|
+
return [];
|
|
127
|
+
},
|
|
128
|
+
load(id) {
|
|
129
|
+
if (!id.startsWith(RESOLVED_PREFIX))
|
|
130
|
+
return null;
|
|
131
|
+
const rest = id.slice(RESOLVED_PREFIX.length);
|
|
132
|
+
// Check if this is a slug module (contains a slug after the directory path)
|
|
133
|
+
// Slug modules: \0content:/abs/path/to/dir/my-slug
|
|
134
|
+
// Collection modules: \0content:/abs/path/to/dir
|
|
135
|
+
for (const [absoluteDir, items] of collections) {
|
|
136
|
+
const dirPrefix = absoluteDir + SLUG_SEPARATOR;
|
|
137
|
+
if (rest.startsWith(dirPrefix) && rest.length > dirPrefix.length) {
|
|
138
|
+
const slug = rest.slice(dirPrefix.length);
|
|
139
|
+
const item = items.find((i) => i.frontmatter.slug === slug);
|
|
140
|
+
if (!item) {
|
|
141
|
+
throw new Error(`Content item with slug "${slug}" not found`);
|
|
142
|
+
}
|
|
143
|
+
return generateSlugModuleCode(item.html, item.assets);
|
|
144
|
+
}
|
|
145
|
+
if (rest === absoluteDir) {
|
|
146
|
+
return generateCollectionModule(absoluteDir);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// If we get here, the collection hasn't been built yet
|
|
150
|
+
// This happens on first load — build it now
|
|
151
|
+
if (rest.includes(SLUG_SEPARATOR)) {
|
|
152
|
+
// Try to find the directory portion
|
|
153
|
+
// Walk backward from the end to find a valid directory
|
|
154
|
+
const lastSlash = rest.lastIndexOf(SLUG_SEPARATOR);
|
|
155
|
+
const possibleDir = rest.slice(0, lastSlash);
|
|
156
|
+
const slug = rest.slice(lastSlash + 1);
|
|
157
|
+
if (collections.has(possibleDir)) {
|
|
158
|
+
const item = findItemBySlug(possibleDir, slug);
|
|
159
|
+
if (!item) {
|
|
160
|
+
throw new Error(`Content item with slug "${slug}" not found`);
|
|
161
|
+
}
|
|
162
|
+
return generateSlugModuleCode(item.html, item.assets);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
},
|
|
167
|
+
name: "inkwell",
|
|
168
|
+
resolveId(source, importer) {
|
|
169
|
+
if (!source.startsWith(CONTENT_PREFIX))
|
|
170
|
+
return null;
|
|
171
|
+
const rawPath = source.slice(CONTENT_PREFIX.length);
|
|
172
|
+
// If the path is already absolute (resolved from a slug module import), use it directly
|
|
173
|
+
if (path.isAbsolute(rawPath)) {
|
|
174
|
+
return RESOLVED_PREFIX + rawPath;
|
|
175
|
+
}
|
|
176
|
+
// Resolve relative to the importer's directory
|
|
177
|
+
const importerDir = importer ? path.dirname(importer) : config.root;
|
|
178
|
+
// Strip the \0 prefix from importer if it's a virtual module
|
|
179
|
+
const cleanImporterDir = importerDir.replace(/^\0/, "");
|
|
180
|
+
const absoluteDir = path.resolve(cleanImporterDir, rawPath);
|
|
181
|
+
// Build the collection if we haven't yet
|
|
182
|
+
if (!collections.has(absoluteDir)) {
|
|
183
|
+
const items = buildCollection(absoluteDir);
|
|
184
|
+
collections.set(absoluteDir, items);
|
|
185
|
+
// Watch directory for HMR
|
|
186
|
+
if (server) {
|
|
187
|
+
server.watcher.add(absoluteDir);
|
|
188
|
+
}
|
|
189
|
+
watchedDirs.add(absoluteDir);
|
|
190
|
+
}
|
|
191
|
+
return RESOLVED_PREFIX + absoluteDir;
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { MarkedExtension } from "marked";
|
|
2
|
+
export interface ContentPluginOptions {
|
|
3
|
+
/** Whether to recursively scan subdirectories (default: true) */
|
|
4
|
+
recursive?: boolean;
|
|
5
|
+
/** Custom frontmatter validation function. Throw to fail build. */
|
|
6
|
+
validate?: (frontmatter: Record<string, unknown>, filePath: string) => void;
|
|
7
|
+
/** Marked extensions for custom markdown rendering */
|
|
8
|
+
markedExtensions?: MarkedExtension[];
|
|
9
|
+
/** Whether to include draft posts in production (default: false) */
|
|
10
|
+
includeDrafts?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface ContentFrontmatter {
|
|
13
|
+
title: string;
|
|
14
|
+
slug: string;
|
|
15
|
+
date: string;
|
|
16
|
+
draft: boolean;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
export interface AssetReference {
|
|
20
|
+
/** Original relative path as written in the markdown */
|
|
21
|
+
originalPath: string;
|
|
22
|
+
/** Absolute filesystem path */
|
|
23
|
+
absolutePath: string;
|
|
24
|
+
/** Placeholder token used in the HTML template string */
|
|
25
|
+
placeholderToken: string;
|
|
26
|
+
}
|
|
27
|
+
export interface ParsedContentItem {
|
|
28
|
+
frontmatter: ContentFrontmatter;
|
|
29
|
+
filePath: string;
|
|
30
|
+
/** Directory path relative to the configured content directory */
|
|
31
|
+
directoryPath: string;
|
|
32
|
+
/** Rendered HTML with placeholder tokens for assets */
|
|
33
|
+
html: string;
|
|
34
|
+
/** Asset references discovered in this content */
|
|
35
|
+
assets: AssetReference[];
|
|
36
|
+
}
|
|
37
|
+
export interface ContentItem {
|
|
38
|
+
title: string;
|
|
39
|
+
slug: string;
|
|
40
|
+
date: string;
|
|
41
|
+
draft: boolean;
|
|
42
|
+
/** Relative directory path within the content source */
|
|
43
|
+
directory: string;
|
|
44
|
+
/** All frontmatter key-value pairs (excluding title, slug, date, draft) */
|
|
45
|
+
meta: Record<string, unknown>;
|
|
46
|
+
/** Lazy-load the rendered HTML for this content item */
|
|
47
|
+
getHtml: () => Promise<string>;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAA;AAE7C,MAAM,WAAW,oBAAoB;IACpC,iEAAiE;IACjE,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IAC3E,sDAAsD;IACtD,gBAAgB,CAAC,EAAE,eAAe,EAAE,CAAA;IACpC,oEAAoE;IACpE,aAAa,CAAC,EAAE,OAAO,CAAA;CACvB;AAED,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,OAAO,CAAA;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACtB;AAED,MAAM,WAAW,cAAc;IAC9B,wDAAwD;IACxD,YAAY,EAAE,MAAM,CAAA;IACpB,+BAA+B;IAC/B,YAAY,EAAE,MAAM,CAAA;IACpB,yDAAyD;IACzD,gBAAgB,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,iBAAiB;IACjC,WAAW,EAAE,kBAAkB,CAAA;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAChB,kEAAkE;IAClE,aAAa,EAAE,MAAM,CAAA;IACrB,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAA;IACZ,kDAAkD;IAClD,MAAM,EAAE,cAAc,EAAE,CAAA;CACxB;AAED,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,OAAO,CAAA;IACd,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAA;IACjB,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC7B,wDAAwD;IACxD,OAAO,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;CAC9B"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aaronellington/vite-plugin-inkwell",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "A Vite plugin that transforms directories of markdown files into typed, lazy-loaded content collections with frontmatter parsing, asset hashing, and HMR.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Aaron Ellington",
|
|
@@ -20,11 +20,17 @@
|
|
|
20
20
|
],
|
|
21
21
|
"type": "module",
|
|
22
22
|
"exports": {
|
|
23
|
-
".":
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"default": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./types": {
|
|
28
|
+
"types": "./global.d.ts"
|
|
29
|
+
}
|
|
24
30
|
},
|
|
25
31
|
"files": [
|
|
26
|
-
"
|
|
27
|
-
"
|
|
32
|
+
"dist",
|
|
33
|
+
"global.d.ts"
|
|
28
34
|
],
|
|
29
35
|
"engines": {
|
|
30
36
|
"node": ">=22"
|
|
@@ -47,9 +53,11 @@
|
|
|
47
53
|
"vite": "^7.0.0"
|
|
48
54
|
},
|
|
49
55
|
"scripts": {
|
|
56
|
+
"build": "tsc",
|
|
50
57
|
"lint": "tsc -b && biome check . && prettier --check .",
|
|
51
58
|
"fix": "biome check --write . && prettier --write .",
|
|
52
59
|
"test": "playwright test",
|
|
53
|
-
"test:install": "playwright install chromium"
|
|
60
|
+
"test:install": "playwright install chromium",
|
|
61
|
+
"prepublishOnly": "npm run build"
|
|
54
62
|
}
|
|
55
63
|
}
|
package/src/assets.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs"
|
|
2
|
-
import path from "node:path"
|
|
3
|
-
import type { AssetReference } from "./types.ts"
|
|
4
|
-
|
|
5
|
-
const ASSET_REGEX =
|
|
6
|
-
/(?:src|href|poster)=["'](?!(?:https?:|data:|#|\/\/|\/))([^"']+)["']/g
|
|
7
|
-
|
|
8
|
-
export function extractAssetReferences(
|
|
9
|
-
html: string,
|
|
10
|
-
mdFilePath: string,
|
|
11
|
-
): AssetReference[] {
|
|
12
|
-
const mdDir = path.dirname(mdFilePath)
|
|
13
|
-
const assets: AssetReference[] = []
|
|
14
|
-
const seen = new Set<string>()
|
|
15
|
-
|
|
16
|
-
ASSET_REGEX.lastIndex = 0
|
|
17
|
-
for (
|
|
18
|
-
let match = ASSET_REGEX.exec(html);
|
|
19
|
-
match !== null;
|
|
20
|
-
match = ASSET_REGEX.exec(html)
|
|
21
|
-
) {
|
|
22
|
-
const originalPath = match[1]
|
|
23
|
-
if (seen.has(originalPath)) continue
|
|
24
|
-
seen.add(originalPath)
|
|
25
|
-
|
|
26
|
-
const absolutePath = path.resolve(mdDir, originalPath)
|
|
27
|
-
|
|
28
|
-
if (!fs.existsSync(absolutePath)) {
|
|
29
|
-
throw new Error(
|
|
30
|
-
`Missing asset referenced in ${mdFilePath}: "${originalPath}"\n` +
|
|
31
|
-
`Resolved to: ${absolutePath}`,
|
|
32
|
-
)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const placeholderToken = `__CONTENT_ASSET_${assets.length}__`
|
|
36
|
-
assets.push({ absolutePath, originalPath, placeholderToken })
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return assets
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function replaceAssetsWithPlaceholders(
|
|
43
|
-
html: string,
|
|
44
|
-
assets: AssetReference[],
|
|
45
|
-
): string {
|
|
46
|
-
let result = html
|
|
47
|
-
for (const asset of assets) {
|
|
48
|
-
result = result.split(asset.originalPath).join(asset.placeholderToken)
|
|
49
|
-
}
|
|
50
|
-
return result
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function generateSlugModuleCode(
|
|
54
|
-
html: string,
|
|
55
|
-
assets: AssetReference[],
|
|
56
|
-
): string {
|
|
57
|
-
if (assets.length === 0) {
|
|
58
|
-
return `export default ${JSON.stringify(html)};`
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const lines: string[] = []
|
|
62
|
-
|
|
63
|
-
for (let i = 0; i < assets.length; i++) {
|
|
64
|
-
const asset = assets[i]
|
|
65
|
-
lines.push(
|
|
66
|
-
`import __asset_${i}__ from ${JSON.stringify(asset.absolutePath)};`,
|
|
67
|
-
)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
lines.push("")
|
|
71
|
-
lines.push(`let html = ${JSON.stringify(html)};`)
|
|
72
|
-
|
|
73
|
-
for (let i = 0; i < assets.length; i++) {
|
|
74
|
-
const asset = assets[i]
|
|
75
|
-
lines.push(
|
|
76
|
-
`html = html.replaceAll(${JSON.stringify(asset.placeholderToken)}, __asset_${i}__);`,
|
|
77
|
-
)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
lines.push("")
|
|
81
|
-
lines.push("export default html;")
|
|
82
|
-
|
|
83
|
-
return lines.join("\n")
|
|
84
|
-
}
|
package/src/content.ts
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs"
|
|
2
|
-
import path from "node:path"
|
|
3
|
-
import matter from "gray-matter"
|
|
4
|
-
import type { MarkedExtension } from "marked"
|
|
5
|
-
import { Marked } from "marked"
|
|
6
|
-
import { parse as parseToml } from "smol-toml"
|
|
7
|
-
import type { ContentFrontmatter, ParsedContentItem } from "./types.ts"
|
|
8
|
-
|
|
9
|
-
const matterOptions: matter.GrayMatterOption<string, any> = {
|
|
10
|
-
engines: {
|
|
11
|
-
toml: {
|
|
12
|
-
parse: parseToml as unknown as (input: string) => Record<string, unknown>,
|
|
13
|
-
stringify: () => {
|
|
14
|
-
throw new Error("TOML stringify not supported")
|
|
15
|
-
},
|
|
16
|
-
},
|
|
17
|
-
},
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function scanDirectory(dir: string, recursive: boolean): string[] {
|
|
21
|
-
const results: string[] = []
|
|
22
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
23
|
-
|
|
24
|
-
for (const entry of entries) {
|
|
25
|
-
const fullPath = path.join(dir, entry.name)
|
|
26
|
-
if (entry.isDirectory() && recursive) {
|
|
27
|
-
results.push(...scanDirectory(fullPath, recursive))
|
|
28
|
-
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
29
|
-
results.push(fullPath)
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return results
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function parseFrontmatter(
|
|
37
|
-
fileContent: string,
|
|
38
|
-
filePath: string,
|
|
39
|
-
): { data: Record<string, unknown>; content: string } {
|
|
40
|
-
try {
|
|
41
|
-
const result = matter(fileContent, matterOptions)
|
|
42
|
-
return { content: result.content, data: result.data }
|
|
43
|
-
} catch (error) {
|
|
44
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
45
|
-
throw new Error(`Invalid frontmatter in ${filePath}: ${message}`)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function computeSlug(
|
|
50
|
-
frontmatter: Record<string, unknown>,
|
|
51
|
-
filePath: string,
|
|
52
|
-
): string {
|
|
53
|
-
if (
|
|
54
|
-
typeof frontmatter.slug === "string" &&
|
|
55
|
-
frontmatter.slug.trim().length > 0
|
|
56
|
-
) {
|
|
57
|
-
return frontmatter.slug.trim()
|
|
58
|
-
}
|
|
59
|
-
return path.basename(filePath, ".md")
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function checkDuplicateSlugs(items: ParsedContentItem[]): void {
|
|
63
|
-
const seen = new Map<string, string>()
|
|
64
|
-
for (const item of items) {
|
|
65
|
-
const existing = seen.get(item.frontmatter.slug)
|
|
66
|
-
if (existing) {
|
|
67
|
-
throw new Error(
|
|
68
|
-
`Duplicate slug "${item.frontmatter.slug}" found in:\n` +
|
|
69
|
-
` - ${existing}\n` +
|
|
70
|
-
` - ${item.filePath}\n` +
|
|
71
|
-
`Provide an explicit "slug" in frontmatter to resolve.`,
|
|
72
|
-
)
|
|
73
|
-
}
|
|
74
|
-
seen.set(item.frontmatter.slug, item.filePath)
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const FILE_EXT_REGEX = /\.\w{1,10}$/
|
|
79
|
-
|
|
80
|
-
const assetLinkExtension: MarkedExtension = {
|
|
81
|
-
renderer: {
|
|
82
|
-
link({ href, text }) {
|
|
83
|
-
if (href && FILE_EXT_REGEX.test(href)) {
|
|
84
|
-
return `<a href="${href}" target="_blank" rel="noopener noreferrer">${text}</a>`
|
|
85
|
-
}
|
|
86
|
-
return false
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function createRenderer(extensions: MarkedExtension[]): Marked {
|
|
92
|
-
const marked = new Marked()
|
|
93
|
-
marked.use(assetLinkExtension)
|
|
94
|
-
if (extensions.length > 0) {
|
|
95
|
-
marked.use(...extensions)
|
|
96
|
-
}
|
|
97
|
-
return marked
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function parseContentFile(
|
|
101
|
-
filePath: string,
|
|
102
|
-
baseDir: string,
|
|
103
|
-
renderer: Marked,
|
|
104
|
-
validate?: (frontmatter: Record<string, unknown>, filePath: string) => void,
|
|
105
|
-
): ParsedContentItem {
|
|
106
|
-
const fileContent = fs.readFileSync(filePath, "utf-8")
|
|
107
|
-
const { data, content } = parseFrontmatter(fileContent, filePath)
|
|
108
|
-
|
|
109
|
-
if (validate) {
|
|
110
|
-
validate(data, filePath)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const slug = computeSlug(data, filePath)
|
|
114
|
-
const directoryPath = path.relative(baseDir, path.dirname(filePath))
|
|
115
|
-
|
|
116
|
-
const html = renderer.parse(content)
|
|
117
|
-
if (typeof html !== "string") {
|
|
118
|
-
throw new Error(
|
|
119
|
-
`Async marked extensions are not supported. File: ${filePath}`,
|
|
120
|
-
)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const frontmatter: ContentFrontmatter = {
|
|
124
|
-
...data,
|
|
125
|
-
date:
|
|
126
|
-
typeof data.date === "string"
|
|
127
|
-
? data.date
|
|
128
|
-
: data.date instanceof Date
|
|
129
|
-
? data.date.toISOString()
|
|
130
|
-
: "",
|
|
131
|
-
draft: data.draft === true,
|
|
132
|
-
slug,
|
|
133
|
-
title: typeof data.title === "string" ? data.title : "",
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return {
|
|
137
|
-
assets: [],
|
|
138
|
-
directoryPath,
|
|
139
|
-
filePath,
|
|
140
|
-
frontmatter,
|
|
141
|
-
html,
|
|
142
|
-
}
|
|
143
|
-
}
|
package/src/index.ts
DELETED
package/src/plugin.ts
DELETED
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs"
|
|
2
|
-
import path from "node:path"
|
|
3
|
-
import type { Plugin, ResolvedConfig, ViteDevServer } from "vite"
|
|
4
|
-
import {
|
|
5
|
-
extractAssetReferences,
|
|
6
|
-
generateSlugModuleCode,
|
|
7
|
-
replaceAssetsWithPlaceholders,
|
|
8
|
-
} from "./assets.ts"
|
|
9
|
-
import {
|
|
10
|
-
checkDuplicateSlugs,
|
|
11
|
-
createRenderer,
|
|
12
|
-
parseContentFile,
|
|
13
|
-
scanDirectory,
|
|
14
|
-
} from "./content.ts"
|
|
15
|
-
import type { ContentPluginOptions, ParsedContentItem } from "./types.ts"
|
|
16
|
-
|
|
17
|
-
const CONTENT_PREFIX = "inkwell:"
|
|
18
|
-
const RESOLVED_PREFIX = "\0inkwell:"
|
|
19
|
-
const SLUG_SEPARATOR = "/"
|
|
20
|
-
|
|
21
|
-
export function inkwell(options?: ContentPluginOptions): Plugin {
|
|
22
|
-
const opts = options ?? {}
|
|
23
|
-
let config: ResolvedConfig
|
|
24
|
-
let server: ViteDevServer | undefined
|
|
25
|
-
let isProduction = false
|
|
26
|
-
|
|
27
|
-
const renderer = createRenderer(opts.markedExtensions ?? [])
|
|
28
|
-
|
|
29
|
-
// Map from absolute directory path to its parsed content items
|
|
30
|
-
const collections = new Map<string, ParsedContentItem[]>()
|
|
31
|
-
// Track which directories are in use for HMR
|
|
32
|
-
const watchedDirs = new Set<string>()
|
|
33
|
-
|
|
34
|
-
function buildCollection(absoluteDir: string): ParsedContentItem[] {
|
|
35
|
-
if (!fs.existsSync(absoluteDir)) {
|
|
36
|
-
throw new Error(`Content directory does not exist: ${absoluteDir}`)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const recursive = opts.recursive !== false
|
|
40
|
-
const allFiles = scanDirectory(absoluteDir, recursive)
|
|
41
|
-
const items: ParsedContentItem[] = []
|
|
42
|
-
|
|
43
|
-
for (const filePath of allFiles) {
|
|
44
|
-
const item = parseContentFile(
|
|
45
|
-
filePath,
|
|
46
|
-
absoluteDir,
|
|
47
|
-
renderer,
|
|
48
|
-
opts.validate,
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
const assets = extractAssetReferences(item.html, filePath)
|
|
52
|
-
item.assets = assets
|
|
53
|
-
item.html = replaceAssetsWithPlaceholders(item.html, assets)
|
|
54
|
-
|
|
55
|
-
items.push(item)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
checkDuplicateSlugs(items)
|
|
59
|
-
return items
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function getVisibleItems(items: ParsedContentItem[]): ParsedContentItem[] {
|
|
63
|
-
if (isProduction && !opts.includeDrafts) {
|
|
64
|
-
return items.filter((item) => !item.frontmatter.draft)
|
|
65
|
-
}
|
|
66
|
-
return items
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function generateCollectionModule(absoluteDir: string): string {
|
|
70
|
-
const allItems = collections.get(absoluteDir)
|
|
71
|
-
if (!allItems) {
|
|
72
|
-
throw new Error(`No content collection for directory: ${absoluteDir}`)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const items = getVisibleItems(allItems)
|
|
76
|
-
const slugPrefix = CONTENT_PREFIX + absoluteDir + SLUG_SEPARATOR
|
|
77
|
-
|
|
78
|
-
const entries = items.map((item) => {
|
|
79
|
-
const meta: Record<string, unknown> = { ...item.frontmatter }
|
|
80
|
-
delete meta.title
|
|
81
|
-
delete meta.slug
|
|
82
|
-
delete meta.date
|
|
83
|
-
delete meta.draft
|
|
84
|
-
|
|
85
|
-
return [
|
|
86
|
-
" {",
|
|
87
|
-
` title: ${JSON.stringify(item.frontmatter.title)},`,
|
|
88
|
-
` slug: ${JSON.stringify(item.frontmatter.slug)},`,
|
|
89
|
-
` date: ${JSON.stringify(item.frontmatter.date)},`,
|
|
90
|
-
` draft: ${JSON.stringify(item.frontmatter.draft)},`,
|
|
91
|
-
` directory: ${JSON.stringify(item.directoryPath)},`,
|
|
92
|
-
` meta: ${JSON.stringify(meta)},`,
|
|
93
|
-
` getHtml: () => import(${JSON.stringify(slugPrefix + item.frontmatter.slug)}).then(m => m.default),`,
|
|
94
|
-
" }",
|
|
95
|
-
].join("\n")
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
return `export default [\n${entries.join(",\n")}\n];\n`
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function findItemBySlug(
|
|
102
|
-
absoluteDir: string,
|
|
103
|
-
slug: string,
|
|
104
|
-
): ParsedContentItem | undefined {
|
|
105
|
-
const items = collections.get(absoluteDir)
|
|
106
|
-
return items?.find((i) => i.frontmatter.slug === slug)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
configResolved(resolvedConfig) {
|
|
111
|
-
config = resolvedConfig
|
|
112
|
-
isProduction = resolvedConfig.command === "build"
|
|
113
|
-
},
|
|
114
|
-
|
|
115
|
-
configureServer(devServer) {
|
|
116
|
-
server = devServer
|
|
117
|
-
},
|
|
118
|
-
enforce: "pre",
|
|
119
|
-
|
|
120
|
-
hotUpdate(ctx) {
|
|
121
|
-
const { file } = ctx
|
|
122
|
-
if (!file.endsWith(".md")) return
|
|
123
|
-
if (!server) return
|
|
124
|
-
|
|
125
|
-
// Find which watched directory this file belongs to
|
|
126
|
-
let matchedDir: string | undefined
|
|
127
|
-
for (const dir of watchedDirs) {
|
|
128
|
-
if (file.startsWith(dir + path.sep) || file.startsWith(`${dir}/`)) {
|
|
129
|
-
matchedDir = dir
|
|
130
|
-
break
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (!matchedDir) return
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
const items = buildCollection(matchedDir)
|
|
138
|
-
collections.set(matchedDir, items)
|
|
139
|
-
} catch (error) {
|
|
140
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
141
|
-
server.config.logger.error(message)
|
|
142
|
-
return []
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Invalidate the collection module
|
|
146
|
-
const collectionId = RESOLVED_PREFIX + matchedDir
|
|
147
|
-
const mod = this.environment.moduleGraph.getModuleById(collectionId)
|
|
148
|
-
if (mod) {
|
|
149
|
-
this.environment.moduleGraph.invalidateModule(mod)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Invalidate the changed file's slug module
|
|
153
|
-
const items = collections.get(matchedDir)
|
|
154
|
-
const changedItem = items?.find((item) => item.filePath === file)
|
|
155
|
-
if (changedItem) {
|
|
156
|
-
const slugId =
|
|
157
|
-
RESOLVED_PREFIX +
|
|
158
|
-
matchedDir +
|
|
159
|
-
SLUG_SEPARATOR +
|
|
160
|
-
changedItem.frontmatter.slug
|
|
161
|
-
const slugModule = this.environment.moduleGraph.getModuleById(slugId)
|
|
162
|
-
if (slugModule) {
|
|
163
|
-
this.environment.moduleGraph.invalidateModule(slugModule)
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
server.hot.send({ type: "full-reload" })
|
|
168
|
-
return []
|
|
169
|
-
},
|
|
170
|
-
|
|
171
|
-
load(id) {
|
|
172
|
-
if (!id.startsWith(RESOLVED_PREFIX)) return null
|
|
173
|
-
|
|
174
|
-
const rest = id.slice(RESOLVED_PREFIX.length)
|
|
175
|
-
|
|
176
|
-
// Check if this is a slug module (contains a slug after the directory path)
|
|
177
|
-
// Slug modules: \0content:/abs/path/to/dir/my-slug
|
|
178
|
-
// Collection modules: \0content:/abs/path/to/dir
|
|
179
|
-
for (const [absoluteDir, items] of collections) {
|
|
180
|
-
const dirPrefix = absoluteDir + SLUG_SEPARATOR
|
|
181
|
-
if (rest.startsWith(dirPrefix) && rest.length > dirPrefix.length) {
|
|
182
|
-
const slug = rest.slice(dirPrefix.length)
|
|
183
|
-
const item = items.find((i) => i.frontmatter.slug === slug)
|
|
184
|
-
if (!item) {
|
|
185
|
-
throw new Error(`Content item with slug "${slug}" not found`)
|
|
186
|
-
}
|
|
187
|
-
return generateSlugModuleCode(item.html, item.assets)
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (rest === absoluteDir) {
|
|
191
|
-
return generateCollectionModule(absoluteDir)
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// If we get here, the collection hasn't been built yet
|
|
196
|
-
// This happens on first load — build it now
|
|
197
|
-
if (rest.includes(SLUG_SEPARATOR)) {
|
|
198
|
-
// Try to find the directory portion
|
|
199
|
-
// Walk backward from the end to find a valid directory
|
|
200
|
-
const lastSlash = rest.lastIndexOf(SLUG_SEPARATOR)
|
|
201
|
-
const possibleDir = rest.slice(0, lastSlash)
|
|
202
|
-
const slug = rest.slice(lastSlash + 1)
|
|
203
|
-
|
|
204
|
-
if (collections.has(possibleDir)) {
|
|
205
|
-
const item = findItemBySlug(possibleDir, slug)
|
|
206
|
-
if (!item) {
|
|
207
|
-
throw new Error(`Content item with slug "${slug}" not found`)
|
|
208
|
-
}
|
|
209
|
-
return generateSlugModuleCode(item.html, item.assets)
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return null
|
|
214
|
-
},
|
|
215
|
-
name: "inkwell",
|
|
216
|
-
|
|
217
|
-
resolveId(source, importer) {
|
|
218
|
-
if (!source.startsWith(CONTENT_PREFIX)) return null
|
|
219
|
-
|
|
220
|
-
const rawPath = source.slice(CONTENT_PREFIX.length)
|
|
221
|
-
|
|
222
|
-
// If the path is already absolute (resolved from a slug module import), use it directly
|
|
223
|
-
if (path.isAbsolute(rawPath)) {
|
|
224
|
-
return RESOLVED_PREFIX + rawPath
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Resolve relative to the importer's directory
|
|
228
|
-
const importerDir = importer ? path.dirname(importer) : config.root
|
|
229
|
-
// Strip the \0 prefix from importer if it's a virtual module
|
|
230
|
-
const cleanImporterDir = importerDir.replace(/^\0/, "")
|
|
231
|
-
const absoluteDir = path.resolve(cleanImporterDir, rawPath)
|
|
232
|
-
|
|
233
|
-
// Build the collection if we haven't yet
|
|
234
|
-
if (!collections.has(absoluteDir)) {
|
|
235
|
-
const items = buildCollection(absoluteDir)
|
|
236
|
-
collections.set(absoluteDir, items)
|
|
237
|
-
|
|
238
|
-
// Watch directory for HMR
|
|
239
|
-
if (server) {
|
|
240
|
-
server.watcher.add(absoluteDir)
|
|
241
|
-
}
|
|
242
|
-
watchedDirs.add(absoluteDir)
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return RESOLVED_PREFIX + absoluteDir
|
|
246
|
-
},
|
|
247
|
-
}
|
|
248
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import type { MarkedExtension } from "marked"
|
|
2
|
-
|
|
3
|
-
export interface ContentPluginOptions {
|
|
4
|
-
/** Whether to recursively scan subdirectories (default: true) */
|
|
5
|
-
recursive?: boolean
|
|
6
|
-
/** Custom frontmatter validation function. Throw to fail build. */
|
|
7
|
-
validate?: (frontmatter: Record<string, unknown>, filePath: string) => void
|
|
8
|
-
/** Marked extensions for custom markdown rendering */
|
|
9
|
-
markedExtensions?: MarkedExtension[]
|
|
10
|
-
/** Whether to include draft posts in production (default: false) */
|
|
11
|
-
includeDrafts?: boolean
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface ContentFrontmatter {
|
|
15
|
-
title: string
|
|
16
|
-
slug: string
|
|
17
|
-
date: string
|
|
18
|
-
draft: boolean
|
|
19
|
-
[key: string]: unknown
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface AssetReference {
|
|
23
|
-
/** Original relative path as written in the markdown */
|
|
24
|
-
originalPath: string
|
|
25
|
-
/** Absolute filesystem path */
|
|
26
|
-
absolutePath: string
|
|
27
|
-
/** Placeholder token used in the HTML template string */
|
|
28
|
-
placeholderToken: string
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface ParsedContentItem {
|
|
32
|
-
frontmatter: ContentFrontmatter
|
|
33
|
-
filePath: string
|
|
34
|
-
/** Directory path relative to the configured content directory */
|
|
35
|
-
directoryPath: string
|
|
36
|
-
/** Rendered HTML with placeholder tokens for assets */
|
|
37
|
-
html: string
|
|
38
|
-
/** Asset references discovered in this content */
|
|
39
|
-
assets: AssetReference[]
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface ContentItem {
|
|
43
|
-
title: string
|
|
44
|
-
slug: string
|
|
45
|
-
date: string
|
|
46
|
-
draft: boolean
|
|
47
|
-
/** Relative directory path within the content source */
|
|
48
|
-
directory: string
|
|
49
|
-
/** All frontmatter key-value pairs (excluding title, slug, date, draft) */
|
|
50
|
-
meta: Record<string, unknown>
|
|
51
|
-
/** Lazy-load the rendered HTML for this content item */
|
|
52
|
-
getHtml: () => Promise<string>
|
|
53
|
-
}
|