aboutsummaryrefslogtreecommitdiff
path: root/packages/db/src/core/load-file.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/db/src/core/load-file.ts')
-rw-r--r--packages/db/src/core/load-file.ts206
1 files changed, 206 insertions, 0 deletions
diff --git a/packages/db/src/core/load-file.ts b/packages/db/src/core/load-file.ts
new file mode 100644
index 000000000..027deaa60
--- /dev/null
+++ b/packages/db/src/core/load-file.ts
@@ -0,0 +1,206 @@
+import { existsSync } from 'node:fs';
+import { unlink, writeFile } from 'node:fs/promises';
+import { createRequire } from 'node:module';
+import { fileURLToPath, pathToFileURL } from 'node:url';
+import type { AstroConfig } from 'astro';
+import { build as esbuild } from 'esbuild';
+import { CONFIG_FILE_NAMES, VIRTUAL_MODULE_ID } from './consts.js';
+import { INTEGRATION_TABLE_CONFLICT_ERROR } from './errors.js';
+import { errorMap } from './integration/error-map.js';
+import { getConfigVirtualModContents } from './integration/vite-plugin-db.js';
+import { dbConfigSchema } from './schemas.js';
+import './types.js';
+import { getAstroEnv, getDbDirectoryUrl } from './utils.js';
+
+/**
+ * Load a user’s `astro:db` configuration file and additional configuration files provided by integrations.
+ */
+export async function resolveDbConfig({
+ root,
+ integrations,
+}: Pick<AstroConfig, 'root' | 'integrations'>) {
+ const { mod, dependencies } = await loadUserConfigFile(root);
+ const userDbConfig = dbConfigSchema.parse(mod?.default ?? {}, { errorMap });
+ /** Resolved `astro:db` config including tables provided by integrations. */
+ const dbConfig = { tables: userDbConfig.tables ?? {} };
+
+ // Collect additional config and seed files from integrations.
+ const integrationDbConfigPaths: Array<{ name: string; configEntrypoint: string | URL }> = [];
+ const integrationSeedPaths: Array<string | URL> = [];
+ for (const integration of integrations) {
+ const { name, hooks } = integration;
+ if (hooks['astro:db:setup']) {
+ hooks['astro:db:setup']({
+ extendDb({ configEntrypoint, seedEntrypoint }) {
+ if (configEntrypoint) {
+ integrationDbConfigPaths.push({ name, configEntrypoint });
+ }
+ if (seedEntrypoint) {
+ integrationSeedPaths.push(seedEntrypoint);
+ }
+ },
+ });
+ }
+ }
+ for (const { name, configEntrypoint } of integrationDbConfigPaths) {
+ // TODO: config file dependencies are not tracked for integrations for now.
+ const loadedConfig = await loadIntegrationConfigFile(root, configEntrypoint);
+ const integrationDbConfig = dbConfigSchema.parse(loadedConfig.mod?.default ?? {}, {
+ errorMap,
+ });
+ for (const key in integrationDbConfig.tables) {
+ if (key in dbConfig.tables) {
+ const isUserConflict = key in (userDbConfig.tables ?? {});
+ throw new Error(INTEGRATION_TABLE_CONFLICT_ERROR(name, key, isUserConflict));
+ } else {
+ dbConfig.tables[key] = integrationDbConfig.tables[key];
+ }
+ }
+ }
+
+ return {
+ /** Resolved `astro:db` config, including tables added by integrations. */
+ dbConfig,
+ /** Dependencies imported into the user config file. */
+ dependencies,
+ /** Additional `astro:db` seed file paths provided by integrations. */
+ integrationSeedPaths,
+ };
+}
+
+async function loadUserConfigFile(
+ root: URL,
+): Promise<{ mod: { default?: unknown } | undefined; dependencies: string[] }> {
+ let configFileUrl: URL | undefined;
+ for (const fileName of CONFIG_FILE_NAMES) {
+ const fileUrl = new URL(fileName, getDbDirectoryUrl(root));
+ if (existsSync(fileUrl)) {
+ configFileUrl = fileUrl;
+ }
+ }
+ return await loadAndBundleDbConfigFile({ root, fileUrl: configFileUrl });
+}
+
+export function getResolvedFileUrl(root: URL, filePathOrUrl: string | URL): URL {
+ if (typeof filePathOrUrl === 'string') {
+ const { resolve } = createRequire(root);
+ const resolvedFilePath = resolve(filePathOrUrl);
+ return pathToFileURL(resolvedFilePath);
+ }
+ return filePathOrUrl;
+}
+
+async function loadIntegrationConfigFile(root: URL, filePathOrUrl: string | URL) {
+ const fileUrl = getResolvedFileUrl(root, filePathOrUrl);
+ return await loadAndBundleDbConfigFile({ root, fileUrl });
+}
+
+async function loadAndBundleDbConfigFile({
+ root,
+ fileUrl,
+}: {
+ root: URL;
+ fileUrl: URL | undefined;
+}): Promise<{ mod: { default?: unknown } | undefined; dependencies: string[] }> {
+ if (!fileUrl) {
+ return { mod: undefined, dependencies: [] };
+ }
+ const { code, dependencies } = await bundleFile({
+ virtualModContents: getConfigVirtualModContents(),
+ root,
+ fileUrl,
+ });
+ return {
+ mod: await importBundledFile({ code, root }),
+ dependencies,
+ };
+}
+
+/**
+ * Bundle arbitrary `mjs` or `ts` file.
+ * Simplified fork from Vite's `bundleConfigFile` function.
+ *
+ * @see https://github.com/vitejs/vite/blob/main/packages/vite/src/node/config.ts#L961
+ */
+export async function bundleFile({
+ fileUrl,
+ root,
+ virtualModContents,
+}: {
+ fileUrl: URL;
+ root: URL;
+ virtualModContents: string;
+}) {
+ const { ASTRO_DATABASE_FILE } = getAstroEnv();
+ 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,
+ define: {
+ 'import.meta.env.ASTRO_STUDIO_REMOTE_DB_URL': 'undefined',
+ 'import.meta.env.ASTRO_DB_REMOTE_DB_URL': 'undefined',
+ 'import.meta.env.ASTRO_DATABASE_FILE': JSON.stringify(ASTRO_DATABASE_FILE ?? ''),
+ },
+ plugins: [
+ {
+ 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: virtualModContents,
+ // Needed to resolve runtime dependencies
+ resolveDir: fileURLToPath(root),
+ };
+ });
+ },
+ },
+ ],
+ });
+
+ 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
+ */
+export async function importBundledFile({
+ code,
+ root,
+}: {
+ code: string;
+ root: URL;
+}): Promise<{ default?: unknown }> {
+ // Write it to disk, load it with native Node ESM, then delete the file.
+ const tmpFileUrl = new URL(`./db.timestamp-${Date.now()}.mjs`, root);
+ await writeFile(tmpFileUrl, code, { encoding: 'utf8' });
+ try {
+ return await import(/* @vite-ignore */ tmpFileUrl.toString());
+ } finally {
+ try {
+ await unlink(tmpFileUrl);
+ } catch {
+ // already removed if this function is called twice simultaneously
+ }
+ }
+}