import { assert, assertMatch, assertNotMatch } from "@std/assert";
import { StaticCacheFS } from "@commonfabric/static";
import { transformSource } from "../utils.ts";
const staticCache = new StaticCacheFS();
const commonfabric = await staticCache.getText("types/commonfabric.d.ts");
const commonfabricSchema = await staticCache.getText(
"types/commonfabric-schema.d.ts",
);
const options = {
types: {
"commonfabric.d.ts": commonfabric,
"commonfabric-schema.d.ts": commonfabricSchema,
},
};
Deno.test("Closure Transformer hoists nested computed callbacks that close over module-scoped helpers", async () => {
const source = ` import { computed, pattern, UI } from "commonfabric";
const formatDateShort = (dateStr: string) => dateStr.toUpperCase();
export default pattern<{ values: string[] }>((state) => ({
[UI]: (
{state.values.map((dateStr) => (
{computed(() => formatDateShort(dateStr))}
))}
),
}));
`;
const output = await transformSource(source, options);
const normalized = output.replace(/\s+/g, " ");
// After CT-1644 Phase 2 the computed lowers to a lift whose WHOLE call is
// hoisted to a module-scope `const __cfLift_N = __cfHelpers.lift(...)`, with
// the callback inline (no separate `__cfModuleCallback` hardened wrapper —
// CT-1644 subsumes lift from the CT-1585 callback hoister). The hoisted
// const is module-scope, which is the property this test guards: a computed
// closing over the module-scoped helper `formatDateShort` becomes a named,
// module-scope, self-contained unit.
// lift is function-first: the callback leads, schemas trail. The hoisted call
// carries explicit `` type args, so match `lift<…>(` then the callback.
const hoistedMatch = normalized.match(
/const (__cfLift_\d+) = __cfHelpers\.lift<[\s\S]*?>\(\(\{ dateStr \}\) => formatDateShort\(dateStr\)/,
);
assert(hoistedMatch, `expected hoisted lift in output:\n${output}`);
const hoistedName = hoistedMatch[1]!;
// The original site applies the captures to the hoisted name.
assertMatch(
normalized,
new RegExp(`${hoistedName}\\(\\{ dateStr: dateStr \\}\\)`),
);
});
Deno.test("Closure Transformer does not hoist nested handler callbacks that also capture factory parameters", async () => {
const source = ` import { handler } from "commonfabric";
const normalize = (value: string) => value.trim();
export const makeHandler = (allowed: string[]) =>
handler((event: { status?: string } | undefined) => {
const status = normalize(event?.status ?? "");
if (!allowed.includes(status)) {
return;
}
});
`;
const output = await transformSource(source, options);
const normalized = output.replace(/\s+/g, " ");
assertNotMatch(
normalized,
/const \S+ = __cfHardenFn\(\(event: \{ status\?: string \} \| undefined\) =>/,
);
assertMatch(
normalized,
/const makeHandler = .*handler\(.*\(event: \{ status\?: string; \} \| undefined\) => \{.*allowed\.includes\(status\)/,
);
});
Deno.test("CT-1655: whole synthesized mapWithPattern pattern() call is hoisted to module scope", async () => {
// Source shape: a .map() callback whose body invokes a module-level
// pattern factory (`EntryRow`) and reads a module-level constant
// (`UI`). After the closure transformer rewrites .map() to
// `.mapWithPattern(__cfHelpers.pattern(cb, inSchema, outSchema), { params })`,
// the synthesized pattern callback closes only over module-scoped references —
// there are no per-call-site captures (the params object is empty).
//
// CT-1585 originally hoisted just the *callback* to `__cfModuleCallback_N`.
// CT-1655 instead hoists the WHOLE `pattern(...)` call (the first argument of
// mapWithPattern) to a module-scope `const __cfPattern_N = __cfHelpers
// .pattern(...)`, with the callback inline, and rewrites the mapWithPattern
// first argument to reference the hoisted name. (Hoisting the callback here
// too — the CT-1585 mechanic — would double-hoist into a module-load TDZ, so
// `pattern` is removed from that hoister's set.) The property this test
// guards is unchanged: a module-scope-only pattern becomes a named,
// module-scope, self-contained unit.
const source = ` import { pattern, UI } from "commonfabric";
interface Entry { piece: string }
interface RowOutput { rendered: string; [UI]: string }
const EntryRow = pattern((input) => ({
rendered: input.piece,
[UI]: input.piece,
}));
export default pattern<{ filtered: Entry[] }>(({ filtered }) => ({
[UI]: (
{filtered.map((entry) => {
const row = EntryRow({ piece: entry.piece });
return row[UI];
})}
),
}));
`;
const output = await transformSource(source, options);
const normalized = output.replace(/\s+/g, " ");
// The whole pattern() call is hoisted to module scope with its callback
// inline. Hoisting runs *after* PatternCallbackLowering, so the inline
// callback has the fully-lowered shape
// `__cf_pattern_input => { const entry = __cf_pattern_input.key("element"); ... }`.
// The exact const name is generated; capture it for the call-site assertion.
const hoistedMatch = normalized.match(
/const (__cfPattern_\d+) = __cfHelpers\.pattern\(\s*__cf_pattern_input\b/,
);
assert(
hoistedMatch,
`expected whole pattern() call hoisted to module scope. Output was:\n${output}`,
);
const hoistedName = hoistedMatch[1]!;
// The mapWithPattern call site references the hoisted name as its first
// argument (callback no longer inline at the site).
assertMatch(
normalized,
new RegExp(`mapWithPattern\\(\\s*${hoistedName}\\b`),
);
// And the CT-1585 callback-hoist no longer fires for pattern (no separate
// hardened `__cfModuleCallback_N` wrapper for this callback).
assertNotMatch(
normalized,
/const __cfModuleCallback_\d+ = __cfHardenFn\(\(?__cf_pattern_input\b/,
);
});