@toa.io/operations 1.0.0-alpha.295 → 1.0.0-alpha.297

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,21 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [1.0.0-alpha.297](https://github.com/toa-io/toa/compare/v1.0.0-alpha.296...v1.0.0-alpha.297) (2026-09-08)
7
+
8
+ **Note:** Version bump only for package @toa.io/operations
9
+
10
+
11
+
12
+
13
+
14
+ # [1.0.0-alpha.296](https://github.com/toa-io/toa/compare/v1.0.0-alpha.295...v1.0.0-alpha.296) (2026-09-08)
15
+
16
+ ### Bug Fixes
17
+
18
+ * **exposition:** the gateway's image carries every storage provider ([ab19d25](https://github.com/toa-io/toa/commit/ab19d2567764d5a3148b067f186819d7e646908f))
19
+
20
+
6
21
  # [1.0.0-alpha.295](https://github.com/toa-io/toa/compare/v1.0.0-alpha.294...v1.0.0-alpha.295) (2026-09-07)
7
22
 
8
23
  **Note:** Version bump only for package @toa.io/operations
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/operations",
3
- "version": "1.0.0-alpha.295",
3
+ "version": "1.0.0-alpha.297",
4
4
  "type": "module",
5
5
  "description": "Toa Deployment",
6
6
  "homepage": "https://toa.io",
@@ -30,8 +30,8 @@
30
30
  "dependencies": {
31
31
  "@toa.io/generic": "1.0.0-alpha.292",
32
32
  "@toa.io/kubernetes": "1.0.0-alpha.293",
33
- "@toa.io/norm": "1.0.0-alpha.295",
33
+ "@toa.io/norm": "1.0.0-alpha.297",
34
34
  "paseto": "4.0.0"
35
35
  },
36
- "gitHead": "4053b28216cf83889258d3a891b9c7f43137234a"
36
+ "gitHead": "52959bba83817238725f50553d45afde1f231af1"
37
37
  }
@@ -19,5 +19,7 @@ RUN --mount=type=cache,target=/root/.npm,sharing=locked \
19
19
  # what the extensions need for what these components declare: installed beside the extension
20
20
  # that reads the declaration, in Toa's own install, because that is where Node resolves an
21
21
  # extension's imports from — and one copy for the composition rather than one per component
22
+ # `--legacy-peer-deps` for the reason the base image installs with it: what is added here
23
+ # re-resolves the tree, and a peer nothing imports comes back with it — 23 MB of `typescript`
22
24
  RUN --mount=type=cache,target=/root/.npm,sharing=locked \
23
- if [ -s .packages ]; then npm i --prefix /toa --omit=dev $(cat .packages); fi
25
+ if [ -s .packages ]; then npm i --prefix /toa --omit=dev --legacy-peer-deps $(cat .packages); fi
@@ -176,7 +176,9 @@ describe('prepare', () => {
176
176
 
177
177
  const dockerfile = await readFile(join(context, 'Dockerfile'), 'utf8')
178
178
 
179
- assert.ok(dockerfile.includes('npm i --prefix /toa --omit=dev $(cat .packages)'))
179
+ assert.ok(
180
+ dockerfile.includes('npm i --prefix /toa --omit=dev --legacy-peer-deps $(cat .packages)')
181
+ )
180
182
  })
181
183
 
182
184
  it('should hold what a service the composition runs brings', async () => {
@@ -111,3 +111,39 @@ describe('normalized', () => {
111
111
  assert.strictEqual(read.locator.id, 'todos.tasks')
112
112
  })
113
113
  })
114
+
115
+ describe('a service image', () => {
116
+ it('installs what its components declare beside Toa as well', async () => {
117
+ const { Service } = await import('./service.js')
118
+
119
+ const path = join(root, 'extension')
120
+ const component = join(path, 'components', 'octets')
121
+
122
+ await mkdir(component, { recursive: true })
123
+ await writeFile(join(path, 'package.json'), JSON.stringify({ name: 'ext', version: '1' }))
124
+ await writeFile(
125
+ join(component, 'package.json'),
126
+ JSON.stringify({ dependencies: { cloudinary: '2.11.0', jose: '6.2.10' } })
127
+ )
128
+
129
+ const service = new Service('', { version: '1' }, {}, path, {
130
+ group: 'g',
131
+ name: 'n',
132
+ version: '1'
133
+ })
134
+
135
+ service.reference = 'probe'
136
+
137
+ const context = await service.prepare(join(root, 'service'))
138
+
139
+ // what an extension imports on a component's behalf is imported from where Toa is
140
+ assert.strictEqual(
141
+ await readFile(join(context, '.packages'), 'utf8'),
142
+ 'cloudinary@2.11.0\njose@6.2.10'
143
+ )
144
+
145
+ const dockerfile = await readFile(join(context, 'Dockerfile'), 'utf8')
146
+
147
+ assert.ok(dockerfile.includes('npm i --prefix /toa --omit=dev --legacy-peer-deps $(cat .packages)'))
148
+ })
149
+ })
@@ -14,7 +14,12 @@ RUN --mount=type=cache,target=/root/.npm,sharing=locked \
14
14
  # component rather than beside the extension: an application that runs none of them, and the
15
15
  # runtime image every application is built on, carry nothing for them
16
16
  RUN --mount=type=cache,target=/root/.npm,sharing=locked \
17
- for entry in components/*; do if grep -qs '"dependencies"' "$entry/package.json"; then (cd $entry && npm i --omit=dev); fi; done
17
+ for entry in components/*; do if grep -qs '"dependencies"' "$entry/package.json"; then (cd $entry && npm i --omit=dev --legacy-peer-deps); fi; done
18
+
19
+ # and beside Toa as well, because what an extension imports on a component's behalf — a storage
20
+ # provider's SDK — is imported from where the extension is, which is `/toa` and not here
21
+ RUN --mount=type=cache,target=/root/.npm,sharing=locked \
22
+ if [ -s .packages ]; then npm i --prefix /toa --omit=dev --legacy-peer-deps $(cat .packages); fi
18
23
 
19
24
  # no USER: the runtime drops to `node` itself, and only a process that started as root can
20
25
  # close its environment under /proc — see runtime/runtime/bin/toa
@@ -1,8 +1,9 @@
1
1
  import { createRequire } from 'node:module'
2
2
  import { join, dirname } from 'node:path'
3
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
3
4
 
4
5
  import { Image } from './image.js'
5
- import { cp } from 'node:fs/promises'
6
+ import { cp, writeFile } from 'node:fs/promises'
6
7
 
7
8
  // a service is named the way a package is, and its directory is where it lives
8
9
  const require = createRequire(import.meta.url)
@@ -60,11 +61,46 @@ export class Service extends Image {
60
61
  const context = await super.prepare(root)
61
62
 
62
63
  await cp(this.#path, context, { recursive: true })
64
+ await writeFile(join(context, PACKAGES), this.#packages().join('\n'))
63
65
 
64
66
  return context
65
67
  }
68
+
69
+ /**
70
+ * What the components this service runs declare, as `name@version`.
71
+ *
72
+ * Each is installed beside its component, where the component's own modules import it, and
73
+ * beside Toa, where an extension that imports on a component's behalf is — a storage
74
+ * provider's SDK is imported by `@toa.io/extensions.storages`, not by the component that
75
+ * declares the storage.
76
+ *
77
+ * @returns {string[]}
78
+ */
79
+ #packages() {
80
+ const directory = join(this.#path, 'components')
81
+
82
+ if (!existsSync(directory)) return []
83
+
84
+ /** @type {Record<string, string>} */
85
+ const packages = {}
86
+
87
+ for (const label of readdirSync(directory)) {
88
+ const manifest = join(directory, label, 'package.json')
89
+
90
+ if (!existsSync(manifest)) continue
91
+
92
+ Object.assign(packages, JSON.parse(readFileSync(manifest, 'utf8')).dependencies)
93
+ }
94
+
95
+ return Object.entries(packages)
96
+ .map(([name, version]) => `${name}@${version}`)
97
+ .sort()
98
+ }
66
99
  }
67
100
 
101
+ /** Where the install reads what the components need, one `name@version` per line. */
102
+ const PACKAGES = '.packages'
103
+
68
104
  /**
69
105
  * Where the extension is installed, which is what its image is built from. A deploy install
70
106
  * carries what an extension declares and not the extension, so a service is built only where