@savvy-web/bundler 2.0.9 → 2.0.11

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.
Files changed (3) hide show
  1. package/index.d.ts +5 -0
  2. package/package.json +2 -2
  3. package/run.js +174 -152
package/index.d.ts CHANGED
@@ -263,6 +263,11 @@ interface RunOptions {
263
263
  target: "dev" | "prod";
264
264
  reports: ReadonlyArray<BuildReport>;
265
265
  now?: () => Date;
266
+ buildOk?: boolean | undefined;
267
+ failure?: {
268
+ name?: string | undefined;
269
+ message: string;
270
+ } | undefined;
266
271
  }) => string | undefined;
267
272
  /** Injectable ambient-.d.ts copier (defaults to copyAmbientDts). */
268
273
  readonly copyAmbientDts?: ((o: CopyAmbientDtsOptions) => void) | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/bundler",
3
- "version": "2.0.9",
3
+ "version": "2.0.11",
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,7 +29,7 @@
29
29
  "./package.json": "./package.json"
30
30
  },
31
31
  "dependencies": {
32
- "@savvy-web/tsdown-plugins": "2.1.8",
32
+ "@savvy-web/tsdown-plugins": "2.2.0",
33
33
  "@tsdown/exe": "^0.22.12",
34
34
  "effect": "4.0.0-beta.101",
35
35
  "rolldown": "^1.2.0",
package/run.js CHANGED
@@ -5,6 +5,23 @@ import { dirname, join } from "node:path";
5
5
  import { Effect } from "effect";
6
6
 
7
7
  //#region src/run.ts
8
+ /** Reduce a thrown value to the `{ name?, message }` shape the issues artifact stamps on a failed build. */
9
+ function describeFailure(err) {
10
+ if (err instanceof Error) return err.name !== "" ? {
11
+ name: err.name,
12
+ message: err.message
13
+ } : { message: err.message };
14
+ if (typeof err === "object" && err !== null) {
15
+ const rec = err;
16
+ const name = typeof rec._tag === "string" ? rec._tag : typeof rec.name === "string" ? rec.name : void 0;
17
+ const message = typeof rec.message === "string" ? rec.message : String(err);
18
+ return name !== void 0 ? {
19
+ name,
20
+ message
21
+ } : { message };
22
+ }
23
+ return { message: String(err) };
24
+ }
8
25
  /** Read and parse package.json at cwd, returning an empty object on any error. */
9
26
  function readPackageJson(cwd) {
10
27
  try {
@@ -56,164 +73,167 @@ async function runBuild(config, options) {
56
73
  const version = options.readVersion ? options.readVersion() : pkg.version ?? "0.0.0";
57
74
  const packageName = options.readPackageName ? options.readPackageName() : pkg.name ?? "unknown";
58
75
  const collector = new BuildCollector();
59
- const jsx = resolveJsxConfig((options.readTsconfigJsx ?? (() => readTsconfigJsx(cwd)))(), config.jsx);
60
- const tsconfigPath = (options.writeTsconfig ?? ((c) => writeResolvedTsconfig({
61
- cwd: c,
62
- ...jsx?.runtime === "automatic" ? {
63
- jsx: "react-jsx",
64
- jsxImportSource: jsx.importSource
65
- } : {},
66
- ...jsx?.runtime === "classic" ? { jsx: "react" } : {}
67
- })))(cwd);
68
- const exportsMap = options.readExports ? options.readExports() : pkg.exports;
69
- const publishTargets = (options.readPublishTargets ?? (() => {
70
- const declared = pkg.publishConfig?.targets;
71
- return declared !== void 0 && !Array.isArray(declared) && typeof declared === "object" ? declared : void 0;
72
- }))();
73
- const writeBinding = options.writeTargetsBinding ?? writeTargetsBinding;
74
- const osCpuForValidate = options.readOsCpu ? options.readOsCpu() : {
75
- os: pkg.os ?? [],
76
- cpu: pkg.cpu ?? []
77
- };
78
- await Effect.runPromise(Effect.flatMap(ConfigValidator, (v) => v.validate({
79
- baseName: packageName,
80
- hasExports: exportsMap !== void 0 && Object.keys(exportsMap).length > 0,
81
- ...publishTargets !== void 0 ? { targets: publishTargets } : {},
82
- ...config.exe !== void 0 ? { exe: config.exe } : {},
83
- osCpu: osCpuForValidate,
84
- ...config.meta !== void 0 && config.meta !== false ? { meta: config.meta } : {},
85
- ...config.looseFiles !== void 0 ? { looseFiles: config.looseFiles } : {}
86
- })).pipe(Effect.provide(ConfigValidatorLive)));
87
- const exeSpecs = config.exe !== void 0 ? normalizeExeOptions(config.exe, osCpuForValidate) : [];
88
- if (config.exe !== void 0 && (exeSpecs.length !== 1 || (exeSpecs[0]?.targets.length ?? 0) !== 1)) throw new Error(`exe build requires exactly one binary with one target (got ${exeSpecs.length} spec(s), ${exeSpecs[0]?.targets.length ?? 0} target(s) on the first). A package's exports["."] resolves to a single SEA — cross-platform binaries must each ship as their own per-platform package.`);
89
- const exeSpec = exeSpecs[0];
90
- const exeTarget = exeSpec?.targets[0];
91
- const exeFileName = exeSpec && exeTarget ? computeExeFileName(exeSpec.fileName, exeTarget) : void 0;
92
- const exeEntrySource = exeSpec?.entry ?? "./src/bin.ts";
93
- const exeRewrite = config.exe !== void 0 && exeFileName !== void 0 ? {
94
- source: exeEntrySource,
95
- fileName: exeFileName,
96
- dir: "bin"
97
- } : void 0;
98
- const entries = packageJsonEntries({
99
- pkg: {
100
- exports: exportsMap ?? pkg.exports,
101
- bin: pkg.bin
102
- },
103
- ...config.exe !== void 0 ? { excludeSources: [exeEntrySource] } : {}
104
- });
105
- const hasJsEntries = Object.keys(entries).length > 0;
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
- });
116
- if (target === "meta") {
117
- (options.writeOutput ?? ((o) => process.stdout.write(`${o.content}\n`)))({
118
- target: "stdout",
119
- contentType: "text/plain",
120
- content: `meta: --target meta is deprecated and now a no-op; meta is emitted by --target prod (${packageName}).`
121
- });
122
- return;
123
- }
124
- if (target === "exe") {
125
- if (config.exe === void 0) throw new Error("`savvy build --target exe` requires an `exe` option in the build config");
126
- await (options.runExeBuild ?? runExeBuild)({
127
- cwd,
128
- outDir: join(cwd, "dist", "dev", "pkg", "bin"),
129
- specs: exeSpecs,
130
- collector,
131
- groupId: "dev",
132
- verbose
133
- });
134
- (options.writeOutput ?? ((o) => process.stdout.write(`${o.content}\n`)))({
135
- target: "stdout",
136
- contentType: "text/plain",
137
- content: `exe: compiled ${exeSpecs.length} binary/binaries for ${packageName}`
138
- });
139
- return;
140
- }
141
- let overridePartitions = [];
142
- let baseEntries = entries;
143
- let dualExports;
144
- const subdirExports = /* @__PURE__ */ new Set();
145
- if (config.overrides) {
146
- const partitions = [];
147
- const overriddenEntryNames = /* @__PURE__ */ new Set();
148
- const baseFormatHasCjs = (config.format ?? ["esm"]).includes("cjs");
149
- const dualExportKeys = /* @__PURE__ */ new Set();
150
- const exportPathByEntry = deriveExportPaths(entries, exportsMap);
151
- for (const ov of config.overrides) {
152
- if (ov.outSubdir !== void 0 && ov.entries.length !== 1) throw new Error(`overrides: outSubdir "${ov.outSubdir}" must pin exactly one export path (got ${ov.entries.length})`);
153
- const partEntry = {};
154
- for (const exportPath of ov.entries) {
155
- if (exportPath !== "." && !exportPath.startsWith("./")) throw new Error(`overrides: entry "${exportPath}" must be a canonical export path — use "." for the root or a "./"-prefixed subpath (e.g. "./changesets/markdownlint")`);
156
- const flatName = createEntryName(exportPath, false);
157
- const src = entries[flatName];
158
- if (src === void 0) throw new Error(`overrides: export path "${exportPath}" (entry "${flatName}") is not a build entry of ${packageName}`);
159
- const entryName = ov.outSubdir !== void 0 ? "index" : flatName;
160
- partEntry[entryName] = src;
161
- overriddenEntryNames.add(flatName);
162
- if (ov.outSubdir !== void 0) subdirExports.add(exportPath);
163
- if ((ov.format ?? config.format ?? ["esm"]).includes("cjs")) dualExportKeys.add(exportPath);
164
- }
165
- partitions.push({
166
- entry: partEntry,
167
- ...ov.format !== void 0 ? { format: ov.format } : {},
168
- ...ov.externals !== void 0 ? { externals: ov.externals } : {},
169
- ...ov.bundle !== void 0 ? { bundle: ov.bundle } : {},
170
- ...ov.bundleNodeModules !== void 0 ? { bundleNodeModules: ov.bundleNodeModules } : {},
171
- ...ov.bundledPackages !== void 0 ? { bundledPackages: ov.bundledPackages } : {},
172
- ...ov.dtsExternals !== void 0 ? { dtsExternals: ov.dtsExternals } : {},
173
- ...ov.platform !== void 0 ? { platform: ov.platform } : {},
174
- ...ov.css !== void 0 ? { css: ov.css } : {},
175
- ...ov.outSubdir !== void 0 ? { outSubdir: ov.outSubdir } : {}
176
- });
177
- }
178
- const onlyBase = {};
179
- for (const [name, src] of Object.entries(entries)) {
180
- if (overriddenEntryNames.has(name)) continue;
181
- onlyBase[name] = src;
182
- if (baseFormatHasCjs) dualExportKeys.add(exportPathByEntry[name] ?? (name === "index" ? "." : `./${name}`));
183
- }
184
- overridePartitions = partitions;
185
- baseEntries = onlyBase;
186
- dualExports = dualExportKeys;
187
- }
188
- const looseFiles = config.looseFiles !== void 0 ? normalizeLooseFiles(config.looseFiles) : void 0;
189
- const { groups, resolution } = target === "dev" ? {
190
- groups: [{
191
- id: "dev",
192
- name: packageName
193
- }],
194
- resolution: void 0
195
- } : deriveProdGroups(publishTargets, packageName);
196
- const explicitFormat = config.output?.format;
197
- const writeOutput = options.writeOutput ?? ((o) => process.stdout.write(`${o.content}\n`));
198
- const renderAndWrite = async () => {
199
- const rendered = await Effect.runPromise(renderReport(collector.snapshot(packageName), {
200
- ...explicitFormat !== void 0 ? { explicitFormat } : {},
201
- verbose,
202
- noColor: process.env.NO_COLOR !== void 0 || !process.stdout.isTTY
203
- }).pipe(Effect.provide(ReportPipelineLive)));
204
- for (const output of rendered) writeOutput(output);
205
- };
206
- const writeIssuesBestEffort = () => {
76
+ const writeIssuesBestEffort = (err) => {
207
77
  if (target !== "dev" && target !== "prod") return;
208
78
  try {
209
79
  (options.writeIssues ?? writeIssuesArtifact)({
210
80
  cwd,
211
81
  target,
212
- reports: collector.snapshot(packageName)
82
+ reports: collector.snapshot(packageName),
83
+ buildOk: err === void 0,
84
+ ...err !== void 0 ? { failure: describeFailure(err) } : {}
213
85
  });
214
86
  } catch {}
215
87
  };
88
+ let renderAndWrite;
216
89
  try {
90
+ const jsx = resolveJsxConfig((options.readTsconfigJsx ?? (() => readTsconfigJsx(cwd)))(), config.jsx);
91
+ const tsconfigPath = (options.writeTsconfig ?? ((c) => writeResolvedTsconfig({
92
+ cwd: c,
93
+ ...jsx?.runtime === "automatic" ? {
94
+ jsx: "react-jsx",
95
+ jsxImportSource: jsx.importSource
96
+ } : {},
97
+ ...jsx?.runtime === "classic" ? { jsx: "react" } : {}
98
+ })))(cwd);
99
+ const exportsMap = options.readExports ? options.readExports() : pkg.exports;
100
+ const publishTargets = (options.readPublishTargets ?? (() => {
101
+ const declared = pkg.publishConfig?.targets;
102
+ return declared !== void 0 && !Array.isArray(declared) && typeof declared === "object" ? declared : void 0;
103
+ }))();
104
+ const writeBinding = options.writeTargetsBinding ?? writeTargetsBinding;
105
+ const osCpuForValidate = options.readOsCpu ? options.readOsCpu() : {
106
+ os: pkg.os ?? [],
107
+ cpu: pkg.cpu ?? []
108
+ };
109
+ await Effect.runPromise(Effect.flatMap(ConfigValidator, (v) => v.validate({
110
+ baseName: packageName,
111
+ hasExports: exportsMap !== void 0 && Object.keys(exportsMap).length > 0,
112
+ ...publishTargets !== void 0 ? { targets: publishTargets } : {},
113
+ ...config.exe !== void 0 ? { exe: config.exe } : {},
114
+ osCpu: osCpuForValidate,
115
+ ...config.meta !== void 0 && config.meta !== false ? { meta: config.meta } : {},
116
+ ...config.looseFiles !== void 0 ? { looseFiles: config.looseFiles } : {}
117
+ })).pipe(Effect.provide(ConfigValidatorLive)));
118
+ const exeSpecs = config.exe !== void 0 ? normalizeExeOptions(config.exe, osCpuForValidate) : [];
119
+ if (config.exe !== void 0 && (exeSpecs.length !== 1 || (exeSpecs[0]?.targets.length ?? 0) !== 1)) throw new Error(`exe build requires exactly one binary with one target (got ${exeSpecs.length} spec(s), ${exeSpecs[0]?.targets.length ?? 0} target(s) on the first). A package's exports["."] resolves to a single SEA — cross-platform binaries must each ship as their own per-platform package.`);
120
+ const exeSpec = exeSpecs[0];
121
+ const exeTarget = exeSpec?.targets[0];
122
+ const exeFileName = exeSpec && exeTarget ? computeExeFileName(exeSpec.fileName, exeTarget) : void 0;
123
+ const exeEntrySource = exeSpec?.entry ?? "./src/bin.ts";
124
+ const exeRewrite = config.exe !== void 0 && exeFileName !== void 0 ? {
125
+ source: exeEntrySource,
126
+ fileName: exeFileName,
127
+ dir: "bin"
128
+ } : void 0;
129
+ const entries = packageJsonEntries({
130
+ pkg: {
131
+ exports: exportsMap ?? pkg.exports,
132
+ bin: pkg.bin
133
+ },
134
+ ...config.exe !== void 0 ? { excludeSources: [exeEntrySource] } : {}
135
+ });
136
+ const hasJsEntries = Object.keys(entries).length > 0;
137
+ validateSubdirOverrides(config.overrides, entries, packageName);
138
+ const ambient = extractAmbientDts({
139
+ exports: exportsMap ?? pkg.exports,
140
+ bin: pkg.bin
141
+ }, {});
142
+ assertNoEntryCollisions(Object.keys(entries), ambient);
143
+ if (ambient.length > 0 && !hasJsEntries && config.exe === void 0) throw new ConfigValidationError({
144
+ path: "exports",
145
+ 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"
146
+ });
147
+ if (target === "meta") {
148
+ (options.writeOutput ?? ((o) => process.stdout.write(`${o.content}\n`)))({
149
+ target: "stdout",
150
+ contentType: "text/plain",
151
+ content: `meta: --target meta is deprecated and now a no-op; meta is emitted by --target prod (${packageName}).`
152
+ });
153
+ return;
154
+ }
155
+ if (target === "exe") {
156
+ if (config.exe === void 0) throw new Error("`savvy build --target exe` requires an `exe` option in the build config");
157
+ await (options.runExeBuild ?? runExeBuild)({
158
+ cwd,
159
+ outDir: join(cwd, "dist", "dev", "pkg", "bin"),
160
+ specs: exeSpecs,
161
+ collector,
162
+ groupId: "dev",
163
+ verbose
164
+ });
165
+ (options.writeOutput ?? ((o) => process.stdout.write(`${o.content}\n`)))({
166
+ target: "stdout",
167
+ contentType: "text/plain",
168
+ content: `exe: compiled ${exeSpecs.length} binary/binaries for ${packageName}`
169
+ });
170
+ return;
171
+ }
172
+ let overridePartitions = [];
173
+ let baseEntries = entries;
174
+ let dualExports;
175
+ const subdirExports = /* @__PURE__ */ new Set();
176
+ if (config.overrides) {
177
+ const partitions = [];
178
+ const overriddenEntryNames = /* @__PURE__ */ new Set();
179
+ const baseFormatHasCjs = (config.format ?? ["esm"]).includes("cjs");
180
+ const dualExportKeys = /* @__PURE__ */ new Set();
181
+ const exportPathByEntry = deriveExportPaths(entries, exportsMap);
182
+ for (const ov of config.overrides) {
183
+ if (ov.outSubdir !== void 0 && ov.entries.length !== 1) throw new Error(`overrides: outSubdir "${ov.outSubdir}" must pin exactly one export path (got ${ov.entries.length})`);
184
+ const partEntry = {};
185
+ for (const exportPath of ov.entries) {
186
+ if (exportPath !== "." && !exportPath.startsWith("./")) throw new Error(`overrides: entry "${exportPath}" must be a canonical export path — use "." for the root or a "./"-prefixed subpath (e.g. "./changesets/markdownlint")`);
187
+ const flatName = createEntryName(exportPath, false);
188
+ const src = entries[flatName];
189
+ if (src === void 0) throw new Error(`overrides: export path "${exportPath}" (entry "${flatName}") is not a build entry of ${packageName}`);
190
+ const entryName = ov.outSubdir !== void 0 ? "index" : flatName;
191
+ partEntry[entryName] = src;
192
+ overriddenEntryNames.add(flatName);
193
+ if (ov.outSubdir !== void 0) subdirExports.add(exportPath);
194
+ if ((ov.format ?? config.format ?? ["esm"]).includes("cjs")) dualExportKeys.add(exportPath);
195
+ }
196
+ partitions.push({
197
+ entry: partEntry,
198
+ ...ov.format !== void 0 ? { format: ov.format } : {},
199
+ ...ov.externals !== void 0 ? { externals: ov.externals } : {},
200
+ ...ov.bundle !== void 0 ? { bundle: ov.bundle } : {},
201
+ ...ov.bundleNodeModules !== void 0 ? { bundleNodeModules: ov.bundleNodeModules } : {},
202
+ ...ov.bundledPackages !== void 0 ? { bundledPackages: ov.bundledPackages } : {},
203
+ ...ov.dtsExternals !== void 0 ? { dtsExternals: ov.dtsExternals } : {},
204
+ ...ov.platform !== void 0 ? { platform: ov.platform } : {},
205
+ ...ov.css !== void 0 ? { css: ov.css } : {},
206
+ ...ov.outSubdir !== void 0 ? { outSubdir: ov.outSubdir } : {}
207
+ });
208
+ }
209
+ const onlyBase = {};
210
+ for (const [name, src] of Object.entries(entries)) {
211
+ if (overriddenEntryNames.has(name)) continue;
212
+ onlyBase[name] = src;
213
+ if (baseFormatHasCjs) dualExportKeys.add(exportPathByEntry[name] ?? (name === "index" ? "." : `./${name}`));
214
+ }
215
+ overridePartitions = partitions;
216
+ baseEntries = onlyBase;
217
+ dualExports = dualExportKeys;
218
+ }
219
+ const looseFiles = config.looseFiles !== void 0 ? normalizeLooseFiles(config.looseFiles) : void 0;
220
+ const { groups, resolution } = target === "dev" ? {
221
+ groups: [{
222
+ id: "dev",
223
+ name: packageName
224
+ }],
225
+ resolution: void 0
226
+ } : deriveProdGroups(publishTargets, packageName);
227
+ const explicitFormat = config.output?.format;
228
+ const writeOutput = options.writeOutput ?? ((o) => process.stdout.write(`${o.content}\n`));
229
+ renderAndWrite = async () => {
230
+ const rendered = await Effect.runPromise(renderReport(collector.snapshot(packageName), {
231
+ ...explicitFormat !== void 0 ? { explicitFormat } : {},
232
+ verbose,
233
+ noColor: process.env.NO_COLOR !== void 0 || !process.stdout.isTTY
234
+ }).pipe(Effect.provide(ReportPipelineLive)));
235
+ for (const output of rendered) writeOutput(output);
236
+ };
217
237
  if (hasJsEntries || config.exe === void 0) await build({
218
238
  cwd,
219
239
  version,
@@ -306,12 +326,14 @@ async function runBuild(config, options) {
306
326
  }
307
327
  }
308
328
  } catch (err) {
309
- await renderAndWrite();
310
- writeIssuesBestEffort();
329
+ writeIssuesBestEffort(err);
330
+ if (renderAndWrite !== void 0) try {
331
+ await renderAndWrite();
332
+ } catch {}
311
333
  throw err;
312
334
  }
313
- await renderAndWrite();
314
335
  writeIssuesBestEffort();
336
+ if (renderAndWrite !== void 0) await renderAndWrite();
315
337
  }
316
338
  /**
317
339
  * Sugar front door: define + run in one call, deriving `cwd`/`argv` from process globals.