@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 +54 -9
- package/config.js +3 -1
- package/index.d.ts +28 -3
- package/package.json +4 -3
- package/run.js +18 -7
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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 ===
|
|
80
|
-
|
|
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 !==
|
|
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({
|