@toa.io/userland 1.0.0-alpha.273 → 1.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,211 @@
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
+ # [1.0.0-alpha.277](https://github.com/toa-io/toa/compare/v1.0.0-alpha.276...v1.0.0-alpha.277) (2026-09-03)
7
+
8
+ ### Bug Fixes
9
+
10
+ * settle what the module loader made asynchronous ([d4ab9df](https://github.com/toa-io/toa/commit/d4ab9dfb09e382da311dba08f52214e7645f4a6c))
11
+ * what the loader resolves, and what it will not ([1b8a02f](https://github.com/toa-io/toa/commit/1b8a02ff0a5e0f18ee62e3f71bc81116f86f9af1))
12
+
13
+
14
+ # [1.0.0-alpha.274](https://github.com/toa-io/toa/compare/v1.0.0-alpha.273...v1.0.0-alpha.274) (2026-09-02)
15
+
16
+
17
+ * Configuration is served by a component, followed live, and kept per epoch (#1017) ([b05997d](https://github.com/toa-io/toa/commit/b05997d3adb784f238a222cb46878b23859339a2)), closes [#1017](https://github.com/toa-io/toa/issues/1017)
18
+
19
+
20
+ ### BREAKING CHANGES
21
+
22
+ * a secret configuration value is an object, not a string;
23
+ read it with `unwrap()`.
24
+
25
+ Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
26
+
27
+ * test(configuration): wait for the second round rather than assume it
28
+
29
+ Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
30
+
31
+ * refactor(configuration)!: read a secret with its own unwrap
32
+
33
+ Component code depends on no Toa package, so a secret is read as
34
+ `context.configuration.apiKey.unwrap()` and the helper in `@toa.io/generic`
35
+ goes. A value a component reads as a secret is therefore given as a reference:
36
+ `identity.basic` no longer defaults `pepper` to `''`, the development context
37
+ and the feature harness give the token keys, the federation client secret and
38
+ the signing key as references, and the harness has a step for the secrets a
39
+ scenario refers to. CONTRIBUTING states the rule.
40
+ * `identity.basic` `pepper` has no default; a value read as a
41
+ secret must be given as a `$NAME` reference.
42
+
43
+ Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
44
+
45
+ * feat(configuration): list the configurations
46
+
47
+ `configuration.values.list` returns every component's configuration for its
48
+ deployed epoch, by component name, and `GET /configuration/values/` serves it
49
+ behind `system:configuration:get`.
50
+
51
+ Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
52
+
53
+ * fix(exposition): write a value in YAML the way its toJSON says
54
+
55
+ A redacted secret in a reply was dumped as an empty object; it is written as
56
+ `<REDACTED>`, as JSON and msgpack already write it.
57
+
58
+ Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
59
+
60
+ * test(configuration): a secret is an object, and stays redacted in a reply
61
+
62
+ A component with `{ a: 1, b: $SECRET_B }` returns its configuration as
63
+ `{ a: 1, b: '<REDACTED>' }` and the secret itself through `unwrap()`. The reply
64
+ step also accepts a reply as a caller across a process boundary receives it,
65
+ since a call within the process hands the object over as it is.
66
+
67
+ Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
68
+
69
+ * chore(schemas): bump the dependencies
70
+
71
+ ajv 8.18.0 to 8.20.0, ajv-formats 2.1.1 to 3.0.1, better-ajv-errors 1.2.0
72
+ to 2.0.3, fast-glob 3.2.12 to 3.3.3 and js-yaml 4.3.1 to 5.4.1 — the version
73
+ exposition already uses. The three majors keep the CommonJS entry points this
74
+ library calls: ajv-formats as a function, better-ajv-errors as .default, and
75
+ js-yaml's load.
76
+
77
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
78
+
79
+ * feat(configuration)!: serve the schema with the value
80
+
81
+ GET /configuration/values/:component/ answers { configuration, schema, epoch }
82
+ where it answered with the configuration alone, and list carries the schema of
83
+ every component. Nothing reads a configuration without needing to know what it
84
+ is checked against, and the schema was only ever on the values service.
85
+
86
+ An epoch the deployment does not know has no schema; the value stored for it
87
+ is still returned.
88
+
89
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
90
+
91
+ * feat(configuration): publish a configuration UI
92
+
93
+ The values service serves a page at /.configuration on port 8003, out of the
94
+ directory ui builds: the configured components, a screen per component showing
95
+ what it holds now, and a dialog creating the next one. Reading needs
96
+ system:configuration:get and creating needs system:configuration:create.
97
+
98
+ A configuration is immutable, so the dialog opens on the current value and what
99
+ is left out of it is left out of the component. The editor takes YAML, the
100
+ schema is applied before anything is sent, and a secret is a reference the page
101
+ never shows.
102
+
103
+ The page is always published: the annotation is the per-component values map
104
+ and has nowhere to carry a switch.
105
+
106
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
107
+
108
+ * feat(introspection): link a component to its configuration
109
+
110
+ A card carrying @toa.io/extensions.configuration reads that extension as a link
111
+ to the configuration UI rather than as a name, and configuration joins the
112
+ namespaces the runtime provides, so its components band under System.
113
+
114
+ Deployed, both pages sit behind the same ingress and the path alone is right;
115
+ locally each console has its own port.
116
+
117
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
118
+
119
+ * feat(introspection): scroll the map, zoom with a modifier
120
+
121
+ The map was grabbed to move and zoomed with a bare wheel, which is neither what
122
+ a page does nor what the wheel does anywhere else. A plain scroll now pans on
123
+ both axes and the wheel scales only with the modifier held — a trackpad pinch
124
+ arrives as one of those on every platform.
125
+
126
+ A card's own scrollable box keeps its wheel, in both paths.
127
+
128
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
129
+
130
+ * refactor(introspection): centre the filter in the header
131
+
132
+ The sides take equal space, which is what leaves the filter between them.
133
+ Neither side may shrink under what it holds, or the tabs run out from under
134
+ their side and across the filter, and the filter across the sign-out button;
135
+ the title gives way instead.
136
+
137
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
138
+
139
+ * fix(configuration): connect the UI to the values service
140
+
141
+ The page was declared as a dependency from inside `open`, and a connector connects
142
+ what it depends on before `open` runs — so the server was constructed and never
143
+ listened. Nothing caught it: the UI feature builds the server itself.
144
+
145
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
146
+
147
+ * feat(configuration)!: reserve resources in the annotation
148
+
149
+ The values service deploys like any other and must state what it may take, but the
150
+ annotation is a map of component ids and had nowhere to put it — so `toa env` failed
151
+ for every context using configuration unless it declared a context-wide `resources`.
152
+
153
+ `resources` is now the service's own, read the way `introspection` and `realtime` read
154
+ theirs. A component actually named `resources` is written `default.resources`, which is
155
+ what its id is anyway.
156
+
157
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
158
+
159
+ * feat(configuration): follow the system colour scheme
160
+
161
+ The dark tokens were behind a `.dark` class nothing sets, and the `dark:` variant was
162
+ pointed at that class too — so a component's own dark styling never applied either.
163
+ Both now key off `prefers-color-scheme`, and `color-scheme` hands the native chrome
164
+ the same answer.
165
+
166
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
167
+
168
+ * feat(introspection): follow the system colour scheme
169
+
170
+ The dark tokens were behind a `.dark` class nothing sets, and the `dark:` variant was
171
+ pointed at that class too — so a component's own dark styling never applied either.
172
+ Both now key off `prefers-color-scheme`, and `color-scheme` hands the native chrome
173
+ the same answer.
174
+
175
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
176
+
177
+ * feat(configuration): present a change as an edit of what is there
178
+
179
+ A configuration is immutable and creating one replaces the whole of it, but the dialog
180
+ opens on the current value and the page never shows an older one — so what the reader
181
+ does is edit. The words follow: Edit rather than New, Update rather than Create, and
182
+ the paragraph explaining the model goes, along with the field's label, which the title
183
+ above it already said.
184
+
185
+ The editor also stayed as wide as its longest line — `field-sizing: content` sizes to
186
+ content, and no `max-width` clamps that — so a long value painted outside the dialog.
187
+ It is fixed now, the dialog takes the width the screen has rather than a fixed step,
188
+ and the band held for errors no longer leaves a gap when there are none.
189
+
190
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
191
+
192
+ * feat(configuration): mark a component that keeps a secret
193
+
194
+ A key after the name on the row, wherever the configuration holds a reference, however
195
+ deep. The predicate reads the same lines the value screen renders, so the two cannot
196
+ disagree about what counts as a secret.
197
+
198
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
199
+
200
+ * feat(configuration): lead back to the list from the title
201
+
202
+ Every address ends in a slash, so the way up is the address itself on the list and one
203
+ level above it on a component's screen — which holds wherever the page is mounted.
204
+
205
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
206
+
207
+
208
+
209
+
210
+
6
211
  # [1.0.0-alpha.273](https://github.com/toa-io/toa/compare/v1.0.0-alpha.272...v1.0.0-alpha.273) (2026-09-02)
7
212
 
8
213
  **Note:** Version bump only for package @toa.io/userland
@@ -1,7 +1,3 @@
1
- 'use strict'
2
-
3
- async function computation (_, context) {
1
+ export async function computation (_, context) {
4
2
  return context.state.value
5
3
  }
6
-
7
- exports.computation = computation
@@ -1,7 +1,3 @@
1
- 'use strict'
2
-
3
- async function computation (key, context) {
1
+ export async function computation (key, context) {
4
2
  return context.state.values.get(key)
5
3
  }
6
-
7
- exports.computation = computation
@@ -1,6 +1,4 @@
1
- 'use strict'
2
-
3
- class Computation {
1
+ export class Computation {
4
2
  #context
5
3
 
6
4
  async mount (context) {
@@ -11,5 +9,3 @@ class Computation {
11
9
  return this.#context.configuration.signal
12
10
  }
13
11
  }
14
-
15
- exports.Computation = Computation
@@ -1,7 +1,3 @@
1
- 'use strict'
2
-
3
- async function computation (input) {
1
+ export async function computation (input) {
4
2
  return input.a + input.b
5
3
  }
6
-
7
- exports.computation = computation
@@ -1,6 +1,4 @@
1
- 'use strict'
2
-
3
- async function computation (input, context) {
1
+ export async function computation (input, context) {
4
2
  let value = input.value
5
3
 
6
4
  for (let i = 0; i < input.times; i++) {
@@ -9,5 +7,3 @@ async function computation (input, context) {
9
7
 
10
8
  return value
11
9
  }
12
-
13
- exports.computation = computation
@@ -1,10 +1,6 @@
1
- 'use strict'
2
-
3
- async function computation (input, context) {
1
+ export async function computation (input, context) {
4
2
  const request = { input }
5
3
 
6
4
  // noinspection JSUnresolvedVariable
7
5
  return context.remote.math.calculations.add(request)
8
6
  }
9
-
10
- exports.computation = computation
@@ -1,6 +1,4 @@
1
- 'use strict'
2
-
3
- async function computation (input, context) {
1
+ export async function computation (input, context) {
4
2
  const criteria = `material==${input}`
5
3
  const query = { criteria, limit }
6
4
 
@@ -9,5 +7,3 @@ async function computation (input, context) {
9
7
  }
10
8
 
11
9
  const limit = 10
12
-
13
- exports.computation = computation
@@ -1,8 +1,4 @@
1
- 'use strict'
2
-
3
- const request = async (payload) => ({
1
+ export const request = async (payload) => ({
4
2
  input: { booked: true },
5
3
  query: { id: payload.pot }
6
4
  })
7
-
8
- exports.request = request
@@ -1,6 +1,4 @@
1
- 'use strict'
2
-
3
- async function computation (_, context) {
1
+ export async function computation (_, context) {
4
2
  const response = await context.fetch('http://api.example.com/path/to/resource')
5
3
  const type = response.headers.get('content-type')
6
4
 
@@ -11,5 +9,3 @@ async function computation (_, context) {
11
9
  }
12
10
 
13
11
  const ERR_UNSUPPORTED_MEDIA_TYPE = new Error('UNSUPPORTED_MEDIA_TYPE')
14
-
15
- exports.computation = computation
@@ -1,16 +1,21 @@
1
- 'use strict'
1
+ import { it, before, after } from 'node:test'
2
+ import assert from 'node:assert/strict'
2
3
 
3
- const { resolve } = require('node:path')
4
- const { exceptions: { RequestContractException } } = require('@toa.io/core')
5
- const stage = require('@toa.io/userland/stage')
4
+ import { resolve } from 'node:path'
5
+ import { exceptions } from '@toa.io/core'
6
+ import * as stage from '@toa.io/userland/stage'
6
7
 
7
- const root = resolve(__dirname, '../components')
8
+ const { RequestContractException } = exceptions
9
+
10
+ const root = resolve(import.meta.dirname, '../components')
8
11
 
9
12
  let echo
10
13
  let math
11
14
 
12
- beforeAll(async () => {
15
+ before(async () => {
13
16
  process.env.TOA_DEV = '1'
17
+ process.env.TOA_CONFIGURATION_DEFAULT_ECHO = '{}'
18
+ process.env.TOA_CONFIGURATION_TEA_POTS = '{}'
14
19
 
15
20
  const paths = ['echo', 'math.calculations'].map((rel) => resolve(root, rel))
16
21
 
@@ -20,7 +25,7 @@ beforeAll(async () => {
20
25
  math = await stage.remote('math.calculations')
21
26
  })
22
27
 
23
- afterAll(async () => {
28
+ after(async () => {
24
29
  await stage.shutdown()
25
30
 
26
31
  delete process.env.TOA_DEV
@@ -29,13 +34,12 @@ afterAll(async () => {
29
34
  it('should call endpoint', async () => {
30
35
  const reply = await echo.invoke('signal', {})
31
36
 
32
- expect(reply).toStrictEqual('quack')
37
+ assert.deepStrictEqual(reply, 'quack')
33
38
  })
34
39
 
35
40
  it('should throw on invalid input', async () => {
36
41
  const a = 'not a number'
37
42
  const b = 'neither'
38
43
 
39
- await expect(math.invoke('add', { input: { a, b } }))
40
- .rejects.toBeInstanceOf(RequestContractException)
44
+ await assert.rejects(math.invoke('add', { input: { a, b } }), (error) => { assert.ok(error instanceof RequestContractException); return true })
41
45
  })
@@ -1,51 +1,98 @@
1
- 'use strict'
1
+ import { it, before, after } from 'node:test'
2
+ import assert from 'node:assert/strict'
2
3
 
3
- const { resolve } = require('node:path')
4
- const stage = require('@toa.io/userland/stage')
4
+ import { resolve } from 'node:path'
5
+ import * as boot from '@toa.io/boot'
6
+ import { Connector, Locator } from '@toa.io/core'
7
+ import { timeout } from '@toa.io/generic'
8
+ import * as stage from '@toa.io/userland/stage'
5
9
 
6
- const binding = stage.binding.binding
7
- const root = resolve(__dirname, '../components')
10
+ const root = resolve(import.meta.dirname, '../components')
8
11
 
9
12
  let remote
13
+ let emitter
14
+ let consumer
10
15
 
11
- beforeAll(async () => {
16
+ before(async () => {
12
17
  process.env.TOA_DEV = '1'
18
+ process.env.TOA_CONFIGURATION_TEA_POTS = '{}'
19
+
20
+ // the outbox pumps on a tick; at the default five seconds a test would end before it ran
21
+ process.env.TOA_OUTBOX_INTERVAL = '100'
13
22
 
14
23
  const path = resolve(root, 'tea.pots')
15
24
 
16
25
  await stage.composition([path])
17
26
 
18
27
  remote = await stage.remote('tea.pots')
28
+
29
+ // what `store.orders` would emit, sent the way it would send it
30
+ emitter = await boot.bindings.emit(AMQP, new Locator('orders', 'store'), 'created')
31
+
32
+ await emitter.connect()
19
33
  })
20
34
 
21
- afterAll(async () => {
35
+ after(async () => {
36
+ await consumer?.disconnect()
37
+ await emitter.disconnect()
22
38
  await stage.shutdown()
23
39
 
24
40
  delete process.env.TOA_DEV
41
+ delete process.env.TOA_CONFIGURATION_TEA_POTS
42
+ delete process.env.TOA_OUTBOX_INTERVAL
25
43
  })
26
44
 
27
45
  it('should receive event', async () => {
28
46
  const created = await remote.invoke('transit', { input: { material: 'glass' } })
29
47
  const id = created.id
30
- const payload = { pot: id }
31
- const message = { payload }
32
48
  const request = { query: { id } }
33
49
 
34
- await binding.emit('store.orders.created', message)
50
+ await emitter.emit({ payload: { pot: id } })
51
+
52
+ // the receiver books the pot on delivery, which is asynchronous
53
+ const deadline = Date.now() + 5000
35
54
 
36
- const reply = await remote.invoke('observe', request)
55
+ let reply
37
56
 
38
- expect(reply.booked).toStrictEqual(true)
57
+ do {
58
+ await timeout(50)
59
+
60
+ reply = await remote.invoke('observe', request)
61
+ } while (reply.booked !== true && Date.now() < deadline)
62
+
63
+ assert.deepStrictEqual(reply.booked, true)
39
64
  })
40
65
 
41
66
  it('should emit event', async () => {
42
- expect.assertions(1)
43
-
44
67
  const material = 'steel'
45
68
 
46
- await binding.subscribe('tea.pots.created', (pot) => {
47
- expect(pot.payload.material).toStrictEqual(material)
69
+ const received = new Promise((resolve) => {
70
+ consumer = boot.receive('tea.pots.created', new Subscriber(resolve))
48
71
  })
49
72
 
73
+ consumer = await consumer
74
+
75
+ await consumer.connect()
50
76
  await remote.invoke('transit', { input: { material } })
77
+
78
+ const payload = await received
79
+
80
+ assert.deepStrictEqual(payload.material, material)
51
81
  })
82
+
83
+ /** What the event consumer hands deliveries to. */
84
+ class Subscriber extends Connector {
85
+ #handler
86
+
87
+ constructor (handler) {
88
+ super()
89
+
90
+ this.#handler = handler
91
+ }
92
+
93
+ async receive (message) {
94
+ this.#handler(message.payload)
95
+ }
96
+ }
97
+
98
+ const AMQP = '@toa.io/bindings.amqp'
@@ -1,22 +1,25 @@
1
- 'use strict'
1
+ import { it, before, after } from 'node:test'
2
+ import assert from 'node:assert/strict'
2
3
 
3
- const { resolve } = require('node:path')
4
- const stage = require('@toa.io/userland/stage')
4
+ import { resolve } from 'node:path'
5
+ import * as stage from '@toa.io/userland/stage'
5
6
 
6
- const root = resolve(__dirname, '../components')
7
+ const root = resolve(import.meta.dirname, '../components')
7
8
 
8
9
  /** @type {toa.core.Component} */
9
10
  let component
10
11
 
11
- beforeAll(async () => {
12
+ before(async () => {
12
13
  process.env.TOA_DEV = '1'
14
+ process.env.TOA_CONFIGURATION_DEFAULT_ECHO = '{}'
15
+ process.env.TOA_CONFIGURATION_TEA_POTS = '{}'
13
16
 
14
17
  const path = resolve(root, 'math.calculations')
15
18
 
16
19
  component = await stage.component(path)
17
20
  })
18
21
 
19
- afterAll(async () => {
22
+ after(async () => {
20
23
  await stage.shutdown()
21
24
 
22
25
  delete process.env.TOA_DEV
@@ -28,6 +31,6 @@ it('should invoke', async () => {
28
31
 
29
32
  const reply = await component.invoke('add', { input: { a, b } })
30
33
 
31
- expect(reply.exception).toBeUndefined()
32
- expect(reply.output).toStrictEqual(a + b)
34
+ assert.strictEqual(reply.exception, undefined)
35
+ assert.deepStrictEqual(reply.output, a + b)
33
36
  })
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@toa.io/userland",
3
- "version": "1.0.0-alpha.273",
3
+ "version": "1.0.0-alpha.277",
4
+ "type": "module",
4
5
  "description": "Toa development kit",
5
6
  "homepage": "https://toa.io",
6
7
  "author": {
@@ -22,15 +23,20 @@
22
23
  "access": "public"
23
24
  },
24
25
  "main": "src/index.js",
26
+ "exports": {
27
+ ".": "./src/index.js",
28
+ "./stage": "./stage/src/index.js",
29
+ "./package.json": "./package.json"
30
+ },
25
31
  "types": "types/index.d.ts",
26
32
  "dependencies": {
27
- "@toa.io/boot": "1.0.0-alpha.273",
28
- "@toa.io/core": "1.0.0-alpha.273",
29
- "@toa.io/generic": "1.0.0-alpha.273",
30
- "@toa.io/norm": "1.0.0-alpha.273",
31
- "@toa.io/schemas": "1.0.0-alpha.273",
33
+ "@toa.io/boot": "1.0.0-alpha.277",
34
+ "@toa.io/core": "1.0.0-alpha.277",
35
+ "@toa.io/generic": "1.0.0-alpha.277",
36
+ "@toa.io/norm": "1.0.0-alpha.277",
37
+ "@toa.io/schemas": "1.0.0-alpha.277",
32
38
  "@toa.io/storages.null": "0.22.0",
33
- "js-yaml": "4.3.1"
39
+ "js-yaml": "5.4.1"
34
40
  },
35
- "gitHead": "ad3284850ece95ffd5325fd0995707fc144c9c3d"
41
+ "gitHead": "0c0071418c2c56a44e621ba5ced5cdd23dff1738"
36
42
  }
@@ -1,4 +1,5 @@
1
1
  {
2
+ "type": "module",
2
3
  "private": true,
3
4
  "main": "src/index.js",
4
5
  "types": "types/index.d.ts"
@@ -1,10 +1,8 @@
1
- 'use strict'
2
-
3
- const boot = require('@toa.io/boot')
4
- const { state } = require('./state')
1
+ import * as boot from '@toa.io/boot'
2
+ import { state } from './state.js'
5
3
 
6
4
  /** @type {toa.stage.Component} */
7
- const component = async (path, options) => {
5
+ export const component = async (path, options) => {
8
6
  const manifest = await boot.manifest(path, options)
9
7
  const component = await boot.component(manifest)
10
8
 
@@ -14,5 +12,3 @@ const component = async (path, options) => {
14
12
 
15
13
  return component
16
14
  }
17
-
18
- exports.component = component
@@ -1,15 +1,11 @@
1
- 'use strict'
2
-
3
- const boot = require('@toa.io/boot')
4
- const { state } = require('./state')
1
+ import * as boot from '@toa.io/boot'
2
+ import { state } from './state.js'
5
3
 
6
4
  /** @type {toa.stage.Composition} */
7
- const composition = async (paths, options) => {
5
+ export const composition = async (paths, options) => {
8
6
  const composition = await boot.composition(paths, options)
9
7
 
10
8
  await composition.connect()
11
9
 
12
10
  state.compositions.push(composition)
13
11
  }
14
-
15
- exports.composition = composition
@@ -1,16 +1,8 @@
1
- 'use strict'
1
+ import { manifest } from './manifest.js'
2
+ import { component } from './component.js'
3
+ import { composition } from './composition.js'
4
+ import { service } from './service.js'
5
+ import { remote } from './remote.js'
6
+ import { shutdown } from './shutdown.js'
2
7
 
3
- const { manifest } = require('./manifest')
4
- const { component } = require('./component')
5
- const { composition } = require('./composition')
6
- const { service } = require('./service')
7
- const { remote } = require('./remote')
8
- const { shutdown } = require('./shutdown')
9
-
10
- exports.manifest = manifest
11
- exports.component = component
12
- exports.composition = composition
13
- exports.compose = composition
14
- exports.serve = service
15
- exports.remote = remote
16
- exports.shutdown = shutdown
8
+ export { manifest, component, composition, composition as compose, service as serve, remote, shutdown }
@@ -1,12 +1,8 @@
1
- 'use strict'
2
-
3
- const boot = require('@toa.io/boot')
1
+ import * as boot from '@toa.io/boot'
4
2
 
5
3
  /**
6
4
  * @type {toa.stage.Manifest}
7
5
  */
8
- const manifest = async (path) => {
6
+ export const manifest = async (path) => {
9
7
  return boot.manifest(path)
10
8
  }
11
-
12
- exports.manifest = manifest
@@ -1,12 +1,10 @@
1
- 'use strict'
1
+ import * as boot from '@toa.io/boot'
2
+ import { Locator } from '@toa.io/core'
2
3
 
3
- const boot = require('@toa.io/boot')
4
- const { Locator } = require('@toa.io/core')
5
-
6
- const { state } = require('./state')
4
+ import { state } from './state.js'
7
5
 
8
6
  /** @type {toa.stage.Remote} */
9
- const remote = async (id) => {
7
+ export const remote = async (id) => {
10
8
  const segments = id.split('.')
11
9
 
12
10
  if (segments.length === 1) segments.unshift('default')
@@ -28,5 +26,3 @@ const remote = async (id) => {
28
26
  * `unknown` on the application's map.
29
27
  */
30
28
  const SOURCE = { service: 'features' }
31
-
32
- exports.remote = remote
@@ -1,14 +1,17 @@
1
- 'use strict'
1
+ import { createRequire } from 'node:module'
2
+ import { pathToFileURL } from 'node:url'
3
+ import * as boot from '@toa.io/boot'
4
+ import { state } from './state.js'
5
+ import { shortcuts } from '@toa.io/norm'
2
6
 
3
- const boot = require('@toa.io/boot')
4
- const { state } = require('./state')
5
- const { shortcuts } = require('@toa.io/norm')
7
+ // a shortcut resolves to a package name, which is not a path
8
+ const require = createRequire(import.meta.url)
6
9
 
7
- const service = async (ref) => {
10
+ export const service = async (ref) => {
8
11
  const path = shortcuts.resolve(ref)
9
- const { Factory } = require(path)
12
+ const { Factory } = await import(pathToFileURL(require.resolve(path)).href)
10
13
  const factory = new Factory(boot)
11
- const service = factory.service()
14
+ const service = await factory.service()
12
15
 
13
16
  await service.connect()
14
17
 
@@ -16,5 +19,3 @@ const service = async (ref) => {
16
19
 
17
20
  return service
18
21
  }
19
-
20
- exports.service = service
@@ -1,9 +1,7 @@
1
- 'use strict'
2
-
3
- const { state } = require('./state')
1
+ import { state } from './state.js'
4
2
 
5
3
  /** @type {toa.stage.Shutdown} */
6
- const shutdown = async () => {
4
+ export const shutdown = async () => {
7
5
  const components = state.components.map((component) => component.disconnect())
8
6
  const compositions = state.compositions.map((composition) => composition.disconnect())
9
7
  const services = state.services.map((service) => service.disconnect())
@@ -14,5 +12,3 @@ const shutdown = async () => {
14
12
 
15
13
  state.reset()
16
14
  }
17
-
18
- exports.shutdown = shutdown
@@ -1,6 +1,4 @@
1
- 'use strict'
2
-
3
- const state = {}
1
+ export const state = {}
4
2
 
5
3
  const reset = () => {
6
4
  Object.assign(state, {
@@ -15,4 +13,3 @@ const reset = () => {
15
13
  reset()
16
14
 
17
15
  /** @type {toa.stage.State} */
18
- exports.state = state
@@ -1,16 +1,14 @@
1
- 'use strict'
1
+ import { mock } from 'node:test'
2
2
 
3
- const { generate } = require('randomstring')
3
+ import { generate } from 'randomstring'
4
4
 
5
5
  const connector = () => ({
6
- connect: jest.fn(),
7
- disconnect: jest.fn(),
8
- link: jest.fn()
6
+ connect: mock.fn(),
7
+ disconnect: mock.fn(),
8
+ link: mock.fn()
9
9
  })
10
10
 
11
- const manifest = jest.fn(async () => generate())
12
- const component = jest.fn(async () => connector())
13
- const composition = jest.fn(async () => connector())
14
- const remote = jest.fn(async () => connector())
15
-
16
- module.exports = { manifest, component, composition, remote }
11
+ export const manifest = mock.fn(async () => generate())
12
+ export const component = mock.fn(async () => connector())
13
+ export const composition = mock.fn(async () => connector())
14
+ export const remote = mock.fn(async () => connector())
@@ -1,25 +1,31 @@
1
- 'use strict'
1
+ import { it, mock as mocking } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { isDeepStrictEqual } from 'node:util'
2
4
 
3
- const { generate } = require('randomstring')
5
+ import { generate } from 'randomstring'
6
+ import * as _boot from './boot.mock.js'
4
7
 
5
8
  const mock = {
6
- boot: require('./boot.mock')
9
+ boot: _boot
7
10
  }
8
11
 
9
- jest.mock('@toa.io/boot', () => mock.boot)
12
+ mocking.module('@toa.io/boot', { namedExports: mock.boot })
10
13
 
11
- const stage = require('../')
14
+ const stage = await import('../src/index.js')
12
15
 
13
16
  it('should be', () => {
14
- expect(stage.component).toBeDefined()
17
+ assert.notStrictEqual(stage.component, undefined)
15
18
  })
16
19
 
17
20
  it('should boot component', async () => {
18
21
  const path = generate()
19
22
  const component = await stage.component(path)
20
23
 
21
- expect(mock.boot.manifest.mock.calls[0][0]).toStrictEqual(path)
22
- expect(mock.boot.component).toHaveBeenCalledWith(await mock.boot.manifest.mock.results[0].value)
23
- expect(component).toStrictEqual(await mock.boot.component.mock.results[0].value)
24
- expect(component.connect).toHaveBeenCalled()
24
+ assert.deepStrictEqual(mock.boot.manifest.mock.calls[0].arguments[0], path)
25
+ const manifest = await mock.boot.manifest.mock.calls[0].result
26
+
27
+ assert.ok(mock.boot.component.mock.calls.some((call) =>
28
+ call.arguments.length === 1 && isDeepStrictEqual(call.arguments[0], manifest)))
29
+ assert.deepStrictEqual(component, await mock.boot.component.mock.calls[0].result)
30
+ assert.ok(component.connect.mock.callCount() > 0)
25
31
  })
@@ -1,27 +1,29 @@
1
- 'use strict'
1
+ import { it, mock as mocking } from 'node:test'
2
+ import assert from 'node:assert/strict'
2
3
 
3
- const { generate } = require('randomstring')
4
+ import { generate } from 'randomstring'
5
+ import * as _boot from './boot.mock.js'
4
6
 
5
7
  const mock = {
6
- boot: require('./boot.mock')
8
+ boot: _boot
7
9
  }
8
10
 
9
- jest.mock('@toa.io/boot', () => mock.boot)
11
+ mocking.module('@toa.io/boot', { namedExports: mock.boot })
10
12
 
11
- const stage = require('../')
13
+ const stage = await import('../src/index.js')
12
14
 
13
15
  const paths = [generate(), generate()]
14
16
 
15
17
  it('should be', () => {
16
- expect(stage.composition).toBeDefined()
18
+ assert.notStrictEqual(stage.composition, undefined)
17
19
  })
18
20
 
19
21
  it('should boot composition', async () => {
20
22
  await stage.composition(paths)
21
23
 
22
- expect(mock.boot.composition.mock.calls[0][0]).toStrictEqual(paths)
24
+ assert.deepStrictEqual(mock.boot.composition.mock.calls[0].arguments[0], paths)
23
25
 
24
- const composition = await mock.boot.composition.mock.results[0].value
26
+ const composition = await mock.boot.composition.mock.calls[0].result
25
27
 
26
- expect(composition.connect).toHaveBeenCalled()
28
+ assert.ok(composition.connect.mock.callCount() > 0)
27
29
  })
@@ -1,17 +1,20 @@
1
- 'use strict'
1
+ import { it, mock as mocking } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { isDeepStrictEqual } from 'node:util'
2
4
 
3
- const { generate } = require('randomstring')
5
+ import { generate } from 'randomstring'
6
+ import * as _boot from './boot.mock.js'
4
7
 
5
8
  const mock = {
6
- boot: require('./boot.mock')
9
+ boot: _boot
7
10
  }
8
11
 
9
- jest.mock('@toa.io/boot', () => mock.boot)
12
+ mocking.module('@toa.io/boot', { namedExports: mock.boot })
10
13
 
11
- const stage = require('../')
14
+ const stage = await import('../src/index.js')
12
15
 
13
16
  it('should be', () => {
14
- expect(stage.manifest).toBeDefined()
17
+ assert.notStrictEqual(stage.manifest, undefined)
15
18
  })
16
19
 
17
20
  it('should boot manifest', async () => {
@@ -19,6 +22,6 @@ it('should boot manifest', async () => {
19
22
 
20
23
  const manifest = await stage.manifest(path)
21
24
 
22
- expect(mock.boot.manifest).toHaveBeenCalledWith(path)
23
- expect(manifest).toStrictEqual(await mock.boot.manifest.mock.results[0].value)
25
+ assert.ok(mock.boot.manifest.mock.calls.some((call) => call.arguments.length === 1 && isDeepStrictEqual(call.arguments[0], path)))
26
+ assert.deepStrictEqual(manifest, await mock.boot.manifest.mock.calls[0].result)
24
27
  })
@@ -1,17 +1,19 @@
1
- 'use strict'
1
+ import { it, mock as mocking } from 'node:test'
2
+ import assert from 'node:assert/strict'
2
3
 
3
- const { generate } = require('randomstring')
4
+ import { generate } from 'randomstring'
5
+ import * as _boot from './boot.mock.js'
4
6
 
5
7
  const mock = {
6
- boot: require('./boot.mock')
8
+ boot: _boot
7
9
  }
8
10
 
9
- jest.mock('@toa.io/boot', () => mock.boot)
11
+ mocking.module('@toa.io/boot', { namedExports: mock.boot })
10
12
 
11
- const stage = require('../')
13
+ const stage = await import('../src/index.js')
12
14
 
13
15
  it('should be', () => {
14
- expect(stage.remote).toBeDefined()
16
+ assert.notStrictEqual(stage.remote, undefined)
15
17
  })
16
18
 
17
19
  it('should connect remote', async () => {
@@ -21,12 +23,12 @@ it('should connect remote', async () => {
21
23
 
22
24
  const remote = await stage.remote(id)
23
25
 
24
- expect(mock.boot.remote).toHaveBeenCalled()
25
- expect(remote).toStrictEqual(await mock.boot.remote.mock.results[0].value)
26
- expect(remote.connect).toHaveBeenCalled()
26
+ assert.ok(mock.boot.remote.mock.callCount() > 0)
27
+ assert.deepStrictEqual(remote, await mock.boot.remote.mock.calls[0].result)
28
+ assert.ok(remote.connect.mock.callCount() > 0)
27
29
 
28
- const locator = mock.boot.remote.mock.calls[0][0]
30
+ const locator = mock.boot.remote.mock.calls[0].arguments[0]
29
31
 
30
- expect(locator).toBeDefined()
31
- expect(locator).toMatchObject({ name, namespace })
32
+ assert.notStrictEqual(locator, undefined)
33
+ assert.partialDeepStrictEqual(locator, { name, namespace })
32
34
  })
@@ -1,24 +1,27 @@
1
- 'use strict'
1
+ import { it, beforeEach, mock as mocking } from 'node:test'
2
+ import assert from 'node:assert/strict'
2
3
 
3
- const { generate } = require('randomstring')
4
+ import { generate } from 'randomstring'
5
+ import * as _boot from './boot.mock.js'
6
+ import * as _state from './state.mock.js'
4
7
 
5
8
  const mock = {
6
- boot: require('./boot.mock'),
7
- state: require('./state.mock')
9
+ boot: _boot,
10
+ state: _state
8
11
  }
9
12
 
10
- jest.mock('@toa.io/boot', () => mock.boot)
11
- jest.mock('../src/state', () => mock.state)
13
+ mocking.module('@toa.io/boot', { namedExports: mock.boot })
14
+ mocking.module('../src/state', { namedExports: mock.state })
12
15
 
13
- const { state } = require('../src/state')
14
- const stage = require('../')
16
+ const { state } = await import('../src/state.js')
17
+ const stage = await import('../src/index.js')
15
18
 
16
19
  beforeEach(() => {
17
- jest.clearAllMocks()
20
+ resetCalls()
18
21
  })
19
22
 
20
23
  it('should be', () => {
21
- expect(stage.shutdown).toBeDefined()
24
+ assert.notStrictEqual(stage.shutdown, undefined)
22
25
  })
23
26
 
24
27
  it('should disconnect components', async () => {
@@ -27,7 +30,7 @@ it('should disconnect components', async () => {
27
30
 
28
31
  await stage.shutdown()
29
32
 
30
- expect(component.disconnect).toHaveBeenCalled()
33
+ assert.ok(component.disconnect.mock.callCount() > 0)
31
34
  })
32
35
 
33
36
  it('should disconnect compositions', async () => {
@@ -36,9 +39,9 @@ it('should disconnect compositions', async () => {
36
39
  await stage.composition(paths)
37
40
  await stage.shutdown()
38
41
 
39
- const composition = await mock.boot.composition.mock.results[0].value
42
+ const composition = await mock.boot.composition.mock.calls[0].result
40
43
 
41
- expect(composition.disconnect).toHaveBeenCalled()
44
+ assert.ok(composition.disconnect.mock.callCount() > 0)
42
45
  })
43
46
 
44
47
  it('should disconnect remotes', async () => {
@@ -47,11 +50,21 @@ it('should disconnect remotes', async () => {
47
50
  const remote = await stage.remote(id)
48
51
  await stage.shutdown()
49
52
 
50
- expect(remote.disconnect).toHaveBeenCalled()
53
+ assert.ok(remote.disconnect.mock.callCount() > 0)
51
54
  })
52
55
 
53
56
  it('should reset state', async () => {
54
57
  await stage.shutdown()
55
58
 
56
- expect(state.reset).toHaveBeenCalled()
59
+ assert.ok(state.reset.mock.callCount() > 0)
57
60
  })
61
+
62
+ function resetCalls (target = [assert, mock, stage], seen = new Set()) {
63
+ if (target === null || typeof target !== 'object' || seen.has(target)) return
64
+
65
+ seen.add(target)
66
+
67
+ for (const value of Object.values(target))
68
+ if (typeof value === 'function' && value.mock !== undefined) value.mock.resetCalls()
69
+ else resetCalls(value, seen)
70
+ }
@@ -1,8 +1,7 @@
1
- 'use strict'
1
+ import { mock } from 'node:test'
2
2
 
3
- const original = jest.requireActual('../src/state')
3
+ // this module defines the replacement, so it still sees the real one
4
+ import * as original from '../src/state.js'
4
5
 
5
- const reset = jest.fn(() => original.state.reset())
6
- const state = { ...original.state, reset }
7
-
8
- module.exports = { state }
6
+ const reset = mock.fn(() => original.state.reset())
7
+ export const state = { ...original.state, reset }
@@ -1,9 +1,10 @@
1
- 'use strict'
1
+ import { it } from 'node:test'
2
+ import assert from 'node:assert/strict'
2
3
 
3
- const { state } = require('../src/state')
4
+ import { state } from '../src/state.js'
4
5
 
5
6
  it('should be', () => {
6
- expect(state).toBeDefined()
7
+ assert.notStrictEqual(state, undefined)
7
8
  })
8
9
 
9
10
  it('should reset', () => {
@@ -11,9 +12,9 @@ it('should reset', () => {
11
12
  const composition = /** @type {toa.core.Connector} */ 2
12
13
  const remote = /** @type {toa.core.Component} */ 3
13
14
 
14
- expect(state.components.length).toStrictEqual(0)
15
- expect(state.compositions.length).toStrictEqual(0)
16
- expect(state.remotes.length).toStrictEqual(0)
15
+ assert.deepStrictEqual(state.components.length, 0)
16
+ assert.deepStrictEqual(state.compositions.length, 0)
17
+ assert.deepStrictEqual(state.remotes.length, 0)
17
18
 
18
19
  state.components.push(component)
19
20
  state.compositions.push(composition)
@@ -21,7 +22,7 @@ it('should reset', () => {
21
22
 
22
23
  state.reset()
23
24
 
24
- expect(state.components.length).toStrictEqual(0)
25
- expect(state.compositions.length).toStrictEqual(0)
26
- expect(state.remotes.length).toStrictEqual(0)
25
+ assert.deepStrictEqual(state.components.length, 0)
26
+ assert.deepStrictEqual(state.compositions.length, 0)
27
+ assert.deepStrictEqual(state.remotes.length, 0)
27
28
  })