@intentius/chant 0.20.0 → 0.22.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/build.d.ts +7 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
- 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 +64 -3
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/index.d.ts +12 -0
- 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 +58 -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/dist/serializer-walker.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/build.ts +9 -0
- package/src/cli/commands/build.ts +9 -0
- package/src/cli/commands/check-lexicon-examples.ts +16 -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 +900 -1
- package/src/discovery/fold-import.ts +441 -47
- package/src/discovery/index.ts +25 -1
- package/src/fold/fold.test.ts +277 -0
- package/src/fold/fold.ts +219 -58
- package/src/fold/foldable-helpers.ts +171 -0
- package/src/fold/subset-doc-parity.test.ts +27 -0
- package/src/fold/subset.test.ts +177 -0
- package/src/fold/subset.ts +139 -31
- 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/serializer-walker.ts +14 -0
package/src/discovery/index.ts
CHANGED
|
@@ -56,6 +56,19 @@ export interface DiscoveryOptions {
|
|
|
56
56
|
*/
|
|
57
57
|
intrinsics?: IntrinsicDef[];
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* chant #1063 — the lexicon NAMES loaded for this build (`["aws", "k8s"]`),
|
|
61
|
+
* i.e. what `resolveProjectLexicons()` returned and `loadPlugins()` then
|
|
62
|
+
* imported. Threaded into the fold session as the ALLOWLIST of packages a
|
|
63
|
+
* bare import specifier may be resolved into, so a lexicon's plain data
|
|
64
|
+
* exports (`Azure`/`GCP`'s pseudo-parameter namespaces, AWS's `S3Actions`,
|
|
65
|
+
* gitlab's `CI`) fold as identifier values instead of failing the file.
|
|
66
|
+
* Only meaningful when {@link fold} is set. Default: none — a caller that
|
|
67
|
+
* doesn't say which lexicons are active gets no bare-specifier resolution
|
|
68
|
+
* at all, rather than a looser fallback.
|
|
69
|
+
*/
|
|
70
|
+
lexicons?: readonly string[];
|
|
71
|
+
|
|
59
72
|
/**
|
|
60
73
|
* chant #1045 Phase 2 — opt-in: whatever would otherwise reach the
|
|
61
74
|
* in-process `importModule` step (every file, when {@link fold} isn't set;
|
|
@@ -156,7 +169,9 @@ export async function discover(path: string, options?: DiscoveryOptions): Promis
|
|
|
156
169
|
// a project file imported by several others is folded exactly once, so
|
|
157
170
|
// every referrer shares the identical constructed Declarable/
|
|
158
171
|
// CompositeInstance objects rather than each building its own copy.
|
|
159
|
-
const foldSession = options?.fold
|
|
172
|
+
const foldSession = options?.fold
|
|
173
|
+
? createFoldSession(options.intrinsics, buildParamValuesMap, options.lexicons)
|
|
174
|
+
: undefined;
|
|
160
175
|
if (options?.fold) {
|
|
161
176
|
for (const file of files) {
|
|
162
177
|
foldAttempts.set(file, await tryFoldFile(file, options.intrinsics, foldSession));
|
|
@@ -166,6 +181,15 @@ export async function discover(path: string, options?: DiscoveryOptions): Promis
|
|
|
166
181
|
? await planFoldTaint(
|
|
167
182
|
files,
|
|
168
183
|
new Map(files.map((file) => [file, foldAttempts.get(file)?.ok === true])),
|
|
184
|
+
// chant #1044 — which files' OBJECTS each successful fold captured,
|
|
185
|
+
// so a file forced back to run also invalidates the folds that
|
|
186
|
+
// already hold its instances (see planFoldTaint's doc).
|
|
187
|
+
new Map(
|
|
188
|
+
files.flatMap((file) => {
|
|
189
|
+
const attempt = foldAttempts.get(file);
|
|
190
|
+
return attempt?.ok === true ? [[file, attempt.liveSources] as const] : [];
|
|
191
|
+
}),
|
|
192
|
+
),
|
|
169
193
|
)
|
|
170
194
|
: new Set<string>();
|
|
171
195
|
|
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 = `
|