@savvy-web/bundler 0.2.1 → 0.4.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.
package/README.md CHANGED
@@ -87,7 +87,16 @@ With no `targets` map the build falls back to the single-`npm` group above.
87
87
 
88
88
  ## API Extractor meta
89
89
 
90
- Set the optional `meta` field on `defineBuild` to generate an [API Extractor](https://api-extractor.com/) api-model from a package's type declarations:
90
+ The bundler generates an [API Extractor](https://api-extractor.com/) api-model from a package's type declarations. Two behaviors come online:
91
+
92
+ - `savvy build --target meta` runs API Extractor over the dev build's `.d.ts` — no tsdown build, so it depends only on a prior `--target dev`. It writes the api-model (`<unscoped>.api.json`, `tsdoc-metadata.json` and a resolved `tsconfig.json`) into each `localPaths` directory.
93
+ - `savvy build --target prod` additionally emits the same bundle into `dist/prod/npm/meta` as a release asset alongside `pkg/`.
94
+
95
+ The `meta` field on `defineBuild` is tri-state and controls these:
96
+
97
+ - **Omitted** (or `undefined`) — generation runs with default options. `--target meta` works with no configuration and `--target prod` emits the meta asset. This is the default; you do not need a `meta` field to use `--target meta`.
98
+ - **An object** — override the defaults: `localPaths` (directories the api-model is copied into on `--target meta`) and `tsdoc` (warning suppression and custom tags).
99
+ - **`false`** — opt out entirely; both `--target meta` and the prod meta asset become no-ops.
91
100
 
92
101
  ```ts
93
102
  const config = defineBuild({
@@ -101,14 +110,10 @@ const config = defineBuild({
101
110
  },
102
111
  },
103
112
  });
104
- ```
105
-
106
- With `meta` set, two behaviors come online:
107
113
 
108
- - `savvy build --target meta` runs API Extractor over the dev build's `.d.ts` — no tsdown build, so it depends only on a prior `--target dev`. It writes the api-model (`<unscoped>.api.json`, `tsdoc-metadata.json` and a resolved `tsconfig.json`) into each `localPaths` directory.
109
- - `savvy build --target prod` additionally emits the same bundle into `dist/prod/npm/meta` as a release asset alongside `pkg/`.
110
-
111
- `meta` is optional; omit it and neither behavior runs. `--target meta` errors if the config has no `meta` field.
114
+ // Or opt out of api-model generation altogether:
115
+ // const config = defineBuild({ meta: false });
116
+ ```
112
117
 
113
118
  ## Executable binaries
114
119
 
@@ -197,6 +202,22 @@ const config = defineBuild({
197
202
 
198
203
  Each override carries the same `format`, `bundle`, `externals`, `bundleNodeModules`, `bundledPackages` and `dtsExternals` fields as the base config. An override does not inherit the base `externals` — list what that partition needs. The build errors if an override names an export path the package does not declare.
199
204
 
205
+ ## Loose files
206
+
207
+ Every output above lands at a path the package's `exports` map addresses. Some files have to sit at a fixed name the runtime resolves by convention, outside that graph — a pnpm config dependency, for one, forbids runtime `dependencies` and resolves its `pnpmfile.mjs`/`pnpmfile.cjs` by filename at the package root. Use `looseFiles` to emit a standalone bundled file at a literal output path, with no exports entry, no declaration and no api-model:
208
+
209
+ ```ts
210
+ const config = defineBuild({
211
+ bundleNodeModules: true,
212
+ looseFiles: {
213
+ "pnpmfile.mjs": "./src/pnpmfile.ts",
214
+ "pnpmfile.cjs": "./src/pnpmfile.ts",
215
+ },
216
+ });
217
+ ```
218
+
219
+ Each key is the literal output filename written into the package root; each value is a source path (a bare string) or a `{ source, format }` object. The format is inferred from the key extension — `.mjs` is ESM, `.cjs` is CJS — so the example above bundles the one source into both an ESM and a CJS file from a single config. A `.js` key is format-ambiguous and needs an explicit `format`. Pair `looseFiles` with `bundleNodeModules` so each file is self-contained, since a config dependency cannot resolve runtime `dependencies` of its own.
220
+
200
221
  ## Minified output
201
222
 
202
223
  Prod output is not minified by default. This builder targets Node libraries, where readable output matters more than bundle size — minified code degrades stack traces and trips some security scanners. Set `minify` to opt back in:
@@ -222,6 +243,28 @@ const config = defineBuild({
222
243
  });
223
244
  ```
224
245
 
246
+ ## Build-time constants
247
+
248
+ The build injects `process.env.__PACKAGE_VERSION__` as a compile-time constant set to the package's version, so source can read its own version without importing `package.json` at runtime:
249
+
250
+ ```ts
251
+ // somewhere in src/
252
+ const version = process.env.__PACKAGE_VERSION__;
253
+ // the reference is replaced at build time with the package's version as a string literal
254
+ ```
255
+
256
+ Add your own compile-time replacements with the `define` field. Values are inserted verbatim, so string literals must be pre-quoted:
257
+
258
+ ```ts
259
+ const config = defineBuild({
260
+ define: {
261
+ "process.env.FLAG": JSON.stringify("on"),
262
+ },
263
+ });
264
+ ```
265
+
266
+ `define` merges with the auto-injected version constant; a key of `process.env.__PACKAGE_VERSION__` in your own `define` wins.
267
+
225
268
  ## Features
226
269
 
227
270
  - **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.
@@ -235,8 +278,10 @@ const config = defineBuild({
235
278
  - **Dual-format output** — esm-only by default; set `format` to `["esm", "cjs"]` for a require-able CJS output with default-export interop, `.d.cts` declarations and dual `import`/`require` export conditions.
236
279
  - **Dependency bundling** — declared dependencies stay external by default; `bundle`, `bundleNodeModules`, `bundledPackages` and `dtsExternals` force-inline specific packages or all node_modules into the output, inline select declarations into the `.d.ts` or hold a package out of the declaration bundle when its types cannot be inlined.
237
280
  - **Per-entry overrides** — `overrides` pins a subset of export entries to their own format and bundling, so one entry can ship dual-format CJS in an otherwise ESM-only package without changing the rest.
281
+ - **Loose files** — `looseFiles` emits standalone bundled files at literal output paths outside the exports/declaration/api-model graph, with the format inferred from the key extension; pair with `bundleNodeModules` for self-contained pnpm config-dependency pnpmfiles.
238
282
  - **Readable prod output** — prod output is unminified by default to keep stack traces legible and pass security scanners; `minify` opts back in.
239
283
  - **Default manifest stripping** — the published `package.json` drops build- and dev-only fields automatically; a custom `transform` replaces the default and can re-apply it via `defaultManifestTransform`.
284
+ - **Build-time constants** — the package version is injected as `process.env.__PACKAGE_VERSION__`, and the `define` field adds your own verbatim compile-time replacements.
240
285
  - **Fast-fail config validation** — `runBuild` validates the config (`publishConfig.targets`, `exe`, `meta`) before any build work, raising a typed `ConfigValidationError` on the first violation.
241
286
  - **One devDependency** — `tsdown` is a regular dependency, pinned and tested transitively, so you never carry it or its plugin peers in your own tree.
242
287
  - **Injectable orchestration** — `runBuild` takes its IO dependencies as options, so the build is testable without spawning a real bundle.
@@ -244,7 +289,7 @@ const config = defineBuild({
244
289
 
245
290
  ## API
246
291
 
247
- - `defineBuild(input)` — normalizes a build config (`externals`, `bundle`, `bundleNodeModules`, `bundledPackages`, `dtsExternals`, `minify`, `devManifest`, `transform`, `output`, `meta`, `jsx`, `exe`, `format`, `overrides`), 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.
292
+ - `defineBuild(input)` — normalizes a build config (`externals`, `bundle`, `bundleNodeModules`, `bundledPackages`, `dtsExternals`, `minify`, `devManifest`, `transform`, `output`, `meta`, `jsx`, `exe`, `format`, `overrides`, `looseFiles`, `define`), 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.
248
293
  - `runBuild(config, options)` — the orchestrator. Parses `--target`/`--watch` from `options.argv`, reads `package.json` at `options.cwd`, derives entries, drives the build for the selected target and renders a report. Every IO dependency on `options` is injectable for tests.
249
294
  - `parseArgs(argv)` — the argument parser behind `runBuild`, exported for embedding.
250
295
 
package/config.js CHANGED
@@ -18,7 +18,9 @@ function defineBuild(input = {}) {
18
18
  jsx: input.jsx,
19
19
  exe: input.exe,
20
20
  format: input.format,
21
- overrides: input.overrides
21
+ overrides: input.overrides,
22
+ looseFiles: input.looseFiles,
23
+ define: input.define
22
24
  };
23
25
  }
24
26
  function parseArgs(argv) {
package/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { BuildFormat, BuildTargetGroupsOptions, ExeConfig, GenerateMetaOptions, Json, JsxConfig, MetaOptions, MetaResult, PublishTargets, RenderedOutput, RunExeBuildOptions, TargetGroupRef, TargetResolution, TsconfigJsx, defaultManifestTransform } from "@savvy-web/tsdown-plugins";
1
+ import { BuildFormat, BuildTargetGroupsOptions, ExeConfig, GenerateMetaOptions, Json, JsxConfig, LooseFiles, MetaOptions, MetaResult, PublishTargets, RenderedOutput, RunExeBuildOptions, TargetGroupRef, TargetResolution, TsconfigJsx, defaultManifestTransform } from "@savvy-web/tsdown-plugins";
2
2
 
3
3
  //#region src/config.d.ts
4
4
  interface BuildEntryOverride {
@@ -75,7 +75,13 @@ interface BuildConfigInput {
75
75
  targetGroup: TargetGroupRef;
76
76
  }) => Json;
77
77
  readonly output?: OutputConfig;
78
- readonly meta?: MetaOptions;
78
+ /**
79
+ * API-model (meta) generation. Tri-state: omit it (or `undefined`) to generate with
80
+ * DEFAULT options — `savvy build --target meta` always works and `--target prod` emits the
81
+ * meta release asset. Pass an object to override the defaults (`localPaths`, `tsdoc`). Pass
82
+ * `false` to opt OUT entirely — both `--target meta` and the prod meta asset become no-ops.
83
+ */
84
+ readonly meta?: MetaOptions | false;
79
85
  readonly jsx?: JsxConfig | undefined;
80
86
  readonly exe?: ExeConfig | ReadonlyArray<ExeConfig> | undefined;
81
87
  /**
@@ -90,6 +96,21 @@ interface BuildConfigInput {
90
96
  * an otherwise ESM-only package (e.g. silk's `./changesets/markdownlint`).
91
97
  */
92
98
  readonly overrides?: ReadonlyArray<BuildEntryOverride> | undefined;
99
+ /**
100
+ * Standalone bundled output files emitted at literal paths (e.g. pnpm config-dependency
101
+ * pnpmfiles), outside the exports/dts/meta graph. Keys are literal output filenames; values
102
+ * are a source path (bare string) or `{ source, format }`. Format is inferred from a
103
+ * `.mjs`/`.cjs` key and required for an ambiguous `.js` key. Pair with `bundleNodeModules`
104
+ * to make each file self-contained.
105
+ */
106
+ readonly looseFiles?: LooseFiles | undefined;
107
+ /**
108
+ * Compile-time global replacements forwarded to the tsdown/rolldown build `define`.
109
+ * Values are inserted VERBATIM, so string literals must be quoted:
110
+ * `{ "process.env.FLAG": JSON.stringify("on") }`. Merged with the auto-injected
111
+ * `process.env.__PACKAGE_VERSION__` define; a user key of the same name wins.
112
+ */
113
+ readonly define?: Record<string, string> | undefined;
93
114
  }
94
115
  interface BuildConfig {
95
116
  readonly formats: ReadonlyArray<"esm">;
@@ -126,12 +147,16 @@ interface BuildConfig {
126
147
  targetGroup: TargetGroupRef;
127
148
  }) => Json) | undefined;
128
149
  readonly output?: OutputConfig | undefined;
129
- readonly meta?: MetaOptions | undefined;
150
+ readonly meta?: MetaOptions | false | undefined;
130
151
  readonly jsx?: JsxConfig | undefined;
131
152
  readonly exe?: ExeConfig | ReadonlyArray<ExeConfig> | undefined;
132
153
  /** Output module formats forwarded to the tsdown build (esm-only by default; add "cjs" for dual-format). */
133
154
  readonly format?: ReadonlyArray<BuildFormat> | undefined;
134
155
  readonly overrides?: ReadonlyArray<BuildEntryOverride> | undefined;
156
+ /** Standalone bundled output files emitted at literal paths, outside the exports/dts/meta graph. */
157
+ readonly looseFiles?: LooseFiles | undefined;
158
+ /** Compile-time global replacements forwarded to the build `define` (merged with the auto-version). */
159
+ readonly define?: Record<string, string> | undefined;
135
160
  }
136
161
  /** Normalize + validate a defineBuild config. Pure when imported; self-runs when entry (see run.ts). */
137
162
  declare function defineBuild(input?: BuildConfigInput): BuildConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/bundler",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
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,10 +24,11 @@
24
24
  "types": "./index.d.ts",
25
25
  "import": "./index.js"
26
26
  },
27
- "./ecma.json": "./public/ecma.json"
27
+ "./ecma.json": "./public/ecma.json",
28
+ "./package.json": "./package.json"
28
29
  },
29
30
  "dependencies": {
30
- "@savvy-web/tsdown-plugins": "0.2.1",
31
+ "@savvy-web/tsdown-plugins": "0.4.0",
31
32
  "@tsdown/exe": "^0.22.1",
32
33
  "tsdown": "^0.22.2"
33
34
  },
package/run.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { parseArgs } from "./config.js";
2
- import { ConfigValidator, ConfigValidatorLive, ReportPipelineLive, buildTargetGroups, createEntryName, generateMeta, normalizeExeOptions, normalizeMetaOptions, packageJsonEntries, readTsconfigJsx, removeDeclarationMaps, renderReport, resolveJsxConfig, resolveTargets, runExeBuild, writeResolvedTsconfig, writeTargetsBinding } from "@savvy-web/tsdown-plugins";
2
+ import { ConfigValidator, ConfigValidatorLive, ReportPipelineLive, buildTargetGroups, createEntryName, generateMeta, normalizeExeOptions, normalizeLooseFiles, normalizeMetaOptions, packageJsonEntries, readTsconfigJsx, removeDeclarationMaps, renderReport, resolveJsxConfig, resolveTargets, runExeBuild, writeResolvedTsconfig, writeTargetsBinding } from "@savvy-web/tsdown-plugins";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { Effect } from "effect";
@@ -73,11 +73,19 @@ async function runBuild(config, options) {
73
73
  ...publishTargets !== void 0 ? { targets: publishTargets } : {},
74
74
  ...config.exe !== void 0 ? { exe: config.exe } : {},
75
75
  osCpu: osCpuForValidate,
76
- ...config.meta !== void 0 ? { meta: config.meta } : {}
76
+ ...config.meta !== void 0 && config.meta !== false ? { meta: config.meta } : {},
77
+ ...config.looseFiles !== void 0 ? { looseFiles: config.looseFiles } : {}
77
78
  })).pipe(Effect.provide(ConfigValidatorLive)));
78
79
  if (target === "meta") {
79
- if (config.meta === void 0) throw new Error("`savvy build --target meta` requires a `meta` option in the build config");
80
- const norm = normalizeMetaOptions(config.meta);
80
+ if (config.meta === false) {
81
+ (options.writeOutput ?? ((o) => process.stdout.write(`${o.content}\n`)))({
82
+ target: "stdout",
83
+ contentType: "text/plain",
84
+ content: `meta: generation disabled (meta: false) for ${packageName}`
85
+ });
86
+ return;
87
+ }
88
+ const norm = normalizeMetaOptions(config.meta ?? {});
81
89
  const dtsBasenames = {};
82
90
  for (const name of Object.keys(entries)) dtsBasenames[name] = name;
83
91
  await runGenerateMeta({
@@ -153,6 +161,7 @@ async function runBuild(config, options) {
153
161
  baseEntries = onlyBase;
154
162
  dualExports = dualExportKeys;
155
163
  }
164
+ const looseFiles = config.looseFiles !== void 0 ? normalizeLooseFiles(config.looseFiles) : void 0;
156
165
  const startMs = Date.now();
157
166
  const { groups, resolution } = target === "dev" ? {
158
167
  groups: [{
@@ -177,13 +186,15 @@ async function runBuild(config, options) {
177
186
  ...config.transform !== void 0 ? { transform: config.transform } : {},
178
187
  ...jsx !== void 0 ? { jsx } : {},
179
188
  ...config.format !== void 0 ? { format: config.format } : {},
189
+ ...config.define !== void 0 ? { define: config.define } : {},
180
190
  ...overridePartitions.length > 0 ? { overrides: overridePartitions } : {},
181
- ...dualExports !== void 0 ? { dualExports } : {}
191
+ ...dualExports !== void 0 ? { dualExports } : {},
192
+ ...looseFiles !== void 0 ? { looseFiles } : {}
182
193
  });
183
194
  if (target === "prod" && resolution !== void 0) writeBinding(cwd, resolution);
184
- if (target === "prod" && config.meta !== void 0) {
195
+ if (target === "prod" && config.meta !== false) {
185
196
  const metaGroupId = (groups.find((g) => g.name === packageName) ?? groups[0])?.id ?? "npm";
186
- const norm = normalizeMetaOptions(config.meta);
197
+ const norm = normalizeMetaOptions(config.meta ?? {});
187
198
  const dtsBasenames = {};
188
199
  for (const name of Object.keys(entries)) dtsBasenames[name] = name;
189
200
  await runGenerateMeta({