Skip to content

Commit

Permalink
feat(cloudflare): add D1 database binding
Browse files Browse the repository at this point in the history
This addition allows using D1 databases within the Astro dev server.
  • Loading branch information
alexanderniebuhr committed Sep 24, 2023
1 parent 24d691a commit cea45d1
Show file tree
Hide file tree
Showing 9 changed files with 181 additions and 23 deletions.
5 changes: 5 additions & 0 deletions .changeset/hungry-mails-boil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/cloudflare': minor
---

Introduce support for local D1 bindings. Enhance development experience by allowing direct integration with `astro dev`.
3 changes: 2 additions & 1 deletion packages/integrations/cloudflare/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# Astro cloudflare directory mode creates a function directory
functions
functions
.mf
1 change: 1 addition & 0 deletions packages/integrations/cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"dependencies": {
"@astrojs/underscore-redirects": "workspace:*",
"@cloudflare/workers-types": "^4.20230821.0",
"miniflare": "^3.20230918.0",
"@iarna/toml": "^2.2.5",
"@miniflare/cache": "^2.14.1",
"@miniflare/shared": "^2.14.1",
Expand Down
32 changes: 28 additions & 4 deletions packages/integrations/cloudflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@ import { NoOpLog } from '@miniflare/shared';
import { MemoryStorage } from '@miniflare/storage-memory';
import { AstroError } from 'astro/errors';
import esbuild from 'esbuild';
import { Miniflare } from 'miniflare';
import * as fs from 'node:fs';
import * as os from 'node:os';
import { dirname, relative, sep } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import glob from 'tiny-glob';
import { getCFObject } from './utils/getCFObject.js';
import { getEnvVars } from './utils/parser.js';
import { deduplicatePatterns } from './utils/deduplicatePatterns.js';
import { getAdapter } from './utils/getAdapter.js';
import { wasmModuleLoader } from './utils/wasm-module-loader.js';
import { getCFObject } from './utils/getCFObject.js';
import { getD1Bindings, getEnvVars } from './utils/parser.js';
import { prependForwardSlash } from './utils/prependForwardSlash.js';
import { deduplicatePatterns } from './utils/deduplicatePatterns.js';
import { rewriteWasmImportPath } from './utils/rewriteWasmImportPath.js';
import { wasmModuleLoader } from './utils/wasm-module-loader.js';

export type { AdvancedRuntime } from './entrypoints/server.advanced.js';
export type { DirectoryRuntime } from './entrypoints/server.directory.js';
Expand Down Expand Up @@ -69,6 +70,7 @@ class StorageFactory {
export default function createIntegration(args?: Options): AstroIntegration {
let _config: AstroConfig;
let _buildConfig: BuildConfig;
let _mf: Miniflare;
let _entryPoints = new Map<RouteData, URL>();

const SERVER_BUILD_FOLDER = '/$server_build/';
Expand Down Expand Up @@ -122,6 +124,21 @@ export default function createIntegration(args?: Options): AstroIntegration {
try {
const cf = await getCFObject(runtimeMode);
const vars = await getEnvVars();
const D1Bindings = await getD1Bindings();
let bindingsEnv = new Object({});

// monkey patch to get rid of PWD error
process.env.PWD = process.cwd();
_mf = new Miniflare({
modules: true,
script: '',
d1Databases: D1Bindings,
d1Persist: true,
});
for (const D1Binding of D1Bindings) {
const db = await _mf.getD1Database(D1Binding);
Reflect.set(bindingsEnv, D1Binding, db);
}

const clientLocalsSymbol = Symbol.for('astro.locals');
Reflect.set(req, clientLocalsSymbol, {
Expand All @@ -136,6 +153,7 @@ export default function createIntegration(args?: Options): AstroIntegration {
// will be fetched from git dynamically once we support mocking of bindings
CF_PAGES_COMMIT_SHA: 'TBA',
CF_PAGES_URL: `http://${req.headers.host}`,
...bindingsEnv,
...vars,
},
cf: cf,
Expand All @@ -157,6 +175,12 @@ export default function createIntegration(args?: Options): AstroIntegration {
});
}
},
'astro:server:done': async () => {
if (_mf) {
await _mf.ready;
await _mf.dispose();
}
},
'astro:build:setup': ({ vite, target }) => {
if (target === 'server') {
vite.resolve ||= {};
Expand Down
18 changes: 17 additions & 1 deletion packages/integrations/cloudflare/src/utils/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ function getVarsForDev(config: any, configPath: string | undefined): any {
return config.vars;
}
}
export async function getEnvVars() {

function parseConfig() {
let rawConfig;
const configPath = findWranglerToml(process.cwd(), false); // false = args.experimentalJsonConfig
if (!configPath) {
Expand All @@ -129,6 +130,21 @@ export async function getEnvVars() {
if (configPath?.endsWith('toml')) {
rawConfig = parseTOML(fs.readFileSync(configPath).toString(), configPath);
}
return { rawConfig, configPath };
}

export async function getEnvVars() {
const { rawConfig, configPath } = parseConfig();
const vars = getVarsForDev(rawConfig, configPath);
return vars;
}

export async function getD1Bindings() {
const { rawConfig } = parseConfig();
if (!rawConfig) return [];
if (!rawConfig?.d1_databases) return [];
const bindings = (rawConfig?.d1_databases as []).map(
(binding: { binding: string }) => binding.binding
);
return bindings;
}
17 changes: 12 additions & 5 deletions packages/integrations/cloudflare/test/cf.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,6 @@ describe('Astro Cloudflare Runtime', () => {
adapter: cloudflare({
runtime: 'local',
}),
image: {
service: {
entrypoint: 'astro/assets/services/noop',
},
},
});
process.chdir('./test/fixtures/cf');
devServer = await fixture.startDevServer();
Expand All @@ -76,4 +71,16 @@ describe('Astro Cloudflare Runtime', () => {
expect($('#hasRuntime').text()).to.equal('true');
expect($('#hasCache').text()).to.equal('true');
});

it('adds D1 mocking', async () => {
expect(await fixture.pathExists('../.mf/d1')).to.be.true;

let res = await fixture.fetch('/d1');
expect(res.status).to.equal(200);
let html = await res.text();
let $ = cheerio.load(html);
expect($('#hasDB').text()).to.equal('true');
expect($('#hasPRODDB').text()).to.equal('true');
expect($('#hasACCESS').text()).to.equal('true');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
const runtime = Astro.locals.runtime;
const db = runtime.env?.D1;
await db.exec("CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)");
await db.exec("INSERT INTO test (name) VALUES ('true')");
const result = await db.prepare("SELECT * FROM test").all();
---

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>D1</title>
</head>
<body>
<pre id="hasDB">{!!runtime.env?.D1}</pre>
<pre id="hasPRODDB">{!!runtime.env?.D1_PROD}</pre>
<pre id="hasACCESS">{!!result.results[0].name}</pre>
</body>
</html>
12 changes: 12 additions & 0 deletions packages/integrations/cloudflare/test/fixtures/cf/wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,15 @@ name = "test"

[vars]
COOL = "ME"

[[d1_databases]]
binding = "D1" # Should match preview_database_id, i.e. available in your Worker on env.DB
database_name = "<DATABASE_NAME>"
database_id = "<unique-ID-for-your-database>"
preview_database_id = "D1" # Required for Pages local development

[[d1_databases]]
binding = "D1_PROD" # Should match preview_database_id
database_name = "<DATABASE_NAME>"
database_id = "<unique-ID-for-your-database>"
preview_database_id = "D1_PROD" # Required for Pages local development
Loading

0 comments on commit cea45d1

Please sign in to comment.