Add a Next.js tab to the ESM bundler setup - #8128
Conversation
Closes the gap raised in maplibre#8126: the ESM section covers Vite, webpack, esbuild and Rollup, but not Next.js, whose default bundler is Turbopack. Next.js inlines the entry module, so the documented webpack recipe, new URL('maplibre-gl/dist/maplibre-gl-worker.mjs', import.meta.url), does not resolve to the worker there. Verified against Next 16.2.12 with both `next build` (Turbopack) and `next build --webpack`: the map mounts and gets a canvas with a live WebGL 2 context, styledata and render fire, and then no vector tile is ever requested, because maplibre ends up calling new Worker("", {type: "module"}). The tab documents serving the worker from public/ and pointing setWorkerUrl at it, which was verified working on both bundlers. Two details are called out because both are silent failures if missed: maplibre-gl-shared.mjs has to be copied alongside the worker, since the worker imports it by relative path; and the copy belongs in a prebuild step rather than in version control, so it cannot fall out of step with the installed version and run an old worker against a new entry. Assisted-By: Claude Opus 5 (Claude Code)
|
To be fair, next.js is not a bundler, so I don't think it should have a section of its own. If I'm wrong and next.js is using webpack, I would recommend adding a section inside the webpack docs to showcase other options in webpack, including this one. Does it makes sense? |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #8128 +/- ##
=======================================
Coverage 93.10% 93.10%
=======================================
Files 300 300
Lines 24899 24899
Branches 6540 6540
=======================================
+ Hits 23181 23183 +2
+ Misses 1718 1716 -2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Follows up on the suggestion in maplibre#8126 to use postinstall instead. It is the more natural hook, since the copy's only input is node_modules, but it is not sufficient on its own, so the tab now says which to use and why. Measured on pnpm 11: package managers skip lifecycle scripts when an install has no work to do. With dependencies already current and the destination directory deleted, neither `pnpm install` nor `pnpm install --force` restored the worker; only a fresh node_modules did. It also does not run under --ignore-scripts. A pre hook runs at the moment the file is needed, which is the property that matters. Two further traps found while applying this to a real project, both silent, both now called out: - npm lifecycle prefixes match the exact script name, so prebuild runs before build and not before a custom script like build:local. A project whose CI builds through a custom script gets no worker and a blank map. - Adding the destination to .gitignore does nothing if the files were already committed, and a committed copy keeps working after a maplibre upgrade while running the old worker against the new entry. Assisted-By: Claude Opus 5 (Claude Code)
|
@HarelM That makes sense. Next.js is a framework, not a bundler. The tricky part is that Next.js uses Turbopack by default, but can also use webpack, and I reproduced the same worker issue with both. So I'd suggest:
I've only tested Turbopack through Next.js, not standalone. Would that match what you had in mind? |
|
Yes, I think that makes sense. Thanks for looking into it and for your time! |
CommanderStorm
left a comment
There was a problem hiding this comment.
a bit of cleanup suggestion to not make this look soo "vibed"
- drop the reference to the webpack recipe being "above" (tabs have no order) - use fenced-block title= for the filenames instead of leading comments - trim the prose around the pre hook and postinstall Assisted-By: Claude Opus 5 (Claude Code)
…lly load Adds test/integration/bundler/nextjs/, covering Next's default Turbopack bundler through the worker setup documented in the Next.js docs tab. The harness now serves each example's own build output as the site root rather than serving the repo root, so root-absolute URLs behave the way they would in a real deployment, and it accepts Next's `out/` alongside `dist/`. It also now waits for the map to request a vector tile. The canvas mounts even when the worker is dead, so canvas presence alone passed builds where the worker 404'd on its `maplibre-gl-shared.mjs` sibling and no tile was ever fetched. Corrects the docs explanation to match measured behaviour: Next does emit the worker for the webpack recipe, it just doesn't emit the shared sibling. Assisted-By: Claude Opus 5 (Claude Code)
|
Pushed two commits. Thanks @HarelM and @CommanderStorm for the quick reviews! My original explanation was wrong. Next does resolve the worker and does not fall back to @HarelM I added One deviation from what we agreed: the Next.js content is still its own tab rather than folded into the webpack tab. @CommanderStorm reviewed the tab later the same day with five inline suggestions on its contents and none on it being separate, so I applied those rather than restructure underneath an open review. Folding it into the webpack tab is a small change, so just say which you'd prefer. |
The root tsconfig lists every bundler example under `exclude`, since each one is a standalone project with its own tsconfig and dependencies. The new nextjs example was missing from that list, so `npm run typecheck` compiled its .tsx files with the repo's own `jsx: react` setting and failed with TS2686 on the React UMD global. Also lists Next.js in the pointer to the bundler examples. Assisted-By: Claude Opus 5 (Claude Code)
35145c8 to
cb7ad05
Compare
Updated code block titles in documentation for clarity.
|
The problem I see with adding next.js is that next.js is a framework and not a bundler, if we add it, why not add react, angular, astro, hugo and many others... I don't know... |
I totally understand, I'm going to go with your suggestion, thanks! |
Addresses the slippery-slope concern in review: the other tabs are named after bundlers, and a Next.js tab invites Astro, Angular and the rest. Turbopack is the bundler, so the tab is named for it and says up front that Next.js is where you will meet it. Renames the example to test/integration/bundler/turbopack/ to match. It is still a Next.js app, since Turbopack is not practical to drive standalone, and both READMEs now say so. Keeps the note that Next's other mode, next build --webpack, needs the same setup, because the asset handling is Next's rather than Turbopack's alone. Assisted-By: Claude Opus 5 (Claude Code)
Launch Checklist
CHANGELOG.mdunder the## mainsection (no runtime change, and recent test- and docs-only PRs here skip it)What this adds
=== "Next.js"tab in the ESM bundler setup indocs/index.md.test/integration/bundler/nextjs/, a Next.js example covering Turbopack through the recipe the tab documents.test/integration/bundler/bundlers.test.ts: it now waits for the map to request a vector tile, and it serves each example's own build output as the site root rather than the repo root, accepting Next'sout/alongsidedist/.Closes #8126
Why
The ESM section covers Vite, webpack, esbuild and Rollup. Next.js isn't covered, which is what #8126 asks for, and it can't simply reuse the webpack tab.
Both of Next's bundlers turn the documented webpack recipe
into a hashed asset under
/_next/static/media/, which the browser then fetches successfully. What they do not emit is the worker'smaplibre-gl-shared.mjssibling, so the worker 404s on its first import and dies before it can handle a message. This is the same failure the Vite tab already documents for plain?url.Because a failed worker isn't currently surfaced (#8018), the result is a map that mounts, gets a canvas with a live WebGL 2 context, fires
styledataandrender, and then never requests a single vector tile. It just sits there and there's nothing in the docs pointing the way out.What I verified
Chromium 151 via Puppeteer, WebGL 2 available, recording the URL passed to the
Workerconstructor and counting actual.pbfrequests.The documented webpack recipe: worker fetched with HTTP 200,
maplibre-gl-shared.mjs404, zero vector tiles, in every combination tried.next build(Turbopack)next build --webpacknext build(Turbopack)next devnext build(Turbopack)Serving both files from
public/and pointingsetWorkerUrlat the worker loads tiles normally under bothnext build(Turbopack) andnext build --webpack.An earlier revision of this description attributed the failure to Next inlining the entry module, with maplibre then falling back to
new Worker("", {type: "module"}). That is not what happens. The constructed URL is the hashed asset path in every run above, never empty. The visible symptom is identical either way, which is why the wrong mechanism held up for so long.bundlers.test.tswould not have caught any of this: it only checked for a.maplibregl-canvaselement, which a dead worker still produces. The tile assertion it now makes was verified by pointing the new example back at the webpack recipe and watching it fail.Why a
prehook and notpostinstall#8126 suggested
postinstall, which is the more natural hook since the copy's only input isnode_modules. Measured on pnpm 11, it is not sufficient on its own: package managers skip lifecycle scripts when an install has no work to do.node_modulespnpm install, deps current, destination deletedpnpm install --force, sameIt also does not run under
--ignore-scripts. In both cases the result is a missing worker and a blank map, the same silent failure the tab exists to prevent. Aprehook runs at the moment the file is needed, so the tab uses that.Two further traps, both silent, both hit while applying this to a real project:
prebuildruns beforebuildand not before a custom script such asbuild:local. A project whose CI builds through a custom script gets no worker and a blank map. That is exactly what happened to me, and it went unnoticed because the destination directory was still present from an earlier manual run. This one is called out in the tab..gitignoredoes nothing if the files were already committed, and a stale committed copy keeps working after a maplibre upgrade while running the old worker against the new entry. Left out of the tab to keep it short.Placement
The tab sits after Rollup and before "CDN / No bundler", as its own tab rather than folded into the webpack tab. Happy to move it either way.
Note
This documents a workaround. If #8024 lands and failed workers start reporting an error, the tab is still needed, but the surrounding text could get shorter.