@mzebley/mark-down-cli 1.2.1 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  # mark↓ CLI
2
- *(published as `@mzebley/mark-down-cli`)*
2
+
3
+ _(published as `@mzebley/mark-down-cli`)_
3
4
 
4
5
  `mark-down` is the command-line companion to the mark↓ runtime. It scans Markdown snippets, parses YAML front matter, and emits a sorted `snippets-index.json` manifest consumed by [the core client](../core/README.md). For a full project overview, see the [monorepo README](../../README.md).
5
6
 
@@ -59,7 +60,7 @@ The CLI walks the directory tree, gathers front matter, and writes `snippets-ind
59
60
  - Resolves snippet metadata from `snippets-index.json` (auto-detected next to the HTML file or provided via `--manifest`).
60
61
  - Loads Markdown from disk, strips front matter, and renders HTML with the same `marked` pipeline as the runtime.
61
62
  - Injects the rendered HTML as the element `innerHTML` and writes the result to `dist/<file>.html` by default.
62
- - Use `--outDir` to change the output directory or `--inPlace` to overwrite the source file.
63
+ - Use `--outDir` to change the output directory, `--inPlace` to overwrite the source file, or `--sanitize` to apply HTML sanitization.
63
64
  - Unknown slugs are left untouched and logged as warnings. Table-of-contents generation remains a runtime concern.
64
65
 
65
66
  ## Configuration options
@@ -73,6 +74,7 @@ The CLI stays intentionally small so it can be composed inside any toolchain. Cu
73
74
  - `--manifest <path>` – path to `snippets-index.json`. Defaults to the file next to `<inputHtml>`.
74
75
  - `--outDir <dir>` – output directory for compiled HTML. Defaults to `dist`.
75
76
  - `--inPlace` – overwrite the input HTML file instead of writing to `dist/`.
77
+ - `--sanitize [policy]` – sanitize rendered HTML (`default`, `strict`, or `permissive`). Omitting the policy defaults to `default`.
76
78
 
77
79
  Add flags directly after the command (`mark-down build content/snippets -o public/snippets-index.json`). Package scripts can capture these options as well.
78
80
 
package/dist/index.cjs CHANGED
@@ -62,13 +62,20 @@ async function buildManifest(sourceDir) {
62
62
  for (const absolutePath of files) {
63
63
  const relativePath = import_node_path.default.relative(cwd, absolutePath);
64
64
  const normalizedPath = toPosix(relativePath);
65
- const content = await import_promises.default.readFile(absolutePath, "utf8");
66
- const parsed = (0, import_gray_matter.default)(content, MATTER_OPTIONS);
67
- const snippet = createSnippet(normalizedPath, parsed.data ?? {});
68
- if (snippet.draft) {
69
- continue;
65
+ try {
66
+ const content = await import_promises.default.readFile(absolutePath, "utf8");
67
+ const parsed = (0, import_gray_matter.default)(content, MATTER_OPTIONS);
68
+ const snippet = createSnippet(normalizedPath, parsed.data ?? {});
69
+ if (snippet.draft) {
70
+ continue;
71
+ }
72
+ manifest.push(snippet);
73
+ } catch (error) {
74
+ console.warn(
75
+ `mark\u2193: failed to load snippet at '${normalizedPath}'`,
76
+ error
77
+ );
70
78
  }
71
- manifest.push(snippet);
72
79
  }
73
80
  ensureUniqueSlugs(manifest);
74
81
  manifest.sort((a, b) => {
@@ -239,7 +246,10 @@ var import_mark_down2 = require("@mzebley/mark-down");
239
246
  var DEFAULT_OUT_DIR = "dist";
240
247
  async function compilePage(inputHtml, options = {}) {
241
248
  const sourcePath = import_node_path3.default.resolve(inputHtml);
242
- await assertExists(sourcePath, `Input HTML file not found at '${inputHtml}'.`);
249
+ await assertExists(
250
+ sourcePath,
251
+ `Input HTML file not found at '${inputHtml}'.`
252
+ );
243
253
  const manifestPath = await resolveManifestPath(sourcePath, options.manifest);
244
254
  const manifestDir = import_node_path3.default.dirname(manifestPath);
245
255
  const manifest = await loadManifest(manifestPath);
@@ -274,9 +284,15 @@ async function compilePage(inputHtml, options = {}) {
274
284
  body = frontMatter.content;
275
285
  frontMatterSlug = frontMatter.slug;
276
286
  } catch (error) {
277
- console.warn(`mark\u2193: failed to parse front matter for '${entry.path}'`, error);
287
+ console.warn(
288
+ `mark\u2193: failed to parse front matter for '${entry.path}'`,
289
+ error
290
+ );
291
+ }
292
+ let html = (0, import_mark_down2.renderMarkdown)(body);
293
+ if (options.sanitize) {
294
+ html = (0, import_mark_down2.sanitizeMarkup)(html, options.sanitize);
278
295
  }
279
- const html = (0, import_mark_down2.renderMarkdown)(body);
280
296
  element.html(html);
281
297
  if (!element.attr("id")) {
282
298
  element.attr("id", frontMatterSlug ?? `snippet-${slug}`);
@@ -294,7 +310,10 @@ async function compilePage(inputHtml, options = {}) {
294
310
  }
295
311
  async function resolveManifestPath(inputHtml, manifestFlag) {
296
312
  const manifestPath = manifestFlag ? import_node_path3.default.resolve(manifestFlag) : import_node_path3.default.join(import_node_path3.default.dirname(import_node_path3.default.resolve(inputHtml)), "snippets-index.json");
297
- await assertExists(manifestPath, `Manifest file not found at '${manifestPath}'.`);
313
+ await assertExists(
314
+ manifestPath,
315
+ `Manifest file not found at '${manifestPath}'.`
316
+ );
298
317
  return manifestPath;
299
318
  }
300
319
  async function loadManifest(manifestPath) {
@@ -302,7 +321,9 @@ async function loadManifest(manifestPath) {
302
321
  try {
303
322
  raw = await import_promises2.default.readFile(manifestPath, "utf8");
304
323
  } catch (error) {
305
- throw new Error(`Failed to read manifest at '${manifestPath}': ${String(error)}`);
324
+ throw new Error(
325
+ `Failed to read manifest at '${manifestPath}': ${String(error)}`
326
+ );
306
327
  }
307
328
  try {
308
329
  const parsed = JSON.parse(raw);
@@ -311,7 +332,9 @@ async function loadManifest(manifestPath) {
311
332
  }
312
333
  return parsed;
313
334
  } catch (error) {
314
- throw new Error(`Failed to parse manifest at '${manifestPath}': ${String(error)}`);
335
+ throw new Error(
336
+ `Failed to parse manifest at '${manifestPath}': ${String(error)}`
337
+ );
315
338
  }
316
339
  }
317
340
  async function assertExists(target, message) {
@@ -327,7 +350,10 @@ var program = new import_commander.Command();
327
350
  program.name("mark-down").description(`${brand} CLI for building snippet manifests`).version("1.2.1");
328
351
  program.command("build").argument("[sourceDir]", "directory containing snippets", "content/snippets").option("-o, --output <path>", "where to write snippets-index.json").action(async (sourceDir, options) => {
329
352
  try {
330
- const result = await buildManifestFile({ sourceDir, outputPath: options.output });
353
+ const result = await buildManifestFile({
354
+ sourceDir,
355
+ outputPath: options.output
356
+ });
331
357
  logEvent("info", "manifest.written", {
332
358
  outputPath: result.outputPath,
333
359
  snippetCount: result.manifest.length
@@ -343,17 +369,26 @@ program.command("watch").argument("[sourceDir]", "directory containing snippets"
343
369
  handleError(error);
344
370
  }
345
371
  });
346
- program.command("compile-page").argument("<inputHtml>", "HTML file containing data-snippet placeholders").option("--manifest <path>", "path to snippets-index.json").option("--outDir <path>", "output directory for compiled HTML", "dist").option("--inPlace", "overwrite the input HTML file instead of writing to outDir").action(async (inputHtml, options) => {
347
- try {
348
- await compilePage(inputHtml, {
349
- manifest: options.manifest,
350
- outDir: options.outDir,
351
- inPlace: options.inPlace
352
- });
353
- } catch (error) {
354
- handleError(error);
372
+ program.command("compile-page").argument("<inputHtml>", "HTML file containing data-snippet placeholders").option("--manifest <path>", "path to snippets-index.json").option("--outDir <path>", "output directory for compiled HTML", "dist").option(
373
+ "--inPlace",
374
+ "overwrite the input HTML file instead of writing to outDir"
375
+ ).option(
376
+ "--sanitize [policy]",
377
+ "sanitize rendered HTML (default|strict|permissive)"
378
+ ).action(
379
+ async (inputHtml, options) => {
380
+ try {
381
+ await compilePage(inputHtml, {
382
+ manifest: options.manifest,
383
+ outDir: options.outDir,
384
+ inPlace: options.inPlace,
385
+ sanitize: resolveSanitizeOption(options.sanitize)
386
+ });
387
+ } catch (error) {
388
+ handleError(error);
389
+ }
355
390
  }
356
- });
391
+ );
357
392
  program.parseAsync(process.argv).catch(handleError);
358
393
  function handleError(error) {
359
394
  const err = error;
@@ -370,4 +405,23 @@ function handleError(error) {
370
405
  });
371
406
  process.exit(1);
372
407
  }
408
+ function resolveSanitizeOption(value) {
409
+ if (!value) {
410
+ return void 0;
411
+ }
412
+ if (value === true) {
413
+ return { policy: "default" };
414
+ }
415
+ const policy = value.trim();
416
+ if (!policy) {
417
+ return { policy: "default" };
418
+ }
419
+ if (!isSanitizePolicy(policy)) {
420
+ throw new Error(`Unknown sanitize policy '${policy}'.`);
421
+ }
422
+ return { policy };
423
+ }
424
+ function isSanitizePolicy(value) {
425
+ return value === "default" || value === "strict" || value === "permissive";
426
+ }
373
427
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/manifest.ts","../src/errors.ts","../src/watch.ts","../src/logger.ts","../src/compile-page.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { Command } from \"commander\";\nimport { buildManifestFile } from \"./manifest.js\";\nimport { watch as watchSnippets } from \"./watch.js\";\nimport { brand, logEvent } from \"./logger.js\";\nimport { DuplicateSlugError } from \"./errors.js\";\nimport { compilePage } from \"./compile-page.js\";\n\nconst program = new Command();\nprogram\n .name(\"mark-down\")\n .description(`${brand} CLI for building snippet manifests`)\n .version(\"1.2.1\");\n\nprogram\n .command(\"build\")\n .argument(\"[sourceDir]\", \"directory containing snippets\", \"content/snippets\")\n .option(\"-o, --output <path>\", \"where to write snippets-index.json\")\n .action(async (sourceDir: string, options: { output?: string }) => {\n try {\n const result = await buildManifestFile({ sourceDir, outputPath: options.output });\n logEvent(\"info\", \"manifest.written\", {\n outputPath: result.outputPath,\n snippetCount: result.manifest.length\n });\n } catch (error) {\n handleError(error);\n }\n });\n\nprogram\n .command(\"watch\")\n .argument(\"[sourceDir]\", \"directory containing snippets\", \"content/snippets\")\n .option(\"-o, --output <path>\", \"where to write snippets-index.json\")\n .action(async (sourceDir: string, options: { output?: string }) => {\n try {\n await watchSnippets(sourceDir, options.output);\n } catch (error) {\n handleError(error);\n }\n });\n\nprogram\n .command(\"compile-page\")\n .argument(\"<inputHtml>\", \"HTML file containing data-snippet placeholders\")\n .option(\"--manifest <path>\", \"path to snippets-index.json\")\n .option(\"--outDir <path>\", \"output directory for compiled HTML\", \"dist\")\n .option(\"--inPlace\", \"overwrite the input HTML file instead of writing to outDir\")\n .action(async (inputHtml: string, options: { manifest?: string; outDir?: string; inPlace?: boolean }) => {\n try {\n await compilePage(inputHtml, {\n manifest: options.manifest,\n outDir: options.outDir,\n inPlace: options.inPlace\n });\n } catch (error) {\n handleError(error);\n }\n });\n\nprogram.parseAsync(process.argv).catch(handleError);\n\nfunction handleError(error: unknown) {\n const err = error as Error;\n if (err instanceof DuplicateSlugError) {\n logEvent(\"error\", \"manifest.duplicate_slug\", {\n message: err.message,\n slugs: err.duplicates\n });\n process.exit(2);\n }\n logEvent(\"error\", \"cli.error\", {\n message: err.message,\n stack: err.stack\n });\n process.exit(1);\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport fg from \"fast-glob\";\nimport matter from \"gray-matter\";\nimport YAML from \"yaml\";\nimport { normalizeSlug, type SnippetMeta } from \"@mzebley/mark-down\";\nimport { DuplicateSlugError } from \"./errors.js\";\n\nconst MATTER_OPTIONS = {\n engines: {\n yaml: (source: string) => YAML.parse(source) ?? {}\n }\n};\n\nexport interface BuildOptions {\n sourceDir: string;\n outputPath?: string;\n}\n\nexport interface BuildResult {\n manifest: SnippetMeta[];\n outputPath: string;\n}\n\nexport async function buildManifestFile(options: BuildOptions): Promise<BuildResult> {\n const manifest = await buildManifest(options.sourceDir);\n const target = options.outputPath ?? path.join(options.sourceDir, \"snippets-index.json\");\n await fs.writeFile(target, JSON.stringify(manifest, null, 2));\n return { manifest, outputPath: target };\n}\n\nexport async function buildManifest(sourceDir: string): Promise<SnippetMeta[]> {\n const cwd = path.resolve(sourceDir);\n const files = await fg([\"**/*.md\"], { cwd, absolute: true });\n const manifest: SnippetMeta[] = [];\n\n for (const absolutePath of files) {\n const relativePath = path.relative(cwd, absolutePath);\n const normalizedPath = toPosix(relativePath);\n const content = await fs.readFile(absolutePath, \"utf8\");\n const parsed = matter(content, MATTER_OPTIONS);\n const snippet = createSnippet(normalizedPath, parsed.data ?? {});\n if (snippet.draft) {\n continue;\n }\n manifest.push(snippet);\n }\n\n ensureUniqueSlugs(manifest);\n\n manifest.sort((a, b) => {\n const orderA = typeof a.order === \"number\" ? a.order : Number.POSITIVE_INFINITY;\n const orderB = typeof b.order === \"number\" ? b.order : Number.POSITIVE_INFINITY;\n if (orderA !== orderB) {\n return orderA - orderB;\n }\n const titleA = a.title?.toLowerCase() ?? \"\";\n const titleB = b.title?.toLowerCase() ?? \"\";\n return titleA.localeCompare(titleB);\n });\n\n return manifest;\n}\n\nexport function createSnippet(relativePath: string, frontMatter: Record<string, unknown>): SnippetMeta {\n const group = deriveGroup(relativePath);\n const slugSource = typeof frontMatter.slug === \"string\" && frontMatter.slug.trim().length\n ? frontMatter.slug\n : relativePath.replace(/\\.md$/i, \"\");\n const slug = normalizeSlug(slugSource);\n\n const { title, order, type, tags, draft } = normalizeKnownFields(frontMatter);\n const extra = collectExtra(frontMatter);\n\n return {\n slug,\n title,\n order,\n type,\n tags,\n draft,\n path: relativePath,\n group,\n extra\n };\n}\n\nfunction normalizeKnownFields(data: Record<string, unknown>) {\n return {\n title: typeof data.title === \"string\" ? data.title : undefined,\n order: typeof data.order === \"number\"\n ? data.order\n : data.order === null\n ? null\n : undefined,\n type: typeof data.type === \"string\" ? data.type : undefined,\n tags: normalizeTags(data.tags),\n draft: data.draft === true ? true : undefined\n };\n}\n\nfunction collectExtra(data: Record<string, unknown>): Record<string, unknown> | undefined {\n const extra: Record<string, unknown> = {};\n const reserved = new Set([\"slug\", \"title\", \"order\", \"type\", \"tags\", \"draft\"]);\n for (const [key, value] of Object.entries(data)) {\n if (reserved.has(key)) {\n continue;\n }\n extra[key] = value;\n }\n return Object.keys(extra).length ? extra : undefined;\n}\n\nfunction normalizeTags(value: unknown): string[] | undefined {\n if (!value) {\n return undefined;\n }\n if (Array.isArray(value)) {\n return value.map((entry) => String(entry));\n }\n if (typeof value === \"string\") {\n return value\n .split(\",\")\n .map((entry) => entry.trim())\n .filter(Boolean);\n }\n return undefined;\n}\n\nfunction deriveGroup(relativePath: string): string {\n const dirname = toPosix(path.dirname(relativePath));\n if (dirname === \".\" || !dirname.length) {\n return \"root\";\n }\n return dirname;\n}\n\nfunction toPosix(value: string): string {\n return value.split(path.sep).join(\"/\");\n}\n\nfunction ensureUniqueSlugs(manifest: SnippetMeta[]) {\n const seen = new Map<string, string>();\n const duplicates = new Set<string>();\n for (const snippet of manifest) {\n if (seen.has(snippet.slug)) {\n duplicates.add(snippet.slug);\n } else {\n seen.set(snippet.slug, snippet.path);\n }\n }\n if (duplicates.size) {\n throw new DuplicateSlugError([...duplicates.values()]);\n }\n}\n","export class DuplicateSlugError extends Error {\n readonly duplicates: string[];\n\n constructor(duplicates: string[]) {\n super(`Duplicate slugs detected: ${duplicates.join(\", \")}`);\n this.name = \"DuplicateSlugError\";\n this.duplicates = duplicates;\n }\n}\n","import path from \"node:path\";\nimport chokidar from \"chokidar\";\nimport { buildManifestFile, type BuildResult } from \"./manifest.js\";\nimport { logEvent } from \"./logger.js\";\n\nexport async function watch(sourceDir: string, outputPath?: string) {\n const cwd = path.resolve(sourceDir);\n logEvent(\"info\", \"watch.start\", {\n directory: cwd,\n outputPath: outputPath ?? path.join(cwd, \"snippets-index.json\")\n });\n await rebuild(cwd, outputPath);\n\n const watcher = chokidar.watch([\"**/*.md\"], {\n cwd,\n ignoreInitial: true,\n awaitWriteFinish: {\n stabilityThreshold: 200,\n pollInterval: 50\n }\n });\n\n const schedule = debounce(async () => {\n await rebuild(cwd, outputPath);\n }, 150);\n\n watcher.on(\"all\", (event, filePath) => {\n logEvent(\"info\", \"watch.change\", { event, file: filePath });\n schedule();\n });\n}\n\nasync function rebuild(sourceDir: string, outputPath?: string): Promise<BuildResult | void> {\n try {\n const result = await buildManifestFile({ sourceDir, outputPath });\n logEvent(\"info\", \"manifest.updated\", {\n outputPath: result.outputPath,\n snippetCount: result.manifest.length\n });\n return result;\n } catch (error) {\n const err = error as Error;\n logEvent(\"error\", \"manifest.update_failed\", {\n message: err.message,\n stack: err.stack\n });\n }\n}\n\nfunction debounce<T extends (...args: unknown[]) => Promise<unknown> | void>(\n fn: T,\n delay: number\n) {\n let timer: NodeJS.Timeout | null = null;\n return (...args: Parameters<T>) => {\n if (timer) {\n clearTimeout(timer);\n }\n timer = setTimeout(() => {\n timer = null;\n void fn(...args);\n }, delay);\n };\n}\n","export const brand = \"mark↓\";\n\nexport type LogLevel = \"info\" | \"warn\" | \"error\";\n\nexport interface LogFields {\n message?: string;\n [key: string]: unknown;\n}\n\nexport function logEvent(level: LogLevel, event: string, fields: LogFields = {}) {\n const entry = {\n brand,\n level,\n event,\n timestamp: new Date().toISOString(),\n ...fields\n };\n const output = `${JSON.stringify(entry)}\\n`;\n const stream = level === \"error\" ? process.stderr : process.stdout;\n stream.write(output);\n}\n\nexport function log(message: string, fields?: LogFields) {\n if (fields) {\n logEvent(\"info\", message, fields);\n return;\n }\n logEvent(\"info\", \"message\", { message });\n}\n\nexport function logError(message: string, fields?: LogFields) {\n if (fields) {\n logEvent(\"error\", message, fields);\n return;\n }\n logEvent(\"error\", \"message\", { message });\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { load as loadHtml } from \"cheerio\";\nimport { parseFrontMatter, renderMarkdown, type SnippetMeta } from \"@mzebley/mark-down\";\nimport { logEvent } from \"./logger.js\";\n\nexport interface CompilePageOptions {\n manifest?: string;\n outDir?: string;\n inPlace?: boolean;\n}\n\nconst DEFAULT_OUT_DIR = \"dist\";\n\nexport async function compilePage(inputHtml: string, options: CompilePageOptions = {}): Promise<string> {\n const sourcePath = path.resolve(inputHtml);\n await assertExists(sourcePath, `Input HTML file not found at '${inputHtml}'.`);\n\n const manifestPath = await resolveManifestPath(sourcePath, options.manifest);\n const manifestDir = path.dirname(manifestPath);\n const manifest = await loadManifest(manifestPath);\n\n const rawHtml = await fs.readFile(sourcePath, \"utf8\");\n const doctypeMatch = rawHtml.match(/^(<!doctype[^>]*>\\s*)/i);\n const doctype = doctypeMatch?.[1] ?? \"\";\n const dom = loadHtml(rawHtml, { decodeEntities: false });\n\n const targets = dom(\"[data-snippet]\").toArray();\n for (const node of targets) {\n const element = dom(node);\n const slug = element.attr(\"data-snippet\");\n if (!slug) {\n continue;\n }\n const entry = manifest.find((item) => item.slug === slug);\n if (!entry) {\n console.warn(`mark↓: no snippet found for \"${slug}\"`);\n continue;\n }\n\n const snippetPath = path.resolve(manifestDir, entry.path);\n let raw: string;\n try {\n raw = await fs.readFile(snippetPath, \"utf8\");\n } catch (error) {\n console.warn(`mark↓: failed to read snippet at '${entry.path}'`, error);\n continue;\n }\n\n let body = raw;\n let frontMatterSlug: string | undefined;\n try {\n const frontMatter = parseFrontMatter(raw);\n body = frontMatter.content;\n frontMatterSlug = frontMatter.slug;\n } catch (error) {\n console.warn(`mark↓: failed to parse front matter for '${entry.path}'`, error);\n }\n\n const html = renderMarkdown(body);\n element.html(html);\n\n if (!element.attr(\"id\")) {\n element.attr(\"id\", frontMatterSlug ?? `snippet-${slug}`);\n }\n }\n\n const outputDir = options.inPlace ? path.dirname(sourcePath) : path.resolve(options.outDir ?? DEFAULT_OUT_DIR);\n if (!options.inPlace) {\n await fs.mkdir(outputDir, { recursive: true });\n }\n const outputPath = options.inPlace\n ? sourcePath\n : path.join(outputDir, path.basename(sourcePath));\n\n const outputHtml = `${doctype}${dom.html() ?? \"\"}`;\n await fs.writeFile(outputPath, outputHtml);\n\n logEvent(\"info\", \"compile_page.written\", { outputPath });\n return outputPath;\n}\n\nasync function resolveManifestPath(inputHtml: string, manifestFlag?: string): Promise<string> {\n const manifestPath = manifestFlag\n ? path.resolve(manifestFlag)\n : path.join(path.dirname(path.resolve(inputHtml)), \"snippets-index.json\");\n await assertExists(manifestPath, `Manifest file not found at '${manifestPath}'.`);\n return manifestPath;\n}\n\nasync function loadManifest(manifestPath: string): Promise<SnippetMeta[]> {\n let raw: string;\n try {\n raw = await fs.readFile(manifestPath, \"utf8\");\n } catch (error) {\n throw new Error(`Failed to read manifest at '${manifestPath}': ${String(error)}`);\n }\n\n try {\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n throw new Error(\"Manifest must be a JSON array.\");\n }\n return parsed as SnippetMeta[];\n } catch (error) {\n throw new Error(`Failed to parse manifest at '${manifestPath}': ${String(error)}`);\n }\n}\n\nasync function assertExists(target: string, message: string) {\n try {\n await fs.access(target);\n } catch {\n throw new Error(message);\n }\n}\n"],"mappings":"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","names":["YAML","path","fs","fg","matter","import_node_path","path","chokidar","import_promises","import_node_path","import_mark_down","path","fs","loadHtml"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/manifest.ts","../src/errors.ts","../src/watch.ts","../src/logger.ts","../src/compile-page.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { Command } from \"commander\";\nimport { buildManifestFile } from \"./manifest.js\";\nimport { watch as watchSnippets } from \"./watch.js\";\nimport { brand, logEvent } from \"./logger.js\";\nimport { DuplicateSlugError } from \"./errors.js\";\nimport { compilePage } from \"./compile-page.js\";\nimport type { SanitizeOptions, SanitizePolicy } from \"@mzebley/mark-down\";\n\nconst program = new Command();\nprogram\n .name(\"mark-down\")\n .description(`${brand} CLI for building snippet manifests`)\n .version(\"1.2.1\");\n\nprogram\n .command(\"build\")\n .argument(\"[sourceDir]\", \"directory containing snippets\", \"content/snippets\")\n .option(\"-o, --output <path>\", \"where to write snippets-index.json\")\n .action(async (sourceDir: string, options: { output?: string }) => {\n try {\n const result = await buildManifestFile({\n sourceDir,\n outputPath: options.output,\n });\n logEvent(\"info\", \"manifest.written\", {\n outputPath: result.outputPath,\n snippetCount: result.manifest.length,\n });\n } catch (error) {\n handleError(error);\n }\n });\n\nprogram\n .command(\"watch\")\n .argument(\"[sourceDir]\", \"directory containing snippets\", \"content/snippets\")\n .option(\"-o, --output <path>\", \"where to write snippets-index.json\")\n .action(async (sourceDir: string, options: { output?: string }) => {\n try {\n await watchSnippets(sourceDir, options.output);\n } catch (error) {\n handleError(error);\n }\n });\n\nprogram\n .command(\"compile-page\")\n .argument(\"<inputHtml>\", \"HTML file containing data-snippet placeholders\")\n .option(\"--manifest <path>\", \"path to snippets-index.json\")\n .option(\"--outDir <path>\", \"output directory for compiled HTML\", \"dist\")\n .option(\n \"--inPlace\",\n \"overwrite the input HTML file instead of writing to outDir\",\n )\n .option(\n \"--sanitize [policy]\",\n \"sanitize rendered HTML (default|strict|permissive)\",\n )\n .action(\n async (\n inputHtml: string,\n options: {\n manifest?: string;\n outDir?: string;\n inPlace?: boolean;\n sanitize?: string | boolean;\n },\n ) => {\n try {\n await compilePage(inputHtml, {\n manifest: options.manifest,\n outDir: options.outDir,\n inPlace: options.inPlace,\n sanitize: resolveSanitizeOption(options.sanitize),\n });\n } catch (error) {\n handleError(error);\n }\n },\n );\n\nprogram.parseAsync(process.argv).catch(handleError);\n\nfunction handleError(error: unknown) {\n const err = error as Error;\n if (err instanceof DuplicateSlugError) {\n logEvent(\"error\", \"manifest.duplicate_slug\", {\n message: err.message,\n slugs: err.duplicates,\n });\n process.exit(2);\n }\n logEvent(\"error\", \"cli.error\", {\n message: err.message,\n stack: err.stack,\n });\n process.exit(1);\n}\n\nfunction resolveSanitizeOption(\n value?: string | boolean,\n): SanitizeOptions | undefined {\n if (!value) {\n return undefined;\n }\n if (value === true) {\n return { policy: \"default\" };\n }\n const policy = value.trim();\n if (!policy) {\n return { policy: \"default\" };\n }\n if (!isSanitizePolicy(policy)) {\n throw new Error(`Unknown sanitize policy '${policy}'.`);\n }\n return { policy };\n}\n\nfunction isSanitizePolicy(value: string): value is SanitizePolicy {\n return value === \"default\" || value === \"strict\" || value === \"permissive\";\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport fg from \"fast-glob\";\nimport matter from \"gray-matter\";\nimport YAML from \"yaml\";\nimport { normalizeSlug, type SnippetMeta } from \"@mzebley/mark-down\";\nimport { DuplicateSlugError } from \"./errors.js\";\n\nconst MATTER_OPTIONS = {\n engines: {\n yaml: (source: string) => YAML.parse(source) ?? {},\n },\n};\n\nexport interface BuildOptions {\n sourceDir: string;\n outputPath?: string;\n}\n\nexport interface BuildResult {\n manifest: SnippetMeta[];\n outputPath: string;\n}\n\nexport async function buildManifestFile(\n options: BuildOptions,\n): Promise<BuildResult> {\n const manifest = await buildManifest(options.sourceDir);\n const target =\n options.outputPath ?? path.join(options.sourceDir, \"snippets-index.json\");\n await fs.writeFile(target, JSON.stringify(manifest, null, 2));\n return { manifest, outputPath: target };\n}\n\nexport async function buildManifest(sourceDir: string): Promise<SnippetMeta[]> {\n const cwd = path.resolve(sourceDir);\n const files = await fg([\"**/*.md\"], { cwd, absolute: true });\n const manifest: SnippetMeta[] = [];\n\n for (const absolutePath of files) {\n const relativePath = path.relative(cwd, absolutePath);\n const normalizedPath = toPosix(relativePath);\n try {\n const content = await fs.readFile(absolutePath, \"utf8\");\n const parsed = matter(content, MATTER_OPTIONS);\n const snippet = createSnippet(normalizedPath, parsed.data ?? {});\n if (snippet.draft) {\n continue;\n }\n manifest.push(snippet);\n } catch (error) {\n console.warn(\n `mark↓: failed to load snippet at '${normalizedPath}'`,\n error,\n );\n }\n }\n\n ensureUniqueSlugs(manifest);\n\n manifest.sort((a, b) => {\n const orderA =\n typeof a.order === \"number\" ? a.order : Number.POSITIVE_INFINITY;\n const orderB =\n typeof b.order === \"number\" ? b.order : Number.POSITIVE_INFINITY;\n if (orderA !== orderB) {\n return orderA - orderB;\n }\n const titleA = a.title?.toLowerCase() ?? \"\";\n const titleB = b.title?.toLowerCase() ?? \"\";\n return titleA.localeCompare(titleB);\n });\n\n return manifest;\n}\n\nexport function createSnippet(\n relativePath: string,\n frontMatter: Record<string, unknown>,\n): SnippetMeta {\n const group = deriveGroup(relativePath);\n const slugSource =\n typeof frontMatter.slug === \"string\" && frontMatter.slug.trim().length\n ? frontMatter.slug\n : relativePath.replace(/\\.md$/i, \"\");\n const slug = normalizeSlug(slugSource);\n\n const { title, order, type, tags, draft } = normalizeKnownFields(frontMatter);\n const extra = collectExtra(frontMatter);\n\n return {\n slug,\n title,\n order,\n type,\n tags,\n draft,\n path: relativePath,\n group,\n extra,\n };\n}\n\nfunction normalizeKnownFields(data: Record<string, unknown>) {\n return {\n title: typeof data.title === \"string\" ? data.title : undefined,\n order:\n typeof data.order === \"number\"\n ? data.order\n : data.order === null\n ? null\n : undefined,\n type: typeof data.type === \"string\" ? data.type : undefined,\n tags: normalizeTags(data.tags),\n draft: data.draft === true ? true : undefined,\n };\n}\n\nfunction collectExtra(\n data: Record<string, unknown>,\n): Record<string, unknown> | undefined {\n const extra: Record<string, unknown> = {};\n const reserved = new Set([\"slug\", \"title\", \"order\", \"type\", \"tags\", \"draft\"]);\n for (const [key, value] of Object.entries(data)) {\n if (reserved.has(key)) {\n continue;\n }\n extra[key] = value;\n }\n return Object.keys(extra).length ? extra : undefined;\n}\n\nfunction normalizeTags(value: unknown): string[] | undefined {\n if (!value) {\n return undefined;\n }\n if (Array.isArray(value)) {\n return value.map((entry) => String(entry));\n }\n if (typeof value === \"string\") {\n return value\n .split(\",\")\n .map((entry) => entry.trim())\n .filter(Boolean);\n }\n return undefined;\n}\n\nfunction deriveGroup(relativePath: string): string {\n const dirname = toPosix(path.dirname(relativePath));\n if (dirname === \".\" || !dirname.length) {\n return \"root\";\n }\n return dirname;\n}\n\nfunction toPosix(value: string): string {\n return value.split(path.sep).join(\"/\");\n}\n\nfunction ensureUniqueSlugs(manifest: SnippetMeta[]) {\n const seen = new Map<string, string>();\n const duplicates = new Set<string>();\n for (const snippet of manifest) {\n if (seen.has(snippet.slug)) {\n duplicates.add(snippet.slug);\n } else {\n seen.set(snippet.slug, snippet.path);\n }\n }\n if (duplicates.size) {\n throw new DuplicateSlugError([...duplicates.values()]);\n }\n}\n","export class DuplicateSlugError extends Error {\n readonly duplicates: string[];\n\n constructor(duplicates: string[]) {\n super(`Duplicate slugs detected: ${duplicates.join(\", \")}`);\n this.name = \"DuplicateSlugError\";\n this.duplicates = duplicates;\n }\n}\n","import path from \"node:path\";\nimport chokidar from \"chokidar\";\nimport { buildManifestFile, type BuildResult } from \"./manifest.js\";\nimport { logEvent } from \"./logger.js\";\n\nexport async function watch(sourceDir: string, outputPath?: string) {\n const cwd = path.resolve(sourceDir);\n logEvent(\"info\", \"watch.start\", {\n directory: cwd,\n outputPath: outputPath ?? path.join(cwd, \"snippets-index.json\"),\n });\n await rebuild(cwd, outputPath);\n\n const watcher = chokidar.watch([\"**/*.md\"], {\n cwd,\n ignoreInitial: true,\n awaitWriteFinish: {\n stabilityThreshold: 200,\n pollInterval: 50,\n },\n });\n\n const schedule = debounce(async () => {\n await rebuild(cwd, outputPath);\n }, 150);\n\n watcher.on(\"all\", (event, filePath) => {\n logEvent(\"info\", \"watch.change\", { event, file: filePath });\n schedule();\n });\n}\n\nasync function rebuild(\n sourceDir: string,\n outputPath?: string,\n): Promise<BuildResult | void> {\n try {\n const result = await buildManifestFile({ sourceDir, outputPath });\n logEvent(\"info\", \"manifest.updated\", {\n outputPath: result.outputPath,\n snippetCount: result.manifest.length,\n });\n return result;\n } catch (error) {\n const err = error as Error;\n logEvent(\"error\", \"manifest.update_failed\", {\n message: err.message,\n stack: err.stack,\n });\n }\n}\n\nfunction debounce<T extends (...args: unknown[]) => Promise<unknown> | void>(\n fn: T,\n delay: number,\n) {\n let timer: NodeJS.Timeout | null = null;\n return (...args: Parameters<T>) => {\n if (timer) {\n clearTimeout(timer);\n }\n timer = setTimeout(() => {\n timer = null;\n void fn(...args);\n }, delay);\n };\n}\n","export const brand = \"mark↓\";\n\nexport type LogLevel = \"info\" | \"warn\" | \"error\";\n\nexport interface LogFields {\n message?: string;\n [key: string]: unknown;\n}\n\nexport function logEvent(\n level: LogLevel,\n event: string,\n fields: LogFields = {},\n) {\n const entry = {\n brand,\n level,\n event,\n timestamp: new Date().toISOString(),\n ...fields,\n };\n const output = `${JSON.stringify(entry)}\\n`;\n const stream = level === \"error\" ? process.stderr : process.stdout;\n stream.write(output);\n}\n\nexport function log(message: string, fields?: LogFields) {\n if (fields) {\n logEvent(\"info\", message, fields);\n return;\n }\n logEvent(\"info\", \"message\", { message });\n}\n\nexport function logError(message: string, fields?: LogFields) {\n if (fields) {\n logEvent(\"error\", message, fields);\n return;\n }\n logEvent(\"error\", \"message\", { message });\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { load as loadHtml } from \"cheerio\";\nimport {\n parseFrontMatter,\n renderMarkdown,\n sanitizeMarkup,\n type SanitizeOptions,\n type SnippetMeta,\n} from \"@mzebley/mark-down\";\nimport { logEvent } from \"./logger.js\";\n\nexport interface CompilePageOptions {\n manifest?: string;\n outDir?: string;\n inPlace?: boolean;\n sanitize?: SanitizeOptions;\n}\n\nconst DEFAULT_OUT_DIR = \"dist\";\n\nexport async function compilePage(\n inputHtml: string,\n options: CompilePageOptions = {},\n): Promise<string> {\n const sourcePath = path.resolve(inputHtml);\n await assertExists(\n sourcePath,\n `Input HTML file not found at '${inputHtml}'.`,\n );\n\n const manifestPath = await resolveManifestPath(sourcePath, options.manifest);\n const manifestDir = path.dirname(manifestPath);\n const manifest = await loadManifest(manifestPath);\n\n const rawHtml = await fs.readFile(sourcePath, \"utf8\");\n const doctypeMatch = rawHtml.match(/^(<!doctype[^>]*>\\s*)/i);\n const doctype = doctypeMatch?.[1] ?? \"\";\n const dom = loadHtml(rawHtml, { decodeEntities: false });\n\n const targets = dom(\"[data-snippet]\").toArray();\n for (const node of targets) {\n const element = dom(node);\n const slug = element.attr(\"data-snippet\");\n if (!slug) {\n continue;\n }\n const entry = manifest.find((item) => item.slug === slug);\n if (!entry) {\n console.warn(`mark↓: no snippet found for \"${slug}\"`);\n continue;\n }\n\n const snippetPath = path.resolve(manifestDir, entry.path);\n let raw: string;\n try {\n raw = await fs.readFile(snippetPath, \"utf8\");\n } catch (error) {\n console.warn(`mark↓: failed to read snippet at '${entry.path}'`, error);\n continue;\n }\n\n let body = raw;\n let frontMatterSlug: string | undefined;\n try {\n const frontMatter = parseFrontMatter(raw);\n body = frontMatter.content;\n frontMatterSlug = frontMatter.slug;\n } catch (error) {\n console.warn(\n `mark↓: failed to parse front matter for '${entry.path}'`,\n error,\n );\n }\n\n let html = renderMarkdown(body);\n if (options.sanitize) {\n html = sanitizeMarkup(html, options.sanitize);\n }\n element.html(html);\n\n if (!element.attr(\"id\")) {\n element.attr(\"id\", frontMatterSlug ?? `snippet-${slug}`);\n }\n }\n\n const outputDir = options.inPlace\n ? path.dirname(sourcePath)\n : path.resolve(options.outDir ?? DEFAULT_OUT_DIR);\n if (!options.inPlace) {\n await fs.mkdir(outputDir, { recursive: true });\n }\n const outputPath = options.inPlace\n ? sourcePath\n : path.join(outputDir, path.basename(sourcePath));\n\n const outputHtml = `${doctype}${dom.html() ?? \"\"}`;\n await fs.writeFile(outputPath, outputHtml);\n\n logEvent(\"info\", \"compile_page.written\", { outputPath });\n return outputPath;\n}\n\nasync function resolveManifestPath(\n inputHtml: string,\n manifestFlag?: string,\n): Promise<string> {\n const manifestPath = manifestFlag\n ? path.resolve(manifestFlag)\n : path.join(path.dirname(path.resolve(inputHtml)), \"snippets-index.json\");\n await assertExists(\n manifestPath,\n `Manifest file not found at '${manifestPath}'.`,\n );\n return manifestPath;\n}\n\nasync function loadManifest(manifestPath: string): Promise<SnippetMeta[]> {\n let raw: string;\n try {\n raw = await fs.readFile(manifestPath, \"utf8\");\n } catch (error) {\n throw new Error(\n `Failed to read manifest at '${manifestPath}': ${String(error)}`,\n );\n }\n\n try {\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n throw new Error(\"Manifest must be a JSON array.\");\n }\n return parsed as SnippetMeta[];\n } catch (error) {\n throw new Error(\n `Failed to parse manifest at '${manifestPath}': ${String(error)}`,\n );\n }\n}\n\nasync function assertExists(target: string, message: string) {\n try {\n await fs.access(target);\n } catch {\n throw new Error(message);\n }\n}\n"],"mappings":"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","names":["YAML","path","fs","fg","matter","import_node_path","path","chokidar","import_promises","import_node_path","import_mark_down","path","fs","loadHtml"]}
package/dist/index.mjs CHANGED
@@ -39,13 +39,20 @@ async function buildManifest(sourceDir) {
39
39
  for (const absolutePath of files) {
40
40
  const relativePath = path.relative(cwd, absolutePath);
41
41
  const normalizedPath = toPosix(relativePath);
42
- const content = await fs.readFile(absolutePath, "utf8");
43
- const parsed = matter(content, MATTER_OPTIONS);
44
- const snippet = createSnippet(normalizedPath, parsed.data ?? {});
45
- if (snippet.draft) {
46
- continue;
42
+ try {
43
+ const content = await fs.readFile(absolutePath, "utf8");
44
+ const parsed = matter(content, MATTER_OPTIONS);
45
+ const snippet = createSnippet(normalizedPath, parsed.data ?? {});
46
+ if (snippet.draft) {
47
+ continue;
48
+ }
49
+ manifest.push(snippet);
50
+ } catch (error) {
51
+ console.warn(
52
+ `mark\u2193: failed to load snippet at '${normalizedPath}'`,
53
+ error
54
+ );
47
55
  }
48
- manifest.push(snippet);
49
56
  }
50
57
  ensureUniqueSlugs(manifest);
51
58
  manifest.sort((a, b) => {
@@ -212,11 +219,18 @@ function debounce(fn, delay) {
212
219
  import fs2 from "fs/promises";
213
220
  import path3 from "path";
214
221
  import { load as loadHtml } from "cheerio";
215
- import { parseFrontMatter, renderMarkdown } from "@mzebley/mark-down";
222
+ import {
223
+ parseFrontMatter,
224
+ renderMarkdown,
225
+ sanitizeMarkup
226
+ } from "@mzebley/mark-down";
216
227
  var DEFAULT_OUT_DIR = "dist";
217
228
  async function compilePage(inputHtml, options = {}) {
218
229
  const sourcePath = path3.resolve(inputHtml);
219
- await assertExists(sourcePath, `Input HTML file not found at '${inputHtml}'.`);
230
+ await assertExists(
231
+ sourcePath,
232
+ `Input HTML file not found at '${inputHtml}'.`
233
+ );
220
234
  const manifestPath = await resolveManifestPath(sourcePath, options.manifest);
221
235
  const manifestDir = path3.dirname(manifestPath);
222
236
  const manifest = await loadManifest(manifestPath);
@@ -251,9 +265,15 @@ async function compilePage(inputHtml, options = {}) {
251
265
  body = frontMatter.content;
252
266
  frontMatterSlug = frontMatter.slug;
253
267
  } catch (error) {
254
- console.warn(`mark\u2193: failed to parse front matter for '${entry.path}'`, error);
268
+ console.warn(
269
+ `mark\u2193: failed to parse front matter for '${entry.path}'`,
270
+ error
271
+ );
272
+ }
273
+ let html = renderMarkdown(body);
274
+ if (options.sanitize) {
275
+ html = sanitizeMarkup(html, options.sanitize);
255
276
  }
256
- const html = renderMarkdown(body);
257
277
  element.html(html);
258
278
  if (!element.attr("id")) {
259
279
  element.attr("id", frontMatterSlug ?? `snippet-${slug}`);
@@ -271,7 +291,10 @@ async function compilePage(inputHtml, options = {}) {
271
291
  }
272
292
  async function resolveManifestPath(inputHtml, manifestFlag) {
273
293
  const manifestPath = manifestFlag ? path3.resolve(manifestFlag) : path3.join(path3.dirname(path3.resolve(inputHtml)), "snippets-index.json");
274
- await assertExists(manifestPath, `Manifest file not found at '${manifestPath}'.`);
294
+ await assertExists(
295
+ manifestPath,
296
+ `Manifest file not found at '${manifestPath}'.`
297
+ );
275
298
  return manifestPath;
276
299
  }
277
300
  async function loadManifest(manifestPath) {
@@ -279,7 +302,9 @@ async function loadManifest(manifestPath) {
279
302
  try {
280
303
  raw = await fs2.readFile(manifestPath, "utf8");
281
304
  } catch (error) {
282
- throw new Error(`Failed to read manifest at '${manifestPath}': ${String(error)}`);
305
+ throw new Error(
306
+ `Failed to read manifest at '${manifestPath}': ${String(error)}`
307
+ );
283
308
  }
284
309
  try {
285
310
  const parsed = JSON.parse(raw);
@@ -288,7 +313,9 @@ async function loadManifest(manifestPath) {
288
313
  }
289
314
  return parsed;
290
315
  } catch (error) {
291
- throw new Error(`Failed to parse manifest at '${manifestPath}': ${String(error)}`);
316
+ throw new Error(
317
+ `Failed to parse manifest at '${manifestPath}': ${String(error)}`
318
+ );
292
319
  }
293
320
  }
294
321
  async function assertExists(target, message) {
@@ -304,7 +331,10 @@ var program = new Command();
304
331
  program.name("mark-down").description(`${brand} CLI for building snippet manifests`).version("1.2.1");
305
332
  program.command("build").argument("[sourceDir]", "directory containing snippets", "content/snippets").option("-o, --output <path>", "where to write snippets-index.json").action(async (sourceDir, options) => {
306
333
  try {
307
- const result = await buildManifestFile({ sourceDir, outputPath: options.output });
334
+ const result = await buildManifestFile({
335
+ sourceDir,
336
+ outputPath: options.output
337
+ });
308
338
  logEvent("info", "manifest.written", {
309
339
  outputPath: result.outputPath,
310
340
  snippetCount: result.manifest.length
@@ -320,17 +350,26 @@ program.command("watch").argument("[sourceDir]", "directory containing snippets"
320
350
  handleError(error);
321
351
  }
322
352
  });
323
- program.command("compile-page").argument("<inputHtml>", "HTML file containing data-snippet placeholders").option("--manifest <path>", "path to snippets-index.json").option("--outDir <path>", "output directory for compiled HTML", "dist").option("--inPlace", "overwrite the input HTML file instead of writing to outDir").action(async (inputHtml, options) => {
324
- try {
325
- await compilePage(inputHtml, {
326
- manifest: options.manifest,
327
- outDir: options.outDir,
328
- inPlace: options.inPlace
329
- });
330
- } catch (error) {
331
- handleError(error);
353
+ program.command("compile-page").argument("<inputHtml>", "HTML file containing data-snippet placeholders").option("--manifest <path>", "path to snippets-index.json").option("--outDir <path>", "output directory for compiled HTML", "dist").option(
354
+ "--inPlace",
355
+ "overwrite the input HTML file instead of writing to outDir"
356
+ ).option(
357
+ "--sanitize [policy]",
358
+ "sanitize rendered HTML (default|strict|permissive)"
359
+ ).action(
360
+ async (inputHtml, options) => {
361
+ try {
362
+ await compilePage(inputHtml, {
363
+ manifest: options.manifest,
364
+ outDir: options.outDir,
365
+ inPlace: options.inPlace,
366
+ sanitize: resolveSanitizeOption(options.sanitize)
367
+ });
368
+ } catch (error) {
369
+ handleError(error);
370
+ }
332
371
  }
333
- });
372
+ );
334
373
  program.parseAsync(process.argv).catch(handleError);
335
374
  function handleError(error) {
336
375
  const err = error;
@@ -347,4 +386,23 @@ function handleError(error) {
347
386
  });
348
387
  process.exit(1);
349
388
  }
389
+ function resolveSanitizeOption(value) {
390
+ if (!value) {
391
+ return void 0;
392
+ }
393
+ if (value === true) {
394
+ return { policy: "default" };
395
+ }
396
+ const policy = value.trim();
397
+ if (!policy) {
398
+ return { policy: "default" };
399
+ }
400
+ if (!isSanitizePolicy(policy)) {
401
+ throw new Error(`Unknown sanitize policy '${policy}'.`);
402
+ }
403
+ return { policy };
404
+ }
405
+ function isSanitizePolicy(value) {
406
+ return value === "default" || value === "strict" || value === "permissive";
407
+ }
350
408
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/manifest.ts","../src/errors.ts","../src/watch.ts","../src/logger.ts","../src/compile-page.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { Command } from \"commander\";\nimport { buildManifestFile } from \"./manifest.js\";\nimport { watch as watchSnippets } from \"./watch.js\";\nimport { brand, logEvent } from \"./logger.js\";\nimport { DuplicateSlugError } from \"./errors.js\";\nimport { compilePage } from \"./compile-page.js\";\n\nconst program = new Command();\nprogram\n .name(\"mark-down\")\n .description(`${brand} CLI for building snippet manifests`)\n .version(\"1.2.1\");\n\nprogram\n .command(\"build\")\n .argument(\"[sourceDir]\", \"directory containing snippets\", \"content/snippets\")\n .option(\"-o, --output <path>\", \"where to write snippets-index.json\")\n .action(async (sourceDir: string, options: { output?: string }) => {\n try {\n const result = await buildManifestFile({ sourceDir, outputPath: options.output });\n logEvent(\"info\", \"manifest.written\", {\n outputPath: result.outputPath,\n snippetCount: result.manifest.length\n });\n } catch (error) {\n handleError(error);\n }\n });\n\nprogram\n .command(\"watch\")\n .argument(\"[sourceDir]\", \"directory containing snippets\", \"content/snippets\")\n .option(\"-o, --output <path>\", \"where to write snippets-index.json\")\n .action(async (sourceDir: string, options: { output?: string }) => {\n try {\n await watchSnippets(sourceDir, options.output);\n } catch (error) {\n handleError(error);\n }\n });\n\nprogram\n .command(\"compile-page\")\n .argument(\"<inputHtml>\", \"HTML file containing data-snippet placeholders\")\n .option(\"--manifest <path>\", \"path to snippets-index.json\")\n .option(\"--outDir <path>\", \"output directory for compiled HTML\", \"dist\")\n .option(\"--inPlace\", \"overwrite the input HTML file instead of writing to outDir\")\n .action(async (inputHtml: string, options: { manifest?: string; outDir?: string; inPlace?: boolean }) => {\n try {\n await compilePage(inputHtml, {\n manifest: options.manifest,\n outDir: options.outDir,\n inPlace: options.inPlace\n });\n } catch (error) {\n handleError(error);\n }\n });\n\nprogram.parseAsync(process.argv).catch(handleError);\n\nfunction handleError(error: unknown) {\n const err = error as Error;\n if (err instanceof DuplicateSlugError) {\n logEvent(\"error\", \"manifest.duplicate_slug\", {\n message: err.message,\n slugs: err.duplicates\n });\n process.exit(2);\n }\n logEvent(\"error\", \"cli.error\", {\n message: err.message,\n stack: err.stack\n });\n process.exit(1);\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport fg from \"fast-glob\";\nimport matter from \"gray-matter\";\nimport YAML from \"yaml\";\nimport { normalizeSlug, type SnippetMeta } from \"@mzebley/mark-down\";\nimport { DuplicateSlugError } from \"./errors.js\";\n\nconst MATTER_OPTIONS = {\n engines: {\n yaml: (source: string) => YAML.parse(source) ?? {}\n }\n};\n\nexport interface BuildOptions {\n sourceDir: string;\n outputPath?: string;\n}\n\nexport interface BuildResult {\n manifest: SnippetMeta[];\n outputPath: string;\n}\n\nexport async function buildManifestFile(options: BuildOptions): Promise<BuildResult> {\n const manifest = await buildManifest(options.sourceDir);\n const target = options.outputPath ?? path.join(options.sourceDir, \"snippets-index.json\");\n await fs.writeFile(target, JSON.stringify(manifest, null, 2));\n return { manifest, outputPath: target };\n}\n\nexport async function buildManifest(sourceDir: string): Promise<SnippetMeta[]> {\n const cwd = path.resolve(sourceDir);\n const files = await fg([\"**/*.md\"], { cwd, absolute: true });\n const manifest: SnippetMeta[] = [];\n\n for (const absolutePath of files) {\n const relativePath = path.relative(cwd, absolutePath);\n const normalizedPath = toPosix(relativePath);\n const content = await fs.readFile(absolutePath, \"utf8\");\n const parsed = matter(content, MATTER_OPTIONS);\n const snippet = createSnippet(normalizedPath, parsed.data ?? {});\n if (snippet.draft) {\n continue;\n }\n manifest.push(snippet);\n }\n\n ensureUniqueSlugs(manifest);\n\n manifest.sort((a, b) => {\n const orderA = typeof a.order === \"number\" ? a.order : Number.POSITIVE_INFINITY;\n const orderB = typeof b.order === \"number\" ? b.order : Number.POSITIVE_INFINITY;\n if (orderA !== orderB) {\n return orderA - orderB;\n }\n const titleA = a.title?.toLowerCase() ?? \"\";\n const titleB = b.title?.toLowerCase() ?? \"\";\n return titleA.localeCompare(titleB);\n });\n\n return manifest;\n}\n\nexport function createSnippet(relativePath: string, frontMatter: Record<string, unknown>): SnippetMeta {\n const group = deriveGroup(relativePath);\n const slugSource = typeof frontMatter.slug === \"string\" && frontMatter.slug.trim().length\n ? frontMatter.slug\n : relativePath.replace(/\\.md$/i, \"\");\n const slug = normalizeSlug(slugSource);\n\n const { title, order, type, tags, draft } = normalizeKnownFields(frontMatter);\n const extra = collectExtra(frontMatter);\n\n return {\n slug,\n title,\n order,\n type,\n tags,\n draft,\n path: relativePath,\n group,\n extra\n };\n}\n\nfunction normalizeKnownFields(data: Record<string, unknown>) {\n return {\n title: typeof data.title === \"string\" ? data.title : undefined,\n order: typeof data.order === \"number\"\n ? data.order\n : data.order === null\n ? null\n : undefined,\n type: typeof data.type === \"string\" ? data.type : undefined,\n tags: normalizeTags(data.tags),\n draft: data.draft === true ? true : undefined\n };\n}\n\nfunction collectExtra(data: Record<string, unknown>): Record<string, unknown> | undefined {\n const extra: Record<string, unknown> = {};\n const reserved = new Set([\"slug\", \"title\", \"order\", \"type\", \"tags\", \"draft\"]);\n for (const [key, value] of Object.entries(data)) {\n if (reserved.has(key)) {\n continue;\n }\n extra[key] = value;\n }\n return Object.keys(extra).length ? extra : undefined;\n}\n\nfunction normalizeTags(value: unknown): string[] | undefined {\n if (!value) {\n return undefined;\n }\n if (Array.isArray(value)) {\n return value.map((entry) => String(entry));\n }\n if (typeof value === \"string\") {\n return value\n .split(\",\")\n .map((entry) => entry.trim())\n .filter(Boolean);\n }\n return undefined;\n}\n\nfunction deriveGroup(relativePath: string): string {\n const dirname = toPosix(path.dirname(relativePath));\n if (dirname === \".\" || !dirname.length) {\n return \"root\";\n }\n return dirname;\n}\n\nfunction toPosix(value: string): string {\n return value.split(path.sep).join(\"/\");\n}\n\nfunction ensureUniqueSlugs(manifest: SnippetMeta[]) {\n const seen = new Map<string, string>();\n const duplicates = new Set<string>();\n for (const snippet of manifest) {\n if (seen.has(snippet.slug)) {\n duplicates.add(snippet.slug);\n } else {\n seen.set(snippet.slug, snippet.path);\n }\n }\n if (duplicates.size) {\n throw new DuplicateSlugError([...duplicates.values()]);\n }\n}\n","export class DuplicateSlugError extends Error {\n readonly duplicates: string[];\n\n constructor(duplicates: string[]) {\n super(`Duplicate slugs detected: ${duplicates.join(\", \")}`);\n this.name = \"DuplicateSlugError\";\n this.duplicates = duplicates;\n }\n}\n","import path from \"node:path\";\nimport chokidar from \"chokidar\";\nimport { buildManifestFile, type BuildResult } from \"./manifest.js\";\nimport { logEvent } from \"./logger.js\";\n\nexport async function watch(sourceDir: string, outputPath?: string) {\n const cwd = path.resolve(sourceDir);\n logEvent(\"info\", \"watch.start\", {\n directory: cwd,\n outputPath: outputPath ?? path.join(cwd, \"snippets-index.json\")\n });\n await rebuild(cwd, outputPath);\n\n const watcher = chokidar.watch([\"**/*.md\"], {\n cwd,\n ignoreInitial: true,\n awaitWriteFinish: {\n stabilityThreshold: 200,\n pollInterval: 50\n }\n });\n\n const schedule = debounce(async () => {\n await rebuild(cwd, outputPath);\n }, 150);\n\n watcher.on(\"all\", (event, filePath) => {\n logEvent(\"info\", \"watch.change\", { event, file: filePath });\n schedule();\n });\n}\n\nasync function rebuild(sourceDir: string, outputPath?: string): Promise<BuildResult | void> {\n try {\n const result = await buildManifestFile({ sourceDir, outputPath });\n logEvent(\"info\", \"manifest.updated\", {\n outputPath: result.outputPath,\n snippetCount: result.manifest.length\n });\n return result;\n } catch (error) {\n const err = error as Error;\n logEvent(\"error\", \"manifest.update_failed\", {\n message: err.message,\n stack: err.stack\n });\n }\n}\n\nfunction debounce<T extends (...args: unknown[]) => Promise<unknown> | void>(\n fn: T,\n delay: number\n) {\n let timer: NodeJS.Timeout | null = null;\n return (...args: Parameters<T>) => {\n if (timer) {\n clearTimeout(timer);\n }\n timer = setTimeout(() => {\n timer = null;\n void fn(...args);\n }, delay);\n };\n}\n","export const brand = \"mark↓\";\n\nexport type LogLevel = \"info\" | \"warn\" | \"error\";\n\nexport interface LogFields {\n message?: string;\n [key: string]: unknown;\n}\n\nexport function logEvent(level: LogLevel, event: string, fields: LogFields = {}) {\n const entry = {\n brand,\n level,\n event,\n timestamp: new Date().toISOString(),\n ...fields\n };\n const output = `${JSON.stringify(entry)}\\n`;\n const stream = level === \"error\" ? process.stderr : process.stdout;\n stream.write(output);\n}\n\nexport function log(message: string, fields?: LogFields) {\n if (fields) {\n logEvent(\"info\", message, fields);\n return;\n }\n logEvent(\"info\", \"message\", { message });\n}\n\nexport function logError(message: string, fields?: LogFields) {\n if (fields) {\n logEvent(\"error\", message, fields);\n return;\n }\n logEvent(\"error\", \"message\", { message });\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { load as loadHtml } from \"cheerio\";\nimport { parseFrontMatter, renderMarkdown, type SnippetMeta } from \"@mzebley/mark-down\";\nimport { logEvent } from \"./logger.js\";\n\nexport interface CompilePageOptions {\n manifest?: string;\n outDir?: string;\n inPlace?: boolean;\n}\n\nconst DEFAULT_OUT_DIR = \"dist\";\n\nexport async function compilePage(inputHtml: string, options: CompilePageOptions = {}): Promise<string> {\n const sourcePath = path.resolve(inputHtml);\n await assertExists(sourcePath, `Input HTML file not found at '${inputHtml}'.`);\n\n const manifestPath = await resolveManifestPath(sourcePath, options.manifest);\n const manifestDir = path.dirname(manifestPath);\n const manifest = await loadManifest(manifestPath);\n\n const rawHtml = await fs.readFile(sourcePath, \"utf8\");\n const doctypeMatch = rawHtml.match(/^(<!doctype[^>]*>\\s*)/i);\n const doctype = doctypeMatch?.[1] ?? \"\";\n const dom = loadHtml(rawHtml, { decodeEntities: false });\n\n const targets = dom(\"[data-snippet]\").toArray();\n for (const node of targets) {\n const element = dom(node);\n const slug = element.attr(\"data-snippet\");\n if (!slug) {\n continue;\n }\n const entry = manifest.find((item) => item.slug === slug);\n if (!entry) {\n console.warn(`mark↓: no snippet found for \"${slug}\"`);\n continue;\n }\n\n const snippetPath = path.resolve(manifestDir, entry.path);\n let raw: string;\n try {\n raw = await fs.readFile(snippetPath, \"utf8\");\n } catch (error) {\n console.warn(`mark↓: failed to read snippet at '${entry.path}'`, error);\n continue;\n }\n\n let body = raw;\n let frontMatterSlug: string | undefined;\n try {\n const frontMatter = parseFrontMatter(raw);\n body = frontMatter.content;\n frontMatterSlug = frontMatter.slug;\n } catch (error) {\n console.warn(`mark↓: failed to parse front matter for '${entry.path}'`, error);\n }\n\n const html = renderMarkdown(body);\n element.html(html);\n\n if (!element.attr(\"id\")) {\n element.attr(\"id\", frontMatterSlug ?? `snippet-${slug}`);\n }\n }\n\n const outputDir = options.inPlace ? path.dirname(sourcePath) : path.resolve(options.outDir ?? DEFAULT_OUT_DIR);\n if (!options.inPlace) {\n await fs.mkdir(outputDir, { recursive: true });\n }\n const outputPath = options.inPlace\n ? sourcePath\n : path.join(outputDir, path.basename(sourcePath));\n\n const outputHtml = `${doctype}${dom.html() ?? \"\"}`;\n await fs.writeFile(outputPath, outputHtml);\n\n logEvent(\"info\", \"compile_page.written\", { outputPath });\n return outputPath;\n}\n\nasync function resolveManifestPath(inputHtml: string, manifestFlag?: string): Promise<string> {\n const manifestPath = manifestFlag\n ? path.resolve(manifestFlag)\n : path.join(path.dirname(path.resolve(inputHtml)), \"snippets-index.json\");\n await assertExists(manifestPath, `Manifest file not found at '${manifestPath}'.`);\n return manifestPath;\n}\n\nasync function loadManifest(manifestPath: string): Promise<SnippetMeta[]> {\n let raw: string;\n try {\n raw = await fs.readFile(manifestPath, \"utf8\");\n } catch (error) {\n throw new Error(`Failed to read manifest at '${manifestPath}': ${String(error)}`);\n }\n\n try {\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n throw new Error(\"Manifest must be a JSON array.\");\n }\n return parsed as SnippetMeta[];\n } catch (error) {\n throw new Error(`Failed to parse manifest at '${manifestPath}': ${String(error)}`);\n }\n}\n\nasync function assertExists(target: string, message: string) {\n try {\n await fs.access(target);\n } catch {\n throw new Error(message);\n }\n}\n"],"mappings":"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","names":["path","path","fs","path","path","fs"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/manifest.ts","../src/errors.ts","../src/watch.ts","../src/logger.ts","../src/compile-page.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { Command } from \"commander\";\nimport { buildManifestFile } from \"./manifest.js\";\nimport { watch as watchSnippets } from \"./watch.js\";\nimport { brand, logEvent } from \"./logger.js\";\nimport { DuplicateSlugError } from \"./errors.js\";\nimport { compilePage } from \"./compile-page.js\";\nimport type { SanitizeOptions, SanitizePolicy } from \"@mzebley/mark-down\";\n\nconst program = new Command();\nprogram\n .name(\"mark-down\")\n .description(`${brand} CLI for building snippet manifests`)\n .version(\"1.2.1\");\n\nprogram\n .command(\"build\")\n .argument(\"[sourceDir]\", \"directory containing snippets\", \"content/snippets\")\n .option(\"-o, --output <path>\", \"where to write snippets-index.json\")\n .action(async (sourceDir: string, options: { output?: string }) => {\n try {\n const result = await buildManifestFile({\n sourceDir,\n outputPath: options.output,\n });\n logEvent(\"info\", \"manifest.written\", {\n outputPath: result.outputPath,\n snippetCount: result.manifest.length,\n });\n } catch (error) {\n handleError(error);\n }\n });\n\nprogram\n .command(\"watch\")\n .argument(\"[sourceDir]\", \"directory containing snippets\", \"content/snippets\")\n .option(\"-o, --output <path>\", \"where to write snippets-index.json\")\n .action(async (sourceDir: string, options: { output?: string }) => {\n try {\n await watchSnippets(sourceDir, options.output);\n } catch (error) {\n handleError(error);\n }\n });\n\nprogram\n .command(\"compile-page\")\n .argument(\"<inputHtml>\", \"HTML file containing data-snippet placeholders\")\n .option(\"--manifest <path>\", \"path to snippets-index.json\")\n .option(\"--outDir <path>\", \"output directory for compiled HTML\", \"dist\")\n .option(\n \"--inPlace\",\n \"overwrite the input HTML file instead of writing to outDir\",\n )\n .option(\n \"--sanitize [policy]\",\n \"sanitize rendered HTML (default|strict|permissive)\",\n )\n .action(\n async (\n inputHtml: string,\n options: {\n manifest?: string;\n outDir?: string;\n inPlace?: boolean;\n sanitize?: string | boolean;\n },\n ) => {\n try {\n await compilePage(inputHtml, {\n manifest: options.manifest,\n outDir: options.outDir,\n inPlace: options.inPlace,\n sanitize: resolveSanitizeOption(options.sanitize),\n });\n } catch (error) {\n handleError(error);\n }\n },\n );\n\nprogram.parseAsync(process.argv).catch(handleError);\n\nfunction handleError(error: unknown) {\n const err = error as Error;\n if (err instanceof DuplicateSlugError) {\n logEvent(\"error\", \"manifest.duplicate_slug\", {\n message: err.message,\n slugs: err.duplicates,\n });\n process.exit(2);\n }\n logEvent(\"error\", \"cli.error\", {\n message: err.message,\n stack: err.stack,\n });\n process.exit(1);\n}\n\nfunction resolveSanitizeOption(\n value?: string | boolean,\n): SanitizeOptions | undefined {\n if (!value) {\n return undefined;\n }\n if (value === true) {\n return { policy: \"default\" };\n }\n const policy = value.trim();\n if (!policy) {\n return { policy: \"default\" };\n }\n if (!isSanitizePolicy(policy)) {\n throw new Error(`Unknown sanitize policy '${policy}'.`);\n }\n return { policy };\n}\n\nfunction isSanitizePolicy(value: string): value is SanitizePolicy {\n return value === \"default\" || value === \"strict\" || value === \"permissive\";\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport fg from \"fast-glob\";\nimport matter from \"gray-matter\";\nimport YAML from \"yaml\";\nimport { normalizeSlug, type SnippetMeta } from \"@mzebley/mark-down\";\nimport { DuplicateSlugError } from \"./errors.js\";\n\nconst MATTER_OPTIONS = {\n engines: {\n yaml: (source: string) => YAML.parse(source) ?? {},\n },\n};\n\nexport interface BuildOptions {\n sourceDir: string;\n outputPath?: string;\n}\n\nexport interface BuildResult {\n manifest: SnippetMeta[];\n outputPath: string;\n}\n\nexport async function buildManifestFile(\n options: BuildOptions,\n): Promise<BuildResult> {\n const manifest = await buildManifest(options.sourceDir);\n const target =\n options.outputPath ?? path.join(options.sourceDir, \"snippets-index.json\");\n await fs.writeFile(target, JSON.stringify(manifest, null, 2));\n return { manifest, outputPath: target };\n}\n\nexport async function buildManifest(sourceDir: string): Promise<SnippetMeta[]> {\n const cwd = path.resolve(sourceDir);\n const files = await fg([\"**/*.md\"], { cwd, absolute: true });\n const manifest: SnippetMeta[] = [];\n\n for (const absolutePath of files) {\n const relativePath = path.relative(cwd, absolutePath);\n const normalizedPath = toPosix(relativePath);\n try {\n const content = await fs.readFile(absolutePath, \"utf8\");\n const parsed = matter(content, MATTER_OPTIONS);\n const snippet = createSnippet(normalizedPath, parsed.data ?? {});\n if (snippet.draft) {\n continue;\n }\n manifest.push(snippet);\n } catch (error) {\n console.warn(\n `mark↓: failed to load snippet at '${normalizedPath}'`,\n error,\n );\n }\n }\n\n ensureUniqueSlugs(manifest);\n\n manifest.sort((a, b) => {\n const orderA =\n typeof a.order === \"number\" ? a.order : Number.POSITIVE_INFINITY;\n const orderB =\n typeof b.order === \"number\" ? b.order : Number.POSITIVE_INFINITY;\n if (orderA !== orderB) {\n return orderA - orderB;\n }\n const titleA = a.title?.toLowerCase() ?? \"\";\n const titleB = b.title?.toLowerCase() ?? \"\";\n return titleA.localeCompare(titleB);\n });\n\n return manifest;\n}\n\nexport function createSnippet(\n relativePath: string,\n frontMatter: Record<string, unknown>,\n): SnippetMeta {\n const group = deriveGroup(relativePath);\n const slugSource =\n typeof frontMatter.slug === \"string\" && frontMatter.slug.trim().length\n ? frontMatter.slug\n : relativePath.replace(/\\.md$/i, \"\");\n const slug = normalizeSlug(slugSource);\n\n const { title, order, type, tags, draft } = normalizeKnownFields(frontMatter);\n const extra = collectExtra(frontMatter);\n\n return {\n slug,\n title,\n order,\n type,\n tags,\n draft,\n path: relativePath,\n group,\n extra,\n };\n}\n\nfunction normalizeKnownFields(data: Record<string, unknown>) {\n return {\n title: typeof data.title === \"string\" ? data.title : undefined,\n order:\n typeof data.order === \"number\"\n ? data.order\n : data.order === null\n ? null\n : undefined,\n type: typeof data.type === \"string\" ? data.type : undefined,\n tags: normalizeTags(data.tags),\n draft: data.draft === true ? true : undefined,\n };\n}\n\nfunction collectExtra(\n data: Record<string, unknown>,\n): Record<string, unknown> | undefined {\n const extra: Record<string, unknown> = {};\n const reserved = new Set([\"slug\", \"title\", \"order\", \"type\", \"tags\", \"draft\"]);\n for (const [key, value] of Object.entries(data)) {\n if (reserved.has(key)) {\n continue;\n }\n extra[key] = value;\n }\n return Object.keys(extra).length ? extra : undefined;\n}\n\nfunction normalizeTags(value: unknown): string[] | undefined {\n if (!value) {\n return undefined;\n }\n if (Array.isArray(value)) {\n return value.map((entry) => String(entry));\n }\n if (typeof value === \"string\") {\n return value\n .split(\",\")\n .map((entry) => entry.trim())\n .filter(Boolean);\n }\n return undefined;\n}\n\nfunction deriveGroup(relativePath: string): string {\n const dirname = toPosix(path.dirname(relativePath));\n if (dirname === \".\" || !dirname.length) {\n return \"root\";\n }\n return dirname;\n}\n\nfunction toPosix(value: string): string {\n return value.split(path.sep).join(\"/\");\n}\n\nfunction ensureUniqueSlugs(manifest: SnippetMeta[]) {\n const seen = new Map<string, string>();\n const duplicates = new Set<string>();\n for (const snippet of manifest) {\n if (seen.has(snippet.slug)) {\n duplicates.add(snippet.slug);\n } else {\n seen.set(snippet.slug, snippet.path);\n }\n }\n if (duplicates.size) {\n throw new DuplicateSlugError([...duplicates.values()]);\n }\n}\n","export class DuplicateSlugError extends Error {\n readonly duplicates: string[];\n\n constructor(duplicates: string[]) {\n super(`Duplicate slugs detected: ${duplicates.join(\", \")}`);\n this.name = \"DuplicateSlugError\";\n this.duplicates = duplicates;\n }\n}\n","import path from \"node:path\";\nimport chokidar from \"chokidar\";\nimport { buildManifestFile, type BuildResult } from \"./manifest.js\";\nimport { logEvent } from \"./logger.js\";\n\nexport async function watch(sourceDir: string, outputPath?: string) {\n const cwd = path.resolve(sourceDir);\n logEvent(\"info\", \"watch.start\", {\n directory: cwd,\n outputPath: outputPath ?? path.join(cwd, \"snippets-index.json\"),\n });\n await rebuild(cwd, outputPath);\n\n const watcher = chokidar.watch([\"**/*.md\"], {\n cwd,\n ignoreInitial: true,\n awaitWriteFinish: {\n stabilityThreshold: 200,\n pollInterval: 50,\n },\n });\n\n const schedule = debounce(async () => {\n await rebuild(cwd, outputPath);\n }, 150);\n\n watcher.on(\"all\", (event, filePath) => {\n logEvent(\"info\", \"watch.change\", { event, file: filePath });\n schedule();\n });\n}\n\nasync function rebuild(\n sourceDir: string,\n outputPath?: string,\n): Promise<BuildResult | void> {\n try {\n const result = await buildManifestFile({ sourceDir, outputPath });\n logEvent(\"info\", \"manifest.updated\", {\n outputPath: result.outputPath,\n snippetCount: result.manifest.length,\n });\n return result;\n } catch (error) {\n const err = error as Error;\n logEvent(\"error\", \"manifest.update_failed\", {\n message: err.message,\n stack: err.stack,\n });\n }\n}\n\nfunction debounce<T extends (...args: unknown[]) => Promise<unknown> | void>(\n fn: T,\n delay: number,\n) {\n let timer: NodeJS.Timeout | null = null;\n return (...args: Parameters<T>) => {\n if (timer) {\n clearTimeout(timer);\n }\n timer = setTimeout(() => {\n timer = null;\n void fn(...args);\n }, delay);\n };\n}\n","export const brand = \"mark↓\";\n\nexport type LogLevel = \"info\" | \"warn\" | \"error\";\n\nexport interface LogFields {\n message?: string;\n [key: string]: unknown;\n}\n\nexport function logEvent(\n level: LogLevel,\n event: string,\n fields: LogFields = {},\n) {\n const entry = {\n brand,\n level,\n event,\n timestamp: new Date().toISOString(),\n ...fields,\n };\n const output = `${JSON.stringify(entry)}\\n`;\n const stream = level === \"error\" ? process.stderr : process.stdout;\n stream.write(output);\n}\n\nexport function log(message: string, fields?: LogFields) {\n if (fields) {\n logEvent(\"info\", message, fields);\n return;\n }\n logEvent(\"info\", \"message\", { message });\n}\n\nexport function logError(message: string, fields?: LogFields) {\n if (fields) {\n logEvent(\"error\", message, fields);\n return;\n }\n logEvent(\"error\", \"message\", { message });\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { load as loadHtml } from \"cheerio\";\nimport {\n parseFrontMatter,\n renderMarkdown,\n sanitizeMarkup,\n type SanitizeOptions,\n type SnippetMeta,\n} from \"@mzebley/mark-down\";\nimport { logEvent } from \"./logger.js\";\n\nexport interface CompilePageOptions {\n manifest?: string;\n outDir?: string;\n inPlace?: boolean;\n sanitize?: SanitizeOptions;\n}\n\nconst DEFAULT_OUT_DIR = \"dist\";\n\nexport async function compilePage(\n inputHtml: string,\n options: CompilePageOptions = {},\n): Promise<string> {\n const sourcePath = path.resolve(inputHtml);\n await assertExists(\n sourcePath,\n `Input HTML file not found at '${inputHtml}'.`,\n );\n\n const manifestPath = await resolveManifestPath(sourcePath, options.manifest);\n const manifestDir = path.dirname(manifestPath);\n const manifest = await loadManifest(manifestPath);\n\n const rawHtml = await fs.readFile(sourcePath, \"utf8\");\n const doctypeMatch = rawHtml.match(/^(<!doctype[^>]*>\\s*)/i);\n const doctype = doctypeMatch?.[1] ?? \"\";\n const dom = loadHtml(rawHtml, { decodeEntities: false });\n\n const targets = dom(\"[data-snippet]\").toArray();\n for (const node of targets) {\n const element = dom(node);\n const slug = element.attr(\"data-snippet\");\n if (!slug) {\n continue;\n }\n const entry = manifest.find((item) => item.slug === slug);\n if (!entry) {\n console.warn(`mark↓: no snippet found for \"${slug}\"`);\n continue;\n }\n\n const snippetPath = path.resolve(manifestDir, entry.path);\n let raw: string;\n try {\n raw = await fs.readFile(snippetPath, \"utf8\");\n } catch (error) {\n console.warn(`mark↓: failed to read snippet at '${entry.path}'`, error);\n continue;\n }\n\n let body = raw;\n let frontMatterSlug: string | undefined;\n try {\n const frontMatter = parseFrontMatter(raw);\n body = frontMatter.content;\n frontMatterSlug = frontMatter.slug;\n } catch (error) {\n console.warn(\n `mark↓: failed to parse front matter for '${entry.path}'`,\n error,\n );\n }\n\n let html = renderMarkdown(body);\n if (options.sanitize) {\n html = sanitizeMarkup(html, options.sanitize);\n }\n element.html(html);\n\n if (!element.attr(\"id\")) {\n element.attr(\"id\", frontMatterSlug ?? `snippet-${slug}`);\n }\n }\n\n const outputDir = options.inPlace\n ? path.dirname(sourcePath)\n : path.resolve(options.outDir ?? DEFAULT_OUT_DIR);\n if (!options.inPlace) {\n await fs.mkdir(outputDir, { recursive: true });\n }\n const outputPath = options.inPlace\n ? sourcePath\n : path.join(outputDir, path.basename(sourcePath));\n\n const outputHtml = `${doctype}${dom.html() ?? \"\"}`;\n await fs.writeFile(outputPath, outputHtml);\n\n logEvent(\"info\", \"compile_page.written\", { outputPath });\n return outputPath;\n}\n\nasync function resolveManifestPath(\n inputHtml: string,\n manifestFlag?: string,\n): Promise<string> {\n const manifestPath = manifestFlag\n ? path.resolve(manifestFlag)\n : path.join(path.dirname(path.resolve(inputHtml)), \"snippets-index.json\");\n await assertExists(\n manifestPath,\n `Manifest file not found at '${manifestPath}'.`,\n );\n return manifestPath;\n}\n\nasync function loadManifest(manifestPath: string): Promise<SnippetMeta[]> {\n let raw: string;\n try {\n raw = await fs.readFile(manifestPath, \"utf8\");\n } catch (error) {\n throw new Error(\n `Failed to read manifest at '${manifestPath}': ${String(error)}`,\n );\n }\n\n try {\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n throw new Error(\"Manifest must be a JSON array.\");\n }\n return parsed as SnippetMeta[];\n } catch (error) {\n throw new Error(\n `Failed to parse manifest at '${manifestPath}': ${String(error)}`,\n );\n }\n}\n\nasync function assertExists(target: string, message: string) {\n try {\n await fs.access(target);\n } catch {\n throw new Error(message);\n }\n}\n"],"mappings":"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","names":["path","path","fs","path","path","fs"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mzebley/mark-down-cli",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "mark↓ CLI for building snippet manifests",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,6 +9,9 @@
9
9
  "main": "dist/index.mjs",
10
10
  "module": "dist/index.mjs",
11
11
  "types": "dist/index.d.ts",
12
+ "files": [
13
+ "dist"
14
+ ],
12
15
  "publishConfig": {
13
16
  "access": "public"
14
17
  },
@@ -17,7 +20,7 @@
17
20
  "dev": "tsup --config tsup.config.ts --watch"
18
21
  },
19
22
  "dependencies": {
20
- "@mzebley/mark-down": "^1.2.1",
23
+ "@mzebley/mark-down": "^1.2.2",
21
24
  "cheerio": "^1.0.0",
22
25
  "chokidar": "^3.6.0",
23
26
  "commander": "^11.1.0",
@@ -1,116 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { load as loadHtml } from "cheerio";
4
- import { parseFrontMatter, renderMarkdown, type SnippetMeta } from "@mzebley/mark-down";
5
- import { logEvent } from "./logger.js";
6
-
7
- export interface CompilePageOptions {
8
- manifest?: string;
9
- outDir?: string;
10
- inPlace?: boolean;
11
- }
12
-
13
- const DEFAULT_OUT_DIR = "dist";
14
-
15
- export async function compilePage(inputHtml: string, options: CompilePageOptions = {}): Promise<string> {
16
- const sourcePath = path.resolve(inputHtml);
17
- await assertExists(sourcePath, `Input HTML file not found at '${inputHtml}'.`);
18
-
19
- const manifestPath = await resolveManifestPath(sourcePath, options.manifest);
20
- const manifestDir = path.dirname(manifestPath);
21
- const manifest = await loadManifest(manifestPath);
22
-
23
- const rawHtml = await fs.readFile(sourcePath, "utf8");
24
- const doctypeMatch = rawHtml.match(/^(<!doctype[^>]*>\s*)/i);
25
- const doctype = doctypeMatch?.[1] ?? "";
26
- const dom = loadHtml(rawHtml, { decodeEntities: false });
27
-
28
- const targets = dom("[data-snippet]").toArray();
29
- for (const node of targets) {
30
- const element = dom(node);
31
- const slug = element.attr("data-snippet");
32
- if (!slug) {
33
- continue;
34
- }
35
- const entry = manifest.find((item) => item.slug === slug);
36
- if (!entry) {
37
- console.warn(`mark↓: no snippet found for "${slug}"`);
38
- continue;
39
- }
40
-
41
- const snippetPath = path.resolve(manifestDir, entry.path);
42
- let raw: string;
43
- try {
44
- raw = await fs.readFile(snippetPath, "utf8");
45
- } catch (error) {
46
- console.warn(`mark↓: failed to read snippet at '${entry.path}'`, error);
47
- continue;
48
- }
49
-
50
- let body = raw;
51
- let frontMatterSlug: string | undefined;
52
- try {
53
- const frontMatter = parseFrontMatter(raw);
54
- body = frontMatter.content;
55
- frontMatterSlug = frontMatter.slug;
56
- } catch (error) {
57
- console.warn(`mark↓: failed to parse front matter for '${entry.path}'`, error);
58
- }
59
-
60
- const html = renderMarkdown(body);
61
- element.html(html);
62
-
63
- if (!element.attr("id")) {
64
- element.attr("id", frontMatterSlug ?? `snippet-${slug}`);
65
- }
66
- }
67
-
68
- const outputDir = options.inPlace ? path.dirname(sourcePath) : path.resolve(options.outDir ?? DEFAULT_OUT_DIR);
69
- if (!options.inPlace) {
70
- await fs.mkdir(outputDir, { recursive: true });
71
- }
72
- const outputPath = options.inPlace
73
- ? sourcePath
74
- : path.join(outputDir, path.basename(sourcePath));
75
-
76
- const outputHtml = `${doctype}${dom.html() ?? ""}`;
77
- await fs.writeFile(outputPath, outputHtml);
78
-
79
- logEvent("info", "compile_page.written", { outputPath });
80
- return outputPath;
81
- }
82
-
83
- async function resolveManifestPath(inputHtml: string, manifestFlag?: string): Promise<string> {
84
- const manifestPath = manifestFlag
85
- ? path.resolve(manifestFlag)
86
- : path.join(path.dirname(path.resolve(inputHtml)), "snippets-index.json");
87
- await assertExists(manifestPath, `Manifest file not found at '${manifestPath}'.`);
88
- return manifestPath;
89
- }
90
-
91
- async function loadManifest(manifestPath: string): Promise<SnippetMeta[]> {
92
- let raw: string;
93
- try {
94
- raw = await fs.readFile(manifestPath, "utf8");
95
- } catch (error) {
96
- throw new Error(`Failed to read manifest at '${manifestPath}': ${String(error)}`);
97
- }
98
-
99
- try {
100
- const parsed = JSON.parse(raw);
101
- if (!Array.isArray(parsed)) {
102
- throw new Error("Manifest must be a JSON array.");
103
- }
104
- return parsed as SnippetMeta[];
105
- } catch (error) {
106
- throw new Error(`Failed to parse manifest at '${manifestPath}': ${String(error)}`);
107
- }
108
- }
109
-
110
- async function assertExists(target: string, message: string) {
111
- try {
112
- await fs.access(target);
113
- } catch {
114
- throw new Error(message);
115
- }
116
- }
package/src/errors.ts DELETED
@@ -1,9 +0,0 @@
1
- export class DuplicateSlugError extends Error {
2
- readonly duplicates: string[];
3
-
4
- constructor(duplicates: string[]) {
5
- super(`Duplicate slugs detected: ${duplicates.join(", ")}`);
6
- this.name = "DuplicateSlugError";
7
- this.duplicates = duplicates;
8
- }
9
- }
package/src/index.ts DELETED
@@ -1,77 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Command } from "commander";
3
- import { buildManifestFile } from "./manifest.js";
4
- import { watch as watchSnippets } from "./watch.js";
5
- import { brand, logEvent } from "./logger.js";
6
- import { DuplicateSlugError } from "./errors.js";
7
- import { compilePage } from "./compile-page.js";
8
-
9
- const program = new Command();
10
- program
11
- .name("mark-down")
12
- .description(`${brand} CLI for building snippet manifests`)
13
- .version("1.2.1");
14
-
15
- program
16
- .command("build")
17
- .argument("[sourceDir]", "directory containing snippets", "content/snippets")
18
- .option("-o, --output <path>", "where to write snippets-index.json")
19
- .action(async (sourceDir: string, options: { output?: string }) => {
20
- try {
21
- const result = await buildManifestFile({ sourceDir, outputPath: options.output });
22
- logEvent("info", "manifest.written", {
23
- outputPath: result.outputPath,
24
- snippetCount: result.manifest.length
25
- });
26
- } catch (error) {
27
- handleError(error);
28
- }
29
- });
30
-
31
- program
32
- .command("watch")
33
- .argument("[sourceDir]", "directory containing snippets", "content/snippets")
34
- .option("-o, --output <path>", "where to write snippets-index.json")
35
- .action(async (sourceDir: string, options: { output?: string }) => {
36
- try {
37
- await watchSnippets(sourceDir, options.output);
38
- } catch (error) {
39
- handleError(error);
40
- }
41
- });
42
-
43
- program
44
- .command("compile-page")
45
- .argument("<inputHtml>", "HTML file containing data-snippet placeholders")
46
- .option("--manifest <path>", "path to snippets-index.json")
47
- .option("--outDir <path>", "output directory for compiled HTML", "dist")
48
- .option("--inPlace", "overwrite the input HTML file instead of writing to outDir")
49
- .action(async (inputHtml: string, options: { manifest?: string; outDir?: string; inPlace?: boolean }) => {
50
- try {
51
- await compilePage(inputHtml, {
52
- manifest: options.manifest,
53
- outDir: options.outDir,
54
- inPlace: options.inPlace
55
- });
56
- } catch (error) {
57
- handleError(error);
58
- }
59
- });
60
-
61
- program.parseAsync(process.argv).catch(handleError);
62
-
63
- function handleError(error: unknown) {
64
- const err = error as Error;
65
- if (err instanceof DuplicateSlugError) {
66
- logEvent("error", "manifest.duplicate_slug", {
67
- message: err.message,
68
- slugs: err.duplicates
69
- });
70
- process.exit(2);
71
- }
72
- logEvent("error", "cli.error", {
73
- message: err.message,
74
- stack: err.stack
75
- });
76
- process.exit(1);
77
- }
package/src/logger.ts DELETED
@@ -1,37 +0,0 @@
1
- export const brand = "mark↓";
2
-
3
- export type LogLevel = "info" | "warn" | "error";
4
-
5
- export interface LogFields {
6
- message?: string;
7
- [key: string]: unknown;
8
- }
9
-
10
- export function logEvent(level: LogLevel, event: string, fields: LogFields = {}) {
11
- const entry = {
12
- brand,
13
- level,
14
- event,
15
- timestamp: new Date().toISOString(),
16
- ...fields
17
- };
18
- const output = `${JSON.stringify(entry)}\n`;
19
- const stream = level === "error" ? process.stderr : process.stdout;
20
- stream.write(output);
21
- }
22
-
23
- export function log(message: string, fields?: LogFields) {
24
- if (fields) {
25
- logEvent("info", message, fields);
26
- return;
27
- }
28
- logEvent("info", "message", { message });
29
- }
30
-
31
- export function logError(message: string, fields?: LogFields) {
32
- if (fields) {
33
- logEvent("error", message, fields);
34
- return;
35
- }
36
- logEvent("error", "message", { message });
37
- }
package/src/manifest.ts DELETED
@@ -1,155 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import fg from "fast-glob";
4
- import matter from "gray-matter";
5
- import YAML from "yaml";
6
- import { normalizeSlug, type SnippetMeta } from "@mzebley/mark-down";
7
- import { DuplicateSlugError } from "./errors.js";
8
-
9
- const MATTER_OPTIONS = {
10
- engines: {
11
- yaml: (source: string) => YAML.parse(source) ?? {}
12
- }
13
- };
14
-
15
- export interface BuildOptions {
16
- sourceDir: string;
17
- outputPath?: string;
18
- }
19
-
20
- export interface BuildResult {
21
- manifest: SnippetMeta[];
22
- outputPath: string;
23
- }
24
-
25
- export async function buildManifestFile(options: BuildOptions): Promise<BuildResult> {
26
- const manifest = await buildManifest(options.sourceDir);
27
- const target = options.outputPath ?? path.join(options.sourceDir, "snippets-index.json");
28
- await fs.writeFile(target, JSON.stringify(manifest, null, 2));
29
- return { manifest, outputPath: target };
30
- }
31
-
32
- export async function buildManifest(sourceDir: string): Promise<SnippetMeta[]> {
33
- const cwd = path.resolve(sourceDir);
34
- const files = await fg(["**/*.md"], { cwd, absolute: true });
35
- const manifest: SnippetMeta[] = [];
36
-
37
- for (const absolutePath of files) {
38
- const relativePath = path.relative(cwd, absolutePath);
39
- const normalizedPath = toPosix(relativePath);
40
- const content = await fs.readFile(absolutePath, "utf8");
41
- const parsed = matter(content, MATTER_OPTIONS);
42
- const snippet = createSnippet(normalizedPath, parsed.data ?? {});
43
- if (snippet.draft) {
44
- continue;
45
- }
46
- manifest.push(snippet);
47
- }
48
-
49
- ensureUniqueSlugs(manifest);
50
-
51
- manifest.sort((a, b) => {
52
- const orderA = typeof a.order === "number" ? a.order : Number.POSITIVE_INFINITY;
53
- const orderB = typeof b.order === "number" ? b.order : Number.POSITIVE_INFINITY;
54
- if (orderA !== orderB) {
55
- return orderA - orderB;
56
- }
57
- const titleA = a.title?.toLowerCase() ?? "";
58
- const titleB = b.title?.toLowerCase() ?? "";
59
- return titleA.localeCompare(titleB);
60
- });
61
-
62
- return manifest;
63
- }
64
-
65
- export function createSnippet(relativePath: string, frontMatter: Record<string, unknown>): SnippetMeta {
66
- const group = deriveGroup(relativePath);
67
- const slugSource = typeof frontMatter.slug === "string" && frontMatter.slug.trim().length
68
- ? frontMatter.slug
69
- : relativePath.replace(/\.md$/i, "");
70
- const slug = normalizeSlug(slugSource);
71
-
72
- const { title, order, type, tags, draft } = normalizeKnownFields(frontMatter);
73
- const extra = collectExtra(frontMatter);
74
-
75
- return {
76
- slug,
77
- title,
78
- order,
79
- type,
80
- tags,
81
- draft,
82
- path: relativePath,
83
- group,
84
- extra
85
- };
86
- }
87
-
88
- function normalizeKnownFields(data: Record<string, unknown>) {
89
- return {
90
- title: typeof data.title === "string" ? data.title : undefined,
91
- order: typeof data.order === "number"
92
- ? data.order
93
- : data.order === null
94
- ? null
95
- : undefined,
96
- type: typeof data.type === "string" ? data.type : undefined,
97
- tags: normalizeTags(data.tags),
98
- draft: data.draft === true ? true : undefined
99
- };
100
- }
101
-
102
- function collectExtra(data: Record<string, unknown>): Record<string, unknown> | undefined {
103
- const extra: Record<string, unknown> = {};
104
- const reserved = new Set(["slug", "title", "order", "type", "tags", "draft"]);
105
- for (const [key, value] of Object.entries(data)) {
106
- if (reserved.has(key)) {
107
- continue;
108
- }
109
- extra[key] = value;
110
- }
111
- return Object.keys(extra).length ? extra : undefined;
112
- }
113
-
114
- function normalizeTags(value: unknown): string[] | undefined {
115
- if (!value) {
116
- return undefined;
117
- }
118
- if (Array.isArray(value)) {
119
- return value.map((entry) => String(entry));
120
- }
121
- if (typeof value === "string") {
122
- return value
123
- .split(",")
124
- .map((entry) => entry.trim())
125
- .filter(Boolean);
126
- }
127
- return undefined;
128
- }
129
-
130
- function deriveGroup(relativePath: string): string {
131
- const dirname = toPosix(path.dirname(relativePath));
132
- if (dirname === "." || !dirname.length) {
133
- return "root";
134
- }
135
- return dirname;
136
- }
137
-
138
- function toPosix(value: string): string {
139
- return value.split(path.sep).join("/");
140
- }
141
-
142
- function ensureUniqueSlugs(manifest: SnippetMeta[]) {
143
- const seen = new Map<string, string>();
144
- const duplicates = new Set<string>();
145
- for (const snippet of manifest) {
146
- if (seen.has(snippet.slug)) {
147
- duplicates.add(snippet.slug);
148
- } else {
149
- seen.set(snippet.slug, snippet.path);
150
- }
151
- }
152
- if (duplicates.size) {
153
- throw new DuplicateSlugError([...duplicates.values()]);
154
- }
155
- }
package/src/watch.ts DELETED
@@ -1,64 +0,0 @@
1
- import path from "node:path";
2
- import chokidar from "chokidar";
3
- import { buildManifestFile, type BuildResult } from "./manifest.js";
4
- import { logEvent } from "./logger.js";
5
-
6
- export async function watch(sourceDir: string, outputPath?: string) {
7
- const cwd = path.resolve(sourceDir);
8
- logEvent("info", "watch.start", {
9
- directory: cwd,
10
- outputPath: outputPath ?? path.join(cwd, "snippets-index.json")
11
- });
12
- await rebuild(cwd, outputPath);
13
-
14
- const watcher = chokidar.watch(["**/*.md"], {
15
- cwd,
16
- ignoreInitial: true,
17
- awaitWriteFinish: {
18
- stabilityThreshold: 200,
19
- pollInterval: 50
20
- }
21
- });
22
-
23
- const schedule = debounce(async () => {
24
- await rebuild(cwd, outputPath);
25
- }, 150);
26
-
27
- watcher.on("all", (event, filePath) => {
28
- logEvent("info", "watch.change", { event, file: filePath });
29
- schedule();
30
- });
31
- }
32
-
33
- async function rebuild(sourceDir: string, outputPath?: string): Promise<BuildResult | void> {
34
- try {
35
- const result = await buildManifestFile({ sourceDir, outputPath });
36
- logEvent("info", "manifest.updated", {
37
- outputPath: result.outputPath,
38
- snippetCount: result.manifest.length
39
- });
40
- return result;
41
- } catch (error) {
42
- const err = error as Error;
43
- logEvent("error", "manifest.update_failed", {
44
- message: err.message,
45
- stack: err.stack
46
- });
47
- }
48
- }
49
-
50
- function debounce<T extends (...args: unknown[]) => Promise<unknown> | void>(
51
- fn: T,
52
- delay: number
53
- ) {
54
- let timer: NodeJS.Timeout | null = null;
55
- return (...args: Parameters<T>) => {
56
- if (timer) {
57
- clearTimeout(timer);
58
- }
59
- timer = setTimeout(() => {
60
- timer = null;
61
- void fn(...args);
62
- }, delay);
63
- };
64
- }
package/tsconfig.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "module": "ES2020",
6
- "moduleResolution": "node"
7
- },
8
- "include": ["src/**/*"]
9
- }
package/tsup.config.ts DELETED
@@ -1,15 +0,0 @@
1
- import { defineConfig } from "tsup";
2
-
3
- export default defineConfig({
4
- entry: ["src/index.ts"],
5
- format: ["esm", "cjs"],
6
- dts: true,
7
- sourcemap: true,
8
- splitting: false,
9
- clean: true,
10
- outExtension({ format }) {
11
- return {
12
- js: format === "esm" ? ".mjs" : ".cjs"
13
- };
14
- }
15
- });