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

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,16 @@
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.292](https://github.com/toa-io/toa/compare/v1.0.0-alpha.291...v1.0.0-alpha.292) (2026-09-07)
7
+
8
+ ### Features
9
+
10
+ * **cli:** the deployment library is a peer, and the binary is the CLI's ([a923f81](https://github.com/toa-io/toa/commit/a923f8173a55ed1c091213a7c7db1863ca1d0ba6))
11
+ * **definitions:** the version of Toa is the definitions package's ([3d0272e](https://github.com/toa-io/toa/commit/3d0272ea0ab862f4b6c2e58a2f003f92d0f8a87f))
12
+ * **definitions:** what a package declares is read from one package ([7052341](https://github.com/toa-io/toa/commit/70523411b5d9e2b204c999aa02365173f3bb518d))
13
+ * **norm:** a package's definition is read by one resolver ([bffd41c](https://github.com/toa-io/toa/commit/bffd41c8d18c42786b0bf321623088b9dabc4724))
14
+
15
+
6
16
  # [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
17
 
8
18
  **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.292",
4
4
  "type": "module",
5
5
  "description": "Toa declarations normalization and validation",
6
6
  "author": "temich <tema.gurtovoy@gmail.com>",
@@ -21,11 +21,13 @@
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",
24
+ "@toa.io/core": "1.0.0-alpha.292",
25
+ "@toa.io/definitions": "1.0.0-alpha.292",
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.292",
27
29
  "fast-glob": "3.3.3",
28
30
  "js-yaml": "5.4.1"
29
31
  },
30
- "gitHead": "bcd8d8d07bd3e64fc826af8719472404662cc37f"
32
+ "gitHead": "0f6aa3ad9dbec86bb632a7975ea61d1dde1d8fc5"
31
33
  }
@@ -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
  }
@@ -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 }
@@ -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
3
  export const resolve = async (references, annotations = {}) => {
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,
@@ -40,7 +39,7 @@ export const resolve = async (references, annotations = {}) => {
40
39
  */
41
40
  async function optional(reference) {
42
41
  try {
43
- return (await load(reference)).module
42
+ return (await definition(reference)).module
44
43
  } catch {
45
44
  return null
46
45
  }
@@ -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,5 @@
1
1
  export { context } from './context.js'
2
2
  export { component } from './component.js'
3
+ export { definition } from './definition.js'
3
4
  export * as shortcuts from './shortcuts.js'
4
5
  export * as entity from './entity.js'
@@ -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" }
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,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
- })