import { build } from 'vite'; import { resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; import { readFileSync, rmSync, mkdirSync, existsSync, cpSync } from 'fs'; const __dirname = resolve(fileURLToPath(import.meta.url), '..'); function cssModulePlugin() { return { name: 'css-module-plugin', enforce: 'pre', transform(code, id) { if (id.endsWith('.js') || id.endsWith('.mjs') || id.endsWith('.ts')) { let cleanCode = code.replace( /import\s+([\w\s{},*]+)\s+from\s+(['"][\w@\-.\/]+\.css['"])\s*(?:with|assert)\s*\{\s*type\s*:\s*['"]css['"]\s*\};?/gs, 'import $1 from $2;' ); cleanCode = cleanCode.replace( /(import|export)\s+([\w\s{},*]+\s+from\s+)?(['"][\w@\-.\/]+\.css)(['"])/g, '$1 $2$3.js$4' ); // Dynamically inline all SVG files fetched at runtime const regex = /const\s+(\w+)\s*=\s*await\s+fetch\(\s*new\s+URL\(\s*(['"])([^'"]+)\2\s*,\s*import\.meta\.url\s*\)\.href\s*\)\.then\(\s*(\w+)\s*=>\s*\4\.text\(\)\s*\);?/g; cleanCode = cleanCode.replace(regex, (match, varName, quote, relPath) => { const absoluteSvgPath = resolve(dirname(id), relPath); if (existsSync(absoluteSvgPath)) { const svgContent = readFileSync(absoluteSvgPath, 'utf-8'); if (varName === 'LogoDead') { const dataUrl = `data:image/svg+xml;utf8,${encodeURIComponent(svgContent)}`; return `const ${varName} = ${JSON.stringify(dataUrl)};`; } else { return `const ${varName} = ${JSON.stringify(svgContent)};`; } } return match; }); // Patch MapStyleComposer.js layout["text-field"].includes crash for complex style sheets if (id.includes('MapStyleComposer.js')) { cleanCode = cleanCode.replace( /if\s*\(\s*l\.layout\s*\[\s*(['"])text-field\1\s*\]\s*\.includes\s*\(\s*(['"])name:latin\2\s*\)\s*\)/g, "if(typeof l.layout['text-field'] === 'string' && l.layout['text-field'].includes('name:latin'))" ); // Patch getMapStyle() to forward glyphs property to the final style object cleanCode = cleanCode.replace( /return\s*\{\s*version\s*:\s*8\s*,\s*name\s*:\s*(['"])Panoramax Map Style\1\s*,\s*sources\s*,\s*layers\s*:\s*layers\.concat\(\s*labelLayers\s*\)\.filter\(\s*l\s*=>\s*l\.id\s*\)\s*,\s*metadata\s*,\s*sprite\s*:\s*sprite\.length\s*>\s*0\s*\?\s*sprite\s*:\s*undefined\s*,?\s*\};/g, `return { version: 8, name: "Panoramax Map Style", sources, layers: layers.concat(labelLayers).filter(l => l.id), metadata, sprite: sprite.length > 0 ? sprite : undefined, glyphs: (this.basemap && this.layerRanges.basemaps[this.basemap] && this.layerRanges.basemaps[this.basemap].glyphs) || "https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf", };` ); } return { code: cleanCode, map: null }; } }, async resolveId(source, importer) { if (source.endsWith('.css.js')) { const rawSource = source.slice(0, -3); const resolved = await this.resolve(rawSource, importer); if (resolved) { return resolved.id + '.js'; } } return null; }, load(id) { if (id.endsWith('.css.js')) { const cssFilePath = id.slice(0, -3); let cssContent = readFileSync(cssFilePath, 'utf-8'); // Rewrite Atkinson font relative paths to local Django static paths cssContent = cssContent.replace( /url\(['"]?(?:\.\/)?files\/(atkinson-hyperlegible-next-[^'")]+\.(?:woff2|woff))['"]?\)/g, 'url(/static/panoramax/files/$1)' ); const escapedCss = JSON.stringify(cssContent); return ` const cssText = ${escapedCss}; if (typeof document !== 'undefined') { const style = document.createElement('style'); style.appendChild(document.createTextNode(cssText)); document.head.appendChild(style); } let sheet = null; if (typeof CSSStyleSheet !== 'undefined') { sheet = new CSSStyleSheet(); sheet.replaceSync(cssText); } export default sheet; `; } return null; } }; } const configs = [ // 1. Panoramax Viewer { configFile: false, plugins: [cssModulePlugin()], build: { target: 'esnext', lib: { entry: resolve(__dirname, 'index.js'), name: 'PanoramaxViewer', formats: ['es'], fileName: () => 'js/panoramax-viewer.js' }, outDir: resolve(__dirname, '../static/panoramax'), emptyOutDir: false } }, // 2. PMTiles { configFile: false, build: { target: 'esnext', lib: { entry: resolve(__dirname, 'node_modules/pmtiles/dist/index.js'), name: 'pmtiles', formats: ['es'], fileName: () => 'js/pmtiles.js' }, outDir: resolve(__dirname, '../static/panoramax'), emptyOutDir: false } }, // 3. GeoJSON-VT { configFile: false, build: { target: 'esnext', lib: { entry: resolve(__dirname, 'node_modules/geojson-vt/geojson-vt.js'), name: 'geojsonvt', formats: ['es'], fileName: () => 'js/geojson-vt.js' }, outDir: resolve(__dirname, '../static/panoramax'), emptyOutDir: false } }, // 4. PBF { configFile: false, build: { target: 'esnext', lib: { entry: resolve(__dirname, 'node_modules/pbf/index.js'), name: 'pbf', formats: ['es'], fileName: () => 'js/pbf.js' }, outDir: resolve(__dirname, '../static/panoramax'), emptyOutDir: false } } ]; async function run() { const outPath = resolve(__dirname, '../static/panoramax'); if (existsSync(outPath)) { rmSync(outPath, { recursive: true, force: true }); } mkdirSync(outPath, { recursive: true }); // Copy font files to the static output directory const fontsSrc = resolve(__dirname, 'node_modules/@fontsource/atkinson-hyperlegible-next/files'); const fontsDest = resolve(__dirname, '../static/panoramax/files'); if (existsSync(fontsSrc)) { console.log('Copying Atkinson font files...'); mkdirSync(fontsDest, { recursive: true }); cpSync(fontsSrc, fontsDest, { recursive: true }); } for (const config of configs) { const name = config.build.lib.fileName(); console.log(`\n--- Building: ${name} ---`); await build(config); } console.log('\n--- All builds completed successfully! ---'); } run().catch(err => { console.error(err); process.exit(1); });