diff --git a/packages/db/src/integration.ts b/packages/db/src/integration.ts index 6d4ea53e73..f17cee9ead 100644 --- a/packages/db/src/integration.ts +++ b/packages/db/src/integration.ts @@ -3,15 +3,20 @@ import { vitePluginDb } from './vite-plugin-db.js'; import { vitePluginInjectEnvTs } from './vite-plugin-inject-env-ts.js'; import { typegen } from './typegen.js'; import { collectionsSchema } from './types.js'; +import { seed } from './seed.js'; export function integration(): AstroIntegration { return { name: 'astro:db', hooks: { - async 'astro:config:setup'({ updateConfig, config }) { + async 'astro:config:setup'({ updateConfig, config, command }) { // TODO: refine where we load collections // @matthewp: may want to load collections by path at runtime const collections = collectionsSchema.parse(config.db?.collections ?? {}); + const isDev = command === 'dev'; + if (!isDev) { + await seed({ collections, root: config.root }); + } updateConfig({ vite: { plugins: [ @@ -20,6 +25,7 @@ export function integration(): AstroIntegration { vitePluginDb({ collections, root: config.root, + isDev, }), // @ts-ignore vitePluginInjectEnvTs(config), diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts new file mode 100644 index 0000000000..2caa978f0e --- /dev/null +++ b/packages/db/src/seed.ts @@ -0,0 +1,106 @@ +import { existsSync, unlinkSync, writeFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { type BuildOptions, build as esbuild } from 'esbuild'; +import { SUPPORTED_SEED_FILES, VIRTUAL_MODULE_ID } from './consts.js'; +import { getVirtualModContents } from './vite-plugin-db.js'; +import type { DBCollections } from './types.js'; + +export async function seed({ collections, root }: { collections: DBCollections; root: URL }) { + let seedFileUrl: URL | undefined; + for (const filename of SUPPORTED_SEED_FILES) { + const fileUrl = new URL(filename, root); + if (!existsSync(fileUrl)) continue; + + seedFileUrl = fileUrl; + break; + } + if (!seedFileUrl) return; + + const { code } = await bundleFile(seedFileUrl, [ + { + name: 'resolve-astro-db', + setup(build) { + build.onResolve({ filter: /^astro:db$/ }, ({ path }) => { + return { path, namespace: VIRTUAL_MODULE_ID }; + }); + build.onLoad({ namespace: VIRTUAL_MODULE_ID, filter: /.*/ }, () => { + return { + contents: getVirtualModContents({ + collections, + root, + isDev: false, + }), + // Needed to resolve `@packages/studio` internals + resolveDir: process.cwd(), + }; + }); + }, + }, + ]); + // seed file supports top-level await. Runs when config is loaded! + await loadBundledFile({ code, root }); + + console.info('Seeding complete 🌱'); +} + +/** + * Bundle config file to support `.ts` files. Simplified fork from Vite's `bundleConfigFile` + * function: + * + * @see https://github.com/vitejs/vite/blob/main/packages/vite/src/node/config.ts#L961 + */ +async function bundleFile( + fileUrl: URL, + esbuildPlugins?: BuildOptions['plugins'] +): Promise<{ code: string; dependencies: string[] }> { + const result = await esbuild({ + absWorkingDir: process.cwd(), + entryPoints: [fileURLToPath(fileUrl)], + outfile: 'out.js', + packages: 'external', + write: false, + target: ['node16'], + platform: 'node', + bundle: true, + format: 'esm', + sourcemap: 'inline', + metafile: true, + plugins: esbuildPlugins, + }); + + const file = result.outputFiles[0]; + if (!file) { + throw new Error(`Unexpected: no output file`); + } + + return { + code: file.text, + dependencies: Object.keys(result.metafile.inputs), + }; +} + +/** + * Forked from Vite config loader, replacing CJS-based path concat with ESM only + * + * @see https://github.com/vitejs/vite/blob/main/packages/vite/src/node/config.ts#L1074 + */ +async function loadBundledFile({ + code, + root, +}: { + root: URL; + code: string; +}): Promise<{ default?: unknown }> { + // Write it to disk, load it with native Node ESM, then delete the file. + const tmpFileUrl = new URL(`studio.seed.timestamp-${Date.now()}.mjs`, root); + writeFileSync(tmpFileUrl, code); + try { + return await import(tmpFileUrl.pathname); + } finally { + try { + unlinkSync(tmpFileUrl); + } catch { + // already removed if this function is called twice simultaneously + } + } +} diff --git a/packages/db/src/vite-plugin-db.ts b/packages/db/src/vite-plugin-db.ts index 253db9cdff..348fd188e9 100644 --- a/packages/db/src/vite-plugin-db.ts +++ b/packages/db/src/vite-plugin-db.ts @@ -15,9 +15,11 @@ const resolvedVirtualModuleId = '\0' + VIRTUAL_MODULE_ID; export function vitePluginDb({ collections, root, + isDev: isDev, }: { collections: DBCollections; root: URL; + isDev: boolean; }): VitePlugin { return { name: 'astro:db', @@ -29,7 +31,7 @@ export function vitePluginDb({ }, load(id) { if (id !== resolvedVirtualModuleId) return; - return getLocalVirtualModuleContents({ collections, root }); + return getVirtualModContents({ collections, root, isDev }); }, }; } @@ -38,12 +40,14 @@ const seedErrorMessage = `${red( '⚠️ Failed to seed data.' )} Is the seed file out-of-date with recent schema changes?`; -export function getLocalVirtualModuleContents({ +export function getVirtualModContents({ collections, root, + isDev, }: { collections: DBCollections; root: URL; + isDev: boolean; }) { const seedFile = SUPPORTED_SEED_FILES.map((f) => fileURLToPath(new URL(f, root))).find((f) => existsSync(f) @@ -57,7 +61,7 @@ export * from ${DRIZZLE_MOD_IMPORT}; ${getStringifiedCollectionExports(collections)} ${ - seedFile + seedFile && isDev ? `try { await import(${JSON.stringify(seedFile)}); } catch {