@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as ts from "typescript";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { existsSync, statSync, readFileSync, realpathSync } from "node:fs";
|
|
4
|
-
import { dirname, basename, join, isAbsolute, resolve as resolvePath } from "node:path";
|
|
4
|
+
import { dirname, basename, join, sep, isAbsolute, resolve as resolvePath } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { isDeclarable, type Declarable } from "../declarable";
|
|
@@ -16,8 +16,10 @@ import {
|
|
|
16
16
|
type FoldedResource,
|
|
17
17
|
type FoldedValue,
|
|
18
18
|
type FoldedIntrinsic,
|
|
19
|
+
type FoldedHelperCall,
|
|
19
20
|
type SymbolicValue,
|
|
20
21
|
} from "../fold/fold";
|
|
22
|
+
import { isChantOwnedSpecifier } from "../fold/foldable-helpers";
|
|
21
23
|
import { importModule } from "./import";
|
|
22
24
|
import type { IntrinsicDef } from "../lexicon";
|
|
23
25
|
import type { BuildParamValue } from "../build-params";
|
|
@@ -82,6 +84,20 @@ export type FoldFileResult =
|
|
|
82
84
|
* exact same object every referencing file sees.
|
|
83
85
|
*/
|
|
84
86
|
exportedValues: Map<string, unknown>;
|
|
87
|
+
/**
|
|
88
|
+
* chant #1044 — the OTHER project files whose exported OBJECTS this
|
|
89
|
+
* fold consumed (a cross-file `Declarable`, composite instance, or any
|
|
90
|
+
* other non-primitive reached through `buildExternals`/a re-export).
|
|
91
|
+
*
|
|
92
|
+
* Object identity is the thing that cannot survive one side of the
|
|
93
|
+
* build folding while the other runs, so `planFoldTaint` needs to know
|
|
94
|
+
* who consumed whose objects: if a file here is forced back to run, the
|
|
95
|
+
* instance THIS file already captured is not the instance discovery
|
|
96
|
+
* will collect, and serialization fails on an entity with no logical
|
|
97
|
+
* name. A primitive (string, number, boolean, null) is never recorded —
|
|
98
|
+
* it has no identity to disagree about.
|
|
99
|
+
*/
|
|
100
|
+
liveSources: ReadonlySet<string>;
|
|
85
101
|
}
|
|
86
102
|
| { ok: false; reason: string };
|
|
87
103
|
|
|
@@ -1114,13 +1130,23 @@ async function resolveSymbolicValue(text: string, ctx: ResolveCtx): Promise<unkn
|
|
|
1114
1130
|
}
|
|
1115
1131
|
|
|
1116
1132
|
/**
|
|
1117
|
-
* Revive a folded value tree: replace any
|
|
1118
|
-
* envelope with the real value it
|
|
1119
|
-
*
|
|
1120
|
-
*
|
|
1121
|
-
*
|
|
1133
|
+
* Revive a folded value tree: replace any
|
|
1134
|
+
* `{__intrinsic}`/`{__helper}`/`{__symbol}` envelope with the real value it
|
|
1135
|
+
* represents.
|
|
1136
|
+
*
|
|
1137
|
+
* `requireLiveRefs` tracks whether the CURRENT node is (transitively) an
|
|
1138
|
+
* argument being handed to a real function that will inspect it — a
|
|
1139
|
+
* `{__intrinsic}`'s own interpolated `values`, or a `{__helper}` call's
|
|
1140
|
+
* arguments (chant #1082). In that position a symbolic `{__attrRef}` envelope
|
|
1141
|
+
* is rejected rather than passed along: the receiving implementation needs a
|
|
1142
|
+
* genuine `AttrRef` instance (`instanceof` checks, `WeakRef` derefs — see
|
|
1143
|
+
* `SubIntrinsic`, and `LexiconOutput`'s constructor in ../lexicon-output.ts),
|
|
1144
|
+
* and handing it a look-alike plain object produces output that is wrong
|
|
1145
|
+
* rather than absent. Everywhere else the envelope is left untouched, because
|
|
1146
|
+
* the serializer's generic walker already understands it — see the module-doc
|
|
1147
|
+
* note above.
|
|
1122
1148
|
*/
|
|
1123
|
-
async function reviveFoldedValue(value: FoldedValue, ctx: ResolveCtx,
|
|
1149
|
+
async function reviveFoldedValue(value: FoldedValue, ctx: ResolveCtx, requireLiveRefs: boolean): Promise<unknown> {
|
|
1124
1150
|
if (value === null || typeof value !== "object") return value;
|
|
1125
1151
|
|
|
1126
1152
|
// chant #1020 — a REAL, already-constructed live object reached via
|
|
@@ -1141,7 +1167,7 @@ async function reviveFoldedValue(value: FoldedValue, ctx: ResolveCtx, insideIntr
|
|
|
1141
1167
|
|
|
1142
1168
|
if (Array.isArray(value)) {
|
|
1143
1169
|
const revived: unknown[] = [];
|
|
1144
|
-
for (const el of value) revived.push(await reviveFoldedValue(el, ctx,
|
|
1170
|
+
for (const el of value) revived.push(await reviveFoldedValue(el, ctx, requireLiveRefs));
|
|
1145
1171
|
return revived;
|
|
1146
1172
|
}
|
|
1147
1173
|
|
|
@@ -1154,17 +1180,33 @@ async function reviveFoldedValue(value: FoldedValue, ctx: ResolveCtx, insideIntr
|
|
|
1154
1180
|
const intrinsic = value as FoldedIntrinsic;
|
|
1155
1181
|
const Fn = await resolveImportedExport(intrinsic.__intrinsic, ctx);
|
|
1156
1182
|
if (typeof Fn !== "function") {
|
|
1157
|
-
throw cheapError(`intrinsic
|
|
1183
|
+
throw cheapError(`intrinsic "${intrinsic.__intrinsic}" did not resolve to a function`);
|
|
1158
1184
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1185
|
+
// Two authored forms, one envelope family (../fold/fold.ts's
|
|
1186
|
+
// `FoldedIntrinsic`): the tagged template replays as
|
|
1187
|
+
// `Name(strings, ...values)`, the plain call (chant #1044) as
|
|
1188
|
+
// `Name(...args)`. Both hand their interior to the REAL function the
|
|
1189
|
+
// file itself imported, with `requireLiveRefs` — an intrinsic inspects
|
|
1190
|
+
// what it is given (`SubIntrinsic`'s `instanceof` checks, `Ref`'s
|
|
1191
|
+
// `getLogicalName`), so a look-alike `{__attrRef}` envelope must be
|
|
1192
|
+
// rejected here rather than silently serialized as something else.
|
|
1193
|
+
const revived: unknown[] = [];
|
|
1194
|
+
if ("args" in intrinsic) {
|
|
1195
|
+
for (const a of intrinsic.args) revived.push(await reviveFoldedValue(a, ctx, true));
|
|
1196
|
+
return (Fn as (...fnArgs: unknown[]) => unknown)(...revived);
|
|
1197
|
+
}
|
|
1198
|
+
for (const v of intrinsic.values) revived.push(await reviveFoldedValue(v, ctx, true));
|
|
1199
|
+
return (Fn as (...fnArgs: unknown[]) => unknown)(intrinsic.strings, ...revived);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
if ("__helper" in value) {
|
|
1203
|
+
return reviveHelperCall(value as FoldedHelperCall, ctx);
|
|
1162
1204
|
}
|
|
1163
1205
|
|
|
1164
1206
|
if ("__attrRef" in value) {
|
|
1165
|
-
if (
|
|
1207
|
+
if (requireLiveRefs) {
|
|
1166
1208
|
throw cheapError(
|
|
1167
|
-
"a same-file resource reference
|
|
1209
|
+
"a same-file resource reference passed to a folded intrinsic or authoring helper is not foldable yet",
|
|
1168
1210
|
);
|
|
1169
1211
|
}
|
|
1170
1212
|
return value;
|
|
@@ -1178,11 +1220,93 @@ async function reviveFoldedValue(value: FoldedValue, ctx: ResolveCtx, insideIntr
|
|
|
1178
1220
|
|
|
1179
1221
|
const revived: Record<string, unknown> = {};
|
|
1180
1222
|
for (const [key, v] of Object.entries(value)) {
|
|
1181
|
-
revived[key] = await reviveFoldedValue(v as FoldedValue, ctx,
|
|
1223
|
+
revived[key] = await reviveFoldedValue(v as FoldedValue, ctx, requireLiveRefs);
|
|
1182
1224
|
}
|
|
1183
1225
|
return revived;
|
|
1184
1226
|
}
|
|
1185
1227
|
|
|
1228
|
+
/**
|
|
1229
|
+
* chant #1082 — the provenance half of the registered-authoring-helper check
|
|
1230
|
+
* (the shape/name half is `fold()`'s, see ../fold/foldable-helpers.ts's module
|
|
1231
|
+
* doc). Being in the allowlist is not permission to invoke whatever the name
|
|
1232
|
+
* happens to be bound to: the name must be bound by THIS FILE'S OWN `import`,
|
|
1233
|
+
* and that import must actually come from chant. Only then is the real
|
|
1234
|
+
* function called, with the folded arguments — the same function the run path
|
|
1235
|
+
* would have called, from the same module the source itself named, so fold
|
|
1236
|
+
* cannot diverge from run by construction.
|
|
1237
|
+
*
|
|
1238
|
+
* Anything short of that throws, which falls the whole file back to run: a
|
|
1239
|
+
* local `function phase(...)`, a `phase` imported from the project's own
|
|
1240
|
+
* helpers, a chant-owned import that turns out not to be a function.
|
|
1241
|
+
*/
|
|
1242
|
+
async function reviveHelperCall(call: FoldedHelperCall, ctx: ResolveCtx): Promise<unknown> {
|
|
1243
|
+
const name = call.__helper;
|
|
1244
|
+
const binding = ctx.imports.get(name);
|
|
1245
|
+
if (!binding) {
|
|
1246
|
+
throw cheapError(`authoring helper "${name}(...)" is not a resolvable import`);
|
|
1247
|
+
}
|
|
1248
|
+
if (!isChantOwnedHelperBinding(binding, ctx)) {
|
|
1249
|
+
throw cheapError(
|
|
1250
|
+
`"${name}" is imported from "${binding.specifier}", which is not chant's own — ` +
|
|
1251
|
+
`only chant's registered authoring helpers fold as calls`,
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
const Fn = await resolveImportedExport(name, ctx);
|
|
1256
|
+
if (typeof Fn !== "function") {
|
|
1257
|
+
throw cheapError(`authoring helper "${name}" did not resolve to a function`);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// `requireLiveRefs` — a helper receives its arguments as real values and may
|
|
1261
|
+
// inspect them (`output()` derefs the ref's `WeakRef` parent), so a symbolic
|
|
1262
|
+
// `{__attrRef}` envelope is rejected here rather than silently wrapped into
|
|
1263
|
+
// a wrong result. A ref that resolved cross-file to a genuine live `AttrRef`
|
|
1264
|
+
// passes straight through (see `reviveFoldedValue`'s early return).
|
|
1265
|
+
const args: unknown[] = [];
|
|
1266
|
+
for (const arg of call.args) args.push(await reviveFoldedValue(arg, ctx, true));
|
|
1267
|
+
return (Fn as (...fnArgs: unknown[]) => unknown)(...args);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/**
|
|
1271
|
+
* True when `binding` names a helper import chant itself owns: a published
|
|
1272
|
+
* chant package specifier ({@link isChantOwnedSpecifier}), or — for in-repo
|
|
1273
|
+
* and test callers, which import chant-core by relative/absolute path the way
|
|
1274
|
+
* this module's own fixtures do — a specifier that resolves to a file inside
|
|
1275
|
+
* chant-core's own tree.
|
|
1276
|
+
*
|
|
1277
|
+
* The path arm resolves only for a relative/absolute specifier, never a bare
|
|
1278
|
+
* one: resolving an arbitrary bare specifier here would reintroduce the
|
|
1279
|
+
* pathological cold-resolution cost chant#1020 measured (see
|
|
1280
|
+
* {@link fastResolveBareSpecifier}), and a bare specifier chant publishes is
|
|
1281
|
+
* already covered by the text arm.
|
|
1282
|
+
*/
|
|
1283
|
+
function isChantOwnedHelperBinding(binding: ImportBinding, ctx: ResolveCtx): boolean {
|
|
1284
|
+
if (isChantOwnedSpecifier(binding.specifier)) return true;
|
|
1285
|
+
if (!isProjectFileSpecifier(binding.specifier)) return false;
|
|
1286
|
+
let targetPath: string;
|
|
1287
|
+
try {
|
|
1288
|
+
targetPath = resolveModulePathMemoized(binding.specifier, ctx.file, ctx.resolvePathCache);
|
|
1289
|
+
} catch {
|
|
1290
|
+
return false;
|
|
1291
|
+
}
|
|
1292
|
+
const root = chantCoreRoot();
|
|
1293
|
+
return targetPath === root || targetPath.startsWith(root + sep);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* chant-core's own module root — `packages/core/src` in this repo,
|
|
1298
|
+
* `<pkg>/dist` in a published install — derived from THIS module's location.
|
|
1299
|
+
* Pure string arithmetic on `import.meta.url`, no filesystem access at all, so
|
|
1300
|
+
* it is safe in the #1045 sandbox child (which locks reads to an allowlist);
|
|
1301
|
+
* lazy and memoized purely to avoid doing it per call.
|
|
1302
|
+
*/
|
|
1303
|
+
let chantCoreRootMemo: string | undefined;
|
|
1304
|
+
function chantCoreRoot(): string {
|
|
1305
|
+
// .../<root>/discovery/fold-import.ts -> .../<root>
|
|
1306
|
+
chantCoreRootMemo ??= dirname(dirname(fileURLToPath(import.meta.url)));
|
|
1307
|
+
return chantCoreRootMemo;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1186
1310
|
/** Revive every value in a folded props/attributes object (see {@link reviveFoldedValue}). */
|
|
1187
1311
|
async function reviveFoldedProps(
|
|
1188
1312
|
props: { [key: string]: FoldedValue },
|
|
@@ -1220,11 +1344,27 @@ async function resolveResourceEntity(
|
|
|
1220
1344
|
// real runtime values before constructing the entity. A no-op walk when
|
|
1221
1345
|
// this file used no registered intrinsics (the overwhelming majority of
|
|
1222
1346
|
// cases today).
|
|
1223
|
-
|
|
1224
|
-
|
|
1347
|
+
//
|
|
1348
|
+
// chant #1082 — when `spec.args` is present the constructor's argument list
|
|
1349
|
+
// isn't the classic `(props)`/`(props, attributes)` shape (AWS's `Parameter`
|
|
1350
|
+
// is `(type, props)`), so revive the whole list and spread it below instead
|
|
1351
|
+
// of reviving `spec.props`, which in that case is only a view onto one of
|
|
1352
|
+
// its entries and would be double-counted.
|
|
1353
|
+
let ctorArgs: unknown[];
|
|
1225
1354
|
try {
|
|
1226
|
-
|
|
1227
|
-
|
|
1355
|
+
if (spec.args) {
|
|
1356
|
+
ctorArgs = [];
|
|
1357
|
+
for (const arg of spec.args) ctorArgs.push(await reviveFoldedValue(arg, ctx, false));
|
|
1358
|
+
} else {
|
|
1359
|
+
const props = await reviveFoldedProps(spec.props, ctx);
|
|
1360
|
+
// The runtime constructor's optional second argument (`attributes` —
|
|
1361
|
+
// CFN's DependsOn/Condition/DeletionPolicy/…, see createResource in
|
|
1362
|
+
// ../runtime.ts) is only present in `spec` when the source actually
|
|
1363
|
+
// passed one (see foldResource in ../fold/fold.ts). Passing `undefined`
|
|
1364
|
+
// when it's absent matches the run path's own default
|
|
1365
|
+
// (`attributes ?? {}` inside the constructor).
|
|
1366
|
+
ctorArgs = [props, spec.attributes ? await reviveFoldedProps(spec.attributes, ctx) : undefined];
|
|
1367
|
+
}
|
|
1228
1368
|
} catch (err) {
|
|
1229
1369
|
return {
|
|
1230
1370
|
ok: false,
|
|
@@ -1264,17 +1404,10 @@ async function resolveResourceEntity(
|
|
|
1264
1404
|
return { ok: false, reason: `"${binding.imported}" from "${binding.specifier}" is not a constructor` };
|
|
1265
1405
|
}
|
|
1266
1406
|
|
|
1267
|
-
//
|
|
1268
|
-
//
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
// absent matches the run path's own default (`attributes ?? {}` inside the
|
|
1272
|
-
// constructor).
|
|
1273
|
-
const ResourceCtor = Ctor as new (
|
|
1274
|
-
props: Record<string, unknown>,
|
|
1275
|
-
attributes?: Record<string, unknown>,
|
|
1276
|
-
) => Declarable;
|
|
1277
|
-
const entity = new ResourceCtor(props, attributes);
|
|
1407
|
+
// Constructed with exactly the arguments the source wrote (see the revival
|
|
1408
|
+
// block above for how `ctorArgs` was built for each of the two shapes).
|
|
1409
|
+
const ResourceCtor = Ctor as new (...ctorArguments: unknown[]) => Declarable;
|
|
1410
|
+
const entity = new ResourceCtor(...ctorArgs);
|
|
1278
1411
|
return { ok: true, entity };
|
|
1279
1412
|
}
|
|
1280
1413
|
|
|
@@ -1394,9 +1527,11 @@ async function buildExternals(
|
|
|
1394
1527
|
imports: Map<string, ImportBinding>,
|
|
1395
1528
|
namespaceImports: Map<string, NamespaceImportBinding>,
|
|
1396
1529
|
session: FoldSession,
|
|
1397
|
-
): Promise<{ externals: Map<string, unknown>; failures: Map<string, string> }> {
|
|
1530
|
+
): Promise<{ externals: Map<string, unknown>; failures: Map<string, string>; liveSources: Set<string> }> {
|
|
1398
1531
|
const externals = new Map<string, unknown>();
|
|
1399
1532
|
const failures = new Map<string, string>();
|
|
1533
|
+
// chant #1044 — see `FoldFileResult.liveSources`.
|
|
1534
|
+
const liveSources = new Set<string>();
|
|
1400
1535
|
|
|
1401
1536
|
for (const [localName, binding] of imports) {
|
|
1402
1537
|
// chant #1064 — a named `params` import that resolves to chant-core's own
|
|
@@ -1468,7 +1603,9 @@ async function buildExternals(
|
|
|
1468
1603
|
continue;
|
|
1469
1604
|
}
|
|
1470
1605
|
if (result.exportedValues.has(binding.imported)) {
|
|
1471
|
-
|
|
1606
|
+
const value = result.exportedValues.get(binding.imported);
|
|
1607
|
+
externals.set(localName, value);
|
|
1608
|
+
if (hasObjectIdentity(value)) liveSources.add(targetPath);
|
|
1472
1609
|
} else {
|
|
1473
1610
|
failures.set(
|
|
1474
1611
|
localName,
|
|
@@ -1494,9 +1631,28 @@ async function buildExternals(
|
|
|
1494
1631
|
// access on it (`ns.someExport`) is then just an ordinary bracket index,
|
|
1495
1632
|
// exactly like on a real composite instance (see `isIndexableObject`).
|
|
1496
1633
|
externals.set(localName, Object.fromEntries(result.exportedValues));
|
|
1634
|
+
for (const value of result.exportedValues.values()) {
|
|
1635
|
+
if (hasObjectIdentity(value)) {
|
|
1636
|
+
liveSources.add(targetPath);
|
|
1637
|
+
break;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1497
1640
|
}
|
|
1498
1641
|
|
|
1499
|
-
return { externals, failures };
|
|
1642
|
+
return { externals, failures, liveSources };
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
/**
|
|
1646
|
+
* True when `value` is something whose IDENTITY matters across the
|
|
1647
|
+
* fold/run boundary — any object or function, as opposed to a primitive
|
|
1648
|
+
* (chant #1044). Deliberately coarse: an object that merely *contains* a
|
|
1649
|
+
* `Declarable` is as identity-bearing as the Declarable itself, and cheaply
|
|
1650
|
+
* treating every object as such avoids a deep walk whose only payoff would
|
|
1651
|
+
* be keeping a handful of extra files folded inside an entry that already
|
|
1652
|
+
* falls back. See {@link FoldFileResult.liveSources}.
|
|
1653
|
+
*/
|
|
1654
|
+
function hasObjectIdentity(value: unknown): boolean {
|
|
1655
|
+
return value !== null && (typeof value === "object" || typeof value === "function");
|
|
1500
1656
|
}
|
|
1501
1657
|
|
|
1502
1658
|
/**
|
|
@@ -1540,7 +1696,7 @@ async function tryFoldFileCore(file: string, session: FoldSession): Promise<Fold
|
|
|
1540
1696
|
if (scan.declarators.length === 0) return { ok: false, reason: "no foldable resource exports" };
|
|
1541
1697
|
|
|
1542
1698
|
const collected = collectImports(sourceFile);
|
|
1543
|
-
const { externals, failures } = await buildExternals(file, collected.named, collected.namespaces, session);
|
|
1699
|
+
const { externals, failures, liveSources } = await buildExternals(file, collected.named, collected.namespaces, session);
|
|
1544
1700
|
|
|
1545
1701
|
const ctx: ResolveCtx = {
|
|
1546
1702
|
file,
|
|
@@ -1644,11 +1800,16 @@ async function tryFoldFileCore(file: string, session: FoldSession): Promise<Fold
|
|
|
1644
1800
|
reason: locatedMessage(decl.specifierNode, `"${imported}" is not exported by "${decl.specifier}"`),
|
|
1645
1801
|
};
|
|
1646
1802
|
}
|
|
1647
|
-
|
|
1803
|
+
const value = result.exportedValues.get(imported);
|
|
1804
|
+
// chant #1044 — a re-export hands another file's OBJECT straight
|
|
1805
|
+
// through under this file's name, so it is a live-identity edge
|
|
1806
|
+
// exactly like an imported binding is (see `liveSources`).
|
|
1807
|
+
if (hasObjectIdentity(value)) liveSources.add(targetPath);
|
|
1808
|
+
applyResolvedValue(exportedName, value, entities, exportedValues);
|
|
1648
1809
|
}
|
|
1649
1810
|
}
|
|
1650
1811
|
|
|
1651
|
-
return { ok: true, entities, exportedValues };
|
|
1812
|
+
return { ok: true, entities, exportedValues, liveSources };
|
|
1652
1813
|
} catch (err) {
|
|
1653
1814
|
// Any unexpected failure degrades to "fall back to run" rather than
|
|
1654
1815
|
// taking discovery down with it — fold is opt-in, not a new failure mode.
|
|
@@ -1725,6 +1886,21 @@ export async function tryFoldFile(
|
|
|
1725
1886
|
* the discovered files' relative-import graph, seeded from every file that
|
|
1726
1887
|
* doesn't fold on its own.
|
|
1727
1888
|
*
|
|
1889
|
+
* chant #1044 adds the OTHER half of the same hazard, in the opposite
|
|
1890
|
+
* direction along the same edges. Forward taint covers "a run file imports a
|
|
1891
|
+
* folded file"; it does not cover "a FOLDED file consumed the objects of a
|
|
1892
|
+
* file that later got forced to run". Once a plain-call intrinsic can fold,
|
|
1893
|
+
* that second case is easy to reach: in `lexicons/aws/examples/lambda-api`,
|
|
1894
|
+
* `health-api.ts` folds and captures `params.ts`'s real `Parameter` instance
|
|
1895
|
+
* through `Ref(environment)`, while `params.ts` itself is forced to run
|
|
1896
|
+
* because a DIFFERENT sibling (`data-bucket.ts`) imports it and falls back.
|
|
1897
|
+
* Discovery then collects the run instance and serializes the folded one —
|
|
1898
|
+
* the same "Logical name not set" crash described above, arriving from the
|
|
1899
|
+
* other side. So `liveSources` (see {@link FoldFileResult}) contributes
|
|
1900
|
+
* reverse edges here: a tainted file taints every folded file that captured
|
|
1901
|
+
* one of its objects. Only object identity propagates — a file that imported
|
|
1902
|
+
* a plain string from a tainted file has nothing to disagree about.
|
|
1903
|
+
*
|
|
1728
1904
|
* chant #1020 changes the calculus but not this function: `alb.ts` can now
|
|
1729
1905
|
* often fold `network.vpc.VpcId` too (see `buildExternals`/`foldFileMemoized`
|
|
1730
1906
|
* above), by reusing THE EXACT SAME `tryFoldFile("network.ts")` call (memoized
|
|
@@ -1740,6 +1916,7 @@ export async function tryFoldFile(
|
|
|
1740
1916
|
export async function planFoldTaint(
|
|
1741
1917
|
files: readonly string[],
|
|
1742
1918
|
wouldFold: ReadonlyMap<string, boolean>,
|
|
1919
|
+
liveSources?: ReadonlyMap<string, ReadonlySet<string>>,
|
|
1743
1920
|
): Promise<Set<string>> {
|
|
1744
1921
|
const fileSet = new Set(files);
|
|
1745
1922
|
|
|
@@ -1789,6 +1966,22 @@ export async function planFoldTaint(
|
|
|
1789
1966
|
edges.set(file, targets);
|
|
1790
1967
|
}
|
|
1791
1968
|
|
|
1969
|
+
// chant #1044 — reverse edges: consumed-file -> the folded files that
|
|
1970
|
+
// captured its objects. Same taint set, same fixpoint walk; see this
|
|
1971
|
+
// function's doc for the crash this closes.
|
|
1972
|
+
for (const [consumer, sources] of liveSources ?? []) {
|
|
1973
|
+
if (!fileSet.has(consumer)) continue;
|
|
1974
|
+
for (const source of sources) {
|
|
1975
|
+
if (!fileSet.has(source)) continue;
|
|
1976
|
+
let back = edges.get(source);
|
|
1977
|
+
if (!back) {
|
|
1978
|
+
back = new Set<string>();
|
|
1979
|
+
edges.set(source, back);
|
|
1980
|
+
}
|
|
1981
|
+
back.add(consumer);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1792
1985
|
const tainted = new Set<string>(files.filter((f) => wouldFold.get(f) !== true));
|
|
1793
1986
|
const queue = [...tainted];
|
|
1794
1987
|
while (queue.length > 0) {
|
package/src/discovery/index.ts
CHANGED
|
@@ -166,6 +166,15 @@ export async function discover(path: string, options?: DiscoveryOptions): Promis
|
|
|
166
166
|
? await planFoldTaint(
|
|
167
167
|
files,
|
|
168
168
|
new Map(files.map((file) => [file, foldAttempts.get(file)?.ok === true])),
|
|
169
|
+
// chant #1044 — which files' OBJECTS each successful fold captured,
|
|
170
|
+
// so a file forced back to run also invalidates the folds that
|
|
171
|
+
// already hold its instances (see planFoldTaint's doc).
|
|
172
|
+
new Map(
|
|
173
|
+
files.flatMap((file) => {
|
|
174
|
+
const attempt = foldAttempts.get(file);
|
|
175
|
+
return attempt?.ok === true ? [[file, attempt.liveSources] as const] : [];
|
|
176
|
+
}),
|
|
177
|
+
),
|
|
169
178
|
)
|
|
170
179
|
: new Set<string>();
|
|
171
180
|
|