alchemy 2.0.0-beta.17 → 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 (182) 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/AWS/CloudFront/CachePolicy.d.ts +106 -0
  13. package/lib/AWS/CloudFront/CachePolicy.d.ts.map +1 -0
  14. package/lib/AWS/CloudFront/CachePolicy.js +147 -0
  15. package/lib/AWS/CloudFront/CachePolicy.js.map +1 -0
  16. package/lib/AWS/CloudFront/KeyGroup.d.ts +72 -0
  17. package/lib/AWS/CloudFront/KeyGroup.d.ts.map +1 -0
  18. package/lib/AWS/CloudFront/KeyGroup.js +136 -0
  19. package/lib/AWS/CloudFront/KeyGroup.js.map +1 -0
  20. package/lib/AWS/CloudFront/OriginRequestPolicy.d.ts +84 -0
  21. package/lib/AWS/CloudFront/OriginRequestPolicy.d.ts.map +1 -0
  22. package/lib/AWS/CloudFront/OriginRequestPolicy.js +139 -0
  23. package/lib/AWS/CloudFront/OriginRequestPolicy.js.map +1 -0
  24. package/lib/AWS/CloudFront/PublicKey.d.ts +72 -0
  25. package/lib/AWS/CloudFront/PublicKey.d.ts.map +1 -0
  26. package/lib/AWS/CloudFront/PublicKey.js +137 -0
  27. package/lib/AWS/CloudFront/PublicKey.js.map +1 -0
  28. package/lib/AWS/CloudFront/ResponseHeadersPolicy.d.ts +114 -0
  29. package/lib/AWS/CloudFront/ResponseHeadersPolicy.d.ts.map +1 -0
  30. package/lib/AWS/CloudFront/ResponseHeadersPolicy.js +162 -0
  31. package/lib/AWS/CloudFront/ResponseHeadersPolicy.js.map +1 -0
  32. package/lib/AWS/CloudFront/index.d.ts +5 -0
  33. package/lib/AWS/CloudFront/index.d.ts.map +1 -1
  34. package/lib/AWS/CloudFront/index.js +5 -0
  35. package/lib/AWS/CloudFront/index.js.map +1 -1
  36. package/lib/AWS/ECS/CapacityProvider.d.ts +102 -0
  37. package/lib/AWS/ECS/CapacityProvider.d.ts.map +1 -0
  38. package/lib/AWS/ECS/CapacityProvider.js +217 -0
  39. package/lib/AWS/ECS/CapacityProvider.js.map +1 -0
  40. package/lib/AWS/ECS/index.d.ts +1 -0
  41. package/lib/AWS/ECS/index.d.ts.map +1 -1
  42. package/lib/AWS/ECS/index.js +1 -0
  43. package/lib/AWS/ECS/index.js.map +1 -1
  44. package/lib/AWS/Providers.d.ts.map +1 -1
  45. package/lib/AWS/Providers.js +7 -1
  46. package/lib/AWS/Providers.js.map +1 -1
  47. package/lib/Agent/lsp/client.d.ts +2 -2
  48. package/lib/Apply.d.ts.map +1 -1
  49. package/lib/Apply.js +41 -9
  50. package/lib/Apply.js.map +1 -1
  51. package/lib/Axiom/Annotation.d.ts +49 -0
  52. package/lib/Axiom/Annotation.d.ts.map +1 -0
  53. package/lib/Axiom/Annotation.js +74 -0
  54. package/lib/Axiom/Annotation.js.map +1 -0
  55. package/lib/Axiom/ApiToken.d.ts +62 -0
  56. package/lib/Axiom/ApiToken.d.ts.map +1 -0
  57. package/lib/Axiom/ApiToken.js +98 -0
  58. package/lib/Axiom/ApiToken.js.map +1 -0
  59. package/lib/Axiom/AuthProvider.d.ts +48 -0
  60. package/lib/Axiom/AuthProvider.d.ts.map +1 -0
  61. package/lib/Axiom/AuthProvider.js +177 -0
  62. package/lib/Axiom/AuthProvider.js.map +1 -0
  63. package/lib/Axiom/Chart.d.ts +72 -0
  64. package/lib/Axiom/Chart.d.ts.map +1 -0
  65. package/lib/Axiom/Chart.js +14 -0
  66. package/lib/Axiom/Chart.js.map +1 -0
  67. package/lib/Axiom/Credentials.d.ts +12 -0
  68. package/lib/Axiom/Credentials.d.ts.map +1 -0
  69. package/lib/Axiom/Credentials.js +34 -0
  70. package/lib/Axiom/Credentials.js.map +1 -0
  71. package/lib/Axiom/Dashboard.d.ts +124 -0
  72. package/lib/Axiom/Dashboard.d.ts.map +1 -0
  73. package/lib/Axiom/Dashboard.js +140 -0
  74. package/lib/Axiom/Dashboard.js.map +1 -0
  75. package/lib/Axiom/Dataset.d.ts +101 -0
  76. package/lib/Axiom/Dataset.d.ts.map +1 -0
  77. package/lib/Axiom/Dataset.js +183 -0
  78. package/lib/Axiom/Dataset.js.map +1 -0
  79. package/lib/Axiom/Monitor.d.ts +74 -0
  80. package/lib/Axiom/Monitor.d.ts.map +1 -0
  81. package/lib/Axiom/Monitor.js +103 -0
  82. package/lib/Axiom/Monitor.js.map +1 -0
  83. package/lib/Axiom/Notifier.d.ts +63 -0
  84. package/lib/Axiom/Notifier.d.ts.map +1 -0
  85. package/lib/Axiom/Notifier.js +83 -0
  86. package/lib/Axiom/Notifier.js.map +1 -0
  87. package/lib/Axiom/Providers.d.ts +15 -0
  88. package/lib/Axiom/Providers.d.ts.map +1 -0
  89. package/lib/Axiom/Providers.js +32 -0
  90. package/lib/Axiom/Providers.js.map +1 -0
  91. package/lib/Axiom/View.d.ts +55 -0
  92. package/lib/Axiom/View.d.ts.map +1 -0
  93. package/lib/Axiom/View.js +79 -0
  94. package/lib/Axiom/View.js.map +1 -0
  95. package/lib/Axiom/VirtualField.d.ts +42 -0
  96. package/lib/Axiom/VirtualField.d.ts.map +1 -0
  97. package/lib/Axiom/VirtualField.js +71 -0
  98. package/lib/Axiom/VirtualField.js.map +1 -0
  99. package/lib/Axiom/index.d.ts +13 -0
  100. package/lib/Axiom/index.d.ts.map +1 -0
  101. package/lib/Axiom/index.js +13 -0
  102. package/lib/Axiom/index.js.map +1 -0
  103. package/lib/Bundle/Bundle.d.ts +20 -2
  104. package/lib/Bundle/Bundle.d.ts.map +1 -1
  105. package/lib/Bundle/Bundle.js +18 -3
  106. package/lib/Bundle/Bundle.js.map +1 -1
  107. package/lib/Bundle/PurePlugin.d.ts +109 -0
  108. package/lib/Bundle/PurePlugin.d.ts.map +1 -0
  109. package/lib/Bundle/PurePlugin.js +469 -0
  110. package/lib/Bundle/PurePlugin.js.map +1 -0
  111. package/lib/Bundle/index.d.ts +1 -0
  112. package/lib/Bundle/index.d.ts.map +1 -1
  113. package/lib/Bundle/index.js +1 -0
  114. package/lib/Bundle/index.js.map +1 -1
  115. package/lib/Cloudflare/Workers/Worker.d.ts +12 -0
  116. package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
  117. package/lib/Cloudflare/Workers/Worker.js +19 -4
  118. package/lib/Cloudflare/Workers/Worker.js.map +1 -1
  119. package/lib/GitHub/Secrets.d.ts +45 -0
  120. package/lib/GitHub/Secrets.d.ts.map +1 -0
  121. package/lib/GitHub/Secrets.js +30 -0
  122. package/lib/GitHub/Secrets.js.map +1 -0
  123. package/lib/GitHub/index.d.ts +1 -0
  124. package/lib/GitHub/index.d.ts.map +1 -1
  125. package/lib/GitHub/index.js +1 -0
  126. package/lib/GitHub/index.js.map +1 -1
  127. package/lib/Output.d.ts.map +1 -1
  128. package/lib/Output.js +1 -1
  129. package/lib/Output.js.map +1 -1
  130. package/lib/Plan.d.ts.map +1 -1
  131. package/lib/Plan.js +10 -4
  132. package/lib/Plan.js.map +1 -1
  133. package/lib/Resource.d.ts +2 -2
  134. package/lib/Resource.d.ts.map +1 -1
  135. package/lib/Telemetry/Attributes.d.ts +43 -0
  136. package/lib/Telemetry/Attributes.d.ts.map +1 -0
  137. package/lib/Telemetry/Attributes.js +144 -0
  138. package/lib/Telemetry/Attributes.js.map +1 -0
  139. package/lib/Telemetry/Layer.d.ts +14 -0
  140. package/lib/Telemetry/Layer.d.ts.map +1 -0
  141. package/lib/Telemetry/Layer.js +50 -0
  142. package/lib/Telemetry/Layer.js.map +1 -0
  143. package/lib/Telemetry/Metrics.d.ts +34 -0
  144. package/lib/Telemetry/Metrics.d.ts.map +1 -0
  145. package/lib/Telemetry/Metrics.js +57 -0
  146. package/lib/Telemetry/Metrics.js.map +1 -0
  147. package/package.json +12 -1
  148. package/src/AWS/CloudFront/CachePolicy.ts +271 -0
  149. package/src/AWS/CloudFront/KeyGroup.ts +233 -0
  150. package/src/AWS/CloudFront/OriginRequestPolicy.ts +256 -0
  151. package/src/AWS/CloudFront/PublicKey.ts +245 -0
  152. package/src/AWS/CloudFront/ResponseHeadersPolicy.ts +308 -0
  153. package/src/AWS/CloudFront/index.ts +11 -0
  154. package/src/AWS/ECS/CapacityProvider.ts +336 -0
  155. package/src/AWS/ECS/index.ts +4 -0
  156. package/src/AWS/Providers.ts +12 -0
  157. package/src/Apply.ts +118 -9
  158. package/src/Axiom/Annotation.ts +95 -0
  159. package/src/Axiom/ApiToken.ts +125 -0
  160. package/src/Axiom/AuthProvider.ts +341 -0
  161. package/src/Axiom/Chart.ts +94 -0
  162. package/src/Axiom/Credentials.ts +70 -0
  163. package/src/Axiom/Dashboard.ts +192 -0
  164. package/src/Axiom/Dataset.ts +291 -0
  165. package/src/Axiom/Monitor.ts +122 -0
  166. package/src/Axiom/Notifier.ts +104 -0
  167. package/src/Axiom/Providers.ts +58 -0
  168. package/src/Axiom/View.ts +106 -0
  169. package/src/Axiom/VirtualField.ts +90 -0
  170. package/src/Axiom/index.ts +12 -0
  171. package/src/Bundle/Bundle.ts +39 -1
  172. package/src/Bundle/PurePlugin.ts +556 -0
  173. package/src/Bundle/index.ts +1 -0
  174. package/src/Cloudflare/Workers/Worker.ts +37 -6
  175. package/src/GitHub/Secrets.ts +66 -0
  176. package/src/GitHub/index.ts +1 -0
  177. package/src/Output.ts +2 -1
  178. package/src/Plan.ts +13 -4
  179. package/src/Resource.ts +2 -2
  180. package/src/Telemetry/Attributes.ts +216 -0
  181. package/src/Telemetry/Layer.ts +65 -0
  182. package/src/Telemetry/Metrics.ts +101 -0
@@ -0,0 +1,556 @@
1
+ import type {
2
+ CallExpression,
3
+ ExportDefaultDeclaration,
4
+ ExportNamedDeclaration,
5
+ Expression,
6
+ ExpressionStatement,
7
+ NewExpression,
8
+ Program,
9
+ Statement,
10
+ VariableDeclaration,
11
+ } from "@oxc-project/types";
12
+ import MagicString from "magic-string";
13
+ import * as fs from "node:fs/promises";
14
+ import * as path from "node:path";
15
+ import picomatch from "picomatch";
16
+ import type * as rolldown from "rolldown";
17
+ import { parseAst } from "rolldown/parseAst";
18
+
19
+ /**
20
+ * Default packages whose modules will receive `/*#__PURE__*\/` annotations.
21
+ * Mirrors what `effect-smol` ships via `babel-plugin-annotate-pure-calls`
22
+ * applied to its own `dist/` output.
23
+ *
24
+ * `alchemy` is included so its own resources (which consist almost entirely
25
+ * of `Effect.fn(...)`, `Context.Service(...)(...)`, `Layer.effect(...)` and
26
+ * `Data.TaggedError(...)` top-level calls) become tree-shakeable. The
27
+ * package already declares `"sideEffects": false`, so it is safe.
28
+ */
29
+ export const DEFAULT_PURE_PACKAGES: ReadonlyArray<string> = [
30
+ "effect",
31
+ "@effect/*",
32
+ "alchemy",
33
+ "@alchemy.run/*",
34
+ ];
35
+
36
+ /**
37
+ * Options for {@link purePlugin}.
38
+ */
39
+ export interface PurePluginOptions {
40
+ /**
41
+ * Extra package names or globs to annotate, in addition to
42
+ * {@link DEFAULT_PURE_PACKAGES}. Globs are matched with picomatch
43
+ * against the package name (e.g. `effect`, `@effect/cluster`).
44
+ */
45
+ readonly packages?: ReadonlyArray<string>;
46
+ /**
47
+ * If true, the configured `packages` list replaces the defaults
48
+ * entirely instead of extending them.
49
+ * @default false
50
+ */
51
+ readonly replaceDefaults?: boolean;
52
+ /**
53
+ * If true, also marks matched modules as side-effect free
54
+ * (mirrors `"sideEffects": []` in package.json), so rolldown drops
55
+ * unused re-exports from those packages.
56
+ * @default true
57
+ */
58
+ readonly markSideEffectFree?: boolean;
59
+ /**
60
+ * If true, automatically detect the npm package that owns the bundle
61
+ * entry (by walking up to the nearest `package.json`) and annotate it
62
+ * too. This makes the user's own source tree-shakeable without any
63
+ * configuration.
64
+ *
65
+ * The package's `sideEffects` field is NOT consulted for this gate —
66
+ * if your entry's package has init-time side effects you wish to
67
+ * preserve, declare them in `package.json` (`"sideEffects": ["./foo.ts"]`)
68
+ * or set this option to `false`.
69
+ *
70
+ * @default true
71
+ */
72
+ readonly autoDetectEntryPackage?: boolean;
73
+ }
74
+
75
+ const PURE_COMMENT = "/*#__PURE__*/ ";
76
+ const SUPPORTED_FILE_RE = /\.(?:m?[jt]sx?|cjs|cts)$/;
77
+
78
+ /**
79
+ * Rolldown plugin that injects `/*#__PURE__*\/` annotations on top-level
80
+ * call/new expressions of modules belonging to the configured packages,
81
+ * enabling tree-shaking of `effect`, `@effect/*`, and any user-listed
82
+ * packages without requiring a babel post-build pass.
83
+ */
84
+ export const purePlugin = (
85
+ options: PurePluginOptions = {},
86
+ ): rolldown.Plugin => {
87
+ const patterns = options.replaceDefaults
88
+ ? [...(options.packages ?? DEFAULT_PURE_PACKAGES)]
89
+ : [...DEFAULT_PURE_PACKAGES, ...(options.packages ?? [])];
90
+ const markSideEffectFreeOpt = options.markSideEffectFree ?? true;
91
+ const autoDetect = options.autoDetectEntryPackage ?? true;
92
+ // Mutable so the `options` hook can append the auto-detected entry
93
+ // package without rebuilding the plugin instance.
94
+ let isMatch = picomatch(patterns);
95
+ // Per-bundle cache of directory -> owning package metadata.
96
+ // Avoids walking the filesystem for every module of a large package.
97
+ const pkgInfoCache = new Map<string, PackageInfo | null>();
98
+ // Resolved absolute paths of entry modules. Marking an entry module as
99
+ // `moduleSideEffects: false` makes rolldown treat its top-level
100
+ // statements (including `console.log` etc.) as eliminable, often
101
+ // collapsing the whole bundle. We always preserve entries' side
102
+ // effects, regardless of the package they belong to.
103
+ const entryPaths = new Set<string>();
104
+
105
+ return {
106
+ name: "alchemy:annotate-pure",
107
+ async options(opts) {
108
+ collectEntryPaths(opts, entryPaths);
109
+ if (!autoDetect) return null;
110
+ const detected = await detectEntryPackage(opts);
111
+ if (detected === null) return null;
112
+ if (patterns.includes(detected)) return null;
113
+ patterns.push(detected);
114
+ isMatch = picomatch(patterns);
115
+ return null;
116
+ },
117
+ transform: {
118
+ filter: { id: SUPPORTED_FILE_RE },
119
+ async handler(code, id) {
120
+ const info = await resolvePackageInfo(id, pkgInfoCache);
121
+ if (info === null || info.name === null || !isMatch(info.name)) {
122
+ return null;
123
+ }
124
+ const cleanId = id.replace(/[?#].*$/, "");
125
+ const isEntry = entryPaths.has(cleanId);
126
+ // Only override `moduleSideEffects` when the owning package
127
+ // explicitly opts in via `sideEffects: false` / `[]`. Pure
128
+ // annotations themselves are always safe to add (they only mean
129
+ // "if the result is unused, the call may be dropped"), but
130
+ // marking arbitrary modules side-effect-free could erase
131
+ // intentional registrations / mutations the author made at the
132
+ // top level of files in packages that did not declare so.
133
+ const markSideEffectFree =
134
+ markSideEffectFreeOpt && !isEntry && isSideEffectFree(info.sideEffects);
135
+ const annotated = annotateModule(code, id);
136
+ if (annotated === null) {
137
+ return markSideEffectFree
138
+ ? { code, moduleSideEffects: false }
139
+ : null;
140
+ }
141
+ return {
142
+ code: annotated.code,
143
+ map: annotated.map,
144
+ moduleSideEffects: markSideEffectFree ? false : null,
145
+ };
146
+ },
147
+ },
148
+ };
149
+ };
150
+
151
+ /**
152
+ * Captures the absolute paths of every entry module declared on the
153
+ * rolldown input options into `out`. Handles all three input shapes:
154
+ * string, array of strings, and `{ name -> path }` record.
155
+ */
156
+ function collectEntryPaths(
157
+ opts: rolldown.InputOptions,
158
+ out: Set<string>,
159
+ ): void {
160
+ const cwd = opts.cwd ?? process.cwd();
161
+ const add = (entry: unknown) => {
162
+ if (typeof entry !== "string") return;
163
+ out.add(path.resolve(cwd, entry));
164
+ };
165
+ if (typeof opts.input === "string") add(opts.input);
166
+ else if (Array.isArray(opts.input)) for (const e of opts.input) add(e);
167
+ else if (opts.input && typeof opts.input === "object") {
168
+ for (const e of Object.values(opts.input)) add(e);
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Walks up from each entry path (and `cwd`) to find the nearest
174
+ * `package.json`, and returns its `name` if the package is safely
175
+ * annotatable — i.e. it declares `sideEffects: false` or `[]`. Returns
176
+ * `null` if no entry package can be found, the entry has no name, or the
177
+ * package opts out of side-effect-free treatment.
178
+ */
179
+ async function detectEntryPackage(
180
+ opts: rolldown.InputOptions,
181
+ ): Promise<string | null> {
182
+ const inputs: string[] = [];
183
+ if (typeof opts.input === "string") inputs.push(opts.input);
184
+ else if (Array.isArray(opts.input)) inputs.push(...opts.input);
185
+ else if (opts.input && typeof opts.input === "object") {
186
+ inputs.push(...(Object.values(opts.input) as string[]));
187
+ }
188
+ if (opts.cwd) inputs.push(opts.cwd);
189
+ if (inputs.length === 0) inputs.push(process.cwd());
190
+
191
+ for (const candidate of inputs) {
192
+ const meta = await findOwningPackageMeta(candidate);
193
+ if (meta === null || meta.name === null) continue;
194
+ return meta.name;
195
+ }
196
+ return null;
197
+ }
198
+
199
+ /**
200
+ * Metadata extracted from a `package.json`. `name` may be `null` when the
201
+ * file exists but has no `"name"` field (rare, but possible for private
202
+ * subpackage roots).
203
+ */
204
+ export interface PackageInfo {
205
+ readonly name: string | null;
206
+ readonly sideEffects: unknown;
207
+ }
208
+
209
+ async function findOwningPackageMeta(
210
+ start: string,
211
+ ): Promise<PackageInfo | null> {
212
+ // Resolve to absolute and start from the directory containing `start`
213
+ // (or `start` itself if it is already a directory).
214
+ let dir = path.resolve(start);
215
+ try {
216
+ if ((await fs.stat(dir)).isFile()) dir = path.dirname(dir);
217
+ } catch {
218
+ dir = path.dirname(dir);
219
+ }
220
+ for (let i = 0; i < 64; i++) {
221
+ const pj = path.join(dir, "package.json");
222
+ try {
223
+ const stat = await fs.stat(pj);
224
+ if (stat.isFile()) {
225
+ const contents = await fs.readFile(pj, "utf8");
226
+ const json = JSON.parse(contents) as {
227
+ name?: unknown;
228
+ sideEffects?: unknown;
229
+ };
230
+ return {
231
+ name: typeof json.name === "string" ? json.name : null,
232
+ sideEffects: json.sideEffects,
233
+ };
234
+ }
235
+ } catch {
236
+ // keep climbing
237
+ }
238
+ const parent = path.dirname(dir);
239
+ if (parent === dir) break;
240
+ dir = parent;
241
+ }
242
+ return null;
243
+ }
244
+
245
+ /**
246
+ * Mirrors how rolldown / rollup interpret the `sideEffects` package.json
247
+ * field. We only treat `false` and `[]` as "fully side-effect free"; an
248
+ * array of files or `true` is treated as not safe to auto-annotate the
249
+ * whole package.
250
+ */
251
+ function isSideEffectFree(value: unknown): boolean {
252
+ if (value === false) return true;
253
+ if (Array.isArray(value) && value.length === 0) return true;
254
+ return false;
255
+ }
256
+
257
+ /**
258
+ * Extracts the npm package name from a resolved module id by walking back to
259
+ * the last `node_modules/` segment. Handles scoped packages. This is the
260
+ * fast path that does not hit the filesystem; it works for ordinary
261
+ * `node_modules/<pkg>/...` ids but NOT for workspace-linked sources whose
262
+ * resolved path is e.g. `<repo>/packages/alchemy/src/Bundle/PurePlugin.ts`.
263
+ *
264
+ * @example
265
+ * packageNameFromId("/proj/node_modules/effect/dist/Effect.js")
266
+ * // => "effect"
267
+ * packageNameFromId("/proj/node_modules/@effect/cluster/dist/index.js")
268
+ * // => "@effect/cluster"
269
+ */
270
+ export function packageNameFromId(id: string): string | null {
271
+ const normalized = id.replace(/\\/g, "/");
272
+ const idx = normalized.lastIndexOf("/node_modules/");
273
+ if (idx === -1) return null;
274
+ const rest = normalized.slice(idx + "/node_modules/".length);
275
+ const parts = rest.split("/");
276
+ if (parts.length === 0 || parts[0] === "") return null;
277
+ if (parts[0].startsWith("@")) {
278
+ if (parts.length < 2) return null;
279
+ return `${parts[0]}/${parts[1]}`;
280
+ }
281
+ return parts[0];
282
+ }
283
+
284
+ /**
285
+ * Resolves the owning {@link PackageInfo} for a module id by walking up
286
+ * the directory tree to the nearest `package.json`. This is what makes
287
+ * the plugin work for workspace-linked sources (e.g. our own
288
+ * `packages/alchemy/src/**` when consumers import via the `worker`/`bun`
289
+ * conditions which resolve to `.ts`).
290
+ *
291
+ * Caches both positive and negative results per directory to keep the
292
+ * filesystem walk bounded across an entire bundle pass.
293
+ */
294
+ export async function resolvePackageInfo(
295
+ id: string,
296
+ cache: Map<string, PackageInfo | null>,
297
+ ): Promise<PackageInfo | null> {
298
+ // Strip rolldown's `?query` / `#hash` suffixes if present.
299
+ const cleanId = id.replace(/[?#].*$/, "");
300
+
301
+ // Fast path: id matches `node_modules/<pkg>/...`. We still try to read
302
+ // the package's `package.json` to learn its `sideEffects` field, but if
303
+ // the file is unreadable (or this is a virtual id from a test) we fall
304
+ // back to a minimal record using the path-derived name.
305
+ const fastName = packageNameFromId(cleanId);
306
+
307
+ let dir = path.dirname(cleanId);
308
+ // Remember dirs we visited for this lookup so we can backfill their
309
+ // cache entries — but ONLY descendants of the resolved package root.
310
+ // Caching shared ancestors like `/proj/node_modules` would poison
311
+ // sibling packages (e.g. effect and lodash both live under it).
312
+ const visited: string[] = [];
313
+ let foundRoot: string | null = null;
314
+ let foundInfo: PackageInfo | null = null;
315
+ // Hard ceiling to prevent runaway walks on weird ids.
316
+ for (let i = 0; i < 64; i++) {
317
+ const cached = cache.get(dir);
318
+ if (cached !== undefined) {
319
+ cacheDescendants(cache, visited, dir, cached);
320
+ return cached;
321
+ }
322
+ visited.push(dir);
323
+ const pkgJsonPath = path.join(dir, "package.json");
324
+ let info: PackageInfo | null = null;
325
+ try {
326
+ const stat = await fs.stat(pkgJsonPath);
327
+ if (stat.isFile()) {
328
+ const contents = await fs.readFile(pkgJsonPath, "utf8");
329
+ const json = JSON.parse(contents) as {
330
+ name?: unknown;
331
+ sideEffects?: unknown;
332
+ };
333
+ info = {
334
+ name: typeof json.name === "string" ? json.name : null,
335
+ sideEffects: json.sideEffects,
336
+ };
337
+ }
338
+ } catch {
339
+ // package.json missing or unreadable — keep climbing.
340
+ }
341
+ if (info !== null) {
342
+ foundRoot = dir;
343
+ foundInfo = info;
344
+ break;
345
+ }
346
+ const parent = path.dirname(dir);
347
+ if (parent === dir) break;
348
+ dir = parent;
349
+ }
350
+ if (foundInfo !== null && foundRoot !== null) {
351
+ cacheDescendants(cache, visited, foundRoot, foundInfo);
352
+ return foundInfo;
353
+ }
354
+ // No `package.json` found on disk. If we have a path-derived name
355
+ // (from a `node_modules/<pkg>/...` segment), use it with no
356
+ // `sideEffects` info — annotation can still happen, the override is
357
+ // gated separately. Cache only the directly-owning dir so we don't
358
+ // poison sibling lookups under the same `node_modules`.
359
+ if (fastName !== null) {
360
+ const fallback: PackageInfo = { name: fastName, sideEffects: undefined };
361
+ if (visited[0]) cache.set(visited[0], fallback);
362
+ return fallback;
363
+ }
364
+ if (visited[0]) cache.set(visited[0], null);
365
+ return null;
366
+ }
367
+
368
+ /**
369
+ * Caches `info` only at directories that are at or below `root` — i.e.
370
+ * directories that genuinely belong to the same package. Dirs above the
371
+ * package root are shared with siblings (e.g. `node_modules`) and must
372
+ * not be tagged.
373
+ */
374
+ function cacheDescendants(
375
+ cache: Map<string, PackageInfo | null>,
376
+ visited: string[],
377
+ root: string,
378
+ info: PackageInfo | null,
379
+ ): void {
380
+ for (const v of visited) {
381
+ if (v === root || isPathInside(v, root)) cache.set(v, info);
382
+ }
383
+ }
384
+
385
+ /**
386
+ * `true` if `child` is `parent` or a directory contained beneath it.
387
+ * Path-only check; does not touch the filesystem.
388
+ */
389
+ function isPathInside(child: string, parent: string): boolean {
390
+ if (child === parent) return true;
391
+ const rel = path.relative(parent, child);
392
+ return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
393
+ }
394
+
395
+ /**
396
+ * Convenience wrapper that returns just the package name. Equivalent to
397
+ * `resolvePackageInfo(...)?.name ?? packageNameFromId(id)`.
398
+ */
399
+ export async function resolvePackageName(
400
+ id: string,
401
+ cache: Map<string, string | null>,
402
+ ): Promise<string | null> {
403
+ const cleanId = id.replace(/[?#].*$/, "");
404
+ const fast = packageNameFromId(cleanId);
405
+ if (fast !== null) return fast;
406
+ // Adapter cache so callers can keep a single-value cache shape.
407
+ const adapter = new Map<string, PackageInfo | null>();
408
+ for (const [k, v] of cache) {
409
+ adapter.set(k, v === null ? null : { name: v, sideEffects: undefined });
410
+ }
411
+ const info = await resolvePackageInfo(cleanId, adapter);
412
+ for (const [k, v] of adapter) {
413
+ cache.set(k, v?.name ?? null);
414
+ }
415
+ return info?.name ?? null;
416
+ }
417
+
418
+ interface AnnotatedModule {
419
+ readonly code: string;
420
+ readonly map: ReturnType<MagicString["generateMap"]>;
421
+ }
422
+
423
+ /**
424
+ * Parses `code` and inserts `/*#__PURE__*\/` before every top-level
425
+ * `CallExpression` / `NewExpression` callee. Returns `null` if the file
426
+ * does not need to be modified (parse failure or no annotations added).
427
+ */
428
+ export function annotateModule(
429
+ code: string,
430
+ filename: string,
431
+ ): AnnotatedModule | null {
432
+ let program: Program;
433
+ try {
434
+ // Use TS lang so the parser tolerates TS syntax (`as`, `satisfies`,
435
+ // non-null assertions) even when scanning published .js dist files
436
+ // — TS is a strict superset of JS for our purposes here.
437
+ program = parseAst(code, { sourceType: "module", lang: "ts" }, filename);
438
+ } catch {
439
+ return null;
440
+ }
441
+
442
+ const s = new MagicString(code);
443
+ let mutated = false;
444
+
445
+ const annotateCall = (call: CallExpression | NewExpression) => {
446
+ if (isIIFE(call)) return;
447
+ // For `new X()`, anchor BEFORE the `new` keyword so we get
448
+ // `/*#__PURE__*/ new X()` (matches babel-plugin-annotate-pure-calls).
449
+ const anchor = call.type === "NewExpression" ? call.start : call.callee.start;
450
+ if (alreadyAnnotated(code, anchor)) return;
451
+ s.appendLeft(anchor, PURE_COMMENT);
452
+ mutated = true;
453
+ };
454
+
455
+ const annotateExpression = (expr: Expression | null | undefined) => {
456
+ if (!expr) return;
457
+ switch (expr.type) {
458
+ case "CallExpression":
459
+ case "NewExpression":
460
+ annotateCall(expr);
461
+ return;
462
+ case "SequenceExpression":
463
+ for (const inner of expr.expressions) annotateExpression(inner);
464
+ return;
465
+ case "ParenthesizedExpression":
466
+ annotateExpression(expr.expression);
467
+ return;
468
+ case "LogicalExpression":
469
+ annotateExpression(expr.left);
470
+ annotateExpression(expr.right);
471
+ return;
472
+ case "ConditionalExpression":
473
+ annotateExpression(expr.consequent);
474
+ annotateExpression(expr.alternate);
475
+ return;
476
+ case "AssignmentExpression":
477
+ annotateExpression(expr.right);
478
+ return;
479
+ case "TSAsExpression":
480
+ case "TSSatisfiesExpression":
481
+ case "TSNonNullExpression":
482
+ case "TSTypeAssertion":
483
+ annotateExpression(expr.expression);
484
+ return;
485
+ case "ChainExpression": {
486
+ const inner = expr.expression;
487
+ if (inner.type === "CallExpression") annotateCall(inner);
488
+ return;
489
+ }
490
+ default:
491
+ return;
492
+ }
493
+ };
494
+
495
+ const visitTopLevel = (node: Statement) => {
496
+ switch (node.type) {
497
+ case "ExpressionStatement":
498
+ annotateExpression((node as ExpressionStatement).expression);
499
+ return;
500
+ case "VariableDeclaration":
501
+ for (const decl of (node as VariableDeclaration).declarations) {
502
+ annotateExpression(decl.init);
503
+ }
504
+ return;
505
+ case "ExportNamedDeclaration": {
506
+ const decl = (node as ExportNamedDeclaration).declaration;
507
+ if (decl) visitTopLevel(decl as Statement);
508
+ return;
509
+ }
510
+ case "ExportDefaultDeclaration": {
511
+ const decl = (node as ExportDefaultDeclaration).declaration;
512
+ if (
513
+ decl &&
514
+ decl.type !== "FunctionDeclaration" &&
515
+ decl.type !== "ClassDeclaration" &&
516
+ decl.type !== "TSInterfaceDeclaration"
517
+ ) {
518
+ annotateExpression(decl as Expression);
519
+ }
520
+ return;
521
+ }
522
+ default:
523
+ return;
524
+ }
525
+ };
526
+
527
+ for (const node of program.body) {
528
+ // Directive nodes (e.g. "use strict") have type "ExpressionStatement"
529
+ // with a `directive` field and a string literal expression — they
530
+ // cannot contain calls, so visiting them is a safe no-op.
531
+ visitTopLevel(node as Statement);
532
+ }
533
+
534
+ if (!mutated) return null;
535
+ return {
536
+ code: s.toString(),
537
+ map: s.generateMap({ source: filename, hires: true, includeContent: true }),
538
+ };
539
+ }
540
+
541
+ function isIIFE(node: CallExpression | NewExpression): boolean {
542
+ let callee: Expression = node.callee;
543
+ while (callee.type === "ParenthesizedExpression") {
544
+ callee = callee.expression;
545
+ }
546
+ return (
547
+ callee.type === "FunctionExpression" ||
548
+ callee.type === "ArrowFunctionExpression"
549
+ );
550
+ }
551
+
552
+ function alreadyAnnotated(code: string, pos: number): boolean {
553
+ const start = Math.max(0, pos - 32);
554
+ const slice = code.slice(start, pos);
555
+ return slice.includes("/*#__PURE__*/") || slice.includes("/*@__PURE__*/");
556
+ }
@@ -1,3 +1,4 @@
1
1
  export * from "./Bundle.ts";
2
2
  export * from "./Docker.ts";
3
+ export * from "./PurePlugin.ts";
3
4
  export * from "./TempRoot.ts";
@@ -272,6 +272,17 @@ export interface WorkerProps<
272
272
  * @default false
273
273
  */
274
274
  metafile?: boolean;
275
+ /**
276
+ * Configures the {@link Bundle.purePlugin} which annotates top-level
277
+ * call/new expressions in matching packages with `/*#__PURE__*\/`
278
+ * so rolldown can tree-shake them.
279
+ *
280
+ * - `undefined` (default): plugin is enabled with default packages
281
+ * (`effect`, `@effect/*`).
282
+ * - `PurePluginOptions`: plugin is enabled with the provided options.
283
+ * - `false`: plugin is disabled.
284
+ */
285
+ pure?: Bundle.BundleExtraOptions["pure"];
275
286
  };
276
287
  }
277
288
 
@@ -751,8 +762,19 @@ export const Worker: Platform<
751
762
  Effect.map((json) => {
752
763
  try {
753
764
  const value = JSON.parse(json);
754
- if (!Redacted.isRedacted(value)) {
755
- return Redacted.make(value.value);
765
+ // The `set` path serializes Redacted values as
766
+ // `{_tag: "Redacted", value: ...}`. After JSON.parse the
767
+ // result is a plain object — `Redacted.isRedacted` would
768
+ // always return `false` on it — so detect the marker shape
769
+ // and rebuild the Redacted wrapper. Plain values pass
770
+ // through unchanged.
771
+ if (
772
+ value !== null &&
773
+ typeof value === "object" &&
774
+ (value as { _tag?: unknown })._tag === "Redacted" &&
775
+ "value" in (value as object)
776
+ ) {
777
+ return Redacted.make((value as { value: unknown }).value);
756
778
  }
757
779
  return value;
758
780
  } catch {
@@ -763,13 +785,21 @@ export const Worker: Platform<
763
785
  set: (id: string, output: Output.Output) =>
764
786
  Effect.sync(() => {
765
787
  const key = id.replaceAll(/[^a-zA-Z0-9]/g, "_");
788
+ // Preserve `Redacted`-ness across the Output → env → Cloudflare
789
+ // binding boundary so the put-worker loop can deploy secrets via
790
+ // `secret_text` instead of leaking them as `plain_text`. The JSON
791
+ // payload still carries the `{_tag: "Redacted", …}` marker so the
792
+ // runtime `get` accessor can rebuild the wrapper after Cloudflare
793
+ // hands the binding back as a plain string.
766
794
  env[key] = output.pipe(
767
795
  Output.map((value) =>
768
796
  Redacted.isRedacted(value)
769
- ? JSON.stringify({
770
- _tag: "Redacted",
771
- value: Redacted.value(value),
772
- })
797
+ ? Redacted.make(
798
+ JSON.stringify({
799
+ _tag: "Redacted",
800
+ value: Redacted.value(value),
801
+ }),
802
+ )
773
803
  : JSON.stringify(value),
774
804
  ),
775
805
  );
@@ -1226,6 +1256,7 @@ export const WorkerProvider = () =>
1226
1256
  keepNames: true,
1227
1257
  dir: `.alchemy/bundles/${id}`,
1228
1258
  },
1259
+ { pure: props.build?.pure },
1229
1260
  );
1230
1261
 
1231
1262
  if (props.isExternal) {
@@ -0,0 +1,66 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Redacted from "effect/Redacted";
3
+
4
+ import type { Input } from "../Input.ts";
5
+ import { Secret } from "./Secret.ts";
6
+
7
+ export interface SecretsProps {
8
+ /**
9
+ * Repository owner (user or organization).
10
+ */
11
+ owner: string;
12
+
13
+ /**
14
+ * Repository name.
15
+ */
16
+ repository: string;
17
+
18
+ /**
19
+ * Optional environment name. When set every secret is scoped to that
20
+ * GitHub Actions environment instead of the whole repository.
21
+ */
22
+ environment?: string;
23
+
24
+ /**
25
+ * Map of secret name to value. Plain strings are wrapped with
26
+ * `Redacted.make`; already-redacted values are passed through.
27
+ */
28
+ secrets: Record<string, Input<string | Redacted.Redacted<string>>>;
29
+ }
30
+
31
+ /**
32
+ * Bulk-creates a set of {@link Secret}s in the same repository (and
33
+ * optionally the same environment).
34
+ *
35
+ * Each entry in `secrets` becomes one `GitHub.Secret` resource, using the
36
+ * map key as both the alchemy logical id and the secret name.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * yield* GitHub.Secrets({
41
+ * owner: "alchemy-run",
42
+ * repository: "alchemy-effect",
43
+ * secrets: {
44
+ * AXIOM_INGEST_TOKEN: tokenValue,
45
+ * AXIOM_DATASET_TRACES: traces.name,
46
+ * },
47
+ * });
48
+ * ```
49
+ */
50
+ export const Secrets = ({
51
+ owner,
52
+ repository,
53
+ environment,
54
+ secrets,
55
+ }: SecretsProps) =>
56
+ Effect.all(
57
+ Object.entries(secrets).map(([name, value]) =>
58
+ Secret(name, {
59
+ owner,
60
+ repository,
61
+ environment,
62
+ name,
63
+ value: Redacted.make(value),
64
+ }),
65
+ ),
66
+ );