@reventlessdev/reventless-aws 3.0.0-alpha.276 → 3.0.0-alpha.278

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/CHANGELOG.md CHANGED
@@ -3,6 +3,21 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.278 (2026-08-09)
7
+
8
+ **Note:** Version bump only for package @reventlessdev/reventless-aws
9
+
10
+
11
+
12
+
13
+
14
+ # 3.0.0-alpha.277 (2026-08-09)
15
+
16
+ ### Features
17
+
18
+ * **core,aws:** bundle a runtime extension's companion packages, guard imports at deploy ([e975175](https://github.com/ReventlessDev/reventless-core/commit/e9751758f51582a8e46db362219f725bb5f1bcde))
19
+
20
+
6
21
  # 3.0.0-alpha.276 (2026-08-09)
7
22
 
8
23
  **Note:** Version bump only for package @reventlessdev/reventless-aws
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.276",
3
+ "version": "3.0.0-alpha.278",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
@@ -12,18 +12,18 @@
12
12
  "@aws-sdk/s3-request-presigner": "3.970.0",
13
13
  "sury": "11.0.0-alpha.4",
14
14
  "uuid": "^13.0.0",
15
- "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.5",
16
15
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
17
- "@reventlessdev/rescript-node": "2.0.0-alpha.2",
18
- "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.68",
16
+ "@reventlessdev/rescript-node": "2.0.0-alpha.3",
19
17
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
18
+ "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.6",
19
+ "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.69",
20
20
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
21
- "@reventlessdev/reventless-core": "3.0.0-alpha.219",
22
- "@reventlessdev/reventless-infra": "3.0.0-alpha.130",
21
+ "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
22
+ "@reventlessdev/reventless-core": "3.0.0-alpha.221",
23
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.132",
23
24
  "@reventlessdev/reventless-interop": "3.0.0-alpha.30",
24
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.83",
25
- "@reventlessdev/reventless-spec": "3.0.0-alpha.104",
26
- "@reventlessdev/rescript-uuid": "2.0.0-alpha.0"
25
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.85",
26
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.106"
27
27
  },
28
28
  "devDependencies": {
29
29
  "rescript": "12.3.0",
@@ -256,17 +256,138 @@ let runtimeExtensionSpecifiers = (): array<string> =>
256
256
  ReventlessCore.RuntimeExtension.moduleUrls()->Array.map(getModuleSpecifier)
257
257
 
258
258
  /**
259
- * Add every registered runtime extension's package to `packageDirs`, so the
260
- * archive carries the modules the entry shell imports at cold start.
259
+ * Add every registered runtime extension's package to `packageDirs` and every
260
+ * package its `companionModuleUrls` name so the archive carries both the
261
+ * modules the entry shell imports at cold start and the packages those modules
262
+ * import. Companions are bundle-only: they never appear in `RUNTIME_EXTENSIONS`;
263
+ * Node resolves them from the archive's `node_modules` when the extension's own
264
+ * import runs.
261
265
  *
262
266
  * Rooted through `getModuleSpecifier`, which caches the package root it walked
263
267
  * to. That matters: an extension is an out-of-tree package the framework has no
264
- * dependency on, so a framework-rooted `require.resolve` would not find it.
268
+ * dependency on, so a framework-rooted `require.resolve` would not find it
269
+ * and the same holds for its companions, which is why the contract asks for a
270
+ * module URL per companion rather than a bare package name.
271
+ *
272
+ * Returns the packages this mechanism put into the archive (name → root): the
273
+ * set `assertRuntimeExtensionImportsResolvable` scans.
265
274
  */
266
- let addRuntimeExtensionPackages = (packageDirs: dict<string>) =>
267
- runtimeExtensionSpecifiers()->Array.forEach(specifier => {
275
+ let addRuntimeExtensionPackages = (packageDirs: dict<string>): dict<string> => {
276
+ let added: dict<string> = Dict.make()
277
+ runtimeExtensionSpecifiers()
278
+ ->Array.concat(
279
+ ReventlessCore.RuntimeExtension.companionModuleUrls()->Array.map(getModuleSpecifier),
280
+ )
281
+ ->Array.forEach(specifier => {
268
282
  let pkgName = extractPackageName(specifier)
269
- packageDirs->Dict.set(pkgName, resolvePackageRoot(pkgName))
283
+ let root = resolvePackageRoot(pkgName)
284
+ packageDirs->Dict.set(pkgName, root)
285
+ added->Dict.set(pkgName, root)
286
+ })
287
+ added
288
+ }
289
+
290
+ /**
291
+ * The bare package specifiers of a module source's static top-level `import` /
292
+ * re-`export` declarations. A regex, not a parser: `import` declarations are
293
+ * only legal at a module's top level, so line-anchored matching is exact up to
294
+ * strings/comments that spell out an import — an acceptable imprecision for a
295
+ * guard (see `assertRuntimeExtensionImportsResolvable`). Dynamic `import()`
296
+ * deliberately does not match.
297
+ */
298
+ let staticImportSpecifiers = (source: string): array<string> => {
299
+ let re = RegExp.fromString(
300
+ "(?:^|\\n)\\s*(?:import|export)\\s*(?:[\\w$*{},\\s]+?from\\s*)?[\"']([^\"']+)[\"']",
301
+ ~flags="g",
302
+ )
303
+ let specifiers: array<string> = []
304
+ let scanning = ref(true)
305
+ while scanning.contents {
306
+ switch re->RegExp.exec(source) {
307
+ | Some(result) =>
308
+ switch result->RegExp.Result.matches->Array.getUnsafe(0) {
309
+ | Some(specifier) => specifiers->Array.push(specifier)
310
+ | None => ()
311
+ }
312
+ | None => scanning := false
313
+ }
314
+ }
315
+ specifiers
316
+ }
317
+
318
+ let isBareSpecifier = (s: string) =>
319
+ !(s->String.startsWith(".")) &&
320
+ !(s->String.startsWith("/")) &&
321
+ !(s->String.startsWith("file:")) &&
322
+ !(s->String.startsWith("data:")) &&
323
+ // `#…` is a package-internal `imports` alias, resolved inside the package.
324
+ !(s->String.startsWith("#"))
325
+
326
+ let nodeBuiltins = Set.fromArray(NodeModule.builtinModules)
327
+
328
+ /**
329
+ * Whether a deployed runtime resolves this bare specifier without it riding in
330
+ * the archive: Node builtins, the @aws-sdk and @smithy scopes reachable through
331
+ * the ESM fallback dirs (runtime SDK dir and layer), or a package resolvable
332
+ * from the framework — an approximation of the Lambda layer, which bundles
333
+ * reventless-aws's dependency closure. The approximation errs toward passing
334
+ * (workspace hoisting can resolve more than the layer carries); a false pass
335
+ * still lands on the explicit `companionModuleUrls` declaration.
336
+ */
337
+ let isRuntimeProvided = (specifier: string, ~pkgName: string): bool =>
338
+ specifier->String.startsWith("node:") ||
339
+ nodeBuiltins->Set.has(specifier) ||
340
+ nodeBuiltins->Set.has(pkgName) ||
341
+ pkgName->String.startsWith("@aws-sdk/") ||
342
+ pkgName->String.startsWith("@smithy/") ||
343
+ (
344
+ try {
345
+ let _ = resolvePackageRoot(pkgName)
346
+ true
347
+ } catch {
348
+ | _ => false
349
+ }
350
+ )
351
+
352
+ /**
353
+ * Deploy-time guard: every bare package statically imported by a bundled
354
+ * runtime-extension (or companion) package must either be in the archive
355
+ * (`bundledPackages`) or be provided by the deployed runtime. Throws at archive
356
+ * build on a miss, naming the package, the file, the specifier and the
357
+ * `companionModuleUrls` remedy.
358
+ *
359
+ * Without this the failure mode is the worst kind: the deploy is green, the
360
+ * entry shell catches the load failure by design, logs at ERROR and fires zero
361
+ * extensions — the feature the extension carries is silently off on every cold
362
+ * start. Static parsing is a partial truth (dynamic `import()` escapes it),
363
+ * which is exactly why it is the check and not the mechanism: a false negative
364
+ * here still lands on the explicit declaration; the declaration never depends
365
+ * on parsing.
366
+ */
367
+ let assertRuntimeExtensionImportsResolvable = (
368
+ ~extensionPackages: dict<string>,
369
+ ~bundledPackages: dict<string>,
370
+ ) =>
371
+ extensionPackages->Dict.forEachWithKey((pkgRoot, pkgName) => {
372
+ let assets: dict<Pulumi.Archive.assetOrArchive> = Dict.make()
373
+ let paths: array<(string, string)> = []
374
+ walkDir(~dir=pkgRoot, ~prefix="", ~assets, ~paths)
375
+ paths->Array.forEach(((relPath, absPath)) =>
376
+ if relPath->String.endsWith(".mjs") || relPath->String.endsWith(".js") {
377
+ NodeFs.readFileSync(absPath)
378
+ ->staticImportSpecifiers
379
+ ->Array.forEach(specifier =>
380
+ if isBareSpecifier(specifier) {
381
+ let dep = extractPackageName(specifier)
382
+ if !(bundledPackages->Dict.has(dep)) && !isRuntimeProvided(specifier, ~pkgName=dep) {
383
+ JsError.throwWithMessage(
384
+ `runtime extension package "${pkgName}" imports "${specifier}" (${relPath}), which is neither bundled into the code archive nor provided in the deployed runtime. The extension would be skipped at every cold start with "could not be loaded". Declare the package in the extension's companionModuleUrls — the import.meta.url of one of its modules — so it rides into the archive alongside the extension.`,
385
+ )
386
+ }
387
+ }
388
+ )
389
+ }
390
+ )
270
391
  })
271
392
 
272
393
  /**
@@ -322,11 +443,15 @@ let buildCodeArchive = (
322
443
  } else {
323
444
  packageDirs
324
445
  }
325
- // Registered runtime extensions ride along, so the entry shell can import them
326
- // at cold start. No-op when nothing is registered — the archive, and therefore
327
- // `sourceCodeHash`, is unchanged.
446
+ // Registered runtime extensions ride along (with their declared companion
447
+ // packages), so the entry shell can import them at cold start. No-op when
448
+ // nothing is registered — the archive, and therefore `sourceCodeHash`, is
449
+ // unchanged. The guard then fails the deploy on any statically imported
450
+ // package that would not resolve at cold start — the alternative is a green
451
+ // deploy whose extensions are silently skipped.
328
452
  if bundleRuntimeExtensions {
329
- allPackageDirs->addRuntimeExtensionPackages
453
+ let extensionPackages = allPackageDirs->addRuntimeExtensionPackages
454
+ assertRuntimeExtensionImportsResolvable(~extensionPackages, ~bundledPackages=allPackageDirs)
330
455
  }
331
456
  let packageContentHashes: ref<array<string>> = ref([])
332
457
  allPackageDirs->Dict.forEachWithKey((pkgRoot, pkgName) => {
@@ -182,9 +182,76 @@ function runtimeExtensionSpecifiers() {
182
182
  }
183
183
 
184
184
  function addRuntimeExtensionPackages(packageDirs) {
185
- RuntimeExtension$ReventlessCore.moduleUrls().map(getModuleSpecifier).forEach(specifier => {
185
+ let added = {};
186
+ RuntimeExtension$ReventlessCore.moduleUrls().map(getModuleSpecifier).concat(RuntimeExtension$ReventlessCore.companionModuleUrls().map(getModuleSpecifier)).forEach(specifier => {
186
187
  let pkgName = extractPackageName(specifier);
187
- packageDirs[pkgName] = resolvePackageRoot(undefined, pkgName);
188
+ let root = resolvePackageRoot(undefined, pkgName);
189
+ packageDirs[pkgName] = root;
190
+ added[pkgName] = root;
191
+ });
192
+ return added;
193
+ }
194
+
195
+ function staticImportSpecifiers(source) {
196
+ let re = new RegExp("(?:^|\\n)\\s*(?:import|export)\\s*(?:[\\w$*{},\\s]+?from\\s*)?[\"']([^\"']+)[\"']", "g");
197
+ let specifiers = [];
198
+ let scanning = true;
199
+ while (scanning) {
200
+ let result = re.exec(source);
201
+ if (result == null) {
202
+ scanning = false;
203
+ } else {
204
+ let specifier = result.slice(1)[0];
205
+ if (specifier !== undefined) {
206
+ specifiers.push(specifier);
207
+ }
208
+ }
209
+ };
210
+ return specifiers;
211
+ }
212
+
213
+ function isBareSpecifier(s) {
214
+ if (!s.startsWith(".") && !s.startsWith("/") && !s.startsWith("file:") && !s.startsWith("data:")) {
215
+ return !s.startsWith("#");
216
+ } else {
217
+ return false;
218
+ }
219
+ }
220
+
221
+ let nodeBuiltins = new Set(Nodemodule.builtinModules);
222
+
223
+ function isRuntimeProvided(specifier, pkgName) {
224
+ if (specifier.startsWith("node:") || nodeBuiltins.has(specifier) || nodeBuiltins.has(pkgName) || pkgName.startsWith("@aws-sdk/") || pkgName.startsWith("@smithy/")) {
225
+ return true;
226
+ }
227
+ try {
228
+ resolvePackageRoot(undefined, pkgName);
229
+ return true;
230
+ } catch (exn) {
231
+ return false;
232
+ }
233
+ }
234
+
235
+ function assertRuntimeExtensionImportsResolvable(extensionPackages, bundledPackages) {
236
+ Stdlib_Dict.forEachWithKey(extensionPackages, (pkgRoot, pkgName) => {
237
+ let assets = {};
238
+ let paths = [];
239
+ walkDir(pkgRoot, "", assets, paths);
240
+ paths.forEach(param => {
241
+ let relPath = param[0];
242
+ if (relPath.endsWith(".mjs") || relPath.endsWith(".js")) {
243
+ staticImportSpecifiers(Nodefs.readFileSync(param[1], "utf8")).forEach(specifier => {
244
+ if (!isBareSpecifier(specifier)) {
245
+ return;
246
+ }
247
+ let dep = extractPackageName(specifier);
248
+ if (!(dep in bundledPackages) && !isRuntimeProvided(specifier, dep)) {
249
+ return Stdlib_JsError.throwWithMessage(`runtime extension package "` + pkgName + `" imports "` + specifier + `" (` + relPath + `), which is neither bundled into the code archive nor provided in the deployed runtime. The extension would be skipped at every cold start with "could not be loaded". Declare the package in the extension's companionModuleUrls — the import.meta.url of one of its modules — so it rides into the archive alongside the extension.`);
250
+ }
251
+ });
252
+ return;
253
+ }
254
+ });
188
255
  });
189
256
  }
190
257
 
@@ -207,7 +274,8 @@ function buildCodeArchive(entryPointModule, packageDirs, extraStringAssetsOpt, b
207
274
  allPackageDirs = packageDirs;
208
275
  }
209
276
  if (bundleRuntimeExtensions) {
210
- addRuntimeExtensionPackages(allPackageDirs);
277
+ let extensionPackages = addRuntimeExtensionPackages(allPackageDirs);
278
+ assertRuntimeExtensionImportsResolvable(extensionPackages, allPackageDirs);
211
279
  }
212
280
  let packageContentHashes = {
213
281
  contents: []
@@ -254,6 +322,11 @@ export {
254
322
  createFilteredPackageArchive,
255
323
  runtimeExtensionSpecifiers,
256
324
  addRuntimeExtensionPackages,
325
+ staticImportSpecifiers,
326
+ isBareSpecifier,
327
+ nodeBuiltins,
328
+ isRuntimeProvided,
329
+ assertRuntimeExtensionImportsResolvable,
257
330
  buildCodeArchive,
258
331
  }
259
332
  /* localRequire Not a pure module */
@@ -0,0 +1,172 @@
1
+ open JestGlobals
2
+
3
+ // The companion half of the runtime-extension seam: an extension declares the
4
+ // packages its runtime import graph reaches (`companionModuleUrls`), the
5
+ // bundler carries them into the archive, and the archive build fails loudly on
6
+ // any statically imported package that would not resolve at cold start. See
7
+ // docs/plans/runtime-extension-companion-packages.md.
8
+
9
+ // realpath because macOS hands out /var/... symlinks for tmpdir while the
10
+ // package-root walk reports paths as given.
11
+ let makePkg = (name: string, files: array<(string, string)>): string => {
12
+ let root = NodeFs.realpathSync(NodeFs.mkdtempSync(NodePath.join([NodeOs.tmpdir(), "rt-ext-"])))
13
+ NodeFs.writeFileSync(
14
+ NodePath.join([root, "package.json"]),
15
+ `{"name":"${name}","version":"1.0.0"}`,
16
+ )
17
+ files->Array.forEach(((relPath, content)) => {
18
+ let abs = NodePath.join([root, relPath])
19
+ NodeFs.mkdirSync(NodePath.dirname(abs), {recursive: true})
20
+ NodeFs.writeFileSync(abs, content)
21
+ })
22
+ root
23
+ }
24
+
25
+ let fileUrl = (absPath: string) => "file://" ++ absPath
26
+
27
+ let extensionModule = (~companions: array<string>=[], url: string): module(
28
+ ReventlessCore.RuntimeExtension.Extension
29
+ ) => {
30
+ module E = {
31
+ let moduleUrl = url
32
+ let companionModuleUrls = companions
33
+ let onColdStart = (~runtimeKind as _, ~component as _, ~plugin as _, ~platform as _) => ()
34
+ }
35
+ module(E: ReventlessCore.RuntimeExtension.Extension)
36
+ }
37
+
38
+ let caughtMessage = (run: unit => unit): option<string> =>
39
+ try {
40
+ run()
41
+ None
42
+ } catch {
43
+ | exn => Some(exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr(""))
44
+ }
45
+
46
+ // One extension package whose artifact imports its companion plus one specifier
47
+ // from every runtime-provided class the guard must wave through: a node:
48
+ // builtin, a bare builtin, a framework dependency (the layer approximation),
49
+ // and the @aws-sdk scope (runtime SDK dir).
50
+ let extPkgName = "@fixture/runtime-ext"
51
+ let companionPkgName = "@fixture/runtime-ext-companion"
52
+ let extRoot = makePkg(
53
+ extPkgName,
54
+ [
55
+ (
56
+ "src/Ext_Extension.res.mjs",
57
+ `import * as Companion from "${companionPkgName}";
58
+ import * as Fs from "node:fs";
59
+ import * as Path from "path";
60
+ import * as Effect from "effect";
61
+ import * as Ddb from "@aws-sdk/client-dynamodb";
62
+ export const moduleUrl = import.meta.url;
63
+ export const onColdStart = () => Companion.mark(Fs, Path, Effect, Ddb);
64
+ `,
65
+ ),
66
+ ],
67
+ )
68
+ let companionRoot = makePkg(
69
+ companionPkgName,
70
+ [
71
+ (
72
+ "src/Companion.res.mjs",
73
+ `export const moduleUrl = import.meta.url;
74
+ export const mark = (x) => x;
75
+ `,
76
+ ),
77
+ ],
78
+ )
79
+ let extUrl = fileUrl(NodePath.join([extRoot, "src/Ext_Extension.res.mjs"]))
80
+ let companionUrl = fileUrl(NodePath.join([companionRoot, "src/Companion.res.mjs"]))
81
+
82
+ let badPkgName = "@fixture/runtime-ext-bad"
83
+ let badRoot = makePkg(
84
+ badPkgName,
85
+ [
86
+ (
87
+ "src/Bad_Extension.res.mjs",
88
+ `import * as Missing from "@fixture/never-declared";
89
+ export const moduleUrl = import.meta.url;
90
+ export const onColdStart = () => Missing.x;
91
+ `,
92
+ ),
93
+ ],
94
+ )
95
+ let badUrl = fileUrl(NodePath.join([badRoot, "src/Bad_Extension.res.mjs"]))
96
+
97
+ let build = () =>
98
+ Util_Bundle.buildCodeArchive(
99
+ ~entryPointModule="@reventlessdev/reventless-aws/src/adapter/Runtime/Entry.res.mjs",
100
+ ~packageDirs=Dict.make(),
101
+ )
102
+
103
+ describe("Util_Bundle — runtime-extension companion packages", () => {
104
+ beforeEach(() => ReventlessCore.RuntimeExtension.reset())
105
+ afterAll(() => {
106
+ ReventlessCore.RuntimeExtension.reset()
107
+ [extRoot, companionRoot, badRoot]->Array.forEach(root =>
108
+ NodeFs.rmSync(root, {recursive: true, force: true})
109
+ )
110
+ })
111
+
112
+ testSync("a declared companion package is added to the archive's package set", () => {
113
+ ReventlessCore.RuntimeExtension.use(extensionModule(~companions=[companionUrl], extUrl))
114
+
115
+ let packageDirs: dict<string> = Dict.make()
116
+ let added = Util_Bundle.addRuntimeExtensionPackages(packageDirs)
117
+
118
+ expect(packageDirs->Dict.get(extPkgName))->toEqual(Some(extRoot))
119
+ expect(packageDirs->Dict.get(companionPkgName))->toEqual(Some(companionRoot))
120
+ // The returned set — what the import guard scans — matches what was added.
121
+ expect(added->Dict.keysToArray->Array.toSorted(String.compare))->toEqual([
122
+ extPkgName,
123
+ companionPkgName,
124
+ ])
125
+ })
126
+
127
+ testSync("the archive builds when every import is declared or runtime-provided", () => {
128
+ ReventlessCore.RuntimeExtension.use(extensionModule(~companions=[companionUrl], extUrl))
129
+ expect(build().sourceCodeHash->String.length > 0)->toBe(true)
130
+ })
131
+
132
+ testSync("sourceCodeHash shifts when the companion's content shifts", () => {
133
+ ReventlessCore.RuntimeExtension.use(extensionModule(~companions=[companionUrl], extUrl))
134
+ let before = build().sourceCodeHash
135
+
136
+ let companionFile = NodePath.join([companionRoot, "src/Companion.res.mjs"])
137
+ let original = NodeFs.readFileSync(companionFile)
138
+ NodeFs.writeFileSync(
139
+ companionFile,
140
+ original ++ "export const changed = true;\n",
141
+ )
142
+ let after = build().sourceCodeHash
143
+ NodeFs.writeFileSync(companionFile, original)
144
+
145
+ expect(before == after)->toBe(false)
146
+ })
147
+
148
+ testSync("an empty registry leaves the archive byte-identical", () => {
149
+ let withSeam = build().sourceCodeHash
150
+ let withoutSeam =
151
+ Util_Bundle.buildCodeArchive(
152
+ ~entryPointModule="@reventlessdev/reventless-aws/src/adapter/Runtime/Entry.res.mjs",
153
+ ~packageDirs=Dict.make(),
154
+ ~bundleRuntimeExtensions=false,
155
+ ).sourceCodeHash
156
+ expect(withSeam)->toBe(withoutSeam)
157
+ })
158
+
159
+ testSync("an undeclared, un-bundled import fails the archive build, naming the remedy", () => {
160
+ ReventlessCore.RuntimeExtension.use(extensionModule(badUrl))
161
+
162
+ switch caughtMessage(() => {
163
+ let _ = build()
164
+ }) {
165
+ | None => JsError.throwWithMessage("expected buildCodeArchive to throw")
166
+ | Some(message) =>
167
+ expect(message->String.includes(badPkgName))->toBe(true)
168
+ expect(message->String.includes("@fixture/never-declared"))->toBe(true)
169
+ expect(message->String.includes("companionModuleUrls"))->toBe(true)
170
+ }
171
+ })
172
+ })
@@ -0,0 +1,171 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Nodefs from "node:fs";
4
+ import * as Nodeos from "node:os";
5
+ import * as Nodepath from "node:path";
6
+ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
7
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
8
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
9
+ import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
10
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
11
+ import * as Util_Bundle$ReventlessAws from "../src/util/Util_Bundle.res.mjs";
12
+ import * as RuntimeExtension$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/RuntimeExtension/RuntimeExtension.res.mjs";
13
+
14
+ function makePkg(name, files) {
15
+ let root = Nodefs.realpathSync(Nodefs.mkdtempSync(Nodepath.join(Nodeos.tmpdir(), "rt-ext-")));
16
+ Nodefs.writeFileSync(Nodepath.join(root, "package.json"), `{"name":"` + name + `","version":"1.0.0"}`, "utf8");
17
+ files.forEach(param => {
18
+ let abs = Nodepath.join(root, param[0]);
19
+ Nodefs.mkdirSync(Nodepath.dirname(abs), {
20
+ recursive: true
21
+ });
22
+ Nodefs.writeFileSync(abs, param[1], "utf8");
23
+ });
24
+ return root;
25
+ }
26
+
27
+ function fileUrl(absPath) {
28
+ return "file://" + absPath;
29
+ }
30
+
31
+ function extensionModule(companionsOpt, url) {
32
+ let companions = companionsOpt !== undefined ? companionsOpt : [];
33
+ let onColdStart = (param, param$1, param$2, param$3) => {};
34
+ return {
35
+ moduleUrl: url,
36
+ companionModuleUrls: companions,
37
+ onColdStart: onColdStart
38
+ };
39
+ }
40
+
41
+ function caughtMessage(run) {
42
+ try {
43
+ run();
44
+ return;
45
+ } catch (raw_exn) {
46
+ let exn = Primitive_exceptions.internalToException(raw_exn);
47
+ return Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "");
48
+ }
49
+ }
50
+
51
+ let extPkgName = "@fixture/runtime-ext";
52
+
53
+ let companionPkgName = "@fixture/runtime-ext-companion";
54
+
55
+ let extRoot = makePkg(extPkgName, [[
56
+ "src/Ext_Extension.res.mjs",
57
+ `import * as Companion from "` + companionPkgName + `";
58
+ import * as Fs from "node:fs";
59
+ import * as Path from "path";
60
+ import * as Effect from "effect";
61
+ import * as Ddb from "@aws-sdk/client-dynamodb";
62
+ export const moduleUrl = import.meta.url;
63
+ export const onColdStart = () => Companion.mark(Fs, Path, Effect, Ddb);
64
+ `
65
+ ]]);
66
+
67
+ let companionRoot = makePkg(companionPkgName, [[
68
+ "src/Companion.res.mjs",
69
+ `export const moduleUrl = import.meta.url;
70
+ export const mark = (x) => x;
71
+ `
72
+ ]]);
73
+
74
+ let extUrl = "file://" + Nodepath.join(extRoot, "src/Ext_Extension.res.mjs");
75
+
76
+ let companionUrl = "file://" + Nodepath.join(companionRoot, "src/Companion.res.mjs");
77
+
78
+ let badPkgName = "@fixture/runtime-ext-bad";
79
+
80
+ let badRoot = makePkg(badPkgName, [[
81
+ "src/Bad_Extension.res.mjs",
82
+ `import * as Missing from "@fixture/never-declared";
83
+ export const moduleUrl = import.meta.url;
84
+ export const onColdStart = () => Missing.x;
85
+ `
86
+ ]]);
87
+
88
+ let badUrl = "file://" + Nodepath.join(badRoot, "src/Bad_Extension.res.mjs");
89
+
90
+ function build() {
91
+ return Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/Entry.res.mjs", {}, undefined, undefined);
92
+ }
93
+
94
+ globalThis.describe("Util_Bundle — runtime-extension companion packages", () => {
95
+ globalThis.beforeEach(() => RuntimeExtension$ReventlessCore.reset());
96
+ globalThis.afterAll(() => {
97
+ RuntimeExtension$ReventlessCore.reset();
98
+ [
99
+ extRoot,
100
+ companionRoot,
101
+ badRoot
102
+ ].forEach(root => {
103
+ Nodefs.rmSync(root, {
104
+ recursive: true,
105
+ force: true
106
+ });
107
+ });
108
+ });
109
+ globalThis.test("a declared companion package is added to the archive's package set", () => {
110
+ RuntimeExtension$ReventlessCore.use(extensionModule([companionUrl], extUrl));
111
+ let packageDirs = {};
112
+ let added = Util_Bundle$ReventlessAws.addRuntimeExtensionPackages(packageDirs);
113
+ globalThis.expect(packageDirs[extPkgName]).toEqual(extRoot);
114
+ globalThis.expect(packageDirs[companionPkgName]).toEqual(companionRoot);
115
+ globalThis.expect(Object.keys(added).toSorted(Primitive_string.compare)).toEqual([
116
+ extPkgName,
117
+ companionPkgName
118
+ ]);
119
+ });
120
+ globalThis.test("the archive builds when every import is declared or runtime-provided", () => {
121
+ RuntimeExtension$ReventlessCore.use(extensionModule([companionUrl], extUrl));
122
+ globalThis.expect(build().sourceCodeHash.length > 0).toBe(true);
123
+ });
124
+ globalThis.test("sourceCodeHash shifts when the companion's content shifts", () => {
125
+ RuntimeExtension$ReventlessCore.use(extensionModule([companionUrl], extUrl));
126
+ let before = build().sourceCodeHash;
127
+ let companionFile = Nodepath.join(companionRoot, "src/Companion.res.mjs");
128
+ let original = Nodefs.readFileSync(companionFile, "utf8");
129
+ Nodefs.writeFileSync(companionFile, original + "export const changed = true;\n", "utf8");
130
+ let after = build().sourceCodeHash;
131
+ Nodefs.writeFileSync(companionFile, original, "utf8");
132
+ globalThis.expect(before === after).toBe(false);
133
+ });
134
+ globalThis.test("an empty registry leaves the archive byte-identical", () => {
135
+ let withSeam = build().sourceCodeHash;
136
+ let withoutSeam = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/Entry.res.mjs", {}, undefined, false).sourceCodeHash;
137
+ globalThis.expect(withSeam).toBe(withoutSeam);
138
+ });
139
+ globalThis.test("an undeclared, un-bundled import fails the archive build, naming the remedy", () => {
140
+ RuntimeExtension$ReventlessCore.use(extensionModule(undefined, badUrl));
141
+ let message = caughtMessage(() => {
142
+ build();
143
+ });
144
+ if (message !== undefined) {
145
+ globalThis.expect(message.includes(badPkgName)).toBe(true);
146
+ globalThis.expect(message.includes("@fixture/never-declared")).toBe(true);
147
+ globalThis.expect(message.includes("companionModuleUrls")).toBe(true);
148
+ return;
149
+ } else {
150
+ return Stdlib_JsError.throwWithMessage("expected buildCodeArchive to throw");
151
+ }
152
+ });
153
+ });
154
+
155
+ export {
156
+ makePkg,
157
+ fileUrl,
158
+ extensionModule,
159
+ caughtMessage,
160
+ extPkgName,
161
+ companionPkgName,
162
+ extRoot,
163
+ companionRoot,
164
+ extUrl,
165
+ companionUrl,
166
+ badPkgName,
167
+ badRoot,
168
+ badUrl,
169
+ build,
170
+ }
171
+ /* extRoot Not a pure module */