You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
197 lines
11 KiB
Markdown
197 lines
11 KiB
Markdown
# 07 — Tooling & build
|
|
|
|
## Scripts
|
|
|
|
| `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
|
|
|
|
`package.json` pins `typescript@^7.0.2`. This choice constrains the lint
|
|
setup below. `tsconfig.json` highlights:
|
|
|
|
- `module: ESNext`, `moduleResolution: bundler`, `target: ESNext`
|
|
- `allowImportingTsExtensions` + `rewriteRelativeImportExtensions` — source
|
|
imports each other with explicit `.ts` extensions, and `tsc` rewrites them
|
|
to `.js` in the emitted output
|
|
- `declaration` + `declarationMap` — every module ships `.d.ts` + `.d.ts.map`
|
|
- `strict`, `isolatedModules`, `skipLibCheck`
|
|
- `include: ["src/**/*"]` only — `test/**` never reaches `dist/`. The
|
|
root-level `oxfmt.config.ts` is outside this glob too, so it's never
|
|
compiled. Note this glob **does** pull in `src/index.npm.ts` and
|
|
`src/core/globals.ts`, so the whole `src/` tree sees the `declare global`
|
|
augmentations at build/typecheck time even though the JSR entry doesn't
|
|
import them (see "Two registries" below).
|
|
|
|
## Linting: oxlint (not ESLint)
|
|
|
|
`oxlint.config.ts` — `oxlint@^1`, configured with `defineConfig`:
|
|
`plugins: ['typescript', 'unicorn', 'oxc']`, `categories` correctness→error /
|
|
suspicious+pedantic→warn, `max-lines*` and (in `test/`)
|
|
`max-classes-per-file` turned off.
|
|
|
|
**Why not `@typescript-eslint`:** it has no released version supporting
|
|
TypeScript 7 — its peer range caps at `<6.1.0`, and even loading
|
|
`@typescript-eslint/parser` crashes against TS 7's package shape. `oxlint`
|
|
has its own parser and never touches the `typescript` package, so it works
|
|
regardless of TS version.
|
|
|
|
**The tradeoff:** no type-aware rules — no `no-floating-promises`, no
|
|
`no-unnecessary-condition`, etc. Worth revisiting once `@typescript-eslint`
|
|
supports TS 7.
|
|
|
|
## Formatting: oxfmt
|
|
|
|
`oxfmt.config.ts` — `oxfmt` (the oxc project's formatter), configured with its
|
|
`defineConfig` default export:
|
|
|
|
```ts
|
|
export default defineConfig({
|
|
semi: true,
|
|
singleQuote: true,
|
|
trailingComma: 'all',
|
|
printWidth: 100,
|
|
tabWidth: 2,
|
|
});
|
|
```
|
|
|
|
- Same toolchain family as `oxlint`; single native binary, no plugin
|
|
ecosystem to pin against TS 7.
|
|
- Prettier-compatible options; the values above are Prettier's popular
|
|
defaults, so running `oxfmt` over the existing tree is a near no-op.
|
|
- oxfmt auto-discovers `oxfmt.config.ts` (search order: `.oxfmtrc.json` →
|
|
`.oxfmtrc.jsonc` → `oxfmt.config.ts` → `oxfmt.config.mts`); no `--config`
|
|
flag needed. It reads `.gitignore` automatically.
|
|
- One behavioural difference from Prettier: oxfmt formats fenced code blocks
|
|
_inside_ Markdown.
|
|
- Pre-1.0 — expect its output to shift between releases.
|
|
|
|
## Testing: Vitest + jsdom
|
|
|
|
`vitest.config.ts` — `environment: 'jsdom'`, `include: test/**/*.test.ts`,
|
|
`setupFiles: ['./test/setup.ts']`.
|
|
|
|
- **`test/setup.ts`** stubs `ResizeObserver` — jsdom doesn't implement it and
|
|
`leaflet-map.ts` constructs one unconditionally, so without the stub even
|
|
_importing_ the component throws. Nothing here depends on it firing. It also
|
|
`import`s `src/core/globals.ts` so the ambient `HTMLElementTagNameMap` /
|
|
`HTMLElementEventMap` augmentations are in scope for `tsconfig.test.json`
|
|
(tests import element modules directly, bypassing the npm entry that would
|
|
otherwise supply them).
|
|
- **Real Leaflet objects work under jsdom** for everything this library
|
|
verifies: option/attribute wiring, event forwarding, child binding. No
|
|
browser needed.
|
|
- **`onAdd()`-only state:** the `<img>`/`<video>` behind an overlay, a
|
|
marker's `dragging` handler, `marker.getElement()` — these exist only once
|
|
the layer is added to a real map. Tests touching them append through a
|
|
`<leaflet-map>` rather than standalone.
|
|
- **`test/core/with-props.test.ts`** tests the mixin against a fake
|
|
Leaflet-like class; the exception is child registration, which needs real
|
|
`Popup`/`Tooltip`/`Layer` because `#onChildRegister` branches on
|
|
`instanceof`.
|
|
- **`test/load-order.test.ts`** — see [06](./06-load-order.md); it's the only
|
|
test reproducing real page load order.
|
|
- `test/**` is typechecked separately via `tsconfig.test.json` (`extends` the
|
|
main config, `rootDir: '.'`, `noEmit`, adds `test/**/*` to `include`).
|
|
|
|
## Build output
|
|
|
|
`tsc` compiles `src/` → `dist/` as **individual ESM modules** — `.js` +
|
|
`.d.ts` + `.d.ts.map` per source file, no bundling step. `dist/elements/` and
|
|
`dist/components/` mirror the `src/` split one-to-one.
|
|
|
|
- Consumers import the package root (`dist/index.npm.js` on npm,
|
|
`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` /
|
|
`Popup` / `Tooltip`, so a second copy under our own `node_modules` would
|
|
silently break it. Kept in `devDependencies` too, for local dev and tests.
|
|
The Leaflet **type** packages (`@types/leaflet`, `@types/geojson`) are
|
|
regular `dependencies` — the emitted `.d.ts` reference them directly, and
|
|
`leaflet` ships no types of its own.
|
|
- `package.json` `exports`: `.` → `dist/index.npm.js` (+ types — this is the
|
|
npm entry, `src/index.npm.ts` compiled; see below);
|
|
`./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. 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.*` | `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 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
|
|
|
|
The two registries resolve **different entry files**:
|
|
|
|
- **`src/index.ts`** (JSR's `.`, and the shared base) — every element class,
|
|
every helper, the load-bearing `import './components/*.ts'` side effects.
|
|
**No `declare global`.**
|
|
- **`src/index.npm.ts`** (npm's `.`) — `import './core/globals.ts'; export *
|
|
from './index.ts';`. Compiled to `dist/index.npm.js`.
|
|
- **`src/core/globals.ts`** — the two ambient `declare global` blocks
|
|
(`HTMLElementTagNameMap`, `HTMLElementEventMap`). Imported only by
|
|
`src/index.npm.ts` and `test/setup.ts`; listed in `jsr.json`'s
|
|
`publish.exclude` so it's not even uploaded to JSR.
|
|
|
|
**Why:** JSR's "no slow types" check (below) rejects `declare global` anywhere
|
|
in the module graph reachable from `jsr.json`'s `exports`. Keeping the
|
|
augmentations in a module that graph never reaches lets JSR publish with full
|
|
fast types while npm consumers still get `querySelector('leaflet-map')` /
|
|
`addEventListener('leaflet-register', …)` typing for free through the package
|
|
root. JSR (`jsr:@buddy/leaflet-components`) consumers don't get the ambient
|
|
augmentations and supply their own if they want them.
|
|
|
|
### JSR "no slow types"
|
|
|
|
JSR statically analyses the public API of what it publishes and refuses
|
|
constructs it can't resolve without a full `tsc` run. For this package that
|
|
means:
|
|
|
|
- **No `declare global`** anywhere reachable from `jsr.json`'s `exports` —
|
|
handled by the entry split above.
|
|
- **No call expression as a superclass.** `class Foo extends WithProps({…})`
|
|
is rejected; every element file uses `const Base = WithProps(PROPS)` then
|
|
`extends Base` instead.
|
|
- **Explicit types on anything that leaks into the public API.** `const PROPS`
|
|
gets a written-out `{ key: PropDef<…>; … }` annotation (or `as const` when
|
|
every value is a bare reference); `const Base` gets
|
|
`: LeafletElementConstructor<TheLeafletClass, typeof PROPS>`; the shared
|
|
fragments in `shared-props.ts` carry their own object-type annotations; the
|
|
`Positional<T, Obj>` alias in `props.ts` exists to keep those annotations
|
|
short.
|
|
|
|
`npx jsr publish --dry-run` runs the full check offline and is the gate:
|
|
it must print **"Success"** with zero slow-type errors before publishing.
|
|
Pass `--allow-dirty` to run it against an uncommitted tree.
|