@reventlessdev/reventless-core 3.0.0-alpha.219 → 3.0.0-alpha.220

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.220 (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.219 (2026-08-09)
7
14
 
8
15
  **Note:** Version bump only for package @reventlessdev/reventless-core
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.219",
3
+ "version": "3.0.0-alpha.220",
4
4
  "description": "Core package for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -29,16 +29,16 @@
29
29
  "sury": "11.0.0-alpha.4",
30
30
  "uuid": "^13.0.0",
31
31
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
32
- "@reventlessdev/rescript-fast-csv": "2.0.0-alpha.2",
33
32
  "@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
34
33
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
35
- "@reventlessdev/rescript-node": "2.0.0-alpha.2",
36
- "@reventlessdev/rescript-ssh2": "2.0.0-alpha.2",
34
+ "@reventlessdev/rescript-fast-csv": "2.0.0-alpha.3",
37
35
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
38
- "@reventlessdev/reventless-infra": "3.0.0-alpha.130",
36
+ "@reventlessdev/rescript-node": "2.0.0-alpha.3",
37
+ "@reventlessdev/rescript-ssh2": "2.0.0-alpha.3",
39
38
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
40
- "@reventlessdev/reventless-interop": "3.0.0-alpha.30",
41
- "@reventlessdev/reventless-spec": "3.0.0-alpha.104"
39
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.131",
40
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.105",
41
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.30"
42
42
  },
43
43
  "devDependencies": {
44
44
  "rescript": "12.3.0",
@@ -90,6 +90,28 @@ trade than a loud, skipped hook.
90
90
  */
91
91
  module type Extension = {
92
92
  let moduleUrl: string
93
+ /**
94
+ `import.meta.url` of one module per COMPANION package this extension imports at
95
+ runtime — packages that must ride into the code archive alongside the
96
+ extension's own. `[]` for an extension importing nothing outside the
97
+ framework-provided set.
98
+
99
+ Needed because a companion is invisible to every conventional lookup: the
100
+ ecosystem convention for split packages declares the neutral core as a
101
+ `peerDependency` (true in the deploy program's process, false in the Lambda),
102
+ so a `dependencies` walk finds nothing; and a companion named by bare package
103
+ name has no resolution root the framework could use — the one thing that
104
+ resolves robustly is a module URL the companion states about itself, exactly
105
+ like `moduleUrl`. A companion package exposes one by exporting
106
+ `let moduleUrl: string = %raw("import.meta.url")` from any module the
107
+ extension already imports (spec modules can lift the
108
+ `@@reventless.spec`-injected one).
109
+
110
+ Companions are bundle-only: the entry shell imports only the extension
111
+ modules, and Node resolves companions from the archive's `node_modules` like
112
+ any other import.
113
+ */
114
+ let companionModuleUrls: array<string>
93
115
  let onColdStart: coldStartHook
94
116
  }
95
117
 
@@ -127,6 +149,18 @@ let moduleUrls = (): array<string> =>
127
149
  E.moduleUrl
128
150
  })
129
151
 
152
+ /**
153
+ Every registered extension's companion module URLs, flattened in registration
154
+ order. Read only by the provider's code-archive builder — unlike `moduleUrls`,
155
+ these never reach a runtime's config: companions are bundled so the extension's
156
+ own imports resolve, not imported by the entry shell.
157
+ */
158
+ let companionModuleUrls = (): array<string> =>
159
+ extensions.contents->Array.flatMap(e => {
160
+ module E = unpack(e)
161
+ E.companionModuleUrls
162
+ })
163
+
130
164
  let log = Logger.fromEnv()
131
165
 
132
166
  /**
@@ -26,6 +26,10 @@ function moduleUrls() {
26
26
  return extensions.contents.map(e => e.moduleUrl);
27
27
  }
28
28
 
29
+ function companionModuleUrls() {
30
+ return extensions.contents.flatMap(e => e.companionModuleUrls);
31
+ }
32
+
29
33
  let log = Logger$ReventlessCore.fromEnv();
30
34
 
31
35
  function runHook(hook, index, runtimeKind, component, plugin, platform) {
@@ -52,6 +56,7 @@ export {
52
56
  reset,
53
57
  isEmpty,
54
58
  moduleUrls,
59
+ companionModuleUrls,
55
60
  log,
56
61
  runHook,
57
62
  notifyColdStartHooks,
@@ -47,6 +47,7 @@ describe("RuntimeExtension reaches the runtime callback hooks", () => {
47
47
  let seenAtColdStart: array<string> = []
48
48
  module Gatekeeper: RuntimeExtension.Extension = {
49
49
  let moduleUrl = "file:///pkg/gatekeeper.res.mjs"
50
+ let companionModuleUrls = []
50
51
  let onColdStart = (~runtimeKind as _, ~component, ~plugin as _, ~platform as _) => {
51
52
  seenAtColdStart->Array.push(component)
52
53
  CommandGenerator_Callback.registerCommandInterceptor(async (
@@ -53,6 +53,7 @@ globalThis.describe("RuntimeExtension reaches the runtime callback hooks", () =>
53
53
  });
54
54
  globalThis.test("an interceptor registered at cold start denies a later command", async () => {
55
55
  let seenAtColdStart = [];
56
+ let companionModuleUrls = [];
56
57
  let onColdStart = (param, component, param$1, param$2) => {
57
58
  seenAtColdStart.push(component);
58
59
  CommandGenerator_Callback$ReventlessCore.registerCommandInterceptor(async (param, componentName, param$1, tag, param$2) => ({
@@ -62,6 +63,7 @@ globalThis.describe("RuntimeExtension reaches the runtime callback hooks", () =>
62
63
  };
63
64
  let Gatekeeper = {
64
65
  moduleUrl: "file:///pkg/gatekeeper.res.mjs",
66
+ companionModuleUrls: companionModuleUrls,
65
67
  onColdStart: onColdStart
66
68
  };
67
69
  RuntimeExtension$ReventlessCore.use(Gatekeeper);
@@ -7,11 +7,12 @@ type call = {
7
7
  platform: option<string>,
8
8
  }
9
9
 
10
- let recorderNamed = (label: string, into: array<(string, call)>): module(
10
+ let recorderNamed = (~companions: array<string>=[], label: string, into: array<(string, call)>): module(
11
11
  RuntimeExtension.Extension
12
12
  ) => {
13
13
  module R = {
14
14
  let moduleUrl = `file:///pkg/${label}.res.mjs`
15
+ let companionModuleUrls = companions
15
16
  let onColdStart = (~runtimeKind, ~component, ~plugin, ~platform) =>
16
17
  into->Array.push((label, {runtimeKind, component, plugin, platform}))
17
18
  }
@@ -32,6 +33,7 @@ describe("RuntimeExtension", () => {
32
33
  testSync("empty registry is the default and notifyColdStart is silent", () => {
33
34
  expect(RuntimeExtension.isEmpty())->toBe(true)
34
35
  expect(RuntimeExtension.moduleUrls())->toEqual([])
36
+ expect(RuntimeExtension.companionModuleUrls())->toEqual([])
35
37
  fireAggregate()
36
38
  expect(true)->toBe(true)
37
39
  })
@@ -77,10 +79,38 @@ describe("RuntimeExtension", () => {
77
79
  ])
78
80
  })
79
81
 
82
+ testSync("companionModuleUrls flattens every registration's companions, in order", () => {
83
+ let calls: array<(string, call)> = []
84
+ RuntimeExtension.use(
85
+ recorderNamed(
86
+ ~companions=["file:///pkg/companion-a.res.mjs", "file:///pkg/companion-b.res.mjs"],
87
+ "tracing",
88
+ calls,
89
+ ),
90
+ )
91
+ RuntimeExtension.use(recorderNamed("accounting", calls))
92
+ RuntimeExtension.use(
93
+ recorderNamed(~companions=["file:///pkg/companion-c.res.mjs"], "quota", calls),
94
+ )
95
+
96
+ expect(RuntimeExtension.companionModuleUrls())->toEqual([
97
+ "file:///pkg/companion-a.res.mjs",
98
+ "file:///pkg/companion-b.res.mjs",
99
+ "file:///pkg/companion-c.res.mjs",
100
+ ])
101
+ // Companions are bundle-only: the entry shell's import list is untouched.
102
+ expect(RuntimeExtension.moduleUrls())->toEqual([
103
+ "file:///pkg/tracing.res.mjs",
104
+ "file:///pkg/accounting.res.mjs",
105
+ "file:///pkg/quota.res.mjs",
106
+ ])
107
+ })
108
+
80
109
  testSync("a throwing extension is skipped; the runtime and its siblings survive", () => {
81
110
  let calls: array<(string, call)> = []
82
111
  module Broken: RuntimeExtension.Extension = {
83
112
  let moduleUrl = "file:///pkg/broken.res.mjs"
113
+ let companionModuleUrls = []
84
114
  let onColdStart = (~runtimeKind as _, ~component as _, ~plugin as _, ~platform as _) =>
85
115
  JsError.throwWithMessage("extension blew up")
86
116
  }
@@ -3,7 +3,8 @@
3
3
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
4
4
  import * as RuntimeExtension$ReventlessCore from "../../src/adapter/RuntimeExtension/RuntimeExtension.res.mjs";
5
5
 
6
- function recorderNamed(label, into) {
6
+ function recorderNamed(companionsOpt, label, into) {
7
+ let companions = companionsOpt !== undefined ? companionsOpt : [];
7
8
  let moduleUrl = `file:///pkg/` + label + `.res.mjs`;
8
9
  let onColdStart = (runtimeKind, component, plugin, platform) => {
9
10
  into.push([
@@ -18,6 +19,7 @@ function recorderNamed(label, into) {
18
19
  };
19
20
  return {
20
21
  moduleUrl: moduleUrl,
22
+ companionModuleUrls: companions,
21
23
  onColdStart: onColdStart
22
24
  };
23
25
  }
@@ -31,12 +33,13 @@ globalThis.describe("RuntimeExtension", () => {
31
33
  globalThis.test("empty registry is the default and notifyColdStart is silent", () => {
32
34
  globalThis.expect(RuntimeExtension$ReventlessCore.isEmpty()).toBe(true);
33
35
  globalThis.expect(RuntimeExtension$ReventlessCore.moduleUrls()).toEqual([]);
36
+ globalThis.expect(RuntimeExtension$ReventlessCore.companionModuleUrls()).toEqual([]);
34
37
  fireAggregate();
35
38
  globalThis.expect(true).toBe(true);
36
39
  });
37
40
  globalThis.test("a registered extension receives the runtime's full identity", () => {
38
41
  let calls = [];
39
- RuntimeExtension$ReventlessCore.use(recorderNamed("tracing", calls));
42
+ RuntimeExtension$ReventlessCore.use(recorderNamed(undefined, "tracing", calls));
40
43
  fireAggregate();
41
44
  globalThis.expect(calls).toEqual([[
42
45
  "tracing",
@@ -50,8 +53,8 @@ globalThis.describe("RuntimeExtension", () => {
50
53
  });
51
54
  globalThis.test("several extensions compose, in registration order", () => {
52
55
  let calls = [];
53
- RuntimeExtension$ReventlessCore.use(recorderNamed("tracing", calls));
54
- RuntimeExtension$ReventlessCore.use(recorderNamed("accounting", calls));
56
+ RuntimeExtension$ReventlessCore.use(recorderNamed(undefined, "tracing", calls));
57
+ RuntimeExtension$ReventlessCore.use(recorderNamed(undefined, "accounting", calls));
55
58
  fireAggregate();
56
59
  globalThis.expect(calls.map(param => param[0])).toEqual([
57
60
  "tracing",
@@ -60,23 +63,44 @@ globalThis.describe("RuntimeExtension", () => {
60
63
  });
61
64
  globalThis.test("moduleUrls reports every registration, in order", () => {
62
65
  let calls = [];
63
- RuntimeExtension$ReventlessCore.use(recorderNamed("tracing", calls));
64
- RuntimeExtension$ReventlessCore.use(recorderNamed("accounting", calls));
66
+ RuntimeExtension$ReventlessCore.use(recorderNamed(undefined, "tracing", calls));
67
+ RuntimeExtension$ReventlessCore.use(recorderNamed(undefined, "accounting", calls));
65
68
  globalThis.expect(RuntimeExtension$ReventlessCore.isEmpty()).toBe(false);
66
69
  globalThis.expect(RuntimeExtension$ReventlessCore.moduleUrls()).toEqual([
67
70
  "file:///pkg/tracing.res.mjs",
68
71
  "file:///pkg/accounting.res.mjs"
69
72
  ]);
70
73
  });
74
+ globalThis.test("companionModuleUrls flattens every registration's companions, in order", () => {
75
+ let calls = [];
76
+ RuntimeExtension$ReventlessCore.use(recorderNamed([
77
+ "file:///pkg/companion-a.res.mjs",
78
+ "file:///pkg/companion-b.res.mjs"
79
+ ], "tracing", calls));
80
+ RuntimeExtension$ReventlessCore.use(recorderNamed(undefined, "accounting", calls));
81
+ RuntimeExtension$ReventlessCore.use(recorderNamed(["file:///pkg/companion-c.res.mjs"], "quota", calls));
82
+ globalThis.expect(RuntimeExtension$ReventlessCore.companionModuleUrls()).toEqual([
83
+ "file:///pkg/companion-a.res.mjs",
84
+ "file:///pkg/companion-b.res.mjs",
85
+ "file:///pkg/companion-c.res.mjs"
86
+ ]);
87
+ globalThis.expect(RuntimeExtension$ReventlessCore.moduleUrls()).toEqual([
88
+ "file:///pkg/tracing.res.mjs",
89
+ "file:///pkg/accounting.res.mjs",
90
+ "file:///pkg/quota.res.mjs"
91
+ ]);
92
+ });
71
93
  globalThis.test("a throwing extension is skipped; the runtime and its siblings survive", () => {
72
94
  let calls = [];
95
+ let companionModuleUrls = [];
73
96
  let onColdStart = (param, param$1, param$2, param$3) => Stdlib_JsError.throwWithMessage("extension blew up");
74
97
  let Broken = {
75
98
  moduleUrl: "file:///pkg/broken.res.mjs",
99
+ companionModuleUrls: companionModuleUrls,
76
100
  onColdStart: onColdStart
77
101
  };
78
102
  RuntimeExtension$ReventlessCore.use(Broken);
79
- RuntimeExtension$ReventlessCore.use(recorderNamed("accounting", calls));
103
+ RuntimeExtension$ReventlessCore.use(recorderNamed(undefined, "accounting", calls));
80
104
  fireAggregate();
81
105
  globalThis.expect(calls.map(param => param[0])).toEqual(["accounting"]);
82
106
  });
@@ -96,7 +120,7 @@ globalThis.describe("RuntimeExtension", () => {
96
120
  });
97
121
  globalThis.test("reset clears the registry", () => {
98
122
  let calls = [];
99
- RuntimeExtension$ReventlessCore.use(recorderNamed("tracing", calls));
123
+ RuntimeExtension$ReventlessCore.use(recorderNamed(undefined, "tracing", calls));
100
124
  RuntimeExtension$ReventlessCore.reset();
101
125
  fireAggregate();
102
126
  globalThis.expect(RuntimeExtension$ReventlessCore.isEmpty()).toBe(true);
@@ -228,6 +228,7 @@ describe("CommandTopic command-outcome hook:", () => {
228
228
  // seam already reaches, so an extension picks it up alongside the other four.
229
229
  module Accountant: RuntimeExtension.Extension = {
230
230
  let moduleUrl = "file:///pkg/accountant.res.mjs"
231
+ let companionModuleUrls = []
231
232
  let onColdStart = (~runtimeKind as _, ~component as _, ~plugin as _, ~platform as _) =>
232
233
  record()
233
234
  }
@@ -321,9 +321,11 @@ globalThis.describe("CommandTopic command-outcome hook:", () => {
321
321
  globalThis.expect(observed.contents.length).toBe(0);
322
322
  });
323
323
  globalThis.test("an out-of-tree extension can register it from onColdStart", async () => {
324
+ let companionModuleUrls = [];
324
325
  let onColdStart = (param, param$1, param$2, param$3) => record();
325
326
  let Accountant = {
326
327
  moduleUrl: "file:///pkg/accountant.res.mjs",
328
+ companionModuleUrls: companionModuleUrls,
327
329
  onColdStart: onColdStart
328
330
  };
329
331
  RuntimeExtension$ReventlessCore.use(Accountant);