@savvy-web/bundler 0.12.0 → 1.0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Savvy Web Strategy, LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -15,22 +15,16 @@ pnpm add -D @savvy-web/bundler
15
15
 
16
16
  ## Quick start
17
17
 
18
- Add a `savvy.build.ts` to the package root. It both exports a config object and runs the build when invoked directly:
18
+ Add a `savvy.build.ts` to the package root:
19
19
 
20
20
  ```ts
21
21
  // savvy.build.ts
22
- import { defineBuild, runBuild } from "@savvy-web/bundler";
22
+ import { build } from "@savvy-web/bundler";
23
23
 
24
- const config = defineBuild({
24
+ await build({
25
25
  format: ["esm"],
26
26
  devManifest: "preserve",
27
27
  });
28
-
29
- export default config;
30
-
31
- if (import.meta.main) {
32
- await runBuild(config, { cwd: import.meta.dirname, argv: process.argv.slice(2) });
33
- }
34
28
  ```
35
29
 
36
30
  Wire the two targets into `package.json` scripts and run them with Node's native TypeScript support (Node 24.11+):
@@ -60,7 +54,7 @@ The bundler ships its shared TypeScript base as a subpath export. Extend it from
60
54
  ```json
61
55
  {
62
56
  "$schema": "https://json.schemastore.org/tsconfig",
63
- "extends": ["@savvy-web/bundler/tsconfig/ecma.json"]
57
+ "extends": "@savvy-web/bundler/tsconfig/ecma.json"
64
58
  }
65
59
  ```
66
60
 
@@ -168,6 +162,25 @@ const config = defineBuild({
168
162
 
169
163
  A dual-format build emits an ESM `.js` and a require-able CJS `.cjs` plus matching `.d.ts` and `.d.cts` declarations, and writes a manifest carrying both `import` and `require` export conditions. The CJS output uses default-export interop — `module.exports` is the module's default export, so a `require()` of the package yields that value directly. Omit `format`, or pass `["esm"]`, for an ESM-only build.
170
164
 
165
+ ## Ambient type exports
166
+
167
+ Most declarations are generated from your source. For a types-only export backed by a hand-authored declaration file — global augmentations, module shims, ambient `declare` blocks — point the `exports` entry straight at a `.d.ts`, either as a bare string or under a `types` key:
168
+
169
+ ```json
170
+ {
171
+ "exports": {
172
+ "./globals": "./src/globals.d.ts",
173
+ "./env": { "types": "./src/env.d.ts" }
174
+ }
175
+ }
176
+ ```
177
+
178
+ The build copies each declaration file verbatim into every built target dir and rewrites its published manifest pointer to a key-derived `{ types: "./<name>.d.ts" }`, preserving the source's `.d.ts`/`.d.cts`/`.d.mts` extension. No `transform` and no post-build copy step are involved.
179
+
180
+ Two constraints keep the copy sound. The declaration must be self-contained — a relative `import` or `export` inside it fails the build, since nothing pulls the referenced file into the output. And an export that pairs a hand-authored `types` with a runtime source (`import`/`require`/`default`) fails too: the bundler generates types from a runtime source, so the two cannot be mixed on one entry.
181
+
182
+ One further limit on scope: ambient declarations are supported only as named subpath exports (such as `"./globals"` or `"./env"`); a package must ship at least one JS or exe entry alongside its ambient declarations (a purely types-only package with no JS entries is not supported).
183
+
171
184
  ## Bundling dependencies
172
185
 
173
186
  Dependencies you declare in `package.json` are externalized automatically — they stay `import`ed from the published `.js` and referenced from the `.d.ts`, and the consumer resolves them from their own `node_modules`. You don't list declared deps anywhere; `externals` exists only to externalize a package tsdown would otherwise bundle (a transitive dep you reference but don't declare). Four fields change the bundling posture, for the cases where a dependency cannot be left external:
@@ -297,9 +310,10 @@ const config = defineBuild({
297
310
 
298
311
  ## Features
299
312
 
300
- - **One self-executing config** — `savvy.build.ts` exports a `defineBuild` object for tooling to introspect and runs the build when invoked directly. No factory-notation config file.
313
+ - **One self-executing config** — `savvy.build.ts` is a top-level `await build({...})` call that derives `cwd` and `argv` from process globals. No main guard, no `export default`, no factory-notation config file.
301
314
  - **Build targets** — `dev` for local linking, `prod` for a resolved publishable manifest (which also emits an API Extractor api-model) and `exe` for SEA binaries, on disjoint `dist/dev` and `dist/prod` output paths for clean caching.
302
315
  - **Bundled declarations** — per-module JavaScript with a single rolled-up `.d.ts` per public entry, so re-exported types stay reachable through your published export subpaths.
316
+ - **Ambient type exports** — a types-only `exports` entry backed by a hand-authored `.d.ts` (a bare string or `{ types }`) is copied verbatim into every target dir with its manifest pointer rewritten to a key-derived path, no custom `transform` or post-build copy needed; the declaration must be self-contained and may not be mixed with a runtime source.
303
317
  - **Shared tsconfig base** — extend `@savvy-web/bundler/ecma.json` for the ESNext/NodeNext/strict settings the build expects.
304
318
  - **Manifest resolution** — `catalog:` and `workspace:` specifiers are resolved against the workspace for the published target, and preserved for the linked dev target.
305
319
  - **Multi-target publishing** — a `publishConfig.targets` map publishes one package to several registries or under several names; `--target prod` builds the distinct byte variants and writes a `targets.json` binding for the release step.
@@ -320,6 +334,7 @@ const config = defineBuild({
320
334
 
321
335
  ## API
322
336
 
337
+ - `build(input?, overrides?)` — the front door: calls `defineBuild(input)` then `runBuild`, deriving `cwd` from the entry script's directory (`process.argv[1]`) and `argv` from `process.argv.slice(2)`. Pass `overrides` (any `RunOptions` key) as the test IO seam.
323
338
  - `defineBuild(input)` — normalizes a build config (`externals`, `bundle`, `bundleNodeModules`, `bundledPackages`, `dtsExternals`, `minify`, `devManifest`, `transform`, `output`, `meta`, `jsx`, `exe`, `format`, `overrides`, `looseFiles`, `define`, `plugins`), applying defaults. The `format` field controls the output module formats forwarded to tsdown (esm-only by default; add `"cjs"` for a dual-format esm+cjs build). `minify` defaults to false, `transform` defaults to a manifest stripper, and `overrides` pins a subset of entries to their own format and bundling. Pure; it does not run the build.
324
339
  - `runBuild(config, options)` — the orchestrator. Parses `--target`/`--watch`/`--verbose` from `options.argv`, reads `package.json` at `options.cwd`, derives entries, drives the build for the selected target and renders a report. `--verbose` expands the report to a per-file table; the report is quiet by default. Every IO dependency on `options` is injectable for tests.
325
340
  - `parseArgs(argv)` — the argument parser behind `runBuild`, exported for embedding.
package/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { BuildFormat, BuildPlatform, BuildReport, BuildTargetGroupsOptions, CssOptions, ExeConfig, GenerateMetaOptions, Json, JsxConfig, LooseFiles, MetaOptions, MetaResult, NextVersions, PublishTargets, RenderedOutput, RunExeBuildOptions, TargetGroupRef, TargetResolution, TsconfigJsx, defaultManifestTransform } from "@savvy-web/tsdown-plugins";
1
+ import { AmbientDtsEntry, BuildFormat, BuildPlatform, BuildReport, BuildTargetGroupsOptions, CopyAmbientDtsOptions, CssOptions, ExeConfig, GenerateMetaOptions, Json, JsxConfig, LooseFiles, MetaOptions, MetaResult, NextVersions, PublishTargets, RenderedOutput, RunExeBuildOptions, TargetGroupRef, TargetResolution, TsconfigJsx, defaultManifestTransform, extractAmbientDts } from "@savvy-web/tsdown-plugins";
2
2
  import { Plugin, Plugin as Plugin$1 } from "rolldown";
3
3
 
4
4
  //#region src/config.d.ts
@@ -242,9 +242,23 @@ interface RunOptions {
242
242
  reports: ReadonlyArray<BuildReport>;
243
243
  now?: () => Date;
244
244
  }) => string | undefined;
245
+ /** Injectable ambient-.d.ts copier (defaults to copyAmbientDts). */
246
+ readonly copyAmbientDts?: ((o: CopyAmbientDtsOptions) => void) | undefined;
245
247
  }
246
248
  /** Run a build from a normalized config. Pure orchestration; all IO injectable. @public */
247
249
  declare function runBuild(config: BuildConfig, options: RunOptions): Promise<void>;
250
+ /**
251
+ * Sugar front door: define + run in one call, deriving `cwd`/`argv` from process globals.
252
+ *
253
+ * `cwd` is the directory of the entry script (`process.argv[1]`) — the faithful equivalent of the
254
+ * old `import.meta.dirname`, correct even when invoked by an explicit path from another directory.
255
+ * `argv` is `process.argv.slice(2)`, so `--target` and friends are read internally; the package.json
256
+ * build scripts stay `node savvy.build.ts --target <t>`. `overrides` merges last as the test/advanced
257
+ * IO seam (the same injectables as {@link RunOptions}).
258
+ *
259
+ * @public
260
+ */
261
+ declare function build(input?: BuildConfigInput, overrides?: Partial<RunOptions>): Promise<void>;
248
262
  //#endregion
249
- export { type BuildConfig, type BuildConfigInput, type BuildEntryOverride, type OutputConfig, type ParsedArgs, type Plugin, type RunOptions, defaultManifestTransform, defineBuild, parseArgs, runBuild };
263
+ export { type AmbientDtsEntry, type BuildConfig, type BuildConfigInput, type BuildEntryOverride, type OutputConfig, type ParsedArgs, type Plugin, type RunOptions, build, defaultManifestTransform, defineBuild, extractAmbientDts, parseArgs, runBuild };
250
264
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { defineBuild, parseArgs } from "./config.js";
2
- import { runBuild } from "./run.js";
3
- import { defaultManifestTransform } from "@savvy-web/tsdown-plugins";
2
+ import { build, runBuild } from "./run.js";
3
+ import { defaultManifestTransform, extractAmbientDts } from "@savvy-web/tsdown-plugins";
4
4
 
5
- export { defaultManifestTransform, defineBuild, parseArgs, runBuild };
5
+ export { build, defaultManifestTransform, defineBuild, extractAmbientDts, parseArgs, runBuild };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/bundler",
3
- "version": "0.12.0",
3
+ "version": "1.0.1",
4
4
  "private": false,
5
5
  "description": "Zero-config tsdown-based bundler for Silk Suite TypeScript packages",
6
6
  "homepage": "https://github.com/savvy-web/systems/tree/main/packages/bundler",
@@ -24,13 +24,12 @@
24
24
  "types": "./index.d.ts",
25
25
  "import": "./index.js"
26
26
  },
27
- "./ecma.json": "./public/ecma.json",
28
- "./tsconfig/ecma.json": "./public/ecma.json",
27
+ "./tsconfig/ecma.json": "./tsconfig/ecma.json",
29
28
  "./package.json": "./package.json"
30
29
  },
31
30
  "dependencies": {
32
- "@savvy-web/tsdown-plugins": "0.12.0",
33
- "@tsdown/exe": "^0.22.1",
31
+ "@savvy-web/tsdown-plugins": "1.0.1",
32
+ "@tsdown/exe": "^0.22.3",
34
33
  "effect": "^3.21.4",
35
34
  "rolldown": "^1.1.3",
36
35
  "tsdown": "^0.22.3"
package/run.js CHANGED
@@ -1,7 +1,7 @@
1
- import { parseArgs } from "./config.js";
2
- import { BuildCollector, ConfigValidator, ConfigValidatorLive, ReportPipelineLive, buildEmittedManifest, buildTargetGroups, computeExeFileName, createEntryName, deriveExportPaths, normalizeExeOptions, normalizeLooseFiles, packageJsonEntries, readTsconfigJsx, removeDeclarationMaps, renderReport, resolveJsxConfig, resolveTargets, runExeBuild, runMetaPass, writeIssuesArtifact, writeResolvedTsconfig, writeTargetsBinding } from "@savvy-web/tsdown-plugins";
1
+ import { defineBuild, parseArgs } from "./config.js";
2
+ import { BuildCollector, ConfigValidationError, ConfigValidator, ConfigValidatorLive, ReportPipelineLive, assertNoEntryCollisions, buildEmittedManifest, buildTargetGroups, computeExeFileName, copyAmbientDts, createEntryName, deriveExportPaths, extractAmbientDts, normalizeExeOptions, normalizeLooseFiles, packageJsonEntries, readTsconfigJsx, removeDeclarationMaps, renderReport, resolveJsxConfig, resolveTargets, runExeBuild, runMetaPass, writeIssuesArtifact, writeResolvedTsconfig, writeTargetsBinding } from "@savvy-web/tsdown-plugins";
3
3
  import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
- import { join } from "node:path";
4
+ import { dirname, join } from "node:path";
5
5
  import { Effect } from "effect";
6
6
 
7
7
  //#region src/run.ts
@@ -96,12 +96,23 @@ async function runBuild(config, options) {
96
96
  dir: "bin"
97
97
  } : void 0;
98
98
  const entries = packageJsonEntries({
99
- pkg,
100
- cwd,
99
+ pkg: {
100
+ exports: exportsMap ?? pkg.exports,
101
+ bin: pkg.bin
102
+ },
101
103
  ...config.exe !== void 0 ? { excludeSources: [exeEntrySource] } : {}
102
104
  });
103
105
  const hasJsEntries = Object.keys(entries).length > 0;
104
106
  validateSubdirOverrides(config.overrides, entries, packageName);
107
+ const ambient = extractAmbientDts({
108
+ exports: exportsMap ?? pkg.exports,
109
+ bin: pkg.bin
110
+ }, {});
111
+ assertNoEntryCollisions(Object.keys(entries), ambient);
112
+ if (ambient.length > 0 && !hasJsEntries && config.exe === void 0) throw new ConfigValidationError({
113
+ path: "exports",
114
+ reason: "a types-only package with only ambient .d.ts exports is not supported — add at least one JS entry (or an exe) alongside the ambient declarations"
115
+ });
105
116
  if (target === "meta") {
106
117
  (options.writeOutput ?? ((o) => process.stdout.write(`${o.content}\n`)))({
107
118
  target: "stdout",
@@ -248,6 +259,14 @@ async function runBuild(config, options) {
248
259
  });
249
260
  }
250
261
  if (target === "prod") for (const g of groups) removeDeclarationMaps(join(cwd, "dist", "prod", g.id, "pkg"));
262
+ if (ambient.length > 0 && (target === "dev" || target === "prod")) {
263
+ const copyAmbient = options.copyAmbientDts ?? copyAmbientDts;
264
+ for (const g of groups) copyAmbient({
265
+ ambient,
266
+ srcCwd: cwd,
267
+ outDir: target === "dev" ? join(cwd, "dist", "dev", "pkg") : join(cwd, "dist", "prod", g.id, "pkg")
268
+ });
269
+ }
251
270
  if (config.exe !== void 0 && exeSpec !== void 0 && exeRewrite !== void 0 && exeFileName !== void 0) {
252
271
  const runExe = options.runExeBuild ?? runExeBuild;
253
272
  const groupOutDir = (g) => target === "dev" ? join(cwd, "dist", "dev", "pkg") : join(cwd, "dist", "prod", g.id, "pkg");
@@ -293,6 +312,24 @@ async function runBuild(config, options) {
293
312
  await renderAndWrite();
294
313
  writeIssuesBestEffort();
295
314
  }
315
+ /**
316
+ * Sugar front door: define + run in one call, deriving `cwd`/`argv` from process globals.
317
+ *
318
+ * `cwd` is the directory of the entry script (`process.argv[1]`) — the faithful equivalent of the
319
+ * old `import.meta.dirname`, correct even when invoked by an explicit path from another directory.
320
+ * `argv` is `process.argv.slice(2)`, so `--target` and friends are read internally; the package.json
321
+ * build scripts stay `node savvy.build.ts --target <t>`. `overrides` merges last as the test/advanced
322
+ * IO seam (the same injectables as {@link RunOptions}).
323
+ *
324
+ * @public
325
+ */
326
+ async function build(input = {}, overrides = {}) {
327
+ return runBuild(defineBuild(input), {
328
+ cwd: process.argv[1] ? dirname(process.argv[1]) : process.cwd(),
329
+ argv: process.argv.slice(2),
330
+ ...overrides
331
+ });
332
+ }
296
333
 
297
334
  //#endregion
298
- export { runBuild };
335
+ export { build, runBuild };
@@ -8,12 +8,12 @@
8
8
  "declarationMap": false,
9
9
  "emitDeclarationOnly": false,
10
10
  "esModuleInterop": true,
11
+ "exactOptionalPropertyTypes": true,
11
12
  "explainFiles": false,
12
13
  "forceConsistentCasingInFileNames": true,
13
14
  "incremental": true,
14
15
  "isolatedDeclarations": false,
15
16
  "isolatedModules": true,
16
- "exactOptionalPropertyTypes": true,
17
17
  "jsx": "preserve",
18
18
  "lib": ["esnext"],
19
19
  "module": "nodenext",
@@ -28,8 +28,8 @@
28
28
  "target": "es2025",
29
29
  "tsBuildInfoFile": "${configDir}/dist/.tsbuildinfo.lib",
30
30
  "typeRoots": ["${configDir}/node_modules/@types", "${configDir}/types"],
31
- "verbatimModuleSyntax": true,
32
- "types": ["node"]
31
+ "types": ["node"],
32
+ "verbatimModuleSyntax": true
33
33
  },
34
34
  "exclude": ["${configDir}/node_modules", "${configDir}/dist/**/*"],
35
35
  "include": [