@savvy-web/tsdown-plugins 0.4.2 → 0.6.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
@@ -47,7 +47,7 @@ export default defineConfig({
47
47
  - **Executable binaries** — `normalizeExeOptions` fills the SEA defaults and infers targets from the package's `os`/`cpu`; `runExeBuild` drives `@tsdown/exe` to compile the binaries.
48
48
  - **Config validation** — the `ConfigValidator` Effect service (with `ConfigValidatorLive`) fast-fails on a bad `publishConfig.targets`, `exe` or `meta` config, raising the typed `ConfigValidationError`.
49
49
  - **dts tsconfig port** — `buildResolvedTsconfig` and `writeResolvedTsconfig` write a temp tsconfig with absolute paths so type declarations emit cleanly under pnpm symlinks.
50
- - **Per-target build loop** — `deriveTargetGroupOptions` and `buildTargetGroups` map a target to its `tsdown` options and run the build once per target, exposed as a helper so the escape hatch gets multi-target builds too. A `format` of `["esm", "cjs"]` (the `BuildFormat` type) derives a dual-format build — a require-able CJS output with default-export interop and `.d.cts` declarations alongside the ESM one. The `bundleNodeModules`, `bundledPackages` and `dtsExternals` options thread the dependency-bundling posture into both the JS and declaration passes. The `define` option forwards compile-time global replacements to both passes, merged with an auto-injected `process.env.__PACKAGE_VERSION__` constant.
50
+ - **Per-target build loop** — `deriveTargetGroupOptions` and `buildTargetGroups` map a target to its `tsdown` options and run the build once per target, exposed as a helper so the escape hatch gets multi-target builds too. A `format` of `["esm", "cjs"]` (the `BuildFormat` type) derives a dual-format build — a require-able CJS output with default-export interop and `.d.cts` declarations alongside the ESM one. The `bundleNodeModules`, `bundledPackages` and `dtsExternals` options thread the dependency-bundling posture into both the JS and declaration passes. A per-entry override partition can also set `platform` (the JS-pass target, `"browser"` for a client bundle), `css` (forwarded to tsdown's `css` option for `@tsdown/css`) and `outSubdir` (build the partition into an isolated `<group>/pkg/<subdir>/` sub-package). The `define` option forwards compile-time global replacements to both passes, merged with an auto-injected `process.env.__PACKAGE_VERSION__` constant.
51
51
  - **Loose files** — `normalizeLooseFiles` resolves a `LooseFiles` map of literal output filenames to `NormalizedLooseFile` descriptors, inferring the module format from each `.mjs`/`.cjs` key and raising `ConfigValidationError` on a path separator, an unsupported extension or an ambiguous `.js`. `buildTargetGroups` takes the normalized form as its `looseFiles` option and emits one extra single-entry, bundled, declaration-free and manifest-free pass per file per target group, inheriting the group's bundling posture so each file is self-contained.
52
52
  - **Bundled declarations** — each target runs two `tsdown` passes: a JavaScript pass that preserves per-module output, then a declaration-only pass that rolls every re-exported type into a single `.d.ts` per public entry (`deriveDtsPassOptions`). Per-module JavaScript stays intact while consumers keep reaching re-exported types through your published subpaths.
53
53
  - **API Extractor meta** — `generateMeta` runs [API Extractor](https://api-extractor.com/) over a package's emitted `.d.ts` to write an api-model bundle (`.api.json`, `tsdoc-metadata.json`, resolved `tsconfig.json`); `normalizeMetaOptions` fills the `MetaOptions` defaults that drive it.
@@ -3,7 +3,7 @@ import { cjsDefaultInterop } from "./cjs-default-interop.js";
3
3
  import { nodeBuiltinDefaultInterop } from "./node-builtin-default-interop.js";
4
4
  import { syncPublicDir } from "./sync-public.js";
5
5
  import { deriveDtsPassOptions, deriveTargetGroupOptions, outDirFor } from "./target-groups.js";
6
- import { join } from "node:path";
6
+ import { dirname, join } from "node:path";
7
7
 
8
8
  //#region src/build/build-target-groups.ts
9
9
  /**
@@ -55,27 +55,37 @@ async function buildTargetGroups(options) {
55
55
  ...part.format !== void 0 ? { format: part.format } : {},
56
56
  ...options.minify !== void 0 ? { minify: options.minify } : {},
57
57
  ...options.jsx !== void 0 ? { jsx: options.jsx } : {},
58
- ...options.define !== void 0 ? { define: options.define } : {}
58
+ ...options.define !== void 0 ? { define: options.define } : {},
59
+ ...part.platform !== void 0 ? { platform: part.platform } : {}
59
60
  };
60
61
  const js = deriveTargetGroupOptions(deriveInput);
61
62
  const dts = deriveDtsPassOptions(deriveInput);
63
+ const partOutDir = part.outSubdir !== void 0 ? join(js.outDir, part.outSubdir) : js.outDir;
62
64
  const targetGroup = {
63
65
  id: group.id,
64
66
  name: group.name,
65
67
  isProd: js.isProd
66
68
  };
69
+ const srcDir = dirname(Object.values(part.entry)[0] ?? "");
70
+ const jsEntry = part.outSubdir !== void 0 ? [
71
+ `${srcDir}/**/*.{ts,tsx,mts,cts}`,
72
+ `!${srcDir}/**/*.{test,spec}.{ts,tsx,mts,cts}`,
73
+ `!${srcDir}/**/*.d.{ts,cts,mts}`
74
+ ] : js.entry;
67
75
  const manifestPlugin = isBase ? emitManifest({
68
76
  targetGroup,
69
77
  devManifest: options.devManifest,
70
78
  transform: options.transform,
71
79
  sourceDir: options.cwd,
72
- dual: options.dualExports ?? js.format.includes("cjs")
80
+ dual: options.dualExports ?? js.format.includes("cjs"),
81
+ ...options.subdirExports !== void 0 ? { subdirExports: options.subdirExports } : {},
82
+ ...options.exeRewrite !== void 0 ? { exeRewrite: options.exeRewrite } : {}
73
83
  }) : void 0;
74
84
  await build({
75
85
  config: false,
76
86
  cwd: options.cwd,
77
- entry: js.entry,
78
- outDir: js.outDir,
87
+ entry: jsEntry,
88
+ outDir: partOutDir,
79
89
  format: js.format,
80
90
  platform: js.platform,
81
91
  sourcemap: js.sourcemap,
@@ -85,6 +95,7 @@ async function buildTargetGroups(options) {
85
95
  fixedExtension: js.fixedExtension,
86
96
  dts: js.dts,
87
97
  define: js.define,
98
+ ...part.css !== void 0 ? { css: part.css } : {},
88
99
  ...partExternals?.length || partBundleNodeModules || partBundle?.length ? { deps: {
89
100
  ...partExternals?.length ? { neverBundle: partExternals } : {},
90
101
  ...partBundle?.length ? { alwaysBundle: partBundle } : {},
@@ -104,7 +115,7 @@ async function buildTargetGroups(options) {
104
115
  config: false,
105
116
  cwd: options.cwd,
106
117
  entry: dts.entry,
107
- outDir: dts.outDir,
118
+ outDir: partOutDir,
108
119
  format: dts.format,
109
120
  platform: dts.platform,
110
121
  sourcemap: dts.sourcemap,
@@ -15,7 +15,7 @@ function deriveTargetGroupOptions(options) {
15
15
  format,
16
16
  unbundle: true,
17
17
  clean: true,
18
- platform: "node",
18
+ platform: options.platform ?? "node",
19
19
  fixedExtension: false,
20
20
  entry: options.entry,
21
21
  dts: false,
package/entry/extract.js CHANGED
@@ -1,5 +1,6 @@
1
1
  //#region src/entry/extract.ts
2
- const isTypeScriptFile = (p) => p.endsWith(".ts") || p.endsWith(".tsx");
2
+ const isDeclarationFile = (p) => p.endsWith(".d.ts") || p.endsWith(".d.cts") || p.endsWith(".d.mts");
3
+ const isTypeScriptFile = (p) => !isDeclarationFile(p) && (p.endsWith(".ts") || p.endsWith(".tsx"));
3
4
  /** /dist/*.js to /src/*.ts; otherwise unchanged. */
4
5
  const resolveToTypeScript = (p) => p.endsWith(".js") && p.includes("/dist/") ? p.replace("/dist/", "/src/").replace(/\.js$/, ".ts") : p;
5
6
  /** Resolve an export value to a source path: import || default || types (NOT require). */
@@ -29,9 +30,12 @@ function extractEntries(pkg, options = {}) {
29
30
  const entries = {};
30
31
  const exportPaths = {};
31
32
  const exportsAsIndexes = options.exportsAsIndexes ?? false;
33
+ const stripDot = (p) => p.replace(/^\.\//, "");
34
+ const excluded = new Set((options.excludeSources ?? []).map(stripDot));
35
+ const isExcluded = (src) => excluded.has(stripDot(src));
32
36
  const exports = pkg.exports;
33
37
  if (typeof exports === "string") {
34
- if (isTypeScriptFile(exports)) {
38
+ if (isTypeScriptFile(exports) && !isExcluded(exports)) {
35
39
  entries.index = exports;
36
40
  exportPaths.index = ".";
37
41
  }
@@ -41,6 +45,7 @@ function extractEntries(pkg, options = {}) {
41
45
  if (!sourcePath) continue;
42
46
  const resolved = resolveToTypeScript(sourcePath);
43
47
  if (!isTypeScriptFile(resolved)) continue;
48
+ if (isExcluded(sourcePath) || isExcluded(resolved)) continue;
44
49
  const name = createEntryName(key, exportsAsIndexes);
45
50
  if (name in entries) {
46
51
  const previousKey = exportPaths[name];
@@ -52,10 +57,11 @@ function extractEntries(pkg, options = {}) {
52
57
  const bin = pkg.bin;
53
58
  if (typeof bin === "string") {
54
59
  const resolved = resolveToTypeScript(bin);
55
- if (isTypeScriptFile(resolved)) entries["bin/cli"] = resolved;
60
+ if (isTypeScriptFile(resolved) && !isExcluded(bin) && !isExcluded(resolved)) entries["bin/cli"] = resolved;
56
61
  } else if (bin && typeof bin === "object") for (const [command, p] of Object.entries(bin)) {
57
62
  if (typeof p !== "string") continue;
58
63
  const resolved = resolveToTypeScript(p);
64
+ if (isExcluded(p) || isExcluded(resolved)) continue;
59
65
  if (isTypeScriptFile(resolved)) entries[`bin/${command}`] = resolved;
60
66
  }
61
67
  return {
@@ -5,7 +5,10 @@ import { readFileSync } from "node:fs";
5
5
  //#region src/entry/package-json-entries.ts
6
6
  /** Derive a tsdown `entry` record (name to source path) from a package.json. */
7
7
  function packageJsonEntries(options = {}) {
8
- return extractEntries(options.pkg ?? JSON.parse(readFileSync(resolve(options.cwd ?? process.cwd(), "package.json"), "utf-8")), { exportsAsIndexes: options.exportsAsIndexes }).entries;
8
+ return extractEntries(options.pkg ?? JSON.parse(readFileSync(resolve(options.cwd ?? process.cwd(), "package.json"), "utf-8")), {
9
+ exportsAsIndexes: options.exportsAsIndexes,
10
+ excludeSources: options.excludeSources
11
+ }).entries;
9
12
  }
10
13
 
11
14
  //#endregion
package/exe/build.js CHANGED
@@ -1,22 +1,37 @@
1
+ import { join } from "node:path";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+
1
5
  //#region src/exe/build.ts
2
6
  /** Compile each SEA binary via tsdown's exe mode. One tsdown build per spec. */
3
7
  async function runExeBuild(options) {
4
8
  const build = options.build ?? (await import("tsdown")).build;
5
- for (const spec of options.specs) await build({
6
- cwd: options.cwd,
7
- config: false,
8
- entry: [spec.entry],
9
- format: "esm",
10
- platform: "node",
11
- clean: false,
12
- deps: { alwaysBundle: (id) => !id.startsWith("node:") },
13
- exe: {
14
- fileName: spec.fileName,
15
- outDir: options.outDir,
16
- seaConfig: spec.seaConfig,
17
- targets: spec.targets
9
+ for (const spec of options.specs) {
10
+ const scratch = mkdtempSync(join(tmpdir(), "savvy-exe-"));
11
+ try {
12
+ await build({
13
+ cwd: options.cwd,
14
+ config: false,
15
+ entry: [spec.entry],
16
+ format: "esm",
17
+ platform: "node",
18
+ clean: false,
19
+ outDir: scratch,
20
+ deps: { alwaysBundle: (id) => !id.startsWith("node:") },
21
+ exe: {
22
+ fileName: spec.fileName,
23
+ outDir: options.outDir,
24
+ seaConfig: spec.seaConfig,
25
+ targets: spec.targets
26
+ }
27
+ });
28
+ } finally {
29
+ rmSync(scratch, {
30
+ recursive: true,
31
+ force: true
32
+ });
18
33
  }
19
- });
34
+ }
20
35
  }
21
36
 
22
37
  //#endregion
@@ -0,0 +1,12 @@
1
+ //#region src/exe/filename.ts
2
+ /**
3
+ * The exact filename `@tsdown/exe` emits for a SEA target, mirroring tsdown's
4
+ * `resolveOutputFileName`: base fileName + `-<platform>-<arch>` + `.exe` on win.
5
+ * Single source of truth so the manifest value never drifts from the on-disk file.
6
+ */
7
+ function computeExeFileName(fileName, target) {
8
+ return `${fileName}${`-${target.platform}-${target.arch}`}${target.platform === "win" ? ".exe" : ""}`;
9
+ }
10
+
11
+ //#endregion
12
+ export { computeExeFileName };
package/index.d.ts CHANGED
@@ -67,6 +67,19 @@ declare function defaultManifestTransform({
67
67
  }: {
68
68
  pkg: Json;
69
69
  }): Json;
70
+ /**
71
+ * Describes a SEA binary the bundler compiled for this package. When present,
72
+ * {@link transformManifest} rewrites every `exports`/`bin` value equal to `source`
73
+ * to the emitted binary path and adds it to `files` so it ships in the tarball.
74
+ */
75
+ interface ExeRewrite {
76
+ /** The exe entry source path (matches exports/bin values to rewrite). */
77
+ readonly source: string;
78
+ /** The emitted SEA filename (already suffixed, incl. .exe on win). */
79
+ readonly fileName: string;
80
+ /** Relative dir the binary is emitted into (e.g. "bin"). */
81
+ readonly dir: string;
82
+ }
70
83
  /**
71
84
  * Rewrite an exports map: TS string targets become a types/import conditions object.
72
85
  * Each TS condition also gets a `require` entry when `dual` is `true` (uniform) or when
@@ -74,8 +87,11 @@ declare function defaultManifestTransform({
74
87
  *
75
88
  * The output path is derived from the export KEY via the shared entry-name function,
76
89
  * never from the source path, so the manifest target always matches the emitted file.
90
+ *
91
+ * Export keys in `subdirExports` are built into an isolated `<key>/index.*` subdir (e.g. an
92
+ * RSPress `./runtime`), so their conditions gain an `/index` segment.
77
93
  */
78
- declare function transformExports(exports: unknown, dual?: DualExports): unknown;
94
+ declare function transformExports(exports: unknown, dual?: DualExports, subdirExports?: ReadonlySet<string>): unknown;
79
95
  /** Rewrite bin: TS targets to bin/[command].js (string to bin/cli.js); strip leading ./ otherwise. */
80
96
  declare function transformBin(bin: unknown): unknown;
81
97
  /** FINAL guard: strip leading ./ from bin paths (npm 11.x drops ./-prefixed bins). */
@@ -85,6 +101,10 @@ interface TransformManifestOptions {
85
101
  readonly transform?: ((pkg: Json) => Json) | undefined;
86
102
  /** Which exports emit dual import/require conditions. boolean = uniform; Set = per-export-key. */
87
103
  readonly dual?: DualExports | undefined;
104
+ /** Export keys built into a `<key>/index.*` subdir (e.g. an RSPress `./runtime`). */
105
+ readonly subdirExports?: ReadonlySet<string> | undefined;
106
+ /** When set, rewrite exports/bin values equal to `source` to the SEA path and add it to `files`. */
107
+ readonly exeRewrite?: ExeRewrite | undefined;
88
108
  }
89
109
  /** Apply the full standard manifest transform (excluding catalog resolution, done upstream). */
90
110
  declare function transformManifest(pkg: Json, options?: TransformManifestOptions): Json;
@@ -106,6 +126,10 @@ interface BuildEmittedManifestOptions {
106
126
  }) => Json) | undefined;
107
127
  /** Which exports emit dual import/require conditions. boolean (uniform) or a Set of export keys (per-entry). */
108
128
  readonly dual?: DualExports | undefined;
129
+ /** Export keys built into a `<key>/index.*` subdir (e.g. an RSPress `./runtime`). */
130
+ readonly subdirExports?: ReadonlySet<string> | undefined;
131
+ /** When set, rewrite exports/bin values equal to the exe source to the SEA path and add it to `files`. */
132
+ readonly exeRewrite?: ExeRewrite | undefined;
109
133
  }
110
134
  /** Compute the final manifest bytes for a TargetGroup (catalog resolution + standard transforms). */
111
135
  declare function buildEmittedManifest(options: BuildEmittedManifestOptions): Promise<Json>;
@@ -120,6 +144,10 @@ interface EmitManifestOptions {
120
144
  readonly sourceDir: string;
121
145
  /** Which exports emit dual import/require conditions. boolean (uniform) or a Set of export keys (per-entry). */
122
146
  readonly dual?: DualExports | undefined;
147
+ /** Export keys built into a `<key>/index.*` subdir (e.g. an RSPress `./runtime`). */
148
+ readonly subdirExports?: ReadonlySet<string> | undefined;
149
+ /** When set, rewrite exports/bin values equal to the exe source to the SEA path and add it to `files`. */
150
+ readonly exeRewrite?: ExeRewrite | undefined;
123
151
  }
124
152
  /** Rolldown plugin: emit the transformed package.json + LICENSE/README into the output pkg/ root. */
125
153
  declare function emitManifest(options: EmitManifestOptions): Plugin;
@@ -129,6 +157,8 @@ declare function emitManifest(options: EmitManifestOptions): Plugin;
129
157
  type TargetGroupId = string;
130
158
  /** An output module format the build can emit. */
131
159
  type BuildFormat = "esm" | "cjs";
160
+ /** Bundling platform for the JS pass. Defaults to "node". Use "browser" for web runtime partitions. */
161
+ type BuildPlatform = "node" | "browser" | "neutral";
132
162
  /** A prod/dev group to build: its folder id and the resolved package name its manifest carries. */
133
163
  interface BuildGroupSpec {
134
164
  readonly id: TargetGroupId;
@@ -142,6 +172,8 @@ interface DeriveOptions {
142
172
  readonly tsconfigPath: string;
143
173
  readonly devManifest: "preserve" | "resolve";
144
174
  readonly externals?: ReadonlyArray<string>;
175
+ /** JS-pass platform. Defaults to "node"; set "browser" for an RSPress runtime partition. */
176
+ readonly platform?: BuildPlatform | undefined;
145
177
  /** Output formats to emit. Defaults to esm-only when unset. */
146
178
  readonly format?: ReadonlyArray<BuildFormat> | undefined;
147
179
  /** Minify prod output (prod groups only; dev is never minified). Defaults to false. */
@@ -188,7 +220,7 @@ interface DerivedTsdownOptions {
188
220
  readonly unbundle: true;
189
221
  /** JS pass starts fresh; it owns the outDir before the dts pass appends to it. */
190
222
  readonly clean: true;
191
- readonly platform: "node";
223
+ readonly platform: BuildPlatform;
192
224
  /**
193
225
  * Controls output file extensions. Always false for this builder.
194
226
  *
@@ -270,6 +302,19 @@ declare function normalizeLooseFiles(files: LooseFiles): ReadonlyArray<Normalize
270
302
  //#region src/build/build-target-groups.d.ts
271
303
  /** Signature compatible with tsdown's `build(inlineConfig)`. */
272
304
  type TsdownBuild = (config: Record<string, unknown>) => Promise<unknown>;
305
+ /**
306
+ * CSS handling for a partition's JS pass, forwarded VERBATIM to tsdown's `css` option (consumed
307
+ * by `@tsdown/css`). Structurally typed so tsdown-plugins takes no dependency on `@tsdown/css`.
308
+ * The package whose runtime is built must install `@tsdown/css`; tsdown loads it lazily.
309
+ */
310
+ interface CssOptions {
311
+ readonly modules?: boolean | {
312
+ readonly localsConvention?: string;
313
+ readonly namedExport?: boolean;
314
+ readonly [k: string]: unknown;
315
+ };
316
+ readonly [k: string]: unknown;
317
+ }
273
318
  /**
274
319
  * One entry partition built with its own format + bundling posture, layered into the
275
320
  * SAME outDir as the base build (clean:false). Anything omitted falls back to the base
@@ -283,6 +328,17 @@ interface EntryOverride {
283
328
  readonly bundleNodeModules?: boolean | undefined;
284
329
  readonly bundledPackages?: ReadonlyArray<string> | undefined;
285
330
  readonly dtsExternals?: ReadonlyArray<string> | undefined;
331
+ /** JS-pass platform for this partition. Defaults to the base "node". Use "browser" for a web runtime. */
332
+ readonly platform?: BuildPlatform | undefined;
333
+ /** CSS handling forwarded to tsdown's `css` option (JS pass only). Enables `@tsdown/css`. */
334
+ readonly css?: CssOptions | undefined;
335
+ /**
336
+ * Build this partition into `<groupOutDir>/<outSubdir>/` instead of the shared group root.
337
+ * Isolates a sub-package (e.g. an RSPress `./runtime`) so its bundleless per-file output cannot
338
+ * collide with the base partition's output and its barrel path is deterministic. The partition's
339
+ * entry should be `{ index: <barrel source> }` so it emits `<outSubdir>/index.js` + `<outSubdir>/index.d.ts`.
340
+ */
341
+ readonly outSubdir?: string | undefined;
286
342
  }
287
343
  interface BuildTargetGroupsOptions {
288
344
  readonly cwd: string;
@@ -375,6 +431,10 @@ interface BuildTargetGroupsOptions {
375
431
  * `format`-includes-cjs behavior.
376
432
  */
377
433
  readonly dualExports?: DualExports | undefined;
434
+ /** Export keys built into a `<key>/index.*` subdir (e.g. an RSPress `./runtime`). */
435
+ readonly subdirExports?: ReadonlySet<string> | undefined;
436
+ /** When set, rewrite the emitted manifest's exports/bin values equal to the exe source to the SEA path and add it to `files`. */
437
+ readonly exeRewrite?: ExeRewrite | undefined;
378
438
  /** Injectable for tests; defaults to tsdown's build. */
379
439
  readonly build?: TsdownBuild;
380
440
  }
@@ -713,6 +773,8 @@ interface PackageJsonLike {
713
773
  }
714
774
  interface ExtractOptions {
715
775
  readonly exportsAsIndexes?: boolean | undefined;
776
+ /** Source paths to NOT turn into JS build entries (e.g. an exe entry compiled as a SEA). */
777
+ readonly excludeSources?: ReadonlyArray<string> | undefined;
716
778
  }
717
779
  interface ExtractResult {
718
780
  /** entry name to TS source path */
@@ -759,6 +821,14 @@ interface RunExeBuildOptions {
759
821
  /** Compile each SEA binary via tsdown's exe mode. One tsdown build per spec. */
760
822
  declare function runExeBuild(options: RunExeBuildOptions): Promise<void>;
761
823
  //#endregion
824
+ //#region src/exe/filename.d.ts
825
+ /**
826
+ * The exact filename `@tsdown/exe` emits for a SEA target, mirroring tsdown's
827
+ * `resolveOutputFileName`: base fileName + `-<platform>-<arch>` + `.exe` on win.
828
+ * Single source of truth so the manifest value never drifts from the on-disk file.
829
+ */
830
+ declare function computeExeFileName(fileName: string, target: ExeTarget): string;
831
+ //#endregion
762
832
  //#region src/meta/generate.d.ts
763
833
  interface GenerateMetaOptions {
764
834
  readonly cwd: string;
@@ -998,7 +1068,7 @@ declare class OutputRenderer extends OutputRenderer_base {}
998
1068
  declare const OutputRendererLive: Layer.Layer<OutputRenderer, never, never>;
999
1069
  //#endregion
1000
1070
  //#region src/report/pipeline.d.ts
1001
- declare const ReportPipelineLive: Layer.Layer<FormatSelector | EnvironmentDetector | ExecutorResolver | OutputRenderer, never, never>;
1071
+ declare const ReportPipelineLive: Layer.Layer<EnvironmentDetector | ExecutorResolver | FormatSelector | OutputRenderer, never, never>;
1002
1072
  interface RenderReportOptions {
1003
1073
  readonly explicitFormat?: OutputFormat;
1004
1074
  /** Override env detection (mainly for tests). */
@@ -1509,5 +1579,5 @@ declare function resolveTargets(options: {
1509
1579
  baseName: string;
1510
1580
  }): TargetResolution;
1511
1581
  //#endregion
1512
- export { type BuildEmittedManifestOptions, type BuildFormat, type BuildGroupSpec, type BuildReport, BuildReport as BuildReportSchema, type BuildTargetGroupsOptions, CatalogAssemblyError, CatalogResolutionError, CiAnnotationsFormatter, ConfigValidationError, ConfigValidator, ConfigValidatorLive, DEFAULT_EXE_NODE_VERSION, type DeriveOptions, type DerivedTsdownOptions, type DualExports, type EmitManifestOptions, type EntryOverride, type Environment, EnvironmentDetector, EnvironmentDetectorLive, type ExeBuild, type ExeConfig, 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 NormalizedExe, type NormalizedLooseFile, type NormalizedMeta, type OutputFormat, OutputRenderer, OutputRendererLive, type PackageJsonEntriesOptions, type PackageJsonLike, type PkgOsCpu, 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, 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 ValidationInput, type WarningSuppressionRule, buildEmittedManifest, buildResolvedTsconfig, buildTargetGroups, cjsDefaultInterop, createEntryName, createTimer, defaultManifestTransform, deriveTargetGroupOptions, emitManifest, extractEntries, formatTime, generateBuildReportSchema, generateMeta, isTargetObject, nodeBuiltinDefaultInterop, normalizeBinPaths, normalizeExeOptions, normalizeLooseFiles, normalizeMetaOptions, packageJsonEntries, readTsconfigJsx, removeDeclarationMaps, renderReport, resolveJsxConfig, resolveManifest, resolvePortableTsconfig, resolveTargets, runExeBuild, syncPublicDir, transformBin, transformExports, transformManifest, writeResolvedTsconfig, writeTargetsBinding };
1582
+ export { type BuildEmittedManifestOptions, type BuildFormat, type BuildGroupSpec, 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 DualExports, type EmitManifestOptions, 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 NormalizedExe, type NormalizedLooseFile, type NormalizedMeta, type OutputFormat, OutputRenderer, OutputRendererLive, type PackageJsonEntriesOptions, type PackageJsonLike, type PkgOsCpu, 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, 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 ValidationInput, type WarningSuppressionRule, buildEmittedManifest, buildResolvedTsconfig, buildTargetGroups, cjsDefaultInterop, computeExeFileName, createEntryName, createTimer, defaultManifestTransform, deriveTargetGroupOptions, emitManifest, extractEntries, formatTime, generateBuildReportSchema, generateMeta, isTargetObject, nodeBuiltinDefaultInterop, normalizeBinPaths, normalizeExeOptions, normalizeLooseFiles, normalizeMetaOptions, packageJsonEntries, readTsconfigJsx, removeDeclarationMaps, renderReport, resolveJsxConfig, resolveManifest, resolvePortableTsconfig, resolveTargets, runExeBuild, syncPublicDir, transformBin, transformExports, transformManifest, writeResolvedTsconfig, writeTargetsBinding };
1513
1583
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -18,6 +18,7 @@ import { ConfigValidatorLive } from "./config-validation/ConfigValidatorLive.js"
18
18
  import { buildResolvedTsconfig, writeResolvedTsconfig } from "./dts/resolved-tsconfig.js";
19
19
  import { packageJsonEntries } from "./entry/package-json-entries.js";
20
20
  import { runExeBuild } from "./exe/build.js";
21
+ import { computeExeFileName } from "./exe/filename.js";
21
22
  import { readTsconfigJsx, resolveJsxConfig } from "./jsx/config.js";
22
23
  import { normalizeMetaOptions } from "./meta/config.js";
23
24
  import { TsconfigResolver, resolvePortableTsconfig } from "./meta/tsconfig-resolver.js";
@@ -42,4 +43,4 @@ import { generateBuildReportSchema } from "./report/schema-export.js";
42
43
  import { writeTargetsBinding } from "./targets/binding.js";
43
44
  import { CatalogAssemblyError, CatalogResolutionError } from "workspaces-effect";
44
45
 
45
- export { 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, buildEmittedManifest, buildResolvedTsconfig, buildTargetGroups, cjsDefaultInterop, createEntryName, createTimer, defaultManifestTransform, deriveTargetGroupOptions, emitManifest, extractEntries, formatTime, generateBuildReportSchema, generateMeta, isTargetObject, nodeBuiltinDefaultInterop, normalizeBinPaths, normalizeExeOptions, normalizeLooseFiles, normalizeMetaOptions, packageJsonEntries, readTsconfigJsx, removeDeclarationMaps, renderReport, resolveJsxConfig, resolveManifest, resolvePortableTsconfig, resolveTargets, runExeBuild, syncPublicDir, transformBin, transformExports, transformManifest, writeResolvedTsconfig, writeTargetsBinding };
46
+ export { 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, buildEmittedManifest, buildResolvedTsconfig, buildTargetGroups, cjsDefaultInterop, computeExeFileName, createEntryName, createTimer, defaultManifestTransform, deriveTargetGroupOptions, emitManifest, extractEntries, formatTime, generateBuildReportSchema, generateMeta, isTargetObject, nodeBuiltinDefaultInterop, normalizeBinPaths, normalizeExeOptions, normalizeLooseFiles, normalizeMetaOptions, packageJsonEntries, readTsconfigJsx, removeDeclarationMaps, renderReport, resolveJsxConfig, resolveManifest, resolvePortableTsconfig, resolveTargets, runExeBuild, syncPublicDir, transformBin, transformExports, transformManifest, writeResolvedTsconfig, writeTargetsBinding };
@@ -19,7 +19,9 @@ async function buildEmittedManifest(options) {
19
19
  pkg: p,
20
20
  targetGroup
21
21
  }) : void 0,
22
- dual: options.dual ?? false
22
+ dual: options.dual ?? false,
23
+ subdirExports: options.subdirExports,
24
+ exeRewrite: options.exeRewrite
23
25
  });
24
26
  }
25
27
  /** Rolldown plugin: emit the transformed package.json + LICENSE/README into the output pkg/ root. */
@@ -33,7 +35,9 @@ function emitManifest(options) {
33
35
  targetGroup: options.targetGroup,
34
36
  devManifest: options.devManifest ?? "preserve",
35
37
  transform: options.transform,
36
- dual: options.dual
38
+ dual: options.dual,
39
+ subdirExports: options.subdirExports,
40
+ exeRewrite: options.exeRewrite
37
41
  });
38
42
  this.emitFile({
39
43
  type: "asset",
@@ -42,6 +42,26 @@ function defaultManifestTransform({ pkg }) {
42
42
  return out;
43
43
  }
44
44
  const stripLeadingDotSlash = (p) => p.startsWith("./") ? p.slice(2) : p;
45
+ const sameSource = (a, b) => stripLeadingDotSlash(a) === stripLeadingDotSlash(b);
46
+ /** The export-path ("./bin/<file>") and files-entry ("bin/<file>") forms for a SEA. */
47
+ const exeRelPath = (r) => {
48
+ const filesEntry = `${r.dir}/${r.fileName}`;
49
+ return {
50
+ exportPath: `./${filesEntry}`,
51
+ filesEntry
52
+ };
53
+ };
54
+ /** Rewrite exports values that equal the exe source to a plain SEA-path string. */
55
+ const rewriteExeExports = (exports, r) => {
56
+ const { exportPath } = exeRelPath(r);
57
+ if (typeof exports === "string") return sameSource(exports, r.source) ? exportPath : exports;
58
+ if (exports && typeof exports === "object") {
59
+ const out = {};
60
+ for (const [key, value] of Object.entries(exports)) out[key] = typeof value === "string" && sameSource(value, r.source) ? exportPath : value;
61
+ return out;
62
+ }
63
+ return exports;
64
+ };
45
65
  /**
46
66
  * Built .js output basename for an export, derived from the entry NAME (the basename
47
67
  * the build actually emits) rather than the source path. The entry namer flattens
@@ -50,15 +70,16 @@ const stripLeadingDotSlash = (p) => p.startsWith("./") ? p.slice(2) : p;
50
70
  * always matches the file tsdown emits. The build never sets exportsAsIndexes, so the
51
71
  * manifest mirrors the flat (false) naming here.
52
72
  */
53
- const toBuiltJs = (exportKey) => `./${createEntryName(exportKey, false)}.js`;
54
- const toBuiltDts = (exportKey) => toBuiltJs(exportKey).replace(/\.js$/, ".d.ts");
55
- const toBuiltCjs = (exportKey) => toBuiltJs(exportKey).replace(/\.js$/, ".cjs");
56
- const isTs = (p) => p.endsWith(".ts") || p.endsWith(".tsx");
73
+ const toBuiltJs = (exportKey, subdirExports) => subdirExports?.has(exportKey) ? `./${createEntryName(exportKey, false)}/index.js` : `./${createEntryName(exportKey, false)}.js`;
74
+ const toBuiltDts = (exportKey, subdirExports) => toBuiltJs(exportKey, subdirExports).replace(/\.js$/, ".d.ts");
75
+ const toBuiltCjs = (exportKey, subdirExports) => toBuiltJs(exportKey, subdirExports).replace(/\.js$/, ".cjs");
76
+ const isDeclarationFile = (p) => p.endsWith(".d.ts") || p.endsWith(".d.cts") || p.endsWith(".d.mts");
77
+ const isTs = (p) => !isDeclarationFile(p) && (p.endsWith(".ts") || p.endsWith(".tsx"));
57
78
  /** Build the conditions object for a TS export target (adds require when dual-format). */
58
- const tsConditions = (exportKey, dual) => ({
59
- types: toBuiltDts(exportKey),
60
- import: toBuiltJs(exportKey),
61
- ...dual ? { require: toBuiltCjs(exportKey) } : {}
79
+ const tsConditions = (exportKey, dual, subdirExports) => ({
80
+ types: toBuiltDts(exportKey, subdirExports),
81
+ import: toBuiltJs(exportKey, subdirExports),
82
+ ...dual ? { require: toBuiltCjs(exportKey, subdirExports) } : {}
62
83
  });
63
84
  /**
64
85
  * Rewrite an exports map: TS string targets become a types/import conditions object.
@@ -67,9 +88,12 @@ const tsConditions = (exportKey, dual) => ({
67
88
  *
68
89
  * The output path is derived from the export KEY via the shared entry-name function,
69
90
  * never from the source path, so the manifest target always matches the emitted file.
91
+ *
92
+ * Export keys in `subdirExports` are built into an isolated `<key>/index.*` subdir (e.g. an
93
+ * RSPress `./runtime`), so their conditions gain an `/index` segment.
70
94
  */
71
- function transformExports(exports, dual = false) {
72
- if (typeof exports === "string") return isTs(exports) ? tsConditions(".", isDualKey(dual, ".")) : exports;
95
+ function transformExports(exports, dual = false, subdirExports) {
96
+ if (typeof exports === "string") return isTs(exports) ? tsConditions(".", isDualKey(dual, "."), subdirExports) : exports;
73
97
  if (exports && typeof exports === "object") {
74
98
  const out = {};
75
99
  for (const [key, value] of Object.entries(exports)) {
@@ -77,8 +101,8 @@ function transformExports(exports, dual = false) {
77
101
  out[key] = value;
78
102
  continue;
79
103
  }
80
- if (typeof value === "string" && isTs(value)) out[key] = tsConditions(key, isDualKey(dual, key));
81
- else out[key] = transformExports(value, dual);
104
+ if (typeof value === "string" && isTs(value)) out[key] = tsConditions(key, isDualKey(dual, key), subdirExports);
105
+ else out[key] = transformExports(value, dual, subdirExports);
82
106
  }
83
107
  return out;
84
108
  }
@@ -112,9 +136,24 @@ function transformManifest(pkg, options = {}) {
112
136
  ...rest,
113
137
  private: isPrivate
114
138
  };
139
+ if (options.exeRewrite) {
140
+ const r = options.exeRewrite;
141
+ const { exportPath, filesEntry } = exeRelPath(r);
142
+ if (result.exports !== void 0 && result.exports !== null) result.exports = rewriteExeExports(result.exports, r);
143
+ if (typeof result.bin === "string") {
144
+ if (sameSource(result.bin, r.source)) result.bin = exportPath;
145
+ } else if (result.bin && typeof result.bin === "object") {
146
+ const nextBin = {};
147
+ for (const [cmd, val] of Object.entries(result.bin)) nextBin[cmd] = sameSource(val, r.source) ? exportPath : val;
148
+ result.bin = nextBin;
149
+ }
150
+ const files = Array.isArray(result.files) ? result.files.slice() : [];
151
+ if (!files.includes(filesEntry)) files.push(filesEntry);
152
+ result.files = files;
153
+ }
115
154
  if (result.exports !== void 0 && result.exports !== null) {
116
155
  const original = result.exports;
117
- const transformed = transformExports(original, options.dual ?? false);
156
+ const transformed = transformExports(original, options.dual ?? false, options.subdirExports);
118
157
  const asMap = typeof original === "string" ? { ".": transformed } : { ...transformed };
119
158
  if (!("./package.json" in asMap)) asMap["./package.json"] = "./package.json";
120
159
  result.exports = asMap;
@@ -97,6 +97,7 @@ var TsconfigResolver = class TsconfigResolver {
97
97
  [ScriptTarget.ES2022, "es2022"],
98
98
  [ScriptTarget.ES2023, "es2023"],
99
99
  [ScriptTarget.ES2024, "es2024"],
100
+ [ScriptTarget.ES2025, "es2025"],
100
101
  [ScriptTarget.ESNext, "esnext"],
101
102
  [ScriptTarget.JSON, "json"]
102
103
  ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/tsdown-plugins",
3
- "version": "0.4.2",
3
+ "version": "0.6.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",
@@ -33,7 +33,7 @@
33
33
  "@microsoft/tsdoc": "^0.16.0",
34
34
  "@microsoft/tsdoc-config": "^0.18.1",
35
35
  "deep-equal": "^2.2.3",
36
- "json-schema-effect": "^0.2.2",
36
+ "json-schema-effect": "^0.2.4",
37
37
  "picocolors": "^1.1.1",
38
38
  "sort-package-json": "^4.0.0",
39
39
  "std-env": "^4.1.0",