@olivervea/pipelines 2.0.0-beta.2 → 2.0.0-beta.20
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 +125 -62
- package/manifest.schema.json +208 -11
- package/package.json +6 -4
- package/src/assets.d.ts +2 -2
- package/src/build/build.ts +121 -51
- package/src/build/cli.ts +29 -22
- package/src/build/elf.ts +8 -7
- package/src/build/hashes.ts +82 -20
- package/src/build/project.ts +95 -28
- package/src/builder/pipeline.ts +344 -88
- package/src/builder/refs.ts +15 -5
- package/src/core/brands.ts +7 -5
- package/src/core/config-refs.ts +103 -0
- package/src/core/config.ts +224 -0
- package/src/core/external.ts +86 -0
- package/src/core/handlers.ts +61 -15
- package/src/core/models.ts +32 -15
- package/src/core/origin.ts +23 -0
- package/src/core/secrets.ts +2 -2
- package/src/core/steps.ts +89 -38
- package/src/core/types.ts +5 -1
- package/src/core/webhooks.ts +21 -7
- package/src/ctx/files.ts +111 -40
- package/src/ctx/index.ts +84 -31
- package/src/ctx/sh.ts +105 -44
- package/src/ctx/types.ts +12 -4
- package/src/describe/describe.ts +701 -133
- package/src/describe/manifest-schema.ts +258 -61
- package/src/describe/mapper-source.ts +249 -104
- package/src/describe/record.ts +67 -37
- package/src/dev/cli.ts +72 -35
- package/src/harness/env.ts +45 -25
- package/src/harness/execute.ts +58 -29
- package/src/harness/index.ts +22 -9
- package/src/harness/main.ts +239 -78
- package/src/harness/result.ts +38 -22
- package/src/harness/validate.ts +77 -15
- package/src/index.ts +124 -11
- package/src/library/checks.ts +28 -0
- package/src/library/compose-deploy.ts +113 -0
- package/src/library/github-source.ts +100 -47
- package/src/library/helm-deploy-ssh.ts +452 -108
- package/src/library/helm-deploy.ts +449 -0
- package/src/library/http-health.ts +7 -5
- package/src/library/image-build.ts +499 -105
- package/src/library/index.ts +48 -12
- package/src/library/models.ts +33 -9
- package/src/library/ssh.ts +67 -0
- package/src/testing/index.ts +74 -36
- package/src/util/canonical.ts +7 -4
- package/src/util/handler-hash.ts +17 -7
- package/src/util/hash.ts +5 -5
- package/src/util/tar.ts +86 -36
- package/src/library/script.ts +0 -85
|
@@ -12,34 +12,103 @@
|
|
|
12
12
|
// function (from) { return { … }; }
|
|
13
13
|
// Free identifiers (`RELEASE`, `CONFIG.url`) are allowed: they are module constants, recorded as
|
|
14
14
|
// `{const}` by the proxy. This requires the harness bundle not to be minified.
|
|
15
|
-
import { MapperError } from
|
|
15
|
+
import { MapperError } from './record.ts';
|
|
16
16
|
|
|
17
17
|
type Tok =
|
|
18
|
-
| { t:
|
|
19
|
-
| { t:
|
|
20
|
-
| { t:
|
|
21
|
-
| { t:
|
|
22
|
-
| { t:
|
|
23
|
-
| { t:
|
|
18
|
+
| { t: 'ident'; v: string; pos: number }
|
|
19
|
+
| { t: 'str'; v: string; pos: number }
|
|
20
|
+
| { t: 'num'; v: string; pos: number }
|
|
21
|
+
| { t: 'punc'; v: string; pos: number }
|
|
22
|
+
| { t: 'op'; v: string; pos: number }
|
|
23
|
+
| { t: 'eof'; v: ''; pos: number };
|
|
24
24
|
|
|
25
|
-
const PUNCS = [
|
|
26
|
-
const OPS = [
|
|
25
|
+
const PUNCS = ['...', '=>', '?.', '(', ')', '{', '}', '[', ']', ',', ':', '.', ';'];
|
|
26
|
+
const OPS = [
|
|
27
|
+
'===',
|
|
28
|
+
'!==',
|
|
29
|
+
'**=',
|
|
30
|
+
'??=',
|
|
31
|
+
'&&=',
|
|
32
|
+
'||=',
|
|
33
|
+
'==',
|
|
34
|
+
'!=',
|
|
35
|
+
'<=',
|
|
36
|
+
'>=',
|
|
37
|
+
'&&',
|
|
38
|
+
'||',
|
|
39
|
+
'??',
|
|
40
|
+
'++',
|
|
41
|
+
'--',
|
|
42
|
+
'**',
|
|
43
|
+
'<<',
|
|
44
|
+
'>>',
|
|
45
|
+
'+=',
|
|
46
|
+
'-=',
|
|
47
|
+
'*=',
|
|
48
|
+
'/=',
|
|
49
|
+
'?',
|
|
50
|
+
'!',
|
|
51
|
+
'+',
|
|
52
|
+
'-',
|
|
53
|
+
'*',
|
|
54
|
+
'/',
|
|
55
|
+
'%',
|
|
56
|
+
'<',
|
|
57
|
+
'>',
|
|
58
|
+
'=',
|
|
59
|
+
'&',
|
|
60
|
+
'|',
|
|
61
|
+
'^',
|
|
62
|
+
'~',
|
|
63
|
+
'@',
|
|
64
|
+
'#',
|
|
65
|
+
];
|
|
27
66
|
|
|
28
67
|
function describeOp(op: string): string {
|
|
29
|
-
if (op ===
|
|
30
|
-
if (op ===
|
|
31
|
-
if (op ===
|
|
32
|
-
if (op ===
|
|
33
|
-
if ([
|
|
34
|
-
|
|
35
|
-
if (
|
|
68
|
+
if (op === '?') return 'a ternary (`? :`)';
|
|
69
|
+
if (op === '??') return 'nullish coalescing (`??`)';
|
|
70
|
+
if (op === '&&' || op === '||') return `a logical operator (\`${op}\`)`;
|
|
71
|
+
if (op === '!') return 'negation (`!`)';
|
|
72
|
+
if (['===', '!==', '==', '!=', '<', '>', '<=', '>='].includes(op))
|
|
73
|
+
return `a comparison (\`${op}\`)`;
|
|
74
|
+
if (['+', '-', '*', '/', '%', '**'].includes(op))
|
|
75
|
+
return `arithmetic/concatenation (\`${op}\`)`;
|
|
76
|
+
if (op.endsWith('=') || op === '++' || op === '--') return `an assignment (\`${op}\`)`;
|
|
36
77
|
return `the operator \`${op}\``;
|
|
37
78
|
}
|
|
38
79
|
|
|
39
80
|
const KEYWORDS = new Set([
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
81
|
+
'typeof',
|
|
82
|
+
'void',
|
|
83
|
+
'delete',
|
|
84
|
+
'new',
|
|
85
|
+
'await',
|
|
86
|
+
'yield',
|
|
87
|
+
'this',
|
|
88
|
+
'super',
|
|
89
|
+
'class',
|
|
90
|
+
'function',
|
|
91
|
+
'async',
|
|
92
|
+
'if',
|
|
93
|
+
'else',
|
|
94
|
+
'for',
|
|
95
|
+
'while',
|
|
96
|
+
'do',
|
|
97
|
+
'switch',
|
|
98
|
+
'case',
|
|
99
|
+
'try',
|
|
100
|
+
'catch',
|
|
101
|
+
'throw',
|
|
102
|
+
'var',
|
|
103
|
+
'let',
|
|
104
|
+
'const',
|
|
105
|
+
'in',
|
|
106
|
+
'instanceof',
|
|
107
|
+
'import',
|
|
108
|
+
'export',
|
|
109
|
+
'return',
|
|
110
|
+
'arguments',
|
|
111
|
+
'eval',
|
|
43
112
|
]);
|
|
44
113
|
|
|
45
114
|
function tokenize(src: string): Tok[] {
|
|
@@ -47,57 +116,72 @@ function tokenize(src: string): Tok[] {
|
|
|
47
116
|
let i = 0;
|
|
48
117
|
while (i < src.length) {
|
|
49
118
|
const c = src[i]!;
|
|
50
|
-
if (/\s/.test(c)) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
119
|
+
if (/\s/.test(c)) {
|
|
120
|
+
i++;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (src.startsWith('//', i)) {
|
|
124
|
+
const e = src.indexOf('\n', i);
|
|
125
|
+
i = e < 0 ? src.length : e + 1;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (src.startsWith('/*', i)) {
|
|
129
|
+
const e = src.indexOf('*/', i + 2);
|
|
130
|
+
if (e < 0) throw new MapperError('mapper source has an unterminated comment');
|
|
55
131
|
i = e + 2;
|
|
56
132
|
continue;
|
|
57
133
|
}
|
|
58
|
-
if (c ===
|
|
134
|
+
if (c === '`') throw new MapperError('mapper uses a template string');
|
|
59
135
|
if (c === '"' || c === "'") {
|
|
60
136
|
let j = i + 1;
|
|
61
|
-
let v =
|
|
137
|
+
let v = '';
|
|
62
138
|
for (;;) {
|
|
63
|
-
if (j >= src.length)
|
|
139
|
+
if (j >= src.length)
|
|
140
|
+
throw new MapperError('mapper source has an unterminated string');
|
|
64
141
|
const d = src[j]!;
|
|
65
142
|
if (d === c) break;
|
|
66
|
-
if (d ===
|
|
143
|
+
if (d === '\\') {
|
|
144
|
+
v += src.slice(j, j + 2);
|
|
145
|
+
j += 2;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
67
148
|
v += d;
|
|
68
149
|
j++;
|
|
69
150
|
}
|
|
70
|
-
toks.push({ t:
|
|
151
|
+
toks.push({ t: 'str', v, pos: i });
|
|
71
152
|
i = j + 1;
|
|
72
153
|
continue;
|
|
73
154
|
}
|
|
74
|
-
const num =
|
|
75
|
-
|
|
76
|
-
|
|
155
|
+
const num =
|
|
156
|
+
/^(?:0[xXoObB][0-9a-fA-F_]+|(?:\d[\d_]*)?\.?\d[\d_]*(?:[eE][+-]?\d+)?)n?/.exec(
|
|
157
|
+
src.slice(i),
|
|
158
|
+
);
|
|
159
|
+
if (num && /[\d.]/.test(c) && num[0] !== '.') {
|
|
160
|
+
toks.push({ t: 'num', v: num[0], pos: i });
|
|
77
161
|
i += num[0].length;
|
|
78
162
|
continue;
|
|
79
163
|
}
|
|
80
164
|
const id = /^[\p{ID_Start}$_][\p{ID_Continue}$]*/u.exec(src.slice(i));
|
|
81
165
|
if (id) {
|
|
82
|
-
toks.push({ t:
|
|
166
|
+
toks.push({ t: 'ident', v: id[0], pos: i });
|
|
83
167
|
i += id[0].length;
|
|
84
168
|
continue;
|
|
85
169
|
}
|
|
86
170
|
const p = PUNCS.find((x) => src.startsWith(x, i));
|
|
87
|
-
if (p && !(p ===
|
|
88
|
-
toks.push({ t:
|
|
171
|
+
if (p && !(p === '?.' && /\d/.test(src[i + 2] ?? ''))) {
|
|
172
|
+
toks.push({ t: 'punc', v: p, pos: i });
|
|
89
173
|
i += p.length;
|
|
90
174
|
continue;
|
|
91
175
|
}
|
|
92
176
|
const o = OPS.find((x) => src.startsWith(x, i));
|
|
93
177
|
if (o) {
|
|
94
|
-
toks.push({ t:
|
|
178
|
+
toks.push({ t: 'op', v: o, pos: i });
|
|
95
179
|
i += o.length;
|
|
96
180
|
continue;
|
|
97
181
|
}
|
|
98
182
|
throw new MapperError(`mapper source has an unexpected character '${c}'`);
|
|
99
183
|
}
|
|
100
|
-
toks.push({ t:
|
|
184
|
+
toks.push({ t: 'eof', v: '', pos: src.length });
|
|
101
185
|
return toks;
|
|
102
186
|
}
|
|
103
187
|
|
|
@@ -107,132 +191,193 @@ export function analyzeMapperSource(src: string): void {
|
|
|
107
191
|
let k = 0;
|
|
108
192
|
const peek = (o = 0) => toks[Math.min(k + o, toks.length - 1)]!;
|
|
109
193
|
const next = () => toks[k++]!;
|
|
110
|
-
const is = (t: Tok[
|
|
111
|
-
|
|
194
|
+
const is = (t: Tok['t'], v?: string, o = 0) =>
|
|
195
|
+
peek(o).t === t && (v === undefined || peek(o).v === v);
|
|
196
|
+
const unexpected = (what = 'here'): never => {
|
|
112
197
|
const tok = peek();
|
|
113
|
-
if (tok.t ===
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (tok.t ===
|
|
118
|
-
|
|
198
|
+
if (tok.t === 'op')
|
|
199
|
+
throw new MapperError(
|
|
200
|
+
`mapper uses ${describeOp(tok.v)}; mappers may only contain property access and literals — move logic into the handler or a when block`,
|
|
201
|
+
);
|
|
202
|
+
if (tok.t === 'punc' && tok.v === '?.')
|
|
203
|
+
throw new MapperError(
|
|
204
|
+
'mapper uses optional chaining (`?.`); declare the input optional(...) and map the whole ref instead',
|
|
205
|
+
);
|
|
206
|
+
if (tok.t === 'punc' && tok.v === '...')
|
|
207
|
+
throw new MapperError('mapper uses spread (`...`); list the fields explicitly');
|
|
208
|
+
if (tok.t === 'punc' && tok.v === '(')
|
|
209
|
+
throw new MapperError(
|
|
210
|
+
'mapper calls a function or method; mappers may only contain property access and literals',
|
|
211
|
+
);
|
|
212
|
+
if (tok.t === 'ident' && KEYWORDS.has(tok.v))
|
|
213
|
+
throw new MapperError(
|
|
214
|
+
`mapper uses \`${tok.v}\`; mappers may only contain property access and literals`,
|
|
215
|
+
);
|
|
216
|
+
throw new MapperError(
|
|
217
|
+
`mapper source is not an object literal of property accesses and literals (unexpected ${tok.t === 'eof' ? 'end' : `'${tok.v}'`} ${what})`,
|
|
218
|
+
);
|
|
119
219
|
};
|
|
120
|
-
const expect = (t: Tok[
|
|
220
|
+
const expect = (t: Tok['t'], v?: string) =>
|
|
221
|
+
is(t, v) ? next() : unexpected(`(expected ${v ?? t})`);
|
|
121
222
|
|
|
122
223
|
const identifier = () => {
|
|
123
|
-
if (!is(
|
|
224
|
+
if (!is('ident') || KEYWORDS.has(peek().v)) unexpected('(expected an identifier)');
|
|
124
225
|
return next();
|
|
125
226
|
};
|
|
126
227
|
|
|
127
228
|
// ── parameters ──
|
|
128
229
|
const params = () => {
|
|
129
|
-
if (is(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
230
|
+
if (is('ident')) {
|
|
231
|
+
identifier();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
expect('punc', '(');
|
|
235
|
+
if (is('punc', ')')) {
|
|
236
|
+
next();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (is('punc', '{')) destructuring();
|
|
133
240
|
else identifier();
|
|
134
|
-
if (is(
|
|
135
|
-
if (is(
|
|
136
|
-
expect(
|
|
241
|
+
if (is('punc', ',')) next();
|
|
242
|
+
if (is('op', '=')) throw new MapperError('mapper parameter has a default value');
|
|
243
|
+
expect('punc', ')');
|
|
137
244
|
};
|
|
138
245
|
const destructuring = () => {
|
|
139
|
-
expect(
|
|
140
|
-
while (!is(
|
|
141
|
-
if (is(
|
|
142
|
-
|
|
246
|
+
expect('punc', '{');
|
|
247
|
+
while (!is('punc', '}')) {
|
|
248
|
+
if (is('str')) {
|
|
249
|
+
next();
|
|
250
|
+
expect('punc', ':');
|
|
251
|
+
identifier();
|
|
252
|
+
} else {
|
|
143
253
|
identifier();
|
|
144
|
-
if (is(
|
|
254
|
+
if (is('punc', ':')) {
|
|
255
|
+
next();
|
|
256
|
+
identifier();
|
|
257
|
+
}
|
|
145
258
|
}
|
|
146
|
-
if (is(
|
|
147
|
-
|
|
259
|
+
if (is('op', '='))
|
|
260
|
+
throw new MapperError('mapper parameter destructuring has a default value');
|
|
261
|
+
if (!is('punc', ',')) break;
|
|
148
262
|
next();
|
|
149
263
|
}
|
|
150
|
-
expect(
|
|
264
|
+
expect('punc', '}');
|
|
151
265
|
};
|
|
152
266
|
|
|
153
267
|
// ── values ──
|
|
154
268
|
const value = (): void => {
|
|
155
|
-
if (is(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (is(
|
|
269
|
+
if (is('punc', '(')) {
|
|
270
|
+
next();
|
|
271
|
+
value();
|
|
272
|
+
expect('punc', ')');
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (is('punc', '{')) return object();
|
|
276
|
+
if (is('punc', '[')) return array();
|
|
277
|
+
if (is('str') || is('num')) {
|
|
278
|
+
next();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (is('op', '-') && is('num', undefined, 1)) {
|
|
282
|
+
next();
|
|
283
|
+
next();
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (is('ident', 'true') || is('ident', 'false') || is('ident', 'null')) {
|
|
287
|
+
next();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (is('ident', 'undefined'))
|
|
291
|
+
throw new MapperError('mapper uses `undefined`; leave optional fields out instead');
|
|
162
292
|
chain();
|
|
163
293
|
};
|
|
164
294
|
const chain = () => {
|
|
165
295
|
identifier();
|
|
166
296
|
for (;;) {
|
|
167
|
-
if (is(
|
|
168
|
-
|
|
297
|
+
if (is('punc', '.')) {
|
|
298
|
+
next();
|
|
299
|
+
if (!is('ident')) unexpected('(expected a property name)');
|
|
300
|
+
next();
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (is('punc', '[')) {
|
|
169
304
|
next();
|
|
170
|
-
if (!is(
|
|
305
|
+
if (!is('str') && !is('num'))
|
|
306
|
+
throw new MapperError(
|
|
307
|
+
'mapper uses a computed property access; only literal keys (from["step-name"]) are allowed',
|
|
308
|
+
);
|
|
171
309
|
next();
|
|
172
|
-
expect(
|
|
310
|
+
expect('punc', ']');
|
|
173
311
|
continue;
|
|
174
312
|
}
|
|
175
313
|
break;
|
|
176
314
|
}
|
|
177
315
|
};
|
|
178
316
|
const array = () => {
|
|
179
|
-
expect(
|
|
180
|
-
while (!is(
|
|
181
|
-
if (is(
|
|
317
|
+
expect('punc', '[');
|
|
318
|
+
while (!is('punc', ']')) {
|
|
319
|
+
if (is('punc', ',')) unexpected('(array holes are not allowed)');
|
|
182
320
|
value();
|
|
183
|
-
if (!is(
|
|
321
|
+
if (!is('punc', ',')) break;
|
|
184
322
|
next();
|
|
185
323
|
}
|
|
186
|
-
expect(
|
|
324
|
+
expect('punc', ']');
|
|
187
325
|
};
|
|
188
326
|
const object = () => {
|
|
189
|
-
expect(
|
|
190
|
-
while (!is(
|
|
191
|
-
if (is(
|
|
192
|
-
if (is(
|
|
193
|
-
|
|
327
|
+
expect('punc', '{');
|
|
328
|
+
while (!is('punc', '}')) {
|
|
329
|
+
if (is('punc', '...')) unexpected();
|
|
330
|
+
if (is('punc', '['))
|
|
331
|
+
throw new MapperError(
|
|
332
|
+
'mapper uses a computed key; only literal keys are allowed',
|
|
333
|
+
);
|
|
334
|
+
if (is('ident') && (is('punc', ',', 1) || is('punc', '}', 1))) {
|
|
194
335
|
// shorthand `{ input }` — a destructured root or a module constant
|
|
195
336
|
identifier();
|
|
196
337
|
} else {
|
|
197
|
-
if (is(
|
|
198
|
-
else unexpected(
|
|
199
|
-
if (is(
|
|
200
|
-
|
|
338
|
+
if (is('ident') || is('str') || is('num')) next();
|
|
339
|
+
else unexpected('(expected a property name)');
|
|
340
|
+
if (is('punc', '('))
|
|
341
|
+
throw new MapperError(
|
|
342
|
+
'mapper defines a method; mappers may only contain property access and literals',
|
|
343
|
+
);
|
|
344
|
+
expect('punc', ':');
|
|
201
345
|
value();
|
|
202
346
|
}
|
|
203
|
-
if (!is(
|
|
347
|
+
if (!is('punc', ',')) break;
|
|
204
348
|
next();
|
|
205
349
|
}
|
|
206
|
-
expect(
|
|
350
|
+
expect('punc', '}');
|
|
207
351
|
};
|
|
208
352
|
const returnBody = () => {
|
|
209
|
-
expect(
|
|
210
|
-
if (!is(
|
|
353
|
+
expect('punc', '{');
|
|
354
|
+
if (!is('ident', 'return'))
|
|
355
|
+
unexpected('(a block-bodied mapper must consist of a single `return { … }`)');
|
|
211
356
|
next();
|
|
212
357
|
object();
|
|
213
|
-
if (is(
|
|
214
|
-
expect(
|
|
358
|
+
if (is('punc', ';')) next();
|
|
359
|
+
expect('punc', '}');
|
|
215
360
|
};
|
|
216
361
|
|
|
217
362
|
// ── function ──
|
|
218
|
-
if (is(
|
|
219
|
-
if (is(
|
|
363
|
+
if (is('ident', 'async')) throw new MapperError('mapper must not be async');
|
|
364
|
+
if (is('ident', 'function')) {
|
|
220
365
|
next();
|
|
221
|
-
if (is(
|
|
222
|
-
if (is(
|
|
366
|
+
if (is('op', '*')) throw new MapperError('mapper must not be a generator');
|
|
367
|
+
if (is('ident')) next();
|
|
223
368
|
params();
|
|
224
369
|
returnBody();
|
|
225
370
|
} else {
|
|
226
371
|
params();
|
|
227
|
-
expect(
|
|
228
|
-
if (is(
|
|
372
|
+
expect('punc', '=>');
|
|
373
|
+
if (is('punc', '(')) {
|
|
229
374
|
next();
|
|
230
375
|
object();
|
|
231
|
-
expect(
|
|
232
|
-
} else if (is(
|
|
376
|
+
expect('punc', ')');
|
|
377
|
+
} else if (is('punc', '{')) {
|
|
233
378
|
returnBody();
|
|
234
|
-
} else unexpected(
|
|
379
|
+
} else unexpected('(the mapper body must be an object literal: from => ({ … }))');
|
|
235
380
|
}
|
|
236
|
-
if (is(
|
|
237
|
-
if (!is(
|
|
381
|
+
if (is('punc', ';')) next();
|
|
382
|
+
if (!is('eof')) unexpected('after the mapper body');
|
|
238
383
|
}
|
package/src/describe/record.ts
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
// `in`, assignment and JSON.stringify. It CANNOT see truthiness, `!`, `??`, `||`, ternaries or
|
|
5
5
|
// `===` against a value — those are rejected by the static source analysis (mapper-source.ts),
|
|
6
6
|
// and primarily by the `Ref<T>` types.
|
|
7
|
-
import { REF_PATH } from
|
|
7
|
+
import { REF_PATH } from '../core/brands.ts';
|
|
8
8
|
|
|
9
9
|
export class MapperError extends Error {
|
|
10
|
-
override name =
|
|
10
|
+
override name = 'MapperError';
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/** Manifest mapping node: `{ref}` | `{const}` | a nested object of mappings. */
|
|
@@ -24,22 +24,28 @@ function refProxy(path: string): unknown {
|
|
|
24
24
|
return new Proxy(target, {
|
|
25
25
|
get(_t, prop) {
|
|
26
26
|
if (prop === REF_PATH) return path;
|
|
27
|
-
if (prop ===
|
|
28
|
-
if (prop === Symbol.toPrimitive || prop ===
|
|
29
|
-
return fail(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (prop
|
|
27
|
+
if (prop === 'then') return undefined; // `await ref` must not recurse
|
|
28
|
+
if (prop === Symbol.toPrimitive || prop === 'toString' || prop === 'valueOf')
|
|
29
|
+
return fail(
|
|
30
|
+
'coerces a value (template string / concatenation / arithmetic / comparison)',
|
|
31
|
+
);
|
|
32
|
+
if (prop === 'toJSON') return fail('serializes a reference (JSON.stringify)');
|
|
33
|
+
if (typeof prop === 'symbol')
|
|
34
|
+
return fail(`uses ${String(prop)} (iteration/spread/inspection)`);
|
|
35
|
+
if (prop.includes('.'))
|
|
36
|
+
return fail(
|
|
37
|
+
`accesses field '${prop}' (field names containing '.' cannot be mapped)`,
|
|
38
|
+
);
|
|
33
39
|
return refProxy(`${path}.${prop}`);
|
|
34
40
|
},
|
|
35
|
-
apply: () => fail(
|
|
36
|
-
construct: () => fail(
|
|
37
|
-
set: () => fail(
|
|
38
|
-
deleteProperty: () => fail(
|
|
39
|
-
has: () => fail(
|
|
40
|
-
ownKeys: () => fail(
|
|
41
|
-
defineProperty: () => fail(
|
|
42
|
-
getPrototypeOf: () => fail(
|
|
41
|
+
apply: () => fail('calls a function/method'),
|
|
42
|
+
construct: () => fail('constructs'),
|
|
43
|
+
set: () => fail('assigns'),
|
|
44
|
+
deleteProperty: () => fail('deletes'),
|
|
45
|
+
has: () => fail('uses the `in` operator'),
|
|
46
|
+
ownKeys: () => fail('enumerates/spreads'),
|
|
47
|
+
defineProperty: () => fail('defines a property'),
|
|
48
|
+
getPrototypeOf: () => fail('inspects the prototype (instanceof)'),
|
|
43
49
|
});
|
|
44
50
|
}
|
|
45
51
|
|
|
@@ -49,59 +55,83 @@ export function rootProxy(available: ReadonlySet<string>): unknown {
|
|
|
49
55
|
{},
|
|
50
56
|
{
|
|
51
57
|
get(_t, prop) {
|
|
52
|
-
if (typeof prop ===
|
|
58
|
+
if (typeof prop === 'symbol' || prop === 'then') return undefined;
|
|
53
59
|
if (!available.has(prop))
|
|
54
|
-
throw new MapperError(
|
|
60
|
+
throw new MapperError(
|
|
61
|
+
`mapper references '${prop}', which is not available at this stage (available: ${[...available].sort().join(', ') || 'none'})`,
|
|
62
|
+
);
|
|
55
63
|
return refProxy(prop);
|
|
56
64
|
},
|
|
57
65
|
has: () => {
|
|
58
|
-
throw new MapperError(
|
|
66
|
+
throw new MapperError('mapper uses the `in` operator on its argument');
|
|
59
67
|
},
|
|
60
68
|
ownKeys: () => {
|
|
61
|
-
throw new MapperError(
|
|
69
|
+
throw new MapperError('mapper enumerates/spreads its argument');
|
|
62
70
|
},
|
|
63
71
|
},
|
|
64
72
|
);
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
export const refPathOf = (v: unknown): string | undefined =>
|
|
68
|
-
typeof v ===
|
|
76
|
+
typeof v === 'function'
|
|
77
|
+
? ((v as unknown as Record<symbol, unknown>)[REF_PATH] as string | undefined)
|
|
78
|
+
: undefined;
|
|
69
79
|
|
|
70
80
|
function toMapping(v: unknown, at: string): Mapping {
|
|
71
81
|
const path = refPathOf(v);
|
|
72
82
|
if (path !== undefined) return { ref: path };
|
|
73
|
-
if (typeof v ===
|
|
83
|
+
if (typeof v === 'function')
|
|
84
|
+
throw new MapperError(`mapper returns a function at '${at}'`);
|
|
74
85
|
if (v === undefined) throw new MapperError(`mapper returns undefined at '${at}'`);
|
|
75
|
-
if (v === null || typeof v ===
|
|
76
|
-
if (typeof v ===
|
|
77
|
-
if (!Number.isFinite(v))
|
|
86
|
+
if (v === null || typeof v === 'string' || typeof v === 'boolean') return { const: v };
|
|
87
|
+
if (typeof v === 'number') {
|
|
88
|
+
if (!Number.isFinite(v))
|
|
89
|
+
throw new MapperError(`mapper returns a non-finite number at '${at}'`);
|
|
78
90
|
return { const: v };
|
|
79
91
|
}
|
|
80
92
|
if (Array.isArray(v)) {
|
|
81
93
|
const items = v.map((x, i) => toMapping(x, `${at}[${i}]`));
|
|
82
|
-
if (items.every((e) =>
|
|
83
|
-
|
|
94
|
+
if (items.every((e) => 'const' in e && Object.keys(e).length === 1))
|
|
95
|
+
return { const: items.map((e) => (e as { const: unknown }).const) };
|
|
96
|
+
throw new MapperError(
|
|
97
|
+
`mapper puts references inside an array at '${at}' (only constant arrays are supported)`,
|
|
98
|
+
);
|
|
84
99
|
}
|
|
85
|
-
if (typeof v ===
|
|
86
|
-
if (
|
|
100
|
+
if (typeof v === 'object') {
|
|
101
|
+
if (
|
|
102
|
+
Object.getPrototypeOf(v) !== Object.prototype &&
|
|
103
|
+
Object.getPrototypeOf(v) !== null
|
|
104
|
+
)
|
|
87
105
|
throw new MapperError(`mapper returns a non-plain object at '${at}'`);
|
|
88
106
|
const keys = Object.keys(v);
|
|
89
|
-
if (keys.length === 1 && (keys[0] ===
|
|
90
|
-
throw new MapperError(
|
|
107
|
+
if (keys.length === 1 && (keys[0] === 'ref' || keys[0] === 'const'))
|
|
108
|
+
throw new MapperError(
|
|
109
|
+
`mapper builds an object whose only field is '${keys[0]}' at '${at}'; that is reserved by the manifest's mapping encoding — map the whole object with a ref instead`,
|
|
110
|
+
);
|
|
91
111
|
const out: Record<string, Mapping> = {};
|
|
92
|
-
for (const k of keys)
|
|
112
|
+
for (const k of keys)
|
|
113
|
+
out[k] = toMapping((v as Record<string, unknown>)[k], at ? `${at}.${k}` : k);
|
|
93
114
|
return out;
|
|
94
115
|
}
|
|
95
116
|
throw new MapperError(`mapper returns an unsupported value (${typeof v}) at '${at}'`);
|
|
96
117
|
}
|
|
97
118
|
|
|
98
119
|
/** Calls `fn` with recording proxies and returns `{ inputKey: Mapping }`. */
|
|
99
|
-
export function recordMapper(
|
|
120
|
+
export function recordMapper(
|
|
121
|
+
fn: (from: unknown) => unknown,
|
|
122
|
+
available: ReadonlySet<string>,
|
|
123
|
+
): Record<string, Mapping> {
|
|
100
124
|
const result = fn(rootProxy(available));
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
125
|
+
if (
|
|
126
|
+
refPathOf(result) !== undefined ||
|
|
127
|
+
typeof result !== 'object' ||
|
|
128
|
+
result === null ||
|
|
129
|
+
Array.isArray(result)
|
|
130
|
+
)
|
|
131
|
+
throw new MapperError('mapper must return an object literal');
|
|
132
|
+
if (result instanceof Promise) throw new MapperError('mapper must be synchronous');
|
|
104
133
|
const out: Record<string, Mapping> = {};
|
|
105
|
-
for (const k of Object.keys(result))
|
|
134
|
+
for (const k of Object.keys(result))
|
|
135
|
+
out[k] = toMapping((result as Record<string, unknown>)[k], k);
|
|
106
136
|
return out;
|
|
107
137
|
}
|