alchemy 0.10.3 → 0.11.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.
@@ -1,5 +1,6 @@
1
1
  import { Architecture, Runtime } from "@aws-sdk/client-lambda";
2
2
  import type { Context } from "../context";
3
+ import type { Bundle } from "../esbuild/bundle";
3
4
  import { Resource } from "../resource";
4
5
  /**
5
6
  * Properties for creating or updating a Lambda function
@@ -10,9 +11,9 @@ export interface FunctionProps {
10
11
  */
11
12
  functionName: string;
12
13
  /**
13
- * Path to the zip file containing the function code
14
+ * Bundle for the function
14
15
  */
15
- zipPath: string;
16
+ bundle: Bundle;
16
17
  /**
17
18
  * ARN of the IAM role that Lambda assumes when executing the function
18
19
  */
@@ -21,7 +22,7 @@ export interface FunctionProps {
21
22
  * Function handler in the format 'file.function'
22
23
  * For Node.js this is typically 'index.handler' or similar
23
24
  */
24
- handler?: string;
25
+ handler: string;
25
26
  /**
26
27
  * Lambda runtime environment for the function
27
28
  * @default nodejs20.x if not specified
@@ -1,6 +1,4 @@
1
1
  import { AddPermissionCommand, Architecture, CreateFunctionCommand, CreateFunctionUrlConfigCommand, DeleteFunctionCommand, DeleteFunctionUrlConfigCommand, GetFunctionCommand, GetFunctionConfigurationCommand, GetFunctionUrlConfigCommand, LambdaClient, ResourceNotFoundException, Runtime, UpdateFunctionCodeCommand, UpdateFunctionConfigurationCommand, UpdateFunctionUrlConfigCommand, } from "@aws-sdk/client-lambda";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
2
  import { Resource } from "../resource";
5
3
  import { ignore } from "../util/ignore";
6
4
  /**
@@ -60,7 +58,7 @@ import { ignore } from "../util/ignore";
60
58
  export const Function = Resource("lambda::Function", async function (id, props) {
61
59
  const client = new LambdaClient({});
62
60
  const region = await resolveRegion(client);
63
- const code = await zipCode(props.zipPath);
61
+ const code = await zipCode(props);
64
62
  if (this.phase === "delete") {
65
63
  // Delete function URL if it exists
66
64
  if (this.output?.url) {
@@ -408,9 +406,10 @@ async function waitForFunctionStabilization(client, functionName) {
408
406
  }
409
407
  }
410
408
  // Helper to zip the code
411
- async function zipCode(filePath) {
412
- const fileContent = await fs.promises.readFile(filePath);
413
- const fileName = path.basename(filePath);
409
+ async function zipCode(props) {
410
+ const fileContent = props.bundle.content;
411
+ const fileName = props.handler.split(".").reverse().slice(0, -1).reverse().join(".") +
412
+ (props.bundle.format === "cjs" ? ".cjs" : ".mjs");
414
413
  // Create a zip buffer in memory
415
414
  const zip = new (await import("jszip")).default();
416
415
  zip.file(fileName, fileContent);
@@ -126,6 +126,8 @@ async function getRefreshedAuthConfig() {
126
126
  return authConfig;
127
127
  }
128
128
  async function writeWranglerConfig(config) {
129
+ if (config.exists === false)
130
+ return;
129
131
  const TOML = await import("@iarna/toml");
130
132
  const configPath = await findWranglerConfig();
131
133
  config = {
@@ -140,9 +142,21 @@ async function writeWranglerConfig(config) {
140
142
  const authConfigCache = {};
141
143
  async function readWranglerConfig() {
142
144
  const configPath = await findWranglerConfig();
143
- const config = (authConfigCache[configPath] ??= await parseTOML(await fs.readFile(configPath, "utf-8")));
144
- config.path = configPath;
145
- return config;
145
+ try {
146
+ const config = (authConfigCache[configPath] ??= await parseTOML(await fs.readFile(configPath, "utf-8")));
147
+ config.path = configPath;
148
+ return config;
149
+ }
150
+ catch (e) {
151
+ if (e.code === "ENOENT") {
152
+ // The config doesn't exist
153
+ return {
154
+ path: configPath,
155
+ exists: false
156
+ };
157
+ }
158
+ throw e;
159
+ }
146
160
  }
147
161
  let wranglerConfigPath;
148
162
  async function findWranglerConfig() {
@@ -139,6 +139,7 @@ export const D1DatabaseResource = Resource("cloudflare::D1Database", async funct
139
139
  readReplication: dbData.result.read_replication,
140
140
  primaryLocationHint: props.primaryLocationHint,
141
141
  accountId: api.accountId,
142
+ migrationsDir: props.migrationsDir
142
143
  });
143
144
  }
144
145
  });
@@ -456,7 +456,15 @@ async function bundleWorkerScript(props) {
456
456
  external: [...(bundleOptions.external ?? []), "cloudflare:workers"],
457
457
  });
458
458
  try {
459
- return await fs.readFile(bundle.path, "utf-8");
459
+ if (bundle.content) {
460
+ return bundle.content;
461
+ }
462
+ else if (bundle.path) {
463
+ return await fs.readFile(bundle.path, "utf-8");
464
+ }
465
+ else {
466
+ throw new Error("Failed to create bundle");
467
+ }
460
468
  }
461
469
  catch (error) {
462
470
  console.error("Error reading bundle:", error);
@@ -132,6 +132,7 @@ export interface WranglerJsonSpec {
132
132
  binding: string;
133
133
  database_id: string;
134
134
  database_name: string;
135
+ migrations_dir?: string;
135
136
  }[];
136
137
  /**
137
138
  * Assets bindings
@@ -113,6 +113,7 @@ function processBindings(spec, bindings) {
113
113
  binding: bindingName,
114
114
  database_id: binding.id,
115
115
  database_name: binding.name,
116
+ migrations_dir: binding.migrationsDir,
116
117
  });
117
118
  }
118
119
  else if (binding.type === "queue") {
@@ -1,4 +1,4 @@
1
- import * as esbuild from "esbuild";
1
+ import esbuild from "esbuild";
2
2
  import type { Context } from "../context";
3
3
  import { Resource } from "../resource";
4
4
  /**
@@ -64,17 +64,25 @@ export interface BundleProps {
64
64
  /**
65
65
  * Output returned after bundle creation/update
66
66
  */
67
- export interface Bundle extends Resource<"esbuild::Bundle"> {
67
+ export interface Bundle<P extends BundleProps = BundleProps> extends Resource<"esbuild::Bundle">, BundleProps {
68
68
  /**
69
69
  * Path to the bundled file
70
70
  * Absolute or relative path to the generated bundle
71
71
  */
72
- path: string;
72
+ path: P extends {
73
+ outdir: string;
74
+ } | {
75
+ outfile: string;
76
+ } ? string : undefined;
73
77
  /**
74
78
  * SHA-256 hash of the bundle contents
75
79
  * Used for cache busting and content verification
76
80
  */
77
81
  hash: string;
82
+ /**
83
+ * The content of the bundle (the .js or .mjs file)
84
+ */
85
+ content: string;
78
86
  }
79
87
  /**
80
88
  * esbuild Bundle Resource
@@ -92,8 +100,9 @@ export interface Bundle extends Resource<"esbuild::Bundle"> {
92
100
  * target: "node18"
93
101
  * });
94
102
  */
95
- export declare const Bundle: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<Bundle>, id: string, props: BundleProps) => Promise<Bundle>);
103
+ export declare const Bundle: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | (<Props extends BundleProps>(this: Context<Bundle<any>>, id: string, props: Props) => Promise<Bundle<Props>>);
96
104
  export declare function bundle(props: BundleProps): Promise<esbuild.BuildResult<{
105
+ write: boolean;
97
106
  entryPoints: string[];
98
107
  outdir: string | undefined;
99
108
  outfile: string | undefined;
@@ -105,7 +114,6 @@ export declare function bundle(props: BundleProps): Promise<esbuild.BuildResult<
105
114
  external: string[];
106
115
  platform: "browser" | "node" | "neutral" | undefined;
107
116
  metafile: true;
108
- write: true;
109
117
  splitting?: boolean;
110
118
  preserveSymlinks?: boolean;
111
119
  outbase?: string;
@@ -1,6 +1,6 @@
1
- import * as esbuild from "esbuild";
1
+ import esbuild from "esbuild";
2
2
  import crypto from "node:crypto";
3
- import fs from "node:fs";
3
+ import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { Resource } from "../resource";
6
6
  /**
@@ -22,20 +22,25 @@ import { Resource } from "../resource";
22
22
  export const Bundle = Resource("esbuild::Bundle", {
23
23
  alwaysUpdate: true,
24
24
  }, async function (id, props) {
25
- // Determine output path
26
- const outDirPath = props.outfile != null ? path.dirname(props.outfile) : props.outdir;
27
- if (outDirPath == null) {
28
- throw new Error(`You need to specify either outfile or outdir in your bundle configuration ${JSON.stringify(props)}`);
29
- }
30
- // Ensure output directory exists
31
- await fs.promises.mkdir(path.dirname(outDirPath), { recursive: true });
32
25
  if (this.phase === "delete") {
33
- await fs.promises.rm(outDirPath, { recursive: true });
26
+ console.log("delete", this.output.path);
27
+ if (this.output.path) {
28
+ try {
29
+ await fs.rm(this.output.path, { force: true });
30
+ }
31
+ catch (error) {
32
+ if (error instanceof Error && error.message.includes("ENOENT")) {
33
+ // File doesn't exist, so we can ignore the error
34
+ }
35
+ else {
36
+ throw error;
37
+ }
38
+ }
39
+ }
40
+ return this.destroy();
34
41
  }
35
42
  const result = await bundle(props);
36
- // Check that bundle created output an file for the given entrypoint
37
- // Use bundle metada data to retrieve its content
38
- const bundleOutputPath = Object.entries(result.metafile.outputs).find(([file, output]) => {
43
+ const bundlePath = Object.entries(result.metafile.outputs).find(([_, output]) => {
39
44
  if (output.entryPoint === undefined) {
40
45
  return false;
41
46
  }
@@ -44,29 +49,37 @@ export const Bundle = Resource("esbuild::Bundle", {
44
49
  return (relativeOutput ===
45
50
  path.relative(process.cwd(), path.resolve(process.cwd(), props.entryPoint)));
46
51
  })?.[0];
47
- if (!bundleOutputPath) {
48
- throw new Error(`Unable to find a compiled file`);
52
+ const outputFile = result.outputFiles?.[0];
53
+ if (outputFile === undefined && bundlePath === undefined) {
54
+ throw new Error("Failed to create bundle");
55
+ }
56
+ else if (outputFile) {
57
+ return this({
58
+ ...props,
59
+ path: bundlePath,
60
+ hash: outputFile.hash,
61
+ content: outputFile.text,
62
+ });
63
+ }
64
+ else {
65
+ const content = await fs.readFile(bundlePath, "utf-8");
66
+ return this({
67
+ ...props,
68
+ path: bundlePath,
69
+ hash: crypto.createHash("sha256").update(content).digest("hex"),
70
+ content,
71
+ });
49
72
  }
50
- // Calculate hash of the output
51
- const bundleOutputContent = await fs.promises.readFile(bundleOutputPath);
52
- const hash = crypto
53
- .createHash("sha256")
54
- .update(bundleOutputContent)
55
- .digest("hex");
56
- // Store metadata in context
57
- await this.set("metafile", result.metafile);
58
- await this.set("hash", hash);
59
- // Return output info
60
- return this({
61
- path: bundleOutputPath,
62
- hash,
63
- });
64
73
  });
65
74
  export async function bundle(props) {
66
75
  return await esbuild.build({
67
76
  ...props.options,
77
+ write: !(props.outdir === undefined && props.outfile === undefined),
78
+ // write:
79
+ // props.outdir === undefined && props.outfile === undefined ? false : true,
80
+ // write: false,
68
81
  entryPoints: [props.entryPoint],
69
- outdir: props.outdir,
82
+ outdir: props.outdir ? props.outdir : props.outfile ? undefined : ".out",
70
83
  outfile: props.outfile,
71
84
  bundle: true,
72
85
  format: props.format,
@@ -80,6 +93,5 @@ export async function bundle(props) {
80
93
  ],
81
94
  platform: props.platform,
82
95
  metafile: true,
83
- write: true,
84
96
  });
85
97
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alchemy",
3
- "version": "0.10.3",
3
+ "version": "0.11.1",
4
4
  "type": "module",
5
5
  "module": "./lib/index.js",
6
6
  "scripts": {
@@ -15,9 +15,8 @@ import {
15
15
  UpdateFunctionConfigurationCommand,
16
16
  UpdateFunctionUrlConfigCommand,
17
17
  } from "@aws-sdk/client-lambda";
18
- import fs from "node:fs";
19
- import path from "node:path";
20
18
  import type { Context } from "../context";
19
+ import type { Bundle } from "../esbuild/bundle";
21
20
  import { Resource } from "../resource";
22
21
  import { ignore } from "../util/ignore";
23
22
 
@@ -31,9 +30,9 @@ export interface FunctionProps {
31
30
  functionName: string;
32
31
 
33
32
  /**
34
- * Path to the zip file containing the function code
33
+ * Bundle for the function
35
34
  */
36
- zipPath: string;
35
+ bundle: Bundle;
37
36
 
38
37
  /**
39
38
  * ARN of the IAM role that Lambda assumes when executing the function
@@ -44,7 +43,7 @@ export interface FunctionProps {
44
43
  * Function handler in the format 'file.function'
45
44
  * For Node.js this is typically 'index.handler' or similar
46
45
  */
47
- handler?: string;
46
+ handler: string;
48
47
 
49
48
  /**
50
49
  * Lambda runtime environment for the function
@@ -301,7 +300,7 @@ export const Function = Resource(
301
300
  const client = new LambdaClient({});
302
301
  const region = await resolveRegion(client);
303
302
 
304
- const code = await zipCode(props.zipPath);
303
+ const code = await zipCode(props);
305
304
 
306
305
  if (this.phase === "delete") {
307
306
  // Delete function URL if it exists
@@ -714,9 +713,12 @@ async function waitForFunctionStabilization(
714
713
  }
715
714
 
716
715
  // Helper to zip the code
717
- async function zipCode(filePath: string): Promise<Buffer> {
718
- const fileContent = await fs.promises.readFile(filePath);
719
- const fileName = path.basename(filePath);
716
+ async function zipCode(props: FunctionProps): Promise<Buffer> {
717
+ const fileContent = props.bundle.content;
718
+
719
+ const fileName =
720
+ props.handler.split(".").reverse().slice(0, -1).reverse().join(".") +
721
+ (props.bundle.format === "cjs" ? ".cjs" : ".mjs");
720
722
 
721
723
  // Create a zip buffer in memory
722
724
  const zip = new (await import("jszip")).default();
@@ -239,6 +239,8 @@ interface WranglerConfig {
239
239
  refresh_token?: string;
240
240
  expiration_time?: string;
241
241
  scopes?: string[];
242
+ /** exists is `false` if the config file doesn't exist, like in CI */
243
+ exists?: boolean;
242
244
  /** @deprecated - this field was only provided by the deprecated v1 `wrangler config` command. */
243
245
  api_token?: string;
244
246
  }
@@ -258,6 +260,8 @@ async function getRefreshedAuthConfig(): Promise<WranglerConfig> {
258
260
  }
259
261
 
260
262
  async function writeWranglerConfig(config: WranglerConfig) {
263
+ if (config.exists === false) return
264
+
261
265
  const TOML = await import("@iarna/toml");
262
266
  const configPath = await findWranglerConfig();
263
267
  config = {
@@ -274,11 +278,24 @@ const authConfigCache: Record<string, WranglerConfig> = {};
274
278
 
275
279
  async function readWranglerConfig(): Promise<WranglerConfig> {
276
280
  const configPath = await findWranglerConfig();
277
- const config = (authConfigCache[configPath] ??= await parseTOML(
278
- await fs.readFile(configPath, "utf-8")
279
- ));
280
- config.path = configPath;
281
- return config;
281
+ try {
282
+ const config = (authConfigCache[configPath] ??= await parseTOML(
283
+ await fs.readFile(configPath, "utf-8")
284
+ ));
285
+ config.path = configPath;
286
+
287
+ return config
288
+ } catch (e: any) {
289
+ if (e.code === "ENOENT") {
290
+ // The config doesn't exist
291
+ return {
292
+ path: configPath,
293
+ exists: false
294
+ }
295
+ }
296
+
297
+ throw e
298
+ }
282
299
  }
283
300
 
284
301
  let wranglerConfigPath: string | undefined;
@@ -278,6 +278,7 @@ export const D1DatabaseResource = Resource(
278
278
  readReplication: dbData.result.read_replication,
279
279
  primaryLocationHint: props.primaryLocationHint,
280
280
  accountId: api.accountId,
281
+ migrationsDir: props.migrationsDir
281
282
  });
282
283
  }
283
284
  }
@@ -747,7 +747,13 @@ async function bundleWorkerScript<B extends Bindings>(props: WorkerProps) {
747
747
  });
748
748
 
749
749
  try {
750
- return await fs.readFile(bundle.path, "utf-8");
750
+ if (bundle.content) {
751
+ return bundle.content;
752
+ } else if (bundle.path) {
753
+ return await fs.readFile(bundle.path, "utf-8");
754
+ } else {
755
+ throw new Error("Failed to create bundle");
756
+ }
751
757
  } catch (error) {
752
758
  console.error("Error reading bundle:", error);
753
759
  throw new Error("Error reading bundle");
@@ -207,6 +207,7 @@ export interface WranglerJsonSpec {
207
207
  binding: string;
208
208
  database_id: string;
209
209
  database_name: string;
210
+ migrations_dir?: string
210
211
  }[];
211
212
 
212
213
  /**
@@ -254,6 +255,7 @@ function processBindings(spec: WranglerJsonSpec, bindings: Bindings): void {
254
255
  binding: string;
255
256
  database_id: string;
256
257
  database_name: string;
258
+ migrations_dir?: string;
257
259
  }[] = [];
258
260
  const queues: { binding: string; queue: string }[] = [];
259
261
  const vectorizeIndexes: { binding: string; index_name: string }[] = [];
@@ -314,6 +316,7 @@ function processBindings(spec: WranglerJsonSpec, bindings: Bindings): void {
314
316
  binding: bindingName,
315
317
  database_id: binding.id,
316
318
  database_name: binding.name,
319
+ migrations_dir: binding.migrationsDir,
317
320
  });
318
321
  } else if (binding.type === "queue") {
319
322
  queues.push({
@@ -1,6 +1,6 @@
1
- import * as esbuild from "esbuild";
1
+ import esbuild from "esbuild";
2
2
  import crypto from "node:crypto";
3
- import fs from "node:fs";
3
+ import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import type { Context } from "../context";
6
6
  import { Resource } from "../resource";
@@ -78,18 +78,25 @@ export interface BundleProps {
78
78
  /**
79
79
  * Output returned after bundle creation/update
80
80
  */
81
- export interface Bundle extends Resource<"esbuild::Bundle"> {
81
+ export interface Bundle<P extends BundleProps = BundleProps>
82
+ extends Resource<"esbuild::Bundle">,
83
+ BundleProps {
82
84
  /**
83
85
  * Path to the bundled file
84
86
  * Absolute or relative path to the generated bundle
85
87
  */
86
- path: string;
88
+ path: P extends { outdir: string } | { outfile: string } ? string : undefined;
87
89
 
88
90
  /**
89
91
  * SHA-256 hash of the bundle contents
90
92
  * Used for cache busting and content verification
91
93
  */
92
94
  hash: string;
95
+
96
+ /**
97
+ * The content of the bundle (the .js or .mjs file)
98
+ */
99
+ content: string;
93
100
  }
94
101
 
95
102
  /**
@@ -113,32 +120,31 @@ export const Bundle = Resource(
113
120
  {
114
121
  alwaysUpdate: true,
115
122
  },
116
- async function (
117
- this: Context<Bundle>,
123
+ async function <Props extends BundleProps>(
124
+ this: Context<Bundle<any>>,
118
125
  id: string,
119
- props: BundleProps
120
- ): Promise<Bundle> {
121
- // Determine output path
122
- const outDirPath =
123
- props.outfile != null ? path.dirname(props.outfile) : props.outdir;
124
-
125
- if (outDirPath == null) {
126
- throw new Error(
127
- `You need to specify either outfile or outdir in your bundle configuration ${JSON.stringify(props)}`
128
- );
129
- }
130
-
131
- // Ensure output directory exists
132
- await fs.promises.mkdir(path.dirname(outDirPath), { recursive: true });
126
+ props: Props
127
+ ): Promise<Bundle<Props>> {
133
128
  if (this.phase === "delete") {
134
- await fs.promises.rm(outDirPath, { recursive: true });
129
+ console.log("delete", this.output.path);
130
+ if (this.output.path) {
131
+ try {
132
+ await fs.rm(this.output.path, { force: true });
133
+ } catch (error) {
134
+ if (error instanceof Error && error.message.includes("ENOENT")) {
135
+ // File doesn't exist, so we can ignore the error
136
+ } else {
137
+ throw error;
138
+ }
139
+ }
140
+ }
141
+ return this.destroy();
135
142
  }
136
143
 
137
144
  const result = await bundle(props);
138
- // Check that bundle created output an file for the given entrypoint
139
- // Use bundle metada data to retrieve its content
140
- const bundleOutputPath = Object.entries(result.metafile.outputs).find(
141
- ([file, output]) => {
145
+
146
+ const bundlePath = Object.entries(result.metafile.outputs).find(
147
+ ([_, output]) => {
142
148
  if (output.entryPoint === undefined) {
143
149
  return false;
144
150
  }
@@ -157,34 +163,37 @@ export const Bundle = Resource(
157
163
  }
158
164
  )?.[0];
159
165
 
160
- if (!bundleOutputPath) {
161
- throw new Error(`Unable to find a compiled file`);
166
+ const outputFile = result.outputFiles?.[0];
167
+ if (outputFile === undefined && bundlePath === undefined) {
168
+ throw new Error("Failed to create bundle");
169
+ } else if (outputFile) {
170
+ return this({
171
+ ...props,
172
+ path: bundlePath,
173
+ hash: outputFile.hash,
174
+ content: outputFile.text,
175
+ });
176
+ } else {
177
+ const content = await fs.readFile(bundlePath!, "utf-8");
178
+ return this({
179
+ ...props,
180
+ path: bundlePath,
181
+ hash: crypto.createHash("sha256").update(content).digest("hex"),
182
+ content,
183
+ });
162
184
  }
163
-
164
- // Calculate hash of the output
165
- const bundleOutputContent = await fs.promises.readFile(bundleOutputPath);
166
- const hash = crypto
167
- .createHash("sha256")
168
- .update(bundleOutputContent)
169
- .digest("hex");
170
-
171
- // Store metadata in context
172
- await this.set("metafile", result.metafile);
173
- await this.set("hash", hash);
174
-
175
- // Return output info
176
- return this({
177
- path: bundleOutputPath,
178
- hash,
179
- });
180
185
  }
181
186
  );
182
187
 
183
188
  export async function bundle(props: BundleProps) {
184
189
  return await esbuild.build({
185
190
  ...props.options,
191
+ write: !(props.outdir === undefined && props.outfile === undefined),
192
+ // write:
193
+ // props.outdir === undefined && props.outfile === undefined ? false : true,
194
+ // write: false,
186
195
  entryPoints: [props.entryPoint],
187
- outdir: props.outdir,
196
+ outdir: props.outdir ? props.outdir : props.outfile ? undefined : ".out",
188
197
  outfile: props.outfile,
189
198
  bundle: true,
190
199
  format: props.format,
@@ -198,6 +207,5 @@ export async function bundle(props: BundleProps) {
198
207
  ],
199
208
  platform: props.platform,
200
209
  metafile: true,
201
- write: true,
202
210
  });
203
211
  }