From 3b207922dd9dffb5d9e7a370478127e11bf0720d Mon Sep 17 00:00:00 2001 From: Martin Leduc <31558169+DecimalTurn@users.noreply.github.com> Date: Mon, 24 Aug 2026 17:13:12 -0400 Subject: [PATCH 1/2] feat: publish browser build separately --- .github/workflows/publish.yml | 8 ++++ CHANGELOG.md | 8 ++++ README.md | 4 +- browser-tests/browser-build-smoke.spec.ts | 49 ++++++++++++++++++++++ package.json | 7 ++-- pnpm-lock.yaml | 18 -------- scripts/prepare-browser-package.mjs | 42 +++++++++++++++++++ tsdown.config.ts | 50 +++++++++++++++++------ 8 files changed, 150 insertions(+), 36 deletions(-) create mode 100644 browser-tests/browser-build-smoke.spec.ts create mode 100644 scripts/prepare-browser-package.mjs 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..6e8cff0a --- /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.0`, + 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 + //