@toa.io/operations 1.0.0-alpha.29 → 1.0.0-alpha.291

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.
Files changed (75) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/image/runtime.Dockerfile +4 -0
  3. package/package.json +8 -10
  4. package/readme.md +93 -0
  5. package/src/deployment/.deployment/.describe/components.js +1 -5
  6. package/src/deployment/.deployment/.describe/compositions.js +26 -7
  7. package/src/deployment/.deployment/.describe/dependencies.js +1 -6
  8. package/src/deployment/.deployment/.describe/events.js +63 -0
  9. package/src/deployment/.deployment/.describe/fold.js +42 -0
  10. package/src/deployment/.deployment/.describe/fold.test.js +108 -0
  11. package/src/deployment/.deployment/.describe/index.js +4 -13
  12. package/src/deployment/.deployment/.describe/mounts.js +17 -0
  13. package/src/deployment/.deployment/.describe/resources.js +103 -0
  14. package/src/deployment/.deployment/.describe/resources.test.js +129 -0
  15. package/src/deployment/.deployment/.describe/services.js +32 -6
  16. package/src/deployment/.deployment/.describe/services.test.js +78 -0
  17. package/src/deployment/.deployment/.describe/variables.js +5 -9
  18. package/src/deployment/.deployment/declare.js +1 -5
  19. package/src/deployment/.deployment/describe.js +125 -10
  20. package/src/deployment/.deployment/index.js +3 -9
  21. package/src/deployment/.deployment/merge.js +66 -6
  22. package/src/deployment/.deployment/merge.test.js +133 -0
  23. package/src/deployment/chart/templates/components.yaml +1 -1
  24. package/src/deployment/chart/templates/compositions.yaml +96 -4
  25. package/src/deployment/chart/templates/mono.yaml +182 -0
  26. package/src/deployment/chart/templates/services.yaml +82 -7
  27. package/src/deployment/chart/values.yaml +23 -1
  28. package/src/deployment/composition.js +6 -6
  29. package/src/deployment/deployment.js +55 -25
  30. package/src/deployment/drain.js +70 -0
  31. package/src/deployment/drain.test.js +64 -0
  32. package/src/deployment/factory.js +121 -32
  33. package/src/deployment/images/bundle.js +82 -0
  34. package/src/deployment/images/composition.Dockerfile +8 -16
  35. package/src/deployment/images/composition.js +9 -56
  36. package/src/deployment/images/dependencies.Dockerfile +16 -0
  37. package/src/deployment/images/dependencies.js +141 -0
  38. package/src/deployment/images/dependencies.test.js +214 -0
  39. package/src/deployment/images/factory.js +31 -15
  40. package/src/deployment/images/format.js +52 -0
  41. package/src/deployment/images/format.test.js +74 -0
  42. package/src/deployment/images/image.fixtures.js +13 -13
  43. package/src/deployment/images/image.js +77 -38
  44. package/src/deployment/images/image.test.js +11 -5
  45. package/src/deployment/images/index.js +1 -5
  46. package/src/deployment/images/mono.Dockerfile +11 -0
  47. package/src/deployment/images/mono.js +15 -0
  48. package/src/deployment/images/publish.js +93 -0
  49. package/src/deployment/images/runtime-base.test.js +97 -0
  50. package/src/deployment/images/service.Dockerfile +5 -4
  51. package/src/deployment/images/service.js +13 -13
  52. package/src/deployment/index.js +1 -5
  53. package/src/deployment/operator.js +13 -13
  54. package/src/deployment/operator.test.js +8 -7
  55. package/src/deployment/registry.js +165 -47
  56. package/src/deployment/registry.test.js +407 -0
  57. package/src/deployment/service.js +4 -6
  58. package/src/deployment/workspace.js +13 -8
  59. package/src/index.js +1 -3
  60. package/src/process.js +51 -13
  61. package/src/process.test.js +33 -0
  62. package/types/_deployment/composition.d.ts +1 -3
  63. package/types/_deployment/dependency.d.ts +13 -7
  64. package/types/_deployment/deployment.d.ts +3 -7
  65. package/types/_deployment/factory.d.ts +2 -4
  66. package/types/_deployment/images/factory.d.ts +6 -8
  67. package/types/_deployment/images/image.d.ts +16 -1
  68. package/types/_deployment/images/registry.d.ts +8 -11
  69. package/types/_deployment/operator.d.ts +7 -9
  70. package/types/_deployment/registry.d.ts +4 -4
  71. package/types/_deployment/service.d.ts +4 -3
  72. package/types/dependency.ts +79 -0
  73. package/types/index.ts +1 -0
  74. package/types/dependency.d.ts +0 -39
  75. package/types/index.d.ts +0 -1
@@ -0,0 +1,141 @@
1
+ import { basename, join } from 'node:path'
2
+ import { existsSync, readFileSync } from 'node:fs'
3
+ import { copyFile, mkdir } from 'node:fs/promises'
4
+ import { createHash } from 'node:crypto'
5
+
6
+ import { Image } from './image.js'
7
+
8
+ /**
9
+ * What a bundle's components depend on, installed on the base: the layers that weigh the
10
+ * most and change the least, so they are an image of their own. Its tag digests everything
11
+ * the install reads, and nothing else: a change to the sources leaves it where it is, and a
12
+ * bundle is laid over it without building or moving it again.
13
+ *
14
+ * @implements {toa.deployment.images.Image}
15
+ */
16
+ export class Dependencies extends Image {
17
+ dockerfile = join(import.meta.dirname, 'dependencies.Dockerfile')
18
+ arguments = true
19
+
20
+ /** @type {toa.deployment.images.Bundle} */
21
+ #owner
22
+
23
+ #runtime
24
+ #registry
25
+
26
+ /** @type {string | undefined} */
27
+ #version
28
+
29
+ /**
30
+ * @param {string} scope
31
+ * @param {toa.norm.context.Runtime} runtime
32
+ * @param {toa.norm.context.Registry} registry
33
+ * @param {toa.deployment.images.Bundle} owner the bundle laid over this image
34
+ */
35
+ constructor(scope, runtime, registry, owner) {
36
+ super(scope, runtime, registry)
37
+
38
+ this.#owner = owner
39
+ this.#runtime = runtime
40
+ this.#registry = registry
41
+ }
42
+
43
+ /** Same repository as the bundle: a tag apart, not a name apart. */
44
+ get name() {
45
+ return this.#owner.name
46
+ }
47
+
48
+ get version() {
49
+ if (this.#version !== undefined) return this.#version
50
+
51
+ const hash = createHash('sha256')
52
+ const build = this.#registry.build ?? {}
53
+
54
+ const inputs = [
55
+ this.#runtime.version,
56
+ this.#runtime.registry,
57
+ this.#runtime.proxy,
58
+ this.base,
59
+ build.image,
60
+ build.run,
61
+ this.run,
62
+ ...(build.arguments ?? [])
63
+ ]
64
+
65
+ for (const input of inputs) hash.update(input ?? '').update('\0')
66
+
67
+ for (const [label, files] of this.#manifests())
68
+ for (const file of files)
69
+ hash.update(label).update(basename(file)).update(readFileSync(file))
70
+
71
+ this.#version = hash.digest('hex').slice(0, 8)
72
+
73
+ return this.#version
74
+ }
75
+
76
+ digest() {
77
+ return PREFIX + this.version
78
+ }
79
+
80
+ get base() {
81
+ if (this.#owner.image !== undefined) return this.#owner.image
82
+
83
+ const images = new Set(
84
+ this.#owner.components.map((component) => component.build?.image)
85
+ )
86
+
87
+ if (images.size > 1) throw new Error(this.#owner.conflict())
88
+
89
+ return images.values().next().value
90
+ }
91
+
92
+ get run() {
93
+ const commands = []
94
+
95
+ for (const component of this.#owner.components) {
96
+ const run = component.build?.run
97
+
98
+ if (run !== undefined) commands.push(run)
99
+ }
100
+
101
+ return commands.join('\n')
102
+ }
103
+
104
+ async prepare(root) {
105
+ const context = await super.prepare(join(root, DIRECTORY))
106
+
107
+ for (const [label, files] of this.#manifests()) {
108
+ const target = join(context, label)
109
+
110
+ await mkdir(target, { recursive: true })
111
+
112
+ for (const file of files) await copyFile(file, join(target, basename(file)))
113
+ }
114
+
115
+ return context
116
+ }
117
+
118
+ /**
119
+ * The files the install reads, by component label, in a fixed order.
120
+ *
121
+ * @returns {Array<[string, string[]]>}
122
+ */
123
+ #manifests() {
124
+ const manifests = []
125
+
126
+ for (const component of this.#owner.components) {
127
+ const files = MANIFESTS.map((name) => join(component.path, name)).filter(existsSync)
128
+
129
+ if (files.length > 0) manifests.push([component.locator.label, files])
130
+ }
131
+
132
+ return manifests.sort(([a], [b]) => a.localeCompare(b))
133
+ }
134
+ }
135
+
136
+ const PREFIX = 'deps-'
137
+
138
+ /** Where the contexts land, apart from the bundles', whose names they share. */
139
+ const DIRECTORY = 'dependencies'
140
+
141
+ const MANIFESTS = ['package.json', 'package-lock.json']
@@ -0,0 +1,214 @@
1
+ import { describe, it, beforeEach } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { join } from 'node:path'
5
+ import { mkdtemp, mkdir, writeFile, readdir, readFile } from 'node:fs/promises'
6
+ import { tmpdir } from 'node:os'
7
+
8
+ import { Composition } from './composition.js'
9
+ import { Mono } from './mono.js'
10
+
11
+ /** @type {string} */
12
+ let root
13
+
14
+ /** @type {toa.norm.context.Runtime} */
15
+ let runtime
16
+
17
+ /** @type {toa.norm.context.Registry} */
18
+ let registry
19
+
20
+ /** @type {object} */
21
+ let composition
22
+
23
+ beforeEach(async () => {
24
+ root = await mkdtemp(join(tmpdir(), 'toa-dependencies-test'))
25
+ runtime = { version: '1.0.0-alpha.288' }
26
+ registry = { base: 'example.com/reg', platforms: ['linux/amd64'] }
27
+
28
+ composition = {
29
+ name: 'mono',
30
+ components: [
31
+ await component('one', { dependencies: { left: '1.0.0' } }),
32
+ await component('two'),
33
+ await component('three', { devDependencies: { right: '1.0.0' } }, 'lock')
34
+ ]
35
+ }
36
+ })
37
+
38
+ describe('reference', () => {
39
+ it('should share the repository and stand a prefix apart', () => {
40
+ const image = create()
41
+
42
+ assert.match(
43
+ image.dependencies.reference,
44
+ /^example\.com\/reg\/acme\/composition-mono:deps-[0-9a-f]{8}$/
45
+ )
46
+ assert.match(
47
+ image.reference,
48
+ /^example\.com\/reg\/acme\/composition-mono:[0-9a-f]{8}$/
49
+ )
50
+ assert.strictEqual(image.base, image.dependencies.reference)
51
+ })
52
+
53
+ it('should be the same for the same inputs', () => {
54
+ assert.strictEqual(create().dependencies.reference, create().dependencies.reference)
55
+ })
56
+
57
+ it('should change with a manifest', async () => {
58
+ const before = create()
59
+
60
+ await writeFile(
61
+ join(composition.components[0].path, 'package.json'),
62
+ JSON.stringify({ dependencies: { left: '1.0.1' } })
63
+ )
64
+
65
+ const after = create()
66
+
67
+ assert.notStrictEqual(after.dependencies.reference, before.dependencies.reference)
68
+ assert.notStrictEqual(after.reference, before.reference)
69
+ })
70
+
71
+ it('should change with a lockfile', async () => {
72
+ const before = create()
73
+
74
+ await writeFile(join(composition.components[2].path, 'package-lock.json'), 'other')
75
+
76
+ assert.notStrictEqual(create().dependencies.reference, before.dependencies.reference)
77
+ })
78
+
79
+ it('should change with what is run and with the runtime', () => {
80
+ const before = create()
81
+
82
+ composition.components[1].build = { run: 'apk add git' }
83
+
84
+ const run = create()
85
+
86
+ assert.notStrictEqual(run.dependencies.reference, before.dependencies.reference)
87
+
88
+ runtime.version = '1.0.0-alpha.289'
89
+
90
+ assert.notStrictEqual(create().dependencies.reference, run.dependencies.reference)
91
+ })
92
+
93
+ it('should change with the registry build settings', () => {
94
+ const before = create()
95
+
96
+ registry.build = { arguments: ['TOKEN'] }
97
+
98
+ assert.notStrictEqual(create().dependencies.reference, before.dependencies.reference)
99
+ })
100
+
101
+ it('should not change with the sources', async () => {
102
+ const before = create()
103
+
104
+ await writeFile(
105
+ join(composition.components[0].path, 'index.js'),
106
+ 'export const changed = true'
107
+ )
108
+ composition.components[0].version = 'ffffffff'
109
+
110
+ const after = create()
111
+
112
+ assert.strictEqual(after.dependencies.reference, before.dependencies.reference)
113
+ assert.notStrictEqual(after.reference, before.reference)
114
+ })
115
+ })
116
+
117
+ describe('prepare', () => {
118
+ it('should hold the manifests and nothing else', async () => {
119
+ const image = create()
120
+ const context = await image.dependencies.prepare(root)
121
+
122
+ assert.strictEqual(
123
+ context,
124
+ join(root, 'dependencies', `composition-mono.${image.dependencies.version}`)
125
+ )
126
+
127
+ const entries = (await readdir(context, { recursive: true })).sort()
128
+
129
+ assert.deepStrictEqual(entries, [
130
+ '.dockerignore',
131
+ 'Dockerfile',
132
+ 'one',
133
+ 'one/package.json',
134
+ 'three',
135
+ 'three/package-lock.json',
136
+ 'three/package.json'
137
+ ])
138
+
139
+ const dockerfile = await readFile(join(context, 'Dockerfile'), 'utf8')
140
+
141
+ assert.ok(dockerfile.includes('FROM ghcr.io/toa-io/runtime:1.0.0-alpha.288'))
142
+ assert.ok(dockerfile.includes('npm i --omit=dev'))
143
+ assert.ok(dockerfile.includes('WORKDIR /composition'))
144
+ assert.doesNotMatch(dockerfile, /USER node|CMD /)
145
+ })
146
+
147
+ it('should lay the sources over the dependencies', async () => {
148
+ const image = create()
149
+
150
+ image.dependencies.tag()
151
+
152
+ const context = await image.prepare(root)
153
+ const dockerfile = await readFile(join(context, 'Dockerfile'), 'utf8')
154
+ const lines = dockerfile
155
+ .split('\n')
156
+ .filter((line) => line !== '' && !line.startsWith('#'))
157
+
158
+ // no USER: the runtime starts as root and drops to `node` itself, and nothing else
159
+ // touches the filesystem, which is what lets the layer be laid over a base never pulled
160
+ assert.deepStrictEqual(lines, [
161
+ `FROM ${image.dependencies.reference}`,
162
+ 'COPY --link . /composition',
163
+ 'CMD toa compose *'
164
+ ])
165
+
166
+ const entries = (await readdir(context, { recursive: true })).sort()
167
+
168
+ assert.ok(entries.includes('one/index.js'))
169
+ assert.ok(entries.includes('two/package.json')) // declared for the component that had none
170
+ assert.ok(!entries.some((entry) => entry.includes('node_modules')))
171
+ })
172
+
173
+ it('should run mono the same way', async () => {
174
+ const image = new Mono('acme', runtime, registry, composition)
175
+
176
+ image.tag()
177
+
178
+ assert.match(image.dependencies.reference, /\/mono:deps-[0-9a-f]{8}$/)
179
+
180
+ const context = await image.prepare(root)
181
+ const dockerfile = await readFile(join(context, 'Dockerfile'), 'utf8')
182
+
183
+ assert.ok(dockerfile.includes('CMD toa mono *'))
184
+ assert.doesNotMatch(dockerfile, /^USER /m)
185
+ })
186
+ })
187
+
188
+ /** @returns {Composition} */
189
+ function create() {
190
+ const image = new Composition('acme', runtime, registry, composition)
191
+
192
+ image.tag()
193
+
194
+ return image
195
+ }
196
+
197
+ /**
198
+ * @param {string} label
199
+ * @param {object} [manifest]
200
+ * @param {string} [lock]
201
+ */
202
+ async function component(label, manifest, lock) {
203
+ const path = join(root, 'components', label)
204
+
205
+ await mkdir(join(path, 'node_modules', 'left'), { recursive: true })
206
+ await writeFile(join(path, 'index.js'), 'export const changed = false')
207
+ await writeFile(join(path, 'node_modules', 'left', 'index.js'), '')
208
+
209
+ if (manifest !== undefined)
210
+ await writeFile(join(path, 'package.json'), JSON.stringify(manifest))
211
+ if (lock !== undefined) await writeFile(join(path, 'package-lock.json'), lock)
212
+
213
+ return { path, version: 'abcdef12', locator: { id: `default.${label}`, label } }
214
+ }
@@ -1,12 +1,8 @@
1
- 'use strict'
1
+ import { Composition } from './composition.js'
2
+ import { Service } from './service.js'
3
+ import { Mono } from './mono.js'
2
4
 
3
- const { Composition } = require('./composition')
4
- const { Service } = require('./service')
5
-
6
- /**
7
- * @implements {toa.deployment.images.Factory}
8
- */
9
- class Factory {
5
+ export class Factory {
10
6
  /** @type {string} */
11
7
  #scope
12
8
 
@@ -21,7 +17,7 @@ class Factory {
21
17
  * @param {toa.norm.context.Runtime} runtime
22
18
  * @param {toa.norm.context.Registry} registry
23
19
  */
24
- constructor (scope, runtime, registry) {
20
+ constructor(scope, runtime, registry) {
25
21
  this.#scope = scope
26
22
  this.#runtime = runtime
27
23
  this.#registry = registry
@@ -30,8 +26,13 @@ class Factory {
30
26
  /**
31
27
  * @returns {Composition}
32
28
  */
33
- composition (composition) {
34
- const instance = new Composition(this.#scope, this.#runtime, this.#registry, composition)
29
+ composition(composition) {
30
+ const instance = new Composition(
31
+ this.#scope,
32
+ this.#runtime,
33
+ this.#registry,
34
+ composition
35
+ )
35
36
 
36
37
  instance.tag()
37
38
 
@@ -41,13 +42,28 @@ class Factory {
41
42
  /**
42
43
  * @returns {Service}
43
44
  */
44
- service (path, service) {
45
- const instance = new Service(this.#scope, this.#runtime, this.#registry, path, service)
45
+ service(path, service) {
46
+ const instance = new Service(
47
+ this.#scope,
48
+ this.#runtime,
49
+ this.#registry,
50
+ path,
51
+ service
52
+ )
46
53
 
47
54
  instance.tag()
48
55
 
49
56
  return instance
50
57
  }
51
- }
52
58
 
53
- exports.Factory = Factory
59
+ /**
60
+ * @returns {Mono}
61
+ */
62
+ mono(composition) {
63
+ const instance = new Mono(this.#scope, this.#runtime, this.#registry, composition)
64
+
65
+ instance.tag()
66
+
67
+ return instance
68
+ }
69
+ }
@@ -0,0 +1,52 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { writeFile } from 'node:fs/promises'
3
+ import { dirname, join, parse } from 'node:path'
4
+
5
+ /**
6
+ * A component is copied into the image on its own, away from the package that
7
+ * declared what its files are. Node reads a `.js` as CommonJS unless a manifest
8
+ * beside it says otherwise, so the one the component was written under is
9
+ * restated where it lands.
10
+ *
11
+ * A component that ships its own manifest already says so, and is left alone.
12
+ *
13
+ * @param {string} source the component in the workspace
14
+ * @param {string} target where it was copied
15
+ * @param {string} name
16
+ */
17
+ export async function declare(source, target, name) {
18
+ if (existsSync(join(target, MANIFEST))) return
19
+
20
+ await writeFile(
21
+ join(target, MANIFEST),
22
+ JSON.stringify({ name, private: true, type: format(source) }, null, 2) + '\n'
23
+ )
24
+ }
25
+
26
+ /**
27
+ * The module format a directory's files are read as, which the nearest manifest
28
+ * above it decides.
29
+ *
30
+ * @param {string} directory
31
+ * @returns {'module' | 'commonjs'}
32
+ */
33
+ export function format(directory) {
34
+ const { root } = parse(directory)
35
+
36
+ let current = directory
37
+
38
+ while (current !== root) {
39
+ const manifest = join(current, MANIFEST)
40
+
41
+ if (existsSync(manifest))
42
+ return JSON.parse(readFileSync(manifest, 'utf8')).type === 'module'
43
+ ? 'module'
44
+ : 'commonjs'
45
+
46
+ current = dirname(current)
47
+ }
48
+
49
+ return 'commonjs'
50
+ }
51
+
52
+ const MANIFEST = 'package.json'
@@ -0,0 +1,74 @@
1
+ import { it, describe, before, after } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { mkdtemp, mkdir, writeFile, readFile, rm } from 'node:fs/promises'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+
7
+ import { declare, format } from './format.js'
8
+
9
+ let root
10
+
11
+ before(async () => {
12
+ root = await mkdtemp(join(tmpdir(), 'format-'))
13
+ })
14
+
15
+ after(async () => {
16
+ await rm(root, { recursive: true, force: true })
17
+ })
18
+
19
+ describe('format', () => {
20
+ it('should read the nearest manifest above', async () => {
21
+ const workspace = join(root, 'modules')
22
+ const component = join(workspace, 'components', 'one')
23
+
24
+ await mkdir(component, { recursive: true })
25
+ await writeFile(join(workspace, 'package.json'), JSON.stringify({ type: 'module' }))
26
+
27
+ assert.strictEqual(format(component), 'module')
28
+ })
29
+
30
+ it('should default to commonjs', async () => {
31
+ const workspace = join(root, 'scripts')
32
+ const component = join(workspace, 'components', 'one')
33
+
34
+ await mkdir(component, { recursive: true })
35
+ await writeFile(join(workspace, 'package.json'), JSON.stringify({ name: 'scripts' }))
36
+
37
+ assert.strictEqual(format(component), 'commonjs')
38
+ })
39
+ })
40
+
41
+ describe('declare', () => {
42
+ it('should state the format the component was written under', async () => {
43
+ const workspace = join(root, 'stated')
44
+ const source = join(workspace, 'components', 'one')
45
+ const target = join(root, 'image', 'one')
46
+
47
+ await mkdir(source, { recursive: true })
48
+ await mkdir(target, { recursive: true })
49
+ await writeFile(join(workspace, 'package.json'), JSON.stringify({ type: 'module' }))
50
+
51
+ await declare(source, target, 'default-one')
52
+
53
+ const manifest = JSON.parse(await readFile(join(target, 'package.json'), 'utf8'))
54
+
55
+ assert.strictEqual(manifest.type, 'module')
56
+ assert.strictEqual(manifest.name, 'default-one')
57
+ assert.strictEqual(manifest.private, true)
58
+ })
59
+
60
+ it('should leave a manifest the component ships alone', async () => {
61
+ const source = join(root, 'own', 'components', 'one')
62
+ const target = join(root, 'image', 'own')
63
+
64
+ await mkdir(source, { recursive: true })
65
+ await mkdir(target, { recursive: true })
66
+
67
+ const own = JSON.stringify({ name: 'own', dependencies: { matchacho: '0.6.0' } })
68
+
69
+ await writeFile(join(target, 'package.json'), own)
70
+ await declare(source, target, 'default-own')
71
+
72
+ assert.strictEqual(await readFile(join(target, 'package.json'), 'utf8'), own)
73
+ })
74
+ })
@@ -1,7 +1,5 @@
1
- 'use strict'
2
-
3
- const { Image } = require('./image')
4
- const { generate } = require('randomstring')
1
+ import { Image } from './image.js'
2
+ import { generate } from 'randomstring'
5
3
 
6
4
  const version = '168b04ff'
7
5
  const name = generate()
@@ -10,11 +8,11 @@ const name = generate()
10
8
  * @implements {toa.deployment.images.Image}
11
9
  */
12
10
  class Class extends Image {
13
- get name () {
11
+ get name() {
14
12
  return name
15
13
  }
16
14
 
17
- get version () {
15
+ get version() {
18
16
  return version
19
17
  }
20
18
  }
@@ -29,10 +27,12 @@ const registry = {
29
27
  base: 'node:alpine'
30
28
  }
31
29
 
32
- exports.scope = generate()
33
- exports.name = name
34
- exports.version = 'ba2409fc'
35
- exports.Class = Class
36
- exports.runtime = runtime
37
- exports.registry = registry
38
- exports.process = process
30
+ export const scope = generate()
31
+
32
+ // distinct from the module's own `version` above
33
+ const published = 'ba2409fc'
34
+
35
+ // the fixture stands in for the global process
36
+ const current = process
37
+
38
+ export { name, Class, runtime, registry, current as process, published as version }