Cesium's live code editor and example gallery. Browse examples
highlighting features of the Cesium API and edit and run them
diff --git a/package.json b/package.json
index 99ce5e90b601..1e79c6b745ef 100644
--- a/package.json
+++ b/package.json
@@ -114,15 +114,15 @@
"build-watch": "gulp buildWatch",
"build-ts": "gulp buildTs",
"build-third-party": "gulp buildThirdParty",
- "build-apps": "gulp buildApps",
- "build-sandcastle": "npm run build-app --workspace packages/sandcastle",
+ "build-apps": "gulp -f gulpfile.apps.js buildApps",
+ "build-sandcastle": "gulp -f gulpfile.apps.js buildSandcastle",
"clean": "gulp clean",
"cloc": "gulp cloc",
"coverage": "gulp coverage",
"build-docs": "gulp buildDocs",
"build-docs-watch": "gulp buildDocsWatch",
"eslint": "eslint \"./**/*.*js\" \"./**/*.*ts*\" \"./**/*.html\" --cache --quiet",
- "make-zip": "gulp makeZip",
+ "make-zip": "gulp -f gulpfile.makezip.js makeZip",
"markdownlint": "markdownlint \"**/*.md\"",
"release": "gulp release",
"website-release": "gulp websiteRelease",
diff --git a/packages/sandcastle/README.md b/packages/sandcastle/README.md
index 6c14ae2a5daf..7f7c88147595 100644
--- a/packages/sandcastle/README.md
+++ b/packages/sandcastle/README.md
@@ -5,12 +5,23 @@ This package is the application for Sandcastle.
## Running/Building
- `npm run dev`: run the development server
-- `npm run build`: alias for `npm run build-app`
- `npm run build-app`: build to static files in `/Apps/Sandcastle2` for hosting/access from the root cesium dev server
-- `npm run build-ci`: build to static files in `/Apps/Sandcastle2` and configure paths as needed for CI deployment
Linting and style is managed under the project root's scripts.
+## Building Sandcastle
+
+There are 2 main conceptual ways that Sandcastle gets built which mostly revolve around how to access CesiumJS resources:
+
+1. Sandcastle points to "external" paths for CesiumJS resources
+2. Sandcastle is built to 1 static location that is co-located with all CesiumJS files. ie they're all copied into the built location
+
+The first method is useful and desired when developing the project locally and you want to refer to the actively built and updated CesiumJS files as you do other work. This is how the Sandcastle development server (`npm run dev`) and the local static version at `/Apps/Sandcastle2` are built.
+
+The second method is used when building Sandcastle to be deployed to the website or other static location. You can think of this as "bundling" all the necessary files needed for Sandcastle into 1 single directory.
+
+Regardless the method you want to use Sandcastle is always built using the exported `buildStatic`, `createSandcastleConfig` and `buildGalleryList` functions.
+
## Gallery structure
The gallery for Sandcastle is located in the `gallery` directory. A "single sandcastle" consists of 4 files which should be contained in a sub-directory that matches the id of the sandcastle.
@@ -48,6 +59,10 @@ thumbnail: thumbnail.jpg
development: false
```
+### Thumbnails
+
+Thumbnails should be any image that represents what the sandcastle does. Often this will just be the Viewer with or without any Sandcastle interaction buttons. Thumbnail files should be limited in size to help save on bandwidth. Currently most are around 225px in width.
+
## Expanding the ESLint configuration
diff --git a/packages/sandcastle/gallery/cesium-inspector/sandcastle.yaml b/packages/sandcastle/gallery/cesium-inspector/sandcastle.yaml
index 4c7f67aecd8d..5c3e6d029227 100644
--- a/packages/sandcastle/gallery/cesium-inspector/sandcastle.yaml
+++ b/packages/sandcastle/gallery/cesium-inspector/sandcastle.yaml
@@ -5,3 +5,4 @@ labels:
- Development
- Entities
thumbnail: thumbnail.jpg
+development: true
diff --git a/packages/sandcastle/gallery/fog/sandcastle.yaml b/packages/sandcastle/gallery/fog/sandcastle.yaml
index c8910f10cad0..deb0d5c6dd64 100644
--- a/packages/sandcastle/gallery/fog/sandcastle.yaml
+++ b/packages/sandcastle/gallery/fog/sandcastle.yaml
@@ -4,3 +4,4 @@ description: Control fog parameters.
labels:
- Development
thumbnail: thumbnail.jpg
+development: true
diff --git a/packages/sandcastle/index.html b/packages/sandcastle/index.html
index 9e8ca7c615e0..aa91a42847a0 100644
--- a/packages/sandcastle/index.html
+++ b/packages/sandcastle/index.html
@@ -24,7 +24,7 @@
}
-
+
diff --git a/packages/sandcastle/index.js b/packages/sandcastle/index.js
new file mode 100644
index 000000000000..066e014278c9
--- /dev/null
+++ b/packages/sandcastle/index.js
@@ -0,0 +1,2 @@
+export { buildGalleryList } from "./scripts/buildGallery.js";
+export { buildStatic, createSandcastleConfig } from "./scripts/buildStatic.js";
diff --git a/packages/sandcastle/package.json b/packages/sandcastle/package.json
index c6490514726c..f7a9c47add74 100644
--- a/packages/sandcastle/package.json
+++ b/packages/sandcastle/package.json
@@ -3,15 +3,10 @@
"private": true,
"version": "0.0.4",
"type": "module",
- "files": [
- "scripts/buildGallery.js"
- ],
+ "main": "index.js",
"scripts": {
"dev": "npm run build-gallery && vite --config vite.config.dev.ts",
- "build": "npm run build-app",
- "build-app": "tsc -b && npm run build-gallery && vite build --config vite.config.app.ts",
- "build-ci": "tsc -b && npm run build-gallery && vite build --config vite.config.ci.ts",
- "build-prod": "tsc -b && npm run build-gallery && vite build --config vite.config.prod.ts",
+ "build": "echo 'Sandcastle cannot be built directly. Use the exported buildStatic() function instead'",
"build-gallery": "node scripts/buildGallery.js"
},
"dependencies": {
@@ -30,6 +25,7 @@
"react-dom": "^19.0.0",
"react-stay-scrolled": "^9.0.0",
"react-use": "^17.6.0",
+ "typescript": "^5.9.3",
"yargs": "^18.0.0"
},
"devDependencies": {
@@ -43,7 +39,6 @@
"pagefind": "^1.3.0",
"rimraf": "^6.0.1",
"slugify": "^1.6.6",
- "typescript": "^5.9.3",
"vite": "^7.1.9",
"vite-plugin-static-copy": "^3.1.3",
"yaml": "^2.8.0"
diff --git a/packages/sandcastle/sandcastle.config.js b/packages/sandcastle/sandcastle.config.js
index a9e6103e4bdc..ee6778e1b516 100644
--- a/packages/sandcastle/sandcastle.config.js
+++ b/packages/sandcastle/sandcastle.config.js
@@ -3,7 +3,7 @@ import process from "process";
const config = {
root: ".",
sourceUrl: "https://github.com/CesiumGS/cesium/blob/main/packages/sandcastle",
- publicDir: "./public",
+ publicDirectory: "./public",
gallery: {
files: ["gallery"],
searchOptions: {
diff --git a/packages/sandcastle/scripts/buildGallery.js b/packages/sandcastle/scripts/buildGallery.js
index f03d0caa2037..eaa1dceeafb7 100644
--- a/packages/sandcastle/scripts/buildGallery.js
+++ b/packages/sandcastle/scripts/buildGallery.js
@@ -174,7 +174,11 @@ export async function buildGalleryList(options = {}) {
if (
check(!/^[a-zA-Z0-9-.]+$/.test(slug), `"${slug}" is not a valid slug`) ||
check(!title, `${slug} - Missing title`) ||
- check(!description, `${slug} - Missing description`)
+ check(!description, `${slug} - Missing description`) ||
+ check(
+ !development && labels.includes("Development"),
+ `${slug} has Development label but not marked as development sandcastle`,
+ )
) {
continue;
}
@@ -300,7 +304,7 @@ if (import.meta.url.endsWith(`${pathToFileURL(process.argv[1])}`)) {
try {
const config = await import(pathToFileURL(configPath).href);
- const { root, publicDir, gallery, sourceUrl } = config.default;
+ const { root, publicDirectory, gallery, sourceUrl } = config.default;
// Paths are specified relative to the config file
const configDir = dirname(configPath);
@@ -316,7 +320,7 @@ if (import.meta.url.endsWith(`${pathToFileURL(process.argv[1])}`)) {
buildGalleryOptions = {
rootDirectory: configRoot,
- publicDirectory: publicDir,
+ publicDirectory: publicDirectory,
galleryFiles: files,
sourceUrl,
defaultThumbnail,
diff --git a/packages/sandcastle/scripts/buildStatic.js b/packages/sandcastle/scripts/buildStatic.js
new file mode 100644
index 000000000000..2089fe33abee
--- /dev/null
+++ b/packages/sandcastle/scripts/buildStatic.js
@@ -0,0 +1,152 @@
+import { build, defineConfig } from "vite";
+import baseConfig from "../vite.config.js";
+import { fileURLToPath } from "url";
+import { viteStaticCopy } from "vite-plugin-static-copy";
+import { dirname, join } from "path";
+import { cesiumPathReplace, insertImportMap } from "../vite-plugins.js";
+import typescriptCompile from "./typescriptCompile.js";
+
+/** @import { UserConfig, LogLevel } from 'vite' */
+
+/**
+ * @typedef {Object} ImportObject
+ * @property {string} path The path to use for the import map. ie the path the app can expect to find this at
+ * @property {string} typesPath The path to use for intellisense types in monaco
+ */
+
+/**
+ * @typedef {Object} ImportList
+ */
+
+/**
+ * Check if the given key is in the imports list and throw an error if not
+ * @param {ImportList} imports
+ * @param {string} name
+ */
+function checkForImport(imports, name) {
+ if (!imports[name]) {
+ throw new Error(`Missing import for ${name}`);
+ }
+}
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+
+/**
+ * Create the Vite configuration for building Sandcastle.
+ * Set where it should build to and the base path for vite and CesiumJS files.
+ *
+ * Most importantly specify the paths the app can find the library imports.
+ *
+ * If you are copying files to the built directory ensure the source files exist BEFORE attempting to build Sandcastle
+ *
+ * @param {object} options
+ * @param {string} options.outDir Path to build files into
+ * @param {string} options.basePath Base path for files/routes
+ * @param {string} options.cesiumBaseUrl Base path for CesiumJS. This should include the CesiumJS assets and workers etc.
+ * @param {string} options.cesiumVersion CesiumJS version to display in the top right
+ * @param {string} [options.commitSha] Optional commit hash to display in the top right of the application
+ * @param {ImportList} options.imports Set of imports to add to the import map for the iframe and standalone html pages. These paths should match the URL where it can be accessed within the current environment.
+ * @param {{src: string, dest: string}[]} [options.copyExtraFiles] Extra paths passed to viteStaticCopy. Use this to consolidate files for a singular static deployment (ie during production). Source paths should be absolute, dest paths should be relative to the page root. It is up to you to ensure these files exist BEFORE building sandcastle.
+ */
+export function createSandcastleConfig({
+ outDir,
+ basePath,
+ cesiumBaseUrl,
+ cesiumVersion,
+ commitSha,
+ imports,
+ copyExtraFiles = [],
+}) {
+ if (!cesiumVersion || cesiumVersion === "") {
+ throw new Error("Must provide a CesiumJS version");
+ }
+
+ /** @type {UserConfig} */
+ const config = { ...baseConfig };
+
+ config.base = basePath;
+
+ config.build = {
+ ...config.build,
+ outDir: outDir,
+ };
+
+ const copyPlugin = viteStaticCopy({
+ targets: [
+ { src: "templates/Sandcastle.(d.ts|js)", dest: "templates" },
+ ...copyExtraFiles,
+ ],
+ });
+
+ checkForImport(imports, "cesium");
+ checkForImport(imports, "@cesium/engine");
+ checkForImport(imports, "@cesium/widgets");
+ if (imports["Sandcastle"]) {
+ throw new Error(
+ "Don't specify the Sandcastle import this is taken care of internally",
+ );
+ }
+
+ /** @type {Object} */
+ const importMap = {
+ Sandcastle: "../templates/Sandcastle.js",
+ };
+ /** @type {Object} */
+ const typePaths = {
+ Sandcastle: "../templates/Sandcastle.d.ts",
+ };
+ for (const [key, value] of Object.entries(imports)) {
+ importMap[key] = value.path;
+ typePaths[key] = value.typesPath;
+ }
+
+ config.define = {
+ ...config.define,
+ __VITE_TYPE_IMPORT_PATHS__: JSON.stringify(typePaths),
+ __CESIUM_VERSION__: JSON.stringify(`Cesium ${cesiumVersion}`),
+ __COMMIT_SHA__: JSON.stringify(commitSha ?? undefined),
+ };
+
+ const plugins = config.plugins ?? [];
+ config.plugins = [
+ ...plugins,
+ copyPlugin,
+ cesiumPathReplace(cesiumBaseUrl),
+ insertImportMap(importMap, ["bucket.html", "standalone.html"]),
+ ];
+
+ return defineConfig(config);
+}
+
+/**
+ * Build Sandcastle out to a specified location as static files.
+ * The config should be generated with the createSandcastleConfig function.
+ *
+ * The build will only set up the paths for "external" resources from the app.
+ * If you are copying files to the built directory ensure the source files exist BEFORE attempting to build Sandcastle
+ *
+ * @param {UserConfig} config
+ * @param {LogLevel} logLevel
+ * @returns {Promise}
+ */
+export async function buildStatic(config, logLevel = "warn") {
+ // We have to do the compile for the Sandcastle API outside of the vite build
+ // because we need to reference the js file and types directly from the app
+ // and we don't want them bundled with the rest of the code
+ const exitCode = await typescriptCompile(
+ join(__dirname, "../templates/tsconfig.lib.json"),
+ );
+
+ if (exitCode === 0) {
+ console.log(`Sandcastle typescript build complete`);
+ } else {
+ throw new Error("Sandcastle typescript build failed");
+ }
+
+ console.log("Building Sandcastle with Vite");
+ await build({
+ ...config,
+ root: join(__dirname, "../"),
+ logLevel,
+ });
+}
diff --git a/packages/sandcastle/scripts/typescriptCompile.js b/packages/sandcastle/scripts/typescriptCompile.js
new file mode 100644
index 000000000000..22d667f485e2
--- /dev/null
+++ b/packages/sandcastle/scripts/typescriptCompile.js
@@ -0,0 +1,33 @@
+import { spawn } from "node:child_process";
+import { join } from "node:path";
+import { fileURLToPath } from "node:url";
+
+/**
+ * Compile a typescript project from it's config file using the tsc CLI
+ *
+ * @param {string} configPath Absolute path to the config file to build
+ * @returns {number} exit code from the tsc command
+ */
+export default async function typescriptCompile(configPath) {
+ const tsPath = import.meta.resolve("typescript");
+ const binPath = fileURLToPath(join(tsPath, "../../bin/tsc"));
+ return new Promise((resolve, reject) => {
+ const ls = spawn(binPath, ["-p", configPath]);
+
+ ls.stdout.on("data", (data) => {
+ console.log(`stdout: ${data}`);
+ });
+
+ ls.stderr.on("data", (data) => {
+ console.error(`stderr: ${data}`);
+ });
+
+ ls.on("close", (code) => {
+ if (code === 0) {
+ resolve(code);
+ } else {
+ reject(code);
+ }
+ });
+ });
+}
diff --git a/packages/sandcastle/src/SandcastleEditor.tsx b/packages/sandcastle/src/SandcastleEditor.tsx
index be7ea1bf8a1b..38bd83dad378 100644
--- a/packages/sandcastle/src/SandcastleEditor.tsx
+++ b/packages/sandcastle/src/SandcastleEditor.tsx
@@ -51,9 +51,6 @@ self.MonacoEnvironment = {
// open network access
loader.config({ monaco });
-const TYPES_URL = `${__PAGE_BASE_URL__}Source/Cesium.d.ts`;
-const SANDCASTLE_TYPES_URL = `templates/Sandcastle.d.ts`;
-
export type SandcastleEditorRef = {
formatCode(): void;
};
@@ -210,25 +207,63 @@ function SandcastleEditor({
async function setTypes(monaco: Monaco) {
// https://microsoft.github.io/monaco-editor/playground.html?source=v0.52.2#example-extending-language-services-configure-javascript-defaults
- const cesiumTypes = await (await fetch(TYPES_URL)).text();
- // define a "global" variable so types work even with out the import statement
- const cesiumTypesWithGlobal = `${cesiumTypes}\nvar Cesium: typeof import('cesium');`;
- monaco.languages.typescript.javascriptDefaults.addExtraLib(
- cesiumTypesWithGlobal,
- "ts:cesium.d.ts",
+ const typeImportPaths = __VITE_TYPE_IMPORT_PATHS__ ?? {};
+
+ const typeImports: {
+ url: string;
+ filename: string;
+ transformTypes?: (typesContent: string) => string;
+ }[] = [
+ {
+ url: typeImportPaths["cesium"],
+ filename: "ts:cesium.d.ts",
+ transformTypes(typesContent: string) {
+ // define a "global" variable so types work even with out the import statement
+ return `${typesContent}\nvar Cesium: typeof import('cesium');`;
+ },
+ },
+ {
+ url: typeImportPaths["Sandcastle"],
+ filename: "ts:sandcastle.d.ts",
+ transformTypes(typesContent: string) {
+ return `declare module 'Sandcastle' {
+ ${typesContent}
+ }
+ var Sandcastle: typeof import('Sandcastle').default;`;
+ },
+ },
+ ];
+
+ const extraImportNames = Object.keys(typeImportPaths).filter(
+ (name) => !["cesium", "Sandcastle"].includes(name),
);
+ for (const extraName of extraImportNames) {
+ typeImports.push({
+ url: typeImportPaths[extraName],
+ filename: `ts:${extraName.replace(/@\//, "-")}.d.ts`,
+ });
+ }
- const sandcastleTypes = await (await fetch(SANDCASTLE_TYPES_URL)).text();
- // surround in a module so the import statement works nicely
- // also define a "global" so types show even if you don't have the import
- const sandcastleModuleTypes = `declare module 'Sandcastle' {
- ${sandcastleTypes}
- }
- var Sandcastle: typeof import('Sandcastle').default;`;
-
- monaco.languages.typescript.javascriptDefaults.addExtraLib(
- sandcastleModuleTypes,
- "ts:sandcastle.d.ts",
+ await Promise.allSettled(
+ typeImports.map(async (typeImport) => {
+ const { url, transformTypes, filename } = typeImport;
+ if (!url) {
+ return;
+ }
+ try {
+ const responseText = await (await fetch(url)).text();
+ const typesContent = transformTypes
+ ? transformTypes(responseText)
+ : responseText;
+ monaco.languages.typescript.javascriptDefaults.addExtraLib(
+ typesContent,
+ filename,
+ );
+ } catch (error) {
+ console.error(`Unable to load types for ${filename} at ${url}`);
+ console.error(error);
+ }
+ }),
);
}
diff --git a/packages/sandcastle/standalone.html b/packages/sandcastle/standalone.html
index a6694686596d..26aea087d302 100644
--- a/packages/sandcastle/standalone.html
+++ b/packages/sandcastle/standalone.html
@@ -8,14 +8,9 @@
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
Cesium Demo
-
+
+
@@ -104,7 +99,11 @@
unstable_loadStyles(document);
-
+
diff --git a/packages/sandcastle/templates/bucket.html b/packages/sandcastle/templates/bucket.html
index e56946aca7bf..99ee48356fbf 100644
--- a/packages/sandcastle/templates/bucket.html
+++ b/packages/sandcastle/templates/bucket.html
@@ -8,14 +8,6 @@
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
Cesium Demo
-