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.
leaflet-components/docs/07-tooling-and-build.md

5.8 KiB

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
format oxfmt '*.md' 'src/**/*.{ts,js,json,md}' 'test/**/*.ts'
test / test:watch vitest run / vitest

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.

Linting: oxlint (not ESLint)

oxlint.config.tsoxlint@^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.tsoxfmt (the oxc project's formatter), configured with its defineConfig default export:

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.jsoncoxfmt.config.tsoxfmt.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.tsenvironment: '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.
  • 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; 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 dist/index.js (or any single module) directly.
  • No CJS, no UMD build.
  • Leaflet is always external — never bundled. It's a dependencies entry and a bare import in the output.
  • package.json exports: .dist/index.js (+ types); ./elementsdist/elements/index.js (the class barrel, no define); ./elements/*.js and ./components/*.js → the matching dist/ module; ./dist/* still there for deep imports.
  • sideEffects: true — the components/* modules (and index.js) call customElements.define() at import time, so a bundler must not tree-shake them away. The elements/* modules have no side effect.

Publishing to two registries

Registry Entry Ships
npm package.json main/module/typesdist/… compiled dist/ + README.md (files field); prepublishOnly runs build
JSR jsr.json exports./src/index.ts (+ ./elements) TypeScript source directly (JSR compiles per-consumer)

Both are version 0.1.0; keep them in step. JSR publishes src/ with its .ts import extensions intact, which JSR supports natively.