/* * Copyright (C) 2026 TomTom Navigation B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Vite options shared by every MCP App build. * * The apps ship as a single inlined HTML file (one `ui://` resource each), so * this config is what decides that no sibling assets are emitted. The e2e * worker test builds through it too, so a change here cannot silently diverge * from what that test exercises. */ import { createRequire } from 'module'; import path from 'path'; import { fileURLToPath } from 'url'; import { build } from 'rolldown'; import type { InlineConfig, Plugin } from 'vite'; import { viteSingleFile } from 'vite-plugin-singlefile'; import { MAPLIBRE_CDN_ORIGIN } from '../src/tools/helpers/appCsp'; export const ROOT_DIR = fileURLToPath(new URL('..', import.meta.url)); export const APPS_DIR = path.join(ROOT_DIR, 'src/apps'); /** Module whose contents are replaced with the bundled MapLibre worker. */ const WORKER_SOURCE_MODULE = path.join(APPS_DIR, 'shared/maplibre-worker-source.ts'); const require = createRequire(import.meta.url); /** MapLibre's `dist/` on the CDN, pinned to the installed version the SDK was resolved against. */ export const MAPLIBRE_CDN_DIST = `${MAPLIBRE_CDN_ORIGIN}/npm/maplibre-gl@${require('maplibre-gl/package.json').version}/dist`; /** The maps-sdk's own MapLibre worker, which Vite emits beside the bundle. */ const SDK_WORKER_ASSET = /^maplibre-gl-worker-[\w-]+\.js$/; let workerSource: Promise | undefined; // The worker chunk and the shared chunk it imports become one script, so the // blob it runs from has nothing left to fetch. Bundled once per process. function bundleMaplibreWorker(): Promise { workerSource ??= build({ input: require.resolve('maplibre-gl/dist/maplibre-gl-worker.mjs'), platform: 'browser', write: false, output: { format: 'es', minify: true }, }).then(({ output }) => output .filter((chunk) => chunk.type === 'chunk') .map((chunk) => chunk.code) .join('\n'), ); return workerSource; } /** * Imports MapLibre from the CDN instead of inlining it, which would make up * most of every app. The SDK's import resolves to the same module, so both * share one MapLibre. * * The worker cannot come from the CDN: ChatGPT's `worker-src blob:` also * governs a module worker's imports. It is bundled into * `maplibre-worker-source.ts` instead, which `useInlinedMaplibreWorker()` * turns into a blob, and the worker the SDK would register is dropped from the * output, keeping the bundle a single file. */ function loadMaplibreFromCdn(): Plugin { return { name: 'load-maplibre-from-cdn', enforce: 'pre', resolveId(source) { if (source !== 'maplibre-gl') return null; return { id: `${MAPLIBRE_CDN_DIST}/maplibre-gl.mjs`, external: true }; }, async load(id) { const [filePath] = id.split('?'); if (path.resolve(filePath) !== WORKER_SOURCE_MODULE) return null; return `export default ${JSON.stringify(await bundleMaplibreWorker())};`; }, generateBundle(_options, bundle) { for (const fileName of Object.keys(bundle)) { if (SDK_WORKER_ASSET.test(fileName)) delete bundle[fileName]; } }, }; } export interface AppBuildTarget { /** Directory Vite treats as the app root. */ appDir: string; /** Entry HTML file. */ htmlPath: string; /** Where the single-file bundle is written. */ outDir: string; logLevel?: InlineConfig['logLevel']; } export function appViteConfig({ appDir, htmlPath, outDir, logLevel = 'error', }: AppBuildTarget): InlineConfig { return { root: appDir, logLevel, resolve: { alias: { '@shared': path.join(APPS_DIR, 'shared') } }, plugins: [loadMaplibreFromCdn(), viteSingleFile()], build: { outDir, emptyOutDir: true, rolldownOptions: { input: htmlPath }, minify: 'oxc', }, }; }