From f22c37c7d75f60e754c6bec89d1d6cb13bfddb9e Mon Sep 17 00:00:00 2001 From: Michael Schmuki <michael@schmuki.io> Date: Thu, 2 May 2024 21:50:41 +0200 Subject: [PATCH] Feature: Let the qgis-js loader find the relatrive runtime files from "assets/wasm" in order to use it from a CDN --- docs/bundling.md | 61 +++++++++++++++++++++++++++++++++- packages/qgis-js/src/loader.ts | 58 +++++++++++++++++++++++++++----- sites/dev/src/index.ts | 7 ++-- 3 files changed, 113 insertions(+), 13 deletions(-) diff --git a/docs/bundling.md b/docs/bundling.md index c3fb65b..182bd81 100644 --- a/docs/bundling.md +++ b/docs/bundling.md @@ -1,3 +1,62 @@ # Bundling -<!-- FIXME: Write up on how to bundle .wasm assets with Vite and Webpack --> +qgis-js consists of two parts: The runtime generated by Emscripten and the TypeScript/JavaScript API, that can be seen as a wrapper around the runtime. The wrapper can be imported, used and bundled (e.g. tree-shaked) like any other JavaScript library. But it is important that the runtime is not modified and served as is. + +See the [`qgis-js` Package `README.md`](../packages/qgis-js/README.md) for more information about the files involved. Everything inside `assets/wasm` is part of the runtime. + +To not confuse any downstream bundler, the runtime is [dynamically loaded](../packages/qgis-js/src/loader.ts) in a way that it will not be processed. Therefore **it is up to the end user to include the runtime files in the final build**. + +### Explicitly specifying the runtime location + +The runtime location can be specified with the `prefix` configuration option. This is useful when the runtime is not in the same directory as the main script or served from a different server (e.g. a CDN). + +```js +const { api } = await qgis({ + prefix: "/path/to/runtime/assets", +}); +``` + +## Examples + +### qgis-js with [Vite](https://vitejs.dev/) + +One can use the [vite-plugin-static-copy](https://github.com/sapphi-red/vite-plugin-static-copy). + +For an example see the [`vite.config.ts`](./examples/qgis-js-example-ol/vite.config.js) in the [qgis-js-example-ol](./examples/qgis-js-example-ol) project and note that the COOP/COEP headers have to be set after the plugin (see [`compatibility.md`](./compatibility.md) for more information). + +### qgis.js with [Webpack](https://webpack.js.org/) + +With Webpack one can use the [copy-webpack-plugin](https://www.npmjs.com/package/copy-webpack-plugin). + +Note that the COOP/COEP headers have to be set in the `webpack.config.js` (see [`compatibility.md`](./compatibility.md) for more information). + +### Using qgis-js from a CDN + +An example of how to use qgis-js from a CDN (e.g. [jsDelivr](https://www.jsdelivr.com/)): + +```html +<!doctype html> +<html lang="en"> + <head> + <title>qgis-js</title> + </head> + <body> + <script type="module"> + // import qgis-js from a CDN + const { qgis } = await import( + "https://cdn.jsdelivr.net/npm/qgis-js/dist/qgis.js" + ); + // boot the qgis-js runtime + const { api } = await qgis(); + // use the qgis-js api + const rect = new api.Rectangle(1, 1, 42, 42); + const center = rect.center(); + console.log(`Center: x: ${center.x}, y: ${center.y}`); + </script> + </body> +</html> +``` + +Note that the main script has to be explicitly loaded with `qgis-js/dist/qgis.js` (Or a prefix pointing to `qgis-js/dist/assets/wasm` has to be set ([see above](#explicitly-specifying-the-runtime-location))). + +And also ensure that the HTML document has the correct COOP/COEP headers set (see [`compatibility.md`](./compatibility.md) for more information). diff --git a/packages/qgis-js/src/loader.ts b/packages/qgis-js/src/loader.ts index d213b58..f236b16 100644 --- a/packages/qgis-js/src/loader.ts +++ b/packages/qgis-js/src/loader.ts @@ -22,13 +22,12 @@ interface QtRuntimeFactory { /** * Loads the QtRuntimeFactory Emscripten module with the given prefix. * - * @param prefix - The prefix to use for the module path. + * @param mainScriptPath - The import path of the main script * @returns A promise that resolves with the QtRuntimeFactory module. */ -function loadModule(prefix: string = "/"): Promise<QtRuntimeFactory> { +function loadModule(mainScriptPath: string): Promise<QtRuntimeFactory> { return new Promise(async (resolve, reject) => { try { - const mainScriptPath = prefix + "/" + "qgis-js" + ".js"; // hack to import es module without vite knowing about it const createQtAppInstance = ( await new Function(`return import("${mainScriptPath}")`)() @@ -48,14 +47,57 @@ function loadModule(prefix: string = "/"): Promise<QtRuntimeFactory> { * @param config The {@link QgisRuntimeConfig} that will be taken into account during loading and initialization. * @returns A promise that resolves to a {@link QgisRuntime}. */ -export async function qgis(config: QgisRuntimeConfig): Promise<QgisRuntime> { - return new Promise(async (resolve) => { - const { createQtAppInstance } = await loadModule(config.prefix); +export async function qgis( + config: QgisRuntimeConfig = {}, +): Promise<QgisRuntime> { + return new Promise(async (resolve, reject) => { + let prefix: string | undefined = undefined; + if (config.prefix) { + prefix = config.prefix; + } else { + const url = import.meta.url; + if (/.*\/src\/loader\.[ts|js]\?*[^/]*$/.test(url)) { + console.warn( + [ + `qgis-js loader is running in development mode and no "prefix" seems to be configured.`, + ` - Consider adding the QgisRuntimePlugin when bundling with Vite.`, + ` - For more information see: https://github.com/qgis/qgis-js/blob/main/docs/bundling.md`, + ].join("\n"), + ); + if (typeof window !== "undefined") { + prefix = new URL("assets/wasm", window.location.href).pathname; + } + } else { + prefix = new URL("assets/wasm", import.meta.url).href; + } + } + + if (!prefix) { + prefix = "assets/wasm"; + } else { + prefix = prefix.replace(/\/$/, ""); // ensure no trailing slash + } + + let qtRuntimeFactory: QtRuntimeFactory | undefined = undefined; + try { + const mainScriptPath = `${prefix}/qgis-js.js`; + qtRuntimeFactory = await loadModule(mainScriptPath); + } catch (error) { + reject( + new Error(`Unable to load the qgis-js.js script`, { cause: error }), + ); + return; + } - const canvas = document.querySelector("#screen") as HTMLDivElement | null; + const { createQtAppInstance } = qtRuntimeFactory!; + + let canvas: HTMLDivElement | undefined = undefined; + if (typeof document !== "undefined") { + canvas = document?.querySelector("#screen") as HTMLDivElement; + } const runtimePromise = createQtAppInstance({ - locateFile: (path: string) => `${config.prefix}/` + path, + locateFile: (path: string) => `${prefix}/` + path, preRun: [ function (module: any) { module.qtContainerElements = canvas ? [canvas] : []; diff --git a/sites/dev/src/index.ts b/sites/dev/src/index.ts index 924b5a2..5170b30 100644 --- a/sites/dev/src/index.ts +++ b/sites/dev/src/index.ts @@ -87,10 +87,9 @@ async function initDemo() { // boot the runtime if (timer) console.time("boot"); const { api, fs } = await qgis({ - prefix: "/qgis-js/assets/wasm", - onStatus: (status: string) => { - onStatus(status); - }, + // use assets form QgisRuntimePlugin + prefix: new URL("assets/wasm", window.location.href).pathname, + onStatus: (status: string) => onStatus(status), }); if (timer) console.timeEnd("boot");