alchemy 0.3.0 → 0.3.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 (107) hide show
  1. package/lib/ai/approve.d.ts +99 -0
  2. package/lib/ai/approve.js +76 -0
  3. package/lib/ai/astro-file.js +7 -24
  4. package/lib/ai/client.d.ts +7 -0
  5. package/lib/ai/client.js +45 -0
  6. package/lib/ai/css-file.js +7 -24
  7. package/lib/ai/data.d.ts +26 -21
  8. package/lib/ai/data.js +33 -22
  9. package/lib/ai/document.d.ts +64 -4
  10. package/lib/ai/document.js +102 -55
  11. package/lib/ai/html-file.js +7 -24
  12. package/lib/ai/index.d.ts +2 -0
  13. package/lib/ai/index.js +2 -0
  14. package/lib/ai/json-file.js +11 -32
  15. package/lib/ai/review.d.ts +122 -0
  16. package/lib/ai/review.js +101 -0
  17. package/lib/ai/typescript-file.d.ts +1 -2
  18. package/lib/ai/typescript-file.js +8 -39
  19. package/lib/ai/vue-file.js +7 -24
  20. package/lib/ai/yaml-file.js +12 -48
  21. package/lib/alchemy.js +17 -2
  22. package/lib/apply.js +1 -1
  23. package/lib/cloudflare/generate-asset-manifest.js +1 -1
  24. package/lib/cloudflare/static-site-router.js +16 -7
  25. package/lib/cloudflare/static-site.js +3 -0
  26. package/lib/cloudflare/upload-asset-manifest.js +4 -2
  27. package/lib/fs/copy-file.d.ts +54 -0
  28. package/lib/fs/copy-file.js +63 -0
  29. package/lib/fs/file.d.ts +13 -0
  30. package/lib/fs/file.js +24 -5
  31. package/lib/fs/index.d.ts +6 -0
  32. package/lib/fs/index.js +6 -0
  33. package/lib/fs/static-astro-file.d.ts +34 -0
  34. package/lib/fs/static-astro-file.js +39 -0
  35. package/lib/fs/static-css-file.d.ts +24 -0
  36. package/lib/fs/static-css-file.js +29 -0
  37. package/lib/fs/static-html-file.d.ts +31 -0
  38. package/lib/fs/static-html-file.js +36 -0
  39. package/lib/fs/static-json-file.d.ts +2 -2
  40. package/lib/fs/static-json-file.js +3 -2
  41. package/lib/fs/static-text-file.d.ts +1 -1
  42. package/lib/fs/static-text-file.js +3 -2
  43. package/lib/fs/static-typescript-file.d.ts +2 -2
  44. package/lib/fs/static-typescript-file.js +3 -2
  45. package/lib/fs/static-vue-file.d.ts +28 -0
  46. package/lib/fs/static-vue-file.js +33 -0
  47. package/lib/fs/static-yaml-file.d.ts +11 -8
  48. package/lib/fs/static-yaml-file.js +3 -2
  49. package/lib/internal/getting-started.d.ts +15 -4
  50. package/lib/internal/getting-started.js +34 -41
  51. package/lib/internal/index.d.ts +3 -0
  52. package/lib/internal/index.js +3 -0
  53. package/lib/internal/providers.d.ts +7 -3
  54. package/lib/internal/providers.js +42 -18
  55. package/lib/internal/tutorial.d.ts +104 -0
  56. package/lib/internal/tutorial.js +251 -0
  57. package/lib/web/vitepress/config.d.ts +10 -0
  58. package/lib/web/vitepress/config.js +31 -0
  59. package/lib/web/vitepress/home-page.d.ts +5 -3
  60. package/lib/web/vitepress/home-page.js +48 -35
  61. package/lib/web/vitepress/index.d.ts +1 -0
  62. package/lib/web/vitepress/index.js +1 -0
  63. package/lib/web/vitepress/vitepress.d.ts +5 -4
  64. package/lib/web/vitepress/vitepress.js +17 -34
  65. package/package.json +1 -1
  66. package/src/ai/approve.ts +163 -0
  67. package/src/ai/astro-file.ts +11 -30
  68. package/src/ai/client.ts +59 -0
  69. package/src/ai/css-file.ts +11 -30
  70. package/src/ai/data.ts +50 -25
  71. package/src/ai/document.ts +146 -65
  72. package/src/ai/html-file.ts +11 -30
  73. package/src/ai/index.ts +2 -0
  74. package/src/ai/json-file.ts +13 -37
  75. package/src/ai/review.ts +213 -0
  76. package/src/ai/typescript-file.ts +14 -48
  77. package/src/ai/vue-file.ts +11 -30
  78. package/src/ai/yaml-file.ts +13 -53
  79. package/src/alchemy.ts +18 -7
  80. package/src/apply.ts +8 -8
  81. package/src/cloudflare/generate-asset-manifest.ts +4 -4
  82. package/src/cloudflare/static-site-router.ts +18 -8
  83. package/src/cloudflare/static-site.ts +11 -7
  84. package/src/cloudflare/upload-asset-manifest.ts +6 -4
  85. package/src/cloudflare/worker.ts +44 -43
  86. package/src/fs/copy-file.ts +115 -0
  87. package/src/fs/file.ts +35 -9
  88. package/src/fs/index.ts +6 -0
  89. package/src/fs/static-astro-file.ts +45 -0
  90. package/src/fs/static-css-file.ts +35 -0
  91. package/src/fs/static-html-file.ts +42 -0
  92. package/src/fs/static-json-file.ts +4 -3
  93. package/src/fs/static-text-file.ts +5 -2
  94. package/src/fs/static-typescript-file.ts +4 -3
  95. package/src/fs/static-vue-file.ts +39 -0
  96. package/src/fs/static-yaml-file.ts +13 -9
  97. package/src/internal/getting-started.ts +51 -43
  98. package/src/internal/index.ts +3 -0
  99. package/src/internal/providers.ts +59 -27
  100. package/src/internal/tutorial.ts +392 -0
  101. package/src/web/vitepress/config.ts +48 -0
  102. package/src/web/vitepress/home-page.ts +54 -42
  103. package/src/web/vitepress/index.ts +1 -0
  104. package/src/web/vitepress/vitepress.ts +31 -48
  105. package/lib/internal/project.d.ts +0 -13
  106. package/lib/internal/project.js +0 -101
  107. package/src/internal/project.ts +0 -119
@@ -0,0 +1,213 @@
1
+ import { generateText, type CoreMessage } from "ai";
2
+ import type { Context } from "../context";
3
+ import { Resource } from "../resource";
4
+ import type { Secret } from "../secret";
5
+ import { createModel, withRateLimitRetry, type ModelConfig } from "./client";
6
+
7
+ /**
8
+ * Properties for creating or updating a Review
9
+ */
10
+ export interface ReviewProps {
11
+ /**
12
+ * Content to be reviewed
13
+ * Use alchemy template literals to include file context:
14
+ * @example
15
+ * content: await alchemy`
16
+ * Review this code:
17
+ * ${alchemy.file("src/api.ts")}
18
+ * `
19
+ *
20
+ * Required unless messages are provided
21
+ */
22
+ content?: string;
23
+
24
+ /**
25
+ * Prompt for the review
26
+ * This should include specific instructions for the review
27
+ * @example
28
+ * prompt: "Review this code for security vulnerabilities, performance issues, and best practices."
29
+ *
30
+ * Required unless messages are provided
31
+ */
32
+ prompt?: string;
33
+
34
+ /**
35
+ * Message history for the conversation
36
+ * If provided, this will be used instead of the prompt and content
37
+ */
38
+ messages?: CoreMessage[];
39
+
40
+ /**
41
+ * System prompt for the model
42
+ * This is used to provide instructions to the model about how to format the response
43
+ * The default system prompt instructs the model to act as a reviewer
44
+ */
45
+ system?: string;
46
+
47
+ /**
48
+ * AI API key to use for generating content
49
+ * If not provided, will use OPENAI_API_KEY environment variable
50
+ */
51
+ apiKey?: Secret;
52
+
53
+ /**
54
+ * Model configuration
55
+ */
56
+ model?: ModelConfig;
57
+
58
+ /**
59
+ * Temperature for controlling randomness in generation.
60
+ * Higher values (e.g., 0.8) make output more random,
61
+ * lower values (e.g., 0.2) make it more deterministic.
62
+ * @default 0.7
63
+ */
64
+ temperature?: number;
65
+
66
+ /**
67
+ * Maximum number of tokens to generate.
68
+ * Higher values allow for longer documents but may increase cost and generation time.
69
+ * @default 10000
70
+ */
71
+ maxTokens?: number;
72
+ }
73
+
74
+ /**
75
+ * Output returned after Review creation/update
76
+ */
77
+ export interface Review extends ReviewProps, Resource<"ai::Review"> {
78
+ /**
79
+ * Content of the review
80
+ */
81
+ content: string;
82
+
83
+ /**
84
+ * Updated message history with the review response appended
85
+ */
86
+ messages: CoreMessage[];
87
+
88
+ /**
89
+ * Time at which the review was created
90
+ */
91
+ createdAt: number;
92
+
93
+ /**
94
+ * Time at which the review was last updated
95
+ */
96
+ updatedAt: number;
97
+ }
98
+
99
+ /**
100
+ * Default system prompt for review generation
101
+ */
102
+ const DEFAULT_REVIEW_SYSTEM_PROMPT = `You are an expert code reviewer and technical writer. Your task is to provide a thorough review of the provided content. Your review should include:
103
+
104
+ 1. A high-level summary of the content
105
+ 2. Detailed feedback on the content's quality, accuracy, and completeness
106
+ 3. Specific suggestions for improvement
107
+ 4. Alternative approaches or solutions where applicable
108
+ 5. Best practices that should be followed
109
+ 6. Potential issues or concerns that should be addressed
110
+
111
+ Provide your review in a clear, well-structured format.`;
112
+
113
+ /**
114
+ * Creates or updates a Review resource
115
+ *
116
+ * @example
117
+ * // Create a review of a code file
118
+ * const codeReview = await Review("api-review", {
119
+ * content: await alchemy`
120
+ * Review this API implementation:
121
+ * ${alchemy.file("src/api.ts")}
122
+ * `,
123
+ * prompt: "Review this API implementation for security vulnerabilities, performance issues, and best practices. Suggest improvements and alternative approaches."
124
+ * });
125
+ *
126
+ * @example
127
+ * // Create a review of a document
128
+ * const docReview = await Review("tutorial-review", {
129
+ * content: await alchemy`
130
+ * Review this tutorial:
131
+ * ${alchemy.file("docs/tutorials/getting-started.md")}
132
+ * `,
133
+ * prompt: "Review this tutorial for clarity, accuracy, and completeness. Suggest improvements to make it more user-friendly."
134
+ * });
135
+ *
136
+ * @example
137
+ * // Create a review with message history
138
+ * const reviewWithHistory = await Review("code-review-iteration-2", {
139
+ * content: "Updated code implementation",
140
+ * prompt: "Review this updated implementation",
141
+ * messages: [
142
+ * { role: "user", content: "Can you review my code?" },
143
+ * { role: "assistant", content: "I'll review your code. Please share it." },
144
+ * { role: "user", content: "Here's my implementation: [code]" },
145
+ * { role: "assistant", content: "Here's my review of your code: [previous review]" }
146
+ * ]
147
+ * });
148
+ */
149
+ export const Review = Resource(
150
+ "ai::Review",
151
+ async function (
152
+ this: Context<Review>,
153
+ id: string,
154
+ props: ReviewProps
155
+ ): Promise<Review> {
156
+ if (this.phase === "delete") {
157
+ return this.destroy();
158
+ }
159
+
160
+ // Use provided system prompt or default
161
+ const system = props.system || DEFAULT_REVIEW_SYSTEM_PROMPT;
162
+
163
+ // Create messages array if not provided
164
+ const messages =
165
+ props.messages ||
166
+ (props.content && props.prompt
167
+ ? [
168
+ {
169
+ role: "user",
170
+ content: `${props.content}\n\n${props.prompt}`,
171
+ },
172
+ ]
173
+ : []);
174
+
175
+ if (messages.length === 0) {
176
+ throw new Error(
177
+ "Either messages or both content and prompt must be provided"
178
+ );
179
+ }
180
+
181
+ // Generate review content with rate limit retry
182
+ const { text } = await withRateLimitRetry(async () => {
183
+ return generateText({
184
+ model: createModel(props),
185
+ messages,
186
+ system,
187
+ maxTokens: props.maxTokens || 8192,
188
+ providerOptions: props.model?.options,
189
+ ...(props.temperature === undefined
190
+ ? {}
191
+ : { temperature: props.temperature }),
192
+ });
193
+ });
194
+
195
+ // Create updated message history with the AI's response
196
+ const updatedMessages = [
197
+ ...messages,
198
+ {
199
+ role: "assistant" as const,
200
+ content: text,
201
+ },
202
+ ];
203
+
204
+ // Return the review with the direct text from the LLM and updated messages
205
+ return this({
206
+ ...props,
207
+ content: text,
208
+ messages: updatedMessages,
209
+ createdAt: Date.now(),
210
+ updatedAt: Date.now(),
211
+ });
212
+ }
213
+ );
@@ -1,12 +1,9 @@
1
1
  import { generateText } from "ai";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
4
- import prettier from "prettier";
5
2
  import type { Context } from "../context";
3
+ import { StaticTypeScriptFile } from "../fs/static-typescript-file";
6
4
  import { Resource } from "../resource";
7
5
  import type { Secret } from "../secret";
8
- import { ignore } from "../util/ignore";
9
- import { type ModelConfig, createModel } from "./client";
6
+ import { createModel, type ModelConfig } from "./client";
10
7
 
11
8
  /**
12
9
  * Properties for creating or updating a TypeScriptFile
@@ -65,7 +62,7 @@ export interface TypeScriptFileProps {
65
62
  * Prettier configuration to use for formatting the TypeScript code
66
63
  * If not provided, will use the default Prettier configuration
67
64
  */
68
- prettierConfig?: prettier.Options;
65
+ prettierConfig?: object;
69
66
  }
70
67
 
71
68
  /**
@@ -166,20 +163,11 @@ export const TypeScriptFile = Resource(
166
163
  async function (
167
164
  this: Context<TypeScriptFile>,
168
165
  id: string,
169
- props: TypeScriptFileProps,
166
+ props: TypeScriptFileProps
170
167
  ): Promise<TypeScriptFile> {
171
- // Ensure directory exists
172
- await fs.mkdir(path.dirname(props.path), { recursive: true });
173
-
168
+ // Handle delete phase
174
169
  if (this.phase === "delete") {
175
- try {
176
- await fs.unlink(props.path);
177
- } catch (error: any) {
178
- // Ignore if file doesn't exist
179
- if (error.code !== "ENOENT") {
180
- throw error;
181
- }
182
- }
170
+ // StaticTypeScriptFile will handle the deletion
183
171
  return this.destroy();
184
172
  }
185
173
 
@@ -218,46 +206,24 @@ export const TypeScriptFile = Resource(
218
206
 
219
207
  if (retryResult.error) {
220
208
  throw new Error(
221
- `Failed to generate valid TypeScript code: ${retryResult.error}`,
209
+ `Failed to generate valid TypeScript code: ${retryResult.error}`
222
210
  );
223
211
  }
224
212
 
225
213
  code = retryResult.code;
226
214
  }
227
215
 
228
- // Format the code with Prettier
229
- try {
230
- // Set default parser to typescript
231
- const prettierOptions: prettier.Options = {
232
- parser: "typescript",
233
- ...props.prettierConfig,
234
- };
235
-
236
- // Format the code
237
- code = await prettier.format(code, prettierOptions);
238
- } catch (error) {
239
- // If Prettier formatting fails, just use the unformatted code
240
- console.warn("Failed to format TypeScript code with Prettier:", error);
241
- }
242
-
243
- if (this.phase === "update" && props.path !== this.props.path) {
244
- await ignore("ENOENT", () => fs.unlink(this.props.path));
245
- }
246
-
247
- // Write content to file
248
- await fs.writeFile(props.path, code);
249
-
250
- // Get file stats for timestamps
251
- const stats = await fs.stat(props.path);
216
+ // Use StaticTypeScriptFile to create/update the file
217
+ const file = await StaticTypeScriptFile("file", props.path, code);
252
218
 
253
219
  // Return the resource
254
220
  return this({
255
221
  ...props,
256
- content: code,
257
- createdAt: stats.birthtimeMs,
258
- updatedAt: stats.mtimeMs,
222
+ content: file.content,
223
+ createdAt: Date.now(),
224
+ updatedAt: Date.now(),
259
225
  });
260
- },
226
+ }
261
227
  );
262
228
 
263
229
  /**
@@ -268,7 +234,7 @@ export const TypeScriptFile = Resource(
268
234
  * @returns The extracted TypeScript code or error message
269
235
  */
270
236
  async function extractTypeScriptCode(
271
- text: string,
237
+ text: string
272
238
  ): Promise<{ code: string; error?: string }> {
273
239
  const tsCodeRegex = /```ts\s*([\s\S]*?)```/g;
274
240
  const matches = Array.from(text.matchAll(tsCodeRegex));
@@ -1,10 +1,8 @@
1
1
  import { generateText } from "ai";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
4
2
  import type { Context } from "../context";
3
+ import { StaticVueFile } from "../fs/static-vue-file";
5
4
  import { Resource } from "../resource";
6
5
  import type { Secret } from "../secret";
7
- import { ignore } from "../util/ignore";
8
6
  import { type ModelConfig, createModel } from "./client";
9
7
 
10
8
  /**
@@ -149,20 +147,10 @@ export const VueFile = Resource(
149
147
  async function (
150
148
  this: Context<VueFile>,
151
149
  id: string,
152
- props: VueFileProps,
150
+ props: VueFileProps
153
151
  ): Promise<VueFile> {
154
- // Ensure directory exists
155
- await fs.mkdir(path.dirname(props.path), { recursive: true });
156
-
152
+ // Handle deletion phase
157
153
  if (this.phase === "delete") {
158
- try {
159
- await fs.unlink(props.path);
160
- } catch (error: any) {
161
- // Ignore if file doesn't exist
162
- if (error.code !== "ENOENT") {
163
- throw error;
164
- }
165
- }
166
154
  return this.destroy();
167
155
  }
168
156
 
@@ -201,31 +189,24 @@ export const VueFile = Resource(
201
189
 
202
190
  if (retryResult.error) {
203
191
  throw new Error(
204
- `Failed to generate valid Vue code: ${retryResult.error}`,
192
+ `Failed to generate valid Vue code: ${retryResult.error}`
205
193
  );
206
194
  }
207
195
 
208
196
  code = retryResult.code;
209
197
  }
210
198
 
211
- if (this.phase === "update" && props.path !== this.props.path) {
212
- await ignore("ENOENT", () => fs.unlink(this.props.path));
213
- }
214
-
215
- // Write content to file
216
- await fs.writeFile(props.path, code);
217
-
218
- // Get file stats for timestamps
219
- const stats = await fs.stat(props.path);
199
+ // Use StaticVueFile to create/update the file
200
+ const file = await StaticVueFile("file", props.path, code);
220
201
 
221
202
  // Return the resource
222
203
  return this({
223
204
  ...props,
224
- content: code,
225
- createdAt: stats.birthtimeMs,
226
- updatedAt: stats.mtimeMs,
205
+ content: file.content,
206
+ createdAt: Date.now(),
207
+ updatedAt: Date.now(),
227
208
  });
228
- },
209
+ }
229
210
  );
230
211
 
231
212
  /**
@@ -236,7 +217,7 @@ export const VueFile = Resource(
236
217
  * @returns The extracted Vue code or error message
237
218
  */
238
219
  async function extractVueCode(
239
- text: string,
220
+ text: string
240
221
  ): Promise<{ code: string; error?: string }> {
241
222
  const vueCodeRegex = /```vue\s*([\s\S]*?)```/g;
242
223
  const matches = Array.from(text.matchAll(vueCodeRegex));
@@ -1,11 +1,9 @@
1
1
  import { generateObject, generateText } from "ai";
2
2
  import type { JsonSchema, Type, type } from "arktype";
3
- import fs from "node:fs/promises";
4
- import path from "node:path";
5
3
  import type { Context } from "../context";
4
+ import { StaticYamlFile } from "../fs/static-yaml-file";
6
5
  import { Resource } from "../resource";
7
6
  import type { Secret } from "../secret";
8
- import { ignore } from "../util/ignore";
9
7
  import { ark } from "./ark";
10
8
  import { type ModelConfig, createModel } from "./client";
11
9
 
@@ -200,32 +198,11 @@ export const YAMLFile = Resource("ai::YAMLFile", async function <
200
198
  >(this: Context<YAMLFile<T extends Type<any, any> ? type.infer<T> : any>>, id: string, props: YAMLFileProps<T>): Promise<
201
199
  YAMLFile<T extends Type<any, any> ? type.infer<T> : any>
202
200
  > {
203
- // Ensure directory exists
204
- await fs.mkdir(path.dirname(props.path), { recursive: true });
205
-
201
+ // Handle deletion phase
206
202
  if (this.phase === "delete") {
207
- try {
208
- await fs.unlink(props.path);
209
- } catch (error: any) {
210
- // Ignore if file doesn't exist
211
- if (error.code !== "ENOENT") {
212
- throw error;
213
- }
214
- }
215
203
  return this.destroy();
216
204
  }
217
205
 
218
- // Dynamic import js-yaml to avoid dependency issues
219
- // This allows the code to work even if js-yaml is not installed
220
- let yaml: typeof import("yaml");
221
- try {
222
- yaml = await import("yaml");
223
- } catch (err) {
224
- throw new Error(
225
- "The 'yaml' package is required for the YAMLFile resource. Please install it with: bun add yaml",
226
- );
227
- }
228
-
229
206
  let yamlContent: string;
230
207
  let yamlObject: any;
231
208
 
@@ -247,16 +224,9 @@ export const YAMLFile = Resource("ai::YAMLFile", async function <
247
224
 
248
225
  yamlObject = object;
249
226
 
250
- // Convert object to YAML
251
- try {
252
- yamlContent = yaml.stringify(yamlObject, {
253
- indent: 2,
254
- });
255
- } catch (error) {
256
- throw new Error(
257
- `Failed to convert object to YAML: ${(error as Error).message}`,
258
- );
259
- }
227
+ // Let StaticYamlFile handle the YAML conversion
228
+ const file = await StaticYamlFile("file", props.path, yamlObject);
229
+ yamlContent = file.content;
260
230
  } else {
261
231
  // Use fence-based extraction
262
232
  // Use provided system prompt or default
@@ -301,32 +271,22 @@ export const YAMLFile = Resource("ai::YAMLFile", async function <
301
271
 
302
272
  yamlContent = content;
303
273
 
304
- // Parse YAML to validate and get object representation
305
- try {
306
- yamlObject = yaml.parse(yamlContent);
307
- } catch (error) {
308
- throw new Error(`Failed to parse YAML: ${(error as Error).message}`);
309
- }
310
- }
274
+ // Create the file with the string content
275
+ const file = await StaticYamlFile("file", props.path, yamlContent);
311
276
 
312
- if (this.phase === "update" && props.path !== this.props.path) {
313
- await ignore("ENOENT", () => fs.unlink(this.props.path));
277
+ // We need to parse the YAML to get the object representation
278
+ const yaml = await import("yaml");
279
+ yamlObject = yaml.parse(yamlContent);
314
280
  }
315
281
 
316
- // Write content to file
317
- await fs.writeFile(props.path, yamlContent);
318
-
319
- // Get file stats for timestamps
320
- const stats = await fs.stat(props.path);
321
-
322
282
  // Return the resource
323
283
  return this({
324
284
  ...props,
325
285
  schema: props.schema,
326
286
  content: yamlContent,
327
287
  yaml: yamlObject,
328
- createdAt: stats.birthtimeMs,
329
- updatedAt: stats.mtimeMs,
288
+ createdAt: Date.now(),
289
+ updatedAt: Date.now(),
330
290
  });
331
291
  });
332
292
 
@@ -338,7 +298,7 @@ export const YAMLFile = Resource("ai::YAMLFile", async function <
338
298
  * @returns The extracted YAML or error message
339
299
  */
340
300
  async function extractYAMLContent(
341
- text: string,
301
+ text: string
342
302
  ): Promise<{ content: string; error?: string }> {
343
303
  // Check for yaml or yml fence blocks
344
304
  const yamlCodeRegex = /```(yaml|yml)\s*([\s\S]*?)```/g;
package/src/alchemy.ts CHANGED
@@ -111,6 +111,16 @@ function _alchemy(
111
111
  values.map(async function resolve(value): Promise<string> {
112
112
  if (typeof value === "string") {
113
113
  return indent + value;
114
+ } else if (value === null) {
115
+ return "null";
116
+ } else if (value === undefined) {
117
+ return "undefined";
118
+ } else if (
119
+ typeof value === "number" ||
120
+ typeof value === "boolean" ||
121
+ typeof value === "bigint"
122
+ ) {
123
+ return value.toString();
114
124
  } else if (value instanceof Promise) {
115
125
  return resolve(await value);
116
126
  } else if (isFileRef(value)) {
@@ -128,14 +138,15 @@ function _alchemy(
128
138
  } else if (Array.isArray(value)) {
129
139
  return (
130
140
  await Promise.all(
131
- value.map(async (value, i) => `${i}. ${await resolve(value)}`),
141
+ value.map(async (value, i) => `${i}. ${await resolve(value)}`)
132
142
  )
133
143
  ).join("\n");
134
144
  } else {
135
145
  // TODO: support other types
136
- throw new Error(`Unsupported value type: ${JSON.stringify(value)}`);
146
+ console.log(value);
147
+ throw new Error(`Unsupported value type: ${value}`);
137
148
  }
138
- }),
149
+ })
139
150
  );
140
151
 
141
152
  // Construct the string template by joining template parts with interpolated values
@@ -144,12 +155,12 @@ function _alchemy(
144
155
  part
145
156
  .split("\n")
146
157
  .map((line) =>
147
- line.startsWith(indent) ? line.slice(indent.length) : line,
158
+ line.startsWith(indent) ? line.slice(indent.length) : line
148
159
  )
149
- .join("\n"),
160
+ .join("\n")
150
161
  )
151
162
  .flatMap((part, i) =>
152
- i < stringValues.length ? [part, stringValues[i] ?? ""] : [part],
163
+ i < stringValues.length ? [part, stringValues[i] ?? ""] : [part]
153
164
  )
154
165
  .join("")
155
166
  .split("\n");
@@ -240,7 +251,7 @@ export interface AlchemyOptions {
240
251
  */
241
252
  function scope(
242
253
  id: string | undefined,
243
- options?: AlchemyOptions,
254
+ options?: AlchemyOptions
244
255
  // TODO: maybe we want to allow using _ = await alchemy.scope(import.meta)
245
256
  // | [meta: ImportMeta]
246
257
  ): Scope {
package/src/apply.ts CHANGED
@@ -19,7 +19,7 @@ export interface ApplyOptions {
19
19
  export async function apply<Out extends Resource>(
20
20
  resource: PendingResource<Out>,
21
21
  props: ResourceProps | undefined,
22
- options?: ApplyOptions,
22
+ options?: ApplyOptions
23
23
  ): Promise<Awaited<Out>> {
24
24
  const scope = resource.Scope;
25
25
  try {
@@ -64,18 +64,18 @@ export async function apply<Out extends Resource>(
64
64
  "@secret": value.unencrypted,
65
65
  }
66
66
  : value,
67
- 2,
67
+ 2
68
68
  );
69
69
  const newProps = JSON.stringify(
70
70
  await serialize(scope, props, {
71
71
  encrypt: false,
72
72
  }),
73
73
  null,
74
- 2,
74
+ 2
75
75
  );
76
76
  if (oldProps === newProps && alwaysUpdate !== true) {
77
77
  if (!quiet) {
78
- console.log(`Skip: "${resource.FQN}" (no changes)`);
78
+ // console.log(`Skip: "${resource.FQN}" (no changes)`);
79
79
  }
80
80
  return state.output as Awaited<Out>;
81
81
  }
@@ -88,7 +88,7 @@ export async function apply<Out extends Resource>(
88
88
 
89
89
  if (!quiet) {
90
90
  console.log(
91
- `${phase === "create" ? "Create" : "Update"}: "${resource.FQN}"`,
91
+ `${phase === "create" ? "Create" : "Update"}: "${resource.FQN}"`
92
92
  );
93
93
  }
94
94
 
@@ -108,7 +108,7 @@ export async function apply<Out extends Resource>(
108
108
  replace: () => {
109
109
  if (isReplaced) {
110
110
  console.warn(
111
- `Resource ${resource.Kind} ${resource.FQN} is already marked as REPLACE`,
111
+ `Resource ${resource.Kind} ${resource.FQN} is already marked as REPLACE`
112
112
  );
113
113
  return;
114
114
  }
@@ -117,11 +117,11 @@ export async function apply<Out extends Resource>(
117
117
  });
118
118
 
119
119
  const output = await alchemy.run(resource.ID, async () =>
120
- provider.handler.bind(ctx)(resource.ID, props),
120
+ provider.handler.bind(ctx)(resource.ID, props)
121
121
  );
122
122
  if (!quiet) {
123
123
  console.log(
124
- `${phase === "create" ? "Created" : "Updated"}: "${resource.FQN}"`,
124
+ `${phase === "create" ? "Created" : "Updated"}: "${resource.FQN}"`
125
125
  );
126
126
  }
127
127
 
@@ -17,10 +17,10 @@ export async function generateAssetManifest(
17
17
  cacheControl: "max-age=0,no-cache,no-store,must-revalidate",
18
18
  },
19
19
  {
20
- files: ["**/*.js", "**/*.css"],
20
+ files: ["**/*.js", "**/*.css", "**/*.png", "**/*.webp"],
21
21
  cacheControl: "max-age=31536000,public,immutable",
22
22
  },
23
- ],
23
+ ]
24
24
  ): Promise<AssetManifest> {
25
25
  // First, collect all file info based on fileOptions (reversed to prioritize later rules)
26
26
  const fileInfos: Array<{
@@ -55,7 +55,7 @@ export async function generateAssetManifest(
55
55
  : (fileOption.ignore ?? [])),
56
56
  ],
57
57
  });
58
- }),
58
+ })
59
59
  )
60
60
  )
61
61
  .flat()
@@ -86,7 +86,7 @@ export async function generateAssetManifest(
86
86
  cacheControl,
87
87
  contentType: contentType ?? getContentType(file),
88
88
  } satisfies AssetManifestEntry;
89
- }),
89
+ })
90
90
  );
91
91
 
92
92
  return manifestEntries;