@olivervea/pipelines 2.0.0-beta.3 → 2.0.0-beta.30

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.
Files changed (64) hide show
  1. package/README.md +163 -78
  2. package/manifest.schema.json +300 -16
  3. package/package.json +6 -4
  4. package/src/assets.d.ts +2 -2
  5. package/src/build/build.ts +98 -64
  6. package/src/build/cli.ts +33 -23
  7. package/src/build/elf.ts +16 -7
  8. package/src/build/hashes.ts +40 -51
  9. package/src/build/project.ts +55 -52
  10. package/src/builder/pipeline.ts +348 -89
  11. package/src/builder/refs.ts +15 -5
  12. package/src/core/brands.ts +6 -5
  13. package/src/core/config-refs.ts +103 -0
  14. package/src/core/config.ts +224 -0
  15. package/src/core/external.ts +86 -0
  16. package/src/core/handlers.ts +62 -15
  17. package/src/core/matrix.ts +150 -0
  18. package/src/core/models.ts +37 -15
  19. package/src/core/secrets.ts +2 -2
  20. package/src/core/steps.ts +113 -38
  21. package/src/core/types.ts +5 -1
  22. package/src/core/webhooks.ts +21 -7
  23. package/src/ctx/files.ts +119 -42
  24. package/src/ctx/index.ts +103 -38
  25. package/src/ctx/links.ts +30 -0
  26. package/src/ctx/sh.ts +104 -44
  27. package/src/ctx/types.ts +24 -4
  28. package/src/describe/config-schema.ts +98 -0
  29. package/src/describe/context.ts +187 -0
  30. package/src/describe/describe.ts +63 -355
  31. package/src/describe/failure-handlers.ts +34 -0
  32. package/src/describe/manifest-schema.ts +304 -61
  33. package/src/describe/mapper-source.ts +252 -105
  34. package/src/describe/record.ts +67 -37
  35. package/src/describe/schemas.ts +71 -0
  36. package/src/describe/stages.ts +166 -0
  37. package/src/describe/step.ts +544 -0
  38. package/src/describe/triggers.ts +72 -0
  39. package/src/dev/cli.ts +62 -35
  40. package/src/harness/env.ts +47 -25
  41. package/src/harness/execute.ts +74 -30
  42. package/src/harness/exit.ts +1 -1
  43. package/src/harness/index.ts +19 -8
  44. package/src/harness/main.ts +234 -98
  45. package/src/harness/result.ts +45 -24
  46. package/src/harness/validate.ts +82 -15
  47. package/src/index.ts +139 -10
  48. package/src/library/checks.ts +11 -6
  49. package/src/library/compose-deploy.ts +115 -0
  50. package/src/library/github-source.ts +100 -47
  51. package/src/library/helm-deploy-ssh.ts +426 -112
  52. package/src/library/helm-deploy.ts +529 -0
  53. package/src/library/http-health.ts +7 -5
  54. package/src/library/image-build.ts +510 -162
  55. package/src/library/index.ts +48 -14
  56. package/src/library/models.ts +35 -14
  57. package/src/library/ssh.ts +44 -18
  58. package/src/testing/index.ts +100 -35
  59. package/src/util/canonical.ts +7 -4
  60. package/src/util/code-hash.ts +17 -0
  61. package/src/util/hash.ts +5 -5
  62. package/src/util/tar.ts +83 -38
  63. package/src/library/script.ts +0 -85
  64. 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 "./record.ts";
15
+ import { MapperError } from './record.ts';
16
16
 
17
17
  type Tok =
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 };
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 === "?") return "a ternary (`? :`)";
30
- if (op === "??") return "nullish coalescing (`??`)";
31
- if (op === "&&" || op === "||") return `a logical operator (\`${op}\`)`;
32
- if (op === "!") return "negation (`!`)";
33
- if (["===", "!==", "==", "!=", "<", ">", "<=", ">="].includes(op)) return `a comparison (\`${op}\`)`;
34
- if (["+", "-", "*", "/", "%", "**"].includes(op)) return `arithmetic/concatenation (\`${op}\`)`;
35
- if (op.endsWith("=") || op === "++" || op === "--") return `an assignment (\`${op}\`)`;
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
- "typeof", "void", "delete", "new", "await", "yield", "this", "super", "class", "function", "async",
41
- "if", "else", "for", "while", "do", "switch", "case", "try", "catch", "throw", "var", "let", "const",
42
- "in", "instanceof", "import", "export", "return", "arguments", "eval",
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)) { i++; continue; }
51
- if (src.startsWith("//", i)) { const e = src.indexOf("\n", i); i = e < 0 ? src.length : e + 1; continue; }
52
- if (src.startsWith("/*", i)) {
53
- const e = src.indexOf("*/", i + 2);
54
- if (e < 0) throw new MapperError("mapper source has an unterminated comment");
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 === "`") throw new MapperError("mapper uses a template string");
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) throw new MapperError("mapper source has an unterminated string");
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 === "\\") { v += src.slice(j, j + 2); j += 2; continue; }
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: "str", v, pos: i });
153
+ toks.push({ t: 'str', v, pos: i });
71
154
  i = j + 1;
72
155
  continue;
73
156
  }
74
- const num = /^(?:0[xXoObB][0-9a-fA-F_]+|(?:\d[\d_]*)?\.?\d[\d_]*(?:[eE][+-]?\d+)?)n?/.exec(src.slice(i));
75
- if (num && /[\d.]/.test(c) && num[0] !== ".") {
76
- toks.push({ t: "num", v: num[0], pos: i });
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: "ident", v: id[0], pos: i });
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 === "?." && /\d/.test(src[i + 2] ?? ""))) {
88
- toks.push({ t: "punc", v: p, pos: i });
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: "op", v: o, pos: i });
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: "eof", v: "", pos: src.length });
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["t"], v?: string, o = 0) => peek(o).t === t && (v === undefined || peek(o).v === v);
111
- const unexpected = (what = "here"): never => {
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 === "op") throw new MapperError(`mapper uses ${describeOp(tok.v)}; mappers may only contain property access and literals — move logic into the handler or a when block`);
114
- if (tok.t === "punc" && tok.v === "?.") throw new MapperError("mapper uses optional chaining (`?.`); declare the input optional(...) and map the whole ref instead");
115
- if (tok.t === "punc" && tok.v === "...") throw new MapperError("mapper uses spread (`...`); list the fields explicitly");
116
- if (tok.t === "punc" && tok.v === "(") throw new MapperError("mapper calls a function or method; mappers may only contain property access and literals");
117
- if (tok.t === "ident" && KEYWORDS.has(tok.v)) throw new MapperError(`mapper uses \`${tok.v}\`; mappers may only contain property access and literals`);
118
- throw new MapperError(`mapper source is not an object literal of property accesses and literals (unexpected ${tok.t === "eof" ? "end" : `'${tok.v}'`} ${what})`);
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["t"], v?: string) => (is(t, v) ? next() : unexpected(`(expected ${v ?? t})`));
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("ident") || KEYWORDS.has(peek().v)) unexpected("(expected an identifier)");
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("ident")) { identifier(); return; }
130
- expect("punc", "(");
131
- if (is("punc", ")")) { next(); return; }
132
- if (is("punc", "{")) destructuring();
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("punc", ",")) next();
135
- if (is("op", "=")) throw new MapperError("mapper parameter has a default value");
136
- expect("punc", ")");
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("punc", "{");
140
- while (!is("punc", "}")) {
141
- if (is("str")) { next(); expect("punc", ":"); identifier(); }
142
- else {
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("punc", ":")) { next(); identifier(); }
256
+ if (is('punc', ':')) {
257
+ next();
258
+ identifier();
259
+ }
145
260
  }
146
- if (is("op", "=")) throw new MapperError("mapper parameter destructuring has a default value");
147
- if (!is("punc", ",")) break;
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("punc", "}");
266
+ expect('punc', '}');
151
267
  };
152
268
 
153
269
  // ── values ──
154
270
  const value = (): void => {
155
- if (is("punc", "(")) { next(); value(); expect("punc", ")"); return; }
156
- if (is("punc", "{")) return object();
157
- if (is("punc", "[")) return array();
158
- if (is("str") || is("num")) { next(); return; }
159
- if (is("op", "-") && is("num", undefined, 1)) { next(); next(); return; }
160
- if (is("ident", "true") || is("ident", "false") || is("ident", "null")) { next(); return; }
161
- if (is("ident", "undefined")) throw new MapperError("mapper uses `undefined`; leave optional fields out instead");
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("punc", ".")) { next(); if (!is("ident")) unexpected("(expected a property name)"); next(); continue; }
168
- if (is("punc", "[")) {
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("str") && !is("num")) throw new MapperError("mapper uses a computed property access; only literal keys (from[\"step-name\"]) are allowed");
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("punc", "]");
312
+ expect('punc', ']');
173
313
  continue;
174
314
  }
175
315
  break;
176
316
  }
177
317
  };
178
318
  const array = () => {
179
- expect("punc", "[");
180
- while (!is("punc", "]")) {
181
- if (is("punc", ",")) unexpected("(array holes are not allowed)");
319
+ expect('punc', '[');
320
+ while (!is('punc', ']')) {
321
+ if (is('punc', ',')) unexpected('(array holes are not allowed)');
182
322
  value();
183
- if (!is("punc", ",")) break;
323
+ if (!is('punc', ',')) break;
184
324
  next();
185
325
  }
186
- expect("punc", "]");
326
+ expect('punc', ']');
187
327
  };
188
328
  const object = () => {
189
- expect("punc", "{");
190
- while (!is("punc", "}")) {
191
- if (is("punc", "...")) unexpected();
192
- if (is("punc", "[")) throw new MapperError("mapper uses a computed key; only literal keys are allowed");
193
- if (is("ident") && (is("punc", ",", 1) || is("punc", "}", 1))) {
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("ident") || is("str") || is("num")) next();
198
- else unexpected("(expected a property name)");
199
- if (is("punc", "(")) throw new MapperError("mapper defines a method; mappers may only contain property access and literals");
200
- expect("punc", ":");
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("punc", ",")) break;
349
+ if (!is('punc', ',')) break;
204
350
  next();
205
351
  }
206
- expect("punc", "}");
352
+ expect('punc', '}');
207
353
  };
208
354
  const returnBody = () => {
209
- expect("punc", "{");
210
- if (!is("ident", "return")) unexpected("(a block-bodied mapper must consist of a single `return { … }`)");
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("punc", ";")) next();
214
- expect("punc", "}");
360
+ if (is('punc', ';')) next();
361
+ expect('punc', '}');
215
362
  };
216
363
 
217
364
  // ── function ──
218
- if (is("ident", "async")) throw new MapperError("mapper must not be async");
219
- if (is("ident", "function")) {
365
+ if (is('ident', 'async')) throw new MapperError('mapper must not be async');
366
+ if (is('ident', 'function')) {
220
367
  next();
221
- if (is("op", "*")) throw new MapperError("mapper must not be a generator");
222
- if (is("ident")) next();
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("punc", "=>");
228
- if (is("punc", "(")) {
374
+ expect('punc', '=>');
375
+ if (is('punc', '(')) {
229
376
  next();
230
377
  object();
231
- expect("punc", ")");
232
- } else if (is("punc", "{")) {
378
+ expect('punc', ')');
379
+ } else if (is('punc', '{')) {
233
380
  returnBody();
234
- } else unexpected("(the mapper body must be an object literal: from => ({ … }))");
381
+ } else unexpected('(the mapper body must be an object literal: from => ({ … }))');
235
382
  }
236
- if (is("punc", ";")) next();
237
- if (!is("eof")) unexpected("after the mapper body");
383
+ if (is('punc', ';')) next();
384
+ if (!is('eof')) unexpected('after the mapper body');
238
385
  }
@@ -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 "../core/brands.ts";
7
+ import { REF_PATH } from '../core/brands.ts';
8
8
 
9
9
  export class MapperError extends Error {
10
- override name = "MapperError";
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 === "then") return undefined; // `await ref` must not recurse
28
- if (prop === Symbol.toPrimitive || prop === "toString" || prop === "valueOf")
29
- return fail("coerces a value (template string / concatenation / arithmetic / comparison)");
30
- if (prop === "toJSON") return fail("serializes a reference (JSON.stringify)");
31
- if (typeof prop === "symbol") return fail(`uses ${String(prop)} (iteration/spread/inspection)`);
32
- if (prop.includes(".")) return fail(`accesses field '${prop}' (field names containing '.' cannot be mapped)`);
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("calls a function/method"),
36
- construct: () => fail("constructs"),
37
- set: () => fail("assigns"),
38
- deleteProperty: () => fail("deletes"),
39
- has: () => fail("uses the `in` operator"),
40
- ownKeys: () => fail("enumerates/spreads"),
41
- defineProperty: () => fail("defines a property"),
42
- getPrototypeOf: () => fail("inspects the prototype (instanceof)"),
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 === "symbol" || prop === "then") return undefined;
58
+ if (typeof prop === 'symbol' || prop === 'then') return undefined;
53
59
  if (!available.has(prop))
54
- throw new MapperError(`mapper references '${prop}', which is not available at this stage (available: ${[...available].sort().join(", ") || "none"})`);
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("mapper uses the `in` operator on its argument");
66
+ throw new MapperError('mapper uses the `in` operator on its argument');
59
67
  },
60
68
  ownKeys: () => {
61
- throw new MapperError("mapper enumerates/spreads its argument");
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 === "function" ? ((v as unknown as Record<symbol, unknown>)[REF_PATH] as string | undefined) : undefined;
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 === "function") throw new MapperError(`mapper returns a function at '${at}'`);
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 === "string" || typeof v === "boolean") return { const: v };
76
- if (typeof v === "number") {
77
- if (!Number.isFinite(v)) throw new MapperError(`mapper returns a non-finite number at '${at}'`);
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) => "const" in e && Object.keys(e).length === 1)) return { const: items.map((e) => (e as { const: unknown }).const) };
83
- throw new MapperError(`mapper puts references inside an array at '${at}' (only constant arrays are supported)`);
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 === "object") {
86
- if (Object.getPrototypeOf(v) !== Object.prototype && Object.getPrototypeOf(v) !== null)
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] === "ref" || keys[0] === "const"))
90
- throw new MapperError(`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`);
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) out[k] = toMapping((v as Record<string, unknown>)[k], at ? `${at}.${k}` : k);
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(fn: (from: unknown) => unknown, available: ReadonlySet<string>): Record<string, Mapping> {
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 (refPathOf(result) !== undefined || typeof result !== "object" || result === null || Array.isArray(result))
102
- throw new MapperError("mapper must return an object literal");
103
- if (result instanceof Promise) throw new MapperError("mapper must be synchronous");
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)) out[k] = toMapping((result as Record<string, unknown>)[k], k);
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
  }