@savvy-web/bundler 0.7.0 → 0.8.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
@@ -297,7 +297,7 @@ const config = defineBuild({
297
297
  ## API
298
298
 
299
299
  - `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.
300
- - `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.
300
+ - `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.
301
301
  - `parseArgs(argv)` — the argument parser behind `runBuild`, exported for embedding.
302
302
 
303
303
  ## Turbo tasks
package/config.js CHANGED
@@ -27,16 +27,19 @@ function parseArgs(argv) {
27
27
  let target = "dev";
28
28
  let watch = false;
29
29
  let noExe = false;
30
+ let verbose = false;
30
31
  for (let i = 0; i < argv.length; i++) if (argv[i] === "--target") {
31
32
  const v = argv[i + 1];
32
33
  if (v === "dev" || v === "prod" || v === "meta" || v === "exe") target = v;
33
34
  i++;
34
35
  } else if (argv[i] === "--watch") watch = true;
35
36
  else if (argv[i] === "--no-exe") noExe = true;
37
+ else if (argv[i] === "--verbose") verbose = true;
36
38
  return {
37
39
  target,
38
40
  watch,
39
- noExe
41
+ noExe,
42
+ verbose
40
43
  };
41
44
  }
42
45
 
package/index.d.ts CHANGED
@@ -176,6 +176,7 @@ interface ParsedArgs {
176
176
  readonly watch: boolean;
177
177
  /** Skip the SEA compile step of a dev/prod build (the manifest is still programmed). Used by `prepare`. */
178
178
  readonly noExe: boolean;
179
+ readonly verbose: boolean;
179
180
  }
180
181
  declare function parseArgs(argv: ReadonlyArray<string>): ParsedArgs;
181
182
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/bundler",
3
- "version": "0.7.0",
3
+ "version": "0.8.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",
@@ -29,11 +29,32 @@
29
29
  "./package.json": "./package.json"
30
30
  },
31
31
  "dependencies": {
32
- "@savvy-web/tsdown-plugins": "0.7.0",
32
+ "@savvy-web/tsdown-plugins": "0.8.0",
33
33
  "@tsdown/exe": "^0.22.1",
34
+ "effect": "^3.21.3",
34
35
  "tsdown": "^0.22.3"
35
36
  },
36
37
  "peerDependencies": {
37
- "effect": ">=3.21.0"
38
+ "@types/node": "^25.9.0",
39
+ "@types/react": "^19.2.0",
40
+ "@types/react-dom": "^19.2.0",
41
+ "@typescript/native-preview": "^7.0.0-dev.20260513.1",
42
+ "react": "^19.2.0",
43
+ "react-dom": "^19.2.0",
44
+ "typescript": "^6.0.0"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "@types/react": {
48
+ "optional": true
49
+ },
50
+ "@types/react-dom": {
51
+ "optional": true
52
+ },
53
+ "react": {
54
+ "optional": true
55
+ },
56
+ "react-dom": {
57
+ "optional": true
58
+ }
38
59
  }
39
60
  }
package/run.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { parseArgs } from "./config.js";
2
- import { ConfigValidator, ConfigValidatorLive, ReportPipelineLive, buildEmittedManifest, buildTargetGroups, computeExeFileName, createEntryName, generateMeta, normalizeExeOptions, normalizeLooseFiles, normalizeMetaOptions, packageJsonEntries, readTsconfigJsx, removeDeclarationMaps, renderReport, resolveJsxConfig, resolveNextVersions, resolveTargets, rewriteMetaVersions, runExeBuild, writeResolvedTsconfig, writeTargetsBinding } from "@savvy-web/tsdown-plugins";
2
+ import { BuildCollector, ConfigValidator, ConfigValidatorLive, ReportPipelineLive, buildEmittedManifest, buildTargetGroups, computeExeFileName, createEntryName, generateMeta, normalizeExeOptions, normalizeLooseFiles, normalizeMetaOptions, packageJsonEntries, readTsconfigJsx, removeDeclarationMaps, renderReport, resolveJsxConfig, resolveNextVersions, resolveTargets, rewriteMetaVersions, runExeBuild, writeResolvedTsconfig, writeTargetsBinding } from "@savvy-web/tsdown-plugins";
3
3
  import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { Effect } from "effect";
@@ -74,12 +74,13 @@ function applySubdirMetaEntries(overrides, dtsBasenames, exportPaths) {
74
74
  }
75
75
  /** Run a build from a normalized config. Pure orchestration; all IO injectable. */
76
76
  async function runBuild(config, options) {
77
- const { target, noExe } = parseArgs(options.argv);
77
+ const { target, noExe, verbose } = parseArgs(options.argv);
78
78
  const build = options.buildTargetGroups ?? buildTargetGroups;
79
79
  const cwd = options.cwd;
80
80
  const pkg = readPackageJson(cwd);
81
81
  const version = options.readVersion ? options.readVersion() : pkg.version ?? "0.0.0";
82
82
  const packageName = options.readPackageName ? options.readPackageName() : pkg.name ?? "unknown";
83
+ const collector = new BuildCollector();
83
84
  const jsx = resolveJsxConfig((options.readTsconfigJsx ?? (() => readTsconfigJsx(cwd)))(), config.jsx);
84
85
  const tsconfigPath = (options.writeTsconfig ?? ((c) => writeResolvedTsconfig({
85
86
  cwd: c,
@@ -140,7 +141,10 @@ async function runBuild(config, options) {
140
141
  await (options.runExeBuild ?? runExeBuild)({
141
142
  cwd,
142
143
  outDir: join(cwd, "dist", "dev", "pkg", "bin"),
143
- specs: exeSpecs
144
+ specs: exeSpecs,
145
+ collector,
146
+ groupId: "dev",
147
+ verbose
144
148
  });
145
149
  (options.writeOutput ?? ((o) => process.stdout.write(`${o.content}\n`)))({
146
150
  target: "stdout",
@@ -197,7 +201,6 @@ async function runBuild(config, options) {
197
201
  dualExports = dualExportKeys;
198
202
  }
199
203
  const looseFiles = config.looseFiles !== void 0 ? normalizeLooseFiles(config.looseFiles) : void 0;
200
- const startMs = Date.now();
201
204
  const { groups, resolution } = target === "dev" ? {
202
205
  groups: [{
203
206
  id: "dev",
@@ -205,108 +208,113 @@ async function runBuild(config, options) {
205
208
  }],
206
209
  resolution: void 0
207
210
  } : deriveProdGroups(publishTargets, packageName);
208
- if (hasJsEntries || config.exe === void 0) await build({
209
- cwd,
210
- version,
211
- entry: config.overrides !== void 0 ? baseEntries : entries,
212
- tsconfigPath,
213
- groups,
214
- devManifest: config.devManifest,
215
- externals: config.externals,
216
- ...config.bundledPackages !== void 0 ? { bundledPackages: config.bundledPackages } : {},
217
- ...config.dtsExternals !== void 0 ? { dtsExternals: config.dtsExternals } : {},
218
- ...config.bundleNodeModules !== void 0 ? { bundleNodeModules: config.bundleNodeModules } : {},
219
- ...config.bundle !== void 0 ? { bundle: config.bundle } : {},
220
- ...config.minify !== void 0 ? { minify: config.minify } : {},
221
- ...config.transform !== void 0 ? { transform: config.transform } : {},
222
- ...jsx !== void 0 ? { jsx } : {},
223
- ...config.format !== void 0 ? { format: config.format } : {},
224
- ...config.define !== void 0 ? { define: config.define } : {},
225
- ...overridePartitions.length > 0 ? { overrides: overridePartitions } : {},
226
- ...dualExports !== void 0 ? { dualExports } : {},
227
- ...subdirExports.size > 0 ? { subdirExports } : {},
228
- ...looseFiles !== void 0 ? { looseFiles } : {},
229
- ...exeRewrite !== void 0 ? { exeRewrite } : {}
230
- });
231
- if (target === "prod" && resolution !== void 0) writeBinding(cwd, resolution);
232
- if (target === "prod" && config.meta !== false && (config.exe === void 0 || hasJsEntries)) {
233
- const norm = normalizeMetaOptions(config.meta ?? {});
234
- const canonicalId = (groups.find((g) => g.name === packageName) ?? groups[0])?.id ?? "npm";
235
- const dtsBasenames = {};
236
- for (const name of Object.keys(entries)) dtsBasenames[name] = name;
237
- const exportPaths = deriveExportPaths(entries, exportsMap);
238
- applySubdirMetaEntries(config.overrides, dtsBasenames, exportPaths);
239
- const resolveNext = options.resolveNextVersions ?? resolveNextVersions;
240
- const nextVersions = norm.optimistic ? await resolveNext(cwd) : void 0;
241
- const manifestTransform = nextVersions ? (m) => rewriteMetaVersions(m, nextVersions.versions, packageName) : void 0;
242
- for (const g of groups) await runGenerateMeta({
211
+ const explicitFormat = config.output?.format;
212
+ const writeOutput = options.writeOutput ?? ((o) => process.stdout.write(`${o.content}\n`));
213
+ const renderAndWrite = async () => {
214
+ const rendered = await Effect.runPromise(renderReport(collector.snapshot(packageName), {
215
+ ...explicitFormat !== void 0 ? { explicitFormat } : {},
216
+ verbose,
217
+ noColor: process.env.NO_COLOR !== void 0 || !process.stdout.isTTY
218
+ }).pipe(Effect.provide(ReportPipelineLive)));
219
+ for (const output of rendered) writeOutput(output);
220
+ };
221
+ try {
222
+ if (hasJsEntries || config.exe === void 0) await build({
243
223
  cwd,
244
- packageName,
224
+ version,
225
+ entry: config.overrides !== void 0 ? baseEntries : entries,
245
226
  tsconfigPath,
246
- dtsDir: join(cwd, "dist", "prod", g.id, "pkg"),
247
- entries: dtsBasenames,
248
- exportPaths,
249
- outMetaDir: join(cwd, "dist", "prod", g.id, "meta"),
250
- localPaths: g.id === canonicalId ? norm.localPaths : [],
251
- tsdoc: norm.tsdoc,
252
- ...manifestTransform !== void 0 ? { manifestTransform } : {}
227
+ groups,
228
+ devManifest: config.devManifest,
229
+ externals: config.externals,
230
+ ...config.bundledPackages !== void 0 ? { bundledPackages: config.bundledPackages } : {},
231
+ ...config.dtsExternals !== void 0 ? { dtsExternals: config.dtsExternals } : {},
232
+ ...config.bundleNodeModules !== void 0 ? { bundleNodeModules: config.bundleNodeModules } : {},
233
+ ...config.bundle !== void 0 ? { bundle: config.bundle } : {},
234
+ ...config.minify !== void 0 ? { minify: config.minify } : {},
235
+ ...config.transform !== void 0 ? { transform: config.transform } : {},
236
+ ...jsx !== void 0 ? { jsx } : {},
237
+ ...config.format !== void 0 ? { format: config.format } : {},
238
+ ...config.define !== void 0 ? { define: config.define } : {},
239
+ ...overridePartitions.length > 0 ? { overrides: overridePartitions } : {},
240
+ ...dualExports !== void 0 ? { dualExports } : {},
241
+ ...subdirExports.size > 0 ? { subdirExports } : {},
242
+ ...looseFiles !== void 0 ? { looseFiles } : {},
243
+ ...exeRewrite !== void 0 ? { exeRewrite } : {},
244
+ collector,
245
+ verbose
253
246
  });
254
- }
255
- if (target === "prod") for (const g of groups) removeDeclarationMaps(join(cwd, "dist", "prod", g.id, "pkg"));
256
- if (config.exe !== void 0 && exeSpec !== void 0 && exeRewrite !== void 0 && exeFileName !== void 0) {
257
- const runExe = options.runExeBuild ?? runExeBuild;
258
- const groupOutDir = (g) => target === "dev" ? join(cwd, "dist", "dev", "pkg") : join(cwd, "dist", "prod", g.id, "pkg");
259
- for (const g of groups) {
260
- const outDir = groupOutDir(g);
261
- if (!hasJsEntries) {
262
- const manifest = await buildEmittedManifest({
263
- pkg,
264
- targetGroup: {
265
- id: g.id,
266
- name: g.name,
267
- isProd: target === "prod"
268
- },
269
- devManifest: config.devManifest,
270
- ...config.transform !== void 0 ? { transform: config.transform } : {},
271
- exeRewrite
272
- });
273
- mkdirSync(outDir, { recursive: true });
274
- writeFileSync(join(outDir, "package.json"), `${JSON.stringify(manifest, null, " ")}\n`);
275
- for (const file of ["LICENSE", "README.md"]) try {
276
- copyFileSync(join(cwd, file), join(outDir, file));
277
- } catch {}
278
- }
279
- if (!noExe) {
280
- const binDir = join(outDir, "bin");
281
- await runExe({
282
- cwd,
283
- outDir: binDir,
284
- specs: exeSpecs
285
- });
286
- if (options.runExeBuild === void 0 && !existsSync(join(binDir, exeFileName))) throw new Error(`exe: expected compiled binary at ${join(binDir, exeFileName)} but it is missing — tsdown's emitted name may have drifted from computeExeFileName`);
247
+ if (target === "prod" && resolution !== void 0) writeBinding(cwd, resolution);
248
+ if (target === "prod" && config.meta !== false && (config.exe === void 0 || hasJsEntries)) {
249
+ const norm = normalizeMetaOptions(config.meta ?? {});
250
+ const canonicalId = (groups.find((g) => g.name === packageName) ?? groups[0])?.id ?? "npm";
251
+ const dtsBasenames = {};
252
+ for (const name of Object.keys(entries)) if (!name.startsWith("bin/")) dtsBasenames[name] = name;
253
+ const exportPaths = deriveExportPaths(entries, exportsMap);
254
+ applySubdirMetaEntries(config.overrides, dtsBasenames, exportPaths);
255
+ const resolveNext = options.resolveNextVersions ?? resolveNextVersions;
256
+ const nextVersions = norm.optimistic ? await resolveNext(cwd) : void 0;
257
+ const manifestTransform = nextVersions ? (m) => rewriteMetaVersions(m, nextVersions.versions, packageName) : void 0;
258
+ for (const g of groups) await runGenerateMeta({
259
+ cwd,
260
+ packageName,
261
+ tsconfigPath,
262
+ dtsDir: join(cwd, "dist", "prod", g.id, "pkg"),
263
+ entries: dtsBasenames,
264
+ exportPaths,
265
+ outMetaDir: join(cwd, "dist", "prod", g.id, "meta"),
266
+ localPaths: g.id === canonicalId ? norm.localPaths : [],
267
+ tsdoc: norm.tsdoc,
268
+ ...manifestTransform !== void 0 ? { manifestTransform } : {},
269
+ onMessage: (e) => {
270
+ if (e.level === "error") collector.recordError(g.id, e);
271
+ else collector.recordWarning(g.id, e);
272
+ }
273
+ });
274
+ }
275
+ if (target === "prod") for (const g of groups) removeDeclarationMaps(join(cwd, "dist", "prod", g.id, "pkg"));
276
+ if (config.exe !== void 0 && exeSpec !== void 0 && exeRewrite !== void 0 && exeFileName !== void 0) {
277
+ const runExe = options.runExeBuild ?? runExeBuild;
278
+ const groupOutDir = (g) => target === "dev" ? join(cwd, "dist", "dev", "pkg") : join(cwd, "dist", "prod", g.id, "pkg");
279
+ for (const g of groups) {
280
+ const outDir = groupOutDir(g);
281
+ if (!hasJsEntries) {
282
+ const manifest = await buildEmittedManifest({
283
+ pkg,
284
+ targetGroup: {
285
+ id: g.id,
286
+ name: g.name,
287
+ isProd: target === "prod"
288
+ },
289
+ devManifest: config.devManifest,
290
+ ...config.transform !== void 0 ? { transform: config.transform } : {},
291
+ exeRewrite
292
+ });
293
+ mkdirSync(outDir, { recursive: true });
294
+ writeFileSync(join(outDir, "package.json"), `${JSON.stringify(manifest, null, " ")}\n`);
295
+ for (const file of ["LICENSE", "README.md"]) try {
296
+ copyFileSync(join(cwd, file), join(outDir, file));
297
+ } catch {}
298
+ }
299
+ if (!noExe) {
300
+ const binDir = join(outDir, "bin");
301
+ await runExe({
302
+ cwd,
303
+ outDir: binDir,
304
+ specs: exeSpecs,
305
+ collector,
306
+ groupId: g.id,
307
+ verbose
308
+ });
309
+ if (options.runExeBuild === void 0 && !existsSync(join(binDir, exeFileName))) throw new Error(`exe: expected compiled binary at ${join(binDir, exeFileName)} but it is missing — tsdown's emitted name may have drifted from computeExeFileName`);
310
+ }
287
311
  }
288
312
  }
313
+ } catch (err) {
314
+ await renderAndWrite();
315
+ throw err;
289
316
  }
290
- const totalMs = Date.now() - startMs;
291
- const reportEntries = Object.keys(entries);
292
- const report = {
293
- package: packageName,
294
- targetGroups: groups.map((g) => ({
295
- id: g.id,
296
- entries: reportEntries,
297
- emittedFiles: [],
298
- timings: { totalMs },
299
- warnings: [],
300
- errors: []
301
- }))
302
- };
303
- const explicitFormat = config.output?.format;
304
- const rendered = await Effect.runPromise(renderReport([report], {
305
- ...explicitFormat !== void 0 ? { explicitFormat } : {},
306
- noColor: process.env.NO_COLOR !== void 0 || !process.stdout.isTTY
307
- }).pipe(Effect.provide(ReportPipelineLive)));
308
- const writeOutput = options.writeOutput ?? ((o) => process.stdout.write(`${o.content}\n`));
309
- for (const output of rendered) writeOutput(output);
317
+ await renderAndWrite();
310
318
  }
311
319
 
312
320
  //#endregion