@toa.io/norm 1.0.0-alpha.291 → 1.0.0-alpha.293

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,24 @@
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.293](https://github.com/toa-io/toa/compare/v1.0.0-alpha.292...v1.0.0-alpha.293) (2026-09-07)
7
+
8
+ **Note:** Version bump only for package @toa.io/norm
9
+
10
+
11
+
12
+
13
+
14
+ # [1.0.0-alpha.292](https://github.com/toa-io/toa/compare/v1.0.0-alpha.291...v1.0.0-alpha.292) (2026-09-07)
15
+
16
+ ### Features
17
+
18
+ * **cli:** the deployment library is a peer, and the binary is the CLI's ([a923f81](https://github.com/toa-io/toa/commit/a923f8173a55ed1c091213a7c7db1863ca1d0ba6))
19
+ * **definitions:** the version of Toa is the definitions package's ([3d0272e](https://github.com/toa-io/toa/commit/3d0272ea0ab862f4b6c2e58a2f003f92d0f8a87f))
20
+ * **definitions:** what a package declares is read from one package ([7052341](https://github.com/toa-io/toa/commit/70523411b5d9e2b204c999aa02365173f3bb518d))
21
+ * **norm:** a package's definition is read by one resolver ([bffd41c](https://github.com/toa-io/toa/commit/bffd41c8d18c42786b0bf321623088b9dabc4724))
22
+
23
+
6
24
  # [1.0.0-alpha.291](https://github.com/toa-io/toa/compare/v1.0.0-alpha.290...v1.0.0-alpha.291) (2026-09-07)
7
25
 
8
26
  **Note:** Version bump only for package @toa.io/norm
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/norm",
3
- "version": "1.0.0-alpha.291",
3
+ "version": "1.0.0-alpha.293",
4
4
  "type": "module",
5
5
  "description": "Toa declarations normalization and validation",
6
6
  "author": "temich <tema.gurtovoy@gmail.com>",
@@ -21,11 +21,12 @@
21
21
  "test": "echo \"Error: run tests from root\" && exit 1"
22
22
  },
23
23
  "dependencies": {
24
- "@toa.io/core": "1.0.0-alpha.291",
25
- "@toa.io/generic": "1.0.0-alpha.291",
26
- "@toa.io/schemas": "1.0.0-alpha.291",
27
- "fast-glob": "3.3.3",
28
- "js-yaml": "5.4.1"
24
+ "@toa.io/core": "1.0.0-alpha.293",
25
+ "@toa.io/definitions": "1.0.0-alpha.293",
26
+ "@toa.io/generic": "1.0.0-alpha.292",
27
+ "@toa.io/prototype": "1.0.0-alpha.289",
28
+ "@toa.io/schemas": "1.0.0-alpha.293",
29
+ "fast-glob": "3.3.3"
29
30
  },
30
- "gitHead": "bcd8d8d07bd3e64fc826af8719472404662cc37f"
31
+ "gitHead": "1e05b3ab64758324c58c425f1f8e52e9f35f1da8"
31
32
  }
@@ -3,6 +3,5 @@ export { entity } from './entity.js'
3
3
  export { events } from './events.js'
4
4
  export { extensions } from './extensions.js'
5
5
  export { operations } from './operations.js'
6
- export { properties } from './properties.js'
7
6
  export { receivers } from './receivers.js'
8
7
  export { version } from './version.js'
@@ -1,3 +1,5 @@
1
+ import { definition } from '../../definition.js'
2
+
1
3
  export const events = async (component) => {
2
4
  if (component.events === undefined) return
3
5
 
@@ -14,7 +16,7 @@ export const events = async (component) => {
14
16
  /** The first binding that carries events, which only its module can say. */
15
17
  async function asynchronous(bindings) {
16
18
  for (const binding of bindings) {
17
- const { properties } = await import(binding)
19
+ const { properties } = (await definition(binding)).module
18
20
 
19
21
  if (properties.async === true) return binding
20
22
  }
@@ -5,7 +5,6 @@ import {
5
5
  events,
6
6
  receivers,
7
7
  extensions,
8
- properties,
9
8
  version
10
9
  } from './.expand/index.js'
11
10
 
@@ -15,7 +14,6 @@ export async function expand(manifest) {
15
14
  operations(manifest)
16
15
  events(manifest)
17
16
  receivers(manifest)
18
- properties(manifest)
19
17
  extensions(manifest)
20
18
 
21
19
  await version(manifest)
@@ -1,7 +1,6 @@
1
1
  import { find } from '@toa.io/generic'
2
2
  import { resolve } from '../shortcuts.js'
3
-
4
- const cache = {}
3
+ import { definition } from '../definition.js'
5
4
 
6
5
  export const extensions = async (manifest) => {
7
6
  manifest.extensions = Object.assign({}, PREDEFINED, manifest.extensions)
@@ -14,9 +13,7 @@ export const extensions = async (manifest) => {
14
13
  // relative path
15
14
  if (key[0] === '.') key = find(key, manifest.path)
16
15
 
17
- cache[key] ??= import(key)
18
-
19
- const extension = await cache[key]
16
+ const { module: extension } = await definition(key)
20
17
 
21
18
  if (extension.manifest !== undefined) {
22
19
  declaration = extension.manifest(declaration, manifest)
@@ -1,4 +1,5 @@
1
1
  import { merge } from '@toa.io/generic'
2
+ import { definition } from '../definition.js'
2
3
 
3
4
  const bridge = async (root, manifest) => {
4
5
  await Promise.all([
@@ -18,7 +19,7 @@ const define = async (root, manifest, property) => {
18
19
  if (item.bridge === undefined || item.bridge === manifest.bridge) continue // default bridge later
19
20
 
20
21
  const bridge = item.bridge || manifest.bridge
21
- const { define } = await req(bridge)
22
+ const { define } = await bridged(bridge)
22
23
  const definition = await define[singular](root, endpoint)
23
24
 
24
25
  merge(item, definition)
@@ -53,20 +54,20 @@ const define = async (root, manifest, property) => {
53
54
  }
54
55
  }
55
56
 
56
- const cache = {}
57
-
58
- // the promise is what is remembered, so a bridge is loaded once
59
- function req(mod) {
60
- cache[mod] ??= import(mod)
61
-
62
- return cache[mod]
63
- }
64
-
65
57
  const scan = async (bridge, root, property) => {
66
- const { define } = await req(bridge)
58
+ const { define } = await bridged(bridge)
67
59
 
68
60
  if (property in define) return define[property](root)
69
61
  else return undefined
70
62
  }
71
63
 
64
+ async function bridged(reference) {
65
+ const { module } = await definition(reference)
66
+
67
+ if (module.define === undefined)
68
+ throw new Error(`Bridge '${reference}' defines nothing: it exports no 'define'`)
69
+
70
+ return module
71
+ }
72
+
72
73
  export { bridge as merge }
@@ -303,7 +303,4 @@ properties:
303
303
  extensions:
304
304
  type: object
305
305
 
306
- properties:
307
- type: object
308
-
309
306
  additionalProperties: false
@@ -3,6 +3,7 @@ import path from 'node:path'
3
3
  import { readFileSync } from 'node:fs'
4
4
  import { yaml } from '@toa.io/generic'
5
5
  import * as schemas from '@toa.io/schemas'
6
+ import { definition } from '../definition.js'
6
7
 
7
8
  const object = yaml.load(
8
9
  readFileSync(path.resolve(import.meta.dirname, 'schema.yaml'), 'utf8')
@@ -34,7 +35,7 @@ const entity = (manifest) => {
34
35
 
35
36
  const events = async (manifest) => {
36
37
  for (const [label, event] of Object.entries(manifest.events)) {
37
- const { properties } = await import(event.binding)
38
+ const { properties } = (await definition(event.binding)).module
38
39
 
39
40
  if (properties.async !== true) {
40
41
  throw new Error(`Event '${label}' binding '${event.binding}' is not async`)
@@ -1,5 +1,6 @@
1
+ import { Locator } from '@toa.io/core'
1
2
  import { component as load } from '../../component.js'
2
- import { load as loadDependency } from './load.js'
3
+ import { definition } from '../../definition.js'
3
4
 
4
5
  export const extensions = async (context) => {
5
6
  const extensions = {}
@@ -84,15 +85,28 @@ async function extractExtensionComponents(components, extensions, annotations) {
84
85
  async function extract(reference, extensions, annotations) {
85
86
  extensions[reference] = []
86
87
 
87
- const { metadata, module: mod } = await loadDependency(reference)
88
+ const { name, module: mod } = await definition(reference)
88
89
 
89
90
  if (mod.components === undefined) return []
90
91
 
91
92
  // the annotation decides whether an extension contributes components at all
92
- const annotation = annotations?.[metadata?.name ?? reference]
93
+ const annotation = annotations?.[name]
94
+ const { manifests, paths } = mod.components(annotation)
95
+
96
+ // a definition carries the manifests, read where the extension is not installed;
97
+ // an entry names the directories, where it is
98
+ if (manifests !== undefined) return manifests.map(revive)
99
+
93
100
  const extracted = []
94
101
 
95
- for (const path of mod.components(annotation).paths) extracted.push(await load(path))
102
+ for (const path of paths) extracted.push(await load(path))
96
103
 
97
104
  return extracted
98
105
  }
106
+
107
+ /** A manifest as a digest carries it, which is one norm read, without the locator. */
108
+ function revive(manifest) {
109
+ manifest.locator = new Locator(manifest.name, manifest.namespace)
110
+
111
+ return manifest
112
+ }
@@ -1,11 +1,10 @@
1
- import { load } from './load.js'
1
+ import { definition } from '../../definition.js'
2
2
 
3
- export const resolve = async (references, annotations = {}) => {
3
+ export const resolve = async (references, annotations = {}, context = {}) => {
4
4
  const dependencies = {}
5
5
 
6
6
  for (const [dependency, components] of Object.entries(references)) {
7
- const { metadata, module } = await load(dependency)
8
- const id = metadata.name
7
+ const { name: id, module } = await definition(dependency)
9
8
 
10
9
  const instances = components.map((component) => ({
11
10
  locator: component.locator,
@@ -20,6 +19,27 @@ export const resolve = async (references, annotations = {}) => {
20
19
  if (annotation !== undefined && module.annotation !== undefined) {
21
20
  annotations[id] = module.annotation(annotation, instances)
22
21
  }
22
+
23
+ // what a declaration costs to install, read from the definition rather than from the
24
+ // package: a deploy installs it, and nothing that declares nothing carries it. A component
25
+ // is asked for its own instance; a composition that runs the package's service is asked
26
+ // with none, because what a service brings is not a component's to declare
27
+ if (module.installs !== undefined) {
28
+ for (const instance of instances)
29
+ Object.assign(
30
+ (instance.component.packages ??= {}),
31
+ module.installs(instance, annotations[id])
32
+ )
33
+
34
+ const service = module.installs(undefined, annotations[id])
35
+
36
+ // mono runs every service in one process, so it takes every service's packages
37
+ Object.assign((context.packages ??= {}), service)
38
+
39
+ for (const composition of context.compositions ?? [])
40
+ if (composition.services?.includes(dependency) === true)
41
+ Object.assign((composition.packages ??= {}), service)
42
+ }
23
43
  }
24
44
 
25
45
  for (const dependency of Object.keys(annotations)) {
@@ -40,7 +60,7 @@ export const resolve = async (references, annotations = {}) => {
40
60
  */
41
61
  async function optional(reference) {
42
62
  try {
43
- return (await load(reference)).module
63
+ return (await definition(reference)).module
44
64
  } catch {
45
65
  return null
46
66
  }
@@ -5,5 +5,5 @@ export const dependencies = async (context) => {
5
5
  const c = connectors(context, components)
6
6
  const references = { ...c, ...e }
7
7
 
8
- return resolve(references, context.annotations)
8
+ return resolve(references, context.annotations, context)
9
9
  }
@@ -1,14 +1,14 @@
1
- import * as runtime from '@toa.io/runtime'
1
+ import * as definitions from '@toa.io/definitions'
2
2
 
3
3
  /**
4
4
  * @param {toa.norm.context.Declaration | Object} context
5
5
  */
6
6
  export const normalize = (context) => {
7
- if (context.runtime === undefined) context.runtime = { version: runtime.version }
7
+ if (context.runtime === undefined) context.runtime = { version: definitions.version }
8
8
  if (typeof context.runtime === 'string') context.runtime = { version: context.runtime }
9
9
 
10
10
  if (context.runtime.version === undefined || context.runtime.version === '.') {
11
- context.runtime.version = runtime.version
11
+ context.runtime.version = definitions.version
12
12
  }
13
13
 
14
14
  if (typeof context.registry === 'string') context.registry = { base: context.registry }
@@ -0,0 +1,56 @@
1
+ import { createRequire } from 'node:module'
2
+ import { readFileSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+ import { pathToFileURL } from 'node:url'
5
+ import * as definitions from '@toa.io/definitions'
6
+
7
+ // import.meta.resolve takes no paths, and a dependency is named rather than
8
+ // written as a path
9
+ const require = createRequire(import.meta.url)
10
+
11
+ /**
12
+ * What a package declares, read without running it.
13
+ *
14
+ * A first-party package's definition is in `@toa.io/definitions`. Another package's is
15
+ * its `definition.js`, or its entry where it ships none — so an extension that exports
16
+ * its `manifest` and `deployment` beside its `Factory` is read as it always was.
17
+ *
18
+ * @param {string} reference a package name, or a directory
19
+ * @returns {Promise<toa.norm.Definition>}
20
+ */
21
+ export function definition(reference) {
22
+ cache[reference] ??= load(reference)
23
+
24
+ return cache[reference]
25
+ }
26
+
27
+ const cache = {}
28
+
29
+ async function load(reference) {
30
+ const known = await definitions.definition(reference)
31
+
32
+ if (known !== undefined) return { name: reference, module: known }
33
+
34
+ const name = metadata(reference)?.name ?? reference
35
+ const module = await import(pathToFileURL(resolve(reference)).href)
36
+
37
+ return { name, module }
38
+ }
39
+
40
+ function resolve(reference) {
41
+ try {
42
+ return require.resolve(join(reference, 'definition.js'))
43
+ } catch {
44
+ return require.resolve(reference)
45
+ }
46
+ }
47
+
48
+ function metadata(reference) {
49
+ try {
50
+ return JSON.parse(
51
+ readFileSync(require.resolve(join(reference, 'package.json')), 'utf8')
52
+ )
53
+ } catch {
54
+ return null
55
+ }
56
+ }
package/src/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  export { context } from './context.js'
2
2
  export { component } from './component.js'
3
+ export { plain, revive, NORMALIZED } from './manifest.js'
4
+ export { definition } from './definition.js'
3
5
  export * as shortcuts from './shortcuts.js'
4
6
  export * as entity from './entity.js'
@@ -0,0 +1,154 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { dirname, isAbsolute, join, parse, relative, sep } from 'node:path'
3
+
4
+ import { Locator } from '@toa.io/core'
5
+ import { find } from '@toa.io/generic'
6
+
7
+ /**
8
+ * A normalised manifest, as it is carried in a file beside the component it is of.
9
+ *
10
+ * A build normalises every component to tag its image; the process that runs it normalised them
11
+ * again at start, reading every operation module's source through its bridge to do it. What the
12
+ * build derived is written instead, and the two cannot disagree: the sources it was read from are
13
+ * what the image's tag digests, and the version of Toa that read them is in that tag as well.
14
+ *
15
+ * What cannot be carried is where things are. A workspace and an image put a component in one
16
+ * directory and Toa's own packages in another, so every path is written as what it is of rather
17
+ * than as where it was — the component itself, or the package that holds it — and is resolved
18
+ * again on the other side, the way norm resolved it.
19
+ *
20
+ * A key with no value is not carried: a file has no way to say `undefined`, and nothing reads
21
+ * one apart from a key that is not there.
22
+ */
23
+ export function plain(manifest) {
24
+ const { locator, packages, path, ...rest } = manifest
25
+
26
+ // what is carried is written, not the manifest the caller goes on using, and it is written
27
+ // as a file holds it: a key with no value does not survive and is not meant to
28
+ const declared = JSON.parse(JSON.stringify(rest))
29
+
30
+ walk(declared, (item) => {
31
+ item.path = carried(item.path, path)
32
+ })
33
+
34
+ local(declared, path)
35
+
36
+ return declared
37
+ }
38
+
39
+ /** What `plain` wrote, read back beside the component it is of. */
40
+ export function revive(declared, path) {
41
+ const manifest = { ...structuredClone(declared), path }
42
+
43
+ walk(manifest, (item) => {
44
+ item.path = resolved(item.path, path)
45
+ })
46
+
47
+ manifest.locator = new Locator(manifest.name, manifest.namespace)
48
+
49
+ return manifest
50
+ }
51
+
52
+ /**
53
+ * Everything in a manifest that says where a module of it is: what an inherited one declares is
54
+ * in the prototype it came from, and what the component declares is in the component.
55
+ */
56
+ function walk(manifest, rewrite) {
57
+ for (const property of ROOTED)
58
+ for (const item of Object.values(manifest[property] ?? {}))
59
+ if (item.path !== undefined) rewrite(item)
60
+
61
+ for (let prototype = manifest.prototype; prototype != null; prototype = prototype.prototype)
62
+ if (prototype.path !== undefined) rewrite(prototype)
63
+ }
64
+
65
+ /** Where it is written. A workspace has none: it is a build's output, read by a process. */
66
+ export const NORMALIZED = 'manifest.toa.json'
67
+
68
+ /** Where norm says a module is, beside what it says about it. */
69
+ const ROOTED = ['events', 'receivers', 'guards']
70
+
71
+ /** The component's own directory, which is where it is read back. */
72
+ const SELF = '.'
73
+
74
+ function carried(path, root) {
75
+ if (path === root) return SELF
76
+ if (path.startsWith(root + sep)) return SELF + '/' + relative(root, path).split(sep).join('/')
77
+
78
+ return specifier(path)
79
+ }
80
+
81
+ function resolved(path, root) {
82
+ if (path === SELF) return root
83
+ if (path.startsWith(SELF + '/')) return join(root, path.slice(2))
84
+
85
+ return find(path, root, MANIFEST)
86
+ }
87
+
88
+ /**
89
+ * A directory in a package, named the way a manifest would name it, so that it is found again
90
+ * wherever the package is installed. A prototype that is a directory of the application's own is
91
+ * refused here rather than in the container: an image carries components and nothing between
92
+ * them, so nothing would be there to find, whether it is normalised here or there.
93
+ */
94
+ function specifier(path) {
95
+ const root = above(path)
96
+
97
+ if (root !== undefined) {
98
+ const { name, private: hidden } = JSON.parse(
99
+ readFileSync(join(root, 'package.json'), 'utf8')
100
+ )
101
+
102
+ if (name !== undefined && hidden !== true)
103
+ return [name, ...relative(root, path).split(sep)].filter(Boolean).join('/')
104
+ }
105
+
106
+ throw new Error(
107
+ `'${path}' is in no package, so an image cannot carry what is in it: ` +
108
+ 'what a component inherits is reached by the reference of the package it is in.'
109
+ )
110
+ }
111
+
112
+ /** The directory of the package a path is in. */
113
+ function above(path) {
114
+ const { root } = parse(path)
115
+
116
+ let current = path
117
+
118
+ while (current !== root) {
119
+ if (existsSync(join(current, 'package.json'))) return current
120
+
121
+ current = dirname(current)
122
+ }
123
+
124
+ return undefined
125
+ }
126
+
127
+ /**
128
+ * Nothing that is carried may say where it was on the machine that read it. What is checked is
129
+ * what can be told apart from a value of the application's own — an HTTP route is `/accounts`
130
+ * and is a path to nobody — so: anything inside the component, and any directory a manifest
131
+ * declares a component or a prototype in. This is what catches a path norm starts writing
132
+ * somewhere this file does not know about, at the build rather than at the start of a container
133
+ * that cannot find it.
134
+ */
135
+ function local(value, root, at = '') {
136
+ if (typeof value === 'string') {
137
+ if (value === root || value.startsWith(root + sep) || component(value))
138
+ throw new Error(`'${at}' carries a path of the machine that read it: ${value}`)
139
+
140
+ return
141
+ }
142
+
143
+ if (value === null || typeof value !== 'object') return
144
+
145
+ for (const [key, item] of Object.entries(value))
146
+ local(item, root, at === '' ? key : `${at}.${key}`)
147
+ }
148
+
149
+ /** Whether a value is a directory a component or a prototype is declared in. */
150
+ function component(value) {
151
+ return isAbsolute(value) && existsSync(join(value, MANIFEST))
152
+ }
153
+
154
+ const MANIFEST = 'manifest.toa.yaml'
package/src/shortcuts.js CHANGED
@@ -41,7 +41,6 @@ export const SHORTCUTS = {
41
41
  node: '@toa.io/bridges.node',
42
42
  bash: '@toa.io/bridges.bash',
43
43
  mongodb: '@toa.io/storages.mongodb',
44
- queues: '@toa.io/storages.queues',
45
44
  exposition: '@toa.io/extensions.exposition',
46
45
  realtime: '@toa.io/extensions.realtime',
47
46
  configuration: '@toa.io/extensions.configuration',
@@ -30,14 +30,3 @@ it('should keep declared version', async () => {
30
30
 
31
31
  assert.deepStrictEqual(source.version, '1.0.0')
32
32
  })
33
-
34
- it('should recognize storages.queues', async () => {
35
- const queues = { foo: 'bar' }
36
-
37
- source.queues = clone(queues)
38
-
39
- await expand(source)
40
-
41
- assert.strictEqual(source.queues, undefined)
42
- assert.partialDeepStrictEqual(source.properties['@toa.io/storages.queues'], queues)
43
- })
@@ -1,4 +1,4 @@
1
- import * as runtime from '@toa.io/runtime'
1
+ import * as definitions from '@toa.io/definitions'
2
2
  import { it, beforeEach } from 'node:test'
3
3
  import assert from 'node:assert/strict'
4
4
 
@@ -20,7 +20,7 @@ it('should resolve local version', () => {
20
20
  normalize(context)
21
21
 
22
22
  assert.notDeepStrictEqual(context.runtime, '.')
23
- assert.deepStrictEqual(context.runtime.version, runtime.version)
23
+ assert.deepStrictEqual(context.runtime.version, definitions.version)
24
24
  })
25
25
 
26
26
  it('should expand registry', () => {
@@ -0,0 +1,35 @@
1
+ import { it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { join } from 'node:path'
4
+
5
+ import { definition } from '../src/definition.js'
6
+
7
+ it('reads a definition.js where a package ships one', async () => {
8
+ const { name, module } = await definition(join(import.meta.dirname, 'dummies/defined'))
9
+
10
+ assert.equal(name, 'dummies.defined')
11
+ assert.equal(module.declares, true)
12
+ assert.equal(module.runs, undefined)
13
+ })
14
+
15
+ it('reads the entry where a package ships no definition.js', async () => {
16
+ const path = join(import.meta.dirname, '../')
17
+ const { name, module } = await definition(path)
18
+
19
+ assert.equal(name, '@toa.io/norm')
20
+ assert.ok(module.context instanceof Function)
21
+ })
22
+
23
+ it('names a directory without package.json by its reference', async () => {
24
+ const path = join(import.meta.dirname, 'dummies/bare')
25
+ const { name, module } = await definition(path)
26
+
27
+ assert.equal(name, path)
28
+ assert.equal(module.declares, true)
29
+ })
30
+
31
+ it('loads a definition once', async () => {
32
+ const path = join(import.meta.dirname, 'dummies/bare')
33
+
34
+ assert.equal(await definition(path), await definition(path))
35
+ })
@@ -0,0 +1 @@
1
+ export const declares = true
@@ -0,0 +1 @@
1
+ export const declares = true
@@ -0,0 +1 @@
1
+ export const runs = true
@@ -0,0 +1 @@
1
+ { "name": "dummies.defined", "type": "module", "main": "index.js" }
@@ -0,0 +1,126 @@
1
+ import { it, describe } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { resolve } from 'node:path'
5
+
6
+ import { component } from '../src/component.js'
7
+ import { plain, revive } from '../src/manifest.js'
8
+
9
+ /** the prototype itself: an entity, operations and events, and no prototype of its own */
10
+ const PROTOTYPE = resolve(import.meta.dirname, '../../prototype')
11
+
12
+ /** a component on the default prototype, with an entity, events and receivers */
13
+ const COMPONENT = resolve(
14
+ import.meta.dirname,
15
+ '../../../features/steps/.workspace/components/collection/external.consumer'
16
+ )
17
+
18
+ /** a component whose prototype is a directory of the workspace's own */
19
+ const EXTENDED = resolve(
20
+ import.meta.dirname,
21
+ '../../../features/steps/.workspace/components/collection/configuration.extended'
22
+ )
23
+
24
+ const carry = (manifest) => JSON.parse(JSON.stringify(plain(manifest)))
25
+
26
+ describe('a manifest carried through a file', () => {
27
+ for (const [name, path] of [
28
+ ['the prototype', PROTOTYPE],
29
+ ['a component', COMPONENT]
30
+ ])
31
+ it(`comes back the same: ${name}`, async () => {
32
+ const manifest = await component(path)
33
+
34
+ assert.deepStrictEqual(revive(carry(manifest), path), manifest)
35
+ })
36
+
37
+ it('does not carry a key with no value', () => {
38
+ const carried = plain({ name: 'a', namespace: 'b', path: '/x', operations: { echo: { input: undefined } } })
39
+
40
+ assert.deepStrictEqual(carried.operations.echo, {})
41
+ })
42
+
43
+ it('leaves the manifest it was given alone', async () => {
44
+ const manifest = await component(COMPONENT)
45
+ const before = JSON.stringify(manifest)
46
+
47
+ plain(manifest)
48
+
49
+ assert.strictEqual(JSON.stringify(manifest), before)
50
+ })
51
+
52
+ it('is read beside the component wherever it is', async () => {
53
+ const carried = carry(await component(COMPONENT))
54
+ const revived = revive(carried, '/composition/external-consumer')
55
+
56
+ assert.strictEqual(revived.path, '/composition/external-consumer')
57
+
58
+ // what the component declares is in the component
59
+ assert.strictEqual(
60
+ revived.receivers['something_happened'].path,
61
+ '/composition/external-consumer'
62
+ )
63
+ })
64
+
65
+ it('reads what is inherited where the prototype is', async () => {
66
+ const prototype = (await component(COMPONENT)).prototype.path
67
+ const revived = revive(carry(await component(COMPONENT)), '/composition/external-consumer')
68
+
69
+ // an event of the prototype's is in the prototype, wherever the component is
70
+ assert.strictEqual(revived.events.created.path, prototype)
71
+ })
72
+
73
+ it('carries what is inherited by the package that holds it', async () => {
74
+ const carried = carry(await component(COMPONENT))
75
+
76
+ assert.strictEqual(carried.prototype.path, '@toa.io/prototype')
77
+ assert.strictEqual(carried.events.created.path, '@toa.io/prototype')
78
+ assert.strictEqual(carried.receivers['something_happened'].path, '.')
79
+ })
80
+ })
81
+
82
+ describe('what cannot be carried', () => {
83
+ it('leaves out where the manifest is and what only a deploy reads', async () => {
84
+ const manifest = await component(PROTOTYPE)
85
+
86
+ manifest.packages = { cloudinary: '2.11.0' }
87
+
88
+ const carried = plain(manifest)
89
+
90
+ assert.ok(!('locator' in carried))
91
+ assert.ok(!('path' in carried))
92
+ assert.ok(!('packages' in carried))
93
+ })
94
+
95
+ it('refuses a prototype an image would not have', async () => {
96
+ const manifest = await component(EXTENDED)
97
+
98
+ assert.throws(() => plain(manifest), /is in no package, so an image cannot carry what is in it/)
99
+ })
100
+
101
+ it('refuses a path inside the component it does not know about', () => {
102
+ assert.throws(
103
+ () => plain({ name: 'a', namespace: 'b', path: '/x', invented: { at: '/x/y' } }),
104
+ /carries a path of the machine that read it/
105
+ )
106
+ })
107
+
108
+ it('refuses a component directory it does not know about', async () => {
109
+ const manifest = await component(PROTOTYPE)
110
+
111
+ manifest.invented = { at: COMPONENT }
112
+
113
+ assert.throws(() => plain(manifest), /carries a path of the machine that read it/)
114
+ })
115
+
116
+ it('leaves a value of the application\'s own alone', () => {
117
+ const carried = plain({
118
+ name: 'a',
119
+ namespace: 'b',
120
+ path: '/x',
121
+ extensions: { exposition: { routes: [{ path: '/accounts' }] } }
122
+ })
123
+
124
+ assert.strictEqual(carried.extensions.exposition.routes[0].path, '/accounts')
125
+ })
126
+ })
@@ -65,10 +65,11 @@ type Declaration = {
65
65
  events?: Events
66
66
  receivers?: Record<string, Receiver>
67
67
  extensions?: Record<string, object>
68
- properties?: Record<string, object>
69
68
  }
70
69
 
71
70
  export type Manifest = Declaration & {
72
71
  locator: Locator
73
72
  path: string
73
+ /** what a deploy installs beside this component for what it declares, by package name */
74
+ packages?: Record<string, string>
74
75
  }
@@ -23,6 +23,8 @@ interface Composition {
23
23
  name: string
24
24
  components: Manifest[]
25
25
  services?: string[]
26
+ /** what a deploy installs for the services this composition runs, by package name */
27
+ packages?: Record<string, string>
26
28
  }
27
29
 
28
30
  export interface Dependency<T = undefined> {
@@ -38,6 +40,8 @@ interface Context extends Declaration {
38
40
  compositions?: Composition[]
39
41
  components?: Manifest[]
40
42
  dependencies?: Record<string, Dependency[]>
43
+ /** what a deploy installs for every service, which is what `mono` runs */
44
+ packages?: Record<string, string>
41
45
  }
42
46
 
43
47
  export function context(path: string, environment?: string): Promise<Context>
package/types/index.d.ts CHANGED
@@ -3,3 +3,11 @@ export * as component from './component.js'
3
3
 
4
4
  export { Manifest } from './component.js'
5
5
  export * as entity from './entity.js'
6
+
7
+ export interface Definition {
8
+ /** The package's name, or the reference where it has no package.json */
9
+ name: string
10
+ module: Record<string, any>
11
+ }
12
+
13
+ export function definition(reference: string): Promise<Definition>
@@ -1,10 +0,0 @@
1
- import { recognize } from '../../shortcuts.js'
2
-
3
- export function properties(manifest) {
4
- recognize(SHORTCUTS, manifest, 'properties')
5
- recognize(SHORTCUTS, manifest.properties)
6
- }
7
-
8
- const SHORTCUTS = {
9
- queues: '@toa.io/storages.queues'
10
- }
@@ -1,39 +0,0 @@
1
- import { createRequire } from 'node:module'
2
- import { readFileSync } from 'node:fs'
3
- import { join } from 'node:path'
4
- import { pathToFileURL } from 'node:url'
5
-
6
- // import.meta.resolve takes no paths, and a dependency is named rather than
7
- // written as a path
8
- const require = createRequire(import.meta.url)
9
-
10
- /**
11
- * @param {string} path
12
- * @returns {Promise<{ metadata: object, module: object }>}
13
- */
14
- export async function load(path) {
15
- const metadata = loadMetadata(path)
16
- const module = await loadModule(path)
17
-
18
- return { metadata, module }
19
- }
20
-
21
- function loadMetadata(reference) {
22
- try {
23
- return JSON.parse(
24
- readFileSync(require.resolve(join(reference, 'package.json')), 'utf8')
25
- )
26
- } catch {
27
- return null
28
- }
29
- }
30
-
31
- /**
32
- * @param {string} reference
33
- * @returns {Promise<object>}
34
- */
35
- async function loadModule(reference) {
36
- const path = require.resolve(reference)
37
-
38
- return await import(pathToFileURL(path).href)
39
- }
@@ -1,25 +0,0 @@
1
- import { it } from 'node:test'
2
- import assert from 'node:assert/strict'
3
-
4
- import { join } from 'node:path'
5
- import { load } from '../../src/.context/.dependencies/load.js'
6
-
7
- it('should be', async () => {
8
- assert.ok(load instanceof Function)
9
- })
10
-
11
- it('should load module', async () => {
12
- const path = join(import.meta.dirname, '../../')
13
- const { metadata, module } = await load(path)
14
-
15
- assert.deepStrictEqual(metadata.name, '@toa.io/norm')
16
- assert.ok(module.context instanceof Function)
17
- })
18
-
19
- it('should return null metadata if no package.json', async () => {
20
- const path = join(import.meta.dirname, '../../src')
21
- const { metadata, module } = await load(path)
22
-
23
- assert.strictEqual(metadata, null)
24
- assert.ok(module.context instanceof Function)
25
- })