From 43e74ebdf384d30cc73e7b6bfc65240f5ded9347 Mon Sep 17 00:00:00 2001 From: Elijah Potter Date: Fri, 28 Nov 2025 12:36:04 -0700 Subject: [PATCH] refactor(core): remove implementation details from public API (#2256) --- harper-brill/src/lib.rs | 12 +- harper-cli/src/annotate_tokens.rs | 2 +- harper-cli/src/main.rs | 3 +- harper-core/src/lib.rs | 10 +- harper-core/src/vec_ext.rs | 1 + harper-core/tests/pos_tags.rs | 6 +- harper-ls/src/dictionary_io.rs | 2 +- .../src/chunker/brill_chunker/mod.rs | 5 + harper-pos-utils/src/chunker/burn_chunker.rs | 3 + harper-pos-utils/src/chunker/mod.rs | 1 + harper-pos-utils/src/chunker/np_extraction.rs | 2 + .../src/tagger/brill_tagger/mod.rs | 6 + harper-pos-utils/src/tagger/mod.rs | 1 + harper-wasm/src/lib.rs | 3 +- .../chrome-plugin/src/background/index.ts | 2 +- packages/components/package.json | 2 +- packages/harper.js/package.json | 5 +- packages/harper.js/renderPage.js | 4 +- packages/harper.js/src/Linter.ts | 10 +- packages/harper.js/src/LocalLinter.ts | 16 +- packages/harper.js/src/Serializer.test.ts | 84 ++++++++ packages/harper.js/src/Serializer.ts | 152 +++++++++++++ packages/harper.js/src/Summary.ts | 3 +- packages/harper.js/src/WorkerLinter/index.ts | 10 +- packages/harper.js/src/WorkerLinter/worker.ts | 10 +- packages/harper.js/src/binary.test.ts | 63 ------ packages/harper.js/src/binary.ts | 204 ++++-------------- packages/harper.js/src/main.ts | 7 +- pnpm-lock.yaml | 191 +++++++++------- pnpm-workspace.yaml | 16 +- 30 files changed, 473 insertions(+), 363 deletions(-) create mode 100644 packages/harper.js/src/Serializer.test.ts create mode 100644 packages/harper.js/src/Serializer.ts delete mode 100644 packages/harper.js/src/binary.test.ts diff --git a/harper-brill/src/lib.rs b/harper-brill/src/lib.rs index 8c343337..bed130fb 100644 --- a/harper-brill/src/lib.rs +++ b/harper-brill/src/lib.rs @@ -1,10 +1,11 @@ -use harper_pos_utils::{BurnChunkerCpu, CachedChunker}; use lazy_static::lazy_static; use std::num::NonZero; use std::rc::Rc; use std::sync::Arc; -pub use harper_pos_utils::{BrillChunker, BrillTagger, Chunker, FreqDict, Tagger, UPOS}; +pub use harper_pos_utils::{ + BrillChunker, BrillTagger, BurnChunkerCpu, CachedChunker, Chunker, FreqDict, Tagger, UPOS, +}; const BRILL_TAGGER_SOURCE: &str = include_str!("../trained_tagger_model.json"); @@ -16,6 +17,8 @@ fn uncached_brill_tagger() -> BrillTagger { serde_json::from_str(BRILL_TAGGER_SOURCE).unwrap() } +/// Get a copy of a shared, lazily-initialized [`BrillTagger`]. There will be only one instance +/// per-process. pub fn brill_tagger() -> Arc> { (*BRILL_TAGGER).clone() } @@ -30,6 +33,8 @@ fn uncached_brill_chunker() -> BrillChunker { serde_json::from_str(BRILL_CHUNKER_SOURCE).unwrap() } +/// Get a copy of a shared, lazily-initialized [`BrillChunker`]. There will be only one instance +/// per-process. pub fn brill_chunker() -> Arc { (*BRILL_CHUNKER).clone() } @@ -48,6 +53,9 @@ fn uncached_burn_chunker() -> CachedChunker { ) } +/// Get a copy of a shared, lazily-initialized [`BurnChunkerCpu`]. There will be only one instance +/// per-process. Since neural net inference is extremely expensive, this chunker is memoized as +/// well. pub fn burn_chunker() -> Rc> { (BURN_CHUNKER).with(|c| c.clone()) } diff --git a/harper-cli/src/annotate_tokens.rs b/harper-cli/src/annotate_tokens.rs index e6b6ebd7..3a738611 100644 --- a/harper-cli/src/annotate_tokens.rs +++ b/harper-cli/src/annotate_tokens.rs @@ -26,7 +26,7 @@ impl Annotation { /// Gets an iterator of annotation `Label` from the given document. /// - /// This is similar to [`self::iter_from_document()`], but this additionally converts + /// This is similar to [`Self::iter_from_document`], but this additionally converts /// the [`Annotation`] into [`ariadne::Label`] for convenience. pub(super) fn iter_labels_from_document<'inpt_id>( annotation_type: AnnotationType, diff --git a/harper-cli/src/main.rs b/harper-cli/src/main.rs index f86debe3..ee945b64 100644 --- a/harper-cli/src/main.rs +++ b/harper-cli/src/main.rs @@ -17,8 +17,7 @@ use harper_comments::CommentParser; use harper_core::linting::LintGroup; use harper_core::parsers::{Markdown, MarkdownOptions, OrgMode, PlainEnglish}; use harper_core::{ - CharStringExt, Dialect, DictWordMetadata, Document, Span, TokenKind, TokenStringExt, - dict_word_metadata_orthography::OrthFlags, + CharStringExt, Dialect, DictWordMetadata, Document, OrthFlags, Span, TokenKind, TokenStringExt, }; use harper_ink::InkParser; use harper_literate_haskell::LiterateHaskellParser; diff --git a/harper-core/src/lib.rs b/harper-core/src/lib.rs index bf5c7a66..b08bbb78 100644 --- a/harper-core/src/lib.rs +++ b/harper-core/src/lib.rs @@ -4,8 +4,8 @@ mod char_ext; mod char_string; mod currency; -pub mod dict_word_metadata; -pub mod dict_word_metadata_orthography; +mod dict_word_metadata; +mod dict_word_metadata_orthography; mod document; mod edit_distance; pub mod expr; @@ -35,8 +35,8 @@ use std::collections::{BTreeMap, VecDeque}; pub use char_string::{CharString, CharStringExt}; pub use currency::Currency; pub use dict_word_metadata::{ - AdverbData, ConjunctionData, Degree, DeterminerData, Dialect, DictWordMetadata, NounData, - PronounData, VerbData, VerbForm, + AdverbData, ConjunctionData, Degree, DeterminerData, Dialect, DialectFlags, DictWordMetadata, + NounData, PronounData, VerbData, VerbForm, VerbFormFlags, }; pub use dict_word_metadata_orthography::{OrthFlags, Orthography}; pub use document::Document; @@ -54,7 +54,7 @@ pub use token_kind::TokenKind; pub use token_string_ext::TokenStringExt; pub use vec_ext::VecExt; -/// Return harper-core version +/// Return `harper-core` version pub fn core_version() -> &'static str { env!("CARGO_PKG_VERSION") } diff --git a/harper-core/src/vec_ext.rs b/harper-core/src/vec_ext.rs index 930913a4..2c1bcf9d 100644 --- a/harper-core/src/vec_ext.rs +++ b/harper-core/src/vec_ext.rs @@ -1,5 +1,6 @@ use std::collections::VecDeque; +/// Extensions on top of [`Vec`] that make certain common operations easier. pub trait VecExt { /// Removes a list of indices from a Vector. /// Assumes that the provided indices are already in sorted order. diff --git a/harper-core/tests/pos_tags.rs b/harper-core/tests/pos_tags.rs index de582702..05fccecc 100644 --- a/harper-core/tests/pos_tags.rs +++ b/harper-core/tests/pos_tags.rs @@ -80,10 +80,10 @@ //! - All other token kinds are denoted by their variant name. use std::borrow::Cow; -use harper_core::dict_word_metadata::VerbFormFlags; -use harper_core::dict_word_metadata_orthography::OrthFlags; use harper_core::spell::FstDictionary; -use harper_core::{Degree, Dialect, DictWordMetadata, Document, TokenKind}; +use harper_core::{ + Degree, Dialect, DictWordMetadata, Document, OrthFlags, TokenKind, VerbFormFlags, +}; mod snapshot; diff --git a/harper-ls/src/dictionary_io.rs b/harper-ls/src/dictionary_io.rs index 6b519a4c..f0a22223 100644 --- a/harper-ls/src/dictionary_io.rs +++ b/harper-ls/src/dictionary_io.rs @@ -1,4 +1,4 @@ -use harper_core::dict_word_metadata::DialectFlags; +use harper_core::DialectFlags; use itertools::Itertools; use std::path::Path; diff --git a/harper-pos-utils/src/chunker/brill_chunker/mod.rs b/harper-pos-utils/src/chunker/brill_chunker/mod.rs index c2046cd2..6394aa4d 100644 --- a/harper-pos-utils/src/chunker/brill_chunker/mod.rs +++ b/harper-pos-utils/src/chunker/brill_chunker/mod.rs @@ -13,6 +13,11 @@ use crate::{ use patch::Patch; use serde::{Deserialize, Serialize}; +/// A [`Chunker`] implementation based on the work by Eric Brill. +/// +/// Additional reading: +/// +/// - [Continuations on Transformation-based Learning](https://elijahpotter.dev/articles/more-transformation-based-learning) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BrillChunker { base: UPOSFreqDict, diff --git a/harper-pos-utils/src/chunker/burn_chunker.rs b/harper-pos-utils/src/chunker/burn_chunker.rs index 591adbf5..c17599e8 100644 --- a/harper-pos-utils/src/chunker/burn_chunker.rs +++ b/harper-pos-utils/src/chunker/burn_chunker.rs @@ -73,6 +73,9 @@ impl NpModel { } } +/// A [`Chunker`] that uses a BiLSTM and the Burn machine learning framework. +/// +/// Additional details in this [talk](https://elijahpotter.dev/articles/i-spoke-at-wordcamp-u.s.-in-2025) pub struct BurnChunker { vocab: HashMap, model: NpModel, diff --git a/harper-pos-utils/src/chunker/mod.rs b/harper-pos-utils/src/chunker/mod.rs index 1c83874b..ccdfaf76 100644 --- a/harper-pos-utils/src/chunker/mod.rs +++ b/harper-pos-utils/src/chunker/mod.rs @@ -13,6 +13,7 @@ pub use cached_chunker::CachedChunker; pub use upos_freq_dict::UPOSFreqDict; /// An implementer of this trait is capable of identifying the noun phrases in a provided sentence. +/// [See here](https://en.wikipedia.org/wiki/Shallow_parsing) for more details on what this is and how it can work. pub trait Chunker { /// Iterate over the sentence, identifying the noun phrases contained within. /// A token marked `true` is a component of a noun phrase. diff --git a/harper-pos-utils/src/chunker/np_extraction.rs b/harper-pos-utils/src/chunker/np_extraction.rs index caa62504..b734ff8e 100644 --- a/harper-pos-utils/src/chunker/np_extraction.rs +++ b/harper-pos-utils/src/chunker/np_extraction.rs @@ -1,3 +1,5 @@ +//! Methods for extracting nominal phrases from datasets. + use std::collections::VecDeque; use hashbrown::HashSet; diff --git a/harper-pos-utils/src/tagger/brill_tagger/mod.rs b/harper-pos-utils/src/tagger/brill_tagger/mod.rs index 93d8a37c..491f27fc 100644 --- a/harper-pos-utils/src/tagger/brill_tagger/mod.rs +++ b/harper-pos-utils/src/tagger/brill_tagger/mod.rs @@ -13,6 +13,12 @@ use super::error_counter::{ErrorCounter, ErrorKind}; use crate::{Tagger, UPOS}; +/// A [`Tagger`] implementation based on the work by Eric Brill. +/// +/// Additional reading: +/// +/// - [Brill tagger](https://en.wikipedia.org/wiki/Brill_tagger) +/// - [Transformation-based Learning for POS Tagging](https://elijahpotter.dev/articles/transformation-based-learning) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BrillTagger where diff --git a/harper-pos-utils/src/tagger/mod.rs b/harper-pos-utils/src/tagger/mod.rs index 57012784..0e03dc31 100644 --- a/harper-pos-utils/src/tagger/mod.rs +++ b/harper-pos-utils/src/tagger/mod.rs @@ -11,6 +11,7 @@ pub use freq_dict::FreqDict; pub use freq_dict_builder::FreqDictBuilder; /// An implementer of this trait is capable of assigned Part-of-Speech tags to a provided sentence. +/// This is widely useful for various applications. [See here.](https://en.wikipedia.org/wiki/Part-of-speech_tagging) pub trait Tagger { fn tag_sentence(&self, sentence: &[String]) -> Vec>; } diff --git a/harper-wasm/src/lib.rs b/harper-wasm/src/lib.rs index bdb8ecc3..a630695c 100644 --- a/harper-wasm/src/lib.rs +++ b/harper-wasm/src/lib.rs @@ -4,7 +4,7 @@ use std::convert::Into; use std::io::Cursor; use std::sync::Arc; -use harper_core::dict_word_metadata::DialectFlags; +use harper_core::DialectFlags; use harper_core::language_detection::is_doc_likely_english; use harper_core::linting::{LintGroup, Linter as _}; use harper_core::parsers::{IsolateEnglish, Markdown, Parser, PlainEnglish}; @@ -64,6 +64,7 @@ impl Language { } } +/// Specifies an English Dialect, often used for linting. #[wasm_bindgen] #[derive(Serialize, Deserialize, Debug, Clone, Copy)] pub enum Dialect { diff --git a/packages/chrome-plugin/src/background/index.ts b/packages/chrome-plugin/src/background/index.ts index 1e7792da..087f69ce 100644 --- a/packages/chrome-plugin/src/background/index.ts +++ b/packages/chrome-plugin/src/background/index.ts @@ -367,7 +367,7 @@ async function setActivationKey(key: ActivationKey) { function initializeLinter(dialect: Dialect) { linter = new LocalLinter({ - binary: new BinaryModule(chrome.runtime.getURL('./wasm/harper_wasm_bg.wasm')), + binary: BinaryModule.create(chrome.runtime.getURL('./wasm/harper_wasm_bg.wasm')), dialect, }); diff --git a/packages/components/package.json b/packages/components/package.json index 841ee010..6124a3e9 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -49,7 +49,7 @@ "svelte": "^5.41.0", "svelte-check": "^4.3.3", "tailwindcss": "^4.1.14", - "typescript": "^5.9.3", + "typescript": "catalog:", "vite": "^7.1.10" }, "keywords": [ diff --git a/packages/harper.js/package.json b/packages/harper.js/package.json index a8039994..44e7b0f9 100644 --- a/packages/harper.js/package.json +++ b/packages/harper.js/package.json @@ -23,9 +23,10 @@ "api:documenter": "api-documenter markdown -i temp" }, "devDependencies": { - "@microsoft/api-documenter": "^7.26.10", - "@microsoft/api-extractor": "^7.50.1", + "@microsoft/api-documenter": "^7.28.1", + "@microsoft/api-extractor": "^7.55.1", "@vitest/browser": "^3.0.6", + "@vitest/ui": "3.0.8", "harper-wasm": "workspace:*", "marked": "^16.4.1", "p-lazy": "^5.0.0", diff --git a/packages/harper.js/renderPage.js b/packages/harper.js/renderPage.js index 64d0d75b..c613be95 100644 --- a/packages/harper.js/renderPage.js +++ b/packages/harper.js/renderPage.js @@ -13,7 +13,7 @@ renderer.link = ({ href, title, text }) => { href = `${href.slice(0, href.length - 3)}.html`; } const titleAttr = title ? ` title="${title}"` : ''; - return `${text}`; + return `${text.replaceAll('\\_', '_')}`; }; const markdown = fs.readFileSync(input, 'utf8'); @@ -30,7 +30,7 @@ const html = ` href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css" > - + ${body} diff --git a/packages/harper.js/src/Linter.ts b/packages/harper.js/src/Linter.ts index fa333d94..3d657f6f 100644 --- a/packages/harper.js/src/Linter.ts +++ b/packages/harper.js/src/Linter.ts @@ -53,13 +53,16 @@ export default interface Linter { /** Get the linting rule descriptions as an object, formatted in Markdown. */ getLintDescriptions(): Promise>; - /** Get the linting rule descriptions as a JSON map, formatted in HTML. */ + /** Get the linting rule descriptions as a JSON map, formatted in HTML. + * Wraps the function on the BinaryModule by the same name. */ getLintDescriptionsHTMLAsJSON(): Promise; - /** Get the linting rule descriptions as an object, formatted in HTML */ + /** Get the linting rule descriptions as an object, formatted in HTML. + * Wraps the function on the BinaryModule by the same name. */ getLintDescriptionsHTML(): Promise>; - /** Convert a string to Chicago-style title case. */ + /** Convert a string to Chicago-style title case. + Wraps the function on the BinaryModule by the same name. */ toTitleCase(text: string): Promise; /** Ignore future instances of a lint from a previous linting run in future invocations. */ @@ -110,6 +113,7 @@ export default interface Linter { importStatsFile(statsFile: string): Promise; } +/** The properties and information needed to construct a Linter. */ export interface LinterInit { /** The module or path to the WebAssembly binary. */ binary: BinaryModule; diff --git a/packages/harper.js/src/LocalLinter.ts b/packages/harper.js/src/LocalLinter.ts index 0803412d..c2356732 100644 --- a/packages/harper.js/src/LocalLinter.ts +++ b/packages/harper.js/src/LocalLinter.ts @@ -1,18 +1,20 @@ import type { Dialect, Lint, Suggestion, Linter as WasmLinter } from 'harper-wasm'; import { Language } from 'harper-wasm'; import LazyPromise from 'p-lazy'; -import type { BinaryModule } from './binary'; +import type { SuperBinaryModule } from './binary'; import type Linter from './Linter'; import type { LinterInit } from './Linter'; import type { LintConfig, LintOptions } from './main'; -/** A Linter that runs in the current JavaScript context (meaning it is allowed to block the event loop). */ +/** A Linter that runs in the current JavaScript context (meaning it is allowed to block the event loop). + * See the interface definition for more details. */ export default class LocalLinter implements Linter { - binary: BinaryModule; + binary: SuperBinaryModule; private inner: Promise; constructor(init: LinterInit) { - this.binary = init.binary; + this.binary = init.binary as SuperBinaryModule; + this.binary.setup(); this.inner = this.createInner(init.dialect); } @@ -73,11 +75,11 @@ export default class LocalLinter implements Linter { } async getDefaultLintConfigAsJSON(): Promise { - return this.binary.getDefaultLintConfigAsJSON(); + return await this.binary.getDefaultLintConfigAsJSON(); } async getDefaultLintConfig(): Promise { - return this.binary.getDefaultLintConfig(); + return await this.binary.getDefaultLintConfig(); } async setLintConfig(config: LintConfig): Promise { @@ -96,7 +98,7 @@ export default class LocalLinter implements Linter { } async toTitleCase(text: string): Promise { - return this.binary.toTitleCase(text); + return await this.binary.toTitleCase(text); } async getLintDescriptions(): Promise> { diff --git a/packages/harper.js/src/Serializer.test.ts b/packages/harper.js/src/Serializer.test.ts new file mode 100644 index 00000000..8dd64357 --- /dev/null +++ b/packages/harper.js/src/Serializer.test.ts @@ -0,0 +1,84 @@ +import { Span } from 'harper-wasm'; +import { beforeEach, describe, expect, test } from 'vitest'; +import { binary } from './binary'; +import LocalLinter from './LocalLinter'; +import Serializer from './Serializer'; + +describe('Serializer', () => { + let serializer = new Serializer(binary); + + beforeEach(() => { + serializer = new Serializer(binary); + }); + + test('works with strings', async () => { + const start = 'This is a string'; + + const end = await serializer.deserializeArg( + structuredClone(await serializer.serializeArg(start)), + ); + + expect(end).toBe(start); + expect(typeof end).toBe(typeof start); + }); + + test('works with false booleans', async () => { + const start = false; + + const end = await serializer.deserializeArg( + structuredClone(await serializer.serializeArg(start)), + ); + + expect(end).toBe(start); + expect(typeof end).toBe(typeof start); + }); + + test('works with true booleans', async () => { + const start = true; + + const end = await serializer.deserializeArg( + structuredClone(await serializer.serializeArg(start)), + ); + + expect(end).toBe(start); + expect(typeof end).toBe(typeof start); + }); + + test('works with numbers', async () => { + const start = 123; + + const end = await serializer.deserializeArg( + structuredClone(await serializer.serializeArg(start)), + ); + + expect(end).toBe(start); + expect(typeof end).toBe(typeof start); + }); + + test('works with Spans', async () => { + const start = Span.new(123, 321); + + const end = await serializer.deserializeArg( + structuredClone(await serializer.serializeArg(start)), + ); + + expect(end.start).toBe(start.start); + expect(end.len()).toBe(start.len()); + expect(typeof end).toBe(typeof start); + }); + + test('works with Lints', async () => { + const linter = new LocalLinter({ binary }); + const lints = await linter.lint('This is an test.'); + const start = lints[0]; + + expect(start).not.toBeNull(); + + const end = await serializer.deserializeArg( + structuredClone(await serializer.serializeArg(start)), + ); + + expect(end.message()).toBe(start.message()); + expect(end.lint_kind()).toBe(start.lint_kind()); + }); +}); diff --git a/packages/harper.js/src/Serializer.ts b/packages/harper.js/src/Serializer.ts new file mode 100644 index 00000000..edd5d8e3 --- /dev/null +++ b/packages/harper.js/src/Serializer.ts @@ -0,0 +1,152 @@ +import type { BinaryModule, SuperBinaryModule } from './binary'; +import { assert } from './utils'; + +export type SerializableTypes = + | 'string' + | 'number' + | 'boolean' + | 'object' + | 'Suggestion' + | 'Lint' + | 'Span' + | 'Array' + | 'undefined' + | 'bigint'; + +/** Serializable argument to a procedure to be run on the web worker. */ +export interface RequestArg { + json: string; + type: SerializableTypes; +} + +/** An object that is sent to the web worker to request work to be done. */ +export interface SerializedRequest { + /** The procedure to be executed. */ + procName: string; + /** The arguments to the procedure */ + args: RequestArg[]; +} + +/** An object that is received by the web worker to request work to be done. */ +export interface DeserializedRequest { + /** The procedure to be executed. */ + procName: string; + /** The arguments to the procedure */ + args: any[]; +} + +export function isSerializedRequest(v: unknown): v is SerializedRequest { + return typeof v === 'object' && v !== null && 'procName' in v && 'args' in v; +} + +/** An internal class that helps the `WorkerLinter` shuffle data across a messaging channel. */ +export default class Serializer { + binary: SuperBinaryModule; + + constructor(binary: BinaryModule) { + this.binary = binary as SuperBinaryModule; + this.binary.setup(); + } + + async serializeArg(arg: any): Promise { + const { Lint, Span, Suggestion } = await this.binary.getBinaryModule(); + + if (Array.isArray(arg)) { + return { + json: JSON.stringify(await Promise.all(arg.map((a) => this.serializeArg(a)))), + type: 'Array', + }; + } + + const argType = typeof arg; + switch (argType) { + case 'string': + case 'number': + case 'boolean': + case 'undefined': + return { json: JSON.stringify(arg), type: argType }; + case 'bigint': + return { json: arg.toString(), type: argType }; + } + + if (arg.to_json !== undefined) { + const json = arg.to_json(); + let type: SerializableTypes | undefined; + + if (arg instanceof Lint) { + type = 'Lint'; + } else if (arg instanceof Suggestion) { + type = 'Suggestion'; + } else if (arg instanceof Span) { + type = 'Span'; + } + + if (type === undefined) { + throw new Error('Unhandled case: type undefined'); + } + + return { json, type }; + } + + if (argType == 'object') { + return { + json: JSON.stringify( + await Promise.all( + Object.entries(arg).map(([key, value]) => this.serializeArg([key, value])), + ), + ), + type: 'object', + }; + } + + throw new Error(`Unhandled case: ${arg}`); + } + + async serialize(req: DeserializedRequest): Promise { + return { + procName: req.procName, + args: await Promise.all(req.args.map((arg) => this.serializeArg(arg))), + }; + } + + async deserializeArg(requestArg: RequestArg): Promise { + const { Lint, Span, Suggestion } = await this.binary.getBinaryModule(); + + switch (requestArg.type) { + case 'bigint': + return BigInt(requestArg.json); + case 'undefined': + return undefined; + case 'boolean': + case 'number': + case 'string': + return JSON.parse(requestArg.json); + case 'Suggestion': + return Suggestion.from_json(requestArg.json); + case 'Lint': + return Lint.from_json(requestArg.json); + case 'Span': + return Span.from_json(requestArg.json); + case 'Array': { + const parsed = JSON.parse(requestArg.json); + assert(Array.isArray(parsed)); + return await Promise.all(parsed.map((arg) => this.deserializeArg(arg))); + } + case 'object': { + const parsed = JSON.parse(requestArg.json); + return Object.fromEntries( + await Promise.all(parsed.map((val: any) => this.deserializeArg(val))), + ); + } + default: + throw new Error(`Unhandled case: ${requestArg.type}`); + } + } + + async deserialize(request: SerializedRequest): Promise { + return { + procName: request.procName, + args: await Promise.all(request.args.map((arg) => this.deserializeArg(arg))), + }; + } +} diff --git a/packages/harper.js/src/Summary.ts b/packages/harper.js/src/Summary.ts index b32f450c..a37d9d32 100644 --- a/packages/harper.js/src/Summary.ts +++ b/packages/harper.js/src/Summary.ts @@ -1,5 +1,6 @@ /** - * Represents the summary of linting results. + * Represents the summary of linting results and history. + * Useful to show linting statistics or insights to the user. */ export default interface Summary { /** diff --git a/packages/harper.js/src/WorkerLinter/index.ts b/packages/harper.js/src/WorkerLinter/index.ts index f248ad3d..885c06b0 100644 --- a/packages/harper.js/src/WorkerLinter/index.ts +++ b/packages/harper.js/src/WorkerLinter/index.ts @@ -1,8 +1,10 @@ import type { Dialect, Lint, Suggestion } from 'harper-wasm'; -import type { BinaryModule, DeserializedRequest } from '../binary'; +import type { BinaryModule } from '../binary'; import type Linter from '../Linter'; import type { LinterInit } from '../Linter'; import type { LintConfig, LintOptions } from '../main'; +import type { DeserializedRequest } from '../Serializer'; +import Serializer from '../Serializer'; import Worker from './worker.ts?worker&inline'; /** The data necessary to complete a request once the worker has responded. */ @@ -18,6 +20,7 @@ export interface RequestItem { * NOTE: This class will not work properly in Node. In that case, just use `LocalLinter`. */ export default class WorkerLinter implements Linter { private binary: BinaryModule; + private serializer: Serializer; private dialect?: Dialect; private worker: Worker; private requestQueue: RequestItem[]; @@ -25,6 +28,7 @@ export default class WorkerLinter implements Linter { constructor(init: LinterInit) { this.binary = init.binary; + this.serializer = new Serializer(this.binary); this.dialect = init.dialect; this.worker = new Worker(); this.requestQueue = []; @@ -43,7 +47,7 @@ export default class WorkerLinter implements Linter { private setupMainEventListeners() { this.worker.onmessage = (e: MessageEvent) => { const { resolve } = this.requestQueue.shift()!; - this.binary.deserializeArg(e.data).then((v) => { + this.serializer.deserializeArg(e.data).then((v) => { resolve(v); this.working = false; @@ -209,7 +213,7 @@ export default class WorkerLinter implements Linter { if (this.requestQueue.length > 0) { const { request } = this.requestQueue[0]; - const serialized = await this.binary.serialize(request); + const serialized = await this.serializer.serialize(request); this.worker.postMessage(serialized); } else { this.working = false; diff --git a/packages/harper.js/src/WorkerLinter/worker.ts b/packages/harper.js/src/WorkerLinter/worker.ts index 84e158bc..f1ffb1b8 100644 --- a/packages/harper.js/src/WorkerLinter/worker.ts +++ b/packages/harper.js/src/WorkerLinter/worker.ts @@ -1,7 +1,8 @@ /// import './shims'; -import { BinaryModule, isSerializedRequest, type SerializedRequest } from '../binary'; +import { SuperBinaryModule } from '../binary'; import LocalLinter from '../LocalLinter'; +import Serializer, { isSerializedRequest, type SerializedRequest } from '../Serializer'; // Notify the main thread that we are ready self.postMessage('ready'); @@ -11,16 +12,17 @@ self.onmessage = (e) => { if (typeof binaryUrl !== 'string') { throw new TypeError(`Expected binary to be a string of url but got ${typeof binaryUrl}.`); } - const binary = new BinaryModule(binaryUrl); + const binary = SuperBinaryModule.create(binaryUrl); + const serializer = new Serializer(binary); const linter = new LocalLinter({ binary, dialect }); async function processRequest(v: SerializedRequest) { - const { procName, args } = await binary.deserialize(v); + const { procName, args } = await serializer.deserialize(v); if (procName in linter) { // @ts-expect-error const res = await linter[procName](...args); - postMessage(await binary.serializeArg(res)); + postMessage(await serializer.serializeArg(res)); } } diff --git a/packages/harper.js/src/binary.test.ts b/packages/harper.js/src/binary.test.ts deleted file mode 100644 index 9dd2759f..00000000 --- a/packages/harper.js/src/binary.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Span } from 'harper-wasm'; -import { expect, test } from 'vitest'; -import { binary } from './binary'; -import LocalLinter from './LocalLinter'; - -test('works with strings', async () => { - const start = 'This is a string'; - - const end = await binary.deserializeArg(structuredClone(await binary.serializeArg(start))); - - expect(end).toBe(start); - expect(typeof end).toBe(typeof start); -}); - -test('works with false booleans', async () => { - const start = false; - - const end = await binary.deserializeArg(structuredClone(await binary.serializeArg(start))); - - expect(end).toBe(start); - expect(typeof end).toBe(typeof start); -}); - -test('works with true booleans', async () => { - const start = true; - - const end = await binary.deserializeArg(structuredClone(await binary.serializeArg(start))); - - expect(end).toBe(start); - expect(typeof end).toBe(typeof start); -}); - -test('works with numbers', async () => { - const start = 123; - - const end = await binary.deserializeArg(structuredClone(await binary.serializeArg(start))); - - expect(end).toBe(start); - expect(typeof end).toBe(typeof start); -}); - -test('works with Spans', async () => { - const start = Span.new(123, 321); - - const end = await binary.deserializeArg(structuredClone(await binary.serializeArg(start))); - - expect(end.start).toBe(start.start); - expect(end.len()).toBe(start.len()); - expect(typeof end).toBe(typeof start); -}); - -test('works with Lints', async () => { - const linter = new LocalLinter({ binary }); - const lints = await linter.lint('This is an test.'); - const start = lints[0]; - - expect(start).not.toBeNull(); - - const end = await binary.deserializeArg(structuredClone(await binary.serializeArg(start))); - - expect(end.message()).toBe(start.message()); - expect(end.lint_kind()).toBe(start.lint_kind()); -}); diff --git a/packages/harper.js/src/binary.ts b/packages/harper.js/src/binary.ts index 1f2924cf..45a73bb6 100644 --- a/packages/harper.js/src/binary.ts +++ b/packages/harper.js/src/binary.ts @@ -4,9 +4,8 @@ import { default as binaryUrl } from 'harper-wasm/harper_wasm_bg.wasm?no-inline' import LazyPromise from 'p-lazy'; import pMemoize from 'p-memoize'; import type { LintConfig } from './main'; -import { assert } from './utils'; -export const loadBinary = pMemoize(async (binary: string) => { +const loadBinary = pMemoize(async (binary: string) => { const exports = await import('harper-wasm'); let input: InitInput; @@ -26,186 +25,61 @@ export const loadBinary = pMemoize(async (binary: string) => { return exports; }); -export type SerializableTypes = - | 'string' - | 'number' - | 'boolean' - | 'object' - | 'Suggestion' - | 'Lint' - | 'Span' - | 'Array' - | 'undefined' - | 'bigint'; - -/** Serializable argument to a procedure to be run on the web worker. */ -export interface RequestArg { - json: string; - type: SerializableTypes; -} - -/** An object that is sent to the web worker to request work to be done. */ -export interface SerializedRequest { - /** The procedure to be executed. */ - procName: string; - /** The arguments to the procedure */ - args: RequestArg[]; -} - -/** An object that is received by the web worker to request work to be done. */ -export interface DeserializedRequest { - /** The procedure to be executed. */ - procName: string; - /** The arguments to the procedure */ - args: any[]; -} - -export function isSerializedRequest(v: unknown): v is SerializedRequest { - return typeof v === 'object' && v !== null && 'procName' in v && 'args' in v; -} - -/** This class aims to define the communication protocol between the main thread and the worker. - * Note that much of the complication here comes from the fact that we can't serialize function calls or referenced WebAssembly memory.*/ +/** A wrapper around the underlying WebAssembly module that contains Harper's core code. Used to construct a `Linter`, as well as access some miscellaneous other functions. */ export class BinaryModule { - public url: string | URL; + public url: string | URL = ''; + private inner: Promise | null = null; - private inner: Promise; + /** Load a binary from a specified URL. This is the only recommended way to construct this type. */ + public static create(url: string | URL): BinaryModule { + const module = new SuperBinaryModule(); - constructor(url: string | URL) { - this.url = url; - this.inner = LazyPromise.from(() => - loadBinary(typeof this.url === 'string' ? this.url : this.url.href), + module.url = url; + module.inner = LazyPromise.from(() => + loadBinary(typeof module.url === 'string' ? module.url : module.url.href), ); + + return module; } - async getDefaultLintConfigAsJSON(): Promise { - const exported = await this.inner; + public async getDefaultLintConfigAsJSON(): Promise { + const exported = await this.inner!; return exported.get_default_lint_config_as_json(); } - async getDefaultLintConfig(): Promise { - const exported = await this.inner; + public async getDefaultLintConfig(): Promise { + const exported = await this.inner!; return exported.get_default_lint_config(); } - async toTitleCase(text: string): Promise { - const exported = await this.inner; + public async toTitleCase(text: string): Promise { + const exported = await this.inner!; return exported.to_title_case(text); } - async setup(): Promise { - const exported = await this.inner; + public async setup(): Promise { + const exported = await this.inner!; exported.setup(); } - - async createLinter(dialect?: Dialect): Promise { - const exported = await this.inner; - return exported.Linter.new(dialect ?? Dialect.American); - } - - async serializeArg(arg: any): Promise { - const { Lint, Span, Suggestion } = await this.inner; - - if (Array.isArray(arg)) { - return { - json: JSON.stringify(await Promise.all(arg.map((a) => this.serializeArg(a)))), - type: 'Array', - }; - } - - const argType = typeof arg; - switch (argType) { - case 'string': - case 'number': - case 'boolean': - case 'undefined': - return { json: JSON.stringify(arg), type: argType }; - case 'bigint': - return { json: arg.toString(), type: argType }; - } - - if (arg.to_json !== undefined) { - const json = arg.to_json(); - let type: SerializableTypes | undefined; - - if (arg instanceof Lint) { - type = 'Lint'; - } else if (arg instanceof Suggestion) { - type = 'Suggestion'; - } else if (arg instanceof Span) { - type = 'Span'; - } - - if (type === undefined) { - throw new Error('Unhandled case: type undefined'); - } - - return { json, type }; - } - - if (argType == 'object') { - return { - json: JSON.stringify( - await Promise.all( - Object.entries(arg).map(([key, value]) => this.serializeArg([key, value])), - ), - ), - type: 'object', - }; - } - - throw new Error(`Unhandled case: ${arg}`); - } - - async serialize(req: DeserializedRequest): Promise { - return { - procName: req.procName, - args: await Promise.all(req.args.map((arg) => this.serializeArg(arg))), - }; - } - - async deserializeArg(requestArg: RequestArg): Promise { - const { Lint, Span, Suggestion } = await this.inner; - - switch (requestArg.type) { - case 'bigint': - return BigInt(requestArg.json); - case 'undefined': - return undefined; - case 'boolean': - case 'number': - case 'string': - return JSON.parse(requestArg.json); - case 'Suggestion': - return Suggestion.from_json(requestArg.json); - case 'Lint': - return Lint.from_json(requestArg.json); - case 'Span': - return Span.from_json(requestArg.json); - case 'Array': { - const parsed = JSON.parse(requestArg.json); - assert(Array.isArray(parsed)); - return await Promise.all(parsed.map((arg) => this.deserializeArg(arg))); - } - case 'object': { - const parsed = JSON.parse(requestArg.json); - return Object.fromEntries( - await Promise.all(parsed.map((val: any) => this.deserializeArg(val))), - ); - } - default: - throw new Error(`Unhandled case: ${requestArg.type}`); - } - } - - async deserialize(request: SerializedRequest): Promise { - return { - procName: request.procName, - args: await Promise.all(request.args.map((arg) => this.deserializeArg(arg))), - }; - } } -export const binary = /*@__PURE__*/ new BinaryModule(binaryUrl); +export class SuperBinaryModule extends BinaryModule { + async createLinter(dialect?: Dialect): Promise { + const exported = await this.getBinaryModule(); + return exported.Linter.new(dialect ?? Dialect.American); + } -export const binaryInlined = /*@__PURE__*/ new BinaryModule(binaryInlinedUrl); + async getBinaryModule(): Promise { + return await LazyPromise.from(() => + loadBinary(typeof this.url === 'string' ? this.url : this.url.href), + ); + } +} + +/** A version of the Harper WebAssembly binary stored inline as a data URL. + * Can be tree-shaken if unused. */ +export const binary = /*@__PURE__*/ BinaryModule.create(binaryUrl); + +/** A version of the Harper WebAssembly binary stored inline as a data URL. + * Can be tree-shaken if unused. */ +export const binaryInlined = /*@__PURE__*/ BinaryModule.create(binaryInlinedUrl); diff --git a/packages/harper.js/src/main.ts b/packages/harper.js/src/main.ts index dd73e7fe..276fd330 100644 --- a/packages/harper.js/src/main.ts +++ b/packages/harper.js/src/main.ts @@ -4,11 +4,6 @@ export { BinaryModule, binary, binaryInlined, - type DeserializedRequest, - isSerializedRequest, - type RequestArg, - type SerializableTypes, - type SerializedRequest, } from './binary'; export type { default as Linter, LinterInit } from './Linter'; export { default as LocalLinter } from './LocalLinter'; @@ -18,7 +13,7 @@ export { default as WorkerLinter } from './WorkerLinter'; * This is a record, since you shouldn't hard-code the existence of any particular rules and should generalize based on this struct. */ export type LintConfig = Record; -/** The option used to configure the parser for an individual linting operation. */ +/** Options available to configure Harper's parser for an individual linting operation. */ export interface LintOptions { /** The markup language that is being passed. Defaults to `markdown`. */ language?: 'plaintext' | 'markdown'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 34524ded..5e8dacff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,7 @@ catalogs: specifier: ^2.8.1 version: 2.8.1 typescript: - specifier: ^5.8.2 + specifier: ^5.9.3 version: 5.9.3 importers: @@ -169,7 +169,7 @@ importers: specifier: ^4.1.14 version: 4.1.17 typescript: - specifier: ^5.9.3 + specifier: 'catalog:' version: 5.9.3 vite: specifier: ^7.1.10 @@ -178,14 +178,17 @@ importers: packages/harper.js: devDependencies: '@microsoft/api-documenter': - specifier: ^7.26.10 - version: 7.26.17(@types/node@22.13.10) + specifier: ^7.28.1 + version: 7.28.1(@types/node@22.13.10) '@microsoft/api-extractor': - specifier: ^7.50.1 - version: 7.52.1(@types/node@22.13.10) + specifier: ^7.55.1 + version: 7.55.1(@types/node@22.13.10) '@vitest/browser': specifier: ^3.0.6 version: 3.0.8(@testing-library/dom@10.4.0)(@types/node@22.13.10)(playwright@1.52.0)(typescript@5.9.3)(vite@6.3.5(@types/node@22.13.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0))(vitest@3.0.8) + '@vitest/ui': + specifier: 3.0.8 + version: 3.0.8(vitest@3.0.8) harper-wasm: specifier: workspace:* version: link:../../harper-wasm/pkg @@ -218,7 +221,7 @@ importers: version: 0.3.0(vite@6.3.5(@types/node@22.13.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0)) vitest: specifier: ^3.0.5 - version: 3.0.8(@types/debug@4.1.12)(@types/node@22.13.10)(@vitest/browser@3.0.8)(jiti@2.6.1)(jsdom@20.0.3)(lightningcss@1.30.2)(msw@2.7.3(@types/node@22.13.10)(typescript@5.9.3))(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0) + version: 3.0.8(@types/debug@4.1.12)(@types/node@22.13.10)(@vitest/browser@3.0.8)(@vitest/ui@3.0.8)(jiti@2.6.1)(jsdom@20.0.3)(lightningcss@1.30.2)(msw@2.7.3(@types/node@22.13.10)(typescript@5.9.3))(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0) packages/harper.js/examples/commonjs-simple: dependencies: @@ -340,7 +343,7 @@ importers: version: 6.3.5(@types/node@22.13.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0) vitest: specifier: ^3.0.8 - version: 3.0.8(@types/debug@4.1.12)(@types/node@22.13.10)(@vitest/browser@3.0.8)(jiti@2.6.1)(jsdom@20.0.3)(lightningcss@1.30.2)(msw@2.7.3(@types/node@22.13.10)(typescript@5.9.3))(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0) + version: 3.0.8(@types/debug@4.1.12)(@types/node@22.13.10)(@vitest/browser@3.0.8)(@vitest/ui@3.0.8)(jiti@2.6.1)(jsdom@20.0.3)(lightningcss@1.30.2)(msw@2.7.3(@types/node@22.13.10)(typescript@5.9.3))(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0) packages/vscode-plugin: dependencies: @@ -2603,22 +2606,22 @@ packages: '@marijn/find-cluster-break@1.0.2': resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} - '@microsoft/api-documenter@7.26.17': - resolution: {integrity: sha512-LOS9U6EJVpejN0vNXFBPdbRW9D2bPJI8FYfLGSaEeiqPfMBlH5QYLKt+OaJomx72uj9Ei6/W49mftK/c+UGPjg==} + '@microsoft/api-documenter@7.28.1': + resolution: {integrity: sha512-mkAss5DhyKtjQ9n+KURxOjz769WhTE376L9D0e6cX/98GBq1fv4SD6AVIOOFxYIsQv+NivskjUhsa+Geq7T2tQ==} hasBin: true - '@microsoft/api-extractor-model@7.30.4': - resolution: {integrity: sha512-RobC0gyVYsd2Fao9MTKOfTdBm41P/bCMUmzS5mQ7/MoAKEqy0FOBph3JOYdq4X4BsEnMEiSHc+0NUNmdzxCpjA==} + '@microsoft/api-extractor-model@7.32.1': + resolution: {integrity: sha512-u4yJytMYiUAnhcNQcZDTh/tVtlrzKlyKrQnLOV+4Qr/5gV+cpufWzCYAB1Q23URFqD6z2RoL2UYncM9xJVGNKA==} - '@microsoft/api-extractor@7.52.1': - resolution: {integrity: sha512-m3I5uAwE05orsu3D1AGyisX5KxsgVXB+U4bWOOaX/Z7Ftp/2Cy41qsNhO6LPvSxHBaapyser5dVorF1t5M6tig==} + '@microsoft/api-extractor@7.55.1': + resolution: {integrity: sha512-l8Z+8qrLkZFM3HM95Dbpqs6G39fpCa7O5p8A7AkA6hSevxkgwsOlLrEuPv0ADOyj5dI1Af5WVDiwpKG/ya5G3w==} hasBin: true - '@microsoft/tsdoc-config@0.17.1': - resolution: {integrity: sha512-UtjIFe0C6oYgTnad4q1QP4qXwLhe6tIpNTRStJ2RZEPIkqQPREAwE5spzVxsdn9UaEMUqhh0AqSx3X4nWAKXWw==} + '@microsoft/tsdoc-config@0.18.0': + resolution: {integrity: sha512-8N/vClYyfOH+l4fLkkr9+myAoR6M7akc8ntBJ4DJdWH2b09uVfr71+LTMpNyG19fNqWDg8KEDZhx5wxuqHyGjw==} - '@microsoft/tsdoc@0.15.1': - resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@microsoft/tsdoc@0.16.0': + resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} '@mswjs/interceptors@0.37.6': resolution: {integrity: sha512-wK+5pLK5XFmgtH3aQ2YVvA3HohS3xqV/OxuVOdNx9Wpnz7VE/fnC+e1A7ln6LFYeck7gOJ/dsZV6OLplOtAJ2w==} @@ -3307,27 +3310,35 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - '@rushstack/node-core-library@5.12.0': - resolution: {integrity: sha512-QSwwzgzWoil1SCQse+yCHwlhRxNv2dX9siPnAb9zR/UmMhac4mjMrlMZpk64BlCeOFi1kJKgXRkihSwRMbboAQ==} + '@rushstack/node-core-library@5.19.0': + resolution: {integrity: sha512-BxAopbeWBvNJ6VGiUL+5lbJXywTdsnMeOS8j57Cn/xY10r6sV/gbsTlfYKjzVCUBZATX2eRzJHSMCchsMTGN6A==} peerDependencies: '@types/node': '*' peerDependenciesMeta: '@types/node': optional: true - '@rushstack/rig-package@0.5.3': - resolution: {integrity: sha512-olzSSjYrvCNxUFZowevC3uz8gvKr3WTpHQ7BkpjtRpA3wK+T0ybep/SRUMfr195gBzJm5gaXw0ZMgjIyHqJUow==} - - '@rushstack/terminal@0.15.1': - resolution: {integrity: sha512-3vgJYwumcjoDOXU3IxZfd616lqOdmr8Ezj4OWgJZfhmiBK4Nh7eWcv8sU8N/HdzXcuHDXCRGn/6O2Q75QvaZMA==} + '@rushstack/problem-matcher@0.1.1': + resolution: {integrity: sha512-Fm5XtS7+G8HLcJHCWpES5VmeMyjAKaWeyZU5qPzZC+22mPlJzAsOxymHiWIfuirtPckX3aptWws+K2d0BzniJA==} peerDependencies: '@types/node': '*' peerDependenciesMeta: '@types/node': optional: true - '@rushstack/ts-command-line@4.23.6': - resolution: {integrity: sha512-7WepygaF3YPEoToh4MAL/mmHkiIImQq3/uAkQX46kVoKTNOOlCtFGyNnze6OYuWw2o9rxsyrHVfIBKxq/am2RA==} + '@rushstack/rig-package@0.6.0': + resolution: {integrity: sha512-ZQmfzsLE2+Y91GF15c65L/slMRVhF6Hycq04D4TwtdGaUAbIXXg9c5pKA5KFU7M4QMaihoobp9JJYpYcaY3zOw==} + + '@rushstack/terminal@0.19.4': + resolution: {integrity: sha512-f4XQk02CrKfrMgyOfhYd3qWI944dLC21S4I/LUhrlAP23GTMDNG6EK5effQtFkISwUKCgD9vMBrJZaPSUquxWQ==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/ts-command-line@5.1.4': + resolution: {integrity: sha512-H0I6VdJ6sOUbktDFpP2VW5N29w8v4hRoNZOQz02vtEi6ZTYL1Ju8u+TcFiFawUDrUsx/5MQTUhd79uwZZVwVlA==} '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -4321,6 +4332,11 @@ packages: '@vitest/spy@3.0.8': resolution: {integrity: sha512-MR+PzJa+22vFKYb934CejhR4BeRpMSoxkvNoDit68GQxRLSf11aT6CTj3XaqUU9rxgWJFnqicN/wxw6yBRkI1Q==} + '@vitest/ui@3.0.8': + resolution: {integrity: sha512-MfTjaLU+Gw/lYorgwFZ06Cym+Mj9hPfZh/Q91d4JxyAHiicAakPTvS7zYCSHF+5cErwu2PVBe1alSjuh6L/UiA==} + peerDependencies: + vitest: 3.0.8 + '@vitest/utils@3.0.8': resolution: {integrity: sha512-nkBC3aEhfX2PdtQI/QwAWp8qZWwzASsU4Npbcd5RdMPBSSLCpkZp52P3xku3s3uA0HIEhGvEcF8rNkBsz9dQ4Q==} @@ -6178,6 +6194,10 @@ packages: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} + diff@8.0.2: + resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} + engines: {node: '>=0.3.1'} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -6867,6 +6887,9 @@ packages: fflate@0.4.8: resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + file-entry-cache@10.0.7: resolution: {integrity: sha512-txsf5fu3anp2ff3+gOJJzRImtrtm/oa9tYLN0iTuINZ++EyVR/nRrg2fKYwvG/pXDofcrvvb0scEbX3NyW/COw==} @@ -8165,10 +8188,6 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@3.13.1: - resolution: {integrity: sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==} - hasBin: true - js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true @@ -11115,8 +11134,8 @@ packages: third-party-web@0.26.5: resolution: {integrity: sha512-tDuKQJUTfjvi9Fcrs1s6YAQAB9mzhTSbBZMfNgtWNmJlHuoFeXO6dzBFdGeCWRvYL50jQGK0jPsBZYxqZQJ2SA==} - third-party-web@0.28.0: - resolution: {integrity: sha512-4P798O67JmIKRJfJ1HSOkIsZrx2+FuaN2jTQX+imHXFPbGp17KSMDabYxrRT011B3gBzaoHFhUkBlEkNZN8vuQ==} + third-party-web@0.29.0: + resolution: {integrity: sha512-nBDSJw5B7Sl1YfsATG2XkW5qgUPODbJhXw++BKygi9w6O/NKS98/uY/nR/DxDq2axEjL6halHW1v+jhm/j1DBQ==} through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -11133,10 +11152,6 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyglobby@0.2.12: - resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==} - engines: {node: '>=12.0.0'} - tinyglobby@0.2.13: resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} engines: {node: '>=12.0.0'} @@ -15054,37 +15069,38 @@ snapshots: '@marijn/find-cluster-break@1.0.2': {} - '@microsoft/api-documenter@7.26.17(@types/node@22.13.10)': + '@microsoft/api-documenter@7.28.1(@types/node@22.13.10)': dependencies: - '@microsoft/api-extractor-model': 7.30.4(@types/node@22.13.10) - '@microsoft/tsdoc': 0.15.1 - '@rushstack/node-core-library': 5.12.0(@types/node@22.13.10) - '@rushstack/terminal': 0.15.1(@types/node@22.13.10) - '@rushstack/ts-command-line': 4.23.6(@types/node@22.13.10) - js-yaml: 3.13.1 + '@microsoft/api-extractor-model': 7.32.1(@types/node@22.13.10) + '@microsoft/tsdoc': 0.16.0 + '@rushstack/node-core-library': 5.19.0(@types/node@22.13.10) + '@rushstack/terminal': 0.19.4(@types/node@22.13.10) + '@rushstack/ts-command-line': 5.1.4(@types/node@22.13.10) + js-yaml: 4.1.0 resolve: 1.22.10 transitivePeerDependencies: - '@types/node' - '@microsoft/api-extractor-model@7.30.4(@types/node@22.13.10)': + '@microsoft/api-extractor-model@7.32.1(@types/node@22.13.10)': dependencies: - '@microsoft/tsdoc': 0.15.1 - '@microsoft/tsdoc-config': 0.17.1 - '@rushstack/node-core-library': 5.12.0(@types/node@22.13.10) + '@microsoft/tsdoc': 0.16.0 + '@microsoft/tsdoc-config': 0.18.0 + '@rushstack/node-core-library': 5.19.0(@types/node@22.13.10) transitivePeerDependencies: - '@types/node' - '@microsoft/api-extractor@7.52.1(@types/node@22.13.10)': + '@microsoft/api-extractor@7.55.1(@types/node@22.13.10)': dependencies: - '@microsoft/api-extractor-model': 7.30.4(@types/node@22.13.10) - '@microsoft/tsdoc': 0.15.1 - '@microsoft/tsdoc-config': 0.17.1 - '@rushstack/node-core-library': 5.12.0(@types/node@22.13.10) - '@rushstack/rig-package': 0.5.3 - '@rushstack/terminal': 0.15.1(@types/node@22.13.10) - '@rushstack/ts-command-line': 4.23.6(@types/node@22.13.10) + '@microsoft/api-extractor-model': 7.32.1(@types/node@22.13.10) + '@microsoft/tsdoc': 0.16.0 + '@microsoft/tsdoc-config': 0.18.0 + '@rushstack/node-core-library': 5.19.0(@types/node@22.13.10) + '@rushstack/rig-package': 0.6.0 + '@rushstack/terminal': 0.19.4(@types/node@22.13.10) + '@rushstack/ts-command-line': 5.1.4(@types/node@22.13.10) + diff: 8.0.2 lodash: 4.17.21 - minimatch: 3.0.8 + minimatch: 10.0.3 resolve: 1.22.10 semver: 7.5.4 source-map: 0.6.1 @@ -15092,14 +15108,14 @@ snapshots: transitivePeerDependencies: - '@types/node' - '@microsoft/tsdoc-config@0.17.1': + '@microsoft/tsdoc-config@0.18.0': dependencies: - '@microsoft/tsdoc': 0.15.1 + '@microsoft/tsdoc': 0.16.0 ajv: 8.12.0 jju: 1.4.0 resolve: 1.22.10 - '@microsoft/tsdoc@0.15.1': {} + '@microsoft/tsdoc@0.16.0': {} '@mswjs/interceptors@0.37.6': dependencies: @@ -15200,7 +15216,7 @@ snapshots: '@paulirish/trace_engine@0.0.44': dependencies: - third-party-web: 0.28.0 + third-party-web: 0.29.0 '@php-wasm/node-polyfills@0.6.16': {} @@ -15712,7 +15728,7 @@ snapshots: '@rtsao/scc@1.1.0': {} - '@rushstack/node-core-library@5.12.0(@types/node@22.13.10)': + '@rushstack/node-core-library@5.19.0(@types/node@22.13.10)': dependencies: ajv: 8.13.0 ajv-draft-04: 1.0.0(ajv@8.13.0) @@ -15725,21 +15741,26 @@ snapshots: optionalDependencies: '@types/node': 22.13.10 - '@rushstack/rig-package@0.5.3': + '@rushstack/problem-matcher@0.1.1(@types/node@22.13.10)': + optionalDependencies: + '@types/node': 22.13.10 + + '@rushstack/rig-package@0.6.0': dependencies: resolve: 1.22.10 strip-json-comments: 3.1.1 - '@rushstack/terminal@0.15.1(@types/node@22.13.10)': + '@rushstack/terminal@0.19.4(@types/node@22.13.10)': dependencies: - '@rushstack/node-core-library': 5.12.0(@types/node@22.13.10) + '@rushstack/node-core-library': 5.19.0(@types/node@22.13.10) + '@rushstack/problem-matcher': 0.1.1(@types/node@22.13.10) supports-color: 8.1.1 optionalDependencies: '@types/node': 22.13.10 - '@rushstack/ts-command-line@4.23.6(@types/node@22.13.10)': + '@rushstack/ts-command-line@5.1.4(@types/node@22.13.10)': dependencies: - '@rushstack/terminal': 0.15.1(@types/node@22.13.10) + '@rushstack/terminal': 0.19.4(@types/node@22.13.10) '@types/argparse': 1.0.38 argparse: 1.0.10 string-argv: 0.3.2 @@ -16963,7 +16984,7 @@ snapshots: dependencies: '@sveltejs/kit': 2.48.5(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.43.12)(vite@6.3.5(@types/node@22.13.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0)))(svelte@5.43.12)(vite@6.3.5(@types/node@22.13.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0)) kolorist: 1.8.0 - tinyglobby: 0.2.12 + tinyglobby: 0.2.15 vite-plugin-pwa: 0.21.1(vite@6.3.5(@types/node@22.13.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0) '@vitest/browser@3.0.8(@testing-library/dom@10.4.0)(@types/node@22.13.10)(playwright@1.52.0)(typescript@5.9.3)(vite@6.3.5(@types/node@22.13.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0))(vitest@3.0.8)': @@ -16975,7 +16996,7 @@ snapshots: msw: 2.7.3(@types/node@22.13.10)(typescript@5.9.3) sirv: 3.0.1 tinyrainbow: 2.0.0 - vitest: 3.0.8(@types/debug@4.1.12)(@types/node@22.13.10)(@vitest/browser@3.0.8)(jiti@2.6.1)(jsdom@20.0.3)(lightningcss@1.30.2)(msw@2.7.3(@types/node@22.13.10)(typescript@5.9.3))(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0) + vitest: 3.0.8(@types/debug@4.1.12)(@types/node@22.13.10)(@vitest/browser@3.0.8)(@vitest/ui@3.0.8)(jiti@2.6.1)(jsdom@20.0.3)(lightningcss@1.30.2)(msw@2.7.3(@types/node@22.13.10)(typescript@5.9.3))(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0) ws: 8.18.1 optionalDependencies: playwright: 1.52.0 @@ -17022,6 +17043,17 @@ snapshots: dependencies: tinyspy: 3.0.2 + '@vitest/ui@3.0.8(vitest@3.0.8)': + dependencies: + '@vitest/utils': 3.0.8 + fflate: 0.8.2 + flatted: 3.3.3 + pathe: 2.0.3 + sirv: 3.0.1 + tinyglobby: 0.2.15 + tinyrainbow: 2.0.0 + vitest: 3.0.8(@types/debug@4.1.12)(@types/node@22.13.10)(@vitest/browser@3.0.8)(@vitest/ui@3.0.8)(jiti@2.6.1)(jsdom@20.0.3)(lightningcss@1.30.2)(msw@2.7.3(@types/node@22.13.10)(typescript@5.9.3))(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0) + '@vitest/utils@3.0.8': dependencies: '@vitest/pretty-format': 3.0.8 @@ -19816,6 +19848,8 @@ snapshots: diff@4.0.2: {} + diff@8.0.2: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -20707,6 +20741,8 @@ snapshots: fflate@0.4.8: {} + fflate@0.8.2: {} + file-entry-cache@10.0.7: dependencies: flat-cache: 6.1.7 @@ -22306,11 +22342,6 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@3.13.1: - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - js-yaml@3.14.1: dependencies: argparse: 1.0.10 @@ -25765,7 +25796,7 @@ snapshots: third-party-web@0.26.5: {} - third-party-web@0.28.0: {} + third-party-web@0.29.0: {} through@2.3.8: {} @@ -25777,11 +25808,6 @@ snapshots: tinyexec@0.3.2: {} - tinyglobby@0.2.12: - dependencies: - fdir: 6.4.3(picomatch@4.0.2) - picomatch: 4.0.2 - tinyglobby@0.2.13: dependencies: fdir: 6.4.4(picomatch@4.0.2) @@ -26293,7 +26319,7 @@ snapshots: vite-plugin-dts@4.5.3(@types/node@22.13.10)(rollup@4.53.3)(typescript@5.9.3)(vite@6.3.5(@types/node@22.13.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0)): dependencies: - '@microsoft/api-extractor': 7.52.1(@types/node@22.13.10) + '@microsoft/api-extractor': 7.55.1(@types/node@22.13.10) '@rollup/pluginutils': 5.1.4(rollup@4.53.3) '@volar/typescript': 2.4.12 '@vue/language-core': 2.2.0(typescript@5.9.3) @@ -26415,7 +26441,7 @@ snapshots: optionalDependencies: vite: 7.2.2(@types/node@22.13.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0) - vitest@3.0.8(@types/debug@4.1.12)(@types/node@22.13.10)(@vitest/browser@3.0.8)(jiti@2.6.1)(jsdom@20.0.3)(lightningcss@1.30.2)(msw@2.7.3(@types/node@22.13.10)(typescript@5.9.3))(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0): + vitest@3.0.8(@types/debug@4.1.12)(@types/node@22.13.10)(@vitest/browser@3.0.8)(@vitest/ui@3.0.8)(jiti@2.6.1)(jsdom@20.0.3)(lightningcss@1.30.2)(msw@2.7.3(@types/node@22.13.10)(typescript@5.9.3))(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0): dependencies: '@vitest/expect': 3.0.8 '@vitest/mocker': 3.0.8(msw@2.7.3(@types/node@22.13.10)(typescript@5.9.3))(vite@6.3.5(@types/node@22.13.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0)) @@ -26441,6 +26467,7 @@ snapshots: '@types/debug': 4.1.12 '@types/node': 22.13.10 '@vitest/browser': 3.0.8(@testing-library/dom@10.4.0)(@types/node@22.13.10)(playwright@1.52.0)(typescript@5.9.3)(vite@6.3.5(@types/node@22.13.10)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0))(vitest@3.0.8) + '@vitest/ui': 3.0.8(vitest@3.0.8) jsdom: 20.0.3 transitivePeerDependencies: - jiti diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 32eb1eb2..7127d418 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,19 +3,19 @@ packages: - harper-wasm/pkg - packages/harper.js/examples/* catalog: - typescript: ^5.8.2 + typescript: ^5.9.3 tslib: ^2.8.1 - '@types/node': ^22.13.10 - '@babel/runtime': ^7.26.10 + "@types/node": ^22.13.10 + "@babel/runtime": ^7.26.10 onlyBuiltDependencies: - - '@biomejs/biome' - - '@parcel/watcher' - - '@swc/core' - - '@vscode/vsce-sign' + - "@biomejs/biome" + - "@parcel/watcher" + - "@swc/core" + - "@vscode/vsce-sign" - core-js - core-js-pure - esbuild - keytar - msw - svelte-preprocess - - '@tailwindcss/oxide' + - "@tailwindcss/oxide"