@savvy-web/tsdown-plugins 0.7.0 → 0.9.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.
Files changed (57) hide show
  1. package/README.md +2 -1
  2. package/build/build-target-groups.js +42 -9
  3. package/build/cjs-default-interop.js +1 -0
  4. package/build/loose-files.js +1 -0
  5. package/build/node-builtin-default-interop.js +15 -9
  6. package/build/strip-maps.js +1 -0
  7. package/build/sync-public.js +1 -0
  8. package/build/target-groups.js +7 -2
  9. package/catalog/resolve-catalogs.js +1 -0
  10. package/changesets/next-versions.js +1 -0
  11. package/config-validation/ConfigValidator.js +5 -1
  12. package/config-validation/ConfigValidatorLive.js +6 -2
  13. package/dts/resolved-tsconfig.js +10 -2
  14. package/entry/extract.js +1 -0
  15. package/entry/package-json-entries.js +5 -1
  16. package/errors.js +10 -2
  17. package/exe/build.js +20 -2
  18. package/exe/config.js +6 -1
  19. package/exe/filename.js +1 -0
  20. package/index.d.ts +679 -593
  21. package/index.js +8 -4
  22. package/jsx/config.js +2 -0
  23. package/manifest/emit-manifest.js +10 -2
  24. package/manifest/transform.js +18 -4
  25. package/meta/api-extractor.js +41 -1
  26. package/meta/config.js +5 -1
  27. package/meta/generate.js +6 -2
  28. package/meta/optimistic.js +1 -0
  29. package/meta/run-pass.js +75 -0
  30. package/meta/tsconfig-resolver.js +12 -12
  31. package/package.json +1 -1
  32. package/report/collector.js +141 -0
  33. package/report/formatters/ci-annotations.js +9 -2
  34. package/report/formatters/diagnostics.js +23 -0
  35. package/report/formatters/json.js +1 -0
  36. package/report/formatters/markdown.js +19 -9
  37. package/report/formatters/silent.js +1 -0
  38. package/report/formatters/terminal.js +32 -3
  39. package/report/issues-artifact.js +88 -0
  40. package/report/layers/EnvironmentDetectorLive.js +1 -0
  41. package/report/layers/ExecutorResolverLive.js +1 -0
  42. package/report/layers/FormatSelectorLive.js +1 -0
  43. package/report/layers/OutputRendererLive.js +2 -1
  44. package/report/metrics-plugin.js +63 -0
  45. package/report/pipeline.js +6 -1
  46. package/report/schema.js +52 -10
  47. package/report/services/EnvironmentDetector.js +1 -0
  48. package/report/services/ExecutorResolver.js +1 -0
  49. package/report/services/FormatSelector.js +1 -0
  50. package/report/services/OutputRenderer.js +1 -0
  51. package/report/timer.js +6 -1
  52. package/report/tsdown-logger.js +42 -0
  53. package/targets/binding.js +5 -1
  54. package/targets/config.js +5 -1
  55. package/targets/resolve-targets.js +5 -1
  56. package/tsdoc-metadata.json +11 -0
  57. package/report/schema-export.js +0 -18
package/README.md CHANGED
@@ -50,8 +50,9 @@ export default defineConfig({
50
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
- - **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.
53
+ - **API Extractor meta** — `runMetaPass` is the single meta-generation orchestrator the bundler front door and both self-hosting escape hatches share: it derives the export paths, applies the optimistic next-version forward-look and drives API Extractor over a package's emitted `.d.ts` to write an api-model bundle (`.api.json`, `tsdoc-metadata.json`, resolved `tsconfig.json`). `generateMeta` is the lower-level pass it wraps, and `normalizeMetaOptions` fills the `MetaOptions` defaults that drive it.
54
54
  - **Output reporter** — `renderReport` plus the `BuildReport` schema and a set of formatters (terminal, JSON, markdown, CI annotations, silent) render a build report for humans, agents or CI.
55
+ - **Issues artifact** — `writeIssuesArtifact` (with the pure `flattenIssues` and `serializeIssues` behind it) writes a deduplicated `dist/<target>/issues.json` on every build, collecting the build's warnings, errors and suppressed diagnostics in a stable JSON shape (`BuildIssues`/`PlainDiagnostic`) so an agent or CI script reads the diagnostics straight from disk instead of parsing terminal output.
55
56
 
56
57
  ## Effect
57
58
 
@@ -1,4 +1,7 @@
1
1
  import { emitManifest } from "../manifest/emit-manifest.js";
2
+ import { buildMetricsPlugin } from "../report/metrics-plugin.js";
3
+ import { createTimer } from "../report/timer.js";
4
+ import { createTsdownLogger } from "../report/tsdown-logger.js";
2
5
  import { cjsDefaultInterop } from "./cjs-default-interop.js";
3
6
  import { nodeBuiltinDefaultInterop } from "./node-builtin-default-interop.js";
4
7
  import { syncPublicDir } from "./sync-public.js";
@@ -20,11 +23,28 @@ import { dirname, join } from "node:path";
20
23
  * build (JS and the dts plugin share it), so a single pass cannot give per-module JS + bundled
21
24
  * dts. Per-module dts breaks type portability (TS2883); bundling the JS re-bundles workspace
22
25
  * consumers. The split keeps per-module JS AND rolled-up, self-contained declarations.
26
+ * @public
23
27
  */
24
28
  async function buildTargetGroups(options) {
25
29
  const build = options.build ?? (await import("tsdown")).build;
26
30
  const publicDir = join(options.cwd, "public");
31
+ const collector = options.collector;
32
+ const verbose = options.verbose ?? false;
33
+ const instrument = (groupId) => collector === void 0 ? {} : {
34
+ logLevel: "silent",
35
+ customLogger: createTsdownLogger(collector, groupId)
36
+ };
37
+ const metricsPlugins = (groupId, pass) => collector === void 0 ? [] : [buildMetricsPlugin(collector, groupId, pass, verbose)];
38
+ const timed = async (groupId, pass, run) => {
39
+ const timer = createTimer();
40
+ try {
41
+ await run();
42
+ } finally {
43
+ if (collector !== void 0) collector.recordPassTiming(groupId, pass, timer.elapsed());
44
+ }
45
+ };
27
46
  for (const group of options.groups) {
47
+ if (collector !== void 0) collector.registerGroup(group.id, Object.keys(options.entry));
28
48
  const partitions = [{
29
49
  entry: options.entry,
30
50
  ...options.format !== void 0 ? { format: options.format } : {},
@@ -81,7 +101,7 @@ async function buildTargetGroups(options) {
81
101
  ...options.subdirExports !== void 0 ? { subdirExports: options.subdirExports } : {},
82
102
  ...options.exeRewrite !== void 0 ? { exeRewrite: options.exeRewrite } : {}
83
103
  }) : void 0;
84
- await build({
104
+ await timed(group.id, "js", () => build({
85
105
  config: false,
86
106
  cwd: options.cwd,
87
107
  entry: jsEntry,
@@ -95,6 +115,7 @@ async function buildTargetGroups(options) {
95
115
  fixedExtension: js.fixedExtension,
96
116
  dts: js.dts,
97
117
  define: js.define,
118
+ ...instrument(group.id),
98
119
  ...part.css !== void 0 ? { css: part.css } : {},
99
120
  ...partExternals?.length || partBundleNodeModules || partBundle?.length ? { deps: {
100
121
  ...partExternals?.length ? { neverBundle: partExternals } : {},
@@ -106,12 +127,14 @@ async function buildTargetGroups(options) {
106
127
  plugins: [
107
128
  ...manifestPlugin ? [manifestPlugin] : [],
108
129
  ...js.format.includes("cjs") ? [nodeBuiltinDefaultInterop(), cjsDefaultInterop()] : [],
109
- ...options.extraPlugins ?? []
130
+ ...options.extraPlugins ?? [],
131
+ ...metricsPlugins(group.id, "js")
110
132
  ]
111
- });
133
+ }));
112
134
  if (isBase) syncPublicDir(publicDir, join(js.outDir, "public"));
113
135
  const dtsNeverBundle = [...partExternals ?? [], ...partDtsExternals ?? []];
114
- await build({
136
+ if (Object.keys(dts.entry).length === 0) continue;
137
+ await timed(group.id, "dts", () => build({
115
138
  config: false,
116
139
  cwd: options.cwd,
117
140
  entry: dts.entry,
@@ -124,6 +147,7 @@ async function buildTargetGroups(options) {
124
147
  fixedExtension: dts.fixedExtension,
125
148
  dts: dts.dts,
126
149
  define: dts.define,
150
+ ...instrument(group.id),
127
151
  ...dtsNeverBundle.length > 0 || partBundleNodeModules || dts.bundledPackages ? { deps: {
128
152
  ...dtsNeverBundle.length > 0 ? { neverBundle: dtsNeverBundle } : {},
129
153
  ...partBundleNodeModules ? {
@@ -135,8 +159,12 @@ async function buildTargetGroups(options) {
135
159
  } : {}
136
160
  } } : {},
137
161
  ...dts.jsx !== void 0 ? { inputOptions: { jsx: dts.jsx } } : {},
138
- plugins: [...dts.format.includes("cjs") ? [nodeBuiltinDefaultInterop(), cjsDefaultInterop()] : [], ...options.extraPlugins ?? []]
139
- });
162
+ plugins: [
163
+ ...dts.format.includes("cjs") ? [nodeBuiltinDefaultInterop(), cjsDefaultInterop()] : [],
164
+ ...options.extraPlugins ?? [],
165
+ ...metricsPlugins(group.id, "dts")
166
+ ]
167
+ }));
140
168
  }
141
169
  const looseOutDir = outDirFor(options.cwd, group.id);
142
170
  const isProdGroup = group.id !== "dev";
@@ -147,7 +175,7 @@ async function buildTargetGroups(options) {
147
175
  };
148
176
  for (const lf of options.looseFiles ?? []) {
149
177
  const hasCjs = lf.format === "cjs";
150
- await build({
178
+ await timed(group.id, "loose", () => build({
151
179
  config: false,
152
180
  cwd: options.cwd,
153
181
  entry: { [lf.entryName]: lf.source },
@@ -164,10 +192,15 @@ async function buildTargetGroups(options) {
164
192
  "process.env.__PACKAGE_VERSION__": JSON.stringify(options.version),
165
193
  ...options.define
166
194
  },
195
+ ...instrument(group.id),
167
196
  ...Object.keys(looseDeps).length > 0 ? { deps: looseDeps } : {},
168
197
  ...hasCjs ? { cjsDefault: true } : {},
169
- plugins: [...hasCjs ? [nodeBuiltinDefaultInterop(), cjsDefaultInterop()] : [], ...options.extraPlugins ?? []]
170
- });
198
+ plugins: [
199
+ ...hasCjs ? [nodeBuiltinDefaultInterop(), cjsDefaultInterop()] : [],
200
+ ...options.extraPlugins ?? [],
201
+ ...metricsPlugins(group.id, "loose")
202
+ ]
203
+ }));
171
204
  }
172
205
  }
173
206
  }
@@ -75,6 +75,7 @@ const INTEROP_FOOTER = [
75
75
  *
76
76
  * The emitted footer is also self-guarded (`module.exports.default !== void 0`), so it is a
77
77
  * runtime no-op whenever the static gate is ever too generous.
78
+ * @public
78
79
  */
79
80
  function cjsDefaultInterop() {
80
81
  return {
@@ -13,6 +13,7 @@ const EXT_FORMAT = {
13
13
  * a missing `source` is surfaced later by tsdown's entry resolution. Throws
14
14
  * {@link ConfigValidationError} on any structural problem so the bundler's ConfigValidator
15
15
  * surfaces it as a typed, fast-fail config error.
16
+ * @public
16
17
  */
17
18
  function normalizeLooseFiles(files) {
18
19
  const out = [];
@@ -18,13 +18,15 @@ function isNodeBuiltin(spec) {
18
18
  * Why this exists — a rolldown 1.1.0 codegen defect (verified against the latest
19
19
  * published rolldown 1.1.0 / tsdown 0.22.2, with no newer release to upgrade to):
20
20
  *
21
- * // SOURCE (e.g. vfile's lib/minproc.js)
22
- * export {default as minproc} from 'node:process'
23
- * // ...consumed as minproc.cwd()
21
+ * ```ts
22
+ * // SOURCE (e.g. vfile's lib/minproc.js)
23
+ * export {default as minproc} from 'node:process'
24
+ * // ...consumed as minproc.cwd()
24
25
  *
25
- * // rolldown CJS OUTPUT (BROKEN)
26
- * let node_process = require("node:process");
27
- * node_process.default.cwd() // <- require("node:process").default is undefined
26
+ * // rolldown CJS OUTPUT (BROKEN)
27
+ * let node_process = require("node:process");
28
+ * node_process.default.cwd() // <- require("node:process").default is undefined
29
+ * ```
28
30
  *
29
31
  * For a default import of an EXTERNAL Node builtin, rolldown emits a bare
30
32
  * `require("node:x")` WITHOUT its `__toESM` interop wrapper, yet still accesses
@@ -41,14 +43,18 @@ function isNodeBuiltin(spec) {
41
43
  * layer, which is why the correction happens here on the source.
42
44
  *
43
45
  * Rewrites (the two static forms that occur in practice, anchored to statement start):
44
- * - `import NAME from "node:x"` -> `import * as NAME from "node:x"`
45
- * - `export { default as NAME } from "node:x"` -> `export * as NAME from "node:x"`
46
- * - `import NAME, { a, b } from "node:x"` -> `import * as NAME from "node:x"; import { a, b } from "node:x"`
46
+ *
47
+ * ```ts
48
+ * import NAME from "node:x" -> import * as NAME from "node:x"
49
+ * export { default as NAME } from "node:x" -> export * as NAME from "node:x"
50
+ * import NAME, { a, b } from "node:x" -> import * as NAME from "node:x"; import { a, b } from "node:x"
51
+ * ```
47
52
  *
48
53
  * The namespace binding NAME carries the builtin's named exports (`NAME.cwd`,
49
54
  * `NAME.join`, ...), which is exactly how a default import of a builtin is consumed
50
55
  * in practice. ESM output is unaffected at runtime (a namespace import of a builtin
51
56
  * resolves to the same members), so the plugin is safe to attach to dual builds.
57
+ * @public
52
58
  */
53
59
  function nodeBuiltinDefaultInterop() {
54
60
  const DEFAULT_REEXPORT = /(^|\n)([ \t]*)export\s*\{\s*default\s+as\s+([A-Za-z_$][\w$]*)\s*\}\s*from\s*(["'])([^"']+)\4/g;
@@ -15,6 +15,7 @@ import { readdirSync, rmSync } from "node:fs";
15
15
  *
16
16
  * Recurses, but skips `node_modules` so it does not traverse a self-contained bundle's
17
17
  * vendored tree — only the package's own emitted declarations carry maps worth stripping.
18
+ * @public
18
19
  */
19
20
  function removeDeclarationMaps(pkgDir) {
20
21
  const removed = [];
@@ -42,6 +42,7 @@ function pruneEmptyDirs(dir) {
42
42
  *
43
43
  * The byte-diff keeps unchanged files (and their timestamps) untouched, so a large copied asset
44
44
  * tree — e.g. the mcp markdown corpus — is not rewritten on every build.
45
+ * @public
45
46
  */
46
47
  function syncPublicDir(sourceDir, targetDir) {
47
48
  if (!existsSync(sourceDir)) return;
@@ -3,7 +3,11 @@ import { join } from "node:path";
3
3
  //#region src/build/target-groups.ts
4
4
  /** The output dir for a group: dev -> dist/dev/pkg, prod -> dist/prod/<group>/pkg. */
5
5
  const outDirFor = (cwd, group) => group === "dev" ? join(cwd, "dist/dev/pkg") : join(cwd, "dist/prod", group, "pkg");
6
- /** Derive the JS-pass tsdown options for one TargetGroup (per-module JS, no dts). */
6
+ /**
7
+ * Derive the JS-pass tsdown options for one TargetGroup (per-module JS, no dts).
8
+ *
9
+ * @public
10
+ */
7
11
  function deriveTargetGroupOptions(options) {
8
12
  const isProd = options.group !== "dev";
9
13
  const format = options.format ?? ["esm"];
@@ -32,6 +36,7 @@ function deriveTargetGroupOptions(options) {
32
36
  function deriveDtsPassOptions(options) {
33
37
  const isProd = options.group !== "dev";
34
38
  const format = options.format ?? ["esm"];
39
+ const entry = Object.fromEntries(Object.entries(options.entry).filter(([name]) => !name.startsWith("bin/")));
35
40
  return {
36
41
  outDir: outDirFor(options.cwd, options.group),
37
42
  sourcemap: false,
@@ -40,7 +45,7 @@ function deriveDtsPassOptions(options) {
40
45
  clean: false,
41
46
  platform: "node",
42
47
  fixedExtension: false,
43
- entry: options.entry,
48
+ entry,
44
49
  dts: {
45
50
  tsconfig: options.tsconfigPath,
46
51
  emitDtsOnly: true
@@ -12,6 +12,7 @@ import { Effect } from "effect";
12
12
  *
13
13
  * Rejects with `CatalogResolutionError` on an unresolvable reference, or
14
14
  * `CatalogAssemblyError` if the workspace catalog set cannot be assembled.
15
+ * @public
15
16
  */
16
17
  function resolveManifest(pkg) {
17
18
  const program = Effect.gen(function* () {
@@ -9,6 +9,7 @@ import { getPackages } from "@manypkg/get-packages";
9
9
  * each package's CURRENT version, then overlays `newVersion` for changeset-affected packages
10
10
  * via `@changesets/get-release-plan`. Never rejects: any failure (not a workspace, missing
11
11
  * `.changeset/config.json`, parse error) degrades to current versions (or an empty map).
12
+ * @public
12
13
  */
13
14
  async function resolveNextVersions(cwd) {
14
15
  try {
@@ -1,7 +1,11 @@
1
1
  import { Context } from "effect";
2
2
 
3
3
  //#region src/config-validation/ConfigValidator.ts
4
- /** Fast-fail config validator; runs first in the bundler over the resolved config. */
4
+ /**
5
+ * Fast-fail config validator; runs first in the bundler over the resolved config.
6
+ *
7
+ * @public
8
+ */
5
9
  var ConfigValidator = class extends Context.Tag("@savvy-web/tsdown-plugins/ConfigValidator")() {};
6
10
 
7
11
  //#endregion
@@ -7,7 +7,7 @@ import { Effect, Layer } from "effect";
7
7
  import { existsSync, statSync } from "node:fs";
8
8
 
9
9
  //#region src/config-validation/ConfigValidatorLive.ts
10
- const VALID_SYNTAX_KINDS = new Set([
10
+ const VALID_SYNTAX_KINDS = /* @__PURE__ */ new Set([
11
11
  "block",
12
12
  "inline",
13
13
  "modifier"
@@ -50,7 +50,11 @@ function check(input) {
50
50
  }
51
51
  if (input.looseFiles !== void 0) normalizeLooseFiles(input.looseFiles);
52
52
  }
53
- /** Live ConfigValidator: wraps the synchronous rule set, surfacing ConfigValidationError as a typed Effect failure. */
53
+ /**
54
+ * Live ConfigValidator: wraps the synchronous rule set, surfacing ConfigValidationError as a typed Effect failure.
55
+ *
56
+ * @public
57
+ */
54
58
  const ConfigValidatorLive = Layer.succeed(ConfigValidator, { validate: (input) => Effect.try({
55
59
  try: () => check(input),
56
60
  catch: (e) => e instanceof ConfigValidationError ? e : new ConfigValidationError({
@@ -3,7 +3,11 @@ import { writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
 
5
5
  //#region src/dts/resolved-tsconfig.ts
6
- /** Build the portable absolute-path tsconfig object (ported from rslib writeBundleTempConfig). */
6
+ /**
7
+ * Build the portable absolute-path tsconfig object (ported from rslib writeBundleTempConfig).
8
+ *
9
+ * @public
10
+ */
7
11
  function buildResolvedTsconfig(options) {
8
12
  const cwd = options.cwd;
9
13
  return {
@@ -32,7 +36,11 @@ function buildResolvedTsconfig(options) {
32
36
  exclude: [join(cwd, "node_modules"), join(cwd, "dist/**/*")]
33
37
  };
34
38
  }
35
- /** Write the resolved tsconfig to a temp file and return its absolute path. */
39
+ /**
40
+ * Write the resolved tsconfig to a temp file and return its absolute path.
41
+ *
42
+ * @public
43
+ */
36
44
  function writeResolvedTsconfig(options) {
37
45
  const cfg = buildResolvedTsconfig(options);
38
46
  const path = join(tmpdir(), `tsconfig-bundle-${process.pid}-${options.cwd.replace(/[^\w]/g, "_")}.json`);
package/entry/extract.js CHANGED
@@ -26,6 +26,7 @@ const createEntryName = (exportKey, exportsAsIndexes) => {
26
26
  const withoutPrefix = exportKey.replace(/^\.\//, "");
27
27
  return exportsAsIndexes ? `${withoutPrefix}/index` : withoutPrefix.replace(/\//g, "-");
28
28
  };
29
+ /** @public */
29
30
  function extractEntries(pkg, options = {}) {
30
31
  const entries = {};
31
32
  const exportPaths = {};
@@ -3,7 +3,11 @@ import { resolve } from "node:path";
3
3
  import { readFileSync } from "node:fs";
4
4
 
5
5
  //#region src/entry/package-json-entries.ts
6
- /** Derive a tsdown `entry` record (name to source path) from a package.json. */
6
+ /**
7
+ * Derive a tsdown `entry` record (name to source path) from a package.json.
8
+ *
9
+ * @public
10
+ */
7
11
  function packageJsonEntries(options = {}) {
8
12
  return extractEntries(options.pkg ?? JSON.parse(readFileSync(resolve(options.cwd ?? process.cwd(), "package.json"), "utf-8")), {
9
13
  exportsAsIndexes: options.exportsAsIndexes,
package/errors.js CHANGED
@@ -19,13 +19,21 @@ var BuildFailed = class extends Data.TaggedError("BuildFailed") {
19
19
  return `Build failed for TargetGroup "${this.targetGroup}": ${this.reason}`;
20
20
  }
21
21
  };
22
- /** API Extractor meta generation failed for an entry. */
22
+ /**
23
+ * API Extractor meta generation failed for an entry.
24
+ *
25
+ * @public
26
+ */
23
27
  var MetaGenerationError = class extends Data.TaggedError("MetaGenerationError") {
24
28
  get message() {
25
29
  return `Meta generation failed for entry "${this.entry}": ${this.reason}`;
26
30
  }
27
31
  };
28
- /** A savvy.build.ts or publishConfig.targets config is structurally invalid; raised before any build work. */
32
+ /**
33
+ * A savvy.build.ts or publishConfig.targets config is structurally invalid; raised before any build work.
34
+ *
35
+ * @public
36
+ */
29
37
  var ConfigValidationError = class extends Data.TaggedError("ConfigValidationError") {
30
38
  get message() {
31
39
  return `Config validation failed at "${this.path}": ${this.reason}`;
package/exe/build.js CHANGED
@@ -1,13 +1,28 @@
1
+ import { buildMetricsPlugin } from "../report/metrics-plugin.js";
2
+ import { createTimer } from "../report/timer.js";
3
+ import { createTsdownLogger } from "../report/tsdown-logger.js";
1
4
  import { join } from "node:path";
2
5
  import { mkdtempSync, rmSync } from "node:fs";
3
6
  import { tmpdir } from "node:os";
4
7
 
5
8
  //#region src/exe/build.ts
6
- /** Compile each SEA binary via tsdown's exe mode. One tsdown build per spec. */
9
+ /**
10
+ * Compile each SEA binary via tsdown's exe mode. One tsdown build per spec.
11
+ *
12
+ * @public
13
+ */
7
14
  async function runExeBuild(options) {
8
15
  const build = options.build ?? (await import("tsdown")).build;
9
16
  for (const spec of options.specs) {
10
17
  const scratch = mkdtempSync(join(tmpdir(), "savvy-exe-"));
18
+ const collector = options.collector;
19
+ const groupId = options.groupId;
20
+ const instrument = collector !== void 0 && groupId !== void 0 ? {
21
+ logLevel: "silent",
22
+ customLogger: createTsdownLogger(collector, groupId)
23
+ } : {};
24
+ const plugins = collector !== void 0 && groupId !== void 0 ? [buildMetricsPlugin(collector, groupId, "exe", options.verbose ?? false)] : [];
25
+ const timer = createTimer();
11
26
  try {
12
27
  await build({
13
28
  cwd: options.cwd,
@@ -23,9 +38,12 @@ async function runExeBuild(options) {
23
38
  outDir: options.outDir,
24
39
  seaConfig: spec.seaConfig,
25
40
  targets: spec.targets
26
- }
41
+ },
42
+ ...instrument,
43
+ ...plugins.length > 0 ? { plugins } : {}
27
44
  });
28
45
  } finally {
46
+ if (collector !== void 0 && groupId !== void 0) collector.recordPassTiming(groupId, "exe", timer.elapsed());
29
47
  rmSync(scratch, {
30
48
  recursive: true,
31
49
  force: true
package/exe/config.js CHANGED
@@ -1,5 +1,9 @@
1
1
  //#region src/exe/config.ts
2
- /** Default Node runtime embedded in the SEA (parity with the vitest-agent reference). */
2
+ /**
3
+ * Default Node runtime embedded in the SEA (parity with the vitest-agent reference).
4
+ *
5
+ * @public
6
+ */
3
7
  const DEFAULT_EXE_NODE_VERSION = "25.9.0";
4
8
  /** Map a package.json os value to the tsdown exe platform token. */
5
9
  function platformToken(os) {
@@ -24,6 +28,7 @@ function inferTargets(pkg) {
24
28
  *
25
29
  * Pure function; structural validation (missing fileName, empty targets) lives in the
26
30
  * config-validation layer.
31
+ * @public
27
32
  */
28
33
  function normalizeExeOptions(exe, pkg) {
29
34
  return (Array.isArray(exe) ? exe : [exe]).map((c) => {
package/exe/filename.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * The exact filename `@tsdown/exe` emits for a SEA target, mirroring tsdown's
4
4
  * `resolveOutputFileName`: base fileName + `-<platform>-<arch>` + `.exe` on win.
5
5
  * Single source of truth so the manifest value never drifts from the on-disk file.
6
+ * @public
6
7
  */
7
8
  function computeExeFileName(fileName, target) {
8
9
  return `${fileName}${`-${target.platform}-${target.arch}`}${target.platform === "win" ? ".exe" : ""}`;