From ebd6f20cdf5da94651b6bf7970f198de7b5049ed Mon Sep 17 00:00:00 2001 From: Buddy Date: Tue, 8 Sep 2026 21:57:24 -0700 Subject: [PATCH] chore: packaging polish for 0.1.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ship `src/` in the npm tarball alongside `dist/` — the emitted `.d.ts.map` reference `../../src/*.ts`, so "go to definition" only lands on real code if the source is there; also gives npm/JSR source parity. (+16 kB packed.) - `sideEffects` becomes an explicit allowlist (`dist/index.js`, `dist/index.npm.js`, `dist/components/*.js`) instead of `true`, so bundlers can tree-shake the side-effect-free `elements/*` and `core/*`. - add `scripts/sync-version.mjs`: copies `package.json` version into `jsr.json`. Wired into the `version` npm lifecycle hook (so `npm version` bumps and stages both) and re-run in `prepublishOnly`. - add `repository` / `bugs` / `homepage`; `author` -> "Buddy Sandidge" (was "buddy", now matches LICENSE); a few more `keywords`. - note in README/CLAUDE/docs that per-component deep imports (`./elements/*.js`, `./components/*.js`) are npm-only — JSR has no subpath patterns, so `jsr.json` exposes just `.` and `./elements`. - exclude `scripts/` and `CLAUDE.md` from the JSR tarball. Verified: `typecheck`, `lint`, `test` (76), `build`, `jsr publish --dry-run` ("Success"), and a fresh `npm pack` consumer typecheck all pass. --- CLAUDE.md | 21 ++++++++++------ README.md | 4 ++- docs/01-architecture.md | 3 ++- docs/07-tooling-and-build.md | 47 +++++++++++++++++++++++------------- jsr.json | 2 ++ package.json | 31 +++++++++++++++++++----- scripts/sync-version.mjs | 22 +++++++++++++++++ 7 files changed, 97 insertions(+), 33 deletions(-) create mode 100644 scripts/sync-version.mjs diff --git a/CLAUDE.md b/CLAUDE.md index c413c15..d99163e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,7 +12,7 @@ stays a terse working cheat-sheet; `docs/` is the "why". ```bash npm run build # tsc emits individual ESM modules to dist/ (no bundling) npm run typecheck # tsc --noEmit, then tsc -p tsconfig.test.json for test/ -npm run lint # oxlint over src/ and test/ +npm run lint # oxlint over src/, test/, scripts/ npm run format # oxfmt formatting npm run test # vitest run npm run test:watch # vitest, watch mode @@ -45,7 +45,7 @@ Every component is split across two files with the same basename: - **`src/elements/leaflet-foo.ts`** — a `const PROPS`, a `const Base = WithProps(PROPS)` (explicitly typed — see below), then `export default class LeafletFooElement extends Base {...}`. The class only; **no `customElements.define`**, no side effects. `src/elements/index.ts` is a barrel re-exporting all of them by name (`LeafletFooElement`), order-free because nothing here registers a tag. - **`src/components/leaflet-foo.ts`** — three lines: `import LeafletFooElement from '../elements/leaflet-foo.ts'`, `customElements.define('leaflet-foo', LeafletFooElement)`, `export { LeafletFooElement }`. Importing this module (or `src/index.ts` / `src/index.npm.ts`) is what actually registers the tag. -Most consumers just import the package root (or a `components/*` module) and get the tags defined. The `elements/*` classes exist for plugin authors who want to subclass an element or register it under a different tag name without triggering the built-in `define`. Package subpath exports (`leaflet-web-components/elements`, `.../elements/leaflet-foo.js`, `.../components/leaflet-foo.js`) map straight onto `dist/`. +Most consumers just import the package root (or a `components/*` module) and get the tags defined. The `elements/*` classes exist for plugin authors who want to subclass an element or register it under a different tag name without triggering the built-in `define`. Package subpath exports (`leaflet-web-components/elements`, `.../elements/leaflet-foo.js`, `.../components/leaflet-foo.js`) map straight onto `dist/` — the per-file ones are npm-only, since JSR (`jsr.json` `exports`) has no subpath patterns and exposes just `.` and `./elements`. ### Two package entry points: `src/index.ts` vs `src/index.npm.ts` @@ -113,13 +113,18 @@ If you add or change what events a component fires, keep `#forwardEvents`'s `det ### Output & dual publish -`tsc` compiles `src/` → `dist/` as individual ESM modules (`.js` + `.d.ts` + `.d.ts.map`) — no bundling step, so `dist/elements/` and `dist/components/` mirror `src/`. There is no CJS or UMD build. Leaflet is always external (never bundled). Imports within source use `.ts` extensions; `rewriteRelativeImportExtensions` in tsconfig strips them to `.js` in the emitted `.js` (the `.d.ts` keep `.ts` specifiers, which TS resolves to the sibling `.d.ts`). +`tsc` compiles `src/` → `dist/` as individual ESM modules (`.js` + `.d.ts` + `.d.ts.map`) — no bundling step, so `dist/elements/` and `dist/components/` mirror `src/`. There is no CJS or UMD build. Imports within source use `.ts` extensions; `rewriteRelativeImportExtensions` strips them to `.js` in the emitted `.js` (the `.d.ts` keep `.ts` specifiers, which TS resolves to the sibling `.d.ts`). -Two registries, different entry files (both pinned to the same version — keep them in step): +- **`leaflet` is a `peerDependency`** (`^1.9.4`), also kept in `devDependencies` for local dev/tests — a second copy would break the `instanceof`-based nesting protocol. **`@types/leaflet` + `@types/geojson` are regular `dependencies`** (the emitted `.d.ts` reference them; `leaflet` ships no types). +- **`files` ships `dist/` and `src/`** — the `.d.ts.map` point at `../../src/*.ts`, so source has to be there for "go to definition" (and it matches JSR). +- **`sideEffects`** is the explicit list `dist/index.js`, `dist/index.npm.js`, `dist/components/*.js` — the only modules that `customElements.define()` on import; everything else is tree-shakeable. +- **No `./dist/*` export.** The per-file `./elements/*.js` / `./components/*.js` subpaths are **npm-only** (JSR has no subpath patterns). -| Registry | Package name | Entry | Ships | -| -------- | --------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| npm | `leaflet-web-components` | `package.json` `.`/`main`/`module`/`types` → `dist/index.npm.*` | compiled `dist/` + `README.md` + `LICENSE` (`files` field); `prepublishOnly` runs `typecheck` + `test` + `build` | -| JSR | `@buddy/leaflet-components` | `jsr.json` `exports` → `./src/index.ts` (+ `./elements`) | TypeScript source directly, minus `jsr.json`'s `publish.exclude` (which drops `src/index.npm.ts`, `src/core/globals.ts`, `test/`, `docs/`, `dist/`, configs) | +Two registries, different entry files. Both carry a `version`; `scripts/sync-version.mjs` (the `version` lifecycle hook, and re-run in `prepublishOnly`) copies `package.json`'s version into `jsr.json`. + +| Registry | Package name | Entry | Ships | +| -------- | --------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| npm | `leaflet-web-components` | `package.json` `.`/`main`/`module`/`types` → `dist/index.npm.*` | `dist/` + `src/` + `README.md` + `LICENSE` (`files` field); `prepublishOnly` runs `sync-version` + `typecheck` + `test` + `build` | +| JSR | `@buddy/leaflet-components` | `jsr.json` `exports` → `./src/index.ts` (+ `./elements`) | TypeScript source directly, minus `jsr.json`'s `publish.exclude` (`src/index.npm.ts`, `src/core/globals.ts`, `test/`, `docs/`, `scripts/`, `dist/`, configs, `CLAUDE.md`) | The npm entry (`src/index.npm.ts`) adds the `declare global` augmentations via `src/core/globals.ts`; the JSR entry (`src/index.ts`) can't, because JSR's "no slow types" check forbids `declare global` in its published graph. That constraint is also why every element file uses the explicitly-typed `const PROPS` / `const Base` shape (see the `WithProps` section). `npx jsr publish --dry-run` is the authoritative check; it must print "Success" with no slow-type errors. diff --git a/README.md b/README.md index 1344ec1..843fcca 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Also published to JSR as [`@buddy/leaflet-components`](https://jsr.io/@buddy/lea ## Import options -The package is ESM-only — `tsc` emits `dist/` as individual modules, one per component, with no bundling step. Deep imports work for registering only what you use. +The package is ESM-only — `tsc` emits `dist/` as individual modules, one per component, with no bundling step. Deep imports let you register only what you use. ```js // Registers all components @@ -27,6 +27,8 @@ import 'leaflet-web-components'; import 'leaflet-web-components/components/leaflet-marker.js'; ``` +Per-component deep imports (`./components/*.js`, `./elements/*.js`) are an **npm-only** convenience — JSR doesn't support subpath patterns, so from JSR the entry points are just the root and `@buddy/leaflet-components/elements` (the class barrel). JSR consumers who want selective registration import the class and call `customElements.define()` themselves. + Each component is two modules with the same basename: `components/leaflet-marker.js` is the side-effecting one that calls `customElements.define()`, and `elements/leaflet-marker.js` is just the class (`default` export, **no** `define`). Import from `elements/` when you want to subclass a component or register it under a different tag name: ```js diff --git a/docs/01-architecture.md b/docs/01-architecture.md index 9431d52..4514a29 100644 --- a/docs/01-architecture.md +++ b/docs/01-architecture.md @@ -33,7 +33,8 @@ extends Base` (where `const Base = WithProps(PROPS)`) — the class alone, no `package.json` maps `leaflet-web-components/elements`, `.../elements/leaflet-foo.js` and `.../components/leaflet-foo.js` onto the -matching `dist/` files. +matching `dist/` files. The per-file subpaths are npm-only — `jsr.json` +exposes just `.` and `./elements` (JSR has no subpath patterns). ### Two package entry points diff --git a/docs/07-tooling-and-build.md b/docs/07-tooling-and-build.md index a9702a8..fe8683c 100644 --- a/docs/07-tooling-and-build.md +++ b/docs/07-tooling-and-build.md @@ -2,13 +2,14 @@ ## Scripts -| `npm run …` | Does | -| --------------------- | ----------------------------------------------------------------------- | -| `build` | `rm -rf dist && tsc --outDir dist` | -| `typecheck` | `tsc --noEmit`, then `tsc -p tsconfig.test.json` | -| `lint` | `oxlint src test` | -| `format` | `oxfmt '*.md' 'docs/**/*.md' 'src/**/*.{ts,js,json,md}' 'test/**/*.ts'` | -| `test` / `test:watch` | `vitest run` / `vitest` | +| `npm run …` | Does | +| --------------------- | ---------------------------------------------------------------------------- | +| `build` | `rm -rf dist && tsc --outDir dist` | +| `typecheck` | `tsc --noEmit`, then `tsc -p tsconfig.test.json` | +| `lint` | `oxlint src test scripts` | +| `format` | `oxfmt` over `*.md`, `docs/`, `src/`, `test/`, `scripts/` | +| `test` / `test:watch` | `vitest run` / `vitest` | +| `sync-version` | copy `package.json` version → `jsr.json` (also the `version` lifecycle hook) | ## TypeScript 7 @@ -109,6 +110,10 @@ export default defineConfig({ `src/index.ts` on JSR — see "Publishing" below) or any single module directly. - **No CJS, no UMD** build. +- The npm tarball ships **`src/` as well as `dist/`** (`files` field) — the + `.d.ts.map` files reference `../../src/*.ts`, so shipping the source is what + makes "go to definition" land on real code, and it matches what JSR + publishes. - **Leaflet is always external** — never bundled, a bare `import` in the output. It's a **`peerDependency`** (`^1.9.4`), not a regular dependency: the nesting protocol keys off `instanceof` against Leaflet's own `Layer` / @@ -122,20 +127,28 @@ export default defineConfig({ `./elements` → `dist/elements/index.js` (the class barrel, no `define`); `./elements/*.js` and `./components/*.js` → the matching `dist/` module. There is deliberately **no `./dist/*`** wildcard — `core/*` is not a stable - contract, and the root export already surfaces the toolkit. -- `sideEffects: true` — the `components/*` modules (and both `index*.js` - entries) call `customElements.define()` at import time, so a bundler must - not tree-shake them away. The `elements/*` modules have no side effect. + contract, and the root export already surfaces the toolkit. The per-file + `./elements/*.js` / `./components/*.js` patterns are **npm-only**: JSR has + no subpath-pattern support, so `jsr.json` `exports` is just `.` + + `./elements`. +- `sideEffects` is an explicit allowlist — `dist/index.js`, + `dist/index.npm.js`, `dist/components/*.js` — the only modules that run + `customElements.define()` at import. Everything else (`elements/*`, + `core/*`, and `core/globals.js`, whose augmentation is type-only) is + side-effect-free, so a bundler may drop it when unused. ## Publishing to two registries -| Registry | Package name | Entry | Ships | -| -------- | --------------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | -| npm | `leaflet-web-components` | `package.json` `.`/`main`/`module`/`types` → `dist/index.npm.*` | compiled `dist/` + `README.md` + `LICENSE` (`files` field); `prepublishOnly` runs `typecheck` + `test` + `build` | -| JSR | `@buddy/leaflet-components` | `jsr.json` `exports` → `./src/index.ts` (+ `./elements`) | TypeScript source directly (JSR compiles per-consumer), minus `jsr.json`'s `publish.exclude` | +| Registry | Package name | Entry | Ships | +| -------- | --------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| npm | `leaflet-web-components` | `package.json` `.`/`main`/`module`/`types` → `dist/index.npm.*` | `dist/` + `src/` + `README.md` + `LICENSE` (`files` field); `prepublishOnly` runs `sync-version` + `typecheck` + `test` + `build` | +| JSR | `@buddy/leaflet-components` | `jsr.json` `exports` → `./src/index.ts` (+ `./elements`) | TypeScript source directly (JSR compiles per-consumer), minus `jsr.json`'s `publish.exclude` | -Both are pinned to the same version; keep them in step. JSR publishes `src/` -with its `.ts` import extensions intact, which JSR supports natively. +Both carry a `version` and must stay in step. `scripts/sync-version.mjs` +copies `package.json`'s version into `jsr.json`; it's wired into the `version` +npm lifecycle script (so `npm version ` updates and stages both) and +run again from `prepublishOnly`. JSR publishes `src/` with its `.ts` import +extensions intact, which JSR supports natively. ### The npm/JSR entry split diff --git a/jsr.json b/jsr.json index 48ff19f..ffdb570 100644 --- a/jsr.json +++ b/jsr.json @@ -14,9 +14,11 @@ "docs/", "demos/", "dist/", + "scripts/", "index.html", "*.config.ts", "tsconfig*.json", + "CLAUDE.md", ".idea/", ".claude/" ] diff --git a/package.json b/package.json index 1a66737..e07d2a8 100644 --- a/package.json +++ b/package.json @@ -24,28 +24,47 @@ }, "./package.json": "./package.json" }, - "sideEffects": true, + "sideEffects": [ + "./dist/index.js", + "./dist/index.npm.js", + "./dist/components/*.js" + ], "files": [ "dist", + "src", "README.md", "LICENSE" ], "scripts": { "build": "rm -rf dist && tsc --outDir dist", "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json", - "lint": "oxlint src test", - "format": "oxfmt '*.md' 'docs/**/*.md' 'src/**/*.{ts,js,json,md}' 'test/**/*.ts'", + "lint": "oxlint src test scripts", + "format": "oxfmt '*.md' 'docs/**/*.md' 'src/**/*.{ts,js,json,md}' 'test/**/*.ts' 'scripts/**/*.mjs'", "test": "vitest run", "test:watch": "vitest", - "prepublishOnly": "npm run typecheck && npm run test && npm run build" + "sync-version": "node scripts/sync-version.mjs", + "version": "npm run sync-version && git add jsr.json", + "prepublishOnly": "npm run sync-version && npm run typecheck && npm run test && npm run build" }, "keywords": [ "leaflet", + "leafletjs", "web-components", - "maps" + "custom-elements", + "webcomponents", + "maps", + "map" ], - "author": "buddy", + "author": "Buddy Sandidge", "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://git.buddy.wtf/lib/leaflet-components.git" + }, + "bugs": { + "url": "https://git.buddy.wtf/lib/leaflet-components/issues" + }, + "homepage": "https://git.buddy.wtf/lib/leaflet-components#readme", "type": "module", "dependencies": { "@types/geojson": "^7946.0.16", diff --git a/scripts/sync-version.mjs b/scripts/sync-version.mjs new file mode 100644 index 0000000..74de4e5 --- /dev/null +++ b/scripts/sync-version.mjs @@ -0,0 +1,22 @@ +// Keeps `jsr.json`'s version in step with `package.json`. +// +// Wired into the `version` npm lifecycle script, so `npm version ` +// updates both files and stages `jsr.json` into the same commit/tag. Also +// runs from `prepublishOnly` as a guard, and can be run by hand +// (`npm run sync-version`). Exits non-zero only on a real filesystem error; +// a no-op when the versions already match. +import { readFileSync, writeFileSync } from 'node:fs'; + +const pkgUrl = new URL('../package.json', import.meta.url); +const jsrUrl = new URL('../jsr.json', import.meta.url); + +const pkg = JSON.parse(readFileSync(pkgUrl, 'utf8')); +const jsr = JSON.parse(readFileSync(jsrUrl, 'utf8')); + +if (jsr.version === pkg.version) { + process.exit(0); +} + +jsr.version = pkg.version; +writeFileSync(jsrUrl, `${JSON.stringify(jsr, null, 2)}\n`); +console.log(`sync-version: jsr.json ${jsr.version} (from package.json)`);