Skip to content

Commit efaf024

Browse files
committed
--wip-- [skip ci]
1 parent 663234f commit efaf024

13 files changed

Lines changed: 3637 additions & 2418 deletions

.tool-versions

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
nodejs lts
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import fs from "node:fs";
2+
3+
import type { Plugin } from "vite";
4+
5+
/**
6+
* Import XML/KML files from lib/Models as raw text strings.
7+
*/
8+
export function xmlRawPlugin(): Plugin {
9+
return {
10+
name: "xml-raw",
11+
transform(_code, id) {
12+
if (
13+
(id.endsWith(".xml") || id.endsWith(".kml")) &&
14+
id.includes("lib/Models")
15+
) {
16+
const content = fs.readFileSync(id, "utf-8");
17+
return {
18+
code: `export default ${JSON.stringify(content)};`,
19+
map: null
20+
};
21+
}
22+
}
23+
};
24+
}
25+
26+
/**
27+
* Strip moment.js locale imports to save ~500KB.
28+
* Webpack used IgnorePlugin for this; in Vite we return an empty module.
29+
*/
30+
export function momentLocalePlugin(): Plugin {
31+
return {
32+
name: "moment-locale-strip",
33+
resolveId(source, importer) {
34+
if (source === "./locale" && importer && importer.includes("moment")) {
35+
return "\0moment-locale-empty";
36+
}
37+
},
38+
load(id) {
39+
if (id === "\0moment-locale-empty") {
40+
return "export default {};";
41+
}
42+
}
43+
};
44+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { cpSync, createReadStream, existsSync, statSync } from "node:fs";
2+
import path from "node:path";
3+
4+
import type { Plugin, ResolvedConfig } from "vite";
5+
6+
const MIME_TYPES: Record<string, string> = {
7+
".js": "application/javascript",
8+
".mjs": "application/javascript",
9+
".json": "application/json",
10+
".wasm": "application/wasm",
11+
".png": "image/png",
12+
".jpg": "image/jpeg",
13+
".gif": "image/gif",
14+
".svg": "image/svg+xml",
15+
".xml": "application/xml",
16+
".glb": "model/gltf-binary",
17+
".bin": "application/octet-stream"
18+
};
19+
20+
/**
21+
* Serves Cesium Workers, Assets, and ThirdParty from the terriajs-cesium
22+
* package and terriajs wwwroot static assets during dev mode.
23+
*
24+
* At runtime, Cesium resolves assets relative to `cesiumBaseUrl` which is
25+
* set to `build/TerriaJS/build/Cesium/build/` by the app's index.js.
26+
*/
27+
export function cesiumPlugin(options: {
28+
terriaJSBasePath: string;
29+
cesiumDir: string;
30+
}): Plugin {
31+
const { terriaJSBasePath, cesiumDir } = options;
32+
const terriaJSWwwroot = path.join(terriaJSBasePath, "wwwroot");
33+
let outDir = "";
34+
35+
return {
36+
name: "cesium-assets",
37+
38+
configResolved(config: ResolvedConfig) {
39+
outDir = path.resolve(config.root, config.build.outDir);
40+
},
41+
42+
closeBundle() {
43+
// Copy terriajs wwwroot → outDir/TerriaJS/ (mirrors gulp copy-terriajs-assets)
44+
const destPath = path.join(outDir, "TerriaJS");
45+
cpSync(terriaJSWwwroot, destPath, { recursive: true });
46+
},
47+
48+
configureServer(server) {
49+
// Serve terriajs wwwroot at /build/TerriaJS/
50+
// This mirrors the gulp `copy-terriajs-assets` task
51+
server.middlewares.use((req, res, next) => {
52+
if (!req.url) return next();
53+
54+
const url = decodeURIComponent(req.url.split("?")[0]);
55+
56+
// Serve terriajs wwwroot assets at /build/TerriaJS/
57+
if (url.startsWith("/build/TerriaJS/")) {
58+
const assetPath = url.slice("/build/TerriaJS/".length);
59+
const filePath = path.join(terriaJSWwwroot, assetPath);
60+
return serveFile(filePath, res, next);
61+
}
62+
63+
// Serve WASM with correct MIME type
64+
if (url.endsWith(".wasm")) {
65+
const wasmPath = url.startsWith("/build/")
66+
? path.join(terriaJSWwwroot, url.slice(1))
67+
: null;
68+
if (wasmPath && existsSync(wasmPath)) {
69+
return serveFile(wasmPath, res, next);
70+
}
71+
}
72+
73+
next();
74+
});
75+
}
76+
};
77+
78+
function serveFile(
79+
filePath: string,
80+
res: import("http").ServerResponse,
81+
next: () => void
82+
) {
83+
if (!existsSync(filePath) || statSync(filePath).isDirectory()) {
84+
return next();
85+
}
86+
const ext = path.extname(filePath);
87+
const mime = MIME_TYPES[ext] || "application/octet-stream";
88+
res.setHeader("Content-Type", mime);
89+
res.setHeader("Cache-Control", "no-cache");
90+
createReadStream(filePath).pipe(res);
91+
}
92+
}
93+
94+
/**
95+
* Strip Cesium debug pragmas in production builds.
96+
* Removes code between //>>includeStart('debug') and //>>includeEnd('debug').
97+
*/
98+
export function cesiumDebugStripPlugin(cesiumDir: string): Plugin {
99+
const pragmaRegex =
100+
/\/\/>>includeStart\('debug', pragmas\.debug\);?[^]*?\/\/>>includeEnd\('debug'\);?/g;
101+
102+
return {
103+
name: "cesium-strip-debug",
104+
apply: "build",
105+
transform(code, id) {
106+
if (!id.endsWith(".js") || !id.includes(cesiumDir)) return;
107+
return { code: code.replace(pragmaRegex, ""), map: null };
108+
}
109+
};
110+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import fs from "node:fs";
2+
3+
import type { Plugin } from "vite";
4+
5+
const VIRTUAL_EXT = ".module.scss";
6+
const REAL_EXT = ".scss";
7+
8+
/**
9+
* Rewrites .scss imports from JS/TS files to .module.scss so Vite's CSS
10+
* Modules pipeline kicks in. The `load` hook reads the actual .scss file
11+
* when Vite requests the virtual .module.scss.
12+
*
13+
* SCSS-to-SCSS imports (@use, @import, @forward) are NOT intercepted — only
14+
* JS/TS → SCSS imports are rewritten.
15+
*
16+
* `composes: from` is handled by vite-css-modules which routes composed
17+
* dependencies through Vite's module graph for proper deduplication.
18+
*/
19+
export function scssCssModulesPlugin(): Plugin {
20+
return {
21+
name: "scss-css-modules",
22+
enforce: "pre",
23+
24+
async resolveId(source, importer, options) {
25+
if (!source.endsWith(REAL_EXT)) return null;
26+
if (!importer || !/\.[jt]sx?$/.test(importer)) return null;
27+
28+
const resolved = await this.resolve(source, importer, {
29+
...options,
30+
skipSelf: true
31+
});
32+
if (!resolved) return null;
33+
34+
return resolved.id.replace(/\.scss$/, VIRTUAL_EXT);
35+
},
36+
37+
load(id) {
38+
if (!id.endsWith(VIRTUAL_EXT)) return null;
39+
40+
const realPath = id.slice(0, -VIRTUAL_EXT.length) + REAL_EXT;
41+
if (!fs.existsSync(realPath)) return null;
42+
43+
return fs.readFileSync(realPath, "utf-8");
44+
}
45+
};
46+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import fs from "node:fs";
2+
import path from "node:path";
3+
4+
import type { Plugin } from "vite";
5+
6+
interface IconDir {
7+
dir: string;
8+
namespace: string;
9+
}
10+
11+
/**
12+
* Vite plugin that collects SVG icons from configured directories, compiles
13+
* them into `<symbol>` elements via svg-sprite, and injects them into the HTML.
14+
*
15+
* Icons are scanned eagerly at startup so the sprite is ready when
16+
* `transformIndexHtml` runs (before any JS modules are loaded in dev mode).
17+
*
18+
* Individual SVG imports return `{ id: "namespace-iconname" }` matching the
19+
* shape expected by the Icon component (`<use xlinkHref={"#" + glyph.id} />`).
20+
*/
21+
export function svgSpritePlugin(iconDirs: IconDir[]): Plugin {
22+
const iconRegistry = new Map<
23+
string,
24+
Map<string, { path: string; content: string }>
25+
>();
26+
27+
function scanIconDirs() {
28+
for (const { dir, namespace } of iconDirs) {
29+
if (!fs.existsSync(dir)) continue;
30+
31+
const icons = new Map<string, { path: string; content: string }>();
32+
for (const file of fs.readdirSync(dir)) {
33+
if (!file.endsWith(".svg")) continue;
34+
const filePath = path.join(dir, file);
35+
icons.set(path.basename(file, ".svg"), {
36+
path: filePath,
37+
content: fs.readFileSync(filePath, "utf-8")
38+
});
39+
}
40+
iconRegistry.set(namespace, icons);
41+
}
42+
}
43+
44+
async function buildSprites(): Promise<string[]> {
45+
const SVGSpriter = (await import("svg-sprite")).default;
46+
const spriteDivs: string[] = [];
47+
48+
for (const [namespace, icons] of iconRegistry.entries()) {
49+
const spriter = new SVGSpriter({
50+
mode: {
51+
symbol: { inline: true, sprite: `sprite-${namespace}`, bust: false }
52+
},
53+
svg: { xmlDeclaration: false, doctypeDeclaration: false },
54+
shape: {
55+
id: {
56+
generator: (svg: string) =>
57+
`${namespace}-${path.basename(svg.replace(/\s+/g, "_"), ".svg")}`
58+
},
59+
transform: [
60+
{
61+
svgo: {
62+
plugins: [{ name: "preset-default" }, "removeXMLNS"]
63+
}
64+
}
65+
]
66+
}
67+
});
68+
69+
for (const [iconName, data] of icons) {
70+
spriter.add(data.path, `${iconName}.svg`, data.content);
71+
}
72+
73+
const { result } = await spriter.compileAsync();
74+
const svgContent = result.symbol.sprite.contents.toString();
75+
const safeNs = namespace.replace(/[^a-zA-Z0-9_]/g, "_");
76+
spriteDivs.push(
77+
`<div id="svg-sprite-${safeNs}" style="display:none">${svgContent}</div>`
78+
);
79+
}
80+
81+
return spriteDivs;
82+
}
83+
84+
// Pre-compile the sprite once at startup
85+
let spriteHtmlPromise: Promise<string> | undefined;
86+
87+
return {
88+
name: "svg-sprite",
89+
90+
buildStart() {
91+
scanIconDirs();
92+
spriteHtmlPromise = buildSprites().then((divs) => divs.join(""));
93+
},
94+
95+
transform(_code, id) {
96+
for (const { dir, namespace } of iconDirs) {
97+
if (!id.endsWith(".svg") || !id.startsWith(dir)) continue;
98+
99+
const name = path.basename(id, ".svg");
100+
const symbolId = `${namespace}-${name}`;
101+
102+
return {
103+
code: [
104+
`export default { id: "${symbolId}" };`,
105+
`export const id = "${symbolId}";`,
106+
`export const name = "${name}";`
107+
].join("\n"),
108+
map: null
109+
};
110+
}
111+
},
112+
113+
async transformIndexHtml(html) {
114+
const spriteHtml = await spriteHtmlPromise;
115+
if (!spriteHtml) return html;
116+
117+
return html.replace(
118+
'<div id="svg-sprites" style="display: none"></div>',
119+
`<div id="svg-sprites" style="display: none">${spriteHtml}</div>`
120+
);
121+
}
122+
};
123+
}

0 commit comments

Comments
 (0)