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
18 changes: 6 additions & 12 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,18 +136,12 @@ jobs:
with:
name: dist
path: dist
- run: mkdir ./bundle
- name: "Create Bundle"
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
version=$(npx semantic-release --dry-run | grep -oP 'The next release version is \K[0-9]+\.[0-9]+\.[0-9]+') || true
if [ $version ]
then
npm run bundle:webpack -- --name=v$version
npm run bundle:latest -- --name=v$version
cp -r ./examples/prebuilt/ bundle/example/
fi
# Releases no longer publish anything to the `pages` branch. The old
# per-release ~4MB webpack bundle grew that branch to ~7.5GB; the docs
# now point browser consumers directly at a CDN, so new versions need no
# Pages files at all. URLs of previously published versions are covered
# by a ONE-TIME backfill of redirect stubs — see
# scripts/generate-redirects.ts (deliberately not wired into CI).
- name: Release
env:
# You may be tempted to make this github.token, this won't work
Expand Down
94 changes: 74 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,40 +171,94 @@ eyereasoner --nope --quiet ./socrates.n3 --query ./socrates-query.n3

## Browser Builds

For convenience we provide deploy bundled versions of the eyereasoner on github pages which can be directly used in an HTML document as shown in [this example](https://github.com/eyereasoner/eye-js/tree/main/examples/prebuilt/index.html) which is also [deployed on github pages](https://eyereasoner.github.io/eye-js/example/index.html).
`eyereasoner` can be used directly in the browser — with no build step and no self-hosting — straight from a public ESM CDN such as [esm.sh](https://esm.sh) or [jsDelivr](https://www.jsdelivr.com/). See [this example](https://github.com/eyereasoner/eye-js/tree/main/examples/prebuilt/index.html), which is also [deployed on github pages](https://eyereasoner.github.io/eye-js/example/index.html).

There are two ways to load it:

- **Separate WebAssembly assets (recommended).** The SWI-Prolog engine is fetched as a real `.wasm` binary (plus its `.data` archive) next to a small JS driver: the browser compiles the WebAssembly while it streams from the network, the binaries are cached independently of the `eyereasoner` release, and the main thread never parses a multi-megabyte JavaScript file with the WASM inlined as a string.
- **Zero-config single URL.** The npm package is fully self-contained — the SWI-Prolog WASM and the EYE image are inlined in the JavaScript — so a single `import` works with no further setup, at the cost of shipping the WebAssembly inside JavaScript.

Measured over the wire (brotli-compressed): the single-URL graph transfers ≈1.9 MB of JavaScript; the separate-asset delivery transfers ≈2.7 MB in total (the split `.wasm` itself is ~0.4 MB *smaller* than its JS-inlined form, but the split SWIPL build also downloads the 1.6 MB SWI-Prolog `.data` archive, which the inlined `no-data` build replaces with the EYE image). The separate-asset delivery trades those extra bytes for streaming compilation, less main-thread JS parsing, and cross-release caching.

### Separate WebAssembly assets (recommended)

The package's own `SWIPL` build inlines the WASM, so this recipe does two things: an [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) swaps that inlined dependency for `swipl-wasm`'s split web build (`?external=swipl-wasm` is what makes the dependency mappable), and a small wrapper tells the split build where its `.wasm`/`.data` live on the CDN:

```html
<script type="importmap">
{
"imports": {
"swipl-wasm/dist/swipl/swipl-bundle-no-data": "https://esm.sh/swipl-wasm@7.0.10/dist/swipl/swipl-web.js",
"swipl-wasm/dist/strToBuffer": "https://esm.sh/swipl-wasm@7.0.10/dist/strToBuffer.js"
}
}
</script>
<script type="module">
import SWIPL from 'https://esm.sh/swipl-wasm@7.0.10/dist/swipl/swipl-web.js';
import { n3reasoner } from 'https://esm.sh/eyereasoner@21.1.10?external=swipl-wasm';

// swipl-web.wasm (streaming-compiled) and swipl-web.data are fetched from here
const assets = 'https://cdn.jsdelivr.net/npm/swipl-wasm@7.0.10/dist/swipl/';
const SwiplWeb = (options) => SWIPL({
...options,
preRun: [options.preRun].flat().filter(Boolean), // this build needs preRun to be an array
locateFile: (file) => assets + file,
});

const result = await n3reasoner(
':Socrates a :Man. {?s a :Man} => {?s a :Mortal}.',
undefined,
{ SWIPL: SwiplWeb },
);
console.log(result); // :Socrates a :Mortal.
</script>
```

There is a bundled version for each release - which can be found at the url:
<p align=center>
https://eyereasoner.github.io/eye-js/vMajor/vMinor/vPatch/index.js
> **Keep the versions in lockstep.** The EYE image inside each `eyereasoner` release is built against the *exact* `swipl-wasm` version that release pins (`eyereasoner@21.1.10` pins `swipl-wasm@7.0.10` — see the `dependencies` in [its package.json](https://cdn.jsdelivr.net/npm/eyereasoner@21.1.10/package.json)). When you bump `eyereasoner`, update the `swipl-wasm` version in the import map and asset URLs to match.

for instance v2.3.14 has the url https://eyereasoner.github.io/eye-js/2/3/14/index.js. We also have shortcuts for:
- the latest version https://eyereasoner.github.io/eye-js/latest/index.js,
- the latest of each major version https://eyereasoner.github.io/eye-js/vMajor/latest/index.js, and
- the latest of each minor version https://eyereasoner.github.io/eye-js/vMajor/vMinor/latest/index.js
### Zero-config single URL

Available versions can be browsed at https://github.com/eyereasoner/eye-js/tree/pages.
```html
<script type="module">
import { n3reasoner } from 'https://esm.sh/eyereasoner';
const result = await n3reasoner(':Socrates a :Man. {?s a :Man} => {?s a :Mortal}.');
console.log(result); // :Socrates a :Mortal.
</script>
```

Github also serves these files with a `gzip` content encoding which compresses the script to ~1.4MB when being served.
You can pin a version or version range with the usual npm semver syntax, which the CDN resolves for you:
- the latest version: `https://esm.sh/eyereasoner`
- the latest of a major version: `https://esm.sh/eyereasoner@2`
- the latest of a minor version: `https://esm.sh/eyereasoner@2.3`
- an exact patch version: `https://esm.sh/eyereasoner@2.3.14`

![](./github-transfer.png)
[jsDelivr](https://www.jsdelivr.com/) works as a drop-in alternative, e.g. `https://cdn.jsdelivr.net/npm/eyereasoner@2/+esm`.

### Serving Files
### Classic `<script>` global

When self-hosting the bundled files, ensure your server includes `charset=utf-8` in the `Content-Type` header for JavaScript files:
No CDN can turn the published CommonJS build of an already-released version into a *synchronous* classic-script global, so a global has to be populated from inside a module script (and is therefore only available asynchronously). Import the exports you need and assign them:

```html
<script type="module">
import { n3reasoner } from 'https://esm.sh/eyereasoner';
window.n3reasoner = n3reasoner; // now callable from non-module scripts
</script>
```
Content-Type: text/javascript; charset=utf-8
```

Without the charset, WASM streaming instantiation may fail in headless browsers with errors like:
- Firefox: `CompileError: wasm validation error: at offset 642: byte size mismatch in type section`
- Chromium: `CompileError: WebAssembly.instantiate(): section was shorter than expected size`
This works with either delivery above.

### Migrating from the GitHub Pages bundles

Most static file servers (e.g., `express.static()`) set this automatically, but custom streaming handlers using `createReadStream().pipe(res)` may not.
Earlier releases were served as webpack bundles from `https://eyereasoner.github.io/eye-js/vMajor/vMinor/vPatch/index.js` (for instance `https://eyereasoner.github.io/eye-js/2/3/14/index.js`), along with `latest`, `vMajor/latest` and `vMajor/vMinor/latest` shortcuts. **The URLs of already-published versions keep working**: a one-time backfill replaced the bundles with tiny stubs that transparently load the equivalent version from the CDN (the classic `index.js` global becomes *asynchronously* populated — a Proxy keeps `await eyereasoner.n3reasoner(...)` working). New releases do not publish anything to GitHub Pages, so new code should use the CDN URLs above directly. Every published version also remains available forever from the npm package (`dist/` ships with the package).

### Dynamic imports

We also distribute bundles that can be dynamically imported on github pages; for example
The CDN modules can also be dynamically imported at runtime:
```ts
const { n3reasoner } = await import('https://esm.sh/eyereasoner@2');
```

The previous `https://eyereasoner.github.io/eye-js/vMajor/latest/dynamic-import.js` URLs keep working too — they redirect to the CDN and re-export the module as `eyereasoner`:
```ts
const { eyereasoner } = await import('https://eyereasoner.github.io/eye-js/2/latest/dynamic-import.js');

Expand Down
34 changes: 29 additions & 5 deletions examples/prebuilt/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,38 @@
<button id="execute">Execute</button>
<div id="result"></div>
</body>
<script src="https://eyereasoner.github.io/eye-js/2/latest/index.js"></script>
<script>
<!-- The import map swaps eyereasoner's internal WASM-inlined SWIPL build for
swipl-wasm's split web build, so the SWI-Prolog engine is fetched as a
real (streaming-compiled, separately cached) .wasm binary instead of
being inlined in JavaScript. See README "Browser Builds".
NB: the swipl-wasm version must match the one pinned by the eyereasoner
release in use (eyereasoner@21.1.10 pins swipl-wasm@7.0.10). -->
<script type="importmap">
{
"imports": {
"swipl-wasm/dist/swipl/swipl-bundle-no-data": "https://esm.sh/swipl-wasm@7.0.10/dist/swipl/swipl-web.js",
"swipl-wasm/dist/strToBuffer": "https://esm.sh/swipl-wasm@7.0.10/dist/strToBuffer.js"
}
}
</script>
<script type="module">
import SWIPL from 'https://esm.sh/swipl-wasm@7.0.10/dist/swipl/swipl-web.js';
import { n3reasoner } from 'https://esm.sh/eyereasoner@21.1.10?external=swipl-wasm';

// swipl-web.wasm and swipl-web.data are fetched from here at runtime
const assets = 'https://cdn.jsdelivr.net/npm/swipl-wasm@7.0.10/dist/swipl/';
const SwiplWeb = (options) => SWIPL({
...options,
preRun: [options.preRun].flat().filter(Boolean), // this build needs preRun to be an array
locateFile: (file) => assets + file,
});

document.getElementById('execute').addEventListener("click", async () => {
document.getElementById("result").innerHTML = (await eyereasoner.n3reasoner(

document.getElementById("result").innerHTML = (await n3reasoner(
document.getElementById("data").value,
undefined,
{ output: 'derivations' }
{ output: 'derivations', SWIPL: SwiplWeb }
)).replaceAll('\n', '<br>');

});
Expand Down
Loading
Loading