@savvy-web/tsdown-plugins 0.11.2 → 1.0.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/README.md CHANGED
@@ -41,6 +41,7 @@ export default defineConfig({
41
41
 
42
42
  - **Entry detection** — `packageJsonEntries` and `extractEntries` derive build entries from a package's `exports` and `bin`, matching the rules used across the Silk Suite builders.
43
43
  - **Manifest transforms** — `transformManifest`, `transformExports`, `transformBin` and `normalizeBinPaths` rewrite a source `package.json` into a publishable one; `emitManifest` is the rolldown plugin that writes it. A dual-format build emits both `import` and `require` export conditions, and a `"./package.json": "./package.json"` entry is added to the exports map so consumers can `import "<pkg>/package.json"`.
44
+ - **Ambient `.d.ts` exports** — `extractAmbientDts` and `classifyDtsExport` pick the types-only, hand-authored declaration exports out of a package's `exports` map; `transformExports` rewrites each to a key-derived `{ types }` pointer and `copyAmbientDts` copies the source declaration verbatim into every target dir, preserving its extension. `findRelativeSpecifiers` rejects a non-self-contained declaration and `mixedDtsExportError` rejects an export that mixes a hand-authored `types` with a runtime source.
44
45
  - **Catalog resolution** — `resolveManifest` resolves `catalog:` and `workspace:` specifiers against the workspace, delegating to `workspaces-effect`'s `CatalogResolver`.
45
46
  - **Multi-target resolution** — `resolveTargets` turns a `publishConfig.targets` map into the distinct byte-variant groups to build and the registry bindings for each; `writeTargetsBinding` persists that resolution as `dist/prod/targets.json` for the release step.
46
47
  - **JSX resolution** — `resolveJsxConfig` and `readTsconfigJsx` derive the effective JSX transform from a package's tsconfig, with an explicit override winning.
@@ -1,15 +1,24 @@
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";
4
6
  import { createTsdownLogger } from "../report/tsdown-logger.js";
5
7
  import { cjsDefaultInterop } from "./cjs-default-interop.js";
6
8
  import { nodeBuiltinDefaultInterop } from "./node-builtin-default-interop.js";
7
- import { syncPublicDir } from "./sync-public.js";
9
+ import { copyPublicDir } 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 } : {},
@@ -130,39 +140,93 @@ async function buildTargetGroups(options) {
130
140
  ...metricsPlugins(group.id, "js")
131
141
  ]
132
142
  }));
133
- if (isBase) syncPublicDir(publicDir, join(js.outDir, "public"));
134
143
  const dtsNeverBundle = [...partExternals ?? [], ...partDtsExternals ?? []];
135
144
  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
- }));
145
+ const dtsDeps = dtsNeverBundle.length > 0 || partBundleNodeModules || dts.bundledPackages ? { deps: {
146
+ ...dtsNeverBundle.length > 0 ? { neverBundle: dtsNeverBundle } : {},
147
+ ...partBundleNodeModules ? {
148
+ skipNodeModulesBundle: false,
149
+ ...dts.bundledPackages ? { dts: { alwaysBundle: dts.bundledPackages } } : {}
150
+ } : dts.bundledPackages ? {
151
+ skipNodeModulesBundle: true,
152
+ dts: { alwaysBundle: dts.bundledPackages }
153
+ } : {}
154
+ } } : {};
155
+ const stubEntries = /* @__PURE__ */ new Map();
156
+ const readSource = (src) => {
157
+ try {
158
+ return readFileSync(isAbsolute(src) ? src : join(options.cwd, src), "utf-8");
159
+ } catch {
160
+ return;
161
+ }
162
+ };
163
+ const baseSrc = dts.entry[STUB_BASE_ENTRY];
164
+ if (baseSrc !== void 0 && Object.keys(dts.entry).length > 1) {
165
+ const baseSource = readSource(baseSrc);
166
+ const base = baseSource !== void 0 ? collectExportNames(baseSource) : void 0;
167
+ if (base?.complete === true) for (const [entryName, entrySource] of Object.entries(dts.entry)) {
168
+ if (entryName === STUB_BASE_ENTRY || entryName.includes("/")) continue;
169
+ const source = readSource(entrySource);
170
+ if (source === void 0) continue;
171
+ const analysis = analyzeReexportBarrel(source);
172
+ if (!analysis.isPureNamedReexportBarrel) continue;
173
+ const all = [...analysis.valueNames, ...analysis.typeNames];
174
+ if (all.length === 0 || !all.every((n) => base.names.has(n))) continue;
175
+ stubEntries.set(entryName, {
176
+ valueNames: analysis.valueNames,
177
+ typeNames: analysis.typeNames
178
+ });
179
+ }
180
+ }
181
+ await timed(group.id, "dts", async () => {
182
+ for (const [entryName, entrySource] of Object.entries(dts.entry)) {
183
+ const stub = stubEntries.get(entryName);
184
+ if (stub !== void 0) {
185
+ const variants = [{
186
+ file: `${entryName}.d.ts`,
187
+ spec: `./${STUB_BASE_ENTRY}.js`
188
+ }];
189
+ if (dts.format.includes("cjs")) variants.push({
190
+ file: `${entryName}.d.cts`,
191
+ spec: `./${STUB_BASE_ENTRY}.cjs`
192
+ });
193
+ for (const variant of variants) {
194
+ const content = renderReexportStub({
195
+ valueNames: stub.valueNames,
196
+ typeNames: stub.typeNames,
197
+ baseSpecifier: variant.spec
198
+ });
199
+ writeFileSync(join(partOutDir, variant.file), content, "utf-8");
200
+ collector?.recordEmitted(group.id, "dts", {
201
+ path: variant.file,
202
+ bytes: Buffer.byteLength(content)
203
+ });
204
+ }
205
+ continue;
206
+ }
207
+ await build({
208
+ config: false,
209
+ cwd: options.cwd,
210
+ entry: { [entryName]: entrySource },
211
+ outDir: partOutDir,
212
+ format: dts.format,
213
+ platform: dts.platform,
214
+ sourcemap: dts.sourcemap,
215
+ unbundle: dts.unbundle,
216
+ clean: false,
217
+ fixedExtension: dts.fixedExtension,
218
+ dts: dts.dts,
219
+ define: dts.define,
220
+ ...instrument(group.id),
221
+ ...dtsDeps,
222
+ plugins: [
223
+ ...dts.format.includes("cjs") ? [nodeBuiltinDefaultInterop(), cjsDefaultInterop()] : [],
224
+ ...options.extraPlugins ?? [],
225
+ ...metricsPlugins(group.id, "dts")
226
+ ]
227
+ });
228
+ }
229
+ });
166
230
  if (js.isProd && options.emitDeclarations === true && Object.keys(decl.entry).length > 0) {
167
231
  const partDeclDir = part.outSubdir !== void 0 ? join(decl.outDir, part.outSubdir) : decl.outDir;
168
232
  try {
@@ -237,6 +301,7 @@ async function buildTargetGroups(options) {
237
301
  ]
238
302
  }));
239
303
  }
304
+ copyPublicDir(publicDir, outDirFor(options.cwd, group.id));
240
305
  }
241
306
  }
242
307
 
@@ -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,7 @@
1
+ import { ConfigValidationError } from "../errors.js";
2
+ import { findRelativeSpecifiers } from "../dts/relative-imports.js";
3
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync } from "node:fs";
1
4
  import { dirname, join, relative } from "node:path";
2
- import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
3
5
 
4
6
  //#region src/build/sync-public.ts
5
7
  /** Recursively collect every file path under `dir`, relative to `base`. */
@@ -17,52 +19,76 @@ function sameBytes(a, b) {
17
19
  if (statSync(a).size !== statSync(b).size) return false;
18
20
  return readFileSync(a).equals(readFileSync(b));
19
21
  }
20
- /** Remove empty directories under `dir` (deepest-first); `dir` itself is left in place. */
21
- function pruneEmptyDirs(dir) {
22
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
23
- if (!entry.isDirectory()) continue;
24
- const sub = join(dir, entry.name);
25
- pruneEmptyDirs(sub);
26
- if (readdirSync(sub).length === 0) rmSync(sub, {
27
- recursive: true,
28
- force: true
29
- });
30
- }
22
+ /** Throw the standard public-asset collision error for a destination-relative path. */
23
+ function throwPublicCollision(rel) {
24
+ throw new ConfigValidationError({
25
+ path: `public/${rel}`,
26
+ reason: `public asset "${rel}" collides with a built output at the package root — rename or remove it`
27
+ });
31
28
  }
32
29
  /**
33
- * Mirror `sourceDir` into `targetDir`, idempotently.
30
+ * Copy the CONTENTS of `sourceDir` into `outDir`, additively.
34
31
  *
35
- * Replaces tsdown's built-in `copy`, whose non-recursive mkdir throws `EEXIST` when the target
36
- * already exists (re-builds, `prepare`-on-install, concurrent turbo invocations). Behavior:
32
+ * Each `sourceDir/<rel>` copies to `outDir/<rel>` only the `public/` directory itself is dropped;
33
+ * the substructure under it is preserved (`public/tsconfig/ecma.json` becomes `<pkg>/tsconfig/ecma.json`,
34
+ * NOT `<pkg>/ecma.json`). The published manifest mirrors this drop via `transformExports`, which strips
35
+ * a leading `public/` from export values. This function NEVER deletes: `outDir` is the shared package
36
+ * root that the JS/dts passes own, so deleting "files not in source" would wipe the build product.
37
+ * Stale-asset pruning on a non-clean rebuild is therefore out of scope (a full build's `clean: true` handles it).
37
38
  *
38
- * - source absent: no-op.
39
- * - target absent: copy `sourceDir` wholesale.
40
- * - target present: copy only files that are new or whose bytes differ, then delete target files
41
- * that no longer exist in the source and prune the directories left empty.
42
- *
43
- * The byte-diff keeps unchanged files (and their timestamps) untouched, so a large copied asset
44
- * tree — e.g. the mcp markdown corpus — is not rewritten on every build.
39
+ * Collision guard: when a destination already exists, identical bytes mean a prior copy of the same
40
+ * asset (skipped); anything else differing bytes, a directory where a file is needed, or a file
41
+ * where a parent directory is needed means a built output occupies that path, so it throws
42
+ * {@link ConfigValidationError} rather than clobbering it or surfacing a raw fs error.
45
43
  * @public
46
44
  */
47
- function syncPublicDir(sourceDir, targetDir) {
45
+ function copyPublicDir(sourceDir, outDir) {
48
46
  if (!existsSync(sourceDir)) return;
49
- if (!existsSync(targetDir)) {
50
- cpSync(sourceDir, targetDir, { recursive: true });
51
- return;
52
- }
53
- const sourceFiles = listFilesRel(sourceDir);
54
- const sourceSet = new Set(sourceFiles);
55
- for (const rel of sourceFiles) {
47
+ for (const rel of listFilesRel(sourceDir)) {
56
48
  const src = join(sourceDir, rel);
57
- const dst = join(targetDir, rel);
49
+ const dst = join(outDir, rel);
50
+ if (existsSync(dst)) {
51
+ if (statSync(dst).isFile() && sameBytes(src, dst)) continue;
52
+ throwPublicCollision(rel);
53
+ }
54
+ try {
55
+ mkdirSync(dirname(dst), { recursive: true });
56
+ copyFileSync(src, dst);
57
+ } catch (err) {
58
+ const code = err.code;
59
+ if (code === "ENOTDIR" || code === "EEXIST" || code === "EISDIR") throwPublicCollision(rel);
60
+ throw err;
61
+ }
62
+ }
63
+ }
64
+ /**
65
+ * Copy each ambient `.d.ts` export's source verbatim into `outDir/<outName>`, byte-stable (an
66
+ * unchanged file keeps its timestamp). The copy is NOT compiled or bundled, so the build owns two
67
+ * fast-fail checks: the source must exist, and it must be self-contained — a relative
68
+ * import/export/reference would not resolve once the file is flattened to the package root.
69
+ *
70
+ * Throws {@link ConfigValidationError} on a missing source or any relative specifier.
71
+ * @public
72
+ */
73
+ function copyAmbientDts(options) {
74
+ for (const a of options.ambient) {
75
+ const src = join(options.srcCwd, a.source);
76
+ if (!existsSync(src)) throw new ConfigValidationError({
77
+ path: `exports."${a.exportKey}"`,
78
+ reason: `ambient .d.ts source not found: ${a.source}`
79
+ });
80
+ const relativeSpecifiers = findRelativeSpecifiers(readFileSync(src, "utf-8"), a.source);
81
+ if (relativeSpecifiers.length > 0) throw new ConfigValidationError({
82
+ path: `exports."${a.exportKey}"`,
83
+ reason: `ambient .d.ts "${a.source}" has relative import(s) [${relativeSpecifiers.join(", ")}] that cannot resolve after a verbatim copy — use bare package specifiers or self-contained declare module/global blocks`
84
+ });
85
+ const dst = join(options.outDir, a.outName);
58
86
  if (!existsSync(dst) || !sameBytes(src, dst)) {
59
87
  mkdirSync(dirname(dst), { recursive: true });
60
88
  copyFileSync(src, dst);
61
89
  }
62
90
  }
63
- for (const rel of listFilesRel(targetDir)) if (!sourceSet.has(rel)) rmSync(join(targetDir, rel), { force: true });
64
- pruneEmptyDirs(targetDir);
65
91
  }
66
92
 
67
93
  //#endregion
68
- export { syncPublicDir };
94
+ export { copyAmbientDts, copyPublicDir };
@@ -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 };
@@ -0,0 +1,32 @@
1
+ import ts from "typescript";
2
+
3
+ //#region src/dts/relative-imports.ts
4
+ const isRelative = (s) => s.startsWith("./") || s.startsWith("../");
5
+ /**
6
+ * Find every relative module specifier in a declaration source: static `import`/`export … from`,
7
+ * `import("…")` type nodes, and `/// <reference path="…" />`. Pure parsing — no I/O.
8
+ *
9
+ * A non-empty result means the file is NOT self-contained and would break when copied verbatim to a
10
+ * flattened output location, so the ambient-copy step rejects it.
11
+ * @public
12
+ */
13
+ function findRelativeSpecifiers(source, fileName = "ambient.d.ts") {
14
+ const sf = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
15
+ const found = /* @__PURE__ */ new Set();
16
+ for (const ref of sf.referencedFiles) if (isRelative(ref.fileName)) found.add(ref.fileName);
17
+ const visit = (node) => {
18
+ if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier !== void 0 && ts.isStringLiteral(node.moduleSpecifier)) {
19
+ if (isRelative(node.moduleSpecifier.text)) found.add(node.moduleSpecifier.text);
20
+ } else if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument) && ts.isStringLiteral(node.argument.literal)) {
21
+ if (isRelative(node.argument.literal.text)) found.add(node.argument.literal.text);
22
+ } else if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference) && ts.isStringLiteral(node.moduleReference.expression)) {
23
+ if (isRelative(node.moduleReference.expression.text)) found.add(node.moduleReference.expression.text);
24
+ }
25
+ ts.forEachChild(node, visit);
26
+ };
27
+ visit(sf);
28
+ return [...found];
29
+ }
30
+
31
+ //#endregion
32
+ export { findRelativeSpecifiers };
@@ -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 };
@@ -0,0 +1,102 @@
1
+ import { ConfigValidationError } from "../errors.js";
2
+ import { createEntryName } from "./extract.js";
3
+
4
+ //#region src/entry/ambient-dts.ts
5
+ const DECLARATION_EXTS = [
6
+ ".d.ts",
7
+ ".d.cts",
8
+ ".d.mts"
9
+ ];
10
+ /** The declaration-file extension of a path, or undefined when it is not a declaration file. @public */
11
+ function declarationExt(p) {
12
+ return DECLARATION_EXTS.find((e) => p.endsWith(e));
13
+ }
14
+ /**
15
+ * Classify an export value:
16
+ * - `ambient` — a types-only declaration source (bare `.d.ts` string, or `{ types: "*.d.ts" }` with no runtime source).
17
+ * - `mixed` — a declaration `types` AND a compilable runtime source (`import`/`require`/`default` → `.ts`/`.tsx`).
18
+ * - `none` — anything else (normal runtime export, json, etc.).
19
+ * @public
20
+ */
21
+ function classifyDtsExport(value) {
22
+ if (typeof value === "string") return declarationExt(value) !== void 0 ? {
23
+ kind: "ambient",
24
+ source: value
25
+ } : { kind: "none" };
26
+ if (value && typeof value === "object") {
27
+ const o = value;
28
+ const typesVal = typeof o.types === "string" ? o.types : void 0;
29
+ const typesIsDecl = typesVal !== void 0 && declarationExt(typesVal) !== void 0;
30
+ const hasRuntime = Object.entries(o).some(([k, v]) => k !== "types" && typeof v === "string" && declarationExt(v) === void 0);
31
+ if (typesIsDecl) return hasRuntime ? { kind: "mixed" } : {
32
+ kind: "ambient",
33
+ source: typesVal
34
+ };
35
+ }
36
+ return { kind: "none" };
37
+ }
38
+ /**
39
+ * Output basename (including the preserved declaration extension) for an ambient export, derived
40
+ * from the export KEY — consistent with how JS entries are named. @public
41
+ */
42
+ function ambientOutName(exportKey, source, exportsAsIndexes = false) {
43
+ const ext = declarationExt(source) ?? ".d.ts";
44
+ return `${createEntryName(exportKey, exportsAsIndexes)}${ext}`;
45
+ }
46
+ /** The shared mixed-export error (Decision 2), used by both the extractor and the manifest transform. @public */
47
+ function mixedDtsExportError(exportKey) {
48
+ return new ConfigValidationError({
49
+ path: `exports."${exportKey}"`,
50
+ reason: "an export with a runtime source (import/require/default) cannot also hand-author its `types` as a .d.ts; the bundler generates types from the source"
51
+ });
52
+ }
53
+ /**
54
+ * Extract the types-only `.d.ts` exports from a package's `exports` map. Pure.
55
+ * Throws {@link ConfigValidationError} on a mixed export (Decision 2) or an ambient-vs-ambient
56
+ * output-name collision. @public
57
+ */
58
+ function extractAmbientDts(pkg, options = {}) {
59
+ const exports = pkg.exports;
60
+ if (!exports || typeof exports !== "object") return [];
61
+ const exportsAsIndexes = options.exportsAsIndexes ?? false;
62
+ const out = [];
63
+ const byName = /* @__PURE__ */ new Map();
64
+ for (const [key, value] of Object.entries(exports)) {
65
+ if (key === "./package.json" || key.endsWith(".json")) continue;
66
+ if (declarationExt(key) !== void 0) continue;
67
+ const cls = classifyDtsExport(value);
68
+ if (cls.kind === "none") continue;
69
+ if (cls.kind === "mixed") throw mixedDtsExportError(key);
70
+ const outName = ambientOutName(key, cls.source, exportsAsIndexes);
71
+ const prev = byName.get(outName);
72
+ if (prev !== void 0) throw new ConfigValidationError({
73
+ path: `exports."${key}"`,
74
+ reason: `ambient .d.ts export flattens to "${outName}", colliding with export key "${prev}". Rename one so each produces a distinct file.`
75
+ });
76
+ byName.set(outName, key);
77
+ out.push({
78
+ exportKey: key,
79
+ source: cls.source,
80
+ outName
81
+ });
82
+ }
83
+ return out;
84
+ }
85
+ /**
86
+ * Throw {@link ConfigValidationError} if any ambient output name collides with a JS build-entry name.
87
+ * The JS entry names carry no extension, so each ambient `outName` is compared with its declaration
88
+ * extension stripped. @public
89
+ */
90
+ function assertNoEntryCollisions(jsEntryNames, ambient) {
91
+ const js = new Set(jsEntryNames);
92
+ for (const a of ambient) {
93
+ const base = a.outName.replace(/\.d\.(ts|cts|mts)$/, "");
94
+ if (js.has(base)) throw new ConfigValidationError({
95
+ path: `exports."${a.exportKey}"`,
96
+ reason: `ambient .d.ts export "${a.outName}" collides with the JS build entry "${base}". Rename the export so each produces a distinct output.`
97
+ });
98
+ }
99
+ }
100
+
101
+ //#endregion
102
+ export { ambientOutName, assertNoEntryCollisions, classifyDtsExport, declarationExt, extractAmbientDts, mixedDtsExportError };
@@ -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
@@ -1,4 +1,11 @@
1
- import { CatalogAssemblyError, CatalogResolutionError, ManifestLike, ManifestLike as ManifestLike$1 } from "workspaces-effect";
1
+ /**
2
+ * Find every relative module specifier in a declaration source: static `import`/`export … from`,
3
+ * `import("…")` type nodes, and `/// <reference path="…" />`. Pure parsing — no I/O.
4
+ *
5
+ * A non-empty result means the file is NOT self-contained and would break when copied verbatim to a
6
+ * flattened output location, so the ambient-copy step rejects it.
7
+ * @public
8
+ */import { CatalogAssemblyError, CatalogResolutionError, ManifestLike, ManifestLike as ManifestLike$1 } from "workspaces-effect";
2
9
  import { Plugin } from "rolldown";
3
10
  import { Context, Effect, Layer, Schema } from "effect";
4
11
  import { JsxEmit, ModuleDetectionKind, ModuleKind, ModuleResolutionKind, NewLineKind, ParsedCommandLine, ScriptTarget } from "typescript";
@@ -177,21 +184,21 @@ declare const DiagnosticEntry_base: Schema.Class<DiagnosticEntry, {
177
184
  line: Schema.optional<typeof Schema.Number>;
178
185
  column: Schema.optional<typeof Schema.Number>;
179
186
  }>, never, {
180
- readonly text: string;
181
- } & {
182
- readonly source: "tsdown" | "rolldown" | "api-extractor";
183
- } & {
184
- readonly level: "warn" | "error";
187
+ readonly ciFatal?: boolean | undefined;
185
188
  } & {
186
189
  readonly code?: string | undefined;
187
190
  } & {
188
- readonly ciFatal?: boolean | undefined;
191
+ readonly column?: number | undefined;
189
192
  } & {
190
193
  readonly file?: string | undefined;
191
194
  } & {
192
195
  readonly line?: number | undefined;
193
196
  } & {
194
- readonly column?: number | undefined;
197
+ readonly level: "error" | "warn";
198
+ } & {
199
+ readonly source: "api-extractor" | "rolldown" | "tsdown";
200
+ } & {
201
+ readonly text: string;
195
202
  }, {}, {}>;
196
203
  /**
197
204
  * A captured warning or error, from tsdown's logger, rolldown's onLog, or API Extractor.
@@ -208,11 +215,11 @@ declare const EmittedFile_base: Schema.Class<EmittedFile, {
208
215
  bytes: typeof Schema.Number;
209
216
  gzip: Schema.optional<typeof Schema.Number>;
210
217
  }>, never, {
211
- readonly path: string;
218
+ readonly gzip?: number | undefined;
212
219
  } & {
213
220
  readonly bytes: number;
214
221
  } & {
215
- readonly gzip?: number | undefined;
222
+ readonly path: string;
216
223
  }, {}, {}>;
217
224
  /**
218
225
  * One emitted output file with its in-memory byte size (gzip only when --verbose).
@@ -229,11 +236,11 @@ declare const PassReport_base: Schema.Class<PassReport, {
229
236
  files: Schema.Array$<typeof EmittedFile>;
230
237
  ms: typeof Schema.Number;
231
238
  }>, never, {
232
- readonly ms: number;
239
+ readonly files: readonly EmittedFile[];
233
240
  } & {
234
- readonly id: "js" | "dts" | "loose" | "exe" | "meta";
241
+ readonly id: "dts" | "exe" | "js" | "loose" | "meta";
235
242
  } & {
236
- readonly files: readonly EmittedFile[];
243
+ readonly ms: number;
237
244
  }, {}, {}>;
238
245
  /**
239
246
  * One build pass within a target group (js / dts / loose / exe / meta).
@@ -260,17 +267,17 @@ declare const TargetGroupReport_base: Schema.Class<TargetGroupReport, {
260
267
  }>, never, {
261
268
  readonly entries: readonly string[];
262
269
  } & {
263
- readonly id: string;
270
+ readonly errors: readonly DiagnosticEntry[];
264
271
  } & {
265
- readonly timings: ReportTimings;
272
+ readonly id: string;
266
273
  } & {
267
274
  readonly passes: readonly PassReport[];
268
275
  } & {
269
- readonly warnings: readonly DiagnosticEntry[];
276
+ readonly suppressed: readonly DiagnosticEntry[];
270
277
  } & {
271
- readonly errors: readonly DiagnosticEntry[];
278
+ readonly timings: ReportTimings;
272
279
  } & {
273
- readonly suppressed: readonly DiagnosticEntry[];
280
+ readonly warnings: readonly DiagnosticEntry[];
274
281
  }, {}, {}>;
275
282
  /** @public */
276
283
  declare class TargetGroupReport extends TargetGroupReport_base {}
@@ -762,23 +769,156 @@ declare function nodeBuiltinDefaultInterop(): Plugin;
762
769
  */
763
770
  declare function removeDeclarationMaps(pkgDir: string): string[];
764
771
  //#endregion
772
+ //#region src/errors.d.ts
773
+ declare const MetaGenerationError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => import("effect/Cause").YieldableError & {
774
+ readonly _tag: "MetaGenerationError";
775
+ } & Readonly<A>;
776
+ /**
777
+ * API Extractor meta generation failed for an entry.
778
+ *
779
+ * @public
780
+ */
781
+ declare class MetaGenerationError extends MetaGenerationError_base<{
782
+ readonly entry: string;
783
+ readonly reason: string;
784
+ }> {
785
+ get message(): string;
786
+ }
787
+ declare const ConfigValidationError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => import("effect/Cause").YieldableError & {
788
+ readonly _tag: "ConfigValidationError";
789
+ } & Readonly<A>;
790
+ /**
791
+ * A savvy.build.ts or publishConfig.targets config is structurally invalid; raised before any build work.
792
+ *
793
+ * @public
794
+ */
795
+ declare class ConfigValidationError extends ConfigValidationError_base<{
796
+ readonly path: string;
797
+ readonly reason: string;
798
+ }> {
799
+ get message(): string;
800
+ }
801
+ //#endregion
802
+ //#region src/entry/extract.d.ts
803
+ /** @public */
804
+ interface PackageJsonLike {
805
+ readonly exports?: unknown;
806
+ readonly bin?: unknown;
807
+ }
808
+ /** @public */
809
+ interface ExtractOptions {
810
+ readonly exportsAsIndexes?: boolean | undefined;
811
+ /** Source paths to NOT turn into JS build entries (e.g. an exe entry compiled as a SEA). */
812
+ readonly excludeSources?: ReadonlyArray<string> | undefined;
813
+ }
814
+ /** @public */
815
+ interface ExtractResult {
816
+ /** entry name to TS source path */
817
+ readonly entries: Record<string, string>;
818
+ /** entry name to original export key (for downstream output-map alignment) */
819
+ readonly exportPaths: Record<string, string>;
820
+ }
821
+ /**
822
+ * Map an export key to the tsdown entry name (the emitted output basename).
823
+ *
824
+ * `.` becomes `index`; otherwise the leading `./` is stripped and, unless
825
+ * `exportsAsIndexes` is set, nested slashes are flattened to dashes
826
+ * (e.g. `./changesets/markdownlint` to `changesets-markdownlint`). The manifest
827
+ * transform reuses this so the declared output path always matches the emitted file.
828
+ *
829
+ * @internal
830
+ */
831
+ declare const createEntryName: (exportKey: string, exportsAsIndexes: boolean) => string;
832
+ /** @public */
833
+ declare function extractEntries(pkg: PackageJsonLike, options?: ExtractOptions): ExtractResult;
834
+ //#endregion
835
+ //#region src/entry/ambient-dts.d.ts
836
+ /** The declaration-file extension of a path, or undefined when it is not a declaration file. @public */
837
+ declare function declarationExt(p: string): ".d.ts" | ".d.cts" | ".d.mts" | undefined;
838
+ /** Classification of a single export value for ambient-.d.ts handling. @public */
839
+ type DtsExportClass = {
840
+ readonly kind: "ambient";
841
+ readonly source: string;
842
+ } | {
843
+ readonly kind: "mixed";
844
+ } | {
845
+ readonly kind: "none";
846
+ };
847
+ /**
848
+ * Classify an export value:
849
+ * - `ambient` — a types-only declaration source (bare `.d.ts` string, or `{ types: "*.d.ts" }` with no runtime source).
850
+ * - `mixed` — a declaration `types` AND a compilable runtime source (`import`/`require`/`default` → `.ts`/`.tsx`).
851
+ * - `none` — anything else (normal runtime export, json, etc.).
852
+ * @public
853
+ */
854
+ declare function classifyDtsExport(value: unknown): DtsExportClass;
855
+ /**
856
+ * Output basename (including the preserved declaration extension) for an ambient export, derived
857
+ * from the export KEY — consistent with how JS entries are named. @public
858
+ */
859
+ declare function ambientOutName(exportKey: string, source: string, exportsAsIndexes?: boolean): string;
860
+ /** The shared mixed-export error (Decision 2), used by both the extractor and the manifest transform. @public */
861
+ declare function mixedDtsExportError(exportKey: string): ConfigValidationError;
862
+ /** One ambient `.d.ts` export resolved for copy + manifest. @public */
863
+ interface AmbientDtsEntry {
864
+ readonly exportKey: string;
865
+ readonly source: string;
866
+ readonly outName: string;
867
+ }
868
+ /** @public */
869
+ interface ExtractAmbientOptions {
870
+ readonly exportsAsIndexes?: boolean | undefined;
871
+ }
872
+ /**
873
+ * Extract the types-only `.d.ts` exports from a package's `exports` map. Pure.
874
+ * Throws {@link ConfigValidationError} on a mixed export (Decision 2) or an ambient-vs-ambient
875
+ * output-name collision. @public
876
+ */
877
+ declare function extractAmbientDts(pkg: PackageJsonLike, options?: ExtractAmbientOptions): ReadonlyArray<AmbientDtsEntry>;
878
+ /**
879
+ * Throw {@link ConfigValidationError} if any ambient output name collides with a JS build-entry name.
880
+ * The JS entry names carry no extension, so each ambient `outName` is compared with its declaration
881
+ * extension stripped. @public
882
+ */
883
+ declare function assertNoEntryCollisions(jsEntryNames: ReadonlyArray<string>, ambient: ReadonlyArray<AmbientDtsEntry>): void;
884
+ //#endregion
765
885
  //#region src/build/sync-public.d.ts
766
886
  /**
767
- * Mirror `sourceDir` into `targetDir`, idempotently.
887
+ * Copy the CONTENTS of `sourceDir` into `outDir`, additively.
768
888
  *
769
- * Replaces tsdown's built-in `copy`, whose non-recursive mkdir throws `EEXIST` when the target
770
- * already exists (re-builds, `prepare`-on-install, concurrent turbo invocations). Behavior:
889
+ * Each `sourceDir/<rel>` copies to `outDir/<rel>` only the `public/` directory itself is dropped;
890
+ * the substructure under it is preserved (`public/tsconfig/ecma.json` becomes `<pkg>/tsconfig/ecma.json`,
891
+ * NOT `<pkg>/ecma.json`). The published manifest mirrors this drop via `transformExports`, which strips
892
+ * a leading `public/` from export values. This function NEVER deletes: `outDir` is the shared package
893
+ * root that the JS/dts passes own, so deleting "files not in source" would wipe the build product.
894
+ * Stale-asset pruning on a non-clean rebuild is therefore out of scope (a full build's `clean: true` handles it).
771
895
  *
772
- * - source absent: no-op.
773
- * - target absent: copy `sourceDir` wholesale.
774
- * - target present: copy only files that are new or whose bytes differ, then delete target files
775
- * that no longer exist in the source and prune the directories left empty.
896
+ * Collision guard: when a destination already exists, identical bytes mean a prior copy of the same
897
+ * asset (skipped); anything else differing bytes, a directory where a file is needed, or a file
898
+ * where a parent directory is needed means a built output occupies that path, so it throws
899
+ * {@link ConfigValidationError} rather than clobbering it or surfacing a raw fs error.
900
+ * @public
901
+ */
902
+ declare function copyPublicDir(sourceDir: string, outDir: string): void;
903
+ /** @public */
904
+ interface CopyAmbientDtsOptions {
905
+ /** The ambient exports to copy (from `extractAmbientDts`). */
906
+ readonly ambient: ReadonlyArray<AmbientDtsEntry>;
907
+ /** Package root the `source` paths are relative to. */
908
+ readonly srcCwd: string;
909
+ /** The built package dir to copy into (e.g. `dist/dev/pkg`). */
910
+ readonly outDir: string;
911
+ }
912
+ /**
913
+ * Copy each ambient `.d.ts` export's source verbatim into `outDir/<outName>`, byte-stable (an
914
+ * unchanged file keeps its timestamp). The copy is NOT compiled or bundled, so the build owns two
915
+ * fast-fail checks: the source must exist, and it must be self-contained — a relative
916
+ * import/export/reference would not resolve once the file is flattened to the package root.
776
917
  *
777
- * The byte-diff keeps unchanged files (and their timestamps) untouched, so a large copied asset
778
- * tree — e.g. the mcp markdown corpus — is not rewritten on every build.
918
+ * Throws {@link ConfigValidationError} on a missing source or any relative specifier.
779
919
  * @public
780
920
  */
781
- declare function syncPublicDir(sourceDir: string, targetDir: string): void;
921
+ declare function copyAmbientDts(options: CopyAmbientDtsOptions): void;
782
922
  //#endregion
783
923
  //#region src/catalog/resolve-catalogs.d.ts
784
924
  /**
@@ -817,36 +957,6 @@ interface NextVersions {
817
957
  */
818
958
  declare function resolveNextVersions(cwd: string): Promise<NextVersions>;
819
959
  //#endregion
820
- //#region src/errors.d.ts
821
- declare const MetaGenerationError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => import("effect/Cause").YieldableError & {
822
- readonly _tag: "MetaGenerationError";
823
- } & Readonly<A>;
824
- /**
825
- * API Extractor meta generation failed for an entry.
826
- *
827
- * @public
828
- */
829
- declare class MetaGenerationError extends MetaGenerationError_base<{
830
- readonly entry: string;
831
- readonly reason: string;
832
- }> {
833
- get message(): string;
834
- }
835
- declare const ConfigValidationError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => import("effect/Cause").YieldableError & {
836
- readonly _tag: "ConfigValidationError";
837
- } & Readonly<A>;
838
- /**
839
- * A savvy.build.ts or publishConfig.targets config is structurally invalid; raised before any build work.
840
- *
841
- * @public
842
- */
843
- declare class ConfigValidationError extends ConfigValidationError_base<{
844
- readonly path: string;
845
- readonly reason: string;
846
- }> {
847
- get message(): string;
848
- }
849
- //#endregion
850
960
  //#region src/exe/config.d.ts
851
961
  /**
852
962
  * Default Node runtime embedded in the SEA (parity with the vitest-agent reference).
@@ -1109,6 +1219,72 @@ declare class ConfigValidator extends ConfigValidator_base {}
1109
1219
  */
1110
1220
  declare const ConfigValidatorLive: Layer.Layer<ConfigValidator, never, never>;
1111
1221
  //#endregion
1222
+ //#region src/dts/reexport-stub.d.ts
1223
+ /**
1224
+ * The analysis of an entry source treated as a candidate re-export barrel.
1225
+ *
1226
+ * @public
1227
+ */
1228
+ interface ReexportBarrelAnalysis {
1229
+ /** Value (non-type-only) names the module re-exports, after `as` aliasing. */
1230
+ readonly valueNames: ReadonlyArray<string>;
1231
+ /** Type-only names the module re-exports (`export type { … }`), after `as` aliasing. */
1232
+ readonly typeNames: ReadonlyArray<string>;
1233
+ /**
1234
+ * True iff EVERY top-level statement is a NAMED re-export `from` another module
1235
+ * (`export { … } from "…"` / `export type { … } from "…"`). A module that declares anything
1236
+ * locally, re-exports a namespace (`export * as NS from`), star-re-exports (`export * from`), or
1237
+ * has a bare `export { … }` without `from` is NOT a pure named barrel and cannot be expressed as
1238
+ * a thin re-export stub of another entry.
1239
+ */
1240
+ readonly isPureNamedReexportBarrel: boolean;
1241
+ }
1242
+ /**
1243
+ * The set of names a module exports, plus whether that set is fully known.
1244
+ *
1245
+ * @public
1246
+ */
1247
+ interface ModuleExportNames {
1248
+ readonly names: ReadonlySet<string>;
1249
+ /**
1250
+ * False when the module contains a star re-export (`export * from "…"`) whose target exports
1251
+ * cannot be enumerated from this source alone — the name set is then a lower bound, not complete,
1252
+ * so callers must not use it for a strict subset decision.
1253
+ */
1254
+ readonly complete: boolean;
1255
+ }
1256
+ /**
1257
+ * Analyze an entry source as a candidate pure re-export barrel: classify its re-exported names into
1258
+ * value vs type-only and report whether it is expressible as a thin stub. Pure parsing — no I/O.
1259
+ *
1260
+ * @public
1261
+ */
1262
+ declare function analyzeReexportBarrel(source: string, fileName?: string): ReexportBarrelAnalysis;
1263
+ /**
1264
+ * Collect every name a module exports (named re-exports, namespace re-exports, and local `export`
1265
+ * declarations). Used to test whether a barrel's re-exports are a strict subset of a base entry, so
1266
+ * a stub re-exporting from that base resolves every symbol. Pure parsing — no I/O.
1267
+ *
1268
+ * @public
1269
+ */
1270
+ declare function collectExportNames(source: string, fileName?: string): ModuleExportNames;
1271
+ /**
1272
+ * Render a thin re-export-stub `.d.ts`/`.d.cts` body: named re-exports of `valueNames` and
1273
+ * `typeNames` from `baseSpecifier` (the published file of the base entry, e.g. `./index.js` for the
1274
+ * ESM `.d.ts` or `./index.cjs` for the CJS `.d.cts`). Names are sorted so the output is
1275
+ * deterministic. Returns the empty string when there is nothing to re-export.
1276
+ *
1277
+ * @public
1278
+ */
1279
+ declare function renderReexportStub(options: {
1280
+ readonly valueNames: ReadonlyArray<string>;
1281
+ readonly typeNames: ReadonlyArray<string>;
1282
+ readonly baseSpecifier: string;
1283
+ }): string;
1284
+ //#endregion
1285
+ //#region src/dts/relative-imports.d.ts
1286
+ declare function findRelativeSpecifiers(source: string, fileName?: string): string[];
1287
+ //#endregion
1112
1288
  //#region src/dts/resolved-tsconfig.d.ts
1113
1289
  /** @public */
1114
1290
  interface ResolvedTsconfigOptions {
@@ -1139,39 +1315,23 @@ declare function buildResolvedTsconfig(options: ResolvedTsconfigOptions): Resolv
1139
1315
  * @public
1140
1316
  */
1141
1317
  declare function writeResolvedTsconfig(options: ResolvedTsconfigOptions): string;
1142
- //#endregion
1143
- //#region src/entry/extract.d.ts
1144
- /** @public */
1145
- interface PackageJsonLike {
1146
- readonly exports?: unknown;
1147
- readonly bin?: unknown;
1148
- }
1149
- /** @public */
1150
- interface ExtractOptions {
1151
- readonly exportsAsIndexes?: boolean | undefined;
1152
- /** Source paths to NOT turn into JS build entries (e.g. an exe entry compiled as a SEA). */
1153
- readonly excludeSources?: ReadonlyArray<string> | undefined;
1154
- }
1155
- /** @public */
1156
- interface ExtractResult {
1157
- /** entry name to TS source path */
1158
- readonly entries: Record<string, string>;
1159
- /** entry name to original export key (for downstream output-map alignment) */
1160
- readonly exportPaths: Record<string, string>;
1161
- }
1162
1318
  /**
1163
- * Map an export key to the tsdown entry name (the emitted output basename).
1319
+ * Derive a dts-EMIT variant of an already-written resolved tsconfig that adds
1320
+ * `stableTypeOrdering: true`, and return its path. This makes the TypeScript declaration emitter
1321
+ * (rolldown-plugin-dts on `typescript@6`) order union/type members deterministically, so a
1322
+ * multi-union `.d.ts` (e.g. an Effect `Layer.Layer<…>` requirement channel) does not flip member
1323
+ * order across otherwise-identical builds (#156). It is kept in a SEPARATE file from the
1324
+ * api-extractor tsconfig on purpose: `@microsoft/api-extractor` pins `typescript ~5.9`, which
1325
+ * predates the flag and hard-errors on the unknown compiler option — so only the emit passes
1326
+ * (which run on TS6) ever see it, while the api-extractor compile reads the original clean config.
1164
1327
  *
1165
- * `.` becomes `index`; otherwise the leading `./` is stripped and, unless
1166
- * `exportsAsIndexes` is set, nested slashes are flattened to dashes
1167
- * (e.g. `./changesets/markdownlint` to `changesets-markdownlint`). The manifest
1168
- * transform reuses this so the declared output path always matches the emitted file.
1328
+ * Best-effort: if the base tsconfig cannot be read or parsed (e.g. a synthetic test path that was
1329
+ * never written), the original path is returned unchanged the emit then simply keeps TS's
1330
+ * default ordering rather than aborting the build at this layer.
1169
1331
  *
1170
- * @internal
1332
+ * @public
1171
1333
  */
1172
- declare const createEntryName: (exportKey: string, exportsAsIndexes: boolean) => string;
1173
- /** @public */
1174
- declare function extractEntries(pkg: PackageJsonLike, options?: ExtractOptions): ExtractResult;
1334
+ declare function writeDtsEmitTsconfig(resolvedTsconfigPath: string): string;
1175
1335
  //#endregion
1176
1336
  //#region src/entry/package-json-entries.d.ts
1177
1337
  /** @public */
@@ -1718,5 +1878,5 @@ declare function resolveTargets(options: {
1718
1878
  baseName: string;
1719
1879
  }): TargetResolution;
1720
1880
  //#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 };
1881
+ export { type AmbientDtsEntry, 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 CopyAmbientDtsOptions, type CssOptions, DEFAULT_EXE_NODE_VERSION, type DeriveOptions, type DerivedTsdownOptions, type DiagnosticEntry, type DiagnosticInput, type DtsExportClass, 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 ExtractAmbientOptions, 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, ambientOutName, analyzeReexportBarrel, applySubdirMetaEntries, assertNoEntryCollisions, buildEmittedManifest, buildMetricsPlugin, buildResolvedTsconfig, buildTargetGroups, cjsDefaultInterop, classifyDtsExport, collectExportNames, computeExeFileName, copyAmbientDts, copyPublicDir, createEntryName, createTimer, createTsdownLogger, declarationExt, defaultManifestTransform, deriveExportPaths, deriveTargetGroupOptions, emitManifest, extractAmbientDts, extractEntries, findRelativeSpecifiers, flattenIssues, formatTime, generateMeta, isTargetObject, mixedDtsExportError, nodeBuiltinDefaultInterop, normalizeBinPaths, normalizeExeOptions, normalizeLooseFiles, normalizeMetaOptions, packageJsonEntries, readTsconfigJsx, removeDeclarationMaps, renderReexportStub, renderReport, resolveJsxConfig, resolveManifest, resolveNextVersions, resolvePortableTsconfig, resolveTargets, rewriteMetaVersions, runExeBuild, runMetaPass, serializeIssues, transformBin, transformExports, transformManifest, writeDtsEmitTsconfig, writeIssuesArtifact, writeResolvedTsconfig, writeTargetsBinding };
1722
1882
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,5 +1,9 @@
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";
4
+ import { ConfigValidationError, MetaGenerationError } from "./errors.js";
2
5
  import { createEntryName, extractEntries } from "./entry/extract.js";
6
+ import { ambientOutName, assertNoEntryCollisions, classifyDtsExport, declarationExt, extractAmbientDts, mixedDtsExportError } from "./entry/ambient-dts.js";
3
7
  import { defaultManifestTransform, normalizeBinPaths, transformBin, transformExports, transformManifest } from "./manifest/transform.js";
4
8
  import { buildEmittedManifest, emitManifest } from "./manifest/emit-manifest.js";
5
9
  import { buildMetricsPlugin } from "./report/metrics-plugin.js";
@@ -7,10 +11,10 @@ import { createTimer, formatTime } from "./report/timer.js";
7
11
  import { createTsdownLogger } from "./report/tsdown-logger.js";
8
12
  import { cjsDefaultInterop } from "./build/cjs-default-interop.js";
9
13
  import { nodeBuiltinDefaultInterop } from "./build/node-builtin-default-interop.js";
10
- import { syncPublicDir } from "./build/sync-public.js";
14
+ import { findRelativeSpecifiers } from "./dts/relative-imports.js";
15
+ import { copyAmbientDts, copyPublicDir } from "./build/sync-public.js";
11
16
  import { deriveTargetGroupOptions } from "./build/target-groups.js";
12
17
  import { buildTargetGroups } from "./build/build-target-groups.js";
13
- import { ConfigValidationError, MetaGenerationError } from "./errors.js";
14
18
  import { normalizeLooseFiles } from "./build/loose-files.js";
15
19
  import { removeDeclarationMaps } from "./build/strip-maps.js";
16
20
  import { resolveNextVersions } from "./changesets/next-versions.js";
@@ -19,7 +23,6 @@ import { DEFAULT_EXE_NODE_VERSION, normalizeExeOptions } from "./exe/config.js";
19
23
  import { isTargetObject } from "./targets/config.js";
20
24
  import { resolveTargets } from "./targets/resolve-targets.js";
21
25
  import { ConfigValidatorLive } from "./config-validation/ConfigValidatorLive.js";
22
- import { buildResolvedTsconfig, writeResolvedTsconfig } from "./dts/resolved-tsconfig.js";
23
26
  import { packageJsonEntries } from "./entry/package-json-entries.js";
24
27
  import { runExeBuild } from "./exe/build.js";
25
28
  import { computeExeFileName } from "./exe/filename.js";
@@ -49,4 +52,4 @@ import { ReportPipelineLive, renderReport } from "./report/pipeline.js";
49
52
  import { writeTargetsBinding } from "./targets/binding.js";
50
53
  import { CatalogAssemblyError, CatalogResolutionError } from "workspaces-effect";
51
54
 
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 };
55
+ 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, ambientOutName, analyzeReexportBarrel, applySubdirMetaEntries, assertNoEntryCollisions, buildEmittedManifest, buildMetricsPlugin, buildResolvedTsconfig, buildTargetGroups, cjsDefaultInterop, classifyDtsExport, collectExportNames, computeExeFileName, copyAmbientDts, copyPublicDir, createEntryName, createTimer, createTsdownLogger, declarationExt, defaultManifestTransform, deriveExportPaths, deriveTargetGroupOptions, emitManifest, extractAmbientDts, extractEntries, findRelativeSpecifiers, flattenIssues, formatTime, generateMeta, isTargetObject, mixedDtsExportError, nodeBuiltinDefaultInterop, normalizeBinPaths, normalizeExeOptions, normalizeLooseFiles, normalizeMetaOptions, packageJsonEntries, readTsconfigJsx, removeDeclarationMaps, renderReexportStub, renderReport, resolveJsxConfig, resolveManifest, resolveNextVersions, resolvePortableTsconfig, resolveTargets, rewriteMetaVersions, runExeBuild, runMetaPass, serializeIssues, 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
  /**
@@ -1,4 +1,5 @@
1
1
  import { createEntryName } from "../entry/extract.js";
2
+ import { ambientOutName, classifyDtsExport, mixedDtsExportError } from "../entry/ambient-dts.js";
2
3
  import sortPackageJson from "sort-package-json";
3
4
 
4
5
  //#region src/manifest/transform.ts
@@ -83,6 +84,16 @@ const tsConditions = (exportKey, dual, subdirExports) => ({
83
84
  ...dual ? { require: toBuiltCjs(exportKey, subdirExports) } : {}
84
85
  });
85
86
  /**
87
+ * A `./public/<path>` export value points into the staged `public/` dir, whose CONTENTS `copyPublicDir`
88
+ * copies into the package root (only the `public/` segment is dropped; substructure is kept). So the
89
+ * published manifest value drops that same `public/` segment: `./public/tsconfig/ecma.json` becomes
90
+ * `./tsconfig/ecma.json`, resolving the file copyPublicDir placed at `<pkg>/tsconfig/ecma.json`.
91
+ * Non-strings and non-public values pass through unchanged.
92
+ */
93
+ function stripPublicPrefix(value) {
94
+ return typeof value === "string" && value.startsWith("./public/") ? `./${value.slice(9)}` : value;
95
+ }
96
+ /**
86
97
  * Rewrite an exports map: TS string targets become a types/import conditions object.
87
98
  * Each TS condition also gets a `require` entry when `dual` is `true` (uniform) or when
88
99
  * the export key is in the `dual` Set (per-entry).
@@ -95,12 +106,18 @@ const tsConditions = (exportKey, dual, subdirExports) => ({
95
106
  * @public
96
107
  */
97
108
  function transformExports(exports, dual = false, subdirExports) {
98
- if (typeof exports === "string") return isTs(exports) ? tsConditions(".", isDualKey(dual, "."), subdirExports) : exports;
109
+ if (typeof exports === "string") return isTs(exports) ? tsConditions(".", isDualKey(dual, "."), subdirExports) : stripPublicPrefix(exports);
99
110
  if (exports && typeof exports === "object") {
100
111
  const out = {};
101
112
  for (const [key, value] of Object.entries(exports)) {
102
- if (key === "./package.json" || key.endsWith(".json")) {
103
- out[key] = value;
113
+ if (key === "./package.json" || key.endsWith(".json") || isDeclarationFile(key)) {
114
+ out[key] = stripPublicPrefix(value);
115
+ continue;
116
+ }
117
+ const cls = classifyDtsExport(value);
118
+ if (cls.kind === "mixed") throw mixedDtsExportError(key);
119
+ if (cls.kind === "ambient") {
120
+ out[key] = { types: `./${ambientOutName(key, cls.source)}` };
104
121
  continue;
105
122
  }
106
123
  if (typeof value === "string" && isTs(value)) out[key] = tsConditions(key, isDualKey(dual, key), subdirExports);
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": "1.0.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
  /**