Ship MapView as the tabler-map.js add-on bundle

- tabler.js is back to its size before the maps
This commit is contained in:
codecalm committed 2026-10-04 23:29:57 +02:00
1 parent d460fb47da
commit 409482d5db
13 files changed
+73 -21

No files matched your search

+5 -2
View File
@@ -7,14 +7,17 @@ description: >-
# The framework JavaScript
`core/js` builds two shipped bundles. Everything here is public API and is bound by a size budget.
`core/js` builds three shipped bundles. Everything here is public API and is bound by a size budget.
## 1. Two entry points
## 1. Three entry points
| Entry | Ships as | Holds |
| -------------------- | ---------------------------------- | ---------------------------------------------------------------------------- |
| `js/tabler.ts` | `tabler.js` / `.esm.js` (+ `.min`) | plugin initialisers, the Bootstrap components, the `tabler` helper namespace |
| `js/tabler-theme.ts` | `tabler-theme.js` (+ variants) | the colour-mode/theme switcher only |
| `js/tabler-map.ts` | `tabler-map.js` (+ variants) | the `MapView` component, as an add-on that extends the `tabler` global |
`tabler-map.js` is loaded after `tabler.js`, and only on pages with a map: a component that few pages use goes in an add-on bundle like this one, not in `tabler.js`. Its UMD build uses `extend` (`core/.build/vite.config.mts`), so it adds its exports to the existing `tabler` global.
`tabler-theme.js` is loaded right after `<body>` and **not deferred**, so the chosen theme applies before the first paint. It stays tiny on purpose (bundlewatch: 1 kB raw, 800 B minified) — do not add anything to it that is not needed before paint.
+8 -2
View File
@@ -9,20 +9,26 @@ interface CreateViteConfigOptions {
outDir: string
banner?: string
minify?: boolean | 'esbuild'
/** UMD only: add the exports to an existing global of the same name instead of replacing it */
extend?: boolean
}
/**
* Creates a Vite configuration for building libraries
*/
export function createViteConfig({ entry, name, fileName, formats, outDir, banner, minify = false }: CreateViteConfigOptions): UserConfig {
export function createViteConfig({ entry, name, fileName, formats, outDir, banner, minify = false, extend = false }: CreateViteConfigOptions): UserConfig {
// Vite 8 (Rolldown) always emits const bindings and no longer accepts the
// Rollup-only generatedCode.constBindings option
const rollupOutput: { banner?: string } = {}
const rollupOutput: { banner?: string; extend?: boolean } = {}
if (banner) {
rollupOutput.banner = banner
}
if (extend) {
rollupOutput.extend = true
}
const config: UserConfig = {
// Library builds: never copy <root>/public into outDir (see copy-assets.ts).
publicDir: false,
+5 -1
View File
@@ -8,7 +8,10 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
const baseName = process.env.BASE_NAME || 'tabler'
const entryFile = baseName
const libraryName = baseName
// `tabler-map` is an add-on to the main bundle: its UMD build extends the
// `tabler` global instead of creating one of its own.
const isAddon = baseName === 'tabler-map'
const libraryName = isAddon ? 'tabler' : baseName
const bannerText = getBanner()
@@ -26,4 +29,5 @@ export default createViteConfig({
outDir: path.resolve(__dirname, '../dist/js'),
banner: bannerText,
minify: false,
extend: isAddon,
})
+6
View File
@@ -0,0 +1,6 @@
// The map component ships as its own bundle, so pages without a map do not
// carry it. Load it after `tabler.js`: the UMD build adds `MapView` to the
// `tabler` global.
import './src/map'
export { default as MapView } from './src/map'
-2
View File
@@ -4,7 +4,6 @@ import './src/confetti'
import './src/countup'
import './src/datepicker'
import './src/input-mask'
import './src/map'
import './src/dropdown'
import './src/sidebar'
import './src/tooltip'
@@ -27,7 +26,6 @@ export { default as Confetti } from './src/confetti'
export { default as CountUp } from './src/countup'
export { default as Datepicker } from './src/datepicker'
export { default as InputMask } from './src/input-mask'
export { default as MapView } from './src/map'
export { default as OtpInput } from './src/otp-input'
export { default as Sortable } from './src/sortable'
export { default as Sparkline } from './src/sparkline'
+1
View File
@@ -58,6 +58,7 @@
"maplibre-gl": {
"npm": "maplibre-gl",
"module": { "file": "dist/maplibre-gl.mjs", "global": "maplibregl" },
"addon": "tabler-map",
"css": ["dist/maplibre-gl.css"],
"extra": ["dist/maplibre-gl.mjs", "dist/maplibre-gl-shared.mjs", "dist/maplibre-gl-worker.mjs"]
},
+24 -6
View File
@@ -17,12 +17,14 @@
"lint:scss:vars": "find-unused-sass-variables scss/ --ignoreFiles scss/_variables-deprecated.scss",
"js": "pnpm run js:build && pnpm run js:min && pnpm run js:types",
"js:types": "tsc -p tsconfig.types.json",
"js:build": "concurrently \"pnpm run js:build:standalone\" \"pnpm run js:build:theme\"",
"js:build": "concurrently \"pnpm run js:build:standalone\" \"pnpm run js:build:theme\" \"pnpm run js:build:map\"",
"js:build:theme": "cross-env BASE_NAME=tabler-theme vite build --config .build/vite.config.mts",
"js:build:map": "cross-env BASE_NAME=tabler-map vite build --config .build/vite.config.mts",
"js:build:standalone": "cross-env BASE_NAME=tabler vite build --config .build/vite.config.mts",
"js:min": "concurrently \"pnpm run js:min:standalone\" \"pnpm run js:min:theme\"",
"js:min": "concurrently \"pnpm run js:min:standalone\" \"pnpm run js:min:theme\" \"pnpm run js:min:map\"",
"js:min:standalone": "concurrently \"terser dist/js/tabler.js --compress passes=2,ecma=2020 --ecma 2020 --mangle --source-map \\\"content=dist/js/tabler.js.map,filename=dist/js/tabler.min.js.map,url=tabler.min.js.map\\\" -o dist/js/tabler.min.js\" \"terser dist/js/tabler.esm.js --module --compress passes=2,ecma=2020 --ecma 2020 --mangle --source-map \\\"content=dist/js/tabler.esm.js.map,filename=dist/js/tabler.esm.min.js.map,url=tabler.esm.min.js.map\\\" -o dist/js/tabler.esm.min.js\"",
"js:min:theme": "concurrently \"terser dist/js/tabler-theme.js --compress passes=2,ecma=2020 --ecma 2020 --mangle --source-map \\\"content=dist/js/tabler-theme.js.map,filename=dist/js/tabler-theme.min.js.map,url=tabler-theme.min.js.map\\\" -o dist/js/tabler-theme.min.js\" \"terser dist/js/tabler-theme.esm.js --module --compress passes=2,ecma=2020 --ecma 2020 --mangle --source-map \\\"content=dist/js/tabler-theme.esm.js.map,filename=dist/js/tabler-theme.esm.min.js.map,url=tabler-theme.esm.min.js.map\\\" -o dist/js/tabler-theme.esm.min.js\"",
"js:min:map": "concurrently \"terser dist/js/tabler-map.js --compress passes=2,ecma=2020 --ecma 2020 --mangle --source-map \\\"content=dist/js/tabler-map.js.map,filename=dist/js/tabler-map.min.js.map,url=tabler-map.min.js.map\\\" -o dist/js/tabler-map.min.js\" \"terser dist/js/tabler-map.esm.js --module --compress passes=2,ecma=2020 --ecma 2020 --mangle --source-map \\\"content=dist/js/tabler-map.esm.js.map,filename=dist/js/tabler-map.esm.min.js.map,url=tabler-map.esm.min.js.map\\\" -o dist/js/tabler-map.esm.min.js\"",
"copy": "concurrently \"pnpm run copy:img\" \"pnpm run copy:libs\"",
"copy:img": "tsx .build/copy-img.ts",
"copy:libs": "tsx .build/copy-libs.ts",
@@ -212,19 +214,35 @@
},
{
"path": "./dist/js/tabler.js",
"maxSize": "61 kB"
"maxSize": "59 kB"
},
{
"path": "./dist/js/tabler.min.js",
"maxSize": "41 kB"
"maxSize": "40 kB"
},
{
"path": "./dist/js/tabler.esm.js",
"maxSize": "61 kB"
"maxSize": "59 kB"
},
{
"path": "./dist/js/tabler.esm.min.js",
"maxSize": "41 kB"
"maxSize": "40 kB"
},
{
"path": "./dist/js/tabler-map.js",
"maxSize": "10 kB"
},
{
"path": "./dist/js/tabler-map.esm.js",
"maxSize": "10 kB"
},
{
"path": "./dist/js/tabler-map.min.js",
"maxSize": "7 kB"
},
{
"path": "./dist/js/tabler-map.esm.min.js",
"maxSize": "7 kB"
},
{
"path": "./dist/js/tabler-theme.js",
+1 -1
View File
@@ -8,5 +8,5 @@
"rootDir": "js",
"outDir": "dist/types"
},
"include": ["js/tabler.ts", "js/tabler-theme.ts", "js/src/**/*"]
"include": ["js/tabler.ts", "js/tabler-theme.ts", "js/tabler-map.ts", "js/src/**/*"]
}
+10 -3
View File
@@ -61,12 +61,15 @@ MapLibre GL JS is published as an ES module. Tabler uses version 6. Load the sty
// The map component looks for the library here
window.maplibregl = maplibregl
</script>
<script src="${site.cdnUrl}/dist/js/tabler.min.js" defer></script>`}
<script src="${site.cdnUrl}/dist/js/tabler.min.js" defer></script>
<script src="${site.cdnUrl}/dist/js/tabler-map.min.js" defer></script>`}
/>
The map component is not part of `tabler.js`. It ships as a small add-on, `tabler-map.js`, so pages without a map do not carry it. Load it after `tabler.js`; it adds `MapView` to the `tabler` object.
The library loads two more files from the same folder, `maplibre-gl-shared.mjs` and `maplibre-gl-worker.mjs`, so keep the three together when you host them yourself.
With a bundler, install the package and set the global before you import Tabler:
With a bundler, install the package, set the global, and then import the add-on:
```bash
npm install maplibre-gl
@@ -77,8 +80,12 @@ import * as maplibregl from 'maplibre-gl'
import 'maplibre-gl/dist/maplibre-gl.css'
window.maplibregl = maplibregl
const { MapView } = await import('@tabler/core/dist/js/tabler-map.esm.js')
```
The add-on is imported after the global is set, because it turns the map elements on the page into maps as soon as it loads.
<Danger title="Use version 6.4.1 or newer">
Older versions, including all of version 5, have a known security issue in the code that cleans the attribution text.
</Danger>
@@ -316,7 +323,7 @@ Live versions of a route, an area, clusters, a bubble map, a globe with arcs and
### Via data attributes
An element with `data-bs-toggle="map"` becomes a map when the Tabler script loads:
An element with `data-bs-toggle="map"` becomes a map when the `tabler-map.js` script loads:
<CodeDocs name="map-init" file="core/js/src/map.ts" />
+3 -1
View File
@@ -142,7 +142,7 @@ const editFilePath =
// docs-libs for this page. In dev, file names stay unchanged
// (e.g. dist/plyr.min.js keeps .min).
type Lib = { npm?: string; js?: string[]; css?: string[]; module?: { file: string; global: string } };
type Lib = { npm?: string; js?: string[]; css?: string[]; module?: { file: string; global: string }; addon?: string };
const libEntries = Object.entries(libs as Record<string, Lib>);
const libUrl = (lib: Lib, file: string) =>
isExternal(file)
@@ -151,6 +151,7 @@ const libUrl = (lib: Lib, file: string) =>
const docsLibCss = libEntries
.filter(([name]) => docsLibs.includes(name))
.flatMap(([, lib]) => (lib.css ?? []).map((file) => libUrl(lib, file)));
const docsLibAddons = libEntries.filter(([name]) => docsLibs.includes(name)).flatMap(([, lib]) => (lib.addon ? [lib.addon] : []));
const docsLibModules = libEntries
.filter(([name]) => docsLibs.includes(name))
.flatMap(([, lib]) => (lib.module ? [`import * as lib from '${libUrl(lib, lib.module.file)}'; window.${lib.module.global} = lib`] : []));
@@ -525,6 +526,7 @@ const relatedPages = await Promise.all(
<!-- BEGIN GLOBAL MANDATORY SCRIPTS -->
<script is:inline src="/dist/js/tabler.js" defer></script>
{docsLibAddons.map((addon) => <script is:inline src={`/dist/js/${addon}.js`} defer />)}
<!-- END GLOBAL MANDATORY SCRIPTS -->
<PageScripts />
@@ -73,12 +73,13 @@ const themeAttributes = themeDataAttributes({ ...theme, 'navbar-position': navba
// whatever core's own watcher last emitted, which is unminified.
const min = import.meta.env.DEV ? '' : '.min'
type Lib = { npm?: string; js?: string[]; css?: string[]; module?: { file: string; global: string } }
type Lib = { npm?: string; js?: string[]; css?: string[]; module?: { file: string; global: string }; addon?: string }
const pageLibEntries = Object.entries(libs as Record<string, Lib>).filter(([name]) => pageLibs.includes(name))
const libHref = (lib: Lib, file: string) => `/dist/libs/${lib.npm}/${file}`
const libCssFiles = pageLibEntries.flatMap(([, lib]) => (lib.css ?? []).map((file) => libHref(lib, file)))
const libJsFiles = pageLibEntries.flatMap(([, lib]) => (lib.js ?? []).map((file) => libHref(lib, file)))
// ES-module-only libraries: imported in a module script and exposed as a global (see BaseLayout)
const libAddons = pageLibEntries.flatMap(([, lib]) => (lib.addon ? [lib.addon] : []))
const libModules = pageLibEntries.flatMap(([, lib]) => (lib.module ? [`import * as lib from '${libHref(lib, lib.module.file)}'; window.${lib.module.global} = lib`] : []))
---
@@ -182,6 +183,7 @@ const libModules = pageLibEntries.flatMap(([, lib]) => (lib.module ? [`import *
{libJsFiles.map((src) => <script is:inline src={src} defer />)}
{libModules.map((code) => <Fragment set:html={`<script type="module">${code}</script>`} />)}
<script is:inline src={`/dist/js/tabler${min}.js`} defer></script>
{libAddons.map((addon) => <script is:inline src={`/dist/js/${addon}${min}.js`} defer />)}
<PageScripts />
+3 -1
View File
@@ -45,12 +45,13 @@ const rtlSuffix = rtl ? '.rtl' : ''
// build instead of hardcoding one variant.
const min = import.meta.env.DEV ? '' : '.min'
type Lib = { npm?: string; js?: string[]; css?: string[]; module?: { file: string; global: string } }
type Lib = { npm?: string; js?: string[]; css?: string[]; module?: { file: string; global: string }; addon?: string }
const pageLibEntries = Object.entries(libs as Record<string, Lib>).filter(([name]) => pageLibs.includes(name))
const libHref = (lib: Lib, file: string) => `/dist/libs/${lib.npm}/${file}`
const libCssFiles = pageLibEntries.flatMap(([, lib]) => (lib.css ?? []).map((file) => libHref(lib, file)))
const libJsFiles = pageLibEntries.flatMap(([, lib]) => (lib.js ?? []).map((file) => libHref(lib, file)))
// ES-module-only libraries: imported in a module script and exposed as a global (see BaseLayout)
const libAddons = pageLibEntries.flatMap(([, lib]) => (lib.addon ? [lib.addon] : []))
const libModules = pageLibEntries.flatMap(([, lib]) => (lib.module ? [`import * as lib from '${libHref(lib, lib.module.file)}'; window.${lib.module.global} = lib`] : []))
---
@@ -173,6 +174,7 @@ const libModules = pageLibEntries.flatMap(([, lib]) => (lib.module ? [`import *
{libJsFiles.map((src) => <script is:inline src={src} defer />)}
{libModules.map((code) => <Fragment set:html={`<script type="module">${code}</script>`} />)}
<script is:inline src={`/dist/js/tabler${min}.js`} defer></script>
{libAddons.map((addon) => <script is:inline src={`/dist/js/${addon}${min}.js`} defer />)}
<!-- components that render via CaptureScript (e.g. Chart) queue their init script
into shared/lib/page-scripts instead of inlining it where used; this drains it. -->
+4 -1
View File
@@ -39,7 +39,7 @@ const isProduction = process.env.VERCEL_ENV === 'production'
// RTL swaps tabler/plugin stylesheets for their .rtl.css variants (demo stays LTR)
const rtlSuffix = rtl ? '.rtl' : ''
type Lib = { npm?: string; js?: string[]; css?: string[]; head?: boolean; module?: { file: string; global: string } }
type Lib = { npm?: string; js?: string[]; css?: string[]; head?: boolean; module?: { file: string; global: string }; addon?: string }
const unknownLibs = pageLibs.filter((name) => !(name in libs))
if (unknownLibs.length > 0) {
throw new Error(`BaseLayout: unknown pageLibs ${unknownLibs.map((name) => `"${name}"`).join(', ')} — use a key from core/libs.json`)
@@ -52,6 +52,8 @@ const libCssFiles = pageLibEntries.flatMap(([, lib]) => (lib.css ?? []).map((fil
// handed to the classic page scripts as a global. Module scripts are deferred,
// so the global is set before DOMContentLoaded.
const libModules = pageLibEntries.flatMap(([, lib]) => (lib.module ? [`import * as lib from '${libHref(lib, lib.module.file)}'\n window.${lib.module.global} = lib`] : []))
// Tabler add-on bundles (dist/js) that a library needs, loaded after tabler.js
const libAddons = pageLibEntries.flatMap(([, lib]) => (lib.addon ? [lib.addon] : []))
const libJsFiles = (head: boolean) => pageLibEntries.filter(([, lib]) => Boolean(lib.head) === head).flatMap(([, lib]) => (lib.js ?? []).map((file) => libHref(lib, file)))
---
@@ -137,6 +139,7 @@ const libJsFiles = (head: boolean) => pageLibEntries.filter(([, lib]) => Boolean
<!-- BEGIN GLOBAL MANDATORY SCRIPTS -->
<script is:inline src={`${base}/dist/js/tabler${min}.js`} defer></script>
{libAddons.map((addon) => <script is:inline src={`${base}/dist/js/${addon}${min}.js`} defer />)}
<!-- END GLOBAL MANDATORY SCRIPTS -->
<PageScripts />