alchemy 0.37.1 → 0.38.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/bin/alchemy.mjs +44306 -48055
- package/bin/alchemy.ts +80 -85
- package/bin/commands/create.ts +263 -0
- package/bin/constants.ts +13 -0
- package/bin/errors.ts +11 -0
- package/bin/services/dependencies.ts +47 -0
- package/bin/services/get-package-version.ts +11 -0
- package/bin/services/package-manager.ts +113 -0
- package/bin/services/template-manager.ts +137 -0
- package/bin/types.ts +83 -0
- package/lib/alchemy.d.ts.map +1 -1
- package/lib/alchemy.js +12 -3
- package/lib/alchemy.js.map +1 -1
- package/lib/apply.d.ts +2 -0
- package/lib/apply.d.ts.map +1 -1
- package/lib/apply.js +47 -12
- package/lib/apply.js.map +1 -1
- package/lib/build-date.d.ts +6 -0
- package/lib/build-date.d.ts.map +1 -0
- package/lib/build-date.js +8 -0
- package/lib/build-date.js.map +1 -0
- package/lib/cloudflare/api.d.ts.map +1 -1
- package/lib/cloudflare/api.js +2 -1
- package/lib/cloudflare/api.js.map +1 -1
- package/lib/cloudflare/do-state-store/internal.d.ts.map +1 -1
- package/lib/cloudflare/do-state-store/internal.js +9 -1
- package/lib/cloudflare/do-state-store/internal.js.map +1 -1
- package/lib/cloudflare/pipeline.d.ts +1 -1
- package/lib/cloudflare/pipeline.d.ts.map +1 -1
- package/lib/cloudflare/pipeline.js +6 -1
- package/lib/cloudflare/pipeline.js.map +1 -1
- package/lib/cloudflare/worker-stub.d.ts +23 -2
- package/lib/cloudflare/worker-stub.d.ts.map +1 -1
- package/lib/cloudflare/worker-stub.js +16 -2
- package/lib/cloudflare/worker-stub.js.map +1 -1
- package/lib/cloudflare/worker.d.ts +4 -3
- package/lib/cloudflare/worker.d.ts.map +1 -1
- package/lib/cloudflare/worker.js +2 -1
- package/lib/cloudflare/worker.js.map +1 -1
- package/lib/context.d.ts +2 -2
- package/lib/context.d.ts.map +1 -1
- package/lib/destroy.d.ts +8 -2
- package/lib/destroy.d.ts.map +1 -1
- package/lib/destroy.js +19 -5
- package/lib/destroy.js.map +1 -1
- package/lib/scope.d.ts +26 -4
- package/lib/scope.d.ts.map +1 -1
- package/lib/scope.js +104 -9
- package/lib/scope.js.map +1 -1
- package/lib/sentry/project.js +1 -1
- package/lib/sentry/project.js.map +1 -1
- package/lib/util/exists.d.ts +2 -0
- package/lib/util/exists.d.ts.map +1 -0
- package/lib/util/exists.js +11 -0
- package/lib/util/exists.js.map +1 -0
- package/lib/util/logger.js +1 -1
- package/lib/util/logger.js.map +1 -1
- package/lib/util/mutex.d.ts +9 -0
- package/lib/util/mutex.d.ts.map +1 -0
- package/lib/util/mutex.js +35 -0
- package/lib/util/mutex.js.map +1 -0
- package/package.json +16 -6
- package/src/alchemy.ts +17 -5
- package/src/apply.ts +67 -17
- package/src/build-date.ts +8 -0
- package/src/cloudflare/api.ts +2 -1
- package/src/cloudflare/do-state-store/internal.ts +10 -1
- package/src/cloudflare/pipeline.ts +6 -0
- package/src/cloudflare/worker-stub.ts +31 -2
- package/src/cloudflare/worker.ts +4 -3
- package/src/context.ts +2 -2
- package/src/destroy.ts +30 -7
- package/src/scope.ts +145 -10
- package/src/sentry/project.ts +1 -1
- package/src/util/exists.ts +10 -0
- package/src/util/logger.ts +1 -1
- package/src/util/mutex.ts +33 -0
- package/templates/astro/.vscode/extensions.json +4 -0
- package/templates/astro/.vscode/launch.json +11 -0
- package/templates/astro/README.md +48 -0
- package/templates/astro/_env.example +1 -0
- package/templates/astro/_gitignore +27 -0
- package/templates/astro/alchemy.run.ts +16 -0
- package/templates/astro/astro.config.mjs +8 -0
- package/templates/astro/package.json +23 -0
- package/templates/astro/public/favicon.svg +9 -0
- package/templates/astro/src/assets/astro.svg +1 -0
- package/templates/astro/src/assets/background.svg +1 -0
- package/templates/astro/src/components/Welcome.astro +210 -0
- package/templates/astro/src/layouts/Layout.astro +22 -0
- package/templates/astro/src/pages/api/hello.ts +19 -0
- package/templates/astro/src/pages/index.astro +11 -0
- package/templates/astro/tsconfig.json +18 -0
- package/templates/astro/types/env.d.ts +16 -0
- package/templates/astro/wrangler.jsonc +6 -0
- package/templates/nuxt/README.md +75 -0
- package/templates/nuxt/_env +1 -0
- package/templates/nuxt/_env.example +1 -0
- package/templates/nuxt/_gitignore +27 -0
- package/templates/nuxt/alchemy.run.ts +16 -0
- package/templates/nuxt/app.vue +6 -0
- package/templates/nuxt/nuxt.config.ts +13 -0
- package/templates/nuxt/package.json +26 -0
- package/templates/nuxt/public/favicon.ico +0 -0
- package/templates/nuxt/public/robots.txt +2 -0
- package/templates/nuxt/server/api/hello.ts +7 -0
- package/templates/nuxt/server/middleware/auth.ts +5 -0
- package/templates/nuxt/server/middleware/hello.ts +5 -0
- package/templates/nuxt/server/tsconfig.json +3 -0
- package/templates/nuxt/tsconfig.json +15 -0
- package/templates/nuxt/types/env.d.ts +16 -0
- package/templates/react-router/.vscode/settings.json +5 -0
- package/templates/react-router/README.md +79 -0
- package/templates/react-router/_env +1 -0
- package/templates/react-router/_env.example +1 -0
- package/templates/react-router/_gitignore +15 -0
- package/templates/react-router/alchemy.run.ts +17 -0
- package/templates/react-router/app/app.css +15 -0
- package/templates/react-router/app/entry.server.tsx +43 -0
- package/templates/react-router/app/root.tsx +75 -0
- package/templates/react-router/app/routes/home.tsx +17 -0
- package/templates/react-router/app/routes.ts +3 -0
- package/templates/react-router/app/welcome/logo-dark.svg +23 -0
- package/templates/react-router/app/welcome/logo-light.svg +23 -0
- package/templates/react-router/app/welcome/welcome.tsx +90 -0
- package/templates/react-router/package.json +34 -0
- package/templates/react-router/public/favicon.ico +0 -0
- package/templates/react-router/react-router.config.ts +8 -0
- package/templates/react-router/tsconfig.cloudflare.json +27 -0
- package/templates/react-router/tsconfig.json +17 -0
- package/templates/react-router/tsconfig.node.json +20 -0
- package/templates/react-router/types/env.d.ts +16 -0
- package/templates/react-router/vite.config.ts +14 -0
- package/templates/react-router/workers/app.ts +23 -0
- package/templates/react-router/wrangler.jsonc +6 -0
- package/templates/rwsdk/.cursor/rules/rwsdk_rwsdk-interruptors.mdc +247 -0
- package/templates/rwsdk/.cursor/rules/rwsdk_rwsdk-middleware.mdc +75 -0
- package/templates/rwsdk/.cursor/rules/rwsdk_rwsdk-react.mdc +110 -0
- package/templates/rwsdk/.cursor/rules/rwsdk_rwsdk-request-response.mdc +213 -0
- package/templates/rwsdk/.devcontainer/Dockerfile +8 -0
- package/templates/rwsdk/.devcontainer/devcontainer.json +21 -0
- package/templates/rwsdk/README.md +62 -0
- package/templates/rwsdk/_env +5 -0
- package/templates/rwsdk/_env.example +5 -0
- package/templates/rwsdk/_gitignore +71 -0
- package/templates/rwsdk/alchemy.run.ts +29 -0
- package/templates/rwsdk/package.json +61 -0
- package/templates/rwsdk/prisma/schema.prisma +43 -0
- package/templates/rwsdk/src/app/Document.tsx +16 -0
- package/templates/rwsdk/src/app/headers.ts +32 -0
- package/templates/rwsdk/src/app/pages/Home.tsx +13 -0
- package/templates/rwsdk/src/app/pages/user/Login.tsx +79 -0
- package/templates/rwsdk/src/app/pages/user/functions.ts +171 -0
- package/templates/rwsdk/src/app/pages/user/routes.ts +17 -0
- package/templates/rwsdk/src/app/shared/links.ts +3 -0
- package/templates/rwsdk/src/client.tsx +3 -0
- package/templates/rwsdk/src/db.ts +26 -0
- package/templates/rwsdk/src/scripts/seed.ts +21 -0
- package/templates/rwsdk/src/session/durableObject.ts +63 -0
- package/templates/rwsdk/src/session/store.ts +13 -0
- package/templates/rwsdk/src/worker.tsx +63 -0
- package/templates/rwsdk/tsconfig.json +47 -0
- package/templates/rwsdk/types/env.d.ts +16 -0
- package/templates/rwsdk/types/rw.d.ts +5 -0
- package/templates/rwsdk/types/vite.d.ts +4 -0
- package/templates/rwsdk/vite.config.mts +6 -0
- package/templates/rwsdk/wrangler.jsonc +28 -0
- package/templates/sveltekit/README.md +38 -0
- package/templates/sveltekit/_env +1 -0
- package/templates/sveltekit/_env.example +1 -0
- package/templates/sveltekit/_gitignore +26 -0
- package/templates/sveltekit/_npmrc +1 -0
- package/templates/sveltekit/_prettierrc +3 -0
- package/templates/sveltekit/alchemy.run.ts +16 -0
- package/templates/sveltekit/bun.lock +1099 -0
- package/templates/sveltekit/package.json +33 -0
- package/templates/sveltekit/src/app.css +1 -0
- package/templates/sveltekit/src/app.d.ts +13 -0
- package/templates/sveltekit/src/app.html +12 -0
- package/templates/sveltekit/src/routes/+layout.svelte +7 -0
- package/templates/sveltekit/src/routes/+page.svelte +2 -0
- package/templates/sveltekit/static/favicon.png +0 -0
- package/templates/sveltekit/svelte.config.js +12 -0
- package/templates/sveltekit/tsconfig.json +27 -0
- package/templates/sveltekit/types/env.d.ts +16 -0
- package/templates/sveltekit/vite.config.ts +6 -0
- package/templates/sveltekit/wrangler.jsonc +7 -0
- package/templates/tanstack-start/.vscode/settings.json +11 -0
- package/templates/tanstack-start/README.md +72 -0
- package/templates/tanstack-start/_env +1 -0
- package/templates/tanstack-start/_env.example +1 -0
- package/templates/tanstack-start/_gitignore +22 -0
- package/templates/tanstack-start/_prettierignore +4 -0
- package/templates/tanstack-start/alchemy.run.ts +16 -0
- package/templates/tanstack-start/package.json +37 -0
- package/templates/tanstack-start/public/android-chrome-192x192.png +0 -0
- package/templates/tanstack-start/public/android-chrome-512x512.png +0 -0
- package/templates/tanstack-start/public/apple-touch-icon.png +0 -0
- package/templates/tanstack-start/public/favicon-16x16.png +0 -0
- package/templates/tanstack-start/public/favicon-32x32.png +0 -0
- package/templates/tanstack-start/public/favicon.ico +0 -0
- package/templates/tanstack-start/public/favicon.png +0 -0
- package/templates/tanstack-start/public/site.webmanifest +19 -0
- package/templates/tanstack-start/src/components/DefaultCatchBoundary.tsx +53 -0
- package/templates/tanstack-start/src/components/NotFound.tsx +25 -0
- package/templates/tanstack-start/src/components/PostError.tsx +5 -0
- package/templates/tanstack-start/src/components/UserError.tsx +5 -0
- package/templates/tanstack-start/src/router.tsx +22 -0
- package/templates/tanstack-start/src/routes/__root.tsx +146 -0
- package/templates/tanstack-start/src/routes/_pathlessLayout/_nested-layout/route-a.tsx +10 -0
- package/templates/tanstack-start/src/routes/_pathlessLayout/_nested-layout/route-b.tsx +10 -0
- package/templates/tanstack-start/src/routes/_pathlessLayout/_nested-layout.tsx +34 -0
- package/templates/tanstack-start/src/routes/_pathlessLayout.tsx +16 -0
- package/templates/tanstack-start/src/routes/api/users.$userId.ts +28 -0
- package/templates/tanstack-start/src/routes/api/users.ts +59 -0
- package/templates/tanstack-start/src/routes/customScript[.]js.ts +10 -0
- package/templates/tanstack-start/src/routes/deferred.tsx +62 -0
- package/templates/tanstack-start/src/routes/index.tsx +12 -0
- package/templates/tanstack-start/src/routes/posts.$postId.tsx +34 -0
- package/templates/tanstack-start/src/routes/posts.index.tsx +8 -0
- package/templates/tanstack-start/src/routes/posts.tsx +38 -0
- package/templates/tanstack-start/src/routes/posts_.$postId.deep.tsx +29 -0
- package/templates/tanstack-start/src/routes/redirect.tsx +9 -0
- package/templates/tanstack-start/src/routes/users.$userId.tsx +44 -0
- package/templates/tanstack-start/src/routes/users.index.tsx +18 -0
- package/templates/tanstack-start/src/routes/users.tsx +49 -0
- package/templates/tanstack-start/src/styles/app.css +12 -0
- package/templates/tanstack-start/src/utils/loggingMiddleware.tsx +41 -0
- package/templates/tanstack-start/src/utils/posts.tsx +40 -0
- package/templates/tanstack-start/src/utils/seo.ts +33 -0
- package/templates/tanstack-start/src/utils/users.tsx +5 -0
- package/templates/tanstack-start/tsconfig.json +31 -0
- package/templates/tanstack-start/types/env.d.ts +16 -0
- package/templates/tanstack-start/vite.config.ts +33 -0
- package/templates/tanstack-start/wrangler.jsonc +7 -0
- package/templates/typescript/README.md +15 -0
- package/templates/typescript/_gitignore +37 -0
- package/templates/typescript/alchemy.run.ts +15 -0
- package/templates/typescript/index.ts +1 -0
- package/templates/typescript/package.json +18 -0
- package/templates/typescript/src/worker.js +5 -0
- package/templates/typescript/src/worker.ts +7 -0
- package/templates/typescript/tsconfig.json +21 -0
- package/templates/typescript/types/env.d.ts +16 -0
- package/templates/vite/README.md +54 -0
- package/templates/vite/_env.example +1 -0
- package/templates/vite/_gitignore +27 -0
- package/templates/vite/alchemy.run.ts +17 -0
- package/templates/vite/eslint.config.js +28 -0
- package/templates/vite/index.html +13 -0
- package/templates/vite/package.json +35 -0
- package/templates/vite/public/vite.svg +1 -0
- package/templates/vite/src/App.css +42 -0
- package/templates/vite/src/App.tsx +35 -0
- package/templates/vite/src/assets/react.svg +1 -0
- package/templates/vite/src/index.css +68 -0
- package/templates/vite/src/main.tsx +10 -0
- package/templates/vite/src/vite-env.d.ts +1 -0
- package/templates/vite/tsconfig.json +34 -0
- package/templates/vite/types/env.d.ts +16 -0
- package/templates/vite/vite.config.ts +9 -0
- package/templates/vite/worker/index.ts +12 -0
- package/templates/vite/wrangler.jsonc +6 -0
- package/bin/create-alchemy.ts +0 -1220
package/src/cloudflare/api.ts
CHANGED
|
@@ -196,7 +196,8 @@ export class CloudflareApi {
|
|
|
196
196
|
(error) =>
|
|
197
197
|
error instanceof InternalError ||
|
|
198
198
|
error instanceof TooManyRequestsError ||
|
|
199
|
-
error instanceof ForbiddenError
|
|
199
|
+
error instanceof ForbiddenError ||
|
|
200
|
+
error.code === "ECONNRESET",
|
|
200
201
|
10, // Maximum 10 attempts (1 initial + 9 retries)
|
|
201
202
|
1000, // Start with 1s delay, will exponentially increase
|
|
202
203
|
);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
2
3
|
import { bundle } from "../../esbuild/index.ts";
|
|
4
|
+
import { exists } from "../../util/exists.ts";
|
|
3
5
|
import { withExponentialBackoff } from "../../util/retry.ts";
|
|
4
6
|
import { handleApiError } from "../api-error.ts";
|
|
5
7
|
import type { CloudflareApi } from "../api.ts";
|
|
@@ -217,8 +219,15 @@ export async function getAccountSubdomain(api: CloudflareApi) {
|
|
|
217
219
|
}
|
|
218
220
|
|
|
219
221
|
async function bundleWorkerScript() {
|
|
222
|
+
const thisDir =
|
|
223
|
+
typeof __dirname !== "undefined"
|
|
224
|
+
? __dirname
|
|
225
|
+
: path.dirname(fileURLToPath(import.meta.url));
|
|
226
|
+
// we can be running from .ts or .js, so resolve it defensively
|
|
227
|
+
const workerTs = path.join(thisDir, "worker.ts");
|
|
228
|
+
const workerJs = path.join(thisDir, "worker.js");
|
|
220
229
|
const result = await bundle({
|
|
221
|
-
entryPoint:
|
|
230
|
+
entryPoint: (await exists(workerTs)) ? workerTs : workerJs,
|
|
222
231
|
bundle: true,
|
|
223
232
|
format: "esm",
|
|
224
233
|
target: "es2022",
|
|
@@ -522,6 +522,7 @@ export async function createPipeline(
|
|
|
522
522
|
api: CloudflareApi,
|
|
523
523
|
pipelineName: string,
|
|
524
524
|
props: PipelineProps,
|
|
525
|
+
attempt = 0,
|
|
525
526
|
): Promise<CloudflarePipelineResponse> {
|
|
526
527
|
// Prepare the create payload
|
|
527
528
|
const createPayload = preparePipelinePayload(api, pipelineName, props);
|
|
@@ -532,6 +533,11 @@ export async function createPipeline(
|
|
|
532
533
|
);
|
|
533
534
|
|
|
534
535
|
if (!createResponse.ok) {
|
|
536
|
+
if (createResponse.status === 404 && attempt < 3) {
|
|
537
|
+
// bucket does not exist, this might be transient, let's retry
|
|
538
|
+
await new Promise((resolve) => setTimeout(resolve, 1000 * (1 + attempt)));
|
|
539
|
+
return await createPipeline(api, pipelineName, props, attempt + 1);
|
|
540
|
+
}
|
|
535
541
|
return await handleApiError(
|
|
536
542
|
createResponse,
|
|
537
543
|
"creating",
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type CloudflareApi,
|
|
8
8
|
type CloudflareApiOptions,
|
|
9
9
|
} from "./api.ts";
|
|
10
|
+
import { configureURL } from "./worker.ts";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Properties for creating a Worker stub
|
|
@@ -19,6 +20,14 @@ export interface WorkerStubProps<
|
|
|
19
20
|
*/
|
|
20
21
|
name: string;
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Whether to enable a workers.dev URL for this worker
|
|
25
|
+
*
|
|
26
|
+
* If true, the worker will be available at {name}.{subdomain}.workers.dev
|
|
27
|
+
* @default true
|
|
28
|
+
*/
|
|
29
|
+
url?: boolean;
|
|
30
|
+
|
|
22
31
|
/**
|
|
23
32
|
* The RPC class to use for the worker.
|
|
24
33
|
*
|
|
@@ -39,6 +48,12 @@ export interface WorkerStub<
|
|
|
39
48
|
*/
|
|
40
49
|
name: string;
|
|
41
50
|
|
|
51
|
+
/**
|
|
52
|
+
* The worker's URL if enabled
|
|
53
|
+
* Format: {name}.{subdomain}.workers.dev
|
|
54
|
+
*/
|
|
55
|
+
url?: string;
|
|
56
|
+
|
|
42
57
|
/**
|
|
43
58
|
* Optional type branding for the worker's RPC entrypoint.
|
|
44
59
|
*
|
|
@@ -59,12 +74,21 @@ export function isWorkerStub(resource: Resource): resource is WorkerStub {
|
|
|
59
74
|
* exists and creates an empty one if needed.
|
|
60
75
|
*
|
|
61
76
|
* @example
|
|
62
|
-
* // Reserve a worker name without deploying code
|
|
77
|
+
* // Reserve a worker name without deploying code, with URL enabled (default)
|
|
63
78
|
* const workerStub = await WorkerStub("my-worker", {
|
|
64
79
|
* name: "my-reserved-worker"
|
|
65
80
|
* });
|
|
66
81
|
*
|
|
67
|
-
* console.log(`Worker ${workerStub.name}
|
|
82
|
+
* console.log(`Worker ${workerStub.name} is available at: ${workerStub.url}`);
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* // Reserve a worker name without enabling URL
|
|
86
|
+
* const workerStub = await WorkerStub("my-worker", {
|
|
87
|
+
* name: "my-reserved-worker",
|
|
88
|
+
* url: false
|
|
89
|
+
* });
|
|
90
|
+
*
|
|
91
|
+
* console.log(`Worker ${workerStub.name} created without URL`);
|
|
68
92
|
*/
|
|
69
93
|
export const WorkerStub = Resource("cloudflare::WorkerStub", async function <
|
|
70
94
|
RPC extends Rpc.WorkerEntrypointBranded = Rpc.WorkerEntrypointBranded,
|
|
@@ -84,11 +108,16 @@ export const WorkerStub = Resource("cloudflare::WorkerStub", async function <
|
|
|
84
108
|
await createEmptyWorker(api, props.name);
|
|
85
109
|
}
|
|
86
110
|
|
|
111
|
+
// Configure URL if requested (defaults to true)
|
|
112
|
+
const enableUrl = props.url ?? true;
|
|
113
|
+
const workerUrl = await configureURL(this, api, props.name, enableUrl);
|
|
114
|
+
|
|
87
115
|
// Return the worker stub info
|
|
88
116
|
return this({
|
|
89
117
|
type: "service",
|
|
90
118
|
__rpc__: props.rpc as unknown as RPC,
|
|
91
119
|
...props,
|
|
120
|
+
url: workerUrl,
|
|
92
121
|
}) as WorkerStub<RPC>;
|
|
93
122
|
});
|
|
94
123
|
|
package/src/cloudflare/worker.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { BUILD_DATE } from "../build-date.ts";
|
|
2
3
|
import type { Context } from "../context.ts";
|
|
3
4
|
import type { BundleProps } from "../esbuild/bundle.ts";
|
|
4
5
|
import { InnerResourceScope, Resource, ResourceKind } from "../resource.ts";
|
|
@@ -178,7 +179,7 @@ export interface BaseWorkerProps<
|
|
|
178
179
|
|
|
179
180
|
/**
|
|
180
181
|
* The compatibility date for the worker
|
|
181
|
-
* @default
|
|
182
|
+
* @default BUILD_DATE - automatically pinned to the package build date
|
|
182
183
|
*/
|
|
183
184
|
compatibilityDate?: string;
|
|
184
185
|
|
|
@@ -861,7 +862,7 @@ export function Worker<const B extends Bindings>(
|
|
|
861
862
|
});
|
|
862
863
|
}
|
|
863
864
|
|
|
864
|
-
export const DEFAULT_COMPATIBILITY_DATE =
|
|
865
|
+
export const DEFAULT_COMPATIBILITY_DATE = BUILD_DATE;
|
|
865
866
|
|
|
866
867
|
export const _Worker = Resource(
|
|
867
868
|
"cloudflare::Worker",
|
|
@@ -1467,7 +1468,7 @@ export async function assertWorkerDoesNotExist<B extends Bindings>(
|
|
|
1467
1468
|
}
|
|
1468
1469
|
|
|
1469
1470
|
export async function configureURL<B extends Bindings>(
|
|
1470
|
-
ctx: Context<Worker<B
|
|
1471
|
+
ctx: Context<Worker<B>> | Context<WorkerStub>,
|
|
1471
1472
|
api: CloudflareApi,
|
|
1472
1473
|
workerName: string,
|
|
1473
1474
|
url: boolean,
|
package/src/context.ts
CHANGED
|
@@ -53,7 +53,7 @@ export interface BaseContext<Out extends Resource> {
|
|
|
53
53
|
* Indicate that this resource is being replaced.
|
|
54
54
|
* This will cause the resource to be deleted at the end of the stack's CREATE phase.
|
|
55
55
|
*/
|
|
56
|
-
replace():
|
|
56
|
+
replace(): never;
|
|
57
57
|
/**
|
|
58
58
|
* Terminate the resource lifecycle handler and destroy the resource.
|
|
59
59
|
*
|
|
@@ -97,7 +97,7 @@ export function context<
|
|
|
97
97
|
seq: number;
|
|
98
98
|
props: Props;
|
|
99
99
|
state: State<Kind, Props, Out>;
|
|
100
|
-
replace: () =>
|
|
100
|
+
replace: () => never;
|
|
101
101
|
}): Context<Out> {
|
|
102
102
|
type InternalSymbols =
|
|
103
103
|
| typeof ResourceID
|
package/src/destroy.ts
CHANGED
|
@@ -6,10 +6,11 @@ import {
|
|
|
6
6
|
ResourceFQN,
|
|
7
7
|
ResourceID,
|
|
8
8
|
ResourceKind,
|
|
9
|
+
type ResourceProps,
|
|
9
10
|
ResourceScope,
|
|
10
11
|
ResourceSeq,
|
|
11
12
|
} from "./resource.ts";
|
|
12
|
-
import { isScope, Scope } from "./scope.ts";
|
|
13
|
+
import { isScope, type PendingDeletions, Scope } from "./scope.ts";
|
|
13
14
|
import { formatFQN } from "./util/cli.ts";
|
|
14
15
|
import { logger } from "./util/logger.ts";
|
|
15
16
|
|
|
@@ -18,6 +19,10 @@ export class DestroyedSignal extends Error {}
|
|
|
18
19
|
export interface DestroyOptions {
|
|
19
20
|
quiet?: boolean;
|
|
20
21
|
strategy?: "sequential" | "parallel";
|
|
22
|
+
replace?: {
|
|
23
|
+
props?: ResourceProps | undefined;
|
|
24
|
+
output?: Resource<string>;
|
|
25
|
+
};
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
function isScopeArgs(a: any): a is [scope: Scope, options?: DestroyOptions] {
|
|
@@ -40,7 +45,8 @@ export async function destroy<Type extends string>(
|
|
|
40
45
|
} satisfies DestroyOptions;
|
|
41
46
|
|
|
42
47
|
await scope.run(async () => {
|
|
43
|
-
// destroy all active resources
|
|
48
|
+
// destroy all active and pending resources
|
|
49
|
+
await scope.destroyPendingDeletions();
|
|
44
50
|
await destroyAll(Array.from(scope.resources.values()), options);
|
|
45
51
|
|
|
46
52
|
// then detect orphans and destroy them
|
|
@@ -109,7 +115,7 @@ export async function destroy<Type extends string>(
|
|
|
109
115
|
id: instance[ResourceID],
|
|
110
116
|
fqn: instance[ResourceFQN],
|
|
111
117
|
seq: instance[ResourceSeq],
|
|
112
|
-
props: state.props,
|
|
118
|
+
props: options?.replace?.props ?? state.props,
|
|
113
119
|
state,
|
|
114
120
|
replace: () => {
|
|
115
121
|
throw new Error("Cannot replace a resource that is being deleted");
|
|
@@ -127,10 +133,10 @@ export async function destroy<Type extends string>(
|
|
|
127
133
|
parent: scope,
|
|
128
134
|
},
|
|
129
135
|
async (scope) => {
|
|
130
|
-
nestedScope = scope;
|
|
136
|
+
nestedScope = options?.replace?.props == null ? scope : undefined;
|
|
131
137
|
return await Provider.handler.bind(ctx)(
|
|
132
138
|
instance[ResourceID],
|
|
133
|
-
state.props!,
|
|
139
|
+
options?.replace?.props ?? state.props!,
|
|
134
140
|
);
|
|
135
141
|
},
|
|
136
142
|
);
|
|
@@ -146,7 +152,21 @@ export async function destroy<Type extends string>(
|
|
|
146
152
|
await destroy(nestedScope, options);
|
|
147
153
|
}
|
|
148
154
|
|
|
149
|
-
|
|
155
|
+
if (options?.replace == null) {
|
|
156
|
+
if (nestedScope) {
|
|
157
|
+
await destroy(nestedScope, options);
|
|
158
|
+
}
|
|
159
|
+
await scope.deleteResource(instance[ResourceID]);
|
|
160
|
+
} else {
|
|
161
|
+
let pendingDeletions =
|
|
162
|
+
await state.output[ResourceScope].get<PendingDeletions>(
|
|
163
|
+
"pendingDeletions",
|
|
164
|
+
);
|
|
165
|
+
pendingDeletions = pendingDeletions?.filter(
|
|
166
|
+
(deletion) => deletion.resource[ResourceID] !== instance[ResourceID],
|
|
167
|
+
);
|
|
168
|
+
await scope.set("pendingDeletions", pendingDeletions);
|
|
169
|
+
}
|
|
150
170
|
|
|
151
171
|
if (!quiet) {
|
|
152
172
|
logger.task(instance[ResourceFQN], {
|
|
@@ -165,11 +185,14 @@ export async function destroy<Type extends string>(
|
|
|
165
185
|
|
|
166
186
|
export async function destroyAll(
|
|
167
187
|
resources: Resource[],
|
|
168
|
-
options?: DestroyOptions,
|
|
188
|
+
options?: DestroyOptions & { force?: boolean },
|
|
169
189
|
) {
|
|
170
190
|
if (options?.strategy !== "parallel") {
|
|
171
191
|
const sorted = resources.sort((a, b) => b[ResourceSeq] - a[ResourceSeq]);
|
|
172
192
|
for (const resource of sorted) {
|
|
193
|
+
if (isScope(resource)) {
|
|
194
|
+
await resource.destroyPendingDeletions();
|
|
195
|
+
}
|
|
173
196
|
await destroy(resource, options);
|
|
174
197
|
}
|
|
175
198
|
} else {
|
package/src/scope.ts
CHANGED
|
@@ -1,16 +1,32 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import type { Phase } from "./alchemy.ts";
|
|
3
|
-
import { destroyAll } from "./destroy.ts";
|
|
3
|
+
import { destroy, destroyAll } from "./destroy.ts";
|
|
4
4
|
import { FileSystemStateStore } from "./fs/file-system-state-store.ts";
|
|
5
|
-
import {
|
|
6
|
-
|
|
5
|
+
import {
|
|
6
|
+
ResourceFQN,
|
|
7
|
+
ResourceID,
|
|
8
|
+
ResourceKind,
|
|
9
|
+
ResourceScope,
|
|
10
|
+
ResourceSeq,
|
|
11
|
+
type PendingResource,
|
|
12
|
+
type Resource,
|
|
13
|
+
type ResourceProps,
|
|
14
|
+
} from "./resource.ts";
|
|
15
|
+
import type { State, StateStore, StateStoreType } from "./state.ts";
|
|
7
16
|
import {
|
|
8
17
|
createDummyLogger,
|
|
9
18
|
createLoggerInstance,
|
|
10
19
|
type LoggerApi,
|
|
11
20
|
} from "./util/cli.ts";
|
|
21
|
+
import { AsyncMutex } from "./util/mutex.ts";
|
|
12
22
|
import type { ITelemetryClient } from "./util/telemetry/client.ts";
|
|
13
23
|
|
|
24
|
+
export class RootScopeStateAttemptError extends Error {
|
|
25
|
+
constructor() {
|
|
26
|
+
super("Root scope cannot contain state");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
14
30
|
export interface ScopeOptions {
|
|
15
31
|
appName?: string;
|
|
16
32
|
stage?: string;
|
|
@@ -24,6 +40,11 @@ export interface ScopeOptions {
|
|
|
24
40
|
logger?: LoggerApi;
|
|
25
41
|
}
|
|
26
42
|
|
|
43
|
+
export type PendingDeletions = Array<{
|
|
44
|
+
resource: Resource<string>;
|
|
45
|
+
oldProps?: ResourceProps;
|
|
46
|
+
}>;
|
|
47
|
+
|
|
27
48
|
// TODO: support browser
|
|
28
49
|
const DEFAULT_STAGE = process.env.ALCHEMY_STAGE ?? process.env.USER ?? "dev";
|
|
29
50
|
|
|
@@ -47,7 +68,7 @@ export class Scope {
|
|
|
47
68
|
new AsyncLocalStorage<Scope>());
|
|
48
69
|
public static globals: Scope[] = (globalThis.__ALCHEMY_GLOBALS__ ??= []);
|
|
49
70
|
|
|
50
|
-
public static
|
|
71
|
+
public static getScope(): Scope | undefined {
|
|
51
72
|
const scope = Scope.storage.getStore();
|
|
52
73
|
if (!scope) {
|
|
53
74
|
if (Scope.globals.length > 0) {
|
|
@@ -63,7 +84,7 @@ export class Scope {
|
|
|
63
84
|
}
|
|
64
85
|
|
|
65
86
|
public static get current(): Scope {
|
|
66
|
-
const scope = Scope.
|
|
87
|
+
const scope = Scope.getScope();
|
|
67
88
|
if (!scope) throw new Error("Not running within an Alchemy Scope");
|
|
68
89
|
return scope;
|
|
69
90
|
}
|
|
@@ -81,6 +102,7 @@ export class Scope {
|
|
|
81
102
|
public readonly phase: Phase;
|
|
82
103
|
public readonly logger: LoggerApi;
|
|
83
104
|
public readonly telemetryClient: ITelemetryClient;
|
|
105
|
+
public readonly dataMutex: AsyncMutex;
|
|
84
106
|
|
|
85
107
|
private isErrored = false;
|
|
86
108
|
private finalized = false;
|
|
@@ -96,7 +118,7 @@ export class Scope {
|
|
|
96
118
|
`Scope name ${this.scopeName} cannot contain double colons`,
|
|
97
119
|
);
|
|
98
120
|
}
|
|
99
|
-
this.parent = options.parent ?? Scope.
|
|
121
|
+
this.parent = options.parent ?? Scope.getScope();
|
|
100
122
|
this.stage = options?.stage ?? this.parent?.stage ?? DEFAULT_STAGE;
|
|
101
123
|
this.parent?.children.set(this.scopeName!, this);
|
|
102
124
|
this.quiet = options.quiet ?? this.parent?.quiet ?? false;
|
|
@@ -131,6 +153,7 @@ export class Scope {
|
|
|
131
153
|
}
|
|
132
154
|
this.telemetryClient =
|
|
133
155
|
options.telemetryClient ?? this.parent?.telemetryClient!;
|
|
156
|
+
this.dataMutex = new AsyncMutex();
|
|
134
157
|
}
|
|
135
158
|
|
|
136
159
|
public get root(): Scope {
|
|
@@ -141,7 +164,7 @@ export class Scope {
|
|
|
141
164
|
return root;
|
|
142
165
|
}
|
|
143
166
|
|
|
144
|
-
public async
|
|
167
|
+
public async deleteResource(resourceID: ResourceID) {
|
|
145
168
|
await this.state.delete(resourceID);
|
|
146
169
|
this.resources.delete(resourceID);
|
|
147
170
|
}
|
|
@@ -158,7 +181,7 @@ export class Scope {
|
|
|
158
181
|
if (this.parent) {
|
|
159
182
|
return [...this.parent.chain, ...thisScope];
|
|
160
183
|
}
|
|
161
|
-
return [...app,
|
|
184
|
+
return [...app, ...thisScope];
|
|
162
185
|
}
|
|
163
186
|
|
|
164
187
|
public fail() {
|
|
@@ -179,6 +202,78 @@ export class Scope {
|
|
|
179
202
|
return [...this.chain, resourceID].join("/");
|
|
180
203
|
}
|
|
181
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Centralizes the "lock → locate the right scope → hand the caller a live
|
|
207
|
+
* ScopeState instance and a persist() helper".
|
|
208
|
+
*
|
|
209
|
+
* @param fn Your operation on the scope state.
|
|
210
|
+
* • `state` is already resolved and, if we're at the root, created.
|
|
211
|
+
* • `persist` will write the (possibly-mutated) state back.
|
|
212
|
+
*/
|
|
213
|
+
private async withScopeState<R>(
|
|
214
|
+
fn: (
|
|
215
|
+
state: State<string, ResourceProps | undefined, Resource<string>>, // current state for this.scopeName
|
|
216
|
+
persist: (
|
|
217
|
+
next: State<string, ResourceProps | undefined, Resource<string>>,
|
|
218
|
+
) => Promise<void>, // helper to save changes
|
|
219
|
+
) => Promise<R>,
|
|
220
|
+
): Promise<R> {
|
|
221
|
+
return this.dataMutex.lock(async () => {
|
|
222
|
+
// 1. We must know where to look.
|
|
223
|
+
if (!this.parent || !this.scopeName) {
|
|
224
|
+
throw new RootScopeStateAttemptError();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 2. Pull (or lazily create) the state bucket we care about.
|
|
228
|
+
const isRoot = this.parent.scopeName === this.root.scopeName;
|
|
229
|
+
const state =
|
|
230
|
+
(await this.parent.state.get(this.scopeName)) ??
|
|
231
|
+
(isRoot
|
|
232
|
+
? {
|
|
233
|
+
//todo(michael): should this have a different type cause its root?
|
|
234
|
+
kind: "alchemy::Scope",
|
|
235
|
+
id: this.scopeName!,
|
|
236
|
+
fqn: this.root.fqn(this.scopeName!),
|
|
237
|
+
seq: this.seq(),
|
|
238
|
+
status: "created",
|
|
239
|
+
data: {},
|
|
240
|
+
output: {
|
|
241
|
+
[ResourceID]: this.scopeName!,
|
|
242
|
+
[ResourceFQN]: this.root.fqn(this.scopeName!),
|
|
243
|
+
[ResourceKind]: "alchemy::Scope",
|
|
244
|
+
[ResourceScope]: this,
|
|
245
|
+
[ResourceSeq]: this.seq(),
|
|
246
|
+
},
|
|
247
|
+
props: {},
|
|
248
|
+
}
|
|
249
|
+
: undefined);
|
|
250
|
+
|
|
251
|
+
if (!state) throw new RootScopeStateAttemptError();
|
|
252
|
+
|
|
253
|
+
return fn(state, (updated) =>
|
|
254
|
+
this.parent!.state.set(this.scopeName!, updated),
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
public async set<T>(key: string, value: T): Promise<void> {
|
|
260
|
+
return this.withScopeState<void>(async (state, persist) => {
|
|
261
|
+
state.data[key] = value;
|
|
262
|
+
await persist(state); // only one line to save!
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
public async get<T>(key: string): Promise<T> {
|
|
267
|
+
return this.withScopeState<T>(async (state) => state.data[key]);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
public async delete(key: string): Promise<void> {
|
|
271
|
+
return this.withScopeState<void>(async (state, persist) => {
|
|
272
|
+
delete state.data[key];
|
|
273
|
+
await persist(state);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
182
277
|
public async run<T>(fn: (scope: Scope) => Promise<T>): Promise<T> {
|
|
183
278
|
return Scope.storage.run(this, () => fn(this));
|
|
184
279
|
}
|
|
@@ -198,7 +293,11 @@ export class Scope {
|
|
|
198
293
|
return null;
|
|
199
294
|
}
|
|
200
295
|
|
|
201
|
-
public async finalize() {
|
|
296
|
+
public async finalize(force?: boolean) {
|
|
297
|
+
const shouldForce =
|
|
298
|
+
force ||
|
|
299
|
+
this.parent === undefined ||
|
|
300
|
+
this?.parent?.scopeName === this.root.scopeName;
|
|
202
301
|
if (this.phase === "read") {
|
|
203
302
|
this.rootTelemetryClient?.record({
|
|
204
303
|
event: "app.success",
|
|
@@ -206,7 +305,7 @@ export class Scope {
|
|
|
206
305
|
});
|
|
207
306
|
return;
|
|
208
307
|
}
|
|
209
|
-
if (this.finalized) {
|
|
308
|
+
if (this.finalized && !shouldForce) {
|
|
210
309
|
return;
|
|
211
310
|
}
|
|
212
311
|
if (this.parent === undefined && Scope.globals.length > 0) {
|
|
@@ -227,12 +326,23 @@ export class Scope {
|
|
|
227
326
|
const orphanIds = Array.from(
|
|
228
327
|
resourceIds.filter((id) => !aliveIds.has(id)),
|
|
229
328
|
);
|
|
329
|
+
|
|
330
|
+
if (shouldForce) {
|
|
331
|
+
await this.destroyPendingDeletions();
|
|
332
|
+
await Promise.all(
|
|
333
|
+
Array.from(this.children.values()).map((child) =>
|
|
334
|
+
child.finalize(shouldForce),
|
|
335
|
+
),
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
230
339
|
const orphans = await Promise.all(
|
|
231
340
|
orphanIds.map(async (id) => (await this.state.get(id))!.output),
|
|
232
341
|
);
|
|
233
342
|
await destroyAll(orphans, {
|
|
234
343
|
quiet: this.quiet,
|
|
235
344
|
strategy: "sequential",
|
|
345
|
+
force: shouldForce,
|
|
236
346
|
});
|
|
237
347
|
this.rootTelemetryClient?.record({
|
|
238
348
|
event: "app.success",
|
|
@@ -250,6 +360,31 @@ export class Scope {
|
|
|
250
360
|
await this.rootTelemetryClient?.finalize();
|
|
251
361
|
}
|
|
252
362
|
|
|
363
|
+
public async destroyPendingDeletions() {
|
|
364
|
+
const pendingDeletions =
|
|
365
|
+
(await this.get<PendingDeletions>("pendingDeletions").catch((e) => {
|
|
366
|
+
if (e instanceof RootScopeStateAttemptError) {
|
|
367
|
+
return [];
|
|
368
|
+
}
|
|
369
|
+
throw e;
|
|
370
|
+
})) ?? [];
|
|
371
|
+
if (pendingDeletions) {
|
|
372
|
+
for (const { resource, oldProps } of pendingDeletions) {
|
|
373
|
+
//todo(michael): ugly hack due to the way scope is serialized
|
|
374
|
+
const realResource = this.resources.get(resource[ResourceID])!;
|
|
375
|
+
resource[ResourceScope] = realResource?.[ResourceScope] ?? this;
|
|
376
|
+
await destroy(resource, {
|
|
377
|
+
quiet: this.quiet,
|
|
378
|
+
strategy: "sequential",
|
|
379
|
+
replace: {
|
|
380
|
+
props: oldProps,
|
|
381
|
+
output: resource,
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
253
388
|
/**
|
|
254
389
|
* Defers execution of a function until the Alchemy application finalizes.
|
|
255
390
|
*/
|
package/src/sentry/project.ts
CHANGED
package/src/util/logger.ts
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple async mutex that ensures only one async operation executes at a time
|
|
3
|
+
*/
|
|
4
|
+
export class AsyncMutex {
|
|
5
|
+
private locked = false;
|
|
6
|
+
private queue: Array<() => void> = [];
|
|
7
|
+
|
|
8
|
+
async lock<T>(operation: () => Promise<T>): Promise<T> {
|
|
9
|
+
return new Promise<T>((resolve, reject) => {
|
|
10
|
+
const execute = async () => {
|
|
11
|
+
this.locked = true;
|
|
12
|
+
try {
|
|
13
|
+
const result = await operation();
|
|
14
|
+
resolve(result);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
reject(error);
|
|
17
|
+
} finally {
|
|
18
|
+
this.locked = false;
|
|
19
|
+
const next = this.queue.shift();
|
|
20
|
+
if (next) {
|
|
21
|
+
next();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
if (this.locked) {
|
|
27
|
+
this.queue.push(execute);
|
|
28
|
+
} else {
|
|
29
|
+
execute();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Astro Starter Kit: Basics
|
|
2
|
+
|
|
3
|
+
```sh
|
|
4
|
+
bun create astro@latest -- --template basics
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
[](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
|
|
8
|
+
[](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
|
|
9
|
+
[](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
|
|
10
|
+
|
|
11
|
+
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
## 🚀 Project Structure
|
|
16
|
+
|
|
17
|
+
Inside of your Astro project, you'll see the following folders and files:
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
/
|
|
21
|
+
├── public/
|
|
22
|
+
│ └── favicon.svg
|
|
23
|
+
├── src/
|
|
24
|
+
│ ├── layouts/
|
|
25
|
+
│ │ └── Layout.astro
|
|
26
|
+
│ └── pages/
|
|
27
|
+
│ └── index.astro
|
|
28
|
+
└── package.json
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
To learn more about the folder structure of an Astro project, refer to [our guide on project structure](https://docs.astro.build/en/basics/project-structure/).
|
|
32
|
+
|
|
33
|
+
## 🧞 Commands
|
|
34
|
+
|
|
35
|
+
All commands are run from the root of the project, from a terminal:
|
|
36
|
+
|
|
37
|
+
| Command | Action |
|
|
38
|
+
| :------------------------ | :----------------------------------------------- |
|
|
39
|
+
| `bun install` | Installs dependencies |
|
|
40
|
+
| `bun dev` | Starts local dev server at `localhost:4321` |
|
|
41
|
+
| `bun build` | Build your production site to `./dist/` |
|
|
42
|
+
| `bun preview` | Preview your build locally, before deploying |
|
|
43
|
+
| `bun astro ...` | Run CLI commands like `astro add`, `astro check` |
|
|
44
|
+
| `bun astro -- --help` | Get help using the Astro CLI |
|
|
45
|
+
|
|
46
|
+
## 👀 Want to learn more?
|
|
47
|
+
|
|
48
|
+
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALCHEMY_PASSWORD=change-me
|