@toa.io/norm 1.0.0-alpha.307 → 1.0.0-alpha.308

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.308](https://github.com/toa-io/toa/compare/v1.0.0-alpha.307...v1.0.0-alpha.308) (2026-09-15)
7
+
8
+ **Note:** Version bump only for package @toa.io/norm
9
+
10
+
11
+
12
+
13
+
6
14
  # [1.0.0-alpha.307](https://github.com/toa-io/toa/compare/v1.0.0-alpha.306...v1.0.0-alpha.307) (2026-09-15)
7
15
 
8
16
  **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.307",
3
+ "version": "1.0.0-alpha.308",
4
4
  "type": "module",
5
5
  "description": "Toa declarations normalization and validation",
6
6
  "author": "temich <tema.gurtovoy@gmail.com>",
@@ -21,12 +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.306",
25
- "@toa.io/definitions": "1.0.0-alpha.307",
24
+ "@toa.io/core": "1.0.0-alpha.308",
25
+ "@toa.io/definitions": "1.0.0-alpha.308",
26
26
  "@toa.io/generic": "1.0.0-alpha.304",
27
27
  "@toa.io/prototype": "1.0.0-alpha.301",
28
28
  "@toa.io/schemas": "1.0.0-alpha.304",
29
29
  "fast-glob": "3.3.3"
30
30
  },
31
- "gitHead": "d5f4b2e97e315ba8ab657039d0407565c96e34bc"
31
+ "gitHead": "66eb0d54032f60e6ead9e13fd9e21fb60c611277"
32
32
  }
@@ -1,22 +1,27 @@
1
1
  import { join } from 'node:path'
2
2
  import { createHash } from 'node:crypto'
3
3
  import fs from 'node:fs/promises'
4
+ import glob from 'fast-glob'
5
+
6
+ import { definition } from '../../definition.js'
4
7
 
5
8
  export async function version(manifest) {
6
- manifest.version ??= await hash(manifest.path)
9
+ manifest.version ??= await hash(manifest)
7
10
  }
8
11
 
9
12
  /**
10
13
  * Identifies a build of a component: it becomes the tag of its image, so it must be the
11
- * same for the same sources and different for different ones — on any machine.
14
+ * same for the same sources and different for different ones — on any machine, and whichever
15
+ * build produced them.
12
16
  *
13
- * What the image excludes cannot change it: `node_modules` is in the `.dockerignore` the
14
- * build writes (see `operations/src/deployment/images/image.js`), so hashing it would only
15
- * retag an identical image after a local install.
17
+ * What a build leaves out cannot change it: the bridge states what no build ships, a component
18
+ * adds to that with `ignore` and puts one back with `!`, and `files` names what its version is
19
+ * made of where a build copies only some of it. Its manifest is always one of them, because
20
+ * what it declares is what a caller is held to.
16
21
  */
17
- async function hash(path) {
18
- const files = (await list(path)).sort()
19
- const digests = await Promise.all(files.map((file) => digest(join(path, file))))
22
+ async function hash(manifest) {
23
+ const files = (await list(manifest)).sort()
24
+ const digests = await Promise.all(files.map((file) => digest(join(manifest.path, file))))
20
25
  const total = createHash('sha256')
21
26
 
22
27
  // the path is part of it: moving a file changes the build even if no content did
@@ -26,24 +31,48 @@ async function hash(path) {
26
31
  }
27
32
 
28
33
  /**
29
- * @param {string} root
30
- * @param {string} [path] relative
31
- * @param {string[]} [acc]
32
- * @returns {Promise<string[]>} paths relative to `root`
34
+ * @param {toa.norm.Component} manifest
35
+ * @returns {Promise<string[]>} paths relative to the component
33
36
  */
34
- async function list(root, path = '', acc = []) {
35
- const entries = await fs.readdir(join(root, path), { withFileTypes: true })
37
+ async function list(manifest) {
38
+ const files = await glob(patterns(manifest.files), {
39
+ cwd: manifest.path,
40
+ dot: true,
41
+ onlyFiles: true,
42
+ followSymbolicLinks: false,
43
+ ignore: await ignore(manifest)
44
+ })
45
+
46
+ // what a component declares is what a caller is held to, so its manifest is always one of them
47
+ if (!files.includes(MANIFEST) && (await exists(join(manifest.path, MANIFEST))))
48
+ files.push(MANIFEST)
49
+
50
+ return files
51
+ }
36
52
 
37
- for (const entry of entries) {
38
- if (EXCLUDED.has(entry.name)) continue
53
+ /** A path names what is under it, and a pattern is taken as it is. */
54
+ function patterns(files = ['**']) {
55
+ return files.flatMap((file) => (file.includes('*') ? [file] : [file, file + '/**']))
56
+ }
39
57
 
40
- const relative = path === '' ? entry.name : `${path}/${entry.name}`
58
+ async function ignore(manifest) {
59
+ const { module } = await definition(manifest.bridge)
60
+ const stated = manifest.ignore ?? []
61
+ const back = new Set(
62
+ stated.filter((pattern) => pattern[0] === '!').map((pattern) => pattern.slice(1))
63
+ )
41
64
 
42
- if (entry.isDirectory()) await list(root, relative, acc)
43
- else if (entry.isFile()) acc.push(relative)
44
- }
65
+ return [
66
+ ...(module.ignore ?? []).filter((pattern) => !back.has(pattern)),
67
+ ...stated.filter((pattern) => pattern[0] !== '!')
68
+ ]
69
+ }
45
70
 
46
- return acc
71
+ async function exists(path) {
72
+ return await fs
73
+ .access(path)
74
+ .then(() => true)
75
+ .catch(() => false)
47
76
  }
48
77
 
49
78
  async function digest(path) {
@@ -52,4 +81,4 @@ async function digest(path) {
52
81
  .digest()
53
82
  }
54
83
 
55
- const EXCLUDED = new Set(['node_modules', '.git'])
84
+ const MANIFEST = 'manifest.toa.yaml'
@@ -34,6 +34,18 @@ properties:
34
34
  path:
35
35
  type: string
36
36
 
37
+ files:
38
+ # what this component's version is made of, where a build of it copies only some of it
39
+ type: array
40
+ items:
41
+ type: string
42
+
43
+ ignore:
44
+ # what its version is not made of, added to what the bridge states; `!` puts one of those back
45
+ type: array
46
+ items:
47
+ type: string
48
+
37
49
  locator:
38
50
  type: object
39
51
  properties:
package/src/map.js CHANGED
@@ -1,21 +1,24 @@
1
+ import { contract } from '@toa.io/core'
2
+
1
3
  /**
2
- * Every component a Context has, with the version it runs: its own, the ones its extensions
3
- * bring, and the ones it evicts. A map is not rendered into a workload the way variables are, and
4
- * an evicted component is called like any other, so it is looked up like any other.
4
+ * Every component a Context has, with the version it runs and the contract of that version: its
5
+ * own, the ones its extensions bring, and the ones it evicts. A map is not rendered into a
6
+ * workload the way variables are, and an evicted component is called like any other, so it is
7
+ * described like any other.
5
8
  *
6
9
  * @param {toa.norm.Context} context
7
- * @returns {Record<string, string>}
10
+ * @returns {Record<string, import('@toa.io/core').Contract>}
8
11
  */
9
12
  export const map = (context) => {
10
- const versions = new Map()
13
+ const components = new Map()
11
14
 
12
15
  for (const component of context.components ?? [])
13
- versions.set(component.locator.id, component.version)
16
+ components.set(component.locator.id, contract.component(component))
14
17
 
15
18
  // what an extension brings reaches a context as an instance of what it depends on
16
19
  for (const instances of Object.values(context.dependencies ?? {}))
17
20
  for (const { component } of instances)
18
- versions.set(component.locator.id, component.version)
21
+ components.set(component.locator.id, contract.component(component))
19
22
 
20
- return Object.fromEntries([...versions].sort(([one], [other]) => one.localeCompare(other)))
23
+ return Object.fromEntries([...components].sort(([one], [other]) => one.localeCompare(other)))
21
24
  }
@@ -58,6 +58,10 @@ export type Entity = {
58
58
 
59
59
  type Declaration = {
60
60
  prototype?: string
61
+ /** what this component's version is made of; everything under it by default */
62
+ files?: string[]
63
+ /** what its version is not made of, beside what the bridge states */
64
+ ignore?: string[]
61
65
  namespace: string
62
66
  name: string
63
67
  version: string
@@ -34,7 +34,7 @@ export interface Dependency<T = undefined> {
34
34
  component: Manifest
35
35
  }
36
36
 
37
- interface Context extends Declaration {
37
+ export interface Context extends Declaration {
38
38
  runtime?: Runtime
39
39
  environment?: string
40
40
  registry?: Registry
package/types/index.d.ts CHANGED
@@ -1,9 +1,15 @@
1
+ import type { Contract } from '@toa.io/core'
2
+ import type { Context } from './context.d.ts'
3
+
1
4
  export * as context from './context.d.ts'
2
5
  export * as component from './component.d.ts'
3
6
 
4
7
  export { Manifest } from './component.d.ts'
5
8
  export * as entity from './entity.d.ts'
6
9
 
10
+ /** Every component a Context has, by id, with the contract of the version it runs. */
11
+ export function map(context: Context): Record<string, Contract>
12
+
7
13
  export interface Definition {
8
14
  /** The package's name, or the reference where it has no package.json */
9
15
  name: string