@savvy-web/bundler 0.2.1 → 0.3.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
113
 
106
- With `meta` set, two behaviors come online:
107
-
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
 
@@ -222,6 +227,28 @@ const config = defineBuild({
222
227
  });
223
228
  ```
224
229
 
230
+ ## Build-time constants
231
+
232
+ 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:
233
+
234
+ ```ts
235
+ // somewhere in src/
236
+ const version = process.env.__PACKAGE_VERSION__;
237
+ // the reference is replaced at build time with the package's version as a string literal
238
+ ```
239
+
240
+ Add your own compile-time replacements with the `define` field. Values are inserted verbatim, so string literals must be pre-quoted:
241
+
242
+ ```ts
243
+ const config = defineBuild({
244
+ define: {
245
+ "process.env.FLAG": JSON.stringify("on"),
246
+ },
247
+ });
248
+ ```
249
+
250
+ `define` merges with the auto-injected version constant; a key of `process.env.__PACKAGE_VERSION__` in your own `define` wins.
251
+
225
252
  ## Features
226
253
 
227
254
  - **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.
@@ -237,6 +264,7 @@ const config = defineBuild({
237
264
  - **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.
238
265
  - **Readable prod output** — prod output is unminified by default to keep stack traces legible and pass security scanners; `minify` opts back in.
239
266
  - **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`.
267
+ - **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
268
  - **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
269
  - **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
270
  - **Injectable orchestration** — `runBuild` takes its IO dependencies as options, so the build is testable without spawning a real bundle.
@@ -244,7 +272,7 @@ const config = defineBuild({
244
272
 
245
273
  ## API
246
274
 
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.
275
+ - `defineBuild(input)` — normalizes a build config (`externals`, `bundle`, `bundleNodeModules`, `bundledPackages`, `dtsExternals`, `minify`, `devManifest`, `transform`, `output`, `meta`, `jsx`, `exe`, `format`, `overrides`, `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
276
  - `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
277
  - `parseArgs(argv)` — the argument parser behind `runBuild`, exported for embedding.
250
278
 
package/config.js CHANGED
@@ -18,7 +18,8 @@ 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
+ define: input.define
22
23
  };
23
24
  }
24
25
  function parseArgs(argv) {
package/index.d.ts CHANGED
@@ -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,13 @@ 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
+ * Compile-time global replacements forwarded to the tsdown/rolldown build `define`.
101
+ * Values are inserted VERBATIM, so string literals must be quoted:
102
+ * `{ "process.env.FLAG": JSON.stringify("on") }`. Merged with the auto-injected
103
+ * `process.env.__PACKAGE_VERSION__` define; a user key of the same name wins.
104
+ */
105
+ readonly define?: Record<string, string> | undefined;
93
106
  }
94
107
  interface BuildConfig {
95
108
  readonly formats: ReadonlyArray<"esm">;
@@ -126,12 +139,14 @@ interface BuildConfig {
126
139
  targetGroup: TargetGroupRef;
127
140
  }) => Json) | undefined;
128
141
  readonly output?: OutputConfig | undefined;
129
- readonly meta?: MetaOptions | undefined;
142
+ readonly meta?: MetaOptions | false | undefined;
130
143
  readonly jsx?: JsxConfig | undefined;
131
144
  readonly exe?: ExeConfig | ReadonlyArray<ExeConfig> | undefined;
132
145
  /** Output module formats forwarded to the tsdown build (esm-only by default; add "cjs" for dual-format). */
133
146
  readonly format?: ReadonlyArray<BuildFormat> | undefined;
134
147
  readonly overrides?: ReadonlyArray<BuildEntryOverride> | undefined;
148
+ /** Compile-time global replacements forwarded to the build `define` (merged with the auto-version). */
149
+ readonly define?: Record<string, string> | undefined;
135
150
  }
136
151
  /** Normalize + validate a defineBuild config. Pure when imported; self-runs when entry (see run.ts). */
137
152
  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.3.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.3.0",
31
32
  "@tsdown/exe": "^0.22.1",
32
33
  "tsdown": "^0.22.2"
33
34
  },
package/run.js CHANGED
@@ -73,11 +73,18 @@ 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
77
  })).pipe(Effect.provide(ConfigValidatorLive)));
78
78
  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);
79
+ if (config.meta === false) {
80
+ (options.writeOutput ?? ((o) => process.stdout.write(`${o.content}\n`)))({
81
+ target: "stdout",
82
+ contentType: "text/plain",
83
+ content: `meta: generation disabled (meta: false) for ${packageName}`
84
+ });
85
+ return;
86
+ }
87
+ const norm = normalizeMetaOptions(config.meta ?? {});
81
88
  const dtsBasenames = {};
82
89
  for (const name of Object.keys(entries)) dtsBasenames[name] = name;
83
90
  await runGenerateMeta({
@@ -177,13 +184,14 @@ async function runBuild(config, options) {
177
184
  ...config.transform !== void 0 ? { transform: config.transform } : {},
178
185
  ...jsx !== void 0 ? { jsx } : {},
179
186
  ...config.format !== void 0 ? { format: config.format } : {},
187
+ ...config.define !== void 0 ? { define: config.define } : {},
180
188
  ...overridePartitions.length > 0 ? { overrides: overridePartitions } : {},
181
189
  ...dualExports !== void 0 ? { dualExports } : {}
182
190
  });
183
191
  if (target === "prod" && resolution !== void 0) writeBinding(cwd, resolution);
184
- if (target === "prod" && config.meta !== void 0) {
192
+ if (target === "prod" && config.meta !== false) {
185
193
  const metaGroupId = (groups.find((g) => g.name === packageName) ?? groups[0])?.id ?? "npm";
186
- const norm = normalizeMetaOptions(config.meta);
194
+ const norm = normalizeMetaOptions(config.meta ?? {});
187
195
  const dtsBasenames = {};
188
196
  for (const name of Object.keys(entries)) dtsBasenames[name] = name;
189
197
  await runGenerateMeta({