diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml
index 18c1ee2d..02d025be 100644
--- a/.github/workflows/nodejs.yml
+++ b/.github/workflows/nodejs.yml
@@ -28,7 +28,10 @@ jobs:
with:
node-version: "24.15"
- run: npm ci
- - run: echo "CACHE_KEY=$(cat ./build-config.json | jq '[.[]] | map(.version, .commit) | @tsv')" >> $GITHUB_ENV
+ # The cache key covers everything that determines the WASM artifacts:
+ # the pinned dependency versions AND the Docker build definition, so
+ # that changes to docker/ invalidate previously cached artifacts.
+ - run: echo "CACHE_KEY=$(cat ./build-config.json | jq '[.[]] | map(.version, .commit) | @tsv')-$(cat docker/* | sha256sum | cut -c1-16)" >> $GITHUB_ENV
- name: Restore cached build
id: cache
diff --git a/.gitignore b/.gitignore
index 8ce380fc..0b597446 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,8 @@ node_modules
dist/swipl/*.js
dist/swipl/swipl-web.data
dist/swipl/swipl-web.wasm
+dist/swipl/swipl-web-no-data.wasm
+examples/no-data-image.pvm
examples/webpack/node_modules
examples/webpack/public/*.data
examples/webpack/public/*.wasm
@@ -12,6 +14,8 @@ dist/loadImage.js
dist/loadImage.d.ts
dist/loadImageDefault.js
dist/loadImageDefault.d.ts
+dist/loadImageWeb.js
+dist/loadImageWeb.d.ts
dist/generateImage.js
dist/generateImage.d.ts
dist/strToBuffer.js
diff --git a/README.md b/README.md
index f61803ce..95ce30c6 100644
--- a/README.md
+++ b/README.md
@@ -98,6 +98,45 @@ the `.js` file instead.
```
+## Using the No-Data Web Build
+
+The build matrix also contains `swipl-web-no-data.js`: like `swipl-web.js`
+the WebAssembly stays in an external `swipl-web-no-data.wasm` file (so
+browsers can compile it with `WebAssembly.instantiateStreaming` and cache it
+independently of your application code), but like `swipl-bundle-no-data.js`
+there is no `.data` file at all.
+
+Because the standard Prolog library is not shipped, this build must be
+booted from a saved state (see "Generating an Image" below):
+
+```html
+
+
+
+```
+
+This is the smallest way to deliver SWI-Prolog over the wire: roughly the
+brotli-compressed size of the `.wasm` plus a small JS glue, instead of
+additionally downloading the `.data` file (`swipl-web`) or downloading and
+parsing a single large JavaScript file with the WebAssembly embedded in
+base64 (`swipl-bundle-no-data`).
+
+You can run this example by executing `npm run test:serve-http` and
+visiting (the test
+suite generates the saved state used by that page).
+
## Generating an Image
Often you will want to bundle a pre-built image of your Prolog file. The easiest way to do this is using the `swipl-generate` command to generate the image. For example, in `./examples/generation`, the script `npx swipl-generate ./max.pl ./dist/max.ts` will generate a file `./dist/max.ts` which contains the image of `./max.pl`. This file can then be imported into your project and used as follows:
diff --git a/dist/loadImageWeb.ts b/dist/loadImageWeb.ts
new file mode 100644
index 00000000..ee0a3aae
--- /dev/null
+++ b/dist/loadImageWeb.ts
@@ -0,0 +1,6 @@
+import SWIPL from './swipl/swipl-web-no-data';
+import { loadImage } from './loadImage'
+
+export default function(image: string | Buffer | Uint8Array) {
+ return loadImage(image, SWIPL);
+}
diff --git a/dist/swipl/swipl-web-no-data.d.ts b/dist/swipl/swipl-web-no-data.d.ts
new file mode 100644
index 00000000..e5007941
--- /dev/null
+++ b/dist/swipl/swipl-web-no-data.d.ts
@@ -0,0 +1,2 @@
+import SWIPL = require("../common");
+export = SWIPL;
diff --git a/docker/Dockerfile b/docker/Dockerfile
index b8ec0143..bae846a4 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -38,6 +38,14 @@ RUN git submodule update --init --depth 1 -j 100 \
packages/nlp packages/pcre packages/plunit packages/sgml packages/RDF \
packages/semweb packages/zlib
+# Add the swipl-web-no-data link target: an external (streamable) .wasm
+# with no .data preload and no base64 embedding. The snippet is
+# appended (rather than applied as a context patch) so it keeps working
+# across SWIPL_COMMIT bumps, and it is guarded to become a no-op if
+# swipl-devel ever ships the target itself.
+COPY swipl-web-no-data.cmake /tmp/swipl-web-no-data.cmake
+RUN cat /tmp/swipl-web-no-data.cmake >> /swipl-devel/cmake/EmscriptenTargets.cmake
+
# Build SWIPL
WORKDIR /swipl-devel/build.wasm
RUN mkdir -p /swipl-devel/build.wasm
diff --git a/docker/swipl-web-no-data.cmake b/docker/swipl-web-no-data.cmake
new file mode 100644
index 00000000..c09ae4fc
--- /dev/null
+++ b/docker/swipl-web-no-data.cmake
@@ -0,0 +1,27 @@
+
+# Create swipl-web-no-data.js + swipl-web-no-data.wasm
+#
+# Same packaging as swipl-web (external, streamable .wasm and a small
+# MODULARIZE'd JS glue), but without `--preload-file`, so no
+# swipl-web-no-data.data is produced and the glue never fetches one.
+# Like swipl-bundle-no-data, the result cannot load the standard
+# Prolog library and must be booted from a saved state
+# (e.g. `-x image.pvm`).
+#
+# This file is appended to cmake/EmscriptenTargets.cmake of swipl-devel
+# by the npm-swipl-wasm Docker build. It reuses WASM_DIST_LINK_FLAGS,
+# SWIPL_SRC, PREJS and POSTJS defined earlier in that file. The block
+# is guarded so it becomes a no-op if a future swipl-devel ships the
+# target itself.
+if(NOT TARGET swipl-web-no-data)
+ set(WASM_NO_DATA_WEB_LINK_FLAGS
+ -Wno-unused-main)
+ join_list(WASM_NO_DATA_WEB_LINK_FLAGS_STRING " "
+ ${WASM_NO_DATA_WEB_LINK_FLAGS} ${WASM_DIST_LINK_FLAGS})
+ add_executable(swipl-web-no-data ${SWIPL_SRC})
+ set_target_properties(swipl-web-no-data PROPERTIES
+ LINK_FLAGS "${WASM_NO_DATA_WEB_LINK_FLAGS_STRING}")
+ target_link_libraries(swipl-web-no-data libswipl)
+ set_property(TARGET swipl-web-no-data PROPERTY LINK_DEPENDS
+ ${POSTJS} ${PREJS})
+endif()
diff --git a/examples/browser-no-data.html b/examples/browser-no-data.html
new file mode 100644
index 00000000..8001f6f2
--- /dev/null
+++ b/examples/browser-no-data.html
@@ -0,0 +1,30 @@
+
+
+
+
+ SWI-Prolog WebAssembly no-data browser test
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
index bb0ac079..1913137d 100644
--- a/package.json
+++ b/package.json
@@ -54,6 +54,8 @@
"build:wasm-docker:extract:data": "docker cp swipl-wasm:/swipl-devel/build.wasm/src/swipl-web.data dist/swipl/swipl-web.data",
"build:wasm-docker:extract:wasm": "docker cp swipl-wasm:/swipl-devel/build.wasm/src/swipl-web.wasm dist/swipl/swipl-web.wasm",
"build:wasm-docker:extract:web": "docker cp swipl-wasm:/swipl-devel/build.wasm/src/swipl-web.js dist/swipl/swipl-web.js",
+ "build:wasm-docker:extract:webNoData": "docker cp swipl-wasm:/swipl-devel/build.wasm/src/swipl-web-no-data.js dist/swipl/swipl-web-no-data.js",
+ "build:wasm-docker:extract:webNoDataWasm": "docker cp swipl-wasm:/swipl-devel/build.wasm/src/swipl-web-no-data.wasm dist/swipl/swipl-web-no-data.wasm",
"build:wasm-docker:extract:bundle": "docker cp swipl-wasm:/swipl-devel/build.wasm/src/swipl-bundle.js dist/swipl/swipl-bundle.js",
"build:wasm-docker:extract:bundleNoData": "docker cp swipl-wasm:/swipl-devel/build.wasm/src/swipl-bundle-no-data.js dist/swipl/swipl-bundle-no-data.js",
"build:wasm-docker:extract:node": "docker cp swipl-wasm:/swipl-devel/build.wasm/src/swipl-web.js dist/swipl/swipl.js",
diff --git a/tests/browser.js b/tests/browser.js
index ffe1dc04..8a36bfe5 100644
--- a/tests/browser.js
+++ b/tests/browser.js
@@ -1,3 +1,5 @@
+const assert = require("assert");
+const fs = require("fs");
const http = require("http");
const path = require("path");
const static = require("node-static");
@@ -47,4 +49,50 @@ describe("SWI-Prolog WebAssembly on Browser", () => {
server.close();
}
});
+
+ it("should run the no-data web build from a saved state fetching only the wasm", async () => {
+ // The no-data builds do not ship the Prolog library, so the page
+ // boots from a saved state that we generate up front.
+ const imagePath = path.join(__dirname, "..", "examples", "no-data-image.pvm");
+ const SWIPL_BUNDLE = require("../dist/swipl/swipl-bundle");
+ const bundle = await SWIPL_BUNDLE({
+ arguments: ["-q", "-f", "prolog.pl"],
+ preRun: [(module) => module.FS.writeFile("prolog.pl", "hello(world).\n")],
+ });
+ bundle.prolog.query('qsave_program("image.pvm")').once();
+ fs.writeFileSync(imagePath, bundle.FS.readFile("/image.pvm"));
+
+ const server = await createServer();
+ try {
+ const browser = await puppeteer.launch({ headless: "new" });
+ try {
+ const page = await browser.newPage();
+ page.setDefaultTimeout(1_5000);
+ page.setDefaultNavigationTimeout(1_5000);
+ const requestedPaths = [];
+ page.on("request", (request) => {
+ requestedPaths.push(new URL(request.url()).pathname);
+ });
+ await page.goto("http://localhost:8080/examples/browser-no-data.html");
+ await page.waitForSelector("#solution");
+ await page.waitForFunction(() => {
+ const solutionElement = document.querySelector("#solution");
+ return solutionElement.textContent === "world";
+ });
+ assert.ok(
+ requestedPaths.some((p) => p.endsWith("/swipl-web-no-data.wasm")),
+ "the external wasm must be fetched"
+ );
+ assert.ok(
+ requestedPaths.every((p) => !p.endsWith(".data")),
+ "no .data file may be fetched"
+ );
+ } finally {
+ await browser.close();
+ }
+ } finally {
+ server.close();
+ fs.rmSync(imagePath, { force: true });
+ }
+ });
});
diff --git a/tests/node.js b/tests/node.js
index cf5a7d21..ad433c73 100644
--- a/tests/node.js
+++ b/tests/node.js
@@ -1,4 +1,5 @@
const assert = require("assert");
+const fs = require("fs");
const path = require("path");
describe("SWI-Prolog WebAssembly on Node.js", () => {
@@ -189,3 +190,42 @@ describe("SWI-Prolog WebAssembly on Node.js", () => {
});
}
});
+
+describe("SWI-Prolog WebAssembly no-data web build", () => {
+ const SWIPL_WEB_NO_DATA = require("../dist/swipl/swipl-web-no-data");
+ const SWIPL_BUNDLE = require("../dist/swipl/swipl-bundle");
+ const swiplDir = path.join(__dirname, "..", "dist", "swipl");
+
+ // Build a saved state to boot from; the no-data builds do not ship
+ // the Prolog library, so they can only start from an image.
+ async function generateImage() {
+ const bundle = await SWIPL_BUNDLE({
+ arguments: ["-q", "-f", "prolog.pl"],
+ preRun: [(module) => module.FS.writeFile("prolog.pl", "hello(world).\n")],
+ });
+ bundle.prolog.query('qsave_program("image.pvm")').once();
+ return bundle.FS.readFile("/image.pvm");
+ }
+
+ it("should ship an external wasm and no data preload", () => {
+ const glue = fs.readFileSync(path.join(swiplDir, "swipl-web-no-data.js"), "utf8");
+ assert.ok(glue.includes("swipl-web-no-data.wasm"), "glue must reference the external wasm");
+ assert.ok(!glue.includes("swipl-web-no-data.data"), "glue must not reference a data preload");
+ assert.ok(!fs.existsSync(path.join(swiplDir, "swipl-web-no-data.data")), "no data file may be produced");
+ });
+
+ it("should boot from a saved state fetching only the wasm", async () => {
+ const image = await generateImage();
+ const located = [];
+ const swipl = await SWIPL_WEB_NO_DATA({
+ arguments: ["-q", "-x", "image.pvm"],
+ preRun: [(module) => module.FS.writeFile("image.pvm", image)],
+ locateFile: (name) => {
+ located.push(name);
+ return path.join(swiplDir, name);
+ },
+ });
+ assert.strictEqual(swipl.prolog.query("hello(X).").once().X, "world");
+ assert.ok(located.every((name) => !name.endsWith(".data")), "no .data file may be requested");
+ });
+});