Replaces eager imports of map hooks and Leaflet in app.ts with dynamic-import wrappers (lazyHook / lazyMapHook). Each page-specific hook now ships as its own esbuild chunk fetched on first mount; Leaflet is loaded once and shared across every map page. Pages that never mount these hooks (/submit, /algo, /about, etc.) no longer pay for them at all.
107 lines
3.4 KiB
TypeScript
107 lines
3.4 KiB
TypeScript
// Wraps a Phoenix LiveView hook so its implementation module is fetched
|
||
// over the network only when the hook actually mounts. esbuild's
|
||
// `--splitting --format=esm` emits the dynamic import as its own chunk,
|
||
// keeping it out of the core app.js bundle on pages that don't use it.
|
||
//
|
||
// Two flavors:
|
||
// * lazyHook(loader) – plain dynamic-import wrapper.
|
||
// * lazyMapHook(loader) – also ensures Leaflet is loaded and assigned
|
||
// to window.L once before the impl mounts,
|
||
// shared across every map page.
|
||
|
||
type HookImpl = Record<string, any>
|
||
type ModuleLoader = () => Promise<Record<string, HookImpl>>
|
||
|
||
interface LazyState {
|
||
_impl?: HookImpl
|
||
_pendingUpdate?: boolean
|
||
_destroyed?: boolean
|
||
}
|
||
|
||
let leafletLoader: Promise<void> | null = null
|
||
|
||
function loadLeaflet(): Promise<void> {
|
||
if (!leafletLoader) {
|
||
leafletLoader = import("../vendor/leaflet/leaflet.js").then((mod) => {
|
||
// UMD: when imported as ESM, the namespace itself is the L value.
|
||
// Some bundlers wrap it under .default — handle both.
|
||
window.L = (mod as any).default ?? mod
|
||
})
|
||
}
|
||
return leafletLoader
|
||
}
|
||
|
||
// Pick the first non-lifecycle export from the module as the hook impl,
|
||
// or use the named export matching `hookName` when given.
|
||
function pickImpl(mod: Record<string, HookImpl>, hookName?: string): HookImpl {
|
||
if (hookName && mod[hookName]) return mod[hookName]
|
||
for (const key of Object.keys(mod)) {
|
||
if (key === "default") continue
|
||
const v = mod[key]
|
||
if (v && typeof v === "object" && ("mounted" in v || "updated" in v || "destroyed" in v)) {
|
||
return v
|
||
}
|
||
}
|
||
if (mod.default) return mod.default
|
||
throw new Error("lazyHook: no hook export found in module")
|
||
}
|
||
|
||
function attach(hook: ViewHook & LazyState, impl: HookImpl) {
|
||
hook._impl = impl
|
||
for (const k of Object.keys(impl)) {
|
||
if (k === "mounted" || k === "updated" || k === "destroyed") continue
|
||
;(hook as any)[k] = impl[k]
|
||
}
|
||
}
|
||
|
||
export function lazyHook(loader: ModuleLoader, hookName?: string) {
|
||
return {
|
||
mounted(this: ViewHook & LazyState) {
|
||
const hook = this
|
||
loader().then((mod) => {
|
||
if (hook._destroyed) return
|
||
const impl = pickImpl(mod, hookName)
|
||
attach(hook, impl)
|
||
impl.mounted?.call(hook)
|
||
if (hook._pendingUpdate) {
|
||
impl.updated?.call(hook)
|
||
hook._pendingUpdate = false
|
||
}
|
||
})
|
||
},
|
||
updated(this: ViewHook & LazyState) {
|
||
if (this._impl?.updated) this._impl.updated.call(this)
|
||
else this._pendingUpdate = true
|
||
},
|
||
destroyed(this: ViewHook & LazyState) {
|
||
this._destroyed = true
|
||
this._impl?.destroyed?.call(this)
|
||
},
|
||
}
|
||
}
|
||
|
||
export function lazyMapHook(loader: ModuleLoader, hookName?: string) {
|
||
return {
|
||
mounted(this: ViewHook & LazyState) {
|
||
const hook = this
|
||
Promise.all([loadLeaflet(), loader()]).then(([, mod]) => {
|
||
if (hook._destroyed) return
|
||
const impl = pickImpl(mod, hookName)
|
||
attach(hook, impl)
|
||
impl.mounted?.call(hook)
|
||
if (hook._pendingUpdate) {
|
||
impl.updated?.call(hook)
|
||
hook._pendingUpdate = false
|
||
}
|
||
})
|
||
},
|
||
updated(this: ViewHook & LazyState) {
|
||
if (this._impl?.updated) this._impl.updated.call(this)
|
||
else this._pendingUpdate = true
|
||
},
|
||
destroyed(this: ViewHook & LazyState) {
|
||
this._destroyed = true
|
||
this._impl?.destroyed?.call(this)
|
||
},
|
||
}
|
||
}
|