@nuxthub/core
Version:
Build full-stack Nuxt applications, with zero configuration.
1,036 lines (1,002 loc) • 41.5 kB
JavaScript
import { readFile, writeFile, mkdir, copyFile, stat } from 'node:fs/promises';
import { logger, createResolver, addServerPlugin, getLayerDirectories, updateTemplates, addTemplate, addTypeTemplate, addServerHandler, addServerImports, addImportsDir, defineNuxtModule } from '@nuxt/kit';
import { join, resolve as resolve$1, relative } from 'pathe';
import { defu } from 'defu';
import { readPackageJSON, findWorkspaceDir } from 'pkg-types';
import { provider } from 'std-env';
import chokidar from 'chokidar';
import { glob } from 'tinyglobby';
import { copyDatabaseMigrationsToHubDir, copyDatabaseQueriesToHubDir, copyDatabaseAssets, applyBuildTimeMigrations, getDatabaseSchemaPathMetadata, buildDatabaseSchema } from './db/lib';
import { existsSync } from 'node:fs';
import { createHooks } from 'hookable';
import { getPort } from 'get-port-please';
const version = "0.10.6";
const log$5 = logger.withTag("nuxt:hub");
function logWhenReady(nuxt, message, type = "info") {
if (nuxt.options._prepare) {
return;
}
if (nuxt.options.dev) {
nuxt.hooks.hookOnce("modules:done", () => {
log$5[type](message);
});
} else {
log$5[type](message);
}
}
const { resolve, resolvePath } = createResolver(import.meta.url);
function addWranglerBinding(nuxt, type, binding) {
nuxt.options.nitro.cloudflare ||= {};
nuxt.options.nitro.cloudflare.wrangler ||= {};
nuxt.options.nitro.cloudflare.wrangler[type] ||= [];
const existing = nuxt.options.nitro.cloudflare.wrangler[type];
if (!existing.some((b) => b.binding === binding.binding)) {
existing.push(binding);
}
}
function resolveCacheConfig(hub) {
if (!hub.cache) return false;
const userConfig = typeof hub.cache === "object" ? hub.cache : {};
if (userConfig.driver) {
if (userConfig.driver === "cloudflare-kv-binding") {
return defu(userConfig, { binding: "CACHE" });
}
return userConfig;
}
if (hub.hosting.includes("cloudflare")) {
return defu(userConfig, {
driver: "cloudflare-kv-binding",
binding: "CACHE"
});
}
if (hub.hosting.includes("vercel")) {
return defu(userConfig, {
driver: "vercel-runtime-cache"
});
}
return defu(userConfig, {
driver: "fs-lite",
base: join(hub.dir, "cache")
});
}
async function setupCache(nuxt, hub, _deps) {
hub.cache = resolveCacheConfig(hub);
if (!hub.cache) return;
const cacheConfig = hub.cache;
if (cacheConfig.driver === "cloudflare-kv-binding" && cacheConfig.namespaceId) {
addWranglerBinding(nuxt, "kv_namespaces", { binding: cacheConfig.binding || "CACHE", id: cacheConfig.namespaceId });
}
const { namespaceId: _namespaceId, ...cacheStorageConfig } = cacheConfig;
nuxt.options.nitro.storage ||= {};
nuxt.options.nitro.storage.cache = defu(nuxt.options.nitro.storage.cache, cacheStorageConfig);
if (nuxt.options.dev) {
nuxt.options.nitro.devStorage ||= {};
nuxt.options.nitro.devStorage.cache = defu(nuxt.options.nitro.devStorage.cache, cacheStorageConfig);
}
logWhenReady(nuxt, `\`hub:cache\` using \`${cacheConfig.driver.split("/").pop()}\` driver`);
}
const log$4 = logger.withTag("nuxt:hub");
const cloudflareHooks = createHooks();
function setupCloudflare(nuxt, hub) {
nuxt.options.nitro.cloudflare ||= {};
nuxt.options.nitro.cloudflare.nodeCompat = true;
nuxt.options.nitro.cloudflare.deployConfig = true;
nuxt.options.nitro.prerender ||= {};
nuxt.options.nitro.prerender.autoSubfolderIndex ||= false;
if (!hub.hosting.includes("pages")) {
nuxt.options.nitro.cloudflare.wrangler = defu(nuxt.options.nitro.cloudflare.wrangler, {
compatibility_flags: ["nodejs_compat"]
});
}
if (nuxt.options.dev || nuxt.options._prepare) {
return;
}
nuxt.hook("close", async (nuxt2) => {
const cloudflareEnv = process.env.CLOUDFLARE_ENV;
await processWranglerConfigFile(nuxt2, cloudflareEnv);
});
}
const NON_INHERITABLE_KEYS = [
"define",
"vars",
"durable_objects",
"workflows",
"cloudchamber",
"containers",
"kv_namespaces",
"send_email",
"queues",
"r2_buckets",
"d1_databases",
"vectorize",
"hyperdrive",
"services",
"analytics_engine_datasets",
"browser",
"ai",
"images",
"version_metadata",
"unsafe",
"mtls_certificates",
"tail_consumers",
"dispatch_namespaces",
"pipelines",
"secrets_store_secrets"
];
function processWranglerConfigEnv(config, targetEnv) {
if (!config.env || Object.keys(config.env).length === 0) {
const { env: _2, ...rest } = config;
return rest;
}
const envConfig = config.env[targetEnv];
if (!envConfig) {
const { env: _2, ...rest } = config;
return rest;
}
const { env: _, ...baseConfig } = config;
const nonInheritableSet = new Set(NON_INHERITABLE_KEYS);
const filteredConfig = Object.fromEntries(
Object.entries(baseConfig).filter(([key]) => !nonInheritableSet.has(key))
);
return { ...filteredConfig, ...envConfig };
}
async function processWranglerConfigFile(nuxt, targetEnv) {
const wranglerPath = join(nuxt.options.rootDir, ".output", "server", "wrangler.json");
if (!existsSync(wranglerPath)) {
log$4.warn(`No wrangler.json found at ${wranglerPath}, skipping wrangler processing`);
return;
}
try {
const content = await readFile(wranglerPath, "utf-8");
let config = JSON.parse(content);
if (targetEnv && config.env?.[targetEnv]) {
config = processWranglerConfigEnv(config, targetEnv);
log$4.info(`Using wrangler environment \`${targetEnv}\``);
} else {
if (config.env) {
const { env: _, ...rest } = config;
config = rest;
}
if (targetEnv) {
log$4.info(`No environment \`${targetEnv}\` found in wrangler config, using top-level configuration`);
}
}
await cloudflareHooks.callHook("wrangler:config", config);
await writeFile(wranglerPath, JSON.stringify(config, null, 2), "utf-8");
} catch (error) {
log$4.error(`Failed to process wrangler config: ${error}`);
}
}
const log$3 = logger.withTag("nuxt:hub");
function generateLazyDbTemplate(imports, getDbBody) {
return `${imports}
import * as schema from './db/schema.mjs'
let _db
function getDb() {
if (!_db) {
try {
${getDbBody}
} catch (e) { throw new Error('[nuxt-hub] ' + e.message) }
}
return _db
}
const db = new Proxy({}, { get(_, prop) { return getDb()[prop] } })
export { db, schema }
`;
}
async function resolveDatabaseConfig(nuxt, hub) {
if (!hub.db) return false;
let config = typeof hub.db === "string" ? { dialect: hub.db } : hub.db;
config = defu(config, {
migrationsDirs: getLayerDirectories(nuxt).map((layer) => join(layer.server, "db/migrations")),
queriesPaths: [],
applyMigrationsDuringBuild: true
});
switch (config.dialect) {
case "sqlite": {
const userExplicitLibsql = config.driver === "libsql";
if (process.env.TURSO_DATABASE_URL && process.env.TURSO_AUTH_TOKEN) {
config.driver = "libsql";
config.connection = defu(config.connection, {
url: process.env.TURSO_DATABASE_URL,
authToken: process.env.TURSO_AUTH_TOKEN
});
break;
}
if (config.driver === "d1-http") {
config.connection = defu(config.connection, {
accountId: process.env.NUXT_HUB_CLOUDFLARE_ACCOUNT_ID || void 0,
apiToken: process.env.NUXT_HUB_CLOUDFLARE_API_TOKEN || void 0,
databaseId: process.env.NUXT_HUB_CLOUDFLARE_DATABASE_ID || void 0
});
if (!config.connection?.accountId || !config.connection?.apiToken || !config.connection?.databaseId) {
throw new Error("D1 HTTP driver requires NUXT_HUB_CLOUDFLARE_ACCOUNT_ID, NUXT_HUB_CLOUDFLARE_API_TOKEN, and NUXT_HUB_CLOUDFLARE_DATABASE_ID environment variables");
}
break;
}
if (hub.hosting.includes("cloudflare") && !nuxt.options.dev && !nuxt.options._prepare) {
config.driver = "d1";
break;
}
if (userExplicitLibsql) {
config.connection = defu(config.connection, { url: "" });
break;
}
config.driver ||= "libsql";
config.connection = defu(config.connection, { url: `file:${join(hub.dir, "db/sqlite.db")}` });
await mkdir(join(hub.dir, "db"), { recursive: true });
break;
}
case "postgresql": {
if (hub.hosting.includes("cloudflare") && config.connection?.hyperdriveId && !config.driver) {
config.driver = "postgres-js";
break;
}
config.connection = defu(config.connection, { url: process.env.POSTGRES_URL || process.env.POSTGRESQL_URL || process.env.DATABASE_URL || "" });
if (config.applyMigrationsDuringBuild && config.driver && ["neon-http", "postgres-js"].includes(config.driver) && !config.connection.url) {
throw new Error(`\`${config.driver}\` driver requires \`DATABASE_URL\`, \`POSTGRES_URL\`, or \`POSTGRESQL_URL\` environment variable when \`applyMigrationsDuringBuild\` is enabled`);
}
if (config.connection.url) {
config.driver ||= "postgres-js";
break;
}
config.driver ||= "pglite";
config.connection = defu(config.connection, { dataDir: join(hub.dir, "db/pglite") });
await mkdir(join(hub.dir, "db/pglite"), { recursive: true });
break;
}
case "mysql": {
if (hub.hosting.includes("cloudflare") && config.connection?.hyperdriveId && !config.driver) {
config.driver = "mysql2";
break;
}
config.driver ||= "mysql2";
config.connection = defu(config.connection, { uri: process.env.MYSQL_URL || process.env.DATABASE_URL || "" });
if (config.applyMigrationsDuringBuild && !config.connection.uri) {
throw new Error("MySQL requires DATABASE_URL or MYSQL_URL environment variable when `applyMigrationsDuringBuild` is enabled");
}
break;
}
}
if (config.driver === "d1") {
config.applyMigrationsDuringBuild = false;
}
return config;
}
async function setupDatabase(nuxt, hub, deps) {
hub.db = await resolveDatabaseConfig(nuxt, hub);
if (!hub.db) return;
const { dialect, driver, connection, migrationsDirs, queriesPaths } = hub.db;
logWhenReady(nuxt, `\`hub:db\` using \`${dialect}\` database with \`${driver}\` driver`, "info");
if (driver === "d1" && connection?.databaseId) {
addWranglerBinding(nuxt, "d1_databases", { binding: "DB", database_id: connection.databaseId });
}
if (["postgres-js", "mysql2"].includes(driver) && connection?.hyperdriveId) {
const binding = driver === "postgres-js" ? "POSTGRES" : "MYSQL";
addWranglerBinding(nuxt, "hyperdrive", { binding, id: connection.hyperdriveId });
}
if (!deps["drizzle-orm"] || !deps["drizzle-kit"]) {
logWhenReady(nuxt, "Please run `npx nypm i drizzle-orm drizzle-kit` to properly setup Drizzle ORM with NuxtHub.", "error");
}
if (driver === "postgres-js" && !deps["postgres"]) {
logWhenReady(nuxt, "Please run `npx nypm i postgres` to use PostgreSQL as database.", "error");
} else if (driver === "neon-http" && !deps["@neondatabase/serverless"]) {
logWhenReady(nuxt, "Please run `npx nypm i @neondatabase/serverless` to use Neon serverless database.", "error");
} else if (driver === "pglite" && !deps["@electric-sql/pglite"]) {
logWhenReady(nuxt, "Please run `npx nypm i @electric-sql/pglite` to use PGlite as database.", "error");
} else if (driver === "mysql2" && !deps.mysql2) {
logWhenReady(nuxt, "Please run `npx nypm i mysql2` to use MySQL as database.", "error");
} else if (driver === "libsql" && !deps["@libsql/client"]) {
logWhenReady(nuxt, "Please run `npx nypm i @libsql/client` to use SQLite as database.", "error");
}
addServerPlugin(resolve("db/runtime/plugins/migrations.dev"));
nuxt.hook("modules:done", async () => {
await generateDatabaseSchema(nuxt, hub);
await nuxt.callHook("hub:db:migrations:dirs", migrationsDirs);
await copyDatabaseMigrationsToHubDir(hub);
await nuxt.callHook("hub:db:queries:paths", queriesPaths, dialect);
await copyDatabaseQueriesToHubDir(hub);
});
nuxt.hook("nitro:build:public-assets", async (nitro) => {
await copyDatabaseAssets(nitro, hub);
await applyBuildTimeMigrations(nitro, hub);
});
if (driver === "d1") {
cloudflareHooks.hook("wrangler:config", (config) => {
const d1Databases = config.d1_databases;
if (!d1Databases?.length) return;
const dbBinding = d1Databases.find((db) => db.binding === "DB");
if (dbBinding) {
dbBinding.migrations_table ||= "_hub_migrations";
dbBinding.migrations_dir ||= ".output/server/db/migrations/";
}
});
}
await setupDatabaseClient(nuxt, hub);
await setupDatabaseConfig(nuxt, hub);
}
async function generateDatabaseSchema(nuxt, hub) {
if (!hub.db) return;
const dialect = hub.db.dialect;
const getSchemaPaths = async () => {
const schemaPatterns = getLayerDirectories(nuxt).map((layer) => [
resolve$1(layer.server, "db/schema.ts"),
resolve$1(layer.server, `db/schema.${dialect}.ts`),
resolve$1(layer.server, "db/schema/*.ts")
]).flat();
let schemaPaths2 = await glob(schemaPatterns, { absolute: true, onlyFiles: true });
await nuxt.callHook("hub:db:schema:extend", { dialect, paths: schemaPaths2 });
schemaPaths2 = schemaPaths2.filter((path) => {
const meta = getDatabaseSchemaPathMetadata(path);
return !meta.dialect || meta.dialect === dialect;
});
return schemaPaths2;
};
let schemaPaths = await getSchemaPaths();
if (nuxt.options.dev && !nuxt.options._prepare) {
const watchDirs = getLayerDirectories(nuxt).map((layer) => resolve$1(layer.server, "db"));
const watcher = chokidar.watch(watchDirs, {
ignoreInitial: true
});
watcher.on("all", async (event, path) => {
if (!path.endsWith("db/schema.ts") && !path.endsWith(`db/schema.${dialect}.ts`) && !path.includes("/db/schema/")) return;
if (["add", "unlink", "change"].includes(event) === false) return;
const meta = getDatabaseSchemaPathMetadata(path);
if (meta.dialect && meta.dialect !== dialect) return;
log$3.info(`Database schema ${event === "add" ? "added" : event === "unlink" ? "removed" : "changed"}: \`${relative(nuxt.options.rootDir, path)}\``);
log$3.info("Make sure to run `npx nuxt db generate` to generate the database migrations.");
schemaPaths = await getSchemaPaths();
await updateTemplates({ filter: (template) => template.filename.includes("hub/db/schema.entry.ts") });
await buildDatabaseSchema(nuxt.options.buildDir, { relativeDir: nuxt.options.rootDir, alias: nuxt.options.alias });
const physicalDbDir = join(nuxt.options.rootDir, "node_modules", "@nuxthub", "db");
try {
await copyFile(join(nuxt.options.buildDir, "hub/db/schema.mjs"), join(physicalDbDir, "schema.mjs"));
await copyFile(join(nuxt.options.buildDir, "hub/db/schema.d.mts"), join(physicalDbDir, "schema.d.mts"));
} catch (error) {
}
});
nuxt.hook("close", () => watcher.close());
}
addTemplate({
filename: "hub/db/schema.entry.ts",
getContents: () => `${schemaPaths.map((path) => `export * from '${path}'`).join("\n")}`,
write: true
});
nuxt.hooks.hookOnce("app:templatesGenerated", async () => {
await buildDatabaseSchema(nuxt.options.buildDir, { relativeDir: nuxt.options.rootDir, alias: nuxt.options.alias });
const physicalDbDir = join(nuxt.options.rootDir, "node_modules", "@nuxthub", "db");
await mkdir(physicalDbDir, { recursive: true });
try {
await copyFile(join(nuxt.options.buildDir, "hub/db/schema.mjs"), join(physicalDbDir, "schema.mjs"));
const buildDirSource = join(nuxt.options.buildDir, "hub/db/schema.d.mts");
const nuxtDirSource = join(nuxt.options.rootDir, ".nuxt/hub/db/schema.d.mts");
let schemaTypes;
try {
schemaTypes = await readFile(buildDirSource, "utf-8");
} catch {
try {
schemaTypes = await readFile(nuxtDirSource, "utf-8");
} catch {
}
}
if (schemaTypes && schemaTypes.length > 50) {
await writeFile(join(physicalDbDir, "schema.d.mts"), schemaTypes);
} else if (!nuxt.options.test) {
await writeFile(join(physicalDbDir, "schema.d.mts"), `export * from './schema.mjs'`);
}
} catch (error) {
log$3.warn(`Failed to copy schema to node_modules/.hub/: ${error}`);
}
});
nuxt.options.alias ||= {};
addTypeTemplate({
filename: "hub/db/schema.d.ts",
getContents: () => `declare module 'hub:db:schema' {
export * from '#build/hub/db/schema.mjs'
}`
}, { nitro: true, nuxt: true });
nuxt.options.alias["hub:db:schema"] = "@nuxthub/db/schema";
}
async function setupDatabaseClient(nuxt, hub) {
const { dialect, driver, connection, mode, casing, replicas } = hub.db;
const driverForTypes = driver === "d1-http" ? "sqlite-proxy" : driver;
const databaseTypes = `declare module 'hub:db' {
export * from '@nuxthub/db'
}`;
addTypeTemplate({
filename: "hub/db.d.ts",
getContents: () => databaseTypes
}, { nitro: true, nuxt: true });
const modeOption = dialect === "mysql" ? `, mode: '${mode || "default"}'` : "";
const casingOption = casing ? `, casing: '${casing}'` : "";
let drizzleOrmContent = `import { drizzle } from 'drizzle-orm/${driver}'
import * as schema from './db/schema.mjs'
const db = drizzle({ connection: ${JSON.stringify(connection)}, schema${modeOption}${casingOption} })
export { db, schema }
`;
if (driver === "pglite" && nuxt.options.dev) {
drizzleOrmContent = `import { drizzle } from 'drizzle-orm/pglite'
import { PGlite } from '@electric-sql/pglite'
import * as schema from './db/schema.mjs'
const client = new PGlite(${JSON.stringify(connection.dataDir)})
const db = drizzle({ client, schema${casingOption} })
export { db, schema, client }
`;
addServerHandler({
handler: await resolvePath("db/runtime/api/launch-studio.post.dev"),
method: "post",
route: "/api/_hub/db/launch-studio"
});
}
if (driver === "postgres-js" && nuxt.options.dev) {
const replicaUrls = (replicas || []).filter(Boolean);
const hasReplicas = replicaUrls.length > 0;
drizzleOrmContent = `import { drizzle } from 'drizzle-orm/postgres-js'
${hasReplicas ? `import { withReplicas } from 'drizzle-orm/pg-core'
` : ""}import postgres from 'postgres'
import * as schema from './db/schema.mjs'
const client = postgres('${connection.url}', { onnotice: () => {} })
${hasReplicas ? `const primary = drizzle({ client, schema${casingOption} })
const replicaUrls = ${JSON.stringify(replicaUrls)}
const replicaConnections = replicaUrls.map(replicaUrl => {
const replicaClient = postgres(replicaUrl, { onnotice: () => {} })
return drizzle({ client: replicaClient, schema${casingOption} })
})
const db = withReplicas(primary, replicaConnections)` : `const db = drizzle({ client, schema${casingOption} })`}
export { db, schema }
`;
}
if (driver === "mysql2" && nuxt.options.dev) {
const replicaUrls = (replicas || []).filter(Boolean);
const hasReplicas = replicaUrls.length > 0;
if (hasReplicas) {
drizzleOrmContent = `import { drizzle } from 'drizzle-orm/mysql2'
import { withReplicas } from 'drizzle-orm/mysql-core'
import * as schema from './db/schema.mjs'
const primary = drizzle({ connection: ${JSON.stringify(connection)}, schema${modeOption}${casingOption} })
const replicaUrls = ${JSON.stringify(replicaUrls)}
const replicaConnections = replicaUrls.map(replicaUrl => {
return drizzle({ connection: { uri: replicaUrl }, schema${modeOption}${casingOption} })
})
const db = withReplicas(primary, replicaConnections)
export { db, schema }
`;
}
}
if (driver === "neon-http") {
const urlExpr = connection.url ? `'${connection.url}'` : `process.env.POSTGRES_URL || process.env.POSTGRESQL_URL || process.env.DATABASE_URL`;
drizzleOrmContent = generateLazyDbTemplate(
`import { neon } from '@neondatabase/serverless'
import { drizzle } from 'drizzle-orm/neon-http'`,
` const url = ${urlExpr}
if (!url) throw new Error('DATABASE_URL, POSTGRES_URL, or POSTGRESQL_URL required')
const sql = neon(url)
_db = drizzle(sql, { schema${casingOption} })`
);
}
if (driver === "d1") {
drizzleOrmContent = generateLazyDbTemplate(
`import { drizzle } from 'drizzle-orm/d1'`,
` const binding = process.env.DB || globalThis.__env__?.DB || globalThis.DB
if (!binding) throw new Error('DB binding not found')
_db = drizzle(binding, { schema${casingOption} })`
);
}
if (driver === "d1-http") {
drizzleOrmContent = `import { drizzle } from 'drizzle-orm/sqlite-proxy'
import * as schema from './db/schema.mjs'
const accountId = ${JSON.stringify(connection.accountId)}
const databaseId = ${JSON.stringify(connection.databaseId)}
const apiToken = ${JSON.stringify(connection.apiToken)}
async function d1HttpDriver(sql, params, method) {
if (method === 'values') method = 'all'
const { errors, success, result } = await $fetch(\`https://api.cloudflare.com/client/v4/accounts/\${accountId}/d1/database/\${databaseId}/raw\`, {
method: 'POST',
headers: {
Authorization: \`Bearer \${apiToken}\`,
'Content-Type': 'application/json'
},
async onResponseError({ request, response, options }) {
console.error(
"D1 HTTP Error:",
request,
options.body,
response.status,
response._data,
)
},
body: { sql, params }
})
if (errors?.length > 0 || !success) {
throw new Error(\`D1 HTTP error: \${JSON.stringify({ errors, success, result })}\`)
}
const queryResult = result?.[0]
if (!queryResult?.success) {
throw new Error(\`D1 HTTP error: \${JSON.stringify({ errors, success, result })}\`)
}
const rows = queryResult.results?.rows || []
if (method === 'get') {
if (rows.length === 0) {
return { rows: [] }
}
return { rows: rows[0] }
}
return { rows }
}
const db = drizzle(d1HttpDriver, { schema${casingOption} })
export { db, schema }
`;
}
if (["postgres-js", "mysql2"].includes(driver) && hub.hosting.includes("cloudflare") && connection?.hyperdriveId) {
const bindingName = driver === "postgres-js" ? "POSTGRES" : "MYSQL";
drizzleOrmContent = generateLazyDbTemplate(
`import { drizzle } from 'drizzle-orm/${driver}'`,
` const hyperdrive = process.env.${bindingName} || globalThis.__env__?.${bindingName} || globalThis.${bindingName}
if (!hyperdrive) throw new Error('${bindingName} binding not found')
_db = drizzle({ connection: hyperdrive.connectionString, schema${modeOption}${casingOption} })`
);
}
if (driver === "postgres-js" && !nuxt.options.dev && !hub.hosting.includes("cloudflare")) {
const urlExpr = connection.url ? `'${connection.url}'` : `process.env.POSTGRES_URL || process.env.POSTGRESQL_URL || process.env.DATABASE_URL`;
const replicaUrls = (replicas || []).filter(Boolean);
const hasReplicas = replicaUrls.length > 0;
drizzleOrmContent = generateLazyDbTemplate(
`import { drizzle } from 'drizzle-orm/postgres-js'
${hasReplicas ? `import { withReplicas } from 'drizzle-orm/pg-core'
` : ""}import postgres from 'postgres'`,
` const url = ${urlExpr}
if (!url) throw new Error('DATABASE_URL, POSTGRES_URL, or POSTGRESQL_URL required')
const client = postgres(url, { onnotice: () => {} })
${hasReplicas ? ` const primary = drizzle({ client, schema${casingOption} })
const replicaUrls = ${JSON.stringify(replicaUrls)}
const replicaConnections = replicaUrls.map(replicaUrl => {
const replicaClient = postgres(replicaUrl, { onnotice: () => {} })
return drizzle({ client: replicaClient, schema${casingOption} })
})
_db = withReplicas(primary, replicaConnections)` : ` _db = drizzle({ client, schema${casingOption} })`}`
);
}
if (driver === "mysql2" && !nuxt.options.dev && !hub.hosting.includes("cloudflare")) {
const uriExpr = connection.uri ? `'${connection.uri}'` : `process.env.MYSQL_URL || process.env.DATABASE_URL`;
const replicaUrls = (replicas || []).filter(Boolean);
const hasReplicas = replicaUrls.length > 0;
drizzleOrmContent = generateLazyDbTemplate(
`import { drizzle } from 'drizzle-orm/mysql2'${hasReplicas ? `
import { withReplicas } from 'drizzle-orm/mysql-core'` : ""}`,
` const uri = ${uriExpr}
if (!uri) throw new Error('DATABASE_URL or MYSQL_URL required')
${hasReplicas ? ` const primary = drizzle({ connection: { uri }, schema${modeOption}${casingOption} })
const replicaUrls = ${JSON.stringify(replicaUrls)}
const replicaConnections = replicaUrls.map(replicaUrl => {
return drizzle({ connection: { uri: replicaUrl }, schema${modeOption}${casingOption} })
})
_db = withReplicas(primary, replicaConnections)` : ` _db = drizzle({ connection: { uri }, schema${modeOption}${casingOption} })`}`
);
}
if (driver === "libsql" && !connection.url) {
drizzleOrmContent = generateLazyDbTemplate(
`import { drizzle } from 'drizzle-orm/libsql'`,
` const url = process.env.TURSO_DATABASE_URL || process.env.LIBSQL_URL || process.env.DATABASE_URL
const authToken = process.env.TURSO_AUTH_TOKEN || process.env.LIBSQL_AUTH_TOKEN
if (!url) throw new Error('Database URL not found. Set TURSO_DATABASE_URL, LIBSQL_URL, or DATABASE_URL')
_db = drizzle({ connection: { url, authToken }, schema${casingOption} })`
);
}
const physicalDbDir = join(nuxt.options.rootDir, "node_modules", "@nuxthub", "db");
await mkdir(physicalDbDir, { recursive: true });
await writeFile(
join(physicalDbDir, "db.mjs"),
drizzleOrmContent.replace(/from '\.\/db\/schema\.mjs'/g, "from './schema.mjs'")
);
const physicalDbTypes = `import type { DrizzleConfig } from 'drizzle-orm'
import { drizzle as drizzleCore } from 'drizzle-orm/${driverForTypes}'
import * as schema from './schema.mjs'
/**
* The database schema object
* Defined in server/db/schema.ts and server/db/schema/*.ts
*/
export { schema }
/**
* The ${driver} database client.
*/
export const db: ReturnType<typeof drizzleCore<typeof schema>>
`;
await writeFile(
join(physicalDbDir, "db.d.ts"),
physicalDbTypes
);
const packageJson = {
name: "@nuxthub/db",
version: "0.0.0",
type: "module",
exports: {
".": { types: "./db.d.ts", default: "./db.mjs" },
"./schema": { types: "./schema.d.mts", default: "./schema.mjs" }
}
};
try {
await writeFile(join(physicalDbDir, "package.json"), JSON.stringify(packageJson, null, 2));
const schemaPath = join(physicalDbDir, "schema.mjs");
const schemaDtsPath = join(physicalDbDir, "schema.d.mts");
const schemaExists = await stat(schemaPath).then((s) => s.size > 20).catch(() => false);
const schemaDtsExists = await stat(schemaDtsPath).then((s) => s.size > 20).catch(() => false);
if (!schemaExists) await writeFile(schemaPath, "export {}");
if (!schemaDtsExists) await writeFile(schemaDtsPath, "export {}");
} catch (error) {
throw new Error(`Failed to create @nuxthub/db package files: ${error.message}`);
}
nuxt.options.alias["hub:db"] = "@nuxthub/db";
addServerImports({ name: "db", from: "@nuxthub/db", meta: { description: `The ${driver} database client.` } });
addServerImports({ name: "schema", from: "@nuxthub/db", meta: { description: `The database schema object` } });
}
async function setupDatabaseConfig(nuxt, hub) {
const { dialect, casing } = hub.db;
const casingConfig = casing ? `
casing: '${casing}',` : "";
addTemplate({
filename: "hub/db/drizzle.config.ts",
write: true,
getContents: () => `import { defineConfig } from 'drizzle-kit'
export default defineConfig({
dialect: '${dialect}',${casingConfig}
schema: '${relative(nuxt.options.rootDir, resolve(nuxt.options.buildDir, "hub/db/schema.mjs"))}',
out: '${relative(nuxt.options.rootDir, resolve(nuxt.options.rootDir, `server/db/migrations/${dialect}`))}'
});`
});
}
function resolveKVConfig(hub) {
if (!hub.kv) return false;
if (typeof hub.kv === "object" && "driver" in hub.kv) {
if (hub.kv.driver === "cloudflare-kv-binding") {
return defu(hub.kv, { binding: "KV" });
}
return hub.kv;
}
if (process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN || process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN) {
return defu(hub.kv, {
driver: "upstash",
url: process.env.KV_REST_API_URL || process.env.UPSTASH_REDIS_REST_URL,
token: process.env.KV_REST_API_TOKEN || process.env.UPSTASH_REDIS_REST_TOKEN
});
}
if (process.env.REDIS_URL || process.env.KV_URL?.startsWith("rediss://")) {
return defu(hub.kv, {
driver: "redis",
url: process.env.REDIS_URL || process.env.KV_URL
});
}
if (process.env.S3_ACCESS_KEY_ID && process.env.S3_SECRET_ACCESS_KEY && process.env.S3_BUCKET && process.env.S3_REGION) {
return defu(hub.kv, {
driver: "s3",
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
bucket: process.env.S3_BUCKET,
region: process.env.S3_REGION,
endpoint: process.env.S3_ENDPOINT || void 0
});
}
if (hub.hosting.includes("cloudflare")) {
return defu(hub.kv, {
driver: "cloudflare-kv-binding",
binding: "KV"
});
}
if (hub.hosting.includes("deno")) {
return defu(hub.kv, {
driver: "deno-kv"
});
}
return defu(hub.kv, {
driver: "fs-lite",
base: ".data/kv"
});
}
async function setupKV(nuxt, hub, deps) {
hub.kv = resolveKVConfig(hub);
if (!hub.kv) return;
const kvConfig = hub.kv;
if (kvConfig.driver === "cloudflare-kv-binding" && kvConfig.namespaceId) {
addWranglerBinding(nuxt, "kv_namespaces", { binding: kvConfig.binding || "KV", id: kvConfig.namespaceId });
}
if (kvConfig.driver === "upstash" && !deps["@upstash/redis"]) {
logWhenReady(nuxt, "Please run `npx nypm i @upstash/redis` to use Upstash Redis KV storage", "error");
}
if (kvConfig.driver === "redis" && !deps["ioredis"]) {
logWhenReady(nuxt, "Please run `npx nypm i ioredis` to use Redis KV storage", "error");
}
if (hub.hosting.includes("vercel") && kvConfig.driver === "fs-lite") {
logWhenReady(nuxt, "Vercel hosting requires a Redis connection. Please set the `REDIS_URL` environment variable. See https://vercel.com/marketplace/category/database", "error");
}
const { namespaceId: _namespaceId, ...kvStorageConfig } = kvConfig;
nuxt.options.nitro.storage ||= {};
nuxt.options.nitro.storage.kv = defu(nuxt.options.nitro.storage.kv, kvStorageConfig);
const { driver, ...driverOptions } = kvStorageConfig;
const kvContent = `import { createStorage } from "unstorage"
import driver from "unstorage/drivers/${driver}";
export const kv = createStorage({
driver: driver(${JSON.stringify(driverOptions)}),
});
`;
const physicalKvDir = join(nuxt.options.rootDir, "node_modules", "@nuxthub", "kv");
await mkdir(physicalKvDir, { recursive: true });
await writeFile(
join(physicalKvDir, "kv.mjs"),
kvContent
);
await copyFile(
resolve("kv/runtime/kv.d.ts"),
join(physicalKvDir, "kv.d.ts")
);
const packageJson = {
name: "@nuxthub/kv",
version: "0.0.0",
type: "module",
exports: {
".": {
types: "./kv.d.ts",
default: "./kv.mjs"
}
}
};
await writeFile(
join(physicalKvDir, "package.json"),
JSON.stringify(packageJson, null, 2)
);
nuxt.options.alias["hub:kv"] = "@nuxthub/kv";
addServerImports({ name: "kv", from: "@nuxthub/kv", meta: { description: `The Key-Value storage instance.` } });
addTypeTemplate({
filename: "hub/kv.d.ts",
getContents: () => `declare module 'hub:kv' {
export * from '@nuxthub/kv'
}`
}, { nitro: true, nuxt: true });
logWhenReady(nuxt, `\`hub:kv\` using \`${driver}\` driver`);
}
const log$2 = logger.withTag("nuxt:hub");
const supportedDrivers = ["fs", "s3", "vercel-blob", "cloudflare-r2"];
function resolveBlobConfig(hub, deps) {
if (!hub.blob) return false;
if (typeof hub.blob === "object" && "driver" in hub.blob) {
return hub.blob;
}
if (process.env.S3_ACCESS_KEY_ID && process.env.S3_SECRET_ACCESS_KEY && (process.env.S3_BUCKET || process.env.S3_ENDPOINT)) {
if (!deps["aws4fetch"]) {
log$2.error("Please run `npx nypm i aws4fetch` to use S3");
}
return defu(hub.blob, {
driver: "s3",
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
bucket: process.env.S3_BUCKET || "",
region: process.env.S3_REGION || "auto",
endpoint: process.env.S3_ENDPOINT
});
}
if (hub.hosting.includes("vercel") || process.env.BLOB_READ_WRITE_TOKEN) {
if (!deps["@vercel/blob"]) {
log$2.error("Please run `npx nypm i @vercel/blob` to use Vercel Blob");
}
return defu(hub.blob, {
driver: "vercel-blob",
access: "public"
});
}
if (hub.hosting.includes("cloudflare")) {
return defu(hub.blob, {
driver: "cloudflare-r2",
binding: "BLOB"
});
}
return defu(hub.blob, {
driver: "fs",
dir: join(hub.dir, "blob")
});
}
async function setupBlob(nuxt, hub, deps) {
hub.blob = resolveBlobConfig(hub, deps);
if (!hub.blob) return;
const blobConfig = hub.blob;
if (blobConfig.driver === "cloudflare-r2" && blobConfig.bucketName) {
addWranglerBinding(nuxt, "r2_buckets", { binding: blobConfig.binding || "BLOB", bucket_name: blobConfig.bucketName });
}
addImportsDir(resolve("blob/runtime/app/composables"));
const { driver, bucketName: _bucketName, ...driverOptions } = blobConfig;
if (!supportedDrivers.includes(driver)) {
log$2.error(`Unsupported blob driver: ${driver}. Supported drivers: ${supportedDrivers.join(", ")}`);
return;
}
const blobContent = `import { createBlobStorage } from "@nuxthub/core/blob";
import { createDriver } from "@nuxthub/core/blob/drivers/${driver}";
export { ensureBlob } from "@nuxthub/core/blob";
export const blob = createBlobStorage(createDriver(${JSON.stringify(driverOptions)}));
`;
const physicalBlobDir = join(nuxt.options.rootDir, "node_modules", "@nuxthub", "blob");
await mkdir(physicalBlobDir, { recursive: true });
await writeFile(join(physicalBlobDir, "blob.mjs"), blobContent);
await copyFile(
resolve("blob/runtime/blob.d.ts"),
join(physicalBlobDir, "blob.d.ts")
);
const packageJson = {
name: "@nuxthub/blob",
version: "0.0.0",
type: "module",
exports: {
".": {
types: "./blob.d.ts",
default: "./blob.mjs"
}
}
};
await writeFile(join(physicalBlobDir, "package.json"), JSON.stringify(packageJson, null, 2));
nuxt.options.alias["hub:blob"] = "@nuxthub/blob";
addServerImports({ name: "blob", from: "@nuxthub/blob", meta: { description: `The Blob storage instance.` } });
addServerImports({ name: "ensureBlob", from: "@nuxthub/blob", meta: { description: `Ensure the blob is valid and meets the specified requirements.` } });
addTypeTemplate({
filename: "hub/blob.d.ts",
getContents: () => `declare module 'hub:blob' {
export * from '@nuxthub/blob'
}`
}, { nitro: true, nuxt: true });
if (blobConfig.driver === "vercel-blob") {
nuxt.options.runtimeConfig.public.hub ||= {};
nuxt.options.runtimeConfig.public.hub.blobProvider = "vercel-blob";
logWhenReady(nuxt, "Files stored in Vercel Blob are public. Manually configure a different storage driver if storing sensitive files.", "warn");
}
logWhenReady(nuxt, `\`hub:blob\` using \`${blobConfig.driver}\` driver`);
}
let isReady = false;
let promise = null;
let port = 4983;
const log$1 = logger.withTag("nuxt:hub");
async function launchDrizzleStudio(nuxt, hub) {
const dbConfig = hub.db;
if (!dbConfig || typeof dbConfig === "boolean" || typeof dbConfig === "string") {
throw new Error("Database configuration not resolved properly. Please ensure database is configured in hub.db");
}
port = await getPort({ port: 4983 });
try {
const { dialect, driver, connection } = dbConfig;
const { schema } = await import(nuxt.options.alias["hub:db"]);
if (dialect === "postgresql") {
if (driver === "pglite") {
log$1.info(`Launching Drizzle Studio with PGlite...`);
const nitroDevUrl = `${nuxt.options.devServer.https ? "https" : "http"}://localhost:${nuxt.options.devServer.port || 3e3}`;
await fetch(`${nitroDevUrl}/api/_hub/db/launch-studio?port=${port}`, {
method: "POST"
});
} else {
const { startStudioPostgresServer } = await import('drizzle-kit/api');
log$1.info(`Launching Drizzle Studio with PostgreSQL...`);
await startStudioPostgresServer(schema, connection, { port });
}
} else if (dialect === "mysql") {
const { startStudioMySQLServer } = await import('drizzle-kit/api');
log$1.info(`Launching Drizzle Studio with MySQL...`);
await startStudioMySQLServer(schema, connection, { port });
} else if (dialect === "sqlite") {
const { startStudioSQLiteServer } = await import('drizzle-kit/api');
log$1.info(`Launching Drizzle Studio with SQLite (${driver})...`);
let studioConnection;
if (driver === "d1-http") {
studioConnection = { driver: "d1-http", ...connection };
} else if (driver === "d1") {
const d1Files = await glob(".wrangler/state/v3/d1/miniflare-D1DatabaseObject/*.sqlite", {
cwd: nuxt.options.rootDir,
absolute: true
});
if (!d1Files.length) {
log$1.warn("D1 database file not found. Run the dev server first to create it.");
return;
}
studioConnection = { url: `file:${d1Files[0]}` };
} else {
studioConnection = connection;
}
await startStudioSQLiteServer(schema, studioConnection, { port });
} else {
throw new Error(`Unsupported database dialect: ${dialect}`);
}
isReady = true;
} catch (error) {
log$1.error("Failed to launch Drizzle Studio:", error);
throw error;
}
}
function addDevToolsCustomTabs(nuxt, hub) {
nuxt.hook("devtools:customTabs", (tabs) => {
if (nuxt.options.nitro.experimental?.openAPI) ;
if (hub.db) tabs.push({
category: "server",
name: "hub-db",
title: "Database",
icon: "i-lucide-database",
view: isReady && port ? {
type: "iframe",
src: `https://local.drizzle.studio?port=${port}`,
permissions: ["local-network-access https://local.drizzle.studio"]
} : {
type: "launch",
description: "Launch Drizzle Studio",
actions: [{
label: promise ? "Starting..." : "Launch",
pending: isReady,
handle() {
promise = promise || launchDrizzleStudio(nuxt, hub);
return promise;
}
}]
}
});
});
}
const log = logger.withTag("nuxt:hub");
const module$1 = defineNuxtModule({
meta: {
name: "@nuxthub/core",
configKey: "hub",
version,
docs: "https://hub.nuxt.com"
},
defaults: {},
async setup(options, nuxt) {
if (nuxt.options.nitro.static || nuxt.options._generate) {
log.error("NuxtHub is not compatible with `nuxt generate` as it needs a server to run.");
log.info("To pre-render all pages: `https://hub.nuxt.com/docs/recipes/pre-rendering#pre-render-all-pages`");
return process.exit(1);
}
const rootDir = nuxt.options.rootDir;
const hosting = process.env.NITRO_PRESET || nuxt.options.nitro.preset || provider;
const hub = defu(options, {
// Local storage
dir: ".data",
hosting,
// NuxtHub features
blob: false,
cache: false,
db: false,
kv: false
});
hub.dir = await resolve$1(nuxt.options.rootDir, hub.dir);
await mkdir(hub.dir, { recursive: true }).catch((e) => {
if (e.errno !== -17) throw e;
});
const packageJSON = await readPackageJSON(nuxt.options.rootDir);
const deps = Object.assign({}, packageJSON.dependencies, packageJSON.devDependencies);
await setupBlob(nuxt, hub, deps);
await setupCache(nuxt, hub);
await setupDatabase(nuxt, hub, deps);
await setupKV(nuxt, hub, deps);
const runtimeConfig = nuxt.options.runtimeConfig;
runtimeConfig.hub = hub;
runtimeConfig.public.hub ||= {};
if (nuxt.options._prepare) {
addTemplate({
filename: "hub/db/config.json",
write: true,
getContents: () => JSON.stringify(hub, null, 2)
});
}
if (nuxt.options._prepare) {
return;
}
if (nuxt.options.dev) {
addDevToolsCustomTabs(nuxt, hub);
}
nuxt.options.nitro.experimental = nuxt.options.nitro.experimental || {};
nuxt.options.nitro.experimental.asyncContext = true;
if (!nuxt.options.dev && hub.hosting.includes("cloudflare")) {
setupCloudflare(nuxt, hub);
}
if (nuxt.options.dev) {
const workspaceDir = await findWorkspaceDir(rootDir);
const gitignorePath = join(workspaceDir, ".gitignore");
const gitignore = await readFile(gitignorePath, "utf-8").catch(() => "");
const relativeDir = relative(workspaceDir, hub.dir);
if (!gitignore.includes(relativeDir)) {
await writeFile(gitignorePath, `${gitignore ? gitignore + "\n" : gitignore}${relativeDir}`, "utf-8");
}
}
}
});
export { module$1 as default };