@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 +18 -0
- package/package.json +8 -7
- package/src/.component/.expand/index.js +0 -1
- package/src/.component/.normalize/events.js +3 -1
- package/src/.component/expand.js +0 -2
- package/src/.component/extensions.js +2 -5
- package/src/.component/merge.js +12 -11
- package/src/.component/schema.yaml +0 -3
- package/src/.component/validate.js +2 -1
- package/src/.context/.dependencies/extensions.js +18 -4
- package/src/.context/.dependencies/resolve.js +25 -5
- package/src/.context/dependencies.js +1 -1
- package/src/.context/normalize.js +3 -3
- package/src/definition.js +56 -0
- package/src/index.js +2 -0
- package/src/manifest.js +154 -0
- package/src/shortcuts.js +0 -1
- package/test/component/expand.test.js +0 -11
- package/test/context/normalize.test.js +2 -2
- package/test/definition.test.js +35 -0
- package/test/dummies/bare/index.js +1 -0
- package/test/dummies/defined/definition.js +1 -0
- package/test/dummies/defined/index.js +1 -0
- package/test/dummies/defined/package.json +1 -0
- package/test/manifest.test.js +126 -0
- package/types/component.d.ts +2 -1
- package/types/context.d.ts +4 -0
- package/types/index.d.ts +8 -0
- package/src/.component/.expand/properties.js +0 -10
- package/src/.context/.dependencies/load.js +0 -39
- package/test/context/dependencies.load.test.js +0 -25
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.
|
|
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.
|
|
25
|
-
"@toa.io/
|
|
26
|
-
"@toa.io/
|
|
27
|
-
"
|
|
28
|
-
"
|
|
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": "
|
|
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
|
|
19
|
+
const { properties } = (await definition(binding)).module
|
|
18
20
|
|
|
19
21
|
if (properties.async === true) return binding
|
|
20
22
|
}
|
package/src/.component/expand.js
CHANGED
|
@@ -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
|
-
|
|
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)
|
package/src/.component/merge.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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 {
|
|
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 {
|
|
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?.[
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
|
63
|
+
return (await definition(reference)).module
|
|
44
64
|
} catch {
|
|
45
65
|
return null
|
|
46
66
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import * as
|
|
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:
|
|
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 =
|
|
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'
|
package/src/manifest.js
ADDED
|
@@ -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
|
|
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,
|
|
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
|
+
})
|
package/types/component.d.ts
CHANGED
|
@@ -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
|
}
|
package/types/context.d.ts
CHANGED
|
@@ -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,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
|
-
})
|