alchemy 0.65.0 → 0.66.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 +134 -80
- package/bin/commands/init.ts +6 -3
- package/bin/services/cdp-manager/server.ts +3 -2
- package/bin/services/execute-alchemy.ts +33 -3
- package/bin/services/find-workspace-root.ts +48 -0
- package/lib/alchemy.d.ts +14 -3
- package/lib/alchemy.d.ts.map +1 -1
- package/lib/alchemy.js +17 -5
- package/lib/alchemy.js.map +1 -1
- package/lib/apply.d.ts.map +1 -1
- package/lib/apply.js +51 -1
- package/lib/apply.js.map +1 -1
- package/lib/cloudflare/bucket.js +1 -1
- package/lib/cloudflare/bucket.js.map +1 -1
- package/lib/cloudflare/bundle/plugin-node-compat.js +2 -2
- package/lib/cloudflare/bundle/plugin-node-compat.js.map +1 -1
- package/lib/cloudflare/cloudflare-env-proxy.js +3 -3
- package/lib/cloudflare/cloudflare-env-proxy.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/d1-database.d.ts.map +1 -1
- package/lib/cloudflare/d1-database.js +2 -1
- package/lib/cloudflare/d1-database.js.map +1 -1
- package/lib/cloudflare/d1-local-migrations.d.ts +1 -0
- package/lib/cloudflare/d1-local-migrations.d.ts.map +1 -1
- package/lib/cloudflare/d1-local-migrations.js +3 -2
- package/lib/cloudflare/d1-local-migrations.js.map +1 -1
- package/lib/cloudflare/kv-namespace.js +1 -1
- package/lib/cloudflare/kv-namespace.js.map +1 -1
- package/lib/cloudflare/miniflare/delete.d.ts +3 -2
- package/lib/cloudflare/miniflare/delete.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/delete.js +13 -12
- package/lib/cloudflare/miniflare/delete.js.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-controller.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-controller.js +5 -4
- package/lib/cloudflare/miniflare/miniflare-controller.js.map +1 -1
- package/lib/cloudflare/miniflare/paths.d.ts +4 -4
- package/lib/cloudflare/miniflare/paths.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/paths.js +10 -4
- package/lib/cloudflare/miniflare/paths.js.map +1 -1
- package/lib/cloudflare/miniflare/symlink-miniflare-state.d.ts +2 -0
- package/lib/cloudflare/miniflare/symlink-miniflare-state.d.ts.map +1 -0
- package/lib/cloudflare/miniflare/symlink-miniflare-state.js +19 -0
- package/lib/cloudflare/miniflare/symlink-miniflare-state.js.map +1 -0
- package/lib/cloudflare/next.js +2 -2
- package/lib/cloudflare/next.js.map +1 -1
- package/lib/cloudflare/nuxt/plugin.d.ts.map +1 -1
- package/lib/cloudflare/nuxt/plugin.js +3 -3
- package/lib/cloudflare/nuxt/plugin.js.map +1 -1
- package/lib/cloudflare/redwood/plugin.js +2 -2
- package/lib/cloudflare/redwood/plugin.js.map +1 -1
- package/lib/cloudflare/vite/plugin.d.ts.map +1 -1
- package/lib/cloudflare/vite/plugin.js +6 -3
- package/lib/cloudflare/vite/plugin.js.map +1 -1
- package/lib/cloudflare/website.d.ts.map +1 -1
- package/lib/cloudflare/website.js +5 -16
- package/lib/cloudflare/website.js.map +1 -1
- package/lib/cloudflare/worker.d.ts.map +1 -1
- package/lib/cloudflare/worker.js +4 -2
- package/lib/cloudflare/worker.js.map +1 -1
- package/lib/cloudflare/wrangler.json.js +1 -0
- package/lib/cloudflare/wrangler.json.js.map +1 -1
- package/lib/scope.d.ts +16 -0
- package/lib/scope.d.ts.map +1 -1
- package/lib/scope.js +10 -5
- package/lib/scope.js.map +1 -1
- package/lib/state/file-system-state-store.d.ts.map +1 -1
- package/lib/state/file-system-state-store.js +1 -2
- package/lib/state/file-system-state-store.js.map +1 -1
- package/lib/state/sqlite-state-store.d.ts +11 -0
- package/lib/state/sqlite-state-store.d.ts.map +1 -1
- package/lib/state/sqlite-state-store.js +22 -14
- package/lib/state/sqlite-state-store.js.map +1 -1
- package/lib/stripe/customer.d.ts +0 -8
- package/lib/stripe/customer.d.ts.map +1 -1
- package/lib/stripe/customer.js +0 -6
- package/lib/stripe/customer.js.map +1 -1
- package/lib/stripe/price.d.ts +23 -23
- package/lib/stripe/price.d.ts.map +1 -1
- package/lib/stripe/price.js +6 -22
- package/lib/stripe/price.js.map +1 -1
- package/lib/test/vitest.d.ts.map +1 -1
- package/lib/test/vitest.js +1 -1
- package/lib/test/vitest.js.map +1 -1
- package/lib/util/assert-never.js +1 -1
- package/lib/util/assert-never.js.map +1 -1
- package/lib/util/cli.d.ts.map +1 -1
- package/lib/util/cli.js +12 -13
- package/lib/util/cli.js.map +1 -1
- package/lib/util/memoize.d.ts +1 -1
- package/lib/util/memoize.d.ts.map +1 -1
- package/lib/util/memoize.js +2 -2
- package/lib/util/memoize.js.map +1 -1
- package/lib/util/root-dir.d.ts +2 -0
- package/lib/util/root-dir.d.ts.map +1 -0
- package/lib/util/root-dir.js +2 -0
- package/lib/util/root-dir.js.map +1 -0
- package/package.json +4 -4
- package/src/alchemy.ts +36 -8
- package/src/apply.ts +58 -3
- package/src/cloudflare/bucket.ts +1 -1
- package/src/cloudflare/bundle/plugin-node-compat.ts +2 -2
- package/src/cloudflare/cloudflare-env-proxy.ts +5 -5
- package/src/cloudflare/compatibility-date.gen.ts +1 -1
- package/src/cloudflare/d1-database.ts +2 -1
- package/src/cloudflare/d1-local-migrations.ts +10 -6
- package/src/cloudflare/kv-namespace.ts +1 -1
- package/src/cloudflare/miniflare/delete.ts +19 -11
- package/src/cloudflare/miniflare/miniflare-controller.ts +10 -6
- package/src/cloudflare/miniflare/paths.ts +11 -7
- package/src/cloudflare/miniflare/symlink-miniflare-state.ts +19 -0
- package/src/cloudflare/next.ts +3 -3
- package/src/cloudflare/nuxt/plugin.ts +12 -3
- package/src/cloudflare/redwood/plugin.ts +2 -2
- package/src/cloudflare/vite/plugin.ts +8 -3
- package/src/cloudflare/website.ts +5 -19
- package/src/cloudflare/worker.ts +4 -2
- package/src/cloudflare/wrangler.json.ts +1 -0
- package/src/scope.ts +26 -5
- package/src/state/file-system-state-store.ts +1 -2
- package/src/state/sqlite-state-store.ts +41 -14
- package/src/stripe/customer.ts +0 -14
- package/src/stripe/price.ts +59 -56
- package/src/test/vitest.ts +4 -1
- package/src/util/assert-never.ts +1 -1
- package/src/util/cli.ts +15 -13
- package/src/util/memoize.ts +2 -2
- package/src/util/root-dir.ts +1 -0
- package/templates/nextjs/alchemy.run.ts +2 -2
- package/workers/cloudflare-state-store.js +1 -1
package/src/apply.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "./resource.ts";
|
|
16
16
|
import type { PendingDeletions } from "./scope.ts";
|
|
17
17
|
import { serialize } from "./serde.ts";
|
|
18
|
+
import type { State } from "./state.ts";
|
|
18
19
|
import { formatFQN } from "./util/cli.ts";
|
|
19
20
|
import { logger } from "./util/logger.ts";
|
|
20
21
|
import type { Telemetry } from "./util/telemetry/index.ts";
|
|
@@ -64,16 +65,69 @@ async function _apply<Out extends Resource>(
|
|
|
64
65
|
}
|
|
65
66
|
if (scope.phase === "read") {
|
|
66
67
|
if (state === undefined) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
if (scope.isSelected === false) {
|
|
69
|
+
// we are running in a monorepo and are not the selected app
|
|
70
|
+
if (process.argv.includes("--destroy")) {
|
|
71
|
+
// if we are trying to destroy a downstream app and this (upstream) app does not have data, then exit
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
// if we are in `--deploy`, then poll until state available
|
|
75
|
+
state = await waitForConsistentState();
|
|
76
|
+
} else {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Resource "${resource[ResourceFQN]}" not found and running in 'read' phase. Selected(${scope.isSelected})`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
} else if (scope.isSelected === false) {
|
|
82
|
+
// we are running in a monorepo and are not the selected app, so we need to wait for the process to be consistent
|
|
83
|
+
state = await waitForConsistentState();
|
|
70
84
|
}
|
|
71
85
|
scope.telemetryClient.record({
|
|
72
86
|
event: "resource.read",
|
|
73
87
|
resource: resource[ResourceKind],
|
|
74
88
|
});
|
|
75
89
|
return state.output as Awaited<Out>;
|
|
90
|
+
|
|
91
|
+
// -> poll until it does not (i.e. when the owner process applies the change and updates the state store)
|
|
92
|
+
async function waitForConsistentState() {
|
|
93
|
+
while (true) {
|
|
94
|
+
if (state === undefined) {
|
|
95
|
+
// state doesn't exist yet
|
|
96
|
+
} else if (
|
|
97
|
+
state.status === "creating" ||
|
|
98
|
+
state.status === "updating"
|
|
99
|
+
) {
|
|
100
|
+
// no-op
|
|
101
|
+
} else if (
|
|
102
|
+
state.status === "deleted" ||
|
|
103
|
+
state.status === "deleting"
|
|
104
|
+
) {
|
|
105
|
+
// ok something is wrong, the stack should not be being deleted
|
|
106
|
+
// TODO(sam): better error message
|
|
107
|
+
throw new Error("Resource is being deleted");
|
|
108
|
+
} else if (await inputsAreEqual(state)) {
|
|
109
|
+
// sweet, we've reached a stable state and read can progress
|
|
110
|
+
return state;
|
|
111
|
+
}
|
|
112
|
+
// jitter between 100-300ms
|
|
113
|
+
const jitter = 100 + Math.random() * 200;
|
|
114
|
+
await new Promise((resolve) => setTimeout(resolve, jitter));
|
|
115
|
+
state = await scope.state.get(resource[ResourceID]);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function inputsAreEqual(
|
|
119
|
+
state: State<string, ResourceProps | undefined, Resource<string>>,
|
|
120
|
+
) {
|
|
121
|
+
const oldProps = await serialize(scope, state.props, {
|
|
122
|
+
encrypt: false,
|
|
123
|
+
});
|
|
124
|
+
const newProps = await serialize(scope, props, {
|
|
125
|
+
encrypt: false,
|
|
126
|
+
});
|
|
127
|
+
return JSON.stringify(oldProps) === JSON.stringify(newProps);
|
|
128
|
+
}
|
|
76
129
|
}
|
|
130
|
+
|
|
77
131
|
if (state === undefined) {
|
|
78
132
|
state = {
|
|
79
133
|
kind: resource[ResourceKind],
|
|
@@ -96,6 +150,7 @@ async function _apply<Out extends Resource>(
|
|
|
96
150
|
};
|
|
97
151
|
await scope.state.set(resource[ResourceID], state);
|
|
98
152
|
}
|
|
153
|
+
|
|
99
154
|
const oldOutput = state.output;
|
|
100
155
|
|
|
101
156
|
const alwaysUpdate =
|
package/src/cloudflare/bucket.ts
CHANGED
|
@@ -388,7 +388,7 @@ const _R2Bucket = Resource(
|
|
|
388
388
|
if (this.phase === "delete") {
|
|
389
389
|
if (props.delete !== false) {
|
|
390
390
|
if (this.output.dev?.id) {
|
|
391
|
-
await deleteMiniflareBinding("r2", this.output.dev.id);
|
|
391
|
+
await deleteMiniflareBinding(this.scope, "r2", this.output.dev.id);
|
|
392
392
|
}
|
|
393
393
|
if (props.empty) {
|
|
394
394
|
await emptyBucket(api, bucketName, props);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Plugin } from "esbuild";
|
|
2
2
|
import type { NodeJSCompatMode } from "miniflare";
|
|
3
3
|
import { relative } from "node:path";
|
|
4
|
-
import
|
|
4
|
+
import pc from "picocolors";
|
|
5
5
|
import { dedent } from "../../util/dedent.ts";
|
|
6
6
|
import { logger } from "../../util/logger.ts";
|
|
7
7
|
import { NODEJS_MODULES_RE } from "./nodejs-builtin-modules.ts";
|
|
@@ -109,6 +109,6 @@ export const esbuildPluginNodeCompatImports = (
|
|
|
109
109
|
|
|
110
110
|
function toList(items: string[], absWorkingDir: string | undefined): string {
|
|
111
111
|
return items
|
|
112
|
-
.map((i) => ` - ${
|
|
112
|
+
.map((i) => ` - ${pc.blue(relative(absWorkingDir ?? "/", i))}`)
|
|
113
113
|
.join("\n");
|
|
114
114
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { GetPlatformProxyOptions } from "wrangler";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
getDefaultConfigPath,
|
|
4
|
+
getDefaultPersistPath,
|
|
5
5
|
validateConfigPath,
|
|
6
6
|
validatePersistPath,
|
|
7
7
|
} from "./miniflare/paths.ts";
|
|
@@ -26,7 +26,7 @@ export const getPlatformProxyOptions = (
|
|
|
26
26
|
typeof input.persist === "object" &&
|
|
27
27
|
typeof input.persist.path === "string"
|
|
28
28
|
? input.persist.path
|
|
29
|
-
:
|
|
29
|
+
: getDefaultPersistPath(),
|
|
30
30
|
),
|
|
31
31
|
};
|
|
32
32
|
if (!persist) {
|
|
@@ -39,12 +39,12 @@ export const getPlatformProxyOptions = (
|
|
|
39
39
|
}
|
|
40
40
|
return {
|
|
41
41
|
...input,
|
|
42
|
-
configPath: validateConfigPath(input.configPath ??
|
|
42
|
+
configPath: validateConfigPath(input.configPath ?? getDefaultConfigPath()),
|
|
43
43
|
persist,
|
|
44
44
|
experimental: (input as any).experimental ?? {
|
|
45
45
|
remoteBindings: true,
|
|
46
46
|
},
|
|
47
|
-
}
|
|
47
|
+
};
|
|
48
48
|
};
|
|
49
49
|
|
|
50
50
|
const warned = new Set<string>();
|
|
@@ -283,6 +283,7 @@ const _D1Database = Resource(
|
|
|
283
283
|
databaseId: dev.id,
|
|
284
284
|
migrationsTable: props.migrationsTable ?? DEFAULT_MIGRATIONS_TABLE,
|
|
285
285
|
migrations: props.migrationsFiles,
|
|
286
|
+
rootDir: this.scope.rootDir,
|
|
286
287
|
});
|
|
287
288
|
}
|
|
288
289
|
return this({
|
|
@@ -301,7 +302,7 @@ const _D1Database = Resource(
|
|
|
301
302
|
|
|
302
303
|
if (this.phase === "delete") {
|
|
303
304
|
if (this.output.dev?.id) {
|
|
304
|
-
await deleteMiniflareBinding("d1", this.output.dev.id);
|
|
305
|
+
await deleteMiniflareBinding(this.scope, "d1", this.output.dev.id);
|
|
305
306
|
}
|
|
306
307
|
if (props.delete !== false && this.output?.id) {
|
|
307
308
|
await deleteDatabase(api, this.output.id);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as mf from "miniflare";
|
|
2
|
-
import
|
|
2
|
+
import { getDefaultPersistPath } from "./miniflare/paths.ts";
|
|
3
3
|
|
|
4
4
|
export interface D1LocalMigrationOptions {
|
|
5
|
+
rootDir: string;
|
|
5
6
|
databaseId: string;
|
|
6
7
|
migrationsTable: string;
|
|
7
8
|
migrations: { id: string; sql: string }[];
|
|
@@ -13,15 +14,16 @@ export const applyLocalD1Migrations = async (
|
|
|
13
14
|
const miniflare = new mf.Miniflare({
|
|
14
15
|
script: "",
|
|
15
16
|
modules: true,
|
|
16
|
-
defaultPersistRoot:
|
|
17
|
+
defaultPersistRoot: getDefaultPersistPath(options.rootDir),
|
|
17
18
|
d1Persist: true,
|
|
18
19
|
d1Databases: { DB: options.databaseId },
|
|
19
20
|
log: process.env.DEBUG ? new mf.Log(mf.LogLevel.DEBUG) : undefined,
|
|
20
21
|
});
|
|
21
22
|
try {
|
|
22
23
|
await miniflare.ready;
|
|
23
|
-
|
|
24
|
-
const
|
|
24
|
+
// TODO(sam): don't use `any` once prisma is fixed upstream
|
|
25
|
+
const db: any = await miniflare.getD1Database("DB");
|
|
26
|
+
const session: any = db.withSession("first-primary");
|
|
25
27
|
await session
|
|
26
28
|
.prepare(
|
|
27
29
|
`CREATE TABLE IF NOT EXISTS ${options.migrationsTable} (
|
|
@@ -31,11 +33,13 @@ export const applyLocalD1Migrations = async (
|
|
|
31
33
|
)`,
|
|
32
34
|
)
|
|
33
35
|
.run();
|
|
34
|
-
const appliedMigrations
|
|
36
|
+
const appliedMigrations: {
|
|
37
|
+
results: { name: string }[];
|
|
38
|
+
} = await session
|
|
35
39
|
.prepare(
|
|
36
40
|
`SELECT name FROM ${options.migrationsTable} ORDER BY applied_at ASC`,
|
|
37
41
|
)
|
|
38
|
-
.all
|
|
42
|
+
.all();
|
|
39
43
|
const insertRecord = session.prepare(
|
|
40
44
|
`INSERT INTO ${options.migrationsTable} (name) VALUES (?)`,
|
|
41
45
|
);
|
|
@@ -240,7 +240,7 @@ const _KVNamespace = Resource(
|
|
|
240
240
|
|
|
241
241
|
if (this.phase === "delete") {
|
|
242
242
|
if (this.output.dev?.id) {
|
|
243
|
-
await deleteMiniflareBinding("kv", this.output.dev.id);
|
|
243
|
+
await deleteMiniflareBinding(this.scope, "kv", this.output.dev.id);
|
|
244
244
|
}
|
|
245
245
|
if (this.output.namespaceId && props.delete !== false) {
|
|
246
246
|
await deleteKVNamespace(api, this.output.namespaceId);
|
|
@@ -2,11 +2,11 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import type { RmDirOptions } from "node:fs";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "pathe";
|
|
5
|
+
import type { Scope } from "../../scope.ts";
|
|
5
6
|
import type { DurableObjectNamespace } from "../durable-object-namespace.ts";
|
|
6
7
|
import type { Workflow } from "../workflow.ts";
|
|
7
|
-
import {
|
|
8
|
+
import { getDefaultPersistPath } from "./paths.ts";
|
|
8
9
|
|
|
9
|
-
const PERSIST_ROOT = path.resolve(DEFAULT_PERSIST_PATH);
|
|
10
10
|
const BINDING_TYPE_KEY = {
|
|
11
11
|
d1: "miniflare-D1DatabaseObject",
|
|
12
12
|
kv: "miniflare-KVNamespaceObject",
|
|
@@ -18,8 +18,14 @@ type BindingType = keyof typeof BINDING_TYPE_KEY;
|
|
|
18
18
|
/**
|
|
19
19
|
* Delete a binding from the miniflare directory.
|
|
20
20
|
*/
|
|
21
|
-
export async function deleteMiniflareBinding(
|
|
22
|
-
|
|
21
|
+
export async function deleteMiniflareBinding(
|
|
22
|
+
scope: Scope,
|
|
23
|
+
type: BindingType,
|
|
24
|
+
id: string,
|
|
25
|
+
) {
|
|
26
|
+
const persistRoot = path.resolve(getDefaultPersistPath(scope.rootDir));
|
|
27
|
+
|
|
28
|
+
const bindingPath = path.join(persistRoot, type, BINDING_TYPE_KEY[type]);
|
|
23
29
|
|
|
24
30
|
// delete durable object/sqlite files
|
|
25
31
|
const namespaceId = durableObjectNamespaceIdFromName(
|
|
@@ -33,29 +39,31 @@ export async function deleteMiniflareBinding(type: BindingType, id: string) {
|
|
|
33
39
|
await Promise.all(files.map((file) => fs.rm(path.join(bindingPath, file))));
|
|
34
40
|
|
|
35
41
|
// delete data directory, if present
|
|
36
|
-
await removeDirectory(path.join(
|
|
42
|
+
await removeDirectory(path.join(persistRoot, type, id), {
|
|
37
43
|
recursive: true,
|
|
38
44
|
});
|
|
39
45
|
|
|
40
46
|
// delete the miniflare directory if it is empty
|
|
41
|
-
await cleanMiniflareDirectory();
|
|
47
|
+
await cleanMiniflareDirectory(scope.dotAlchemy);
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
/**
|
|
45
51
|
* Delete local durable object and workflow data for a given Worker script.
|
|
46
52
|
*/
|
|
47
53
|
export async function deleteMiniflareWorkerData(
|
|
54
|
+
scope: Scope,
|
|
48
55
|
scriptName: string,
|
|
49
56
|
input: {
|
|
50
57
|
durableObjects: DurableObjectNamespace[];
|
|
51
58
|
workflows: Workflow[];
|
|
52
59
|
},
|
|
53
60
|
) {
|
|
61
|
+
const persistRoot = path.resolve(getDefaultPersistPath(scope.rootDir));
|
|
54
62
|
await Promise.all(
|
|
55
63
|
input.durableObjects.map((durableObject) =>
|
|
56
64
|
removeDirectory(
|
|
57
65
|
path.join(
|
|
58
|
-
|
|
66
|
+
persistRoot,
|
|
59
67
|
"do",
|
|
60
68
|
`${scriptName}-${durableObject.className}`,
|
|
61
69
|
),
|
|
@@ -69,7 +77,7 @@ export async function deleteMiniflareWorkerData(
|
|
|
69
77
|
input.workflows.map((workflow) =>
|
|
70
78
|
removeDirectory(
|
|
71
79
|
path.join(
|
|
72
|
-
|
|
80
|
+
persistRoot,
|
|
73
81
|
"workflows",
|
|
74
82
|
`miniflare-workflows-${workflow.workflowName}`,
|
|
75
83
|
),
|
|
@@ -79,14 +87,14 @@ export async function deleteMiniflareWorkerData(
|
|
|
79
87
|
),
|
|
80
88
|
),
|
|
81
89
|
);
|
|
82
|
-
await cleanMiniflareDirectory();
|
|
90
|
+
await cleanMiniflareDirectory(persistRoot);
|
|
83
91
|
}
|
|
84
92
|
|
|
85
93
|
/**
|
|
86
94
|
* Delete the miniflare directory iff it is empty.
|
|
87
95
|
*/
|
|
88
|
-
async function cleanMiniflareDirectory() {
|
|
89
|
-
const directory = path.resolve("
|
|
96
|
+
async function cleanMiniflareDirectory(dotAlchemy: string) {
|
|
97
|
+
const directory = path.resolve(dotAlchemy, "miniflare");
|
|
90
98
|
const files = await fs
|
|
91
99
|
.readdir(directory, {
|
|
92
100
|
recursive: true,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as miniflare from "miniflare";
|
|
2
2
|
import assert from "node:assert";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { Scope } from "../../scope.ts";
|
|
4
5
|
import { findOpenPort } from "../../util/find-open-port.ts";
|
|
5
6
|
import type { HTTPServer } from "../../util/http.ts";
|
|
6
7
|
import { logger } from "../../util/logger.ts";
|
|
@@ -10,7 +11,7 @@ import {
|
|
|
10
11
|
type MiniflareWorkerInput,
|
|
11
12
|
} from "./build-worker-options.ts";
|
|
12
13
|
import { MiniflareWorkerProxy } from "./miniflare-worker-proxy.ts";
|
|
13
|
-
import {
|
|
14
|
+
import { getDefaultPersistPath } from "./paths.ts";
|
|
14
15
|
|
|
15
16
|
declare global {
|
|
16
17
|
var ALCHEMY_MINIFLARE_CONTROLLER: MiniflareController | undefined;
|
|
@@ -25,8 +26,8 @@ export class MiniflareController {
|
|
|
25
26
|
mutex = new AsyncMutex();
|
|
26
27
|
|
|
27
28
|
static get singleton() {
|
|
28
|
-
globalThis.ALCHEMY_MINIFLARE_CONTROLLER ??=
|
|
29
|
-
|
|
29
|
+
return (globalThis.ALCHEMY_MINIFLARE_CONTROLLER ??=
|
|
30
|
+
new MiniflareController());
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
async add(input: MiniflareWorkerInput) {
|
|
@@ -75,14 +76,17 @@ export class MiniflareController {
|
|
|
75
76
|
|
|
76
77
|
private async update() {
|
|
77
78
|
return await this.mutex.lock(async () => {
|
|
78
|
-
const options: miniflare.MiniflareOptions
|
|
79
|
+
const options: miniflare.MiniflareOptions & {
|
|
80
|
+
workers: miniflare.WorkerOptions[];
|
|
81
|
+
} = {
|
|
79
82
|
workers: [],
|
|
80
|
-
defaultPersistRoot: path.resolve(
|
|
83
|
+
defaultPersistRoot: path.resolve(
|
|
84
|
+
getDefaultPersistPath(Scope.current.rootDir),
|
|
85
|
+
),
|
|
81
86
|
unsafeDevRegistryPath: miniflare.getDefaultDevRegistryPath(),
|
|
82
87
|
log: process.env.DEBUG
|
|
83
88
|
? new miniflare.Log(miniflare.LogLevel.DEBUG)
|
|
84
89
|
: undefined,
|
|
85
|
-
|
|
86
90
|
// This is required to allow websites and other separate processes
|
|
87
91
|
// to detect Alchemy-managed Durable Objects via the Wrangler dev registry.
|
|
88
92
|
unsafeDevRegistryDurableObjectProxy: true,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { existsSync, readlinkSync, statSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { ALCHEMY_ROOT } from "../../util/root-dir.ts";
|
|
3
5
|
|
|
4
6
|
const dynamicImportContext = new AsyncLocalStorage<boolean>();
|
|
5
7
|
|
|
@@ -10,13 +12,15 @@ export const withSkipPathValidation = <T>(callback: () => T) => {
|
|
|
10
12
|
return dynamicImportContext.run(true, callback);
|
|
11
13
|
};
|
|
12
14
|
|
|
13
|
-
export const
|
|
14
|
-
|
|
15
|
+
export const getDefaultConfigPath = (rootDir: string = process.cwd()) => {
|
|
16
|
+
return path.join(rootDir, ".alchemy", "local", "wrangler.jsonc");
|
|
17
|
+
};
|
|
15
18
|
|
|
16
|
-
export const
|
|
17
|
-
path
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
export const getDefaultPersistPath = (rootDir: string = ALCHEMY_ROOT) => {
|
|
20
|
+
return path.join(rootDir, ".alchemy", "miniflare", "v3");
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const validateConfigPath = (path: string, throws = true) => {
|
|
20
24
|
if (!existsSync(path)) {
|
|
21
25
|
warnOrThrow(
|
|
22
26
|
`The Wrangler config path, "${path}", could not be found. Please run \`alchemy dev\` or \`alchemy deploy\` to create it.`,
|
|
@@ -31,7 +35,7 @@ const DEFAULT_VALIDATE_PERSIST =
|
|
|
31
35
|
!process.argv.some((arg) => ["build", "prepare", "typegen"].includes(arg));
|
|
32
36
|
|
|
33
37
|
export const validatePersistPath = (
|
|
34
|
-
path
|
|
38
|
+
path: string,
|
|
35
39
|
throws = DEFAULT_VALIDATE_PERSIST,
|
|
36
40
|
) => {
|
|
37
41
|
try {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getDefaultPersistPath } from "./paths.ts";
|
|
4
|
+
|
|
5
|
+
export async function writeMiniflareSymlink(rootDir: string, cwd: string) {
|
|
6
|
+
const target = path.resolve(getDefaultPersistPath(cwd));
|
|
7
|
+
await fs.mkdir(target, { recursive: true });
|
|
8
|
+
if (cwd === process.cwd()) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const persistPath = path.resolve(cwd, getDefaultPersistPath(rootDir));
|
|
12
|
+
console.log("link", target, persistPath);
|
|
13
|
+
await fs.mkdir(path.dirname(persistPath), { recursive: true });
|
|
14
|
+
await fs.symlink(target, persistPath).catch((e) => {
|
|
15
|
+
if (e.code !== "EEXIST") {
|
|
16
|
+
throw e;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
package/src/cloudflare/next.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import
|
|
3
|
+
import pc from "picocolors";
|
|
4
4
|
import { getPackageManagerRunner } from "../util/detect-package-manager.ts";
|
|
5
5
|
import { exists } from "../util/exists.ts";
|
|
6
6
|
import ignore from "../util/ignore-matcher.ts";
|
|
@@ -25,8 +25,8 @@ export async function Nextjs<const B extends Bindings>(
|
|
|
25
25
|
if (!isIgnored) {
|
|
26
26
|
logger.warn(
|
|
27
27
|
[
|
|
28
|
-
bold(
|
|
29
|
-
`The ${cyan("wrangler.jsonc")} file for Next.js ${cyan(`"${id}"`)} is not ignored.`,
|
|
28
|
+
pc.bold(
|
|
29
|
+
`The ${pc.cyan("wrangler.jsonc")} file for Next.js ${pc.cyan(`"${id}"`)} is not ignored.`,
|
|
30
30
|
),
|
|
31
31
|
"This may cause unexpected behavior. Please add the file to your .gitignore.",
|
|
32
32
|
].join(" "),
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { CloudflareOptions } from "nitropack/presets/cloudflare/types";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getDefaultConfigPath,
|
|
4
|
+
getDefaultPersistPath,
|
|
5
|
+
validateConfigPath,
|
|
6
|
+
validatePersistPath,
|
|
7
|
+
} from "../miniflare/paths.ts";
|
|
3
8
|
|
|
4
9
|
const alchemy = (
|
|
5
10
|
options: Partial<CloudflareOptions> = {},
|
|
@@ -7,8 +12,12 @@ const alchemy = (
|
|
|
7
12
|
return {
|
|
8
13
|
nodeCompat: true,
|
|
9
14
|
dev: {
|
|
10
|
-
configPath: validateConfigPath(
|
|
11
|
-
|
|
15
|
+
configPath: validateConfigPath(
|
|
16
|
+
options.dev?.configPath ?? getDefaultConfigPath(),
|
|
17
|
+
),
|
|
18
|
+
persistDir: validatePersistPath(
|
|
19
|
+
options.dev?.persistDir ?? getDefaultPersistPath(),
|
|
20
|
+
),
|
|
12
21
|
...options.dev,
|
|
13
22
|
},
|
|
14
23
|
...options,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { redwood, type RedwoodPluginOptions } from "rwsdk/vite";
|
|
2
2
|
import type { PluginOption } from "vite";
|
|
3
|
-
import {
|
|
3
|
+
import { getDefaultConfigPath } from "../miniflare/paths.ts";
|
|
4
4
|
import alchemyVite from "../vite/plugin.ts";
|
|
5
5
|
|
|
6
6
|
const alchemyRedwood = (options?: RedwoodPluginOptions): PluginOption => {
|
|
7
|
-
const configPath = options?.configPath ??
|
|
7
|
+
const configPath = options?.configPath ?? getDefaultConfigPath();
|
|
8
8
|
return [
|
|
9
9
|
redwood({
|
|
10
10
|
...options,
|
|
@@ -2,7 +2,8 @@ import { cloudflare, type PluginConfig } from "@cloudflare/vite-plugin";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { PluginOption } from "vite";
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
getDefaultConfigPath,
|
|
6
|
+
getDefaultPersistPath,
|
|
6
7
|
validateConfigPath,
|
|
7
8
|
validatePersistPath,
|
|
8
9
|
} from "../miniflare/paths.ts";
|
|
@@ -12,7 +13,8 @@ const alchemy = (config?: PluginConfig): PluginOption => {
|
|
|
12
13
|
path: validatePersistPath(
|
|
13
14
|
typeof config?.persistState === "object"
|
|
14
15
|
? config.persistState.path
|
|
15
|
-
:
|
|
16
|
+
: // persist path should default to the /.alchemy/miniflare/v3
|
|
17
|
+
getDefaultPersistPath(),
|
|
16
18
|
),
|
|
17
19
|
};
|
|
18
20
|
if (typeof persistState === "object" && persistState.path.endsWith("v3")) {
|
|
@@ -20,7 +22,10 @@ const alchemy = (config?: PluginConfig): PluginOption => {
|
|
|
20
22
|
}
|
|
21
23
|
return cloudflare({
|
|
22
24
|
...config,
|
|
23
|
-
configPath: validateConfigPath(
|
|
25
|
+
configPath: validateConfigPath(
|
|
26
|
+
// config path doesn't need to be in the root, it can be in the app dir
|
|
27
|
+
config?.configPath ?? getDefaultConfigPath(),
|
|
28
|
+
),
|
|
24
29
|
persistState,
|
|
25
30
|
experimental: config?.experimental ?? {
|
|
26
31
|
remoteBindings: true,
|
|
@@ -10,7 +10,7 @@ import { Assets } from "./assets.ts";
|
|
|
10
10
|
import type { Bindings } from "./bindings.ts";
|
|
11
11
|
import { DEFAULT_COMPATIBILITY_DATE } from "./compatibility-date.gen.ts";
|
|
12
12
|
import { unionCompatibilityFlags } from "./compatibility-presets.ts";
|
|
13
|
-
import {
|
|
13
|
+
import { writeMiniflareSymlink } from "./miniflare/symlink-miniflare-state.ts";
|
|
14
14
|
import { type AssetsConfig, Worker, type WorkerProps } from "./worker.ts";
|
|
15
15
|
import { WranglerJson, type WranglerJsonSpec } from "./wrangler.json.ts";
|
|
16
16
|
|
|
@@ -267,7 +267,9 @@ export async function Website<B extends Bindings>(
|
|
|
267
267
|
await fs.writeFile(paths.entrypoint, content);
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
-
|
|
270
|
+
if (Scope.current.local) {
|
|
271
|
+
await writeMiniflareSymlink(Scope.current.rootDir, paths.cwd);
|
|
272
|
+
}
|
|
271
273
|
|
|
272
274
|
await WranglerJson({
|
|
273
275
|
path: path.relative(paths.cwd, paths.wrangler.path),
|
|
@@ -293,6 +295,7 @@ export async function Website<B extends Bindings>(
|
|
|
293
295
|
...env,
|
|
294
296
|
...(typeof build === "object" ? build.env : {}),
|
|
295
297
|
NODE_ENV: "production",
|
|
298
|
+
ALCHEMY_ROOT: Scope.current.rootDir,
|
|
296
299
|
},
|
|
297
300
|
memoize: typeof build === "object" ? build.memoize : undefined,
|
|
298
301
|
});
|
|
@@ -350,20 +353,3 @@ export async function Website<B extends Bindings>(
|
|
|
350
353
|
dev: url ? { url } : undefined,
|
|
351
354
|
})) as Website<B>;
|
|
352
355
|
}
|
|
353
|
-
|
|
354
|
-
async function writeMiniflareSymlink(cwd: string) {
|
|
355
|
-
const target = path.resolve(DEFAULT_PERSIST_PATH);
|
|
356
|
-
await fs.mkdir(target, { recursive: true });
|
|
357
|
-
|
|
358
|
-
if (cwd === process.cwd()) {
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
const persistPath = path.resolve(cwd, DEFAULT_PERSIST_PATH);
|
|
363
|
-
await fs.mkdir(path.dirname(persistPath), { recursive: true });
|
|
364
|
-
await fs.symlink(target, persistPath).catch((e) => {
|
|
365
|
-
if (e.code !== "EEXIST") {
|
|
366
|
-
throw e;
|
|
367
|
-
}
|
|
368
|
-
});
|
|
369
|
-
}
|
package/src/cloudflare/worker.ts
CHANGED
|
@@ -755,7 +755,9 @@ const _Worker = Resource(
|
|
|
755
755
|
compatibilityDate,
|
|
756
756
|
compatibilityFlags,
|
|
757
757
|
outdir:
|
|
758
|
-
props.bundle?.outdir ??
|
|
758
|
+
props.bundle?.outdir ??
|
|
759
|
+
// the out folder can't be moved to the root of a monorepo, it must be ${cwd} or else miniflare throws a fit
|
|
760
|
+
path.join(process.cwd(), ".alchemy", "out", workerName),
|
|
759
761
|
sourceMap: "sourceMap" in props ? props.sourceMap : undefined,
|
|
760
762
|
});
|
|
761
763
|
|
|
@@ -812,7 +814,7 @@ const _Worker = Resource(
|
|
|
812
814
|
if (options.bundle.isOk()) {
|
|
813
815
|
await options.bundle.value.delete?.();
|
|
814
816
|
}
|
|
815
|
-
await deleteMiniflareWorkerData(options.name, {
|
|
817
|
+
await deleteMiniflareWorkerData(this.scope, options.name, {
|
|
816
818
|
durableObjects: options.durableObjects,
|
|
817
819
|
workflows: options.workflows,
|
|
818
820
|
});
|
package/src/scope.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
} from "./util/idempotent-spawn.ts";
|
|
29
29
|
import { logger } from "./util/logger.ts";
|
|
30
30
|
import { AsyncMutex } from "./util/mutex.ts";
|
|
31
|
+
import { ALCHEMY_ROOT } from "./util/root-dir.ts";
|
|
31
32
|
import type { ITelemetryClient } from "./util/telemetry/client.ts";
|
|
32
33
|
|
|
33
34
|
export class RootScopeStateAttemptError extends Error {
|
|
@@ -89,6 +90,20 @@ export interface ScopeOptions extends ProviderCredentials {
|
|
|
89
90
|
* @default false
|
|
90
91
|
*/
|
|
91
92
|
adopt?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* The path to the root directory of the project.
|
|
95
|
+
*
|
|
96
|
+
* @default process.cwd()
|
|
97
|
+
*/
|
|
98
|
+
rootDir?: string;
|
|
99
|
+
/**
|
|
100
|
+
* Whether this is the application that was selected with `--app`
|
|
101
|
+
*
|
|
102
|
+
* `true` if the application was selected with `--app`
|
|
103
|
+
* `false` if the application was not selected with `--app`
|
|
104
|
+
* `undefined` if the program was not run with `--app`
|
|
105
|
+
*/
|
|
106
|
+
isSelected?: boolean;
|
|
92
107
|
}
|
|
93
108
|
|
|
94
109
|
/**
|
|
@@ -177,7 +192,9 @@ export class Scope {
|
|
|
177
192
|
public readonly logger: LoggerApi;
|
|
178
193
|
public readonly telemetryClient: ITelemetryClient;
|
|
179
194
|
public readonly dataMutex: AsyncMutex;
|
|
195
|
+
public readonly rootDir: string;
|
|
180
196
|
public readonly dotAlchemy: string;
|
|
197
|
+
public readonly isSelected: boolean | undefined;
|
|
181
198
|
|
|
182
199
|
// Provider credentials for scope-level credential overrides
|
|
183
200
|
public readonly providerCredentials: ProviderCredentials;
|
|
@@ -214,17 +231,21 @@ export class Scope {
|
|
|
214
231
|
logger,
|
|
215
232
|
adopt,
|
|
216
233
|
dotAlchemy,
|
|
234
|
+
rootDir,
|
|
235
|
+
isSelected,
|
|
217
236
|
...providerCredentials
|
|
218
237
|
} = options;
|
|
219
238
|
|
|
220
|
-
this.dotAlchemy =
|
|
221
|
-
dotAlchemy ??
|
|
222
|
-
this.parent?.dotAlchemy ??
|
|
223
|
-
path.join(process.cwd(), ".alchemy");
|
|
224
|
-
|
|
225
239
|
this.scopeName = scopeName;
|
|
226
240
|
this.name = this.scopeName;
|
|
227
241
|
this.parent = parent ?? Scope.getScope();
|
|
242
|
+
this.rootDir = rootDir ?? this.parent?.rootDir ?? ALCHEMY_ROOT;
|
|
243
|
+
this.isSelected = isSelected ?? this.parent?.isSelected;
|
|
244
|
+
|
|
245
|
+
this.dotAlchemy =
|
|
246
|
+
dotAlchemy ??
|
|
247
|
+
this.parent?.dotAlchemy ??
|
|
248
|
+
path.resolve(this.rootDir, ".alchemy");
|
|
228
249
|
|
|
229
250
|
// Store provider credentials (TypeScript ensures no conflicts with core options)
|
|
230
251
|
this.providerCredentials = providerCredentials as ProviderCredentials;
|