diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8f4e156b4e0b..9a3b50ea95e8 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,7 @@ name: deploy on: push: branches-ignore: - - 'cesium.com' + - "cesium.com" - production concurrency: group: deploy-${{ github.ref }} @@ -22,14 +22,13 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_REPO: ${{ github.repository }} GITHUB_SHA: ${{ github.sha }} - BASE_URL: /cesium/${{ github.ref_name }}/ DEPLOYED_URL: https://ci-builds.cesium.com/cesium/${{ github.ref_name }}/ steps: - uses: actions/checkout@v5 - name: install node 22 uses: actions/setup-node@v5 with: - node-version: '22' + node-version: "22" - name: npm install run: npm install - name: set the version in package.json @@ -42,8 +41,6 @@ jobs: run: npm pack --workspaces &> /dev/null - name: build apps run: npm run build-apps - - name: build sandcastle v2 - run: npm run build-ci -w packages/sandcastle -- -l warn - uses: ./.github/actions/verify-package - name: deploy to s3 if: ${{ env.AWS_ACCESS_KEY_ID != '' }} diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index 628d8890ac5e..571c956aea34 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -41,14 +41,17 @@ jobs: run: npm install - name: build website release run: npm run website-release - - name: build apps - run: npm run build-apps - name: build types run: npm run build-ts - - name: build prod sandcastle - run: npm run build-prod -w packages/sandcastle -- -l warn + - name: build apps + run: npm run build-apps - name: deploy to cesium.com if: ${{ env.AWS_ACCESS_KEY_ID != '' }} + # Download zip from the Github release and unzip to Build/release/ + # Publish that unzipped code to the bucket for https://cesium.com/downloads/cesiumjs/releases/[version]/... urls + # Publish the documentation files to the bucket for https://cesium.com/learn/cesiumjs/ref-doc/... urls + # Publish the simple viewer app + # Publish sandcastle to the bucket for https://sandcastle.cesium.com/ run: | curl -LO $(curl https://api.github.com/repos/CesiumGS/cesium/releases/latest -H "Authorization: ${GITHUB_TOKEN}" | jq -r '.assets[0].browser_download_url') unzip Cesium-$(cat package.json | jq -r '.version' | sed 's/\.0$//').zip -d Build/release/ -x "Apps" diff --git a/.github/workflows/sandcastle-dev.yml b/.github/workflows/sandcastle-dev.yml index 1aec62462108..e07cf8a343a0 100644 --- a/.github/workflows/sandcastle-dev.yml +++ b/.github/workflows/sandcastle-dev.yml @@ -28,7 +28,7 @@ jobs: - name: build types run: npm run build-ts - name: build prod sandcastle - run: npm run build-prod -w packages/sandcastle -- -l warn + run: npm run build-sandcastle - name: deploy to dev-sandcastle.cesium.com if: ${{ env.AWS_ACCESS_KEY_ID != '' }} run: | diff --git a/gulpfile.js b/gulpfile.js index 50cddce9cba2..54b9697125bf 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -30,6 +30,8 @@ import { createCombinedSpecList, createJsHintOptions, defaultESBuildOptions, + buildSandcastleGallery, + buildNewSandcastleApp, } from "./scripts/build.js"; // Determines the scope of the workspace packages. If the scope is set to cesium, the workspaces should be @cesium/engine. @@ -287,9 +289,20 @@ export async function buildTs() { await createTypeScriptDefinitions(); } -export function buildApps() { - return Promise.all([buildCesiumViewer(), buildSandcastle()]); -} +export const buildNewSandcastle = gulp.series( + async function buildSandcastleApp() { + return buildNewSandcastleApp(isProduction); + }, + async function buildGallery() { + return buildSandcastleGallery(!isProduction); + }, +); + +export const buildApps = gulp.parallel( + buildCesiumViewer, + buildSandcastle, + buildNewSandcastle, +); const filesToClean = [ "Source/Cesium.js", @@ -425,17 +438,6 @@ export async function buildDocsWatch() { return gulp.watch(sourceFiles, buildDocs); } -function combineForSandcastle() { - const outputDirectory = join("Build", "Sandcastle", "CesiumUnminified"); - return buildCesium({ - development: false, - minify: false, - removePragmas: false, - node: false, - outputDirectory: outputDirectory, - }); -} - export const websiteRelease = gulp.series( buildEngine, buildWidgets, @@ -447,14 +449,23 @@ export const websiteRelease = gulp.series( node: false, }); }, - function () { + function websiteReleaseBuildMinified() { return buildCesium({ minify: true, removePragmas: true, node: false, }); }, - combineForSandcastle, + function combineForSandcastle() { + const outputDirectory = join("Build", "Sandcastle", "CesiumUnminified"); + return buildCesium({ + development: false, + minify: false, + removePragmas: false, + node: false, + outputDirectory: outputDirectory, + }); + }, buildDocs, ); @@ -584,121 +595,129 @@ async function pruneScriptsForZip(packageJsonPath) { }); } -export const makeZip = gulp.series(release, async function createZipFile() { - //For now we regenerate the JS glsl to force it to be unminified in the release zip - //See https://github.com/CesiumGS/cesium/pull/3106#discussion_r42793558 for discussion. - await glslToJavaScript(false, "Build/minifyShaders.state", "engine"); - - const packageJsonSrc = await pruneScriptsForZip("package.json"); - const enginePackageJsonSrc = await pruneScriptsForZip( - "packages/engine/package.json", - ); - const widgetsPackageJsonSrc = await pruneScriptsForZip( - "packages/widgets/package.json", - ); +export const makeZip = gulp.series( + release, + async function buildSandcastleApp() { + return buildNewSandcastleApp(false); + }, + async function buildGallery() { + return buildSandcastleGallery(false); + }, + async function createZipFile() { + //For now we regenerate the JS glsl to force it to be unminified in the release zip + //See https://github.com/CesiumGS/cesium/pull/3106#discussion_r42793558 for discussion. + await glslToJavaScript(false, "Build/minifyShaders.state", "engine"); + + const packageJsonSrc = await pruneScriptsForZip("package.json"); + const enginePackageJsonSrc = await pruneScriptsForZip( + "packages/engine/package.json", + ); + const widgetsPackageJsonSrc = await pruneScriptsForZip( + "packages/widgets/package.json", + ); - const src = gulp - .src("index.release.html") - .pipe( - gulpRename((file) => { - if (file.basename === "index.release") { - file.basename = "index"; - } - }), - ) - .pipe(enginePackageJsonSrc) - .pipe(widgetsPackageJsonSrc) - .pipe(packageJsonSrc) - .pipe( - gulpRename((file) => { - if (file.basename === "package.noprepare") { - file.basename = "package"; - } - }), - ) - .pipe( - gulp.src( - [ - "Build/Cesium/**", - "Build/CesiumUnminified/**", - "Build/Documentation/**", - "Build/Specs/**", - "Build/package.json", - "packages/engine/Build/**", - "packages/widgets/Build/**", - "!Build/Specs/e2e/**", - "!Build/InlineWorkers.js", - "!packages/engine/Build/Specs/**", - "!packages/widgets/Build/Specs/**", - "!packages/engine/Build/minifyShaders.state", - ], - { - encoding: false, - base: ".", - }, - ), - ) - .pipe( - gulp.src( - [ - "Apps/**", - "Apps/Sandcastle/.jshintrc", - "packages/engine/index.js", - "packages/engine/index.d.ts", - "packages/engine/LICENSE.md", - "packages/engine/README.md", - "packages/engine/Source/**", - "packages/widgets/index.js", - "packages/widgets/index.d.ts", - "packages/widgets/LICENSE.md", - "packages/widgets/README.md", - "packages/widgets/Source/**", - "Source/**", - "Specs/**", - "ThirdParty/**", - "scripts/**", - "favicon.ico", - ".prettierignore", - "eslint.config.js", - "gulpfile.js", - "server.js", - "index.cjs", - "LICENSE.md", - "CHANGES.md", - "README.md", - "web.config", - "!**/*.gitignore", - "!Specs/e2e/*-snapshots/**", - "!Apps/Sandcastle/gallery/development/**", - "!Apps/Sandcastle2/**", - ], - { - encoding: false, - base: ".", - }, - ), - ) - .pipe( - gulpTap(function (file) { - // Work around an issue with gulp-zip where archives generated on Windows do - // not properly have their directory executable mode set. - // see https://github.com/sindresorhus/gulp-zip/issues/64#issuecomment-205324031 - if (file.isDirectory()) { - file.stat.mode = parseInt("40777", 8); - } - }), - ) - .pipe(gulpZip(`Cesium-${version}.zip`)) - .pipe(gulp.dest(".")); + const src = gulp + .src("index.release.html") + .pipe( + gulpRename((file) => { + if (file.basename === "index.release") { + file.basename = "index"; + } + }), + ) + .pipe(enginePackageJsonSrc) + .pipe(widgetsPackageJsonSrc) + .pipe(packageJsonSrc) + .pipe( + gulpRename((file) => { + if (file.basename === "package.noprepare") { + file.basename = "package"; + } + }), + ) + .pipe( + gulp.src( + [ + "Build/Cesium/**", + "Build/CesiumUnminified/**", + "Build/Documentation/**", + "Build/Specs/**", + "Build/package.json", + "packages/engine/Build/**", + "packages/widgets/Build/**", + "!Build/Specs/e2e/**", + "!Build/InlineWorkers.js", + "!packages/engine/Build/Specs/**", + "!packages/widgets/Build/Specs/**", + "!packages/engine/Build/minifyShaders.state", + ], + { + encoding: false, + base: ".", + }, + ), + ) + .pipe( + gulp.src( + [ + "Apps/**", + "Apps/Sandcastle/.jshintrc", + "packages/engine/index.js", + "packages/engine/index.d.ts", + "packages/engine/LICENSE.md", + "packages/engine/README.md", + "packages/engine/Source/**", + "packages/widgets/index.js", + "packages/widgets/index.d.ts", + "packages/widgets/LICENSE.md", + "packages/widgets/README.md", + "packages/widgets/Source/**", + "Source/**", + "Specs/**", + "ThirdParty/**", + "scripts/**", + "favicon.ico", + ".prettierignore", + "eslint.config.js", + "gulpfile.js", + "server.js", + "index.cjs", + "LICENSE.md", + "CHANGES.md", + "README.md", + "web.config", + "!**/*.gitignore", + "!Specs/e2e/*-snapshots/**", + "!Apps/Sandcastle/gallery/development/**", + ], + { + encoding: false, + base: ".", + }, + ), + ) + .pipe( + gulpTap(function (file) { + // Work around an issue with gulp-zip where archives generated on Windows do + // not properly have their directory executable mode set. + // see https://github.com/sindresorhus/gulp-zip/issues/64#issuecomment-205324031 + if (file.isDirectory()) { + file.stat.mode = parseInt("40777", 8); + } + }), + ) + .pipe(gulpZip(`Cesium-${version}.zip`)) + .pipe(gulp.dest(".")); - await finished(src); + await finished(src); - rimraf.sync("./package.noprepare.json"); - rimraf.sync("./packages/engine/package.noprepare.json"); - rimraf.sync("./packages/widgets/package.noprepare.json"); + rimraf.sync("./package.noprepare.json"); + rimraf.sync("./packages/engine/package.noprepare.json"); + rimraf.sync("./packages/widgets/package.noprepare.json"); - return src; -}); + return src; + }, +); export async function deploySetVersion() { const buildVersion = argv.buildVersion; @@ -716,13 +735,18 @@ export async function deployStatus() { const deployUrl = `${devDeployUrl}`; const zipUrl = `${deployUrl}Cesium-${version}.zip`; const npmUrl = `${deployUrl}cesium-${version}.tgz`; - const coverageUrl = `${devDeployUrl}Build/Coverage/index.html`; + const coverageUrl = `${deployUrl}Build/Coverage/index.html`; return Promise.all([ - setStatus(status, deployUrl, message, "deployment"), - setStatus(status, zipUrl, message, "zip file"), - setStatus(status, npmUrl, message, "npm package"), - setStatus(status, coverageUrl, message, "coverage results"), + setStatus(status, deployUrl, message, "deploy / artifact: deployment"), + setStatus(status, zipUrl, message, "deploy / artifact: zip file"), + setStatus(status, npmUrl, message, "deploy / artifact: npm package"), + setStatus( + status, + coverageUrl, + message, + "deploy / artifact: coverage results", + ), ]); } @@ -1237,13 +1261,6 @@ function generateTypeScriptDefinitions( "raiseEvent(...arguments: Parameters): void;", ); - // Wrap the source to actually be inside of a declared cesium module - // and add any workaround and private utility types. - source = `declare module "@${scope}/${workspaceName}" { -${source} -} -`; - if (importModules) { let imports = ""; Object.keys(importModules).forEach((workspace) => { @@ -1257,6 +1274,13 @@ ${source} source = imports + source; } + // Wrap the source to actually be inside of a declared cesium module + // and add any workaround and private utility types. + source = `declare module "@${scope}/${workspaceName}" { +${source} +} +`; + // Write the final source file back out writeFileSync(definitionsPath, source); diff --git a/index.release.html b/index.release.html index 0cf60e9f9de2..ef8ad8a45484 100644 --- a/index.release.html +++ b/index.release.html @@ -90,7 +90,7 @@

Local links

- Sandcastle + Sandcastle 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 500dd8cb3a53..29cc47624651 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,7 @@ "build-ts": "gulp buildTs", "build-third-party": "gulp buildThirdParty", "build-apps": "gulp buildApps", - "build-sandcastle": "npm run build-app --workspace packages/sandcastle", + "build-sandcastle": "gulp buildNewSandcastle", "clean": "gulp clean", "cloc": "gulp cloc", "coverage": "gulp coverage", @@ -171,4 +171,4 @@ } } } -} \ No newline at end of file +} 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 d7dc776cf46b..6b71466cea97 100644 --- a/packages/sandcastle/package.json +++ b/packages/sandcastle/package.json @@ -3,15 +3,10 @@ "private": true, "version": "0.0.3", "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.8.3", "yargs": "^18.0.0" }, "devDependencies": { @@ -44,7 +40,6 @@ "pagefind": "^1.3.0", "rimraf": "^6.0.1", "slugify": "^1.6.6", - "typescript": "~5.8.3", "vite": "^6.2.0", "vite-plugin-static-copy": "^2.3.1", "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 9d6d61af0dcd..fd27c99f7788 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..3e010a6f2b22 --- /dev/null +++ b/packages/sandcastle/scripts/buildStatic.js @@ -0,0 +1,151 @@ +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 + */ +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 df15be1220c6..be1753352136 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 -