@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/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
 
@@ -23,6 +23,7 @@ import { dirname, join } from "node:path";
23
23
  * build (JS and the dts plugin share it), so a single pass cannot give per-module JS + bundled
24
24
  * dts. Per-module dts breaks type portability (TS2883); bundling the JS re-bundles workspace
25
25
  * consumers. The split keeps per-module JS AND rolled-up, self-contained declarations.
26
+ * @public
26
27
  */
27
28
  async function buildTargetGroups(options) {
28
29
  const build = options.build ?? (await import("tsdown")).build;
@@ -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"];
@@ -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
@@ -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
@@ -6,7 +6,11 @@ import { mkdtempSync, rmSync } from "node:fs";
6
6
  import { tmpdir } from "node:os";
7
7
 
8
8
  //#region src/exe/build.ts
9
- /** 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
+ */
10
14
  async function runExeBuild(options) {
11
15
  const build = options.build ?? (await import("tsdown")).build;
12
16
  for (const spec of options.specs) {
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" : ""}`;