nuxt-ai-ready 2.0.1 → 2.2.0
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/chunks/agent-skills.mjs +4 -1
- package/dist/chunks/prerender.mjs +10 -3
- package/dist/cli.mjs +67 -11
- package/dist/module.d.mts +3 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +4 -1
- package/dist/runtime/app/plugins/md-alternate.server.js +6 -2
- package/dist/runtime/llms-txt-utils.js +2 -2
- package/dist/runtime/markdown-path.d.ts +2 -0
- package/dist/runtime/markdown-path.js +4 -0
- package/dist/runtime/server/db/context.d.ts +2 -0
- package/dist/runtime/server/db/context.js +2 -0
- package/dist/runtime/server/db/drizzle/client.d.ts +6 -1
- package/dist/runtime/server/db/drizzle/client.js +66 -13
- package/dist/runtime/server/db/drizzle/index.d.ts +2 -2
- package/dist/runtime/server/db/drizzle/index.js +1 -2
- package/dist/runtime/server/db/drizzle/providers/dbPath.js +1 -1
- package/dist/runtime/server/db/drizzle/providers/postgres.d.ts +8 -0
- package/dist/runtime/server/db/drizzle/providers/postgres.js +17 -0
- package/dist/runtime/server/db/drizzle/queries.d.ts +1 -5
- package/dist/runtime/server/db/drizzle/queries.js +79 -38
- package/dist/runtime/server/db/drizzle/raw.d.ts +2 -2
- package/dist/runtime/server/db/drizzle/raw.js +32 -13
- package/dist/runtime/server/db/queries.d.ts +30 -27
- package/dist/runtime/server/db/queries.js +142 -91
- package/dist/runtime/server/db/schema/postgres.d.ts +20 -20
- package/dist/runtime/server/db/schema/postgres.js +6 -6
- package/dist/runtime/server/db/shared.d.ts +16 -0
- package/dist/runtime/server/db/shared.js +12 -0
- package/dist/runtime/server/middleware/markdown.js +10 -5
- package/dist/runtime/server/middleware/markdown.prerender.js +4 -1
- package/dist/runtime/server/plugins/db-lifecycle.js +5 -4
- package/dist/runtime/server/plugins/sitemap-seeder.js +9 -4
- package/dist/runtime/server/routes/__ai-ready/cron.get.js +10 -2
- package/dist/runtime/server/routes/__ai-ready/poll.post.js +30 -14
- package/dist/runtime/server/routes/__ai-ready/reindex.post.d.ts +2 -0
- package/dist/runtime/server/routes/__ai-ready/reindex.post.js +23 -0
- package/dist/runtime/server/routes/api-catalog.js +11 -2
- package/dist/runtime/server/routes/llms.txt.get.js +17 -12
- package/dist/runtime/server/routes/sitemap.md.get.d.ts +2 -0
- package/dist/runtime/server/routes/sitemap.md.get.js +51 -0
- package/dist/runtime/server/tasks/ai-ready-cron.js +3 -0
- package/dist/runtime/server/utils/i18n.d.ts +4 -0
- package/dist/runtime/server/utils/i18n.js +13 -0
- package/dist/runtime/server/utils/indexPage.d.ts +0 -1
- package/dist/runtime/server/utils/indexPage.js +10 -4
- package/dist/runtime/server/utils/link-header.d.ts +2 -0
- package/dist/runtime/server/utils/link-header.js +6 -0
- package/dist/runtime/server/utils/markdown-request.js +3 -1
- package/dist/runtime/server/utils/negotiation-decision.d.ts +1 -1
- package/dist/runtime/server/utils/runCron.d.ts +24 -0
- package/dist/runtime/server/utils/runCron.js +21 -4
- package/dist/runtime/server/utils/sitemap-md.d.ts +13 -0
- package/dist/runtime/server/utils/sitemap-md.js +65 -0
- package/dist/runtime/types.d.ts +21 -7
- package/dist/shared/{nuxt-ai-ready.D8qbnNrv.mjs → nuxt-ai-ready.D5URd0TD.mjs} +107 -39
- package/package.json +31 -26
|
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { realpath, readFile } from 'node:fs/promises';
|
|
3
3
|
import { resolve, isAbsolute, relative, sep } from 'node:path';
|
|
4
4
|
import { parseDocument } from 'yaml';
|
|
5
|
-
import { A as AGENT_SKILLS_SCHEMA } from '../shared/nuxt-ai-ready.
|
|
5
|
+
import { A as AGENT_SKILLS_SCHEMA } from '../shared/nuxt-ai-ready.D5URd0TD.mjs';
|
|
6
6
|
import 'node:module';
|
|
7
7
|
import '@nuxt/kit';
|
|
8
8
|
import 'defu';
|
|
@@ -10,9 +10,12 @@ import 'nuxt-site-config/kit';
|
|
|
10
10
|
import 'nuxtseo-shared/kit';
|
|
11
11
|
import 'pkg-types';
|
|
12
12
|
import 'node:url';
|
|
13
|
+
import '../../dist/runtime/markdown-path.js';
|
|
14
|
+
import '../../dist/runtime/server/utils/sitemap-md.js';
|
|
13
15
|
import '../../dist/runtime/server/utils/discovery-response.js';
|
|
14
16
|
import 'pathe';
|
|
15
17
|
import 'nuxtseo-shared/i18n';
|
|
18
|
+
import '../../dist/runtime/route-path.js';
|
|
16
19
|
|
|
17
20
|
const namePattern = /^(?!.*--)[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
18
21
|
const digestPattern = /^sha256:[a-f0-9]{64}$/;
|
|
@@ -5,11 +5,12 @@ import { colorize } from 'consola/utils';
|
|
|
5
5
|
import { resolveLocaleFromRoute } from 'nuxtseo-shared/i18n-runtime';
|
|
6
6
|
import { collectSitemap } from 'sitemapd/parse';
|
|
7
7
|
import { withLeadingSlash, joinURL, withBase } from 'ufo';
|
|
8
|
-
import { l as logger, s as supportsNativeNodeSqlite, M as MARKDOWN_LINK_AVAILABILITY_FILE } from '../shared/nuxt-ai-ready.
|
|
8
|
+
import { l as logger, s as supportsNativeNodeSqlite, M as MARKDOWN_LINK_AVAILABILITY_FILE } from '../shared/nuxt-ai-ready.D5URd0TD.mjs';
|
|
9
9
|
import { normalizePagePath, toMarkdownPath } from '../../dist/runtime/markdown-path.js';
|
|
10
|
-
import {
|
|
10
|
+
import { toDeployedRoute, toLogicalRoute } from '../../dist/runtime/route-path.js';
|
|
11
11
|
import { initSchema, computeContentHash, insertPage, queryAllPages, exportDbDump } from '../../dist/runtime/server/db/shared.js';
|
|
12
12
|
import { buildLlmsFullTxtHeader, formatPageForLlmsFullTxt } from '../../dist/runtime/server/utils/llms-full.js';
|
|
13
|
+
import { SITEMAP_MD_ROUTE, appendSitemapSection } from '../../dist/runtime/server/utils/sitemap-md.js';
|
|
13
14
|
import 'node:crypto';
|
|
14
15
|
import 'node:module';
|
|
15
16
|
import 'defu';
|
|
@@ -320,6 +321,8 @@ function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, extras
|
|
|
320
321
|
logger.debug(`Detected error page: ${pageRoute2}`);
|
|
321
322
|
return;
|
|
322
323
|
}
|
|
324
|
+
if (route.fileName === SITEMAP_MD_ROUTE)
|
|
325
|
+
return;
|
|
323
326
|
if (!route.fileName?.endsWith(".md"))
|
|
324
327
|
return;
|
|
325
328
|
let pageRoute = route.route.replace(RE_MD_EXT, "");
|
|
@@ -331,7 +334,7 @@ function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, extras
|
|
|
331
334
|
await initPromise;
|
|
332
335
|
const parsed = JSON.parse(route.contents || "{}");
|
|
333
336
|
await processMarkdownRoute(state, nuxt, pageRoute, parsed);
|
|
334
|
-
route.contents = parsed.markdown;
|
|
337
|
+
route.contents = options.sitemapMd === false ? parsed.markdown : appendSitemapSection(parsed.markdown, toDeployedRoute(SITEMAP_MD_ROUTE, nitro.options.baseURL));
|
|
335
338
|
route.contentType = "text/markdown; charset=utf-8";
|
|
336
339
|
state.totalProcessingTime += Date.now() - pageStartTime;
|
|
337
340
|
});
|
|
@@ -406,6 +409,10 @@ function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, extras
|
|
|
406
409
|
const llmsStats = await prerenderRoute(nitro, "/llms.txt");
|
|
407
410
|
const llmsFullStats = await stat(state.llmsFullTxtPath);
|
|
408
411
|
nitro._prerenderedRoutes.push({ route: "/llms-full.txt", fileName: "/llms-full.txt" });
|
|
412
|
+
if (options.sitemapMd !== false) {
|
|
413
|
+
const sitemapMdStats = await prerenderRoute(nitro, SITEMAP_MD_ROUTE);
|
|
414
|
+
logger.debug(`Wrote sitemap.md (${(sitemapMdStats.size / 1024).toFixed(1)}kb)`);
|
|
415
|
+
}
|
|
409
416
|
const kb = (b) => (b / 1024).toFixed(1);
|
|
410
417
|
const totalKb = kb(llmsStats.size + llmsFullStats.size);
|
|
411
418
|
const dim = (s) => colorize("dim", s);
|
package/dist/cli.mjs
CHANGED
|
@@ -26,11 +26,22 @@ async function requireSecret(cwd, hint = "") {
|
|
|
26
26
|
}
|
|
27
27
|
return secret;
|
|
28
28
|
}
|
|
29
|
+
function describeHttpError(status, body) {
|
|
30
|
+
const error = body;
|
|
31
|
+
const message = typeof error?.message === "string" && error.message ? error.message : typeof error?.statusMessage === "string" && error.statusMessage ? error.statusMessage : typeof error?.error === "string" && error.error ? error.error : null;
|
|
32
|
+
return message ? `${status}: ${message}` : `request failed with status ${status}`;
|
|
33
|
+
}
|
|
29
34
|
async function fetchJson(url, init, errorLabel = "Failed") {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return null;
|
|
35
|
+
const r = await fetch(url, init).catch((err) => {
|
|
36
|
+
throw new Error(`${errorLabel}: ${err instanceof Error ? err.message : String(err)}`);
|
|
33
37
|
});
|
|
38
|
+
if (!r.ok) {
|
|
39
|
+
const body = await r.json().catch(() => {
|
|
40
|
+
return null;
|
|
41
|
+
});
|
|
42
|
+
throw new Error(`${errorLabel}: ${describeHttpError(r.status, body)}`);
|
|
43
|
+
}
|
|
44
|
+
return r.json();
|
|
34
45
|
}
|
|
35
46
|
const main = defineCommand({
|
|
36
47
|
meta: {
|
|
@@ -65,8 +76,6 @@ const main = defineCommand({
|
|
|
65
76
|
const url = `${args.url}/__ai-ready/status`;
|
|
66
77
|
consola.info(`Fetching status from ${args.url}...`);
|
|
67
78
|
const res = await fetchJson(url, { headers: authHeaders(secret) }, "Failed to connect");
|
|
68
|
-
if (!res)
|
|
69
|
-
return;
|
|
70
79
|
consola.box("AI Ready Status");
|
|
71
80
|
consola.info(`Total pages: ${colors.cyan(res.total?.toString() || "0")}`);
|
|
72
81
|
consola.info(`Indexed: ${colors.green(res.indexed?.toString() || "0")}`);
|
|
@@ -151,8 +160,6 @@ const main = defineCommand({
|
|
|
151
160
|
const url = `${args.url}/__ai-ready/poll?${params}`;
|
|
152
161
|
consola.info(`Triggering poll at ${args.url}...`);
|
|
153
162
|
const res = await fetchJson(url, { method: "POST", headers: authHeaders(secret) });
|
|
154
|
-
if (!res)
|
|
155
|
-
return;
|
|
156
163
|
consola.success(`Indexed: ${colors.green(res.indexed?.toString() || "0")} pages`);
|
|
157
164
|
consola.info(`Remaining: ${colors.yellow(res.remaining?.toString() || "0")}`);
|
|
158
165
|
if (res.errors?.length) {
|
|
@@ -199,8 +206,6 @@ const main = defineCommand({
|
|
|
199
206
|
const url = `${args.url}/__ai-ready/restore?${params}`;
|
|
200
207
|
consola.info(`Restoring database at ${args.url}...`);
|
|
201
208
|
const res = await fetchJson(url, { method: "POST", headers: authHeaders(secret) });
|
|
202
|
-
if (!res)
|
|
203
|
-
return;
|
|
204
209
|
consola.success(`Restored: ${colors.green(res.restored?.toString() || "0")} pages`);
|
|
205
210
|
if (res.cleared) {
|
|
206
211
|
consola.info(`Cleared: ${colors.yellow(res.cleared?.toString() || "0")} existing pages`);
|
|
@@ -252,8 +257,6 @@ const main = defineCommand({
|
|
|
252
257
|
method: "POST",
|
|
253
258
|
headers: secret ? authHeaders(secret) : void 0
|
|
254
259
|
});
|
|
255
|
-
if (!res)
|
|
256
|
-
return;
|
|
257
260
|
if (args.dry) {
|
|
258
261
|
consola.info(`Would prune: ${colors.yellow(res.count?.toString() || "0")} routes`);
|
|
259
262
|
if (res.routes?.length) {
|
|
@@ -268,6 +271,59 @@ const main = defineCommand({
|
|
|
268
271
|
consola.success(`Pruned: ${colors.green(res.pruned?.toString() || "0")} routes`);
|
|
269
272
|
}
|
|
270
273
|
}
|
|
274
|
+
}),
|
|
275
|
+
reindex: () => defineCommand({
|
|
276
|
+
meta: {
|
|
277
|
+
name: "reindex",
|
|
278
|
+
description: "Reindex a single route"
|
|
279
|
+
},
|
|
280
|
+
args: {
|
|
281
|
+
route: {
|
|
282
|
+
type: "positional",
|
|
283
|
+
description: "Route to reindex (e.g. /about)",
|
|
284
|
+
required: true
|
|
285
|
+
},
|
|
286
|
+
url: {
|
|
287
|
+
type: "string",
|
|
288
|
+
alias: "u",
|
|
289
|
+
description: "Site URL (default: http://localhost:3000)",
|
|
290
|
+
default: "http://localhost:3000"
|
|
291
|
+
},
|
|
292
|
+
force: {
|
|
293
|
+
type: "boolean",
|
|
294
|
+
description: "Index even when the page is still fresh",
|
|
295
|
+
default: true,
|
|
296
|
+
negativeDescription: "Skip indexing when the page is still fresh"
|
|
297
|
+
},
|
|
298
|
+
cwd: {
|
|
299
|
+
type: "string",
|
|
300
|
+
description: "Working directory",
|
|
301
|
+
default: "."
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
async run({ args }) {
|
|
305
|
+
const cwd = resolve(args.cwd || ".");
|
|
306
|
+
const secret = await requireSecret(cwd);
|
|
307
|
+
if (!secret)
|
|
308
|
+
return;
|
|
309
|
+
const params = new URLSearchParams();
|
|
310
|
+
params.set("route", args.route);
|
|
311
|
+
if (!args.force)
|
|
312
|
+
params.set("force", "false");
|
|
313
|
+
const url = `${args.url}/__ai-ready/reindex?${params}`;
|
|
314
|
+
consola.info(`Reindexing ${args.route} at ${args.url}...`);
|
|
315
|
+
const res = await fetchJson(url, { method: "POST", headers: authHeaders(secret) });
|
|
316
|
+
if (res.indexed) {
|
|
317
|
+
consola.success(`Indexed: ${colors.green(res.route)}`);
|
|
318
|
+
if (res.contentChanged !== void 0) {
|
|
319
|
+
consola.info(`Content changed: ${res.contentChanged ? colors.green("yes") : colors.yellow("no")}`);
|
|
320
|
+
}
|
|
321
|
+
} else if (res.skipped) {
|
|
322
|
+
consola.info(`Skipped: ${colors.yellow(res.route)} (still fresh)`);
|
|
323
|
+
} else {
|
|
324
|
+
throw new Error(`Failed to reindex ${res.route ?? args.route}${res.error ? `: ${res.error}` : ""}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
271
327
|
})
|
|
272
328
|
}
|
|
273
329
|
});
|
package/dist/module.d.mts
CHANGED
|
@@ -31,7 +31,7 @@ interface ResolvedApiCatalogConfig {
|
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
type DatabaseType = 'sqlite' | 'bun' | 'd1' | 'libsql' | 'neon';
|
|
34
|
+
type DatabaseType = 'sqlite' | 'bun' | 'd1' | 'libsql' | 'neon' | 'postgres';
|
|
35
35
|
/**
|
|
36
36
|
* Database state after parsing. Every consumer reads `_tag` first, so a
|
|
37
37
|
* disabled database can never carry a driver, a path, or credentials.
|
|
@@ -85,6 +85,8 @@ interface ModulePublicRuntimeConfig {
|
|
|
85
85
|
debugCron: boolean;
|
|
86
86
|
contentNegotiation: ContentNegotiationPolicy;
|
|
87
87
|
version: string;
|
|
88
|
+
sitemapMd: boolean;
|
|
89
|
+
describedby: boolean;
|
|
88
90
|
mdreamOptions: ModuleOptions['mdreamOptions'];
|
|
89
91
|
markdownCacheHeaders: Required<NonNullable<ModuleOptions['markdownCacheHeaders']>>;
|
|
90
92
|
database: ResolvedDatabase;
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -7,8 +7,11 @@ import 'defu';
|
|
|
7
7
|
import 'nuxt-site-config/kit';
|
|
8
8
|
import 'nuxtseo-shared/kit';
|
|
9
9
|
import 'pkg-types';
|
|
10
|
-
export { m as default } from './shared/nuxt-ai-ready.
|
|
10
|
+
export { m as default } from './shared/nuxt-ai-ready.D5URd0TD.mjs';
|
|
11
|
+
import '../dist/runtime/markdown-path.js';
|
|
12
|
+
import '../dist/runtime/server/utils/sitemap-md.js';
|
|
11
13
|
import '../dist/runtime/server/utils/discovery-response.js';
|
|
12
14
|
import 'node:url';
|
|
13
15
|
import 'pathe';
|
|
14
16
|
import 'nuxtseo-shared/i18n';
|
|
17
|
+
import '../dist/runtime/route-path.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { defineNuxtPlugin, useHead, useRequestURL } from "nuxt/app";
|
|
1
|
+
import { defineNuxtPlugin, useHead, useRequestURL, useRuntimeConfig } from "nuxt/app";
|
|
2
|
+
import { joinURL } from "ufo";
|
|
2
3
|
import { toMarkdownPath } from "../../markdown-path.js";
|
|
3
4
|
export default defineNuxtPlugin({
|
|
4
5
|
setup() {
|
|
@@ -7,9 +8,12 @@ export default defineNuxtPlugin({
|
|
|
7
8
|
const lastSegment = path.split("/").pop() || "";
|
|
8
9
|
if (lastSegment.includes("."))
|
|
9
10
|
return;
|
|
11
|
+
const runtimeConfig = useRuntimeConfig();
|
|
12
|
+
const describedby = runtimeConfig["nuxt-ai-ready"]?.describedby !== false;
|
|
10
13
|
useHead({
|
|
11
14
|
link: [
|
|
12
|
-
{ rel: "alternate", type: "text/markdown", href: toMarkdownPath(path) }
|
|
15
|
+
{ rel: "alternate", type: "text/markdown", href: toMarkdownPath(path) },
|
|
16
|
+
...describedby ? [{ rel: "describedby", href: joinURL(runtimeConfig.app.baseURL, "llms.txt") }] : []
|
|
13
17
|
]
|
|
14
18
|
});
|
|
15
19
|
}
|
|
@@ -4,7 +4,7 @@ import { useRuntimeConfig } from "#nuxtseo/nitro";
|
|
|
4
4
|
import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
|
|
5
5
|
import { withSiteTrailingSlash, withSiteUrl } from "#site-config/server/composables/utils";
|
|
6
6
|
import { formatLlmsTxtPageLink, normalizeLlmsTxtConfig } from "./llms-txt-format.js";
|
|
7
|
-
import { toMarkdownPath } from "./markdown-path.js";
|
|
7
|
+
import { isReservedPath, toMarkdownPath } from "./markdown-path.js";
|
|
8
8
|
import { normalizePersistedRoute, toDeployedRoute, toLogicalRoute } from "./route-path.js";
|
|
9
9
|
import { queryPages } from "./server/db/queries.js";
|
|
10
10
|
import { logger } from "./server/logger.js";
|
|
@@ -12,7 +12,7 @@ import { fetchSitemapUrls } from "./server/utils/sitemap.js";
|
|
|
12
12
|
export { normalizeLlmsTxtConfig };
|
|
13
13
|
function hasRuntimeMarkdownHandler(pathname) {
|
|
14
14
|
const lastSegment = pathname.split("/").pop() || "";
|
|
15
|
-
return !
|
|
15
|
+
return !isReservedPath(pathname) && !lastSegment.includes(".");
|
|
16
16
|
}
|
|
17
17
|
async function findMarkdownLinkAvailability() {
|
|
18
18
|
if (!import.meta.prerender) {
|
|
@@ -1,2 +1,4 @@
|
|
|
1
|
+
/** True for the `/api` namespace and Nitro's `/_*` and `/@*` internal paths. */
|
|
2
|
+
export declare function isReservedPath(path: string): boolean;
|
|
1
3
|
export declare function normalizePagePath(path: string): string;
|
|
2
4
|
export declare function toMarkdownPath(path: string): string;
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
const RE_TRAILING_SLASHES = /\/+$/;
|
|
2
|
+
const RE_RESERVED_PATH = /^\/(?:api(?:\/|$)|_|@)/;
|
|
3
|
+
export function isReservedPath(path) {
|
|
4
|
+
return RE_RESERVED_PATH.test(path);
|
|
5
|
+
}
|
|
2
6
|
export function normalizePagePath(path) {
|
|
3
7
|
return path.replace(RE_TRAILING_SLASHES, "") || "/";
|
|
4
8
|
}
|
|
@@ -4,14 +4,19 @@ import type { DrizzleD1Database } from 'drizzle-orm/d1';
|
|
|
4
4
|
import type { LibSQLDatabase } from 'drizzle-orm/libsql';
|
|
5
5
|
import type { NeonHttpDatabase } from 'drizzle-orm/neon-http';
|
|
6
6
|
import type { NodeSQLiteDatabase } from 'drizzle-orm/node-sqlite';
|
|
7
|
+
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
|
|
7
8
|
import type { H3Event } from '#nuxtseo/h3';
|
|
8
9
|
export type DatabaseDialect = 'sqlite' | 'postgres';
|
|
9
10
|
type SQLiteDB = BetterSQLite3Database | SQLiteBunDatabase | LibSQLDatabase | DrizzleD1Database | NodeSQLiteDatabase;
|
|
10
|
-
type PostgresDB = NeonHttpDatabase;
|
|
11
|
+
type PostgresDB = NeonHttpDatabase | PostgresJsDatabase;
|
|
11
12
|
export interface DrizzleDatabase {
|
|
12
13
|
dialect: DatabaseDialect;
|
|
13
14
|
db: SQLiteDB | PostgresDB;
|
|
14
15
|
}
|
|
16
|
+
/** Keep request-scoped clients alive until deferred database work finishes. */
|
|
17
|
+
export declare function trackDrizzleWork<T>(event: H3Event, work: Promise<T>): Promise<T>;
|
|
18
|
+
/** Transfer client cleanup to deferred work after the response ends. */
|
|
19
|
+
export declare function finishDrizzleResponse(event: H3Event): Promise<void>;
|
|
15
20
|
/**
|
|
16
21
|
* Get Drizzle database instance
|
|
17
22
|
*/
|
|
@@ -1,29 +1,82 @@
|
|
|
1
|
-
import { DB_CONTEXT_KEY } from "../context.js";
|
|
1
|
+
import { DB_CONTEXT_KEY, DB_PROMISE_CONTEXT_KEY, DB_WORK_CONTEXT_KEY } from "../context.js";
|
|
2
2
|
import { closeDriver } from "./raw.js";
|
|
3
3
|
let fallbackClient;
|
|
4
|
+
let fallbackClientPromise;
|
|
5
|
+
function createDrizzleClient(event) {
|
|
6
|
+
return import("#ai-ready-virtual/db-provider.mjs").then(({ createClient }) => createClient(event));
|
|
7
|
+
}
|
|
8
|
+
function getDrizzleWorkState(event) {
|
|
9
|
+
const context = event.context;
|
|
10
|
+
return context[DB_WORK_CONTEXT_KEY] ??= {
|
|
11
|
+
_tag: "ResponseOpen",
|
|
12
|
+
pending: /* @__PURE__ */ new Set()
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function trackDrizzleWork(event, work) {
|
|
16
|
+
const state = getDrizzleWorkState(event);
|
|
17
|
+
const tracked = work.finally(async () => {
|
|
18
|
+
state.pending.delete(tracked);
|
|
19
|
+
if (state._tag === "ResponseEnded" && state.pending.size === 0)
|
|
20
|
+
await closeDrizzle(event);
|
|
21
|
+
});
|
|
22
|
+
state.pending.add(tracked);
|
|
23
|
+
return tracked;
|
|
24
|
+
}
|
|
25
|
+
export async function finishDrizzleResponse(event) {
|
|
26
|
+
const state = getDrizzleWorkState(event);
|
|
27
|
+
state._tag = "ResponseEnded";
|
|
28
|
+
if (state.pending.size === 0)
|
|
29
|
+
await closeDrizzle(event);
|
|
30
|
+
}
|
|
4
31
|
export async function useDrizzle(event) {
|
|
5
32
|
if (event?.context?.[DB_CONTEXT_KEY]) {
|
|
6
33
|
return event.context[DB_CONTEXT_KEY];
|
|
7
34
|
}
|
|
35
|
+
if (event?.context?.[DB_PROMISE_CONTEXT_KEY]) {
|
|
36
|
+
return event.context[DB_PROMISE_CONTEXT_KEY];
|
|
37
|
+
}
|
|
8
38
|
if (!event && fallbackClient) {
|
|
9
39
|
return fallbackClient;
|
|
10
40
|
}
|
|
11
|
-
|
|
12
|
-
|
|
41
|
+
if (!event && fallbackClientPromise)
|
|
42
|
+
return fallbackClientPromise;
|
|
13
43
|
if (event?.context) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
44
|
+
const context = event.context;
|
|
45
|
+
const promise2 = createDrizzleClient(event).then((client) => {
|
|
46
|
+
context[DB_CONTEXT_KEY] = client;
|
|
47
|
+
return client;
|
|
48
|
+
}).finally(() => {
|
|
49
|
+
if (context[DB_PROMISE_CONTEXT_KEY] === promise2)
|
|
50
|
+
delete context[DB_PROMISE_CONTEXT_KEY];
|
|
51
|
+
});
|
|
52
|
+
context[DB_PROMISE_CONTEXT_KEY] = promise2;
|
|
53
|
+
return promise2;
|
|
17
54
|
}
|
|
18
|
-
|
|
55
|
+
const promise = createDrizzleClient().then((client) => {
|
|
56
|
+
fallbackClient = client;
|
|
57
|
+
return client;
|
|
58
|
+
}).finally(() => {
|
|
59
|
+
if (fallbackClientPromise === promise)
|
|
60
|
+
fallbackClientPromise = void 0;
|
|
61
|
+
});
|
|
62
|
+
fallbackClientPromise = promise;
|
|
63
|
+
return promise;
|
|
19
64
|
}
|
|
20
65
|
export async function closeDrizzle(event) {
|
|
21
|
-
if (event?.context
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
closeDriver(
|
|
66
|
+
if (event?.context) {
|
|
67
|
+
const context = event.context;
|
|
68
|
+
const client = context[DB_CONTEXT_KEY] ?? await context[DB_PROMISE_CONTEXT_KEY];
|
|
69
|
+
if (!client)
|
|
70
|
+
return;
|
|
71
|
+
await closeDriver(client.db);
|
|
72
|
+
delete context[DB_CONTEXT_KEY];
|
|
73
|
+
delete context[DB_PROMISE_CONTEXT_KEY];
|
|
74
|
+
} else if (!event) {
|
|
75
|
+
const client = fallbackClient ?? await fallbackClientPromise;
|
|
76
|
+
if (!client)
|
|
77
|
+
return;
|
|
78
|
+
await closeDriver(client.db);
|
|
27
79
|
fallbackClient = void 0;
|
|
80
|
+
fallbackClientPromise = void 0;
|
|
28
81
|
}
|
|
29
82
|
}
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Drizzle ORM database layer for nuxt-ai-ready
|
|
3
3
|
*/
|
|
4
4
|
export * from '#ai-ready-virtual/db-schema.mjs';
|
|
5
|
-
export { closeDrizzle, useDrizzle } from './client.js';
|
|
5
|
+
export { closeDrizzle, finishDrizzleResponse, trackDrizzleWork, useDrizzle } from './client.js';
|
|
6
6
|
export type { DatabaseDialect, DrizzleDatabase } from './client.js';
|
|
7
|
-
export { completeCronRun, countPages, deleteInfoValue,
|
|
7
|
+
export { completeCronRun, countPages, deleteInfoValue, getAllPages, getContentHashes, getInfoValue, getNextSitemapToCrawl, getPageByRoute, getPageLastmods, getPendingPages, getRecentCronRuns, getSitemapStatus, initSchema, markPageIndexed, markRoutesPending, markSitemapCrawled, markSitemapCrawlPartial, markSitemapError, markSitemapSeeded, resetSitemapErrors, searchPages, seedRoutes, setInfoValue, startCronRun, syncSitemaps, upsertPage, } from './queries.js';
|
|
8
8
|
export type { CronRunOutput, PageInput, PageMetaOutput, PageOutput, SitemapOutput, } from './queries.js';
|
|
9
9
|
export { useRawDb } from './raw.js';
|
|
10
10
|
export type { RawExecutor } from './raw.js';
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
export * from "#ai-ready-virtual/db-schema.mjs";
|
|
2
|
-
export { closeDrizzle, useDrizzle } from "./client.js";
|
|
2
|
+
export { closeDrizzle, finishDrizzleResponse, trackDrizzleWork, useDrizzle } from "./client.js";
|
|
3
3
|
export {
|
|
4
4
|
completeCronRun,
|
|
5
5
|
countPages,
|
|
6
6
|
deleteInfoValue,
|
|
7
|
-
deletePage,
|
|
8
7
|
getAllPages,
|
|
9
8
|
getContentHashes,
|
|
10
9
|
getInfoValue,
|
|
@@ -36,7 +36,7 @@ export async function resolveWritableDbPath(dbPath) {
|
|
|
36
36
|
const fallbackErr = await ensureWritableDir(fallbackDir);
|
|
37
37
|
if (fallbackErr) {
|
|
38
38
|
throw new Error(
|
|
39
|
-
`[ai-ready] Database directory '${dir}' is not writable (${err.code}) and the temp dir fallback ('${fallbackDir}') also failed: ${fallbackErr.message}. Set database.filename to a writable path, or use a
|
|
39
|
+
`[ai-ready] Database directory '${dir}' is not writable (${err.code}) and the temp dir fallback ('${fallbackDir}') also failed: ${fallbackErr.message}. Set database.filename to a writable path, or use a remote driver: database.type 'd1' (Cloudflare), 'neon' (Vercel), 'postgres' (PostgreSQL), or 'libsql' (Turso).`
|
|
40
40
|
);
|
|
41
41
|
}
|
|
42
42
|
const fallback = join(fallbackDir, "pages.db");
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { H3Event } from '#nuxtseo/h3';
|
|
2
|
+
import postgres from 'postgres';
|
|
3
|
+
export declare function createClient(event?: H3Event): Promise<{
|
|
4
|
+
dialect: "postgres";
|
|
5
|
+
db: import("drizzle-orm/postgres-js").PostgresJsDatabase<import("drizzle-orm").EmptyRelations> & {
|
|
6
|
+
$client: postgres.Sql<{}>;
|
|
7
|
+
};
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { drizzle } from "drizzle-orm/postgres-js";
|
|
2
|
+
import postgres from "postgres";
|
|
3
|
+
import { useRuntimeConfig } from "#nuxtseo/nitro";
|
|
4
|
+
import { logger } from "../../../logger.js";
|
|
5
|
+
import { registerDriver } from "../raw.js";
|
|
6
|
+
export async function createClient(event) {
|
|
7
|
+
const config = useRuntimeConfig(event)["nuxt-ai-ready"];
|
|
8
|
+
const connectionString = config.database.url || process.env.POSTGRES_URL || process.env.DATABASE_URL;
|
|
9
|
+
if (!connectionString) {
|
|
10
|
+
throw new Error("[ai-ready] Missing database URL. Set DATABASE_URL or configure database.url");
|
|
11
|
+
}
|
|
12
|
+
logger.debug("[drizzle] Connecting to PostgreSQL");
|
|
13
|
+
const sqlClient = postgres(connectionString, { prepare: false });
|
|
14
|
+
const db = drizzle({ client: sqlClient });
|
|
15
|
+
registerDriver(db, "postgres", sqlClient);
|
|
16
|
+
return { dialect: "postgres", db };
|
|
17
|
+
}
|
|
@@ -65,10 +65,6 @@ export declare function countPages(event: H3Event | undefined, options?: {
|
|
|
65
65
|
indexed?: boolean;
|
|
66
66
|
errors?: boolean;
|
|
67
67
|
}): Promise<number>;
|
|
68
|
-
/**
|
|
69
|
-
* Delete a page by route
|
|
70
|
-
*/
|
|
71
|
-
export declare function deletePage(event: H3Event | undefined, route: string): Promise<void>;
|
|
72
68
|
/**
|
|
73
69
|
* Get pending pages (not yet indexed)
|
|
74
70
|
*/
|
|
@@ -100,7 +96,7 @@ export declare function setInfoValue(event: H3Event | undefined, key: string, va
|
|
|
100
96
|
*/
|
|
101
97
|
export declare function deleteInfoValue(event: H3Event | undefined, key: string): Promise<void>;
|
|
102
98
|
/**
|
|
103
|
-
* Initialize database schema. Rebuilds on
|
|
99
|
+
* Initialize database schema. Rebuilds on a schema version change or when the
|
|
104
100
|
* SQLite FTS5 tokenizer differs from the one baked into the existing virtual
|
|
105
101
|
* table (Postgres has no FTS5; tokenizer comparison is SQLite-only).
|
|
106
102
|
*/
|