@toa.io/operations 1.0.0-alpha.296 → 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,14 @@
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
+
6
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)
7
15
 
8
16
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/operations",
3
- "version": "1.0.0-alpha.296",
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.296",
33
+ "@toa.io/norm": "1.0.0-alpha.297",
34
34
  "paseto": "4.0.0"
35
35
  },
36
- "gitHead": "d0fd4f9d13483f763acd9dd2b3c3eb8cb8856524"
36
+ "gitHead": "52959bba83817238725f50553d45afde1f231af1"
37
37
  }
@@ -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