alchemy 2.0.0-beta.18 → 2.0.0-beta.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/bin/alchemy.js +428 -80
  2. package/bin/alchemy.js.map +1 -1
  3. package/bin/alchemy.ts +2 -0
  4. package/bin/commands/_shared.ts +34 -0
  5. package/bin/commands/bootstrap.ts +10 -3
  6. package/bin/commands/deploy.ts +23 -3
  7. package/bin/commands/login.ts +18 -3
  8. package/bin/commands/logs.ts +11 -2
  9. package/bin/commands/profile.ts +11 -5
  10. package/bin/commands/state.ts +21 -13
  11. package/bin/commands/tail.ts +10 -2
  12. package/lib/Agent/lsp/client.d.ts +2 -2
  13. package/lib/Apply.d.ts.map +1 -1
  14. package/lib/Apply.js +41 -9
  15. package/lib/Apply.js.map +1 -1
  16. package/lib/Axiom/Annotation.d.ts +49 -0
  17. package/lib/Axiom/Annotation.d.ts.map +1 -0
  18. package/lib/Axiom/Annotation.js +74 -0
  19. package/lib/Axiom/Annotation.js.map +1 -0
  20. package/lib/Axiom/ApiToken.d.ts +62 -0
  21. package/lib/Axiom/ApiToken.d.ts.map +1 -0
  22. package/lib/Axiom/ApiToken.js +98 -0
  23. package/lib/Axiom/ApiToken.js.map +1 -0
  24. package/lib/Axiom/AuthProvider.d.ts +48 -0
  25. package/lib/Axiom/AuthProvider.d.ts.map +1 -0
  26. package/lib/Axiom/AuthProvider.js +177 -0
  27. package/lib/Axiom/AuthProvider.js.map +1 -0
  28. package/lib/Axiom/Chart.d.ts +72 -0
  29. package/lib/Axiom/Chart.d.ts.map +1 -0
  30. package/lib/Axiom/Chart.js +14 -0
  31. package/lib/Axiom/Chart.js.map +1 -0
  32. package/lib/Axiom/Credentials.d.ts +12 -0
  33. package/lib/Axiom/Credentials.d.ts.map +1 -0
  34. package/lib/Axiom/Credentials.js +34 -0
  35. package/lib/Axiom/Credentials.js.map +1 -0
  36. package/lib/Axiom/Dashboard.d.ts +124 -0
  37. package/lib/Axiom/Dashboard.d.ts.map +1 -0
  38. package/lib/Axiom/Dashboard.js +140 -0
  39. package/lib/Axiom/Dashboard.js.map +1 -0
  40. package/lib/Axiom/Dataset.d.ts +101 -0
  41. package/lib/Axiom/Dataset.d.ts.map +1 -0
  42. package/lib/Axiom/Dataset.js +183 -0
  43. package/lib/Axiom/Dataset.js.map +1 -0
  44. package/lib/Axiom/Monitor.d.ts +74 -0
  45. package/lib/Axiom/Monitor.d.ts.map +1 -0
  46. package/lib/Axiom/Monitor.js +103 -0
  47. package/lib/Axiom/Monitor.js.map +1 -0
  48. package/lib/Axiom/Notifier.d.ts +63 -0
  49. package/lib/Axiom/Notifier.d.ts.map +1 -0
  50. package/lib/Axiom/Notifier.js +83 -0
  51. package/lib/Axiom/Notifier.js.map +1 -0
  52. package/lib/Axiom/Providers.d.ts +15 -0
  53. package/lib/Axiom/Providers.d.ts.map +1 -0
  54. package/lib/Axiom/Providers.js +32 -0
  55. package/lib/Axiom/Providers.js.map +1 -0
  56. package/lib/Axiom/View.d.ts +55 -0
  57. package/lib/Axiom/View.d.ts.map +1 -0
  58. package/lib/Axiom/View.js +79 -0
  59. package/lib/Axiom/View.js.map +1 -0
  60. package/lib/Axiom/VirtualField.d.ts +42 -0
  61. package/lib/Axiom/VirtualField.d.ts.map +1 -0
  62. package/lib/Axiom/VirtualField.js +71 -0
  63. package/lib/Axiom/VirtualField.js.map +1 -0
  64. package/lib/Axiom/index.d.ts +13 -0
  65. package/lib/Axiom/index.d.ts.map +1 -0
  66. package/lib/Axiom/index.js +13 -0
  67. package/lib/Axiom/index.js.map +1 -0
  68. package/lib/Bundle/Bundle.d.ts +20 -2
  69. package/lib/Bundle/Bundle.d.ts.map +1 -1
  70. package/lib/Bundle/Bundle.js +18 -3
  71. package/lib/Bundle/Bundle.js.map +1 -1
  72. package/lib/Bundle/PurePlugin.d.ts +109 -0
  73. package/lib/Bundle/PurePlugin.d.ts.map +1 -0
  74. package/lib/Bundle/PurePlugin.js +469 -0
  75. package/lib/Bundle/PurePlugin.js.map +1 -0
  76. package/lib/Bundle/index.d.ts +1 -0
  77. package/lib/Bundle/index.d.ts.map +1 -1
  78. package/lib/Bundle/index.js +1 -0
  79. package/lib/Bundle/index.js.map +1 -1
  80. package/lib/Cloudflare/Workers/Worker.d.ts +12 -0
  81. package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
  82. package/lib/Cloudflare/Workers/Worker.js +19 -4
  83. package/lib/Cloudflare/Workers/Worker.js.map +1 -1
  84. package/lib/Output.d.ts.map +1 -1
  85. package/lib/Output.js +1 -1
  86. package/lib/Output.js.map +1 -1
  87. package/lib/Plan.d.ts.map +1 -1
  88. package/lib/Plan.js +10 -4
  89. package/lib/Plan.js.map +1 -1
  90. package/lib/Resource.d.ts +2 -2
  91. package/lib/Resource.d.ts.map +1 -1
  92. package/lib/Telemetry/Attributes.d.ts +43 -0
  93. package/lib/Telemetry/Attributes.d.ts.map +1 -0
  94. package/lib/Telemetry/Attributes.js +144 -0
  95. package/lib/Telemetry/Attributes.js.map +1 -0
  96. package/lib/Telemetry/Layer.d.ts +14 -0
  97. package/lib/Telemetry/Layer.d.ts.map +1 -0
  98. package/lib/Telemetry/Layer.js +50 -0
  99. package/lib/Telemetry/Layer.js.map +1 -0
  100. package/lib/Telemetry/Metrics.d.ts +34 -0
  101. package/lib/Telemetry/Metrics.d.ts.map +1 -0
  102. package/lib/Telemetry/Metrics.js +57 -0
  103. package/lib/Telemetry/Metrics.js.map +1 -0
  104. package/package.json +12 -1
  105. package/src/Apply.ts +118 -9
  106. package/src/Axiom/Annotation.ts +95 -0
  107. package/src/Axiom/ApiToken.ts +125 -0
  108. package/src/Axiom/AuthProvider.ts +341 -0
  109. package/src/Axiom/Chart.ts +94 -0
  110. package/src/Axiom/Credentials.ts +70 -0
  111. package/src/Axiom/Dashboard.ts +192 -0
  112. package/src/Axiom/Dataset.ts +291 -0
  113. package/src/Axiom/Monitor.ts +122 -0
  114. package/src/Axiom/Notifier.ts +104 -0
  115. package/src/Axiom/Providers.ts +58 -0
  116. package/src/Axiom/View.ts +106 -0
  117. package/src/Axiom/VirtualField.ts +90 -0
  118. package/src/Axiom/index.ts +12 -0
  119. package/src/Bundle/Bundle.ts +39 -1
  120. package/src/Bundle/PurePlugin.ts +556 -0
  121. package/src/Bundle/index.ts +1 -0
  122. package/src/Cloudflare/Workers/Worker.ts +37 -6
  123. package/src/Output.ts +2 -1
  124. package/src/Plan.ts +13 -4
  125. package/src/Resource.ts +2 -2
  126. package/src/Telemetry/Attributes.ts +216 -0
  127. package/src/Telemetry/Layer.ts +65 -0
  128. package/src/Telemetry/Metrics.ts +101 -0
@@ -0,0 +1,104 @@
1
+ import * as Operations from "@distilled.cloud/axiom/Operations";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Provider from "../Provider.ts";
4
+ import { Resource } from "../Resource.ts";
5
+ import type { Providers } from "./Providers.ts";
6
+
7
+ export type NotifierProps = Operations.CreateNotifierInput;
8
+
9
+ export type Notifier = Resource<
10
+ "Axiom.Notifier",
11
+ NotifierProps,
12
+ Operations.CreateNotifierOutput & { id: string },
13
+ never,
14
+ Providers
15
+ >;
16
+
17
+ /**
18
+ * An Axiom notifier — an alert destination (Slack, email, PagerDuty,
19
+ * Opsgenie, Discord, Microsoft Teams, generic webhook, or a fully custom
20
+ * webhook with templated body/headers) that {@link Monitor monitors} target
21
+ * via `notifierIds`. Exactly one channel under `properties` should be set.
22
+ *
23
+ * @see https://axiom.co/docs/monitor-data/notifiers
24
+ *
25
+ * @section Creating a Notifier
26
+ * @example Slack incoming webhook
27
+ * ```typescript
28
+ * const slack = yield* Axiom.Notifier("ops-slack", {
29
+ * name: "ops-channel",
30
+ * properties: {
31
+ * slack: { slackUrl: process.env.SLACK_WEBHOOK_URL! },
32
+ * },
33
+ * });
34
+ * ```
35
+ *
36
+ * @example Email distribution list
37
+ * ```typescript
38
+ * yield* Axiom.Notifier("ops-email", {
39
+ * name: "ops-team",
40
+ * properties: { email: { emails: ["sre@example.com", "oncall@example.com"] } },
41
+ * });
42
+ * ```
43
+ *
44
+ * @example PagerDuty integration
45
+ * ```typescript
46
+ * yield* Axiom.Notifier("pagerduty", {
47
+ * name: "primary-oncall",
48
+ * properties: {
49
+ * pagerduty: { routingKey: process.env.PAGERDUTY_ROUTING_KEY!, token: "" },
50
+ * },
51
+ * });
52
+ * ```
53
+ *
54
+ * @example Custom webhook with templated body
55
+ * ```typescript
56
+ * yield* Axiom.Notifier("incident-webhook", {
57
+ * name: "incident.io",
58
+ * properties: {
59
+ * customWebhook: {
60
+ * url: "https://api.incident.io/v2/alert_events",
61
+ * headers: { "Content-Type": "application/json" },
62
+ * secretHeaders: { Authorization: `Bearer ${process.env.INCIDENT_TOKEN}` },
63
+ * body: '{"title": "{{.Monitor.Name}}", "status": "firing"}',
64
+ * },
65
+ * },
66
+ * });
67
+ * ```
68
+ */
69
+ export const Notifier = Resource<Notifier>("Axiom.Notifier");
70
+
71
+ export const NotifierProvider = () =>
72
+ Provider.effect(
73
+ Notifier,
74
+ Effect.gen(function* () {
75
+ const create = yield* Operations.createNotifier;
76
+ const update = yield* Operations.updateNotifier;
77
+ const get = yield* Operations.getNotifier;
78
+ const del = yield* Operations.deleteNotifier;
79
+
80
+ return {
81
+ stables: ["id"],
82
+ create: Effect.fn(function* ({ news }) {
83
+ const result = yield* create(news);
84
+ return { ...result, id: result.id ?? "" };
85
+ }),
86
+ update: Effect.fn(function* ({ news, output }) {
87
+ const result = yield* update({ ...news, id: output.id });
88
+ return { ...result, id: result.id ?? output.id };
89
+ }),
90
+ delete: Effect.fn(function* ({ output }) {
91
+ yield* del({ id: output.id }).pipe(
92
+ Effect.catchTag("NotFound", () => Effect.void),
93
+ );
94
+ }),
95
+ read: Effect.fn(function* ({ output }) {
96
+ if (!output?.id) return undefined;
97
+ return yield* get({ id: output.id }).pipe(
98
+ Effect.map((current) => ({ ...current, id: current.id ?? output.id })),
99
+ Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
100
+ );
101
+ }),
102
+ };
103
+ }),
104
+ );
@@ -0,0 +1,58 @@
1
+ import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
2
+ import * as Layer from "effect/Layer";
3
+ import * as Provider from "../Provider.ts";
4
+ import { Annotation, AnnotationProvider } from "./Annotation.ts";
5
+ import { ApiToken, ApiTokenProvider } from "./ApiToken.ts";
6
+ import { AxiomAuth } from "./AuthProvider.ts";
7
+ import * as Credentials from "./Credentials.ts";
8
+ import { Dashboard, DashboardProvider } from "./Dashboard.ts";
9
+ import { Dataset, DatasetProvider } from "./Dataset.ts";
10
+ import { Monitor, MonitorProvider } from "./Monitor.ts";
11
+ import { Notifier, NotifierProvider } from "./Notifier.ts";
12
+ import { View, ViewProvider } from "./View.ts";
13
+ import { VirtualField, VirtualFieldProvider } from "./VirtualField.ts";
14
+
15
+ export { Credentials } from "@distilled.cloud/axiom/Credentials";
16
+
17
+ export class Providers extends Provider.ProviderCollection<Providers>()(
18
+ "Axiom",
19
+ ) {}
20
+
21
+ export type ProviderRequirements = Layer.Services<ReturnType<typeof providers>>;
22
+
23
+ /**
24
+ * Axiom providers and credentials. Wires up Dataset, ApiToken, Notifier,
25
+ * Monitor, Dashboard, VirtualField, View and Annotation resources, and
26
+ * registers the Axiom AuthProvider so `alchemy login` can configure it.
27
+ */
28
+ export const providers = () =>
29
+ Layer.effect(
30
+ Providers,
31
+ Provider.collection([
32
+ Annotation,
33
+ ApiToken,
34
+ Dashboard,
35
+ Dataset,
36
+ Monitor,
37
+ Notifier,
38
+ View,
39
+ VirtualField,
40
+ ]),
41
+ ).pipe(
42
+ Layer.provide(
43
+ Layer.mergeAll(
44
+ AnnotationProvider(),
45
+ ApiTokenProvider(),
46
+ DashboardProvider(),
47
+ DatasetProvider(),
48
+ MonitorProvider(),
49
+ NotifierProvider(),
50
+ ViewProvider(),
51
+ VirtualFieldProvider(),
52
+ ),
53
+ ),
54
+ Layer.provideMerge(Credentials.fromAuthProvider()),
55
+ Layer.provideMerge(FetchHttpClient.layer),
56
+ Layer.provideMerge(AxiomAuth),
57
+ Layer.orDie,
58
+ );
@@ -0,0 +1,106 @@
1
+ import * as Operations from "@distilled.cloud/axiom/Operations";
2
+ import * as Effect from "effect/Effect";
3
+ import { isResolved } from "../Diff.ts";
4
+ import * as Provider from "../Provider.ts";
5
+ import { Resource } from "../Resource.ts";
6
+ import type { Providers } from "./Providers.ts";
7
+
8
+ export type ViewProps = Operations.CreateViewInput;
9
+
10
+ export type View = Resource<
11
+ "Axiom.View",
12
+ ViewProps,
13
+ Operations.CreateViewOutput & {
14
+ /**
15
+ * Path identifier used by `updateView` / `getView` / `deleteView`.
16
+ * Currently derived from `name` because Axiom's view list/get responses
17
+ * don't expose a separate id field.
18
+ */
19
+ id: string;
20
+ },
21
+ never,
22
+ Providers
23
+ >;
24
+
25
+ /**
26
+ * An Axiom saved view — a named, shareable APL query. Useful for building
27
+ * starter dashboards, providing canned "open in Axiom" links from your app,
28
+ * or pinning common investigations the team revisits.
29
+ *
30
+ * The path identifier is `name`. Renaming a view triggers a replacement
31
+ * (the old one is deleted, a new one is created).
32
+ *
33
+ * @see https://axiom.co/docs/query-data/datasets — APL query reference
34
+ *
35
+ * @section Creating a View
36
+ * @example Recent errors across one dataset
37
+ * ```typescript
38
+ * yield* Axiom.View("recent-errors", {
39
+ * name: "recent-errors",
40
+ * description: "Last 100 5xx responses",
41
+ * datasets: ["my-app-traces"],
42
+ * aplQuery: `
43
+ * ['my-app-traces']
44
+ * | where status >= 500
45
+ * | order by _time desc
46
+ * | take 100
47
+ * `,
48
+ * });
49
+ * ```
50
+ *
51
+ * @example Cross-dataset join (logs + traces by trace_id)
52
+ * ```typescript
53
+ * yield* Axiom.View("trace-with-logs", {
54
+ * name: "trace-with-logs",
55
+ * datasets: ["my-app-traces", "my-app-logs"],
56
+ * aplQuery: `
57
+ * ['my-app-traces']
58
+ * | where duration_ms > 1000
59
+ * | join kind=leftouter (['my-app-logs']) on trace_id
60
+ * `,
61
+ * });
62
+ * ```
63
+ */
64
+ export const View = Resource<View>("Axiom.View");
65
+
66
+ export const ViewProvider = () =>
67
+ Provider.effect(
68
+ View,
69
+ Effect.gen(function* () {
70
+ const create = yield* Operations.createView;
71
+ const update = yield* Operations.updateView;
72
+ const get = yield* Operations.getView;
73
+ const del = yield* Operations.deleteView;
74
+
75
+ return {
76
+ stables: ["id"],
77
+ diff: Effect.fn(function* ({ news, output }) {
78
+ if (!isResolved(news)) return undefined;
79
+ if (output && news.name !== output.name) {
80
+ return { action: "replace" } as const;
81
+ }
82
+ return undefined;
83
+ }),
84
+ create: Effect.fn(function* ({ news }) {
85
+ const result = yield* create(news);
86
+ return { ...result, id: news.name };
87
+ }),
88
+ update: Effect.fn(function* ({ news, output }) {
89
+ const result = yield* update({ ...news, id: output.id });
90
+ return { ...result, id: output.id };
91
+ }),
92
+ delete: Effect.fn(function* ({ output }) {
93
+ yield* del({ id: output.id }).pipe(
94
+ Effect.catchTag("NotFound", () => Effect.void),
95
+ );
96
+ }),
97
+ read: Effect.fn(function* ({ output }) {
98
+ if (!output?.id) return undefined;
99
+ return yield* get({ id: output.id }).pipe(
100
+ Effect.map((current) => ({ ...current, id: output.id })),
101
+ Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
102
+ );
103
+ }),
104
+ };
105
+ }),
106
+ );
@@ -0,0 +1,90 @@
1
+ import * as Operations from "@distilled.cloud/axiom/Operations";
2
+ import * as Effect from "effect/Effect";
3
+ import { isResolved } from "../Diff.ts";
4
+ import * as Provider from "../Provider.ts";
5
+ import { Resource } from "../Resource.ts";
6
+ import type { Providers } from "./Providers.ts";
7
+
8
+ export type VirtualFieldProps = Operations.CreateVirtualFieldInput;
9
+
10
+ export type VirtualField = Resource<
11
+ "Axiom.VirtualField",
12
+ VirtualFieldProps,
13
+ Operations.CreateVirtualFieldOutput,
14
+ never,
15
+ Providers
16
+ >;
17
+
18
+ /**
19
+ * An Axiom virtual field — a saved APL expression that appears as a derived
20
+ * column on a dataset at query time. Use these to standardise common
21
+ * computations (status classes, latency buckets, parsed JSON paths) so
22
+ * dashboards and monitors don't have to redefine them.
23
+ *
24
+ * Bound to a single `dataset`; changing the dataset triggers a replacement.
25
+ *
26
+ * @see https://axiom.co/docs/query-data/virtual-fields
27
+ *
28
+ * @section Creating a Virtual Field
29
+ * @example HTTP status class (e.g. 200 → "2xx")
30
+ * ```typescript
31
+ * yield* Axiom.VirtualField("status-class", {
32
+ * dataset: "my-app-traces",
33
+ * name: "status_class",
34
+ * description: "HTTP response class bucket",
35
+ * expression: 'strcat(tostring(toint(status / 100)), "xx")',
36
+ * type: "string",
37
+ * });
38
+ * ```
39
+ *
40
+ * @example Latency bucket in seconds
41
+ * ```typescript
42
+ * yield* Axiom.VirtualField("latency-bucket", {
43
+ * dataset: "my-app-traces",
44
+ * name: "latency_bucket_s",
45
+ * expression: "bin(duration_ms / 1000.0, 0.5)",
46
+ * type: "number",
47
+ * unit: "s",
48
+ * });
49
+ * ```
50
+ */
51
+ export const VirtualField = Resource<VirtualField>("Axiom.VirtualField");
52
+
53
+ export const VirtualFieldProvider = () =>
54
+ Provider.effect(
55
+ VirtualField,
56
+ Effect.gen(function* () {
57
+ const create = yield* Operations.createVirtualField;
58
+ const update = yield* Operations.updateVirtualField;
59
+ const get = yield* Operations.getVirtualField;
60
+ const del = yield* Operations.deleteVirtualField;
61
+
62
+ return {
63
+ stables: ["id"],
64
+ diff: Effect.fn(function* ({ news, output }) {
65
+ if (!isResolved(news)) return undefined;
66
+ if (output && news.dataset !== output.dataset) {
67
+ return { action: "replace" } as const;
68
+ }
69
+ return undefined;
70
+ }),
71
+ create: Effect.fn(function* ({ news }) {
72
+ return yield* create(news);
73
+ }),
74
+ update: Effect.fn(function* ({ news, output }) {
75
+ return yield* update({ ...news, id: output.id });
76
+ }),
77
+ delete: Effect.fn(function* ({ output }) {
78
+ yield* del({ id: output.id }).pipe(
79
+ Effect.catchTag("NotFound", () => Effect.void),
80
+ );
81
+ }),
82
+ read: Effect.fn(function* ({ output }) {
83
+ if (!output?.id) return undefined;
84
+ return yield* get({ id: output.id }).pipe(
85
+ Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
86
+ );
87
+ }),
88
+ };
89
+ }),
90
+ );
@@ -0,0 +1,12 @@
1
+ export * from "./Annotation.ts";
2
+ export * from "./ApiToken.ts";
3
+ export * from "./AuthProvider.ts";
4
+ export * from "./Chart.ts";
5
+ export * from "./Credentials.ts";
6
+ export * from "./Dashboard.ts";
7
+ export * from "./Dataset.ts";
8
+ export * from "./Monitor.ts";
9
+ export * from "./Notifier.ts";
10
+ export * from "./Providers.ts";
11
+ export * from "./View.ts";
12
+ export * from "./VirtualField.ts";
@@ -7,6 +7,25 @@ import * as Stream from "effect/Stream";
7
7
  import assert from "node:assert";
8
8
  import * as rolldown from "rolldown";
9
9
  import { sha256, sha256Object } from "../Util/sha256.ts";
10
+ import { purePlugin, type PurePluginOptions } from "./PurePlugin.ts";
11
+
12
+ /**
13
+ * Extra options accepted by {@link build} / {@link watch} on top of the
14
+ * standard rolldown input/output options.
15
+ */
16
+ export interface BundleExtraOptions {
17
+ /**
18
+ * Configures the {@link purePlugin} which annotates top-level
19
+ * call/new expressions in matching packages with `/*#__PURE__*\/`
20
+ * so rolldown can tree-shake them.
21
+ *
22
+ * - `undefined` (default): plugin is enabled with default packages
23
+ * (`effect`, `@effect/*`).
24
+ * - `PurePluginOptions`: plugin is enabled with the provided options.
25
+ * - `false`: plugin is disabled.
26
+ */
27
+ readonly pure?: PurePluginOptions | false;
28
+ }
10
29
 
11
30
  export interface BundleOutput {
12
31
  /**
@@ -40,11 +59,13 @@ export class BundleError extends Data.TaggedError("BundleError")<{
40
59
  export const build = (
41
60
  inputOptions: rolldown.InputOptions,
42
61
  outputOptions?: rolldown.OutputOptions,
62
+ extra?: BundleExtraOptions,
43
63
  ): Effect.Effect<BundleOutput, BundleError> =>
44
64
  Effect.tryPromise({
45
65
  try: async () => {
46
66
  const bundle = await rolldown.rolldown({
47
67
  ...inputOptions,
68
+ plugins: withPurePlugin(inputOptions.plugins, extra?.pure),
48
69
  optimization: inputOptions.optimization ?? {
49
70
  inlineConst: {
50
71
  mode: "smart",
@@ -71,6 +92,7 @@ export const build = (
71
92
  export const watch = (
72
93
  inputOptions: rolldown.InputOptions,
73
94
  outputOptions?: rolldown.OutputOptions,
95
+ extra?: BundleExtraOptions,
74
96
  ): Stream.Stream<Result.Result<BundleOutput, BundleError>> =>
75
97
  Stream.callback<Result.Result<rolldown.OutputBundle, BundleError>>((queue) =>
76
98
  Effect.acquireRelease(
@@ -78,7 +100,7 @@ export const watch = (
78
100
  const watcher = rolldown.watch({
79
101
  ...inputOptions,
80
102
  plugins: [
81
- inputOptions.plugins,
103
+ withPurePlugin(inputOptions.plugins, extra?.pure),
82
104
  // The watcher event listener does not receive the bundle output, so we grab it using a plugin.
83
105
  {
84
106
  name: "alchemy:watch-bundle",
@@ -179,6 +201,22 @@ export function bundleOutputFromRolldownOutputBundle(
179
201
  ).pipe(Effect.flatMap(bundleOutputFromFiles));
180
202
  }
181
203
 
204
+ /**
205
+ * Composes the user-provided plugin chain with the default
206
+ * {@link purePlugin} according to the `pure` option.
207
+ *
208
+ * The pure plugin is appended LAST so it sees module ids that have already
209
+ * been resolved into `node_modules/<pkg>/...` by upstream resolver plugins
210
+ * such as `@distilled.cloud/cloudflare-rolldown-plugin`.
211
+ */
212
+ function withPurePlugin(
213
+ plugins: rolldown.RolldownPluginOption,
214
+ pure: PurePluginOptions | false | undefined,
215
+ ): rolldown.RolldownPluginOption {
216
+ if (pure === false) return plugins;
217
+ return [plugins, purePlugin(pure ?? {})];
218
+ }
219
+
182
220
  function bundleErrorFromUnknown(error: unknown): BundleError {
183
221
  const message = error instanceof Error ? error.message : String(error);
184
222
  return new BundleError({