harper.js 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "harper.js",
3
+ "version": "0.13.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "tsc && vite build",
8
+ "test": "vitest run"
9
+ },
10
+ "dependencies": {
11
+ "wasm": "link:../../harper-wasm/pkg"
12
+ },
13
+ "devDependencies": {
14
+ "@vitest/browser": "^2.1.8",
15
+ "playwright": "^1.49.1",
16
+ "typescript": "~5.6.2",
17
+ "vite": "^5.1.8",
18
+ "vite-plugin-dts": "^4.3.0",
19
+ "vite-plugin-virtual": "^0.3.0",
20
+ "vitest": "^2.1.8"
21
+ },
22
+ "main": "dist/harper.js",
23
+ "types": "dist/harper.d.ts"
24
+ }
@@ -0,0 +1,135 @@
1
+ import { expect, test } from 'vitest';
2
+ import WorkerLinter from './WorkerLinter';
3
+ import LocalLinter from './LocalLinter';
4
+
5
+ const linters = {
6
+ WorkerLinter: WorkerLinter,
7
+ LocalLinter: LocalLinter
8
+ };
9
+
10
+ for (const [linterName, Linter] of Object.entries(linters)) {
11
+ test(`${linterName} detects repeated words`, async () => {
12
+ const linter = new Linter();
13
+
14
+ const lints = await linter.lint('The the problem is...');
15
+
16
+ expect(lints.length).toBe(1);
17
+ });
18
+
19
+ test(`${linterName} detects repeated words with multiple synchronous requests`, async () => {
20
+ const linter = new Linter();
21
+
22
+ const promises = [
23
+ linter.lint('The problem is that that...'),
24
+ linter.lint('The problem is...'),
25
+ linter.lint('The the problem is...')
26
+ ];
27
+
28
+ const results = [];
29
+
30
+ for (const promise of promises) {
31
+ results.push(await promise);
32
+ }
33
+
34
+ expect(results[0].length).toBe(1);
35
+ expect(results[0][0].suggestions().length).toBe(1);
36
+ expect(results[1].length).toBe(0);
37
+ expect(results[2].length).toBe(1);
38
+ });
39
+
40
+ test(`${linterName} detects repeated words with concurrent requests`, async () => {
41
+ const linter = new Linter();
42
+
43
+ const promises = [
44
+ linter.lint('The problem is that that...'),
45
+ linter.lint('The problem is...'),
46
+ linter.lint('The the problem is...')
47
+ ];
48
+
49
+ const results = await Promise.all(promises);
50
+
51
+ expect(results[0].length).toBe(1);
52
+ expect(results[0][0].suggestions().length).toBe(1);
53
+ expect(results[1].length).toBe(0);
54
+ expect(results[2].length).toBe(1);
55
+ });
56
+
57
+ test(`${linterName} detects lorem ipsum paragraph as not english`, async () => {
58
+ const linter = new Linter();
59
+
60
+ const result = await linter.isLikelyEnglish(
61
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
62
+ );
63
+
64
+ expect(result).toBeTypeOf('boolean');
65
+ expect(result).toBe(false);
66
+ });
67
+
68
+ test(`${linterName} can run setup without issues`, async () => {
69
+ const linter = new Linter();
70
+
71
+ await linter.setup();
72
+ });
73
+
74
+ test(`${linterName} contains configuration option for repetition`, async () => {
75
+ const linter = new Linter();
76
+
77
+ const lintConfig = await linter.getLintConfig();
78
+ expect(lintConfig).toHaveProperty('repeated_words');
79
+ });
80
+
81
+ test(`${linterName} can both get and set its configuration`, async () => {
82
+ const linter = new Linter();
83
+
84
+ let lintConfig = await linter.getLintConfig();
85
+
86
+ for (const key of Object.keys(lintConfig)) {
87
+ lintConfig[key] = true;
88
+ }
89
+
90
+ await linter.setLintConfig(lintConfig);
91
+ lintConfig = await linter.getLintConfig();
92
+
93
+ for (const key of Object.keys(lintConfig)) {
94
+ expect(lintConfig[key]).toBe(true);
95
+ }
96
+ });
97
+
98
+ test(`${linterName} can make things title case`, async () => {
99
+ const linter = new Linter();
100
+
101
+ const titleCase = await linter.toTitleCase('THIS IS A TEST FOR MAKING TITLES');
102
+
103
+ expect(titleCase).toBe('This Is a Test for Making Titles');
104
+ });
105
+ }
106
+
107
+ test('Linters have the same config format', async () => {
108
+ const configs = [];
109
+
110
+ for (const Linter of Object.values(linters)) {
111
+ const linter = new Linter();
112
+
113
+ configs.push(await linter.getLintConfig());
114
+ }
115
+
116
+ for (const config of configs) {
117
+ expect(config).toEqual(configs[0]);
118
+ expect(config).toBeTypeOf('object');
119
+ }
120
+ });
121
+
122
+ test('Linters have the same JSON config format', async () => {
123
+ const configs = [];
124
+
125
+ for (const Linter of Object.values(linters)) {
126
+ const linter = new Linter();
127
+
128
+ configs.push(await linter.getLintConfigAsJSON());
129
+ }
130
+
131
+ for (const config of configs) {
132
+ expect(config).toEqual(configs[0]);
133
+ expect(config).toBeTypeOf('string');
134
+ }
135
+ });
package/src/Linter.ts ADDED
@@ -0,0 +1,35 @@
1
+ import type { Lint, Span, Suggestion } from 'wasm';
2
+ import { LintConfig } from './main';
3
+
4
+ /** A interface for an object that can perform linting actions. */
5
+ export default interface Linter {
6
+ /** Complete any setup that is necessary before linting. This may include downloading and compiling the WebAssembly binary.
7
+ * This setup will complete when needed regardless of whether you call this function.
8
+ * 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). */
9
+ setup(): Promise<void>;
10
+ /** Lint the provided text. */
11
+ lint(text: string): Promise<Lint[]>;
12
+ /** Apply a suggestion to the given text, returning the transformed result. */
13
+ applySuggestion(text: string, suggestion: Suggestion, span: Span): Promise<string>;
14
+ /** Determine if the provided text is likely to be intended to be English.
15
+ * The algorithm can be described as "proof of concept" and as such does not work terribly well.*/
16
+ isLikelyEnglish(text: string): Promise<boolean>;
17
+ /** Determine which parts of a given string are intended to be English, returning those bits.
18
+ * The algorithm can be described as "proof of concept" and as such does not work terribly well.*/
19
+ isolateEnglish(text: string): Promise<string>;
20
+
21
+ /** Get the linter's current configuration. */
22
+ getLintConfig(): Promise<LintConfig>;
23
+
24
+ /** Set the linter's current configuration. */
25
+ setLintConfig(config: LintConfig): Promise<void>;
26
+
27
+ /** Get the linter's current configuration as JSON. */
28
+ getLintConfigAsJSON(): Promise<string>;
29
+
30
+ /** Set the linter's current configuration from JSON. */
31
+ setLintConfigWithJSON(config: string): Promise<void>;
32
+
33
+ /** Convert a string to Chicago-style title case. */
34
+ toTitleCase(text: string): Promise<string>;
35
+ }
@@ -0,0 +1,77 @@
1
+ import type { Lint, Span, Suggestion, Linter as WasmLinter } from 'wasm';
2
+ import Linter from './Linter';
3
+ import loadWasm from './loadWasm';
4
+ import { LintConfig } from './main';
5
+
6
+ /** A Linter that runs in the current JavaScript context (meaning it is allowed to block the event loop). */
7
+ export default class LocalLinter implements Linter {
8
+ private inner: WasmLinter | undefined;
9
+
10
+ /** Initialize the WebAssembly and construct the inner Linter. */
11
+ private async initialize(): Promise<void> {
12
+ if (!this.inner) {
13
+ const wasm = await loadWasm();
14
+ wasm.setup();
15
+ this.inner = wasm.Linter.new();
16
+ }
17
+ }
18
+
19
+ async setup(): Promise<void> {
20
+ await this.initialize();
21
+ this.inner!.lint('');
22
+ }
23
+
24
+ async lint(text: string): Promise<Lint[]> {
25
+ await this.initialize();
26
+ let lints = this.inner!.lint(text);
27
+
28
+ // We only want to show fixable errors.
29
+ lints = lints.filter((lint) => lint.suggestion_count() > 0);
30
+
31
+ return lints;
32
+ }
33
+
34
+ async applySuggestion(text: string, suggestion: Suggestion, span: Span): Promise<string> {
35
+ const wasm = await loadWasm();
36
+ return wasm.apply_suggestion(text, span, suggestion);
37
+ }
38
+
39
+ async isLikelyEnglish(text: string): Promise<boolean> {
40
+ await this.initialize();
41
+ return this.inner!.is_likely_english(text);
42
+ }
43
+
44
+ async isolateEnglish(text: string): Promise<string> {
45
+ await this.initialize();
46
+ return this.inner!.isolate_english(text);
47
+ }
48
+
49
+ async getLintConfig(): Promise<LintConfig> {
50
+ await this.initialize();
51
+
52
+ return this.inner!.get_lint_config_as_object();
53
+ }
54
+
55
+ async setLintConfig(config: LintConfig): Promise<void> {
56
+ await this.initialize();
57
+
58
+ this.inner!.set_lint_config_from_object(config);
59
+ }
60
+
61
+ async getLintConfigAsJSON(): Promise<string> {
62
+ await this.initialize();
63
+
64
+ return this.inner!.get_lint_config_as_json();
65
+ }
66
+
67
+ async setLintConfigWithJSON(config: string): Promise<void> {
68
+ await this.initialize();
69
+
70
+ this.inner!.set_lint_config_from_json(config);
71
+ }
72
+
73
+ async toTitleCase(text: string): Promise<string> {
74
+ const wasm = await loadWasm();
75
+ return wasm.to_title_case(text);
76
+ }
77
+ }
@@ -0,0 +1,63 @@
1
+ import { expect, test } from 'vitest';
2
+ import { deserializeArg, serializeArg } from './communication';
3
+ import { Span } from 'wasm';
4
+ import LocalLinter from '../LocalLinter';
5
+
6
+ test('works with strings', async () => {
7
+ const start = 'This is a string';
8
+
9
+ const end = await deserializeArg(structuredClone(await serializeArg(start)));
10
+
11
+ expect(end).toBe(start);
12
+ expect(typeof end).toBe(typeof start);
13
+ });
14
+
15
+ test('works with false booleans', async () => {
16
+ const start = false;
17
+
18
+ const end = await deserializeArg(structuredClone(await serializeArg(start)));
19
+
20
+ expect(end).toBe(start);
21
+ expect(typeof end).toBe(typeof start);
22
+ });
23
+
24
+ test('works with true booleans', async () => {
25
+ const start = true;
26
+
27
+ const end = await deserializeArg(structuredClone(await serializeArg(start)));
28
+
29
+ expect(end).toBe(start);
30
+ expect(typeof end).toBe(typeof start);
31
+ });
32
+
33
+ test('works with numbers', async () => {
34
+ const start = 123;
35
+
36
+ const end = await deserializeArg(structuredClone(await serializeArg(start)));
37
+
38
+ expect(end).toBe(start);
39
+ expect(typeof end).toBe(typeof start);
40
+ });
41
+
42
+ test('works with Spans', async () => {
43
+ const start = Span.new(123, 321);
44
+
45
+ const end = await deserializeArg(structuredClone(await serializeArg(start)));
46
+
47
+ expect(end.start).toBe(start.start);
48
+ expect(end.len()).toBe(start.len());
49
+ expect(typeof end).toBe(typeof start);
50
+ });
51
+
52
+ test('works with Lints', async () => {
53
+ const linter = new LocalLinter();
54
+ const lints = await linter.lint('This is an test.');
55
+ const start = lints[0];
56
+
57
+ expect(start).not.toBeNull();
58
+
59
+ const end = await deserializeArg(structuredClone(await serializeArg(start)));
60
+
61
+ expect(end.message()).toBe(start.message());
62
+ expect(end.lint_kind()).toBe(start.lint_kind());
63
+ });
@@ -0,0 +1,111 @@
1
+ /** This module aims to define the communication protocol between the main thread and the worker.
2
+ * Note that most of the complication here comes from the fact that we can't serialize function calls or referenced WebAssembly memory.*/
3
+
4
+ import loadWasm from '../loadWasm';
5
+
6
+ export type Type =
7
+ | 'string'
8
+ | 'number'
9
+ | 'boolean'
10
+ | 'Suggestion'
11
+ | 'Lint'
12
+ | 'Span'
13
+ | 'Array'
14
+ | 'undefined';
15
+
16
+ /** Serializable argument to a procedure to be run on the web worker. */
17
+ export type RequestArg = {
18
+ json: string;
19
+ type: Type;
20
+ };
21
+
22
+ export async function serialize(req: DeserializedRequest): Promise<SerializedRequest> {
23
+ return {
24
+ procName: req.procName,
25
+ args: await Promise.all(req.args.map(serializeArg))
26
+ };
27
+ }
28
+
29
+ export async function serializeArg(arg: any): Promise<RequestArg> {
30
+ const { Lint, Span, Suggestion } = await loadWasm();
31
+
32
+ if (Array.isArray(arg)) {
33
+ return { json: JSON.stringify(await Promise.all(arg.map(serializeArg))), type: 'Array' };
34
+ }
35
+
36
+ switch (typeof arg) {
37
+ case 'string':
38
+ case 'number':
39
+ case 'boolean':
40
+ case 'undefined':
41
+ // @ts-expect-error see the `Type` type.
42
+ return { json: JSON.stringify(arg), type: typeof arg };
43
+ }
44
+
45
+ if (arg.to_json != undefined) {
46
+ const json = arg.to_json();
47
+ let type: Type | undefined = undefined;
48
+
49
+ if (arg instanceof Lint) {
50
+ type = 'Lint';
51
+ } else if (arg instanceof Suggestion) {
52
+ type = 'Suggestion';
53
+ } else if (arg instanceof Span) {
54
+ type = 'Span';
55
+ }
56
+
57
+ if (type == undefined) {
58
+ throw new Error('Unhandled case');
59
+ }
60
+
61
+ return { json, type };
62
+ }
63
+
64
+ throw new Error('Unhandled case');
65
+ }
66
+
67
+ export async function deserializeArg(requestArg: RequestArg): Promise<any> {
68
+ const { Lint, Span, Suggestion } = await loadWasm();
69
+
70
+ switch (requestArg.type) {
71
+ case 'undefined':
72
+ return undefined;
73
+ case 'boolean':
74
+ case 'number':
75
+ case 'string':
76
+ return JSON.parse(requestArg.json);
77
+ case 'Suggestion':
78
+ return Suggestion.from_json(requestArg.json);
79
+ case 'Lint':
80
+ return Lint.from_json(requestArg.json);
81
+ case 'Span':
82
+ return Span.from_json(requestArg.json);
83
+ case 'Array':
84
+ return await Promise.all(JSON.parse(requestArg.json).map(deserializeArg));
85
+ default:
86
+ throw new Error(`Unhandled case: ${requestArg.type}`);
87
+ }
88
+ }
89
+
90
+ /** An object that is sent to the web worker to request work to be done. */
91
+ export type SerializedRequest = {
92
+ /** The procedure to be executed. */
93
+ procName: string;
94
+ /** The arguments to the procedure */
95
+ args: RequestArg[];
96
+ };
97
+
98
+ /** An object that is received by the web worker to request work to be done. */
99
+ export type DeserializedRequest = {
100
+ /** The procedure to be executed. */
101
+ procName: string;
102
+ /** The arguments to the procedure */
103
+ args: any[];
104
+ };
105
+
106
+ export async function deserialize(request: SerializedRequest): Promise<DeserializedRequest> {
107
+ return {
108
+ procName: request.procName,
109
+ args: await Promise.all(request.args.map(deserializeArg))
110
+ };
111
+ }
@@ -0,0 +1,131 @@
1
+ import { DeserializedRequest, deserializeArg, serialize } from './communication';
2
+ import type { Lint, Suggestion, Span } from 'wasm';
3
+ import Linter from '../Linter';
4
+ import Worker from './worker.js?worker&inline';
5
+ import { getWasmUri } from '../loadWasm';
6
+ import { LintConfig } from '../main';
7
+
8
+ /** The data necessary to complete a request once the worker has responded. */
9
+ type RequestItem = {
10
+ resolve: (item: unknown) => void;
11
+ reject: (item: unknown) => void;
12
+ request: DeserializedRequest;
13
+ };
14
+
15
+ /** A Linter that spins up a dedicated web worker to do processing on a separate thread.
16
+ * Main benefit: this Linter will not block the event loop for large documents.
17
+ *
18
+ * NOTE: This class will not work properly in Node. In that case, just use `LocalLinter`.
19
+ * Also requires top-level await to work. */
20
+ export default class WorkerLinter implements Linter {
21
+ private worker;
22
+ private requestQueue: RequestItem[];
23
+ private working = true;
24
+
25
+ constructor() {
26
+ this.worker = new Worker();
27
+ this.requestQueue = [];
28
+
29
+ // Fires when the worker sends 'ready'.
30
+ this.worker.onmessage = () => {
31
+ this.setupMainEventListeners();
32
+
33
+ this.worker.postMessage(getWasmUri());
34
+
35
+ this.working = false;
36
+ this.submitRemainingRequests();
37
+ };
38
+ }
39
+
40
+ private setupMainEventListeners() {
41
+ this.worker.onmessage = (e: MessageEvent) => {
42
+ const { resolve } = this.requestQueue.shift()!;
43
+ deserializeArg(e.data).then((v) => {
44
+ resolve(v);
45
+
46
+ this.working = false;
47
+
48
+ this.submitRemainingRequests();
49
+ });
50
+ };
51
+
52
+ this.worker.onmessageerror = (e: MessageEvent) => {
53
+ const { reject } = this.requestQueue.shift()!;
54
+ reject(e.data);
55
+ this.working = false;
56
+
57
+ this.submitRemainingRequests();
58
+ };
59
+ }
60
+
61
+ setup(): Promise<void> {
62
+ return this.rpc('setup', []);
63
+ }
64
+
65
+ lint(text: string): Promise<Lint[]> {
66
+ return this.rpc('lint', [text]);
67
+ }
68
+
69
+ applySuggestion(text: string, suggestion: Suggestion, span: Span): Promise<string> {
70
+ return this.rpc('applySuggestion', [text, suggestion, span]);
71
+ }
72
+
73
+ isLikelyEnglish(text: string): Promise<boolean> {
74
+ return this.rpc('isLikelyEnglish', [text]);
75
+ }
76
+
77
+ isolateEnglish(text: string): Promise<string> {
78
+ return this.rpc('isolateEnglish', [text]);
79
+ }
80
+
81
+ async getLintConfig(): Promise<LintConfig> {
82
+ return JSON.parse(await this.getLintConfigAsJSON());
83
+ }
84
+
85
+ setLintConfig(config: LintConfig): Promise<void> {
86
+ return this.setLintConfigWithJSON(JSON.stringify(config));
87
+ }
88
+
89
+ getLintConfigAsJSON(): Promise<string> {
90
+ return this.rpc('getLintConfigAsJSON', []);
91
+ }
92
+
93
+ setLintConfigWithJSON(config: string): Promise<void> {
94
+ return this.rpc('setLintConfigWithJSON', [config]);
95
+ }
96
+
97
+ toTitleCase(text: string): Promise<string> {
98
+ return this.rpc('toTitleCase', [text]);
99
+ }
100
+
101
+ /** Run a procedure on the remote worker. */
102
+ private async rpc(procName: string, args: any[]): Promise<any> {
103
+ const promise = new Promise((resolve, reject) => {
104
+ this.requestQueue.push({
105
+ resolve,
106
+ reject,
107
+ request: { procName, args }
108
+ });
109
+
110
+ this.submitRemainingRequests();
111
+ });
112
+
113
+ return promise;
114
+ }
115
+
116
+ private async submitRemainingRequests() {
117
+ if (this.working) {
118
+ return;
119
+ }
120
+
121
+ this.working = true;
122
+
123
+ if (this.requestQueue.length > 0) {
124
+ const { request } = this.requestQueue[0];
125
+
126
+ this.worker.postMessage(await serialize(request));
127
+ } else {
128
+ this.working = false;
129
+ }
130
+ }
131
+ }
@@ -0,0 +1,24 @@
1
+ import { setWasmUri } from '../loadWasm';
2
+ import LocalLinter from '../LocalLinter';
3
+ import { deserialize, serializeArg } from './communication';
4
+
5
+ const linter = new LocalLinter();
6
+
7
+ /** @param {SerializedRequest} v */
8
+ async function processRequest(v) {
9
+ const { procName, args } = await deserialize(v);
10
+
11
+ let res = await linter[procName](...args);
12
+ postMessage(await serializeArg(res));
13
+ }
14
+
15
+ self.onmessage = function (e) {
16
+ setWasmUri(e.data);
17
+
18
+ self.onmessage = function (e) {
19
+ processRequest(e.data);
20
+ };
21
+ };
22
+
23
+ // Notify the main thread that we are ready
24
+ postMessage('ready');
@@ -0,0 +1,26 @@
1
+ // @ts-expect-error because this virtual module hasn't been added to a `d.ts` file.
2
+ import wasmUri from 'virtual:wasm';
3
+
4
+ let curWasmUri = wasmUri;
5
+
6
+ /** Get the currently set data URI for the WebAssembly module.
7
+ * I'm not a huge of the singleton, but we're swapping out same data, just from a different source, so the state doesn't meaningfully change. */
8
+ export function getWasmUri(): string {
9
+ return curWasmUri;
10
+ }
11
+
12
+ /** Set the data URI for the WebAssembly module. */
13
+ export function setWasmUri(uri: string) {
14
+ curWasmUri = uri;
15
+ }
16
+
17
+ /** Load the WebAssembly manually and dynamically, making sure to setup infrastructure.
18
+ * You can use an optional data URL for the WebAssembly file if the module is being loaded from a Web Worker.
19
+ * */
20
+ export default async function loadWasm() {
21
+ const wasm = await import('wasm');
22
+ // @ts-ignore
23
+ await wasm.default({ module_or_path: getWasmUri() });
24
+
25
+ return wasm;
26
+ }
@@ -0,0 +1,8 @@
1
+ import { expect, test } from 'vitest';
2
+ import { SuggestionKind as WasmSuggestionKind } from 'wasm';
3
+ import { SuggestionKind } from './main';
4
+
5
+ test('Wasm and JS SuggestionKinds agree', async () => {
6
+ expect(SuggestionKind.Remove).toBe(WasmSuggestionKind.Remove);
7
+ expect(SuggestionKind.Replace).toBe(WasmSuggestionKind.Replace);
8
+ });
package/src/main.ts ADDED
@@ -0,0 +1,14 @@
1
+ import type { Lint, Span, Suggestion } from 'wasm';
2
+ import Linter from './Linter';
3
+ import LocalLinter from './LocalLinter';
4
+ import WorkerLinter from './WorkerLinter';
5
+
6
+ export { LocalLinter, WorkerLinter };
7
+ export type { Linter, Lint, Span, Suggestion };
8
+
9
+ export enum SuggestionKind {
10
+ Replace = 0,
11
+ Remove = 1
12
+ }
13
+
14
+ export type LintConfig = Record<string, boolean | undefined>;
package/tsconfig.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "module": "ESNext",
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
+ "skipLibCheck": true,
8
+ /* Bundler mode */
9
+ "moduleResolution": "bundler",
10
+ "allowImportingTsExtensions": true,
11
+ "isolatedModules": true,
12
+ "moduleDetection": "force",
13
+ "noEmit": true,
14
+ /* Linting */
15
+ "strict": true,
16
+ "noUnusedLocals": true,
17
+ "noUnusedParameters": true,
18
+ "noFallthroughCasesInSwitch": true,
19
+ "types": ["vite/client"]
20
+ },
21
+ "include": ["src"]
22
+ }