@jigging/jig 0.1.0-alpha.25 → 0.1.0-alpha.27

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.
@@ -21,6 +21,267 @@ class Json1Error extends Error {
21
21
  this.name = "Json1Error";
22
22
  }
23
23
  }
24
+
25
+ class Parser {
26
+ source;
27
+ position = 0;
28
+ nodes = 0;
29
+ constructor(source) {
30
+ this.source = source;
31
+ }
32
+ parse() {
33
+ this.skipWhitespace();
34
+ const value = this.parseValue(1);
35
+ this.skipWhitespace();
36
+ if (this.position !== this.source.length)
37
+ this.fail("unexpected trailing characters");
38
+ return value;
39
+ }
40
+ parseValue(depth) {
41
+ if (depth > JSON_1_LIMITS.depth)
42
+ this.fail("maximum value depth exceeded");
43
+ this.nodes += 1;
44
+ if (this.nodes > JSON_1_LIMITS.nodes)
45
+ this.fail("maximum value nodes exceeded");
46
+ const char = this.source[this.position];
47
+ if (char === '"')
48
+ return this.parseString(false);
49
+ if (char === "{")
50
+ return this.parseObject(depth);
51
+ if (char === "[")
52
+ return this.parseArray(depth);
53
+ if (char === "t")
54
+ return this.parseLiteral("true", true);
55
+ if (char === "f")
56
+ return this.parseLiteral("false", false);
57
+ if (char === "n")
58
+ return this.parseLiteral("null", null);
59
+ if (char === "-" || char !== undefined && char >= "0" && char <= "9") {
60
+ return this.parseNumber();
61
+ }
62
+ return this.fail("expected a JSON value");
63
+ }
64
+ parseObject(depth) {
65
+ this.position += 1;
66
+ const result = Object.create(null);
67
+ const keys = new Set;
68
+ this.skipWhitespace();
69
+ if (this.consume("}"))
70
+ return result;
71
+ let members = 0;
72
+ while (true) {
73
+ if (this.source[this.position] !== '"')
74
+ this.fail("expected an object member name");
75
+ const key = this.parseString(true);
76
+ if (keys.has(key))
77
+ this.fail(`duplicate object member ${key}`);
78
+ keys.add(key);
79
+ members += 1;
80
+ if (members > JSON_1_LIMITS.containerEntries) {
81
+ this.fail("maximum object members exceeded");
82
+ }
83
+ this.skipWhitespace();
84
+ if (!this.consume(":"))
85
+ this.fail("expected ':' after member name");
86
+ this.skipWhitespace();
87
+ result[key] = this.parseValue(depth + 1);
88
+ this.skipWhitespace();
89
+ if (this.consume("}"))
90
+ return result;
91
+ if (!this.consume(","))
92
+ this.fail("expected ',' or '}'");
93
+ this.skipWhitespace();
94
+ }
95
+ }
96
+ parseArray(depth) {
97
+ this.position += 1;
98
+ const result = [];
99
+ this.skipWhitespace();
100
+ if (this.consume("]"))
101
+ return result;
102
+ while (true) {
103
+ if (result.length >= JSON_1_LIMITS.containerEntries) {
104
+ this.fail("maximum array items exceeded");
105
+ }
106
+ result.push(this.parseValue(depth + 1));
107
+ this.skipWhitespace();
108
+ if (this.consume("]"))
109
+ return result;
110
+ if (!this.consume(","))
111
+ this.fail("expected ',' or ']'");
112
+ this.skipWhitespace();
113
+ }
114
+ }
115
+ parseString(memberName) {
116
+ this.position += 1;
117
+ const parts = [];
118
+ let segmentStart = this.position;
119
+ while (this.position < this.source.length) {
120
+ const char = this.source[this.position];
121
+ this.position += 1;
122
+ if (char === '"') {
123
+ parts.push(this.source.slice(segmentStart, this.position - 1));
124
+ const value = parts.join("");
125
+ const limit = memberName ? JSON_1_LIMITS.memberNameBytes : JSON_1_LIMITS.stringBytes;
126
+ if (encoder.encode(value).byteLength > limit)
127
+ this.fail("maximum string bytes exceeded");
128
+ return value;
129
+ }
130
+ if (char === "\\") {
131
+ parts.push(this.source.slice(segmentStart, this.position - 1));
132
+ parts.push(this.parseEscape());
133
+ segmentStart = this.position;
134
+ continue;
135
+ }
136
+ if (char === undefined || char.charCodeAt(0) <= 31) {
137
+ this.fail("invalid character in string");
138
+ }
139
+ const code = char.charCodeAt(0);
140
+ if (code >= 55296 && code <= 57343) {
141
+ const next = this.source[this.position];
142
+ if (code > 56319 || next === undefined || next.charCodeAt(0) < 56320 || next.charCodeAt(0) > 57343)
143
+ this.fail("lone Unicode surrogate");
144
+ this.position += 1;
145
+ }
146
+ }
147
+ return this.fail("unterminated string");
148
+ }
149
+ parseEscape() {
150
+ const escape = this.source[this.position];
151
+ this.position += 1;
152
+ switch (escape) {
153
+ case '"':
154
+ case "\\":
155
+ case "/":
156
+ return escape;
157
+ case "b":
158
+ return "\b";
159
+ case "f":
160
+ return "\f";
161
+ case "n":
162
+ return `
163
+ `;
164
+ case "r":
165
+ return "\r";
166
+ case "t":
167
+ return "\t";
168
+ case "u": {
169
+ const first = this.parseHexCodeUnit();
170
+ if (first >= 55296 && first <= 56319) {
171
+ if (this.source.slice(this.position, this.position + 2) !== "\\u") {
172
+ this.fail("high surrogate without low surrogate");
173
+ }
174
+ this.position += 2;
175
+ const second = this.parseHexCodeUnit();
176
+ if (second < 56320 || second > 57343) {
177
+ this.fail("high surrogate without low surrogate");
178
+ }
179
+ return String.fromCharCode(first, second);
180
+ }
181
+ if (first >= 56320 && first <= 57343)
182
+ this.fail("lone low surrogate");
183
+ return String.fromCharCode(first);
184
+ }
185
+ default:
186
+ return this.fail("invalid string escape");
187
+ }
188
+ }
189
+ parseHexCodeUnit() {
190
+ const token = this.source.slice(this.position, this.position + 4);
191
+ if (!/^[0-9A-Fa-f]{4}$/.test(token))
192
+ this.fail("invalid Unicode escape");
193
+ this.position += 4;
194
+ return Number.parseInt(token, 16);
195
+ }
196
+ parseNumber() {
197
+ const start = this.position;
198
+ this.consume("-");
199
+ if (this.consume("0")) {
200
+ const next = this.source[this.position];
201
+ if (next !== undefined && next >= "0" && next <= "9") {
202
+ this.fail("leading zero in number");
203
+ }
204
+ } else {
205
+ if (!this.consumeDigit("1", "9"))
206
+ this.fail("invalid number");
207
+ while (this.consumeDigit("0", "9")) {}
208
+ }
209
+ if (this.consume(".")) {
210
+ if (!this.consumeDigit("0", "9"))
211
+ this.fail("fraction requires a digit");
212
+ while (this.consumeDigit("0", "9")) {}
213
+ }
214
+ const exponent = this.source[this.position];
215
+ if (exponent === "e" || exponent === "E") {
216
+ this.position += 1;
217
+ const sign = this.source[this.position];
218
+ if (sign === "+" || sign === "-")
219
+ this.position += 1;
220
+ if (!this.consumeDigit("0", "9"))
221
+ this.fail("exponent requires a digit");
222
+ while (this.consumeDigit("0", "9")) {}
223
+ }
224
+ const token = this.source.slice(start, this.position);
225
+ if (token.length > JSON_1_LIMITS.numberTokenBytes) {
226
+ this.fail("maximum number token bytes exceeded");
227
+ }
228
+ const value = Number(token);
229
+ if (!Number.isFinite(value))
230
+ this.fail("non-finite number");
231
+ if (Number.isInteger(value) && !Number.isSafeInteger(value)) {
232
+ this.fail("integral number outside safe range");
233
+ }
234
+ return value;
235
+ }
236
+ parseLiteral(token, value) {
237
+ if (this.source.slice(this.position, this.position + token.length) !== token) {
238
+ this.fail(`invalid literal ${token}`);
239
+ }
240
+ this.position += token.length;
241
+ return value;
242
+ }
243
+ consume(expected) {
244
+ if (this.source[this.position] !== expected)
245
+ return false;
246
+ this.position += 1;
247
+ return true;
248
+ }
249
+ consumeDigit(minimum, maximum) {
250
+ const char = this.source[this.position];
251
+ if (char === undefined || char < minimum || char > maximum)
252
+ return false;
253
+ this.position += 1;
254
+ return true;
255
+ }
256
+ skipWhitespace() {
257
+ while (true) {
258
+ const char = this.source[this.position];
259
+ if (char !== " " && char !== `
260
+ ` && char !== "\r" && char !== "\t")
261
+ return;
262
+ this.position += 1;
263
+ }
264
+ }
265
+ fail(message) {
266
+ throw new Json1Error(`${message} at character ${this.position}`);
267
+ }
268
+ }
269
+ function decodeJson1(bytes) {
270
+ if (bytes.byteLength === 0)
271
+ throw new Json1Error("empty JSON/0 document");
272
+ if (bytes.byteLength > JSON_1_LIMITS.bytes)
273
+ throw new Json1Error("maximum bytes exceeded");
274
+ if (bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191) {
275
+ throw new Json1Error("UTF-8 BOM is not allowed");
276
+ }
277
+ let text;
278
+ try {
279
+ text = decoder.decode(bytes);
280
+ } catch {
281
+ throw new Json1Error("invalid UTF-8");
282
+ }
283
+ return new Parser(text).parse();
284
+ }
24
285
  function validateJson1(value) {
25
286
  visitJson(value, 1, { nodes: 0, encodedBytes: 0, active: new WeakSet }, false);
26
287
  }
@@ -127,11 +388,125 @@ function compareUtf16(left, right) {
127
388
  return left < right ? -1 : left > right ? 1 : 0;
128
389
  }
129
390
 
391
+ // src/internal/project-evaluator-child.ts
392
+ import { spawn } from "child_process";
393
+
394
+ // src/internal/project-evaluator-policy.ts
395
+ var PRIVATE_AUTHOR_EVALUATOR_PROTOCOL = "jig-author-evaluator/1";
396
+ var PRIVATE_AUTHOR_EVALUATOR_MAX_ENTRIES = 256;
397
+ var PRIVATE_AUTHOR_EVALUATOR_ENTRY_MS = 3000;
398
+ var PRIVATE_AUTHOR_EVALUATOR_MACOS_ENTRY_MS = 1e4;
399
+ var PRIVATE_AUTHOR_EVALUATOR_DIRECTORY = "/jig-input/evaluator";
400
+ var PRIVATE_AUTHOR_EVALUATOR_WORKER = `${PRIVATE_AUTHOR_EVALUATOR_DIRECTORY}/project-evaluator-worker.js`;
401
+ var PRIVATE_AUTHOR_EVALUATOR_LIMITS = Object.freeze({
402
+ memoryBytes: 256 * 1024 * 1024,
403
+ pids: 64,
404
+ cpuQuotaMicros: 50000,
405
+ cpuPeriodMicros: 1e5,
406
+ wallClockCeilingMs: PRIVATE_AUTHOR_EVALUATOR_ENTRY_MS,
407
+ cancellationGraceMs: 1000,
408
+ cleanupTimeoutMs: 5000
409
+ });
410
+
411
+ // src/internal/project-evaluator-child.ts
412
+ class PrivateAuthorEvaluatorChildError extends Error {
413
+ code;
414
+ constructor(code, message) {
415
+ super(message);
416
+ this.code = code;
417
+ this.name = "PrivateAuthorEvaluatorChildError";
418
+ }
419
+ }
420
+ async function runPrivateAuthorEvaluatorChild(request, workerPath = PRIVATE_AUTHOR_EVALUATOR_WORKER, sdkPath) {
421
+ const entryWallClockCeilingMs = process.platform === "darwin" ? PRIVATE_AUTHOR_EVALUATOR_MACOS_ENTRY_MS : PRIVATE_AUTHOR_EVALUATOR_ENTRY_MS;
422
+ const started = performance.now();
423
+ const child = spawn(process.execPath, ["--no-env-file", "--no-install", "--config=/dev/null", workerPath], {
424
+ stdio: ["pipe", "pipe", "pipe"],
425
+ env: sdkPath === undefined ? process.env : { ...process.env, JIG_EVALUATOR_SDK: sdkPath }
426
+ });
427
+ let timedOut = false;
428
+ let closed = false;
429
+ const exited = new Promise((resolve, reject) => {
430
+ child.once("error", reject);
431
+ child.once("close", (code, signal) => {
432
+ closed = true;
433
+ resolve({ code, signal });
434
+ });
435
+ });
436
+ const timer = setTimeout(() => {
437
+ timedOut = true;
438
+ child.kill("SIGKILL");
439
+ }, entryWallClockCeilingMs);
440
+ const stdout = collect(child.stdout, JSON_1_LIMITS.bytes);
441
+ const stderr = collect(child.stderr, 64 * 1024);
442
+ const input = new Promise((resolve, reject) => {
443
+ child.stdin.once("error", reject);
444
+ child.stdin.end(request, () => resolve());
445
+ });
446
+ try {
447
+ const [output, , exit] = await Promise.all([stdout, stderr, exited, input]);
448
+ if (timedOut || performance.now() - started >= entryWallClockCeilingMs) {
449
+ throw failure("PROJECT_EVALUATOR_DEADLINE", "declaration reached its hard wall deadline");
450
+ }
451
+ if (exit.code !== 0 || exit.signal !== null) {
452
+ throw failure("PROJECT_EVALUATION_FAILED", "declaration worker did not exit successfully");
453
+ }
454
+ return output;
455
+ } catch (error) {
456
+ if (timedOut) {
457
+ throw failure("PROJECT_EVALUATOR_DEADLINE", "declaration reached its hard wall deadline");
458
+ }
459
+ throw error;
460
+ } finally {
461
+ clearTimeout(timer);
462
+ if (!closed)
463
+ child.kill("SIGKILL");
464
+ await Promise.allSettled([stdout, stderr, exited, input]);
465
+ }
466
+ }
467
+ async function collect(stream, maximum) {
468
+ const chunks = [];
469
+ let size = 0;
470
+ for await (const chunk of stream) {
471
+ size += chunk.byteLength;
472
+ if (size > maximum)
473
+ throw failure("PROJECT_EVALUATION_LIMIT", "declaration output exceeds its byte bound");
474
+ chunks.push(chunk.slice());
475
+ }
476
+ const bytes = new Uint8Array(size);
477
+ let offset = 0;
478
+ for (const chunk of chunks) {
479
+ bytes.set(chunk, offset);
480
+ offset += chunk.byteLength;
481
+ }
482
+ return bytes;
483
+ }
484
+ function failure(code, message) {
485
+ return new PrivateAuthorEvaluatorChildError(code, message);
486
+ }
487
+
488
+ // src/internal/project-evaluator-modules.ts
489
+ function reachableAuthorModules(modules, entryProjectPath) {
490
+ const byPath = new Map(modules.map((module) => [module.projectPath, module]));
491
+ const reachable = new Set;
492
+ const pending = [entryProjectPath];
493
+ while (pending.length > 0) {
494
+ const path = pending.pop();
495
+ if (reachable.has(path))
496
+ continue;
497
+ reachable.add(path);
498
+ for (const edge of byPath.get(path).imports)
499
+ pending.push(edge.projectPath);
500
+ }
501
+ return modules.filter((module) => reachable.has(module.projectPath));
502
+ }
503
+
130
504
  // src/internal/project-evaluator-worker.ts
131
- var PROTOCOL = "jig-author-evaluator/1";
505
+ var PROTOCOL = PRIVATE_AUTHOR_EVALUATOR_PROTOCOL;
132
506
  var MAX_REQUEST_BYTES = 2 * 1024 * 1024;
133
507
  var VM_TIMEOUT_MS = 1000;
134
- var SDK_ENTRY = "/jig-evaluator/project-evaluator-sdk.bundle.js";
508
+ var SDK_ENTRY = process.env.JIG_EVALUATOR_SDK ?? `${PRIVATE_AUTHOR_EVALUATOR_DIRECTORY}/project-evaluator-sdk.bundle.js`;
509
+ delete process.env.JIG_EVALUATOR_SDK;
135
510
  var EVALUATION_CODES = new Set([
136
511
  "PROJECT_AUTHORING_VALUE",
137
512
  "PROJECT_DEFAULT_EXPORT",
@@ -142,15 +517,26 @@ var EVALUATION_CODES = new Set([
142
517
  "PROJECT_EVALUATOR_PROTOCOL"
143
518
  ]);
144
519
  var runtime = globalThis.Bun;
520
+ var entryIndex = null;
145
521
  try {
146
522
  const request = await readRequest();
147
- const code = await build(request);
148
- const value = evaluate(code);
149
- write({ protocol: PROTOCOL, status: "ok", value });
523
+ const values = [];
524
+ let resultBytes = 0;
525
+ for (let index = 0;index < request.entries.length; index += 1) {
526
+ entryIndex = index;
527
+ const value = request.entries.length === 1 ? evaluate(await build(request, request.entries[index])) : await evaluateEntry(request, index);
528
+ resultBytes += canonicalJson(value).byteLength + 1;
529
+ if (resultBytes > JSON_1_LIMITS.bytes - 128) {
530
+ throw tagged("PROJECT_EVALUATION_LIMIT", "declaration batch exceeds its aggregate output bound");
531
+ }
532
+ values.push(value);
533
+ }
534
+ write({ protocol: PROTOCOL, status: "ok", values });
150
535
  } catch (error) {
151
536
  write({
152
537
  protocol: PROTOCOL,
153
538
  status: "error",
539
+ entryIndex,
154
540
  code: errorCode(error),
155
541
  message: boundedMessage(error)
156
542
  });
@@ -166,7 +552,7 @@ async function readRequest() {
166
552
  } catch {
167
553
  throw tagged("PROJECT_EVALUATOR_PROTOCOL", "evaluator request is not valid JSON");
168
554
  }
169
- if (!isRecord(value) || value.protocol !== PROTOCOL || !isAuthoringProfile(value.authoringProfile) || typeof value.entryProjectPath !== "string" || !Array.isArray(value.modules) || Object.keys(value).length !== 4) {
555
+ if (!isRecord(value) || value.protocol !== PROTOCOL || !isAuthoringProfile(value.authoringProfile) || !Array.isArray(value.entries) || value.entries.length === 0 || value.entries.length > PRIVATE_AUTHOR_EVALUATOR_MAX_ENTRIES || !value.entries.every((entry) => typeof entry === "string") || new Set(value.entries).size !== value.entries.length || !Array.isArray(value.modules) || Object.keys(value).length !== 4) {
170
556
  throw tagged("PROJECT_EVALUATOR_PROTOCOL", "evaluator request has an invalid shape");
171
557
  }
172
558
  const paths = new Set;
@@ -195,7 +581,7 @@ async function readRequest() {
195
581
  imports
196
582
  });
197
583
  }
198
- if (!paths.has(value.entryProjectPath)) {
584
+ if (!value.entries.every((entry) => paths.has(entry))) {
199
585
  throw tagged("PROJECT_EVALUATOR_PROTOCOL", "evaluator entry is absent from its module closure");
200
586
  }
201
587
  for (const module of modules) {
@@ -208,11 +594,34 @@ async function readRequest() {
208
594
  return {
209
595
  protocol: PROTOCOL,
210
596
  authoringProfile: value.authoringProfile,
211
- entryProjectPath: value.entryProjectPath,
597
+ entries: value.entries,
212
598
  modules
213
599
  };
214
600
  }
215
- async function build(request) {
601
+ async function evaluateEntry(request, index) {
602
+ const entryProjectPath = request.entries[index];
603
+ const output = await runPrivateAuthorEvaluatorChild(canonicalJson({
604
+ ...request,
605
+ entries: [entryProjectPath],
606
+ modules: reachableAuthorModules(request.modules, entryProjectPath)
607
+ }), process.argv[1], SDK_ENTRY);
608
+ const response = decodeJson1(output);
609
+ if (!isRecord(response) || response.protocol !== PROTOCOL) {
610
+ throw tagged("PROJECT_EVALUATOR_PROTOCOL", "declaration worker returned an invalid envelope");
611
+ }
612
+ if (response.status === "error" && exactKeys(response, ["code", "entryIndex", "message", "protocol", "status"]) && response.entryIndex === 0 && typeof response.code === "string" && EVALUATION_CODES.has(response.code) && typeof response.message === "string") {
613
+ throw tagged(response.code, response.message);
614
+ }
615
+ if (response.status !== "ok" || !exactKeys(response, ["protocol", "status", "values"]) || !Array.isArray(response.values) || response.values.length !== 1) {
616
+ throw tagged("PROJECT_EVALUATOR_PROTOCOL", "declaration worker returned an invalid result");
617
+ }
618
+ return response.values[0];
619
+ }
620
+ function exactKeys(value, expected) {
621
+ const keys = Object.keys(value).sort();
622
+ return keys.length === expected.length && keys.every((key, index) => key === expected[index]);
623
+ }
624
+ async function build(request, entryProjectPath) {
216
625
  const sdkSource = await runtime.file(SDK_ENTRY).text();
217
626
  const modules = new Map(request.modules.map((module) => [module.projectPath, module]));
218
627
  const edges = new Map;
@@ -254,7 +663,7 @@ async function build(request) {
254
663
  if (arguments_.kind !== "import-statement" || arguments_.importer !== "jig-author:bridge") {
255
664
  throw deniedImport(arguments_.path);
256
665
  }
257
- return { path: request.entryProjectPath, namespace: "jig-project" };
666
+ return { path: entryProjectPath, namespace: "jig-project" };
258
667
  });
259
668
  builder.onResolve({ filter: /^jig-author:/ }, (arguments_) => {
260
669
  throw deniedImport(arguments_.path);
@@ -405,6 +814,8 @@ function tagged(code, message) {
405
814
  return Object.assign(new Error(message), { code });
406
815
  }
407
816
  function errorCode(error) {
817
+ if (error instanceof PrivateAuthorEvaluatorChildError)
818
+ return error.code;
408
819
  if (typeof error === "object" && error !== null && "code" in error && error.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") {
409
820
  return "PROJECT_EVALUATION_LIMIT";
410
821
  }
@@ -1459,7 +1459,9 @@ async function requestGrantedHttp(value) {
1459
1459
  finish({ failure: "UNCERTAIN" });
1460
1460
  });
1461
1461
  });
1462
- request.on("error", () => finish({ failure: "UNCERTAIN" }));
1462
+ request.on("error", (error) => finish({
1463
+ failure: error.code === "HPE_HEADER_OVERFLOW" ? "RESOURCE_EXHAUSTED" : "UNCERTAIN"
1464
+ }));
1463
1465
  request.on("upgrade", (_response, socket) => {
1464
1466
  socket.destroy();
1465
1467
  finish({ failure: "INVALID_RESULT" });