Skip to content

Commit

Permalink
feat: removed Vite WebAssembly plugins
Browse files Browse the repository at this point in the history
  • Loading branch information
elijah-potter committed Dec 17, 2024
1 parent 91fbadd commit 3312ad7
Show file tree
Hide file tree
Showing 16 changed files with 113 additions and 163 deletions.
4 changes: 2 additions & 2 deletions harper-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@ pub struct Suggestion {
#[derive(Debug, Serialize, Deserialize)]
#[wasm_bindgen]
pub enum SuggestionKind {
Replace,
Remove,
Replace = 0,
Remove = 1,
}

#[wasm_bindgen]
Expand Down
2 changes: 1 addition & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ build-wasm target:
build-harperjs:
#! /bin/bash
set -eo pipefail
just build-wasm bundler
just build-wasm web

cd "{{justfile_directory()}}/packages/harper.js"
yarn install -f
Expand Down
46 changes: 22 additions & 24 deletions packages/harper.js/package.json
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
{
"name": "harper.js",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"test": "vitest run"
},
"dependencies": {
"wasm": "link:../../harper-wasm/pkg"
},
"devDependencies": {
"@vitest/browser": "^2.1.8",
"playwright": "^1.49.1",
"typescript": "~5.6.2",
"vite": "^5.1.8",
"vite-plugin-dts": "^4.3.0",
"vite-plugin-top-level-await": "^1.4.4",
"vite-plugin-wasm": "^3.3.0",
"vitest": "^2.1.8"
},
"main": "dist/harper.js",
"types": "dist/harper.d.ts"
"name": "harper.js",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"test": "vitest run"
},
"dependencies": {
"wasm": "link:../../harper-wasm/pkg"
},
"devDependencies": {
"@vitest/browser": "^2.1.8",
"playwright": "^1.49.1",
"typescript": "~5.6.2",
"vite": "^5.1.8",
"vite-plugin-dts": "^4.3.0",
"vitest": "^2.1.8"
},
"main": "dist/harper.js",
"types": "dist/harper.d.ts"
}
4 changes: 3 additions & 1 deletion packages/harper.js/src/Linter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import type { Lint, Span, Suggestion } from 'wasm';

/** A interface for an object that can perform linting actions. */
export default interface Linter {
/** Complete any setup that is necessary before linting. This may include downloading and compiling the WebAssembly binary. */
/** Complete any setup that is necessary before linting. This may include downloading and compiling the WebAssembly binary.
* This setup will complete when needed regardless of whether you call this function.
* This function exists to allow you to do this work when it is of least impact to the user experiences (i.e. while you're loading something else). */
setup(): Promise<void>;
/** Lint the provided text. */
lint(text: string): Promise<Lint[]>;
Expand Down
11 changes: 6 additions & 5 deletions packages/harper.js/src/LocalLinter.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import type { Lint, Span, Suggestion } from 'wasm';
import Linter from './Linter';
import loadWasm from './loadWasm';

/** A Linter that runs in the current JavaScript context (meaning it is allowed to block the event loop). */
export default class LocalLinter implements Linter {
async setup(): Promise<void> {
const wasm = await import('wasm');
const wasm = await loadWasm();
wasm.setup();
wasm.lint('');
}

async lint(text: string): Promise<Lint[]> {
const wasm = await import('wasm');
const wasm = await loadWasm();
let lints = wasm.lint(text);

// We only want to show fixable errors.
Expand All @@ -20,17 +21,17 @@ export default class LocalLinter implements Linter {
}

async applySuggestion(text: string, suggestion: Suggestion, span: Span): Promise<string> {
const wasm = await import('wasm');
const wasm = await loadWasm();
return wasm.apply_suggestion(text, span, suggestion);
}

async isLikelyEnglish(text: string): Promise<boolean> {
const wasm = await import('wasm');
const wasm = await loadWasm();
return wasm.is_likely_english(text);
}

async isolateEnglish(text: string): Promise<string> {
const wasm = await import('wasm');
const wasm = await loadWasm();
return wasm.isolate_english(text);
}
}
22 changes: 11 additions & 11 deletions packages/harper.js/src/WorkerLinter/communication.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,46 @@ import { deserializeArg, serializeArg } from './communication';
import { Span } from 'wasm';
import LocalLinter from '../LocalLinter';

test('works with strings', () => {
test('works with strings', async () => {
const start = 'This is a string';

const end = deserializeArg(structuredClone(serializeArg(start)));
const end = await deserializeArg(structuredClone(await serializeArg(start)));

expect(end).toBe(start);
expect(typeof end).toBe(typeof start);
});

test('works with false booleans', () => {
test('works with false booleans', async () => {
const start = false;

const end = deserializeArg(structuredClone(serializeArg(start)));
const end = await deserializeArg(structuredClone(await serializeArg(start)));

expect(end).toBe(start);
expect(typeof end).toBe(typeof start);
});

test('works with true booleans', () => {
test('works with true booleans', async () => {
const start = true;

const end = deserializeArg(structuredClone(serializeArg(start)));
const end = await deserializeArg(structuredClone(await serializeArg(start)));

expect(end).toBe(start);
expect(typeof end).toBe(typeof start);
});

test('works with numbers', () => {
test('works with numbers', async () => {
const start = 123;

const end = deserializeArg(structuredClone(serializeArg(start)));
const end = await deserializeArg(structuredClone(await serializeArg(start)));

expect(end).toBe(start);
expect(typeof end).toBe(typeof start);
});

test('works with Spans', () => {
test('works with Spans', async () => {
const start = Span.new(123, 321);

const end = deserializeArg(structuredClone(serializeArg(start)));
const end = await deserializeArg(structuredClone(await serializeArg(start)));

expect(end.start).toBe(start.start);
expect(end.len()).toBe(start.len());
Expand All @@ -56,7 +56,7 @@ test('works with Lints', async () => {

expect(start).not.toBeNull();

const end = deserializeArg(structuredClone(serializeArg(start)));
const end = await deserializeArg(structuredClone(await serializeArg(start)));

expect(end.message()).toBe(start.message());
expect(end.lint_kind()).toBe(start.lint_kind());
Expand Down
22 changes: 13 additions & 9 deletions packages/harper.js/src/WorkerLinter/communication.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** This module aims to define the communication protocol between the main thread and the worker.
* Note that most of the complication here comes from the fact that we can't serialize function calls or referenced WebAssembly memory.*/

import { Lint, Span, Suggestion } from 'wasm';
import loadWasm from '../loadWasm';

export type Type =
| 'string'
Expand All @@ -19,16 +19,18 @@ export type RequestArg = {
type: Type;
};

export function serialize(req: DeserializedRequest): SerializedRequest {
export async function serialize(req: DeserializedRequest): Promise<SerializedRequest> {
return {
procName: req.procName,
args: req.args.map(serializeArg)
args: await Promise.all(req.args.map(serializeArg))
};
}

export function serializeArg(arg: any): RequestArg {
export async function serializeArg(arg: any): Promise<RequestArg> {
const { Lint, Span, Suggestion } = await loadWasm();

if (Array.isArray(arg)) {
return { json: JSON.stringify(arg.map(serializeArg)), type: 'Array' };
return { json: JSON.stringify(await Promise.all(arg.map(serializeArg))), type: 'Array' };
}

switch (typeof arg) {
Expand Down Expand Up @@ -62,7 +64,9 @@ export function serializeArg(arg: any): RequestArg {
throw new Error('Unhandled case');
}

export function deserializeArg(requestArg: RequestArg): any {
export async function deserializeArg(requestArg: RequestArg): Promise<any> {
const { Lint, Span, Suggestion } = await loadWasm();

switch (requestArg.type) {
case 'undefined':
return undefined;
Expand All @@ -77,7 +81,7 @@ export function deserializeArg(requestArg: RequestArg): any {
case 'Span':
return Span.from_json(requestArg.json);
case 'Array':
return JSON.parse(requestArg.json).map(deserializeArg);
return await Promise.all(JSON.parse(requestArg.json).map(deserializeArg));
default:
throw new Error(`Unhandled case: ${requestArg.type}`);
}
Expand All @@ -99,9 +103,9 @@ export type DeserializedRequest = {
args: any[];
};

export function deserialize(request: SerializedRequest): DeserializedRequest {
export async function deserialize(request: SerializedRequest): Promise<DeserializedRequest> {
return {
procName: request.procName,
args: request.args.map(deserializeArg)
args: await Promise.all(request.args.map(deserializeArg))
};
}
18 changes: 11 additions & 7 deletions packages/harper.js/src/WorkerLinter/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DeserializedRequest, deserializeArg, serialize } from './communication';
import { Lint, Suggestion, Span } from 'wasm';
import type { Lint, Suggestion, Span } from 'wasm';
import Linter from '../Linter';
import Worker from './worker.js?worker';

Expand All @@ -13,7 +13,8 @@ type RequestItem = {
/** A Linter that spins up a dedicated web worker to do processing on a separate thread.
* Main benefit: this Linter will not block the event loop for large documents.
*
* NOTE: This class will not work properly in Node. In that case, just use `LocalLinter`. */
* NOTE: This class will not work properly in Node. In that case, just use `LocalLinter`.
* Also requires top-level await to work. */
export default class WorkerLinter implements Linter {
private worker;
private requestQueue: RequestItem[];
Expand All @@ -33,10 +34,13 @@ export default class WorkerLinter implements Linter {
private setupMainEventListeners() {
this.worker.onmessage = (e: MessageEvent) => {
const { resolve } = this.requestQueue.shift()!;
resolve(deserializeArg(e.data));
this.working = false;
deserializeArg(e.data).then((v) => {
resolve(v);

this.submitRemainingRequests();
this.working = false;

this.submitRemainingRequests();
});
};

this.worker.onmessageerror = (e: MessageEvent) => {
Expand Down Expand Up @@ -81,7 +85,7 @@ export default class WorkerLinter implements Linter {
return promise;
}

private submitRemainingRequests() {
private async submitRemainingRequests() {
if (this.working) {
return;
}
Expand All @@ -91,7 +95,7 @@ export default class WorkerLinter implements Linter {
if (this.requestQueue.length > 0) {
const { request } = this.requestQueue[0];

this.worker.postMessage(serialize(request));
this.worker.postMessage(await serialize(request));
} else {
this.working = false;
}
Expand Down
12 changes: 9 additions & 3 deletions packages/harper.js/src/WorkerLinter/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@ import { deserialize, serializeArg } from './communication';

const linter = new LocalLinter();

self.onmessage = function (e) {
const { procName, args } = deserialize(e.data);
/** @param {SerializedRequest} v */
async function processRequest(v) {
const { procName, args } = await deserialize(v);

let res = await linter[procName](...args);
postMessage(await serializeArg(res));
}

linter[procName](...args).then((res) => postMessage(serializeArg(res)));
self.onmessage = function (e) {
processRequest(e.data);
};

// Notify the main thread that we are ready
Expand Down
9 changes: 9 additions & 0 deletions packages/harper.js/src/loadWasm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import wasmUrl from 'wasm/harper_wasm_bg.wasm?url';

/** Load the WebAssembly manually and dynamically, making sure to setup infrastructure. */
export default async function loadWasm() {
const wasm = await import('wasm');
await wasm.default(wasmUrl);

return wasm;
}
8 changes: 8 additions & 0 deletions packages/harper.js/src/main.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { expect, test } from 'vitest';
import { SuggestionKind as WasmSuggestionKind } from 'wasm';
import { SuggestionKind } from './main';

test('Wasm and JS SuggestionKinds agree', async () => {
expect(SuggestionKind.Remove).toBe(WasmSuggestionKind.Remove);
expect(SuggestionKind.Replace).toBe(WasmSuggestionKind.Replace);
});
9 changes: 7 additions & 2 deletions packages/harper.js/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,10 @@ import Linter from './Linter';
import LocalLinter from './LocalLinter';
import WorkerLinter from './WorkerLinter';

export { Lint, Span, Suggestion, LocalLinter, WorkerLinter };
export type { Linter };
export { LocalLinter, WorkerLinter };
export type { Linter, Lint, Span, Suggestion };

export enum SuggestionKind {
Replace = 0,
Remove = 1
}
13 changes: 8 additions & 5 deletions packages/harper.js/vite.config.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
import { resolve } from 'path';
import dts from 'vite-plugin-dts';
import topLevelAwait from 'vite-plugin-top-level-await';
import wasm from 'vite-plugin-wasm';
import { defineConfig } from 'vite';

export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/main.ts'),
fileName: `harper.js`,
fileName: `harper`,
name: 'harper',
formats: ['es']
}
},
base: './',
plugins: [wasm(), topLevelAwait(), dts({ rollupTypes: true, tsconfigPath: './tsconfig.json' })],
plugins: [dts({ rollupTypes: true, tsconfigPath: './tsconfig.json' })],
worker: {
plugins: [wasm(), topLevelAwait()],
plugins: [],
format: 'es'
},
server: {
fs: {
allow: ['../../harper-wasm/pkg']
}
},
test: {
browser: {
provider: 'playwright',
Expand Down
4 changes: 2 additions & 2 deletions packages/web/src/lib/Editor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import demo from '../../../../demo.md?raw';
import Underlines from '$lib/Underlines.svelte';
import { Button } from 'flowbite-svelte';
import { WorkerLinter } from 'harper.js';
import { Lint, SuggestionKind } from 'wasm';
import { WorkerLinter, SuggestionKind } from 'harper.js';
import type { Lint } from 'harper.js';
import CheckMark from '$lib/CheckMark.svelte';
import { fly } from 'svelte/transition';
Expand Down
Loading

0 comments on commit 3312ad7

Please sign in to comment.