cesium/server.js

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

459 lines
12 KiB
JavaScript
Raw Permalink Normal View History

2022-09-09 00:43:28 +08:00
import fs from "fs";
import path from "path";
import { performance } from "perf_hooks";
import { fileURLToPath, URL } from "url";
2022-09-09 00:43:28 +08:00
import chokidar from "chokidar";
import compression from "compression";
import express from "express";
import yargs from "yargs";
2023-09-15 03:56:25 +08:00
import ContextCache from "./scripts/ContextCache.js";
import createRoute from "./scripts/createRoute.js";
import {
createCesiumJs,
createJsHintOptions,
createCombinedSpecList,
glslToJavaScript,
createIndexJs,
buildCesium,
2025-10-28 00:12:33 +08:00
buildEngine,
buildWidgets,
} from "./scripts/build.js";
2022-09-09 00:43:28 +08:00
const argv = yargs(process.argv)
.options({
port: {
default: 8080,
description: "Port to listen on.",
},
public: {
type: "boolean",
description: "Run a public server that listens on all interfaces.",
},
2022-11-15 22:57:16 +08:00
production: {
type: "boolean",
description: "If true, skip build step and serve existing built files.",
},
2022-09-09 00:43:28 +08:00
})
.help().argv;
// These functions will not exist in the production zip file but they also won't be run
const { getSandcastleConfig, buildSandcastleGallery, buildSandcastleApp } =
argv.production ? {} : await import("./scripts/buildSandcastle.js");
const outputDirectory = path.join("Build", "CesiumDev");
2022-07-12 03:39:36 +08:00
2022-07-14 21:36:46 +08:00
function formatTimeSinceInSeconds(start) {
2022-07-12 03:39:36 +08:00
return Math.ceil((performance.now() - start) / 100) / 10;
}
2022-11-02 03:39:57 +08:00
/**
* Returns CesiumJS bundles configured for development.
*
* @returns {Bundles} The bundles.
*/
async function generateDevelopmentBuild() {
const startTime = performance.now();
2023-02-22 23:11:07 +08:00
// Build @cesium/engine index.js
2022-11-02 03:39:57 +08:00
console.log("[1/3] Building @cesium/engine...");
2025-10-28 00:12:33 +08:00
const engineContexts = await buildEngine({
incremental: true,
minify: false,
write: false,
});
2022-07-12 03:39:36 +08:00
2023-02-22 23:11:07 +08:00
// Build @cesium/widgets index.js
2022-11-02 03:39:57 +08:00
console.log("[2/3] Building @cesium/widgets...");
2025-10-28 00:12:33 +08:00
const widgetContexts = await buildWidgets({
incremental: true,
minify: false,
write: false,
});
2022-07-12 03:39:36 +08:00
2023-02-22 23:11:07 +08:00
// Build CesiumJS and save returned contexts for rebuilding upon request
2022-11-02 03:39:57 +08:00
console.log("[3/3] Building CesiumJS...");
2023-02-22 23:11:07 +08:00
const contexts = await buildCesium({
2022-11-02 03:39:57 +08:00
development: true,
iife: true,
2022-07-12 03:39:36 +08:00
incremental: true,
2022-11-02 03:39:57 +08:00
minify: false,
node: false,
outputDirectory: outputDirectory,
removePragmas: false,
sourcemap: true,
2022-07-13 23:05:58 +08:00
write: false,
2022-07-12 03:39:36 +08:00
});
2022-11-02 03:39:57 +08:00
console.log(
`Cesium built in ${formatTimeSinceInSeconds(startTime)} seconds.`,
);
2022-07-12 03:39:36 +08:00
2025-10-28 00:12:33 +08:00
return { ...contexts, engine: engineContexts, widgets: widgetContexts };
2022-07-12 03:39:36 +08:00
}
// Delay execution of the callback until a short time has elapsed since it was last invoked, preventing
// calls to the same function in quick succession from triggering multiple builds.
const throttleDelay = 500;
const throttle = (callback) => {
let timeout;
return () =>
new Promise((resolve) => {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
resolve(callback());
}, throttleDelay);
});
};
2022-07-12 03:39:36 +08:00
(async function () {
const gzipHeader = Buffer.from("1F8B08", "hex");
2022-11-15 22:57:16 +08:00
const production = argv.production;
2023-02-22 23:11:07 +08:00
let contexts;
2022-11-15 22:57:16 +08:00
if (!production) {
2023-02-22 23:11:07 +08:00
contexts = await generateDevelopmentBuild();
const __dirname = path.dirname(fileURLToPath(import.meta.url));
if (
buildSandcastleApp &&
!fs.existsSync(path.join(__dirname, "/Apps/Sandcastle2/index.html"))
) {
// Sandcastle takes a bit of time to build and is unlikely to change often
// Only build it when we detect it doesn't exist to save on dev time
console.log("Building Sandcastle...");
2025-10-08 03:47:11 +08:00
const startTime = performance.now();
await buildSandcastleApp({
outputToBuildDir: false,
includeDevelopment: true,
});
2025-10-08 03:47:11 +08:00
console.log(
`Sandcastle built in ${formatTimeSinceInSeconds(startTime)} seconds.`,
);
}
2022-11-15 22:57:16 +08:00
}
2022-07-12 03:39:36 +08:00
2021-01-26 00:00:37 +08:00
const app = express();
2025-04-30 01:42:20 +08:00
app.use(function (req, res, next) {
// *NOTE* Any changes you make here must be mirrored in web.config.
const extensionToMimeType = {
".czml": "application/json",
".json": "application/json",
".geojson": "application/json",
".topojson": "application/json",
".wasm": "application/wasm",
".ktx2": "image/ktx2",
".gltf": "model/gltf+json",
".bgltf": "model/gltf-binary",
".glb": "model/gltf-binary",
".b3dm": "application/octet-stream",
".pnts": "application/octet-stream",
".i3dm": "application/octet-stream",
".cmpt": "application/octet-stream",
".geom": "application/octet-stream",
".vctr": "application/octet-stream",
".glsl": "text/plain",
};
const extension = path.extname(req.url);
if (extensionToMimeType[extension]) {
res.contentType(extensionToMimeType[extension]);
}
next();
});
app.use(compression());
2022-07-12 22:36:52 +08:00
//eslint-disable-next-line no-unused-vars
app.use(function (req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept",
);
next();
});
function checkGzipAndNext(req, res, next) {
2022-07-08 23:35:26 +08:00
const baseURL = `${req.protocol}://${req.headers.host}/`;
const reqUrl = new URL(req.url, baseURL);
2021-01-26 00:00:37 +08:00
const filePath = reqUrl.pathname.substring(1);
2021-01-26 00:00:37 +08:00
const readStream = fs.createReadStream(filePath, { start: 0, end: 2 });
2022-07-12 22:36:52 +08:00
//eslint-disable-next-line no-unused-vars
readStream.on("error", function (err) {
next();
});
readStream.on("data", function (chunk) {
if (chunk.equals(gzipHeader)) {
res.header("Content-Encoding", "gzip");
}
next();
});
}
2021-01-26 00:00:37 +08:00
const knownTilesetFormats = [
/\.b3dm/,
/\.pnts/,
/\.i3dm/,
/\.cmpt/,
/\.glb/,
/\.geom/,
/\.vctr/,
/\.subtree/,
/tileset.*\.json$/,
];
app.get(knownTilesetFormats, checkGzipAndNext);
2022-11-15 22:57:16 +08:00
if (!production) {
2023-09-15 03:56:25 +08:00
const iifeWorkersCache = new ContextCache(contexts.iifeWorkers);
2023-07-08 04:16:34 +08:00
const iifeCache = createRoute(
app,
2025-10-28 00:12:33 +08:00
"Build/CesiumUnminified/Cesium.js",
"/Build/CesiumUnminified/Cesium.js{.map}",
2023-09-09 03:59:52 +08:00
contexts.iife,
2023-09-15 03:56:25 +08:00
[iifeWorkersCache],
2023-07-08 04:16:34 +08:00
);
const esmCache = createRoute(
app,
2025-10-28 00:12:33 +08:00
"Build/CesiumUnminified/index.js",
"/Build/CesiumUnminified/index.js{.map}",
2023-07-08 04:16:34 +08:00
contexts.esm,
);
2023-09-15 03:56:25 +08:00
const workersCache = createRoute(
app,
2025-10-28 00:12:33 +08:00
"Build/CesiumUnminified/Workers/*",
2025-05-01 06:31:30 +08:00
"/Build/CesiumUnminified/Workers/*file.js",
2023-09-15 03:56:25 +08:00
contexts.workers,
);
2025-10-28 00:12:33 +08:00
const engineBundleCache = createRoute(
app,
"packages/engine/Build/Unminified/index.js",
"/packages/engine/Build/Unminified/index.js{.map}",
contexts.engine.esm,
);
const widgetsBundleCache = createRoute(
app,
"packages/widgets/Build/Unminified/index.js",
"/packages/widgets/Build/Unminified/index.js{.map}",
contexts.widgets.esm,
);
2022-07-12 22:36:52 +08:00
2024-10-09 01:49:52 +08:00
const glslWatcher = chokidar.watch("packages/engine/Source/Shaders", {
ignored: (path, stats) => {
return !!stats?.isFile() && !path.endsWith(".glsl");
},
ignoreInitial: true,
});
2022-11-15 22:57:16 +08:00
glslWatcher.on("all", async () => {
await glslToJavaScript(false, "Build/minifyShaders.state", "engine");
2023-07-08 04:16:34 +08:00
esmCache.clear();
iifeCache.clear();
2022-11-15 22:57:16 +08:00
});
2022-07-12 22:36:52 +08:00
2022-11-15 22:57:16 +08:00
let jsHintOptionsCache;
2025-10-28 00:12:33 +08:00
const engineSourceWatcher = chokidar.watch(["packages/engine/Source"], {
ignored: [
"packages/engine/Source/Shaders",
"packages/engine/Source/ThirdParty",
(path, stats) => {
return !!stats?.isFile() && !path.endsWith(".js");
},
],
ignoreInitial: true,
});
const widgetsSourceWatcher = chokidar.watch(["packages/widgets/Source"], {
ignored: [
"packages/widgets/Source/ThirdParty",
(path, stats) => {
return !!stats?.isFile() && !path.endsWith(".js");
},
],
ignoreInitial: true,
});
2025-10-28 00:12:33 +08:00
function clearTopLevelCaches() {
2023-07-08 04:16:34 +08:00
esmCache.clear();
iifeCache.clear();
workersCache.clear();
2023-09-15 03:56:25 +08:00
iifeWorkersCache.clear();
2022-11-15 22:57:16 +08:00
jsHintOptionsCache = undefined;
2025-10-28 00:12:33 +08:00
}
2025-10-28 00:12:33 +08:00
engineSourceWatcher.on("all", async () => {
clearTopLevelCaches();
engineBundleCache.clear();
await createIndexJs("engine");
await createCesiumJs();
});
widgetsSourceWatcher.on("all", async () => {
clearTopLevelCaches();
widgetsBundleCache.clear();
2025-10-28 00:12:33 +08:00
await createIndexJs("widgets");
2023-07-08 04:16:34 +08:00
await createCesiumJs();
2022-11-15 22:57:16 +08:00
});
2022-07-12 22:36:52 +08:00
2023-07-08 04:16:34 +08:00
const testWorkersCache = createRoute(
app,
"TestWorkers/*",
2025-05-01 06:31:30 +08:00
"/Build/Specs/TestWorkers/*file",
2023-07-08 04:16:34 +08:00
contexts.testWorkers,
);
chokidar
.watch(["Specs/TestWorkers/*.js"], { ignoreInitial: true })
.on("all", testWorkersCache.clear);
const specsCache = createRoute(
app,
"Specs/*",
2025-05-01 06:31:30 +08:00
"/Build/Specs/*file",
2023-07-08 04:16:34 +08:00
contexts.specs,
);
2024-10-09 01:49:52 +08:00
const specWatcher = chokidar.watch(
["packages/engine/Specs", "packages/widgets/Specs", "Specs"],
{
ignored: [
"packages/engine/Specs/SpecList.js",
"packages/widgets/Specs/SpecList.js",
"Specs/SpecList.js",
"Specs/e2e",
(path, stats) => {
return !!stats?.isFile() && !path.endsWith("Spec.js");
},
],
ignoreInitial: true,
},
);
2023-07-08 04:16:34 +08:00
specWatcher.on("all", async (event) => {
if (event === "add" || event === "unlink") {
await createCombinedSpecList();
}
2023-07-08 04:16:34 +08:00
specsCache.clear();
});
if (!production && getSandcastleConfig && buildSandcastleGallery) {
2025-09-05 05:13:07 +08:00
const { configPath, root, gallery } = await getSandcastleConfig();
const baseDirectory = path.relative(root, path.dirname(configPath));
const galleryFiles = gallery.files.map((pattern) =>
path.join(baseDirectory, pattern),
2025-09-03 08:49:57 +08:00
);
2025-09-05 05:13:07 +08:00
const galleryWatcher = chokidar.watch(galleryFiles, {
ignoreInitial: true,
});
galleryWatcher.on(
"all",
throttle(async () => {
const startTime = performance.now();
try {
2025-10-24 01:22:55 +08:00
await buildSandcastleGallery({ includeDevelopment: true });
console.log(
`Gallery built in ${formatTimeSinceInSeconds(startTime)} seconds.`,
);
} catch (e) {
console.error(e);
}
}),
);
2025-09-03 08:49:57 +08:00
}
2025-06-07 03:49:32 +08:00
2023-07-08 04:16:34 +08:00
// Rebuild jsHintOptions as needed and serve as-is
2022-11-15 22:57:16 +08:00
app.get(
"/Apps/Sandcastle/jsHintOptions.js",
async function (
//eslint-disable-next-line no-unused-vars
req,
res,
//eslint-disable-next-line no-unused-vars
next,
) {
if (!jsHintOptionsCache) {
jsHintOptionsCache = await createJsHintOptions();
}
2022-07-13 23:22:03 +08:00
2022-11-15 22:57:16 +08:00
res.append("Cache-Control", "max-age=0");
res.append("Content-Type", "application/javascript");
res.send(jsHintOptionsCache);
},
);
2022-07-12 22:36:52 +08:00
2022-11-15 22:57:16 +08:00
// Serve any static files starting with "Build/CesiumUnminified" from the
// development build instead. That way, previous build output is preserved
// while the latest is being served
app.use("/Build/CesiumUnminified", express.static("Build/CesiumDev"));
}
2022-07-12 22:36:52 +08:00
2022-09-09 00:43:28 +08:00
app.use(express.static(path.resolve(".")));
2021-01-26 00:00:37 +08:00
const server = app.listen(
argv.port,
argv.public ? undefined : "localhost",
function () {
if (argv.public) {
console.log(
"Cesium development server running publicly. Connect to http://localhost:%d/",
server.address().port,
);
} else {
console.log(
"Cesium development server running locally. Connect to http://localhost:%d/",
server.address().port,
);
}
},
);
server.on("error", function (e) {
if (e.code === "EADDRINUSE") {
console.log(
"Error: Port %d is already in use, select a different port.",
argv.port,
);
2022-09-10 03:28:32 +08:00
console.log("Example: node server.js --port %d", argv.port + 1);
} else if (e.code === "EACCES") {
console.log(
"Error: This process does not have permission to listen on port %d.",
argv.port,
);
if (argv.port < 1024) {
console.log("Try a port number higher than 1024.");
}
}
2023-09-26 00:41:34 +08:00
throw e;
});
server.on("close", function () {
console.log("Cesium development server stopped.");
2023-09-26 00:41:34 +08:00
process.exit(0);
});
2021-01-26 00:00:37 +08:00
let isFirstSig = true;
process.on("SIGINT", function () {
if (isFirstSig) {
console.log("\nCesium development server shutting down.");
2023-09-26 00:41:34 +08:00
server.close();
2022-11-15 22:57:16 +08:00
if (!production) {
2023-02-22 23:11:07 +08:00
contexts.esm.dispose();
contexts.iife.dispose();
contexts.workers.dispose();
2023-02-22 23:11:07 +08:00
contexts.specs.dispose();
2023-07-08 04:16:34 +08:00
contexts.testWorkers.dispose();
2022-11-15 22:57:16 +08:00
}
isFirstSig = false;
} else {
2023-09-26 00:41:34 +08:00
throw new Error("Cesium development server force kill.");
}
});
})();