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");