@savvy-web/tsdown-plugins 0.8.0 → 0.9.1

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 +1 -0
  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 +5 -1
  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 +5 -1
  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 +5 -1
  18. package/exe/config.js +6 -1
  19. package/exe/filename.js +1 -0
  20. package/index.d.ts +502 -571
  21. package/index.js +3 -2
  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 +21 -2
  26. package/meta/config.js +5 -1
  27. package/meta/generate.js +5 -2
  28. package/meta/optimistic.js +1 -0
  29. package/meta/run-pass.js +75 -0
  30. package/meta/tsconfig-resolver.js +6 -6
  31. package/package.json +1 -1
  32. package/report/collector.js +16 -1
  33. package/report/formatters/ci-annotations.js +1 -0
  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 +10 -1
  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 +1 -0
  44. package/report/metrics-plugin.js +2 -0
  45. package/report/pipeline.js +2 -0
  46. package/report/schema.js +24 -3
  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 +1 -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/index.js CHANGED
@@ -28,6 +28,7 @@ import { normalizeMetaOptions } from "./meta/config.js";
28
28
  import { TsconfigResolver, resolvePortableTsconfig } from "./meta/tsconfig-resolver.js";
29
29
  import { generateMeta } from "./meta/generate.js";
30
30
  import { rewriteMetaVersions } from "./meta/optimistic.js";
31
+ import { applySubdirMetaEntries, deriveExportPaths, runMetaPass } from "./meta/run-pass.js";
31
32
  import { BuildReport, ReportTimings, TargetGroupReport } from "./report/schema.js";
32
33
  import { BuildCollector, BuildCollectorTag } from "./report/collector.js";
33
34
  import { CiAnnotationsFormatter } from "./report/formatters/ci-annotations.js";
@@ -35,6 +36,7 @@ import { JsonFormatter } from "./report/formatters/json.js";
35
36
  import { MarkdownFormatter } from "./report/formatters/markdown.js";
36
37
  import { SilentFormatter } from "./report/formatters/silent.js";
37
38
  import { TerminalFormatter } from "./report/formatters/terminal.js";
39
+ import { flattenIssues, serializeIssues, writeIssuesArtifact } from "./report/issues-artifact.js";
38
40
  import { EnvironmentDetector } from "./report/services/EnvironmentDetector.js";
39
41
  import { EnvironmentDetectorLive } from "./report/layers/EnvironmentDetectorLive.js";
40
42
  import { ExecutorResolver } from "./report/services/ExecutorResolver.js";
@@ -44,8 +46,7 @@ import { FormatSelectorLive } from "./report/layers/FormatSelectorLive.js";
44
46
  import { OutputRenderer } from "./report/services/OutputRenderer.js";
45
47
  import { OutputRendererLive } from "./report/layers/OutputRendererLive.js";
46
48
  import { ReportPipelineLive, renderReport } from "./report/pipeline.js";
47
- import { generateBuildReportSchema } from "./report/schema-export.js";
48
49
  import { writeTargetsBinding } from "./targets/binding.js";
49
50
  import { CatalogAssemblyError, CatalogResolutionError } from "workspaces-effect";
50
51
 
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 };
52
+ export { BuildCollector, BuildCollectorTag, BuildReport as BuildReportSchema, CatalogAssemblyError, CatalogResolutionError, CiAnnotationsFormatter, ConfigValidationError, ConfigValidator, ConfigValidatorLive, DEFAULT_EXE_NODE_VERSION, EnvironmentDetector, EnvironmentDetectorLive, ExecutorResolver, ExecutorResolverLive, FormatSelector, FormatSelectorLive, JsonFormatter, MarkdownFormatter, MetaGenerationError, OutputRenderer, OutputRendererLive, ReportPipelineLive, ReportTimings, SilentFormatter, TargetGroupReport as TargetGroupReportSchema, TerminalFormatter, TsconfigResolver, applySubdirMetaEntries, buildEmittedManifest, buildMetricsPlugin, buildResolvedTsconfig, buildTargetGroups, cjsDefaultInterop, computeExeFileName, createEntryName, createTimer, createTsdownLogger, defaultManifestTransform, deriveExportPaths, deriveTargetGroupOptions, emitManifest, extractEntries, flattenIssues, formatTime, generateMeta, isTargetObject, nodeBuiltinDefaultInterop, normalizeBinPaths, normalizeExeOptions, normalizeLooseFiles, normalizeMetaOptions, packageJsonEntries, readTsconfigJsx, removeDeclarationMaps, renderReport, resolveJsxConfig, resolveManifest, resolveNextVersions, resolvePortableTsconfig, resolveTargets, rewriteMetaVersions, runExeBuild, runMetaPass, serializeIssues, syncPublicDir, transformBin, transformExports, transformManifest, writeIssuesArtifact, writeResolvedTsconfig, writeTargetsBinding };
package/jsx/config.js CHANGED
@@ -5,6 +5,7 @@ import { existsSync, readFileSync } from "node:fs";
5
5
  /**
6
6
  * Resolve the effective JSX config: an explicit override wins; otherwise infer from the tsconfig
7
7
  * values. Returns undefined when no JSX transform is needed (preserve/none).
8
+ * @public
8
9
  */
9
10
  function resolveJsxConfig(tsconfig, override) {
10
11
  if (override !== void 0) return override.runtime === "automatic" ? {
@@ -21,6 +22,7 @@ function resolveJsxConfig(tsconfig, override) {
21
22
  /**
22
23
  * Read the jsx-relevant compilerOptions from a package's own tsconfig.json (best-effort;
23
24
  * returns empty on absence or parse error).
25
+ * @public
24
26
  */
25
27
  function readTsconfigJsx(cwd) {
26
28
  const path = join(cwd, "tsconfig.json");
@@ -4,7 +4,11 @@ import { join } from "node:path";
4
4
  import { readFile } from "node:fs/promises";
5
5
 
6
6
  //#region src/manifest/emit-manifest.ts
7
- /** Compute the final manifest bytes for a TargetGroup (catalog resolution + standard transforms). */
7
+ /**
8
+ * Compute the final manifest bytes for a TargetGroup (catalog resolution + standard transforms).
9
+ *
10
+ * @public
11
+ */
8
12
  async function buildEmittedManifest(options) {
9
13
  const { pkg, targetGroup, devManifest, transform } = options;
10
14
  const shouldResolve = targetGroup.isProd || devManifest === "resolve";
@@ -24,7 +28,11 @@ async function buildEmittedManifest(options) {
24
28
  exeRewrite: options.exeRewrite
25
29
  });
26
30
  }
27
- /** Rolldown plugin: emit the transformed package.json + LICENSE/README into the output pkg/ root. */
31
+ /**
32
+ * Rolldown plugin: emit the transformed package.json + LICENSE/README into the output pkg/ root.
33
+ *
34
+ * @public
35
+ */
28
36
  function emitManifest(options) {
29
37
  const sourceDir = options.sourceDir;
30
38
  return {
@@ -21,7 +21,7 @@ const NON_PUBLISHED_FIELDS = [
21
21
  /**
22
22
  * The default `transform` applied to every package's manifest when its
23
23
  * `savvy.build.ts` does not provide one of its own. Strips the build/dev-only
24
- * fields in {@link NON_PUBLISHED_FIELDS} from the emitted package.json.
24
+ * fields in `NON_PUBLISHED_FIELDS` from the emitted package.json.
25
25
  *
26
26
  * This is the pattern nearly every package repeated by hand (inherited from
27
27
  * rslib-builder); `defineBuild` now applies it automatically so a package needs a
@@ -35,6 +35,7 @@ const NON_PUBLISHED_FIELDS = [
35
35
  * Pure: the supplied `pkg` is NOT mutated — a shallow copy with the fields removed
36
36
  * is returned, so external callers invoking this from a custom transform keep their
37
37
  * input intact.
38
+ * @public
38
39
  */
39
40
  function defaultManifestTransform({ pkg }) {
40
41
  const out = { ...pkg };
@@ -91,6 +92,7 @@ const tsConditions = (exportKey, dual, subdirExports) => ({
91
92
  *
92
93
  * Export keys in `subdirExports` are built into an isolated `<key>/index.*` subdir (e.g. an
93
94
  * RSPress `./runtime`), so their conditions gain an `/index` segment.
95
+ * @public
94
96
  */
95
97
  function transformExports(exports, dual = false, subdirExports) {
96
98
  if (typeof exports === "string") return isTs(exports) ? tsConditions(".", isDualKey(dual, "."), subdirExports) : exports;
@@ -108,7 +110,11 @@ function transformExports(exports, dual = false, subdirExports) {
108
110
  }
109
111
  return exports;
110
112
  }
111
- /** Rewrite bin: TS targets to bin/[command].js (string to bin/cli.js); strip leading ./ otherwise. */
113
+ /**
114
+ * Rewrite bin: TS targets to bin/[command].js (string to bin/cli.js); strip leading ./ otherwise.
115
+ *
116
+ * @public
117
+ */
112
118
  function transformBin(bin) {
113
119
  if (typeof bin === "string") return isTs(bin) ? "bin/cli.js" : stripLeadingDotSlash(bin);
114
120
  if (bin && typeof bin === "object") {
@@ -118,7 +124,11 @@ function transformBin(bin) {
118
124
  }
119
125
  return bin;
120
126
  }
121
- /** FINAL guard: strip leading ./ from bin paths (npm 11.x drops ./-prefixed bins). */
127
+ /**
128
+ * FINAL guard: strip leading ./ from bin paths (npm 11.x drops ./-prefixed bins).
129
+ *
130
+ * @public
131
+ */
122
132
  function normalizeBinPaths(bin) {
123
133
  if (typeof bin === "string") return stripLeadingDotSlash(bin);
124
134
  if (bin && typeof bin === "object") {
@@ -128,7 +138,11 @@ function normalizeBinPaths(bin) {
128
138
  }
129
139
  return bin;
130
140
  }
131
- /** Apply the full standard manifest transform (excluding catalog resolution, done upstream). */
141
+ /**
142
+ * Apply the full standard manifest transform (excluding catalog resolution, done upstream).
143
+ *
144
+ * @public
145
+ */
132
146
  function transformManifest(pkg, options = {}) {
133
147
  const { publishConfig, scripts, ...rest } = pkg;
134
148
  const isPrivate = !(publishConfig?.access === "public");
@@ -7,6 +7,8 @@ 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
+ /** messageIds that become a hard build error in CI (they corrupt the .api.json). */
11
+ const CI_FATAL_MESSAGE_IDS = /* @__PURE__ */ new Set(["ae-forgotten-export"]);
10
12
  /** Map an API Extractor message to a collector DiagnosticInput, or undefined if not warn/error. */
11
13
  function mapExtractorMessage(message) {
12
14
  const isError = message.logLevel === ExtractorLogLevel.Error;
@@ -16,6 +18,7 @@ function mapExtractorMessage(message) {
16
18
  source: "api-extractor",
17
19
  level: isError ? "error" : "warn",
18
20
  text: message.text,
21
+ ...message.messageId !== void 0 ? { code: message.messageId } : {},
19
22
  ...message.sourceFilePath !== void 0 ? { file: message.sourceFilePath } : {},
20
23
  ...message.sourceFileLine !== void 0 ? { line: message.sourceFileLine } : {},
21
24
  ...message.sourceFileColumn !== void 0 ? { column: message.sourceFileColumn } : {}
@@ -41,7 +44,15 @@ function runApiExtractor(options) {
41
44
  tsdocMetadataFilePath: options.tsdocMetadataPath
42
45
  } } : {},
43
46
  dtsRollup: { enabled: false },
44
- apiReport: { enabled: false }
47
+ apiReport: { enabled: false },
48
+ messages: {
49
+ extractorMessageReporting: {
50
+ default: { logLevel: "warning" },
51
+ "ae-internal-missing-underscore": { logLevel: "none" },
52
+ ...options.ci === true ? Object.fromEntries([...CI_FATAL_MESSAGE_IDS].map((id) => [id, { logLevel: "error" }])) : {}
53
+ },
54
+ tsdocMessageReporting: { default: { logLevel: "warning" } }
55
+ }
45
56
  },
46
57
  packageJsonFullPath: options.packageJsonPath,
47
58
  configObjectFullPath: void 0,
@@ -53,6 +64,10 @@ function runApiExtractor(options) {
53
64
  showVerboseMessages: false,
54
65
  messageCallback: (message) => {
55
66
  if (suppressor.matches(message.messageId, message.text)) {
67
+ if (options.onSuppressed !== void 0) {
68
+ const entry = mapExtractorMessage(message);
69
+ if (entry !== void 0) options.onSuppressed(entry);
70
+ }
56
71
  message.logLevel = ExtractorLogLevel.None;
57
72
  message.handled = true;
58
73
  return;
@@ -64,7 +79,11 @@ function runApiExtractor(options) {
64
79
  if (options.onMessage !== void 0) {
65
80
  const entry = mapExtractorMessage(message);
66
81
  if (entry !== void 0) {
67
- options.onMessage(entry);
82
+ const ciFatal = options.ci !== true && entry.level === "warn" && entry.code !== void 0 && CI_FATAL_MESSAGE_IDS.has(entry.code);
83
+ options.onMessage(ciFatal ? {
84
+ ...entry,
85
+ ciFatal: true
86
+ } : entry);
68
87
  message.handled = true;
69
88
  }
70
89
  }
package/meta/config.js CHANGED
@@ -4,7 +4,11 @@ function resolveOptimistic(value, env) {
4
4
  if (value === true || value === false) return value;
5
5
  return !(env.CI || env.GITHUB_ACTIONS);
6
6
  }
7
- /** Fill defaults so downstream code never branches on undefined. */
7
+ /**
8
+ * Fill defaults so downstream code never branches on undefined.
9
+ *
10
+ * @public
11
+ */
8
12
  function normalizeMetaOptions(meta, env = process.env) {
9
13
  return {
10
14
  localPaths: meta.localPaths ?? [],
package/meta/generate.js CHANGED
@@ -16,9 +16,10 @@ function unscopedName(name) {
16
16
  * outMetaDir (`<unscoped>.api.json` + the final `package.json` + a portable `tsconfig.json`),
17
17
  * copying that trio into each localPaths dir. The api-extractor `tsdoc-metadata.json` is a
18
18
  * published-package artifact and is written into `dtsDir` (the built pkg/), not the meta bundle.
19
+ * @public
19
20
  */
20
21
  async function generateMeta(options) {
21
- const { cwd, packageName, tsconfigPath, dtsDir, entries, exportPaths, outMetaDir, localPaths, tsdoc, manifestTransform, onMessage } = options;
22
+ const { cwd, packageName, tsconfigPath, dtsDir, entries, exportPaths, outMetaDir, localPaths, tsdoc, manifestTransform, onMessage, ci, onSuppressed } = options;
22
23
  const tsdocConfigPath = writeTsdocConfig(cwd, tsdoc);
23
24
  const packageJsonPath = join(cwd, "package.json");
24
25
  mkdirSync(outMetaDir, { recursive: true });
@@ -42,7 +43,9 @@ async function generateMeta(options) {
42
43
  apiJsonPath: perEntryApiJson,
43
44
  ...isMain && !mainEntryDidTsdocMetadata ? { tsdocMetadataPath } : {},
44
45
  suppressWarnings: tsdoc.suppressWarnings,
45
- ...onMessage !== void 0 ? { onMessage } : {}
46
+ ...ci !== void 0 ? { ci } : {},
47
+ ...onMessage !== void 0 ? { onMessage } : {},
48
+ ...onSuppressed !== void 0 ? { onSuppressed } : {}
46
49
  });
47
50
  if (isMain) mainEntryDidTsdocMetadata = true;
48
51
  perEntryModels.set(entryName, JSON.parse(readFileSync(perEntryApiJson, "utf-8")));
@@ -10,6 +10,7 @@ const DEP_FIELDS = [
10
10
  * dependency version reflect their NEXT release version from `versions`. Pure: returns a new
11
11
  * object, never mutates the input. External/catalog-resolved deps (names absent from `versions`)
12
12
  * are left as-is.
13
+ * @public
13
14
  */
14
15
  function rewriteMetaVersions(pkg, versions, selfName) {
15
16
  const out = { ...pkg };
@@ -0,0 +1,75 @@
1
+ import { createEntryName } from "../entry/extract.js";
2
+ import { resolveNextVersions } from "../changesets/next-versions.js";
3
+ import { normalizeMetaOptions } from "./config.js";
4
+ import { generateMeta } from "./generate.js";
5
+ import { rewriteMetaVersions } from "./optimistic.js";
6
+ import { join } from "node:path";
7
+
8
+ //#region src/meta/run-pass.ts
9
+ /**
10
+ * Meta-pass orchestrator: derives export paths, filters bin/ entries, resolves optimistic
11
+ * next-versions, and calls generateMeta once per publish group.
12
+ * @public
13
+ */
14
+ async function runMetaPass(o) {
15
+ const gen = o.generateMeta ?? generateMeta;
16
+ const norm = normalizeMetaOptions(o.meta);
17
+ const canonicalId = (o.groups.find((g) => g.name === o.packageName) ?? o.groups[0])?.id ?? "npm";
18
+ const dtsBasenames = {};
19
+ const nonBinEntries = {};
20
+ for (const [name, src] of Object.entries(o.entries)) if (!name.startsWith("bin/")) {
21
+ dtsBasenames[name] = name;
22
+ nonBinEntries[name] = src;
23
+ }
24
+ const exportPaths = deriveExportPaths(nonBinEntries, o.exportsMap);
25
+ applySubdirMetaEntries(o.overrides, dtsBasenames, exportPaths);
26
+ const resolveNext = o.resolveNextVersions ?? resolveNextVersions;
27
+ const nextVersions = norm.optimistic ? await resolveNext(o.cwd) : void 0;
28
+ const manifestTransform = nextVersions ? (m) => rewriteMetaVersions(m, nextVersions.versions, o.packageName) : void 0;
29
+ for (const g of o.groups) await gen({
30
+ cwd: o.cwd,
31
+ packageName: o.packageName,
32
+ tsconfigPath: o.tsconfigPath,
33
+ dtsDir: join(o.cwd, "dist", "prod", g.id, "pkg"),
34
+ entries: dtsBasenames,
35
+ exportPaths,
36
+ outMetaDir: join(o.cwd, "dist", "prod", g.id, "meta"),
37
+ localPaths: g.id === canonicalId ? norm.localPaths : [],
38
+ tsdoc: norm.tsdoc,
39
+ ...manifestTransform !== void 0 ? { manifestTransform } : {},
40
+ ci: o.ci,
41
+ onMessage: (e) => e.level === "error" ? o.collector.recordError(g.id, e) : o.collector.recordWarning(g.id, e),
42
+ onSuppressed: (e) => o.collector.recordSuppressed(g.id, e)
43
+ });
44
+ }
45
+ /**
46
+ * Map entry names to export paths using the package exports map. index maps to ".".
47
+ *
48
+ * @public
49
+ */
50
+ function deriveExportPaths(entries, exportsMap) {
51
+ const out = {};
52
+ const sourceToKey = /* @__PURE__ */ new Map();
53
+ if (exportsMap) for (const [key, src] of Object.entries(exportsMap)) sourceToKey.set(src, key);
54
+ for (const [entryName, src] of Object.entries(entries)) out[entryName] = sourceToKey.get(src) ?? (entryName === "index" ? "." : `./${entryName}`);
55
+ return out;
56
+ }
57
+ /**
58
+ * For each `outSubdir` override, point its meta entry at the isolated sub-package barrel.
59
+ *
60
+ * @public
61
+ */
62
+ function applySubdirMetaEntries(overrides, dtsBasenames, exportPaths) {
63
+ if (overrides === void 0) return;
64
+ for (const ov of overrides) {
65
+ if (ov.outSubdir === void 0) continue;
66
+ const exportPath = ov.entries[0];
67
+ if (exportPath === void 0) continue;
68
+ const flatName = createEntryName(exportPath, false);
69
+ dtsBasenames[flatName] = `${ov.outSubdir}/index`;
70
+ exportPaths[flatName] = exportPath;
71
+ }
72
+ }
73
+
74
+ //#endregion
75
+ export { applySubdirMetaEntries, deriveExportPaths, runMetaPass };
@@ -138,42 +138,42 @@ var TsconfigResolver = class TsconfigResolver {
138
138
  ]);
139
139
  /** @internal */
140
140
  static NEW_LINE_MAP = /* @__PURE__ */ new Map([[NewLineKind.CarriageReturnLineFeed, "crlf"], [NewLineKind.LineFeed, "lf"]]);
141
- /** Converts a {@link ScriptTarget} enum value to its string form (e.g. `es2023`). */
141
+ /** Converts a `ScriptTarget` enum value to its string form (e.g. `es2023`). */
142
142
  static convertScriptTarget(target) {
143
143
  if (target === void 0) return void 0;
144
144
  const mapped = TsconfigResolver.SCRIPT_TARGET_MAP.get(target);
145
145
  if (mapped !== void 0) return mapped;
146
146
  return `es${target}`;
147
147
  }
148
- /** Converts a {@link ModuleKind} enum value to its string form (e.g. `nodenext`). */
148
+ /** Converts a `ModuleKind` enum value to its string form (e.g. `nodenext`). */
149
149
  static convertModuleKind(module) {
150
150
  if (module === void 0) return void 0;
151
151
  const mapped = TsconfigResolver.MODULE_KIND_MAP.get(module);
152
152
  if (mapped !== void 0) return mapped;
153
153
  return String(module);
154
154
  }
155
- /** Converts a {@link ModuleResolutionKind} enum value to its string form (e.g. `nodenext`). */
155
+ /** Converts a `ModuleResolutionKind` enum value to its string form (e.g. `nodenext`). */
156
156
  static convertModuleResolution(resolution) {
157
157
  if (resolution === void 0) return void 0;
158
158
  const mapped = TsconfigResolver.MODULE_RESOLUTION_MAP.get(resolution);
159
159
  if (mapped !== void 0) return mapped;
160
160
  return String(resolution);
161
161
  }
162
- /** Converts a {@link JsxEmit} enum value to its string form (e.g. `preserve`, `react-jsx`). */
162
+ /** Converts a `JsxEmit` enum value to its string form (e.g. `preserve`, `react-jsx`). */
163
163
  static convertJsxEmit(jsx) {
164
164
  if (jsx === void 0) return void 0;
165
165
  const mapped = TsconfigResolver.JSX_EMIT_MAP.get(jsx);
166
166
  if (mapped !== void 0) return mapped;
167
167
  return String(jsx);
168
168
  }
169
- /** Converts a {@link ModuleDetectionKind} enum value to its string form (e.g. `force`). */
169
+ /** Converts a `ModuleDetectionKind` enum value to its string form (e.g. `force`). */
170
170
  static convertModuleDetection(detection) {
171
171
  if (detection === void 0) return void 0;
172
172
  const mapped = TsconfigResolver.MODULE_DETECTION_MAP.get(detection);
173
173
  if (mapped !== void 0) return mapped;
174
174
  return String(detection);
175
175
  }
176
- /** Converts a {@link NewLineKind} enum value to its string form (`lf` or `crlf`). */
176
+ /** Converts a `NewLineKind` enum value to its string form (`lf` or `crlf`). */
177
177
  static convertNewLine(newLine) {
178
178
  if (newLine === void 0) return void 0;
179
179
  const mapped = TsconfigResolver.NEW_LINE_MAP.get(newLine);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/tsdown-plugins",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
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",
@@ -6,6 +6,7 @@ import { Context } from "effect";
6
6
  * Stateful build-event accumulator. The write surface is synchronous so it can be called directly
7
7
  * from tsdown's customLogger and API Extractor's messageCallback (both invoked synchronously).
8
8
  * `snapshot` builds the immutable BuildReport the Effect render pipeline consumes.
9
+ * @public
9
10
  */
10
11
  var BuildCollector = class {
11
12
  groups = /* @__PURE__ */ new Map();
@@ -18,8 +19,10 @@ var BuildCollector = class {
18
19
  passes: /* @__PURE__ */ new Map(),
19
20
  warnings: [],
20
21
  errors: [],
22
+ suppressed: [],
21
23
  seenPaths: /* @__PURE__ */ new Set(),
22
- seenDiagnostics: /* @__PURE__ */ new Set()
24
+ seenDiagnostics: /* @__PURE__ */ new Set(),
25
+ seenSuppressed: /* @__PURE__ */ new Set()
23
26
  };
24
27
  this.groups.set(groupId, g);
25
28
  }
@@ -67,6 +70,13 @@ var BuildCollector = class {
67
70
  g.seenDiagnostics.add(key);
68
71
  g.errors.push(toEntry(entry));
69
72
  }
73
+ recordSuppressed(groupId, entry) {
74
+ const g = this.group(groupId);
75
+ const key = diagnosticKey(entry);
76
+ if (g.seenSuppressed.has(key)) return;
77
+ g.seenSuppressed.add(key);
78
+ g.suppressed.push(toEntry(entry));
79
+ }
70
80
  snapshot(packageName) {
71
81
  const targetGroups = [];
72
82
  for (const g of this.groups.values()) {
@@ -91,6 +101,7 @@ var BuildCollector = class {
91
101
  passes,
92
102
  warnings: [...g.warnings],
93
103
  errors: [...g.errors],
104
+ suppressed: [...g.suppressed],
94
105
  timings: new ReportTimings({ totalMs })
95
106
  }));
96
107
  }
@@ -104,6 +115,7 @@ function diagnosticKey(input) {
104
115
  return [
105
116
  input.source,
106
117
  input.level,
118
+ input.code ?? "",
107
119
  input.text,
108
120
  input.file ?? "",
109
121
  String(input.line ?? ""),
@@ -115,11 +127,14 @@ function toEntry(input) {
115
127
  source: input.source,
116
128
  level: input.level,
117
129
  text: input.text,
130
+ ...input.code !== void 0 ? { code: input.code } : {},
131
+ ...input.ciFatal !== void 0 ? { ciFatal: input.ciFatal } : {},
118
132
  ...input.file !== void 0 ? { file: input.file } : {},
119
133
  ...input.line !== void 0 ? { line: input.line } : {},
120
134
  ...input.column !== void 0 ? { column: input.column } : {}
121
135
  });
122
136
  }
137
+ /** @public */
123
138
  var BuildCollectorTag = class extends Context.Tag("@savvy-web/tsdown-plugins/BuildCollector")() {};
124
139
 
125
140
  //#endregion
@@ -1,5 +1,6 @@
1
1
  //#region src/report/formatters/ci-annotations.ts
2
2
  const esc = (s) => s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
3
+ /** @public */
3
4
  const CiAnnotationsFormatter = {
4
5
  format: "ci-annotations",
5
6
  render: (reports) => {
@@ -0,0 +1,23 @@
1
+ //#region src/report/formatters/diagnostics.ts
2
+ /** "code ×N, code2 ×M", sorted by count desc then code asc. */
3
+ function suppressedSummary(entries) {
4
+ const counts = /* @__PURE__ */ new Map();
5
+ for (const e of entries) {
6
+ const code = e.code ?? "unknown";
7
+ counts.set(code, (counts.get(code) ?? 0) + 1);
8
+ }
9
+ return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([code, n]) => `${code} ×${n}`).join(", ");
10
+ }
11
+ /** Number of warnings flagged ci-fatal across every group of a package. */
12
+ function ciFatalCountForPackage(report) {
13
+ let n = 0;
14
+ for (const g of report.targetGroups) for (const w of g.warnings) if (w.ciFatal === true) n += 1;
15
+ return n;
16
+ }
17
+ /** The persuasive callout shown when ci-fatal warnings are present locally. */
18
+ function ciFatalCallout(n) {
19
+ return `⛔ ${n} ${n === 1 ? "issue" : "issues"} here ${n === 1 ? "becomes" : "become"} a hard error in CI and will fail the build. Fix before pushing.`;
20
+ }
21
+
22
+ //#endregion
23
+ export { ciFatalCallout, ciFatalCountForPackage, suppressedSummary };
@@ -1,4 +1,5 @@
1
1
  //#region src/report/formatters/json.ts
2
+ /** @public */
2
3
  const JsonFormatter = {
3
4
  format: "json",
4
5
  render: (reports) => [{
@@ -1,17 +1,27 @@
1
+ import { ciFatalCallout, ciFatalCountForPackage, suppressedSummary } from "./diagnostics.js";
2
+
1
3
  //#region src/report/formatters/markdown.ts
4
+ /** @public */
2
5
  const MarkdownFormatter = {
3
6
  format: "markdown",
4
- render: (reports) => {
7
+ render: (reports, ctx) => {
5
8
  const lines = [];
6
9
  for (const r of reports) {
7
- const failing = r.targetGroups.filter((g) => g.errors.length > 0);
8
- if (failing.length > 0) {
9
- lines.push(`## ${r.package}`);
10
- for (const g of failing) {
11
- lines.push(`- **${g.id}**`);
12
- for (const e of g.errors) lines.push(` - ${e.text}`);
13
- }
14
- } else lines.push(`## ✅ ${r.package}`);
10
+ const hasErrors = r.targetGroups.some((g) => g.errors.length > 0);
11
+ const hasWarnings = r.targetGroups.some((g) => g.warnings.length > 0);
12
+ const hasSuppressed = r.targetGroups.some((g) => g.suppressed.length > 0);
13
+ const icon = hasErrors ? "❌" : hasWarnings || hasSuppressed ? "⚠️" : "✅";
14
+ lines.push(`## ${icon} ${r.package}`);
15
+ for (const g of r.targetGroups) {
16
+ if (g.errors.length === 0 && g.warnings.length === 0 && g.suppressed.length === 0) continue;
17
+ lines.push(`- **${g.id}**`);
18
+ for (const e of g.errors) lines.push(` - ❌ ${e.text}`);
19
+ for (const w of g.warnings) lines.push(` - ⚠️ ${w.text}${w.ciFatal === true ? " [fails CI]" : ""}`);
20
+ if (g.suppressed.length > 0) if (ctx.verbose) for (const s of g.suppressed) lines.push(` - 🔇 ${s.code ?? "?"}: ${s.text}`);
21
+ else lines.push(` - 🔇 suppressed ${g.suppressed.length}: ${suppressedSummary(g.suppressed)}`);
22
+ }
23
+ const fatal = ciFatalCountForPackage(r);
24
+ if (fatal > 0) lines.push(`> ${ciFatalCallout(fatal)}`);
15
25
  }
16
26
  return [{
17
27
  target: "stdout",
@@ -1,4 +1,5 @@
1
1
  //#region src/report/formatters/silent.ts
2
+ /** @public */
2
3
  const SilentFormatter = {
3
4
  format: "silent",
4
5
  render: () => []
@@ -1,4 +1,5 @@
1
1
  import { formatTime } from "../timer.js";
2
+ import { ciFatalCallout, ciFatalCountForPackage, suppressedSummary } from "./diagnostics.js";
2
3
  import pc from "picocolors";
3
4
 
4
5
  //#region src/report/formatters/terminal.ts
@@ -7,6 +8,7 @@ const fileCount = (g) => g.passes.reduce((sum, p) => sum + p.files.length, 0);
7
8
  const diagLine = (d) => {
8
9
  return `${d.file !== void 0 ? ` ${d.file}${d.line !== void 0 ? `:${d.line}` : ""}` : ""} ${d.text}`.trim();
9
10
  };
11
+ /** @public */
10
12
  const TerminalFormatter = {
11
13
  format: "terminal",
12
14
  render: (reports, ctx) => {
@@ -27,8 +29,15 @@ const TerminalFormatter = {
27
29
  }
28
30
  }
29
31
  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)}`);
32
+ for (const w of g.warnings) {
33
+ const tag = w.ciFatal === true ? color(pc.red, " [fails CI]") : "";
34
+ lines.push(` ${color(pc.yellow, "warn")} ${diagLine(w)}${tag}`);
35
+ }
36
+ if (g.suppressed.length > 0) if (ctx.verbose) for (const s of g.suppressed) lines.push(` ${color(pc.dim, `suppressed ${s.code ?? "?"}`)} ${diagLine(s)}`);
37
+ else lines.push(` ${color(pc.dim, `suppressed ${g.suppressed.length}: ${suppressedSummary(g.suppressed)}`)}`);
31
38
  }
39
+ const fatal = ciFatalCountForPackage(r);
40
+ if (fatal > 0) lines.push(` ${color(pc.red, ciFatalCallout(fatal))}`);
32
41
  }
33
42
  const pkgs = reports.length;
34
43
  if (pkgs > 0) {
@@ -0,0 +1,88 @@
1
+ import { dirname, join } from "node:path";
2
+ import { mkdirSync, writeFileSync } from "node:fs";
3
+
4
+ //#region src/report/issues-artifact.ts
5
+ /** Copy a DiagnosticEntry to a plain object, omitting undefined optionals for stable output. */
6
+ function toPlain(d) {
7
+ const out = {
8
+ source: d.source,
9
+ level: d.level,
10
+ text: d.text
11
+ };
12
+ if (d.code !== void 0) out.code = d.code;
13
+ if (d.ciFatal !== void 0) out.ciFatal = d.ciFatal;
14
+ if (d.file !== void 0) out.file = d.file;
15
+ if (d.line !== void 0) out.line = d.line;
16
+ if (d.column !== void 0) out.column = d.column;
17
+ return out;
18
+ }
19
+ /** De-duplicate by the identity-bearing fields (registry target-groups carry identical diagnostics). */
20
+ function dedupe(entries) {
21
+ const seen = /* @__PURE__ */ new Set();
22
+ const out = [];
23
+ for (const e of entries) {
24
+ const key = JSON.stringify([
25
+ e.source,
26
+ e.level,
27
+ e.code ?? "",
28
+ e.text,
29
+ e.file ?? "",
30
+ e.line ?? -1,
31
+ e.column ?? -1
32
+ ]);
33
+ if (seen.has(key)) continue;
34
+ seen.add(key);
35
+ out.push(e);
36
+ }
37
+ return out;
38
+ }
39
+ /**
40
+ * Flatten a build snapshot into the aggregated, de-duplicated issues artifact. Pure.
41
+ *
42
+ * @public
43
+ */
44
+ function flattenIssues(reports, opts) {
45
+ const warnings = [];
46
+ const errors = [];
47
+ const suppressed = [];
48
+ for (const report of reports) for (const g of report.targetGroups) {
49
+ for (const w of g.warnings) warnings.push(toPlain(w));
50
+ for (const e of g.errors) errors.push(toPlain(e));
51
+ for (const s of g.suppressed) suppressed.push(toPlain(s));
52
+ }
53
+ return {
54
+ generatedAt: opts.generatedAt,
55
+ package: reports[0]?.package ?? "unknown",
56
+ target: opts.target,
57
+ warnings: dedupe(warnings),
58
+ errors: dedupe(errors),
59
+ suppressed: dedupe(suppressed)
60
+ };
61
+ }
62
+ /**
63
+ * Serialize the issues artifact to pretty JSON with a trailing newline.
64
+ *
65
+ * @public
66
+ */
67
+ function serializeIssues(issues) {
68
+ return `${JSON.stringify(issues, null, 2)}\n`;
69
+ }
70
+ /**
71
+ * Write the aggregated issues artifact to `<cwd>/dist/<target>/issues.json`. Returns the path written.
72
+ *
73
+ * @public
74
+ */
75
+ function writeIssuesArtifact(opts) {
76
+ const clock = opts.now ?? (() => /* @__PURE__ */ new Date());
77
+ const issues = flattenIssues(opts.reports, {
78
+ target: opts.target,
79
+ generatedAt: clock().toISOString()
80
+ });
81
+ const outPath = join(opts.cwd, "dist", opts.target, "issues.json");
82
+ mkdirSync(dirname(outPath), { recursive: true });
83
+ writeFileSync(outPath, serializeIssues(issues), "utf8");
84
+ return outPath;
85
+ }
86
+
87
+ //#endregion
88
+ export { flattenIssues, serializeIssues, writeIssuesArtifact };
@@ -4,6 +4,7 @@ import { isAgent, isCI } from "std-env";
4
4
 
5
5
  //#region src/report/layers/EnvironmentDetectorLive.ts
6
6
  const isGitHub = () => process.env.GITHUB_ACTIONS === "true" || process.env.GITHUB_ACTIONS === "1";
7
+ /** @public */
7
8
  const EnvironmentDetectorLive = Layer.succeed(EnvironmentDetector, { detect: () => Effect.sync(() => {
8
9
  if (isAgent) return "agent-shell";
9
10
  if (isGitHub()) return "ci-github";
@@ -2,6 +2,7 @@ import { ExecutorResolver } from "../services/ExecutorResolver.js";
2
2
  import { Effect, Layer } from "effect";
3
3
 
4
4
  //#region src/report/layers/ExecutorResolverLive.ts
5
+ /** @public */
5
6
  const ExecutorResolverLive = Layer.succeed(ExecutorResolver, { resolve: (env) => Effect.succeed(env === "agent-shell" ? "agent" : env === "terminal" ? "human" : "ci") });
6
7
 
7
8
  //#endregion