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.
- package/bin/alchemy.js +428 -80
- package/bin/alchemy.js.map +1 -1
- package/bin/alchemy.ts +2 -0
- package/bin/commands/_shared.ts +34 -0
- package/bin/commands/bootstrap.ts +10 -3
- package/bin/commands/deploy.ts +23 -3
- package/bin/commands/login.ts +18 -3
- package/bin/commands/logs.ts +11 -2
- package/bin/commands/profile.ts +11 -5
- package/bin/commands/state.ts +21 -13
- package/bin/commands/tail.ts +10 -2
- package/lib/AWS/CloudFront/CachePolicy.d.ts +106 -0
- package/lib/AWS/CloudFront/CachePolicy.d.ts.map +1 -0
- package/lib/AWS/CloudFront/CachePolicy.js +147 -0
- package/lib/AWS/CloudFront/CachePolicy.js.map +1 -0
- package/lib/AWS/CloudFront/KeyGroup.d.ts +72 -0
- package/lib/AWS/CloudFront/KeyGroup.d.ts.map +1 -0
- package/lib/AWS/CloudFront/KeyGroup.js +136 -0
- package/lib/AWS/CloudFront/KeyGroup.js.map +1 -0
- package/lib/AWS/CloudFront/OriginRequestPolicy.d.ts +84 -0
- package/lib/AWS/CloudFront/OriginRequestPolicy.d.ts.map +1 -0
- package/lib/AWS/CloudFront/OriginRequestPolicy.js +139 -0
- package/lib/AWS/CloudFront/OriginRequestPolicy.js.map +1 -0
- package/lib/AWS/CloudFront/PublicKey.d.ts +72 -0
- package/lib/AWS/CloudFront/PublicKey.d.ts.map +1 -0
- package/lib/AWS/CloudFront/PublicKey.js +137 -0
- package/lib/AWS/CloudFront/PublicKey.js.map +1 -0
- package/lib/AWS/CloudFront/ResponseHeadersPolicy.d.ts +114 -0
- package/lib/AWS/CloudFront/ResponseHeadersPolicy.d.ts.map +1 -0
- package/lib/AWS/CloudFront/ResponseHeadersPolicy.js +162 -0
- package/lib/AWS/CloudFront/ResponseHeadersPolicy.js.map +1 -0
- package/lib/AWS/CloudFront/index.d.ts +5 -0
- package/lib/AWS/CloudFront/index.d.ts.map +1 -1
- package/lib/AWS/CloudFront/index.js +5 -0
- package/lib/AWS/CloudFront/index.js.map +1 -1
- package/lib/AWS/ECS/CapacityProvider.d.ts +102 -0
- package/lib/AWS/ECS/CapacityProvider.d.ts.map +1 -0
- package/lib/AWS/ECS/CapacityProvider.js +217 -0
- package/lib/AWS/ECS/CapacityProvider.js.map +1 -0
- package/lib/AWS/ECS/index.d.ts +1 -0
- package/lib/AWS/ECS/index.d.ts.map +1 -1
- package/lib/AWS/ECS/index.js +1 -0
- package/lib/AWS/ECS/index.js.map +1 -1
- package/lib/AWS/Providers.d.ts.map +1 -1
- package/lib/AWS/Providers.js +7 -1
- package/lib/AWS/Providers.js.map +1 -1
- package/lib/Agent/lsp/client.d.ts +2 -2
- package/lib/Apply.d.ts.map +1 -1
- package/lib/Apply.js +41 -9
- package/lib/Apply.js.map +1 -1
- package/lib/Axiom/Annotation.d.ts +49 -0
- package/lib/Axiom/Annotation.d.ts.map +1 -0
- package/lib/Axiom/Annotation.js +74 -0
- package/lib/Axiom/Annotation.js.map +1 -0
- package/lib/Axiom/ApiToken.d.ts +62 -0
- package/lib/Axiom/ApiToken.d.ts.map +1 -0
- package/lib/Axiom/ApiToken.js +98 -0
- package/lib/Axiom/ApiToken.js.map +1 -0
- package/lib/Axiom/AuthProvider.d.ts +48 -0
- package/lib/Axiom/AuthProvider.d.ts.map +1 -0
- package/lib/Axiom/AuthProvider.js +177 -0
- package/lib/Axiom/AuthProvider.js.map +1 -0
- package/lib/Axiom/Chart.d.ts +72 -0
- package/lib/Axiom/Chart.d.ts.map +1 -0
- package/lib/Axiom/Chart.js +14 -0
- package/lib/Axiom/Chart.js.map +1 -0
- package/lib/Axiom/Credentials.d.ts +12 -0
- package/lib/Axiom/Credentials.d.ts.map +1 -0
- package/lib/Axiom/Credentials.js +34 -0
- package/lib/Axiom/Credentials.js.map +1 -0
- package/lib/Axiom/Dashboard.d.ts +124 -0
- package/lib/Axiom/Dashboard.d.ts.map +1 -0
- package/lib/Axiom/Dashboard.js +140 -0
- package/lib/Axiom/Dashboard.js.map +1 -0
- package/lib/Axiom/Dataset.d.ts +101 -0
- package/lib/Axiom/Dataset.d.ts.map +1 -0
- package/lib/Axiom/Dataset.js +183 -0
- package/lib/Axiom/Dataset.js.map +1 -0
- package/lib/Axiom/Monitor.d.ts +74 -0
- package/lib/Axiom/Monitor.d.ts.map +1 -0
- package/lib/Axiom/Monitor.js +103 -0
- package/lib/Axiom/Monitor.js.map +1 -0
- package/lib/Axiom/Notifier.d.ts +63 -0
- package/lib/Axiom/Notifier.d.ts.map +1 -0
- package/lib/Axiom/Notifier.js +83 -0
- package/lib/Axiom/Notifier.js.map +1 -0
- package/lib/Axiom/Providers.d.ts +15 -0
- package/lib/Axiom/Providers.d.ts.map +1 -0
- package/lib/Axiom/Providers.js +32 -0
- package/lib/Axiom/Providers.js.map +1 -0
- package/lib/Axiom/View.d.ts +55 -0
- package/lib/Axiom/View.d.ts.map +1 -0
- package/lib/Axiom/View.js +79 -0
- package/lib/Axiom/View.js.map +1 -0
- package/lib/Axiom/VirtualField.d.ts +42 -0
- package/lib/Axiom/VirtualField.d.ts.map +1 -0
- package/lib/Axiom/VirtualField.js +71 -0
- package/lib/Axiom/VirtualField.js.map +1 -0
- package/lib/Axiom/index.d.ts +13 -0
- package/lib/Axiom/index.d.ts.map +1 -0
- package/lib/Axiom/index.js +13 -0
- package/lib/Axiom/index.js.map +1 -0
- package/lib/Bundle/Bundle.d.ts +20 -2
- package/lib/Bundle/Bundle.d.ts.map +1 -1
- package/lib/Bundle/Bundle.js +18 -3
- package/lib/Bundle/Bundle.js.map +1 -1
- package/lib/Bundle/PurePlugin.d.ts +109 -0
- package/lib/Bundle/PurePlugin.d.ts.map +1 -0
- package/lib/Bundle/PurePlugin.js +469 -0
- package/lib/Bundle/PurePlugin.js.map +1 -0
- package/lib/Bundle/index.d.ts +1 -0
- package/lib/Bundle/index.d.ts.map +1 -1
- package/lib/Bundle/index.js +1 -0
- package/lib/Bundle/index.js.map +1 -1
- package/lib/Cloudflare/Workers/Worker.d.ts +12 -0
- package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.js +19 -4
- package/lib/Cloudflare/Workers/Worker.js.map +1 -1
- package/lib/GitHub/Secrets.d.ts +45 -0
- package/lib/GitHub/Secrets.d.ts.map +1 -0
- package/lib/GitHub/Secrets.js +30 -0
- package/lib/GitHub/Secrets.js.map +1 -0
- package/lib/GitHub/index.d.ts +1 -0
- package/lib/GitHub/index.d.ts.map +1 -1
- package/lib/GitHub/index.js +1 -0
- package/lib/GitHub/index.js.map +1 -1
- package/lib/Output.d.ts.map +1 -1
- package/lib/Output.js +1 -1
- package/lib/Output.js.map +1 -1
- package/lib/Plan.d.ts.map +1 -1
- package/lib/Plan.js +10 -4
- package/lib/Plan.js.map +1 -1
- package/lib/Resource.d.ts +2 -2
- package/lib/Resource.d.ts.map +1 -1
- package/lib/Telemetry/Attributes.d.ts +43 -0
- package/lib/Telemetry/Attributes.d.ts.map +1 -0
- package/lib/Telemetry/Attributes.js +144 -0
- package/lib/Telemetry/Attributes.js.map +1 -0
- package/lib/Telemetry/Layer.d.ts +14 -0
- package/lib/Telemetry/Layer.d.ts.map +1 -0
- package/lib/Telemetry/Layer.js +50 -0
- package/lib/Telemetry/Layer.js.map +1 -0
- package/lib/Telemetry/Metrics.d.ts +34 -0
- package/lib/Telemetry/Metrics.d.ts.map +1 -0
- package/lib/Telemetry/Metrics.js +57 -0
- package/lib/Telemetry/Metrics.js.map +1 -0
- package/package.json +12 -1
- package/src/AWS/CloudFront/CachePolicy.ts +271 -0
- package/src/AWS/CloudFront/KeyGroup.ts +233 -0
- package/src/AWS/CloudFront/OriginRequestPolicy.ts +256 -0
- package/src/AWS/CloudFront/PublicKey.ts +245 -0
- package/src/AWS/CloudFront/ResponseHeadersPolicy.ts +308 -0
- package/src/AWS/CloudFront/index.ts +11 -0
- package/src/AWS/ECS/CapacityProvider.ts +336 -0
- package/src/AWS/ECS/index.ts +4 -0
- package/src/AWS/Providers.ts +12 -0
- package/src/Apply.ts +118 -9
- package/src/Axiom/Annotation.ts +95 -0
- package/src/Axiom/ApiToken.ts +125 -0
- package/src/Axiom/AuthProvider.ts +341 -0
- package/src/Axiom/Chart.ts +94 -0
- package/src/Axiom/Credentials.ts +70 -0
- package/src/Axiom/Dashboard.ts +192 -0
- package/src/Axiom/Dataset.ts +291 -0
- package/src/Axiom/Monitor.ts +122 -0
- package/src/Axiom/Notifier.ts +104 -0
- package/src/Axiom/Providers.ts +58 -0
- package/src/Axiom/View.ts +106 -0
- package/src/Axiom/VirtualField.ts +90 -0
- package/src/Axiom/index.ts +12 -0
- package/src/Bundle/Bundle.ts +39 -1
- package/src/Bundle/PurePlugin.ts +556 -0
- package/src/Bundle/index.ts +1 -0
- package/src/Cloudflare/Workers/Worker.ts +37 -6
- package/src/GitHub/Secrets.ts +66 -0
- package/src/GitHub/index.ts +1 -0
- package/src/Output.ts +2 -1
- package/src/Plan.ts +13 -4
- package/src/Resource.ts +2 -2
- package/src/Telemetry/Attributes.ts +216 -0
- package/src/Telemetry/Layer.ts +65 -0
- 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
|
+
}
|
package/src/Bundle/index.ts
CHANGED
|
@@ -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
|
-
|
|
755
|
-
|
|
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
|
-
?
|
|
770
|
-
|
|
771
|
-
|
|
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
|
+
);
|