@aaronellington/vite-plugin-inkwell 0.0.1 → 0.0.5

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/content.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ContentItem } from "./src/types.ts"
1
+ import type { ContentItem } from "./dist/types.js"
2
2
 
3
3
  declare module "inkwell:*" {
4
4
  const collection: ContentItem[]
@@ -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"}
@@ -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":"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"}
@@ -0,0 +1,3 @@
1
+ export { inkwell } from "./plugin.js";
2
+ export type { ContentFrontmatter, ContentItem, ContentPluginOptions, ParsedContentItem, } from "./types.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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,2 @@
1
+ export { inkwell } from "./plugin.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA"}
@@ -0,0 +1,4 @@
1
+ import type { Plugin } from "vite";
2
+ import type { ContentPluginOptions } from "./types.js";
3
+ export declare function inkwell(options?: ContentPluginOptions): Plugin;
4
+ //# sourceMappingURL=plugin.d.ts.map
@@ -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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"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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.1",
3
+ "version": "0.0.5",
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,10 +20,13 @@
20
20
  ],
21
21
  "type": "module",
22
22
  "exports": {
23
- ".": "./src/index.ts"
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "default": "./dist/index.js"
26
+ }
24
27
  },
25
28
  "files": [
26
- "src/*.ts",
29
+ "dist",
27
30
  "content.d.ts"
28
31
  ],
29
32
  "engines": {
@@ -47,9 +50,11 @@
47
50
  "vite": "^7.0.0"
48
51
  },
49
52
  "scripts": {
53
+ "build": "tsc",
50
54
  "lint": "tsc -b && biome check . && prettier --check .",
51
55
  "fix": "biome check --write . && prettier --write .",
52
56
  "test": "playwright test",
53
- "test:install": "playwright install chromium"
57
+ "test:install": "playwright install chromium",
58
+ "prepublishOnly": "npm run build"
54
59
  }
55
60
  }
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
@@ -1,7 +0,0 @@
1
- export { inkwell } from "./plugin.ts"
2
- export type {
3
- ContentFrontmatter,
4
- ContentItem,
5
- ContentPluginOptions,
6
- ParsedContentItem,
7
- } from "./types.ts"
package/src/plugin.ts DELETED
@@ -1,247 +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
- handleHotUpdate(ctx) {
121
- const { file, server: hmrServer } = ctx
122
- if (!file.endsWith(".md")) return
123
-
124
- // Find which watched directory this file belongs to
125
- let matchedDir: string | undefined
126
- for (const dir of watchedDirs) {
127
- if (file.startsWith(dir + path.sep) || file.startsWith(`${dir}/`)) {
128
- matchedDir = dir
129
- break
130
- }
131
- }
132
-
133
- if (!matchedDir) return
134
-
135
- try {
136
- const items = buildCollection(matchedDir)
137
- collections.set(matchedDir, items)
138
- } catch (error) {
139
- const message = error instanceof Error ? error.message : String(error)
140
- hmrServer.config.logger.error(message)
141
- return []
142
- }
143
-
144
- // Invalidate the collection module
145
- const collectionId = RESOLVED_PREFIX + matchedDir
146
- const collectionModule = hmrServer.moduleGraph.getModuleById(collectionId)
147
- if (collectionModule) {
148
- hmrServer.moduleGraph.invalidateModule(collectionModule)
149
- }
150
-
151
- // Invalidate the changed file's slug module
152
- const items = collections.get(matchedDir)
153
- const changedItem = items?.find((item) => item.filePath === file)
154
- if (changedItem) {
155
- const slugId =
156
- RESOLVED_PREFIX +
157
- matchedDir +
158
- SLUG_SEPARATOR +
159
- changedItem.frontmatter.slug
160
- const slugModule = hmrServer.moduleGraph.getModuleById(slugId)
161
- if (slugModule) {
162
- hmrServer.moduleGraph.invalidateModule(slugModule)
163
- }
164
- }
165
-
166
- hmrServer.hot.send({ type: "full-reload" })
167
- return []
168
- },
169
-
170
- load(id) {
171
- if (!id.startsWith(RESOLVED_PREFIX)) return null
172
-
173
- const rest = id.slice(RESOLVED_PREFIX.length)
174
-
175
- // Check if this is a slug module (contains a slug after the directory path)
176
- // Slug modules: \0content:/abs/path/to/dir/my-slug
177
- // Collection modules: \0content:/abs/path/to/dir
178
- for (const [absoluteDir, items] of collections) {
179
- const dirPrefix = absoluteDir + SLUG_SEPARATOR
180
- if (rest.startsWith(dirPrefix) && rest.length > dirPrefix.length) {
181
- const slug = rest.slice(dirPrefix.length)
182
- const item = items.find((i) => i.frontmatter.slug === slug)
183
- if (!item) {
184
- throw new Error(`Content item with slug "${slug}" not found`)
185
- }
186
- return generateSlugModuleCode(item.html, item.assets)
187
- }
188
-
189
- if (rest === absoluteDir) {
190
- return generateCollectionModule(absoluteDir)
191
- }
192
- }
193
-
194
- // If we get here, the collection hasn't been built yet
195
- // This happens on first load — build it now
196
- if (rest.includes(SLUG_SEPARATOR)) {
197
- // Try to find the directory portion
198
- // Walk backward from the end to find a valid directory
199
- const lastSlash = rest.lastIndexOf(SLUG_SEPARATOR)
200
- const possibleDir = rest.slice(0, lastSlash)
201
- const slug = rest.slice(lastSlash + 1)
202
-
203
- if (collections.has(possibleDir)) {
204
- const item = findItemBySlug(possibleDir, slug)
205
- if (!item) {
206
- throw new Error(`Content item with slug "${slug}" not found`)
207
- }
208
- return generateSlugModuleCode(item.html, item.assets)
209
- }
210
- }
211
-
212
- return null
213
- },
214
- name: "inkwell",
215
-
216
- resolveId(source, importer) {
217
- if (!source.startsWith(CONTENT_PREFIX)) return null
218
-
219
- const rawPath = source.slice(CONTENT_PREFIX.length)
220
-
221
- // If the path is already absolute (resolved from a slug module import), use it directly
222
- if (path.isAbsolute(rawPath)) {
223
- return RESOLVED_PREFIX + rawPath
224
- }
225
-
226
- // Resolve relative to the importer's directory
227
- const importerDir = importer ? path.dirname(importer) : config.root
228
- // Strip the \0 prefix from importer if it's a virtual module
229
- const cleanImporterDir = importerDir.replace(/^\0/, "")
230
- const absoluteDir = path.resolve(cleanImporterDir, rawPath)
231
-
232
- // Build the collection if we haven't yet
233
- if (!collections.has(absoluteDir)) {
234
- const items = buildCollection(absoluteDir)
235
- collections.set(absoluteDir, items)
236
-
237
- // Watch directory for HMR
238
- if (server) {
239
- server.watcher.add(absoluteDir)
240
- }
241
- watchedDirs.add(absoluteDir)
242
- }
243
-
244
- return RESOLVED_PREFIX + absoluteDir
245
- },
246
- }
247
- }
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
- }