Prepare JS lib

This commit is contained in:
Maximilian Ammann 2022-09-08 18:08:47 +02:00
parent ea52e9c96d
commit 1f3423a70c
10 changed files with 183 additions and 153 deletions

View File

@ -1,15 +1,8 @@
[target.wasm32-unknown-unknown] [target.wasm32-unknown-unknown]
rustflags = [ rustflags = [
# Enabled unstable APIs from web_sys # Enabled unstable APIs from web_sys
"--cfg=web_sys_unstable_apis", "--cfg=web_sys_unstable_apis"
# Enables features which are required for shared-memory
"-C", "target-feature=+atomics,+bulk-memory,+mutable-globals",
# Enables the possibility to import memory into wasm.
# Without --shared-memory it is not possible to use shared WebAssembly.Memory.
"-C", "link-args=--shared-memory --import-memory",
] ]
runner = 'wasm-bindgen-test-runner'
[profile.wasm-dev] [profile.wasm-dev]
inherits = "dev" inherits = "dev"

View File

@ -11,7 +11,7 @@ module it can resolve WebAssembly files or WebWorkers dynamically.
The following syntax is used to resolve referenced WebWorkers: The following syntax is used to resolve referenced WebWorkers:
```ts ```ts
new Worker(new URL("./pool.worker.ts", import.meta.url), { new Worker(new URL("./multithreaded-pool.worker.ts", import.meta.url), {
type: 'module' type: 'module'
}); });
``` ```
@ -107,7 +107,7 @@ See config in `web/lib/build.mjs` for an example usage.
### Babel & TypeScript ### Babel & TypeScript
Babel and TypeScript both can produce ESM modules, but they **fail with transforming references within the source code** Babel and TypeScript both can produce ESM modules, but they **fail with transforming references within the source code**
like `new URL("./pool.worker.ts", import.meta.url)`. There exist some Babel plugins, but none of them is stable. like `new URL("./multithreaded-pool.worker.ts", import.meta.url)`. There exist some Babel plugins, but none of them is stable.
Therefore, we actually need a proper bundler which supports outputting ESM modules. Therefore, we actually need a proper bundler which supports outputting ESM modules.
The only stable solution to this is Parcel. Parcel also has good documentation around the bundling of WebWorkers. The only stable solution to this is Parcel. Parcel also has good documentation around the bundling of WebWorkers.
@ -277,4 +277,4 @@ Example config in `package.json:
### Rollup ### Rollup
Not yet evaluated Not yet evaluated

View File

@ -1 +1,2 @@
declare const WEBGL: boolean declare const WEBGL: boolean
declare const MULTITHREADED: boolean

View File

@ -2,6 +2,7 @@ import {build} from 'esbuild';
import metaUrlPlugin from '@chialab/esbuild-plugin-meta-url'; import metaUrlPlugin from '@chialab/esbuild-plugin-meta-url';
import inlineWorker from 'esbuild-plugin-inline-worker'; import inlineWorker from 'esbuild-plugin-inline-worker';
import yargs from "yargs"; import yargs from "yargs";
import process from "process";
import chokidar from "chokidar"; import chokidar from "chokidar";
import {spawnSync} from "child_process" import {spawnSync} from "child_process"
import {dirname} from "path"; import {dirname} from "path";
@ -9,32 +10,30 @@ import {fileURLToPath} from "url";
let argv = yargs(process.argv.slice(2)) let argv = yargs(process.argv.slice(2))
.option('watch', { .option('watch', {
alias: 'w',
type: 'boolean', type: 'boolean',
description: 'Enable watching' description: 'Enable watching'
}) })
.option('release', { .option('release', {
alias: 'r',
type: 'boolean', type: 'boolean',
description: 'Release mode' description: 'Release mode'
}) })
.option('webgl', { .option('webgl', {
alias: 'g',
type: 'boolean', type: 'boolean',
description: 'Enable webgl' description: 'Enable webgl'
}) })
.option('multithreaded', {
type: 'boolean',
description: 'Enable multithreaded support'
})
.option('esm', { .option('esm', {
alias: 'e',
type: 'boolean', type: 'boolean',
description: 'Enable esm' description: 'Enable esm'
}) })
.option('cjs', { .option('cjs', {
alias: 'c',
type: 'boolean', type: 'boolean',
description: 'Enable cjs' description: 'Enable cjs'
}) })
.option('iife', { .option('iife', {
alias: 'i',
type: 'boolean', type: 'boolean',
description: 'Enable iife' description: 'Enable iife'
}) })
@ -44,6 +43,7 @@ let esm = argv.esm;
let iife = argv.iife; let iife = argv.iife;
let cjs = argv.cjs; let cjs = argv.cjs;
let release = argv.release; let release = argv.release;
let multithreaded = argv.multithreaded;
if (!esm && !iife && !cjs) { if (!esm && !iife && !cjs) {
console.warn("Enabling ESM bundling as no other bundle is enabled.") console.warn("Enabling ESM bundling as no other bundle is enabled.")
@ -56,19 +56,29 @@ if (webgl) {
console.log("WebGL support enabled.") console.log("WebGL support enabled.")
} }
let baseSettings = { if (multithreaded) {
entryPoints: ['src/index.ts'], console.log("multithreaded support enabled.")
bundle: true, }
let baseConfig = {
platform: "browser", platform: "browser",
bundle: true,
assetNames: "assets/[name]", assetNames: "assets/[name]",
define: {"WEBGL": `${webgl}`}, define: {
WEBGL: `${webgl}`,
MULTITHREADED: `${multithreaded}`
},
}
let config = {
...baseConfig,
entryPoints: ['src/index.ts'],
incremental: argv.watch, incremental: argv.watch,
plugins: [ plugins: [
inlineWorker({ inlineWorker({
format: "cjs", platform: "browser", ...baseConfig,
format: "cjs",
target: 'es2022', target: 'es2022',
bundle: true,
assetNames: "assets/[name]",
}), }),
metaUrlPlugin() metaUrlPlugin()
], ],
@ -119,11 +129,22 @@ const wasmPack = () => {
let outDirectory = `${getLibDirectory()}/src/wasm`; let outDirectory = `${getLibDirectory()}/src/wasm`;
let profile = release ? "wasm-release" : "wasm-dev" let profile = release ? "wasm-release" : "wasm-dev"
let cargo = spawnSync('cargo', ["build", // language=toml
let multithreaded_config = `target.wasm32-unknown-unknown.rustflags = [
# Enables features which are required for shared-memory
"-C", "target-feature=+atomics,+bulk-memory,+mutable-globals",
# Enables the possibility to import memory into wasm.
# Without --shared-memory it is not possible to use shared WebAssembly.Memory.
"-C", "link-args=--shared-memory --import-memory",
]`
let cargo = spawnSync('cargo', [
...(multithreaded ? ["--config", multithreaded_config] : []),
"build",
"-p", "web", "--lib", "-p", "web", "--lib",
"--target", "wasm32-unknown-unknown", "--target", "wasm32-unknown-unknown",
"--profile", profile, "--profile", profile,
"--features", `${webgl ? "web-webgl" : ""}`, "--features", `${webgl ? "web-webgl," : ""}`,
"-Z", "build-std=std,panic_abort" "-Z", "build-std=std,panic_abort"
], { ], {
cwd: '.', cwd: '.',
@ -206,7 +227,7 @@ const watchResult = async (result) => {
} }
const esbuild = async (name, globalName = undefined) => { const esbuild = async (name, globalName = undefined) => {
let result = await build({...baseSettings, format: name, globalName, outfile: `dist/esbuild-${name}/module.js`,}); let result = await build({...config, format: name, globalName, outfile: `dist/esbuild-${name}/module.js`,});
if (argv.watch) { if (argv.watch) {
console.log("Watching is enabled.") console.log("Watching is enabled.")

73
web/lib/src/browser.ts Normal file
View File

@ -0,0 +1,73 @@
import {
bigInt,
bulkMemory,
exceptions,
multiValue,
mutableGlobals,
referenceTypes,
saturatedFloatToInt,
signExtensions,
simd,
tailCall,
threads
} from "wasm-feature-detect"
export const checkRequirements = () => {
if (MULTITHREADED) {
if (!isSecureContext) {
return "isSecureContext is false!"
}
if (!crossOriginIsolated) {
return "crossOriginIsolated is false! " +
"The Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy HTTP headers are required."
}
}
if (WEBGL) {
if (!isWebGLSupported()) {
return "WebGL is not supported in this Browser!"
}
} else {
if (!("gpu" in navigator)) {
return "WebGPU is not supported in this Browser!"
}
}
return null
}
export const isWebGLSupported = () => {
try {
const canvas = document.createElement('canvas')
canvas.getContext("webgl")
return true
} catch (x) {
return false
}
}
export const checkWasmFeatures = async () => {
const checkFeature = async function (featureName: string, feature: () => Promise<boolean>) {
let result = await feature();
let msg = `The feature ${featureName} returned: ${result}`;
if (result) {
console.log(msg);
} else {
console.warn(msg);
}
}
await checkFeature("bulkMemory", bulkMemory);
await checkFeature("exceptions", exceptions);
await checkFeature("multiValue", multiValue);
await checkFeature("mutableGlobals", mutableGlobals);
await checkFeature("referenceTypes", referenceTypes);
await checkFeature("saturatedFloatToInt", saturatedFloatToInt);
await checkFeature("signExtensions", signExtensions);
await checkFeature("simd", simd);
await checkFeature("tailCall", tailCall);
await checkFeature("threads", threads);
await checkFeature("bigInt", bigInt);
}

View File

@ -1,92 +1,12 @@
import init, {create_pool_scheduler, run} from "./wasm/maplibre" import {create_pool_scheduler, run} from "./wasm/maplibre"
import {Spector} from "spectorjs" import {Spector} from "spectorjs"
import {WebWorkerMessageType} from "./worker-types" // @ts-ignore esbuild plugin is handling this
import { import MultithreadedPoolWorker from './multithreaded-pool.worker.js';
bigInt, // @ts-ignore esbuild plugin is handling this
bulkMemory,
exceptions,
multiValue,
mutableGlobals,
referenceTypes,
saturatedFloatToInt,
signExtensions,
simd,
tailCall,
threads
} from "wasm-feature-detect"
// @ts-ignore
import PoolWorker from './pool.worker.js'; import PoolWorker from './pool.worker.js';
const isWebGLSupported = () => { import {initialize} from "./module";
try { import {checkRequirements, checkWasmFeatures} from "./browser";
const canvas = document.createElement('canvas')
canvas.getContext("webgl")
return true
} catch (x) {
return false
}
}
const checkWasmFeatures = async () => {
const checkFeature = async function (featureName: string, feature: () => Promise<boolean>) {
let result = await feature();
let msg = `The feature ${featureName} returned: ${result}`;
if (result) {
console.log(msg);
} else {
console.warn(msg);
}
}
await checkFeature("bulkMemory", bulkMemory);
await checkFeature("exceptions", exceptions);
await checkFeature("multiValue", multiValue);
await checkFeature("mutableGlobals", mutableGlobals);
await checkFeature("referenceTypes", referenceTypes);
await checkFeature("saturatedFloatToInt", saturatedFloatToInt);
await checkFeature("signExtensions", signExtensions);
await checkFeature("simd", simd);
await checkFeature("tailCall", tailCall);
await checkFeature("threads", threads);
await checkFeature("bigInt", bigInt);
}
const alertUser = (message: string) => {
console.error(message)
alert(message)
}
const checkRequirements = () => {
if (!isSecureContext) {
alertUser("isSecureContext is false!")
return false
}
if (!crossOriginIsolated) {
alertUser("crossOriginIsolated is false! " +
"The Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy HTTP headers are required.")
return false
}
if (WEBGL) {
if (!isWebGLSupported()) {
alertUser("WebGL is not supported in this Browser!")
return false
}
let spector = new Spector()
spector.displayUI()
} else {
if (!("gpu" in navigator)) {
let message = "WebGPU is not supported in this Browser!"
alertUser(message)
return false
}
}
return true
}
const preventDefaultTouchActions = () => { const preventDefaultTouchActions = () => {
document.body.querySelectorAll("canvas").forEach(canvas => { document.body.querySelectorAll("canvas").forEach(canvas => {
@ -98,23 +18,28 @@ const preventDefaultTouchActions = () => {
export const startMapLibre = async (wasmPath: string | undefined, workerPath: string | undefined) => { export const startMapLibre = async (wasmPath: string | undefined, workerPath: string | undefined) => {
await checkWasmFeatures() await checkWasmFeatures()
if (!checkRequirements()) { let message = checkRequirements();
if (message) {
console.error(message)
alert(message)
return return
} }
if (WEBGL) {
let spector = new Spector()
spector.displayUI()
}
preventDefaultTouchActions(); preventDefaultTouchActions();
let MEMORY_PAGES = 16 * 1024 await initialize(wasmPath);
const memory = new WebAssembly.Memory({initial: 1024, maximum: MEMORY_PAGES, shared: true})
await init(wasmPath, memory)
const schedulerPtr = create_pool_scheduler(() => { const schedulerPtr = create_pool_scheduler(() => {
let worker = workerPath ? new PoolWorker(workerPath, { let CurrentWorker = MULTITHREADED ? MultithreadedPoolWorker : PoolWorker;
type: 'module'
}) : PoolWorker({name: "test"});
return worker; return workerPath ? new Worker(workerPath, {
type: 'module'
}) : CurrentWorker();
}) })
await run(schedulerPtr) await run(schedulerPtr)

28
web/lib/src/module.ts Normal file
View File

@ -0,0 +1,28 @@
import init from "./wasm/maplibre";
export const initialize = async (wasmPath: string) => {
if (MULTITHREADED) {
await initializeSharedModule(wasmPath)
} else {
// @ts-ignore
await init(wasmPath)
}
}
export const initializeExisting = async (module: string, memory?: string) => {
if (MULTITHREADED) {
// @ts-ignore
await init(module, memory)
} else {
// @ts-ignore
await init(module)
}
}
const initializeSharedModule = async (wasmPath) => {
let MEMORY_PAGES = 16 * 1024
const memory = new WebAssembly.Memory({initial: 1024, maximum: MEMORY_PAGES, shared: true})
// @ts-ignore
await init(wasmPath, memory)
}

View File

@ -0,0 +1,19 @@
import {worker_entry} from "./wasm/maplibre"
import {initializeExisting} from "./module";
onmessage = async message => {
const initialised = initializeExisting(message.data[0], message.data[1]).catch(err => {
// Propagate to main `onerror`:
setTimeout(() => {
throw err;
});
// Rethrow to keep promise rejected and prevent execution of further commands:
throw err;
});
self.onmessage = async message => {
// This will queue further commands up until the module is fully initialised:
await initialised;
await worker_entry(message.data);
};
}

View File

@ -1,18 +1,5 @@
import init, {worker_entry} from "./wasm/maplibre" import {initializeExisting} from "./module";
onmessage = async message => { onmessage = async message => {
const initialised = init(message.data[0], message.data[1]).catch(err => {
// Propagate to main `onerror`:
setTimeout(() => {
throw err;
});
// Rethrow to keep promise rejected and prevent execution of further commands:
throw err;
});
self.onmessage = async message => {
// This will queue further commands up until the module is fully initialised:
await initialised;
worker_entry(message.data);
};
} }

View File

@ -1,17 +0,0 @@
/*
import {registerRoute} from 'workbox-routing';
import {CacheFirst} from 'workbox-strategies';
import {CacheableResponsePlugin} from 'workbox-cacheable-response';
registerRoute(
({url}) => url.pathname.endsWith('pbf'),
new CacheFirst({
cacheName: 'pbf-cache',
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200],
})
]
})
);
*/