@reventlessdev/reventless-aws 3.0.0-alpha.276 → 3.0.0-alpha.277
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,13 @@
|
|
|
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.277 (2026-08-09)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **core,aws:** bundle a runtime extension's companion packages, guard imports at deploy ([e975175](https://github.com/ReventlessDev/reventless-core/commit/e9751758f51582a8e46db362219f725bb5f1bcde))
|
|
11
|
+
|
|
12
|
+
|
|
6
13
|
# 3.0.0-alpha.276 (2026-08-09)
|
|
7
14
|
|
|
8
15
|
**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.
|
|
3
|
+
"version": "3.0.0-alpha.277",
|
|
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.
|
|
16
|
-
"@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",
|
|
15
|
+
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.6",
|
|
19
16
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
17
|
+
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.69",
|
|
18
|
+
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
19
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.3",
|
|
20
20
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
21
|
-
"@reventlessdev/reventless-core": "3.0.0-alpha.
|
|
22
|
-
"@reventlessdev/
|
|
21
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.220",
|
|
22
|
+
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
23
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.131",
|
|
23
24
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.30",
|
|
24
|
-
"@reventlessdev/reventless-postgres": "3.0.0-alpha.
|
|
25
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
26
|
-
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0"
|
|
25
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.84",
|
|
26
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.105"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"rescript": "12.3.0",
|
package/src/util/Util_Bundle.res
CHANGED
|
@@ -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
|
|
260
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
326
|
-
// at cold start. No-op when
|
|
327
|
-
// `sourceCodeHash`, is
|
|
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
|
-
|
|
185
|
+
let added = {};
|
|
186
|
+
RuntimeExtension$ReventlessCore.moduleUrls().map(getModuleSpecifier).concat(RuntimeExtension$ReventlessCore.companionModuleUrls().map(getModuleSpecifier)).forEach(specifier => {
|
|
186
187
|
let pkgName = extractPackageName(specifier);
|
|
187
|
-
|
|
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 */
|