@savvy-web/tsdown-plugins 0.11.2 → 0.12.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.
@@ -1,3 +1,5 @@
1
+ import { analyzeReexportBarrel, collectExportNames, renderReexportStub } from "../dts/reexport-stub.js";
2
+ import { writeDtsEmitTsconfig } from "../dts/resolved-tsconfig.js";
1
3
  import { emitManifest } from "../manifest/emit-manifest.js";
2
4
  import { buildMetricsPlugin } from "../report/metrics-plugin.js";
3
5
  import { createTimer } from "../report/timer.js";
@@ -6,10 +8,17 @@ import { cjsDefaultInterop } from "./cjs-default-interop.js";
6
8
  import { nodeBuiltinDefaultInterop } from "./node-builtin-default-interop.js";
7
9
  import { syncPublicDir } from "./sync-public.js";
8
10
  import { deriveDeclarationsPassOptions, deriveDtsPassOptions, deriveTargetGroupOptions, outDirFor } from "./target-groups.js";
9
- import { dirname, join } from "node:path";
11
+ import { readFileSync, writeFileSync } from "node:fs";
12
+ import { dirname, isAbsolute, join } from "node:path";
10
13
 
11
14
  //#region src/build/build-target-groups.ts
12
15
  /**
16
+ * Entry name of the conventional primary (".") export, which `createEntryName` maps to "index". It is
17
+ * the re-export-stub base: a secondary entry that re-exports a subset of it gets a thin stub pointing
18
+ * here instead of a self-contained rollup.
19
+ */
20
+ const STUB_BASE_ENTRY = "index";
21
+ /**
13
22
  * Run tsdown.build() per TargetGroup. Composable so the escape hatch gets multi-group too.
14
23
  *
15
24
  * Each group runs TWO passes to the SAME outDir:
@@ -28,6 +37,7 @@ import { dirname, join } from "node:path";
28
37
  async function buildTargetGroups(options) {
29
38
  const build = options.build ?? (await import("tsdown")).build;
30
39
  const publicDir = join(options.cwd, "public");
40
+ const dtsEmitTsconfigPath = writeDtsEmitTsconfig(options.tsconfigPath);
31
41
  const collector = options.collector;
32
42
  const verbose = options.verbose ?? false;
33
43
  const instrument = (groupId) => collector === void 0 ? {} : {
@@ -68,7 +78,7 @@ async function buildTargetGroups(options) {
68
78
  cwd: options.cwd,
69
79
  version: options.version,
70
80
  entry: part.entry,
71
- tsconfigPath: options.tsconfigPath,
81
+ tsconfigPath: dtsEmitTsconfigPath,
72
82
  devManifest: options.devManifest,
73
83
  ...partExternals !== void 0 ? { externals: partExternals } : {},
74
84
  ...partBundledPackages !== void 0 ? { bundledPackages: partBundledPackages } : {},
@@ -133,36 +143,91 @@ async function buildTargetGroups(options) {
133
143
  if (isBase) syncPublicDir(publicDir, join(js.outDir, "public"));
134
144
  const dtsNeverBundle = [...partExternals ?? [], ...partDtsExternals ?? []];
135
145
  if (Object.keys(dts.entry).length === 0) continue;
136
- await timed(group.id, "dts", () => build({
137
- config: false,
138
- cwd: options.cwd,
139
- entry: dts.entry,
140
- outDir: partOutDir,
141
- format: dts.format,
142
- platform: dts.platform,
143
- sourcemap: dts.sourcemap,
144
- unbundle: dts.unbundle,
145
- clean: false,
146
- fixedExtension: dts.fixedExtension,
147
- dts: dts.dts,
148
- define: dts.define,
149
- ...instrument(group.id),
150
- ...dtsNeverBundle.length > 0 || partBundleNodeModules || dts.bundledPackages ? { deps: {
151
- ...dtsNeverBundle.length > 0 ? { neverBundle: dtsNeverBundle } : {},
152
- ...partBundleNodeModules ? {
153
- skipNodeModulesBundle: false,
154
- ...dts.bundledPackages ? { dts: { alwaysBundle: dts.bundledPackages } } : {}
155
- } : dts.bundledPackages ? {
156
- skipNodeModulesBundle: true,
157
- dts: { alwaysBundle: dts.bundledPackages }
158
- } : {}
159
- } } : {},
160
- plugins: [
161
- ...dts.format.includes("cjs") ? [nodeBuiltinDefaultInterop(), cjsDefaultInterop()] : [],
162
- ...options.extraPlugins ?? [],
163
- ...metricsPlugins(group.id, "dts")
164
- ]
165
- }));
146
+ const dtsDeps = dtsNeverBundle.length > 0 || partBundleNodeModules || dts.bundledPackages ? { deps: {
147
+ ...dtsNeverBundle.length > 0 ? { neverBundle: dtsNeverBundle } : {},
148
+ ...partBundleNodeModules ? {
149
+ skipNodeModulesBundle: false,
150
+ ...dts.bundledPackages ? { dts: { alwaysBundle: dts.bundledPackages } } : {}
151
+ } : dts.bundledPackages ? {
152
+ skipNodeModulesBundle: true,
153
+ dts: { alwaysBundle: dts.bundledPackages }
154
+ } : {}
155
+ } } : {};
156
+ const stubEntries = /* @__PURE__ */ new Map();
157
+ const readSource = (src) => {
158
+ try {
159
+ return readFileSync(isAbsolute(src) ? src : join(options.cwd, src), "utf-8");
160
+ } catch {
161
+ return;
162
+ }
163
+ };
164
+ const baseSrc = dts.entry[STUB_BASE_ENTRY];
165
+ if (baseSrc !== void 0 && Object.keys(dts.entry).length > 1) {
166
+ const baseSource = readSource(baseSrc);
167
+ const base = baseSource !== void 0 ? collectExportNames(baseSource) : void 0;
168
+ if (base?.complete === true) for (const [entryName, entrySource] of Object.entries(dts.entry)) {
169
+ if (entryName === STUB_BASE_ENTRY || entryName.includes("/")) continue;
170
+ const source = readSource(entrySource);
171
+ if (source === void 0) continue;
172
+ const analysis = analyzeReexportBarrel(source);
173
+ if (!analysis.isPureNamedReexportBarrel) continue;
174
+ const all = [...analysis.valueNames, ...analysis.typeNames];
175
+ if (all.length === 0 || !all.every((n) => base.names.has(n))) continue;
176
+ stubEntries.set(entryName, {
177
+ valueNames: analysis.valueNames,
178
+ typeNames: analysis.typeNames
179
+ });
180
+ }
181
+ }
182
+ await timed(group.id, "dts", async () => {
183
+ for (const [entryName, entrySource] of Object.entries(dts.entry)) {
184
+ const stub = stubEntries.get(entryName);
185
+ if (stub !== void 0) {
186
+ const variants = [{
187
+ file: `${entryName}.d.ts`,
188
+ spec: `./${STUB_BASE_ENTRY}.js`
189
+ }];
190
+ if (dts.format.includes("cjs")) variants.push({
191
+ file: `${entryName}.d.cts`,
192
+ spec: `./${STUB_BASE_ENTRY}.cjs`
193
+ });
194
+ for (const variant of variants) {
195
+ const content = renderReexportStub({
196
+ valueNames: stub.valueNames,
197
+ typeNames: stub.typeNames,
198
+ baseSpecifier: variant.spec
199
+ });
200
+ writeFileSync(join(partOutDir, variant.file), content, "utf-8");
201
+ collector?.recordEmitted(group.id, "dts", {
202
+ path: variant.file,
203
+ bytes: Buffer.byteLength(content)
204
+ });
205
+ }
206
+ continue;
207
+ }
208
+ await build({
209
+ config: false,
210
+ cwd: options.cwd,
211
+ entry: { [entryName]: entrySource },
212
+ outDir: partOutDir,
213
+ format: dts.format,
214
+ platform: dts.platform,
215
+ sourcemap: dts.sourcemap,
216
+ unbundle: dts.unbundle,
217
+ clean: false,
218
+ fixedExtension: dts.fixedExtension,
219
+ dts: dts.dts,
220
+ define: dts.define,
221
+ ...instrument(group.id),
222
+ ...dtsDeps,
223
+ plugins: [
224
+ ...dts.format.includes("cjs") ? [nodeBuiltinDefaultInterop(), cjsDefaultInterop()] : [],
225
+ ...options.extraPlugins ?? [],
226
+ ...metricsPlugins(group.id, "dts")
227
+ ]
228
+ });
229
+ }
230
+ });
166
231
  if (js.isProd && options.emitDeclarations === true && Object.keys(decl.entry).length > 0) {
167
232
  const partDeclDir = part.outSubdir !== void 0 ? join(decl.outDir, part.outSubdir) : decl.outDir;
168
233
  try {
@@ -1,5 +1,5 @@
1
- import { join } from "node:path";
2
1
  import { readdirSync, rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
3
 
4
4
  //#region src/build/strip-maps.ts
5
5
  /**
@@ -1,5 +1,5 @@
1
- import { dirname, join, relative } from "node:path";
2
1
  import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
2
+ import { dirname, join, relative } from "node:path";
3
3
 
4
4
  //#region src/build/sync-public.ts
5
5
  /** Recursively collect every file path under `dir`, relative to `base`. */
@@ -3,8 +3,8 @@ import { normalizeLooseFiles } from "../build/loose-files.js";
3
3
  import { ConfigValidator } from "./ConfigValidator.js";
4
4
  import { normalizeExeOptions } from "../exe/config.js";
5
5
  import { resolveTargets } from "../targets/resolve-targets.js";
6
- import { Effect, Layer } from "effect";
7
6
  import { existsSync, statSync } from "node:fs";
7
+ import { Effect, Layer } from "effect";
8
8
 
9
9
  //#region src/config-validation/ConfigValidatorLive.ts
10
10
  const VALID_SYNTAX_KINDS = /* @__PURE__ */ new Set([
@@ -0,0 +1,92 @@
1
+ import ts from "typescript";
2
+
3
+ //#region src/dts/reexport-stub.ts
4
+ function parse(source, fileName) {
5
+ return ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
6
+ }
7
+ /**
8
+ * Analyze an entry source as a candidate pure re-export barrel: classify its re-exported names into
9
+ * value vs type-only and report whether it is expressible as a thin stub. Pure parsing — no I/O.
10
+ *
11
+ * @public
12
+ */
13
+ function analyzeReexportBarrel(source, fileName = "entry.ts") {
14
+ const sf = parse(source, fileName);
15
+ const valueNames = [];
16
+ const typeNames = [];
17
+ let pure = true;
18
+ for (const stmt of sf.statements) {
19
+ if (ts.isExportDeclaration(stmt) && stmt.moduleSpecifier !== void 0) {
20
+ const clause = stmt.exportClause;
21
+ if (clause !== void 0 && ts.isNamedExports(clause)) {
22
+ for (const el of clause.elements) (stmt.isTypeOnly || el.isTypeOnly ? typeNames : valueNames).push(el.name.text);
23
+ continue;
24
+ }
25
+ pure = false;
26
+ continue;
27
+ }
28
+ pure = false;
29
+ }
30
+ return {
31
+ valueNames,
32
+ typeNames,
33
+ isPureNamedReexportBarrel: pure
34
+ };
35
+ }
36
+ /**
37
+ * Collect every name a module exports (named re-exports, namespace re-exports, and local `export`
38
+ * declarations). Used to test whether a barrel's re-exports are a strict subset of a base entry, so
39
+ * a stub re-exporting from that base resolves every symbol. Pure parsing — no I/O.
40
+ *
41
+ * @public
42
+ */
43
+ function collectExportNames(source, fileName = "entry.ts") {
44
+ const sf = parse(source, fileName);
45
+ const names = /* @__PURE__ */ new Set();
46
+ let complete = true;
47
+ const addBindingName = (name) => {
48
+ if (ts.isIdentifier(name)) names.add(name.text);
49
+ else for (const el of name.elements) if (ts.isBindingElement(el)) addBindingName(el.name);
50
+ };
51
+ for (const stmt of sf.statements) {
52
+ if (ts.isExportDeclaration(stmt)) {
53
+ const clause = stmt.exportClause;
54
+ if (clause === void 0) {
55
+ if (stmt.moduleSpecifier !== void 0) complete = false;
56
+ continue;
57
+ }
58
+ if (ts.isNamespaceExport(clause)) {
59
+ names.add(clause.name.text);
60
+ continue;
61
+ }
62
+ for (const el of clause.elements) names.add(el.name.text);
63
+ continue;
64
+ }
65
+ if (!(ts.canHaveModifiers(stmt) ? (ts.getModifiers(stmt) ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword) : false)) continue;
66
+ if (ts.isVariableStatement(stmt)) for (const decl of stmt.declarationList.declarations) addBindingName(decl.name);
67
+ else if ((ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt) || ts.isInterfaceDeclaration(stmt) || ts.isTypeAliasDeclaration(stmt) || ts.isEnumDeclaration(stmt) || ts.isModuleDeclaration(stmt)) && stmt.name !== void 0 && ts.isIdentifier(stmt.name)) names.add(stmt.name.text);
68
+ }
69
+ return {
70
+ names,
71
+ complete
72
+ };
73
+ }
74
+ /**
75
+ * Render a thin re-export-stub `.d.ts`/`.d.cts` body: named re-exports of `valueNames` and
76
+ * `typeNames` from `baseSpecifier` (the published file of the base entry, e.g. `./index.js` for the
77
+ * ESM `.d.ts` or `./index.cjs` for the CJS `.d.cts`). Names are sorted so the output is
78
+ * deterministic. Returns the empty string when there is nothing to re-export.
79
+ *
80
+ * @public
81
+ */
82
+ function renderReexportStub(options) {
83
+ const sortedValues = [...options.valueNames].sort();
84
+ const sortedTypes = [...options.typeNames].sort();
85
+ const lines = [];
86
+ if (sortedValues.length > 0) lines.push(`export { ${sortedValues.join(", ")} } from "${options.baseSpecifier}";`);
87
+ if (sortedTypes.length > 0) lines.push(`export type { ${sortedTypes.join(", ")} } from "${options.baseSpecifier}";`);
88
+ return lines.length > 0 ? `${lines.join("\n")}\n` : "";
89
+ }
90
+
91
+ //#endregion
92
+ export { analyzeReexportBarrel, collectExportNames, renderReexportStub };
@@ -1,5 +1,5 @@
1
- import { join } from "node:path";
2
- import { writeFileSync } from "node:fs";
1
+ import { existsSync, writeFileSync } from "node:fs";
2
+ import { isAbsolute, join, resolve } from "node:path";
3
3
  import { tmpdir } from "node:os";
4
4
 
5
5
  //#region src/dts/resolved-tsconfig.ts
@@ -47,6 +47,33 @@ function writeResolvedTsconfig(options) {
47
47
  writeFileSync(path, `${JSON.stringify(cfg, null, " ")}\n`, "utf-8");
48
48
  return path;
49
49
  }
50
+ /**
51
+ * Derive a dts-EMIT variant of an already-written resolved tsconfig that adds
52
+ * `stableTypeOrdering: true`, and return its path. This makes the TypeScript declaration emitter
53
+ * (rolldown-plugin-dts on `typescript@6`) order union/type members deterministically, so a
54
+ * multi-union `.d.ts` (e.g. an Effect `Layer.Layer<…>` requirement channel) does not flip member
55
+ * order across otherwise-identical builds (#156). It is kept in a SEPARATE file from the
56
+ * api-extractor tsconfig on purpose: `@microsoft/api-extractor` pins `typescript ~5.9`, which
57
+ * predates the flag and hard-errors on the unknown compiler option — so only the emit passes
58
+ * (which run on TS6) ever see it, while the api-extractor compile reads the original clean config.
59
+ *
60
+ * Best-effort: if the base tsconfig cannot be read or parsed (e.g. a synthetic test path that was
61
+ * never written), the original path is returned unchanged — the emit then simply keeps TS's
62
+ * default ordering rather than aborting the build at this layer.
63
+ *
64
+ * @public
65
+ */
66
+ function writeDtsEmitTsconfig(resolvedTsconfigPath) {
67
+ const absBase = isAbsolute(resolvedTsconfigPath) ? resolvedTsconfigPath : resolve(resolvedTsconfigPath);
68
+ if (!existsSync(absBase)) return resolvedTsconfigPath;
69
+ const cfg = {
70
+ extends: absBase,
71
+ compilerOptions: { stableTypeOrdering: true }
72
+ };
73
+ const path = join(tmpdir(), `tsconfig-dts-emit-${process.pid}-${absBase.replace(/[^\w]/g, "_")}.json`);
74
+ writeFileSync(path, `${JSON.stringify(cfg, null, " ")}\n`, "utf-8");
75
+ return path;
76
+ }
50
77
 
51
78
  //#endregion
52
- export { buildResolvedTsconfig, writeResolvedTsconfig };
79
+ export { buildResolvedTsconfig, writeDtsEmitTsconfig, writeResolvedTsconfig };
@@ -1,6 +1,6 @@
1
1
  import { extractEntries } from "./extract.js";
2
- import { resolve } from "node:path";
3
2
  import { readFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
4
 
5
5
  //#region src/entry/package-json-entries.ts
6
6
  /**
package/exe/build.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { buildMetricsPlugin } from "../report/metrics-plugin.js";
2
2
  import { createTimer } from "../report/timer.js";
3
3
  import { createTsdownLogger } from "../report/tsdown-logger.js";
4
- import { join } from "node:path";
5
4
  import { mkdtempSync, rmSync } from "node:fs";
5
+ import { join } from "node:path";
6
6
  import { tmpdir } from "node:os";
7
7
 
8
8
  //#region src/exe/build.ts
package/index.d.ts CHANGED
@@ -177,21 +177,21 @@ declare const DiagnosticEntry_base: Schema.Class<DiagnosticEntry, {
177
177
  line: Schema.optional<typeof Schema.Number>;
178
178
  column: Schema.optional<typeof Schema.Number>;
179
179
  }>, never, {
180
- readonly text: string;
181
- } & {
182
- readonly source: "tsdown" | "rolldown" | "api-extractor";
183
- } & {
184
- readonly level: "warn" | "error";
180
+ readonly ciFatal?: boolean | undefined;
185
181
  } & {
186
182
  readonly code?: string | undefined;
187
183
  } & {
188
- readonly ciFatal?: boolean | undefined;
184
+ readonly column?: number | undefined;
189
185
  } & {
190
186
  readonly file?: string | undefined;
191
187
  } & {
192
188
  readonly line?: number | undefined;
193
189
  } & {
194
- readonly column?: number | undefined;
190
+ readonly level: "error" | "warn";
191
+ } & {
192
+ readonly source: "api-extractor" | "rolldown" | "tsdown";
193
+ } & {
194
+ readonly text: string;
195
195
  }, {}, {}>;
196
196
  /**
197
197
  * A captured warning or error, from tsdown's logger, rolldown's onLog, or API Extractor.
@@ -208,11 +208,11 @@ declare const EmittedFile_base: Schema.Class<EmittedFile, {
208
208
  bytes: typeof Schema.Number;
209
209
  gzip: Schema.optional<typeof Schema.Number>;
210
210
  }>, never, {
211
- readonly path: string;
211
+ readonly gzip?: number | undefined;
212
212
  } & {
213
213
  readonly bytes: number;
214
214
  } & {
215
- readonly gzip?: number | undefined;
215
+ readonly path: string;
216
216
  }, {}, {}>;
217
217
  /**
218
218
  * One emitted output file with its in-memory byte size (gzip only when --verbose).
@@ -229,11 +229,11 @@ declare const PassReport_base: Schema.Class<PassReport, {
229
229
  files: Schema.Array$<typeof EmittedFile>;
230
230
  ms: typeof Schema.Number;
231
231
  }>, never, {
232
- readonly ms: number;
232
+ readonly files: readonly EmittedFile[];
233
233
  } & {
234
- readonly id: "js" | "dts" | "loose" | "exe" | "meta";
234
+ readonly id: "dts" | "exe" | "js" | "loose" | "meta";
235
235
  } & {
236
- readonly files: readonly EmittedFile[];
236
+ readonly ms: number;
237
237
  }, {}, {}>;
238
238
  /**
239
239
  * One build pass within a target group (js / dts / loose / exe / meta).
@@ -260,17 +260,17 @@ declare const TargetGroupReport_base: Schema.Class<TargetGroupReport, {
260
260
  }>, never, {
261
261
  readonly entries: readonly string[];
262
262
  } & {
263
- readonly id: string;
263
+ readonly errors: readonly DiagnosticEntry[];
264
264
  } & {
265
- readonly timings: ReportTimings;
265
+ readonly id: string;
266
266
  } & {
267
267
  readonly passes: readonly PassReport[];
268
268
  } & {
269
- readonly warnings: readonly DiagnosticEntry[];
269
+ readonly suppressed: readonly DiagnosticEntry[];
270
270
  } & {
271
- readonly errors: readonly DiagnosticEntry[];
271
+ readonly timings: ReportTimings;
272
272
  } & {
273
- readonly suppressed: readonly DiagnosticEntry[];
273
+ readonly warnings: readonly DiagnosticEntry[];
274
274
  }, {}, {}>;
275
275
  /** @public */
276
276
  declare class TargetGroupReport extends TargetGroupReport_base {}
@@ -1109,6 +1109,69 @@ declare class ConfigValidator extends ConfigValidator_base {}
1109
1109
  */
1110
1110
  declare const ConfigValidatorLive: Layer.Layer<ConfigValidator, never, never>;
1111
1111
  //#endregion
1112
+ //#region src/dts/reexport-stub.d.ts
1113
+ /**
1114
+ * The analysis of an entry source treated as a candidate re-export barrel.
1115
+ *
1116
+ * @public
1117
+ */
1118
+ interface ReexportBarrelAnalysis {
1119
+ /** Value (non-type-only) names the module re-exports, after `as` aliasing. */
1120
+ readonly valueNames: ReadonlyArray<string>;
1121
+ /** Type-only names the module re-exports (`export type { … }`), after `as` aliasing. */
1122
+ readonly typeNames: ReadonlyArray<string>;
1123
+ /**
1124
+ * True iff EVERY top-level statement is a NAMED re-export `from` another module
1125
+ * (`export { … } from "…"` / `export type { … } from "…"`). A module that declares anything
1126
+ * locally, re-exports a namespace (`export * as NS from`), star-re-exports (`export * from`), or
1127
+ * has a bare `export { … }` without `from` is NOT a pure named barrel and cannot be expressed as
1128
+ * a thin re-export stub of another entry.
1129
+ */
1130
+ readonly isPureNamedReexportBarrel: boolean;
1131
+ }
1132
+ /**
1133
+ * The set of names a module exports, plus whether that set is fully known.
1134
+ *
1135
+ * @public
1136
+ */
1137
+ interface ModuleExportNames {
1138
+ readonly names: ReadonlySet<string>;
1139
+ /**
1140
+ * False when the module contains a star re-export (`export * from "…"`) whose target exports
1141
+ * cannot be enumerated from this source alone — the name set is then a lower bound, not complete,
1142
+ * so callers must not use it for a strict subset decision.
1143
+ */
1144
+ readonly complete: boolean;
1145
+ }
1146
+ /**
1147
+ * Analyze an entry source as a candidate pure re-export barrel: classify its re-exported names into
1148
+ * value vs type-only and report whether it is expressible as a thin stub. Pure parsing — no I/O.
1149
+ *
1150
+ * @public
1151
+ */
1152
+ declare function analyzeReexportBarrel(source: string, fileName?: string): ReexportBarrelAnalysis;
1153
+ /**
1154
+ * Collect every name a module exports (named re-exports, namespace re-exports, and local `export`
1155
+ * declarations). Used to test whether a barrel's re-exports are a strict subset of a base entry, so
1156
+ * a stub re-exporting from that base resolves every symbol. Pure parsing — no I/O.
1157
+ *
1158
+ * @public
1159
+ */
1160
+ declare function collectExportNames(source: string, fileName?: string): ModuleExportNames;
1161
+ /**
1162
+ * Render a thin re-export-stub `.d.ts`/`.d.cts` body: named re-exports of `valueNames` and
1163
+ * `typeNames` from `baseSpecifier` (the published file of the base entry, e.g. `./index.js` for the
1164
+ * ESM `.d.ts` or `./index.cjs` for the CJS `.d.cts`). Names are sorted so the output is
1165
+ * deterministic. Returns the empty string when there is nothing to re-export.
1166
+ *
1167
+ * @public
1168
+ */
1169
+ declare function renderReexportStub(options: {
1170
+ readonly valueNames: ReadonlyArray<string>;
1171
+ readonly typeNames: ReadonlyArray<string>;
1172
+ readonly baseSpecifier: string;
1173
+ }): string;
1174
+ //#endregion
1112
1175
  //#region src/dts/resolved-tsconfig.d.ts
1113
1176
  /** @public */
1114
1177
  interface ResolvedTsconfigOptions {
@@ -1139,6 +1202,23 @@ declare function buildResolvedTsconfig(options: ResolvedTsconfigOptions): Resolv
1139
1202
  * @public
1140
1203
  */
1141
1204
  declare function writeResolvedTsconfig(options: ResolvedTsconfigOptions): string;
1205
+ /**
1206
+ * Derive a dts-EMIT variant of an already-written resolved tsconfig that adds
1207
+ * `stableTypeOrdering: true`, and return its path. This makes the TypeScript declaration emitter
1208
+ * (rolldown-plugin-dts on `typescript@6`) order union/type members deterministically, so a
1209
+ * multi-union `.d.ts` (e.g. an Effect `Layer.Layer<…>` requirement channel) does not flip member
1210
+ * order across otherwise-identical builds (#156). It is kept in a SEPARATE file from the
1211
+ * api-extractor tsconfig on purpose: `@microsoft/api-extractor` pins `typescript ~5.9`, which
1212
+ * predates the flag and hard-errors on the unknown compiler option — so only the emit passes
1213
+ * (which run on TS6) ever see it, while the api-extractor compile reads the original clean config.
1214
+ *
1215
+ * Best-effort: if the base tsconfig cannot be read or parsed (e.g. a synthetic test path that was
1216
+ * never written), the original path is returned unchanged — the emit then simply keeps TS's
1217
+ * default ordering rather than aborting the build at this layer.
1218
+ *
1219
+ * @public
1220
+ */
1221
+ declare function writeDtsEmitTsconfig(resolvedTsconfigPath: string): string;
1142
1222
  //#endregion
1143
1223
  //#region src/entry/extract.d.ts
1144
1224
  /** @public */
@@ -1718,5 +1798,5 @@ declare function resolveTargets(options: {
1718
1798
  baseName: string;
1719
1799
  }): TargetResolution;
1720
1800
  //#endregion
1721
- export { BuildCollector, BuildCollectorTag, type BuildEmittedManifestOptions, type BuildFormat, type BuildGroupSpec, type BuildIssues, type BuildPlatform, type BuildReport, BuildReport as BuildReportSchema, type BuildTargetGroupsOptions, CatalogAssemblyError, CatalogResolutionError, CiAnnotationsFormatter, ConfigValidationError, ConfigValidator, ConfigValidatorLive, type CssOptions, DEFAULT_EXE_NODE_VERSION, type DeriveOptions, type DerivedTsdownOptions, type DiagnosticEntry, type DiagnosticInput, type DualExports, type EmitManifestOptions, type EmittedFile, type EntryOverride, type Environment, EnvironmentDetector, EnvironmentDetectorLive, type ExeBuild, type ExeConfig, type ExeRewrite, type ExeSeaConfig, type ExeTarget, type ExeTargetInput, type Executor, ExecutorResolver, ExecutorResolverLive, type ExtractOptions, type ExtractResult, FormatSelector, FormatSelectorLive, type Formatter, type FormatterContext, type GenerateMetaOptions, type Json, JsonFormatter, type JsxConfig, type LooseFileSpec, type LooseFiles, type ManifestLike, MarkdownFormatter, MetaGenerationError, type MetaOptions, type MetaResult, type NextVersions, type NormalizedExe, type NormalizedLooseFile, type NormalizedMeta, type OutputFormat, OutputRenderer, OutputRendererLive, type PackageJsonEntriesOptions, type PackageJsonLike, type PassKind, type PassReport, type PkgOsCpu, type PlainDiagnostic, type PortableTsconfig, type PublishTargetObject, type PublishTargetValue, type PublishTargets, type RenderReportOptions, type RenderedOutput, ReportPipelineLive, ReportTimings, type ResolvedCompilerOptions, type ResolvedGroup, type ResolvedTarget, type ResolvedTsconfig, type ResolvedTsconfigOptions, type RunExeBuildOptions, type RunMetaPassOptions, SilentFormatter, type TargetGroupId, type TargetGroupRef, type TargetGroupReport, TargetGroupReport as TargetGroupReportSchema, type TargetResolution, TerminalFormatter, type Timer, type TransformManifestOptions, type TsconfigJsx, TsconfigResolver, type TsdocOptions, type TsdocTagDefinition, type TsdownBuild, type TsdownLogger, type ValidationInput, type WarningSuppressionRule, applySubdirMetaEntries, buildEmittedManifest, buildMetricsPlugin, buildResolvedTsconfig, buildTargetGroups, cjsDefaultInterop, computeExeFileName, createEntryName, createTimer, createTsdownLogger, defaultManifestTransform, deriveExportPaths, deriveTargetGroupOptions, emitManifest, extractEntries, flattenIssues, formatTime, generateMeta, isTargetObject, nodeBuiltinDefaultInterop, normalizeBinPaths, normalizeExeOptions, normalizeLooseFiles, normalizeMetaOptions, packageJsonEntries, readTsconfigJsx, removeDeclarationMaps, renderReport, resolveJsxConfig, resolveManifest, resolveNextVersions, resolvePortableTsconfig, resolveTargets, rewriteMetaVersions, runExeBuild, runMetaPass, serializeIssues, syncPublicDir, transformBin, transformExports, transformManifest, writeIssuesArtifact, writeResolvedTsconfig, writeTargetsBinding };
1801
+ export { BuildCollector, BuildCollectorTag, type BuildEmittedManifestOptions, type BuildFormat, type BuildGroupSpec, type BuildIssues, type BuildPlatform, type BuildReport, BuildReport as BuildReportSchema, type BuildTargetGroupsOptions, CatalogAssemblyError, CatalogResolutionError, CiAnnotationsFormatter, ConfigValidationError, ConfigValidator, ConfigValidatorLive, type CssOptions, DEFAULT_EXE_NODE_VERSION, type DeriveOptions, type DerivedTsdownOptions, type DiagnosticEntry, type DiagnosticInput, type DualExports, type EmitManifestOptions, type EmittedFile, type EntryOverride, type Environment, EnvironmentDetector, EnvironmentDetectorLive, type ExeBuild, type ExeConfig, type ExeRewrite, type ExeSeaConfig, type ExeTarget, type ExeTargetInput, type Executor, ExecutorResolver, ExecutorResolverLive, type ExtractOptions, type ExtractResult, FormatSelector, FormatSelectorLive, type Formatter, type FormatterContext, type GenerateMetaOptions, type Json, JsonFormatter, type JsxConfig, type LooseFileSpec, type LooseFiles, type ManifestLike, MarkdownFormatter, MetaGenerationError, type MetaOptions, type MetaResult, type ModuleExportNames, type NextVersions, type NormalizedExe, type NormalizedLooseFile, type NormalizedMeta, type OutputFormat, OutputRenderer, OutputRendererLive, type PackageJsonEntriesOptions, type PackageJsonLike, type PassKind, type PassReport, type PkgOsCpu, type PlainDiagnostic, type PortableTsconfig, type PublishTargetObject, type PublishTargetValue, type PublishTargets, type ReexportBarrelAnalysis, type RenderReportOptions, type RenderedOutput, ReportPipelineLive, ReportTimings, type ResolvedCompilerOptions, type ResolvedGroup, type ResolvedTarget, type ResolvedTsconfig, type ResolvedTsconfigOptions, type RunExeBuildOptions, type RunMetaPassOptions, SilentFormatter, type TargetGroupId, type TargetGroupRef, type TargetGroupReport, TargetGroupReport as TargetGroupReportSchema, type TargetResolution, TerminalFormatter, type Timer, type TransformManifestOptions, type TsconfigJsx, TsconfigResolver, type TsdocOptions, type TsdocTagDefinition, type TsdownBuild, type TsdownLogger, type ValidationInput, type WarningSuppressionRule, analyzeReexportBarrel, applySubdirMetaEntries, buildEmittedManifest, buildMetricsPlugin, buildResolvedTsconfig, buildTargetGroups, cjsDefaultInterop, collectExportNames, computeExeFileName, createEntryName, createTimer, createTsdownLogger, defaultManifestTransform, deriveExportPaths, deriveTargetGroupOptions, emitManifest, extractEntries, flattenIssues, formatTime, generateMeta, isTargetObject, nodeBuiltinDefaultInterop, normalizeBinPaths, normalizeExeOptions, normalizeLooseFiles, normalizeMetaOptions, packageJsonEntries, readTsconfigJsx, removeDeclarationMaps, renderReexportStub, renderReport, resolveJsxConfig, resolveManifest, resolveNextVersions, resolvePortableTsconfig, resolveTargets, rewriteMetaVersions, runExeBuild, runMetaPass, serializeIssues, syncPublicDir, transformBin, transformExports, transformManifest, writeDtsEmitTsconfig, writeIssuesArtifact, writeResolvedTsconfig, writeTargetsBinding };
1722
1802
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { analyzeReexportBarrel, collectExportNames, renderReexportStub } from "./dts/reexport-stub.js";
2
+ import { buildResolvedTsconfig, writeDtsEmitTsconfig, writeResolvedTsconfig } from "./dts/resolved-tsconfig.js";
1
3
  import { resolveManifest } from "./catalog/resolve-catalogs.js";
2
4
  import { createEntryName, extractEntries } from "./entry/extract.js";
3
5
  import { defaultManifestTransform, normalizeBinPaths, transformBin, transformExports, transformManifest } from "./manifest/transform.js";
@@ -19,7 +21,6 @@ import { DEFAULT_EXE_NODE_VERSION, normalizeExeOptions } from "./exe/config.js";
19
21
  import { isTargetObject } from "./targets/config.js";
20
22
  import { resolveTargets } from "./targets/resolve-targets.js";
21
23
  import { ConfigValidatorLive } from "./config-validation/ConfigValidatorLive.js";
22
- import { buildResolvedTsconfig, writeResolvedTsconfig } from "./dts/resolved-tsconfig.js";
23
24
  import { packageJsonEntries } from "./entry/package-json-entries.js";
24
25
  import { runExeBuild } from "./exe/build.js";
25
26
  import { computeExeFileName } from "./exe/filename.js";
@@ -49,4 +50,4 @@ import { ReportPipelineLive, renderReport } from "./report/pipeline.js";
49
50
  import { writeTargetsBinding } from "./targets/binding.js";
50
51
  import { CatalogAssemblyError, CatalogResolutionError } from "workspaces-effect";
51
52
 
52
- export { BuildCollector, BuildCollectorTag, BuildReport as BuildReportSchema, CatalogAssemblyError, CatalogResolutionError, CiAnnotationsFormatter, ConfigValidationError, ConfigValidator, ConfigValidatorLive, DEFAULT_EXE_NODE_VERSION, EnvironmentDetector, EnvironmentDetectorLive, ExecutorResolver, ExecutorResolverLive, FormatSelector, FormatSelectorLive, JsonFormatter, MarkdownFormatter, MetaGenerationError, OutputRenderer, OutputRendererLive, ReportPipelineLive, ReportTimings, SilentFormatter, TargetGroupReport as TargetGroupReportSchema, TerminalFormatter, TsconfigResolver, applySubdirMetaEntries, buildEmittedManifest, buildMetricsPlugin, buildResolvedTsconfig, buildTargetGroups, cjsDefaultInterop, computeExeFileName, createEntryName, createTimer, createTsdownLogger, defaultManifestTransform, deriveExportPaths, deriveTargetGroupOptions, emitManifest, extractEntries, flattenIssues, formatTime, generateMeta, isTargetObject, nodeBuiltinDefaultInterop, normalizeBinPaths, normalizeExeOptions, normalizeLooseFiles, normalizeMetaOptions, packageJsonEntries, readTsconfigJsx, removeDeclarationMaps, renderReport, resolveJsxConfig, resolveManifest, resolveNextVersions, resolvePortableTsconfig, resolveTargets, rewriteMetaVersions, runExeBuild, runMetaPass, serializeIssues, syncPublicDir, transformBin, transformExports, transformManifest, writeIssuesArtifact, writeResolvedTsconfig, writeTargetsBinding };
53
+ export { BuildCollector, BuildCollectorTag, BuildReport as BuildReportSchema, CatalogAssemblyError, CatalogResolutionError, CiAnnotationsFormatter, ConfigValidationError, ConfigValidator, ConfigValidatorLive, DEFAULT_EXE_NODE_VERSION, EnvironmentDetector, EnvironmentDetectorLive, ExecutorResolver, ExecutorResolverLive, FormatSelector, FormatSelectorLive, JsonFormatter, MarkdownFormatter, MetaGenerationError, OutputRenderer, OutputRendererLive, ReportPipelineLive, ReportTimings, SilentFormatter, TargetGroupReport as TargetGroupReportSchema, TerminalFormatter, TsconfigResolver, analyzeReexportBarrel, applySubdirMetaEntries, buildEmittedManifest, buildMetricsPlugin, buildResolvedTsconfig, buildTargetGroups, cjsDefaultInterop, collectExportNames, computeExeFileName, createEntryName, createTimer, createTsdownLogger, defaultManifestTransform, deriveExportPaths, deriveTargetGroupOptions, emitManifest, extractEntries, flattenIssues, formatTime, generateMeta, isTargetObject, nodeBuiltinDefaultInterop, normalizeBinPaths, normalizeExeOptions, normalizeLooseFiles, normalizeMetaOptions, packageJsonEntries, readTsconfigJsx, removeDeclarationMaps, renderReexportStub, renderReport, resolveJsxConfig, resolveManifest, resolveNextVersions, resolvePortableTsconfig, resolveTargets, rewriteMetaVersions, runExeBuild, runMetaPass, serializeIssues, syncPublicDir, transformBin, transformExports, transformManifest, writeDtsEmitTsconfig, writeIssuesArtifact, writeResolvedTsconfig, writeTargetsBinding };
package/jsx/config.js CHANGED
@@ -1,5 +1,5 @@
1
- import { join } from "node:path";
2
1
  import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
3
 
4
4
  //#region src/jsx/config.ts
5
5
  /**
package/meta/generate.js CHANGED
@@ -2,8 +2,8 @@ import { runApiExtractor } from "./api-extractor.js";
2
2
  import { mergeApiModels } from "./merge-models.js";
3
3
  import { resolvePortableTsconfig } from "./tsconfig-resolver.js";
4
4
  import { writeTsdocConfig } from "./tsdoc-config.js";
5
- import { join } from "node:path";
6
5
  import { copyFileSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
+ import { join } from "node:path";
7
7
 
8
8
  //#region src/meta/generate.ts
9
9
  function unscopedName(name) {
@@ -1,5 +1,5 @@
1
- import { join } from "node:path";
2
1
  import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
3
  import { JsxEmit, ModuleDetectionKind, ModuleKind, ModuleResolutionKind, NewLineKind, ScriptTarget, flattenDiagnosticMessageText, getParsedCommandLineOfConfigFile, sys } from "typescript";
4
4
 
5
5
  //#region src/meta/tsconfig-resolver.ts
@@ -1,5 +1,5 @@
1
- import { join } from "node:path";
2
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
3
  import { StandardTags } from "@microsoft/tsdoc";
4
4
  import deepEqual from "deep-equal";
5
5
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/tsdown-plugins",
3
- "version": "0.11.2",
3
+ "version": "0.12.0",
4
4
  "private": false,
5
5
  "description": "Interface-only tsdown/rolldown plugin pack powering @savvy-web/bundler",
6
6
  "homepage": "https://github.com/savvy-web/systems/tree/main/packages/tsdown-plugins",
@@ -1,5 +1,5 @@
1
- import { dirname, join } from "node:path";
2
1
  import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
3
 
4
4
  //#region src/report/issues-artifact.ts
5
5
  /** Copy a DiagnosticEntry to a plain object, omitting undefined optionals for stable output. */
@@ -1,5 +1,5 @@
1
- import { join } from "node:path";
2
1
  import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
3
 
4
4
  //#region src/targets/binding.ts
5
5
  /**