@olivervea/pipelines 2.0.0-beta.3 → 2.0.0-beta.31
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 +165 -78
- package/manifest.schema.json +300 -16
- package/package.json +6 -4
- package/src/assets.d.ts +2 -2
- package/src/build/build.ts +98 -64
- package/src/build/cli.ts +33 -23
- package/src/build/elf.ts +16 -7
- package/src/build/hashes.ts +40 -51
- package/src/build/project.ts +55 -52
- package/src/builder/pipeline.ts +348 -89
- package/src/builder/refs.ts +15 -5
- package/src/core/brands.ts +6 -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 +62 -15
- package/src/core/matrix.ts +150 -0
- package/src/core/models.ts +37 -15
- package/src/core/secrets.ts +2 -2
- package/src/core/steps.ts +113 -38
- package/src/core/types.ts +5 -1
- package/src/core/webhooks.ts +21 -7
- package/src/ctx/files.ts +119 -42
- package/src/ctx/index.ts +110 -39
- package/src/ctx/links.ts +30 -0
- package/src/ctx/sh.ts +106 -44
- package/src/ctx/types.ts +24 -4
- package/src/describe/config-schema.ts +98 -0
- package/src/describe/context.ts +187 -0
- package/src/describe/describe.ts +63 -355
- package/src/describe/failure-handlers.ts +34 -0
- package/src/describe/manifest-schema.ts +304 -61
- package/src/describe/mapper-source.ts +252 -105
- package/src/describe/record.ts +67 -37
- package/src/describe/schemas.ts +71 -0
- package/src/describe/stages.ts +166 -0
- package/src/describe/step.ts +544 -0
- package/src/describe/triggers.ts +72 -0
- package/src/dev/cli.ts +62 -35
- package/src/harness/env.ts +47 -25
- package/src/harness/execute.ts +74 -30
- package/src/harness/exit.ts +1 -1
- package/src/harness/index.ts +19 -8
- package/src/harness/main.ts +245 -99
- package/src/harness/mask.ts +98 -0
- package/src/harness/result.ts +45 -24
- package/src/harness/validate.ts +82 -15
- package/src/index.ts +139 -10
- package/src/library/checks.ts +11 -6
- package/src/library/compose-deploy.ts +115 -0
- package/src/library/github-source.ts +100 -47
- package/src/library/helm-deploy-ssh.ts +426 -112
- package/src/library/helm-deploy.ts +529 -0
- package/src/library/http-health.ts +7 -5
- package/src/library/image-build.ts +510 -162
- package/src/library/index.ts +48 -14
- package/src/library/models.ts +35 -14
- package/src/library/ssh.ts +44 -18
- package/src/testing/index.ts +100 -35
- package/src/util/canonical.ts +7 -4
- package/src/util/code-hash.ts +17 -0
- package/src/util/hash.ts +5 -5
- package/src/util/tar.ts +83 -38
- package/src/library/script.ts +0 -85
- package/src/util/handler-hash.ts +0 -22
|
@@ -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,74 @@ 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
|
-
|
|
59
|
-
if (c === '"' || c === "'") {
|
|
134
|
+
// Bun prints a string constant holding a newline or both quote kinds as a template literal.
|
|
135
|
+
if (c === '"' || 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 (
|
|
143
|
+
if (c === '`' && d === '$' && src[j + 1] === '{')
|
|
144
|
+
throw new MapperError('mapper uses a template string');
|
|
145
|
+
if (d === '\\') {
|
|
146
|
+
v += src.slice(j, j + 2);
|
|
147
|
+
j += 2;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
67
150
|
v += d;
|
|
68
151
|
j++;
|
|
69
152
|
}
|
|
70
|
-
toks.push({ t:
|
|
153
|
+
toks.push({ t: 'str', v, pos: i });
|
|
71
154
|
i = j + 1;
|
|
72
155
|
continue;
|
|
73
156
|
}
|
|
74
|
-
const num =
|
|
75
|
-
|
|
76
|
-
|
|
157
|
+
const num =
|
|
158
|
+
/^(?:0[xXoObB][0-9a-fA-F_]+|(?:\d[\d_]*)?\.?\d[\d_]*(?:[eE][+-]?\d+)?)n?/.exec(
|
|
159
|
+
src.slice(i),
|
|
160
|
+
);
|
|
161
|
+
if (num && /[\d.]/.test(c) && num[0] !== '.') {
|
|
162
|
+
toks.push({ t: 'num', v: num[0], pos: i });
|
|
77
163
|
i += num[0].length;
|
|
78
164
|
continue;
|
|
79
165
|
}
|
|
80
166
|
const id = /^[\p{ID_Start}$_][\p{ID_Continue}$]*/u.exec(src.slice(i));
|
|
81
167
|
if (id) {
|
|
82
|
-
toks.push({ t:
|
|
168
|
+
toks.push({ t: 'ident', v: id[0], pos: i });
|
|
83
169
|
i += id[0].length;
|
|
84
170
|
continue;
|
|
85
171
|
}
|
|
86
172
|
const p = PUNCS.find((x) => src.startsWith(x, i));
|
|
87
|
-
if (p && !(p ===
|
|
88
|
-
toks.push({ t:
|
|
173
|
+
if (p && !(p === '?.' && /\d/.test(src[i + 2] ?? ''))) {
|
|
174
|
+
toks.push({ t: 'punc', v: p, pos: i });
|
|
89
175
|
i += p.length;
|
|
90
176
|
continue;
|
|
91
177
|
}
|
|
92
178
|
const o = OPS.find((x) => src.startsWith(x, i));
|
|
93
179
|
if (o) {
|
|
94
|
-
toks.push({ t:
|
|
180
|
+
toks.push({ t: 'op', v: o, pos: i });
|
|
95
181
|
i += o.length;
|
|
96
182
|
continue;
|
|
97
183
|
}
|
|
98
184
|
throw new MapperError(`mapper source has an unexpected character '${c}'`);
|
|
99
185
|
}
|
|
100
|
-
toks.push({ t:
|
|
186
|
+
toks.push({ t: 'eof', v: '', pos: src.length });
|
|
101
187
|
return toks;
|
|
102
188
|
}
|
|
103
189
|
|
|
@@ -107,132 +193,193 @@ export function analyzeMapperSource(src: string): void {
|
|
|
107
193
|
let k = 0;
|
|
108
194
|
const peek = (o = 0) => toks[Math.min(k + o, toks.length - 1)]!;
|
|
109
195
|
const next = () => toks[k++]!;
|
|
110
|
-
const is = (t: Tok[
|
|
111
|
-
|
|
196
|
+
const is = (t: Tok['t'], v?: string, o = 0) =>
|
|
197
|
+
peek(o).t === t && (v === undefined || peek(o).v === v);
|
|
198
|
+
const unexpected = (what = 'here'): never => {
|
|
112
199
|
const tok = peek();
|
|
113
|
-
if (tok.t ===
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (tok.t ===
|
|
118
|
-
|
|
200
|
+
if (tok.t === 'op')
|
|
201
|
+
throw new MapperError(
|
|
202
|
+
`mapper uses ${describeOp(tok.v)}; mappers may only contain property access and literals — move logic into the handler or a when block`,
|
|
203
|
+
);
|
|
204
|
+
if (tok.t === 'punc' && tok.v === '?.')
|
|
205
|
+
throw new MapperError(
|
|
206
|
+
'mapper uses optional chaining (`?.`); declare the input optional(...) and map the whole ref instead',
|
|
207
|
+
);
|
|
208
|
+
if (tok.t === 'punc' && tok.v === '...')
|
|
209
|
+
throw new MapperError('mapper uses spread (`...`); list the fields explicitly');
|
|
210
|
+
if (tok.t === 'punc' && tok.v === '(')
|
|
211
|
+
throw new MapperError(
|
|
212
|
+
'mapper calls a function or method; mappers may only contain property access and literals',
|
|
213
|
+
);
|
|
214
|
+
if (tok.t === 'ident' && KEYWORDS.has(tok.v))
|
|
215
|
+
throw new MapperError(
|
|
216
|
+
`mapper uses \`${tok.v}\`; mappers may only contain property access and literals`,
|
|
217
|
+
);
|
|
218
|
+
throw new MapperError(
|
|
219
|
+
`mapper source is not an object literal of property accesses and literals (unexpected ${tok.t === 'eof' ? 'end' : `'${tok.v}'`} ${what})`,
|
|
220
|
+
);
|
|
119
221
|
};
|
|
120
|
-
const expect = (t: Tok[
|
|
222
|
+
const expect = (t: Tok['t'], v?: string) =>
|
|
223
|
+
is(t, v) ? next() : unexpected(`(expected ${v ?? t})`);
|
|
121
224
|
|
|
122
225
|
const identifier = () => {
|
|
123
|
-
if (!is(
|
|
226
|
+
if (!is('ident') || KEYWORDS.has(peek().v)) unexpected('(expected an identifier)');
|
|
124
227
|
return next();
|
|
125
228
|
};
|
|
126
229
|
|
|
127
230
|
// ── parameters ──
|
|
128
231
|
const params = () => {
|
|
129
|
-
if (is(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
232
|
+
if (is('ident')) {
|
|
233
|
+
identifier();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
expect('punc', '(');
|
|
237
|
+
if (is('punc', ')')) {
|
|
238
|
+
next();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (is('punc', '{')) destructuring();
|
|
133
242
|
else identifier();
|
|
134
|
-
if (is(
|
|
135
|
-
if (is(
|
|
136
|
-
expect(
|
|
243
|
+
if (is('punc', ',')) next();
|
|
244
|
+
if (is('op', '=')) throw new MapperError('mapper parameter has a default value');
|
|
245
|
+
expect('punc', ')');
|
|
137
246
|
};
|
|
138
247
|
const destructuring = () => {
|
|
139
|
-
expect(
|
|
140
|
-
while (!is(
|
|
141
|
-
if (is(
|
|
142
|
-
|
|
248
|
+
expect('punc', '{');
|
|
249
|
+
while (!is('punc', '}')) {
|
|
250
|
+
if (is('str')) {
|
|
251
|
+
next();
|
|
252
|
+
expect('punc', ':');
|
|
253
|
+
identifier();
|
|
254
|
+
} else {
|
|
143
255
|
identifier();
|
|
144
|
-
if (is(
|
|
256
|
+
if (is('punc', ':')) {
|
|
257
|
+
next();
|
|
258
|
+
identifier();
|
|
259
|
+
}
|
|
145
260
|
}
|
|
146
|
-
if (is(
|
|
147
|
-
|
|
261
|
+
if (is('op', '='))
|
|
262
|
+
throw new MapperError('mapper parameter destructuring has a default value');
|
|
263
|
+
if (!is('punc', ',')) break;
|
|
148
264
|
next();
|
|
149
265
|
}
|
|
150
|
-
expect(
|
|
266
|
+
expect('punc', '}');
|
|
151
267
|
};
|
|
152
268
|
|
|
153
269
|
// ── values ──
|
|
154
270
|
const value = (): void => {
|
|
155
|
-
if (is(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (is(
|
|
271
|
+
if (is('punc', '(')) {
|
|
272
|
+
next();
|
|
273
|
+
value();
|
|
274
|
+
expect('punc', ')');
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (is('punc', '{')) return object();
|
|
278
|
+
if (is('punc', '[')) return array();
|
|
279
|
+
if (is('str') || is('num')) {
|
|
280
|
+
next();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (is('op', '-') && is('num', undefined, 1)) {
|
|
284
|
+
next();
|
|
285
|
+
next();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (is('ident', 'true') || is('ident', 'false') || is('ident', 'null')) {
|
|
289
|
+
next();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (is('ident', 'undefined'))
|
|
293
|
+
throw new MapperError('mapper uses `undefined`; leave optional fields out instead');
|
|
162
294
|
chain();
|
|
163
295
|
};
|
|
164
296
|
const chain = () => {
|
|
165
297
|
identifier();
|
|
166
298
|
for (;;) {
|
|
167
|
-
if (is(
|
|
168
|
-
|
|
299
|
+
if (is('punc', '.')) {
|
|
300
|
+
next();
|
|
301
|
+
if (!is('ident')) unexpected('(expected a property name)');
|
|
302
|
+
next();
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (is('punc', '[')) {
|
|
169
306
|
next();
|
|
170
|
-
if (!is(
|
|
307
|
+
if (!is('str') && !is('num'))
|
|
308
|
+
throw new MapperError(
|
|
309
|
+
'mapper uses a computed property access; only literal keys (from["step-name"]) are allowed',
|
|
310
|
+
);
|
|
171
311
|
next();
|
|
172
|
-
expect(
|
|
312
|
+
expect('punc', ']');
|
|
173
313
|
continue;
|
|
174
314
|
}
|
|
175
315
|
break;
|
|
176
316
|
}
|
|
177
317
|
};
|
|
178
318
|
const array = () => {
|
|
179
|
-
expect(
|
|
180
|
-
while (!is(
|
|
181
|
-
if (is(
|
|
319
|
+
expect('punc', '[');
|
|
320
|
+
while (!is('punc', ']')) {
|
|
321
|
+
if (is('punc', ',')) unexpected('(array holes are not allowed)');
|
|
182
322
|
value();
|
|
183
|
-
if (!is(
|
|
323
|
+
if (!is('punc', ',')) break;
|
|
184
324
|
next();
|
|
185
325
|
}
|
|
186
|
-
expect(
|
|
326
|
+
expect('punc', ']');
|
|
187
327
|
};
|
|
188
328
|
const object = () => {
|
|
189
|
-
expect(
|
|
190
|
-
while (!is(
|
|
191
|
-
if (is(
|
|
192
|
-
if (is(
|
|
193
|
-
|
|
329
|
+
expect('punc', '{');
|
|
330
|
+
while (!is('punc', '}')) {
|
|
331
|
+
if (is('punc', '...')) unexpected();
|
|
332
|
+
if (is('punc', '['))
|
|
333
|
+
throw new MapperError(
|
|
334
|
+
'mapper uses a computed key; only literal keys are allowed',
|
|
335
|
+
);
|
|
336
|
+
if (is('ident') && (is('punc', ',', 1) || is('punc', '}', 1))) {
|
|
194
337
|
// shorthand `{ input }` — a destructured root or a module constant
|
|
195
338
|
identifier();
|
|
196
339
|
} else {
|
|
197
|
-
if (is(
|
|
198
|
-
else unexpected(
|
|
199
|
-
if (is(
|
|
200
|
-
|
|
340
|
+
if (is('ident') || is('str') || is('num')) next();
|
|
341
|
+
else unexpected('(expected a property name)');
|
|
342
|
+
if (is('punc', '('))
|
|
343
|
+
throw new MapperError(
|
|
344
|
+
'mapper defines a method; mappers may only contain property access and literals',
|
|
345
|
+
);
|
|
346
|
+
expect('punc', ':');
|
|
201
347
|
value();
|
|
202
348
|
}
|
|
203
|
-
if (!is(
|
|
349
|
+
if (!is('punc', ',')) break;
|
|
204
350
|
next();
|
|
205
351
|
}
|
|
206
|
-
expect(
|
|
352
|
+
expect('punc', '}');
|
|
207
353
|
};
|
|
208
354
|
const returnBody = () => {
|
|
209
|
-
expect(
|
|
210
|
-
if (!is(
|
|
355
|
+
expect('punc', '{');
|
|
356
|
+
if (!is('ident', 'return'))
|
|
357
|
+
unexpected('(a block-bodied mapper must consist of a single `return { … }`)');
|
|
211
358
|
next();
|
|
212
359
|
object();
|
|
213
|
-
if (is(
|
|
214
|
-
expect(
|
|
360
|
+
if (is('punc', ';')) next();
|
|
361
|
+
expect('punc', '}');
|
|
215
362
|
};
|
|
216
363
|
|
|
217
364
|
// ── function ──
|
|
218
|
-
if (is(
|
|
219
|
-
if (is(
|
|
365
|
+
if (is('ident', 'async')) throw new MapperError('mapper must not be async');
|
|
366
|
+
if (is('ident', 'function')) {
|
|
220
367
|
next();
|
|
221
|
-
if (is(
|
|
222
|
-
if (is(
|
|
368
|
+
if (is('op', '*')) throw new MapperError('mapper must not be a generator');
|
|
369
|
+
if (is('ident')) next();
|
|
223
370
|
params();
|
|
224
371
|
returnBody();
|
|
225
372
|
} else {
|
|
226
373
|
params();
|
|
227
|
-
expect(
|
|
228
|
-
if (is(
|
|
374
|
+
expect('punc', '=>');
|
|
375
|
+
if (is('punc', '(')) {
|
|
229
376
|
next();
|
|
230
377
|
object();
|
|
231
|
-
expect(
|
|
232
|
-
} else if (is(
|
|
378
|
+
expect('punc', ')');
|
|
379
|
+
} else if (is('punc', '{')) {
|
|
233
380
|
returnBody();
|
|
234
|
-
} else unexpected(
|
|
381
|
+
} else unexpected('(the mapper body must be an object literal: from => ({ … }))');
|
|
235
382
|
}
|
|
236
|
-
if (is(
|
|
237
|
-
if (!is(
|
|
383
|
+
if (is('punc', ';')) next();
|
|
384
|
+
if (!is('eof')) unexpected('after the mapper body');
|
|
238
385
|
}
|
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
|
}
|