import * as ts from "typescript"; import { type Declarable } from "../declarable.js"; import { type CompositeInstance } from "../composite.js"; import type { IntrinsicDef } from "../lexicon.js"; import type { BuildParamValue } from "../build-params.js"; /** * Bridges the static folder ({@link ../fold/fold}, #1026) into discovery * (#1022/#1023, epic #1019): attempts to fold one source file into real * `Declarable`/`CompositeInstance` instances with zero execution of the * file's own top-level code, so `discover()` can skip `importModule` for it * entirely. * * The folder only reduces expressions to plain values — it has no notion of * lexicon resource classes or composites. This module supplies that missing * piece: it reads the file's `import` declarations to learn which module * each `new Type(...)` constructor or bare composite-factory call names, * resolves and imports *that* module (a trusted lexicon/vendor module, not * the file under fold), and constructs the real resource/composite instance * from the folded props. Importing the lexicon module is not a regression on * "no module execution" — the run path already imports it to get the same * class/function; the only thing skipped here is executing the file's *own* * statements. * * chant #1022 extends this from leaf resources (`new Type(...)`) to composite * factory calls — `SomeComposite({...})`, * `propagate(SomeComposite({...}), {...})`, member access on the result * (`web.deployment`), and destructuring (`const { a, b } = SomeComposite(...)` * or `export const { a, b } = SomeComposite(...)`). A composite factory is a * pure function of its props (EVL009/EVL010 guarantee its body only * references props, sibling members, and imports), so — exactly like a * resource constructor — it is resolved through the file's imports and * INVOKED with statically-folded props, and the RESULT is what matters: if it * satisfies {@link isCompositeInstance} (or, for a plain resource-returning * helper, {@link isDeclarable}), it's used. Nested composites and * `propagate()`'d shared props need no special-casing there: a nested * composite is just another member the real factory call already produced, * and `propagate` is just another resolvable imported function that receives a * live `CompositeInstance` plus folded shared props and returns it — * `expandComposite()` (invoked downstream by `collectEntities`, unchanged) * does the recursive expansion and the shared-prop merge exactly as it does * for the run path. * * chant #1023 (epic #1019 Phase 5) removes that invocation where it can. When * the callee is a composite defined in a PROJECT file as * `Composite(, "")` and ``'s body stays inside a closed, * documented subset, its body is INTERPRETED instead — the defining module is * never imported, and the members are built here, by the lexicon's own * constructors, from the same folded props. That closes the last place a file * reported as `[fold:fold]` could still execute project-authored code in the * CLI's process (#1093), and lets such a file fold under `--sandbox` rather * than being demoted to the child (#1111). A factory outside the subset keeps * invoking, unchanged. See the contract block above * {@link resolveInterpretableFactory} for the five admissibility rules and * what interpretation preserves. */ /** One exported `const` name folded to a real, constructed `Declarable` or `CompositeInstance`. */ export type FoldedEntity = [name: string, entity: Declarable | CompositeInstance]; export type FoldFileResult = { ok: true; /** * The `Declarable`/`CompositeInstance` subset of {@link * FoldFileResult.exportedValues} — how many resources this file * contributed, for the `[fold:fold] x.ts — N resource(s)` decision * line. chant #1112: NOT what discovery collects from. Collection * reads `exportedValues`, so that `./collect.ts` stays the single * owner of which exports become entities — see {@link * applyResolvedValue}. */ entities: FoldedEntity[]; /** * chant #1020 — EVERY exported name's fully-resolved value, not just * the `Declarable`/`CompositeInstance` ones also listed in `entities`: * a plain value folds too (a string, a number, a plain object). A * successful fold means `scanExports` recognized every export the file * has (anything else disqualifies the whole file), so this IS the * file's complete export namespace — the same table the run path gets * from actually importing it. Two consumers: `discover()` passes it * straight to `collectEntities` (chant #1112), and another file's * cross-file reference resolves against it — see `buildExternals` * below and the module doc on `planFoldTaint` for why a * resource/composite value here MUST be the exact same object every * referencing file sees. */ exportedValues: Map; /** * chant #1044 — the OTHER project files whose exported OBJECTS this * fold consumed (a cross-file `Declarable`, composite instance, or any * other non-primitive reached through `buildExternals`/a re-export). * * Object identity is the thing that cannot survive one side of the * build folding while the other runs, so `planFoldTaint` needs to know * who consumed whose objects: if a file here is forced back to run, the * instance THIS file already captured is not the instance discovery * will collect, and serialization fails on an entity with no logical * name. A primitive (string, number, boolean, null) is never recorded — * it has no identity to disagree about. */ liveSources: ReadonlySet; } | { ok: false; reason: string; }; /** * A per-build memo (chant #1020) so a project file imported by several * others is folded exactly ONCE — every referrer resolves against the SAME * `FoldFileResult`, and therefore the SAME constructed * `Declarable`/`CompositeInstance` objects, no matter how many files * cross-file-reference it or in what order discovery visits them. Also * tracks the current resolution call chain (`stack`) so a genuine reference * cycle (fileA needs fileB needs fileA) is DETECTED — rather than an * infinite recursion / a promise awaiting itself forever — and reported as a * located `FoldError` naming the cycle path. * * `discover()` creates exactly one session per `{ fold: true }` build and * passes it to every top-level `tryFoldFile` call, so its own per-file loop * and any cross-file reference reaching into the same file share the * identical cache. Callers that don't care about cross-file sharing (unit * tests exercising a single file in isolation) can omit it — {@link tryFoldFile} * creates a private, single-call session on their behalf. */ export interface FoldSession { readonly intrinsics: readonly IntrinsicDef[]; readonly cache: Map>; readonly stack: string[]; /** * Per-build memo (chant #1020 hang fix) for {@link importModule} itself — * keyed by resolved absolute module path, shared session-wide exactly like * {@link cache} above. Cross-file resolution means MANY files in one * directory can each independently resolve the SAME constructor/composite- * factory import (e.g. every file that constructs an AWS resource imports * the same lexicon barrel) — before this, every one of those calls issued * its own `await import(path)`, relying entirely on the runtime's own * module cache to make the repeats cheap. That assumption holds for a * plain `node`/`tsx` process, but NOT for a real dynamic import running * inside a vitest worker: `vite-node`'s own SSR module graph can take a * real, non-trivial amount of wall-clock time to re-resolve/re-register an * already-loaded module on EVERY call, not just the first — harmless at * single-digit call counts, but #1020's cross-file resolution can issue * several times as many `importModule` calls for the same handful of large * lexicon barrels within one `discover()` pass as the pre-#1020 single-file * fold did. Memoizing the import itself (not just the path resolution) * caps it at exactly one real `import()` per unique path per session, * regardless of how many files reference it — this is what actually keeps * a session-local retry (e.g. `sandbox-differential.test.ts`'s * `vi.resetModules()` + rebuild path, which reruns fold from a cold * module cache) from compounding into a multi-minute stall. Purely a cache * over an idempotent operation (the same resolved path always yields the * same module namespace object) — doesn't change what folds. */ readonly importCache: Map>>; /** * Per-build memo (chant #1020 hang fix) for {@link resolveModulePath}'s * RELATIVE/absolute-specifier branch only — keyed by * `${dirname(fromFile)}\0${specifier}`. See {@link resolveModulePathMemoized}'s * doc for the full story, including why bare (package) specifiers are * memoized in a separate, process-wide cache instead of this session- * scoped one: a relative specifier resolves against PROJECT source, which * `chant build --watch` can legitimately change between rebuilds (a new * sibling file appearing mid-session), so this cache is intentionally * thrown away with the rest of the session at the end of every * `discover()` call, unlike the bare-specifier one. */ readonly resolvePathCache: Map; /** * chant #1064 — this build's resolved build-time parameter values (see * ../build-params.ts), consulted only by {@link buildExternals}'s one * recognized bare-specifier case: a named `params` import resolving to * ../params.ts. `undefined` when the build supplied none (no `chant.config.ts` * `buildParams` declared, or the caller didn't pass any) — a project that * doesn't use build-time parameters pays nothing extra here. */ readonly buildParams?: Readonly>; /** * chant #1063 — the exact package specifiers of the lexicons LOADED for * this build (`@intentius/chant-lexicon-aws`, …), derived from the lexicon * names the build already resolved (`resolveProjectLexicons` -> * `loadPlugins`, see ../cli/plugins.ts). This is the entire allowlist * {@link buildExternals} will follow a bare import specifier into — see * {@link activeLexiconPackage} for why the set is matched by TEXT and * built from names the build already knows, rather than by resolving * specifiers to find out what they are. * * Empty when the caller supplied no lexicon list, which disables * lexicon-package resolution entirely rather than falling back to * something more permissive: "an active lexicon of this build" is the * boundary, and a build that can't say what its lexicons are hasn't * established one. */ readonly lexiconPackages: ReadonlySet; /** * chant #1093 — this build asked for the #1045 sandbox * (`DiscoveryOptions.sandbox`, `chant build --sandbox`), so fold must not * import or invoke a module the CLI process isn't already trusted to * execute. See {@link isTrustedExecutableBinding} for what that allowlist * is and {@link sandboxedExecutionRefusal} for what happens at each site * that would otherwise execute one. * * `false` (the default, plain `--fold`) leaves every resolution path * exactly as it was: fold already trusts the code enough to fall back to * an in-process `importModule` when it can't fold something, so gating * only the fold half would buy nothing there. */ readonly sandbox: boolean; /** * chant #1023 — per-build memo for {@link readFactoryModule}, keyed by the * resolved absolute path of a module that DEFINES a composite. A composite * defined once and called from a dozen sibling files is parsed, and its * imports resolved, exactly once per build — the same reason * {@link FoldSession.cache} exists for the files discovery folds directly. * * Separate from `cache` because the two ask different questions of the same * file: `cache` asks "what are this module's exported VALUES" (and fails * outright for a module that exports a function declaration, which a * composite-defining module very often does); this asks "what is this * module's static SCOPE" — its consts, its imports, and those imports' * already-resolved values — which is well-defined even when the module as a * whole doesn't fold. `lexicons/aws/examples/lambda-api/src/lambda-api.ts` * is exactly that case: it exports two plain functions alongside its * `Composite`, so it never folds, and its `LambdaApi` is interpretable * regardless. */ readonly factoryModules: Map>; } /** * chant #1063 — the package specifier a lexicon NAME (`"aws"`, `"gitlab"`) * is installed under. The one naming convention the whole CLI already * depends on: `loadPlugin(name)` imports exactly this * (../cli/plugins.ts), `detectLexicons` scans source for exactly this * (../detectLexicon.ts), and `chant init` writes exactly this into * package.json. */ export declare function lexiconPackageName(lexiconName: string): string; /** * Create a fresh, empty {@link FoldSession}. * * @param lexicons - chant #1063: the lexicon NAMES active for this build * (`["aws", "k8s"]`). Converted to package specifiers via * {@link lexiconPackageName}; see {@link FoldSession.lexiconPackages}. * @param sandbox - chant #1093: this build asked for the #1045 sandbox, so * fold may not import or invoke anything outside the trusted allowlist — * see {@link FoldSession.sandbox}. */ export declare function createFoldSession(intrinsics?: readonly IntrinsicDef[], buildParams?: Readonly>, lexicons?: readonly string[], sandbox?: boolean): FoldSession; /** * What the fold pass actually EXECUTED, and what it interpreted instead — the * number chant #1023 is measured by, and the one the #1093/#1111 * execution-boundary report could not state before: `--sandbox` proves * *nothing project-owned ran in this process*, but plain `--fold` still * invoked composite factories in-process, and nothing counted them. * * Purely observational. Two integer increments on paths that were already * about to perform a dynamic `import()` or parse a module, so it costs * nothing measurable and changes no decision. */ export interface FoldExecutionCounts { /** * Composite-factory / wrapper calls {@link resolveCallExpression} resolved * by importing the defining module and CALLING it in this process. */ factoryInvocations: number; /** * Of {@link factoryInvocations}, the ones whose callee came from a PROJECT * FILE (a relative/absolute specifier) rather than a lexicon package or * chant's own — i.e. the ones that execute project-authored code here. Text * only ({@link isProjectFileSpecifier}); no resolution is performed to * classify. */ projectFactoryInvocations: number; /** * Composite factory bodies {@link interpretCompositeFactory} evaluated * statically instead — each one an invocation that did NOT happen. */ factoryInterpretations: number; } /** A snapshot of {@link FoldExecutionCounts}. Process-wide and monotonic — a caller wanting a per-build figure calls {@link resetFoldExecutionCounts} first. */ export declare function foldExecutionCounts(): Readonly; /** Zero {@link foldExecutionCounts}, for a caller measuring one build. */ export declare function resetFoldExecutionCounts(): void; /** Where an imported local identifier came from. */ interface ImportBinding { specifier: string; imported: string; /** The `import ... from "specifier"` declaration's module-specifier * string-literal node (chant #1020) — used to attach a source position to * a cross-file resolution failure (in particular an import-cycle * diagnostic) at the referencing site, not just the defining one. */ specifierNode: ts.StringLiteral; } /** Where a namespace import (`import * as ns from "specifier"`) came from — chant #1020. */ interface NamespaceImportBinding { specifier: string; specifierNode: ts.StringLiteral; } /** * The static scope of a module that DEFINES composites — everything * interpreting one of its factory bodies needs, computed once per module per * build (see {@link FoldSession.factoryModules}). * * Note what is NOT here: the module's exported VALUES. Interpreting a factory * never needs them, which is the whole point — a module can define a perfectly * interpretable composite and still be unfoldable as a module (an exported * function declaration alongside it, say), and those two facts are * independent. */ interface FactoryModuleScope { file: string; sourceFile: ts.SourceFile; /** * The module's top-level `const`s, MINUS every one that resolves to a * `new Type(...)` resource. * * The exclusion is the load-bearing part. `fold()` turns a property access * on a resource-valued const into a symbolic `{__attrRef, entity: ""}` — a name resolved much later, against the entity table of * the file being COLLECTED, which is the calling file and not this one. A * module-level resource shared by every call of a factory is also a * singleton whose identity the run path shares and interpretation would not. * Dropping those consts makes any reference to one an ordinary "unresolved * identifier" failure, which declines the interpretation and invokes * instead — the answer that is right on both counts. */ consts: Map; imports: Map; namespaceImports: Map; /** * This module's own imports, resolved to their real cross-file values (see * {@link ResolveCtx.externals}) — LAZILY, and memoized here once built. * * Laziness is not an optimization detail, it is what keeps this issue from * paying #1020's cost all over again. `resolveCallExpression` reaches * {@link resolveInterpretableFactory} for EVERY call through a project-file * import, the overwhelming majority of which are not composites at all. Only * the parse is spent finding that out; resolving a module's whole import * graph — which recursively folds every project file it names — is spent * only by a call that is actually about to be interpreted. */ resolved?: Promise<{ externals: Map; failures: Map; }>; } /** * The SHAPE half of the contract (rules 3-5) — a lone `ts` node in, a reason * string out, or `undefined` when the factory is admissible. Deliberately * takes nothing but the node: it is the half a caller with no module graph * could evaluate, and keeping it separable is what lets the doc above claim * the shape rules are checkable without the provenance ones. */ export declare function findFactorySubsetViolation(fn: ts.ArrowFunction | ts.FunctionExpression): string | undefined; /** * Attempt to fold one source file with zero execution of its own top-level * code. Returns the folded, instantiated entities on success, or a reason * to fall back to the run path (`importModule`) on the first construct * outside the fold subset. * * @param intrinsics - Lexicon-registered intrinsic tags (chant #1039), e.g. * AWS's `Sub`. Threaded down to {@link fold}/{@link foldResource} so a * registered tagged template folds instead of unconditionally throwing * "unregistered tagged template intrinsic". Defaults to none — the caller * (`discover()`, ultimately `chant build --fold`) is expected to pass the * target lexicons' combined `intrinsics()`. Ignored when `session` is * given (its own `intrinsics`, fixed at creation, apply instead). * @param session - chant #1020: share ONE {@link FoldSession} across every * file in a build (as `discover()` does) so a project file imported by * several others folds exactly once and every referrer shares the same * constructed entities — see {@link FoldSession}'s doc. Omit for a * standalone, single-file fold attempt (creates a private session scoped * to just this call — a cross-file reference reachable from `file` still * resolves, just without sharing its cache with any other top-level call). */ export declare function tryFoldFile(file: string, intrinsics?: readonly IntrinsicDef[], session?: FoldSession): Promise; /** * A file that folds successfully in isolation must still be forced back to * run if some OTHER discovered file — one that itself falls back to run — * imports it (directly, or transitively through another sibling file it * imports). Composite folding is what makes this reachable: `network.ts` * exporting `export const network = VpcDefault({})` folds cleanly on its * own (no cross-file reference needed), landing its Declarables (`vpc`, its * subnets, …) in `entities` as ONE set of real objects, built by literally * invoking `VpcDefault` from inside `tryFoldFile` — bypassing `network.ts` * as a module entirely (that's the whole point of folding it). * * Before chant #1020, a sibling file like `alb.ts` that did * `import { network } from "./network"; ... vpcId: network.vpc.VpcId` * could never fold that cross-file reference and fell back to run — but * running `alb.ts` for real re-executes `import "./network"` for real too, * which (via Node's module cache) produces the SAME `network.ts` module * instance for every OTHER run-fallback file that imports it, but a * DIFFERENT one than the object `tryFoldFile("network.ts")` already built. * `alb.ts`'s AttrRef for `network.vpc.VpcId` then points at an object * that's never in the `entities` map (only the folded one is), so it can * never be assigned a logical name and serialization fails outright — not * drift, a crash. * * The fix: fold and run must never disagree about which object identity a * given file's exports have. Since `network.ts` itself doesn't need * anything cross-file to fold (only a file's own successful fold could ever * reach this taint — an unresolvable cross-file reference already fails * that file's OWN fold attempt), the safe rule is to force `network.ts` * back to run too, so both `alb.ts`'s real import and `network.ts`'s own * discovery entry resolve through the exact same `importModule` call and * share the exact same singleton module instance. This has to propagate * transitively (if `alb.ts` itself is only reachable by importing a file * that imports `network.ts`), so this is a forward-reachability walk over * the discovered files' relative-import graph, seeded from every file that * doesn't fold on its own. * * chant #1044 adds the OTHER half of the same hazard, in the opposite * direction along the same edges. Forward taint covers "a run file imports a * folded file"; it does not cover "a FOLDED file consumed the objects of a * file that later got forced to run". Once a plain-call intrinsic can fold, * that second case is easy to reach: in `lexicons/aws/examples/lambda-api`, * `health-api.ts` folds and captures `params.ts`'s real `Parameter` instance * through `Ref(environment)`, while `params.ts` itself is forced to run * because a DIFFERENT sibling (`data-bucket.ts`) imports it and falls back. * Discovery then collects the run instance and serializes the folded one — * the same "Logical name not set" crash described above, arriving from the * other side. So `liveSources` (see {@link FoldFileResult}) contributes * reverse edges here: a tainted file taints every folded file that captured * one of its objects. Only object identity propagates — a file that imported * a plain string from a tainted file has nothing to disagree about. * * chant #1020 changes the calculus but not this function: `alb.ts` can now * often fold `network.vpc.VpcId` too (see `buildExternals`/`foldFileMemoized` * above), by reusing THE EXACT SAME `tryFoldFile("network.ts")` call (memoized * per `FoldSession`) that `discover()`'s own per-file loop also uses — so * `alb.ts` and `network.ts` share one real `vpc` object without ever * disagreeing. This invariant is still needed for whatever STILL falls back * after #1020 (a call-as-a-value construct, #1044; a shape #1020 doesn't * cover): the edge collection below is unconditional — it doesn't care * whether an edge happens to ALSO be used for cross-file value resolution — * so the exact same forced-taint safety net still applies to that remaining * boundary, unchanged. */ export declare function planFoldTaint(files: readonly string[], wouldFold: ReadonlyMap, liveSources?: ReadonlyMap>): Promise>; export {}; //# sourceMappingURL=fold-import.d.ts.map