Initialize import cycles that re-export a member with export *

An `export * from` declaration binds each re-exported name to the binding
of the module that declares it, like a named re-export, so it has no
initialization state of its own. Import cycles whose modules re-export
another member of the cycle this way now compile with the exact cycle
initialization: re-exported names read the same values, and throw the
same temporal dead zone errors, as in Node. `export * as ns` of a cycle
member keeps the SC1016 refusal.

pi-coding-agent's sdk.js (sdk.js → agent-session-runtime.js →
agent-session-services.js → sdk.js) has this shape.

Refs #1777.
This commit is contained in:
Chris Tate
2026-10-24 00:24:12 -05:00
parent de5557ce5a
commit fd768cd914
7 changed files with 79 additions and 1 deletions
+5 -1
View File
@@ -3927,10 +3927,14 @@ function exactCycleRefusal7(
if (ts.isExportAssignment(stmt) && !stmt.isExportEquals) {
return `the default export expression at ${lineOf(stmt)} has no initialization state inside the cycle`;
}
// `export * from` binds each name to the declaring member's own
// binding, like a named re-export, so only `export * as ns` (a
// namespace object of a member) has state of its own.
if (
ts.isExportDeclaration(stmt) &&
!stmt.isTypeOnly &&
(stmt.exportClause === undefined || ts.isNamespaceExport(stmt.exportClause))
stmt.exportClause !== undefined &&
ts.isNamespaceExport(stmt.exportClause)
) {
const dep = targetOf(stmt);
if (dep !== null && members.has(dep)) {
@@ -0,0 +1,9 @@
{
"order": [
"<repo>/tests/corpus/8213-cycle-star-reexport/services.ts",
"<repo>/tests/corpus/8213-cycle-star-reexport/runtime.ts",
"<repo>/tests/corpus/8213-cycle-star-reexport/sdk.ts",
"<repo>/tests/corpus/8213-cycle-star-reexport/main.ts"
],
"diags": []
}
@@ -0,0 +1,11 @@
// A module of an import cycle re-exports another member with `export *`
// (pi-coding-agent's sdk.js → agent-session-runtime.js →
// agent-session-services.js → sdk.js). The re-exported names are live
// bindings of the member that declares them, so they read the same values,
// and the same temporal dead zone errors, as in Node.
import { createSession, describeRuntime, Runtime, SDK_VERSION } from "./sdk.ts";
import { servicesReport } from "./services.ts";
console.log(servicesReport());
console.log(createSession("main").describe());
console.log(describeRuntime(), new Runtime("direct").name, SDK_VERSION);
@@ -0,0 +1,17 @@
import { createServices, servicesLabel } from "./services.ts";
export class Runtime {
readonly name: string;
constructor(name: string) {
this.name = name;
}
describe(): string {
return `${this.name} runtime with ${servicesLabel}`;
}
}
export const runtimeLabel = "runtime";
export function describeRuntime(): string {
return `${runtimeLabel} (${typeof createServices})`;
}
@@ -0,0 +1,9 @@
import { createServices } from "./services.ts";
export * from "./runtime.ts";
export const SDK_VERSION = "1.4.0";
export function createSession(name: string) {
return createServices(name);
}
@@ -0,0 +1,27 @@
import { Runtime, SDK_VERSION, describeRuntime, runtimeLabel } from "./sdk.ts";
export const servicesLabel = "services";
function probe(read: () => unknown): string {
try {
return String(read());
} catch (error) {
return `${(error as Error).name}: ${(error as Error).message}`;
}
}
// This module runs first: sdk.ts and runtime.ts have not run yet.
const early = [
probe(() => SDK_VERSION),
probe(() => runtimeLabel),
probe(() => typeof describeRuntime),
probe(() => new Runtime("early").name),
];
export function servicesReport(): string {
return `early ${early.join(" | ")}; late ${SDK_VERSION} ${runtimeLabel}`;
}
export function createServices(name: string): Runtime {
return new Runtime(name);
}
@@ -0,0 +1 @@
{ "plain": 3000, "san": 4500 }