Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,45 @@ the `.js` file instead.
</script>
```

## 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
<div id="solution"></div>
<script src="/dist/swipl/swipl-web-no-data.js"></script>
<script>
(async () => {
const response = await fetch("/path/to/your-image.pvm");
const image = new Uint8Array(await response.arrayBuffer());
const swipl = await SWIPL({
arguments: ["-q", "-x", "image.pvm"],
preRun: [(module) => module.FS.writeFile("image.pvm", image)],
locateFile: (path) => `/dist/swipl/${path}`,
});
const solutionElement = document.getElementById("solution");
solutionElement.textContent = swipl.prolog.query("hello(X).").once().X;
})();
</script>
```

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 <http://localhost:8080/examples/browser-no-data.html> (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:
Expand Down
6 changes: 6 additions & 0 deletions dist/loadImageWeb.ts
Original file line number Diff line number Diff line change
@@ -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);
}
2 changes: 2 additions & 0 deletions dist/swipl/swipl-web-no-data.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import SWIPL = require("../common");
export = SWIPL;
8 changes: 8 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions docker/swipl-web-no-data.cmake
Original file line number Diff line number Diff line change
@@ -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()
30 changes: 30 additions & 0 deletions examples/browser-no-data.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF8">
<title>SWI-Prolog WebAssembly no-data browser test</title>
</head>
<body>
<div id="solution"></div>
<script src="/dist/swipl/swipl-web-no-data.js"></script>
<script>
(async () => {
// The no-data build ships no Prolog library, so it boots from a
// saved state. Only swipl-web-no-data.wasm is fetched by the
// runtime itself (streamed via WebAssembly.instantiateStreaming).
const response = await fetch('/examples/no-data-image.pvm');
const image = new Uint8Array(await response.arrayBuffer());
const swipl = await SWIPL({
arguments: ['-q', '-x', 'image.pvm'],
preRun: [(module) => module.FS.writeFile('image.pvm', image)],
locateFile: (path) => {
return `/dist/swipl/${path}`;
}
});
const solutionElement = document.getElementById('solution');
const firstSolution = swipl.prolog.query('hello(X).').once().X;
solutionElement.textContent = firstSolution;
})();
</script>
</body>
</html>
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
48 changes: 48 additions & 0 deletions tests/browser.js
Original file line number Diff line number Diff line change
@@ -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");
Expand Down Expand Up @@ -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 });
}
});
});
40 changes: 40 additions & 0 deletions tests/node.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");

describe("SWI-Prolog WebAssembly on Node.js", () => {
Expand Down Expand Up @@ -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");
});
});
Loading