chore: packaging polish for 0.1.0

- 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.
main
Buddy 2 weeks ago
parent c03eb77d5b
commit ebd6f20cdf

@ -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).
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.*` | 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) |
| -------- | --------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 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.

@ -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

@ -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

@ -3,12 +3,13 @@
## 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'` |
| `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` |
| -------- | --------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| 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 <bump>` 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

@ -14,9 +14,11 @@
"docs/",
"demos/",
"dist/",
"scripts/",
"index.html",
"*.config.ts",
"tsconfig*.json",
"CLAUDE.md",
".idea/",
".claude/"
]

@ -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",

@ -0,0 +1,22 @@
// Keeps `jsr.json`'s version in step with `package.json`.
//
// Wired into the `version` npm lifecycle script, so `npm version <bump>`
// 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)`);
Loading…
Cancel
Save