From abd39dfec3f1ca4a08f06b02026096deda241ea9 Mon Sep 17 00:00:00 2001 From: Nanda Date: Mon, 29 Apr 2024 14:53:03 +1000 Subject: [PATCH 01/10] New build process - Switched to esbuild for bundling - New watch mode - New helper scripts in package.json for testing, linting and typechecking. --- .gitignore | 1 + karma.conf.cjs | 55 ++++ lib/README.md | 1 + {src => lib}/declarations.d.ts | 5 + lib/withSvgSprite.ts | 28 ++ package.json | 47 +++- scripts/bundle.js | 187 +++++++++++++ scripts/checkDevEnv.js | 253 ++++++++++++++++++ scripts/copyCesiumAssets.js | 34 +++ scripts/esbuild/preferEsmModule.js | 17 ++ scripts/esbuild/replaceModule.js | 16 ++ scripts/esbuild/selectLoader.js | 47 ++++ scripts/esbuild/skipExternalModules.js | 14 + scripts/esbuild/svgSprite.js | 62 +++++ scripts/esbuild/writeBuildTimestamp.js | 8 + scripts/findYarnWorkspaceRoot.js | 54 ++++ scripts/isMain.js | 5 + specs/Models/Box3dCatalogItemSpec.ts | 15 ++ specs/SpecMain.ts | 17 ++ specs/jasmine.d.ts | 1 + src/Views/DrawRectangle.tsx | 2 +- src/Views/Foo.jsx | 3 + src/Views/Main.tsx | 2 +- src/index.ts | 4 +- tsconfig.json | 28 +- types/index.d.ts | 3 + types/lib/withSvgSprite.d.ts | 9 + .../Models/Traits/Box3dCatalogItemTraits.d.ts | 15 ++ types/src/Views/DrawRectangle.d.ts | 7 + types/src/Views/Main.d.ts | 10 + types/src/Views/ViewBoxMeasurements.d.ts | 10 + types/src/index.d.ts | 4 + 32 files changed, 928 insertions(+), 36 deletions(-) create mode 100644 karma.conf.cjs create mode 100644 lib/README.md rename {src => lib}/declarations.d.ts (59%) create mode 100644 lib/withSvgSprite.ts create mode 100644 scripts/bundle.js create mode 100644 scripts/checkDevEnv.js create mode 100644 scripts/copyCesiumAssets.js create mode 100644 scripts/esbuild/preferEsmModule.js create mode 100644 scripts/esbuild/replaceModule.js create mode 100644 scripts/esbuild/selectLoader.js create mode 100644 scripts/esbuild/skipExternalModules.js create mode 100644 scripts/esbuild/svgSprite.js create mode 100644 scripts/esbuild/writeBuildTimestamp.js create mode 100644 scripts/findYarnWorkspaceRoot.js create mode 100644 scripts/isMain.js create mode 100644 specs/Models/Box3dCatalogItemSpec.ts create mode 100644 specs/SpecMain.ts create mode 100644 specs/jasmine.d.ts create mode 100644 src/Views/Foo.jsx create mode 100644 types/index.d.ts create mode 100644 types/lib/withSvgSprite.d.ts create mode 100644 types/src/Models/Traits/Box3dCatalogItemTraits.d.ts create mode 100644 types/src/Views/DrawRectangle.d.ts create mode 100644 types/src/Views/Main.d.ts create mode 100644 types/src/Views/ViewBoxMeasurements.d.ts create mode 100644 types/src/index.d.ts diff --git a/.gitignore b/.gitignore index 5b034cd..5c5a6e3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules/ dist/ +build/ yarn.lock yarn-error.log diff --git a/karma.conf.cjs b/karma.conf.cjs new file mode 100644 index 0000000..4c58195 --- /dev/null +++ b/karma.conf.cjs @@ -0,0 +1,55 @@ +"use strict"; + +module.exports = function (config) { + config.set({ + basePath: "build/specs", + proxies: { + "/data/": "/base/data", + "/images/": "/base/images", + "/test/": "/base/test", + "/build/TerriaJS/build/Cesium/build": "/base/Cesium", + "/build/Cesium": "/base/TerriaJS/build/Cesium", + "/build": "/base" + }, + + autoWatch: true, + autoWatchBatchDelay: 500, // Delay between tests, hopefully enough time for the bundler to finish writing everything + + reporters: ["spec"], + + specReporter: { + suppressErrorSummary: false, + suppressFailed: false, + suppressPassed: false, + suppressSkipped: false + }, + + files: [ + { pattern: "stdin.js", watched: true, nocache: true }, + { + pattern: "**/*", + included: false, + served: true, + watched: false, + nocache: true + } + ], + singleRun: true, + failOnEmptyTestSuite: false, + frameworks: ["jasmine"], + browsers: ["ChromeHeadless"], + detectBrowsers: { + enabled: true, + usePhantomJS: false, + postDetection(availableBrowsers) { + return availableBrowsers.filter((b) => /chrom/i.test(b)); + } + }, + plugins: [ + require("karma-spec-reporter"), + require("karma-jasmine"), + require("karma-chrome-launcher"), + require("karma-detect-browsers") + ] + }); +}; diff --git a/lib/README.md b/lib/README.md new file mode 100644 index 0000000..5d39fd4 --- /dev/null +++ b/lib/README.md @@ -0,0 +1 @@ +Note: This folder contains plugin library code. The code for your plugin must be placed in the [src](../src) folder. diff --git a/src/declarations.d.ts b/lib/declarations.d.ts similarity index 59% rename from src/declarations.d.ts rename to lib/declarations.d.ts index 8800d4b..ea80a6f 100644 --- a/src/declarations.d.ts +++ b/lib/declarations.d.ts @@ -2,3 +2,8 @@ declare module "assets/icons/*.svg" { const icon: import("terriajs-plugin-api").IconGlyph; export default icon; } + +declare module "sprite.svg" { + const sprite: string; + export default sprite; +} diff --git a/lib/withSvgSprite.ts b/lib/withSvgSprite.ts new file mode 100644 index 0000000..9c7138a --- /dev/null +++ b/lib/withSvgSprite.ts @@ -0,0 +1,28 @@ +import { TerriaPlugin } from "terriajs-plugin-api"; + +/** + * Load SVG sprite when the plugin is intialized. + * + * SVG icons can be imported in plugin code as: `import someIcon from "assets/icons/someIcon.svg"`. + * During build, these SVG assets are merged into a single sprite. This + * function ensures the sprite is added to the DOM when the plugin is initialized. + */ +export default function withSvgSprite(plugin: TerriaPlugin): TerriaPlugin { + return { + ...plugin, + register(...args) { + document.readyState === "complete" + ? loadSvgSprite() + : window.addEventListener("load", () => loadSvgSprite()); + plugin.register(...args); + } + }; +} + +function loadSvgSprite() { + import("sprite.svg").then(({ default: sprite }) => { + const div = document.createElement("div"); + div.innerHTML = sprite; + document.body.appendChild(div); + }); +} diff --git a/package.json b/package.json index d912c5e..39b7065 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,47 @@ { "name": "terriajs-plugin-sample", - "version": "0.0.1-alpha.8", + "version": "1.0.0", "description": "A sample terriajs plugin.", - "module": "dist/index.js", - "types": "dist/index.d.ts", + "type": "module", "repository": "https://github.com/terriajs/plugin-sample", "license": "Apache-2.0", - "prepare": "rollup -c rollup.config.ts", - "dependencies": { + "module": "build/src/index.js", + "types": "types/index.d.ts", + "files": [ + "build/src" + ], + "peerDependencies": { "terriajs-plugin-api": "0.0.1-alpha.16" }, "devDependencies": { - "@rollup/plugin-commonjs": "^23.0.2", - "@rollup/plugin-node-resolve": "^13.1.3", - "@rollup/plugin-replace": "^5.0.1", - "@rollup/plugin-typescript": "^8.3.1", + "@types/jasmine": "^5.1.4", + "esbuild": "^0.20.2", + "esbuild-plugin-polyfill-node": "^0.3.0", + "jasmine": "^5.1.0", + "karma": "^6.4.3", + "karma-chrome-launcher": "^3.2.0", + "karma-detect-browsers": "^2.3.3", + "karma-jasmine": "^5.1.0", + "karma-spec-reporter": "^0.0.36", + "npm-run-all": "^4.1.5", "prettier": "2.7.1", - "rollup": "^2.70.1", - "rollup-plugin-terser": "^7.0.2", + "semver-intersect": "^1.5.0", + "svg-sprite": "^2.0.4", "typescript": "~5.2.0" }, "scripts": { - "build": "rollup -c rollup.config.ts", - "watch": "rollup -c rollup.config.ts -w", - "test": "exit 0" + "prepublishOnly": "yarn clean && yarn build", + "check-dev-env": "node scripts/checkDevEnv.js", + "build": "run-p -lc bundle typecheck", + "bundle": "node scripts/bundle.js", + "typecheck": "tsc --noEmit --pretty", + "test": "karma start karma.conf.cjs --single-run", + "clean": "rimraf ./build", + "lint": "eslint src lib specs", + "watch": "run-p -lc 'watch:*'", + "watch:bundle": "node scripts/bundle.js --dev --watch", + "prewatch:test": "node scripts/copyCesiumAssets.js", + "watch:test": "karma start karma.conf.cjs --no-single-run", + "watch:typecheck": "tsc --noEmit --pretty --watch --preserveWatchOutput" } } diff --git a/scripts/bundle.js b/scripts/bundle.js new file mode 100644 index 0000000..067877d --- /dev/null +++ b/scripts/bundle.js @@ -0,0 +1,187 @@ +import esbuild from "esbuild"; +import { polyfillNode } from "esbuild-plugin-polyfill-node"; +import globby from "globby"; +import path from "path"; +import yargs from "yargs"; +import { copyCesiumAssets } from "./copyCesiumAssets.js"; +import preferEsmModule from "./esbuild/preferEsmModule.js"; +import replaceModule from "./esbuild/replaceModule.js"; +import selectLoader from "./esbuild/selectLoader.js"; +import skipExternalModules from "./esbuild/skipExternalModules.js"; +import svgSprite from "./esbuild/svgSprite.js"; +import isMain from "./isMain.js"; + +/** + * Output build directory + */ +export const BUILD_DIR = "build"; + +/** + * Shared esbuild config for bundling both src and specs + */ +export const config = { + bundle: true, + color: true, + + define: { + global: "globalThis" + }, + + tsconfig: "./tsconfig.json", + + plugins: [ + // The official @cesium/widgets package imports from `@cesium/engine`. + // Replace it with the `terriajs-cesium` fork instead. + replaceModule("@cesium/engine", "terriajs-cesium"), + + // There are places in the TerriaJS code base where some modules are imported using CJS style `require("x").default`. + // This causes, esbuild to include their NodeJS exports instead of the browser specific ESM modules. + // This plugin forces the use of ESM browser modules instead. + preferEsmModule(["proj4", "i18next"]), + + // Generates sprite.svg.js for icons + svgSprite, + + // Handle the webpackish import paths in TerriaJS code base + selectLoader({ + loaders: [ + { + filter: /^[!]*raw-loader!(.*)$/, + loader: "text" + }, + { + filter: /^file-loader!(.*)$/, + loader: "file" + }, + { + filter: /^worker-loader!(.*)$/, + loader: "empty" + }, + { + filter: /^[!]*style-loader!.*?([^!]*\.css)$/, + loader: "css" + }, + { + filter: /^[!]*style-loader!.*?([^!]*\.scss)$/, + loader: "empty" + } + ] + }) + ], + + loader: { + ".jsx": "tsx", + ".gif": "file", + ".png": "file", + ".jpg": "file", + ".svg": "file", + ".html": "text", + ".glb": "file", + ".xml": "text", + ".DAC": "file", + ".wasm": "file", + ".scss": "empty" + } +}; + +/** + * Invoke the esbuild bundler. + */ +async function runBuilder(config, opts) { + return esbuild + .context(config) + .then((builder) => + opts.watch + ? builder.watch() + : builder.rebuild().then(() => builder.dispose()) + ); +} + +/** + * Create a bundle for spec files. + * + * This will package all the dependencies as a single standalone script that + * Karma can then load and run. + */ +async function bundleSpecs(opts) { + const specsDir = "specs"; + const glob = "**/*Spec.ts"; + const specs = ["SpecMain.ts", ...(await globby(glob, { cwd: specsDir }))]; + const specsBuildDir = path.join(BUILD_DIR, "specs"); + const mergedSpecs = specs.map((s) => `import "./${s}"`).join(";"); + + return Promise.all([ + copyCesiumAssets(), + runBuilder( + { + ...config, + outdir: specsBuildDir, + stdin: { + contents: mergedSpecs, + resolveDir: "./specs", + sourcefile: "specs.js" + }, + + // Options for browser build which will be loaded by Karma + platform: "browser", + target: "esNext", + format: "iife", + minify: false, + sourcemap: true, + + plugins: (config.plugins ?? []).concat([ + // Polyfill NodeJS functions for the browser + polyfillNode() + ]) + }, + opts + ) + ]); +} + +/** + * Create a bundle for src files. + * + * This will only bundle the local code leaving every other dependency to be + * bundled by terriamap's build system. + */ +async function bundleSrc(opts) { + return runBuilder( + { + ...config, + entryPoints: ["src/index.ts"], + outdir: path.join(BUILD_DIR, "src"), + + // The src bundle is further bundled by Terriamap webpack build system + // which expects the package to use es2019 + target: "es2019", + format: "esm", + + // Enable splitting so that dynamic imports result in a separate bundle + splitting: true, + + // eslint-disable-next-line no-unneeded-ternary + minify: opts.dev ? false : true, + // eslint-disable-next-line no-unneeded-ternary + sourcemap: opts.dev ? true : false, + + plugins: (config.plugins ?? []).concat( + // Skip non-local modules from the bundle, these will be included later + // when terriamap builds the plugin + skipExternalModules + ) + }, + opts + ); +} + +/** + * Bundle src and spec files. + */ +export function bundle(opts) { + return Promise.all([bundleSrc(opts), bundleSpecs(opts)]); +} + +if (isMain(import.meta.url)) { + await bundle(yargs(process.argv).argv); +} diff --git a/scripts/checkDevEnv.js b/scripts/checkDevEnv.js new file mode 100644 index 0000000..5eaac3b --- /dev/null +++ b/scripts/checkDevEnv.js @@ -0,0 +1,253 @@ +import fs from "fs/promises"; +import micromatch from "micromatch"; +import { createRequire } from "node:module"; +import path from "path"; +import { intersect as semverIntersect } from "semver-intersect"; +import findYarnWorkspaceRoot from "./findYarnWorkspaceRoot.js"; +import isMain from "./isMain.js"; + +async function checkDevEnv() { + const checks = { + mapWorkspace: { name: "Find map workspace", fn: checkMapWorkspace }, + pluginAddedToWorkspace: { + name: "Plugin added to workspaces setting", + fn: checkPluginAddedToWorkspace + }, + pluginAddedToDeps: { + name: "Plugin added to dependencies", + fn: checkPluginAddedToDeps + }, + pluginVersionsMatch: { + name: "Package versions match", + fn: checkPluginVersions + }, + pluginImportResolvesCorrectly: { + name: "Plugin import resolves correctly", + fn: checkPluginImportResolvesCorrectly + }, + pluginAddedToPluginsRegistry: { + name: "Plugin added to plugins registry", + fn: checkPluginAddedToRegistry + }, + apiVersionsMatch: { + name: "terriajs-plugin-api versions match", + fn: checkApiVersions + } + }; + + const context = { + pluginDir: process.cwd(), + packageJson: await readJsonFile(path.join(process.cwd(), "package.json")) + }; + + for (const check of Object.values(checks)) { + const checkNext = await check.fn(check, context); + if (!check.result?.ok && !checkNext) { + break; + } + } + + return checks; +} + +async function checkMapWorkspace(out, context) { + const workspace = await findYarnWorkspaceRoot(); + const workspaceDeps = Object.assign( + {}, + workspace?.packageJson?.dependencies, + workspace?.packageJson?.devDependencies + ); + const requiredWorkspaceDeps = ["terriajs", "terriajs-plugin-api"]; + const hasRequiredWorkspaceDeps = requiredWorkspaceDeps.every( + (d) => !!workspaceDeps[d] + ); + + context.workspace = workspace; + out.result = + !workspace || !hasRequiredWorkspaceDeps + ? { + error: [ + "You need to place this plugin directory in a terria map workspace directory.", + workspace.dir + ? `Current workspace root '${workspace.dir}' does not look like a terria map project.` + : "Usually this is the `terriamap/packages` directory." + ].join("\n ") + } + : { ok: `Yes (${workspace.dir})`, workspace }; +} + +async function checkPluginAddedToWorkspace(out, { workspace, pluginDir }) { + const relativePluginDir = path.relative(workspace.dir, pluginDir); + const addedToWorkspace = workspace.packages.some((pattern) => + micromatch.isMatch(relativePluginDir, pattern) + ); + out.result = addedToWorkspace + ? { ok: "Yes" } + : { + error: `"${relativePluginDir}" should be added to the "workspaces.packages" settings in '${workspace.packageJsonFile}'` + }; +} + +async function checkPluginAddedToDeps(out, { workspace, packageJson }) { + const packageName = packageJson.name; + const packageDep = workspace.packageJson?.["dependencies"]?.[packageName]; + out.result = packageDep + ? { ok: "Yes" } + : { + error: `Plugin should be added to "dependencies" settings in '${workspace.packageJsonFile}'` + }; +} + +async function checkPluginVersions(out, { workspace, pluginDir, packageJson }) { + const packageName = packageJson?.name; + const localVersion = packageJson?.version; + const workspaceVersion = Object.assign( + {}, + packageJson.peerDependencies, + workspace.packageJson.dependencies, + workspace.packageJson.devDependencies + )[packageName]; + + let ok = false; + try { + ok = !!semverIntersect(localVersion, workspaceVersion); + } catch (err) { + /*nothing to do*/ + } + + const pluginPackageJsonFile = path.join( + path.relative(workspace.dir, pluginDir), + "package.json" + ); + out.result = ok + ? { + ok: `Yes (${localVersion} matches ${workspaceVersion})` + } + : { + error: [ + `Version in ${pluginPackageJsonFile}: ${localVersion}`, + `Version in ${workspace.packageJsonFile}: ${workspaceVersion}` + ].join("\n ") + }; + return true; +} + +async function checkApiVersions(out, { workspace, pluginDir, packageJson }) { + const localVersion = Object.assign( + {}, + packageJson.peerDependencies, + packageJson.dependencies, + packageJson.devDependencies + )["terriajs-plugin-api"]; + const workspaceVersion = Object.assign( + {}, + packageJson.peerDependencies, + workspace.packageJson.dependencies, + workspace.packageJson.devDependencies + )["terriajs-plugin-api"]; + + let ok = false; + try { + ok = !!semverIntersect(localVersion, workspaceVersion); + } catch (err) { + /*nothing to do*/ + } + + const pluginPackageJsonFile = path.join( + path.relative(workspace.dir, pluginDir), + "package.json" + ); + out.result = ok + ? { + ok: `Yes (${localVersion} matches ${workspaceVersion})` + } + : { + error: [ + `Version in ${pluginPackageJsonFile}: ${localVersion}`, + `Version in ${workspace.packageJsonFile}: ${workspaceVersion}` + ].join("\n ") + }; + return true; +} + +async function checkPluginImportResolvesCorrectly( + out, + { workspace, pluginDir, packageJson } +) { + const pluginName = packageJson.name; + const resolvedDir = await nodeResolveDirectory(`${pluginName}/package.json`); + out.result = + resolvedDir === pluginDir + ? { ok: "Yes" } + : { + error: `Importing "${pluginName}" does not correctly resolve to '${pluginDir}'.\n Make sure you have run "yarn install" from the workspace root '${workspace.dir}'` + }; +} + +async function checkPluginAddedToRegistry(out, { workspace, packageJson }) { + const name = packageJson.name; + const pluginRegistryFile = path.join(workspace.dir, "plugins.ts"); + const pluginRegistry = await fs + .readFile(pluginRegistryFile, "utf-8") + .catch(() => ""); + const addedToRegistry = !!pluginRegistry.match( + new RegExp(`import.*?"${name}"`) + ); + out.result = addedToRegistry + ? { ok: "Yes" } + : { + error: `"${name}" missing in plugin registry file '${pluginRegistryFile}'` + }; +} + +function matchVersion(packageName, packageJson, workspacePackageJson) { + const localVersion = Object.assign( + {}, + packageJson.peerDependencies, + packageJson.dependencies, + packageJson.devDependencies + )[packageName]; + + const workspaceVersion = Object.assign( + {}, + packageJson.peerDependencies, + workspacePackageJson.dependencies, + workspacePackageJson.devDependencies + )[packageName]; + + let ok = false; + try { + ok = !!semverIntersect(localVersion, workspaceVersion); + } catch (err) { + /*nothing to do*/ + } + return { ok, packageName, localVersion, workspaceVersion }; +} + +async function nodeResolveDirectory(file) { + const require = createRequire(import.meta.url); + try { + return path.dirname(await fs.realpath(require.resolve(file))); + } catch (err) { + return undefined; + } +} + +async function readJsonFile(file) { + return JSON.parse(await fs.readFile(file)); +} + +if (isMain(import.meta.url)) { + checkDevEnv().then((checks) => { + Object.values(checks).forEach((check) => { + if (check?.result?.error) { + console.log("❌", check.name, "-", "No"); + console.log(" ", check.result.error); + } else if (check?.result?.ok) { + console.log("✅", check.name, "-", check.result.ok); + } else { + console.log("❓", check.name, "-", "not checked"); + } + }); + }); +} diff --git a/scripts/copyCesiumAssets.js b/scripts/copyCesiumAssets.js new file mode 100644 index 0000000..8b67b97 --- /dev/null +++ b/scripts/copyCesiumAssets.js @@ -0,0 +1,34 @@ +import fs from "fs/promises"; +import { createRequire } from "node:module"; +import path from "path"; +import { BUILD_DIR } from "./bundle.js"; +import isMain from "./isMain.js"; + +export function copyCesiumAssets() { + const outDir = path.join(BUILD_DIR, "specs", "Cesium"); + const require = createRequire(import.meta.url); + const cesiumDir = path.dirname( + require.resolve("terriajs-cesium/package.json") + ); + + const copy = (src, dest) => + fs.cp(src, dest, { recursive: true, force: true, errorOnExist: false }); + + return Promise.all([ + copy( + path.join(cesiumDir, "Build", "Workers"), + path.join(outDir, "Workers") + ), + copy(path.join(cesiumDir, "Source", "Assets"), path.join(outDir, "Assets")), + copy( + path.join(cesiumDir, "Source", "ThirdParty"), + path.join(outDir, "ThirdParty") + ) + ]).catch(() => { + /* can error if there are parallel copy attempts */ + }); +} + +if (isMain(import.meta.url)) { + copyCesiumAssets(); +} diff --git a/scripts/esbuild/preferEsmModule.js b/scripts/esbuild/preferEsmModule.js new file mode 100644 index 0000000..51e8327 --- /dev/null +++ b/scripts/esbuild/preferEsmModule.js @@ -0,0 +1,17 @@ +export default function preferEsmModule(moduleNames) { + return { + name: "prefer-esm-module", + setup(build) { + moduleNames.forEach((moduleName) => { + build.onResolve({ filter: new RegExp(`^${moduleName}$`) }, (args) => { + return args.kind === "require-call" + ? build.resolve(args.path, { + kind: "import-statement", + resolveDir: args.resolveDir + }) + : undefined; + }); + }); + } + }; +} diff --git a/scripts/esbuild/replaceModule.js b/scripts/esbuild/replaceModule.js new file mode 100644 index 0000000..e9b5e62 --- /dev/null +++ b/scripts/esbuild/replaceModule.js @@ -0,0 +1,16 @@ +export default function replaceModule(module, replacementModule) { + return { + name: "replace-module", + setup(build) { + build.onResolve( + { filter: new RegExp(`^${module.replace("/", "\\/")}$`) }, + async (args) => { + return build.resolve(replacementModule, { + kind: args.kind, + resolveDir: args.resolveDir + }); + } + ); + } + }; +} diff --git a/scripts/esbuild/selectLoader.js b/scripts/esbuild/selectLoader.js new file mode 100644 index 0000000..116bf4d --- /dev/null +++ b/scripts/esbuild/selectLoader.js @@ -0,0 +1,47 @@ +import fs from "fs/promises"; + +const selectLoader = (options = {}) => ({ + name: "selectLoaderPlugin", + setup(build, { transform } = {}) { + for (const { filter, loader } of options.loaders) { + build.onResolve( + { + filter: filter + }, + async (args) => { + const pathOnly = args.path.match(filter)[1]; + const result = await build.resolve(pathOnly, { + kind: args.kind, + importer: args.importer, + namespace: "file", + resolveDir: args.resolveDir, + pluginData: args.resolveData + }); + return { + ...result, + namespace: "selectLoaderPlugin", + pluginData: { + ...result.pluginData, + loader + } + }; + } + ); + + build.onLoad( + { + filter: /.*/, + namespace: "selectLoaderPlugin" + }, + async (args) => { + return { + contents: await fs.readFile(args.path), + loader: args.pluginData.loader + }; + } + ); + } + } +}); + +export default selectLoader; diff --git a/scripts/esbuild/skipExternalModules.js b/scripts/esbuild/skipExternalModules.js new file mode 100644 index 0000000..7155d4b --- /dev/null +++ b/scripts/esbuild/skipExternalModules.js @@ -0,0 +1,14 @@ +export default { + name: "skipExternalNodeModules", + setup(build) { + build.onResolve({ filter: /.*/ }, (args) => { + if (args.kind === "entry-point" || args.path.startsWith(".")) { + return; + } else { + // Mark all non local imports as external. These will be bundled + // by the terriamap build process + return { path: args.path, external: true }; + } + }); + } +}; diff --git a/scripts/esbuild/svgSprite.js b/scripts/esbuild/svgSprite.js new file mode 100644 index 0000000..6fed33b --- /dev/null +++ b/scripts/esbuild/svgSprite.js @@ -0,0 +1,62 @@ +import fs from "fs/promises"; +import path from "path"; +import SvgSprite from "svg-sprite"; + +/** + * esbuild plugin for generating SVG sprite + */ +export default { + name: "svg-sprite-builder", + async setup(build) { + const symbolPrefix = JSON.parse( + await fs.readFile("package.json", "utf-8") + ).name.replace(/[^0-9a-z]/i, "-"); + const sprite = new SvgSprite({ + mode: { + symbol: { inline: true } + }, + shape: { + id: { generator: `${symbolPrefix}-%s` } + } + }); + + build.onResolve({ filter: /^sprite.svg$/ }, (args) => { + // sprite.svg.js will be generated when build ends (see below). + return { path: "./sprite.svg.js", external: true }; + }); + + build.onResolve({ filter: /assets\/icons\/.*\.svg$/ }, (args) => { + const pluginDir = process.cwd(); + return { + path: path.join(pluginDir, args.path), + namespace: "svg-sprite-builder" + }; + }); + + build.onLoad( + { filter: /.*\.svg$/, namespace: "svg-sprite-builder" }, + async (args) => { + const baseName = path.basename(args.path, path.extname(args.path)); + const symbol = `${symbolPrefix}-${baseName}`; + const svg = await fs.readFile(args.path, "utf-8"); + sprite.add(args.path, path.basename(args.path), svg); + return { + contents: `export default { id: "${symbol}" }`, + loader: "js" + }; + } + ); + + build.onEnd(async (args) => { + const { result } = await sprite.compileAsync(); + const outdir = + build.initialOptions.outdir || + path.dirname(build.initialOptions.outfile); + const spriteFile = path.join(outdir, "sprite.svg.js"); + return fs.writeFile( + spriteFile, + `export default '${result.symbol.sprite.contents}';` + ); + }); + } +}; diff --git a/scripts/esbuild/writeBuildTimestamp.js b/scripts/esbuild/writeBuildTimestamp.js new file mode 100644 index 0000000..55e4857 --- /dev/null +++ b/scripts/esbuild/writeBuildTimestamp.js @@ -0,0 +1,8 @@ +import fs from "fs/promises"; + +export default function writeBuildTimestamp(file) { + return { + name: "writeBuildTimestamp", + setup(build) {} + }; +} diff --git a/scripts/findYarnWorkspaceRoot.js b/scripts/findYarnWorkspaceRoot.js new file mode 100644 index 0000000..405187a --- /dev/null +++ b/scripts/findYarnWorkspaceRoot.js @@ -0,0 +1,54 @@ +import fs from "fs/promises"; +import path from "path"; + +export default async function findYarnWorkspaceRoot() { + const initial = process.cwd(); + let previous = null; + let current = path.normalize(initial); + + do { + const packageJsonFile = path.join(current, "package.json"); + const manifest = await readPackageJson(packageJsonFile); + const ws = extractWorkspaces(manifest); + if (ws && ws.packages) { + return { + dir: current, + packages: ws.packages, + packageJsonFile, + packageJson: manifest + }; + } + + previous = current; + current = path.dirname(current); + } while (current !== previous); + + return null; +} + +async function readPackageJson(file) { + return fs + .readFile(file, "utf-8") + .then((str) => JSON.parse(str)) + .catch((err) => undefined); +} + +function extractWorkspaces(manifest) { + if (!manifest || !manifest.workspaces) { + return undefined; + } + + if (Array.isArray(manifest.workspaces)) { + return { packages: manifest.workspaces }; + } + + if ( + (manifest.workspaces.packages && + Array.isArray(manifest.workspaces.packages)) || + (manifest.workspaces.nohoist && Array.isArray(manifest.workspaces.nohoist)) + ) { + return manifest.workspaces; + } + + return undefined; +} diff --git a/scripts/isMain.js b/scripts/isMain.js new file mode 100644 index 0000000..81bc7dd --- /dev/null +++ b/scripts/isMain.js @@ -0,0 +1,5 @@ +import { fileURLToPath } from "url"; + +export default function isMain(importMetaUrl) { + return fileURLToPath(importMetaUrl) === process.argv[1]; +} diff --git a/specs/Models/Box3dCatalogItemSpec.ts b/specs/Models/Box3dCatalogItemSpec.ts new file mode 100644 index 0000000..0c6ac04 --- /dev/null +++ b/specs/Models/Box3dCatalogItemSpec.ts @@ -0,0 +1,15 @@ +import { Terria } from "terriajs-plugin-api"; +import Box3dCatalogItem from "../../src/Models/Box3dCatalogItem"; + +describe("Box3dCatalogItemSpec", function () { + let terria: Terria; + + beforeEach(function () { + terria = new Terria(); + }); + + it("can be created", function () { + const box3d = new Box3dCatalogItem("test", terria); + expect(box3d).toBeDefined(); + }); +}); diff --git a/specs/SpecMain.ts b/specs/SpecMain.ts new file mode 100644 index 0000000..ac33255 --- /dev/null +++ b/specs/SpecMain.ts @@ -0,0 +1,17 @@ +beforeAll(() => { + // Set base href to root. This is required for correctly loading Cesium + // assets from a Karma context or debug file. + setBaseHref("/"); +}); + +/** + * Set the base href tag + */ +function setBaseHref(href: string) { + let base = document.getElementsByTagName("base")[0]; + if (!base) { + base = document.createElement("base"); + document.head.appendChild(base); + } + base.href = href; +} diff --git a/specs/jasmine.d.ts b/specs/jasmine.d.ts new file mode 100644 index 0000000..1714786 --- /dev/null +++ b/specs/jasmine.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/Views/DrawRectangle.tsx b/src/Views/DrawRectangle.tsx index 1c9c2a6..3a6698f 100644 --- a/src/Views/DrawRectangle.tsx +++ b/src/Views/DrawRectangle.tsx @@ -17,7 +17,7 @@ export const DrawRectangle: React.FC = ({ onDrawingComplete }) => { onDrawingComplete: ({ rectangle }) => { onDrawingComplete(rectangle); } - }); + }, [terria]); userDrawing.enterDrawMode(); return () => { diff --git a/src/Views/Foo.jsx b/src/Views/Foo.jsx new file mode 100644 index 0000000..aad9f41 --- /dev/null +++ b/src/Views/Foo.jsx @@ -0,0 +1,3 @@ +export default function foo() { + parseFloat(123); +} diff --git a/src/Views/Main.tsx b/src/Views/Main.tsx index c2c7e3d..e52b8f2 100644 --- a/src/Views/Main.tsx +++ b/src/Views/Main.tsx @@ -29,7 +29,7 @@ const Main: React.FC = (props) => { // Add it to the workbench so that it appears on the map terria.workbench.add(boxItem); } - }, []); + }, [terria]); // WorkflowPanel opens as a left-side panel replacein the Workbench // It can be used to implement custom workflow UIs diff --git a/src/index.ts b/src/index.ts index e26aefb..431fa8e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import { TerriaPluginContext } from "terriajs-plugin-api"; import Box3dCatalogItem from "./Models/Box3dCatalogItem"; +import withSvgSprite from "../lib/withSvgSprite"; export const toolId = "3d-box-tool"; @@ -14,6 +15,7 @@ const plugin: TerriaPlugin = { description: "A sample plugin that provides a tool for drawing a 3D Box and viewing its measurements.", version: "0.0.1", + register({ viewState }: TerriaPluginContext) { // Register our custom catalog item with Terria CatalogMemberFactory.register(Box3dCatalogItem.type, Box3dCatalogItem); @@ -34,4 +36,4 @@ const plugin: TerriaPlugin = { } }; -export default plugin; +export default withSvgSprite(plugin); diff --git a/tsconfig.json b/tsconfig.json index 123e3ba..82100e0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,31 +3,21 @@ "module": "esNext", "target": "es6", "moduleResolution": "node", - "outDir": "dist/", - "rootDir": "src/", + "outDir": "build/tsc", "jsx": "react", "experimentalDecorators": true, "emitDecoratorMetadata": true, "allowSyntheticDefaultImports": true, "resolveJsonModule": true, - "allowJs": true, - "checkJs": false, "strict": true, + // Set allowJs to true if you want to use plain js/jsx modules - also enable checkJs + "allowJs": false, + "checkJs": false, + // Declaration emit does not work currently when using terriajs models. + "declaration": false, "esModuleInterop": true, - //"skipLibCheck": true - // Although this can result in subtle bugs, they are required for us to - // ignore TS errors on js files inside terria. We'll get rid of them when - // we have a proper terria bundle with type declarations. - //"noImplicitAny": false, - //"strictNullChecks": false, - // Should these thirdparty types be included in terriajs tsconfig "types" settings, - // so that we can avoid specifying it here? - // Refer: https://www.typescriptlang.org/tsconfig#types - "typeRoots": [ - "../../node_modules/terriajs/lib/ThirdParty" - //"../../node_modules" - // "node_modules" - ] + "useDefineForClassFields": true, + "typeRoots": ["../../node_modules/terriajs/lib/ThirdParty"] }, - "include": ["./src/**/*"] + "include": ["./src", "./lib", "./specs"] } diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 0000000..215a081 --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,3 @@ +import { TerriaPlugin } from "terriajs-plugin-api"; +declare const plugin: TerriaPlugin; +export default plugin; diff --git a/types/lib/withSvgSprite.d.ts b/types/lib/withSvgSprite.d.ts new file mode 100644 index 0000000..e6e9d47 --- /dev/null +++ b/types/lib/withSvgSprite.d.ts @@ -0,0 +1,9 @@ +import { TerriaPlugin } from "terriajs-plugin-api"; +/** + * Load SVG sprite when the plugin is intialized. + * + * SVG icons can be imported in plugin code as: `import someIcon from "assets/icons/someIcon.svg"`. + * During build, these SVG assets are merged into a single sprite. This + * function ensures the sprite is added to the DOM when the plugin is initialized. + */ +export default function withSvgSprite(plugin: TerriaPlugin): TerriaPlugin; diff --git a/types/src/Models/Traits/Box3dCatalogItemTraits.d.ts b/types/src/Models/Traits/Box3dCatalogItemTraits.d.ts new file mode 100644 index 0000000..7b54fa7 --- /dev/null +++ b/types/src/Models/Traits/Box3dCatalogItemTraits.d.ts @@ -0,0 +1,15 @@ +import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3"; +import { CatalogMemberTraits, HeadingPitchRollTraits, LatLonHeightTraits, MappableTraits, Model, ModelTraits } from "terriajs-plugin-api"; +export declare class DimensionsTraits extends ModelTraits { + length?: number; + width?: number; + height?: number; + static setFromCartesianScale(model: Model, stratumId: string, scale: Cartesian3): void; +} +declare const Box3dCatalogItemTraits_base: import("terriajs/lib/Traits/TraitsConstructor").default; +export default class Box3dCatalogItemTraits extends Box3dCatalogItemTraits_base { + position?: LatLonHeightTraits; + rotation?: HeadingPitchRollTraits; + dimensions?: DimensionsTraits; +} +export {}; diff --git a/types/src/Views/DrawRectangle.d.ts b/types/src/Views/DrawRectangle.d.ts new file mode 100644 index 0000000..5525cda --- /dev/null +++ b/types/src/Views/DrawRectangle.d.ts @@ -0,0 +1,7 @@ +import React from "react"; +import Rectangle from "terriajs-cesium/Source/Core/Rectangle"; +interface PropsType { + onDrawingComplete: (rectangle: Rectangle | undefined) => void; +} +export declare const DrawRectangle: React.FC; +export {}; diff --git a/types/src/Views/Main.d.ts b/types/src/Views/Main.d.ts new file mode 100644 index 0000000..310e2c1 --- /dev/null +++ b/types/src/Views/Main.d.ts @@ -0,0 +1,10 @@ +import React from "react"; +import Box3dCatalogItem from "../Models/Box3dCatalogItem"; +interface PropsType { + boxItem?: Box3dCatalogItem; +} +/** + * The main tool component + */ +declare const Main: React.FC; +export default Main; diff --git a/types/src/Views/ViewBoxMeasurements.d.ts b/types/src/Views/ViewBoxMeasurements.d.ts new file mode 100644 index 0000000..0dfd4e0 --- /dev/null +++ b/types/src/Views/ViewBoxMeasurements.d.ts @@ -0,0 +1,10 @@ +import React from "react"; +import Box3dCatalogItem from "../Models/Box3dCatalogItem"; +interface PropsType { + boxItem: Box3dCatalogItem; +} +/** + * Displays box position, dimensions and rotation + */ +export declare const ViewBoxMeasurements: React.FC; +export {}; diff --git a/types/src/index.d.ts b/types/src/index.d.ts new file mode 100644 index 0000000..c4d07e2 --- /dev/null +++ b/types/src/index.d.ts @@ -0,0 +1,4 @@ +import { TerriaPlugin } from "terriajs-plugin-api"; +export declare const toolId = "3d-box-tool"; +declare const _default: TerriaPlugin; +export default _default; From 55fb3ff59356e800ee7f9915c0890d97e0d582db Mon Sep 17 00:00:00 2001 From: Nanda Date: Tue, 30 Apr 2024 13:31:18 +1000 Subject: [PATCH 02/10] Improve check for plugin registration. --- scripts/checkDevEnv.js | 47 +++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/scripts/checkDevEnv.js b/scripts/checkDevEnv.js index 5eaac3b..5a62471 100644 --- a/scripts/checkDevEnv.js +++ b/scripts/checkDevEnv.js @@ -6,6 +6,9 @@ import { intersect as semverIntersect } from "semver-intersect"; import findYarnWorkspaceRoot from "./findYarnWorkspaceRoot.js"; import isMain from "./isMain.js"; +/** + * Checks if the plugin development environment is sane. + */ async function checkDevEnv() { const checks = { mapWorkspace: { name: "Find map workspace", fn: checkMapWorkspace }, @@ -76,6 +79,9 @@ async function checkMapWorkspace(out, context) { : { ok: `Yes (${workspace.dir})`, workspace }; } +/** + * Check if the plugin directory is checked out under a terriamap workspace. + */ async function checkPluginAddedToWorkspace(out, { workspace, pluginDir }) { const relativePluginDir = path.relative(workspace.dir, pluginDir); const addedToWorkspace = workspace.packages.some((pattern) => @@ -88,6 +94,9 @@ async function checkPluginAddedToWorkspace(out, { workspace, pluginDir }) { }; } +/** + * Check if `dependencies` in terriamap/package.json includes this plugin. + */ async function checkPluginAddedToDeps(out, { workspace, packageJson }) { const packageName = packageJson.name; const packageDep = workspace.packageJson?.["dependencies"]?.[packageName]; @@ -98,6 +107,9 @@ async function checkPluginAddedToDeps(out, { workspace, packageJson }) { }; } +/** + * Check whether terriamap/package.json has the correct version of this plugin. + */ async function checkPluginVersions(out, { workspace, pluginDir, packageJson }) { const packageName = packageJson?.name; const localVersion = packageJson?.version; @@ -132,6 +144,9 @@ async function checkPluginVersions(out, { workspace, pluginDir, packageJson }) { return true; } +/** + * Check whether the version of terriajs-plugin-api dependency for this plugin and terriamap, both match. + */ async function checkApiVersions(out, { workspace, pluginDir, packageJson }) { const localVersion = Object.assign( {}, @@ -170,6 +185,9 @@ async function checkApiVersions(out, { workspace, pluginDir, packageJson }) { return true; } +/** + * Check if importing the plugin correctly resolves to this plugin directory. + */ async function checkPluginImportResolvesCorrectly( out, { workspace, pluginDir, packageJson } @@ -184,16 +202,29 @@ async function checkPluginImportResolvesCorrectly( }; } +/** + * Check if the plugin has been added to terriamap/plugins.ts. + */ async function checkPluginAddedToRegistry(out, { workspace, packageJson }) { - const name = packageJson.name; + // Transpile the plugins.ts file to JS and check if it imports this plugin library const pluginRegistryFile = path.join(workspace.dir, "plugins.ts"); - const pluginRegistry = await fs - .readFile(pluginRegistryFile, "utf-8") - .catch(() => ""); - const addedToRegistry = !!pluginRegistry.match( - new RegExp(`import.*?"${name}"`) - ); - out.result = addedToRegistry + const esbuild = await import("esbuild"); + const pluginsJs = await esbuild + .build({ + entryPoints: [pluginRegistryFile], + write: false, + minify: true + }) + .then((out) => out.outputFiles[0].text); + + const pluginsFn = await import( + `data:text/javascript;base64,${btoa(pluginsJs)}` + ).then((module) => module.default.toString()); + + const name = packageJson.name; + const importsPlugin = pluginsFn.includes(`import("${name}")`); + + out.result = importsPlugin ? { ok: "Yes" } : { error: `"${name}" missing in plugin registry file '${pluginRegistryFile}'` From 830f906d16125d4cf96ecc3fd215d44b85db8896 Mon Sep 17 00:00:00 2001 From: Nanda Date: Tue, 30 Apr 2024 14:01:02 +1000 Subject: [PATCH 03/10] Lint fix. --- src/Views/DrawRectangle.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Views/DrawRectangle.tsx b/src/Views/DrawRectangle.tsx index 3a6698f..d7a1f83 100644 --- a/src/Views/DrawRectangle.tsx +++ b/src/Views/DrawRectangle.tsx @@ -17,13 +17,13 @@ export const DrawRectangle: React.FC = ({ onDrawingComplete }) => { onDrawingComplete: ({ rectangle }) => { onDrawingComplete(rectangle); } - }, [terria]); + }); userDrawing.enterDrawMode(); return () => { userDrawing.endDrawing(); }; - }, []); + }, [terria, onDrawingComplete]); return

Draw a rectangle on the screen to create a box

; }; From a8358a084d353f90dfbd321fc758aa900d6de2d6 Mon Sep 17 00:00:00 2001 From: Nanda Date: Tue, 30 Apr 2024 14:47:53 +1000 Subject: [PATCH 04/10] Update package scripts. --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 39b7065..0e22c3b 100644 --- a/package.json +++ b/package.json @@ -32,13 +32,13 @@ "scripts": { "prepublishOnly": "yarn clean && yarn build", "check-dev-env": "node scripts/checkDevEnv.js", - "build": "run-p -lc bundle typecheck", + "build": "run-p -lc bundle typecheck lint", "bundle": "node scripts/bundle.js", "typecheck": "tsc --noEmit --pretty", "test": "karma start karma.conf.cjs --single-run", "clean": "rimraf ./build", "lint": "eslint src lib specs", - "watch": "run-p -lc 'watch:*'", + "watch": "run-p -lc watch:*", "watch:bundle": "node scripts/bundle.js --dev --watch", "prewatch:test": "node scripts/copyCesiumAssets.js", "watch:test": "karma start karma.conf.cjs --no-single-run", From c9251ed8005053287f3d67d2a31dcb31d37d37aa Mon Sep 17 00:00:00 2001 From: Nanda Date: Tue, 30 Apr 2024 14:53:41 +1000 Subject: [PATCH 05/10] Remove stray files. --- .gitignore | 1 - rollup.config.ts | 72 ------------------- src/Views/Foo.jsx | 3 - types/lib/withSvgSprite.d.ts | 9 --- .../Models/Traits/Box3dCatalogItemTraits.d.ts | 15 ---- types/src/Views/DrawRectangle.d.ts | 7 -- types/src/Views/Main.d.ts | 10 --- types/src/Views/ViewBoxMeasurements.d.ts | 10 --- types/src/index.d.ts | 4 -- 9 files changed, 131 deletions(-) delete mode 100644 rollup.config.ts delete mode 100644 src/Views/Foo.jsx delete mode 100644 types/lib/withSvgSprite.d.ts delete mode 100644 types/src/Models/Traits/Box3dCatalogItemTraits.d.ts delete mode 100644 types/src/Views/DrawRectangle.d.ts delete mode 100644 types/src/Views/Main.d.ts delete mode 100644 types/src/Views/ViewBoxMeasurements.d.ts delete mode 100644 types/src/index.d.ts diff --git a/.gitignore b/.gitignore index 5c5a6e3..4d44c02 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ node_modules/ -dist/ build/ yarn.lock diff --git a/rollup.config.ts b/rollup.config.ts deleted file mode 100644 index e0d453f..0000000 --- a/rollup.config.ts +++ /dev/null @@ -1,72 +0,0 @@ -import nodeResolve from "@rollup/plugin-node-resolve"; -import typescript from "@rollup/plugin-typescript"; -import * as path from "path"; -import { terser } from "rollup-plugin-terser"; -import packageJson from "./package.json"; - -// Paths to exclude from the bundle -const externalPaths = [ - /^.*\/node_modules\/.*$/, - /^terriajs-cesium\/.*$/, - /^.*@babel\/runtime.*$/, - /^.*\/terriajs\/.*$/ -]; - -export default { - input: "src/index.ts", - output: { - format: "esm", - dir: "dist/" - }, - // preserveSymlinks is required to prevent rollup from expanding references to packages in yarn workspace to relative paths - preserveSymlinks: true, - external: (depPath) => { - // exclude files in exclusionList from the build pipeline - return externalPaths.some((ext) => { - if (typeof ext === "string") { - return depPath === ext; - } else if (ext instanceof RegExp) { - return ext.test(depPath); - } else { - return false; - } - }); - }, - plugins: [ - nodeResolve(), - resolveSvgIcons(), - typescript() - /*terser() // enable terser if you want to minify your code */ - ] -}; - -/** - * Resolve `asset/icons/*.svg` imports and transform it to be picked up by the terriamap webpack loader. - * See: "terriamap/buildprocess/configureWebpackForPlugins.js" - */ -function resolveSvgIcons() { - return { - name: "resolve-svg-icons", - resolveId(importee) { - // rewrite `assets/icons` path to absolute path - return importee.startsWith(path.join("assets", "icons")) - ? path.resolve("./", importee) - : null; - }, - transform(code, id) { - // Transform icon asset files to require() the original svg file - const isIconAsset = - id.endsWith(".svg") && - path.relative(path.join("assets", "icons"), path.dirname(id)) === ""; - - if (isIconAsset) { - const relativeIconPath = path.relative(path.join("."), id); - return { - code: `export default require("${packageJson.name}/${relativeIconPath}")` - }; - } else { - return null; - } - } - }; -} diff --git a/src/Views/Foo.jsx b/src/Views/Foo.jsx deleted file mode 100644 index aad9f41..0000000 --- a/src/Views/Foo.jsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function foo() { - parseFloat(123); -} diff --git a/types/lib/withSvgSprite.d.ts b/types/lib/withSvgSprite.d.ts deleted file mode 100644 index e6e9d47..0000000 --- a/types/lib/withSvgSprite.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { TerriaPlugin } from "terriajs-plugin-api"; -/** - * Load SVG sprite when the plugin is intialized. - * - * SVG icons can be imported in plugin code as: `import someIcon from "assets/icons/someIcon.svg"`. - * During build, these SVG assets are merged into a single sprite. This - * function ensures the sprite is added to the DOM when the plugin is initialized. - */ -export default function withSvgSprite(plugin: TerriaPlugin): TerriaPlugin; diff --git a/types/src/Models/Traits/Box3dCatalogItemTraits.d.ts b/types/src/Models/Traits/Box3dCatalogItemTraits.d.ts deleted file mode 100644 index 7b54fa7..0000000 --- a/types/src/Models/Traits/Box3dCatalogItemTraits.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3"; -import { CatalogMemberTraits, HeadingPitchRollTraits, LatLonHeightTraits, MappableTraits, Model, ModelTraits } from "terriajs-plugin-api"; -export declare class DimensionsTraits extends ModelTraits { - length?: number; - width?: number; - height?: number; - static setFromCartesianScale(model: Model, stratumId: string, scale: Cartesian3): void; -} -declare const Box3dCatalogItemTraits_base: import("terriajs/lib/Traits/TraitsConstructor").default; -export default class Box3dCatalogItemTraits extends Box3dCatalogItemTraits_base { - position?: LatLonHeightTraits; - rotation?: HeadingPitchRollTraits; - dimensions?: DimensionsTraits; -} -export {}; diff --git a/types/src/Views/DrawRectangle.d.ts b/types/src/Views/DrawRectangle.d.ts deleted file mode 100644 index 5525cda..0000000 --- a/types/src/Views/DrawRectangle.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -import React from "react"; -import Rectangle from "terriajs-cesium/Source/Core/Rectangle"; -interface PropsType { - onDrawingComplete: (rectangle: Rectangle | undefined) => void; -} -export declare const DrawRectangle: React.FC; -export {}; diff --git a/types/src/Views/Main.d.ts b/types/src/Views/Main.d.ts deleted file mode 100644 index 310e2c1..0000000 --- a/types/src/Views/Main.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; -import Box3dCatalogItem from "../Models/Box3dCatalogItem"; -interface PropsType { - boxItem?: Box3dCatalogItem; -} -/** - * The main tool component - */ -declare const Main: React.FC; -export default Main; diff --git a/types/src/Views/ViewBoxMeasurements.d.ts b/types/src/Views/ViewBoxMeasurements.d.ts deleted file mode 100644 index 0dfd4e0..0000000 --- a/types/src/Views/ViewBoxMeasurements.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; -import Box3dCatalogItem from "../Models/Box3dCatalogItem"; -interface PropsType { - boxItem: Box3dCatalogItem; -} -/** - * Displays box position, dimensions and rotation - */ -export declare const ViewBoxMeasurements: React.FC; -export {}; diff --git a/types/src/index.d.ts b/types/src/index.d.ts deleted file mode 100644 index c4d07e2..0000000 --- a/types/src/index.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { TerriaPlugin } from "terriajs-plugin-api"; -export declare const toolId = "3d-box-tool"; -declare const _default: TerriaPlugin; -export default _default; From d84bbcdad071160511f89d98f3c37b9dbb126f6d Mon Sep 17 00:00:00 2001 From: Nanda Date: Tue, 30 Apr 2024 14:55:11 +1000 Subject: [PATCH 06/10] Update package.json. --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 0e22c3b..bb0f3cf 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "module": "build/src/index.js", "types": "types/index.d.ts", "files": [ - "build/src" + "build/src", + "types" ], "peerDependencies": { "terriajs-plugin-api": "0.0.1-alpha.16" From a6198df5e388d46cc23e706b365c23215248ecef Mon Sep 17 00:00:00 2001 From: Nanda Date: Tue, 30 Apr 2024 16:14:42 +1000 Subject: [PATCH 07/10] Updated README.md. --- README.md | 208 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 131 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 4a647e5..9818639 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # TerriaJS sample plugin -This repository implements a sample TerriaJS plugin. The plugin implements a +This repository implements a sample TerriaJS plugin. This plugin implements a custom tool for drawing an interactive 3D box on the map. It serves as an example for setting up an loading an external plugin library that adds some new functionality to Terria without forking it. @@ -21,121 +21,175 @@ library and are pre-requisites for understanding the code: - [yarn](yarnpkg.com) - Package manager -Additional documentation for developing with terria is available at -[https://docs.terria.io](https://docs.terria.io/). You can also reach us through our [discussion forum](https://github.com/TerriaJS/terriajs/discussions) if you require additional help. +Additional documentation for developing with Terria is available at +[https://docs.terria.io](https://docs.terria.io/). +👷 This plugin repository is a work in progress and will be updated as the different APIs evolve. Meanwhile expect breaking changes -This plugin repository is a work in progress and will be updated as the different -APIs evolve. Meanwhile expect breaking changes 👷 +💬 Reach us through our [discussion forum](https://github.com/TerriaJS/terriajs/discussions) if you require additional help. -### Current status -- [x] Load external plugins in TerriaJS at build time -- [x] Support for registering custom data types (aka catalog items) -- [x] Initial, limited support for extending UI to add custom workflows -- [ ] Testing -- [ ] Linting +## Guides -# Adding the plugin to your terriamap +- [Installing the plugin](#-installing-the-plugin) +- [Developing your own plugin](#-developing-your-own-plugin) -### Clone terriamap -```bash -git clone https://github.com/terriajs/terriamap -cd terriamap -``` +## 🚀 Installing the plugin -### Add this plugin as dependency in package.json -```bash -yarn add -W 'terriajs-plugin-sample' -``` +If you just want to try out the plugin to see how it works, add the plugin as a dependency to your terriamap and register it in `plugins.ts` file. The steps below show how to do that. -### Add it to the plugin registry file `plugins.ts` -```typescript -const plugins: any[] = [ - import("terriajs-plugin-sample") -]; -... -export default plugins; -``` +1. **Clone terriamap** -Note: The file `plugins.ts` is in the terriamap project root directory. + ```bash + git clone https://github.com/terriajs/terriamap + cd terriamap + ``` -### Now build your terriamap and start the server +2. **Add the plugin package as dependency** -``` -# From the terriamap directory run -yarn run gulp dev -``` + ```bash + yarn add -W terriajs-plugin-sample + ``` + +3. **Add the plugin to `terriamap/plugins.ts`** + + ```typescript + const plugins: any[] = [ + import("terriajs-plugin-sample") + ]; + ... + export default plugins; + ``` + +4. **Build terriamap and run a dev server** + + ```bash + # From the terriamap directory run + yarn run gulp dev + ``` + +#### Testing the plugin Once the server is running visit http://localhost:3001 to load the app. You should see a new plugin button added to the map toolbar on the right hand side. Opening the tool will prompt the user to draw a rectangle on the map, this will place a 3d box of the same dimension on the map. Screenshot of the plugin in action: ![Sample plugin](sample-plugin.png "Sample plugin") -# Plugin development workflow +## 👩‍🔬 Developing your own plugin -This section assumes you have completed the steps for [adding the plugin to your terriamap](#adding-the-plugin-to-your-terriamap). +### Setting up development enviroment Developing the plugin requires correctly setting up the yarn workspace. Your local directory structure should look something like: + ``` terriamap/ packages/ - ├── plugin-sample - └── terriajs + └── plugin-sample ``` -The `terriajs` and `plugin-sample` repositories must be checked out under `terriamap/packages/` folder +This `plugin-sample` repository must be checked out and setup correctly under `terriamap/packages` directory. The steps below shows how to do that. -### Checkout terriajs and sample-plugin into the packages folder +1. Checkout `plugin-sample` into the packages folder -```bash -cd terriamap/ -mkdir -p packages -git clone https://github.com/terriajs/terriajs packages/terriajs -git clone https://github.com/terriajs/plugin-sample packages/plugin-sample -``` + ```bash + cd terriamap/ + mkdir -p packages + git clone https://github.com/terriajs/plugin-sample packages/plugin-sample + ``` -### Add the plugin package to the [yarn workspace](https://classic.yarnpkg.com/lang/en/docs/workspaces/) settings of your terriamap `package.json` file. +2. Add the plugin package to the [yarn workspace](https://classic.yarnpkg.com/lang/en/docs/workspaces/) settings of your terriamap's `package.json` file. -Edit `package.json` for terriamap: + Edit `terriamap/package.json`: -```json - { - "private": true, - "workspaces": { - "packages": [ - "packages/terriajs", - "packages/cesium", - "packages/terriajs-server" - "packages/plugin-sample" // <-- plugin-sample added here - ], + ```json + { + "private": true, + "workspaces": { + "packages": [ + "packages/terriajs", + "packages/cesium", + "packages/terriajs-server" + "packages/plugin-sample" // <-- plugin-sample added here + ], + + ... + + "dependencies": { + "terriajs-plugin-api": "0.0.1-alpha.16", + "terriajs-plugin-sample": "0.0.1-alpha.8", // <-- plugin-sample version should match the version in packages/plugin-sample/package.json + ``` - ... +3. Install the new dependencies + + Make sure you are in the `terriamap` directory and run: - "dependencies": { - "terriajs-plugin-api": "0.0.1-alpha.16", - "terriajs-plugin-sample": "0.0.1-alpha.8", // <-- plugin-sample version should match the version in packages/plugin-sample/package.json -``` + ```bash + yarn install + ``` + +4. Build the plugin-sample -### Build terriamap + ```bash + cd terriamap/packages/plugin-sample + # Start a plugin build process that watches for file changes + yarn run watch + ``` -From your `terriamap` folder run: +5. Build terriamap + + Now, from your `terriamap` folder run: + + ```bash + yarn install + # Starts a terriamap dev server that watches for code changes and rebuilds the map + yarn run gulp dev + ``` + +👉 You need to keep both the yarn commands running, then start making make changes to the plugin code, terriamap will automatically +rebuild your changes. + +👉 Watch for errors from the plugin build process. Note that the app page doesn't reload automatically when the code rebuilds, you +have to refresh the page to see your changes. + +### Troubleshooting + +The plugin provides a script to check if the dev environment has been set up correctly. ```bash -yarn install -# Starts a terriamap dev server that watches for code changes and rebuilds the map -yarn run gulp dev +$ cd packages/plugin-sample +$ yarn check-dev-env ``` -### Build plugin-sample +If it generates an output like below with all checks passing, then your dev enviroment setup is probably correct. ```bash -cd terriamap/packages/plugin-sample -# Start a plugin build process that watches for file changes -yarn run watch +$ node scripts/checkDevEnv.js +✅ Find map workspace - Yes (/home/user/terriamap) +✅ Plugin added to workspaces setting - Yes +✅ Plugin added to dependencies - Yes +✅ Package versions match - Yes (1.0.0 matches ^1.0.0) +✅ Plugin import resolves correctly - Yes +✅ Plugin added to plugins registry - Yes +✅ terriajs-plugin-api versions match - Yes (0.0.1-alpha.16 matches ^0.0.1-alpha.15) +Done in 0.10s. ``` -Note: you need to keep both the yarn commands running, then start making make changes to the plugin code, terriamap will automatically -rebuild your changes. +### Plugin scripts + +The following scripts are available to help with development + +`yarn build` - Bundle `src` and `specs` folders, typecheck and lint. + +`yarn watch` - Watch files and rebuild plugin. + +`yarn test` - Runs the tests + +`yarn typecheck` - Typechecks the files using typescript compiler + +`yarn check-dev-env` - Verifies that the plugin development enviroment is setup correctly + +### Plugin API + +Documentation for the plugin API is still in works, meanwhile please inspect the [terriajs-plugin-api](https://github.com/terriajs/plugin-api) repository for available APIs. + + -Watch for errors from the plugin build process. Note that the app page doesn't reload automatically when the code rebuilds, you -have to refresh the page to see your changes. From 71e4bca3e3e629d7ce5af3b35af7403a719df09a Mon Sep 17 00:00:00 2001 From: Nanda Date: Tue, 30 Apr 2024 16:16:35 +1000 Subject: [PATCH 08/10] Update lib/README.md. --- lib/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/README.md b/lib/README.md index 5d39fd4..0eeb6c7 100644 --- a/lib/README.md +++ b/lib/README.md @@ -1 +1,5 @@ -Note: This folder contains plugin library code. The code for your plugin must be placed in the [src](../src) folder. +👉 Note: + +This folder contains plugin library code. + +The code for your plugin should go under [src](../src) folder. From daa4b84707d3aeda7237d382e9169c1652ad3377 Mon Sep 17 00:00:00 2001 From: Nanda Date: Tue, 30 Apr 2024 16:57:09 +1000 Subject: [PATCH 09/10] Changed README text. --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9818639..9dad3f4 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # TerriaJS sample plugin -This repository implements a sample TerriaJS plugin. This plugin implements a -custom tool for drawing an interactive 3D box on the map. It serves as an -example for setting up an loading an external plugin library that adds some new -functionality to Terria without forking it. +This repository implements a sample TerriaJS plugin which adds a custom tool to +Terria map for drawing an interactive 3D box. + +It serves as an example for setting up and loading an external plugin library +that adds new functionality to Terria without forking it. Plugins allow extending Terria in two ways: From 94baa4034b8839dfce0a93650a166ab5eba3858f Mon Sep 17 00:00:00 2001 From: Nanda Date: Tue, 30 Apr 2024 16:58:03 +1000 Subject: [PATCH 10/10] Add icon. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9dad3f4..2546451 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# TerriaJS sample plugin +# 🔌 TerriaJS sample plugin This repository implements a sample TerriaJS plugin which adds a custom tool to Terria map for drawing an interactive 3D box.