alchemy 0.70.0 → 0.70.1

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 (38) hide show
  1. package/bin/alchemy.js +97 -60
  2. package/bin/alchemy.ts +2 -0
  3. package/bin/commands/telemetry.ts +33 -0
  4. package/bin/services/execute-alchemy.ts +7 -0
  5. package/lib/alchemy.d.ts +4 -0
  6. package/lib/alchemy.d.ts.map +1 -1
  7. package/lib/alchemy.js +1 -0
  8. package/lib/alchemy.js.map +1 -1
  9. package/lib/cloudflare/api.d.ts.map +1 -1
  10. package/lib/cloudflare/api.js +2 -0
  11. package/lib/cloudflare/api.js.map +1 -1
  12. package/lib/cloudflare/bun-spa/bun-spa.d.ts +6 -8
  13. package/lib/cloudflare/bun-spa/bun-spa.d.ts.map +1 -1
  14. package/lib/cloudflare/bun-spa/bun-spa.js +3 -3
  15. package/lib/cloudflare/bun-spa/bun-spa.js.map +1 -1
  16. package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
  17. package/lib/cloudflare/miniflare/build-worker-options.js +30 -3
  18. package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
  19. package/lib/scope.d.ts +5 -0
  20. package/lib/scope.d.ts.map +1 -1
  21. package/lib/scope.js +3 -1
  22. package/lib/scope.js.map +1 -1
  23. package/lib/util/idempotent-spawn.js +1 -1
  24. package/lib/util/idempotent-spawn.js.map +1 -1
  25. package/lib/util/telemetry.d.ts +3 -2
  26. package/lib/util/telemetry.d.ts.map +1 -1
  27. package/lib/util/telemetry.js +34 -10
  28. package/lib/util/telemetry.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/alchemy.ts +5 -0
  31. package/src/cloudflare/api.ts +2 -0
  32. package/src/cloudflare/bun-spa/bun-spa.ts +5 -8
  33. package/src/cloudflare/miniflare/build-worker-options.ts +32 -4
  34. package/src/scope.ts +7 -0
  35. package/src/util/idempotent-spawn.ts +1 -1
  36. package/src/util/telemetry.ts +41 -10
  37. package/templates/tanstack-start/package.json +2 -2
  38. package/workers/tunnel-proxy.js +1 -1
package/src/alchemy.ts CHANGED
@@ -137,6 +137,7 @@ async function _alchemy(
137
137
  password: process.env.ALCHEMY_PASSWORD,
138
138
  adopt: cliArgs.includes("--adopt"),
139
139
  rootDir: path.resolve(parseOption("--root-dir", ALCHEMY_ROOT)),
140
+ profile: parseOption("--profile"),
140
141
  } satisfies Partial<AlchemyOptions>;
141
142
  const mergedOptions = {
142
143
  ...cliOptions,
@@ -295,6 +296,10 @@ export interface AlchemyOptions {
295
296
  * @default process.cwd()
296
297
  */
297
298
  rootDir?: string;
299
+ /**
300
+ * The Alchemy profile to use for authoriziing requests.
301
+ */
302
+ profile?: string;
298
303
  /**
299
304
  * Whether this is the application that was selected with `--app`
300
305
  *
@@ -1,4 +1,5 @@
1
1
  import { Provider, type Credentials } from "../auth.ts";
2
+ import { Scope } from "../scope.ts";
2
3
  import type { Secret } from "../secret.ts";
3
4
  import { isBinary } from "../serde.ts";
4
5
  import { memoize } from "../util/memoize.ts";
@@ -99,6 +100,7 @@ export const createCloudflareApi = memoize(
99
100
  try {
100
101
  const profile =
101
102
  options.profile ??
103
+ Scope.getScope()?.profile ??
102
104
  process.env.CLOUDFLARE_PROFILE ??
103
105
  process.env.ALCHEMY_PROFILE ??
104
106
  "default";
@@ -14,21 +14,18 @@ import {
14
14
  Website,
15
15
  type WebsiteProps,
16
16
  } from "../website.ts";
17
- import type { Worker } from "../worker.ts";
18
17
 
19
18
  export interface BunSPAProps<B extends Bindings> extends WebsiteProps<B> {
20
19
  frontend: string;
21
20
  outDir?: string;
22
21
  }
23
22
 
24
- export type BunSPA<B extends Bindings> = B extends { ASSETS: any }
25
- ? never
26
- : Worker<B & { ASSETS: Assets }>;
23
+ export type BunSPA<B extends Bindings> = Website<B> & { apiUrl: string };
27
24
 
28
25
  export async function BunSPA<B extends Bindings>(
29
26
  id: string,
30
27
  props: BunSPAProps<B>,
31
- ): Promise<BunSPA<B>> {
28
+ ): Promise<BunSPA<B> & { apiUrl: string }> {
32
29
  const frontendPath = path.resolve(props.frontend);
33
30
  if (!(await exists(frontendPath))) {
34
31
  throw new Error(`Frontend path ${frontendPath} does not exist`);
@@ -66,6 +63,7 @@ export async function BunSPA<B extends Bindings>(
66
63
  ),
67
64
  });
68
65
 
66
+ let apiUrl = website.url!;
69
67
  // in dev
70
68
  if (scope.local) {
71
69
  const cwd = props.cwd ?? process.cwd();
@@ -74,7 +72,6 @@ export async function BunSPA<B extends Bindings>(
74
72
  props,
75
73
  `bun '${path.relative(cwd, frontendPath)}'`,
76
74
  );
77
- console.log("backend url", website.url);
78
75
  const secrets = props.wrangler?.secrets ?? !props.wrangler?.path;
79
76
  const env = {
80
77
  ...(process.env ?? {}),
@@ -101,11 +98,11 @@ export async function BunSPA<B extends Bindings>(
101
98
  ...process.env,
102
99
  NODE_ENV: "development",
103
100
  ALCHEMY_ROOT: Scope.current.rootDir,
104
- PUBLIC_BACKEND_URL: website.url!,
101
+ PUBLIC_BACKEND_URL: apiUrl,
105
102
  },
106
103
  });
107
104
  }
108
- return website;
105
+ return { ...website, apiUrl } as BunSPA<B>;
109
106
  }
110
107
 
111
108
  async function validateBunfigToml(cwd: string): Promise<void> {
@@ -1,6 +1,7 @@
1
1
  import * as miniflare from "miniflare";
2
2
  import { assertNever } from "../../util/assert-never.ts";
3
3
  import type { HTTPServer } from "../../util/http.ts";
4
+ import { logger } from "../../util/logger.ts";
4
5
  import type { CloudflareApi } from "../api.ts";
5
6
  import type {
6
7
  Binding,
@@ -79,10 +80,7 @@ export const buildWorkerOptions = async (
79
80
  ],
80
81
  containerEngine: {
81
82
  localDocker: {
82
- socketPath:
83
- process.platform === "win32"
84
- ? "//./pipe/docker_engine"
85
- : "unix:///var/run/docker.sock",
83
+ socketPath: await getDockerSocketPath(),
86
84
  },
87
85
  },
88
86
  // This exposes the worker as a route that can be accessed by setting the MF-Route-Override header.
@@ -480,3 +478,33 @@ const isRemoteBinding = (binding: Binding) => {
480
478
  !!binding.dev.remote
481
479
  );
482
480
  };
481
+
482
+ /**
483
+ * DOCKER_HOST env is standardized
484
+ * docker has an option to expose on tcp://localhost:2375; so we check 2375 if the user has it enabled
485
+ * the pipe on windows doesn't work half the time(even though the pipe exists). This seems like a strange error on how miniflare parses the pipe
486
+ * @returns The Docker path
487
+ */
488
+ async function getDockerSocketPath() {
489
+ if (process.env.DOCKER_HOST) {
490
+ return process.env.DOCKER_HOST;
491
+ }
492
+ // Check if docker is running on tcp://localhost:2375 using fetch
493
+ try {
494
+ const url = "http://localhost:2375/_ping";
495
+ const res = await fetch(url, { method: "GET" });
496
+ if (res.ok) {
497
+ const text = await res.text();
498
+ if (text.trim() === "OK") {
499
+ return "localhost:2375";
500
+ }
501
+ }
502
+ } catch {}
503
+ if (process.platform === "win32") {
504
+ logger.warn(
505
+ "Using the pipe on Windows is unstable. If you have issues, try setting DOCKER_HOST or enabling 'Expose daemon on tcp://localhost:2375 without TLS' in docker desktop",
506
+ );
507
+ return "//./pipe/docker_engine";
508
+ }
509
+ return "unix:///var/run/docker.sock";
510
+ }
package/src/scope.ts CHANGED
@@ -103,6 +103,10 @@ export interface ScopeOptions extends ProviderCredentials {
103
103
  * @default process.cwd()
104
104
  */
105
105
  rootDir?: string;
106
+ /**
107
+ * The Alchemy profile to use for authoriziing requests.
108
+ */
109
+ profile?: string;
106
110
  /**
107
111
  * Whether this is the application that was selected with `--app`
108
112
  *
@@ -203,6 +207,7 @@ export class Scope {
203
207
  public readonly rootDir: string;
204
208
  public readonly dotAlchemy: string;
205
209
  public readonly isSelected: boolean | undefined;
210
+ public readonly profile: string | undefined;
206
211
 
207
212
  // Provider credentials for scope-level credential overrides
208
213
  public readonly providerCredentials: ProviderCredentials;
@@ -242,6 +247,7 @@ export class Scope {
242
247
  rootDir,
243
248
  isSelected,
244
249
  noTrack,
250
+ profile,
245
251
  ...providerCredentials
246
252
  } = options;
247
253
 
@@ -268,6 +274,7 @@ export class Scope {
268
274
  }
269
275
 
270
276
  this.stage = stage ?? this.parent?.stage ?? DEFAULT_STAGE;
277
+ this.profile = profile ?? this.parent?.profile;
271
278
  this.parent?.children.set(this.scopeName!, this);
272
279
  this.quiet = quiet ?? this.parent?.quiet ?? false;
273
280
  if (this.parent && !this.scopeName) {
@@ -112,7 +112,7 @@ export async function idempotentSpawn({
112
112
  const child = spawn(cmd, {
113
113
  shell: true,
114
114
  cwd,
115
- stdio: ["ignore", out.fd, out.fd], // stdout/stderr -> files (OS-level)
115
+ stdio: ["inherit", out.fd, out.fd], // stdout/stderr -> files (OS-level)
116
116
  env,
117
117
  detached: false,
118
118
  });
@@ -1,6 +1,6 @@
1
1
  import envPaths from "env-paths";
2
2
  import { exec } from "node:child_process";
3
- import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import fs from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "pathe";
6
6
  import pkg from "../../package.json" with { type: "json" };
@@ -9,11 +9,13 @@ import { Scope } from "../scope.ts";
9
9
  import { logger } from "./logger.ts";
10
10
  import { memoize } from "./memoize.ts";
11
11
 
12
- export const CONFIG_PATH = path.join(os.homedir(), ".alchemy", "id");
13
- export const CONFIG_PATH_LEGACY = path.join(
12
+ const ALCHEMY_DIR = path.join(os.homedir(), ".alchemy");
13
+ const ID_PATH = path.join(ALCHEMY_DIR, "id");
14
+ const ID_PATH_LEGACY = path.join(
14
15
  envPaths("alchemy", { suffix: "" }).config,
15
16
  "id",
16
17
  );
18
+ const DISABLED_PATH = path.join(ALCHEMY_DIR, "telemetry-disabled");
17
19
 
18
20
  export const TELEMETRY_DISABLED =
19
21
  !!process.env.ALCHEMY_TELEMETRY_DISABLED || !!process.env.DO_NOT_TRACK;
@@ -23,26 +25,43 @@ export const TELEMETRY_API_URL =
23
25
  export const SUPPRESS_TELEMETRY_ERRORS =
24
26
  !!process.env.ALCHEMY_TELEMETRY_SUPPRESS_ERRORS;
25
27
 
28
+ export const getGlobalTelemetryDisabled = memoize(async () => {
29
+ const disabled = await fs
30
+ .readFile(DISABLED_PATH, "utf-8")
31
+ .then((data) => data.trim() === "true")
32
+ .catch(() => false);
33
+ return disabled;
34
+ });
35
+
36
+ export async function setGlobalTelemetryDisabled() {
37
+ await fs.mkdir(ALCHEMY_DIR, { recursive: true });
38
+ await fs.writeFile(DISABLED_PATH, "true");
39
+ }
40
+
41
+ export async function setGlobalTelemetryEnabled() {
42
+ await fs.rm(DISABLED_PATH, { force: true });
43
+ }
44
+
26
45
  async function getOrCreateUserId() {
27
46
  async function readUserId(path: string) {
28
47
  try {
29
- return (await readFile(path, "utf-8")).trim();
48
+ return (await fs.readFile(path, "utf-8")).trim();
30
49
  } catch {
31
50
  return null;
32
51
  }
33
52
  }
34
53
 
35
- const id = await readUserId(CONFIG_PATH);
54
+ const id = await readUserId(ID_PATH);
36
55
  if (id) {
37
56
  return id;
38
57
  }
39
58
 
40
- const legacyId = await readUserId(CONFIG_PATH_LEGACY);
59
+ const legacyId = await readUserId(ID_PATH_LEGACY);
41
60
 
42
61
  try {
43
62
  const id = legacyId ?? crypto.randomUUID();
44
- await mkdir(path.dirname(CONFIG_PATH), { recursive: true });
45
- await writeFile(CONFIG_PATH, id);
63
+ await fs.mkdir(ALCHEMY_DIR, { recursive: true });
64
+ await fs.writeFile(ID_PATH, id);
46
65
  if (!legacyId) {
47
66
  console.warn(
48
67
  [
@@ -59,7 +78,11 @@ async function getOrCreateUserId() {
59
78
 
60
79
  async function getRootCommitHash() {
61
80
  return new Promise<string | null>((resolve) => {
62
- exec("git rev-list --max-parents=0 HEAD", (err, stdout) => {
81
+ const command =
82
+ process.platform === "win32"
83
+ ? `git rev-list --max-parents=0 HEAD | ForEach-Object { if (-not (git cat-file -p $_ | Select-String "^parent ")) { $_ } }`
84
+ : `git rev-list --max-parents=0 HEAD | xargs -r -I{} sh -c 'git cat-file -p {} | grep -q "^parent " || echo {}'`;
85
+ exec(command, (err, stdout) => {
63
86
  if (err) {
64
87
  resolve(null);
65
88
  return;
@@ -236,6 +259,14 @@ export type AlchemyTelemetryData = {
236
259
  duration: number;
237
260
  };
238
261
 
262
+ async function isTelemetryDisabled() {
263
+ return (
264
+ Scope.getScope()?.noTrack ||
265
+ TELEMETRY_DISABLED ||
266
+ (await getGlobalTelemetryDisabled())
267
+ );
268
+ }
269
+
239
270
  export async function createAndSendEvent(
240
271
  data:
241
272
  | CliTelemetryData
@@ -244,7 +275,7 @@ export async function createAndSendEvent(
244
275
  | AlchemyTelemetryData,
245
276
  error?: Error,
246
277
  ) {
247
- if (Scope.getScope()?.noTrack || TELEMETRY_DISABLED) {
278
+ if (await isTelemetryDisabled()) {
248
279
  return;
249
280
  }
250
281
  try {
@@ -24,8 +24,8 @@
24
24
  "vite-tsconfig-paths": "^5.1.4"
25
25
  },
26
26
  "devDependencies": {
27
- "@cloudflare/vite-plugin": "catalog:",
28
- "@cloudflare/workers-types": "catalog:",
27
+ "@cloudflare/vite-plugin": "^1.11.6",
28
+ "@cloudflare/workers-types": "^4.20250805.0",
29
29
  "@testing-library/dom": "^10.4.0",
30
30
  "@testing-library/react": "^16.2.0",
31
31
  "@types/node": "^22.10.2",
@@ -34,7 +34,7 @@ var h={async fetch(e,r){let n=new URL(e.url);n.host=r.TUNNEL_HOST;let l=new Head
34
34
  Alchemy</a>.</p>
35
35
  </div>
36
36
  <div class="bg-slate-200 px-5 py-3 flex flex-col w-full max-w-lg">
37
- <p class="text-sm text-slate-500">Alchemy 0.70.0</p>
37
+ <p class="text-sm text-slate-500">Alchemy 0.70.1</p>
38
38
  </div>
39
39
  </div>
40
40
  </body>