Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +54 to +55

- 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

Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<script type="module">` usage via a CDN. The main `dist/toml-patch.js` build is now unminified so bundler consumers get better tree-shaking and readable stack traces.

### Fixed

- Docs: The README's browser install snippet loaded an ESM file via a non-module `<script src="...">` tag, which throws a `SyntaxError` in real browsers. It now uses `<script type="module">` and the new browser build.

## [3.0.4] - 2026-08-24

### Fixed
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ $ npm install --save @decimalturn/toml-patch
For browser usage, you can use unpkg:

```html
<script src="https://unpkg.com/@decimalturn/toml-patch/dist/toml-patch.js"></script>
<script type="module">
import * as TOML from 'https://unpkg.com/@decimalturn/toml-patch@browser/dist/browser/toml-patch.js';
</script>
```

## API
Expand Down
49 changes: 49 additions & 0 deletions browser-tests/browser-build-smoke.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
18 changes: 0 additions & 18 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 42 additions & 0 deletions scripts/prepare-browser-package.mjs
Original file line number Diff line number Diff line change
@@ -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`,
);
50 changes: 37 additions & 13 deletions tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
// <script type="module"> usage via a CDN (unpkg/jsdelivr), where users
// pay for every byte on every page load and have no bundler of their own.
entry: {
'toml-patch': 'src/index.ts',
},
format: 'esm',
outDir: 'dist/browser',
clean: false,
dts: false,
minify: true,
fixedExtension: false,
banner: {
js: banner,
},
},
});
]);