alchemy 2.0.0-beta.51 → 2.0.0-beta.52
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/exec.js +16 -22
- package/bin/exec.js.map +1 -1
- package/lib/Apply.js +15 -3
- package/lib/Apply.js.map +1 -1
- package/lib/Auth/AuthProvider.js +1 -1
- package/lib/Auth/AuthProvider.js.map +1 -1
- package/lib/Binding.d.ts.map +1 -1
- package/lib/Binding.js +12 -8
- package/lib/Binding.js.map +1 -1
- package/lib/Build/DevServer.d.ts +113 -0
- package/lib/Build/DevServer.d.ts.map +1 -0
- package/lib/Build/DevServer.js +212 -0
- package/lib/Build/DevServer.js.map +1 -0
- package/lib/Build/Local.d.ts +2 -0
- package/lib/Build/Local.d.ts.map +1 -0
- package/lib/Build/Local.js +4 -0
- package/lib/Build/Local.js.map +1 -0
- package/lib/Bundle/Bundle.js +1 -1
- package/lib/Bundle/Bundle.js.map +1 -1
- package/lib/Cloudflare/Access.js +1 -1
- package/lib/Cloudflare/Access.js.map +1 -1
- package/lib/Cloudflare/AiGateway/AiGateway.d.ts +1 -1
- package/lib/Cloudflare/AiGateway/AiGateway.js +1 -1
- package/lib/Cloudflare/Browser/Browser.d.ts +84 -12
- package/lib/Cloudflare/Browser/Browser.d.ts.map +1 -1
- package/lib/Cloudflare/Browser/Browser.js +80 -9
- package/lib/Cloudflare/Browser/Browser.js.map +1 -1
- package/lib/Cloudflare/Browser/BrowserBinding.d.ts +90 -16
- package/lib/Cloudflare/Browser/BrowserBinding.d.ts.map +1 -1
- package/lib/Cloudflare/Browser/BrowserBinding.js +41 -6
- package/lib/Cloudflare/Browser/BrowserBinding.js.map +1 -1
- package/lib/Cloudflare/Browser/index.d.ts +1 -1
- package/lib/Cloudflare/Browser/index.d.ts.map +1 -1
- package/lib/Cloudflare/Browser/index.js.map +1 -1
- package/lib/Cloudflare/Container/ContainerBinding.d.ts.map +1 -1
- package/lib/Cloudflare/Container/ContainerBinding.js +2 -4
- package/lib/Cloudflare/Container/ContainerBinding.js.map +1 -1
- package/lib/Cloudflare/Images/Images.d.ts +4 -3
- package/lib/Cloudflare/Images/Images.d.ts.map +1 -1
- package/lib/Cloudflare/Images/Images.js.map +1 -1
- package/lib/Cloudflare/Providers.d.ts +1 -1
- package/lib/Cloudflare/Providers.d.ts.map +1 -1
- package/lib/Cloudflare/Providers.js +4 -1
- package/lib/Cloudflare/Providers.js.map +1 -1
- package/lib/Cloudflare/RateLimit/RateLimit.d.ts +4 -3
- package/lib/Cloudflare/RateLimit/RateLimit.d.ts.map +1 -1
- package/lib/Cloudflare/RateLimit/RateLimit.js.map +1 -1
- package/lib/Cloudflare/Website/StaticSite.d.ts +64 -12
- package/lib/Cloudflare/Website/StaticSite.d.ts.map +1 -1
- package/lib/Cloudflare/Website/StaticSite.js +80 -30
- package/lib/Cloudflare/Website/StaticSite.js.map +1 -1
- package/lib/Cloudflare/Website/Vite.d.ts +36 -7
- package/lib/Cloudflare/Website/Vite.d.ts.map +1 -1
- package/lib/Cloudflare/Website/Vite.js +26 -8
- package/lib/Cloudflare/Website/Vite.js.map +1 -1
- package/lib/Cloudflare/Workers/Assets.d.ts +10 -4
- package/lib/Cloudflare/Workers/Assets.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Assets.js +6 -6
- package/lib/Cloudflare/Workers/Assets.js.map +1 -1
- package/lib/Cloudflare/Workers/DynamicWorkerLoader.d.ts +4 -4
- package/lib/Cloudflare/Workers/InferEnv.d.ts +1 -1
- package/lib/Cloudflare/Workers/InferEnv.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/LocalWorkerProvider.js +45 -11
- package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -1
- package/lib/Cloudflare/Workers/RpcDurableObjectNamespace.d.ts +5 -4
- package/lib/Cloudflare/Workers/RpcDurableObjectNamespace.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/RpcDurableObjectNamespace.js +4 -2
- package/lib/Cloudflare/Workers/RpcDurableObjectNamespace.js.map +1 -1
- package/lib/Cloudflare/Workers/RpcWorker.d.ts +3 -2
- package/lib/Cloudflare/Workers/RpcWorker.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/RpcWorker.js +5 -3
- package/lib/Cloudflare/Workers/RpcWorker.js.map +1 -1
- package/lib/Cloudflare/Workers/VersionMetadata.d.ts +99 -0
- package/lib/Cloudflare/Workers/VersionMetadata.d.ts.map +1 -0
- package/lib/Cloudflare/Workers/VersionMetadata.js +65 -0
- package/lib/Cloudflare/Workers/VersionMetadata.js.map +1 -0
- package/lib/Cloudflare/Workers/VersionMetadataBinding.d.ts +35 -0
- package/lib/Cloudflare/Workers/VersionMetadataBinding.d.ts.map +1 -0
- package/lib/Cloudflare/Workers/VersionMetadataBinding.js +31 -0
- package/lib/Cloudflare/Workers/VersionMetadataBinding.js.map +1 -0
- package/lib/Cloudflare/Workers/Worker.d.ts +18 -18
- package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.js +23 -37
- package/lib/Cloudflare/Workers/Worker.js.map +1 -1
- package/lib/Cloudflare/Workers/WorkerAsyncBindings.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/WorkerAsyncBindings.js +7 -0
- package/lib/Cloudflare/Workers/WorkerAsyncBindings.js.map +1 -1
- package/lib/Cloudflare/Workers/WorkerBinding.d.ts +2 -1
- package/lib/Cloudflare/Workers/WorkerBinding.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/WorkerBinding.js.map +1 -1
- package/lib/Cloudflare/Workers/Workflow.d.ts +8 -1
- package/lib/Cloudflare/Workers/Workflow.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Workflow.js +1 -0
- package/lib/Cloudflare/Workers/Workflow.js.map +1 -1
- package/lib/Cloudflare/Workers/WorkflowBridge.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/WorkflowBridge.js +31 -2
- package/lib/Cloudflare/Workers/WorkflowBridge.js.map +1 -1
- package/lib/Cloudflare/Workers/index.d.ts +2 -0
- package/lib/Cloudflare/Workers/index.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/index.js +2 -0
- package/lib/Cloudflare/Workers/index.js.map +1 -1
- package/lib/Cloudflare/Zaraz/ZarazConfig.d.ts.map +1 -1
- package/lib/Diff.d.ts.map +1 -1
- package/lib/Diff.js +7 -0
- package/lib/Diff.js.map +1 -1
- package/lib/Drizzle/Postgres.d.ts +13 -12
- package/lib/Drizzle/Postgres.d.ts.map +1 -1
- package/lib/Drizzle/Postgres.js +13 -12
- package/lib/Drizzle/Postgres.js.map +1 -1
- package/lib/Drizzle/Schema.d.ts +1 -1
- package/lib/Drizzle/Schema.d.ts.map +1 -1
- package/lib/Drizzle/Schema.js +14 -6
- package/lib/Drizzle/Schema.js.map +1 -1
- package/lib/Local/RpcSerialization.js +5 -2
- package/lib/Local/RpcSerialization.js.map +1 -1
- package/lib/Platform.d.ts.map +1 -1
- package/lib/Platform.js +27 -25
- package/lib/Platform.js.map +1 -1
- package/lib/Resource.d.ts +1 -1
- package/lib/Resource.d.ts.map +1 -1
- package/lib/Resource.js +9 -12
- package/lib/Resource.js.map +1 -1
- package/lib/Runtime.d.ts +1 -1
- package/lib/Runtime.d.ts.map +1 -1
- package/lib/Runtime.js +3 -4
- package/lib/Runtime.js.map +1 -1
- package/lib/Util/effect.d.ts.map +1 -1
- package/lib/Util/effect.js +13 -15
- package/lib/Util/effect.js.map +1 -1
- package/lib/Util/poll.d.ts +0 -1
- package/lib/Util/poll.d.ts.map +1 -1
- package/lib/Util/poll.js +2 -2
- package/lib/Util/poll.js.map +1 -1
- package/lib/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +20 -20
- package/src/Apply.ts +22 -3
- package/src/Auth/AuthProvider.ts +1 -1
- package/src/Binding.ts +45 -38
- package/src/Build/DevServer.ts +330 -0
- package/src/Build/Local.ts +4 -0
- package/src/Bundle/Bundle.ts +1 -1
- package/src/Cloudflare/Access.ts +1 -1
- package/src/Cloudflare/AiGateway/AiGateway.ts +1 -1
- package/src/Cloudflare/Browser/Browser.ts +84 -12
- package/src/Cloudflare/Browser/BrowserBinding.ts +205 -33
- package/src/Cloudflare/Browser/index.ts +15 -2
- package/src/Cloudflare/Container/ContainerBinding.ts +5 -10
- package/src/Cloudflare/Images/Images.ts +4 -3
- package/src/Cloudflare/Providers.ts +5 -0
- package/src/Cloudflare/RateLimit/RateLimit.ts +4 -3
- package/src/Cloudflare/Website/StaticSite.ts +154 -41
- package/src/Cloudflare/Website/Vite.ts +73 -25
- package/src/Cloudflare/Workers/Assets.ts +10 -8
- package/src/Cloudflare/Workers/DynamicWorkerLoader.ts +4 -4
- package/src/Cloudflare/Workers/InferEnv.ts +14 -12
- package/src/Cloudflare/Workers/LocalWorkerProvider.ts +54 -13
- package/src/Cloudflare/Workers/RpcDurableObjectNamespace.ts +11 -8
- package/src/Cloudflare/Workers/RpcWorker.ts +8 -7
- package/src/Cloudflare/Workers/VersionMetadata.ts +129 -0
- package/src/Cloudflare/Workers/VersionMetadataBinding.ts +82 -0
- package/src/Cloudflare/Workers/Worker.ts +60 -81
- package/src/Cloudflare/Workers/WorkerAsyncBindings.ts +6 -0
- package/src/Cloudflare/Workers/WorkerBinding.ts +2 -0
- package/src/Cloudflare/Workers/Workflow.ts +12 -1
- package/src/Cloudflare/Workers/WorkflowBridge.ts +47 -6
- package/src/Cloudflare/Workers/index.ts +2 -0
- package/src/Diff.ts +7 -0
- package/src/Drizzle/Postgres.ts +13 -12
- package/src/Drizzle/Schema.ts +16 -6
- package/src/Local/RpcSerialization.ts +5 -2
- package/src/Platform.ts +36 -32
- package/src/Resource.ts +12 -13
- package/src/Runtime.ts +3 -7
- package/src/Util/effect.ts +13 -17
- package/src/Util/poll.ts +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alchemy",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.52",
|
|
4
4
|
"homepage": "https://alchemy.run",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Sam Goodwin <sam@alchemy.run>",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"sideEffects": false,
|
|
22
22
|
"scripts": {
|
|
23
23
|
"dev": "tsdown --watch",
|
|
24
|
-
"build": "bun bundle",
|
|
24
|
+
"build": "tsc -b && tsc -b tsconfig.bin.json && bun bundle",
|
|
25
25
|
"bundle": "tsdown",
|
|
26
26
|
"bundle:watch": "tsdown --watch",
|
|
27
27
|
"publish:npm": "cp ../README.md . && bun publish && rm README.md",
|
|
@@ -291,16 +291,16 @@
|
|
|
291
291
|
"@alchemy.run/node-utils": "0.0.4",
|
|
292
292
|
"@aws-sdk/credential-providers": "^3.0.0",
|
|
293
293
|
"@clack/prompts": "^0.11.0",
|
|
294
|
-
"@distilled.cloud/aws": "^0.
|
|
295
|
-
"@distilled.cloud/axiom": "^0.
|
|
296
|
-
"@distilled.cloud/cloudflare": "^0.
|
|
297
|
-
"@distilled.cloud/cloudflare-rolldown-plugin": "0.10.
|
|
298
|
-
"@distilled.cloud/cloudflare-runtime": "0.10.
|
|
299
|
-
"@distilled.cloud/cloudflare-vite-plugin": "0.10.
|
|
300
|
-
"@distilled.cloud/core": "^0.
|
|
301
|
-
"@distilled.cloud/neon": "^0.
|
|
302
|
-
"@distilled.cloud/planetscale": "^0.
|
|
303
|
-
"@effect/vitest": ">=4.0.0-beta.
|
|
294
|
+
"@distilled.cloud/aws": "^0.23.1",
|
|
295
|
+
"@distilled.cloud/axiom": "^0.23.1",
|
|
296
|
+
"@distilled.cloud/cloudflare": "^0.23.1",
|
|
297
|
+
"@distilled.cloud/cloudflare-rolldown-plugin": "0.10.5",
|
|
298
|
+
"@distilled.cloud/cloudflare-runtime": "0.10.5",
|
|
299
|
+
"@distilled.cloud/cloudflare-vite-plugin": "0.10.5",
|
|
300
|
+
"@distilled.cloud/core": "^0.23.1",
|
|
301
|
+
"@distilled.cloud/neon": "^0.23.1",
|
|
302
|
+
"@distilled.cloud/planetscale": "^0.23.1",
|
|
303
|
+
"@effect/vitest": ">=4.0.0-beta.78 || >=4.0.0",
|
|
304
304
|
"@libsql/client": "^0.17.0",
|
|
305
305
|
"@octokit/rest": "^22.0.1",
|
|
306
306
|
"@smithy/node-config-provider": "^4.0.0",
|
|
@@ -328,9 +328,9 @@
|
|
|
328
328
|
"@clack/prompts": "^0.11.0",
|
|
329
329
|
"@cloudflare/workers-types": "^4.20250805.0",
|
|
330
330
|
"@cloudflare/puppeteer": "^1.1.0",
|
|
331
|
-
"@effect/platform-bun": ">=4.0.0-beta.
|
|
332
|
-
"@effect/platform-node": ">=4.0.0-beta.
|
|
333
|
-
"@effect/platform-node-shared": ">=4.0.0-beta.
|
|
331
|
+
"@effect/platform-bun": ">=4.0.0-beta.78 || >=4.0.0",
|
|
332
|
+
"@effect/platform-node": ">=4.0.0-beta.78 || >=4.0.0",
|
|
333
|
+
"@effect/platform-node-shared": ">=4.0.0-beta.78 || >=4.0.0",
|
|
334
334
|
"@oxc-project/types": "^0.127.0",
|
|
335
335
|
"@types/pg": "^8.11.0",
|
|
336
336
|
"@types/aws-lambda": "^8.10.152",
|
|
@@ -340,7 +340,7 @@
|
|
|
340
340
|
"@types/react": "^19.2.2",
|
|
341
341
|
"@types/ws": "^8.18.1",
|
|
342
342
|
"better-auth": "^1.6.2",
|
|
343
|
-
"effect": ">=4.0.0-beta.
|
|
343
|
+
"effect": ">=4.0.0-beta.78 || >=4.0.0",
|
|
344
344
|
"react-devtools-core": "^7.0.1",
|
|
345
345
|
"solid-js": "latest",
|
|
346
346
|
"tsconfig-paths": "^4.2.0",
|
|
@@ -350,12 +350,12 @@
|
|
|
350
350
|
"ws": "^8.20.0"
|
|
351
351
|
},
|
|
352
352
|
"peerDependencies": {
|
|
353
|
-
"@effect/platform-bun": ">=4.0.0-beta.
|
|
354
|
-
"@effect/platform-node": ">=4.0.0-beta.
|
|
355
|
-
"@effect/sql-pg": ">=4.0.0-beta.
|
|
353
|
+
"@effect/platform-bun": ">=4.0.0-beta.78 || >=4.0.0",
|
|
354
|
+
"@effect/platform-node": ">=4.0.0-beta.78 || >=4.0.0",
|
|
355
|
+
"@effect/sql-pg": ">=4.0.0-beta.78 || >=4.0.0",
|
|
356
356
|
"drizzle-kit": ">=1.0.0-rc.1",
|
|
357
357
|
"drizzle-orm": ">=1.0.0-rc.1",
|
|
358
|
-
"effect": ">=4.0.0-beta.
|
|
358
|
+
"effect": ">=4.0.0-beta.78 || >=4.0.0",
|
|
359
359
|
"vite": "^8.0.7",
|
|
360
360
|
"ws": "^8.20.0"
|
|
361
361
|
},
|
package/src/Apply.ts
CHANGED
|
@@ -1544,7 +1544,20 @@ const collectGarbage = Effect.fnUntraced(function* (
|
|
|
1544
1544
|
});
|
|
1545
1545
|
}
|
|
1546
1546
|
|
|
1547
|
-
|
|
1547
|
+
// Honor `retain` for the old generation of a replacement, mirroring
|
|
1548
|
+
// the orphan-delete path above. Delete-node retain is already
|
|
1549
|
+
// handled with an early return; this guards the replaced
|
|
1550
|
+
// old-generation physical delete.
|
|
1551
|
+
const retainOldGeneration =
|
|
1552
|
+
!isDeleteNode(node) && node.removalPolicy === "retain";
|
|
1553
|
+
|
|
1554
|
+
if (retainOldGeneration) {
|
|
1555
|
+
yield* scopedSession.note(
|
|
1556
|
+
"Retaining replaced resource (removal policy: retain)...",
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
if (attr !== undefined && !retainOldGeneration) {
|
|
1548
1561
|
yield* provider
|
|
1549
1562
|
.delete({
|
|
1550
1563
|
id: logicalId,
|
|
@@ -1573,7 +1586,9 @@ const collectGarbage = Effect.fnUntraced(function* (
|
|
|
1573
1586
|
});
|
|
1574
1587
|
yield* report("deleted");
|
|
1575
1588
|
} else {
|
|
1576
|
-
|
|
1589
|
+
if (!retainOldGeneration) {
|
|
1590
|
+
yield* scopedSession.note("Cleaning up replaced resource...");
|
|
1591
|
+
}
|
|
1577
1592
|
if (
|
|
1578
1593
|
node.old.status === "replacing" ||
|
|
1579
1594
|
node.old.status === "replaced"
|
|
@@ -1613,7 +1628,11 @@ const collectGarbage = Effect.fnUntraced(function* (
|
|
|
1613
1628
|
removalPolicy: node.removalPolicy,
|
|
1614
1629
|
});
|
|
1615
1630
|
}
|
|
1616
|
-
yield* scopedSession.note(
|
|
1631
|
+
yield* scopedSession.note(
|
|
1632
|
+
retainOldGeneration
|
|
1633
|
+
? "Replaced resource retained."
|
|
1634
|
+
: "Replaced resource cleanup complete.",
|
|
1635
|
+
);
|
|
1617
1636
|
}
|
|
1618
1637
|
}),
|
|
1619
1638
|
));
|
package/src/Auth/AuthProvider.ts
CHANGED
package/src/Binding.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as Context from "effect/Context";
|
|
2
2
|
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Effectable from "effect/Effectable";
|
|
3
4
|
import * as Layer from "effect/Layer";
|
|
4
5
|
import * as Option from "effect/Option";
|
|
5
|
-
import { SingleShotGen } from "effect/Utils";
|
|
6
6
|
import type { Input } from "./Input.ts";
|
|
7
7
|
import * as Namespace from "./Namespace.ts";
|
|
8
8
|
import { ALCHEMY_PHASE } from "./Phase.ts";
|
|
@@ -157,7 +157,7 @@ export const Policy =
|
|
|
157
157
|
),
|
|
158
158
|
);
|
|
159
159
|
|
|
160
|
-
const
|
|
160
|
+
const evaluate = () =>
|
|
161
161
|
Effect.all([Self, Service]).pipe(
|
|
162
162
|
Effect.map(
|
|
163
163
|
([resource, fn]) =>
|
|
@@ -176,44 +176,51 @@ export const Policy =
|
|
|
176
176
|
),
|
|
177
177
|
);
|
|
178
178
|
// @ts-expect-error
|
|
179
|
-
return Object.assign(
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
) =>
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
// @ts-expect-error
|
|
196
|
-
(...args: Parameters<Shape>) =>
|
|
197
|
-
Self.use((self) => fn(self as ResourceLike, ...args)),
|
|
198
|
-
),
|
|
199
|
-
effect: (
|
|
200
|
-
fn: Effect.Effect<
|
|
201
|
-
(
|
|
179
|
+
return Object.assign(
|
|
180
|
+
self,
|
|
181
|
+
// `self` is a Context tag (already an Effect). Spreading the Effect
|
|
182
|
+
// prototype retargets `yield* Policy` from the raw tag to the resolved
|
|
183
|
+
// bind-function while keeping `Effect.isEffect(Policy)` true so
|
|
184
|
+
// `Effect.all`/`forEach` behave.
|
|
185
|
+
Effectable.Prototype({
|
|
186
|
+
label: `Policy<${Identifier}>`,
|
|
187
|
+
evaluate,
|
|
188
|
+
}),
|
|
189
|
+
{
|
|
190
|
+
bind: (...args: any[]) =>
|
|
191
|
+
evaluate().pipe(Effect.flatMap((fn) => fn(...args))),
|
|
192
|
+
layer: {
|
|
193
|
+
succeed: (
|
|
194
|
+
fn: (
|
|
202
195
|
self: ResourceLike,
|
|
203
196
|
...args: Parameters<Shape>
|
|
204
|
-
) => Effect.Effect<void
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
197
|
+
) => Effect.Effect<void>,
|
|
198
|
+
) =>
|
|
199
|
+
Layer.succeed(
|
|
200
|
+
self,
|
|
201
|
+
// @ts-expect-error
|
|
202
|
+
(...args: Parameters<Shape>) =>
|
|
203
|
+
Self.use((self) => fn(self as ResourceLike, ...args)),
|
|
204
|
+
),
|
|
205
|
+
effect: (
|
|
206
|
+
fn: Effect.Effect<
|
|
207
|
+
(
|
|
208
|
+
self: ResourceLike,
|
|
209
|
+
...args: Parameters<Shape>
|
|
210
|
+
) => Effect.Effect<void>
|
|
211
|
+
>,
|
|
212
|
+
) =>
|
|
213
|
+
Layer.effect(
|
|
214
|
+
self,
|
|
215
|
+
// @ts-expect-error
|
|
216
|
+
Effect.map(
|
|
217
|
+
fn,
|
|
218
|
+
(fn) =>
|
|
219
|
+
(...args: Parameters<Shape>) =>
|
|
220
|
+
Self.use((self) => fn(self as ResourceLike, ...args)),
|
|
221
|
+
),
|
|
215
222
|
),
|
|
216
|
-
|
|
223
|
+
},
|
|
217
224
|
},
|
|
218
|
-
|
|
225
|
+
);
|
|
219
226
|
};
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import * as Deferred from "effect/Deferred";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Exit from "effect/Exit";
|
|
4
|
+
import * as Hash from "effect/Hash";
|
|
5
|
+
import * as Layer from "effect/Layer";
|
|
6
|
+
import * as Option from "effect/Option";
|
|
7
|
+
import * as Redacted from "effect/Redacted";
|
|
8
|
+
import * as Scope from "effect/Scope";
|
|
9
|
+
import * as Stream from "effect/Stream";
|
|
10
|
+
import { ChildProcess } from "effect/unstable/process";
|
|
11
|
+
import {
|
|
12
|
+
ChildProcessSpawner,
|
|
13
|
+
type ChildProcessHandle,
|
|
14
|
+
} from "effect/unstable/process/ChildProcessSpawner";
|
|
15
|
+
import { AlchemyContext } from "../AlchemyContext.ts";
|
|
16
|
+
import { isResolved } from "../Diff.ts";
|
|
17
|
+
import * as RpcProvider from "../Local/RpcProvider.ts";
|
|
18
|
+
import * as Provider from "../Provider.ts";
|
|
19
|
+
import { Resource } from "../Resource.ts";
|
|
20
|
+
|
|
21
|
+
export interface DevServerProps {
|
|
22
|
+
/**
|
|
23
|
+
* Shell command to run as a long-lived dev process (e.g. `npm run dev`).
|
|
24
|
+
*/
|
|
25
|
+
command: string;
|
|
26
|
+
/**
|
|
27
|
+
* Working directory for the command. Defaults to `process.cwd()`.
|
|
28
|
+
*/
|
|
29
|
+
cwd?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Extra environment variables passed to the command on top of `process.env`.
|
|
32
|
+
*/
|
|
33
|
+
env?: Record<string, string | Redacted.Redacted<string>>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A long-lived shell process scoped to a stack instance, started during
|
|
38
|
+
* `alchemy dev`, restarted when inputs are changes.
|
|
39
|
+
*
|
|
40
|
+
* The provider runs inside the dev sidecar (see `Build/Local.ts`) so the
|
|
41
|
+
* child process survives user-code HMR — Alchemy's user process can restart
|
|
42
|
+
* without killing your `npm run dev` server. During deploy this performs a no-op
|
|
43
|
+
*
|
|
44
|
+
* The child's stdout/stderr are mirrored to the sidecar's terminal so the
|
|
45
|
+
* user still sees colored dev-server output, and each line is scanned for
|
|
46
|
+
* the first `http(s)://…` URL. The first match (with ANSI escapes stripped)
|
|
47
|
+
* is exposed as `url` — useful for surfacing a dev server's local URL back
|
|
48
|
+
* out to whatever resource declared this `DevServer`.
|
|
49
|
+
*/
|
|
50
|
+
export interface DevServer extends Resource<
|
|
51
|
+
"Build.DevServer",
|
|
52
|
+
DevServerProps,
|
|
53
|
+
{
|
|
54
|
+
/**
|
|
55
|
+
* URL extracted from the first matching stdout/stderr line. Best-effort:
|
|
56
|
+
* `undefined` if no URL appeared within {@link URL_EXTRACT_TIMEOUT}.
|
|
57
|
+
*/
|
|
58
|
+
url?: string;
|
|
59
|
+
}
|
|
60
|
+
> {}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A long-lived shell process scoped to a stack instance, started during
|
|
64
|
+
* `alchemy dev` and restarted when inputs change. During deploy this is
|
|
65
|
+
* a no-op — `DevServer` resources only run in dev mode.
|
|
66
|
+
*
|
|
67
|
+
* The child process runs inside the dev sidecar so it survives
|
|
68
|
+
* user-code HMR restarts. Its stdout/stderr are mirrored to the
|
|
69
|
+
* terminal and scanned for the first `http(s)://…` URL, which is
|
|
70
|
+
* exposed as the `url` output attribute.
|
|
71
|
+
*
|
|
72
|
+
* @resource
|
|
73
|
+
*
|
|
74
|
+
* @section Basic Usage
|
|
75
|
+
* Pass a shell command that starts a long-lived dev server. Alchemy
|
|
76
|
+
* runs it in the background and extracts the first URL it prints.
|
|
77
|
+
*
|
|
78
|
+
* @example Start a Vite dev server
|
|
79
|
+
* ```typescript
|
|
80
|
+
* const dev = yield* DevServer("Frontend", {
|
|
81
|
+
* command: "npm run dev",
|
|
82
|
+
* });
|
|
83
|
+
* console.log(dev.url); // e.g. "http://localhost:5173"
|
|
84
|
+
* ```
|
|
85
|
+
*
|
|
86
|
+
* @section Working Directory
|
|
87
|
+
* Use `cwd` to run the command in a subdirectory — useful in
|
|
88
|
+
* monorepos where each package has its own dev server.
|
|
89
|
+
*
|
|
90
|
+
* @example Monorepo package
|
|
91
|
+
* ```typescript
|
|
92
|
+
* const dev = yield* DevServer("Web", {
|
|
93
|
+
* command: "npm run dev",
|
|
94
|
+
* cwd: "apps/web",
|
|
95
|
+
* });
|
|
96
|
+
* ```
|
|
97
|
+
*
|
|
98
|
+
* @section Environment Variables
|
|
99
|
+
* Extra environment variables are merged on top of `process.env`.
|
|
100
|
+
* Sensitive values can be wrapped in `Redacted` to keep them out
|
|
101
|
+
* of logs and state files.
|
|
102
|
+
*
|
|
103
|
+
* @example Custom port and env
|
|
104
|
+
* ```typescript
|
|
105
|
+
* const dev = yield* DevServer("Api", {
|
|
106
|
+
* command: "npm run dev",
|
|
107
|
+
* env: {
|
|
108
|
+
* PORT: "4000",
|
|
109
|
+
* DATABASE_URL: Redacted.make("postgres://..."),
|
|
110
|
+
* },
|
|
111
|
+
* });
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
export const DevServer = Resource<DevServer>("Build.DevServer");
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* How long reconcile waits for a URL to appear in the child's output
|
|
118
|
+
* before giving up and returning `{ url: undefined }`. The child keeps
|
|
119
|
+
* running either way — this only bounds how long the deploy plan waits.
|
|
120
|
+
*
|
|
121
|
+
* Kept small because dev servers (vite, next, zola, …) print their URL
|
|
122
|
+
* within ~1s, and a long wait here just stalls `alchemy dev` startup
|
|
123
|
+
* for commands that never produce a URL.
|
|
124
|
+
*/
|
|
125
|
+
const URL_EXTRACT_TIMEOUT = "5 seconds";
|
|
126
|
+
|
|
127
|
+
// Matches the first plain http(s) URL. Stops at whitespace and at a small
|
|
128
|
+
// set of punctuation typically used to wrap URLs in log output.
|
|
129
|
+
const URL_REGEX = /https?:\/\/[^\s)\],"'`]+/;
|
|
130
|
+
|
|
131
|
+
// ECMA-262 ANSI/VT100 escape sequences — `Vite`, `Next`, etc. surround the
|
|
132
|
+
// URL with color codes that would otherwise be eaten by the URL regex.
|
|
133
|
+
// eslint-disable-next-line no-control-regex
|
|
134
|
+
const ANSI_REGEX = /\x1b\[[0-9;]*m/g;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Live-mode no-op. `DevServer` resources should only be created in dev mode;
|
|
138
|
+
* if one slips into a deploy, this is a noisy no-op rather than a crash.
|
|
139
|
+
*/
|
|
140
|
+
export const LiveDevServerProvider = () =>
|
|
141
|
+
Provider.effect(
|
|
142
|
+
DevServer,
|
|
143
|
+
Effect.succeed(
|
|
144
|
+
DevServer.Provider.of({
|
|
145
|
+
diff: () => Effect.succeed({ action: "noop" }),
|
|
146
|
+
reconcile: () => Effect.succeed({}),
|
|
147
|
+
delete: () => Effect.void,
|
|
148
|
+
}),
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Dev-mode provider. Runs inside the RPC sidecar so the child process
|
|
154
|
+
* outlives user-code restarts. Tracks instances in a module-level closure
|
|
155
|
+
* keyed by resource id; on `reconcile`, it diffs the props hash and either
|
|
156
|
+
* reuses the existing process or interrupts + respawns.
|
|
157
|
+
*/
|
|
158
|
+
export const LocalDevServerProvider = () =>
|
|
159
|
+
RpcProvider.effect(
|
|
160
|
+
DevServer,
|
|
161
|
+
import.meta.resolve(
|
|
162
|
+
// See LocalWorkerProvider — must match the on-disk extension of the
|
|
163
|
+
// sidecar entry file.
|
|
164
|
+
import.meta.url.endsWith(".ts") ? "./Local.ts" : "./Local.js",
|
|
165
|
+
import.meta.url,
|
|
166
|
+
),
|
|
167
|
+
Effect.gen(function* () {
|
|
168
|
+
const spawner = yield* ChildProcessSpawner;
|
|
169
|
+
// The provider's outer scope lives for the lifetime of the sidecar,
|
|
170
|
+
// which is exactly what we want: child processes get forked into
|
|
171
|
+
// sub-scopes off of this one, so they survive any single user-code
|
|
172
|
+
// restart but die when the sidecar shuts down.
|
|
173
|
+
const rootScope = yield* Effect.scope;
|
|
174
|
+
|
|
175
|
+
const instances = new Map<
|
|
176
|
+
string,
|
|
177
|
+
{
|
|
178
|
+
hash: number;
|
|
179
|
+
handle: ChildProcessHandle;
|
|
180
|
+
scope: Scope.Closeable;
|
|
181
|
+
/**
|
|
182
|
+
* Resolves with the first URL found in the child's output. Stored
|
|
183
|
+
* on the instance so that no-op reconciles (same hash, process
|
|
184
|
+
* still running) can await it too — even if the URL hadn't yet
|
|
185
|
+
* appeared at the previous reconcile.
|
|
186
|
+
*/
|
|
187
|
+
urlDeferred: Deferred.Deferred<string>;
|
|
188
|
+
}
|
|
189
|
+
>();
|
|
190
|
+
|
|
191
|
+
const spawn = Effect.fn(function* (
|
|
192
|
+
props: DevServerProps,
|
|
193
|
+
urlDeferred: Deferred.Deferred<string>,
|
|
194
|
+
) {
|
|
195
|
+
const [command, ...args] = props.command.split(" ");
|
|
196
|
+
|
|
197
|
+
const handle = yield* spawner.spawn(
|
|
198
|
+
ChildProcess.make(command, args, {
|
|
199
|
+
cwd: props.cwd ?? process.cwd(),
|
|
200
|
+
env: {
|
|
201
|
+
...process.env,
|
|
202
|
+
...Object.fromEntries(
|
|
203
|
+
Object.entries(props.env ?? {}).map(([k, v]) => [
|
|
204
|
+
k,
|
|
205
|
+
Redacted.isRedacted(v) ? Redacted.value(v) : v,
|
|
206
|
+
]),
|
|
207
|
+
),
|
|
208
|
+
},
|
|
209
|
+
stdin: "inherit",
|
|
210
|
+
// Leave stdout/stderr piped so we can both mirror to the parent
|
|
211
|
+
// terminal AND parse lines for the first URL.
|
|
212
|
+
}),
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
// Mirror + extract on both streams, forked into the current scope so
|
|
216
|
+
// they're interrupted when the instance scope closes.
|
|
217
|
+
yield* mirrorAndExtract(handle.stdout, "stdout", urlDeferred).pipe(
|
|
218
|
+
Effect.forkScoped,
|
|
219
|
+
);
|
|
220
|
+
yield* mirrorAndExtract(handle.stderr, "stderr", urlDeferred).pipe(
|
|
221
|
+
Effect.forkScoped,
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
return handle;
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const stop = Effect.fn(function* (id: string) {
|
|
228
|
+
const existing = instances.get(id);
|
|
229
|
+
if (existing) {
|
|
230
|
+
yield* existing.handle.kill();
|
|
231
|
+
yield* Scope.close(existing.scope, Exit.void);
|
|
232
|
+
instances.delete(id);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const awaitUrl = (deferred: Deferred.Deferred<string>) =>
|
|
237
|
+
Deferred.await(deferred).pipe(
|
|
238
|
+
Effect.timeoutOption(URL_EXTRACT_TIMEOUT),
|
|
239
|
+
Effect.map(Option.getOrUndefined),
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
return DevServer.Provider.of({
|
|
243
|
+
diff: Effect.fn(function* ({ id, news }) {
|
|
244
|
+
if (!isResolved(news)) return undefined;
|
|
245
|
+
const hash = Hash.structure(news);
|
|
246
|
+
if (instances.get(id)?.hash === hash) {
|
|
247
|
+
return { action: "noop" };
|
|
248
|
+
}
|
|
249
|
+
return { action: "update" };
|
|
250
|
+
}),
|
|
251
|
+
reconcile: Effect.fn(function* ({ id, news }) {
|
|
252
|
+
const hash = Hash.structure(news);
|
|
253
|
+
const existing = instances.get(id);
|
|
254
|
+
|
|
255
|
+
if (existing) {
|
|
256
|
+
if (existing.hash === hash && (yield* existing.handle.isRunning)) {
|
|
257
|
+
const url = yield* awaitUrl(existing.urlDeferred);
|
|
258
|
+
return { url };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
yield* stop(id);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const scope = yield* Scope.fork(rootScope);
|
|
265
|
+
const urlDeferred = yield* Deferred.make<string>();
|
|
266
|
+
const handle = yield* spawn(news, urlDeferred).pipe(
|
|
267
|
+
Scope.provide(scope),
|
|
268
|
+
);
|
|
269
|
+
instances.set(id, { hash, handle, scope, urlDeferred });
|
|
270
|
+
const url = yield* awaitUrl(urlDeferred);
|
|
271
|
+
return { url };
|
|
272
|
+
}),
|
|
273
|
+
delete: Effect.fn(function* ({ id }) {
|
|
274
|
+
yield* stop(id);
|
|
275
|
+
}),
|
|
276
|
+
});
|
|
277
|
+
}),
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Drains a child stdout/stderr stream into the matching parent stream while
|
|
282
|
+
* scanning each completed line for the first URL. Resolves `urlDeferred`
|
|
283
|
+
* the first time a URL is found (subsequent calls are no-ops).
|
|
284
|
+
*
|
|
285
|
+
* Modeled on `idempotent-spawn.ts`'s `extract` hook — single-pass over each
|
|
286
|
+
* line, ANSI escapes stripped before matching, with a buffer for incomplete
|
|
287
|
+
* trailing lines across chunk boundaries.
|
|
288
|
+
*/
|
|
289
|
+
const mirrorAndExtract = (
|
|
290
|
+
source: Stream.Stream<Uint8Array, any>,
|
|
291
|
+
sink: "stdout" | "stderr",
|
|
292
|
+
urlDeferred: Deferred.Deferred<string>,
|
|
293
|
+
) =>
|
|
294
|
+
Effect.gen(function* () {
|
|
295
|
+
let lineBuffer = "";
|
|
296
|
+
const decoder = new TextDecoder("utf-8");
|
|
297
|
+
yield* Stream.runForEach(source, (chunk) =>
|
|
298
|
+
Effect.sync(() => {
|
|
299
|
+
// Pass the raw bytes through so terminal colors, cursor moves,
|
|
300
|
+
// progress bars etc. keep working.
|
|
301
|
+
process[sink].write(chunk);
|
|
302
|
+
|
|
303
|
+
lineBuffer += decoder.decode(chunk, { stream: true });
|
|
304
|
+
let newlineIdx = lineBuffer.indexOf("\n");
|
|
305
|
+
while (newlineIdx !== -1) {
|
|
306
|
+
const line = lineBuffer.slice(0, newlineIdx);
|
|
307
|
+
lineBuffer = lineBuffer.slice(newlineIdx + 1);
|
|
308
|
+
newlineIdx = lineBuffer.indexOf("\n");
|
|
309
|
+
const match = line.replace(ANSI_REGEX, "").match(URL_REGEX);
|
|
310
|
+
if (match) {
|
|
311
|
+
// Deferred.doneUnsafe is a synchronous fire-and-forget — first
|
|
312
|
+
// caller wins, subsequent calls are dropped, which is exactly
|
|
313
|
+
// what we want for "record the FIRST URL".
|
|
314
|
+
Deferred.doneUnsafe(urlDeferred, Effect.succeed(match[0]));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}),
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Selects the live or dev DevServer provider based on `AlchemyContext.dev`.
|
|
324
|
+
*/
|
|
325
|
+
export const DevServerProvider = () =>
|
|
326
|
+
Layer.unwrap(
|
|
327
|
+
AlchemyContext.useSync((context) =>
|
|
328
|
+
context.dev ? LocalDevServerProvider() : LiveDevServerProvider(),
|
|
329
|
+
),
|
|
330
|
+
);
|
package/src/Bundle/Bundle.ts
CHANGED
package/src/Cloudflare/Access.ts
CHANGED
|
@@ -277,7 +277,7 @@ export const isAiGateway = (value: unknown): value is AiGateway =>
|
|
|
277
277
|
*
|
|
278
278
|
* export default class Api extends Cloudflare.Worker<Api>()(
|
|
279
279
|
* "Api",
|
|
280
|
-
* { main: import.meta.
|
|
280
|
+
* { main: import.meta.filename },
|
|
281
281
|
* Effect.gen(function* () {
|
|
282
282
|
* const aiGateway = yield* Cloudflare.AiGateway.bind(Gateway);
|
|
283
283
|
*
|