diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index cfd2fab8..f965e6ce 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -50,6 +50,14 @@ jobs:
run: pnpm publish --provenance --no-git-checks
continue-on-error: true
+ - name: Prepare browser package
+ if: steps.pnpm-publish.outcome == 'success'
+ run: pnpm run prepare:browser-package
+
+ - name: Publish browser package
+ if: steps.pnpm-publish.outcome == 'success'
+ run: npm publish ./dist/browser-package --tag browser --provenance --ignore-scripts
+
- name: Check git status
run: git status && git diff
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5e03396e..79c2ef8a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+
+- Build: Ship a separate minified browser build (`dist/browser/toml-patch.js`), published under the `browser` npm dist-tag, for direct `
+
```
## API
diff --git a/browser-tests/browser-build-smoke.spec.ts b/browser-tests/browser-build-smoke.spec.ts
new file mode 100644
index 00000000..fd1b23ed
--- /dev/null
+++ b/browser-tests/browser-build-smoke.spec.ts
@@ -0,0 +1,49 @@
+import { test, expect } from '@playwright/test';
+import { readFileSync, statSync } from 'fs';
+import { join } from 'path';
+
+const bundlePath = join(process.cwd(), 'dist/browser/toml-patch.js');
+const bundle = readFileSync(bundlePath, 'utf-8');
+
+// Load the bundle into the page via a blob URL so it runs in a real browser
+// module context — no Node.js APIs available.
+async function loadTOML(page: import('@playwright/test').Page) {
+ await page.goto('about:blank');
+ await page.evaluate(async (src: string) => {
+ const blob = new Blob([src], { type: 'application/javascript' });
+ const url = URL.createObjectURL(blob);
+ (window as any).__TOML__ = await import(url);
+ URL.revokeObjectURL(url);
+ }, bundle);
+}
+
+test.beforeEach(async ({ page }) => {
+ await loadTOML(page);
+});
+
+test('parse should work in real browser', async ({ page }) => {
+ const result = await page.evaluate(() =>
+ (window as any).__TOML__.parse('key = "hello"')
+ );
+ expect(result).toEqual({ key: 'hello' });
+});
+
+test('stringify should work in real browser', async ({ page }) => {
+ const result = await page.evaluate(() =>
+ (window as any).__TOML__.stringify({ key: 'hello' })
+ );
+ expect(result).toBe('key = "hello"\n');
+});
+
+test('patch should work in real browser', async ({ page }) => {
+ const result = await page.evaluate(() =>
+ (window as any).__TOML__.patch('key = "hello"\n', { key: 'world' })
+ );
+ expect(result).toBe('key = "world"\n');
+});
+
+test('browser build stays minified and smaller than the main build', () => {
+ const mainSize = statSync(join(process.cwd(), 'dist/toml-patch.js')).size;
+ const browserSize = statSync(bundlePath).size;
+ expect(browserSize).toBeLessThan(mainSize);
+});
diff --git a/package.json b/package.json
index fe731c37..150b0f86 100644
--- a/package.json
+++ b/package.json
@@ -59,9 +59,9 @@
"bench:parse": "node benchmark/parse-benchmark.mjs",
"bench:stringify": "node benchmark/stringify-benchmark.mjs",
"profile": "node benchmark/profile.mjs",
- "build": "tsdown",
- "build:profile": "cross-env PROFILE_BUILD=1 tsdown",
+ "build": "rimraf dist && tsdown",
"build:demo": "node scripts/build-demo.mjs",
+ "prepare:browser-package": "node scripts/prepare-browser-package.mjs",
"prepublishOnly": "pnpm run build",
"lint": "oxlint",
"lint:fix": "oxlint --fix",
@@ -77,7 +77,6 @@
"@types/js-yaml": "^4.0.0",
"@types/node": "^24.12.3",
"benchmark": "^2",
- "cross-env": "^10.1.0",
"dedent": "^1.5.3",
"glob": "^13.0.0",
"js-yaml": "4.3.1",
@@ -97,7 +96,7 @@
"printWidth": 100
},
"files": [
- "dist/"
+ "dist/toml-patch.*"
],
"json-comments": {
"tips": "Please install the JsonComments plugin to enable commenting functionality for JSON files, see: https://github.com/zhangfisher/json_comments_extension",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 80f24e9d..25a54e09 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -38,9 +38,6 @@ importers:
benchmark:
specifier: ^2
version: 2.1.4
- cross-env:
- specifier: ^10.1.0
- version: 10.1.0
dedent:
specifier: ^1.5.3
version: 1.7.2
@@ -96,9 +93,6 @@ packages:
'@emnapi/wasi-threads@1.2.2':
resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==}
- '@epic-web/invariant@1.0.0':
- resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==}
-
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@@ -805,11 +799,6 @@ packages:
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
- cross-env@10.1.0:
- resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==}
- engines: {node: '>=20'}
- hasBin: true
-
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -1382,8 +1371,6 @@ snapshots:
tslib: 2.8.1
optional: true
- '@epic-web/invariant@1.0.0': {}
-
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -1813,11 +1800,6 @@ snapshots:
convert-source-map@2.0.0: {}
- cross-env@10.1.0:
- dependencies:
- '@epic-web/invariant': 1.0.0
- cross-spawn: 7.0.6
-
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
diff --git a/scripts/prepare-browser-package.mjs b/scripts/prepare-browser-package.mjs
new file mode 100644
index 00000000..e8e2e0ae
--- /dev/null
+++ b/scripts/prepare-browser-package.mjs
@@ -0,0 +1,42 @@
+import {
+ cpSync,
+ mkdirSync,
+ readFileSync,
+ rmSync,
+ writeFileSync,
+} from 'node:fs';
+import { join } from 'node:path';
+
+const root = process.cwd();
+const packageJson = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
+const browserPackageDir = join(root, 'dist', 'browser-package');
+const browserFile = 'dist/browser/toml-patch.js';
+
+rmSync(browserPackageDir, { force: true, recursive: true });
+mkdirSync(join(browserPackageDir, 'dist', 'browser'), { recursive: true });
+cpSync(join(root, 'dist', 'browser', 'toml-patch.js'), join(browserPackageDir, browserFile));
+
+const browserPackageJson = {
+ name: packageJson.name,
+ version: `${packageJson.version}-browser`,
+ description: packageJson.description,
+ homepage: packageJson.homepage,
+ repository: packageJson.repository,
+ license: packageJson.license,
+ type: 'module',
+ files: ['dist/browser/'],
+ exports: {
+ '.': {
+ import: `./${browserFile}`,
+ default: `./${browserFile}`,
+ },
+ },
+ publishConfig: {
+ access: 'public',
+ },
+};
+
+writeFileSync(
+ join(browserPackageDir, 'package.json'),
+ `${JSON.stringify(browserPackageJson, null, 2)}\n`,
+);
diff --git a/tsdown.config.ts b/tsdown.config.ts
index d453464a..02337eb6 100644
--- a/tsdown.config.ts
+++ b/tsdown.config.ts
@@ -4,18 +4,42 @@ import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const pkg = require('./package.json');
-export default defineConfig({
- entry: {
- 'toml-patch': 'src/index.ts',
+const banner = `//! ${pkg.name} v${pkg.version} - ${pkg.homepage} - @license: ${pkg.license}`;
+
+export default defineConfig([
+ {
+ // Main build: consumed by bundlers (webpack/rollup/esbuild/vite) and Node.
+ // Left unminified so downstream bundlers get real names/structure for
+ // tree-shaking and dead-code elimination, and readable stack traces —
+ // they'll apply their own minification at the end of their own build anyway.
+ entry: {
+ 'toml-patch': 'src/index.ts',
+ },
+ format: 'esm',
+ outDir: 'dist',
+ clean: false,
+ dts: true,
+ minify: false,
+ fixedExtension: false,
+ banner: {
+ js: banner,
+ },
},
- format: 'esm',
- outDir: 'dist',
- clean: true,
- dts: true,
- // Skip minification for profiling builds so function names are readable.
- minify: !process.env.PROFILE_BUILD,
- fixedExtension: false,
- banner: {
- js: `//! ${pkg.name} v${pkg.version} - ${pkg.homepage} - @license: ${pkg.license}`,
+ {
+ // Browser build: a single minified ESM file for direct
+ //