@jigging/jig 0.1.0-alpha.26 → 0.1.0-alpha.29

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,123 @@ 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
- var VM_TIMEOUT_MS = 1000;
134
- var SDK_ENTRY = process.env.JIG_EVALUATOR_SDK ?? "/jig-evaluator/project-evaluator-sdk.bundle.js";
507
+ var SDK_ENTRY = process.env.JIG_EVALUATOR_SDK ?? `${PRIVATE_AUTHOR_EVALUATOR_DIRECTORY}/project-evaluator-sdk.bundle.js`;
135
508
  delete process.env.JIG_EVALUATOR_SDK;
136
509
  var EVALUATION_CODES = new Set([
137
510
  "PROJECT_AUTHORING_VALUE",
@@ -143,15 +516,26 @@ var EVALUATION_CODES = new Set([
143
516
  "PROJECT_EVALUATOR_PROTOCOL"
144
517
  ]);
145
518
  var runtime = globalThis.Bun;
519
+ var entryIndex = null;
146
520
  try {
147
521
  const request = await readRequest();
148
- const code = await build(request);
149
- const value = evaluate(code);
150
- write({ protocol: PROTOCOL, status: "ok", value });
522
+ const values = [];
523
+ let resultBytes = 0;
524
+ for (let index = 0;index < request.entries.length; index += 1) {
525
+ entryIndex = index;
526
+ const value = request.entries.length === 1 ? evaluate(await build(request, request.entries[index])) : await evaluateEntry(request, index);
527
+ resultBytes += canonicalJson(value).byteLength + 1;
528
+ if (resultBytes > JSON_1_LIMITS.bytes - 128) {
529
+ throw tagged("PROJECT_EVALUATION_LIMIT", "declaration batch exceeds its aggregate output bound");
530
+ }
531
+ values.push(value);
532
+ }
533
+ write({ protocol: PROTOCOL, status: "ok", values });
151
534
  } catch (error) {
152
535
  write({
153
536
  protocol: PROTOCOL,
154
537
  status: "error",
538
+ entryIndex,
155
539
  code: errorCode(error),
156
540
  message: boundedMessage(error)
157
541
  });
@@ -167,7 +551,7 @@ async function readRequest() {
167
551
  } catch {
168
552
  throw tagged("PROJECT_EVALUATOR_PROTOCOL", "evaluator request is not valid JSON");
169
553
  }
170
- if (!isRecord(value) || value.protocol !== PROTOCOL || !isAuthoringProfile(value.authoringProfile) || typeof value.entryProjectPath !== "string" || !Array.isArray(value.modules) || Object.keys(value).length !== 4) {
554
+ 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) {
171
555
  throw tagged("PROJECT_EVALUATOR_PROTOCOL", "evaluator request has an invalid shape");
172
556
  }
173
557
  const paths = new Set;
@@ -196,7 +580,7 @@ async function readRequest() {
196
580
  imports
197
581
  });
198
582
  }
199
- if (!paths.has(value.entryProjectPath)) {
583
+ if (!value.entries.every((entry) => paths.has(entry))) {
200
584
  throw tagged("PROJECT_EVALUATOR_PROTOCOL", "evaluator entry is absent from its module closure");
201
585
  }
202
586
  for (const module of modules) {
@@ -209,11 +593,34 @@ async function readRequest() {
209
593
  return {
210
594
  protocol: PROTOCOL,
211
595
  authoringProfile: value.authoringProfile,
212
- entryProjectPath: value.entryProjectPath,
596
+ entries: value.entries,
213
597
  modules
214
598
  };
215
599
  }
216
- async function build(request) {
600
+ async function evaluateEntry(request, index) {
601
+ const entryProjectPath = request.entries[index];
602
+ const output = await runPrivateAuthorEvaluatorChild(canonicalJson({
603
+ ...request,
604
+ entries: [entryProjectPath],
605
+ modules: reachableAuthorModules(request.modules, entryProjectPath)
606
+ }), process.argv[1], SDK_ENTRY);
607
+ const response = decodeJson1(output);
608
+ if (!isRecord(response) || response.protocol !== PROTOCOL) {
609
+ throw tagged("PROJECT_EVALUATOR_PROTOCOL", "declaration worker returned an invalid envelope");
610
+ }
611
+ 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") {
612
+ throw tagged(response.code, response.message);
613
+ }
614
+ if (response.status !== "ok" || !exactKeys(response, ["protocol", "status", "values"]) || !Array.isArray(response.values) || response.values.length !== 1) {
615
+ throw tagged("PROJECT_EVALUATOR_PROTOCOL", "declaration worker returned an invalid result");
616
+ }
617
+ return response.values[0];
618
+ }
619
+ function exactKeys(value, expected) {
620
+ const keys = Object.keys(value).sort();
621
+ return keys.length === expected.length && keys.every((key, index) => key === expected[index]);
622
+ }
623
+ async function build(request, entryProjectPath) {
217
624
  const sdkSource = await runtime.file(SDK_ENTRY).text();
218
625
  const modules = new Map(request.modules.map((module) => [module.projectPath, module]));
219
626
  const edges = new Map;
@@ -255,7 +662,7 @@ async function build(request) {
255
662
  if (arguments_.kind !== "import-statement" || arguments_.importer !== "jig-author:bridge") {
256
663
  throw deniedImport(arguments_.path);
257
664
  }
258
- return { path: request.entryProjectPath, namespace: "jig-project" };
665
+ return { path: entryProjectPath, namespace: "jig-project" };
259
666
  });
260
667
  builder.onResolve({ filter: /^jig-author:/ }, (arguments_) => {
261
668
  throw deniedImport(arguments_.path);
@@ -324,17 +731,11 @@ function evaluate(code) {
324
731
  codeGeneration: { strings: false, wasm: false },
325
732
  name: "jig-author-declaration"
326
733
  });
327
- new vm.Script(guestBootstrap(), { filename: "/jig-bootstrap.js" }).runInContext(context, {
328
- timeout: VM_TIMEOUT_MS
329
- });
734
+ new vm.Script(guestBootstrap(), { filename: "/jig-bootstrap.js" }).runInContext(context);
330
735
  const invocation = `${code}
331
736
  (__jigModule.exports, __jigRequire, __jigModule, "/author.ts", "/");`;
332
- new vm.Script(invocation, { filename: "/author.ts" }).runInContext(context, {
333
- timeout: VM_TIMEOUT_MS
334
- });
335
- const exports = new vm.Script("__jigModule.exports").runInContext(context, {
336
- timeout: VM_TIMEOUT_MS
337
- });
737
+ new vm.Script(invocation, { filename: "/author.ts" }).runInContext(context);
738
+ const exports = new vm.Script("__jigModule.exports").runInContext(context);
338
739
  if (!isRecord(exports) || Object.keys(exports).length !== 1 || !("default" in exports)) {
339
740
  throw tagged("PROJECT_DEFAULT_EXPORT", "compiled author module did not expose one default value");
340
741
  }
@@ -406,9 +807,8 @@ function tagged(code, message) {
406
807
  return Object.assign(new Error(message), { code });
407
808
  }
408
809
  function errorCode(error) {
409
- if (typeof error === "object" && error !== null && "code" in error && error.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") {
410
- return "PROJECT_EVALUATION_LIMIT";
411
- }
810
+ if (error instanceof PrivateAuthorEvaluatorChildError)
811
+ return error.code;
412
812
  if (typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" && EVALUATION_CODES.has(error.code)) {
413
813
  return error.code;
414
814
  }