nuxt-ai-ready 0.7.5 → 0.7.7

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/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "nuxt": ">=4.0.0"
5
5
  },
6
6
  "configKey": "aiReady",
7
- "version": "0.7.5",
7
+ "version": "0.7.7",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -84,11 +84,13 @@ async function initCrawler(state) {
84
84
  return;
85
85
  if (state.dbPath) {
86
86
  await mkdir(dirname(state.dbPath), { recursive: true });
87
- const { default: betterSqlite3 } = await import('db0/connectors/better-sqlite3');
88
- const connector = betterSqlite3({ path: state.dbPath });
87
+ const nodeVersion = Number.parseInt(process.versions.node?.split(".")[0] || "0");
88
+ const connectorPath = nodeVersion >= 22 ? "db0/connectors/node-sqlite" : "db0/connectors/better-sqlite3";
89
+ const { default: connectorFn } = await import(connectorPath);
90
+ const connector = connectorFn({ path: state.dbPath });
89
91
  state.db = createAdapter(connector);
90
92
  await initSchema(state.db);
91
- logger.debug(`Crawler initialized with SQLite at ${state.dbPath}`);
93
+ logger.debug(`Crawler initialized with SQLite at ${state.dbPath} using ${connectorPath}`);
92
94
  }
93
95
  if (state.llmsFullTxtPath) {
94
96
  await mkdir(dirname(state.llmsFullTxtPath), { recursive: true });
@@ -370,6 +372,12 @@ declare module '#ai-ready/adapter' {
370
372
  export default connector
371
373
  }
372
374
 
375
+ declare module '#ai-ready/db-provider' {
376
+ import type { H3Event } from 'h3'
377
+ import type { Connector } from 'db0'
378
+ export function createConnector(event?: H3Event): Promise<Connector>
379
+ }
380
+
373
381
  export {}
374
382
  `
375
383
  });
@@ -404,7 +412,7 @@ function refineDatabaseConfig(config, rootDir) {
404
412
  if (type === "d1") {
405
413
  return {
406
414
  type: "d1",
407
- bindingName: config.bindingName || "AI_READY_DB"
415
+ bindingName: config.bindingName || "DB"
408
416
  };
409
417
  }
410
418
  return {
@@ -469,6 +477,16 @@ const module$1 = defineNuxtModule({
469
477
  const adapterPath = await resolveDatabaseAdapter(dbType);
470
478
  nuxt.options.alias["#ai-ready/adapter"] = adapterPath;
471
479
  nuxt.options.nitro.alias["#ai-ready/adapter"] = adapterPath;
480
+ let providerPath = resolve("./runtime/server/db/provider/sqlite-node");
481
+ if (dbType === "d1") {
482
+ providerPath = resolve("./runtime/server/db/provider/d1");
483
+ } else if (dbType === "libsql") {
484
+ providerPath = resolve("./runtime/server/db/provider/libsql");
485
+ } else if (process.versions.bun) {
486
+ providerPath = resolve("./runtime/server/db/provider/sqlite-bun");
487
+ }
488
+ nuxt.options.alias["#ai-ready/db-provider"] = providerPath;
489
+ nuxt.options.nitro.alias["#ai-ready/db-provider"] = providerPath;
472
490
  if (!nuxt.options.mcp?.name) {
473
491
  nuxt.options.mcp = nuxt.options.mcp || {};
474
492
  nuxt.options.mcp.name = useSiteConfig().name;
@@ -579,12 +597,20 @@ export async function readPageDataFromFilesystem() {
579
597
  return { pages: [], errorRoutes: [] }
580
598
  }
581
599
 
582
- // Use better-sqlite3 to read pages
583
- const Database = (await import('better-sqlite3')).default
584
- const db = new Database(dbPath, { readonly: true })
585
-
586
- const rows = db.prepare('SELECT route, title, description, markdown, headings, keywords, updated_at, is_error FROM ai_ready_pages').all()
587
- db.close()
600
+ let rows = []
601
+ const nodeVersion = Number.parseInt(process.versions.node?.split('.')[0] || '0')
602
+ if (nodeVersion >= 22) {
603
+ const { DatabaseSync } = await import('node:sqlite')
604
+ const db = new DatabaseSync(dbPath, { open: true })
605
+ rows = db.prepare('SELECT route, title, description, markdown, headings, keywords, updated_at, is_error FROM ai_ready_pages').all()
606
+ db.close()
607
+ }
608
+ else {
609
+ const Database = (await import('better-sqlite3')).default
610
+ const db = new Database(dbPath, { readonly: true })
611
+ rows = db.prepare('SELECT route, title, description, markdown, headings, keywords, updated_at, is_error FROM ai_ready_pages').all()
612
+ db.close()
613
+ }
588
614
 
589
615
  const pages = rows.filter(r => !r.is_error).map(r => ({
590
616
  route: r.route,
@@ -1,7 +1,5 @@
1
- import { mkdir } from "node:fs/promises";
2
- import adapter from "#ai-ready/adapter";
3
- import { useNitroApp, useRuntimeConfig } from "nitropack/runtime";
4
- import { dirname } from "pathe";
1
+ import { createConnector } from "#ai-ready/db-provider";
2
+ import { useNitroApp } from "nitropack/runtime";
5
3
  import { createAdapter, initSchema } from "./shared.js";
6
4
  const DB_KEY = "_aiReadyDb";
7
5
  export function useDatabase(event) {
@@ -12,24 +10,7 @@ export function useDatabase(event) {
12
10
  return nitro[DB_KEY];
13
11
  }
14
12
  async function initDatabase(event) {
15
- const config = useRuntimeConfig()["nuxt-ai-ready"];
16
- let connector;
17
- if (config.database.type === "d1") {
18
- const binding = event?.context?.cloudflare?.env?.[config.database.bindingName || "AI_READY_DB"];
19
- if (!binding) {
20
- throw new Error(`D1 binding '${config.database.bindingName || "AI_READY_DB"}' not found in event context`);
21
- }
22
- connector = adapter({ binding });
23
- } else if (config.database.type === "libsql") {
24
- connector = adapter({
25
- url: config.database.url,
26
- authToken: config.database.authToken
27
- });
28
- } else {
29
- const dbPath = config.database.filename || ".data/ai-ready/pages.db";
30
- await mkdir(dirname(dbPath), { recursive: true });
31
- connector = adapter({ path: dbPath });
32
- }
13
+ const connector = await createConnector(event);
33
14
  if (!connector) {
34
15
  throw new Error("Failed to initialize database connector");
35
16
  }
@@ -0,0 +1,3 @@
1
+ import type { Connector } from 'db0';
2
+ import type { H3Event } from 'h3';
3
+ export declare function createConnector(event?: H3Event): Promise<Connector>;
@@ -0,0 +1,28 @@
1
+ import adapter from "#ai-ready/adapter";
2
+ import { useRuntimeConfig } from "nitropack/runtime";
3
+ export async function createConnector(event) {
4
+ const config = useRuntimeConfig(event)["nuxt-ai-ready"];
5
+ const bindingName = config.database.bindingName || "DB";
6
+ let cloudflareEnv;
7
+ try {
8
+ const context = event?.context || useEvent?.()?.context;
9
+ cloudflareEnv = context?.cloudflare?.env;
10
+ } catch {
11
+ cloudflareEnv = globalThis.__env__;
12
+ }
13
+ const binding = cloudflareEnv?.[bindingName];
14
+ if (!binding) {
15
+ const debug = {
16
+ hasEvent: !!event,
17
+ hasContext: !!event?.context,
18
+ hasCloudflare: !!event?.context?.cloudflare,
19
+ hasCloudflareEnv: !!event?.context?.cloudflare?.env,
20
+ hasGlobalEnv: !!globalThis.__env__,
21
+ cloudflareEnvKeys: cloudflareEnv ? Object.keys(cloudflareEnv) : [],
22
+ bindingName
23
+ };
24
+ console.error(`[D1 Debug] Binding '${bindingName}' not found. Context:`, JSON.stringify(debug, null, 2));
25
+ throw new Error(`D1 binding '${bindingName}' not found in event.context.cloudflare.env or globalThis.__env__`);
26
+ }
27
+ return adapter({ binding });
28
+ }
@@ -0,0 +1,3 @@
1
+ import type { Connector } from 'db0';
2
+ import type { H3Event } from 'h3';
3
+ export declare function createConnector(event?: H3Event): Promise<Connector>;
@@ -0,0 +1,9 @@
1
+ import adapter from "#ai-ready/adapter";
2
+ import { useRuntimeConfig } from "nitropack/runtime";
3
+ export async function createConnector(event) {
4
+ const config = useRuntimeConfig(event)["nuxt-ai-ready"];
5
+ return adapter({
6
+ url: config.database.url,
7
+ authToken: config.database.authToken
8
+ });
9
+ }
@@ -0,0 +1,3 @@
1
+ import type { Connector } from 'db0';
2
+ import type { H3Event } from 'h3';
3
+ export declare function createConnector(event?: H3Event): Promise<Connector>;
@@ -0,0 +1,10 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import adapter from "#ai-ready/adapter";
3
+ import { useRuntimeConfig } from "nitropack/runtime";
4
+ import { dirname } from "pathe";
5
+ export async function createConnector(event) {
6
+ const config = useRuntimeConfig(event)["nuxt-ai-ready"];
7
+ const dbPath = config.database.filename || ".data/ai-ready/pages.db";
8
+ await mkdir(dirname(dbPath), { recursive: true });
9
+ return adapter({ path: dbPath });
10
+ }
@@ -0,0 +1,3 @@
1
+ import type { Connector } from 'db0';
2
+ import type { H3Event } from 'h3';
3
+ export declare function createConnector(event?: H3Event): Promise<Connector>;
@@ -0,0 +1,10 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import adapter from "#ai-ready/adapter";
3
+ import { useRuntimeConfig } from "nitropack/runtime";
4
+ import { dirname } from "pathe";
5
+ export async function createConnector(event) {
6
+ const config = useRuntimeConfig(event)["nuxt-ai-ready"];
7
+ const dbPath = config.database.filename || ".data/ai-ready/pages.db";
8
+ await mkdir(dirname(dbPath), { recursive: true });
9
+ return adapter({ path: dbPath });
10
+ }
@@ -9,7 +9,11 @@ export default defineNitroPlugin((nitro) => {
9
9
  nitro.hooks.hook("request", async (event) => {
10
10
  if (!seeding) {
11
11
  seeding = seedFromSitemap(event).catch((err) => {
12
- logger.error("[sitemap-seeder] Failed to seed:", err);
12
+ if (err.message?.includes("D1 binding")) {
13
+ logger.info("[sitemap-seeder] Skipping runtime seeding - using static pages data");
14
+ } else {
15
+ logger.error("[sitemap-seeder] Failed to seed:", err);
16
+ }
13
17
  });
14
18
  }
15
19
  });
@@ -62,6 +62,19 @@ interface DebugInfo {
62
62
  note: string;
63
63
  };
64
64
  };
65
+ cloudflare?: {
66
+ hasContext: boolean;
67
+ hasCloudflare: boolean;
68
+ hasCloudflareEnv: boolean;
69
+ hasContextEnv: boolean;
70
+ contextKeys: string[];
71
+ cloudflareKeys: string[];
72
+ cloudflareEnvKeys: string[];
73
+ databaseConfig: {
74
+ type: string;
75
+ bindingName?: string;
76
+ };
77
+ };
65
78
  diagnostics: {
66
79
  issues: string[];
67
80
  suggestions: string[];
@@ -19,10 +19,23 @@ export default eventHandler(async (event) => {
19
19
  } else {
20
20
  mode = "production";
21
21
  }
22
- const [pages, errorRoutes] = await Promise.all([
23
- queryPages(event),
24
- queryPages(event, { where: { hasError: true } })
25
- ]);
22
+ const issues = [];
23
+ const suggestions = [];
24
+ let pages = [];
25
+ let errorRoutes = [];
26
+ let dbError = null;
27
+ try {
28
+ const [p, e] = await Promise.all([
29
+ queryPages(event),
30
+ queryPages(event, { where: { hasError: true } })
31
+ ]);
32
+ pages = p;
33
+ errorRoutes = e;
34
+ } catch (err) {
35
+ dbError = err.message || String(err);
36
+ issues.push(`Database error: ${dbError}`);
37
+ suggestions.push("Verify your database configuration (D1 binding, SQLite path, etc.)");
38
+ }
26
39
  let source;
27
40
  if (isDev) {
28
41
  source = "empty (dev mode returns empty array)";
@@ -75,8 +88,6 @@ export default eventHandler(async (event) => {
75
88
  pageCount: publicData?.pages?.length ?? 0,
76
89
  source: jsonFileSource
77
90
  };
78
- const issues = [];
79
- const suggestions = [];
80
91
  if (mode === "development") {
81
92
  issues.push("Development mode: page data is intentionally empty");
82
93
  suggestions.push("Run `nuxi generate` or `nuxi build --prerender` to generate page data");
@@ -95,44 +106,61 @@ export default eventHandler(async (event) => {
95
106
  let runtimeSyncInfo;
96
107
  let indexNowInfo;
97
108
  let cronRunsInfo;
98
- if (!isDev && !isPrerender) {
99
- const [total, pending, sitemapSeededAt, cronRuns] = await Promise.all([
100
- countPages(event),
101
- countPages(event, { where: { pending: true } }),
102
- getSitemapSeededAt(event),
103
- getRecentCronRuns(event, 20)
104
- ]);
105
- runtimeSyncInfo = {
106
- total,
107
- indexed: total - pending,
108
- pending,
109
- sitemapSeededAt: sitemapSeededAt ? new Date(sitemapSeededAt).toISOString() : null
110
- };
111
- cronRunsInfo = cronRuns.map((run) => ({
112
- id: run.id,
113
- startedAt: new Date(run.startedAt).toISOString(),
114
- finishedAt: run.finishedAt ? new Date(run.finishedAt).toISOString() : null,
115
- durationMs: run.durationMs,
116
- status: run.status,
117
- pagesIndexed: run.pagesIndexed,
118
- pagesRemaining: run.pagesRemaining,
119
- indexNowSubmitted: run.indexNowSubmitted,
120
- indexNowRemaining: run.indexNowRemaining,
121
- errors: run.errors
122
- }));
123
- if (runtimeConfig.indexNowKey) {
124
- const [indexNowPending, indexNowStats] = await Promise.all([
125
- countPagesNeedingIndexNowSync(event),
126
- getIndexNowStats(event)
109
+ if (!isDev && !isPrerender && !dbError) {
110
+ try {
111
+ const [total, pending, sitemapSeededAt, cronRuns] = await Promise.all([
112
+ countPages(event),
113
+ countPages(event, { where: { pending: true } }),
114
+ getSitemapSeededAt(event),
115
+ getRecentCronRuns(event, 20)
127
116
  ]);
128
- indexNowInfo = {
129
- pending: indexNowPending,
130
- totalSubmitted: indexNowStats.totalSubmitted,
131
- lastSubmittedAt: indexNowStats.lastSubmittedAt ? new Date(indexNowStats.lastSubmittedAt).toISOString() : null,
132
- lastError: indexNowStats.lastError
117
+ runtimeSyncInfo = {
118
+ total,
119
+ indexed: total - pending,
120
+ pending,
121
+ sitemapSeededAt: sitemapSeededAt ? new Date(sitemapSeededAt).toISOString() : null
133
122
  };
123
+ cronRunsInfo = cronRuns.map((run) => ({
124
+ id: run.id,
125
+ startedAt: new Date(run.startedAt).toISOString(),
126
+ finishedAt: run.finishedAt ? new Date(run.finishedAt).toISOString() : null,
127
+ durationMs: run.durationMs,
128
+ status: run.status,
129
+ pagesIndexed: run.pagesIndexed,
130
+ pagesRemaining: run.pagesRemaining,
131
+ indexNowSubmitted: run.indexNowSubmitted,
132
+ indexNowRemaining: run.indexNowRemaining,
133
+ errors: run.errors
134
+ }));
135
+ if (runtimeConfig.indexNowKey) {
136
+ const [indexNowPending, indexNowStats] = await Promise.all([
137
+ countPagesNeedingIndexNowSync(event),
138
+ getIndexNowStats(event)
139
+ ]);
140
+ indexNowInfo = {
141
+ pending: indexNowPending,
142
+ totalSubmitted: indexNowStats.totalSubmitted,
143
+ lastSubmittedAt: indexNowStats.lastSubmittedAt ? new Date(indexNowStats.lastSubmittedAt).toISOString() : null,
144
+ lastError: indexNowStats.lastError
145
+ };
146
+ }
147
+ } catch (err) {
148
+ issues.push(`Runtime stats error: ${err.message || String(err)}`);
134
149
  }
135
150
  }
151
+ const cloudflareInfo = {
152
+ hasContext: !!event.context,
153
+ hasCloudflare: !!event.context?.cloudflare,
154
+ hasCloudflareEnv: !!event.context?.cloudflare?.env,
155
+ hasContextEnv: !!event.context?.env,
156
+ contextKeys: event.context ? Object.keys(event.context) : [],
157
+ cloudflareKeys: event.context?.cloudflare ? Object.keys(event.context.cloudflare) : [],
158
+ cloudflareEnvKeys: event.context?.cloudflare?.env ? Object.keys(event.context.cloudflare.env) : [],
159
+ databaseConfig: {
160
+ type: runtimeConfig.database?.type || "unknown",
161
+ bindingName: runtimeConfig.database?.bindingName
162
+ }
163
+ };
136
164
  const debugInfo = {
137
165
  version: runtimeConfig.version || "unknown",
138
166
  environment: {
@@ -164,6 +192,7 @@ export default eventHandler(async (event) => {
164
192
  pageDataModule: pageDataModuleInfo,
165
193
  readPageDataModule: readPageDataModuleInfo
166
194
  },
195
+ cloudflare: cloudflareInfo,
167
196
  diagnostics: {
168
197
  issues,
169
198
  suggestions
@@ -18,7 +18,15 @@ export default eventHandler(async (event) => {
18
18
  },
19
19
  config.llmsTxt
20
20
  );
21
- const total = await countPages(event);
21
+ let total = 0;
22
+ try {
23
+ total = await countPages(event);
24
+ } catch (err) {
25
+ setHeader(event, "Content-Type", "text/plain; charset=utf-8");
26
+ return `${header}Database error: ${err.message || String(err)}
27
+
28
+ Runtime indexing may not be configured correctly for this environment.`;
29
+ }
22
30
  if (total === 0) {
23
31
  setHeader(event, "Content-Type", "text/plain; charset=utf-8");
24
32
  return `${header}No pages indexed. Run \`nuxi generate\` or enable runtime indexing.`;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-ai-ready",
3
3
  "type": "module",
4
- "version": "0.7.5",
4
+ "version": "0.7.7",
5
5
  "description": "Best practice AI & LLM discoverability for Nuxt sites.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -60,7 +60,7 @@
60
60
  "ufo": "^1.6.2"
61
61
  },
62
62
  "devDependencies": {
63
- "@antfu/eslint-config": "^6.7.3",
63
+ "@antfu/eslint-config": "^7.0.0",
64
64
  "@arethetypeswrong/cli": "^0.18.2",
65
65
  "@headlessui/vue": "^1.7.23",
66
66
  "@nuxt/content": "^3.10.0",
@@ -77,10 +77,10 @@
77
77
  "@vitest/coverage-v8": "^4.0.17",
78
78
  "@vue/test-utils": "^2.4.6",
79
79
  "@vueuse/nuxt": "^14.1.0",
80
- "agents": "^0.3.4",
81
- "ai": "^6.0.30",
80
+ "agents": "^0.3.5",
81
+ "ai": "^6.0.33",
82
82
  "better-sqlite3": "^12.6.0",
83
- "bumpp": "^10.3.2",
83
+ "bumpp": "^10.4.0",
84
84
  "eslint": "^9.39.2",
85
85
  "execa": "^9.6.1",
86
86
  "happy-dom": "^20.1.0",
@@ -94,7 +94,7 @@
94
94
  "vue": "^3.5.26",
95
95
  "vue-router": "^4.6.4",
96
96
  "vue-tsc": "^3.2.2",
97
- "wrangler": "^4.58.0",
97
+ "wrangler": "^4.59.1",
98
98
  "zod": "^4.3.5"
99
99
  },
100
100
  "resolutions": {