tonnage 0.1.3 → 0.2.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/AGENTS.md CHANGED
@@ -8,3 +8,5 @@ CI-first runtime JavaScript bundle measurement with checked-in README reports.
8
8
  - Peer dependencies stay external by default. Use explicit `external` entries only for additional dependencies intentionally supplied by consumers.
9
9
  - Run `tonnage write` to update a generated README section and `tonnage check` in CI to detect drift.
10
10
  - Keep marker names stable and use the `tonnage:<name>:start` and `tonnage:<name>:end` comment forms.
11
+ - For a galley.md document, import `Tonnage` from `tonnage/galley`. Its config paths default to the containing document's directory. The component measures and renders a report without writing a README or requiring section markers.
12
+ - Await `renderTonnageMarkdown(report, config)` when using the programmatic API. CLI reports and document components share Galley's table rendering, escaping literal text and aligning byte columns to the right.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # tonnage
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 0521e43: Render reports through galley.md 0.1.1 native table elements and expose a Tonnage component from tonnage/galley for MDX documents. Galley is now a runtime dependency, renderTonnageMarkdown returns a Promise and must be awaited, and inline code and table text use Galley's Markdown escaping and column alignment.
8
+
3
9
  ## 0.1.3
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -15,11 +15,43 @@ per bundle.
15
15
  Add the generated-section comments to the target README, then create a config
16
16
  beside the package manifest. The complete configuration exhibit is in
17
17
  [examples/tonnage.config.ts](./examples/tonnage.config.ts).
18
+
19
+ ```ts
20
+ import { defineConfig, type TonnageConfig } from "tonnage"
21
+
22
+ // @exhibit-region start config
23
+ const config: TonnageConfig = defineConfig({
24
+ exports: {
25
+ exclude: [`./package.json`],
26
+ },
27
+ marker: `my-package`,
28
+ recipes: [
29
+ {
30
+ entry: `examples/recipes/react-app.ts`,
31
+ name: `React app`,
32
+ platform: `browser`,
33
+ },
34
+ ],
35
+ })
36
+
37
+ export default config
38
+ // @exhibit-region end config
39
+
40
+ ```
41
+
18
42
  Each recipe points to a reviewable entry file such as
19
43
  [examples/recipes/react-app.ts](./examples/recipes/react-app.ts). Use named
20
44
  imports in these files to represent the runtime surface a real application uses;
21
45
  unused package exports are tree-shaken.
22
46
 
47
+ ```ts
48
+ // @exhibit-region start recipe
49
+ export { atom } from "my-package"
50
+ export { useO } from "my-package/react"
51
+ // @exhibit-region end recipe
52
+
53
+ ```
54
+
23
55
  The default generated-section marker is `default`; the example uses `my-package`.
24
56
  The opening and closing comments use the forms
25
57
  `tonnage:my-package:start` and `tonnage:my-package:end`.
@@ -81,6 +113,23 @@ JavaScript bytes, with declarations, source maps, CSS, and other assets excluded
81
113
  Peer dependencies remain external, and shared modules are counted once in each
82
114
  bundle.
83
115
 
116
+ Reports use Galley's native table elements, with right-aligned byte columns and
117
+ literal escaping for import names, entry paths, and recipe labels.
118
+
119
+ For programmatic use, await both measurement and Markdown rendering:
120
+
121
+ ```ts
122
+ import {
123
+ createTonnageReport,
124
+ defineConfig,
125
+ renderTonnageMarkdown,
126
+ } from "tonnage"
127
+
128
+ const config = defineConfig({ platform: "node" })
129
+ const report = await createTonnageReport(config, process.cwd())
130
+ const markdown = await renderTonnageMarkdown(report, config)
131
+ ```
132
+
84
133
  ## Package resolution and platforms
85
134
 
86
135
  Tonnage resolves the package's own public import name from the manifest selected
@@ -98,3 +147,32 @@ commonly fall through to their `default` target. Set `platform` to `browser` or
98
147
  `node` when that runtime is what you want to measure; those choices retain
99
148
  esbuild's native export conditions and main-field behavior. A recipe's
100
149
  `platform` overrides the report-wide setting for that recipe.
150
+
151
+ ## MDX documents
152
+
153
+ Use [galley.md](https://www.npmjs.com/package/galley.md) to compose a README from source examples and reports:
154
+
155
+ ```mdx
156
+ import { Tonnage } from "tonnage/galley"
157
+ import config from "./tonnage.config.ts"
158
+ import Example from "./examples/recipes/react-app.ts" with { type: "code", lang: "ts" }
159
+
160
+ <Example />
161
+
162
+ <Tonnage config={config} />
163
+ ```
164
+
165
+ The component resolves paths relative to the containing document. Pass `directory` to measure another package. It produces the same tables as the CLI without writing a README or requiring generated-section markers. Tonnage includes Galley as a runtime dependency; add `galley.md` directly to your project's development dependencies to run `galley write` and `galley check` when authoring documents.
166
+
167
+ ## Bundle size
168
+
169
+ Package export sizes include complete runtime export surfaces.
170
+
171
+ Sizes are exact minified and level-9 gzip JavaScript byte counts. Declarations, source maps, CSS, and other assets are excluded. Peer dependencies stay external, and shared modules are counted once per bundle.
172
+
173
+ | Import | Minified JS | Gzip JS |
174
+ | ---------------- | ----------: | --------: |
175
+ | `tonnage` | 491,915 B | 153,965 B |
176
+ | `tonnage/galley` | 484,581 B | 151,561 B |
177
+
178
+ Report maintained with [tonnage](https://github.com/jeremybanka/tonnage).
package/README.mdx ADDED
@@ -0,0 +1,143 @@
1
+ import { Tonnage } from "tonnage/galley"
2
+ import Configuration from "./examples/tonnage.config.ts" with { type: "code", lang: "ts" }
3
+ import Recipe from "./examples/recipes/react-app.ts" with { type: "code", lang: "ts" }
4
+
5
+ # tonnage
6
+
7
+ Keep exact bundle size tables in a package README and fail CI when the checked-in
8
+ numbers drift.
9
+
10
+ The tool bundles real public package imports with esbuild, minifies the result,
11
+ and reports JavaScript bytes before and after level-9 gzip. Public-module rows
12
+ retain every runtime export; recipe entry files can select named exports and
13
+ tree-shake the rest. Declarations, source maps, CSS, and other assets are not
14
+ counted. Peer dependencies stay external, and shared modules are counted once
15
+ per bundle.
16
+
17
+ ## Configure
18
+
19
+ Add the generated-section comments to the target README, then create a config
20
+ beside the package manifest. The complete configuration exhibit is in
21
+ [examples/tonnage.config.ts](./examples/tonnage.config.ts).
22
+ <Configuration />
23
+
24
+ Each recipe points to a reviewable entry file such as
25
+ [examples/recipes/react-app.ts](./examples/recipes/react-app.ts). Use named
26
+ imports in these files to represent the runtime surface a real application uses;
27
+ unused package exports are tree-shaken.
28
+
29
+ <Recipe />
30
+
31
+ The default generated-section marker is `default`; the example uses `my-package`.
32
+ The opening and closing comments use the forms
33
+ `tonnage:my-package:start` and `tonnage:my-package:end`.
34
+
35
+ ## Run
36
+
37
+ Run `tonnage write` during development to update the README. Run
38
+ `tonnage check` in CI to recompute the same report without writing and exit
39
+ nonzero when the README is stale. Run `tonnage --help` to see both commands and
40
+ their optional config-path arguments.
41
+
42
+ Pass a config path with `tonnage check path/to/tonnage.config.ts` or
43
+ `tonnage write path/to/tonnage.config.ts`. Otherwise, Tonnage looks in the current
44
+ directory for `tonnage.config.ts`, then `tonnage.config.mjs`, then
45
+ `tonnage.config.js`. Config modules may use top-level `await`. Missing configs
46
+ are errors. Use `--` before a config path that starts with a dash.
47
+
48
+ Unknown options are ignored with warnings on stderr. Warnings do not change the
49
+ exit status; `check` still exits nonzero when the report is stale.
50
+
51
+ By default the tool reads `package.json`, writes `README.md`, and measures every
52
+ public export except `./package.json`. Paths are resolved relative to the config
53
+ file. Use `exports.include` or `exports.exclude` to select subpaths, recipe entry
54
+ files to describe realistic runtime boundaries, and `external` for non-peer
55
+ imports that the package intentionally leaves to its consumers.
56
+
57
+ ### Shell completion
58
+
59
+ With `tonnage` available on your shell's `PATH`, install completion for your shell:
60
+
61
+ ```sh
62
+ tonnage completion install bash
63
+ tonnage completion install zsh
64
+ tonnage completion install fish
65
+ tonnage completion install nushell
66
+ tonnage completion install carapace
67
+ ```
68
+
69
+ Choose the integration you use. Completion suggests commands, help options, and
70
+ config paths without loading your config or measuring bundles. Installation
71
+ checks the shell's prerequisites and writes to its completion directory without
72
+ editing shell profiles. It reports any setup needed for your shell. If Nushell
73
+ already uses Carapace, install only the Carapace integration.
74
+
75
+ Run `tonnage completion bash` (or another target above) to print the integration
76
+ file without installing it. The `completion` command and comline's internal
77
+ completion protocol commands are reserved for shell integration.
78
+
79
+ ## Report layout
80
+
81
+ Generated reports adapt to the measurements they contain. An exports-only or
82
+ recipes-only report puts its table directly below the configured report heading
83
+ and describes only that type of measurement. A report with both types labels the
84
+ tables `Package exports` and `Usage examples` so readers can distinguish complete
85
+ runtime export surfaces from tree-shaken recipe bundles.
86
+
87
+ Every layout states the exact measurement semantics: minified and level-9 gzip
88
+ JavaScript bytes, with declarations, source maps, CSS, and other assets excluded.
89
+ Peer dependencies remain external, and shared modules are counted once in each
90
+ bundle.
91
+
92
+ Reports use Galley's native table elements, with right-aligned byte columns and
93
+ literal escaping for import names, entry paths, and recipe labels.
94
+
95
+ For programmatic use, await both measurement and Markdown rendering:
96
+
97
+ ```ts
98
+ import {
99
+ createTonnageReport,
100
+ defineConfig,
101
+ renderTonnageMarkdown,
102
+ } from "tonnage"
103
+
104
+ const config = defineConfig({ platform: "node" })
105
+ const report = await createTonnageReport(config, process.cwd())
106
+ const markdown = await renderTonnageMarkdown(report, config)
107
+ ```
108
+
109
+ ## Package resolution and platforms
110
+
111
+ Tonnage resolves the package's own public import name from the manifest selected
112
+ by `packageJson`; a workspace package does not need to install or declare itself
113
+ as a dependency. Root and subpath imports still go through esbuild's package
114
+ resolver, so the manifest's `exports` restrictions and conditions apply. Runtime
115
+ dependencies resolve from the measured package, while its peer dependencies stay
116
+ external.
117
+
118
+ The default `platform` is `neutral`. For packages without `exports`, Tonnage
119
+ configures neutral resolution to try the `module` field and then `main`, so
120
+ main-only packages and dependencies are actionable by default. With conditional
121
+ `exports`, neutral does not enable the `browser` or `node` condition, so exports
122
+ commonly fall through to their `default` target. Set `platform` to `browser` or
123
+ `node` when that runtime is what you want to measure; those choices retain
124
+ esbuild's native export conditions and main-field behavior. A recipe's
125
+ `platform` overrides the report-wide setting for that recipe.
126
+
127
+ ## MDX documents
128
+
129
+ Use [galley.md](https://www.npmjs.com/package/galley.md) to compose a README from source examples and reports:
130
+
131
+ ```mdx
132
+ import { Tonnage } from "tonnage/galley"
133
+ import config from "./tonnage.config.ts"
134
+ import Example from "./examples/recipes/react-app.ts" with { type: "code", lang: "ts" }
135
+
136
+ <Example />
137
+
138
+ <Tonnage config={config} />
139
+ ```
140
+
141
+ The component resolves paths relative to the containing document. Pass `directory` to measure another package. It produces the same tables as the CLI without writing a README or requiring generated-section markers. Tonnage includes Galley as a runtime dependency; add `galley.md` directly to your project's development dependencies to run `galley write` and `galley check` when authoring documents.
142
+
143
+ <Tonnage config={{ platform: "node", exports: { include: [".", "./galley"] } }} />
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as runTonnage } from "./run-CZ_FO4C7.mjs";
2
+ import { t as runTonnage } from "./run-BJN1MXRf.mjs";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { pathToFileURL } from "node:url";
@@ -0,0 +1,12 @@
1
+ import { r as TonnageConfig } from "./types-Ddtp95Z3.mjs";
2
+ import { DocumentContext, Element } from "galley.md";
3
+ //#region src/galley.d.ts
4
+ export type TonnageProps = {
5
+ config?: TonnageConfig;
6
+ /** Override the document directory when measuring a different package. */
7
+ directory?: string;
8
+ };
9
+ /** Measure bundles and include the report in a galley.md document. */
10
+ export declare function Tonnage({ config, directory }: TonnageProps, context: DocumentContext): Promise<Element[]>;
11
+ //#endregion
12
+ //# sourceMappingURL=galley.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"galley.d.mts","names":[],"sources":["../src/galley.ts"],"mappings":";;;YAQY;EACX,SAAS;;EAET;;;wBAIqB,UACnB,QAAa,aAAa,cAC5B,SAAS,kBACP,QAAQ"}
@@ -0,0 +1,12 @@
1
+ import { r as reportContent, t as createTonnageReport } from "./report-B3HrJ4UQ.mjs";
2
+ import path from "node:path";
3
+ //#region src/galley.ts
4
+ /** Measure bundles and include the report in a galley.md document. */
5
+ async function Tonnage({ config = {}, directory }, context) {
6
+ const report = await createTonnageReport(config, path.resolve(context.directory, directory ?? "."));
7
+ return reportContent(report, config);
8
+ }
9
+ //#endregion
10
+ export { Tonnage };
11
+
12
+ //# sourceMappingURL=galley.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"galley.mjs","names":[],"sources":["../src/galley.ts"],"sourcesContent":["import path from \"node:path\"\n\nimport type { DocumentContext, Element } from \"galley.md\"\n\nimport { createTonnageReport } from \"./report.ts\"\nimport { reportContent } from \"./report-content.ts\"\nimport type { TonnageConfig } from \"./types.ts\"\n\nexport type TonnageProps = {\n\tconfig?: TonnageConfig\n\t/** Override the document directory when measuring a different package. */\n\tdirectory?: string\n}\n\n/** Measure bundles and include the report in a galley.md document. */\nexport async function Tonnage(\n\t{ config = {}, directory }: TonnageProps,\n\tcontext: DocumentContext,\n): Promise<Element[]> {\n\tconst report = await createTonnageReport(\n\t\tconfig,\n\t\tpath.resolve(context.directory, directory ?? \".\"),\n\t)\n\treturn reportContent(report, config)\n}\n"],"mappings":";;;;AAeA,eAAsB,QACrB,EAAE,SAAS,CAAC,GAAG,aACf,SACqB;CACrB,MAAM,SAAS,MAAM,oBACpB,QACA,KAAK,QAAQ,QAAQ,WAAW,aAAa,GAAG,CACjD;CACA,OAAO,cAAc,QAAQ,MAAM;AACpC"}
package/dist/index.d.mts CHANGED
@@ -1,51 +1,5 @@
1
+ import { a as TonnageMode, c as TonnageReport, i as TonnageExports, l as TonnageRow, n as BundlePlatform, o as TonnageRecipe, r as TonnageConfig, s as TonnageRecipeRow, t as BundleMeasurement, u as TonnageRunResult } from "./types-Ddtp95Z3.mjs";
1
2
  import "esbuild";
2
- //#region src/types.d.ts
3
- type BundlePlatform = `browser` | `neutral` | `node`;
4
- type TonnageExports = {
5
- exclude?: readonly string[];
6
- include?: readonly string[];
7
- };
8
- type TonnageRecipe = {
9
- entry: string;
10
- external?: readonly string[];
11
- name: string;
12
- platform?: BundlePlatform;
13
- };
14
- type TonnageConfig = {
15
- exports?: boolean | TonnageExports;
16
- external?: readonly string[];
17
- heading?: string;
18
- marker?: string;
19
- packageJson?: string;
20
- platform?: BundlePlatform;
21
- readme?: string;
22
- recipes?: readonly TonnageRecipe[];
23
- target?: string;
24
- };
25
- type BundleMeasurement = {
26
- gzipBytes: number;
27
- rawBytes: number;
28
- };
29
- type TonnageRow = BundleMeasurement & {
30
- imports: readonly string[];
31
- name: string;
32
- };
33
- type TonnageRecipeRow = BundleMeasurement & {
34
- entry: string;
35
- name: string;
36
- };
37
- type TonnageReport = {
38
- exports: readonly TonnageRow[];
39
- packageName: string;
40
- recipes: readonly TonnageRecipeRow[];
41
- };
42
- type TonnageMode = `check` | `write`;
43
- type TonnageRunResult = {
44
- changed: boolean;
45
- readmePath: string;
46
- report: TonnageReport;
47
- };
48
- //#endregion
49
3
  //#region src/define-config.d.ts
50
4
  export declare function defineConfig(config: TonnageConfig): TonnageConfig;
51
5
  //#endregion
@@ -69,7 +23,8 @@ export declare function updateGeneratedSection(readme: string, markdown: string,
69
23
  //#endregion
70
24
  //#region src/report.d.ts
71
25
  export declare function createTonnageReport(config: TonnageConfig, configDirectory: string): Promise<TonnageReport>;
72
- export declare function renderTonnageMarkdown(report: TonnageReport, config?: TonnageConfig): string;
26
+ /** Serialize a report with the same Galley elements used by the document component. */
27
+ export declare function renderTonnageMarkdown(report: TonnageReport, config?: TonnageConfig): Promise<string>;
73
28
  //#endregion
74
29
  //#region src/run.d.ts
75
30
  export declare function runTonnage(config: TonnageConfig, options: {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/define-config.ts","../src/measure.ts","../src/readme.ts","../src/report.ts","../src/run.ts"],"mappings":";;KAAY;KAEA;EACX;EACA;;KAGW;EACX;EACA;EACA;EACA,WAAW;;KAGA;EACX,oBAAoB;EACpB;EACA;EACA;EACA;EACA,WAAW;EACX;EACA,mBAAmB;EACnB;;KAGW;EACX;EACA;;KAGW,aAAa;EACxB;EACA;;KAGW,mBAAmB;EAC9B;EACA;;KAGW;EACX,kBAAkB;EAClB;EACA,kBAAkB;;KAGP;KAEA;EACX;EACA;EACA,QAAQ;;;;wBClDO,aAAa,QAAQ,gBAAgB;;;KCKzC;EACX;EACA,WAAW;EACX;EACA;;KAGW,sBAAsB;wBAMZ,aACrB,eACA,SAAS,sBACP,QAAQ;wBAcW,eACrB,4BACA,SAAS,wBACP,QAAQ;;;wBCxCK,eAAe;EAC9B;EACA;;wBASe,uBACf,gBACA,kBACA;;;wBCOqB,oBACrB,QAAQ,eACR,0BACE,QAAQ;wBA+DK,sBACf,QAAQ,eACR,SAAQ;;;wBClFa,WACrB,QAAQ,eACR;EACC;EACA,MAAM;IAEL,QAAQ"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/define-config.ts","../src/measure.ts","../src/readme.ts","../src/report.ts","../src/run.ts"],"mappings":";;;wBAEgB,aAAa,QAAQ,gBAAgB;;;KCKzC;EACX;EACA,WAAW;EACX;EACA;;KAGW,sBAAsB;wBAMZ,aACrB,eACA,SAAS,sBACP,QAAQ;wBAcW,eACrB,4BACA,SAAS,wBACP,QAAQ;;;wBCxCK,eAAe;EAC9B;EACA;;wBASe,uBACf,gBACA,kBACA;;;wBCUqB,oBACrB,QAAQ,eACR,0BACE,QAAQ;;wBAgEW,sBACrB,QAAQ,eACR,SAAQ,gBACN;;;wBCvFmB,WACrB,QAAQ,eACR;EACC;EACA,MAAM;IAEL,QAAQ"}
package/dist/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
- import { a as measureImports, i as measureEntry, n as createTonnageReport, o as tonnageMarkers, r as renderTonnageMarkdown, s as updateGeneratedSection, t as runTonnage } from "./run-CZ_FO4C7.mjs";
1
+ import { n as tonnageMarkers, r as updateGeneratedSection, t as runTonnage } from "./run-BJN1MXRf.mjs";
2
+ import { a as measureImports, i as measureEntry, n as renderTonnageMarkdown, t as createTonnageReport } from "./report-B3HrJ4UQ.mjs";
2
3
  //#region src/define-config.ts
3
4
  function defineConfig(config) {
4
5
  return config;
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/define-config.ts"],"sourcesContent":["import type { TonnageConfig } from \"./types.ts\"\n\nexport function defineConfig(config: TonnageConfig): TonnageConfig {\n\treturn config\n}\n"],"mappings":";;AAEA,SAAgB,aAAa,QAAsC;CAClE,OAAO;AACR"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/define-config.ts"],"sourcesContent":["import type { TonnageConfig } from \"./types.ts\"\n\nexport function defineConfig(config: TonnageConfig): TonnageConfig {\n\treturn config\n}\n"],"mappings":";;;AAEA,SAAgB,aAAa,QAAsC;CAClE,OAAO;AACR"}
@@ -1,30 +1,10 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { renderMarkdown } from "galley.md";
3
4
  import { gzipSync } from "node:zlib";
4
5
  import { build } from "esbuild";
5
6
  import os from "node:os";
6
- //#region src/readme.ts
7
- function tonnageMarkers(marker = `default`) {
8
- validateMarker(marker);
9
- return {
10
- end: `<!-- tonnage:${marker}:end -->`,
11
- start: `<!-- tonnage:${marker}:start -->`
12
- };
13
- }
14
- function updateGeneratedSection(readme, markdown, marker) {
15
- const { end, start } = tonnageMarkers(marker);
16
- const startIndex = readme.indexOf(start);
17
- const endIndex = readme.indexOf(end);
18
- if (startIndex === -1 || endIndex === -1) throw new Error(`README must contain exactly one ${start} and one ${end} marker.`);
19
- if (readme.slice(startIndex + start.length).includes(start) || readme.slice(endIndex + end.length).includes(end)) throw new Error(`README must contain exactly one ${start} and one ${end} marker.`);
20
- if (endIndex < startIndex) throw new Error(`${end} must appear after ${start}.`);
21
- const contentStart = startIndex + start.length;
22
- return `${readme.slice(0, contentStart)}\n\n${markdown.trim()}\n\n${readme.slice(endIndex)}`;
23
- }
24
- function validateMarker(marker) {
25
- if (!/^[a-z0-9][a-z0-9._-]*$/i.test(marker)) throw new Error(`Tonnage marker names may contain letters, numbers, dots, underscores, and hyphens.`);
26
- }
27
- //#endregion
7
+ import { jsx } from "galley.md/jsx-runtime";
28
8
  //#region src/measure.ts
29
9
  async function measureEntry(entry, options) {
30
10
  return measureEntryWithPlugins(entry, options);
@@ -107,6 +87,80 @@ function escapeRegExp(value) {
107
87
  return value.replace(/[.*+?^${}()|[\]\\]/g, `\\$&`);
108
88
  }
109
89
  //#endregion
90
+ //#region src/report-content.ts
91
+ /** Use Galley's JSX runtime directly so the source CLI stays executable by Node. */
92
+ function reportContent(report, config = {}) {
93
+ const hasExports = report.exports.length > 0;
94
+ const hasRecipes = report.recipes.length > 0;
95
+ const hasMultipleSections = hasExports && hasRecipes;
96
+ const content = [jsx("h2", { children: config.heading ?? "Bundle size" })];
97
+ if (hasExports) content.push(jsx("p", { children: "Package export sizes include complete runtime export surfaces." }));
98
+ if (hasRecipes) content.push(jsx("p", { children: "Usage example sizes bundle their entry files and tree-shake unused exports." }));
99
+ content.push(jsx("p", { children: "Sizes are exact minified and level-9 gzip JavaScript byte counts. Declarations, source maps, CSS, and other assets are excluded. Peer dependencies stay external, and shared modules are counted once per bundle." }));
100
+ if (hasExports) {
101
+ if (hasMultipleSections) content.push(jsx("h3", { children: "Package exports" }));
102
+ content.push(table([
103
+ { title: "Import" },
104
+ {
105
+ title: "Minified JS",
106
+ align: "right"
107
+ },
108
+ {
109
+ title: "Gzip JS",
110
+ align: "right"
111
+ }
112
+ ], report.exports.map((row) => [
113
+ code(row.name),
114
+ formatBytes(row.rawBytes),
115
+ formatBytes(row.gzipBytes)
116
+ ])));
117
+ }
118
+ if (hasRecipes) {
119
+ if (hasMultipleSections) content.push(jsx("h3", { children: "Usage examples" }));
120
+ content.push(table([
121
+ { title: "Recipe" },
122
+ { title: "Entry" },
123
+ {
124
+ title: "Minified JS",
125
+ align: "right"
126
+ },
127
+ {
128
+ title: "Gzip JS",
129
+ align: "right"
130
+ }
131
+ ], report.recipes.map((row) => [
132
+ row.name,
133
+ code(row.entry),
134
+ formatBytes(row.rawBytes),
135
+ formatBytes(row.gzipBytes)
136
+ ])));
137
+ }
138
+ content.push(jsx("p", { children: [
139
+ "Report maintained with ",
140
+ jsx("a", {
141
+ href: "https://github.com/jeremybanka/tonnage",
142
+ children: "tonnage"
143
+ }),
144
+ "."
145
+ ] }));
146
+ return content;
147
+ }
148
+ function code(value) {
149
+ return {
150
+ type: "inlineCode",
151
+ value
152
+ };
153
+ }
154
+ function formatBytes(bytes) {
155
+ return `${new Intl.NumberFormat("en-US").format(bytes)} B`;
156
+ }
157
+ function table(columns, rows) {
158
+ return jsx("table", { children: [jsx("thead", { children: jsx("tr", { children: columns.map(({ title, align }) => jsx("th", {
159
+ children: title,
160
+ align
161
+ })) }) }), jsx("tbody", { children: rows.map((cells) => jsx("tr", { children: cells.map((children) => jsx("td", { children })) })) })] });
162
+ }
163
+ //#endregion
110
164
  //#region src/report.ts
111
165
  async function createTonnageReport(config, configDirectory) {
112
166
  const packageJsonPath = path.resolve(configDirectory, config.packageJson ?? `package.json`);
@@ -147,56 +201,13 @@ async function createTonnageReport(config, configDirectory) {
147
201
  recipes: recipeRows
148
202
  };
149
203
  }
150
- function renderTonnageMarkdown(report, config = {}) {
151
- const hasExports = report.exports.length > 0;
152
- const hasRecipes = report.recipes.length > 0;
153
- const hasMultipleSections = hasExports && hasRecipes;
154
- const lines = [
155
- `## ${config.heading ?? `Bundle size`}`,
156
- ``,
157
- ...methodologyLines({
158
- hasExports,
159
- hasRecipes
160
- })
161
- ];
162
- if (hasExports) lines.push(``, ...hasMultipleSections ? [`### Package exports`, ``] : [], ...markdownTable([
163
- `Import`,
164
- `Minified JS`,
165
- `Gzip JS`
166
- ], report.exports.map((row) => [
167
- code(row.name),
168
- formatBytes(row.rawBytes),
169
- formatBytes(row.gzipBytes)
170
- ]), [
171
- `left`,
172
- `right`,
173
- `right`
174
- ]));
175
- if (hasRecipes) lines.push(``, ...hasMultipleSections ? [`### Usage examples`, ``] : [], ...markdownTable([
176
- `Recipe`,
177
- `Entry`,
178
- `Minified JS`,
179
- `Gzip JS`
180
- ], report.recipes.map((row) => [
181
- escapeCell(row.name),
182
- code(row.entry),
183
- formatBytes(row.rawBytes),
184
- formatBytes(row.gzipBytes)
185
- ]), [
186
- `left`,
187
- `left`,
188
- `right`,
189
- `right`
190
- ]));
191
- lines.push(``, `Report maintained with [tonnage](https://github.com/jeremybanka/tonnage).`);
192
- return lines.join(`\n`);
193
- }
194
- function methodologyLines({ hasExports, hasRecipes }) {
195
- const lines = [];
196
- if (hasExports) lines.push(`Package export sizes include complete runtime export surfaces.`);
197
- if (hasRecipes) lines.push(`Usage example sizes bundle their entry files and tree-shake unused exports.`);
198
- lines.push(`Sizes are exact minified and level-9 gzip JavaScript byte counts. Declarations, source maps, CSS, and other assets are excluded. Peer dependencies stay external, and shared modules are counted once per bundle.`);
199
- return lines;
204
+ /** Serialize a report with the same Galley elements used by the document component. */
205
+ async function renderTonnageMarkdown(report, config = {}) {
206
+ const directory = process.cwd();
207
+ return renderMarkdown(reportContent(report, config), {
208
+ directory,
209
+ path: path.resolve(directory, config.readme ?? "README.md")
210
+ });
200
211
  }
201
212
  function selectExportImports(packageName, exportsValue, selection) {
202
213
  if (selection === false) return [];
@@ -217,48 +228,10 @@ function toPackageImport(packageName, exportKey) {
217
228
  if (!exportKey.startsWith(`./`)) throw new Error(`Invalid package export key: ${exportKey}`);
218
229
  return `${packageName}/${exportKey.slice(2)}`;
219
230
  }
220
- function formatBytes(bytes) {
221
- return `${new Intl.NumberFormat(`en-US`).format(bytes)} B`;
222
- }
223
- function code(value) {
224
- return `<code>${escapeCell(value)}</code>`;
225
- }
226
- function escapeCell(value) {
227
- return value.replaceAll(`&`, `&amp;`).replaceAll(`|`, `&#124;`);
228
- }
229
231
  function unique(values) {
230
232
  return [...new Set(values)];
231
233
  }
232
- function markdownTable(headings, rows, alignments) {
233
- const widths = headings.map((heading, column) => Math.max(heading.length, ...rows.map((row) => row[column]?.length ?? 0), 3));
234
- const renderRow = (cells) => `| ${cells.map((cell, column) => padCell(cell, widths[column] ?? 3, alignments[column] ?? `left`)).join(` | `)} |`;
235
- const separator = widths.map((width, column) => alignments[column] === `right` ? `${`-`.repeat(width - 1)}:` : `-`.repeat(width));
236
- return [
237
- renderRow(headings),
238
- renderRow(separator),
239
- ...rows.map(renderRow)
240
- ];
241
- }
242
- function padCell(cell, width, alignment) {
243
- return alignment === `right` ? cell.padStart(width) : cell.padEnd(width);
244
- }
245
- //#endregion
246
- //#region src/run.ts
247
- async function runTonnage(config, options) {
248
- const configDirectory = path.resolve(options.configDirectory ?? process.cwd());
249
- const readmePath = path.resolve(configDirectory, config.readme ?? `README.md`);
250
- const oldReadme = await fs.readFile(readmePath, `utf8`);
251
- const report = await createTonnageReport(config, configDirectory);
252
- const newReadme = updateGeneratedSection(oldReadme, renderTonnageMarkdown(report, config), config.marker);
253
- const changed = oldReadme !== newReadme;
254
- if (options.mode === `write` && changed) await fs.writeFile(readmePath, newReadme);
255
- return {
256
- changed,
257
- readmePath,
258
- report
259
- };
260
- }
261
234
  //#endregion
262
- export { measureImports as a, measureEntry as i, createTonnageReport as n, tonnageMarkers as o, renderTonnageMarkdown as r, updateGeneratedSection as s, runTonnage as t };
235
+ export { measureImports as a, measureEntry as i, renderTonnageMarkdown as n, reportContent as r, createTonnageReport as t };
263
236
 
264
- //# sourceMappingURL=run-CZ_FO4C7.mjs.map
237
+ //# sourceMappingURL=report-B3HrJ4UQ.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report-B3HrJ4UQ.mjs","names":[],"sources":["../src/measure.ts","../src/package-resolution.ts","../src/report-content.ts","../src/report.ts"],"sourcesContent":["import path from \"node:path\"\nimport { gzipSync } from \"node:zlib\"\n\nimport { build, type BuildOptions, type Plugin } from \"esbuild\"\n\nimport type { BundleMeasurement, BundlePlatform } from \"./types.ts\"\n\nexport type MeasureImportsOptions = {\n\texternal?: readonly string[] | undefined\n\tplatform?: BundlePlatform | undefined\n\tresolveDirectory: string\n\ttarget?: string | undefined\n}\n\nexport type MeasureEntryOptions = MeasureImportsOptions\n\ntype InternalMeasureOptions = MeasureImportsOptions & {\n\tplugins?: readonly Plugin[]\n}\n\nexport async function measureEntry(\n\tentry: string,\n\toptions: MeasureEntryOptions,\n): Promise<BundleMeasurement> {\n\treturn measureEntryWithPlugins(entry, options)\n}\n\nexport async function measureEntryWithPlugins(\n\tentry: string,\n\toptions: InternalMeasureOptions,\n): Promise<BundleMeasurement> {\n\treturn measureBuild(\n\t\t{ entryPoints: [path.resolve(options.resolveDirectory, entry)] },\n\t\toptions,\n\t)\n}\n\nexport async function measureImports(\n\timports: readonly string[],\n\toptions: MeasureImportsOptions,\n): Promise<BundleMeasurement> {\n\treturn measureImportsWithPlugins(imports, options)\n}\n\nexport async function measureImportsWithPlugins(\n\timports: readonly string[],\n\toptions: InternalMeasureOptions,\n): Promise<BundleMeasurement> {\n\tif (imports.length === 0) {\n\t\tthrow new Error(`A tonnage entry must import at least one module.`)\n\t}\n\n\tconst entry = imports\n\t\t.map(\n\t\t\t(specifier, index) =>\n\t\t\t\t`export * as entry${index} from ${JSON.stringify(specifier)}`,\n\t\t)\n\t\t.join(`\\n`)\n\treturn measureBuild(\n\t\t{\n\t\t\tstdin: {\n\t\t\t\tcontents: entry,\n\t\t\t\tloader: `js`,\n\t\t\t\tresolveDir: options.resolveDirectory,\n\t\t\t\tsourcefile: `tonnage-entry.js`,\n\t\t\t},\n\t\t},\n\t\toptions,\n\t)\n}\n\nasync function measureBuild(\n\tentry: Pick<BuildOptions, `entryPoints` | `stdin`>,\n\toptions: InternalMeasureOptions,\n): Promise<BundleMeasurement> {\n\tconst platform = options.platform ?? `neutral`\n\n\tconst result = await build({\n\t\tbundle: true,\n\t\tcharset: `utf8`,\n\t\texternal: [\n\t\t\t...(options.external ?? []),\n\t\t\t...(platform === `browser` ? [] : [`node:*`]),\n\t\t],\n\t\tformat: `esm`,\n\t\tlegalComments: `none`,\n\t\tlogLevel: `silent`,\n\t\t...(platform === `neutral` ? { mainFields: [`module`, `main`] } : {}),\n\t\tminify: true,\n\t\toutdir: `out`,\n\t\tplatform,\n\t\tplugins: [...(options.plugins ?? [])],\n\t\t...entry,\n\t\ttarget: options.target ?? `es2022`,\n\t\ttreeShaking: true,\n\t\twrite: false,\n\t})\n\n\tconst outputFiles = [...(result.outputFiles ?? [])]\n\t\t.filter((output) => output.path.endsWith(`.js`))\n\t\t.sort((left, right) => left.path.localeCompare(right.path))\n\tif (outputFiles.length === 0) {\n\t\tthrow new Error(`The tonnage entry did not emit any runtime JavaScript.`)\n\t}\n\tconst rawBytes = outputFiles.reduce(\n\t\t(total, output) => total + output.contents.byteLength,\n\t\t0,\n\t)\n\tconst gzipBytes = outputFiles.reduce(\n\t\t(total, output) =>\n\t\t\ttotal + gzipSync(output.contents, { level: 9 }).byteLength,\n\t\t0,\n\t)\n\n\treturn { gzipBytes, rawBytes }\n}\n","import fs from \"node:fs/promises\"\nimport os from \"node:os\"\nimport path from \"node:path\"\n\nimport type { Plugin } from \"esbuild\"\n\nexport async function withPackageResolver<Result>(\n\tpackageName: string,\n\tpackageDirectory: string,\n\tuseResolver: (plugin: Plugin) => Promise<Result>,\n): Promise<Result> {\n\tconst temporaryDirectory = await fs.mkdtemp(\n\t\tpath.join(os.tmpdir(), `tonnage-package-`),\n\t)\n\ttry {\n\t\tconst nodeModulesDirectory = path.join(temporaryDirectory, `node_modules`)\n\t\tconst packageLinkPath = path.resolve(nodeModulesDirectory, packageName)\n\t\tif (!packageLinkPath.startsWith(`${nodeModulesDirectory}${path.sep}`)) {\n\t\t\tthrow new Error(`Invalid package name: ${packageName}`)\n\t\t}\n\n\t\tawait fs.mkdir(path.dirname(packageLinkPath), { recursive: true })\n\t\tawait fs.symlink(\n\t\t\tpackageDirectory,\n\t\t\tpackageLinkPath,\n\t\t\tprocess.platform === `win32` ? `junction` : `dir`,\n\t\t)\n\n\t\tconst filter = new RegExp(`^${escapeRegExp(packageName)}(?:/.*)?$`)\n\t\tconst plugin: Plugin = {\n\t\t\tname: `tonnage-package-resolution`,\n\t\t\tsetup(build) {\n\t\t\t\tbuild.onResolve({ filter }, async (args) => {\n\t\t\t\t\tif (args.resolveDir === temporaryDirectory) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\treturn build.resolve(args.path, {\n\t\t\t\t\t\timporter: args.importer,\n\t\t\t\t\t\tkind: args.kind,\n\t\t\t\t\t\tnamespace: args.namespace,\n\t\t\t\t\t\tresolveDir: temporaryDirectory,\n\t\t\t\t\t\twith: args.with,\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t},\n\t\t}\n\n\t\treturn await useResolver(plugin)\n\t} finally {\n\t\tawait fs.rm(temporaryDirectory, { force: true, recursive: true })\n\t}\n}\n\nfunction escapeRegExp(value: string): string {\n\treturn value.replace(/[.*+?^${}()|[\\]\\\\]/g, `\\\\$&`)\n}\n","import type { Content, Element } from \"galley.md\"\nimport { jsx } from \"galley.md/jsx-runtime\"\n\nimport type { TonnageConfig, TonnageReport } from \"./types.ts\"\n\n/** Use Galley's JSX runtime directly so the source CLI stays executable by Node. */\nexport function reportContent(\n\treport: TonnageReport,\n\tconfig: TonnageConfig = {},\n): Element[] {\n\tconst hasExports = report.exports.length > 0\n\tconst hasRecipes = report.recipes.length > 0\n\tconst hasMultipleSections = hasExports && hasRecipes\n\tconst content = [jsx(\"h2\", { children: config.heading ?? \"Bundle size\" })]\n\n\tif (hasExports)\n\t\tcontent.push(\n\t\t\tjsx(\"p\", {\n\t\t\t\tchildren:\n\t\t\t\t\t\"Package export sizes include complete runtime export surfaces.\",\n\t\t\t}),\n\t\t)\n\tif (hasRecipes)\n\t\tcontent.push(\n\t\t\tjsx(\"p\", {\n\t\t\t\tchildren:\n\t\t\t\t\t\"Usage example sizes bundle their entry files and tree-shake unused exports.\",\n\t\t\t}),\n\t\t)\n\tcontent.push(\n\t\tjsx(\"p\", {\n\t\t\tchildren:\n\t\t\t\t\"Sizes are exact minified and level-9 gzip JavaScript byte counts. Declarations, source maps, CSS, and other assets are excluded. Peer dependencies stay external, and shared modules are counted once per bundle.\",\n\t\t}),\n\t)\n\n\tif (hasExports) {\n\t\tif (hasMultipleSections)\n\t\t\tcontent.push(jsx(\"h3\", { children: \"Package exports\" }))\n\t\tcontent.push(\n\t\t\ttable(\n\t\t\t\t[\n\t\t\t\t\t{ title: \"Import\" },\n\t\t\t\t\t{ title: \"Minified JS\", align: \"right\" },\n\t\t\t\t\t{ title: \"Gzip JS\", align: \"right\" },\n\t\t\t\t],\n\t\t\t\treport.exports.map((row) => [\n\t\t\t\t\tcode(row.name),\n\t\t\t\t\tformatBytes(row.rawBytes),\n\t\t\t\t\tformatBytes(row.gzipBytes),\n\t\t\t\t]),\n\t\t\t),\n\t\t)\n\t}\n\n\tif (hasRecipes) {\n\t\tif (hasMultipleSections)\n\t\t\tcontent.push(jsx(\"h3\", { children: \"Usage examples\" }))\n\t\tcontent.push(\n\t\t\ttable(\n\t\t\t\t[\n\t\t\t\t\t{ title: \"Recipe\" },\n\t\t\t\t\t{ title: \"Entry\" },\n\t\t\t\t\t{ title: \"Minified JS\", align: \"right\" },\n\t\t\t\t\t{ title: \"Gzip JS\", align: \"right\" },\n\t\t\t\t],\n\t\t\t\treport.recipes.map((row) => [\n\t\t\t\t\trow.name,\n\t\t\t\t\tcode(row.entry),\n\t\t\t\t\tformatBytes(row.rawBytes),\n\t\t\t\t\tformatBytes(row.gzipBytes),\n\t\t\t\t]),\n\t\t\t),\n\t\t)\n\t}\n\n\tcontent.push(\n\t\tjsx(\"p\", {\n\t\t\tchildren: [\n\t\t\t\t\"Report maintained with \",\n\t\t\t\tjsx(\"a\", {\n\t\t\t\t\thref: \"https://github.com/jeremybanka/tonnage\",\n\t\t\t\t\tchildren: \"tonnage\",\n\t\t\t\t}),\n\t\t\t\t\".\",\n\t\t\t],\n\t\t}),\n\t)\n\treturn content\n}\n\nfunction code(value: string): Content {\n\treturn { type: \"inlineCode\", value }\n}\n\nfunction formatBytes(bytes: number): string {\n\treturn `${new Intl.NumberFormat(\"en-US\").format(bytes)} B`\n}\n\nfunction table(\n\tcolumns: readonly { title: string; align?: \"right\" }[],\n\trows: readonly (readonly Content[])[],\n): Element {\n\treturn jsx(\"table\", {\n\t\tchildren: [\n\t\t\tjsx(\"thead\", {\n\t\t\t\tchildren: jsx(\"tr\", {\n\t\t\t\t\tchildren: columns.map(({ title, align }) =>\n\t\t\t\t\t\tjsx(\"th\", { children: title, align }),\n\t\t\t\t\t),\n\t\t\t\t}),\n\t\t\t}),\n\t\t\tjsx(\"tbody\", {\n\t\t\t\tchildren: rows.map((cells) =>\n\t\t\t\t\tjsx(\"tr\", {\n\t\t\t\t\t\tchildren: cells.map((children) => jsx(\"td\", { children })),\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t}),\n\t\t],\n\t})\n}\n","import fs from \"node:fs/promises\"\nimport path from \"node:path\"\n\nimport { renderMarkdown } from \"galley.md\"\n\nimport {\n\tmeasureEntryWithPlugins,\n\tmeasureImportsWithPlugins,\n} from \"./measure.ts\"\nimport { withPackageResolver } from \"./package-resolution.ts\"\nimport { reportContent } from \"./report-content.ts\"\nimport type {\n\tTonnageConfig,\n\tTonnageExports,\n\tTonnageReport,\n\tTonnageRow,\n} from \"./types.ts\"\n\ntype PackageJson = {\n\texports?: unknown\n\tname?: string\n\tpeerDependencies?: Record<string, string>\n}\n\nexport async function createTonnageReport(\n\tconfig: TonnageConfig,\n\tconfigDirectory: string,\n): Promise<TonnageReport> {\n\tconst packageJsonPath = path.resolve(\n\t\tconfigDirectory,\n\t\tconfig.packageJson ?? `package.json`,\n\t)\n\tconst packageJson = JSON.parse(\n\t\tawait fs.readFile(packageJsonPath, `utf8`),\n\t) as PackageJson\n\tif (!packageJson.name) {\n\t\tthrow new Error(`${packageJsonPath} does not declare a package name.`)\n\t}\n\n\tconst resolveDirectory = path.dirname(packageJsonPath)\n\tconst peerDependencies = Object.keys(packageJson.peerDependencies ?? {})\n\tconst external = unique([...peerDependencies, ...(config.external ?? [])])\n\tconst exportImports = selectExportImports(\n\t\tpackageJson.name,\n\t\tpackageJson.exports,\n\t\tconfig.exports,\n\t)\n\n\tconst { exportRows, recipeRows } = await withPackageResolver(\n\t\tpackageJson.name,\n\t\tresolveDirectory,\n\t\tasync (packageResolver) => {\n\t\t\tconst exportRows = await Promise.all(\n\t\t\t\texportImports.map(async (specifier): Promise<TonnageRow> => ({\n\t\t\t\t\t...(await measureImportsWithPlugins([specifier], {\n\t\t\t\t\t\texternal,\n\t\t\t\t\t\tplatform: config.platform,\n\t\t\t\t\t\tplugins: [packageResolver],\n\t\t\t\t\t\tresolveDirectory,\n\t\t\t\t\t\ttarget: config.target,\n\t\t\t\t\t})),\n\t\t\t\t\timports: [specifier],\n\t\t\t\t\tname: specifier,\n\t\t\t\t})),\n\t\t\t)\n\t\t\tconst recipeRows = await Promise.all(\n\t\t\t\t(config.recipes ?? []).map(async (recipe) => ({\n\t\t\t\t\t...(await measureEntryWithPlugins(recipe.entry, {\n\t\t\t\t\t\texternal: unique([...external, ...(recipe.external ?? [])]),\n\t\t\t\t\t\tplatform: recipe.platform ?? config.platform,\n\t\t\t\t\t\tplugins: [packageResolver],\n\t\t\t\t\t\tresolveDirectory,\n\t\t\t\t\t\ttarget: config.target,\n\t\t\t\t\t})),\n\t\t\t\t\tentry: recipe.entry,\n\t\t\t\t\tname: recipe.name,\n\t\t\t\t})),\n\t\t\t)\n\n\t\t\treturn { exportRows, recipeRows }\n\t\t},\n\t)\n\n\treturn {\n\t\texports: exportRows,\n\t\tpackageName: packageJson.name,\n\t\trecipes: recipeRows,\n\t}\n}\n\n/** Serialize a report with the same Galley elements used by the document component. */\nexport async function renderTonnageMarkdown(\n\treport: TonnageReport,\n\tconfig: TonnageConfig = {},\n): Promise<string> {\n\tconst directory = process.cwd()\n\treturn renderMarkdown(reportContent(report, config), {\n\t\tdirectory,\n\t\tpath: path.resolve(directory, config.readme ?? \"README.md\"),\n\t})\n}\n\nfunction selectExportImports(\n\tpackageName: string,\n\texportsValue: unknown,\n\tselection: TonnageConfig[`exports`],\n): string[] {\n\tif (selection === false) {\n\t\treturn []\n\t}\n\n\tconst exportKeys = getExportKeys(exportsValue)\n\tconst options: TonnageExports = typeof selection === `object` ? selection : {}\n\tconst included = options.include\n\t\t? [...options.include]\n\t\t: exportKeys.filter((exportKey) => exportKey !== `./package.json`)\n\tconst excluded = new Set(options.exclude ?? [])\n\n\treturn included\n\t\t.filter((exportKey) => !excluded.has(exportKey))\n\t\t.map((exportKey) => toPackageImport(packageName, exportKey))\n}\n\nfunction getExportKeys(exportsValue: unknown): string[] {\n\tif (typeof exportsValue === `string` || Array.isArray(exportsValue)) {\n\t\treturn [`.`]\n\t}\n\tif (!exportsValue || typeof exportsValue !== `object`) {\n\t\treturn [`.`]\n\t}\n\n\tconst keys = Object.keys(exportsValue)\n\treturn keys.some((key) => key.startsWith(`.`)) ? keys : [`.`]\n}\n\nfunction toPackageImport(packageName: string, exportKey: string): string {\n\tif (exportKey === `.`) {\n\t\treturn packageName\n\t}\n\tif (!exportKey.startsWith(`./`)) {\n\t\tthrow new Error(`Invalid package export key: ${exportKey}`)\n\t}\n\treturn `${packageName}/${exportKey.slice(2)}`\n}\n\nfunction unique(values: readonly string[]): string[] {\n\treturn [...new Set(values)]\n}\n"],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ import { n as renderTonnageMarkdown, t as createTonnageReport } from "./report-B3HrJ4UQ.mjs";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ //#region src/readme.ts
5
+ function tonnageMarkers(marker = `default`) {
6
+ validateMarker(marker);
7
+ return {
8
+ end: `<!-- tonnage:${marker}:end -->`,
9
+ start: `<!-- tonnage:${marker}:start -->`
10
+ };
11
+ }
12
+ function updateGeneratedSection(readme, markdown, marker) {
13
+ const { end, start } = tonnageMarkers(marker);
14
+ const startIndex = readme.indexOf(start);
15
+ const endIndex = readme.indexOf(end);
16
+ if (startIndex === -1 || endIndex === -1) throw new Error(`README must contain exactly one ${start} and one ${end} marker.`);
17
+ if (readme.slice(startIndex + start.length).includes(start) || readme.slice(endIndex + end.length).includes(end)) throw new Error(`README must contain exactly one ${start} and one ${end} marker.`);
18
+ if (endIndex < startIndex) throw new Error(`${end} must appear after ${start}.`);
19
+ const contentStart = startIndex + start.length;
20
+ return `${readme.slice(0, contentStart)}\n\n${markdown.trim()}\n\n${readme.slice(endIndex)}`;
21
+ }
22
+ function validateMarker(marker) {
23
+ if (!/^[a-z0-9][a-z0-9._-]*$/i.test(marker)) throw new Error(`Tonnage marker names may contain letters, numbers, dots, underscores, and hyphens.`);
24
+ }
25
+ //#endregion
26
+ //#region src/run.ts
27
+ async function runTonnage(config, options) {
28
+ const configDirectory = path.resolve(options.configDirectory ?? process.cwd());
29
+ const readmePath = path.resolve(configDirectory, config.readme ?? `README.md`);
30
+ const oldReadme = await fs.readFile(readmePath, `utf8`);
31
+ const report = await createTonnageReport(config, configDirectory);
32
+ const newReadme = updateGeneratedSection(oldReadme, await renderTonnageMarkdown(report, config), config.marker);
33
+ const changed = oldReadme !== newReadme;
34
+ if (options.mode === `write` && changed) await fs.writeFile(readmePath, newReadme);
35
+ return {
36
+ changed,
37
+ readmePath,
38
+ report
39
+ };
40
+ }
41
+ //#endregion
42
+ export { tonnageMarkers as n, updateGeneratedSection as r, runTonnage as t };
43
+
44
+ //# sourceMappingURL=run-BJN1MXRf.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-BJN1MXRf.mjs","names":[],"sources":["../src/readme.ts","../src/run.ts"],"sourcesContent":["export function tonnageMarkers(marker = `default`): {\n\tend: string\n\tstart: string\n} {\n\tvalidateMarker(marker)\n\treturn {\n\t\tend: `<!-- tonnage:${marker}:end -->`,\n\t\tstart: `<!-- tonnage:${marker}:start -->`,\n\t}\n}\n\nexport function updateGeneratedSection(\n\treadme: string,\n\tmarkdown: string,\n\tmarker?: string,\n): string {\n\tconst { end, start } = tonnageMarkers(marker)\n\tconst startIndex = readme.indexOf(start)\n\tconst endIndex = readme.indexOf(end)\n\n\tif (startIndex === -1 || endIndex === -1) {\n\t\tthrow new Error(\n\t\t\t`README must contain exactly one ${start} and one ${end} marker.`,\n\t\t)\n\t}\n\tif (\n\t\treadme.slice(startIndex + start.length).includes(start) ||\n\t\treadme.slice(endIndex + end.length).includes(end)\n\t) {\n\t\tthrow new Error(\n\t\t\t`README must contain exactly one ${start} and one ${end} marker.`,\n\t\t)\n\t}\n\tif (endIndex < startIndex) {\n\t\tthrow new Error(`${end} must appear after ${start}.`)\n\t}\n\n\tconst contentStart = startIndex + start.length\n\treturn `${readme.slice(0, contentStart)}\\n\\n${markdown.trim()}\\n\\n${readme.slice(endIndex)}`\n}\n\nfunction validateMarker(marker: string): void {\n\tif (!/^[a-z0-9][a-z0-9._-]*$/i.test(marker)) {\n\t\tthrow new Error(\n\t\t\t`Tonnage marker names may contain letters, numbers, dots, underscores, and hyphens.`,\n\t\t)\n\t}\n}\n","import fs from \"node:fs/promises\"\nimport path from \"node:path\"\n\nimport { updateGeneratedSection } from \"./readme.ts\"\nimport { createTonnageReport, renderTonnageMarkdown } from \"./report.ts\"\nimport type { TonnageConfig, TonnageMode, TonnageRunResult } from \"./types.ts\"\n\nexport async function runTonnage(\n\tconfig: TonnageConfig,\n\toptions: {\n\t\tconfigDirectory?: string\n\t\tmode: TonnageMode\n\t},\n): Promise<TonnageRunResult> {\n\tconst configDirectory = path.resolve(options.configDirectory ?? process.cwd())\n\tconst readmePath = path.resolve(configDirectory, config.readme ?? `README.md`)\n\tconst oldReadme = await fs.readFile(readmePath, `utf8`)\n\tconst report = await createTonnageReport(config, configDirectory)\n\tconst markdown = await renderTonnageMarkdown(report, config)\n\tconst newReadme = updateGeneratedSection(oldReadme, markdown, config.marker)\n\tconst changed = oldReadme !== newReadme\n\n\tif (options.mode === `write` && changed) {\n\t\tawait fs.writeFile(readmePath, newReadme)\n\t}\n\n\treturn { changed, readmePath, report }\n}\n"],"mappings":";;;;AAAA,SAAgB,eAAe,SAAS,WAGtC;CACD,eAAe,MAAM;CACrB,OAAO;EACN,KAAK,gBAAgB,OAAO;EAC5B,OAAO,gBAAgB,OAAO;CAC/B;AACD;AAEA,SAAgB,uBACf,QACA,UACA,QACS;CACT,MAAM,EAAE,KAAK,UAAU,eAAe,MAAM;CAC5C,MAAM,aAAa,OAAO,QAAQ,KAAK;CACvC,MAAM,WAAW,OAAO,QAAQ,GAAG;CAEnC,IAAI,eAAe,MAAM,aAAa,IACrC,MAAM,IAAI,MACT,mCAAmC,MAAM,WAAW,IAAI,SACzD;CAED,IACC,OAAO,MAAM,aAAa,MAAM,MAAM,CAAC,CAAC,SAAS,KAAK,KACtD,OAAO,MAAM,WAAW,IAAI,MAAM,CAAC,CAAC,SAAS,GAAG,GAEhD,MAAM,IAAI,MACT,mCAAmC,MAAM,WAAW,IAAI,SACzD;CAED,IAAI,WAAW,YACd,MAAM,IAAI,MAAM,GAAG,IAAI,qBAAqB,MAAM,EAAE;CAGrD,MAAM,eAAe,aAAa,MAAM;CACxC,OAAO,GAAG,OAAO,MAAM,GAAG,YAAY,EAAE,MAAM,SAAS,KAAK,EAAE,MAAM,OAAO,MAAM,QAAQ;AAC1F;AAEA,SAAS,eAAe,QAAsB;CAC7C,IAAI,CAAC,0BAA0B,KAAK,MAAM,GACzC,MAAM,IAAI,MACT,oFACD;AAEF;;;ACxCA,eAAsB,WACrB,QACA,SAI4B;CAC5B,MAAM,kBAAkB,KAAK,QAAQ,QAAQ,mBAAmB,QAAQ,IAAI,CAAC;CAC7E,MAAM,aAAa,KAAK,QAAQ,iBAAiB,OAAO,UAAU,WAAW;CAC7E,MAAM,YAAY,MAAM,GAAG,SAAS,YAAY,MAAM;CACtD,MAAM,SAAS,MAAM,oBAAoB,QAAQ,eAAe;CAEhE,MAAM,YAAY,uBAAuB,WAAW,MAD7B,sBAAsB,QAAQ,MAAM,GACG,OAAO,MAAM;CAC3E,MAAM,UAAU,cAAc;CAE9B,IAAI,QAAQ,SAAS,WAAW,SAC/B,MAAM,GAAG,UAAU,YAAY,SAAS;CAGzC,OAAO;EAAE;EAAS;EAAY;CAAO;AACtC"}
@@ -0,0 +1,49 @@
1
+ //#region src/types.d.ts
2
+ type BundlePlatform = `browser` | `neutral` | `node`;
3
+ type TonnageExports = {
4
+ exclude?: readonly string[];
5
+ include?: readonly string[];
6
+ };
7
+ type TonnageRecipe = {
8
+ entry: string;
9
+ external?: readonly string[];
10
+ name: string;
11
+ platform?: BundlePlatform;
12
+ };
13
+ type TonnageConfig = {
14
+ exports?: boolean | TonnageExports;
15
+ external?: readonly string[];
16
+ heading?: string;
17
+ marker?: string;
18
+ packageJson?: string;
19
+ platform?: BundlePlatform;
20
+ readme?: string;
21
+ recipes?: readonly TonnageRecipe[];
22
+ target?: string;
23
+ };
24
+ type BundleMeasurement = {
25
+ gzipBytes: number;
26
+ rawBytes: number;
27
+ };
28
+ type TonnageRow = BundleMeasurement & {
29
+ imports: readonly string[];
30
+ name: string;
31
+ };
32
+ type TonnageRecipeRow = BundleMeasurement & {
33
+ entry: string;
34
+ name: string;
35
+ };
36
+ type TonnageReport = {
37
+ exports: readonly TonnageRow[];
38
+ packageName: string;
39
+ recipes: readonly TonnageRecipeRow[];
40
+ };
41
+ type TonnageMode = `check` | `write`;
42
+ type TonnageRunResult = {
43
+ changed: boolean;
44
+ readmePath: string;
45
+ report: TonnageReport;
46
+ };
47
+ //#endregion
48
+ export { TonnageMode as a, TonnageReport as c, TonnageExports as i, TonnageRow as l, BundlePlatform as n, TonnageRecipe as o, TonnageConfig as r, TonnageRecipeRow as s, BundleMeasurement as t, TonnageRunResult as u };
49
+ //# sourceMappingURL=types-Ddtp95Z3.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-Ddtp95Z3.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";KAAY;KAEA;EACX;EACA;;KAGW;EACX;EACA;EACA;EACA,WAAW;;KAGA;EACX,oBAAoB;EACpB;EACA;EACA;EACA;EACA,WAAW;EACX;EACA,mBAAmB;EACnB;;KAGW;EACX;EACA;;KAGW,aAAa;EACxB;EACA;;KAGW,mBAAmB;EAC9B;EACA;;KAGW;EACX,kBAAkB;EAClB;EACA,kBAAkB;;KAGP;KAEA;EACX;EACA;EACA,QAAQ"}
package/package.json CHANGED
@@ -1,33 +1,19 @@
1
1
  {
2
2
  "name": "tonnage",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Keep deterministic bundle size reports in README files and CI.",
5
- "type": "module",
6
- "license": "MPL-2.0",
7
- "author": "Jeremy Banka",
8
- "engines": {
9
- "node": ">=22.18.0"
10
- },
11
- "publishConfig": {
12
- "access": "public"
13
- },
14
5
  "repository": {
15
6
  "type": "git",
16
7
  "url": "git+https://github.com/jeremybanka/tonnage.git"
17
8
  },
18
- "files": [
19
- "AGENTS.md",
20
- "CHANGELOG.md",
21
- "dist",
22
- "src",
23
- "examples",
24
- "README.md",
25
- "LICENSE"
26
- ],
27
- "bin": {
28
- "tonnage": "dist/cli.mjs"
29
- },
9
+ "license": "MPL-2.0",
10
+ "author": "Jeremy Banka",
11
+ "type": "module",
30
12
  "exports": {
13
+ "./galley": {
14
+ "types": "./dist/galley.d.mts",
15
+ "import": "./dist/galley.mjs"
16
+ },
31
17
  ".": {
32
18
  "import": "./dist/index.mjs",
33
19
  "types": "./dist/index.d.mts"
@@ -36,16 +22,36 @@
36
22
  },
37
23
  "main": "./dist/index.mjs",
38
24
  "types": "./dist/index.d.mts",
25
+ "bin": {
26
+ "tonnage": "dist/cli.mjs"
27
+ },
28
+ "files": [
29
+ "AGENTS.md",
30
+ "CHANGELOG.md",
31
+ "dist",
32
+ "src",
33
+ "examples",
34
+ "README.md",
35
+ "LICENSE"
36
+ ],
39
37
  "dependencies": {
40
- "comline": "0.7.0",
41
- "esbuild": "0.28.2"
38
+ "comline": "0.9.0",
39
+ "esbuild": "0.28.2",
40
+ "galley.md": "0.1.1"
42
41
  },
43
42
  "devDependencies": {
44
- "@types/node": "26.5.1",
45
- "vitest": "npm:@voidzero-dev/vite-plus-test@0.1.24"
43
+ "@types/node": "26.6.5",
44
+ "typescript": "7.1.0-dev.20261020.3"
45
+ },
46
+ "engines": {
47
+ "node": ">=22.18.0"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
46
51
  },
47
52
  "scripts": {
48
53
  "build": "vp pack",
49
- "test": "vp test"
54
+ "test": "vp test",
55
+ "test:watch": "vp test watch"
50
56
  }
51
57
  }
package/src/galley.ts ADDED
@@ -0,0 +1,25 @@
1
+ import path from "node:path"
2
+
3
+ import type { DocumentContext, Element } from "galley.md"
4
+
5
+ import { createTonnageReport } from "./report.ts"
6
+ import { reportContent } from "./report-content.ts"
7
+ import type { TonnageConfig } from "./types.ts"
8
+
9
+ export type TonnageProps = {
10
+ config?: TonnageConfig
11
+ /** Override the document directory when measuring a different package. */
12
+ directory?: string
13
+ }
14
+
15
+ /** Measure bundles and include the report in a galley.md document. */
16
+ export async function Tonnage(
17
+ { config = {}, directory }: TonnageProps,
18
+ context: DocumentContext,
19
+ ): Promise<Element[]> {
20
+ const report = await createTonnageReport(
21
+ config,
22
+ path.resolve(context.directory, directory ?? "."),
23
+ )
24
+ return reportContent(report, config)
25
+ }
@@ -0,0 +1,122 @@
1
+ import type { Content, Element } from "galley.md"
2
+ import { jsx } from "galley.md/jsx-runtime"
3
+
4
+ import type { TonnageConfig, TonnageReport } from "./types.ts"
5
+
6
+ /** Use Galley's JSX runtime directly so the source CLI stays executable by Node. */
7
+ export function reportContent(
8
+ report: TonnageReport,
9
+ config: TonnageConfig = {},
10
+ ): Element[] {
11
+ const hasExports = report.exports.length > 0
12
+ const hasRecipes = report.recipes.length > 0
13
+ const hasMultipleSections = hasExports && hasRecipes
14
+ const content = [jsx("h2", { children: config.heading ?? "Bundle size" })]
15
+
16
+ if (hasExports)
17
+ content.push(
18
+ jsx("p", {
19
+ children:
20
+ "Package export sizes include complete runtime export surfaces.",
21
+ }),
22
+ )
23
+ if (hasRecipes)
24
+ content.push(
25
+ jsx("p", {
26
+ children:
27
+ "Usage example sizes bundle their entry files and tree-shake unused exports.",
28
+ }),
29
+ )
30
+ content.push(
31
+ jsx("p", {
32
+ children:
33
+ "Sizes are exact minified and level-9 gzip JavaScript byte counts. Declarations, source maps, CSS, and other assets are excluded. Peer dependencies stay external, and shared modules are counted once per bundle.",
34
+ }),
35
+ )
36
+
37
+ if (hasExports) {
38
+ if (hasMultipleSections)
39
+ content.push(jsx("h3", { children: "Package exports" }))
40
+ content.push(
41
+ table(
42
+ [
43
+ { title: "Import" },
44
+ { title: "Minified JS", align: "right" },
45
+ { title: "Gzip JS", align: "right" },
46
+ ],
47
+ report.exports.map((row) => [
48
+ code(row.name),
49
+ formatBytes(row.rawBytes),
50
+ formatBytes(row.gzipBytes),
51
+ ]),
52
+ ),
53
+ )
54
+ }
55
+
56
+ if (hasRecipes) {
57
+ if (hasMultipleSections)
58
+ content.push(jsx("h3", { children: "Usage examples" }))
59
+ content.push(
60
+ table(
61
+ [
62
+ { title: "Recipe" },
63
+ { title: "Entry" },
64
+ { title: "Minified JS", align: "right" },
65
+ { title: "Gzip JS", align: "right" },
66
+ ],
67
+ report.recipes.map((row) => [
68
+ row.name,
69
+ code(row.entry),
70
+ formatBytes(row.rawBytes),
71
+ formatBytes(row.gzipBytes),
72
+ ]),
73
+ ),
74
+ )
75
+ }
76
+
77
+ content.push(
78
+ jsx("p", {
79
+ children: [
80
+ "Report maintained with ",
81
+ jsx("a", {
82
+ href: "https://github.com/jeremybanka/tonnage",
83
+ children: "tonnage",
84
+ }),
85
+ ".",
86
+ ],
87
+ }),
88
+ )
89
+ return content
90
+ }
91
+
92
+ function code(value: string): Content {
93
+ return { type: "inlineCode", value }
94
+ }
95
+
96
+ function formatBytes(bytes: number): string {
97
+ return `${new Intl.NumberFormat("en-US").format(bytes)} B`
98
+ }
99
+
100
+ function table(
101
+ columns: readonly { title: string; align?: "right" }[],
102
+ rows: readonly (readonly Content[])[],
103
+ ): Element {
104
+ return jsx("table", {
105
+ children: [
106
+ jsx("thead", {
107
+ children: jsx("tr", {
108
+ children: columns.map(({ title, align }) =>
109
+ jsx("th", { children: title, align }),
110
+ ),
111
+ }),
112
+ }),
113
+ jsx("tbody", {
114
+ children: rows.map((cells) =>
115
+ jsx("tr", {
116
+ children: cells.map((children) => jsx("td", { children })),
117
+ }),
118
+ ),
119
+ }),
120
+ ],
121
+ })
122
+ }
package/src/report.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  import fs from "node:fs/promises"
2
2
  import path from "node:path"
3
3
 
4
+ import { renderMarkdown } from "galley.md"
5
+
4
6
  import {
5
7
  measureEntryWithPlugins,
6
8
  measureImportsWithPlugins,
7
9
  } from "./measure.ts"
8
10
  import { withPackageResolver } from "./package-resolution.ts"
11
+ import { reportContent } from "./report-content.ts"
9
12
  import type {
10
13
  TonnageConfig,
11
14
  TonnageExports,
@@ -85,80 +88,16 @@ export async function createTonnageReport(
85
88
  }
86
89
  }
87
90
 
88
- export function renderTonnageMarkdown(
91
+ /** Serialize a report with the same Galley elements used by the document component. */
92
+ export async function renderTonnageMarkdown(
89
93
  report: TonnageReport,
90
94
  config: TonnageConfig = {},
91
- ): string {
92
- const hasExports = report.exports.length > 0
93
- const hasRecipes = report.recipes.length > 0
94
- const hasMultipleSections = hasExports && hasRecipes
95
- const lines = [
96
- `## ${config.heading ?? `Bundle size`}`,
97
- ``,
98
- ...methodologyLines({ hasExports, hasRecipes }),
99
- ]
100
-
101
- if (hasExports) {
102
- lines.push(
103
- ``,
104
- ...(hasMultipleSections ? [`### Package exports`, ``] : []),
105
- ...markdownTable(
106
- [`Import`, `Minified JS`, `Gzip JS`],
107
- report.exports.map((row) => [
108
- code(row.name),
109
- formatBytes(row.rawBytes),
110
- formatBytes(row.gzipBytes),
111
- ]),
112
- [`left`, `right`, `right`],
113
- ),
114
- )
115
- }
116
-
117
- if (hasRecipes) {
118
- lines.push(
119
- ``,
120
- ...(hasMultipleSections ? [`### Usage examples`, ``] : []),
121
- ...markdownTable(
122
- [`Recipe`, `Entry`, `Minified JS`, `Gzip JS`],
123
- report.recipes.map((row) => [
124
- escapeCell(row.name),
125
- code(row.entry),
126
- formatBytes(row.rawBytes),
127
- formatBytes(row.gzipBytes),
128
- ]),
129
- [`left`, `left`, `right`, `right`],
130
- ),
131
- )
132
- }
133
-
134
- lines.push(
135
- ``,
136
- `Report maintained with [tonnage](https://github.com/jeremybanka/tonnage).`,
137
- )
138
-
139
- return lines.join(`\n`)
140
- }
141
-
142
- function methodologyLines({
143
- hasExports,
144
- hasRecipes,
145
- }: {
146
- hasExports: boolean
147
- hasRecipes: boolean
148
- }): string[] {
149
- const lines: string[] = []
150
- if (hasExports) {
151
- lines.push(`Package export sizes include complete runtime export surfaces.`)
152
- }
153
- if (hasRecipes) {
154
- lines.push(
155
- `Usage example sizes bundle their entry files and tree-shake unused exports.`,
156
- )
157
- }
158
- lines.push(
159
- `Sizes are exact minified and level-9 gzip JavaScript byte counts. Declarations, source maps, CSS, and other assets are excluded. Peer dependencies stay external, and shared modules are counted once per bundle.`,
160
- )
161
- return lines
95
+ ): Promise<string> {
96
+ const directory = process.cwd()
97
+ return renderMarkdown(reportContent(report, config), {
98
+ directory,
99
+ path: path.resolve(directory, config.readme ?? "README.md"),
100
+ })
162
101
  }
163
102
 
164
103
  function selectExportImports(
@@ -204,45 +143,6 @@ function toPackageImport(packageName: string, exportKey: string): string {
204
143
  return `${packageName}/${exportKey.slice(2)}`
205
144
  }
206
145
 
207
- function formatBytes(bytes: number): string {
208
- return `${new Intl.NumberFormat(`en-US`).format(bytes)} B`
209
- }
210
-
211
- function code(value: string): string {
212
- return `<code>${escapeCell(value)}</code>`
213
- }
214
-
215
- function escapeCell(value: string): string {
216
- return value.replaceAll(`&`, `&amp;`).replaceAll(`|`, `&#124;`)
217
- }
218
-
219
146
  function unique(values: readonly string[]): string[] {
220
147
  return [...new Set(values)]
221
148
  }
222
-
223
- function markdownTable(
224
- headings: readonly string[],
225
- rows: readonly (readonly string[])[],
226
- alignments: readonly (`left` | `right`)[],
227
- ): string[] {
228
- const widths = headings.map((heading, column) =>
229
- Math.max(heading.length, ...rows.map((row) => row[column]?.length ?? 0), 3),
230
- )
231
- const renderRow = (cells: readonly string[]): string =>
232
- `| ${cells.map((cell, column) => padCell(cell, widths[column] ?? 3, alignments[column] ?? `left`)).join(` | `)} |`
233
- const separator = widths.map((width, column) =>
234
- alignments[column] === `right`
235
- ? `${`-`.repeat(width - 1)}:`
236
- : `-`.repeat(width),
237
- )
238
-
239
- return [renderRow(headings), renderRow(separator), ...rows.map(renderRow)]
240
- }
241
-
242
- function padCell(
243
- cell: string,
244
- width: number,
245
- alignment: `left` | `right`,
246
- ): string {
247
- return alignment === `right` ? cell.padStart(width) : cell.padEnd(width)
248
- }
package/src/run.ts CHANGED
@@ -16,7 +16,7 @@ export async function runTonnage(
16
16
  const readmePath = path.resolve(configDirectory, config.readme ?? `README.md`)
17
17
  const oldReadme = await fs.readFile(readmePath, `utf8`)
18
18
  const report = await createTonnageReport(config, configDirectory)
19
- const markdown = renderTonnageMarkdown(report, config)
19
+ const markdown = await renderTonnageMarkdown(report, config)
20
20
  const newReadme = updateGeneratedSection(oldReadme, markdown, config.marker)
21
21
  const changed = oldReadme !== newReadme
22
22
 
@@ -1 +0,0 @@
1
- {"version":3,"file":"run-CZ_FO4C7.mjs","names":[],"sources":["../src/readme.ts","../src/measure.ts","../src/package-resolution.ts","../src/report.ts","../src/run.ts"],"sourcesContent":["export function tonnageMarkers(marker = `default`): {\n\tend: string\n\tstart: string\n} {\n\tvalidateMarker(marker)\n\treturn {\n\t\tend: `<!-- tonnage:${marker}:end -->`,\n\t\tstart: `<!-- tonnage:${marker}:start -->`,\n\t}\n}\n\nexport function updateGeneratedSection(\n\treadme: string,\n\tmarkdown: string,\n\tmarker?: string,\n): string {\n\tconst { end, start } = tonnageMarkers(marker)\n\tconst startIndex = readme.indexOf(start)\n\tconst endIndex = readme.indexOf(end)\n\n\tif (startIndex === -1 || endIndex === -1) {\n\t\tthrow new Error(\n\t\t\t`README must contain exactly one ${start} and one ${end} marker.`,\n\t\t)\n\t}\n\tif (\n\t\treadme.slice(startIndex + start.length).includes(start) ||\n\t\treadme.slice(endIndex + end.length).includes(end)\n\t) {\n\t\tthrow new Error(\n\t\t\t`README must contain exactly one ${start} and one ${end} marker.`,\n\t\t)\n\t}\n\tif (endIndex < startIndex) {\n\t\tthrow new Error(`${end} must appear after ${start}.`)\n\t}\n\n\tconst contentStart = startIndex + start.length\n\treturn `${readme.slice(0, contentStart)}\\n\\n${markdown.trim()}\\n\\n${readme.slice(endIndex)}`\n}\n\nfunction validateMarker(marker: string): void {\n\tif (!/^[a-z0-9][a-z0-9._-]*$/i.test(marker)) {\n\t\tthrow new Error(\n\t\t\t`Tonnage marker names may contain letters, numbers, dots, underscores, and hyphens.`,\n\t\t)\n\t}\n}\n","import path from \"node:path\"\nimport { gzipSync } from \"node:zlib\"\n\nimport { build, type BuildOptions, type Plugin } from \"esbuild\"\n\nimport type { BundleMeasurement, BundlePlatform } from \"./types.ts\"\n\nexport type MeasureImportsOptions = {\n\texternal?: readonly string[] | undefined\n\tplatform?: BundlePlatform | undefined\n\tresolveDirectory: string\n\ttarget?: string | undefined\n}\n\nexport type MeasureEntryOptions = MeasureImportsOptions\n\ntype InternalMeasureOptions = MeasureImportsOptions & {\n\tplugins?: readonly Plugin[]\n}\n\nexport async function measureEntry(\n\tentry: string,\n\toptions: MeasureEntryOptions,\n): Promise<BundleMeasurement> {\n\treturn measureEntryWithPlugins(entry, options)\n}\n\nexport async function measureEntryWithPlugins(\n\tentry: string,\n\toptions: InternalMeasureOptions,\n): Promise<BundleMeasurement> {\n\treturn measureBuild(\n\t\t{ entryPoints: [path.resolve(options.resolveDirectory, entry)] },\n\t\toptions,\n\t)\n}\n\nexport async function measureImports(\n\timports: readonly string[],\n\toptions: MeasureImportsOptions,\n): Promise<BundleMeasurement> {\n\treturn measureImportsWithPlugins(imports, options)\n}\n\nexport async function measureImportsWithPlugins(\n\timports: readonly string[],\n\toptions: InternalMeasureOptions,\n): Promise<BundleMeasurement> {\n\tif (imports.length === 0) {\n\t\tthrow new Error(`A tonnage entry must import at least one module.`)\n\t}\n\n\tconst entry = imports\n\t\t.map(\n\t\t\t(specifier, index) =>\n\t\t\t\t`export * as entry${index} from ${JSON.stringify(specifier)}`,\n\t\t)\n\t\t.join(`\\n`)\n\treturn measureBuild(\n\t\t{\n\t\t\tstdin: {\n\t\t\t\tcontents: entry,\n\t\t\t\tloader: `js`,\n\t\t\t\tresolveDir: options.resolveDirectory,\n\t\t\t\tsourcefile: `tonnage-entry.js`,\n\t\t\t},\n\t\t},\n\t\toptions,\n\t)\n}\n\nasync function measureBuild(\n\tentry: Pick<BuildOptions, `entryPoints` | `stdin`>,\n\toptions: InternalMeasureOptions,\n): Promise<BundleMeasurement> {\n\tconst platform = options.platform ?? `neutral`\n\n\tconst result = await build({\n\t\tbundle: true,\n\t\tcharset: `utf8`,\n\t\texternal: [\n\t\t\t...(options.external ?? []),\n\t\t\t...(platform === `browser` ? [] : [`node:*`]),\n\t\t],\n\t\tformat: `esm`,\n\t\tlegalComments: `none`,\n\t\tlogLevel: `silent`,\n\t\t...(platform === `neutral` ? { mainFields: [`module`, `main`] } : {}),\n\t\tminify: true,\n\t\toutdir: `out`,\n\t\tplatform,\n\t\tplugins: [...(options.plugins ?? [])],\n\t\t...entry,\n\t\ttarget: options.target ?? `es2022`,\n\t\ttreeShaking: true,\n\t\twrite: false,\n\t})\n\n\tconst outputFiles = [...(result.outputFiles ?? [])]\n\t\t.filter((output) => output.path.endsWith(`.js`))\n\t\t.sort((left, right) => left.path.localeCompare(right.path))\n\tif (outputFiles.length === 0) {\n\t\tthrow new Error(`The tonnage entry did not emit any runtime JavaScript.`)\n\t}\n\tconst rawBytes = outputFiles.reduce(\n\t\t(total, output) => total + output.contents.byteLength,\n\t\t0,\n\t)\n\tconst gzipBytes = outputFiles.reduce(\n\t\t(total, output) =>\n\t\t\ttotal + gzipSync(output.contents, { level: 9 }).byteLength,\n\t\t0,\n\t)\n\n\treturn { gzipBytes, rawBytes }\n}\n","import fs from \"node:fs/promises\"\nimport os from \"node:os\"\nimport path from \"node:path\"\n\nimport type { Plugin } from \"esbuild\"\n\nexport async function withPackageResolver<Result>(\n\tpackageName: string,\n\tpackageDirectory: string,\n\tuseResolver: (plugin: Plugin) => Promise<Result>,\n): Promise<Result> {\n\tconst temporaryDirectory = await fs.mkdtemp(\n\t\tpath.join(os.tmpdir(), `tonnage-package-`),\n\t)\n\ttry {\n\t\tconst nodeModulesDirectory = path.join(temporaryDirectory, `node_modules`)\n\t\tconst packageLinkPath = path.resolve(nodeModulesDirectory, packageName)\n\t\tif (!packageLinkPath.startsWith(`${nodeModulesDirectory}${path.sep}`)) {\n\t\t\tthrow new Error(`Invalid package name: ${packageName}`)\n\t\t}\n\n\t\tawait fs.mkdir(path.dirname(packageLinkPath), { recursive: true })\n\t\tawait fs.symlink(\n\t\t\tpackageDirectory,\n\t\t\tpackageLinkPath,\n\t\t\tprocess.platform === `win32` ? `junction` : `dir`,\n\t\t)\n\n\t\tconst filter = new RegExp(`^${escapeRegExp(packageName)}(?:/.*)?$`)\n\t\tconst plugin: Plugin = {\n\t\t\tname: `tonnage-package-resolution`,\n\t\t\tsetup(build) {\n\t\t\t\tbuild.onResolve({ filter }, async (args) => {\n\t\t\t\t\tif (args.resolveDir === temporaryDirectory) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\treturn build.resolve(args.path, {\n\t\t\t\t\t\timporter: args.importer,\n\t\t\t\t\t\tkind: args.kind,\n\t\t\t\t\t\tnamespace: args.namespace,\n\t\t\t\t\t\tresolveDir: temporaryDirectory,\n\t\t\t\t\t\twith: args.with,\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t},\n\t\t}\n\n\t\treturn await useResolver(plugin)\n\t} finally {\n\t\tawait fs.rm(temporaryDirectory, { force: true, recursive: true })\n\t}\n}\n\nfunction escapeRegExp(value: string): string {\n\treturn value.replace(/[.*+?^${}()|[\\]\\\\]/g, `\\\\$&`)\n}\n","import fs from \"node:fs/promises\"\nimport path from \"node:path\"\n\nimport {\n\tmeasureEntryWithPlugins,\n\tmeasureImportsWithPlugins,\n} from \"./measure.ts\"\nimport { withPackageResolver } from \"./package-resolution.ts\"\nimport type {\n\tTonnageConfig,\n\tTonnageExports,\n\tTonnageReport,\n\tTonnageRow,\n} from \"./types.ts\"\n\ntype PackageJson = {\n\texports?: unknown\n\tname?: string\n\tpeerDependencies?: Record<string, string>\n}\n\nexport async function createTonnageReport(\n\tconfig: TonnageConfig,\n\tconfigDirectory: string,\n): Promise<TonnageReport> {\n\tconst packageJsonPath = path.resolve(\n\t\tconfigDirectory,\n\t\tconfig.packageJson ?? `package.json`,\n\t)\n\tconst packageJson = JSON.parse(\n\t\tawait fs.readFile(packageJsonPath, `utf8`),\n\t) as PackageJson\n\tif (!packageJson.name) {\n\t\tthrow new Error(`${packageJsonPath} does not declare a package name.`)\n\t}\n\n\tconst resolveDirectory = path.dirname(packageJsonPath)\n\tconst peerDependencies = Object.keys(packageJson.peerDependencies ?? {})\n\tconst external = unique([...peerDependencies, ...(config.external ?? [])])\n\tconst exportImports = selectExportImports(\n\t\tpackageJson.name,\n\t\tpackageJson.exports,\n\t\tconfig.exports,\n\t)\n\n\tconst { exportRows, recipeRows } = await withPackageResolver(\n\t\tpackageJson.name,\n\t\tresolveDirectory,\n\t\tasync (packageResolver) => {\n\t\t\tconst exportRows = await Promise.all(\n\t\t\t\texportImports.map(async (specifier): Promise<TonnageRow> => ({\n\t\t\t\t\t...(await measureImportsWithPlugins([specifier], {\n\t\t\t\t\t\texternal,\n\t\t\t\t\t\tplatform: config.platform,\n\t\t\t\t\t\tplugins: [packageResolver],\n\t\t\t\t\t\tresolveDirectory,\n\t\t\t\t\t\ttarget: config.target,\n\t\t\t\t\t})),\n\t\t\t\t\timports: [specifier],\n\t\t\t\t\tname: specifier,\n\t\t\t\t})),\n\t\t\t)\n\t\t\tconst recipeRows = await Promise.all(\n\t\t\t\t(config.recipes ?? []).map(async (recipe) => ({\n\t\t\t\t\t...(await measureEntryWithPlugins(recipe.entry, {\n\t\t\t\t\t\texternal: unique([...external, ...(recipe.external ?? [])]),\n\t\t\t\t\t\tplatform: recipe.platform ?? config.platform,\n\t\t\t\t\t\tplugins: [packageResolver],\n\t\t\t\t\t\tresolveDirectory,\n\t\t\t\t\t\ttarget: config.target,\n\t\t\t\t\t})),\n\t\t\t\t\tentry: recipe.entry,\n\t\t\t\t\tname: recipe.name,\n\t\t\t\t})),\n\t\t\t)\n\n\t\t\treturn { exportRows, recipeRows }\n\t\t},\n\t)\n\n\treturn {\n\t\texports: exportRows,\n\t\tpackageName: packageJson.name,\n\t\trecipes: recipeRows,\n\t}\n}\n\nexport function renderTonnageMarkdown(\n\treport: TonnageReport,\n\tconfig: TonnageConfig = {},\n): string {\n\tconst hasExports = report.exports.length > 0\n\tconst hasRecipes = report.recipes.length > 0\n\tconst hasMultipleSections = hasExports && hasRecipes\n\tconst lines = [\n\t\t`## ${config.heading ?? `Bundle size`}`,\n\t\t``,\n\t\t...methodologyLines({ hasExports, hasRecipes }),\n\t]\n\n\tif (hasExports) {\n\t\tlines.push(\n\t\t\t``,\n\t\t\t...(hasMultipleSections ? [`### Package exports`, ``] : []),\n\t\t\t...markdownTable(\n\t\t\t\t[`Import`, `Minified JS`, `Gzip JS`],\n\t\t\t\treport.exports.map((row) => [\n\t\t\t\t\tcode(row.name),\n\t\t\t\t\tformatBytes(row.rawBytes),\n\t\t\t\t\tformatBytes(row.gzipBytes),\n\t\t\t\t]),\n\t\t\t\t[`left`, `right`, `right`],\n\t\t\t),\n\t\t)\n\t}\n\n\tif (hasRecipes) {\n\t\tlines.push(\n\t\t\t``,\n\t\t\t...(hasMultipleSections ? [`### Usage examples`, ``] : []),\n\t\t\t...markdownTable(\n\t\t\t\t[`Recipe`, `Entry`, `Minified JS`, `Gzip JS`],\n\t\t\t\treport.recipes.map((row) => [\n\t\t\t\t\tescapeCell(row.name),\n\t\t\t\t\tcode(row.entry),\n\t\t\t\t\tformatBytes(row.rawBytes),\n\t\t\t\t\tformatBytes(row.gzipBytes),\n\t\t\t\t]),\n\t\t\t\t[`left`, `left`, `right`, `right`],\n\t\t\t),\n\t\t)\n\t}\n\n\tlines.push(\n\t\t``,\n\t\t`Report maintained with [tonnage](https://github.com/jeremybanka/tonnage).`,\n\t)\n\n\treturn lines.join(`\\n`)\n}\n\nfunction methodologyLines({\n\thasExports,\n\thasRecipes,\n}: {\n\thasExports: boolean\n\thasRecipes: boolean\n}): string[] {\n\tconst lines: string[] = []\n\tif (hasExports) {\n\t\tlines.push(`Package export sizes include complete runtime export surfaces.`)\n\t}\n\tif (hasRecipes) {\n\t\tlines.push(\n\t\t\t`Usage example sizes bundle their entry files and tree-shake unused exports.`,\n\t\t)\n\t}\n\tlines.push(\n\t\t`Sizes are exact minified and level-9 gzip JavaScript byte counts. Declarations, source maps, CSS, and other assets are excluded. Peer dependencies stay external, and shared modules are counted once per bundle.`,\n\t)\n\treturn lines\n}\n\nfunction selectExportImports(\n\tpackageName: string,\n\texportsValue: unknown,\n\tselection: TonnageConfig[`exports`],\n): string[] {\n\tif (selection === false) {\n\t\treturn []\n\t}\n\n\tconst exportKeys = getExportKeys(exportsValue)\n\tconst options: TonnageExports = typeof selection === `object` ? selection : {}\n\tconst included = options.include\n\t\t? [...options.include]\n\t\t: exportKeys.filter((exportKey) => exportKey !== `./package.json`)\n\tconst excluded = new Set(options.exclude ?? [])\n\n\treturn included\n\t\t.filter((exportKey) => !excluded.has(exportKey))\n\t\t.map((exportKey) => toPackageImport(packageName, exportKey))\n}\n\nfunction getExportKeys(exportsValue: unknown): string[] {\n\tif (typeof exportsValue === `string` || Array.isArray(exportsValue)) {\n\t\treturn [`.`]\n\t}\n\tif (!exportsValue || typeof exportsValue !== `object`) {\n\t\treturn [`.`]\n\t}\n\n\tconst keys = Object.keys(exportsValue)\n\treturn keys.some((key) => key.startsWith(`.`)) ? keys : [`.`]\n}\n\nfunction toPackageImport(packageName: string, exportKey: string): string {\n\tif (exportKey === `.`) {\n\t\treturn packageName\n\t}\n\tif (!exportKey.startsWith(`./`)) {\n\t\tthrow new Error(`Invalid package export key: ${exportKey}`)\n\t}\n\treturn `${packageName}/${exportKey.slice(2)}`\n}\n\nfunction formatBytes(bytes: number): string {\n\treturn `${new Intl.NumberFormat(`en-US`).format(bytes)} B`\n}\n\nfunction code(value: string): string {\n\treturn `<code>${escapeCell(value)}</code>`\n}\n\nfunction escapeCell(value: string): string {\n\treturn value.replaceAll(`&`, `&amp;`).replaceAll(`|`, `&#124;`)\n}\n\nfunction unique(values: readonly string[]): string[] {\n\treturn [...new Set(values)]\n}\n\nfunction markdownTable(\n\theadings: readonly string[],\n\trows: readonly (readonly string[])[],\n\talignments: readonly (`left` | `right`)[],\n): string[] {\n\tconst widths = headings.map((heading, column) =>\n\t\tMath.max(heading.length, ...rows.map((row) => row[column]?.length ?? 0), 3),\n\t)\n\tconst renderRow = (cells: readonly string[]): string =>\n\t\t`| ${cells.map((cell, column) => padCell(cell, widths[column] ?? 3, alignments[column] ?? `left`)).join(` | `)} |`\n\tconst separator = widths.map((width, column) =>\n\t\talignments[column] === `right`\n\t\t\t? `${`-`.repeat(width - 1)}:`\n\t\t\t: `-`.repeat(width),\n\t)\n\n\treturn [renderRow(headings), renderRow(separator), ...rows.map(renderRow)]\n}\n\nfunction padCell(\n\tcell: string,\n\twidth: number,\n\talignment: `left` | `right`,\n): string {\n\treturn alignment === `right` ? cell.padStart(width) : cell.padEnd(width)\n}\n","import fs from \"node:fs/promises\"\nimport path from \"node:path\"\n\nimport { updateGeneratedSection } from \"./readme.ts\"\nimport { createTonnageReport, renderTonnageMarkdown } from \"./report.ts\"\nimport type { TonnageConfig, TonnageMode, TonnageRunResult } from \"./types.ts\"\n\nexport async function runTonnage(\n\tconfig: TonnageConfig,\n\toptions: {\n\t\tconfigDirectory?: string\n\t\tmode: TonnageMode\n\t},\n): Promise<TonnageRunResult> {\n\tconst configDirectory = path.resolve(options.configDirectory ?? process.cwd())\n\tconst readmePath = path.resolve(configDirectory, config.readme ?? `README.md`)\n\tconst oldReadme = await fs.readFile(readmePath, `utf8`)\n\tconst report = await createTonnageReport(config, configDirectory)\n\tconst markdown = renderTonnageMarkdown(report, config)\n\tconst newReadme = updateGeneratedSection(oldReadme, markdown, config.marker)\n\tconst changed = oldReadme !== newReadme\n\n\tif (options.mode === `write` && changed) {\n\t\tawait fs.writeFile(readmePath, newReadme)\n\t}\n\n\treturn { changed, readmePath, report }\n}\n"],"mappings":"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"}