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
@@ -1,9 +1,6 @@
1
1
  import { generateText } from "ai";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
4
- import prettier from "prettier";
2
+ import { StaticTypeScriptFile } from "../fs/static-typescript-file";
5
3
  import { Resource } from "../resource";
6
- import { ignore } from "../util/ignore";
7
4
  import { createModel } from "./client";
8
5
  /**
9
6
  * Default system prompt for TypeScript file generation
@@ -75,18 +72,9 @@ const DEFAULT_TS_SYSTEM_PROMPT = "You are a TypeScript code generator. Create Ty
75
72
  * });
76
73
  */
77
74
  export const TypeScriptFile = Resource("ai::TypeScriptFile", async function (id, props) {
78
- // Ensure directory exists
79
- await fs.mkdir(path.dirname(props.path), { recursive: true });
75
+ // Handle delete phase
80
76
  if (this.phase === "delete") {
81
- try {
82
- await fs.unlink(props.path);
83
- }
84
- catch (error) {
85
- // Ignore if file doesn't exist
86
- if (error.code !== "ENOENT") {
87
- throw error;
88
- }
89
- }
77
+ // StaticTypeScriptFile will handle the deletion
90
78
  return this.destroy();
91
79
  }
92
80
  // Use provided system prompt or default
@@ -121,33 +109,14 @@ export const TypeScriptFile = Resource("ai::TypeScriptFile", async function (id,
121
109
  }
122
110
  code = retryResult.code;
123
111
  }
124
- // Format the code with Prettier
125
- try {
126
- // Set default parser to typescript
127
- const prettierOptions = {
128
- parser: "typescript",
129
- ...props.prettierConfig,
130
- };
131
- // Format the code
132
- code = await prettier.format(code, prettierOptions);
133
- }
134
- catch (error) {
135
- // If Prettier formatting fails, just use the unformatted code
136
- console.warn("Failed to format TypeScript code with Prettier:", error);
137
- }
138
- if (this.phase === "update" && props.path !== this.props.path) {
139
- await ignore("ENOENT", () => fs.unlink(this.props.path));
140
- }
141
- // Write content to file
142
- await fs.writeFile(props.path, code);
143
- // Get file stats for timestamps
144
- const stats = await fs.stat(props.path);
112
+ // Use StaticTypeScriptFile to create/update the file
113
+ const file = await StaticTypeScriptFile("file", props.path, code);
145
114
  // Return the resource
146
115
  return this({
147
116
  ...props,
148
- content: code,
149
- createdAt: stats.birthtimeMs,
150
- updatedAt: stats.mtimeMs,
117
+ content: file.content,
118
+ createdAt: Date.now(),
119
+ updatedAt: Date.now(),
151
120
  });
152
121
  });
153
122
  /**
@@ -1,8 +1,6 @@
1
1
  import { generateText } from "ai";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
2
+ import { StaticVueFile } from "../fs/static-vue-file";
4
3
  import { Resource } from "../resource";
5
- import { ignore } from "../util/ignore";
6
4
  import { createModel } from "./client";
7
5
  /**
8
6
  * Default system prompt for Vue file generation
@@ -66,18 +64,8 @@ const DEFAULT_VUE_SYSTEM_PROMPT = "You are a Vue component generator. Create a s
66
64
  * });
67
65
  */
68
66
  export const VueFile = Resource("ai::VueFile", async function (id, props) {
69
- // Ensure directory exists
70
- await fs.mkdir(path.dirname(props.path), { recursive: true });
67
+ // Handle deletion phase
71
68
  if (this.phase === "delete") {
72
- try {
73
- await fs.unlink(props.path);
74
- }
75
- catch (error) {
76
- // Ignore if file doesn't exist
77
- if (error.code !== "ENOENT") {
78
- throw error;
79
- }
80
- }
81
69
  return this.destroy();
82
70
  }
83
71
  // Use provided system prompt or default
@@ -112,19 +100,14 @@ export const VueFile = Resource("ai::VueFile", async function (id, props) {
112
100
  }
113
101
  code = retryResult.code;
114
102
  }
115
- if (this.phase === "update" && props.path !== this.props.path) {
116
- await ignore("ENOENT", () => fs.unlink(this.props.path));
117
- }
118
- // Write content to file
119
- await fs.writeFile(props.path, code);
120
- // Get file stats for timestamps
121
- const stats = await fs.stat(props.path);
103
+ // Use StaticVueFile to create/update the file
104
+ const file = await StaticVueFile("file", props.path, code);
122
105
  // Return the resource
123
106
  return this({
124
107
  ...props,
125
- content: code,
126
- createdAt: stats.birthtimeMs,
127
- updatedAt: stats.mtimeMs,
108
+ content: file.content,
109
+ createdAt: Date.now(),
110
+ updatedAt: Date.now(),
128
111
  });
129
112
  });
130
113
  /**
@@ -1,8 +1,6 @@
1
1
  import { generateObject, generateText } from "ai";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
2
+ import { StaticYamlFile } from "../fs/static-yaml-file";
4
3
  import { Resource } from "../resource";
5
- import { ignore } from "../util/ignore";
6
4
  import { ark } from "./ark";
7
5
  import { createModel } from "./client";
8
6
  /**
@@ -96,29 +94,10 @@ const DEFAULT_YAML_SYSTEM_PROMPT = "You are a YAML generator. Create valid YAML
96
94
  * });
97
95
  */
98
96
  export const YAMLFile = Resource("ai::YAMLFile", async function (id, props) {
99
- // Ensure directory exists
100
- await fs.mkdir(path.dirname(props.path), { recursive: true });
97
+ // Handle deletion phase
101
98
  if (this.phase === "delete") {
102
- try {
103
- await fs.unlink(props.path);
104
- }
105
- catch (error) {
106
- // Ignore if file doesn't exist
107
- if (error.code !== "ENOENT") {
108
- throw error;
109
- }
110
- }
111
99
  return this.destroy();
112
100
  }
113
- // Dynamic import js-yaml to avoid dependency issues
114
- // This allows the code to work even if js-yaml is not installed
115
- let yaml;
116
- try {
117
- yaml = await import("yaml");
118
- }
119
- catch (err) {
120
- throw new Error("The 'yaml' package is required for the YAMLFile resource. Please install it with: bun add yaml");
121
- }
122
101
  let yamlContent;
123
102
  let yamlObject;
124
103
  // Check if schema is provided
@@ -136,15 +115,9 @@ export const YAMLFile = Resource("ai::YAMLFile", async function (id, props) {
136
115
  : { temperature: props.temperature }),
137
116
  });
138
117
  yamlObject = object;
139
- // Convert object to YAML
140
- try {
141
- yamlContent = yaml.stringify(yamlObject, {
142
- indent: 2,
143
- });
144
- }
145
- catch (error) {
146
- throw new Error(`Failed to convert object to YAML: ${error.message}`);
147
- }
118
+ // Let StaticYamlFile handle the YAML conversion
119
+ const file = await StaticYamlFile("file", props.path, yamlObject);
120
+ yamlContent = file.content;
148
121
  }
149
122
  else {
150
123
  // Use fence-based extraction
@@ -181,29 +154,20 @@ export const YAMLFile = Resource("ai::YAMLFile", async function (id, props) {
181
154
  content = retryResult.content;
182
155
  }
183
156
  yamlContent = content;
184
- // Parse YAML to validate and get object representation
185
- try {
186
- yamlObject = yaml.parse(yamlContent);
187
- }
188
- catch (error) {
189
- throw new Error(`Failed to parse YAML: ${error.message}`);
190
- }
191
- }
192
- if (this.phase === "update" && props.path !== this.props.path) {
193
- await ignore("ENOENT", () => fs.unlink(this.props.path));
157
+ // Create the file with the string content
158
+ const file = await StaticYamlFile("file", props.path, yamlContent);
159
+ // We need to parse the YAML to get the object representation
160
+ const yaml = await import("yaml");
161
+ yamlObject = yaml.parse(yamlContent);
194
162
  }
195
- // Write content to file
196
- await fs.writeFile(props.path, yamlContent);
197
- // Get file stats for timestamps
198
- const stats = await fs.stat(props.path);
199
163
  // Return the resource
200
164
  return this({
201
165
  ...props,
202
166
  schema: props.schema,
203
167
  content: yamlContent,
204
168
  yaml: yamlObject,
205
- createdAt: stats.birthtimeMs,
206
- updatedAt: stats.mtimeMs,
169
+ createdAt: Date.now(),
170
+ updatedAt: Date.now(),
207
171
  });
208
172
  });
209
173
  /**
package/lib/alchemy.js CHANGED
@@ -33,6 +33,17 @@ function _alchemy(...args) {
33
33
  if (typeof value === "string") {
34
34
  return indent + value;
35
35
  }
36
+ else if (value === null) {
37
+ return "null";
38
+ }
39
+ else if (value === undefined) {
40
+ return "undefined";
41
+ }
42
+ else if (typeof value === "number" ||
43
+ typeof value === "boolean" ||
44
+ typeof value === "bigint") {
45
+ return value.toString();
46
+ }
36
47
  else if (value instanceof Promise) {
37
48
  return resolve(await value);
38
49
  }
@@ -55,7 +66,8 @@ function _alchemy(...args) {
55
66
  }
56
67
  else {
57
68
  // TODO: support other types
58
- throw new Error(`Unsupported value type: ${JSON.stringify(value)}`);
69
+ console.log(value);
70
+ throw new Error(`Unsupported value type: ${value}`);
59
71
  }
60
72
  }));
61
73
  // Construct the string template by joining template parts with interpolated values
@@ -104,7 +116,10 @@ _alchemy.secret = secret;
104
116
  * apiKey: alchemy.secret(process.env.API_KEY)
105
117
  * });
106
118
  */
107
- function scope(id, options) {
119
+ function scope(id, options
120
+ // TODO: maybe we want to allow using _ = await alchemy.scope(import.meta)
121
+ // | [meta: ImportMeta]
122
+ ) {
108
123
  const scope = new Scope({
109
124
  ...options,
110
125
  appName: options?.appName,
package/lib/apply.js CHANGED
@@ -46,7 +46,7 @@ export async function apply(resource, props, options) {
46
46
  }), null, 2);
47
47
  if (oldProps === newProps && alwaysUpdate !== true) {
48
48
  if (!quiet) {
49
- console.log(`Skip: "${resource.FQN}" (no changes)`);
49
+ // console.log(`Skip: "${resource.FQN}" (no changes)`);
50
50
  }
51
51
  return state.output;
52
52
  }
@@ -9,7 +9,7 @@ export async function generateAssetManifest(dir, fileOptions = [
9
9
  cacheControl: "max-age=0,no-cache,no-store,must-revalidate",
10
10
  },
11
11
  {
12
- files: ["**/*.js", "**/*.css"],
12
+ files: ["**/*.js", "**/*.css", "**/*.png", "**/*.webp"],
13
13
  cacheControl: "max-age=31536000,public,immutable",
14
14
  },
15
15
  ]) {
@@ -35,34 +35,44 @@ app.notFound(async (ctx) => {
35
35
  }
36
36
  // Fetch from KV
37
37
  {
38
- const object = await env.ASSETS.getWithMetadata(filePath);
38
+ const object = await env.ASSETS.getWithMetadata(filePath, {
39
+ type: "arrayBuffer",
40
+ });
39
41
  if (object.value) {
40
42
  return await respond(200, filePath, object);
41
43
  }
42
44
  }
43
45
  {
44
46
  const guess = filePath + (filePath.endsWith("/") ? "" : "/") + "index.html";
45
- const object = await env.ASSETS.getWithMetadata(guess);
47
+ const object = await env.ASSETS.getWithMetadata(guess, {
48
+ type: "arrayBuffer",
49
+ });
46
50
  if (object.value) {
47
51
  return await respond(200, guess, object);
48
52
  }
49
53
  }
50
54
  {
51
55
  const guess = filePath + ".html";
52
- const object = await env.ASSETS.getWithMetadata(guess);
56
+ const object = await env.ASSETS.getWithMetadata(guess, {
57
+ type: "arrayBuffer",
58
+ });
53
59
  if (object.value) {
54
60
  return await respond(200, guess, object);
55
61
  }
56
62
  }
57
63
  // Handle error page
58
64
  if (env.ERROR_PAGE) {
59
- const object = await env.ASSETS.getWithMetadata(env.ERROR_PAGE);
65
+ const object = await env.ASSETS.getWithMetadata(env.ERROR_PAGE, {
66
+ type: "arrayBuffer",
67
+ });
60
68
  if (object.value) {
61
69
  return await respond(404, env.ERROR_PAGE, object);
62
70
  }
63
71
  }
64
72
  else {
65
- const object = await env.ASSETS.getWithMetadata(env.INDEX_PAGE);
73
+ const object = await env.ASSETS.getWithMetadata(env.INDEX_PAGE, {
74
+ type: "arrayBuffer",
75
+ });
66
76
  if (object.value) {
67
77
  return await respond(200, env.INDEX_PAGE, object);
68
78
  }
@@ -92,8 +102,7 @@ app.notFound(async (ctx) => {
92
102
  headers.set("content-type", object.metadata.contentType);
93
103
  headers.set("cache-control", object.metadata.cacheControl);
94
104
  }
95
- // TODO: do we need base64 encoded here?
96
- // const response = new Response(base64ToArrayBuffer(object.value), {
105
+ // Create response with the raw data directly
97
106
  const response = new Response(object.value, {
98
107
  status,
99
108
  headers,
@@ -122,6 +122,9 @@ export const StaticSite = Resource("cloudflare::StaticSite", {
122
122
  }),
123
123
  generateAssetManifest(props.dir),
124
124
  ]);
125
+ console.log({
126
+ assetManifest,
127
+ });
125
128
  // Step 3: Upload assets to KV
126
129
  await uploadAssetManifest(api, kv.namespaceId, assetManifest);
127
130
  // Prepare the bindings for the worker
@@ -34,8 +34,10 @@ export async function uploadAssetManifest(api, namespaceId, manifest) {
34
34
  // Create FormData for multipart upload
35
35
  const formData = new FormData();
36
36
  formData.append("metadata", JSON.stringify(metadata));
37
- // Add file content as 'value'
38
- const blob = new Blob([content]);
37
+ // Add file content as 'value' - Fix for binary file corruption
38
+ const blob = new Blob([new Uint8Array(content)], {
39
+ type: item.contentType,
40
+ });
39
41
  formData.append("value", blob);
40
42
  try {
41
43
  const response = await api.put(`/accounts/${api.accountId}/storage/kv/namespaces/${namespaceId}/values/${item.key}`, formData);
@@ -0,0 +1,54 @@
1
+ import type { Context } from "../context";
2
+ import { Resource } from "../resource";
3
+ /**
4
+ * Properties for creating a CopyFile resource
5
+ */
6
+ export interface CopyFileProps {
7
+ /**
8
+ * Source path of the file to copy
9
+ */
10
+ src: string;
11
+ /**
12
+ * Destination path where the file should be copied to
13
+ */
14
+ dest: string;
15
+ /**
16
+ * Whether to overwrite the destination file if it already exists
17
+ * @default true
18
+ */
19
+ overwrite?: boolean;
20
+ }
21
+ /**
22
+ * Output returned after CopyFile creation/update
23
+ */
24
+ export interface CopyFile extends Resource<"fs::CopyFile">, CopyFileProps {
25
+ /**
26
+ * Time at which the object was created
27
+ */
28
+ createdAt: number;
29
+ /**
30
+ * Whether the file was successfully copied
31
+ */
32
+ copied: boolean;
33
+ }
34
+ /**
35
+ * CopyFile Resource
36
+ *
37
+ * Copies a file from a source location to a destination location.
38
+ *
39
+ * @example
40
+ * // Copy a file to a new location
41
+ * const copiedFile = await CopyFile("config-copy", {
42
+ * src: "config.json",
43
+ * dest: "backup/config.json"
44
+ * });
45
+ *
46
+ * @example
47
+ * // Copy a file without overwriting if destination exists
48
+ * const safeCopy = await CopyFile("safe-copy", {
49
+ * src: "data.json",
50
+ * dest: "backup/data.json",
51
+ * overwrite: false
52
+ * });
53
+ */
54
+ export declare const CopyFile: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<CopyFile>, id: string, props: CopyFileProps) => Promise<CopyFile>);
@@ -0,0 +1,63 @@
1
+ import fs from "node:fs";
2
+ import { Resource } from "../resource";
3
+ import { ignore } from "../util/ignore";
4
+ /**
5
+ * CopyFile Resource
6
+ *
7
+ * Copies a file from a source location to a destination location.
8
+ *
9
+ * @example
10
+ * // Copy a file to a new location
11
+ * const copiedFile = await CopyFile("config-copy", {
12
+ * src: "config.json",
13
+ * dest: "backup/config.json"
14
+ * });
15
+ *
16
+ * @example
17
+ * // Copy a file without overwriting if destination exists
18
+ * const safeCopy = await CopyFile("safe-copy", {
19
+ * src: "data.json",
20
+ * dest: "backup/data.json",
21
+ * overwrite: false
22
+ * });
23
+ */
24
+ export const CopyFile = Resource("fs::CopyFile", async function (id, props) {
25
+ const { src, dest, overwrite = true } = props;
26
+ if (this.phase === "delete") {
27
+ // When deleting, remove the destination file
28
+ await ignore("ENOENT", async () => fs.promises.unlink(dest));
29
+ return this.destroy();
30
+ }
31
+ else {
32
+ try {
33
+ // Check if source file exists
34
+ await fs.promises.access(src, fs.constants.F_OK);
35
+ // If this is an update and the destination has changed, delete the old file
36
+ if (this.phase === "update" &&
37
+ this.output?.dest &&
38
+ this.output.dest !== dest) {
39
+ await ignore("ENOENT", async () => fs.promises.unlink(this.output.dest));
40
+ }
41
+ // Check if destination file exists
42
+ const destinationExists = await fs.promises
43
+ .access(dest, fs.constants.F_OK)
44
+ .then(() => true)
45
+ .catch(() => false);
46
+ // Copy file if destination doesn't exist or overwrite is true
47
+ if (!destinationExists || overwrite) {
48
+ await fs.promises.copyFile(src, dest);
49
+ }
50
+ return this({
51
+ src,
52
+ dest,
53
+ overwrite,
54
+ copied: true,
55
+ createdAt: Date.now(),
56
+ });
57
+ }
58
+ catch (error) {
59
+ console.error(`Error copying file from ${src} to ${dest}:`, error);
60
+ throw error;
61
+ }
62
+ }
63
+ });
package/lib/fs/file.d.ts CHANGED
@@ -79,6 +79,19 @@ export interface File extends Resource<"fs::File"> {
79
79
  * path: "logs/app.log",
80
80
  * content: "application log entry"
81
81
  * });
82
+ *
83
+ * @example
84
+ * // Update file content and path
85
+ * let file = await File("config.json", {
86
+ * path: "config.json",
87
+ * content: '{ "version": "1.0.0" }'
88
+ * });
89
+ *
90
+ * // Later, update the path and content (old file will be removed)
91
+ * file = await File("config.json", {
92
+ * path: "config/config.json",
93
+ * content: '{ "version": "1.0.1" }'
94
+ * });
82
95
  */
83
96
  export declare const File: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<File>, id: string, props: {
84
97
  path: string;
package/lib/fs/file.js CHANGED
@@ -36,6 +36,19 @@ alchemy.files = async (...args) => {
36
36
  * path: "logs/app.log",
37
37
  * content: "application log entry"
38
38
  * });
39
+ *
40
+ * @example
41
+ * // Update file content and path
42
+ * let file = await File("config.json", {
43
+ * path: "config.json",
44
+ * content: '{ "version": "1.0.0" }'
45
+ * });
46
+ *
47
+ * // Later, update the path and content (old file will be removed)
48
+ * file = await File("config.json", {
49
+ * path: "config/config.json",
50
+ * content: '{ "version": "1.0.1" }'
51
+ * });
39
52
  */
40
53
  export const File = Resource("fs::File", async function (id, props) {
41
54
  const filePath = props?.path ?? id;
@@ -43,12 +56,18 @@ export const File = Resource("fs::File", async function (id, props) {
43
56
  await ignore("ENOENT", async () => fs.promises.unlink(filePath));
44
57
  return this.destroy();
45
58
  }
46
- else {
47
- await fs.promises.mkdir(path.dirname(filePath), {
48
- recursive: true,
49
- });
50
- await fs.promises.writeFile(filePath, props.content);
59
+ else if (this.phase === "update" &&
60
+ this.output &&
61
+ this.output.path !== filePath) {
62
+ // If path has changed, delete the old file
63
+ console.log(`File: Path changed from ${this.output.path} to ${filePath}, removing old file`);
64
+ await ignore("ENOENT", async () => fs.promises.unlink(this.output.path));
51
65
  }
66
+ // Create directory and write file
67
+ await fs.promises.mkdir(path.dirname(filePath), {
68
+ recursive: true,
69
+ });
70
+ await fs.promises.writeFile(filePath, props.content);
52
71
  return this({
53
72
  path: filePath,
54
73
  content: props.content,
package/lib/fs/index.d.ts CHANGED
@@ -1,8 +1,14 @@
1
+ export * from "./copy-file";
1
2
  export * from "./file";
2
3
  export * from "./file-collection";
3
4
  export * from "./file-ref";
5
+ export * from "./file-system-state-store";
4
6
  export * from "./folder";
7
+ export * from "./static-astro-file";
8
+ export * from "./static-css-file";
9
+ export * from "./static-html-file";
5
10
  export * from "./static-json-file";
6
11
  export * from "./static-text-file";
7
12
  export * from "./static-typescript-file";
13
+ export * from "./static-vue-file";
8
14
  export * from "./static-yaml-file";
package/lib/fs/index.js CHANGED
@@ -1,8 +1,14 @@
1
+ export * from "./copy-file";
1
2
  export * from "./file";
2
3
  export * from "./file-collection";
3
4
  export * from "./file-ref";
5
+ export * from "./file-system-state-store";
4
6
  export * from "./folder";
7
+ export * from "./static-astro-file";
8
+ export * from "./static-css-file";
9
+ export * from "./static-html-file";
5
10
  export * from "./static-json-file";
6
11
  export * from "./static-text-file";
7
12
  export * from "./static-typescript-file";
13
+ export * from "./static-vue-file";
8
14
  export * from "./static-yaml-file";
@@ -0,0 +1,34 @@
1
+ import { File } from "./file";
2
+ export type StaticAstroFile = File;
3
+ /**
4
+ * Creates a static Astro component file
5
+ *
6
+ * @example
7
+ * // Create an Astro component file with content
8
+ * const header = await StaticAstroFile("Header.astro",
9
+ * `---
10
+ * import Logo from '../components/Logo.astro';
11
+ * const navItems = ['Home', 'About', 'Contact'];
12
+ * ---
13
+ *
14
+ * <header class="header">
15
+ * <Logo />
16
+ * <nav>
17
+ * <ul>
18
+ * {navItems.map(item => (
19
+ * <li><a href={`/${item.toLowerCase()}`}>{item}</a></li>
20
+ * ))}
21
+ * </ul>
22
+ * </nav>
23
+ * </header>
24
+ *
25
+ * <style>
26
+ * .header {
27
+ * display: flex;
28
+ * justify-content: space-between;
29
+ * padding: 1rem;
30
+ * }
31
+ * </style>`
32
+ * );
33
+ */
34
+ export declare function StaticAstroFile(id: string, ...args: [content: string] | [path: string, content: string]): Promise<StaticAstroFile>;
@@ -0,0 +1,39 @@
1
+ import { File } from "./file";
2
+ /**
3
+ * Creates a static Astro component file
4
+ *
5
+ * @example
6
+ * // Create an Astro component file with content
7
+ * const header = await StaticAstroFile("Header.astro",
8
+ * `---
9
+ * import Logo from '../components/Logo.astro';
10
+ * const navItems = ['Home', 'About', 'Contact'];
11
+ * ---
12
+ *
13
+ * <header class="header">
14
+ * <Logo />
15
+ * <nav>
16
+ * <ul>
17
+ * {navItems.map(item => (
18
+ * <li><a href={`/${item.toLowerCase()}`}>{item}</a></li>
19
+ * ))}
20
+ * </ul>
21
+ * </nav>
22
+ * </header>
23
+ *
24
+ * <style>
25
+ * .header {
26
+ * display: flex;
27
+ * justify-content: space-between;
28
+ * padding: 1rem;
29
+ * }
30
+ * </style>`
31
+ * );
32
+ */
33
+ export function StaticAstroFile(id, ...args) {
34
+ const [path, content] = args.length === 1 ? [id, args[0]] : args;
35
+ return File(id, {
36
+ path,
37
+ content,
38
+ });
39
+ }