contentbit 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/run.js CHANGED
@@ -16153,7 +16153,17 @@ import { pathToFileURL } from "node:url";
16153
16153
  async function loadRegistry(registryPath) {
16154
16154
  const registry2 = createBlockRegistry().use(genericBlocks());
16155
16155
  if (registryPath) {
16156
- const mod = await import(pathToFileURL(registryPath).href);
16156
+ let mod;
16157
+ try {
16158
+ mod = await import(pathToFileURL(registryPath).href);
16159
+ } catch (err) {
16160
+ if (err instanceof Error && "code" in err && err.code === "ERR_UNKNOWN_FILE_EXTENSION") {
16161
+ throw new Error(
16162
+ `Importing a TypeScript registry needs Node 22.18+ (native type stripping): ${registryPath}`
16163
+ );
16164
+ }
16165
+ throw err;
16166
+ }
16157
16167
  if (!Array.isArray(mod.default)) {
16158
16168
  throw new Error(
16159
16169
  `--registry module must default-export an array of block definitions: ${registryPath}`
@@ -16177,10 +16187,131 @@ __export(init_exports, {
16177
16187
  initCommand: () => initCommand
16178
16188
  });
16179
16189
  import { spawn } from "node:child_process";
16190
+ import { existsSync } from "node:fs";
16180
16191
  import { mkdir, readFile, writeFile } from "node:fs/promises";
16181
16192
  import { join } from "node:path";
16182
16193
  import { parseArgs } from "node:util";
16183
- function detectPackageManager() {
16194
+ function blockComponentsTemplate(styled) {
16195
+ const body = styled ? ` return (
16196
+ <figure className="my-6 border-s-2 ps-4">
16197
+ <blockquote className="text-lg italic">{ctx.renderMarkdown(data.markdown)}</blockquote>
16198
+ <figcaption className="text-muted-foreground mt-2 text-sm">
16199
+ \u2014 {String(node.props.author)}
16200
+ {node.props.role ? \`, \${String(node.props.role)}\` : null}
16201
+ </figcaption>
16202
+ </figure>
16203
+ )` : ` return (
16204
+ <figure style={{ margin: '1.5rem 0', borderLeft: '2px solid #d4d4d4', paddingLeft: '1rem' }}>
16205
+ <blockquote style={{ fontStyle: 'italic' }}>{ctx.renderMarkdown(data.markdown)}</blockquote>
16206
+ <figcaption style={{ marginTop: '0.5rem', fontSize: '0.875rem', opacity: 0.7 }}>
16207
+ \u2014 {String(node.props.author)}
16208
+ {node.props.role ? \`, \${String(node.props.role)}\` : null}
16209
+ </figcaption>
16210
+ </figure>
16211
+ )`;
16212
+ return `import type { BlockComponent, BlockComponentProps } from '@contentbit/react'
16213
+
16214
+ // One React component per custom block, keyed by block name. Definitions
16215
+ // live in ./registry.ts \u2014 add a block there, add its component here, and
16216
+ // the rest of the app never changes.
16217
+ function QuoteBlock({ node, ctx }: BlockComponentProps) {
16218
+ const data = node.data as { markdown: string }
16219
+ ${body}
16220
+ }
16221
+
16222
+ export const blockComponents: Record<string, BlockComponent> = {
16223
+ quote: QuoteBlock,
16224
+ }
16225
+ `;
16226
+ }
16227
+ function reactComponent(styled, mdWired, blocksImport) {
16228
+ const mdImport = mdWired ? "import ReactMarkdown from 'react-markdown'\n" : "";
16229
+ const mdProp = mdWired ? "\n renderMarkdown={(md) => <ReactMarkdown>{md}</ReactMarkdown>}" : `
16230
+ // TODO: plug your Markdown library in here, e.g. react-markdown.
16231
+ // One function renders all prose: https://contentbit.dev/docs/guides/markdown
16232
+ // renderMarkdown={(md) => <Markdown source={md} />}`;
16233
+ const rendererImport = styled ? `
16234
+ // The styled pack installed by shadcn. Yours to edit.
16235
+ import { ContentRenderer } from '@/components/content-blocks/content-renderer'` : "";
16236
+ const renderer = styled ? "ContentRenderer" : "ContentBlocks";
16237
+ const reactImport = styled ? "" : "import { ContentBlocks } from '@contentbit/react'\n";
16238
+ return `'use client'
16239
+
16240
+ import { genericBlocks } from '@contentbit/blocks'
16241
+ import { createBlockRegistry, parseDocument, validateDocument } from '@contentbit/core'
16242
+ ${reactImport}${mdImport}${rendererImport}
16243
+ // Everything block-related lives in the blocks/ folder: definitions in
16244
+ // registry.ts (shared with the validate CLI), components in components.tsx.
16245
+ import customBlocks from '${blocksImport}/registry'
16246
+ import { blockComponents } from '${blocksImport}/components'
16247
+
16248
+ const registry = createBlockRegistry().use(genericBlocks()).use(customBlocks)
16249
+
16250
+ export function Content({ source }: { source: string }) {
16251
+ const result = validateDocument(parseDocument(source), registry)
16252
+ return (
16253
+ <${renderer}
16254
+ document={result.document}
16255
+ components={blockComponents}${mdProp}
16256
+ />
16257
+ )
16258
+ }
16259
+ `;
16260
+ }
16261
+ function htmlRenderScript(md) {
16262
+ const wiring = md === "marked" ? `import { marked } from 'marked'
16263
+
16264
+ const renderMarkdown = (md) => marked.parse(md, { async: false })` : md === "markdown-it" ? `import MarkdownIt from 'markdown-it'
16265
+
16266
+ const mdIt = new MarkdownIt() // html: false by default \u2014 raw HTML stays escaped
16267
+ const renderMarkdown = (md) => mdIt.render(md)` : `// TODO: plug a Markdown library in here (marked, markdown-it, remark).
16268
+ const renderMarkdown = undefined`;
16269
+ return `// Render content/example.md to example.html. Run: node scripts/render-example.mjs
16270
+ import { genericBlocks } from '@contentbit/blocks'
16271
+ import { createBlockRegistry, parseDocument, validateDocument } from '@contentbit/core'
16272
+ import { renderToHtml } from '@contentbit/html'
16273
+ import { readFile, writeFile } from 'node:fs/promises'
16274
+ ${wiring}
16275
+
16276
+ const source = await readFile('content/example.md', 'utf8')
16277
+ const registry = createBlockRegistry().use(genericBlocks())
16278
+ const result = validateDocument(parseDocument(source), registry)
16279
+ const html = renderToHtml(result.document, { renderMarkdown })
16280
+ await writeFile('example.html', html, 'utf8')
16281
+ console.log('wrote example.html')
16282
+ `;
16283
+ }
16284
+ function detectFramework(cwd, deps) {
16285
+ if ((deps["@tanstack/react-start"] || deps["@tanstack/react-router"]) && existsSync(join(cwd, "src/routes"))) {
16286
+ return {
16287
+ framework: "tanstack",
16288
+ componentPath: "src/components/content-blocks.tsx",
16289
+ pagePath: "src/routes/example.tsx"
16290
+ };
16291
+ }
16292
+ if (deps.next) {
16293
+ const appDir = existsSync(join(cwd, "src/app")) ? "src/app" : "app";
16294
+ if (existsSync(join(cwd, appDir))) {
16295
+ return {
16296
+ framework: "next",
16297
+ componentPath: "components/content-blocks.tsx",
16298
+ pagePath: `${appDir}/example/page.tsx`
16299
+ };
16300
+ }
16301
+ }
16302
+ return { framework: null, componentPath: "components/content-blocks.tsx", pagePath: null };
16303
+ }
16304
+ function detectPackageManager(cwd) {
16305
+ const locks = [
16306
+ ["pnpm-lock.yaml", "pnpm"],
16307
+ ["yarn.lock", "yarn"],
16308
+ ["bun.lock", "bun"],
16309
+ ["bun.lockb", "bun"],
16310
+ ["package-lock.json", "npm"]
16311
+ ];
16312
+ for (const [file2, pm] of locks) {
16313
+ if (existsSync(join(cwd, file2))) return pm;
16314
+ }
16184
16315
  const agent = process.env.npm_config_user_agent ?? "";
16185
16316
  for (const pm of ["pnpm", "yarn", "bun"]) {
16186
16317
  if (agent.startsWith(pm)) return pm;
@@ -16191,6 +16322,12 @@ function installArgs(pm, dev, pkgs) {
16191
16322
  const add = pm === "npm" ? "install" : "add";
16192
16323
  return dev ? [add, "-D", ...pkgs] : [add, ...pkgs];
16193
16324
  }
16325
+ function dlxCommand(pm) {
16326
+ if (pm === "pnpm") return ["pnpm", ["dlx"]];
16327
+ if (pm === "yarn") return ["yarn", ["dlx"]];
16328
+ if (pm === "bun") return ["bunx", []];
16329
+ return ["npx", ["--yes"]];
16330
+ }
16194
16331
  function runInstall(pm, args, cwd) {
16195
16332
  return new Promise((resolve) => {
16196
16333
  const child = spawn(pm, args, { cwd, stdio: "inherit", shell: process.platform === "win32" });
@@ -16213,9 +16350,12 @@ async function initCommand(args, io) {
16213
16350
  args,
16214
16351
  options: {
16215
16352
  target: { type: "string", short: "t" },
16353
+ md: { type: "string" },
16216
16354
  yes: { type: "boolean", short: "y", default: false },
16217
16355
  cwd: { type: "string", default: process.cwd() },
16218
- "no-install": { type: "boolean", default: false }
16356
+ "no-install": { type: "boolean", default: false },
16357
+ "no-page": { type: "boolean", default: false },
16358
+ "no-styled": { type: "boolean", default: false }
16219
16359
  }
16220
16360
  });
16221
16361
  const cwd = values.cwd;
@@ -16252,13 +16392,38 @@ async function initCommand(args, io) {
16252
16392
  } else {
16253
16393
  target = detected;
16254
16394
  }
16255
- const runtime = ["@contentbit/core", "@contentbit/blocks"];
16395
+ const choices = MD_CHOICES[target];
16396
+ let md;
16397
+ if (values.md) {
16398
+ if (!choices.includes(values.md)) {
16399
+ io.stderr(`Unknown markdown library "${values.md}". Use one of: ${choices.join(", ")}`);
16400
+ return 2;
16401
+ }
16402
+ md = values.md;
16403
+ } else if (choices.length > 1 && !values.yes && process.stdin.isTTY && process.stdout.isTTY) {
16404
+ const { isCancel, select } = await import("@clack/prompts");
16405
+ const answer = await select({
16406
+ message: "Markdown library for prose rendering?",
16407
+ initialValue: choices[0],
16408
+ options: choices.map((c) => ({
16409
+ value: c,
16410
+ label: c,
16411
+ hint: c === "none" ? "wire one yourself later" : "installed and wired for you"
16412
+ }))
16413
+ });
16414
+ if (isCancel(answer)) return 1;
16415
+ md = answer;
16416
+ } else {
16417
+ md = choices[0];
16418
+ }
16419
+ const runtime = ["@contentbit/core", "@contentbit/blocks", "zod"];
16256
16420
  if (target === "react") runtime.push("@contentbit/react");
16257
16421
  if (target === "html") runtime.push("@contentbit/html");
16422
+ if (md !== "none") runtime.push(md);
16258
16423
  if (values["no-install"]) {
16259
16424
  io.stdout(`skipped install: ${runtime.join(" ")} + contentbit (dev)`);
16260
16425
  } else {
16261
- const pm = detectPackageManager();
16426
+ const pm = detectPackageManager(cwd);
16262
16427
  io.stdout(`installing with ${pm}: ${runtime.join(" ")}`);
16263
16428
  if (await runInstall(pm, installArgs(pm, false, runtime), cwd) !== 0) {
16264
16429
  io.stderr("install failed");
@@ -16270,10 +16435,54 @@ async function initCommand(args, io) {
16270
16435
  }
16271
16436
  }
16272
16437
  const files = [
16273
- ["blocks/registry.mjs", REGISTRY_TEMPLATE],
16438
+ ["blocks/registry.ts", REGISTRY_TEMPLATE],
16274
16439
  ["content/example.md", EXAMPLE_CONTENT]
16275
16440
  ];
16276
- if (target === "react") files.push(["components/content-blocks.tsx", REACT_COMPONENT]);
16441
+ const layout = detectFramework(cwd, { ...pkg.dependencies, ...pkg.devDependencies });
16442
+ let styled = false;
16443
+ const componentsJsonPath = join(cwd, "components.json");
16444
+ if (target === "react" && !values["no-styled"] && existsSync(componentsJsonPath)) {
16445
+ const componentsJson = JSON.parse(await readFile(componentsJsonPath, "utf8"));
16446
+ componentsJson.registries ??= {};
16447
+ if (!componentsJson.registries["@contentbit"]) {
16448
+ componentsJson.registries["@contentbit"] = "https://contentbit.dev/r/{name}.json";
16449
+ await writeFile(componentsJsonPath, `${JSON.stringify(componentsJson, null, 2)}
16450
+ `, "utf8");
16451
+ io.stdout("added @contentbit registry to components.json");
16452
+ }
16453
+ if (values["no-install"]) {
16454
+ io.stdout("skipped: shadcn add @contentbit/generic-pack");
16455
+ styled = true;
16456
+ } else {
16457
+ const [bin, prefix] = dlxCommand(detectPackageManager(cwd));
16458
+ io.stdout("installing the styled pack: shadcn add @contentbit/generic-pack");
16459
+ const code = await runInstall(
16460
+ bin,
16461
+ [...prefix, "shadcn@latest", "add", "@contentbit/generic-pack", "--yes"],
16462
+ cwd
16463
+ );
16464
+ if (code === 0) styled = true;
16465
+ else io.stderr("styled pack install failed; falling back to headless defaults");
16466
+ }
16467
+ }
16468
+ if (target === "react") {
16469
+ const depth = layout.componentPath.split("/").length - 1;
16470
+ const blocksImport = `${"../".repeat(depth)}blocks`;
16471
+ files.push(["blocks/components.tsx", blockComponentsTemplate(styled)]);
16472
+ files.push([
16473
+ layout.componentPath,
16474
+ reactComponent(styled, md === "react-markdown", blocksImport)
16475
+ ]);
16476
+ if (!values["no-page"] && layout.pagePath) {
16477
+ files.push([layout.pagePath, layout.framework === "tanstack" ? TANSTACK_PAGE : NEXT_PAGE]);
16478
+ }
16479
+ }
16480
+ if (target === "html") {
16481
+ files.push([
16482
+ "scripts/render-example.mjs",
16483
+ htmlRenderScript(md)
16484
+ ]);
16485
+ }
16277
16486
  for (const [rel, content] of files) {
16278
16487
  const result = await scaffold(join(cwd, rel), content);
16279
16488
  io.stdout(`${result}: ${rel}`);
@@ -16281,57 +16490,77 @@ async function initCommand(args, io) {
16281
16490
  const fresh = JSON.parse(await readFile(pkgPath, "utf8"));
16282
16491
  fresh.scripts ??= {};
16283
16492
  if (!fresh.scripts["content:check"]) {
16284
- fresh.scripts["content:check"] = 'contentbit validate "content/**/*.md" --registry ./blocks/registry.mjs';
16493
+ fresh.scripts["content:check"] = 'contentbit validate "content/**/*.md" --registry ./blocks/registry.ts';
16285
16494
  await writeFile(pkgPath, `${JSON.stringify(fresh, null, 2)}
16286
16495
  `, "utf8");
16287
16496
  io.stdout("added script: content:check");
16288
16497
  }
16289
- const registry2 = await loadRegistry();
16498
+ let registry2;
16499
+ try {
16500
+ registry2 = await loadRegistry(join(cwd, "blocks/registry.ts"));
16501
+ } catch {
16502
+ registry2 = await loadRegistry();
16503
+ }
16290
16504
  const guide = registry2.toAuthoringGuide({ audience: "llm", includeExamples: true });
16291
16505
  await writeFile(join(cwd, "contentbit-guide.md"), guide, "utf8");
16292
16506
  io.stdout("created: contentbit-guide.md (LLM authoring instructions)");
16293
16507
  io.stdout("");
16294
16508
  io.stdout("Done. Next steps:");
16295
- io.stdout(` 1. Validate the starter content: ${detectPackageManager()} run content:check`);
16509
+ io.stdout(` 1. Validate the starter content: ${detectPackageManager(cwd)} run content:check`);
16296
16510
  if (target === "react") {
16297
- io.stdout(' 2. Render it: import { Content } from "./components/content-blocks"');
16511
+ if (!values["no-page"] && layout.pagePath) {
16512
+ io.stdout(" 2. Start the dev server and open /example to see the article rendered.");
16513
+ } else {
16514
+ io.stdout(' 2. Render it: import { Content } from "./components/content-blocks"');
16515
+ io.stdout(" <Content source={...content/example.md as a string} />");
16516
+ }
16298
16517
  io.stdout(" 3. Styled components: pnpm dlx shadcn@latest add @contentbit/generic-pack");
16299
16518
  } else if (target === "html") {
16300
- io.stdout(" 2. Render it: contentbit render content/example.md --target html");
16519
+ io.stdout(" 2. Render it: node scripts/render-example.mjs && open example.html");
16301
16520
  } else {
16302
16521
  io.stdout(" 2. Render it: contentbit render content/example.md --target markdown");
16303
16522
  }
16304
16523
  io.stdout(" Docs: https://contentbit.dev/docs");
16305
16524
  return 0;
16306
16525
  }
16307
- var TARGETS, REGISTRY_TEMPLATE, EXAMPLE_CONTENT, REACT_COMPONENT;
16526
+ var TARGETS, MD_CHOICES, REGISTRY_TEMPLATE, EXAMPLE_CONTENT, TANSTACK_PAGE, NEXT_PAGE;
16308
16527
  var init_init = __esm({
16309
16528
  "src/commands/init.ts"() {
16310
16529
  "use strict";
16311
16530
  init_load_registry();
16312
16531
  TARGETS = ["react", "html", "markdown"];
16313
- REGISTRY_TEMPLATE = `// Custom blocks for this project. The CLI and your app share this module:
16532
+ MD_CHOICES = {
16533
+ react: ["react-markdown", "none"],
16534
+ html: ["marked", "markdown-it", "none"],
16535
+ markdown: ["none"]
16536
+ };
16537
+ REGISTRY_TEMPLATE = `// Custom block definitions for this project. The CLI and your app share
16538
+ // this module \u2014 Node 22.18+ imports TypeScript directly:
16314
16539
  //
16315
- // contentbit validate "content/**/*.md" --registry ./blocks/registry.mjs
16540
+ // contentbit validate "content/**/*.md" --registry ./blocks/registry.ts
16316
16541
  //
16317
- // Define blocks with @contentbit/core and default-export them as an array.
16542
+ // Definitions stay framework-free (the CLI and every render target use
16543
+ // them); React components live next door in blocks/components.tsx.
16318
16544
  // Docs: https://contentbit.dev/docs/guides/custom-blocks
16319
- //
16320
- // import { defineBlock, pipeRows } from '@contentbit/core'
16321
- // import { z } from 'zod'
16322
- //
16323
- // const pricingTable = defineBlock({
16324
- // name: 'pricing-table',
16325
- // description: 'Compares product plans.',
16326
- // props: z.object({ currency: z.enum(['usd', 'eur']).default('usd') }),
16327
- // content: pipeRows({ columns: ['plan', 'price'], minRows: 2 }),
16328
- // authoring: {
16329
- // useWhen: ['Comparing pricing plans'],
16330
- // example: ':::pricing-table\\n- Starter | $0\\n- Pro | $12/mo\\n:::',
16331
- // },
16332
- // })
16545
+ import { defineBlock, markdownBody, type BlockDefinition } from '@contentbit/core'
16546
+ import { z } from 'zod'
16547
+
16548
+ export const quote = defineBlock({
16549
+ name: 'quote',
16550
+ description: 'A pull quote with an author.',
16551
+ props: z.object({
16552
+ author: z.string().min(1),
16553
+ role: z.string().optional(),
16554
+ }),
16555
+ content: markdownBody({ minLength: 3 }),
16556
+ authoring: {
16557
+ useWhen: ['Quoting a person to support a point'],
16558
+ avoidWhen: ['Highlighting your own remark, use callout instead'],
16559
+ example: ':::quote{author="Ada Lovelace"}\\nThe Analytical Engine weaves algebraic patterns.\\n:::',
16560
+ },
16561
+ })
16333
16562
 
16334
- export default []
16563
+ export default [quote] satisfies BlockDefinition<unknown>[]
16335
16564
  `;
16336
16565
  EXAMPLE_CONTENT = `# Hello, Content Blocks
16337
16566
 
@@ -16346,22 +16575,42 @@ Run the validate script and you will get file:line:col diagnostics.
16346
16575
  2. Run \`contentbit validate "content/**/*.md"\`.
16347
16576
  3. Render it with the target you picked at init.
16348
16577
  :::
16578
+
16579
+ This one is a **custom block**, defined in \`blocks/registry.ts\` and rendered
16580
+ by the \`QuoteBlock\` component, in about twenty lines:
16581
+
16582
+ :::quote{author="Ada Lovelace" role="Notes on the Analytical Engine, 1843"}
16583
+ The Analytical Engine weaves algebraic patterns just as the Jacquard loom
16584
+ weaves flowers and leaves.
16585
+ :::
16349
16586
  `;
16350
- REACT_COMPONENT = `import { genericBlocks } from '@contentbit/blocks'
16351
- import { createBlockRegistry, parseDocument, validateDocument } from '@contentbit/core'
16352
- import { ContentBlocks } from '@contentbit/react'
16587
+ TANSTACK_PAGE = `import { createFileRoute } from '@tanstack/react-router'
16353
16588
 
16354
- const registry = createBlockRegistry().use(genericBlocks())
16589
+ import { Content } from '../components/content-blocks'
16590
+ // Vite's ?raw import inlines the Markdown as a string at build time.
16591
+ import source from '../../content/example.md?raw'
16355
16592
 
16356
- export function Content({ source }: { source: string }) {
16357
- const result = validateDocument(parseDocument(source), registry)
16593
+ export const Route = createFileRoute('/example')({ component: ExamplePage })
16594
+
16595
+ function ExamplePage() {
16358
16596
  return (
16359
- <ContentBlocks
16360
- document={result.document}
16361
- // TODO: plug your Markdown library in here, e.g. react-markdown.
16362
- // One function renders all prose: https://contentbit.dev/docs/guides/markdown
16363
- // renderMarkdown={(md) => <Markdown source={md} />}
16364
- />
16597
+ <main style={{ maxWidth: '42rem', margin: '0 auto', padding: '3rem 1.5rem' }}>
16598
+ <Content source={source} />
16599
+ </main>
16600
+ )
16601
+ }
16602
+ `;
16603
+ NEXT_PAGE = `import { readFile } from 'node:fs/promises'
16604
+
16605
+ // If your project has no "@/" path alias, switch to a relative import.
16606
+ import { Content } from '@/components/content-blocks'
16607
+
16608
+ export default async function ExamplePage() {
16609
+ const source = await readFile('content/example.md', 'utf8')
16610
+ return (
16611
+ <main style={{ maxWidth: '42rem', margin: '0 auto', padding: '3rem 1.5rem' }}>
16612
+ <Content source={source} />
16613
+ </main>
16365
16614
  )
16366
16615
  }
16367
16616
  `;
@@ -16653,7 +16902,7 @@ var init_docs = __esm({
16653
16902
  // src/run.ts
16654
16903
  var USAGE = `Usage: contentbit <init|validate|render|instructions|docs> [options]
16655
16904
 
16656
- init [-t react|html|markdown] [-y] [--no-install]
16905
+ init [-t react|html|markdown] [--md ...] [-y] [--no-install] [--no-page]
16657
16906
 
16658
16907
  validate <globs...> [--registry <module.mjs>] [--strict-warnings]
16659
16908
  render <file> --target html|markdown [--registry <module.mjs>] [--out <file>]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contentbit",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "CLI for Content Blocks: validate, render, and generate LLM authoring instructions from your registry.",
5
5
  "keywords": [
6
6
  "cli",
@@ -38,8 +38,8 @@
38
38
  "@clack/prompts": "^1.5.1",
39
39
  "tinyglobby": "^0.2.10",
40
40
  "@contentbit/core": "0.1.0",
41
- "@contentbit/blocks": "0.1.0",
42
- "@contentbit/html": "0.1.0"
41
+ "@contentbit/html": "0.1.0",
42
+ "@contentbit/blocks": "0.1.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^25.0.9",
@@ -47,6 +47,9 @@
47
47
  "typescript": "^5.9.3",
48
48
  "vitest": "^4.0.17"
49
49
  },
50
+ "engines": {
51
+ "node": ">=22.18"
52
+ },
50
53
  "scripts": {
51
54
  "build": "tsc -p tsconfig.build.json && node build.mjs",
52
55
  "test": "vitest run",