@savvy-web/tsdown-plugins 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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";
@@ -24,7 +27,23 @@ import { dirname, join } from "node:path";
24
27
  async function buildTargetGroups(options) {
25
28
  const build = options.build ?? (await import("tsdown")).build;
26
29
  const publicDir = join(options.cwd, "public");
30
+ const collector = options.collector;
31
+ const verbose = options.verbose ?? false;
32
+ const instrument = (groupId) => collector === void 0 ? {} : {
33
+ logLevel: "silent",
34
+ customLogger: createTsdownLogger(collector, groupId)
35
+ };
36
+ const metricsPlugins = (groupId, pass) => collector === void 0 ? [] : [buildMetricsPlugin(collector, groupId, pass, verbose)];
37
+ const timed = async (groupId, pass, run) => {
38
+ const timer = createTimer();
39
+ try {
40
+ await run();
41
+ } finally {
42
+ if (collector !== void 0) collector.recordPassTiming(groupId, pass, timer.elapsed());
43
+ }
44
+ };
27
45
  for (const group of options.groups) {
46
+ if (collector !== void 0) collector.registerGroup(group.id, Object.keys(options.entry));
28
47
  const partitions = [{
29
48
  entry: options.entry,
30
49
  ...options.format !== void 0 ? { format: options.format } : {},
@@ -81,7 +100,7 @@ async function buildTargetGroups(options) {
81
100
  ...options.subdirExports !== void 0 ? { subdirExports: options.subdirExports } : {},
82
101
  ...options.exeRewrite !== void 0 ? { exeRewrite: options.exeRewrite } : {}
83
102
  }) : void 0;
84
- await build({
103
+ await timed(group.id, "js", () => build({
85
104
  config: false,
86
105
  cwd: options.cwd,
87
106
  entry: jsEntry,
@@ -95,6 +114,7 @@ async function buildTargetGroups(options) {
95
114
  fixedExtension: js.fixedExtension,
96
115
  dts: js.dts,
97
116
  define: js.define,
117
+ ...instrument(group.id),
98
118
  ...part.css !== void 0 ? { css: part.css } : {},
99
119
  ...partExternals?.length || partBundleNodeModules || partBundle?.length ? { deps: {
100
120
  ...partExternals?.length ? { neverBundle: partExternals } : {},
@@ -106,12 +126,14 @@ async function buildTargetGroups(options) {
106
126
  plugins: [
107
127
  ...manifestPlugin ? [manifestPlugin] : [],
108
128
  ...js.format.includes("cjs") ? [nodeBuiltinDefaultInterop(), cjsDefaultInterop()] : [],
109
- ...options.extraPlugins ?? []
129
+ ...options.extraPlugins ?? [],
130
+ ...metricsPlugins(group.id, "js")
110
131
  ]
111
- });
132
+ }));
112
133
  if (isBase) syncPublicDir(publicDir, join(js.outDir, "public"));
113
134
  const dtsNeverBundle = [...partExternals ?? [], ...partDtsExternals ?? []];
114
- await build({
135
+ if (Object.keys(dts.entry).length === 0) continue;
136
+ await timed(group.id, "dts", () => build({
115
137
  config: false,
116
138
  cwd: options.cwd,
117
139
  entry: dts.entry,
@@ -124,6 +146,7 @@ async function buildTargetGroups(options) {
124
146
  fixedExtension: dts.fixedExtension,
125
147
  dts: dts.dts,
126
148
  define: dts.define,
149
+ ...instrument(group.id),
127
150
  ...dtsNeverBundle.length > 0 || partBundleNodeModules || dts.bundledPackages ? { deps: {
128
151
  ...dtsNeverBundle.length > 0 ? { neverBundle: dtsNeverBundle } : {},
129
152
  ...partBundleNodeModules ? {
@@ -135,8 +158,12 @@ async function buildTargetGroups(options) {
135
158
  } : {}
136
159
  } } : {},
137
160
  ...dts.jsx !== void 0 ? { inputOptions: { jsx: dts.jsx } } : {},
138
- plugins: [...dts.format.includes("cjs") ? [nodeBuiltinDefaultInterop(), cjsDefaultInterop()] : [], ...options.extraPlugins ?? []]
139
- });
161
+ plugins: [
162
+ ...dts.format.includes("cjs") ? [nodeBuiltinDefaultInterop(), cjsDefaultInterop()] : [],
163
+ ...options.extraPlugins ?? [],
164
+ ...metricsPlugins(group.id, "dts")
165
+ ]
166
+ }));
140
167
  }
141
168
  const looseOutDir = outDirFor(options.cwd, group.id);
142
169
  const isProdGroup = group.id !== "dev";
@@ -147,7 +174,7 @@ async function buildTargetGroups(options) {
147
174
  };
148
175
  for (const lf of options.looseFiles ?? []) {
149
176
  const hasCjs = lf.format === "cjs";
150
- await build({
177
+ await timed(group.id, "loose", () => build({
151
178
  config: false,
152
179
  cwd: options.cwd,
153
180
  entry: { [lf.entryName]: lf.source },
@@ -164,10 +191,15 @@ async function buildTargetGroups(options) {
164
191
  "process.env.__PACKAGE_VERSION__": JSON.stringify(options.version),
165
192
  ...options.define
166
193
  },
194
+ ...instrument(group.id),
167
195
  ...Object.keys(looseDeps).length > 0 ? { deps: looseDeps } : {},
168
196
  ...hasCjs ? { cjsDefault: true } : {},
169
- plugins: [...hasCjs ? [nodeBuiltinDefaultInterop(), cjsDefaultInterop()] : [], ...options.extraPlugins ?? []]
170
- });
197
+ plugins: [
198
+ ...hasCjs ? [nodeBuiltinDefaultInterop(), cjsDefaultInterop()] : [],
199
+ ...options.extraPlugins ?? [],
200
+ ...metricsPlugins(group.id, "loose")
201
+ ]
202
+ }));
171
203
  }
172
204
  }
173
205
  }
@@ -32,6 +32,7 @@ function deriveTargetGroupOptions(options) {
32
32
  function deriveDtsPassOptions(options) {
33
33
  const isProd = options.group !== "dev";
34
34
  const format = options.format ?? ["esm"];
35
+ const entry = Object.fromEntries(Object.entries(options.entry).filter(([name]) => !name.startsWith("bin/")));
35
36
  return {
36
37
  outDir: outDirFor(options.cwd, options.group),
37
38
  sourcemap: false,
@@ -40,7 +41,7 @@ function deriveDtsPassOptions(options) {
40
41
  clean: false,
41
42
  platform: "node",
42
43
  fixedExtension: false,
43
- entry: options.entry,
44
+ entry,
44
45
  dts: {
45
46
  tsconfig: options.tsconfigPath,
46
47
  emitDtsOnly: true
@@ -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"
package/exe/build.js CHANGED
@@ -1,3 +1,6 @@
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";
@@ -8,6 +11,14 @@ async function runExeBuild(options) {
8
11
  const build = options.build ?? (await import("tsdown")).build;
9
12
  for (const spec of options.specs) {
10
13
  const scratch = mkdtempSync(join(tmpdir(), "savvy-exe-"));
14
+ const collector = options.collector;
15
+ const groupId = options.groupId;
16
+ const instrument = collector !== void 0 && groupId !== void 0 ? {
17
+ logLevel: "silent",
18
+ customLogger: createTsdownLogger(collector, groupId)
19
+ } : {};
20
+ const plugins = collector !== void 0 && groupId !== void 0 ? [buildMetricsPlugin(collector, groupId, "exe", options.verbose ?? false)] : [];
21
+ const timer = createTimer();
11
22
  try {
12
23
  await build({
13
24
  cwd: options.cwd,
@@ -23,9 +34,12 @@ async function runExeBuild(options) {
23
34
  outDir: options.outDir,
24
35
  seaConfig: spec.seaConfig,
25
36
  targets: spec.targets
26
- }
37
+ },
38
+ ...instrument,
39
+ ...plugins.length > 0 ? { plugins } : {}
27
40
  });
28
41
  } finally {
42
+ if (collector !== void 0 && groupId !== void 0) collector.recordPassTiming(groupId, "exe", timer.elapsed());
29
43
  rmSync(scratch, {
30
44
  recursive: true,
31
45
  force: true
package/index.d.ts CHANGED
@@ -152,6 +152,148 @@ interface EmitManifestOptions {
152
152
  /** Rolldown plugin: emit the transformed package.json + LICENSE/README into the output pkg/ root. */
153
153
  declare function emitManifest(options: EmitManifestOptions): Plugin;
154
154
  //#endregion
155
+ //#region src/report/schema.d.ts
156
+ declare const ReportTimings_base: Schema.Class<ReportTimings, {
157
+ totalMs: typeof Schema.Number;
158
+ }, Schema.Struct.Encoded<{
159
+ totalMs: typeof Schema.Number;
160
+ }>, never, {
161
+ readonly totalMs: number;
162
+ }, {}, {}>;
163
+ declare class ReportTimings extends ReportTimings_base {}
164
+ declare const DiagnosticEntry_base: Schema.Class<DiagnosticEntry, {
165
+ source: Schema.Literal<["tsdown", "rolldown", "api-extractor"]>;
166
+ level: Schema.Literal<["warn", "error"]>;
167
+ text: typeof Schema.String;
168
+ file: Schema.optional<typeof Schema.String>;
169
+ line: Schema.optional<typeof Schema.Number>;
170
+ column: Schema.optional<typeof Schema.Number>;
171
+ }, Schema.Struct.Encoded<{
172
+ source: Schema.Literal<["tsdown", "rolldown", "api-extractor"]>;
173
+ level: Schema.Literal<["warn", "error"]>;
174
+ text: typeof Schema.String;
175
+ file: Schema.optional<typeof Schema.String>;
176
+ line: Schema.optional<typeof Schema.Number>;
177
+ column: Schema.optional<typeof Schema.Number>;
178
+ }>, never, {
179
+ readonly text: string;
180
+ } & {
181
+ readonly source: "tsdown" | "rolldown" | "api-extractor";
182
+ } & {
183
+ readonly level: "warn" | "error";
184
+ } & {
185
+ readonly file?: string | undefined;
186
+ } & {
187
+ readonly line?: number | undefined;
188
+ } & {
189
+ readonly column?: number | undefined;
190
+ }, {}, {}>;
191
+ /** A captured warning or error, from tsdown's logger, rolldown's onLog, or API Extractor. */
192
+ declare class DiagnosticEntry extends DiagnosticEntry_base {}
193
+ declare const EmittedFile_base: Schema.Class<EmittedFile, {
194
+ path: typeof Schema.String;
195
+ bytes: typeof Schema.Number;
196
+ gzip: Schema.optional<typeof Schema.Number>;
197
+ }, Schema.Struct.Encoded<{
198
+ path: typeof Schema.String;
199
+ bytes: typeof Schema.Number;
200
+ gzip: Schema.optional<typeof Schema.Number>;
201
+ }>, never, {
202
+ readonly path: string;
203
+ } & {
204
+ readonly bytes: number;
205
+ } & {
206
+ readonly gzip?: number | undefined;
207
+ }, {}, {}>;
208
+ /** One emitted output file with its in-memory byte size (gzip only when --verbose). */
209
+ declare class EmittedFile extends EmittedFile_base {}
210
+ declare const PassReport_base: Schema.Class<PassReport, {
211
+ id: Schema.Literal<["js", "dts", "loose", "exe", "meta"]>;
212
+ files: Schema.Array$<typeof EmittedFile>;
213
+ ms: typeof Schema.Number;
214
+ }, Schema.Struct.Encoded<{
215
+ id: Schema.Literal<["js", "dts", "loose", "exe", "meta"]>;
216
+ files: Schema.Array$<typeof EmittedFile>;
217
+ ms: typeof Schema.Number;
218
+ }>, never, {
219
+ readonly ms: number;
220
+ } & {
221
+ readonly id: "js" | "dts" | "loose" | "exe" | "meta";
222
+ } & {
223
+ readonly files: readonly EmittedFile[];
224
+ }, {}, {}>;
225
+ /** One build pass within a target group (js / dts / loose / exe / meta). */
226
+ declare class PassReport extends PassReport_base {}
227
+ declare const TargetGroupReport_base: Schema.Class<TargetGroupReport, {
228
+ id: typeof Schema.String;
229
+ entries: Schema.Array$<typeof Schema.String>;
230
+ passes: Schema.Array$<typeof PassReport>;
231
+ warnings: Schema.Array$<typeof DiagnosticEntry>;
232
+ errors: Schema.Array$<typeof DiagnosticEntry>;
233
+ timings: typeof ReportTimings;
234
+ }, Schema.Struct.Encoded<{
235
+ id: typeof Schema.String;
236
+ entries: Schema.Array$<typeof Schema.String>;
237
+ passes: Schema.Array$<typeof PassReport>;
238
+ warnings: Schema.Array$<typeof DiagnosticEntry>;
239
+ errors: Schema.Array$<typeof DiagnosticEntry>;
240
+ timings: typeof ReportTimings;
241
+ }>, never, {
242
+ readonly entries: readonly string[];
243
+ } & {
244
+ readonly id: string;
245
+ } & {
246
+ readonly timings: ReportTimings;
247
+ } & {
248
+ readonly passes: readonly PassReport[];
249
+ } & {
250
+ readonly warnings: readonly DiagnosticEntry[];
251
+ } & {
252
+ readonly errors: readonly DiagnosticEntry[];
253
+ }, {}, {}>;
254
+ declare class TargetGroupReport extends TargetGroupReport_base {}
255
+ declare const BuildReport_base: Schema.Class<BuildReport, {
256
+ package: typeof Schema.String;
257
+ targetGroups: Schema.Array$<typeof TargetGroupReport>;
258
+ }, Schema.Struct.Encoded<{
259
+ package: typeof Schema.String;
260
+ targetGroups: Schema.Array$<typeof TargetGroupReport>;
261
+ }>, never, {
262
+ readonly package: string;
263
+ } & {
264
+ readonly targetGroups: readonly TargetGroupReport[];
265
+ }, {}, {}>;
266
+ declare class BuildReport extends BuildReport_base {}
267
+ //#endregion
268
+ //#region src/report/collector.d.ts
269
+ type PassKind = PassReport["id"];
270
+ interface DiagnosticInput {
271
+ readonly source: DiagnosticEntry["source"];
272
+ readonly level: DiagnosticEntry["level"];
273
+ readonly text: string;
274
+ readonly file?: string;
275
+ readonly line?: number;
276
+ readonly column?: number;
277
+ }
278
+ /**
279
+ * Stateful build-event accumulator. The write surface is synchronous so it can be called directly
280
+ * from tsdown's customLogger and API Extractor's messageCallback (both invoked synchronously).
281
+ * `snapshot` builds the immutable BuildReport the Effect render pipeline consumes.
282
+ */
283
+ declare class BuildCollector {
284
+ private readonly groups;
285
+ private group;
286
+ private pass;
287
+ registerGroup(groupId: string, entries: ReadonlyArray<string>): void;
288
+ recordEmitted(groupId: string, pass: PassKind, file: EmittedFile): void;
289
+ recordPassTiming(groupId: string, pass: PassKind, ms: number): void;
290
+ recordWarning(groupId: string, entry: DiagnosticInput): void;
291
+ recordError(groupId: string, entry: DiagnosticInput): void;
292
+ snapshot(packageName: string): ReadonlyArray<BuildReport>;
293
+ }
294
+ declare const BuildCollectorTag_base: Context.TagClass<BuildCollectorTag, "@savvy-web/tsdown-plugins/BuildCollector", BuildCollector>;
295
+ declare class BuildCollectorTag extends BuildCollectorTag_base {}
296
+ //#endregion
155
297
  //#region src/build/target-groups.d.ts
156
298
  /** A build group id: "dev" or any prod byte-variant id (e.g. "npm", "github", a custom key). */
157
299
  type TargetGroupId = string;
@@ -437,6 +579,10 @@ interface BuildTargetGroupsOptions {
437
579
  readonly exeRewrite?: ExeRewrite | undefined;
438
580
  /** Injectable for tests; defaults to tsdown's build. */
439
581
  readonly build?: TsdownBuild;
582
+ /** When set, muzzle tsdown (silent + customLogger) and capture metrics/timing into this collector. */
583
+ readonly collector?: BuildCollector | undefined;
584
+ /** Compute gzip sizes for emitted files (verbose render). Forwarded to the metrics plugin. */
585
+ readonly verbose?: boolean | undefined;
440
586
  }
441
587
  /**
442
588
  * Run tsdown.build() per TargetGroup. Composable so the escape hatch gets multi-group too.
@@ -845,6 +991,12 @@ interface RunExeBuildOptions {
845
991
  readonly specs: ReadonlyArray<NormalizedExe>;
846
992
  /** Injectable tsdown build (defaults to tsdown's build function). */
847
993
  readonly build?: ExeBuild | undefined;
994
+ /** When set with groupId, muzzle tsdown and record an "exe" pass into this collector. */
995
+ readonly collector?: BuildCollector | undefined;
996
+ /** Target-group id the exe pass belongs to (required to record into the collector). */
997
+ readonly groupId?: string | undefined;
998
+ /** Compute gzip sizes (verbose render). */
999
+ readonly verbose?: boolean | undefined;
848
1000
  }
849
1001
  /** Compile each SEA binary via tsdown's exe mode. One tsdown build per spec. */
850
1002
  declare function runExeBuild(options: RunExeBuildOptions): Promise<void>;
@@ -880,6 +1032,8 @@ interface GenerateMetaOptions {
880
1032
  * rewrite. When omitted, the package.json is copied verbatim.
881
1033
  */
882
1034
  readonly manifestTransform?: ((pkg: Record<string, unknown>) => Record<string, unknown>) | undefined;
1035
+ /** When set, API Extractor warnings/errors are routed here (and suppressed from console). */
1036
+ readonly onMessage?: ((entry: DiagnosticInput) => void) | undefined;
883
1037
  }
884
1038
  interface MetaResult {
885
1039
  readonly apiJsonPath: string;
@@ -1012,36 +1166,6 @@ declare class TsconfigResolver {
1012
1166
  */
1013
1167
  declare function resolvePortableTsconfig(cwd: string, fallbackConfigPath?: string): PortableTsconfig;
1014
1168
  //#endregion
1015
- //#region src/report/schema.d.ts
1016
- declare const ReportTimings: Schema.Struct<{
1017
- totalMs: typeof Schema.Number;
1018
- }>;
1019
- declare const TargetGroupReport: Schema.Struct<{
1020
- id: typeof Schema.String;
1021
- entries: Schema.Array$<typeof Schema.String>;
1022
- emittedFiles: Schema.Array$<typeof Schema.String>;
1023
- timings: Schema.Struct<{
1024
- totalMs: typeof Schema.Number;
1025
- }>;
1026
- warnings: Schema.Array$<typeof Schema.String>;
1027
- errors: Schema.Array$<typeof Schema.String>;
1028
- }>;
1029
- declare const BuildReport: Schema.Struct<{
1030
- package: typeof Schema.String;
1031
- targetGroups: Schema.Array$<Schema.Struct<{
1032
- id: typeof Schema.String;
1033
- entries: Schema.Array$<typeof Schema.String>;
1034
- emittedFiles: Schema.Array$<typeof Schema.String>;
1035
- timings: Schema.Struct<{
1036
- totalMs: typeof Schema.Number;
1037
- }>;
1038
- warnings: Schema.Array$<typeof Schema.String>;
1039
- errors: Schema.Array$<typeof Schema.String>;
1040
- }>>;
1041
- }>;
1042
- type BuildReport = typeof BuildReport.Type;
1043
- type TargetGroupReport = typeof TargetGroupReport.Type;
1044
- //#endregion
1045
1169
  //#region src/report/formatters/types.d.ts
1046
1170
  interface RenderedOutput {
1047
1171
  readonly target: "stdout" | "file" | "github-summary";
@@ -1050,6 +1174,7 @@ interface RenderedOutput {
1050
1174
  }
1051
1175
  interface FormatterContext {
1052
1176
  readonly noColor: boolean;
1177
+ readonly verbose: boolean;
1053
1178
  }
1054
1179
  interface Formatter {
1055
1180
  readonly format: string;
@@ -1110,6 +1235,16 @@ declare class OutputRenderer extends OutputRenderer_base {}
1110
1235
  //#region src/report/layers/OutputRendererLive.d.ts
1111
1236
  declare const OutputRendererLive: Layer.Layer<OutputRenderer, never, never>;
1112
1237
  //#endregion
1238
+ //#region src/report/metrics-plugin.d.ts
1239
+ /**
1240
+ * Rolldown plugin that records emitted-file metrics into the BuildCollector via writeBundle (which
1241
+ * fires for the JS pass AND the emitDtsOnly dts pass — verified against tsdown 0.22.3), plus a
1242
+ * defensive onLog for rolldown-level diagnostics that bypass tsdown's logger. Append it to each
1243
+ * build pass's `plugins` array. `bytes` is taken from the in-memory chunk/asset content (no fs);
1244
+ * `gzip` is computed only when `verbose`.
1245
+ */
1246
+ declare function buildMetricsPlugin(collector: BuildCollector, groupId: string, pass: PassKind, verbose: boolean): Plugin;
1247
+ //#endregion
1113
1248
  //#region src/report/pipeline.d.ts
1114
1249
  declare const ReportPipelineLive: Layer.Layer<EnvironmentDetector | ExecutorResolver | FormatSelector | OutputRenderer, never, never>;
1115
1250
  interface RenderReportOptions {
@@ -1117,6 +1252,7 @@ interface RenderReportOptions {
1117
1252
  /** Override env detection (mainly for tests). */
1118
1253
  readonly env?: Environment;
1119
1254
  readonly noColor: boolean;
1255
+ readonly verbose?: boolean;
1120
1256
  }
1121
1257
  declare const renderReport: (reports: ReadonlyArray<BuildReport>, options: RenderReportOptions) => Effect.Effect<ReadonlyArray<RenderedOutput>, never, EnvironmentDetector | ExecutorResolver | FormatSelector | OutputRenderer>;
1122
1258
  //#endregion
@@ -1611,6 +1747,25 @@ interface Timer {
1611
1747
  /** Create a wall-clock timer. (Date.now is fine in runtime build code.) */
1612
1748
  declare function createTimer(now?: () => number): Timer;
1613
1749
  //#endregion
1750
+ //#region src/report/tsdown-logger.d.ts
1751
+ /** Structural match for tsdown's Logger interface (tsdown 0.22.x). */
1752
+ interface TsdownLogger {
1753
+ level: "info";
1754
+ info: (...args: unknown[]) => void;
1755
+ warn: (...args: unknown[]) => void;
1756
+ warnOnce: (...args: unknown[]) => void;
1757
+ error: (...args: unknown[]) => void;
1758
+ success: (...args: unknown[]) => void;
1759
+ clearScreen: () => void;
1760
+ }
1761
+ /**
1762
+ * A tsdown `customLogger` that routes warnings/errors into the BuildCollector instead of the
1763
+ * console. Paired with `logLevel: "silent"` in the same build config: silent suppresses tsdown's
1764
+ * own console output while this logger still receives every message (verified against tsdown 0.22.3).
1765
+ * info/success are dropped — file metrics come from the writeBundle plugin and timing from our timer.
1766
+ */
1767
+ declare function createTsdownLogger(collector: BuildCollector, groupId: string): TsdownLogger;
1768
+ //#endregion
1614
1769
  //#region src/targets/binding.d.ts
1615
1770
  /** Write the target-to-group binding to dist/prod/targets.json for the release action to consume. Returns the path. */
1616
1771
  declare function writeTargetsBinding(cwd: string, resolution: TargetResolution): string;
@@ -1622,5 +1777,5 @@ declare function resolveTargets(options: {
1622
1777
  baseName: string;
1623
1778
  }): TargetResolution;
1624
1779
  //#endregion
1625
- 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 NextVersions, 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, resolveNextVersions, resolvePortableTsconfig, resolveTargets, rewriteMetaVersions, runExeBuild, syncPublicDir, transformBin, transformExports, transformManifest, writeResolvedTsconfig, writeTargetsBinding };
1780
+ export { BuildCollector, BuildCollectorTag, 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 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 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 TsdownLogger, type ValidationInput, type WarningSuppressionRule, buildEmittedManifest, buildMetricsPlugin, buildResolvedTsconfig, buildTargetGroups, cjsDefaultInterop, computeExeFileName, createEntryName, createTimer, createTsdownLogger, defaultManifestTransform, deriveTargetGroupOptions, emitManifest, extractEntries, formatTime, generateBuildReportSchema, generateMeta, isTargetObject, nodeBuiltinDefaultInterop, normalizeBinPaths, normalizeExeOptions, normalizeLooseFiles, normalizeMetaOptions, packageJsonEntries, readTsconfigJsx, removeDeclarationMaps, renderReport, resolveJsxConfig, resolveManifest, resolveNextVersions, resolvePortableTsconfig, resolveTargets, rewriteMetaVersions, runExeBuild, syncPublicDir, transformBin, transformExports, transformManifest, writeResolvedTsconfig, writeTargetsBinding };
1626
1781
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -2,6 +2,9 @@ import { resolveManifest } from "./catalog/resolve-catalogs.js";
2
2
  import { createEntryName, extractEntries } from "./entry/extract.js";
3
3
  import { defaultManifestTransform, normalizeBinPaths, transformBin, transformExports, transformManifest } from "./manifest/transform.js";
4
4
  import { buildEmittedManifest, emitManifest } from "./manifest/emit-manifest.js";
5
+ import { buildMetricsPlugin } from "./report/metrics-plugin.js";
6
+ import { createTimer, formatTime } from "./report/timer.js";
7
+ import { createTsdownLogger } from "./report/tsdown-logger.js";
5
8
  import { cjsDefaultInterop } from "./build/cjs-default-interop.js";
6
9
  import { nodeBuiltinDefaultInterop } from "./build/node-builtin-default-interop.js";
7
10
  import { syncPublicDir } from "./build/sync-public.js";
@@ -25,11 +28,12 @@ import { normalizeMetaOptions } from "./meta/config.js";
25
28
  import { TsconfigResolver, resolvePortableTsconfig } from "./meta/tsconfig-resolver.js";
26
29
  import { generateMeta } from "./meta/generate.js";
27
30
  import { rewriteMetaVersions } from "./meta/optimistic.js";
31
+ import { BuildReport, ReportTimings, TargetGroupReport } from "./report/schema.js";
32
+ import { BuildCollector, BuildCollectorTag } from "./report/collector.js";
28
33
  import { CiAnnotationsFormatter } from "./report/formatters/ci-annotations.js";
29
34
  import { JsonFormatter } from "./report/formatters/json.js";
30
35
  import { MarkdownFormatter } from "./report/formatters/markdown.js";
31
36
  import { SilentFormatter } from "./report/formatters/silent.js";
32
- import { createTimer, formatTime } from "./report/timer.js";
33
37
  import { TerminalFormatter } from "./report/formatters/terminal.js";
34
38
  import { EnvironmentDetector } from "./report/services/EnvironmentDetector.js";
35
39
  import { EnvironmentDetectorLive } from "./report/layers/EnvironmentDetectorLive.js";
@@ -40,9 +44,8 @@ import { FormatSelectorLive } from "./report/layers/FormatSelectorLive.js";
40
44
  import { OutputRenderer } from "./report/services/OutputRenderer.js";
41
45
  import { OutputRendererLive } from "./report/layers/OutputRendererLive.js";
42
46
  import { ReportPipelineLive, renderReport } from "./report/pipeline.js";
43
- import { BuildReport, ReportTimings, TargetGroupReport } from "./report/schema.js";
44
47
  import { generateBuildReportSchema } from "./report/schema-export.js";
45
48
  import { writeTargetsBinding } from "./targets/binding.js";
46
49
  import { CatalogAssemblyError, CatalogResolutionError } from "workspaces-effect";
47
50
 
48
- 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, resolveNextVersions, resolvePortableTsconfig, resolveTargets, rewriteMetaVersions, runExeBuild, syncPublicDir, transformBin, transformExports, transformManifest, writeResolvedTsconfig, writeTargetsBinding };
51
+ 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, buildEmittedManifest, buildMetricsPlugin, buildResolvedTsconfig, buildTargetGroups, cjsDefaultInterop, computeExeFileName, createEntryName, createTimer, createTsdownLogger, defaultManifestTransform, deriveTargetGroupOptions, emitManifest, extractEntries, formatTime, generateBuildReportSchema, generateMeta, isTargetObject, nodeBuiltinDefaultInterop, normalizeBinPaths, normalizeExeOptions, normalizeLooseFiles, normalizeMetaOptions, packageJsonEntries, readTsconfigJsx, removeDeclarationMaps, renderReport, resolveJsxConfig, resolveManifest, resolveNextVersions, resolvePortableTsconfig, resolveTargets, rewriteMetaVersions, runExeBuild, syncPublicDir, transformBin, transformExports, transformManifest, writeResolvedTsconfig, writeTargetsBinding };
@@ -7,6 +7,20 @@ import { TSDocConfigFile } from "@microsoft/tsdoc-config";
7
7
 
8
8
  //#region src/meta/api-extractor.ts
9
9
  const require_ = createRequire(import.meta.url);
10
+ /** Map an API Extractor message to a collector DiagnosticInput, or undefined if not warn/error. */
11
+ function mapExtractorMessage(message) {
12
+ const isError = message.logLevel === ExtractorLogLevel.Error;
13
+ const isWarning = message.logLevel === ExtractorLogLevel.Warning;
14
+ if (!isError && !isWarning) return void 0;
15
+ return {
16
+ source: "api-extractor",
17
+ level: isError ? "error" : "warn",
18
+ text: message.text,
19
+ ...message.sourceFilePath !== void 0 ? { file: message.sourceFilePath } : {},
20
+ ...message.sourceFileLine !== void 0 ? { line: message.sourceFileLine } : {},
21
+ ...message.sourceFileColumn !== void 0 ? { column: message.sourceFileColumn } : {}
22
+ };
23
+ }
10
24
  /** Run API Extractor over a single entry's .d.ts, writing the .api.json (and optionally tsdoc-metadata.json). Throws on failure. */
11
25
  function runApiExtractor(options) {
12
26
  const suppressor = createMessageSuppressor(options.suppressWarnings);
@@ -47,6 +61,13 @@ function runApiExtractor(options) {
47
61
  message.logLevel = ExtractorLogLevel.None;
48
62
  message.handled = true;
49
63
  }
64
+ if (options.onMessage !== void 0) {
65
+ const entry = mapExtractorMessage(message);
66
+ if (entry !== void 0) {
67
+ options.onMessage(entry);
68
+ message.handled = true;
69
+ }
70
+ }
50
71
  }
51
72
  });
52
73
  if (!result.succeeded) throw new MetaGenerationError({
package/meta/generate.js CHANGED
@@ -18,7 +18,7 @@ function unscopedName(name) {
18
18
  * published-package artifact and is written into `dtsDir` (the built pkg/), not the meta bundle.
19
19
  */
20
20
  async function generateMeta(options) {
21
- const { cwd, packageName, tsconfigPath, dtsDir, entries, exportPaths, outMetaDir, localPaths, tsdoc, manifestTransform } = options;
21
+ const { cwd, packageName, tsconfigPath, dtsDir, entries, exportPaths, outMetaDir, localPaths, tsdoc, manifestTransform, onMessage } = options;
22
22
  const tsdocConfigPath = writeTsdocConfig(cwd, tsdoc);
23
23
  const packageJsonPath = join(cwd, "package.json");
24
24
  mkdirSync(outMetaDir, { recursive: true });
@@ -41,7 +41,8 @@ async function generateMeta(options) {
41
41
  tsdocConfigPath,
42
42
  apiJsonPath: perEntryApiJson,
43
43
  ...isMain && !mainEntryDidTsdocMetadata ? { tsdocMetadataPath } : {},
44
- suppressWarnings: tsdoc.suppressWarnings
44
+ suppressWarnings: tsdoc.suppressWarnings,
45
+ ...onMessage !== void 0 ? { onMessage } : {}
45
46
  });
46
47
  if (isMain) mainEntryDidTsdocMetadata = true;
47
48
  perEntryModels.set(entryName, JSON.parse(readFileSync(perEntryApiJson, "utf-8")));
@@ -85,7 +85,7 @@ const PRESERVED_STRING_OPTIONS = [
85
85
  */
86
86
  var TsconfigResolver = class TsconfigResolver {
87
87
  /** @internal */
88
- static SCRIPT_TARGET_MAP = new Map([
88
+ static SCRIPT_TARGET_MAP = /* @__PURE__ */ new Map([
89
89
  [ScriptTarget.ES5, "es5"],
90
90
  [ScriptTarget.ES2015, "es2015"],
91
91
  [ScriptTarget.ES2016, "es2016"],
@@ -102,7 +102,7 @@ var TsconfigResolver = class TsconfigResolver {
102
102
  [ScriptTarget.JSON, "json"]
103
103
  ]);
104
104
  /** @internal */
105
- static MODULE_KIND_MAP = new Map([
105
+ static MODULE_KIND_MAP = /* @__PURE__ */ new Map([
106
106
  [ModuleKind.CommonJS, "commonjs"],
107
107
  [ModuleKind.ES2015, "es2015"],
108
108
  [ModuleKind.ES2020, "es2020"],
@@ -115,14 +115,14 @@ var TsconfigResolver = class TsconfigResolver {
115
115
  [ModuleKind.Preserve, "preserve"]
116
116
  ]);
117
117
  /** @internal */
118
- static MODULE_RESOLUTION_MAP = new Map([
118
+ static MODULE_RESOLUTION_MAP = /* @__PURE__ */ new Map([
119
119
  [ModuleResolutionKind.Node10, "node10"],
120
120
  [ModuleResolutionKind.Node16, "node16"],
121
121
  [ModuleResolutionKind.NodeNext, "nodenext"],
122
122
  [ModuleResolutionKind.Bundler, "bundler"]
123
123
  ]);
124
124
  /** @internal */
125
- static JSX_EMIT_MAP = new Map([
125
+ static JSX_EMIT_MAP = /* @__PURE__ */ new Map([
126
126
  [JsxEmit.None, "none"],
127
127
  [JsxEmit.Preserve, "preserve"],
128
128
  [JsxEmit.React, "react"],
@@ -131,13 +131,13 @@ var TsconfigResolver = class TsconfigResolver {
131
131
  [JsxEmit.ReactJSXDev, "react-jsxdev"]
132
132
  ]);
133
133
  /** @internal */
134
- static MODULE_DETECTION_MAP = new Map([
134
+ static MODULE_DETECTION_MAP = /* @__PURE__ */ new Map([
135
135
  [ModuleDetectionKind.Legacy, "legacy"],
136
136
  [ModuleDetectionKind.Auto, "auto"],
137
137
  [ModuleDetectionKind.Force, "force"]
138
138
  ]);
139
139
  /** @internal */
140
- static NEW_LINE_MAP = new Map([[NewLineKind.CarriageReturnLineFeed, "crlf"], [NewLineKind.LineFeed, "lf"]]);
140
+ static NEW_LINE_MAP = /* @__PURE__ */ new Map([[NewLineKind.CarriageReturnLineFeed, "crlf"], [NewLineKind.LineFeed, "lf"]]);
141
141
  /** Converts a {@link ScriptTarget} enum value to its string form (e.g. `es2023`). */
142
142
  static convertScriptTarget(target) {
143
143
  if (target === void 0) return void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/tsdown-plugins",
3
- "version": "0.7.0",
3
+ "version": "0.8.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",
@@ -0,0 +1,126 @@
1
+ import { BuildReport, DiagnosticEntry, EmittedFile, PassReport, ReportTimings, TargetGroupReport } from "./schema.js";
2
+ import { Context } from "effect";
3
+
4
+ //#region src/report/collector.ts
5
+ /**
6
+ * Stateful build-event accumulator. The write surface is synchronous so it can be called directly
7
+ * from tsdown's customLogger and API Extractor's messageCallback (both invoked synchronously).
8
+ * `snapshot` builds the immutable BuildReport the Effect render pipeline consumes.
9
+ */
10
+ var BuildCollector = class {
11
+ groups = /* @__PURE__ */ new Map();
12
+ group(groupId) {
13
+ let g = this.groups.get(groupId);
14
+ if (g === void 0) {
15
+ g = {
16
+ id: groupId,
17
+ entries: [],
18
+ passes: /* @__PURE__ */ new Map(),
19
+ warnings: [],
20
+ errors: [],
21
+ seenPaths: /* @__PURE__ */ new Set(),
22
+ seenDiagnostics: /* @__PURE__ */ new Set()
23
+ };
24
+ this.groups.set(groupId, g);
25
+ }
26
+ return g;
27
+ }
28
+ pass(groupId, pass) {
29
+ const g = this.group(groupId);
30
+ let p = g.passes.get(pass);
31
+ if (p === void 0) {
32
+ p = {
33
+ files: [],
34
+ ms: 0
35
+ };
36
+ g.passes.set(pass, p);
37
+ }
38
+ return p;
39
+ }
40
+ registerGroup(groupId, entries) {
41
+ this.group(groupId).entries = [...entries];
42
+ }
43
+ recordEmitted(groupId, pass, file) {
44
+ const g = this.group(groupId);
45
+ if (g.seenPaths.has(file.path)) return;
46
+ g.seenPaths.add(file.path);
47
+ this.pass(groupId, pass).files.push({
48
+ path: file.path,
49
+ bytes: file.bytes,
50
+ ...file.gzip !== void 0 ? { gzip: file.gzip } : {}
51
+ });
52
+ }
53
+ recordPassTiming(groupId, pass, ms) {
54
+ this.pass(groupId, pass).ms += ms;
55
+ }
56
+ recordWarning(groupId, entry) {
57
+ const g = this.group(groupId);
58
+ const key = diagnosticKey(entry);
59
+ if (g.seenDiagnostics.has(key)) return;
60
+ g.seenDiagnostics.add(key);
61
+ g.warnings.push(toEntry(entry));
62
+ }
63
+ recordError(groupId, entry) {
64
+ const g = this.group(groupId);
65
+ const key = diagnosticKey(entry);
66
+ if (g.seenDiagnostics.has(key)) return;
67
+ g.seenDiagnostics.add(key);
68
+ g.errors.push(toEntry(entry));
69
+ }
70
+ snapshot(packageName) {
71
+ const targetGroups = [];
72
+ for (const g of this.groups.values()) {
73
+ const passes = [];
74
+ let totalMs = 0;
75
+ for (const [id, p] of g.passes) {
76
+ const files = p.files.map((f) => new EmittedFile({
77
+ path: f.path,
78
+ bytes: f.bytes,
79
+ ...f.gzip !== void 0 ? { gzip: f.gzip } : {}
80
+ }));
81
+ passes.push(new PassReport({
82
+ id,
83
+ files,
84
+ ms: p.ms
85
+ }));
86
+ totalMs += p.ms;
87
+ }
88
+ targetGroups.push(new TargetGroupReport({
89
+ id: g.id,
90
+ entries: [...g.entries],
91
+ passes,
92
+ warnings: [...g.warnings],
93
+ errors: [...g.errors],
94
+ timings: new ReportTimings({ totalMs })
95
+ }));
96
+ }
97
+ return [new BuildReport({
98
+ package: packageName,
99
+ targetGroups
100
+ })];
101
+ }
102
+ };
103
+ function diagnosticKey(input) {
104
+ return [
105
+ input.source,
106
+ input.level,
107
+ input.text,
108
+ input.file ?? "",
109
+ String(input.line ?? ""),
110
+ String(input.column ?? "")
111
+ ].join("\0");
112
+ }
113
+ function toEntry(input) {
114
+ return new DiagnosticEntry({
115
+ source: input.source,
116
+ level: input.level,
117
+ text: input.text,
118
+ ...input.file !== void 0 ? { file: input.file } : {},
119
+ ...input.line !== void 0 ? { line: input.line } : {},
120
+ ...input.column !== void 0 ? { column: input.column } : {}
121
+ });
122
+ }
123
+ var BuildCollectorTag = class extends Context.Tag("@savvy-web/tsdown-plugins/BuildCollector")() {};
124
+
125
+ //#endregion
126
+ export { BuildCollector, BuildCollectorTag };
@@ -5,8 +5,14 @@ const CiAnnotationsFormatter = {
5
5
  render: (reports) => {
6
6
  const lines = [];
7
7
  for (const r of reports) for (const g of r.targetGroups) {
8
- for (const e of g.errors) lines.push(`::error title=${esc(r.package)} (${esc(g.id)})::${esc(e)}`);
9
- for (const w of g.warnings) lines.push(`::warning title=${esc(r.package)} (${esc(g.id)})::${esc(w)}`);
8
+ for (const e of g.errors) {
9
+ const loc = e.file !== void 0 ? ` file=${esc(e.file)}${e.line !== void 0 ? `,line=${e.line}` : ""}` : "";
10
+ lines.push(`::error title=${esc(r.package)} (${esc(g.id)})${loc}::${esc(e.text)}`);
11
+ }
12
+ for (const w of g.warnings) {
13
+ const loc = w.file !== void 0 ? ` file=${esc(w.file)}${w.line !== void 0 ? `,line=${w.line}` : ""}` : "";
14
+ lines.push(`::warning title=${esc(r.package)} (${esc(g.id)})${loc}::${esc(w.text)}`);
15
+ }
10
16
  }
11
17
  return lines.length === 0 ? [] : [{
12
18
  target: "stdout",
@@ -9,7 +9,7 @@ const MarkdownFormatter = {
9
9
  lines.push(`## ❌ ${r.package}`);
10
10
  for (const g of failing) {
11
11
  lines.push(`- **${g.id}**`);
12
- for (const e of g.errors) lines.push(` - ${e}`);
12
+ for (const e of g.errors) lines.push(` - ${e.text}`);
13
13
  }
14
14
  } else lines.push(`## ✅ ${r.package}`);
15
15
  }
@@ -2,20 +2,40 @@ import { formatTime } from "../timer.js";
2
2
  import pc from "picocolors";
3
3
 
4
4
  //#region src/report/formatters/terminal.ts
5
+ const fmtBytes = (n) => n < 1024 ? `${n} B` : `${(n / 1024).toFixed(2)} kB`;
6
+ const fileCount = (g) => g.passes.reduce((sum, p) => sum + p.files.length, 0);
7
+ const diagLine = (d) => {
8
+ return `${d.file !== void 0 ? ` ${d.file}${d.line !== void 0 ? `:${d.line}` : ""}` : ""} ${d.text}`.trim();
9
+ };
5
10
  const TerminalFormatter = {
6
11
  format: "terminal",
7
12
  render: (reports, ctx) => {
8
13
  const color = (fn, s) => ctx.noColor ? s : fn(s);
9
14
  const lines = [];
15
+ let totalMs = 0;
10
16
  for (const r of reports) {
11
17
  lines.push(color(pc.bold, r.package));
12
18
  for (const g of r.targetGroups) {
13
19
  const status = g.errors.length ? color(pc.red, "✗") : color(pc.green, "✓");
14
- lines.push(` ${status} ${g.id}${g.emittedFiles.length} files (${formatTime(g.timings.totalMs)})`);
15
- for (const e of g.errors) lines.push(` ${color(pc.red, "error")}: ${e}`);
16
- for (const w of g.warnings) lines.push(` ${color(pc.yellow, "warn")}: ${w}`);
20
+ lines.push(` ${status} ${g.id} ${fileCount(g)} files · ${formatTime(g.timings.totalMs)}`);
21
+ totalMs += g.timings.totalMs;
22
+ if (ctx.verbose) for (const p of g.passes) {
23
+ lines.push(` ${color(pc.dim, `${p.id} (${formatTime(p.ms)})`)}`);
24
+ for (const f of p.files) {
25
+ const gz = f.gzip !== void 0 ? ` │ gzip ${fmtBytes(f.gzip)}` : "";
26
+ lines.push(` ${f.path} ${fmtBytes(f.bytes)}${gz}`);
27
+ }
28
+ }
29
+ for (const e of g.errors) lines.push(` ${color(pc.red, "error")} ${diagLine(e)}`);
30
+ for (const w of g.warnings) lines.push(` ${color(pc.yellow, "warn")} ${diagLine(w)}`);
17
31
  }
18
32
  }
33
+ const pkgs = reports.length;
34
+ if (pkgs > 0) {
35
+ const pkgLabel = `${pkgs} package${pkgs === 1 ? "" : "s"}`;
36
+ const hasErrors = reports.some((r) => r.targetGroups.some((g) => g.errors.length > 0));
37
+ lines.push(hasErrors ? `${color(pc.red, "✗")} build failed · ${pkgLabel} · ${formatTime(totalMs)}` : `${color(pc.green, "✔")} build complete · ${pkgLabel} · ${formatTime(totalMs)}`);
38
+ }
19
39
  const content = lines.join("\n");
20
40
  return content === "" ? [] : [{
21
41
  target: "stdout",
@@ -7,7 +7,7 @@ import { OutputRenderer } from "../services/OutputRenderer.js";
7
7
  import { Effect, Layer } from "effect";
8
8
 
9
9
  //#region src/report/layers/OutputRendererLive.ts
10
- const formatters = new Map([
10
+ const formatters = /* @__PURE__ */ new Map([
11
11
  ["terminal", TerminalFormatter],
12
12
  ["json", JsonFormatter],
13
13
  ["markdown", MarkdownFormatter],
@@ -0,0 +1,62 @@
1
+ import { gzipSync } from "node:zlib";
2
+
3
+ //#region src/report/metrics-plugin.ts
4
+ function contentOf(chunk) {
5
+ const c = chunk;
6
+ if (c.type === "chunk") return c.code;
7
+ if (c.type === "asset") return c.source;
8
+ return c.code ?? c.source;
9
+ }
10
+ function byteLength(content) {
11
+ return typeof content === "string" ? Buffer.byteLength(content) : content.byteLength;
12
+ }
13
+ /**
14
+ * Rolldown plugin that records emitted-file metrics into the BuildCollector via writeBundle (which
15
+ * fires for the JS pass AND the emitDtsOnly dts pass — verified against tsdown 0.22.3), plus a
16
+ * defensive onLog for rolldown-level diagnostics that bypass tsdown's logger. Append it to each
17
+ * build pass's `plugins` array. `bytes` is taken from the in-memory chunk/asset content (no fs);
18
+ * `gzip` is computed only when `verbose`.
19
+ */
20
+ function buildMetricsPlugin(collector, groupId, pass, verbose) {
21
+ return {
22
+ name: "savvy:build-metrics",
23
+ writeBundle(_outputOptions, bundle) {
24
+ for (const [key, chunk] of Object.entries(bundle)) {
25
+ const content = contentOf(chunk);
26
+ if (content === void 0) continue;
27
+ const bytes = byteLength(content);
28
+ collector.recordEmitted(groupId, pass, {
29
+ path: key,
30
+ bytes,
31
+ ...verbose ? { gzip: gzipSync(typeof content === "string" ? Buffer.from(content) : content).length } : {}
32
+ });
33
+ }
34
+ },
35
+ onLog(level, log) {
36
+ const l = log;
37
+ const entry = {
38
+ text: l.message ?? String(log),
39
+ ...l.id !== void 0 ? { file: l.id } : {},
40
+ ...l.loc?.line !== void 0 ? { line: l.loc.line } : {},
41
+ ...l.loc?.column !== void 0 ? { column: l.loc.column } : {}
42
+ };
43
+ if (level === "error") {
44
+ collector.recordError(groupId, {
45
+ source: "rolldown",
46
+ level: "error",
47
+ ...entry
48
+ });
49
+ return;
50
+ }
51
+ if (level === "warn") collector.recordWarning(groupId, {
52
+ source: "rolldown",
53
+ level: "warn",
54
+ ...entry
55
+ });
56
+ return false;
57
+ }
58
+ };
59
+ }
60
+
61
+ //#endregion
62
+ export { buildMetricsPlugin };
@@ -18,7 +18,10 @@ const renderReport = (reports, options) => Effect.gen(function* () {
18
18
  const env = options.env ?? (yield* detector.detect());
19
19
  const executor = yield* executorResolver.resolve(env);
20
20
  const format = yield* formatSelector.select(executor, options.explicitFormat, env);
21
- return yield* renderer.render(reports, format, { noColor: options.noColor });
21
+ return yield* renderer.render(reports, format, {
22
+ noColor: options.noColor,
23
+ verbose: options.verbose ?? false
24
+ });
22
25
  });
23
26
 
24
27
  //#endregion
package/report/schema.js CHANGED
@@ -1,19 +1,40 @@
1
1
  import { Schema } from "effect";
2
2
 
3
3
  //#region src/report/schema.ts
4
- const ReportTimings = Schema.Struct({ totalMs: Schema.Number }).annotations({ identifier: "ReportTimings" });
5
- const TargetGroupReport = Schema.Struct({
4
+ var ReportTimings = class extends Schema.Class("ReportTimings")({ totalMs: Schema.Number }) {};
5
+ /** A captured warning or error, from tsdown's logger, rolldown's onLog, or API Extractor. */
6
+ var DiagnosticEntry = class extends Schema.Class("DiagnosticEntry")({
7
+ source: Schema.Literal("tsdown", "rolldown", "api-extractor"),
8
+ level: Schema.Literal("warn", "error"),
9
+ text: Schema.String,
10
+ file: Schema.optional(Schema.String),
11
+ line: Schema.optional(Schema.Number),
12
+ column: Schema.optional(Schema.Number)
13
+ }) {};
14
+ /** One emitted output file with its in-memory byte size (gzip only when --verbose). */
15
+ var EmittedFile = class extends Schema.Class("EmittedFile")({
16
+ path: Schema.String,
17
+ bytes: Schema.Number,
18
+ gzip: Schema.optional(Schema.Number)
19
+ }) {};
20
+ /** One build pass within a target group (js / dts / loose / exe / meta). */
21
+ var PassReport = class extends Schema.Class("PassReport")({
22
+ id: Schema.Literal("js", "dts", "loose", "exe", "meta"),
23
+ files: Schema.Array(EmittedFile),
24
+ ms: Schema.Number
25
+ }) {};
26
+ var TargetGroupReport = class extends Schema.Class("TargetGroupReport")({
6
27
  id: Schema.String,
7
28
  entries: Schema.Array(Schema.String),
8
- emittedFiles: Schema.Array(Schema.String),
9
- timings: ReportTimings,
10
- warnings: Schema.Array(Schema.String),
11
- errors: Schema.Array(Schema.String)
12
- }).annotations({ identifier: "TargetGroupReport" });
13
- const BuildReport = Schema.Struct({
29
+ passes: Schema.Array(PassReport),
30
+ warnings: Schema.Array(DiagnosticEntry),
31
+ errors: Schema.Array(DiagnosticEntry),
32
+ timings: ReportTimings
33
+ }) {};
34
+ var BuildReport = class extends Schema.Class("BuildReport")({
14
35
  package: Schema.String,
15
36
  targetGroups: Schema.Array(TargetGroupReport)
16
- }).annotations({ identifier: "BuildReport" });
37
+ }) {};
17
38
 
18
39
  //#endregion
19
- export { BuildReport, ReportTimings, TargetGroupReport };
40
+ export { BuildReport, DiagnosticEntry, EmittedFile, PassReport, ReportTimings, TargetGroupReport };
@@ -0,0 +1,41 @@
1
+ //#region src/report/tsdown-logger.ts
2
+ const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
3
+ const join = (args) => args.map((a) => String(a)).join(" ").replace(ANSI, "").trim();
4
+ /**
5
+ * A tsdown `customLogger` that routes warnings/errors into the BuildCollector instead of the
6
+ * console. Paired with `logLevel: "silent"` in the same build config: silent suppresses tsdown's
7
+ * own console output while this logger still receives every message (verified against tsdown 0.22.3).
8
+ * info/success are dropped — file metrics come from the writeBundle plugin and timing from our timer.
9
+ */
10
+ function createTsdownLogger(collector, groupId) {
11
+ const seenOnce = /* @__PURE__ */ new Set();
12
+ return {
13
+ level: "info",
14
+ info: () => {},
15
+ success: () => {},
16
+ clearScreen: () => {},
17
+ warn: (...args) => collector.recordWarning(groupId, {
18
+ source: "tsdown",
19
+ level: "warn",
20
+ text: join(args)
21
+ }),
22
+ warnOnce: (...args) => {
23
+ const text = join(args);
24
+ if (seenOnce.has(text)) return;
25
+ seenOnce.add(text);
26
+ collector.recordWarning(groupId, {
27
+ source: "tsdown",
28
+ level: "warn",
29
+ text
30
+ });
31
+ },
32
+ error: (...args) => collector.recordError(groupId, {
33
+ source: "tsdown",
34
+ level: "error",
35
+ text: join(args)
36
+ })
37
+ };
38
+ }
39
+
40
+ //#endregion
41
+ export { createTsdownLogger };