@intentius/chant 0.20.0 → 0.21.0
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/dist/cli/commands/check-lexicon-intrinsics.d.ts +17 -0
- package/dist/cli/commands/check-lexicon-intrinsics.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/codegen/docs-types.d.ts +2 -0
- package/dist/codegen/docs-types.d.ts.map +1 -1
- package/dist/declarable.d.ts +16 -0
- package/dist/declarable.d.ts.map +1 -1
- package/dist/discovery/entity-wire-codec.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +30 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +89 -16
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/foldable-helpers.d.ts +121 -0
- package/dist/fold/foldable-helpers.d.ts.map +1 -0
- package/dist/fold/subset.d.ts +35 -3
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +2 -0
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +73 -23
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/runtime.d.ts +10 -1
- package/dist/runtime.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/check-lexicon-intrinsics.test.ts +35 -1
- package/src/cli/commands/check-lexicon-intrinsics.ts +38 -2
- package/src/cli/commands/check-lexicon.ts +18 -0
- package/src/codegen/docs-sections.test.ts +7 -1
- package/src/codegen/docs-sections.ts +1 -1
- package/src/codegen/docs-types.ts +2 -0
- package/src/declarable.ts +20 -0
- package/src/discovery/entity-wire-codec.ts +9 -7
- package/src/discovery/fold-import.test.ts +572 -0
- package/src/discovery/fold-import.ts +229 -36
- package/src/discovery/index.ts +9 -0
- package/src/fold/fold.test.ts +277 -0
- package/src/fold/fold.ts +213 -56
- package/src/fold/foldable-helpers.ts +171 -0
- package/src/fold/subset-doc-parity.test.ts +27 -0
- package/src/fold/subset.test.ts +111 -0
- package/src/fold/subset.ts +109 -28
- package/src/lexicon-schema.test.ts +43 -0
- package/src/lexicon-schema.ts +5 -0
- package/src/lexicon.ts +74 -24
- package/src/runtime.ts +11 -2
package/src/fold/fold.test.ts
CHANGED
|
@@ -283,6 +283,237 @@ describe("fold — intrinsic tagged templates", () => {
|
|
|
283
283
|
});
|
|
284
284
|
});
|
|
285
285
|
|
|
286
|
+
/**
|
|
287
|
+
* chant #1044 — registered lexicon intrinsics in PLAIN-CALL form.
|
|
288
|
+
*
|
|
289
|
+
* The registry is the whole door: a name folds as a call only when an active
|
|
290
|
+
* lexicon registered it AND set `foldsAsCall`. Everything below that pairs a
|
|
291
|
+
* folding case with the near-miss that must keep rejecting.
|
|
292
|
+
*/
|
|
293
|
+
describe("fold — registered call-form intrinsics (#1044)", () => {
|
|
294
|
+
/** Shaped like aws's real registration (lexicons/aws/src/plugin.ts). */
|
|
295
|
+
const REF: IntrinsicDef = { name: "Ref", isTag: false, foldsAsCall: true };
|
|
296
|
+
const JOIN: IntrinsicDef = { name: "Join", isTag: false, foldsAsCall: true };
|
|
297
|
+
/** Registered, but its lexicon never opted the call form in — the default. */
|
|
298
|
+
const NOT_OPTED_IN: IntrinsicDef = { name: "Reference", isTag: false };
|
|
299
|
+
const SUB: IntrinsicDef = { name: "Sub", isTag: true };
|
|
300
|
+
|
|
301
|
+
test("an opted-in call folds to the intrinsic node form, arguments folded in order", () => {
|
|
302
|
+
const consts = parseConsts(`const env = "prod"; const x = Ref(env);`);
|
|
303
|
+
const expr = consts.get("x");
|
|
304
|
+
if (!expr) throw new Error("fixture error");
|
|
305
|
+
expect(fold(expr, consts, [REF])).toEqual({ __intrinsic: "Ref", args: ["prod"] });
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("the envelope is the same family the tagged-template form produces — same __intrinsic key, positional args", () => {
|
|
309
|
+
const consts = parseConsts(`const x = Join("-", ["a", "b"]);`);
|
|
310
|
+
const expr = consts.get("x");
|
|
311
|
+
if (!expr) throw new Error("fixture error");
|
|
312
|
+
expect(fold(expr, consts, [JOIN])).toEqual({ __intrinsic: "Join", args: ["-", ["a", "b"]] });
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("calls nest, and a call-form intrinsic folds inside a registered tag's interpolation", () => {
|
|
316
|
+
const consts = parseConsts(`const env = "prod"; const x = Sub\`\${Ref(env)}-fn\`;`);
|
|
317
|
+
const expr = consts.get("x");
|
|
318
|
+
if (!expr) throw new Error("fixture error");
|
|
319
|
+
expect(fold(expr, consts, [SUB, REF])).toEqual({
|
|
320
|
+
__intrinsic: "Sub",
|
|
321
|
+
strings: ["", "-fn"],
|
|
322
|
+
values: [{ __intrinsic: "Ref", args: ["prod"] }],
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("an argument nothing can resolve stays symbolic, exactly as inside a tag", () => {
|
|
327
|
+
const consts = parseConsts(`const x = Ref(AWS.StackName);`);
|
|
328
|
+
const expr = consts.get("x");
|
|
329
|
+
if (!expr) throw new Error("fixture error");
|
|
330
|
+
expect(fold(expr, consts, [REF])).toEqual({
|
|
331
|
+
__intrinsic: "Ref",
|
|
332
|
+
args: [{ __symbol: "AWS.StackName" }],
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("an already-resolved cross-file binding wins over the symbolic path — identity must survive", () => {
|
|
337
|
+
const consts = parseConsts(`const x = Ref(environment);`);
|
|
338
|
+
const expr = consts.get("x");
|
|
339
|
+
if (!expr) throw new Error("fixture error");
|
|
340
|
+
const live = { marker: "the real, shared Parameter instance" };
|
|
341
|
+
const folded = fold(expr, consts, [REF], new Map([["environment", live]])) as {
|
|
342
|
+
__intrinsic: string;
|
|
343
|
+
args: unknown[];
|
|
344
|
+
};
|
|
345
|
+
expect(folded.args[0]).toBe(live);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("a REGISTERED intrinsic that was never opted in is still rejected — the opt-in is the door, not registration", () => {
|
|
349
|
+
const consts = parseConsts(`const x = Reference("db");`);
|
|
350
|
+
const expr = consts.get("x");
|
|
351
|
+
if (!expr) throw new Error("fixture error");
|
|
352
|
+
let error: unknown;
|
|
353
|
+
try {
|
|
354
|
+
fold(expr, consts, [NOT_OPTED_IN]);
|
|
355
|
+
} catch (e) {
|
|
356
|
+
error = e;
|
|
357
|
+
}
|
|
358
|
+
expect(error).toBeInstanceOf(FoldError);
|
|
359
|
+
expect((error as FoldError).message).toContain("function call as a value is not foldable: Reference(...)");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("an opted-in name is rejected when its lexicon isn't among the active ones", () => {
|
|
363
|
+
const consts = parseConsts(`const x = Ref("db");`);
|
|
364
|
+
const expr = consts.get("x");
|
|
365
|
+
if (!expr) throw new Error("fixture error");
|
|
366
|
+
expect(() => fold(expr, consts)).toThrow(FoldError);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("a TAGGED-TEMPLATE intrinsic called as a plain function is rejected — the two forms are disjoint", () => {
|
|
370
|
+
const consts = parseConsts(`const x = Sub("literal");`);
|
|
371
|
+
const expr = consts.get("x");
|
|
372
|
+
if (!expr) throw new Error("fixture error");
|
|
373
|
+
expect(() => fold(expr, consts, [SUB])).toThrow(FoldError);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("a call-form intrinsic used as a TAG is rejected too — opting the call in doesn't register a tag", () => {
|
|
377
|
+
const consts = parseConsts(`const x = Ref\`literal\`;`);
|
|
378
|
+
const expr = consts.get("x");
|
|
379
|
+
if (!expr) throw new Error("fixture error");
|
|
380
|
+
let error: unknown;
|
|
381
|
+
try {
|
|
382
|
+
fold(expr, consts, [REF]);
|
|
383
|
+
} catch (e) {
|
|
384
|
+
error = e;
|
|
385
|
+
}
|
|
386
|
+
expect(error).toBeInstanceOf(FoldError);
|
|
387
|
+
expect((error as FoldError).message).toContain("unregistered tagged template intrinsic: Ref");
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test("a registered name reached as a METHOD (`ns.Ref(...)`) is rejected — only a bare identifier callee folds", () => {
|
|
391
|
+
const consts = parseConsts(`const x = aws.Ref("db");`);
|
|
392
|
+
const expr = consts.get("x");
|
|
393
|
+
if (!expr) throw new Error("fixture error");
|
|
394
|
+
expect(() => fold(expr, consts, [REF])).toThrow(FoldError);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("an array method taking a closure is rejected — .map is arbitrary JS, not a registered intrinsic", () => {
|
|
398
|
+
const consts = parseConsts(`const cidrs = ["10.0.0.0/24"]; const x = cidrs.map((c) => Ref(c));`);
|
|
399
|
+
const expr = consts.get("x");
|
|
400
|
+
if (!expr) throw new Error("fixture error");
|
|
401
|
+
let error: unknown;
|
|
402
|
+
try {
|
|
403
|
+
fold(expr, consts, [REF]);
|
|
404
|
+
} catch (e) {
|
|
405
|
+
error = e;
|
|
406
|
+
}
|
|
407
|
+
expect(error).toBeInstanceOf(FoldError);
|
|
408
|
+
expect((error as FoldError).message).toContain("cidrs.map(...)");
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test("a user-defined function is rejected however it's named", () => {
|
|
412
|
+
const consts = parseConsts(`const x = makeName("a", "b");`);
|
|
413
|
+
const expr = consts.get("x");
|
|
414
|
+
if (!expr) throw new Error("fixture error");
|
|
415
|
+
expect(() => fold(expr, consts, [REF])).toThrow(FoldError);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test("a registered name SHADOWED by a local const is rejected — the local binding wins, and it isn't the lexicon's", () => {
|
|
419
|
+
const consts = parseConsts(`const Ref = (n) => ({ mine: n }); const x = Ref("db");`);
|
|
420
|
+
const expr = consts.get("x");
|
|
421
|
+
if (!expr) throw new Error("fixture error");
|
|
422
|
+
expect(() => fold(expr, consts, [REF])).toThrow(FoldError);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test("an unfoldable argument still rejects the whole call", () => {
|
|
426
|
+
const consts = parseConsts(`const x = Ref(getName());`);
|
|
427
|
+
const expr = consts.get("x");
|
|
428
|
+
if (!expr) throw new Error("fixture error");
|
|
429
|
+
let error: unknown;
|
|
430
|
+
try {
|
|
431
|
+
fold(expr, consts, [REF]);
|
|
432
|
+
} catch (e) {
|
|
433
|
+
error = e;
|
|
434
|
+
}
|
|
435
|
+
expect(error).toBeInstanceOf(FoldError);
|
|
436
|
+
expect((error as FoldError).message).toContain("getName(...)");
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test("a same-file resource passed to an intrinsic call rejects rather than folding to a look-alike envelope", () => {
|
|
440
|
+
const consts = parseConsts(`const bucket = new S3Bucket({ name: "b" }); const x = Ref(bucket);`);
|
|
441
|
+
const expr = consts.get("x");
|
|
442
|
+
if (!expr) throw new Error("fixture error");
|
|
443
|
+
expect(() => fold(expr, consts, [REF])).toThrow(FoldError);
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
describe("fold — registered authoring helpers (#1082)", () => {
|
|
448
|
+
test("a registered helper call folds to the helper-call node form, arguments folded in order", () => {
|
|
449
|
+
const consts = parseConsts(`const stack = "web"; const x = phase("Apply", [{ kind: "cfn-deploy", stack }]);`);
|
|
450
|
+
const expr = consts.get("x");
|
|
451
|
+
if (!expr) throw new Error("fixture error");
|
|
452
|
+
expect(fold(expr, consts)).toEqual({
|
|
453
|
+
__helper: "phase",
|
|
454
|
+
args: ["Apply", [{ kind: "cfn-deploy", stack: "web" }]],
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("helper calls nest, so a fan-out phase folds to nested envelopes", () => {
|
|
459
|
+
const consts = parseConsts(`const x = phase("Outer", [phase("Inner", []), gate("approve")]);`);
|
|
460
|
+
const expr = consts.get("x");
|
|
461
|
+
if (!expr) throw new Error("fixture error");
|
|
462
|
+
expect(fold(expr, consts)).toEqual({
|
|
463
|
+
__helper: "phase",
|
|
464
|
+
args: [
|
|
465
|
+
"Outer",
|
|
466
|
+
[
|
|
467
|
+
{ __helper: "phase", args: ["Inner", []] },
|
|
468
|
+
{ __helper: "gate", args: ["approve"] },
|
|
469
|
+
],
|
|
470
|
+
],
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test("an unfoldable argument still rejects the whole call", () => {
|
|
475
|
+
const consts = parseConsts(`const x = phase("Apply", [{ stack: getName() }]);`);
|
|
476
|
+
const expr = consts.get("x");
|
|
477
|
+
if (!expr) throw new Error("fixture error");
|
|
478
|
+
expect(() => fold(expr, consts)).toThrow(FoldError);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test("an UNREGISTERED name is still rejected — the allowlist is closed, not a general call case", () => {
|
|
482
|
+
const consts = parseConsts(`const x = compose("Apply", []);`);
|
|
483
|
+
const expr = consts.get("x");
|
|
484
|
+
if (!expr) throw new Error("fixture error");
|
|
485
|
+
let error: unknown;
|
|
486
|
+
try {
|
|
487
|
+
fold(expr, consts);
|
|
488
|
+
} catch (e) {
|
|
489
|
+
error = e;
|
|
490
|
+
}
|
|
491
|
+
expect(error).toBeInstanceOf(FoldError);
|
|
492
|
+
expect((error as FoldError).message).toContain("function call as a value is not foldable: compose(...)");
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("a registered name SHADOWED by a local const is rejected — the local binding wins, and it isn't chant's", () => {
|
|
496
|
+
const consts = parseConsts(`const phase = (n) => ({ phase: n }); const x = phase("Apply");`);
|
|
497
|
+
const expr = consts.get("x");
|
|
498
|
+
if (!expr) throw new Error("fixture error");
|
|
499
|
+
expect(() => fold(expr, consts)).toThrow(FoldError);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test("a registered name used as a METHOD (`ns.phase(...)`) is rejected — only a bare identifier callee folds", () => {
|
|
503
|
+
const consts = parseConsts(`const x = helpers.phase("Apply", []);`);
|
|
504
|
+
const expr = consts.get("x");
|
|
505
|
+
if (!expr) throw new Error("fixture error");
|
|
506
|
+
expect(() => fold(expr, consts)).toThrow(FoldError);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test("a spread argument is rejected — fold has no case for one, registered callee or not", () => {
|
|
510
|
+
const consts = parseConsts(`const steps = []; const x = phase(...steps);`);
|
|
511
|
+
const expr = consts.get("x");
|
|
512
|
+
if (!expr) throw new Error("fixture error");
|
|
513
|
+
expect(() => fold(expr, consts)).toThrow(FoldError);
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
|
|
286
517
|
describe("fold — nullish coalescing, ternary, arithmetic", () => {
|
|
287
518
|
test("?? falls through only on null/undefined", () => {
|
|
288
519
|
expect(foldConst(`const x = null ?? "fallback";`, "x")).toBe("fallback");
|
|
@@ -428,6 +659,52 @@ describe("fold — rejections", () => {
|
|
|
428
659
|
});
|
|
429
660
|
});
|
|
430
661
|
|
|
662
|
+
describe("foldResource — constructor argument positions (#1082)", () => {
|
|
663
|
+
test("a props object that isn't the FIRST argument still folds, reported as a positional arg list", () => {
|
|
664
|
+
// AWS's deploy-time Parameter is `(type, props)` — lexicons/aws/src/parameter.ts.
|
|
665
|
+
const src = `export const p = new Parameter("String", { description: "vpc id", defaultValue: "" });`;
|
|
666
|
+
const result = foldModule(src);
|
|
667
|
+
expect(result.p).toEqual({
|
|
668
|
+
ok: true,
|
|
669
|
+
spec: {
|
|
670
|
+
__resource: "Parameter",
|
|
671
|
+
props: { description: "vpc id", defaultValue: "" },
|
|
672
|
+
args: ["String", { description: "vpc id", defaultValue: "" }],
|
|
673
|
+
},
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
test("a constructor with no object-literal argument at all folds to its bare argument list", () => {
|
|
678
|
+
const src = `export const p = new Parameter("String");`;
|
|
679
|
+
const result = foldModule(src);
|
|
680
|
+
expect(result.p).toEqual({ ok: true, spec: { __resource: "Parameter", props: {}, args: ["String"] } });
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
test("the classic (props) shape is unchanged — no `args` list", () => {
|
|
684
|
+
const src = `export const b = new S3Bucket({ name: "b" });`;
|
|
685
|
+
const result = foldModule(src);
|
|
686
|
+
expect(result.b).toEqual({ ok: true, spec: { __resource: "S3Bucket", props: { name: "b" } } });
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
test("the classic (props, attributes) shape is unchanged — no `args` list", () => {
|
|
690
|
+
const src = `export const b = new S3Bucket({ name: "b" }, { DependsOn: "other" });`;
|
|
691
|
+
const result = foldModule(src);
|
|
692
|
+
expect(result.b).toEqual({
|
|
693
|
+
ok: true,
|
|
694
|
+
spec: { __resource: "S3Bucket", props: { name: "b" }, attributes: { DependsOn: "other" } },
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
test("a non-object-literal argument is folded, not rejected for its position — but must still be foldable", () => {
|
|
699
|
+
const src = `export const p = new Parameter(getType(), { description: "x" });`;
|
|
700
|
+
const result = foldModule(src);
|
|
701
|
+
expect(result.p?.ok).toBe(false);
|
|
702
|
+
if (result.p && !result.p.ok) {
|
|
703
|
+
expect(result.p.error).toContain("getType(...)");
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
});
|
|
707
|
+
|
|
431
708
|
describe("foldModule", () => {
|
|
432
709
|
test("folds an exported const resource to { ok: true, spec }", () => {
|
|
433
710
|
const src = `
|