mirror of https://github.com/CesiumGS/cesium.git
459 lines
12 KiB
JavaScript
459 lines
12 KiB
JavaScript
import fs from "fs";
|
|
import path from "path";
|
|
import { performance } from "perf_hooks";
|
|
import { fileURLToPath, URL } from "url";
|
|
|
|
import chokidar from "chokidar";
|
|
import compression from "compression";
|
|
import express from "express";
|
|
import yargs from "yargs";
|
|
|
|
import ContextCache from "./scripts/ContextCache.js";
|
|
import createRoute from "./scripts/createRoute.js";
|
|
|
|
import {
|
|
createCesiumJs,
|
|
createJsHintOptions,
|
|
createCombinedSpecList,
|
|
glslToJavaScript,
|
|
createIndexJs,
|
|
buildCesium,
|
|
buildEngine,
|
|
buildWidgets,
|
|
} from "./scripts/build.js";
|
|
|
|
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.",
|
|
},
|
|
production: {
|
|
type: "boolean",
|
|
description: "If true, skip build step and serve existing built files.",
|
|
},
|
|
})
|
|
.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");
|
|
|
|
function formatTimeSinceInSeconds(start) {
|
|
return Math.ceil((performance.now() - start) / 100) / 10;
|
|
}
|
|
|
|
/**
|
|
* Returns CesiumJS bundles configured for development.
|
|
*
|
|
* @returns {Bundles} The bundles.
|
|
*/
|
|
async function generateDevelopmentBuild() {
|
|
const startTime = performance.now();
|
|
|
|
// Build @cesium/engine index.js
|
|
console.log("[1/3] Building @cesium/engine...");
|
|
const engineContexts = await buildEngine({
|
|
incremental: true,
|
|
minify: false,
|
|
write: false,
|
|
});
|
|
|
|
// Build @cesium/widgets index.js
|
|
console.log("[2/3] Building @cesium/widgets...");
|
|
const widgetContexts = await buildWidgets({
|
|
incremental: true,
|
|
minify: false,
|
|
write: false,
|
|
});
|
|
|
|
// Build CesiumJS and save returned contexts for rebuilding upon request
|
|
console.log("[3/3] Building CesiumJS...");
|
|
const contexts = await buildCesium({
|
|
development: true,
|
|
iife: true,
|
|
incremental: true,
|
|
minify: false,
|
|
node: false,
|
|
outputDirectory: outputDirectory,
|
|
removePragmas: false,
|
|
sourcemap: true,
|
|
write: false,
|
|
});
|
|
|
|
console.log(
|
|
`Cesium built in ${formatTimeSinceInSeconds(startTime)} seconds.`,
|
|
);
|
|
|
|
return { ...contexts, engine: engineContexts, widgets: widgetContexts };
|
|
}
|
|
|
|
// 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);
|
|
});
|
|
};
|
|
|
|
(async function () {
|
|
const gzipHeader = Buffer.from("1F8B08", "hex");
|
|
const production = argv.production;
|
|
|
|
let contexts;
|
|
if (!production) {
|
|
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...");
|
|
const startTime = performance.now();
|
|
await buildSandcastleApp({
|
|
outputToBuildDir: false,
|
|
includeDevelopment: true,
|
|
});
|
|
console.log(
|
|
`Sandcastle built in ${formatTimeSinceInSeconds(startTime)} seconds.`,
|
|
);
|
|
}
|
|
}
|
|
|
|
const app = express();
|
|
|
|
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());
|
|
//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) {
|
|
const baseURL = `${req.protocol}://${req.headers.host}/`;
|
|
const reqUrl = new URL(req.url, baseURL);
|
|
const filePath = reqUrl.pathname.substring(1);
|
|
|
|
const readStream = fs.createReadStream(filePath, { start: 0, end: 2 });
|
|
//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();
|
|
});
|
|
}
|
|
|
|
const knownTilesetFormats = [
|
|
/\.b3dm/,
|
|
/\.pnts/,
|
|
/\.i3dm/,
|
|
/\.cmpt/,
|
|
/\.glb/,
|
|
/\.geom/,
|
|
/\.vctr/,
|
|
/\.subtree/,
|
|
/tileset.*\.json$/,
|
|
];
|
|
app.get(knownTilesetFormats, checkGzipAndNext);
|
|
|
|
if (!production) {
|
|
const iifeWorkersCache = new ContextCache(contexts.iifeWorkers);
|
|
const iifeCache = createRoute(
|
|
app,
|
|
"Build/CesiumUnminified/Cesium.js",
|
|
"/Build/CesiumUnminified/Cesium.js{.map}",
|
|
contexts.iife,
|
|
[iifeWorkersCache],
|
|
);
|
|
const esmCache = createRoute(
|
|
app,
|
|
"Build/CesiumUnminified/index.js",
|
|
"/Build/CesiumUnminified/index.js{.map}",
|
|
contexts.esm,
|
|
);
|
|
const workersCache = createRoute(
|
|
app,
|
|
"Build/CesiumUnminified/Workers/*",
|
|
"/Build/CesiumUnminified/Workers/*file.js",
|
|
contexts.workers,
|
|
);
|
|
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,
|
|
);
|
|
|
|
const glslWatcher = chokidar.watch("packages/engine/Source/Shaders", {
|
|
ignored: (path, stats) => {
|
|
return !!stats?.isFile() && !path.endsWith(".glsl");
|
|
},
|
|
ignoreInitial: true,
|
|
});
|
|
glslWatcher.on("all", async () => {
|
|
await glslToJavaScript(false, "Build/minifyShaders.state", "engine");
|
|
esmCache.clear();
|
|
iifeCache.clear();
|
|
});
|
|
|
|
let jsHintOptionsCache;
|
|
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,
|
|
});
|
|
|
|
function clearTopLevelCaches() {
|
|
esmCache.clear();
|
|
iifeCache.clear();
|
|
workersCache.clear();
|
|
iifeWorkersCache.clear();
|
|
jsHintOptionsCache = undefined;
|
|
}
|
|
|
|
engineSourceWatcher.on("all", async () => {
|
|
clearTopLevelCaches();
|
|
engineBundleCache.clear();
|
|
|
|
await createIndexJs("engine");
|
|
await createCesiumJs();
|
|
});
|
|
|
|
widgetsSourceWatcher.on("all", async () => {
|
|
clearTopLevelCaches();
|
|
widgetsBundleCache.clear();
|
|
|
|
await createIndexJs("widgets");
|
|
await createCesiumJs();
|
|
});
|
|
|
|
const testWorkersCache = createRoute(
|
|
app,
|
|
"TestWorkers/*",
|
|
"/Build/Specs/TestWorkers/*file",
|
|
contexts.testWorkers,
|
|
);
|
|
chokidar
|
|
.watch(["Specs/TestWorkers/*.js"], { ignoreInitial: true })
|
|
.on("all", testWorkersCache.clear);
|
|
|
|
const specsCache = createRoute(
|
|
app,
|
|
"Specs/*",
|
|
"/Build/Specs/*file",
|
|
contexts.specs,
|
|
);
|
|
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,
|
|
},
|
|
);
|
|
specWatcher.on("all", async (event) => {
|
|
if (event === "add" || event === "unlink") {
|
|
await createCombinedSpecList();
|
|
}
|
|
|
|
specsCache.clear();
|
|
});
|
|
|
|
if (!production && getSandcastleConfig && buildSandcastleGallery) {
|
|
const { configPath, root, gallery } = await getSandcastleConfig();
|
|
const baseDirectory = path.relative(root, path.dirname(configPath));
|
|
const galleryFiles = gallery.files.map((pattern) =>
|
|
path.join(baseDirectory, pattern),
|
|
);
|
|
const galleryWatcher = chokidar.watch(galleryFiles, {
|
|
ignoreInitial: true,
|
|
});
|
|
|
|
galleryWatcher.on(
|
|
"all",
|
|
throttle(async () => {
|
|
const startTime = performance.now();
|
|
try {
|
|
await buildSandcastleGallery({ includeDevelopment: true });
|
|
console.log(
|
|
`Gallery built in ${formatTimeSinceInSeconds(startTime)} seconds.`,
|
|
);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
|
|
// Rebuild jsHintOptions as needed and serve as-is
|
|
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();
|
|
}
|
|
|
|
res.append("Cache-Control", "max-age=0");
|
|
res.append("Content-Type", "application/javascript");
|
|
res.send(jsHintOptionsCache);
|
|
},
|
|
);
|
|
|
|
// 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"));
|
|
}
|
|
|
|
app.use(express.static(path.resolve(".")));
|
|
|
|
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,
|
|
);
|
|
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.");
|
|
}
|
|
}
|
|
|
|
throw e;
|
|
});
|
|
|
|
server.on("close", function () {
|
|
console.log("Cesium development server stopped.");
|
|
process.exit(0);
|
|
});
|
|
|
|
let isFirstSig = true;
|
|
process.on("SIGINT", function () {
|
|
if (isFirstSig) {
|
|
console.log("\nCesium development server shutting down.");
|
|
|
|
server.close();
|
|
|
|
if (!production) {
|
|
contexts.esm.dispose();
|
|
contexts.iife.dispose();
|
|
contexts.workers.dispose();
|
|
contexts.specs.dispose();
|
|
contexts.testWorkers.dispose();
|
|
}
|
|
|
|
isFirstSig = false;
|
|
} else {
|
|
throw new Error("Cesium development server force kill.");
|
|
}
|
|
});
|
|
})();
|