alchemy 0.65.1 → 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.
Files changed (119) hide show
  1. package/bin/alchemy.js +132 -78
  2. package/bin/commands/init.ts +6 -3
  3. package/bin/services/cdp-manager/server.ts +3 -2
  4. package/bin/services/execute-alchemy.ts +33 -3
  5. package/bin/services/find-workspace-root.ts +48 -0
  6. package/lib/alchemy.d.ts +14 -3
  7. package/lib/alchemy.d.ts.map +1 -1
  8. package/lib/alchemy.js +17 -5
  9. package/lib/alchemy.js.map +1 -1
  10. package/lib/apply.d.ts.map +1 -1
  11. package/lib/apply.js +51 -1
  12. package/lib/apply.js.map +1 -1
  13. package/lib/cloudflare/bucket.js +1 -1
  14. package/lib/cloudflare/bucket.js.map +1 -1
  15. package/lib/cloudflare/cloudflare-env-proxy.js +3 -3
  16. package/lib/cloudflare/cloudflare-env-proxy.js.map +1 -1
  17. package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
  18. package/lib/cloudflare/compatibility-date.gen.js +1 -1
  19. package/lib/cloudflare/d1-database.d.ts.map +1 -1
  20. package/lib/cloudflare/d1-database.js +2 -1
  21. package/lib/cloudflare/d1-database.js.map +1 -1
  22. package/lib/cloudflare/d1-local-migrations.d.ts +1 -0
  23. package/lib/cloudflare/d1-local-migrations.d.ts.map +1 -1
  24. package/lib/cloudflare/d1-local-migrations.js +3 -2
  25. package/lib/cloudflare/d1-local-migrations.js.map +1 -1
  26. package/lib/cloudflare/kv-namespace.js +1 -1
  27. package/lib/cloudflare/kv-namespace.js.map +1 -1
  28. package/lib/cloudflare/miniflare/delete.d.ts +3 -2
  29. package/lib/cloudflare/miniflare/delete.d.ts.map +1 -1
  30. package/lib/cloudflare/miniflare/delete.js +13 -12
  31. package/lib/cloudflare/miniflare/delete.js.map +1 -1
  32. package/lib/cloudflare/miniflare/miniflare-controller.d.ts.map +1 -1
  33. package/lib/cloudflare/miniflare/miniflare-controller.js +5 -4
  34. package/lib/cloudflare/miniflare/miniflare-controller.js.map +1 -1
  35. package/lib/cloudflare/miniflare/paths.d.ts +4 -4
  36. package/lib/cloudflare/miniflare/paths.d.ts.map +1 -1
  37. package/lib/cloudflare/miniflare/paths.js +10 -4
  38. package/lib/cloudflare/miniflare/paths.js.map +1 -1
  39. package/lib/cloudflare/miniflare/symlink-miniflare-state.d.ts +2 -0
  40. package/lib/cloudflare/miniflare/symlink-miniflare-state.d.ts.map +1 -0
  41. package/lib/cloudflare/miniflare/symlink-miniflare-state.js +19 -0
  42. package/lib/cloudflare/miniflare/symlink-miniflare-state.js.map +1 -0
  43. package/lib/cloudflare/nuxt/plugin.d.ts.map +1 -1
  44. package/lib/cloudflare/nuxt/plugin.js +3 -3
  45. package/lib/cloudflare/nuxt/plugin.js.map +1 -1
  46. package/lib/cloudflare/redwood/plugin.js +2 -2
  47. package/lib/cloudflare/redwood/plugin.js.map +1 -1
  48. package/lib/cloudflare/vite/plugin.d.ts.map +1 -1
  49. package/lib/cloudflare/vite/plugin.js +6 -3
  50. package/lib/cloudflare/vite/plugin.js.map +1 -1
  51. package/lib/cloudflare/website.d.ts.map +1 -1
  52. package/lib/cloudflare/website.js +5 -16
  53. package/lib/cloudflare/website.js.map +1 -1
  54. package/lib/cloudflare/worker.d.ts.map +1 -1
  55. package/lib/cloudflare/worker.js +4 -2
  56. package/lib/cloudflare/worker.js.map +1 -1
  57. package/lib/cloudflare/wrangler.json.js +1 -0
  58. package/lib/cloudflare/wrangler.json.js.map +1 -1
  59. package/lib/scope.d.ts +16 -0
  60. package/lib/scope.d.ts.map +1 -1
  61. package/lib/scope.js +10 -5
  62. package/lib/scope.js.map +1 -1
  63. package/lib/state/file-system-state-store.d.ts.map +1 -1
  64. package/lib/state/file-system-state-store.js +1 -2
  65. package/lib/state/file-system-state-store.js.map +1 -1
  66. package/lib/state/sqlite-state-store.d.ts +11 -0
  67. package/lib/state/sqlite-state-store.d.ts.map +1 -1
  68. package/lib/state/sqlite-state-store.js +22 -14
  69. package/lib/state/sqlite-state-store.js.map +1 -1
  70. package/lib/stripe/customer.d.ts +0 -8
  71. package/lib/stripe/customer.d.ts.map +1 -1
  72. package/lib/stripe/customer.js +0 -6
  73. package/lib/stripe/customer.js.map +1 -1
  74. package/lib/stripe/price.d.ts +23 -23
  75. package/lib/stripe/price.d.ts.map +1 -1
  76. package/lib/stripe/price.js +6 -22
  77. package/lib/stripe/price.js.map +1 -1
  78. package/lib/test/vitest.d.ts.map +1 -1
  79. package/lib/test/vitest.js +1 -1
  80. package/lib/test/vitest.js.map +1 -1
  81. package/lib/util/assert-never.js +1 -1
  82. package/lib/util/assert-never.js.map +1 -1
  83. package/lib/util/cli.d.ts.map +1 -1
  84. package/lib/util/cli.js +12 -13
  85. package/lib/util/cli.js.map +1 -1
  86. package/lib/util/root-dir.d.ts +2 -0
  87. package/lib/util/root-dir.d.ts.map +1 -0
  88. package/lib/util/root-dir.js +2 -0
  89. package/lib/util/root-dir.js.map +1 -0
  90. package/package.json +4 -4
  91. package/src/alchemy.ts +36 -8
  92. package/src/apply.ts +58 -3
  93. package/src/cloudflare/bucket.ts +1 -1
  94. package/src/cloudflare/cloudflare-env-proxy.ts +5 -5
  95. package/src/cloudflare/compatibility-date.gen.ts +1 -1
  96. package/src/cloudflare/d1-database.ts +2 -1
  97. package/src/cloudflare/d1-local-migrations.ts +10 -6
  98. package/src/cloudflare/kv-namespace.ts +1 -1
  99. package/src/cloudflare/miniflare/delete.ts +19 -11
  100. package/src/cloudflare/miniflare/miniflare-controller.ts +10 -6
  101. package/src/cloudflare/miniflare/paths.ts +11 -7
  102. package/src/cloudflare/miniflare/symlink-miniflare-state.ts +19 -0
  103. package/src/cloudflare/nuxt/plugin.ts +12 -3
  104. package/src/cloudflare/redwood/plugin.ts +2 -2
  105. package/src/cloudflare/vite/plugin.ts +8 -3
  106. package/src/cloudflare/website.ts +5 -19
  107. package/src/cloudflare/worker.ts +4 -2
  108. package/src/cloudflare/wrangler.json.ts +1 -0
  109. package/src/scope.ts +26 -5
  110. package/src/state/file-system-state-store.ts +1 -2
  111. package/src/state/sqlite-state-store.ts +41 -14
  112. package/src/stripe/customer.ts +0 -14
  113. package/src/stripe/price.ts +59 -56
  114. package/src/test/vitest.ts +4 -1
  115. package/src/util/assert-never.ts +1 -1
  116. package/src/util/cli.ts +15 -13
  117. package/src/util/root-dir.ts +1 -0
  118. package/templates/nextjs/alchemy.run.ts +2 -2
  119. package/workers/cloudflare-state-store.js +1 -1
@@ -1,7 +1,7 @@
1
1
  import type { GetPlatformProxyOptions } from "wrangler";
2
2
  import {
3
- DEFAULT_CONFIG_PATH,
4
- DEFAULT_PERSIST_PATH,
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
- : DEFAULT_PERSIST_PATH,
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 ?? DEFAULT_CONFIG_PATH),
42
+ configPath: validateConfigPath(input.configPath ?? getDefaultConfigPath()),
43
43
  persist,
44
44
  experimental: (input as any).experimental ?? {
45
45
  remoteBindings: true,
46
46
  },
47
- } as any;
47
+ };
48
48
  };
49
49
 
50
50
  const warned = new Set<string>();
@@ -4,4 +4,4 @@
4
4
  /**
5
5
  * The default Cloudflare Workers compatibility date, set based on the latest workerd release at the time of build.
6
6
  */
7
- export const DEFAULT_COMPATIBILITY_DATE = "2025-09-06";
7
+ export const DEFAULT_COMPATIBILITY_DATE = "2025-09-13";
@@ -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 path from "node:path";
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: path.join(process.cwd(), ".alchemy/miniflare/v3"), // vite plugin forces /v3 suffix
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
- const db: D1Database = await miniflare.getD1Database("DB");
24
- const session: D1DatabaseSession = db.withSession("first-primary");
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 = await session
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<{ name: string }>();
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 { DEFAULT_PERSIST_PATH } from "./paths.ts";
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(type: BindingType, id: string) {
22
- const bindingPath = path.join(PERSIST_ROOT, type, BINDING_TYPE_KEY[type]);
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(PERSIST_ROOT, type, id), {
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
- PERSIST_ROOT,
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
- PERSIST_ROOT,
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(".alchemy/miniflare");
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 { DEFAULT_PERSIST_PATH } from "./paths.ts";
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 ??= new MiniflareController();
29
- return globalThis.ALCHEMY_MINIFLARE_CONTROLLER;
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(DEFAULT_PERSIST_PATH),
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 DEFAULT_CONFIG_PATH = ".alchemy/local/wrangler.jsonc";
14
- export const DEFAULT_PERSIST_PATH = ".alchemy/miniflare/v3";
15
+ export const getDefaultConfigPath = (rootDir: string = process.cwd()) => {
16
+ return path.join(rootDir, ".alchemy", "local", "wrangler.jsonc");
17
+ };
15
18
 
16
- export const validateConfigPath = (
17
- path = DEFAULT_CONFIG_PATH,
18
- throws = true,
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 = DEFAULT_PERSIST_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
+ }
@@ -1,5 +1,10 @@
1
1
  import type { CloudflareOptions } from "nitropack/presets/cloudflare/types";
2
- import { validateConfigPath, validatePersistPath } from "../miniflare/paths.ts";
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(options.dev?.configPath),
11
- persistDir: validatePersistPath(options.dev?.persistDir),
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 { DEFAULT_CONFIG_PATH } from "../miniflare/paths.ts";
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 ?? DEFAULT_CONFIG_PATH;
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
- DEFAULT_PERSIST_PATH,
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
- : DEFAULT_PERSIST_PATH,
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(config?.configPath),
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 { DEFAULT_PERSIST_PATH } from "./miniflare/paths.ts";
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
- await writeMiniflareSymlink(paths.cwd);
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
- }
@@ -755,7 +755,9 @@ const _Worker = Resource(
755
755
  compatibilityDate,
756
756
  compatibilityFlags,
757
757
  outdir:
758
- props.bundle?.outdir ?? path.join(cwd, ".alchemy", "out", workerName),
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
  });
@@ -816,6 +816,7 @@ function processBindings(
816
816
  class_name: binding.className,
817
817
  });
818
818
  } else {
819
+ console.log("binding", binding);
819
820
  // biome-ignore lint/correctness/noVoidTypeReturn: it returns never
820
821
  return assertNever(binding);
821
822
  }
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;
@@ -6,7 +6,6 @@ import { deserialize, serialize } from "../serde.ts";
6
6
  import type { State, StateStore } from "../state.ts";
7
7
  import { ignore } from "../util/ignore.ts";
8
8
 
9
- const stateRootDir = path.join(process.cwd(), ".alchemy");
10
9
  const ALCHEMY_SEPERATOR_CHAR = process.platform === "win32" ? "-" : ":";
11
10
 
12
11
  export class FileSystemStateStore implements StateStore {
@@ -18,7 +17,7 @@ export class FileSystemStateStore implements StateStore {
18
17
  rootDir?: string;
19
18
  },
20
19
  ) {
21
- this.dir = path.join(options?.rootDir ?? stateRootDir, ...scope.chain);
20
+ this.dir = path.join(options?.rootDir ?? scope.dotAlchemy, ...scope.chain);
22
21
  }
23
22
 
24
23
  async init(): Promise<void> {