Set up wasm generation (#42)

This commit is contained in:
Kasper 2025-05-30 11:39:40 +02:00 committed by GitHub
parent 2d89355566
commit 2161f594bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 6568 additions and 3 deletions

35
.github/workflows/deploy.yml vendored Normal file
View File

@ -0,0 +1,35 @@
name: Deploy
on:
push:
branches:
- master
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
jobs:
web:
runs-on: ubuntu-latest
defaults:
run:
working-directory: web
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '22'
- name: Install dependencies
run: npm ci
- name: Build SvelteKit project
run: npm run build
- name: Deploy Project Artifacts to Vercel
run: npx vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}

35
.github/workflows/preview.yml vendored Normal file
View File

@ -0,0 +1,35 @@
name: Deploy
on:
push:
branches-ignore:
- master
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
jobs:
web:
runs-on: ubuntu-latest
defaults:
run:
working-directory: web
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '22'
- name: Install dependencies
run: npm ci
- name: Build SvelteKit project
run: npm run build
- name: Deploy Project Artifacts to Vercel
run: npx vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }}
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}

View File

@ -1,8 +1,10 @@
# Changelog # Changelog
## Next ## Next
- Switch to the fastnum crate's d128 - Create a [website interface](https://cpc.kasper.space)
- Fix exp function - Add wasm version
- Switch to the `fastnum` crate's d128
- Fix `exp` function
## 2.0.0 - 2025 May 30 ## 2.0.0 - 2025 May 30
- Remove the `degrees` keyword which referred to `celcius` by default - Remove the `degrees` keyword which referred to `celcius` by default

157
Cargo.lock generated
View File

@ -23,13 +23,39 @@ version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f781dba93de3a5ef6dc5b17c9958b208f6f3f021623b360fb605ea51ce443f10" checksum = "f781dba93de3a5ef6dc5b17c9958b208f6f3f021623b360fb605ea51ce443f10"
[[package]]
name = "bumpalo"
version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
"cfg-if",
"wasm-bindgen",
]
[[package]] [[package]]
name = "cpc" name = "cpc"
version = "2.0.0" version = "2.0.0"
dependencies = [ dependencies = [
"console_error_panic_hook",
"fastnum", "fastnum",
"js-sys",
"regex", "regex",
"unicode-segmentation", "unicode-segmentation",
"wasm-bindgen",
"web-time",
] ]
[[package]] [[package]]
@ -42,12 +68,52 @@ dependencies = [
"bnum", "bnum",
] ]
[[package]]
name = "js-sys"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.4" version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.11.1" version = "1.11.1"
@ -77,8 +143,99 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rustversion"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
[[package]]
name = "syn"
version = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]] [[package]]
name = "unicode-segmentation" name = "unicode-segmentation"
version = "1.12.0" version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]

View File

@ -18,9 +18,18 @@ categories = [
"value-formatting", "value-formatting",
] ]
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
fastnum = "0.2" fastnum = "0.2"
unicode-segmentation = "1.12" unicode-segmentation = "1.12"
web-time = "1.1.0"
[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.7"
wasm-bindgen = "0.2"
js-sys = "0.3"
[dev-dependencies] [dev-dependencies]
regex = "1.11" regex = "1.11"

View File

@ -11,6 +11,10 @@ It also lets you mix units, so for example `1 km - 1m` results in `999 Meter`.
[List of all supported units](https://docs.rs/cpc/latest/cpc/units/enum.Unit.html) [List of all supported units](https://docs.rs/cpc/latest/cpc/units/enum.Unit.html)
## ## [Web Interface](https://cpc.kasper.space)
Try it out at [cpc.kasper.space](https://cpc.kasper.space)
## CLI Installation ## CLI Installation
Install using `cargo`: Install using `cargo`:
``` ```

View File

@ -25,7 +25,7 @@
use crate::units::Unit; use crate::units::Unit;
use fastnum::{dec128 as d, D128}; use fastnum::{dec128 as d, D128};
use std::fmt::{self, Display}; use std::fmt::{self, Display};
use std::time::Instant; use web_time::Instant;
/// Turns an [`AstNode`](parser::AstNode) into a [`Number`] /// Turns an [`AstNode`](parser::AstNode) into a [`Number`]
pub mod evaluator; pub mod evaluator;
@ -284,6 +284,21 @@ pub fn eval(
} }
} }
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn wasm_eval(expression: &str) -> String {
console_error_panic_hook::set_once();
let result = eval(expression, true, false);
match result {
Ok(result) => result.to_string(),
Err(e) => format!("Error: {e}"),
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

23
web/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

38
web/README.md Normal file
View File

@ -0,0 +1,38 @@
# sv
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npx sv create
# create a new project in my-app
npx sv create my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

5970
web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

31
web/package.json Normal file
View File

@ -0,0 +1,31 @@
{
"name": "web",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"build-wasm": "npx wasm-pack build --target bundler",
"dev": "npm run build-wasm && vite dev",
"build": "npm run build-wasm && vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@sveltejs/adapter-vercel": "^5.7.2",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/vite": "^4.0.0",
"cpc": "file:../pkg",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.0.0",
"vercel": "^42.2.0",
"vite": "^6.2.6",
"vite-plugin-top-level-await": "^1.5.0",
"vite-plugin-wasm": "^3.4.1",
"wasm-pack": "^0.13.1"
}
}

1
web/src/app.css Normal file
View File

@ -0,0 +1 @@
@import 'tailwindcss';

13
web/src/app.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

19
web/src/app.html Normal file
View File

@ -0,0 +1,19 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/png" href="%sveltekit.assets%/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="%sveltekit.assets%/favicon.svg" />
<link rel="shortcut icon" href="%sveltekit.assets%/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="%sveltekit.assets%/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="cpc" />
<link rel="manifest" href="%sveltekit.assets%/site.webmanifest" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

39
web/src/lib/helpers.ts Normal file
View File

@ -0,0 +1,39 @@
type ShortcutOptions = {
shift?: boolean
alt?: boolean
cmd_or_ctrl?: boolean
}
const is_mac = navigator.userAgent.indexOf('Mac') !== -1
function check_modifiers(e: KeyboardEvent | MouseEvent, options: ShortcutOptions) {
const target = {
shift: options.shift || false,
alt: options.alt || false,
ctrl: (!is_mac && options.cmd_or_ctrl) || false,
meta: (is_mac && options.cmd_or_ctrl) || false,
}
const pressed = {
shift: !!e.shiftKey,
alt: !!e.altKey,
ctrl: !!e.ctrlKey,
meta: !!e.metaKey,
}
const ignore_ctrl = is_mac && e instanceof MouseEvent
return (
pressed.shift === target.shift &&
pressed.alt === target.alt &&
(pressed.ctrl === target.ctrl || ignore_ctrl) &&
pressed.meta === target.meta
)
}
export function check_shortcut(e: KeyboardEvent, key: string, options: ShortcutOptions = {}) {
if (e.key.toUpperCase() !== key.toUpperCase()) return false
return check_modifiers(e, options)
}
export function check_mouse_shortcut(e: MouseEvent, options: ShortcutOptions = {}) {
return check_modifiers(e, options)
}

View File

@ -0,0 +1,7 @@
<script lang="ts">
import "../app.css";
let { children } = $props();
</script>
{@render children()}

View File

@ -0,0 +1,2 @@
export const ssr = false;
export const prerender = true;

View File

@ -0,0 +1,99 @@
<script lang="ts">
import { check_shortcut } from "$lib/helpers";
import { wasm_eval } from "cpc";
import { flip } from "svelte/animate";
import { fly } from "svelte/transition";
let input = $state("");
let output = $derived.by(() => {
try {
if (input.trim().length === 0) {
return "";
}
return wasm_eval(input);
} catch (e) {
return "";
}
});
let saved_queries: { id: number; in: string; out: string }[] = $state([]);
</script>
<main class="w-full px-4 lg:px-8 text-base lg:text-lg">
<nav class="flex items-center justify-between py-4 lg:py-6">
<h1 class="text-3xl font-bold text-amber-600 dark:text-amber-400">
cpc
</h1>
<a
href="https://github.com/probablykasper/cpc"
aria-label="GitHub repository"
class="svelte-1ugh5mt"
>
<svg
height="24"
viewBox="-2 -2 28 28"
width="24"
xmlns="http://www.w3.org/2000/svg"
class="svelte-8lfi33 svelte-1ugh5mt"
><path
d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
class="svelte-8lfi33"
></path></svg
>
</a>
</nav>
<input
type="text"
class="border border-gray-500/50 w-full rounded-lg px-3 py-2 outline-none"
bind:value={input}
onkeydown={(e) => {
if (check_shortcut(e, "Enter")) {
const input = e.currentTarget.value;
let out;
try {
out = wasm_eval(e.currentTarget.value);
} catch (e) {
out = "";
}
console.log(out);
saved_queries.unshift({
id: saved_queries.length,
in: input,
out,
});
e.currentTarget.value = "";
output = "";
}
}}
placeholder="10km/h * 1 decade in light seconds"
/>
<div class="pt-1 leading-tight">
<div class="px-3 py-2">
{output}<span class="invisible select-none">x</span>
</div>
{#each saved_queries as query (query.id)}
<div
class="px-3 py-2"
in:fly={{ y: -10, duration: 150 }}
animate:flip={{ duration: 150 }}
>
<p class="opacity-65 text-base leading-tight">{query.in}</p>
<p>{query.out}</p>
</div>
{/each}
</div>
</main>
<style lang="postcss">
@reference "../app.css";
@media (prefers-color-scheme: dark) {
:global(body) {
@apply bg-black text-white;
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
web/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

3
web/static/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,21 @@
{
"name": "cpc",
"short_name": "cpc",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#faff00",
"background_color": "#000000",
"display": "standalone"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

9
web/svelte.config.js Normal file
View File

@ -0,0 +1,9 @@
import adapter from '@sveltejs/adapter-vercel';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
const config = {
preprocess: vitePreprocess(),
kit: { adapter: adapter() }
};
export default config;

19
web/tsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

14
web/vite.config.ts Normal file
View File

@ -0,0 +1,14 @@
import tailwindcss from '@tailwindcss/vite';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import wasm from 'vite-plugin-wasm';
import top_level_await from 'vite-plugin-top-level-await';
export default defineConfig({
plugins: [tailwindcss(), sveltekit(), wasm(), top_level_await()],
server: {
fs: {
allow: ['../pkg']
}
}
});