alchemy 0.2.5 → 0.3.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.
- package/README.md +3 -3
- package/lib/ai/astro-file.d.ts +137 -0
- package/lib/ai/astro-file.js +173 -0
- package/lib/ai/client.d.ts +2 -10
- package/lib/ai/client.js +8 -24
- package/lib/ai/css-file.d.ts +131 -0
- package/lib/ai/css-file.js +158 -0
- package/lib/ai/data.d.ts +45 -5
- package/lib/ai/data.js +45 -10
- package/lib/ai/document.d.ts +44 -20
- package/lib/ai/document.js +95 -28
- package/lib/ai/html-file.d.ts +128 -0
- package/lib/ai/html-file.js +155 -0
- package/lib/ai/index.d.ts +7 -0
- package/lib/ai/index.js +7 -0
- package/lib/ai/json-file.d.ts +160 -0
- package/lib/ai/json-file.js +208 -0
- package/lib/ai/typescript-file.d.ts +140 -0
- package/lib/ai/typescript-file.js +176 -0
- package/lib/ai/vue-file.d.ts +126 -0
- package/lib/ai/vue-file.js +153 -0
- package/lib/ai/yaml-file.d.ts +169 -0
- package/lib/ai/yaml-file.js +236 -0
- package/lib/cloudflare/dns.d.ts +88 -0
- package/lib/cloudflare/dns.js +192 -0
- package/lib/cloudflare/index.d.ts +2 -1
- package/lib/cloudflare/index.js +2 -1
- package/lib/cloudflare/r2-rest-state-store.d.ts +125 -0
- package/lib/cloudflare/r2-rest-state-store.js +254 -0
- package/lib/cloudflare/response.d.ts +12 -0
- package/lib/cloudflare/response.js +0 -0
- package/lib/cloudflare/zone.d.ts +18 -0
- package/lib/cloudflare/zone.js +17 -4
- package/lib/dns/godaddy.d.ts +7 -0
- package/lib/dns/godaddy.js +21 -0
- package/lib/dns/import-dns.d.ts +82 -0
- package/lib/dns/import-dns.js +114 -0
- package/lib/dns/index.d.ts +2 -0
- package/lib/dns/index.js +2 -0
- package/lib/dns/record.d.ts +88 -0
- package/lib/dns/record.js +13 -0
- package/lib/fs/file-system-state-store.d.ts +17 -0
- package/lib/fs/file-system-state-store.js +87 -0
- package/lib/fs/folder.d.ts +10 -0
- package/lib/fs/folder.js +2 -2
- package/lib/fs/index.d.ts +4 -4
- package/lib/fs/index.js +4 -4
- package/lib/fs/{json-file.d.ts → static-json-file.d.ts} +2 -2
- package/lib/fs/static-json-file.js +14 -0
- package/lib/fs/{text-file.d.ts → static-text-file.d.ts} +2 -2
- package/lib/fs/{text-file.js → static-text-file.js} +1 -1
- package/lib/fs/{typescript-file.d.ts → static-typescript-file.d.ts} +2 -2
- package/lib/fs/{typescript-file.js → static-typescript-file.js} +1 -1
- package/lib/fs/{yaml-file.d.ts → static-yaml-file.d.ts} +2 -2
- package/lib/fs/{yaml-file.js → static-yaml-file.js} +1 -1
- package/lib/internal/getting-started.d.ts +10 -0
- package/lib/internal/getting-started.js +94 -0
- package/lib/internal/project.d.ts +13 -0
- package/lib/internal/project.js +101 -0
- package/lib/internal/providers.d.ts +23 -0
- package/lib/internal/providers.js +148 -0
- package/lib/resource.js +8 -1
- package/lib/scope.d.ts +1 -1
- package/lib/scope.js +4 -2
- package/lib/state.d.ts +1 -16
- package/lib/state.js +0 -87
- package/lib/test/bun.d.ts +11 -0
- package/lib/test/bun.js +36 -8
- package/lib/web/astro.d.ts +147 -0
- package/lib/web/astro.js +414 -0
- package/lib/{shadcn/component.js → web/shadcn-component.js} +2 -9
- package/lib/web/shadcn.d.ts +97 -0
- package/lib/web/shadcn.js +99 -0
- package/lib/web/tailwind.d.ts +64 -0
- package/lib/web/tailwind.js +97 -0
- package/lib/{vite → web}/vite.d.ts +5 -0
- package/lib/{vite → web}/vite.js +44 -44
- package/lib/web/vitepress/custom-theme.d.ts +173 -0
- package/lib/web/vitepress/custom-theme.js +336 -0
- package/lib/{vitepress → web/vitepress}/dependencies.d.ts +2 -2
- package/lib/{vitepress → web/vitepress}/dependencies.js +1 -1
- package/lib/web/vitepress/home-page.d.ts +244 -0
- package/lib/web/vitepress/home-page.js +134 -0
- package/lib/{vitepress → web/vitepress}/index.d.ts +1 -0
- package/lib/{vitepress → web/vitepress}/index.js +1 -0
- package/lib/{vitepress → web/vitepress}/vitepress.d.ts +10 -8
- package/lib/{vitepress → web/vitepress}/vitepress.js +11 -12
- package/package.json +6 -2
- package/src/ai/astro-file.ts +288 -0
- package/src/ai/client.ts +8 -28
- package/src/ai/css-file.ts +266 -0
- package/src/ai/data.ts +53 -16
- package/src/ai/document.ts +130 -33
- package/src/ai/html-file.ts +263 -0
- package/src/ai/index.ts +7 -0
- package/src/ai/json-file.ts +349 -0
- package/src/ai/typescript-file.ts +293 -0
- package/src/ai/vue-file.ts +261 -0
- package/src/ai/yaml-file.ts +368 -0
- package/src/apply.ts +0 -1
- package/src/cloudflare/dns.ts +344 -0
- package/src/cloudflare/index.ts +2 -1
- package/src/cloudflare/r2-rest-state-store.ts +353 -0
- package/src/cloudflare/response.ts +9 -0
- package/src/cloudflare/zone.ts +39 -23
- package/src/dns/godaddy.ts +29 -0
- package/src/dns/import-dns.ts +213 -0
- package/src/dns/index.ts +2 -0
- package/src/dns/record.ts +121 -0
- package/src/fs/file-system-state-store.ts +109 -0
- package/src/fs/folder.ts +14 -2
- package/src/fs/index.ts +4 -4
- package/src/fs/{json-file.ts → static-json-file.ts} +13 -3
- package/src/fs/{text-file.ts → static-text-file.ts} +5 -2
- package/src/fs/{typescript-file.ts → static-typescript-file.ts} +3 -3
- package/src/fs/{yaml-file.ts → static-yaml-file.ts} +5 -2
- package/src/internal/getting-started.ts +107 -0
- package/src/internal/project.ts +119 -0
- package/src/internal/providers.ts +209 -0
- package/src/resource.ts +10 -1
- package/src/scope.ts +5 -6
- package/src/state.ts +1 -111
- package/src/test/bun.ts +60 -10
- package/src/web/astro.ts +644 -0
- package/src/{shadcn/component.ts → web/shadcn-component.ts} +5 -13
- package/src/web/shadcn.ts +219 -0
- package/src/web/tailwind.ts +167 -0
- package/src/{vite → web}/vite.ts +54 -56
- package/src/web/vitepress/custom-theme.ts +514 -0
- package/src/{vitepress → web/vitepress}/dependencies.ts +2 -2
- package/src/web/vitepress/home-page.ts +363 -0
- package/src/{vitepress → web/vitepress}/index.ts +1 -0
- package/src/{vitepress → web/vitepress}/vitepress.ts +34 -27
- package/lib/cloudflare/state.d.ts +0 -82
- package/lib/cloudflare/state.js +0 -108
- package/lib/fs/json-file.js +0 -7
- package/lib/internal/docs.d.ts +0 -5
- package/lib/internal/docs.js +0 -287
- package/lib/shadcn/index.d.ts +0 -1
- package/lib/shadcn/index.js +0 -1
- package/lib/vite/index.d.ts +0 -1
- package/lib/vite/index.js +0 -1
- package/lib/vitepress/home-page.d.ts +0 -133
- package/lib/vitepress/home-page.js +0 -11
- package/src/cloudflare/state.ts +0 -138
- package/src/internal/docs.ts +0 -338
- package/src/shadcn/index.ts +0 -1
- package/src/vite/index.ts +0 -1
- package/src/vitepress/home-page.ts +0 -166
- /package/lib/{shadcn/component.d.ts → web/shadcn-component.d.ts} +0 -0
- /package/src/{vitepress → web/vitepress}/index.md +0 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { generateObject, generateText } from "ai";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Resource } from "../resource";
|
|
5
|
+
import { ignore } from "../util/ignore";
|
|
6
|
+
import { ark } from "./ark";
|
|
7
|
+
import { createModel } from "./client";
|
|
8
|
+
/**
|
|
9
|
+
* Default system prompt for YAML file generation without schema
|
|
10
|
+
*/
|
|
11
|
+
const DEFAULT_YAML_SYSTEM_PROMPT = "You are a YAML generator. Create valid YAML based on the user's requirements. Your response MUST include only YAML inside ```yaml fences. Do not include any other text, explanations, or multiple code blocks. Use standard YAML syntax with proper indentation. Use quotes around strings that contain special characters when necessary.";
|
|
12
|
+
/**
|
|
13
|
+
* Resource for generating YAML files using AI models.
|
|
14
|
+
* Can operate in two modes:
|
|
15
|
+
* 1. With schema: Uses generateObject with type validation, then converts to YAML
|
|
16
|
+
* 2. Without schema: Extracts YAML from between ```yaml fences
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // Generate a serverless configuration file
|
|
20
|
+
* const serverlessConfig = await YAMLFile("serverless-config", {
|
|
21
|
+
* path: "./serverless.yml",
|
|
22
|
+
* prompt: await alchemy`
|
|
23
|
+
* Generate a serverless.yml configuration for an AWS Lambda API with:
|
|
24
|
+
* - A service name "user-api"
|
|
25
|
+
* - Node.js 16.x runtime
|
|
26
|
+
* - Three functions: createUser, getUser, and listUsers
|
|
27
|
+
* - API Gateway endpoints for each function
|
|
28
|
+
* - DynamoDB table for users
|
|
29
|
+
* - IAM permissions for DynamoDB access
|
|
30
|
+
* `,
|
|
31
|
+
* model: {
|
|
32
|
+
* id: "gpt-4o",
|
|
33
|
+
* provider: "openai"
|
|
34
|
+
* }
|
|
35
|
+
* });
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* // Generate YAML with schema validation
|
|
39
|
+
* import { type } from "arktype";
|
|
40
|
+
*
|
|
41
|
+
* const k8sConfigSchema = type({
|
|
42
|
+
* apiVersion: "string",
|
|
43
|
+
* kind: "string",
|
|
44
|
+
* metadata: {
|
|
45
|
+
* name: "string",
|
|
46
|
+
* namespace: "string?",
|
|
47
|
+
* labels: "Record<string, string>?"
|
|
48
|
+
* },
|
|
49
|
+
* spec: {
|
|
50
|
+
* replicas: "number",
|
|
51
|
+
* selector: {
|
|
52
|
+
* matchLabels: "Record<string, string>"
|
|
53
|
+
* },
|
|
54
|
+
* template: {
|
|
55
|
+
* metadata: {
|
|
56
|
+
* labels: "Record<string, string>"
|
|
57
|
+
* },
|
|
58
|
+
* spec: {
|
|
59
|
+
* containers: [{
|
|
60
|
+
* name: "string",
|
|
61
|
+
* image: "string",
|
|
62
|
+
* ports: [{
|
|
63
|
+
* containerPort: "number"
|
|
64
|
+
* }]
|
|
65
|
+
* }]
|
|
66
|
+
* }
|
|
67
|
+
* }
|
|
68
|
+
* }
|
|
69
|
+
* });
|
|
70
|
+
*
|
|
71
|
+
* const deployment = await YAMLFile("k8s-deployment", {
|
|
72
|
+
* path: "./kubernetes/deployment.yaml",
|
|
73
|
+
* schema: k8sConfigSchema,
|
|
74
|
+
* prompt: "Generate a Kubernetes deployment for a web application named 'frontend' with 3 replicas using the nginx:latest image and exposing port 80",
|
|
75
|
+
* temperature: 0.2
|
|
76
|
+
* });
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* // Generate GitHub Actions workflow with custom system prompt
|
|
80
|
+
* const workflow = await YAMLFile("github-workflow", {
|
|
81
|
+
* path: "./.github/workflows/ci.yml",
|
|
82
|
+
* prompt: await alchemy`
|
|
83
|
+
* Create a GitHub Actions workflow for a Node.js project that:
|
|
84
|
+
* - Runs on push to main and pull requests
|
|
85
|
+
* - Sets up Node.js 18
|
|
86
|
+
* - Installs dependencies with npm
|
|
87
|
+
* - Runs linting and tests
|
|
88
|
+
* - Builds the project
|
|
89
|
+
* - Deploys to GitHub Pages on success (main branch only)
|
|
90
|
+
* `,
|
|
91
|
+
* system: "You are a DevOps expert specializing in GitHub Actions workflows. Create a single YAML file inside ```yaml fences with no additional text. Follow GitHub Actions best practices and use proper YAML syntax.",
|
|
92
|
+
* model: {
|
|
93
|
+
* id: "claude-3-opus-20240229",
|
|
94
|
+
* provider: "anthropic"
|
|
95
|
+
* }
|
|
96
|
+
* });
|
|
97
|
+
*/
|
|
98
|
+
export const YAMLFile = Resource("ai::YAMLFile", async function (id, props) {
|
|
99
|
+
// Ensure directory exists
|
|
100
|
+
await fs.mkdir(path.dirname(props.path), { recursive: true });
|
|
101
|
+
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
|
+
return this.destroy();
|
|
112
|
+
}
|
|
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
|
+
let yamlContent;
|
|
123
|
+
let yamlObject;
|
|
124
|
+
// Check if schema is provided
|
|
125
|
+
if (props.schema) {
|
|
126
|
+
// Use schema-based generation
|
|
127
|
+
const { object } = await generateObject({
|
|
128
|
+
model: createModel(props),
|
|
129
|
+
schema: ark.schema(props.schema),
|
|
130
|
+
providerOptions: props.model?.options,
|
|
131
|
+
system: props.system ||
|
|
132
|
+
"Generate a valid object based on the provided requirements.",
|
|
133
|
+
prompt: props.prompt,
|
|
134
|
+
...(props.temperature === undefined
|
|
135
|
+
? {}
|
|
136
|
+
: { temperature: props.temperature }),
|
|
137
|
+
});
|
|
138
|
+
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
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
// Use fence-based extraction
|
|
151
|
+
// Use provided system prompt or default
|
|
152
|
+
const system = props.system || DEFAULT_YAML_SYSTEM_PROMPT;
|
|
153
|
+
// Generate initial content
|
|
154
|
+
const { text } = await generateText({
|
|
155
|
+
model: createModel(props),
|
|
156
|
+
prompt: props.prompt,
|
|
157
|
+
system,
|
|
158
|
+
providerOptions: props.model?.options,
|
|
159
|
+
...(props.temperature === undefined
|
|
160
|
+
? {}
|
|
161
|
+
: { temperature: props.temperature }),
|
|
162
|
+
});
|
|
163
|
+
// Extract and validate YAML content
|
|
164
|
+
let { content, error } = await extractYAMLContent(text);
|
|
165
|
+
// Re-prompt if there are validation errors
|
|
166
|
+
if (error) {
|
|
167
|
+
const errorSystem = `${system}\n\nERROR: ${error}\n\nPlease try again and ensure your response contains exactly one YAML block inside \`\`\`yaml fences.`;
|
|
168
|
+
const { text: retryText } = await generateText({
|
|
169
|
+
model: createModel(props),
|
|
170
|
+
prompt: props.prompt,
|
|
171
|
+
system: errorSystem,
|
|
172
|
+
providerOptions: props.model?.options,
|
|
173
|
+
...(props.temperature === undefined
|
|
174
|
+
? {}
|
|
175
|
+
: { temperature: props.temperature }),
|
|
176
|
+
});
|
|
177
|
+
const retryResult = await extractYAMLContent(retryText);
|
|
178
|
+
if (retryResult.error) {
|
|
179
|
+
throw new Error(`Failed to generate valid YAML: ${retryResult.error}`);
|
|
180
|
+
}
|
|
181
|
+
content = retryResult.content;
|
|
182
|
+
}
|
|
183
|
+
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));
|
|
194
|
+
}
|
|
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
|
+
// Return the resource
|
|
200
|
+
return this({
|
|
201
|
+
...props,
|
|
202
|
+
schema: props.schema,
|
|
203
|
+
content: yamlContent,
|
|
204
|
+
yaml: yamlObject,
|
|
205
|
+
createdAt: stats.birthtimeMs,
|
|
206
|
+
updatedAt: stats.mtimeMs,
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
/**
|
|
210
|
+
* Extracts YAML content from between ```yaml fences
|
|
211
|
+
* Validates that exactly one YAML code block exists
|
|
212
|
+
*
|
|
213
|
+
* @param text The text to extract YAML from
|
|
214
|
+
* @returns The extracted YAML or error message
|
|
215
|
+
*/
|
|
216
|
+
async function extractYAMLContent(text) {
|
|
217
|
+
// Check for yaml or yml fence blocks
|
|
218
|
+
const yamlCodeRegex = /```(yaml|yml)\s*([\s\S]*?)```/g;
|
|
219
|
+
const matches = Array.from(text.matchAll(yamlCodeRegex));
|
|
220
|
+
if (matches.length === 0) {
|
|
221
|
+
return {
|
|
222
|
+
content: "",
|
|
223
|
+
error: "No YAML code block found in the response. Please include your YAML within ```yaml fences.",
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (matches.length > 1) {
|
|
227
|
+
return {
|
|
228
|
+
content: "",
|
|
229
|
+
error: "Multiple YAML code blocks found in the response. Please provide exactly one YAML block within ```yaml fences.",
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
const content = matches[0][2].trim();
|
|
233
|
+
// We don't validate YAML parsing here because js-yaml might not be available
|
|
234
|
+
// Validation will happen at usage time if needed
|
|
235
|
+
return { content };
|
|
236
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Context } from "../context";
|
|
2
|
+
import type { DnsRecord as BaseDnsRecord, DnsRecordType, DnsRecordWithMetadata } from "../dns/record";
|
|
3
|
+
import { Resource } from "../resource";
|
|
4
|
+
/**
|
|
5
|
+
* Properties for a DNS record
|
|
6
|
+
*/
|
|
7
|
+
export interface DnsRecordProps extends Omit<BaseDnsRecord, "type"> {
|
|
8
|
+
/**
|
|
9
|
+
* Record type (A, AAAA, CNAME, etc.)
|
|
10
|
+
*/
|
|
11
|
+
type: DnsRecordType;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Output returned after DNS record creation/update
|
|
15
|
+
*/
|
|
16
|
+
export interface DnsRecord extends DnsRecordWithMetadata {
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Properties for managing multiple DNS records
|
|
20
|
+
*/
|
|
21
|
+
export interface DnsRecordsProps {
|
|
22
|
+
/**
|
|
23
|
+
* Zone ID or domain name where records will be created
|
|
24
|
+
*/
|
|
25
|
+
zoneId: string;
|
|
26
|
+
/**
|
|
27
|
+
* Array of DNS records to manage
|
|
28
|
+
*/
|
|
29
|
+
records: DnsRecordProps[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Output returned after DNS records creation/update
|
|
33
|
+
*/
|
|
34
|
+
export interface DnsRecords extends Resource<"cloudflare::DnsRecords"> {
|
|
35
|
+
/**
|
|
36
|
+
* Zone ID where records are created
|
|
37
|
+
*/
|
|
38
|
+
zoneId: string;
|
|
39
|
+
/**
|
|
40
|
+
* Array of created/updated DNS records
|
|
41
|
+
*/
|
|
42
|
+
records: DnsRecord[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Manages a batch of DNS records in a Cloudflare zone.
|
|
46
|
+
* Supports creating, updating, and deleting multiple records at once.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* // Create multiple A and CNAME records
|
|
50
|
+
* const dnsRecords = await DnsRecords("example.com-dns", {
|
|
51
|
+
* zone: "example.com",
|
|
52
|
+
* records: [
|
|
53
|
+
* {
|
|
54
|
+
* name: "www.example.com",
|
|
55
|
+
* type: "A",
|
|
56
|
+
* content: "192.0.2.1",
|
|
57
|
+
* proxied: true
|
|
58
|
+
* },
|
|
59
|
+
* {
|
|
60
|
+
* name: "blog.example.com",
|
|
61
|
+
* type: "CNAME",
|
|
62
|
+
* content: "www.example.com",
|
|
63
|
+
* proxied: true
|
|
64
|
+
* }
|
|
65
|
+
* ]
|
|
66
|
+
* });
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* // Create MX records for email routing
|
|
70
|
+
* const emailRecords = await DnsRecords("example.com-email", {
|
|
71
|
+
* zone: "example.com",
|
|
72
|
+
* records: [
|
|
73
|
+
* {
|
|
74
|
+
* name: "example.com",
|
|
75
|
+
* type: "MX",
|
|
76
|
+
* content: "aspmx.l.google.com",
|
|
77
|
+
* priority: 1
|
|
78
|
+
* },
|
|
79
|
+
* {
|
|
80
|
+
* name: "example.com",
|
|
81
|
+
* type: "MX",
|
|
82
|
+
* content: "alt1.aspmx.l.google.com",
|
|
83
|
+
* priority: 5
|
|
84
|
+
* }
|
|
85
|
+
* ]
|
|
86
|
+
* });
|
|
87
|
+
*/
|
|
88
|
+
export declare const DnsRecords: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<DnsRecords>, id: string, props: DnsRecordsProps) => Promise<DnsRecords>);
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { Resource } from "../resource";
|
|
2
|
+
import { createCloudflareApi } from "./api";
|
|
3
|
+
/**
|
|
4
|
+
* Manages a batch of DNS records in a Cloudflare zone.
|
|
5
|
+
* Supports creating, updating, and deleting multiple records at once.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // Create multiple A and CNAME records
|
|
9
|
+
* const dnsRecords = await DnsRecords("example.com-dns", {
|
|
10
|
+
* zone: "example.com",
|
|
11
|
+
* records: [
|
|
12
|
+
* {
|
|
13
|
+
* name: "www.example.com",
|
|
14
|
+
* type: "A",
|
|
15
|
+
* content: "192.0.2.1",
|
|
16
|
+
* proxied: true
|
|
17
|
+
* },
|
|
18
|
+
* {
|
|
19
|
+
* name: "blog.example.com",
|
|
20
|
+
* type: "CNAME",
|
|
21
|
+
* content: "www.example.com",
|
|
22
|
+
* proxied: true
|
|
23
|
+
* }
|
|
24
|
+
* ]
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* // Create MX records for email routing
|
|
29
|
+
* const emailRecords = await DnsRecords("example.com-email", {
|
|
30
|
+
* zone: "example.com",
|
|
31
|
+
* records: [
|
|
32
|
+
* {
|
|
33
|
+
* name: "example.com",
|
|
34
|
+
* type: "MX",
|
|
35
|
+
* content: "aspmx.l.google.com",
|
|
36
|
+
* priority: 1
|
|
37
|
+
* },
|
|
38
|
+
* {
|
|
39
|
+
* name: "example.com",
|
|
40
|
+
* type: "MX",
|
|
41
|
+
* content: "alt1.aspmx.l.google.com",
|
|
42
|
+
* priority: 5
|
|
43
|
+
* }
|
|
44
|
+
* ]
|
|
45
|
+
* });
|
|
46
|
+
*/
|
|
47
|
+
export const DnsRecords = Resource("cloudflare::DnsRecords", async function (id, props) {
|
|
48
|
+
// Create Cloudflare API client
|
|
49
|
+
const api = await createCloudflareApi();
|
|
50
|
+
// Get zone ID if domain name was provided
|
|
51
|
+
const zoneId = props.zoneId;
|
|
52
|
+
if (this.phase === "delete") {
|
|
53
|
+
if (this.output?.records) {
|
|
54
|
+
// Delete all existing records
|
|
55
|
+
await Promise.all(this.output.records.map(async (record) => {
|
|
56
|
+
try {
|
|
57
|
+
const response = await api.delete(`/zones/${zoneId}/dns_records/${record.id}`);
|
|
58
|
+
if (!response.ok && response.status !== 404) {
|
|
59
|
+
console.error(`Failed to delete DNS record ${record.name}: ${response.statusText}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
console.error(`Error deleting DNS record ${record.name}:`, error);
|
|
64
|
+
}
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
return this.destroy();
|
|
68
|
+
}
|
|
69
|
+
if (this.phase === "update" && this.output?.records) {
|
|
70
|
+
// Get current records to compare with desired state
|
|
71
|
+
const currentRecords = this.output.records;
|
|
72
|
+
const desiredRecords = props.records;
|
|
73
|
+
// Find records to delete (exist in current but not in desired)
|
|
74
|
+
const recordsToDelete = currentRecords.filter((current) => !desiredRecords.some((desired) => desired.name === current.name && desired.type === current.type));
|
|
75
|
+
// Delete orphaned records
|
|
76
|
+
await Promise.all(recordsToDelete.map(async (record) => {
|
|
77
|
+
try {
|
|
78
|
+
const response = await api.delete(`/zones/${zoneId}/dns_records/${record.id}`);
|
|
79
|
+
if (!response.ok && response.status !== 404) {
|
|
80
|
+
console.error(`Failed to delete DNS record ${record.name}: ${response.statusText}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
console.error(`Error deleting DNS record ${record.name}:`, error);
|
|
85
|
+
}
|
|
86
|
+
}));
|
|
87
|
+
// Update or create records
|
|
88
|
+
const updatedRecords = await Promise.all(desiredRecords.map(async (desired) => {
|
|
89
|
+
// Find matching existing record
|
|
90
|
+
const existing = currentRecords.find((current) => current.name === desired.name && current.type === desired.type);
|
|
91
|
+
if (existing) {
|
|
92
|
+
// Update if content or other properties changed
|
|
93
|
+
if (existing.content !== desired.content ||
|
|
94
|
+
existing.ttl !== (desired.ttl || 1) ||
|
|
95
|
+
existing.proxied !== (desired.proxied || false) ||
|
|
96
|
+
existing.priority !== desired.priority ||
|
|
97
|
+
existing.comment !== desired.comment) {
|
|
98
|
+
return createOrUpdateRecord(api, zoneId, desired, existing.id);
|
|
99
|
+
}
|
|
100
|
+
return existing;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// Create new record
|
|
104
|
+
return createOrUpdateRecord(api, zoneId, desired);
|
|
105
|
+
}
|
|
106
|
+
}));
|
|
107
|
+
return this({
|
|
108
|
+
zoneId,
|
|
109
|
+
records: updatedRecords,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
// Create new records
|
|
113
|
+
const uniqueRecords = props.records.reduce((acc, record) => {
|
|
114
|
+
const key = `${record.name}-${record.type}`;
|
|
115
|
+
acc[key] = record;
|
|
116
|
+
return acc;
|
|
117
|
+
}, {});
|
|
118
|
+
const createdRecords = await Promise.all(Object.values(uniqueRecords).map(async (record) => {
|
|
119
|
+
// First check if record exists
|
|
120
|
+
const listResponse = await api.get(`/zones/${zoneId}/dns_records?type=${record.type}&name=${record.name}`);
|
|
121
|
+
if (!listResponse.ok) {
|
|
122
|
+
throw new Error(`Failed to check existing DNS records: ${listResponse.statusText}`);
|
|
123
|
+
}
|
|
124
|
+
const listResult = (await listResponse.json());
|
|
125
|
+
const existingRecord = listResult.result[0];
|
|
126
|
+
return createOrUpdateRecord(api, zoneId, record, existingRecord?.id);
|
|
127
|
+
}));
|
|
128
|
+
return this({
|
|
129
|
+
zoneId,
|
|
130
|
+
records: createdRecords,
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
/**
|
|
134
|
+
* Create or update a DNS record
|
|
135
|
+
*/
|
|
136
|
+
async function createOrUpdateRecord(api, zoneId, record, existingId) {
|
|
137
|
+
const payload = getRecordPayload(record);
|
|
138
|
+
const response = await (existingId
|
|
139
|
+
? api.put(`/zones/${zoneId}/dns_records/${existingId}`, payload)
|
|
140
|
+
: api.post(`/zones/${zoneId}/dns_records`, payload));
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
const errorBody = await response.text();
|
|
143
|
+
// If it's an update operation and the record doesn't exist, fall back to creation
|
|
144
|
+
if (existingId && response.status === 404) {
|
|
145
|
+
try {
|
|
146
|
+
const createResponse = await api.post(`/zones/${zoneId}/dns_records`, payload);
|
|
147
|
+
if (createResponse.ok) {
|
|
148
|
+
return convertCloudflareRecord((await createResponse.json()).result, zoneId);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
// Fall through to the original error
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
throw new Error(`Failed to ${existingId ? "update" : "create"} DNS record ${record.name}: ${response.statusText}\nResponse: ${errorBody}`);
|
|
156
|
+
}
|
|
157
|
+
const result = (await response.json());
|
|
158
|
+
return convertCloudflareRecord(result.result, zoneId);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Get the record payload for create/update operations
|
|
162
|
+
*/
|
|
163
|
+
function getRecordPayload(record) {
|
|
164
|
+
return {
|
|
165
|
+
type: record.type,
|
|
166
|
+
name: record.name,
|
|
167
|
+
content: record.content,
|
|
168
|
+
ttl: record.ttl || 1,
|
|
169
|
+
proxied: record.proxied || false,
|
|
170
|
+
priority: record.priority,
|
|
171
|
+
comment: record.comment,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Convert a Cloudflare DNS record response to our DnsRecord type
|
|
176
|
+
*/
|
|
177
|
+
function convertCloudflareRecord(record, zoneId) {
|
|
178
|
+
return {
|
|
179
|
+
id: record.id,
|
|
180
|
+
name: record.name,
|
|
181
|
+
type: record.type,
|
|
182
|
+
content: record.content,
|
|
183
|
+
ttl: record.ttl,
|
|
184
|
+
proxied: record.proxied,
|
|
185
|
+
priority: record.priority,
|
|
186
|
+
comment: record.comment,
|
|
187
|
+
tags: record.tags,
|
|
188
|
+
createdAt: new Date(record.created_on).getTime(),
|
|
189
|
+
modifiedAt: new Date(record.modified_on).getTime(),
|
|
190
|
+
zoneId,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export * from "./bindings";
|
|
2
2
|
export * from "./bucket";
|
|
3
|
+
export * from "./dns";
|
|
3
4
|
export * from "./durable-object-namespace";
|
|
4
5
|
export * from "./kv-namespace";
|
|
5
|
-
export * from "./state";
|
|
6
|
+
export * from "./r2-rest-state-store";
|
|
6
7
|
export * from "./static-site";
|
|
7
8
|
export * from "./worker";
|
|
8
9
|
export * from "./wrangler.json";
|
package/lib/cloudflare/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export * from "./bindings";
|
|
2
2
|
export * from "./bucket";
|
|
3
|
+
export * from "./dns";
|
|
3
4
|
export * from "./durable-object-namespace";
|
|
4
5
|
export * from "./kv-namespace";
|
|
5
|
-
export * from "./state";
|
|
6
|
+
export * from "./r2-rest-state-store";
|
|
6
7
|
export * from "./static-site";
|
|
7
8
|
export * from "./worker";
|
|
8
9
|
export * from "./wrangler.json";
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { Scope } from "../scope";
|
|
2
|
+
import type { State, StateStore } from "../state";
|
|
3
|
+
/**
|
|
4
|
+
* Options for CloudflareR2StateStore
|
|
5
|
+
*/
|
|
6
|
+
export interface CloudflareR2StateStoreOptions {
|
|
7
|
+
/**
|
|
8
|
+
* The prefix to use for object keys in the R2 bucket
|
|
9
|
+
* This allows multiple state stores to use the same R2 bucket
|
|
10
|
+
*/
|
|
11
|
+
prefix?: string;
|
|
12
|
+
/**
|
|
13
|
+
* The R2 bucket name to use
|
|
14
|
+
* Required - the bucket must already exist
|
|
15
|
+
*/
|
|
16
|
+
bucketName: string;
|
|
17
|
+
/**
|
|
18
|
+
* API key to use (overrides CLOUDFLARE_API_KEY env var)
|
|
19
|
+
*/
|
|
20
|
+
apiKey?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Account ID to use (overrides CLOUDFLARE_ACCOUNT_ID env var)
|
|
23
|
+
*/
|
|
24
|
+
accountId?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Email to use with API Key authentication (overrides CLOUDFLARE_EMAIL env var)
|
|
27
|
+
*/
|
|
28
|
+
email?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* State store implementation using Cloudflare R2 API
|
|
32
|
+
* Uses R2 for immediate consistency compared to KV's eventual consistency
|
|
33
|
+
*/
|
|
34
|
+
export declare class R2RestStateStore implements StateStore {
|
|
35
|
+
readonly scope: Scope;
|
|
36
|
+
private api;
|
|
37
|
+
private prefix;
|
|
38
|
+
private bucketName;
|
|
39
|
+
private apiKey;
|
|
40
|
+
private accountId;
|
|
41
|
+
private email;
|
|
42
|
+
private initialized;
|
|
43
|
+
/**
|
|
44
|
+
* Create a new CloudflareR2StateStore
|
|
45
|
+
*
|
|
46
|
+
* @param scope The scope this store belongs to
|
|
47
|
+
* @param options Options for the state store
|
|
48
|
+
*/
|
|
49
|
+
constructor(scope: Scope, options: CloudflareR2StateStoreOptions);
|
|
50
|
+
/**
|
|
51
|
+
* Initialize the R2 client
|
|
52
|
+
*/
|
|
53
|
+
init(): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* R2 buckets cannot be deleted programmatically via this method
|
|
56
|
+
*/
|
|
57
|
+
deinit(): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* List all resources in the state store
|
|
60
|
+
*/
|
|
61
|
+
list(): Promise<string[]>;
|
|
62
|
+
/**
|
|
63
|
+
* Count the number of items in the state store
|
|
64
|
+
*/
|
|
65
|
+
count(): Promise<number>;
|
|
66
|
+
/**
|
|
67
|
+
* Get a state by key
|
|
68
|
+
*
|
|
69
|
+
* @param key The key to look up
|
|
70
|
+
* @returns The state or undefined if not found
|
|
71
|
+
*/
|
|
72
|
+
get(key: string): Promise<State | undefined>;
|
|
73
|
+
/**
|
|
74
|
+
* Get multiple states by their keys
|
|
75
|
+
*
|
|
76
|
+
* @param ids Array of keys to fetch
|
|
77
|
+
* @returns Record mapping keys to their states
|
|
78
|
+
*/
|
|
79
|
+
getBatch(ids: string[]): Promise<Record<string, State>>;
|
|
80
|
+
/**
|
|
81
|
+
* Get all states in the store
|
|
82
|
+
*
|
|
83
|
+
* @returns Record mapping all keys to their states
|
|
84
|
+
*/
|
|
85
|
+
all(): Promise<Record<string, State>>;
|
|
86
|
+
/**
|
|
87
|
+
* Set a state for a key
|
|
88
|
+
*
|
|
89
|
+
* @param key The key to set
|
|
90
|
+
* @param value The state to store
|
|
91
|
+
*/
|
|
92
|
+
set(key: string, value: State): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* Delete a state by key
|
|
95
|
+
*
|
|
96
|
+
* @param key The key to delete
|
|
97
|
+
*/
|
|
98
|
+
delete(key: string): Promise<void>;
|
|
99
|
+
/**
|
|
100
|
+
* Convert key for storage by replacing slashes with colons
|
|
101
|
+
* since R2 treats slashes as directory separators
|
|
102
|
+
*
|
|
103
|
+
* @param key The original key
|
|
104
|
+
* @returns Key with slashes replaced by colons
|
|
105
|
+
*/
|
|
106
|
+
private convertKeyForStorage;
|
|
107
|
+
/**
|
|
108
|
+
* Convert key from storage by replacing colons with slashes
|
|
109
|
+
*
|
|
110
|
+
* @param key The storage key
|
|
111
|
+
* @returns Key with colons replaced by slashes
|
|
112
|
+
*/
|
|
113
|
+
private convertKeyFromStorage;
|
|
114
|
+
/**
|
|
115
|
+
* Get the full object key for storage
|
|
116
|
+
*
|
|
117
|
+
* @param key The original key
|
|
118
|
+
* @returns The key with prefix for use in the R2 bucket
|
|
119
|
+
*/
|
|
120
|
+
private getObjectKey;
|
|
121
|
+
/**
|
|
122
|
+
* Ensure the store is initialized before operations
|
|
123
|
+
*/
|
|
124
|
+
private ensureInitialized;
|
|
125
|
+
}
|