alchemy 0.64.0 → 0.65.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/bin/alchemy.js +7569 -5772
- package/bin/commands/init.ts +130 -9
- package/bin/constants.ts +4 -0
- package/bin/services/cdp-manager/cdp-proxy.ts +69 -0
- package/bin/services/cdp-manager/server.ts +173 -0
- package/bin/services/execute-alchemy.ts +83 -4
- package/bin/types.ts +1 -0
- package/lib/cloudflare/bundle/plugin-wasm.d.ts.map +1 -1
- package/lib/cloudflare/bundle/plugin-wasm.js +20 -11
- package/lib/cloudflare/bundle/plugin-wasm.js.map +1 -1
- package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
- package/lib/cloudflare/compatibility-date.gen.js +1 -1
- package/lib/cloudflare/hyperdrive.d.ts +7 -2
- package/lib/cloudflare/hyperdrive.d.ts.map +1 -1
- package/lib/cloudflare/hyperdrive.js +39 -57
- package/lib/cloudflare/hyperdrive.js.map +1 -1
- package/lib/cloudflare/index.d.ts +1 -0
- package/lib/cloudflare/index.d.ts.map +1 -1
- package/lib/cloudflare/index.js +1 -0
- package/lib/cloudflare/index.js.map +1 -1
- package/lib/cloudflare/next.d.ts +13 -0
- package/lib/cloudflare/next.d.ts.map +1 -0
- package/lib/cloudflare/next.js +91 -0
- package/lib/cloudflare/next.js.map +1 -0
- package/lib/cloudflare/worker-assets.d.ts +3 -3
- package/lib/cloudflare/worker-assets.d.ts.map +1 -1
- package/lib/cloudflare/worker-assets.js +22 -47
- package/lib/cloudflare/worker-assets.js.map +1 -1
- package/lib/cloudflare/wrangler.json.d.ts +2 -2
- package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
- package/lib/cloudflare/wrangler.json.js +4 -1
- package/lib/cloudflare/wrangler.json.js.map +1 -1
- package/lib/planetscale/branch.d.ts.map +1 -1
- package/lib/planetscale/branch.js +36 -25
- package/lib/planetscale/branch.js.map +1 -1
- package/lib/planetscale/database.d.ts +23 -6
- package/lib/planetscale/database.d.ts.map +1 -1
- package/lib/planetscale/database.js +11 -5
- package/lib/planetscale/database.js.map +1 -1
- package/lib/planetscale/index.d.ts +1 -0
- package/lib/planetscale/index.d.ts.map +1 -1
- package/lib/planetscale/index.js +1 -0
- package/lib/planetscale/index.js.map +1 -1
- package/lib/planetscale/password.d.ts +3 -1
- package/lib/planetscale/password.d.ts.map +1 -1
- package/lib/planetscale/password.js +3 -1
- package/lib/planetscale/password.js.map +1 -1
- package/lib/planetscale/role.d.ts +126 -0
- package/lib/planetscale/role.d.ts.map +1 -0
- package/lib/planetscale/role.js +144 -0
- package/lib/planetscale/role.js.map +1 -0
- package/lib/planetscale/utils.d.ts +17 -3
- package/lib/planetscale/utils.d.ts.map +1 -1
- package/lib/planetscale/utils.js +110 -31
- package/lib/planetscale/utils.js.map +1 -1
- package/lib/secret.d.ts +0 -3
- package/lib/secret.d.ts.map +1 -1
- package/lib/secret.js +0 -2
- package/lib/secret.js.map +1 -1
- package/lib/state/cloudflare-state-store.js +3 -3
- package/lib/state/cloudflare-state-store.js.map +1 -1
- package/lib/test/bun.d.ts +2 -2
- package/lib/test/bun.d.ts.map +1 -1
- package/lib/test/vitest.d.ts +2 -2
- package/lib/test/vitest.d.ts.map +1 -1
- package/lib/test/vitest.js +4 -4
- package/lib/test/vitest.js.map +1 -1
- package/lib/util/content-type.js +2 -2
- package/lib/util/content-type.js.map +1 -1
- package/lib/util/dedent.d.ts.map +1 -1
- package/lib/util/dedent.js +30 -18
- package/lib/util/dedent.js.map +1 -1
- package/lib/util/memoize.d.ts.map +1 -1
- package/lib/util/memoize.js +11 -1
- package/lib/util/memoize.js.map +1 -1
- package/package.json +5 -1
- package/src/cloudflare/bundle/plugin-wasm.ts +34 -14
- package/src/cloudflare/compatibility-date.gen.ts +1 -1
- package/src/cloudflare/hyperdrive.ts +136 -95
- package/src/cloudflare/index.ts +1 -0
- package/src/cloudflare/next.ts +123 -0
- package/src/cloudflare/worker-assets.ts +43 -105
- package/src/cloudflare/wrangler.json.ts +6 -3
- package/src/planetscale/branch.ts +52 -27
- package/src/planetscale/database.ts +75 -46
- package/src/planetscale/index.ts +1 -0
- package/src/planetscale/password.ts +3 -1
- package/src/planetscale/role.ts +278 -0
- package/src/planetscale/utils.ts +172 -30
- package/src/secret.ts +0 -4
- package/src/state/cloudflare-state-store.ts +4 -4
- package/src/test/bun.ts +2 -2
- package/src/test/vitest.ts +6 -6
- package/src/util/content-type.ts +2 -2
- package/src/util/dedent.ts +33 -21
- package/src/util/memoize.ts +13 -1
- package/templates/nextjs/README.md +85 -0
- package/templates/nextjs/_env +1 -0
- package/templates/nextjs/_env.example +1 -0
- package/templates/nextjs/_gitignore +46 -0
- package/templates/nextjs/alchemy.run.ts +16 -0
- package/templates/nextjs/eslint.config.mjs +16 -0
- package/templates/nextjs/next-env.d.ts +5 -0
- package/templates/nextjs/next.config.ts +10 -0
- package/templates/nextjs/open-next.config.ts +5 -0
- package/templates/nextjs/package.json +31 -0
- package/templates/nextjs/src/app/api/hello/route.ts +19 -0
- package/templates/nextjs/src/app/favicon.ico +0 -0
- package/templates/nextjs/src/app/globals.css +72 -0
- package/templates/nextjs/src/app/layout.tsx +19 -0
- package/templates/nextjs/src/app/page.tsx +55 -0
- package/templates/nextjs/tsconfig.json +33 -0
- package/templates/nextjs/types/env.d.ts +14 -0
package/lib/util/dedent.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import assert from "node:assert";
|
|
2
1
|
/**
|
|
3
2
|
* Tagged template literal for removing indentation from a block of text.
|
|
4
3
|
*
|
|
@@ -9,29 +8,42 @@ export function dedent(strings, ...values) {
|
|
|
9
8
|
const raw = String.raw({ raw: strings }, ...values);
|
|
10
9
|
// Split the string by lines
|
|
11
10
|
let lines = raw.split("\n");
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
// Remove leading blank lines
|
|
12
|
+
while (lines.length > 0 && lines[0].trim() === "") {
|
|
13
|
+
lines = lines.slice(1);
|
|
14
|
+
}
|
|
15
|
+
// Remove trailing blank lines
|
|
16
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
|
|
15
17
|
lines = lines.slice(0, lines.length - 1);
|
|
16
18
|
}
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// If no content lines remain, return empty string
|
|
20
|
+
if (lines.length === 0) {
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
// Find the minimum-length indent from content lines (non-blank lines)
|
|
21
24
|
let minIndentLength = Number.POSITIVE_INFINITY;
|
|
22
|
-
for (const line of lines
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
if (line.trim() !== "") {
|
|
27
|
+
const indent = line.match(/^[ \t]*/)?.[0];
|
|
28
|
+
if (indent != null && indent.length < minIndentLength) {
|
|
29
|
+
minIndentLength = indent.length;
|
|
30
|
+
}
|
|
27
31
|
}
|
|
28
32
|
}
|
|
29
|
-
// If
|
|
30
|
-
if (
|
|
31
|
-
|
|
33
|
+
// If no indentation found, return lines as-is
|
|
34
|
+
if (minIndentLength === Number.POSITIVE_INFINITY) {
|
|
35
|
+
return lines.join("\n");
|
|
32
36
|
}
|
|
33
|
-
// Remove indent from all lines,
|
|
34
|
-
lines = lines.map((line) =>
|
|
37
|
+
// Remove indent from all lines, preserving blank lines
|
|
38
|
+
lines = lines.map((line) => {
|
|
39
|
+
if (line.trim() === "") {
|
|
40
|
+
return line; // Preserve blank lines
|
|
41
|
+
}
|
|
42
|
+
return line.startsWith(" ".repeat(minIndentLength)) ||
|
|
43
|
+
line.startsWith("\t".repeat(minIndentLength))
|
|
44
|
+
? line.substring(minIndentLength)
|
|
45
|
+
: line;
|
|
46
|
+
});
|
|
35
47
|
return lines.join("\n");
|
|
36
48
|
}
|
|
37
49
|
//# sourceMappingURL=dedent.js.map
|
package/lib/util/dedent.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dedent.js","sourceRoot":"","sources":["../../src/util/dedent.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"dedent.js","sourceRoot":"","sources":["../../src/util/dedent.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,UAAU,MAAM,CAAC,OAA6B,EAAE,GAAG,MAAiB;IACxE,8DAA8D;IAC9D,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,GAAG,MAAM,CAAC,CAAC;IACpD,4BAA4B;IAC5B,IAAI,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAE5B,6BAA6B;IAC7B,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAClD,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACzB,CAAC;IAED,8BAA8B;IAC9B,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACjE,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC3C,CAAC;IAED,kDAAkD;IAClD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,sEAAsE;IACtE,IAAI,eAAe,GAAG,MAAM,CAAC,iBAAiB,CAAC;IAC/C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YACvB,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YAC1C,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM,CAAC,MAAM,GAAG,eAAe,EAAE,CAAC;gBACtD,eAAe,GAAG,MAAM,CAAC,MAAM,CAAC;YAClC,CAAC;QACH,CAAC;IACH,CAAC;IAED,8CAA8C;IAC9C,IAAI,eAAe,KAAK,MAAM,CAAC,iBAAiB,EAAE,CAAC;QACjD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAED,uDAAuD;IACvD,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACzB,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YACvB,OAAO,IAAI,CAAC,CAAC,uBAAuB;QACtC,CAAC;QACD,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;YACjD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;YAC7C,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,eAAe,CAAC;YACjC,CAAC,CAAC,IAAI,CAAC;IACX,CAAC,CAAC,CAAC;IAEH,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memoize.d.ts","sourceRoot":"","sources":["../../src/util/memoize.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"memoize.d.ts","sourceRoot":"","sources":["../../src/util/memoize.ts"],"names":[],"mappings":"AAGA,KAAK,eAAe,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC,GACpE,CAAC,GACD,CAAC,CAAC;AAEN,wBAAgB,OAAO,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,EAChE,EAAE,EAAE,CAAC,EACL,KAAK,GAAE,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,MAAqB,IAG1C,GAAG,MAAM,UAAU,CAAC,CAAC,CAAC,KAAG,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAanE;AAED,eAAO,MAAM,YAAY,GAAI,GAAG,MAAM,GAAG,EAAE,WAa1C,CAAC"}
|
package/lib/util/memoize.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { isSecret } from "../secret.js";
|
|
2
3
|
export function memoize(fn, keyFn = defaultKeyFn) {
|
|
3
4
|
const cache = new Map();
|
|
4
5
|
return async (...args) => {
|
|
@@ -16,6 +17,15 @@ export function memoize(fn, keyFn = defaultKeyFn) {
|
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
19
|
export const defaultKeyFn = (...args) => {
|
|
19
|
-
return createHash("sha256")
|
|
20
|
+
return createHash("sha256")
|
|
21
|
+
.update(JSON.stringify(args, (_, value) => {
|
|
22
|
+
// Secret names may differ between instantiations,
|
|
23
|
+
// so we unwrap it to make the key deterministic.
|
|
24
|
+
if (isSecret(value)) {
|
|
25
|
+
return value.unencrypted;
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}))
|
|
29
|
+
.digest("hex");
|
|
20
30
|
};
|
|
21
31
|
//# sourceMappingURL=memoize.js.map
|
package/lib/util/memoize.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memoize.js","sourceRoot":"","sources":["../../src/util/memoize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"memoize.js","sourceRoot":"","sources":["../../src/util/memoize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAMxC,MAAM,UAAU,OAAO,CACrB,EAAK,EACL,QAA4C,YAAY;IAExD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAuC,CAAC;IAC7D,OAAO,KAAK,EAAE,GAAG,IAAmB,EAA+B,EAAE;QACnE,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;QAC3B,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC9B,IAAI,MAAM,EAAE,CAAC;YACX,OAAO,MAAM,MAAM,CAAC;QACtB,CAAC;QACD,MAAM,OAAO,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;YAC1C,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAClB,MAAM,KAAK,CAAC;QACd,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QACxB,OAAO,MAAM,OAAO,CAAC;IACvB,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,GAAG,IAAW,EAAE,EAAE;IAC7C,OAAO,UAAU,CAAC,QAAQ,CAAC;SACxB,MAAM,CACL,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE;QAChC,kDAAkD;QAClD,iDAAiD;QACjD,IAAI,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YACpB,OAAO,KAAK,CAAC,WAAW,CAAC;QAC3B,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC,CAAC,CACH;SACA,MAAM,CAAC,KAAK,CAAC,CAAC;AACnB,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alchemy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.65.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"author": "Sam Goodwin <sam@alchemy.run>",
|
|
6
6
|
"homepage": "https://alchemy.run",
|
|
@@ -185,6 +185,7 @@
|
|
|
185
185
|
"@cloudflare/workers-types": "^4.20250805.0",
|
|
186
186
|
"@libsql/client": "^0.15.12",
|
|
187
187
|
"@octokit/rest": "^21.1.1",
|
|
188
|
+
"@opennextjs/cloudflare": "^1.6.5",
|
|
188
189
|
"astro": "^5.13.2",
|
|
189
190
|
"drizzle-orm": "^0.44.2",
|
|
190
191
|
"rwsdk": "^0.1.36",
|
|
@@ -224,6 +225,9 @@
|
|
|
224
225
|
"@cloudflare/workers-types": {
|
|
225
226
|
"optional": true
|
|
226
227
|
},
|
|
228
|
+
"@opennextjs/cloudflare": {
|
|
229
|
+
"optional": true
|
|
230
|
+
},
|
|
227
231
|
"@octokit/rest": {
|
|
228
232
|
"optional": true
|
|
229
233
|
},
|
|
@@ -12,21 +12,41 @@ export function createWasmPlugin() {
|
|
|
12
12
|
build.onStart(() => {
|
|
13
13
|
modules.clear();
|
|
14
14
|
});
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
path.join(build.initialOptions.outdir, name),
|
|
23
|
-
);
|
|
24
|
-
modules.set(args.path, {
|
|
25
|
-
type: "wasm",
|
|
26
|
-
path: name,
|
|
27
|
-
});
|
|
15
|
+
|
|
16
|
+
// Handle imports like `import "./foo.wasm"` and `import "./foo.wasm?module"`
|
|
17
|
+
// TODO(john): Figure out why this suddenly became necessary
|
|
18
|
+
build.onResolve({ filter: /\.wasm(\?.*)?$/ }, async (args) => {
|
|
19
|
+
const resolved = modules.get(args.path);
|
|
20
|
+
if (resolved) {
|
|
21
|
+
return { path: resolved.path, external: true };
|
|
28
22
|
}
|
|
29
|
-
|
|
23
|
+
|
|
24
|
+
// Normalize path and remove the `?module` query param so we have the actual file name to copy
|
|
25
|
+
const name = path.normalize(args.path).replace(/\?.*$/, "");
|
|
26
|
+
|
|
27
|
+
// Resolve path to outdir (required for monorepos if the workdir is not the same as process.cwd())
|
|
28
|
+
assert(
|
|
29
|
+
build.initialOptions.absWorkingDir && build.initialOptions.outdir,
|
|
30
|
+
"Missing absWorkingDir or outdir from esbuild options",
|
|
31
|
+
);
|
|
32
|
+
const outdir = path.resolve(
|
|
33
|
+
build.initialOptions.absWorkingDir,
|
|
34
|
+
build.initialOptions.outdir,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Copy to outdir so it's included in the upload
|
|
38
|
+
await fs.mkdir(outdir, { recursive: true });
|
|
39
|
+
await fs.copyFile(
|
|
40
|
+
path.join(args.resolveDir, name),
|
|
41
|
+
path.join(outdir, name),
|
|
42
|
+
);
|
|
43
|
+
modules.set(args.path, {
|
|
44
|
+
type: "wasm",
|
|
45
|
+
path: name,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Resolve to the normalized file name (the `?module` query param is not needed in workerd)
|
|
49
|
+
return { path: name, external: true };
|
|
30
50
|
});
|
|
31
51
|
},
|
|
32
52
|
};
|
|
@@ -4,8 +4,16 @@ import { Resource } from "../resource.ts";
|
|
|
4
4
|
import { Scope } from "../scope.ts";
|
|
5
5
|
import { Secret } from "../secret.ts";
|
|
6
6
|
import { logger } from "../util/logger.ts";
|
|
7
|
-
import { handleApiError } from "./api-error.ts";
|
|
8
|
-
import {
|
|
7
|
+
import { CloudflareApiError, handleApiError } from "./api-error.ts";
|
|
8
|
+
import {
|
|
9
|
+
extractCloudflareResult,
|
|
10
|
+
type CloudflareApiErrorPayload,
|
|
11
|
+
} from "./api-response.ts";
|
|
12
|
+
import {
|
|
13
|
+
createCloudflareApi,
|
|
14
|
+
type CloudflareApi,
|
|
15
|
+
type CloudflareApiOptions,
|
|
16
|
+
} from "./api.ts";
|
|
9
17
|
|
|
10
18
|
/**
|
|
11
19
|
* Origin configuration for a PostgreSQL or MySQL database connection
|
|
@@ -163,6 +171,12 @@ export interface HyperdriveProps extends CloudflareApiOptions {
|
|
|
163
171
|
*/
|
|
164
172
|
hyperdriveId?: string;
|
|
165
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Whether to adopt an existing hyperdrive config
|
|
176
|
+
* @default false
|
|
177
|
+
*/
|
|
178
|
+
adopt?: boolean;
|
|
179
|
+
|
|
166
180
|
dev?: {
|
|
167
181
|
/**
|
|
168
182
|
* The database connection origin configuration for local development
|
|
@@ -176,46 +190,45 @@ export interface HyperdriveProps extends CloudflareApiOptions {
|
|
|
176
190
|
* Output returned after Cloudflare Hyperdrive creation/update.
|
|
177
191
|
* IMPORTANT: The interface name MUST match the exported resource name.
|
|
178
192
|
*/
|
|
179
|
-
export
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
id: string;
|
|
193
|
+
export type Hyperdrive = Resource<"cloudflare::Hyperdrive"> &
|
|
194
|
+
Omit<HyperdriveProps, "origin" | "dev"> & {
|
|
195
|
+
/**
|
|
196
|
+
* The ID of the resource
|
|
197
|
+
*/
|
|
198
|
+
id: string;
|
|
186
199
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
200
|
+
/**
|
|
201
|
+
* Name of the Hyperdrive configuration
|
|
202
|
+
*/
|
|
203
|
+
name: string;
|
|
191
204
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
205
|
+
/**
|
|
206
|
+
* The Cloudflare-generated UUID of the hyperdrive
|
|
207
|
+
*/
|
|
208
|
+
hyperdriveId: string;
|
|
196
209
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Database connection origin configuration
|
|
212
|
+
*/
|
|
213
|
+
origin: HyperdrivePublicOrigin | HyperdriveOriginWithAccess;
|
|
201
214
|
|
|
202
|
-
/**
|
|
203
|
-
* Local development configuration
|
|
204
|
-
* @internal
|
|
205
|
-
*/
|
|
206
|
-
dev: {
|
|
207
215
|
/**
|
|
208
|
-
*
|
|
216
|
+
* Local development configuration
|
|
217
|
+
* @internal
|
|
209
218
|
*/
|
|
210
|
-
|
|
211
|
-
|
|
219
|
+
dev: {
|
|
220
|
+
/**
|
|
221
|
+
* The connection string to use for local development
|
|
222
|
+
*/
|
|
223
|
+
origin: Secret;
|
|
224
|
+
};
|
|
212
225
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
226
|
+
/**
|
|
227
|
+
* Resource type identifier for binding.
|
|
228
|
+
* @internal
|
|
229
|
+
*/
|
|
230
|
+
type: "hyperdrive";
|
|
231
|
+
};
|
|
219
232
|
|
|
220
233
|
/**
|
|
221
234
|
* Represents a Cloudflare Hyperdrive configuration.
|
|
@@ -327,6 +340,7 @@ interface InternalHyperdriveProps extends CloudflareApiOptions {
|
|
|
327
340
|
origin: Secret;
|
|
328
341
|
force?: boolean;
|
|
329
342
|
};
|
|
343
|
+
adopt?: boolean;
|
|
330
344
|
}
|
|
331
345
|
|
|
332
346
|
const _Hyperdrive = Resource(
|
|
@@ -337,7 +351,7 @@ const _Hyperdrive = Resource(
|
|
|
337
351
|
props: InternalHyperdriveProps,
|
|
338
352
|
): Promise<Hyperdrive> {
|
|
339
353
|
const hyperdriveId = props.hyperdriveId || this.output?.hyperdriveId;
|
|
340
|
-
|
|
354
|
+
const adopt = props.adopt || this.scope.adopt;
|
|
341
355
|
const name =
|
|
342
356
|
props.name ?? this.output?.name ?? this.scope.createPhysicalName(id);
|
|
343
357
|
|
|
@@ -354,13 +368,6 @@ const _Hyperdrive = Resource(
|
|
|
354
368
|
});
|
|
355
369
|
}
|
|
356
370
|
const api = await createCloudflareApi(props);
|
|
357
|
-
const configsPath = `/accounts/${api.accountId}/hyperdrive/configs`;
|
|
358
|
-
|
|
359
|
-
// For create operations, we don't have a hyperdriveId yet
|
|
360
|
-
// For update/delete operations, we need to use the hyperdriveId from props or output
|
|
361
|
-
const configPath = hyperdriveId
|
|
362
|
-
? `${configsPath}/${hyperdriveId}`
|
|
363
|
-
: `${configsPath}`;
|
|
364
371
|
|
|
365
372
|
if (this.phase === "delete") {
|
|
366
373
|
if (!hyperdriveId) {
|
|
@@ -369,7 +376,9 @@ const _Hyperdrive = Resource(
|
|
|
369
376
|
}
|
|
370
377
|
|
|
371
378
|
try {
|
|
372
|
-
const deleteResponse = await api.delete(
|
|
379
|
+
const deleteResponse = await api.delete(
|
|
380
|
+
`/accounts/${api.accountId}/hyperdrive/configs/${hyperdriveId}`,
|
|
381
|
+
);
|
|
373
382
|
// Only swallow 404 Not Found errors, all other errors should be handled
|
|
374
383
|
if (!deleteResponse.ok && deleteResponse.status !== 404) {
|
|
375
384
|
await handleApiError(deleteResponse, "delete", "hyperdrive", id);
|
|
@@ -381,75 +390,107 @@ const _Hyperdrive = Resource(
|
|
|
381
390
|
return this.destroy();
|
|
382
391
|
}
|
|
383
392
|
|
|
384
|
-
let response: Response | undefined;
|
|
385
|
-
let apiResource: any;
|
|
386
|
-
|
|
387
393
|
// Prepare request body with unwrapped secrets
|
|
388
|
-
const requestBody = prepareRequestBody(props);
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
394
|
+
const requestBody = prepareRequestBody({ ...props, name });
|
|
395
|
+
let result: HyperdriveResponse;
|
|
396
|
+
|
|
397
|
+
if (hyperdriveId) {
|
|
398
|
+
result = await extractCloudflareResult<HyperdriveResponse>(
|
|
399
|
+
`update hyperdrive config "${hyperdriveId}"`,
|
|
400
|
+
api.put(
|
|
401
|
+
`/accounts/${api.accountId}/hyperdrive/configs/${hyperdriveId}`,
|
|
402
|
+
requestBody,
|
|
403
|
+
),
|
|
404
|
+
);
|
|
405
|
+
} else {
|
|
406
|
+
try {
|
|
407
|
+
result = await extractCloudflareResult<HyperdriveResponse>(
|
|
408
|
+
`create hyperdrive config "${name}"`,
|
|
409
|
+
api.post(
|
|
410
|
+
`/accounts/${api.accountId}/hyperdrive/configs`,
|
|
411
|
+
requestBody,
|
|
412
|
+
),
|
|
413
|
+
);
|
|
414
|
+
} catch (error) {
|
|
415
|
+
if (
|
|
416
|
+
error instanceof CloudflareApiError &&
|
|
417
|
+
(error.errorData as CloudflareApiErrorPayload[]).some(
|
|
418
|
+
(e) => e.code === 2017, // 2017 is the error code for name conflict
|
|
419
|
+
)
|
|
420
|
+
) {
|
|
421
|
+
if (!adopt) {
|
|
422
|
+
throw new Error(
|
|
423
|
+
`Hyperdrive config "${name}" already exists. Use adopt: true to adopt it.`,
|
|
424
|
+
{ cause: error },
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
const existing = await findHyperdriveConfigByName(api, name);
|
|
428
|
+
if (!existing) {
|
|
429
|
+
throw new Error(
|
|
430
|
+
`Hyperdrive config "${name}" failed to create due to name conflict and could not be found for adoption.`,
|
|
431
|
+
{ cause: error },
|
|
404
432
|
);
|
|
405
|
-
response = await api.put(configPath, requestBody);
|
|
406
|
-
} else if (getResponse.status === 404) {
|
|
407
|
-
// Hyperdrive doesn't exist, create new
|
|
408
|
-
response = await api.post(configsPath, {
|
|
409
|
-
...requestBody,
|
|
410
|
-
// Ensure name is set correctly if not already set
|
|
411
|
-
name: props.name || id,
|
|
412
|
-
});
|
|
413
|
-
} else {
|
|
414
|
-
// Unexpected error during GET check
|
|
415
|
-
await handleApiError(getResponse, "get", "hyperdrive", id);
|
|
416
433
|
}
|
|
434
|
+
result = await extractCloudflareResult<HyperdriveResponse>(
|
|
435
|
+
`adopt hyperdrive config "${name}"`,
|
|
436
|
+
api.put(
|
|
437
|
+
`/accounts/${api.accountId}/hyperdrive/configs/${existing.id}`,
|
|
438
|
+
requestBody,
|
|
439
|
+
),
|
|
440
|
+
);
|
|
417
441
|
} else {
|
|
418
|
-
|
|
419
|
-
response = await api.post(configsPath, {
|
|
420
|
-
...requestBody,
|
|
421
|
-
// Ensure name is set correctly if not already set
|
|
422
|
-
name: props.name || id,
|
|
423
|
-
});
|
|
442
|
+
throw error;
|
|
424
443
|
}
|
|
425
444
|
}
|
|
426
|
-
|
|
427
|
-
if (!response?.ok) {
|
|
428
|
-
const action = this.phase === "update" ? "update" : "create";
|
|
429
|
-
await handleApiError(response!, action, "hyperdrive", id);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const data: { result: Record<string, any> } = await response!.json();
|
|
433
|
-
apiResource = data.result;
|
|
434
|
-
} catch (error) {
|
|
435
|
-
logger.error(`Error ${this.phase} Hyperdrive '${id}':`, error);
|
|
436
|
-
throw error;
|
|
437
445
|
}
|
|
438
446
|
|
|
439
447
|
// Construct the output object from API response and props
|
|
440
448
|
return this({
|
|
441
449
|
id,
|
|
442
|
-
hyperdriveId:
|
|
443
|
-
name:
|
|
450
|
+
hyperdriveId: result.id, // Store the Cloudflare-assigned UUID
|
|
451
|
+
name: result.name,
|
|
444
452
|
origin: props.origin,
|
|
445
|
-
caching:
|
|
446
|
-
mtls:
|
|
453
|
+
caching: result.caching,
|
|
454
|
+
mtls: result.mtls,
|
|
447
455
|
dev: props.dev,
|
|
448
456
|
type: "hyperdrive",
|
|
449
457
|
});
|
|
450
458
|
},
|
|
451
459
|
);
|
|
452
460
|
|
|
461
|
+
interface HyperdriveResponse {
|
|
462
|
+
id: string;
|
|
463
|
+
name: string;
|
|
464
|
+
origin: HyperdriveOrigin;
|
|
465
|
+
caching?: HyperdriveCaching;
|
|
466
|
+
created_on?: string;
|
|
467
|
+
modified_on?: string;
|
|
468
|
+
mtls?: HyperdriveMtls;
|
|
469
|
+
origin_connection_limit?: number;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function findHyperdriveConfigByName(
|
|
473
|
+
api: CloudflareApi,
|
|
474
|
+
name: string,
|
|
475
|
+
page = 1,
|
|
476
|
+
) {
|
|
477
|
+
const response = await api.get(
|
|
478
|
+
`/accounts/${api.accountId}/hyperdrive/configs?page=${page}`,
|
|
479
|
+
);
|
|
480
|
+
const data: {
|
|
481
|
+
result: HyperdriveResponse[];
|
|
482
|
+
result_info?: { total_pages?: number };
|
|
483
|
+
} = await response.json();
|
|
484
|
+
const found = data.result.find((config) => config.name === name);
|
|
485
|
+
if (found) {
|
|
486
|
+
return found;
|
|
487
|
+
}
|
|
488
|
+
if (data.result_info?.total_pages && page < data.result_info.total_pages) {
|
|
489
|
+
return await findHyperdriveConfigByName(api, name, page + 1);
|
|
490
|
+
}
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
|
|
453
494
|
/**
|
|
454
495
|
* Prepare the request body by unwrapping secret values
|
|
455
496
|
*/
|
package/src/cloudflare/index.ts
CHANGED
|
@@ -33,6 +33,7 @@ export * from "./email-rule.ts";
|
|
|
33
33
|
export * from "./hyperdrive.ts";
|
|
34
34
|
export * from "./images.ts";
|
|
35
35
|
export * from "./kv-namespace.ts";
|
|
36
|
+
export * from "./next.ts";
|
|
36
37
|
export * from "./nuxt/nuxt.ts";
|
|
37
38
|
export * from "./orange.ts";
|
|
38
39
|
export * from "./permission-groups.ts";
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { bold, cyan } from "picocolors";
|
|
4
|
+
import { getPackageManagerRunner } from "../util/detect-package-manager.ts";
|
|
5
|
+
import { exists } from "../util/exists.ts";
|
|
6
|
+
import ignore from "../util/ignore-matcher.ts";
|
|
7
|
+
import { logger } from "../util/logger.ts";
|
|
8
|
+
import type { Assets } from "./assets.ts";
|
|
9
|
+
import type { Bindings } from "./bindings.ts";
|
|
10
|
+
import { Website, type WebsiteProps } from "./website.ts";
|
|
11
|
+
import type { Worker } from "./worker.ts";
|
|
12
|
+
|
|
13
|
+
export interface NextjsProps<B extends Bindings> extends WebsiteProps<B> {}
|
|
14
|
+
|
|
15
|
+
export type Nextjs<B extends Bindings> = B extends { ASSETS: any }
|
|
16
|
+
? never
|
|
17
|
+
: Worker<B & { ASSETS: Assets }>;
|
|
18
|
+
|
|
19
|
+
export async function Nextjs<const B extends Bindings>(
|
|
20
|
+
id: string,
|
|
21
|
+
props: NextjsProps<B> = {},
|
|
22
|
+
): Promise<Nextjs<B>> {
|
|
23
|
+
const runner = await getPackageManagerRunner();
|
|
24
|
+
const isIgnored = await isFileIgnored("wrangler.jsonc", props.cwd);
|
|
25
|
+
if (!isIgnored) {
|
|
26
|
+
logger.warn(
|
|
27
|
+
[
|
|
28
|
+
bold(
|
|
29
|
+
`The ${cyan("wrangler.jsonc")} file for Next.js ${cyan(`"${id}"`)} is not ignored.`,
|
|
30
|
+
),
|
|
31
|
+
"This may cause unexpected behavior. Please add the file to your .gitignore.",
|
|
32
|
+
].join(" "),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return await Website(id, {
|
|
36
|
+
...props,
|
|
37
|
+
entrypoint: props.entrypoint ?? ".open-next/worker.js",
|
|
38
|
+
wrangler: {
|
|
39
|
+
path: "wrangler.jsonc",
|
|
40
|
+
secrets: isIgnored,
|
|
41
|
+
...(props.wrangler ?? {}),
|
|
42
|
+
},
|
|
43
|
+
build: normalizeCommand(props.build, {
|
|
44
|
+
command: `${runner} opennextjs-cloudflare build`,
|
|
45
|
+
env: {
|
|
46
|
+
NEXTJS_ENV: "production",
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
dev: normalizeCommand(props.dev, {
|
|
50
|
+
command: `${runner} next dev`,
|
|
51
|
+
env: {
|
|
52
|
+
NEXTJS_ENV: "development",
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
|
|
56
|
+
// OpenNext generates the files, but relies on us to bundle them.
|
|
57
|
+
noBundle: props.noBundle ?? false,
|
|
58
|
+
|
|
59
|
+
spa: false,
|
|
60
|
+
compatibilityFlags: [
|
|
61
|
+
"nodejs_compat",
|
|
62
|
+
"global_fetch_strictly_public",
|
|
63
|
+
...(props.compatibilityFlags ?? []),
|
|
64
|
+
],
|
|
65
|
+
assets: props.assets ?? ".open-next/assets",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if a file is ignored by .gitignore.
|
|
71
|
+
*
|
|
72
|
+
* @param filename - The name of the file to check.
|
|
73
|
+
* @param cwd - The current working directory.
|
|
74
|
+
* @returns True if the file is ignored, false otherwise.
|
|
75
|
+
*/
|
|
76
|
+
const isFileIgnored = async (filename: string, cwd?: string) => {
|
|
77
|
+
const matcher = ignore();
|
|
78
|
+
|
|
79
|
+
// Read .gitignore from current directory and walk up the directory tree
|
|
80
|
+
let currentDir = path.resolve(cwd ?? process.cwd());
|
|
81
|
+
const gitignorePatterns: string[] = [];
|
|
82
|
+
|
|
83
|
+
while (currentDir !== path.dirname(currentDir)) {
|
|
84
|
+
const gitignorePath = path.join(currentDir, ".gitignore");
|
|
85
|
+
try {
|
|
86
|
+
const gitignoreContent = await fs.readFile(gitignorePath, "utf8");
|
|
87
|
+
// unshift to prioritize local .gitignore
|
|
88
|
+
gitignorePatterns.unshift(
|
|
89
|
+
...gitignoreContent
|
|
90
|
+
.split("\n")
|
|
91
|
+
.filter((line) => line.trim() && !line.startsWith("#")),
|
|
92
|
+
);
|
|
93
|
+
} catch {
|
|
94
|
+
// .gitignore doesn't exist in this directory, continue up
|
|
95
|
+
}
|
|
96
|
+
if (await exists(path.join(currentDir, ".git"))) {
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
currentDir = path.dirname(currentDir);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
matcher.add(gitignorePatterns);
|
|
103
|
+
|
|
104
|
+
return matcher.ignores(filename);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const normalizeCommand = (
|
|
108
|
+
input: WebsiteProps<any>["build"],
|
|
109
|
+
defaults: {
|
|
110
|
+
command: string;
|
|
111
|
+
env: Record<string, string>;
|
|
112
|
+
},
|
|
113
|
+
): WebsiteProps<any>["build"] => {
|
|
114
|
+
return {
|
|
115
|
+
command:
|
|
116
|
+
typeof input === "string" ? input : (input?.command ?? defaults.command),
|
|
117
|
+
env: {
|
|
118
|
+
...defaults.env,
|
|
119
|
+
...(typeof input === "object" ? (input?.env ?? {}) : {}),
|
|
120
|
+
},
|
|
121
|
+
memoize: typeof input === "object" ? input?.memoize : undefined,
|
|
122
|
+
};
|
|
123
|
+
};
|