@syncbridge/common 2.5.0 → 2.7.0
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/README.md +7 -0
- package/constants.d.ts +1 -1
- package/constants.js +1 -1
- package/host-link/host-values.d.ts +38 -0
- package/host-link/host-values.js +25 -0
- package/index.d.ts +2 -0
- package/index.js +2 -0
- package/package.json +1 -1
- package/utils/decode-profile.d.ts +7 -0
- package/utils/decode-profile.js +9 -0
- package/utils/registry-names.js +1 -0
- package/utils/value-refs.d.ts +46 -0
- package/utils/value-refs.js +166 -0
package/README.md
CHANGED
|
@@ -141,6 +141,13 @@ property's TypeScript type when not given. Some options:
|
|
|
141
141
|
| `hotPlug` | A change applies while running, without a restart |
|
|
142
142
|
| `index` | Its order in the UI |
|
|
143
143
|
|
|
144
|
+
A worker's configuration may name a value its server or its workspace
|
|
145
|
+
keeps, set once in SyncBridge Console, instead of holding it:
|
|
146
|
+
`${{ vars.HBYS_URL }}/api`. SyncBridge puts the value in before the app
|
|
147
|
+
starts and checks it as any value, so the app sees a plain value of the
|
|
148
|
+
variable's type. `parseValueRefs`, `resolveValueRefs` and
|
|
149
|
+
`profileValueRefs` read such references; `$${{` writes `${{` itself.
|
|
150
|
+
|
|
144
151
|
## Lifecycle
|
|
145
152
|
|
|
146
153
|
Apps and components are `Runnable`s. Override the protected methods, and
|
package/constants.d.ts
CHANGED
package/constants.js
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Where, under Console's API, a host asks for its values */
|
|
2
|
+
export declare const HOST_VALUES_PATH = "host/values";
|
|
3
|
+
/**
|
|
4
|
+
* Where, under the host's API, Console tells it that its values changed:
|
|
5
|
+
* POST, no body; the host then asks for them
|
|
6
|
+
*/
|
|
7
|
+
export declare const HOST_VALUES_CHANGED_PATH = "settings/values/changed";
|
|
8
|
+
/** The names a host's workers use, qualified ("vars.HBYS_URL") */
|
|
9
|
+
export interface HostValuesUses {
|
|
10
|
+
/** Every name its workers' profiles make, each once */
|
|
11
|
+
variables: string[];
|
|
12
|
+
/** Those of them neither the host nor its workspace defines */
|
|
13
|
+
missing: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface HostValuesRequest {
|
|
16
|
+
/** The revision of the values it has: none, it has none */
|
|
17
|
+
revision?: number;
|
|
18
|
+
/** What its workers use: kept by Console, replaced whole each time */
|
|
19
|
+
uses?: HostValuesUses;
|
|
20
|
+
}
|
|
21
|
+
/** A workspace's variables and a host's own, by name */
|
|
22
|
+
export interface HostVariables {
|
|
23
|
+
workspace: Record<string, string>;
|
|
24
|
+
host: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
export interface HostValuesResponse {
|
|
27
|
+
/**
|
|
28
|
+
* The workspace's revision: one counter, raised by any change of its
|
|
29
|
+
* values or of any of its hosts' values
|
|
30
|
+
*/
|
|
31
|
+
revision: number;
|
|
32
|
+
/** Whether the values changed since the revision the host told */
|
|
33
|
+
changed: boolean;
|
|
34
|
+
/** Changed: both trees, whole */
|
|
35
|
+
variables?: HostVariables;
|
|
36
|
+
}
|
|
37
|
+
/** A variable as a host takes it: its own definition, else the workspace's */
|
|
38
|
+
export declare function hostVariable(variables: HostVariables | undefined, name: string): string | undefined;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The values a host's workers name in their profiles (`${{ vars.NAME }}`,
|
|
3
|
+
* value-refs): kept by Console for a workspace and for each of its hosts,
|
|
4
|
+
* a host's own definition of a name over the workspace's. The host asks
|
|
5
|
+
* for them when its link connects and when Console tells it they changed
|
|
6
|
+
* (HOST_VALUES_CHANGED_PATH); and tells which names its workers use, which
|
|
7
|
+
* of them it lacks, whenever that changes. Asked by the host, with a token
|
|
8
|
+
* it signs (HOST_TOKEN_HEADER).
|
|
9
|
+
*/
|
|
10
|
+
/** Where, under Console's API, a host asks for its values */
|
|
11
|
+
export const HOST_VALUES_PATH = 'host/values';
|
|
12
|
+
/**
|
|
13
|
+
* Where, under the host's API, Console tells it that its values changed:
|
|
14
|
+
* POST, no body; the host then asks for them
|
|
15
|
+
*/
|
|
16
|
+
export const HOST_VALUES_CHANGED_PATH = 'settings/values/changed';
|
|
17
|
+
/** A variable as a host takes it: its own definition, else the workspace's */
|
|
18
|
+
export function hostVariable(variables, name) {
|
|
19
|
+
if (!variables)
|
|
20
|
+
return;
|
|
21
|
+
if (Object.prototype.hasOwnProperty.call(variables.host, name))
|
|
22
|
+
return variables.host[name];
|
|
23
|
+
if (Object.prototype.hasOwnProperty.call(variables.workspace, name))
|
|
24
|
+
return variables.workspace[name];
|
|
25
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export * from './host-link/host-identity.js';
|
|
|
17
17
|
export * from './host-link/host-link.js';
|
|
18
18
|
export * from './host-link/host-packages.js';
|
|
19
19
|
export * from './host-link/host-permissions.js';
|
|
20
|
+
export * from './host-link/host-values.js';
|
|
20
21
|
export * from './host-link/host-workers.js';
|
|
21
22
|
export * from './host-link/multiplexer.js';
|
|
22
23
|
export * from './host-link/sealed-box.js';
|
|
@@ -39,4 +40,5 @@ export * from './utils/registry-names.js';
|
|
|
39
40
|
export * from './utils/slot-interfaces.js';
|
|
40
41
|
export * from './utils/type-guards.js';
|
|
41
42
|
export * from './utils/uri-to-id.js';
|
|
43
|
+
export * from './utils/value-refs.js';
|
|
42
44
|
export * from './utils/worker-permissions.js';
|
package/index.js
CHANGED
|
@@ -17,6 +17,7 @@ export * from './host-link/host-identity.js';
|
|
|
17
17
|
export * from './host-link/host-link.js';
|
|
18
18
|
export * from './host-link/host-packages.js';
|
|
19
19
|
export * from './host-link/host-permissions.js';
|
|
20
|
+
export * from './host-link/host-values.js';
|
|
20
21
|
export * from './host-link/host-workers.js';
|
|
21
22
|
export * from './host-link/multiplexer.js';
|
|
22
23
|
export * from './host-link/sealed-box.js';
|
|
@@ -39,4 +40,5 @@ export * from './utils/registry-names.js';
|
|
|
39
40
|
export * from './utils/slot-interfaces.js';
|
|
40
41
|
export * from './utils/type-guards.js';
|
|
41
42
|
export * from './utils/uri-to-id.js';
|
|
43
|
+
export * from './utils/value-refs.js';
|
|
42
44
|
export * from './utils/worker-permissions.js';
|
package/package.json
CHANGED
|
@@ -3,6 +3,13 @@ import { AppMetadata, Profile, VariableMetadataRecord } from '../models/index.js
|
|
|
3
3
|
export interface CodecOptions {
|
|
4
4
|
setDefaultValues?: boolean;
|
|
5
5
|
cryptoSecretKey?: string;
|
|
6
|
+
/**
|
|
7
|
+
* The profile's references not resolved yet (`${{ vars.NAME }}`, see
|
|
8
|
+
* value-refs): a value holding one is kept as written, unchecked, of
|
|
9
|
+
* whatever type its variable is - checked once resolved. A profile
|
|
10
|
+
* edited or stored; never one a worker runs with.
|
|
11
|
+
*/
|
|
12
|
+
unresolved?: boolean;
|
|
6
13
|
}
|
|
7
14
|
/**
|
|
8
15
|
* Decodes profile
|
package/utils/decode-profile.js
CHANGED
|
@@ -5,6 +5,7 @@ import { AppMetadata, ElementComponentMetadata, Profile, ProfileComponent, Varia
|
|
|
5
5
|
import { getModelsDocument } from '../models-document.js';
|
|
6
6
|
import { secretDecryptor, secretEncryptor } from './encrypt-helpers.js';
|
|
7
7
|
import { materializeMetadataSilent } from './materialize-metadata.js';
|
|
8
|
+
import { hasValueRef } from './value-refs.js';
|
|
8
9
|
/**
|
|
9
10
|
* Decodes profile
|
|
10
11
|
*/
|
|
@@ -200,6 +201,14 @@ function _codecValues(stackExecutor, codec, variables, oldValues, options) {
|
|
|
200
201
|
stackExecutor.execute(k, () => {
|
|
201
202
|
if (defVariable == null)
|
|
202
203
|
return;
|
|
204
|
+
/* A reference not resolved yet: kept as written */
|
|
205
|
+
const given = oldVals?.[k];
|
|
206
|
+
if (options?.unresolved &&
|
|
207
|
+
(hasValueRef(given) ||
|
|
208
|
+
(Array.isArray(given) && given.some(hasValueRef)))) {
|
|
209
|
+
target[k] = given;
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
203
212
|
let valueFn = isAny;
|
|
204
213
|
/* If variable is ignored and no default value is set, field should be undefined */
|
|
205
214
|
if (defVariable.ignored && defVariable.default === undefined) {
|
package/utils/registry-names.js
CHANGED
|
@@ -20,6 +20,7 @@ export const SDK_PACKAGES = [
|
|
|
20
20
|
'@syncbridge/syncbuild',
|
|
21
21
|
'@syncbridge/testing',
|
|
22
22
|
'@syncbridge/plugin-api',
|
|
23
|
+
'@syncbridge/pnpm-config',
|
|
23
24
|
];
|
|
24
25
|
/** Whether a name is one of the SDK's packages, which come from npm */
|
|
25
26
|
export const isSdkPackage = (name) => SDK_PACKAGES.includes(name);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Profile } from '../models/index.js';
|
|
2
|
+
/** The name of a variable or a secret: UPPER_SNAKE, at most 64 characters */
|
|
3
|
+
export declare const VALUE_REF_NAME: RegExp;
|
|
4
|
+
/** What a reference names: a variable, or a secret */
|
|
5
|
+
export type ValueRefKind = 'vars' | 'secrets';
|
|
6
|
+
export interface ValueRef {
|
|
7
|
+
kind: ValueRefKind;
|
|
8
|
+
name: string;
|
|
9
|
+
}
|
|
10
|
+
/** A reference as told: "vars.NAME" */
|
|
11
|
+
export declare const qualifiedRef: (ref: ValueRef) => string;
|
|
12
|
+
/** A reference as written in a value: "${{ vars.NAME }}" */
|
|
13
|
+
export declare const formatValueRef: (kind: ValueRefKind, name: string) => string;
|
|
14
|
+
/**
|
|
15
|
+
* Whether a value holds a reference - or writes `${{` itself - so that it
|
|
16
|
+
* is not its final value until resolved
|
|
17
|
+
*/
|
|
18
|
+
export declare function hasValueRef(value: unknown): value is string;
|
|
19
|
+
/** The references of a text, in order, and what is wrong in it */
|
|
20
|
+
export declare function parseValueRefs(text: string): {
|
|
21
|
+
refs: ValueRef[];
|
|
22
|
+
errors: string[];
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* A text with its references replaced by what `lookup` answers - `$${{`
|
|
26
|
+
* by `${{` - and the references it does not answer, qualified
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveValueText(text: string, lookup: (kind: ValueRefKind, name: string) => string | undefined): {
|
|
29
|
+
value: string;
|
|
30
|
+
missing: string[];
|
|
31
|
+
errors: string[];
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* A profile with the references of its values resolved - the app's and
|
|
35
|
+
* each component's, nothing else of it - by `lookup`; the references it
|
|
36
|
+
* does not answer, qualified ("vars.HBYS_URL"), and the expressions that
|
|
37
|
+
* are not references, each with where it is. The profile given is left as
|
|
38
|
+
* it is.
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveValueRefs<T extends Profile>(profile: T, lookup: (kind: ValueRefKind, name: string) => string | undefined): {
|
|
41
|
+
profile: T;
|
|
42
|
+
missing: string[];
|
|
43
|
+
errors: string[];
|
|
44
|
+
};
|
|
45
|
+
/** The references a profile's values make, qualified, each once, sorted */
|
|
46
|
+
export declare function profileValueRefs(profile: Profile): string[];
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* References in a worker's values: `${{ vars.NAME }}` names a variable its
|
|
3
|
+
* host or its workspace keeps, anywhere in a string value and more than
|
|
4
|
+
* once; `${{ secrets.NAME }}` a secret. `$${{` writes `${{` itself. A
|
|
5
|
+
* profile keeps the references as written - stored, synced to Console,
|
|
6
|
+
* restored - and the host resolves them before the profile is decoded, so
|
|
7
|
+
* a resolved value is checked as any value is.
|
|
8
|
+
*/
|
|
9
|
+
/** The name of a variable or a secret: UPPER_SNAKE, at most 64 characters */
|
|
10
|
+
export const VALUE_REF_NAME = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
11
|
+
/** A reference as told: "vars.NAME" */
|
|
12
|
+
export const qualifiedRef = (ref) => `${ref.kind}.${ref.name}`;
|
|
13
|
+
/** A reference as written in a value: "${{ vars.NAME }}" */
|
|
14
|
+
export const formatValueRef = (kind, name) => '${{ ' + kind + '.' + name + ' }}';
|
|
15
|
+
const OPEN = '${{';
|
|
16
|
+
const CLOSE = '}}';
|
|
17
|
+
/**
|
|
18
|
+
* Whether a value holds a reference - or writes `${{` itself - so that it
|
|
19
|
+
* is not its final value until resolved
|
|
20
|
+
*/
|
|
21
|
+
export function hasValueRef(value) {
|
|
22
|
+
return typeof value === 'string' && value.includes(OPEN);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Goes through a text: its pieces of plain text, and its references. An
|
|
26
|
+
* expression that is not one - another kind than vars or secrets, a name
|
|
27
|
+
* not UPPER_SNAKE, no closing braces - is an error, not text: a slip is
|
|
28
|
+
* not kept as written.
|
|
29
|
+
*/
|
|
30
|
+
function scan(text, on) {
|
|
31
|
+
const errors = [];
|
|
32
|
+
let at = 0;
|
|
33
|
+
for (;;) {
|
|
34
|
+
const open = text.indexOf(OPEN, at);
|
|
35
|
+
if (open < 0)
|
|
36
|
+
break;
|
|
37
|
+
/* `$${{`: the text `${{` */
|
|
38
|
+
if (open > 0 && text[open - 1] === '$') {
|
|
39
|
+
on.text(text.slice(at, open - 1) + OPEN);
|
|
40
|
+
at = open + OPEN.length;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
on.text(text.slice(at, open));
|
|
44
|
+
const close = text.indexOf(CLOSE, open + OPEN.length);
|
|
45
|
+
if (close < 0) {
|
|
46
|
+
errors.push(`"${text.slice(open)}" is not closed with "${CLOSE}"`);
|
|
47
|
+
on.text(text.slice(open));
|
|
48
|
+
return errors;
|
|
49
|
+
}
|
|
50
|
+
const inner = text.slice(open + OPEN.length, close).trim();
|
|
51
|
+
const m = /^(vars|secrets)\.(\S+)$/.exec(inner);
|
|
52
|
+
if (m && VALUE_REF_NAME.test(m[2]))
|
|
53
|
+
on.ref({ kind: m[1], name: m[2] });
|
|
54
|
+
else {
|
|
55
|
+
errors.push(`"${OPEN} ${inner} ${CLOSE}" is not a reference: write ` +
|
|
56
|
+
`${formatValueRef('vars', 'NAME')} or ${formatValueRef('secrets', 'NAME')}, ` +
|
|
57
|
+
`NAME in capitals, digits and "_"; "$${OPEN}" for the text "${OPEN}"`);
|
|
58
|
+
on.text(text.slice(open, close + CLOSE.length));
|
|
59
|
+
}
|
|
60
|
+
at = close + CLOSE.length;
|
|
61
|
+
}
|
|
62
|
+
on.text(text.slice(at));
|
|
63
|
+
return errors;
|
|
64
|
+
}
|
|
65
|
+
/** The references of a text, in order, and what is wrong in it */
|
|
66
|
+
export function parseValueRefs(text) {
|
|
67
|
+
const refs = [];
|
|
68
|
+
const errors = scan(text, { text: () => undefined, ref: r => refs.push(r) });
|
|
69
|
+
return { refs, errors };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A text with its references replaced by what `lookup` answers - `$${{`
|
|
73
|
+
* by `${{` - and the references it does not answer, qualified
|
|
74
|
+
*/
|
|
75
|
+
export function resolveValueText(text, lookup) {
|
|
76
|
+
let value = '';
|
|
77
|
+
const missing = [];
|
|
78
|
+
const errors = scan(text, {
|
|
79
|
+
text: piece => (value += piece),
|
|
80
|
+
ref: ref => {
|
|
81
|
+
const found = lookup(ref.kind, ref.name);
|
|
82
|
+
if (found === undefined)
|
|
83
|
+
missing.push(qualifiedRef(ref));
|
|
84
|
+
else
|
|
85
|
+
value += found;
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
return { value, missing, errors };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Each value tree of a profile - the app's, each component's down the
|
|
92
|
+
* components - with where it is: "values", "components/server/values"
|
|
93
|
+
*/
|
|
94
|
+
function eachValues(profile, fn) {
|
|
95
|
+
if (profile.values)
|
|
96
|
+
fn(profile.values, 'values');
|
|
97
|
+
const walk = (components, at) => {
|
|
98
|
+
for (const [key, component] of Object.entries(components || {})) {
|
|
99
|
+
if (!component)
|
|
100
|
+
continue;
|
|
101
|
+
const here = `${at}/${key}`;
|
|
102
|
+
if (component.values)
|
|
103
|
+
fn(component.values, `${here}/values`);
|
|
104
|
+
walk(component.components, `${here}/components`);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
walk(profile.components, 'components');
|
|
108
|
+
}
|
|
109
|
+
/** Each string of a value tree, down its objects and arrays: replaced by `fn` */
|
|
110
|
+
function mapStrings(tree, location, fn) {
|
|
111
|
+
if (typeof tree === 'string')
|
|
112
|
+
return fn(tree, location);
|
|
113
|
+
if (Array.isArray(tree))
|
|
114
|
+
return tree.map((v, i) => mapStrings(v, `${location}/${i}`, fn));
|
|
115
|
+
if (tree && typeof tree === 'object') {
|
|
116
|
+
const out = {};
|
|
117
|
+
for (const [k, v] of Object.entries(tree))
|
|
118
|
+
out[k] = mapStrings(v, `${location}/${k}`, fn);
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
return tree;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* A profile with the references of its values resolved - the app's and
|
|
125
|
+
* each component's, nothing else of it - by `lookup`; the references it
|
|
126
|
+
* does not answer, qualified ("vars.HBYS_URL"), and the expressions that
|
|
127
|
+
* are not references, each with where it is. The profile given is left as
|
|
128
|
+
* it is.
|
|
129
|
+
*/
|
|
130
|
+
export function resolveValueRefs(profile, lookup) {
|
|
131
|
+
const out = structuredClone(profile);
|
|
132
|
+
const missing = new Set();
|
|
133
|
+
const errors = [];
|
|
134
|
+
const resolve = (values, location) => mapStrings(values, location, (text, at) => {
|
|
135
|
+
if (!hasValueRef(text))
|
|
136
|
+
return text;
|
|
137
|
+
const r = resolveValueText(text, lookup);
|
|
138
|
+
r.missing.forEach(m => missing.add(m));
|
|
139
|
+
errors.push(...r.errors.map(e => `${at}: ${e}`));
|
|
140
|
+
return r.value;
|
|
141
|
+
});
|
|
142
|
+
if (out.values)
|
|
143
|
+
out.values = resolve(out.values, 'values');
|
|
144
|
+
const walk = (components, at) => {
|
|
145
|
+
for (const [key, component] of Object.entries(components || {})) {
|
|
146
|
+
if (!component)
|
|
147
|
+
continue;
|
|
148
|
+
const here = `${at}/${key}`;
|
|
149
|
+
if (component.values)
|
|
150
|
+
component.values = resolve(component.values, `${here}/values`);
|
|
151
|
+
walk(component.components, `${here}/components`);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
walk(out.components, 'components');
|
|
155
|
+
return { profile: out, missing: [...missing], errors };
|
|
156
|
+
}
|
|
157
|
+
/** The references a profile's values make, qualified, each once, sorted */
|
|
158
|
+
export function profileValueRefs(profile) {
|
|
159
|
+
const names = new Set();
|
|
160
|
+
eachValues(profile, values => mapStrings(values, '', text => {
|
|
161
|
+
if (hasValueRef(text))
|
|
162
|
+
parseValueRefs(text).refs.forEach(r => names.add(qualifiedRef(r)));
|
|
163
|
+
return text;
|
|
164
|
+
}));
|
|
165
|
+
return [...names].sort();
|
|
166
|
+
}
|