From 680a9b74cc0569501cdf4ee732e8e2d81ffb8e1e Mon Sep 17 00:00:00 2001 From: Dief Bell Date: Mon, 23 Feb 2026 13:15:30 +0000 Subject: [PATCH 01/18] Updated docs for new API --- README.md | 3 +- package/README.md | 160 +++++++++++++++++++++++++--------------------- 2 files changed, 88 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index 0cba8c0..71c808a 100644 --- a/README.md +++ b/README.md @@ -173,9 +173,10 @@ boperators/ - [x] Vite plugin - [x] ESBuild plugin - [x] Add support for Mozilla / V3 source map format, use in webpack plugin. -- [ ] Drop ts-morph dependency??? +- [ ] ~~Drop ts-morph dependency???~~ NOPE - [ ] A lot of logic in plugins, like `transformer` in the `tsc` plugin, that could be unified in core. - [x] Update main package's README for new plugins +- [ ] Sub-package for using Symbols - [ ] e2e test for Bun plugin and tsc plugin - [ ] ??? diff --git a/package/README.md b/package/README.md index 36b1053..71bbf65 100644 --- a/package/README.md +++ b/package/README.md @@ -1,15 +1,15 @@
# boperators -### Operator overloading JavaScript and TypeScript. +### Operator overloading for JavaScript and TypeScript. ![Sym.JS logo](https://github.com/DiefBell/boperators/blob/653ea138f4dcd1e6b4dd112133a4942f70e91fb3/logo.png)
-Operator overloading is a common programming feature that JavaScript lacks. Just something as simple as adding two vectors, we've got to create a `.Add` method or add elements one-at-a-time. +Operator overloading is a common programming feature that JavaScript lacks. Just something as simple as adding two vectors requires a `.add()` method or element-by-element assignment. -`boperators` finally brings operator overloading to JavaScript by leveraging TypeScript typings. You define one or more overload functions on a class for whichever operators you want, and with magic we search for anywhere you've used overloaded operators and substitute in your functions. +`boperators` brings operator overloading to JavaScript by leveraging TypeScript typings. You define overloaded methods on a class for whichever operators you want, and at build time we find every usage of those operators and substitute in your method calls. This is the core library and API, and isn't designed to be used directly. Instead, you can use: - The [Boperators CLI](https://www.npmjs.com/package/@boperators/cli); @@ -31,142 +31,152 @@ npm install -D boperators @boperators/cli @boperators/plugin-ts-language-server ## Defining Overloads -Define overloads as property arrays on your classes, using the operator string as the property name. Overload fields are readonly arrays (with `as const` at the end) so you can define multiple overloads for different types. As long as you don't have overlapping typings between any functions, we can work out which one to use in a given situation. +Operator overloads are standard TypeScript methods whose name is the operator string. Both string literal names and computed bracket names are supported — they are equivalent: -### Static Operators +```typescript +class Vec2 { + // String literal name + static "+"(a: Vec2, b: Vec2): Vec2 { ... } + + // Computed bracket name — identical behaviour + static ["+"](a: Vec2, b: Vec2): Vec2 { ... } +} +``` -Static operators (`+`, `-`, `*`, `/`, `%`, comparisons, logical) are `static readonly` fields with two-parameter functions (LHS and RHS). At least one parameter must match the class type. +Use whichever style you prefer. The examples below use the bracket style. -Arrow functions or function expressions both work for static operators. + +### Static Operators + +Static operators (`+`, `-`, `*`, `/`, `%`, comparisons, logical) are `static` methods with two parameters (LHS and RHS). At least one parameter must match the class type. ```typescript class Vector3 { - static readonly "+" = [ - (a: Vector3, b: Vector3) => new Vector3(a.x + b.x, a.y + b.y, a.z + b.z), - ] as const; - - // Multiple overloads for different RHS types - static readonly "*" = [ - function (a: Vector3, b: Vector3): Vector3 { + static ["+"](a: Vector3, b: Vector3): Vector3 { + return new Vector3(a.x + b.x, a.y + b.y, a.z + b.z); + } + + // Multiple overloads for different RHS types — use TypeScript overload signatures, + // then handle all cases in a single implementation. + static ["*"](a: Vector3, b: Vector3): Vector3; + static ["*"](a: Vector3, b: number): Vector3; + static ["*"](a: Vector3, b: Vector3 | number): Vector3 { + if (b instanceof Vector3) { return new Vector3( a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x, ); - }, - function mutliplyByScalar(a: Vector3, b: number): Vector3 { - return new Vector3(a.x * b, a.y * b, a.z * b); - } - ] as const; + } + return new Vector3(a.x * b, a.y * b, a.z * b); + } // Comparison operators must return boolean - static readonly "==" = [ - (a: Vector3, b: Vector3): boolean => a.length() === b.length(), - ] as const; + static ["=="](a: Vector3, b: Vector3): boolean { + return a.length() === b.length(); + } } ``` -### Instance Operators -Instance operators (`+=`, `-=`, `*=`, `/=`, `%=`, `&&=`, `||=`) are `readonly` instance fields with a single parameter (the RHS). They use `this` to mutate the LHS object and must return `void`. +### Instance Operators -Instance operators **must** use function expressions (not arrow functions), because arrow functions cannot bind `this`. +Instance operators (`+=`, `-=`, `*=`, `/=`, `%=`, `&&=`, `||=`) are instance methods with a single parameter (the RHS). They use `this` for the LHS and must return `void`. ```typescript class Vector3 { - readonly "+=" = [ - function (this: Vector3, rhs: Vector3): void { - this.x += rhs.x; - this.y += rhs.y; - this.z += rhs.z; - }, - ]; + ["+="](rhs: Vector3): void { + this.x += rhs.x; + this.y += rhs.y; + this.z += rhs.z; + } } ``` -Unlike with JavaScript primitives, you can declare a variable as `const` and still use assignment operators with this, as they're only mutating the object. +Unlike with JavaScript primitives, you can declare a variable as `const` and still use assignment operators with it, since they only mutate the object. ```typescript const vec3 = new Vector3(3, 4, 5); vec3 += new Vector3(6, 7, 8); ``` + ### Prefix Unary Operators -Prefix unary operators (`-`, `+`, `!`, `~`) are `static readonly` fields with one-parameter functions. The parameter must match the class type. For operators that also have binary forms (`-`, `+`), both binary and unary overloads can coexist in the same array, distinguished by parameter count. +Prefix unary operators (`-`, `+`, `!`, `~`) are `static` methods with a single parameter matching the class type. + +For operators that also have a binary form (`-`, `+`), both can live on the same method — just add overload signatures for each, distinguished by parameter count. The implementation then handles all cases. ```typescript class Vector3 { - static readonly "-" = [ - // two parameters means binary operation: `a - b` - (a: Vector3, b: Vector3) => - new Vector3(a.x - b.x, a.y - b.y, a.z - b.z), - // single parameter means unary operation, e.g. making a value "negative" - (a: Vector3) => - new Vector3(-a.x, -a.y, -a.z), // unary: -a - ] as const; - - static readonly "!" = [ - (a: Vector3): boolean => - a.x === 0 && a.y === 0 && a.z === 0, - ] as const; + // Unary-only operator + static ["!"](a: Vector3): boolean { + return a.x === 0 && a.y === 0 && a.z === 0; + } + + // Combined binary + unary on the same operator + static ["-"](a: Vector3, b: Vector3): Vector3; + static ["-"](a: Vector3): Vector3; + static ["-"](a: Vector3, b?: Vector3): Vector3 { + if (b) return new Vector3(a.x - b.x, a.y - b.y, a.z - b.z); + return new Vector3(-a.x, -a.y, -a.z); + } } ``` + ### Postfix Unary Operators -Postfix unary operators (`++`, `--`) are `readonly` instance fields with zero-parameter functions (only `this`). They mutate the object and must return `void`. Must use function expressions, not arrow functions. +Postfix unary operators (`++`, `--`) are instance methods with no parameters. They mutate the object via `this` and must return `void`. ```typescript class Counter { value = 0; - readonly "++" = [ - function (this: Counter): void { - this.value++; - }, - ] as const; + ["++"](): void { + this.value++; + } } ``` + ### Using Overloaded Operators Within Definitions -The transform only applies to **consuming code**, not to the overload definitions themselves. If you need to call an overloaded operator inside an overload body (including on the same class), reference the overload array directly: +The transform only applies to **consuming code**, not to the overload definitions themselves. If you need to call an overloaded operator inside an overload body, call the method directly: ```typescript class Expr { - static readonly "-" = [ - // unary negation - (inner: Expr): Expr => new Expr.Neg(inner), - - // binary minus — calls the unary overload and the + overload directly - (lhs: Expr, rhs: Expr): Expr => - lhs + Expr["-"][0](rhs), - - (lhs: Expr, rhs: number): Expr => - lhs + Expr["-"][0](new Expr.Num(rhs)), - - (lhs: number, rhs: Expr): Expr => - new Expr.Num(lhs) + Expr["-"][0](rhs), - ] as const; + static ["-"](inner: Expr): Expr; + static ["-"](lhs: Expr, rhs: Expr): Expr; + static ["-"](lhs: Expr, rhs: number): Expr; + static ["-"](lhs: number, rhs: Expr): Expr; + static ["-"](lhs: Expr | number, rhs?: Expr | number): Expr { + if (rhs === undefined) return new Expr.Neg(lhs as Expr); + + // Call the overload methods directly — don't use operator syntax here, + // as the source transform has not yet run on this code. + const l = typeof lhs === "number" ? new Expr.Num(lhs) : lhs; + const r = typeof rhs === "number" ? Expr["-"](new Expr.Num(rhs)) : Expr["-"](rhs); + return Expr["+"](l, r); + } } ``` -Writing `lhs + -rhs` inside the overload body would **not** be transformed, since the source transform has not yet run on this code. Use `ClassName["op"][index](args)` for static overloads and `obj["op"][index].call(obj, args)` for instance overloads. ## How It Works `boperators` has a two-phase pipeline: -1. **Parse**: `OverloadStore` scans all source files for classes with operator-named properties and indexes them by `(operatorKind, lhsType, rhsType)`. +1. **Parse**: `OverloadStore` scans all source files for classes with operator-named methods and indexes them by `(operatorKind, lhsType, rhsType)`. 2. **Transform**: `OverloadInjector` finds binary and unary expressions, looks up matching overloads, and replaces them: - - **Binary static**: `a + b` becomes `ClassName["+"][0](a, b)` - - **Binary instance**: `a += b` becomes `a["+="][0].call(a, b)` - - **Prefix unary**: `-a` becomes `ClassName["-"][1](a)` - - **Postfix unary**: `x++` becomes `x["++"][0].call(x)` + - **Binary static**: `a + b` becomes `Vector3["+"](a, b)` + - **Instance compound**: `a += b` becomes `a["+="](b)` + - **Prefix unary**: `-a` becomes `Vector3["-"](a)` + - **Postfix unary**: `x++` becomes `x["++"]( )` Imports for referenced classes are automatically added where needed. + ## Supported Operators | Operator | Type | Notes | @@ -201,10 +211,12 @@ Imports for referenced classes are automatically added where needed. | `++` | instance | Postfix increment, must return `void` | | `--` | instance | Postfix decrement, must return `void` | + ## Conflict Detection When parsing overload definitions, if there are duplicate overloads with matching `(operator, lhsType, rhsType)`, a warning is shown (or an error if `--error-on-warning` is set via the CLI). + ## License MIT From 81adbab38e272f0d7c71957ae2d03e84fd884bf0 Mon Sep 17 00:00:00 2001 From: Dief Bell Date: Mon, 23 Feb 2026 14:04:08 +0000 Subject: [PATCH 02/18] Theoretically now using method overloads instead of const array --- package/src/core/OverloadInjector.ts | 12 +- package/src/core/OverloadStore.ts | 570 +++--------------- .../helpers/getOperatorStringFromMethod.ts | 34 ++ 3 files changed, 135 insertions(+), 481 deletions(-) create mode 100644 package/src/core/helpers/getOperatorStringFromMethod.ts diff --git a/package/src/core/OverloadInjector.ts b/package/src/core/OverloadInjector.ts index 4d61398..3755040 100644 --- a/package/src/core/OverloadInjector.ts +++ b/package/src/core/OverloadInjector.ts @@ -93,7 +93,6 @@ export class OverloadInjector { className: classNameRaw, classFilePath, operatorString, - index, isStatic, } = overloadDesc; @@ -118,8 +117,8 @@ export class OverloadInjector { // Build the text code to replace the binary operator with the overload call const overloadCall = isStatic - ? `${className}["${operatorString}"][${index}](${lhs.getText()}, ${rhs.getText()})` - : `${lhs.getText()}["${operatorString}"][${index}].call(${lhs.getText()}, ${rhs.getText()})`; + ? `${className}["${operatorString}"](${lhs.getText()}, ${rhs.getText()})` + : `${lhs.getText()}["${operatorString}"](${rhs.getText()})`; this._logger.debug( `${fileName}: ${expression.getText()} => ${overloadCall}`, @@ -157,7 +156,6 @@ export class OverloadInjector { className: classNameRaw, classFilePath, operatorString, - index, } = overloadDesc; const classSourceFile = @@ -177,7 +175,7 @@ export class OverloadInjector { classModuleSpecifier, ); - const overloadCall = `${className}["${operatorString}"][${index}](${operand.getText()})`; + const overloadCall = `${className}["${operatorString}"](${operand.getText()})`; this._logger.debug( `${fileName}: ${expression.getText()} => ${overloadCall}`, @@ -211,9 +209,9 @@ export class OverloadInjector { ); if (!overloadDesc) continue; - const { operatorString, index } = overloadDesc; + const { operatorString } = overloadDesc; - const overloadCall = `${operand.getText()}["${operatorString}"][${index}].call(${operand.getText()})`; + const overloadCall = `${operand.getText()}["${operatorString}"]()`; this._logger.debug( `${fileName}: ${expression.getText()} => ${overloadCall}`, diff --git a/package/src/core/OverloadStore.ts b/package/src/core/OverloadStore.ts index 428bd15..5a99443 100644 --- a/package/src/core/OverloadStore.ts +++ b/package/src/core/OverloadStore.ts @@ -1,11 +1,7 @@ import { - type ArrowFunction, type ClassDeclaration, - type FunctionDeclaration, - type FunctionExpression, - Node, + type MethodDeclaration, type ParameterDeclaration, - type PropertyDeclaration, SourceFile, SyntaxKind, type Project as TsMorphProject, @@ -13,9 +9,8 @@ import { import { operatorSymbols } from "../lib/operatorSymbols"; import type { BopLogger } from "./BopConfig"; import { ErrorDescription, type ErrorManager } from "./ErrorManager"; -import { getOperatorStringFromProperty } from "./helpers/getOperatorStringFromProperty"; +import { getOperatorStringFromMethod } from "./helpers/getOperatorStringFromMethod"; import { normalizeTypeName } from "./helpers/resolveExpressionType"; -import { unwrapInitializer } from "./helpers/unwrapInitializer"; import { comparisonOperators, instanceOperators, @@ -57,7 +52,6 @@ export type OverloadDescription = { className: string; classFilePath: string; operatorString: string; - index: number; returnType: string; }; @@ -70,7 +64,6 @@ export type OverloadInfo = { className: string; classFilePath: string; operatorString: string; - index: number; isStatic: boolean; /** LHS type name (binary overloads only). */ lhsType?: string; @@ -308,16 +301,36 @@ export class OverloadStore extends Map< const classes = sourceFile.getClasses(); classes.forEach((classDecl) => { + const className = classDecl.getName(); + if (!className) return; // skip anonymous classes + const classType = normalizeTypeName(classDecl.getType().getText()); - classDecl.getProperties().forEach((property) => { - if (!Node.isPropertyDeclaration(property)) return; + // Group method declarations by operator string. + // Each group may contain overload signatures (no body) and an implementation (with body). + const methodGroups = new Map(); + for (const method of classDecl.getMethods()) { + const operatorString = getOperatorStringFromMethod(method); + if (!operatorString || !operatorSymbols.includes(operatorString)) + continue; + let group = methodGroups.get(operatorString); + if (!group) { + group = []; + methodGroups.set(operatorString, group); + } + group.push(method); + } + + methodGroups.forEach((methods, operatorString) => { + // Use overload signatures (no body); fall back to the implementation if there are none. + // In .d.ts files all method declarations lack a body, so they are naturally treated + // as overload signatures without any special-casing. + const overloadSigs = methods.filter((m) => !m.hasBody()); + const sigsToProcess = overloadSigs.length > 0 ? overloadSigs : methods; - const isStatic = property.isStatic(); + if (sigsToProcess.length === 0) return; - const operatorString = getOperatorStringFromProperty(property); - if (!operatorString || !operatorSymbols.includes(operatorString)) - return; + const isStatic = sigsToProcess[0].isStatic(); // Look up the operator in all three maps const binarySyntaxKind = operatorMap[operatorString as OperatorString]; @@ -333,8 +346,7 @@ export class OverloadStore extends Map< ) return; - // Property-level static/instance validation. - // Determine what this property should be based on the operator kinds it supports. + // Validate static/instance context at the method group level const shouldBeStatic = (binarySyntaxKind != null && !instanceOperators.has(binarySyntaxKind)) || @@ -347,110 +359,23 @@ export class OverloadStore extends Map< if ((isStatic && !shouldBeStatic) || (!isStatic && !shouldBeInstance)) { this._errorManager.addWarning( new ErrorDescription( - `Expected overload for operator ${operatorString} ` + - `to be ${isStatic ? "a static" : "an instance"} field.`, - property.getSourceFile().getFilePath(), - property.getStartLineNumber(), - property.getText().split("\n")[0], + `Expected overload for operator "${operatorString}" ` + + `to be ${isStatic ? "a static" : "an instance"} method.`, + sigsToProcess[0].getSourceFile().getFilePath(), + sigsToProcess[0].getStartLineNumber(), + this._minifyString(sigsToProcess[0].getText().split("\n")[0]), ), ); return; } - const rawInitializer = property.getInitializer(); - - // No initializer — try type-annotation-based extraction (.d.ts files) - if (!rawInitializer) { - this._addOverloadsFromTypeAnnotation( - property, - classDecl, - classType, - filePath, - isStatic, - operatorString, - binarySyntaxKind, - prefixUnarySyntaxKind, - postfixUnarySyntaxKind, - ); - return; - } - - const hasAsConst = - Node.isAsExpression(rawInitializer) && - rawInitializer.getTypeNode()?.getText() === "const"; - - const initializer = unwrapInitializer(rawInitializer); - - if (!initializer || !Node.isArrayLiteralExpression(initializer)) { - this._errorManager.addWarning( - new ErrorDescription( - `Overload field for operator ${operatorString} ` + - "must be an array of overload functions.", - property.getSourceFile().getFilePath(), - property.getStartLineNumber(), - this._minifyString(property.getName()), - ), - ); - return; - } - - if (!hasAsConst) { - this._errorManager.addError( - new ErrorDescription( - `Overload array for operator ${operatorString} must use "as const". ` + - "Without it, TypeScript widens the array type and loses individual " + - "function signatures, causing type errors in generated code.", - property.getSourceFile().getFilePath(), - property.getStartLineNumber(), - this._minifyString(property.getText().split("\n")[0] ?? ""), - ), - ); - return; - } - - initializer.getElements().forEach((element, index) => { - if (element.isKind(SyntaxKind.ArrowFunction) && !isStatic) { - this._errorManager.addError( - new ErrorDescription( - `Overload ${index} for operator ${operatorString} must not be an arrow function. ` + - "Use a function expression instead, as arrow functions cannot bind `this` correctly for instance operators.", - element.getSourceFile().getFilePath(), - element.getStartLineNumber(), - this._minifyString(element.getText()), - ), - ); - return; - } - - if ( - !element.isKind(SyntaxKind.FunctionExpression) && - !element.isKind(SyntaxKind.FunctionDeclaration) && - !element.isKind(SyntaxKind.ArrowFunction) - ) { - this._errorManager.addWarning( - new ErrorDescription( - `Expected overload ${index} for operator ${operatorString} to be a function.`, - element.getSourceFile().getFilePath(), - element.getStartLineNumber(), - this._minifyString(element.getText()), - ), - ); - return; - } - - // At this point element is guaranteed to be a function-like node - const funcElement = element as - | FunctionExpression - | ArrowFunction - | FunctionDeclaration; - - // Exclude `this` pseudo-parameter from count - const parameters = funcElement + sigsToProcess.forEach((method) => { + // Exclude the `this` pseudo-parameter if explicitly declared + const parameters = method .getParameters() .filter((p) => p.getName() !== "this"); const paramCount = parameters.length; - // Dispatch by parameter count to determine overload kind if ( paramCount === 2 && isStatic && @@ -459,14 +384,12 @@ export class OverloadStore extends Map< ) { this._addBinaryOverload( binarySyntaxKind, - classDecl, + className, classType, filePath, - property, - funcElement, + method, parameters, operatorString, - index, true, ); } else if ( @@ -477,47 +400,41 @@ export class OverloadStore extends Map< ) { this._addBinaryOverload( binarySyntaxKind, - classDecl, + className, classType, filePath, - property, - funcElement, + method, parameters, operatorString, - index, false, ); } else if (paramCount === 1 && isStatic && prefixUnarySyntaxKind) { this._addPrefixUnaryOverload( prefixUnarySyntaxKind, - classDecl, + className, classType, filePath, - property, - funcElement, + method, parameters, operatorString, - index, ); } else if (paramCount === 0 && !isStatic && postfixUnarySyntaxKind) { this._addPostfixUnaryOverload( postfixUnarySyntaxKind, - classDecl, + className, classType, filePath, - property, - funcElement as FunctionExpression | FunctionDeclaration, + method, operatorString, - index, ); } else { this._errorManager.addWarning( new ErrorDescription( - `Overload function ${index} for operator ${operatorString} ` + + `Overload signature for operator "${operatorString}" ` + `has invalid parameter count (${paramCount}) for this operator context.`, - property.getSourceFile().getFilePath(), - property.getStartLineNumber(), - this._minifyString(funcElement.getText()), + method.getSourceFile().getFilePath(), + method.getStartLineNumber(), + this._minifyString(method.getText().split("\n")[0]), ), ); } @@ -528,14 +445,12 @@ export class OverloadStore extends Map< private _addBinaryOverload( syntaxKind: OperatorSyntaxKind, - classDecl: ClassDeclaration, + className: string, classType: string, filePath: string, - property: PropertyDeclaration, - element: FunctionExpression | ArrowFunction | FunctionDeclaration, + method: MethodDeclaration, parameters: ParameterDeclaration[], operatorString: string, - index: number, isStatic: boolean, ): void { let hasWarning = false; @@ -550,26 +465,26 @@ export class OverloadStore extends Map< if (isStatic && lhsType !== classType && rhsType !== classType) { this._errorManager.addWarning( new ErrorDescription( - `Overload for operator ${operatorString} ` + + `Overload for operator "${operatorString}" ` + "must have either LHS or RHS parameter matching its class type.", - property.getSourceFile().getFilePath(), - property.getStartLineNumber(), - this._minifyString(element.getText()), + method.getSourceFile().getFilePath(), + method.getStartLineNumber(), + this._minifyString(method.getText().split("\n")[0]), ), ); hasWarning = true; } - const returnType = element.getReturnType().getText(); + const returnType = method.getReturnType().getText(); if (comparisonOperators.has(syntaxKind) && returnType !== "boolean") { this._errorManager.addWarning( new ErrorDescription( - `Overload function ${index} for comparison operator ${operatorString} ` + + `Overload for comparison operator "${operatorString}" ` + `must have a return type of 'boolean', got '${returnType}'.`, - property.getSourceFile().getFilePath(), - property.getStartLineNumber(), - this._minifyString(element.getText()), + method.getSourceFile().getFilePath(), + method.getStartLineNumber(), + this._minifyString(method.getText().split("\n")[0]), ), ); hasWarning = true; @@ -578,11 +493,11 @@ export class OverloadStore extends Map< if (!isStatic && returnType !== "void") { this._errorManager.addWarning( new ErrorDescription( - `Overload function ${index} for instance operator ${operatorString} ` + + `Overload for instance operator "${operatorString}" ` + `must have a return type of 'void', got '${returnType}'.`, - property.getSourceFile().getFilePath(), - property.getStartLineNumber(), - this._minifyString(element.getText()), + method.getSourceFile().getFilePath(), + method.getStartLineNumber(), + this._minifyString(method.getText().split("\n")[0]), ), ); hasWarning = true; @@ -598,10 +513,10 @@ export class OverloadStore extends Map< if (lhsMap.has(rhsType)) { this._errorManager.addWarning( new ErrorDescription( - `Duplicate overload for operator ${operatorString} with LHS type ${lhsType} and RHS type ${rhsType}`, - property.getSourceFile().getFilePath(), - property.getStartLineNumber(), - this._minifyString(element.getText()), + `Duplicate overload for operator "${operatorString}" with LHS type ${lhsType} and RHS type ${rhsType}`, + method.getSourceFile().getFilePath(), + method.getStartLineNumber(), + this._minifyString(method.getText().split("\n")[0]), ), ); hasWarning = true; @@ -611,24 +526,17 @@ export class OverloadStore extends Map< lhsMap.set(rhsType, { isStatic, - className: classDecl.getName()!, + className: className, classFilePath: filePath, operatorString, - index, returnType, }); operatorOverloads.set(lhsType, lhsMap); this.set(syntaxKind, operatorOverloads); - const funcName = Node.isFunctionExpression(element) - ? element.getName() - : undefined; const sl = this._shortTypeName.bind(this); - const label = funcName - ? `${funcName}(${sl(lhsType)}, ${sl(rhsType)})` - : `(${sl(lhsType)}, ${sl(rhsType)})`; this._logger.debug( - `Loaded ${classDecl.getName()}["${operatorString}"][${index}]: ${label} => ${sl(element.getReturnType().getText())}${isStatic ? " (static)" : " (instance)"}`, + `Loaded ${className}["${operatorString}"]: (${sl(lhsType)}, ${sl(rhsType)}) => ${sl(returnType)}${isStatic ? " (static)" : " (instance)"}`, ); let fileEntries = this._fileEntries.get(filePath); @@ -641,14 +549,12 @@ export class OverloadStore extends Map< private _addPrefixUnaryOverload( syntaxKind: PrefixUnaryOperatorSyntaxKind, - classDecl: ClassDeclaration, + className: string, classType: string, filePath: string, - property: PropertyDeclaration, - element: FunctionExpression | ArrowFunction | FunctionDeclaration, + method: MethodDeclaration, parameters: ParameterDeclaration[], operatorString: string, - index: number, ): void { let hasWarning = false; @@ -659,11 +565,11 @@ export class OverloadStore extends Map< if (operandType !== classType) { this._errorManager.addWarning( new ErrorDescription( - `Prefix unary overload for operator ${operatorString} ` + + `Prefix unary overload for operator "${operatorString}" ` + "must have its parameter matching its class type.", - property.getSourceFile().getFilePath(), - property.getStartLineNumber(), - this._minifyString(element.getText()), + method.getSourceFile().getFilePath(), + method.getStartLineNumber(), + this._minifyString(method.getText().split("\n")[0]), ), ); hasWarning = true; @@ -676,10 +582,10 @@ export class OverloadStore extends Map< if (operatorOverloads.has(operandType)) { this._errorManager.addWarning( new ErrorDescription( - `Duplicate prefix unary overload for operator ${operatorString} with operand type ${operandType}`, - property.getSourceFile().getFilePath(), - property.getStartLineNumber(), - this._minifyString(element.getText()), + `Duplicate prefix unary overload for operator "${operatorString}" with operand type ${operandType}`, + method.getSourceFile().getFilePath(), + method.getStartLineNumber(), + this._minifyString(method.getText().split("\n")[0]), ), ); hasWarning = true; @@ -687,27 +593,20 @@ export class OverloadStore extends Map< if (hasWarning) return; - const returnType = element.getReturnType().getText(); + const returnType = method.getReturnType().getText(); operatorOverloads.set(operandType, { isStatic: true, - className: classDecl.getName()!, + className: className, classFilePath: filePath, operatorString, - index, returnType, }); this._prefixUnaryOverloads.set(syntaxKind, operatorOverloads); - const funcName = Node.isFunctionExpression(element) - ? element.getName() - : undefined; const sl = this._shortTypeName.bind(this); - const label = funcName - ? `${funcName}(${sl(operandType)})` - : `(${sl(operandType)})`; this._logger.debug( - `Loaded ${classDecl.getName()}["${operatorString}"][${index}]: ${operatorString}${label} => ${sl(returnType)} (prefix unary)`, + `Loaded ${className}["${operatorString}"]: ${operatorString}(${sl(operandType)}) => ${sl(returnType)} (prefix unary)`, ); let fileEntries = this._prefixUnaryFileEntries.get(filePath); @@ -720,26 +619,24 @@ export class OverloadStore extends Map< private _addPostfixUnaryOverload( syntaxKind: PostfixUnaryOperatorSyntaxKind, - classDecl: ClassDeclaration, + className: string, classType: string, filePath: string, - property: PropertyDeclaration, - element: FunctionExpression | FunctionDeclaration, + method: MethodDeclaration, operatorString: string, - index: number, ): void { let hasWarning = false; - const returnType = element.getReturnType().getText(); + const returnType = method.getReturnType().getText(); if (returnType !== "void") { this._errorManager.addWarning( new ErrorDescription( - `Overload function ${index} for postfix operator ${operatorString} ` + + `Overload for postfix operator "${operatorString}" ` + `must have a return type of 'void', got '${returnType}'.`, - property.getSourceFile().getFilePath(), - property.getStartLineNumber(), - this._minifyString(element.getText()), + method.getSourceFile().getFilePath(), + method.getStartLineNumber(), + this._minifyString(method.getText().split("\n")[0]), ), ); hasWarning = true; @@ -753,10 +650,10 @@ export class OverloadStore extends Map< if (operatorOverloads.has(operandType)) { this._errorManager.addWarning( new ErrorDescription( - `Duplicate postfix unary overload for operator ${operatorString} with operand type ${operandType}`, - property.getSourceFile().getFilePath(), - property.getStartLineNumber(), - this._minifyString(element.getText()), + `Duplicate postfix unary overload for operator "${operatorString}" with operand type ${operandType}`, + method.getSourceFile().getFilePath(), + method.getStartLineNumber(), + this._minifyString(method.getText().split("\n")[0]), ), ); hasWarning = true; @@ -766,20 +663,15 @@ export class OverloadStore extends Map< operatorOverloads.set(operandType, { isStatic: false, - className: classDecl.getName()!, + className: className, classFilePath: filePath, operatorString, - index, returnType, }); this._postfixUnaryOverloads.set(syntaxKind, operatorOverloads); - const funcName = Node.isFunctionExpression(element) - ? element.getName() - : undefined; - const label = funcName ? `${funcName}()` : "()"; this._logger.debug( - `Loaded ${classDecl.getName()}["${operatorString}"][${index}]: ${this._shortTypeName(operandType)}${operatorString} ${label} (postfix unary)`, + `Loaded ${className}["${operatorString}"]: ${this._shortTypeName(operandType)}${operatorString} () (postfix unary)`, ); let fileEntries = this._postfixUnaryFileEntries.get(filePath); @@ -790,276 +682,6 @@ export class OverloadStore extends Map< fileEntries.push({ syntaxKind, operandType }); } - /** - * Extracts overload info from a property's type annotation instead of its - * initializer. This handles `.d.ts` declaration files where the array - * literal (`= [...] as const`) has been replaced by a readonly tuple type. - */ - private _addOverloadsFromTypeAnnotation( - property: PropertyDeclaration, - classDecl: ClassDeclaration, - classType: string, - filePath: string, - isStatic: boolean, - operatorString: string, - binarySyntaxKind: OperatorSyntaxKind | undefined, - prefixUnarySyntaxKind: PrefixUnaryOperatorSyntaxKind | undefined, - postfixUnarySyntaxKind: PostfixUnaryOperatorSyntaxKind | undefined, - ): void { - const propertyType = property.getType(); - if (!propertyType.isTuple()) return; - - const tupleElements = propertyType.getTupleElements(); - const className = classDecl.getName(); - if (!className) return; - - const sl = this._shortTypeName.bind(this); - - for (let index = 0; index < tupleElements.length; index++) { - const elementType = tupleElements[index]; - const callSigs = elementType.getCallSignatures(); - if (callSigs.length === 0) continue; - - const sig = callSigs[0]; - - // Extract parameter types, filtering out the `this` pseudo-parameter - const params: { name: string; type: string }[] = []; - for (const sym of sig.getParameters()) { - const name = sym.getName(); - if (name === "this") continue; - const decl = sym.getValueDeclaration(); - if (!decl) continue; - params.push({ - name, - type: normalizeTypeName(decl.getType().getText()), - }); - } - const paramCount = params.length; - const returnType = sig.getReturnType().getText(); - - // --- Binary static --- - if ( - paramCount === 2 && - isStatic && - binarySyntaxKind && - !instanceOperators.has(binarySyntaxKind) - ) { - const lhsType = params[0].type; - const rhsType = params[1].type; - - if (lhsType !== classType && rhsType !== classType) { - this._errorManager.addWarning( - new ErrorDescription( - `Overload for operator ${operatorString} ` + - "must have either LHS or RHS parameter matching its class type.", - filePath, - property.getStartLineNumber(), - this._minifyString(property.getText().split("\n")[0] ?? ""), - ), - ); - continue; - } - - if ( - comparisonOperators.has(binarySyntaxKind) && - returnType !== "boolean" - ) { - this._errorManager.addWarning( - new ErrorDescription( - `Overload function ${index} for comparison operator ${operatorString} ` + - `must have a return type of 'boolean', got '${returnType}'.`, - filePath, - property.getStartLineNumber(), - this._minifyString(property.getText().split("\n")[0] ?? ""), - ), - ); - continue; - } - - const operatorOverloads = - this.get(binarySyntaxKind) ?? - new Map>(); - const lhsMap = - operatorOverloads.get(lhsType) ?? - new Map(); - - if (lhsMap.has(rhsType)) continue; // duplicate — skip silently for .d.ts - - lhsMap.set(rhsType, { - isStatic: true, - className, - classFilePath: filePath, - operatorString, - index, - returnType, - }); - operatorOverloads.set(lhsType, lhsMap); - this.set(binarySyntaxKind, operatorOverloads); - - this._logger.debug( - `Loaded ${className}["${operatorString}"][${index}]: (${sl(lhsType)}, ${sl(rhsType)}) => ${sl(returnType)} (static, from .d.ts)`, - ); - - let fileEntries = this._fileEntries.get(filePath); - if (!fileEntries) { - fileEntries = []; - this._fileEntries.set(filePath, fileEntries); - } - fileEntries.push({ syntaxKind: binarySyntaxKind, lhsType, rhsType }); - } - - // --- Binary instance (compound assignment) --- - else if ( - paramCount === 1 && - !isStatic && - binarySyntaxKind && - instanceOperators.has(binarySyntaxKind) - ) { - const lhsType = classType; - const rhsType = params[0].type; - - if (returnType !== "void") { - this._errorManager.addWarning( - new ErrorDescription( - `Overload function ${index} for instance operator ${operatorString} ` + - `must have a return type of 'void', got '${returnType}'.`, - filePath, - property.getStartLineNumber(), - this._minifyString(property.getText().split("\n")[0] ?? ""), - ), - ); - continue; - } - - const operatorOverloads = - this.get(binarySyntaxKind) ?? - new Map>(); - const lhsMap = - operatorOverloads.get(lhsType) ?? - new Map(); - - if (lhsMap.has(rhsType)) continue; - - lhsMap.set(rhsType, { - isStatic: false, - className, - classFilePath: filePath, - operatorString, - index, - returnType, - }); - operatorOverloads.set(lhsType, lhsMap); - this.set(binarySyntaxKind, operatorOverloads); - - this._logger.debug( - `Loaded ${className}["${operatorString}"][${index}]: (${sl(lhsType)}, ${sl(rhsType)}) => void (instance, from .d.ts)`, - ); - - let fileEntries = this._fileEntries.get(filePath); - if (!fileEntries) { - fileEntries = []; - this._fileEntries.set(filePath, fileEntries); - } - fileEntries.push({ syntaxKind: binarySyntaxKind, lhsType, rhsType }); - } - - // --- Prefix unary --- - else if (paramCount === 1 && isStatic && prefixUnarySyntaxKind) { - const operandType = params[0].type; - - if (operandType !== classType) { - this._errorManager.addWarning( - new ErrorDescription( - `Prefix unary overload for operator ${operatorString} ` + - "must have its parameter matching its class type.", - filePath, - property.getStartLineNumber(), - this._minifyString(property.getText().split("\n")[0] ?? ""), - ), - ); - continue; - } - - const operatorOverloads = - this._prefixUnaryOverloads.get(prefixUnarySyntaxKind) ?? - new Map(); - - if (operatorOverloads.has(operandType)) continue; - - operatorOverloads.set(operandType, { - isStatic: true, - className, - classFilePath: filePath, - operatorString, - index, - returnType, - }); - this._prefixUnaryOverloads.set( - prefixUnarySyntaxKind, - operatorOverloads, - ); - - this._logger.debug( - `Loaded ${className}["${operatorString}"][${index}]: ${operatorString}(${sl(operandType)}) => ${sl(returnType)} (prefix unary, from .d.ts)`, - ); - - let fileEntries = this._prefixUnaryFileEntries.get(filePath); - if (!fileEntries) { - fileEntries = []; - this._prefixUnaryFileEntries.set(filePath, fileEntries); - } - fileEntries.push({ syntaxKind: prefixUnarySyntaxKind, operandType }); - } - - // --- Postfix unary --- - else if (paramCount === 0 && !isStatic && postfixUnarySyntaxKind) { - if (returnType !== "void") { - this._errorManager.addWarning( - new ErrorDescription( - `Overload function ${index} for postfix operator ${operatorString} ` + - `must have a return type of 'void', got '${returnType}'.`, - filePath, - property.getStartLineNumber(), - this._minifyString(property.getText().split("\n")[0] ?? ""), - ), - ); - continue; - } - - const operandType = classType; - const operatorOverloads = - this._postfixUnaryOverloads.get(postfixUnarySyntaxKind) ?? - new Map(); - - if (operatorOverloads.has(operandType)) continue; - - operatorOverloads.set(operandType, { - isStatic: false, - className, - classFilePath: filePath, - operatorString, - index, - returnType, - }); - this._postfixUnaryOverloads.set( - postfixUnarySyntaxKind, - operatorOverloads, - ); - - this._logger.debug( - `Loaded ${className}["${operatorString}"][${index}]: ${sl(operandType)}${operatorString} () (postfix unary, from .d.ts)`, - ); - - let fileEntries = this._postfixUnaryFileEntries.get(filePath); - if (!fileEntries) { - fileEntries = []; - this._postfixUnaryFileEntries.set(filePath, fileEntries); - } - fileEntries.push({ syntaxKind: postfixUnarySyntaxKind, operandType }); - } - } - } - /** * Returns the type hierarchy chain for a given type name: * [self, parent, grandparent, ...]. Primitives like "number" diff --git a/package/src/core/helpers/getOperatorStringFromMethod.ts b/package/src/core/helpers/getOperatorStringFromMethod.ts new file mode 100644 index 0000000..009bd8d --- /dev/null +++ b/package/src/core/helpers/getOperatorStringFromMethod.ts @@ -0,0 +1,34 @@ +import { type MethodDeclaration, SyntaxKind } from "ts-morph"; + +/** + * Extracts the operator string from a class method declaration, if it + * represents an operator overload. + * + * Handles three method name styles: + * - `["+"]` — ComputedPropertyName with a StringLiteral expression + * - `[Operator.PLUS]` — ComputedPropertyName with an enum member expression + * - `"+"` — StringLiteral method name + * + * Returns `undefined` if the method name doesn't resolve to an operator string. + */ +export function getOperatorStringFromMethod( + method: MethodDeclaration, +): string | undefined { + const nameNode = method.getNameNode(); + + if (nameNode.isKind(SyntaxKind.ComputedPropertyName)) { + const expression = nameNode.getExpression(); + if (expression.isKind(SyntaxKind.StringLiteral)) { + return expression.getLiteralValue(); + } + // Handle Operator.PLUS style (enum member access) + const literalValue = expression.getType().getLiteralValue(); + if (typeof literalValue === "string") { + return literalValue; + } + } else if (nameNode.isKind(SyntaxKind.StringLiteral)) { + return nameNode.getLiteralValue(); + } + + return undefined; +} From 313cf1e1a92465761a17ac844a6e2b5577d54924 Mon Sep 17 00:00:00 2001 From: Dief Bell Date: Mon, 23 Feb 2026 14:04:20 +0000 Subject: [PATCH 03/18] chore: bumped version to 0.3.0 --- cli/package.json | 4 ++-- mcp-server/package.json | 4 ++-- package.json | 3 ++- package/package.json | 2 +- plugins/bun/package.json | 4 ++-- plugins/esbuild/package.json | 4 ++-- plugins/ts-language-server/package.json | 4 ++-- plugins/tsc/package.json | 4 ++-- plugins/vite/package.json | 4 ++-- plugins/webpack/package.json | 4 ++-- 10 files changed, 19 insertions(+), 18 deletions(-) diff --git a/cli/package.json b/cli/package.json index 40b22cb..deb3f92 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@boperators/cli", - "version": "0.2.1", + "version": "0.3.0", "license": "MIT", "description": "CLI tool for boperators - transforms TypeScript files with operator overloads.", "repository": { @@ -43,7 +43,7 @@ "@commander-js/extra-typings": "^13.0.0" }, "peerDependencies": { - "boperators": "0.2.1", + "boperators": "0.3.0", "typescript": ">=5.0.0 <5.10.0" }, "devDependencies": { diff --git a/mcp-server/package.json b/mcp-server/package.json index 7e98caf..d605177 100644 --- a/mcp-server/package.json +++ b/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "@boperators/mcp-server", - "version": "0.2.1", + "version": "0.3.0", "license": "MIT", "description": "MCP server for boperators - gives AI assistants access to operator overload information.", "repository": { @@ -42,7 +42,7 @@ ], "dependencies": { "@modelcontextprotocol/sdk": "^1.12.1", - "boperators": "0.2.1", + "boperators": "0.3.0", "zod": "^3.25.0" }, "devDependencies": { diff --git a/package.json b/package.json index d6537d0..195d004 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "format": "biome format --write .", "watch": "concurrently \"bun run --filter boperators watch\" \"bun run --filter @boperators/plugin-ts-language-server watch\" \"bun run --filter @boperators/plugin-tsc watch\" \"bun run --filter @boperators/cli watch\"", "example": "cd example && bun run transform && bun run start", - "test:e2e": "cd example && bun install && bun run webpack" + "test:e2e": "cd example && bun install && bun run webpack", + "bump-versions": "bun run ./scripts/bumpVersion.ts" }, "devDependencies": { "@biomejs/biome": "^2.3.14", diff --git a/package/package.json b/package/package.json index 829d424..9320ca7 100644 --- a/package/package.json +++ b/package/package.json @@ -1,6 +1,6 @@ { "name": "boperators", - "version": "0.2.1", + "version": "0.3.0", "license": "MIT", "description": "Operator overloading for TypeScript.", "repository": { diff --git a/plugins/bun/package.json b/plugins/bun/package.json index ca97b56..179e296 100644 --- a/plugins/bun/package.json +++ b/plugins/bun/package.json @@ -1,6 +1,6 @@ { "name": "@boperators/plugin-bun", - "version": "0.2.1", + "version": "0.3.0", "license": "MIT", "description": "Bun plugin for boperators - transforms operator overloads at runtime.", "repository": { @@ -33,7 +33,7 @@ "index.ts" ], "peerDependencies": { - "boperators": "0.2.1", + "boperators": "0.3.0", "typescript": ">=5.0.0 <5.10.0" }, "devDependencies": { diff --git a/plugins/esbuild/package.json b/plugins/esbuild/package.json index 3ac2fbd..74b2826 100644 --- a/plugins/esbuild/package.json +++ b/plugins/esbuild/package.json @@ -1,6 +1,6 @@ { "name": "@boperators/plugin-esbuild", - "version": "0.2.1", + "version": "0.3.0", "license": "MIT", "description": "esbuild plugin for boperators - transforms operator overloads at build time.", "repository": { @@ -39,7 +39,7 @@ "dist" ], "peerDependencies": { - "boperators": "0.2.1", + "boperators": "0.3.0", "esbuild": ">=0.17.0", "typescript": ">=5.0.0 <5.10.0" }, diff --git a/plugins/ts-language-server/package.json b/plugins/ts-language-server/package.json index d90cfd2..a46ebc1 100644 --- a/plugins/ts-language-server/package.json +++ b/plugins/ts-language-server/package.json @@ -1,6 +1,6 @@ { "name": "@boperators/plugin-ts-language-server", - "version": "0.2.1", + "version": "0.3.0", "license": "MIT", "description": "TypeScript Language Server plugin for boperators - IDE support with source mapping.", "repository": { @@ -36,7 +36,7 @@ "dist" ], "peerDependencies": { - "boperators": "0.2.1", + "boperators": "0.3.0", "typescript": ">=5.0.0 <5.10.0" }, "devDependencies": { diff --git a/plugins/tsc/package.json b/plugins/tsc/package.json index 9117f40..0856424 100644 --- a/plugins/tsc/package.json +++ b/plugins/tsc/package.json @@ -1,6 +1,6 @@ { "name": "@boperators/plugin-tsc", - "version": "0.2.1", + "version": "0.3.0", "license": "MIT", "description": "ts-patch plugin for boperators - transforms operator overloads during tsc compilation.", "repository": { @@ -36,7 +36,7 @@ "dist" ], "peerDependencies": { - "boperators": "0.2.1", + "boperators": "0.3.0", "typescript": ">=5.0.0 <5.10.0" }, "devDependencies": { diff --git a/plugins/vite/package.json b/plugins/vite/package.json index b895d15..3fe29a2 100644 --- a/plugins/vite/package.json +++ b/plugins/vite/package.json @@ -1,6 +1,6 @@ { "name": "@boperators/plugin-vite", - "version": "0.2.1", + "version": "0.3.0", "license": "MIT", "description": "Vite plugin for boperators - transforms operator overloads at build time.", "repository": { @@ -39,7 +39,7 @@ "dist" ], "peerDependencies": { - "boperators": "0.2.1", + "boperators": "0.3.0", "typescript": ">=5.0.0 <5.10.0", "vite": ">=4.0.0" }, diff --git a/plugins/webpack/package.json b/plugins/webpack/package.json index 3281a1a..ba66ba7 100644 --- a/plugins/webpack/package.json +++ b/plugins/webpack/package.json @@ -1,6 +1,6 @@ { "name": "@boperators/webpack-loader", - "version": "0.2.1", + "version": "0.3.0", "license": "MIT", "description": "webpack loader for boperators", "repository": { @@ -37,7 +37,7 @@ "dist" ], "peerDependencies": { - "boperators": "0.2.1", + "boperators": "0.3.0", "typescript": ">=5.0.0 <5.10.0" }, "devDependencies": { From c2b61870c2cc71805117bf8dafbf210acf496485 Mon Sep 17 00:00:00 2001 From: Dief Bell Date: Mon, 23 Feb 2026 14:10:50 +0000 Subject: [PATCH 04/18] Fixed a couple lint issues --- package/src/core/OverloadStore.ts | 1 - plugins/vite/src/index.test.ts | 9 +++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/package/src/core/OverloadStore.ts b/package/src/core/OverloadStore.ts index 5a99443..67844b0 100644 --- a/package/src/core/OverloadStore.ts +++ b/package/src/core/OverloadStore.ts @@ -1,5 +1,4 @@ import { - type ClassDeclaration, type MethodDeclaration, type ParameterDeclaration, SourceFile, diff --git a/plugins/vite/src/index.test.ts b/plugins/vite/src/index.test.ts index 60f40e6..d721ae8 100644 --- a/plugins/vite/src/index.test.ts +++ b/plugins/vite/src/index.test.ts @@ -70,10 +70,8 @@ describe("@boperators/plugin-vite", () => { source: string, filePath: string, ): { code: string; map: unknown } | null | undefined { - // biome-ignore lint/suspicious/noExplicitAny: mocking Rollup plugin context - const fn = plugin.transform as - | ((code: string, id: string) => any) - | undefined; + // biome-ignore lint/complexity/noBannedTypes: TODO, address this properly if we can + const fn = plugin.transform as Function | undefined; return fn?.call({} as never, source, filePath); } @@ -133,9 +131,8 @@ describe("@boperators/plugin-vite", () => { root: tmpDir, }); - // biome-ignore lint/suspicious/noExplicitAny: mocking Rollup plugin context const fn = plugin2.transform as - | ((code: string, id: string) => any) + | ((code: string, id: string) => { code?: string }) | undefined; const result = fn?.call( {} as never, From 5de6ef95f56fe019f5685f698e1d192de600a724 Mon Sep 17 00:00:00 2001 From: Dief Bell Date: Mon, 23 Feb 2026 14:57:57 +0000 Subject: [PATCH 05/18] Updated CLI example --- Claude.md | 2 + examples/cli/src/ColoredVec2.ts | 7 +- examples/cli/src/Mat4.ts | 53 +++--- examples/cli/src/Vec2.ts | 220 ++++++++++------------ examples/cli/src/Vec3.ts | 117 ++++++------ examples/cli/src/test.ts | 16 +- package/src/core/OverloadInjector.test.ts | 18 +- package/src/core/OverloadStore.ts | 38 ++-- 8 files changed, 232 insertions(+), 239 deletions(-) diff --git a/Claude.md b/Claude.md index 3c94782..8f1000c 100644 --- a/Claude.md +++ b/Claude.md @@ -1 +1,3 @@ Always start a session by the openmemory MCP server. Always remind yourself to do this in conversation summaries between sessions. Always store important facts about the repository in the openmemory MCP server. + +Where possible, use scripts from `package.json` files with Bun instead of directly running the commands or using NPM. diff --git a/examples/cli/src/ColoredVec2.ts b/examples/cli/src/ColoredVec2.ts index cd150a3..b7c1957 100644 --- a/examples/cli/src/ColoredVec2.ts +++ b/examples/cli/src/ColoredVec2.ts @@ -19,8 +19,7 @@ export class ColoredVec2 extends Vec2 { } // Override + to return a ColoredVec2 with a blended color label - static readonly "+" = [ - (a: ColoredVec2, b: ColoredVec2): ColoredVec2 => - new ColoredVec2(a.x + b.x, a.y + b.y, `${a.color}+${b.color}`), - ] as const; + static "+"(a: ColoredVec2, b: ColoredVec2): ColoredVec2 { + return new ColoredVec2(a.x + b.x, a.y + b.y, `${a.color}+${b.color}`); + } } diff --git a/examples/cli/src/Mat4.ts b/examples/cli/src/Mat4.ts index ac4cdb5..6a219b2 100644 --- a/examples/cli/src/Mat4.ts +++ b/examples/cli/src/Mat4.ts @@ -34,38 +34,37 @@ export class Mat4 { // ── Binary operators ─────────────────────────────────────────────────────── - static readonly "*" = [ - // [0] Mat4 × Mat4 → Mat4 (matrix multiply) - (a: Mat4, b: Mat4): Mat4 => { - const r = new Array(16).fill(0); - for (let col = 0; col < 4; col++) { - for (let row = 0; row < 4; row++) { - let sum = 0; - for (let k = 0; k < 4; k++) { - sum += a.m[k * 4 + row] * b.m[col * 4 + k]; - } - r[col * 4 + row] = sum; - } - } - return new Mat4(r); - }, - - // [1] Mat4 × Vec3 → Vec3 (transform point, implicit w=1) - (a: Mat4, b: Vec3): Vec3 => - new Vec3( + // Mat4 × Mat4 → Mat4 (matrix multiply), and Mat4 × Vec3 → Vec3 (transform point) + static "*"(a: Mat4, b: Mat4): Mat4; + static "*"(a: Mat4, b: Vec3): Vec3; + static "*"(a: Mat4, b: Mat4 | Vec3): Mat4 | Vec3 { + if (b instanceof Vec3) { + return new Vec3( a.m[0] * b.x + a.m[4] * b.y + a.m[8] * b.z + a.m[12], a.m[1] * b.x + a.m[5] * b.y + a.m[9] * b.z + a.m[13], a.m[2] * b.x + a.m[6] * b.y + a.m[10] * b.z + a.m[14], - ), - ] as const; + ); + } + const r = new Array(16).fill(0); + for (let col = 0; col < 4; col++) { + for (let row = 0; row < 4; row++) { + let sum = 0; + for (let k = 0; k < 4; k++) { + sum += a.m[k * 4 + row] * b.m[col * 4 + k]; + } + r[col * 4 + row] = sum; + } + } + return new Mat4(r); + } // ── Comparison ───────────────────────────────────────────────────────────── - static readonly "==" = [ - (a: Mat4, b: Mat4): boolean => a.m.every((v, i) => v === b.m[i]), - ] as const; + static "=="(a: Mat4, b: Mat4): boolean { + return a.m.every((v, i) => v === b.m[i]); + } - static readonly "!=" = [ - (a: Mat4, b: Mat4): boolean => !a.m.every((v, i) => v === b.m[i]), - ] as const; + static "!="(a: Mat4, b: Mat4): boolean { + return !a.m.every((v, i) => v === b.m[i]); + } } diff --git a/examples/cli/src/Vec2.ts b/examples/cli/src/Vec2.ts index 41779f4..2dc0261 100644 --- a/examples/cli/src/Vec2.ts +++ b/examples/cli/src/Vec2.ts @@ -13,140 +13,128 @@ export class Vec2 { // ── Binary arithmetic ────────────────────────────────────────────────────── - // [0] Vec2 + Vec2 [1] +Vec2 (copy) - static readonly "+" = [ - (a: Vec2, b: Vec2): Vec2 => new Vec2(a.x + b.x, a.y + b.y), - (a: Vec2): Vec2 => new Vec2(a.x, a.y), - ] as const; - - // [0] Vec2 - Vec2 [1] -Vec2 (negate) - static readonly "-" = [ - (a: Vec2, b: Vec2): Vec2 => new Vec2(a.x - b.x, a.y - b.y), - (a: Vec2): Vec2 => new Vec2(-a.x, -a.y), - ] as const; + // Binary Vec2 + Vec2, and prefix unary +Vec2 (copy), combined on one method + static "+"(a: Vec2, b: Vec2): Vec2; + static "+"(a: Vec2): Vec2; + static "+"(a: Vec2, b?: Vec2): Vec2 { + if (b) return new Vec2(a.x + b.x, a.y + b.y); + return new Vec2(a.x, a.y); + } + + // Binary Vec2 - Vec2, and prefix unary -Vec2 (negate), combined on one method + static "-"(a: Vec2, b: Vec2): Vec2; + static "-"(a: Vec2): Vec2; + static "-"(a: Vec2, b?: Vec2): Vec2 { + if (b) return new Vec2(a.x - b.x, a.y - b.y); + return new Vec2(-a.x, -a.y); + } // Scalar operations - static readonly "*" = [ - (a: Vec2, b: number): Vec2 => new Vec2(a.x * b, a.y * b), - ] as const; + static "*"(a: Vec2, b: number): Vec2 { + return new Vec2(a.x * b, a.y * b); + } - static readonly "/" = [ - (a: Vec2, b: number): Vec2 => new Vec2(a.x / b, a.y / b), - ] as const; + static "/"(a: Vec2, b: number): Vec2 { + return new Vec2(a.x / b, a.y / b); + } - static readonly "%" = [ - (a: Vec2, b: number): Vec2 => new Vec2(a.x % b, a.y % b), - ] as const; + static "%"(a: Vec2, b: number): Vec2 { + return new Vec2(a.x % b, a.y % b); + } // Component-wise exponentiation - static readonly "**" = [ - (a: Vec2, b: number): Vec2 => new Vec2(a.x ** b, a.y ** b), - ] as const; + static "**"(a: Vec2, b: number): Vec2 { + return new Vec2(a.x ** b, a.y ** b); + } // ── Comparison (by component equality) ──────────────────────────────────── - static readonly "==" = [ - (a: Vec2, b: Vec2): boolean => a.x === b.x && a.y === b.y, - ] as const; + static "=="(a: Vec2, b: Vec2): boolean { + return a.x === b.x && a.y === b.y; + } - static readonly "===" = [ - (a: Vec2, b: Vec2): boolean => a.x === b.x && a.y === b.y, - ] as const; + static "==="(a: Vec2, b: Vec2): boolean { + return a.x === b.x && a.y === b.y; + } - static readonly "!=" = [ - (a: Vec2, b: Vec2): boolean => a.x !== b.x || a.y !== b.y, - ] as const; + static "!="(a: Vec2, b: Vec2): boolean { + return a.x !== b.x || a.y !== b.y; + } - static readonly "!==" = [ - (a: Vec2, b: Vec2): boolean => a.x !== b.x || a.y !== b.y, - ] as const; + static "!=="(a: Vec2, b: Vec2): boolean { + return a.x !== b.x || a.y !== b.y; + } // Ordered comparison by squared magnitude: |a|² vs |b|² - static readonly "<" = [ - (a: Vec2, b: Vec2): boolean => - a.x * a.x + a.y * a.y < b.x * b.x + b.y * b.y, - ] as const; - - static readonly "<=" = [ - (a: Vec2, b: Vec2): boolean => - a.x * a.x + a.y * a.y <= b.x * b.x + b.y * b.y, - ] as const; - - static readonly ">" = [ - (a: Vec2, b: Vec2): boolean => - a.x * a.x + a.y * a.y > b.x * b.x + b.y * b.y, - ] as const; - - static readonly ">=" = [ - (a: Vec2, b: Vec2): boolean => - a.x * a.x + a.y * a.y >= b.x * b.x + b.y * b.y, - ] as const; + static "<"(a: Vec2, b: Vec2): boolean { + return a.x * a.x + a.y * a.y < b.x * b.x + b.y * b.y; + } + + static "<="(a: Vec2, b: Vec2): boolean { + return a.x * a.x + a.y * a.y <= b.x * b.x + b.y * b.y; + } + + static ">"(a: Vec2, b: Vec2): boolean { + return a.x * a.x + a.y * a.y > b.x * b.x + b.y * b.y; + } + + static ">="(a: Vec2, b: Vec2): boolean { + return a.x * a.x + a.y * a.y >= b.x * b.x + b.y * b.y; + } // ── Prefix unary ─────────────────────────────────────────────────────────── // Returns true if this is the zero vector - static readonly "!" = [(a: Vec2): boolean => a.x === 0 && a.y === 0] as const; + static "!"(a: Vec2): boolean { + return a.x === 0 && a.y === 0; + } // Left-hand perpendicular: (x, y) → (-y, x) (90° CCW rotation) - static readonly "~" = [(a: Vec2): Vec2 => new Vec2(-a.y, a.x)] as const; - - // ── Compound assignment (instance, function expressions, return void) ────── - - readonly "+=" = [ - function (this: Vec2, b: Vec2): void { - this.x += b.x; - this.y += b.y; - }, - ] as const; - - readonly "-=" = [ - function (this: Vec2, b: Vec2): void { - this.x -= b.x; - this.y -= b.y; - }, - ] as const; - - readonly "*=" = [ - function (this: Vec2, b: number): void { - this.x *= b; - this.y *= b; - }, - ] as const; - - readonly "/=" = [ - function (this: Vec2, b: number): void { - this.x /= b; - this.y /= b; - }, - ] as const; - - readonly "%=" = [ - function (this: Vec2, b: number): void { - this.x %= b; - this.y %= b; - }, - ] as const; - - readonly "**=" = [ - function (this: Vec2, b: number): void { - this.x **= b; - this.y **= b; - }, - ] as const; - - // ── Postfix unary (instance, function expressions, return void) ─────────── - - readonly "++" = [ - function (this: Vec2): void { - this.x++; - this.y++; - }, - ] as const; - - readonly "--" = [ - function (this: Vec2): void { - this.x--; - this.y--; - }, - ] as const; + static "~"(a: Vec2): Vec2 { + return new Vec2(-a.y, a.x); + } + + // ── Compound assignment (instance methods, return void) ─────────────────── + + "+="(b: Vec2): void { + this.x += b.x; + this.y += b.y; + } + + "-="(b: Vec2): void { + this.x -= b.x; + this.y -= b.y; + } + + "*="(b: number): void { + this.x *= b; + this.y *= b; + } + + "/="(b: number): void { + this.x /= b; + this.y /= b; + } + + "%="(b: number): void { + this.x %= b; + this.y %= b; + } + + "**="(b: number): void { + this.x **= b; + this.y **= b; + } + + // ── Postfix unary (instance methods, return void) ───────────────────────── + + "++"(): void { + this.x++; + this.y++; + } + + "--"(): void { + this.x--; + this.y--; + } } diff --git a/examples/cli/src/Vec3.ts b/examples/cli/src/Vec3.ts index 6301f5a..b8e2649 100644 --- a/examples/cli/src/Vec3.ts +++ b/examples/cli/src/Vec3.ts @@ -15,73 +15,66 @@ export class Vec3 { // ── Binary arithmetic ────────────────────────────────────────────────────── - static readonly "+" = [ - (a: Vec3, b: Vec3): Vec3 => new Vec3(a.x + b.x, a.y + b.y, a.z + b.z), - ] as const; + static "+"(a: Vec3, b: Vec3): Vec3 { + return new Vec3(a.x + b.x, a.y + b.y, a.z + b.z); + } - // [0] Vec3 - Vec3 [1] -Vec3 (negate) - static readonly "-" = [ - (a: Vec3, b: Vec3): Vec3 => new Vec3(a.x - b.x, a.y - b.y, a.z - b.z), - (a: Vec3): Vec3 => new Vec3(-a.x, -a.y, -a.z), - ] as const; + // Binary Vec3 - Vec3, and prefix unary -Vec3 (negate), combined on one method + static "-"(a: Vec3, b: Vec3): Vec3; + static "-"(a: Vec3): Vec3; + static "-"(a: Vec3, b?: Vec3): Vec3 { + if (b) return new Vec3(a.x - b.x, a.y - b.y, a.z - b.z); + return new Vec3(-a.x, -a.y, -a.z); + } - static readonly "*" = [ - (a: Vec3, b: number): Vec3 => new Vec3(a.x * b, a.y * b, a.z * b), - ] as const; + static "*"(a: Vec3, b: number): Vec3 { + return new Vec3(a.x * b, a.y * b, a.z * b); + } // Cross product: a × b - static readonly "%" = [ - (a: Vec3, b: Vec3): Vec3 => - new Vec3( - a.y * b.z - a.z * b.y, - a.z * b.x - a.x * b.z, - a.x * b.y - a.y * b.x, - ), - ] as const; + static "%"(a: Vec3, b: Vec3): Vec3 { + return new Vec3( + a.y * b.z - a.z * b.y, + a.z * b.x - a.x * b.z, + a.x * b.y - a.y * b.x, + ); + } // ── Comparison ───────────────────────────────────────────────────────────── - static readonly "==" = [ - (a: Vec3, b: Vec3): boolean => a.x === b.x && a.y === b.y && a.z === b.z, - ] as const; - - static readonly "!=" = [ - (a: Vec3, b: Vec3): boolean => a.x !== b.x || a.y !== b.y || a.z !== b.z, - ] as const; - - // ── Compound assignment (instance, function expressions, return void) ────── - - readonly "+=" = [ - function (this: Vec3, b: Vec3): void { - this.x += b.x; - this.y += b.y; - this.z += b.z; - }, - ] as const; - - readonly "-=" = [ - function (this: Vec3, b: Vec3): void { - this.x -= b.x; - this.y -= b.y; - this.z -= b.z; - }, - ] as const; - - readonly "*=" = [ - function (this: Vec3, b: number): void { - this.x *= b; - this.y *= b; - this.z *= b; - }, - ] as const; - - // ── Postfix unary (instance, function expressions, return void) ─────────── - - readonly "++" = [ - function (this: Vec3): void { - this.x++; - this.y++; - this.z++; - }, - ] as const; + static "=="(a: Vec3, b: Vec3): boolean { + return a.x === b.x && a.y === b.y && a.z === b.z; + } + + static "!="(a: Vec3, b: Vec3): boolean { + return a.x !== b.x || a.y !== b.y || a.z !== b.z; + } + + // ── Compound assignment (instance methods, return void) ─────────────────── + + "+="(b: Vec3): void { + this.x += b.x; + this.y += b.y; + this.z += b.z; + } + + "-="(b: Vec3): void { + this.x -= b.x; + this.y -= b.y; + this.z -= b.z; + } + + "*="(b: number): void { + this.x *= b; + this.y *= b; + this.z *= b; + } + + // ── Postfix unary (instance methods, return void) ───────────────────────── + + "++"(): void { + this.x++; + this.y++; + this.z++; + } } diff --git a/examples/cli/src/test.ts b/examples/cli/src/test.ts index 2003dac..e0765d2 100644 --- a/examples/cli/src/test.ts +++ b/examples/cli/src/test.ts @@ -265,9 +265,9 @@ assertEq( const lhs = String(T1 * T2 * point); const rhs = String(T1 * (T2 * point)); assertEq(lhs, rhs, "Mat4: (T1*T2)*p == T1*(T2*p) (associativity)"); -// The key chain: Mat4*Mat4 dispatches to overload [0], Mat4*Vec3 dispatches to overload [1] -// T1 * T2 → Mat4 (uses overload [0]) -// Mat4 * point → Vec3 (uses overload [1]) +// The key chain: Mat4*Mat4 dispatches to the Mat4 overload, Mat4*Vec3 dispatches to the Vec3 overload +// T1 * T2 → Mat4 (uses Mat4 overload) +// Mat4 * point → Vec3 (uses Vec3 overload) assertEq( String(T1 * T2 * point), "Vec3(6, 9, 12)", @@ -301,7 +301,7 @@ assertEq( console.log("::endgroup::"); console.log("::group::Inheritance — fallback to Vec2 overloads"); -// - not overridden → type chain falls through to Vec2["-"][0], returns Vec2 +// - not overridden → type chain falls through to Vec2["-"](), returns Vec2 assertEq(String(cv2 - cv1), "Vec2(2, 2)", "Inheritance: - falls back to Vec2"); // unary - not overridden assertEq( @@ -331,7 +331,7 @@ console.log("::endgroup::"); console.log("::group::Inheritance — mixed type (ColoredVec2 op Vec2)"); // LHS chain: [ColoredVec2, Vec2], RHS chain: [Vec2] // ColoredVec2's + requires both sides to be ColoredVec2, so it is skipped. -// Falls through to Vec2["+"][0](cv1, v1), returning a plain Vec2. +// Falls through to Vec2["+"](cv1, v1), returning a plain Vec2. assertEq( String(cv1 + v1), "Vec2(2, 4)", @@ -340,9 +340,9 @@ assertEq( console.log("::endgroup::"); console.log("::group::Inheritance — inherited postfix and compound assignment"); -// ++ is inherited: boperators calls cv4["++"][0].call(cv4), which is Vec2's -// function and mutates .x and .y in place. cv4 is still a ColoredVec2 instance -// so toString() still includes the color. +// ++ is inherited: boperators calls cv4["++"](), which is Vec2's method and +// mutates .x and .y in place. cv4 is still a ColoredVec2 instance so +// toString() still includes the color. let cv4 = new ColoredVec2(5, 5, "purple"); cv4++; assertEq( diff --git a/package/src/core/OverloadInjector.test.ts b/package/src/core/OverloadInjector.test.ts index ae027ac..88c955a 100644 --- a/package/src/core/OverloadInjector.test.ts +++ b/package/src/core/OverloadInjector.test.ts @@ -5,18 +5,14 @@ import { ErrorManager } from "./ErrorManager"; import { OverloadInjector } from "./OverloadInjector"; import { OverloadStore } from "./OverloadStore"; -// A minimal in-memory Vec2 class with a static "+" overload +// A minimal in-memory Vec2 class with static "+" and "-" overloads const VEC2_SOURCE = ` export class Vec2 { x: number; y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } - static readonly "+" = [ - (a: Vec2, b: Vec2): Vec2 => new Vec2(a.x + b.x, a.y + b.y), - ] as const; - static readonly "-" = [ - (a: Vec2, b: Vec2): Vec2 => new Vec2(a.x - b.x, a.y - b.y), - ] as const; + static "+"(a: Vec2, b: Vec2): Vec2 { return new Vec2(a.x + b.x, a.y + b.y); } + static "-"(a: Vec2, b: Vec2): Vec2 { return new Vec2(a.x - b.x, a.y - b.y); } } `.trim(); @@ -52,7 +48,7 @@ const c = a + b; ); const result = injector.overloadFile(usageFile); - expect(result.text).toContain('Vec2["+"][0](a, b)'); + expect(result.text).toContain('Vec2["+"](a, b)'); }); it("produces non-empty edits when a transformation occurs", () => { @@ -98,7 +94,7 @@ const e = b + c; ); const result = injector.overloadFile(usageFile); - expect(result.text.match(/Vec2\["\+"\]\[0\]/g)?.length).toBe(2); + expect(result.text.match(/Vec2\["\+"\]/g)?.length).toBe(2); }); it("transforms different operators independently", () => { @@ -115,8 +111,8 @@ const diff = a - b; ); const result = injector.overloadFile(usageFile); - expect(result.text).toContain('Vec2["+"][0](a, b)'); - expect(result.text).toContain('Vec2["-"][0](a, b)'); + expect(result.text).toContain('Vec2["+"](a, b)'); + expect(result.text).toContain('Vec2["-"](a, b)'); }); it("returns the same SourceFile reference as the input", () => { diff --git a/package/src/core/OverloadStore.ts b/package/src/core/OverloadStore.ts index 67844b0..6c3c381 100644 --- a/package/src/core/OverloadStore.ts +++ b/package/src/core/OverloadStore.ts @@ -306,7 +306,10 @@ export class OverloadStore extends Map< const classType = normalizeTypeName(classDecl.getType().getText()); // Group method declarations by operator string. - // Each group may contain overload signatures (no body) and an implementation (with body). + // For each implementation, we use its overload signatures for per-type discrimination. + // If there are no overload signatures (simple one-signature methods), we use the + // implementation itself. In .d.ts files getMethods() returns declaration-only nodes with + // no body and no overloads, so they are naturally pushed as-is and treated as individual sigs. const methodGroups = new Map(); for (const method of classDecl.getMethods()) { const operatorString = getOperatorStringFromMethod(method); @@ -317,13 +320,16 @@ export class OverloadStore extends Map< group = []; methodGroups.set(operatorString, group); } - group.push(method); + // Use individual overload signatures for per-type discrimination. + // Fall back to the implementation itself when there are no overloads. + const overloadSigs = method.getOverloads(); + group.push(...(overloadSigs.length > 0 ? overloadSigs : [method])); } methodGroups.forEach((methods, operatorString) => { - // Use overload signatures (no body); fall back to the implementation if there are none. - // In .d.ts files all method declarations lack a body, so they are naturally treated - // as overload signatures without any special-casing. + // All entries are either overload signatures (no body) or implementations + // with no overloads. Use the no-body ones preferentially; if all have bodies + // (implementation-only methods), use them directly. const overloadSigs = methods.filter((m) => !m.hasBody()); const sigsToProcess = overloadSigs.length > 0 ? overloadSigs : methods; @@ -454,12 +460,19 @@ export class OverloadStore extends Map< ): void { let hasWarning = false; - const lhsType = isStatic - ? normalizeTypeName(parameters[0]?.getType().getText() ?? "") - : classType; + // Use the declared type annotation text rather than the resolved type. + // `parameter.getType().getText()` on an overload signature may return a union + // of all overload signatures' types (e.g. `Vec2 | undefined`) instead of the + // type declared in this specific signature (e.g. `Vec2`). + const getParamTypeName = (p: ParameterDeclaration | undefined): string => + normalizeTypeName( + p?.getTypeNode()?.getText() ?? p?.getType().getText() ?? "", + ); + + const lhsType = isStatic ? getParamTypeName(parameters[0]) : classType; const rhsType = isStatic - ? normalizeTypeName(parameters[1]?.getType().getText() ?? "") - : normalizeTypeName(parameters[0]?.getType().getText() ?? ""); + ? getParamTypeName(parameters[1]) + : getParamTypeName(parameters[0]); if (isStatic && lhsType !== classType && rhsType !== classType) { this._errorManager.addWarning( @@ -557,8 +570,11 @@ export class OverloadStore extends Map< ): void { let hasWarning = false; + // Use the declared type annotation to avoid union widening from overload groups. const operandType = normalizeTypeName( - parameters[0]?.getType().getText() ?? "", + parameters[0]?.getTypeNode()?.getText() ?? + parameters[0]?.getType().getText() ?? + "", ); if (operandType !== classType) { From e2c66a1d35880a9512e5ffc35f46ff7dfc1727b7 Mon Sep 17 00:00:00 2001 From: Dief Bell Date: Mon, 23 Feb 2026 15:01:25 +0000 Subject: [PATCH 06/18] Updated NextJS and webpack examples --- examples/esbuild/src/Vec2.ts | 6 +++--- examples/nextjs/src/Vec2.ts | 6 +++--- examples/vite/src/Vec2.ts | 6 +++--- examples/webpack/src/Vec2.ts | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/esbuild/src/Vec2.ts b/examples/esbuild/src/Vec2.ts index 57279d5..384cf80 100644 --- a/examples/esbuild/src/Vec2.ts +++ b/examples/esbuild/src/Vec2.ts @@ -4,9 +4,9 @@ export class Vec2 { public y: number, ) {} - static readonly "+" = [ - (a: Vec2, b: Vec2): Vec2 => new Vec2(a.x + b.x, a.y + b.y), - ] as const; + static "+"(a: Vec2, b: Vec2): Vec2 { + return new Vec2(a.x + b.x, a.y + b.y); + } toString(): string { return `Vec2(${this.x}, ${this.y})`; diff --git a/examples/nextjs/src/Vec2.ts b/examples/nextjs/src/Vec2.ts index 57279d5..384cf80 100644 --- a/examples/nextjs/src/Vec2.ts +++ b/examples/nextjs/src/Vec2.ts @@ -4,9 +4,9 @@ export class Vec2 { public y: number, ) {} - static readonly "+" = [ - (a: Vec2, b: Vec2): Vec2 => new Vec2(a.x + b.x, a.y + b.y), - ] as const; + static "+"(a: Vec2, b: Vec2): Vec2 { + return new Vec2(a.x + b.x, a.y + b.y); + } toString(): string { return `Vec2(${this.x}, ${this.y})`; diff --git a/examples/vite/src/Vec2.ts b/examples/vite/src/Vec2.ts index 57279d5..384cf80 100644 --- a/examples/vite/src/Vec2.ts +++ b/examples/vite/src/Vec2.ts @@ -4,9 +4,9 @@ export class Vec2 { public y: number, ) {} - static readonly "+" = [ - (a: Vec2, b: Vec2): Vec2 => new Vec2(a.x + b.x, a.y + b.y), - ] as const; + static "+"(a: Vec2, b: Vec2): Vec2 { + return new Vec2(a.x + b.x, a.y + b.y); + } toString(): string { return `Vec2(${this.x}, ${this.y})`; diff --git a/examples/webpack/src/Vec2.ts b/examples/webpack/src/Vec2.ts index 57279d5..384cf80 100644 --- a/examples/webpack/src/Vec2.ts +++ b/examples/webpack/src/Vec2.ts @@ -4,9 +4,9 @@ export class Vec2 { public y: number, ) {} - static readonly "+" = [ - (a: Vec2, b: Vec2): Vec2 => new Vec2(a.x + b.x, a.y + b.y), - ] as const; + static "+"(a: Vec2, b: Vec2): Vec2 { + return new Vec2(a.x + b.x, a.y + b.y); + } toString(): string { return `Vec2(${this.x}, ${this.y})`; From 1bc2f1b45cb91635e7266e5b39bacbab22e4df08 Mon Sep 17 00:00:00 2001 From: Dief Bell Date: Mon, 23 Feb 2026 15:27:47 +0000 Subject: [PATCH 07/18] Updated more unit tests for new API, updated readmes of other packages --- README.md | 33 +++--- cli/src/index.test.ts | 8 +- mcp-server/README.md | 12 +-- mcp-server/src/index.ts | 157 ++++++++++++++--------------- package/src/core/SourceMap.test.ts | 2 +- plugins/esbuild/README.md | 2 +- plugins/esbuild/debug-tsx2.ts | 4 +- plugins/esbuild/src/index.test.ts | 8 +- plugins/tsc/README.md | 2 +- plugins/vite/README.md | 2 +- plugins/vite/src/index.test.ts | 12 +-- plugins/webpack/README.md | 2 +- plugins/webpack/src/index.test.ts | 8 +- 13 files changed, 115 insertions(+), 137 deletions(-) diff --git a/README.md b/README.md index 71c808a..6c3ba1b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Operator overloading for TypeScript. ![Sym.JS logo](https://github.com/DiefBell/boperators/blob/653ea138f4dcd1e6b4dd112133a4942f70e91fb3/logo.png) -`boperators` lets you define operator overloads (`+`, `-`, `*=`, `==`, etc.) on your TypeScript classes. It works by transforming your source code at the AST level using [ts-morph](https://ts-morph.com), replacing expressions like `v1 + v2` with the corresponding overload call `Vector3["+"][0](v1, v2)`. +`boperators` lets you define operator overloads (`+`, `-`, `*=`, `==`, etc.) on your TypeScript classes. It works by transforming your source code at the AST level using [ts-morph](https://ts-morph.com), replacing expressions like `v1 + v2` with the corresponding overload call `Vector3["+"](v1, v2)`. ## Quick Start @@ -14,31 +14,26 @@ class Vector3 { public y: number; public z: number; - // Static operator: takes two parameters - static readonly "+" = [ - (a: Vector3, b: Vector3) => - new Vector3(a.x + b.x, a.y + b.y, a.z + b.z), - ] as const; - - // Instance operator: takes one parameter, uses `this` - readonly "+=" = [ - function (this: Vector3, rhs: Vector3): void { - this.x += rhs.x; - this.y += rhs.y; - this.z += rhs.z; - }, - ] as const; + // Static operator: takes two parameters, returns a new instance + static "+"(a: Vector3, b: Vector3): Vector3 { + return new Vector3(a.x + b.x, a.y + b.y, a.z + b.z); + } + + // Instance operator: takes one parameter, mutates in place + "+="(rhs: Vector3): void { + this.x += rhs.x; + this.y += rhs.y; + this.z += rhs.z; + } // ... } // Usage - these get transformed automatically: -const v3 = v1 + v2; // => Vector3["+"][0](v1, v2) -v1 += v2; // => v1["+="][0].call(v1, v2) +const v3 = v1 + v2; // => Vector3["+"](v1, v2) +v1 += v2; // => v1["+="](v2) ``` -> **Important:** Overload arrays **must** use `as const`. Without it, TypeScript widens the array type and loses individual function signatures, causing type errors in the generated code. boperators will error if `as const` is missing. - Overloads defined on a parent class are automatically inherited by subclasses. For example, if `Expr` defines `+` and `*`, a `Sym extends Expr` class can use those operators without redeclaring them. ## Publishing a library diff --git a/cli/src/index.test.ts b/cli/src/index.test.ts index 82e6cc5..3f2884d 100644 --- a/cli/src/index.test.ts +++ b/cli/src/index.test.ts @@ -11,9 +11,7 @@ export class Vec2 { x: number; y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } - static readonly "+" = [ - (a: Vec2, b: Vec2): Vec2 => new Vec2(a.x + b.x, a.y + b.y), - ] as const; + static "+"(a: Vec2, b: Vec2): Vec2 { return new Vec2(a.x + b.x, a.y + b.y); } } `.trim(); @@ -153,7 +151,7 @@ describe("compile command", () => { const { exitCode } = runCLI(["compile"], tmpDir); expect(exitCode).toBe(0); const js = fs.readFileSync(path.join(tmpDir, "dist", "usage.js"), "utf-8"); - expect(js).toContain('Vec2["+"][0](a, b)'); + expect(js).toContain('Vec2["+"](a, b)'); }); it("writes transformed TypeScript to --ts-out", () => { @@ -164,7 +162,7 @@ describe("compile command", () => { ); expect(exitCode).toBe(0); const ts = fs.readFileSync(path.join(tsOut, "usage.ts"), "utf-8"); - expect(ts).toContain('Vec2["+"][0](a, b)'); + expect(ts).toContain('Vec2["+"](a, b)'); }); it("writes source map JSON to --maps-out", () => { diff --git a/mcp-server/README.md b/mcp-server/README.md index a39c284..d8d46e7 100644 --- a/mcp-server/README.md +++ b/mcp-server/README.md @@ -22,7 +22,7 @@ bun add -D @boperators/mcp-server |------|-------------|:-------------------:| | [`list_overloads`](#list_overloads) | List all registered overloads in the project, with optional filtering by class or operator | ✓ | | [`transform_preview`](#transform_preview) | Preview the transformed output for a file or a line range within it | ✓ | -| [`scaffold_overloads`](#scaffold_overloads) | Generate `as const` boilerplate for a set of operators on a named class | — | +| [`scaffold_overloads`](#scaffold_overloads) | Generate method boilerplate for a set of operators on a named class | — | | [`validate_overloads`](#validate_overloads) | Validate overload definitions in a single file and return structured diagnostics | ✓ | | [`explain_expression`](#explain_expression) | Reverse-engineer a transformed call expression back to its original operator and overload metadata | optional | @@ -57,11 +57,11 @@ Transforms a file and returns the original and transformed text side by side, al ### `scaffold_overloads` -Generates ready-to-paste TypeScript property declarations for a list of operators on a given class. Automatically uses the correct form for each operator: +Generates ready-to-paste TypeScript method declarations for a list of operators on a given class. Automatically uses the correct form for each operator: -- **Static binary** (`+`, `-`, `*`, …) — `static readonly "+" = [(a: T, b: T): T => { … }] as const` +- **Static binary** (`+`, `-`, `*`, …) — `static "+"(a: T, b: T): T { … }` - **Comparison** (`>`, `==`, …) — static, returns `boolean` -- **Instance compound** (`+=`, `-=`, …) — instance, `function(this: T, rhs: T): void` +- **Instance compound** (`+=`, `-=`, …) — instance, `"+="(rhs: T): void { … }` - **Prefix unary** (`!`, `~`) — static, one parameter - **Postfix unary** (`++`, `--`) — instance, no parameters, returns `void` @@ -80,7 +80,7 @@ Does not require a `tsconfig` — it is purely generative. Runs the boperators scanning pipeline against a single file in isolation and returns structured diagnostics without modifying any state. Reports: -- **Errors** — wrong arity, missing `as const`, return type violations +- **Errors** — wrong arity, return type violations - **Warnings** — duplicate/conflicting overload registrations - The count of successfully parsed overloads @@ -95,7 +95,7 @@ Runs the boperators scanning pipeline against a single file in isolation and ret ### `explain_expression` -Given a transformed boperators expression (e.g. `Vector3["+"][0](a, b)` or `v["+="][0].call(v, rhs)`), decodes it back to the original operator, identifies whether it is static/instance and binary/unary, and optionally enriches the result with metadata from the project's overload store. +Given a transformed boperators expression (e.g. `Vector3["+"](a, b)` or `v["+="](rhs)` or `v["++"]( )`), decodes it back to the original operator, identifies whether it is static/instance and binary/unary, and optionally enriches the result with metadata from the project's overload store. **Inputs** diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts index b8760bf..01c7a32 100644 --- a/mcp-server/src/index.ts +++ b/mcp-server/src/index.ts @@ -272,57 +272,47 @@ function generateOverloadProperty(className: string, operator: string): string { if (postfixUnaryOperatorStrings.has(operator)) { // Postfix unary: instance, no params, returns void return [ - `\tpublic readonly "${operator}" = [`, - `\t\tfunction (this: ${className}): void {`, - "\t\t\t// TODO: implement", - "\t\t},", - "\t] as const;", + `\tpublic "${operator}"(): void {`, + "\t\t// TODO: implement", + "\t}", ].join("\n"); } if (exclusivePrefixUnaryStrings.has(operator)) { // Prefix unary: static, one param (self), returns ClassName return [ - `\tpublic static readonly "${operator}" = [`, - `\t\t(a: ${className}): ${className} => {`, - "\t\t\t// TODO: implement", - `\t\t\treturn new ${className}();`, - "\t\t},", - "\t] as const;", + `\tpublic static "${operator}"(a: ${className}): ${className} {`, + "\t\t// TODO: implement", + `\t\treturn new ${className}();`, + "\t}", ].join("\n"); } if (instanceOperatorStrings.has(operator)) { // Instance mutation: instance, one param, returns void return [ - `\tpublic readonly "${operator}" = [`, - `\t\tfunction (this: ${className}, other: ${className}): void {`, - "\t\t\t// TODO: implement", - "\t\t},", - "\t] as const;", + `\tpublic "${operator}"(other: ${className}): void {`, + "\t\t// TODO: implement", + "\t}", ].join("\n"); } if (comparisonOperatorStrings.has(operator)) { // Comparison: static, two params, returns boolean return [ - `\tpublic static readonly "${operator}" = [`, - `\t\t(a: ${className}, b: ${className}): boolean => {`, - "\t\t\t// TODO: implement", - "\t\t\treturn false;", - "\t\t},", - "\t] as const;", + `\tpublic static "${operator}"(a: ${className}, b: ${className}): boolean {`, + "\t\t// TODO: implement", + "\t\treturn false;", + "\t}", ].join("\n"); } // Static binary: two params, returns ClassName return [ - `\tpublic static readonly "${operator}" = [`, - `\t\t(a: ${className}, b: ${className}): ${className} => {`, - "\t\t\t// TODO: implement", - `\t\t\treturn new ${className}();`, - "\t\t},", - "\t] as const;", + `\tpublic static "${operator}"(a: ${className}, b: ${className}): ${className} {`, + "\t\t// TODO: implement", + `\t\treturn new ${className}();`, + "\t}", ].join("\n"); } @@ -332,7 +322,7 @@ server.registerTool( description: "Generate TypeScript boilerplate for operator overload definitions on a class. " + "Returns code ready to paste into a class body, with correct static/instance " + - "placement, as const assertions, and this parameters for instance operators.", + "placement and parameter types.", inputSchema: { className: z .string() @@ -392,7 +382,7 @@ server.registerTool( { description: "Validate operator overload definitions in a single file. " + - "Returns structured diagnostics: errors (wrong arity, bad types, missing as const) " + + "Returns structured diagnostics: errors (wrong arity, bad types) " + "and warnings (e.g. conflicting overloads). Does not transform the file.", inputSchema: { tsconfig: z @@ -478,32 +468,39 @@ server.registerTool( // ---------- Tool 5: explain_expression ---------- /** - * Regex for instance-style calls: - * expr["op"][idx].call(expr, ...) - * Captures: [1] operator, [2] index + * Regex for postfix-unary calls (instance, no args): + * expr["op"]() + * Captures: [1] operator */ -const instanceCallPattern = /\["([^"]+)"\]\[(\d+)\]\.call\(/; +const postfixCallPattern = /\["([^"]+)"\]\(\)$/; /** - * Regex for static-style calls: - * ClassName["op"][idx](...) - * Captures: [1] class name, [2] operator, [3] index + * Regex for instance-style calls (lowercase callee): + * expr["op"](other) + * Captures: [1] callee, [2] operator */ -const staticCallPattern = /^(\w+)\["([^"]+)"\]\[(\d+)\]\(/; +const instanceCallPattern = /^([a-z_$]\w*)\["([^"]+)"\]\(/; + +/** + * Regex for static-style calls (uppercase callee / class name): + * ClassName["op"](...) + * Captures: [1] class name, [2] operator + */ +const staticCallPattern = /^([A-Z]\w*)\["([^"]+)"\]\(/; server.registerTool( "explain_expression", { description: "Explain a transformed boperators expression. " + - 'Given an expression like Vector3["+"][0](a, b) or v["++"][0].call(v), ' + + 'Given an expression like Vec2["+"](a, b) or a["+="](b) or a["++"](), ' + "identifies the operator kind, class, and overload entry. " + "Optionally looks up full overload metadata when tsconfig is provided.", inputSchema: { expression: z .string() .describe( - "The transformed expression to explain, e.g. 'Vector3[\"+\"][0](a, b)'.", + "The transformed expression to explain, e.g. 'Vec2[\"+\"](a, b)'.", ), tsconfig: z .string() @@ -518,63 +515,65 @@ server.registerTool( try { const trimmed = expression.trim(); - // Try instance pattern first (has .call) - const instanceMatch = trimmed.match(instanceCallPattern); - if (instanceMatch) { - const [, operator, indexStr] = instanceMatch; - const index = Number.parseInt(indexStr, 10); + // Try postfix unary first: expr["op"]() + const postfixMatch = trimmed.match(postfixCallPattern); + if (postfixMatch) { + const [, operator] = postfixMatch; + const result: Record = { + kind: "postfix unary", + operator, + isStatic: false, + originalExpression: `operand${operator}`, + explanation: `Postfix unary operator "${operator}": mutates the operand in place.`, + }; - // Determine if postfix unary (0 args after .call(expr)) or - // instance binary (.call(expr, rhs)) - const callArgs = trimmed.slice( - trimmed.indexOf(".call(") + 6, - trimmed.lastIndexOf(")"), - ); - const hasSecondArg = callArgs.includes(","); + if (tsconfig) { + const overloadInfo = lookupOverload(tsconfig, operator, false); + if (overloadInfo) Object.assign(result, overloadInfo); + } - const kind = hasSecondArg ? "instance binary" : "postfix unary"; - const originalPattern = hasSecondArg - ? `lhs ${operator} rhs` - : `operand${operator}`; + return { + content: [ + { type: "text" as const, text: JSON.stringify(result, null, 2) }, + ], + }; + } + // Try instance binary: lowercase_var["op"](other) + const instanceMatch = trimmed.match(instanceCallPattern); + if (instanceMatch) { + const [, , operator] = instanceMatch; const result: Record = { - kind, + kind: "instance binary", operator, - index, isStatic: false, - originalExpression: originalPattern, - explanation: hasSecondArg - ? `Instance binary operator "${operator}": mutates the left-hand side in place (e.g. compound assignment).` - : `Postfix unary operator "${operator}": mutates the operand in place.`, + originalExpression: `lhs ${operator} rhs`, + explanation: `Instance binary operator "${operator}": mutates the left-hand side in place (e.g. compound assignment).`, }; if (tsconfig) { - const overloadInfo = lookupOverload(tsconfig, operator, index, false); + const overloadInfo = lookupOverload(tsconfig, operator, false); if (overloadInfo) Object.assign(result, overloadInfo); } return { content: [ - { - type: "text" as const, - text: JSON.stringify(result, null, 2), - }, + { type: "text" as const, text: JSON.stringify(result, null, 2) }, ], }; } - // Try static pattern + // Try static: ClassName["op"](...) const staticMatch = trimmed.match(staticCallPattern); if (staticMatch) { - const [, className, operator, indexStr] = staticMatch; - const index = Number.parseInt(indexStr, 10); + const [, className, operator] = staticMatch; - // Count args to distinguish binary (2 args) from prefix unary (1 arg) + // Count args to distinguish static binary (2 args) from prefix unary (1 arg) const argsStr = trimmed.slice( trimmed.indexOf("](") + 2, trimmed.lastIndexOf(")"), ); - const argCount = argsStr.split(",").length; + const argCount = argsStr.trim() === "" ? 0 : argsStr.split(",").length; const kind = argCount >= 2 ? "static binary" : "prefix unary"; const originalPattern = @@ -583,7 +582,6 @@ server.registerTool( const result: Record = { kind, operator, - index, className, isStatic: true, originalExpression: originalPattern, @@ -597,7 +595,6 @@ server.registerTool( const overloadInfo = lookupOverload( tsconfig, operator, - index, true, className, ); @@ -606,10 +603,7 @@ server.registerTool( return { content: [ - { - type: "text" as const, - text: JSON.stringify(result, null, 2), - }, + { type: "text" as const, text: JSON.stringify(result, null, 2) }, ], }; } @@ -621,8 +615,9 @@ server.registerTool( text: `Could not parse expression as a boperators transformed call. ` + `Expected patterns:\n` + - ` Static: ClassName["op"][index](args)\n` + - ` Instance: expr["op"][index].call(expr, args)`, + ` Static: ClassName["op"](args)\n` + + ` Instance: expr["op"](other)\n` + + ` Postfix: expr["op"]()`, }, ], isError: true, @@ -645,7 +640,6 @@ server.registerTool( function lookupOverload( tsconfig: string, operator: string, - index: number, isStatic: boolean, className?: string, ): Record | null { @@ -655,7 +649,6 @@ function lookupOverload( const match = allOverloads.find((o) => { if (o.operatorString !== operator) return false; - if (o.index !== index) return false; if (className && o.className !== className) return false; if (o.isStatic !== isStatic) return false; return true; diff --git a/package/src/core/SourceMap.test.ts b/package/src/core/SourceMap.test.ts index 202d381..eebf538 100644 --- a/package/src/core/SourceMap.test.ts +++ b/package/src/core/SourceMap.test.ts @@ -105,7 +105,7 @@ describe("computeEdits", () => { // "a + b" gets replaced with a function call — the edit should cover // the expression but leave the surrounding code untouched. const original = "const result = a + b;"; - const transformed = 'const result = Vec["+"][0](a, b);'; + const transformed = 'const result = Vec["+"](a, b);'; const edits = computeEdits(original, transformed); expect(edits.length).toBeGreaterThan(0); // The edit region in the original should not reference Vec diff --git a/plugins/esbuild/README.md b/plugins/esbuild/README.md index 47507dd..4f5a52d 100644 --- a/plugins/esbuild/README.md +++ b/plugins/esbuild/README.md @@ -52,7 +52,7 @@ plugins: [ The plugin initialises once inside `setup(build)`, then transforms files on demand: 1. **Setup** — creates a [ts-morph](https://ts-morph.com) Project from your tsconfig and scans all source files for operator overload definitions -2. **`build.onLoad`** — for each `.ts`/`.tsx` file that matches the filter, replaces operator expressions (e.g. `v1 + v2` becomes `Vector3["+"][0](v1, v2)`) and returns the transformed source to ESBuild with the correct `ts`/`tsx` loader +2. **`build.onLoad`** — for each `.ts`/`.tsx` file that matches the filter, replaces operator expressions (e.g. `v1 + v2` becomes `Vector3["+"](v1, v2)`) and returns the transformed source to ESBuild with the correct `ts`/`tsx` loader If a file contains no overloaded operators it is returned as-is with no overhead. diff --git a/plugins/esbuild/debug-tsx2.ts b/plugins/esbuild/debug-tsx2.ts index 0e4f066..d12a859 100644 --- a/plugins/esbuild/debug-tsx2.ts +++ b/plugins/esbuild/debug-tsx2.ts @@ -12,9 +12,7 @@ import { const VEC2 = `export class Vec2 { x: number; y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } - static readonly "+" = [ - (a: Vec2, b: Vec2): Vec2 => new Vec2(a.x + b.x, a.y + b.y), - ] as const; + static "+"(a: Vec2, b: Vec2): Vec2 { return new Vec2(a.x + b.x, a.y + b.y); } }`; const USAGE = `import { Vec2 } from "./Vec2"; const a = new Vec2(1, 2); diff --git a/plugins/esbuild/src/index.test.ts b/plugins/esbuild/src/index.test.ts index f90400f..9496555 100644 --- a/plugins/esbuild/src/index.test.ts +++ b/plugins/esbuild/src/index.test.ts @@ -11,9 +11,7 @@ export class Vec2 { x: number; y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } - static readonly "+" = [ - (a: Vec2, b: Vec2): Vec2 => new Vec2(a.x + b.x, a.y + b.y), - ] as const; + static "+"(a: Vec2, b: Vec2): Vec2 { return new Vec2(a.x + b.x, a.y + b.y); } } `.trim(); @@ -96,7 +94,7 @@ describe("@boperators/plugin-esbuild", () => { it("transforms a binary overloaded expression", () => { const result = setupPlugin(tmpDir)(path.join(tmpDir, "usage.ts")); expect(result).not.toBeNull(); - expect((result as OnLoadResult).contents).toContain('Vec2["+"][0](a, b)'); + expect((result as OnLoadResult).contents).toContain('Vec2["+"](a, b)'); }); it("sets loader to 'ts' for .ts files", () => { @@ -125,6 +123,6 @@ describe("@boperators/plugin-esbuild", () => { const result = setupPlugin(tmpDir, { project: "tsconfig.json" })( path.join(tmpDir, "usage.ts"), ); - expect((result as OnLoadResult).contents).toContain('Vec2["+"][0](a, b)'); + expect((result as OnLoadResult).contents).toContain('Vec2["+"](a, b)'); }); }); diff --git a/plugins/tsc/README.md b/plugins/tsc/README.md index 5526fc9..6c9f0e6 100644 --- a/plugins/tsc/README.md +++ b/plugins/tsc/README.md @@ -68,7 +68,7 @@ The plugin runs as a ts-patch Program Transformer, which executes during `ts.cre 1. Creates a [ts-morph](https://ts-morph.com) Project from your tsconfig 2. Scans all source files for operator overload definitions -3. Transforms expressions in project files (e.g. `v1 + v2` becomes `Vector3["+"][0](v1, v2)`) +3. Transforms expressions in project files (e.g. `v1 + v2` becomes `Vector3["+"](v1, v2)`) 4. Returns a new TypeScript Program with the transformed source text TypeScript then type-checks and emits the transformed code, which contains only valid function calls. diff --git a/plugins/vite/README.md b/plugins/vite/README.md index 66b3da0..f48ee8a 100644 --- a/plugins/vite/README.md +++ b/plugins/vite/README.md @@ -45,7 +45,7 @@ boperators({ The plugin initialises once when Vite resolves its config, then transforms files on demand: 1. `configResolved` — creates a [ts-morph](https://ts-morph.com) Project from your tsconfig and scans all source files for operator overload definitions -2. `transform` — for each `.ts`/`.tsx` file, syncs ts-morph's in-memory state with the current file content (so HMR edits are picked up without restarting), then replaces operator expressions (e.g. `v1 + v2` becomes `Vector3["+"][0](v1, v2)`) and returns a V3 source map so breakpoints and stack traces map back to the original source +2. `transform` — for each `.ts`/`.tsx` file, syncs ts-morph's in-memory state with the current file content (so HMR edits are picked up without restarting), then replaces operator expressions (e.g. `v1 + v2` becomes `Vector3["+"](v1, v2)`) and returns a V3 source map so breakpoints and stack traces map back to the original source ## Comparison with Other Approaches diff --git a/plugins/vite/src/index.test.ts b/plugins/vite/src/index.test.ts index d721ae8..1c01ca2 100644 --- a/plugins/vite/src/index.test.ts +++ b/plugins/vite/src/index.test.ts @@ -11,9 +11,7 @@ export class Vec2 { x: number; y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } - static readonly "+" = [ - (a: Vec2, b: Vec2): Vec2 => new Vec2(a.x + b.x, a.y + b.y), - ] as const; + static "+"(a: Vec2, b: Vec2): Vec2 { return new Vec2(a.x + b.x, a.y + b.y); } } `.trim(); @@ -78,7 +76,7 @@ describe("@boperators/plugin-vite", () => { it("transforms a binary overloaded expression", () => { const result = callTransform(USAGE_SOURCE, path.join(tmpDir, "usage.ts")); expect(result).not.toBeNull(); - expect(result?.code).toContain('Vec2["+"][0](a, b)'); + expect(result?.code).toContain('Vec2["+"](a, b)'); }); it("provides a V3 source map for the transformed output", () => { @@ -110,7 +108,7 @@ describe("@boperators/plugin-vite", () => { USAGE_SOURCE, `${path.join(tmpDir, "usage.ts")}?t=123456`, ); - expect(result?.code).toContain('Vec2["+"][0](a, b)'); + expect(result?.code).toContain('Vec2["+"](a, b)'); }); it("re-transforms updated source on subsequent calls (HMR behaviour)", () => { @@ -121,7 +119,7 @@ describe("@boperators/plugin-vite", () => { // Second call with identical source — should still work (idempotent) const result = callTransform(USAGE_SOURCE, usagePath); - expect(result?.code).toContain('Vec2["+"][0](a, b)'); + expect(result?.code).toContain('Vec2["+"](a, b)'); }); it("accepts an explicit project option pointing to tsconfig", () => { @@ -139,6 +137,6 @@ describe("@boperators/plugin-vite", () => { USAGE_SOURCE, path.join(tmpDir, "usage.ts"), ); - expect(result?.code).toContain('Vec2["+"][0](a, b)'); + expect(result?.code).toContain('Vec2["+"](a, b)'); }); }); diff --git a/plugins/webpack/README.md b/plugins/webpack/README.md index 857b2fb..5b41ed7 100644 --- a/plugins/webpack/README.md +++ b/plugins/webpack/README.md @@ -119,7 +119,7 @@ The loader runs as a webpack pre-loader, executing before TypeScript compilation 1. Creates a [ts-morph](https://ts-morph.com) Project from your tsconfig 2. Scans all source files for operator overload definitions -3. Transforms expressions in the current file (e.g. `v1 + v2` becomes `Vector3["+"][0](v1, v2)`) +3. Transforms expressions in the current file (e.g. `v1 + v2` becomes `Vector3["+"](v1, v2)`) 4. Generates a V3 source map so stack traces and debugger breakpoints map back to your original source 5. Passes the transformed code to the next loader (e.g. `ts-loader`) diff --git a/plugins/webpack/src/index.test.ts b/plugins/webpack/src/index.test.ts index 393a6e5..91ab6a3 100644 --- a/plugins/webpack/src/index.test.ts +++ b/plugins/webpack/src/index.test.ts @@ -10,9 +10,7 @@ export class Vec2 { x: number; y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } - static readonly "+" = [ - (a: Vec2, b: Vec2): Vec2 => new Vec2(a.x + b.x, a.y + b.y), - ] as const; + static "+"(a: Vec2, b: Vec2): Vec2 { return new Vec2(a.x + b.x, a.y + b.y); } } `.trim(); @@ -89,7 +87,7 @@ describe("@boperators/webpack-loader", () => { path.join(tmpDir, "usage.ts"), ); expect(error).toBeNull(); - expect(code as string).toContain('Vec2["+"][0](a, b)'); + expect(code as string).toContain('Vec2["+"](a, b)'); }); it("provides a V3 source map for the transformed output", () => { @@ -129,7 +127,7 @@ describe("@boperators/webpack-loader", () => { { project: "tsconfig.json" }, ); expect(error).toBeNull(); - expect(code as string).toContain('Vec2["+"][0](a, b)'); + expect(code as string).toContain('Vec2["+"](a, b)'); }); it("calls the error callback when tsconfig cannot be found", () => { From 9299bf4acb9e4d08f00a5a6501a999d2dfc0e3a7 Mon Sep 17 00:00:00 2001 From: Dief Bell Date: Mon, 23 Feb 2026 18:36:19 +0000 Subject: [PATCH 08/18] Created an example for use with Bun, and requiring Bun 1.3+ --- examples/bun/.vscode/settings.json | 3 ++ examples/bun/bun.lock | 55 ++++++++++++++++++++++++++++++ examples/bun/bunfig.toml | 1 + examples/bun/package.json | 13 +++++++ examples/bun/src/Vec2.ts | 17 +++++++++ examples/bun/src/index.ts | 7 ++++ examples/bun/tsconfig.json | 8 +++++ plugins/bun/README.md | 2 ++ plugins/bun/package.json | 3 ++ 9 files changed, 109 insertions(+) create mode 100644 examples/bun/.vscode/settings.json create mode 100644 examples/bun/bun.lock create mode 100644 examples/bun/bunfig.toml create mode 100644 examples/bun/package.json create mode 100644 examples/bun/src/Vec2.ts create mode 100644 examples/bun/src/index.ts create mode 100644 examples/bun/tsconfig.json diff --git a/examples/bun/.vscode/settings.json b/examples/bun/.vscode/settings.json new file mode 100644 index 0000000..63662bf --- /dev/null +++ b/examples/bun/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules\\typescript\\lib" +} diff --git a/examples/bun/bun.lock b/examples/bun/bun.lock new file mode 100644 index 0000000..91efeb2 --- /dev/null +++ b/examples/bun/bun.lock @@ -0,0 +1,55 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "@boperators/example-bun", + "devDependencies": { + "@boperators/plugin-bun": "file:../../plugins/bun", + "@boperators/plugin-ts-language-server": "file:../../plugins/ts-language-server", + "boperators": "file:../../package", + "typescript": "^5.0.0", + }, + }, + }, + "packages": { + "@boperators/plugin-bun": ["@boperators/plugin-bun@file:../../plugins/bun", { "devDependencies": { "@types/bun": "^1.3.8", "boperators": "file:../../package" }, "peerDependencies": { "boperators": "0.3.0", "typescript": ">=5.0.0 <5.10.0" } }], + + "@boperators/plugin-ts-language-server": ["@boperators/plugin-ts-language-server@file:../../plugins/ts-language-server", { "devDependencies": { "boperators": "file:../../package" }, "peerDependencies": { "boperators": "0.3.0", "typescript": ">=5.0.0 <5.10.0" } }], + + "@ts-morph/common": ["@ts-morph/common@0.28.1", "", { "dependencies": { "minimatch": "^10.0.1", "path-browserify": "^1.0.1", "tinyglobby": "^0.2.14" } }, "sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g=="], + + "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], + + "@types/node": ["@types/node@22.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w=="], + + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "boperators": ["boperators@file:../../package", { "dependencies": { "ts-morph": "^27.0.0" }, "devDependencies": { "@types/node": "^22.0.0" }, "peerDependencies": { "typescript": ">=5.0.0 <5.10.0" } }], + + "brace-expansion": ["brace-expansion@5.0.3", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA=="], + + "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], + + "code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "minimatch": ["minimatch@10.2.2", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw=="], + + "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "ts-morph": ["ts-morph@27.0.2", "", { "dependencies": { "@ts-morph/common": "~0.28.1", "code-block-writer": "^13.0.3" } }, "sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "@boperators/plugin-bun/boperators": ["boperators@file:..\\..\\package", {}], + + "@boperators/plugin-ts-language-server/boperators": ["boperators@file:..\\..\\package", {}], + } +} diff --git a/examples/bun/bunfig.toml b/examples/bun/bunfig.toml new file mode 100644 index 0000000..15785d2 --- /dev/null +++ b/examples/bun/bunfig.toml @@ -0,0 +1 @@ +preload = ["./node_modules/@boperators/plugin-bun/index.ts"] diff --git a/examples/bun/package.json b/examples/bun/package.json new file mode 100644 index 0000000..306e7c6 --- /dev/null +++ b/examples/bun/package.json @@ -0,0 +1,13 @@ +{ + "name": "@boperators/example-bun", + "private": true, + "scripts": { + "start": "bun run src/index.ts" + }, + "devDependencies": { + "@boperators/plugin-bun": "file:../../plugins/bun", + "@boperators/plugin-ts-language-server": "file:../../plugins/ts-language-server", + "boperators": "file:../../package", + "typescript": "^5.0.0" + } +} diff --git a/examples/bun/src/Vec2.ts b/examples/bun/src/Vec2.ts new file mode 100644 index 0000000..eaf258a --- /dev/null +++ b/examples/bun/src/Vec2.ts @@ -0,0 +1,17 @@ +export class Vec2 { + constructor( + public x: number, + public y: number, + ) {} + + /** + * Add together two vectors. + */ + static "+"(a: Vec2, b: Vec2): Vec2 { + return new Vec2(a.x + b.x, a.y + b.y); + } + + toString(): string { + return `Vec2(${this.x}, ${this.y})`; + } +} diff --git a/examples/bun/src/index.ts b/examples/bun/src/index.ts new file mode 100644 index 0000000..63716d6 --- /dev/null +++ b/examples/bun/src/index.ts @@ -0,0 +1,7 @@ +import { Vec2 } from "./Vec2"; + +const a = new Vec2(1, 2); +const b = new Vec2(3, 4); +const c = a + b; + +console.log(c.toString()); // Vec2(4, 6) diff --git a/examples/bun/tsconfig.json b/examples/bun/tsconfig.json new file mode 100644 index 0000000..0827c4f --- /dev/null +++ b/examples/bun/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "ES2020", + "strict": true, + "plugins": [{ "name": "@boperators/plugin-ts-language-server" }] + }, + "include": ["src"] +} diff --git a/plugins/bun/README.md b/plugins/bun/README.md index 04d9e3a..69582db 100644 --- a/plugins/bun/README.md +++ b/plugins/bun/README.md @@ -4,6 +4,8 @@ Bun plugin for [boperators](https://www.npmjs.com/package/boperators) that ensures operator overloads work when running TypeScript files directly with Bun, instead of requiring an intermediate transform step. +> **Requires Bun ≥ 1.3.0.** Older versions have a known bug where preloaded plugins prevent TypeScript files from being transpiled correctly. + ## Installation ```sh diff --git a/plugins/bun/package.json b/plugins/bun/package.json index 179e296..d141948 100644 --- a/plugins/bun/package.json +++ b/plugins/bun/package.json @@ -32,6 +32,9 @@ "license.txt", "index.ts" ], + "engines": { + "bun": ">=1.3.0" + }, "peerDependencies": { "boperators": "0.3.0", "typescript": ">=5.0.0 <5.10.0" From 7210bbd9ce135235514df4f0a19fbcf55216d499 Mon Sep 17 00:00:00 2001 From: Dief Bell Date: Mon, 23 Feb 2026 18:39:29 +0000 Subject: [PATCH 09/18] Updated Bun lockfile --- bun.lock | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/bun.lock b/bun.lock index 294e432..b0fedb0 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "devDependencies": { @@ -10,7 +11,7 @@ }, "cli": { "name": "@boperators/cli", - "version": "0.2.1", + "version": "0.3.0", "bin": { "bop": "dist/index.js", "boperate": "dist/index.js", @@ -22,19 +23,19 @@ "boperators": "file:../package", }, "peerDependencies": { - "boperators": "0.2.1", + "boperators": "0.3.0", "typescript": ">=5.0.0 <5.10.0", }, }, "mcp-server": { "name": "@boperators/mcp-server", - "version": "0.2.1", + "version": "0.3.0", "bin": { "boperators-mcp": "./dist/index.js", }, "dependencies": { "@modelcontextprotocol/sdk": "^1.12.1", - "boperators": "0.2.1", + "boperators": "0.3.0", "zod": "^3.25.0", }, "devDependencies": { @@ -43,7 +44,7 @@ }, "package": { "name": "boperators", - "version": "0.2.1", + "version": "0.3.0", "dependencies": { "ts-morph": "^27.0.0", }, @@ -56,69 +57,69 @@ }, "plugins/bun": { "name": "@boperators/plugin-bun", - "version": "0.2.1", + "version": "0.3.0", "devDependencies": { "@types/bun": "^1.3.8", "boperators": "file:../../package", }, "peerDependencies": { - "boperators": "0.2.1", + "boperators": "0.3.0", "typescript": ">=5.0.0 <5.10.0", }, }, "plugins/esbuild": { "name": "@boperators/plugin-esbuild", - "version": "0.2.1", + "version": "0.3.0", "devDependencies": { "@types/node": "^25.2.3", "boperators": "file:../../package", "esbuild": "^0.25.12", }, "peerDependencies": { - "boperators": "0.2.1", + "boperators": "0.3.0", "esbuild": ">=0.17.0", "typescript": ">=5.0.0 <5.10.0", }, }, "plugins/ts-language-server": { "name": "@boperators/plugin-ts-language-server", - "version": "0.2.1", + "version": "0.3.0", "devDependencies": { "boperators": "file:../../package", }, "peerDependencies": { - "boperators": "0.2.1", + "boperators": "0.3.0", "typescript": ">=5.0.0 <5.10.0", }, }, "plugins/tsc": { "name": "@boperators/plugin-tsc", - "version": "0.2.1", + "version": "0.3.0", "devDependencies": { "boperators": "file:../../package", }, "peerDependencies": { - "boperators": "0.2.1", + "boperators": "0.3.0", "typescript": ">=5.0.0 <5.10.0", }, }, "plugins/vite": { "name": "@boperators/plugin-vite", - "version": "0.2.1", + "version": "0.3.0", "devDependencies": { "@types/node": "^25.2.3", "boperators": "file:../../package", "vite": "^6.3.5", }, "peerDependencies": { - "boperators": "0.2.1", + "boperators": "0.3.0", "typescript": ">=5.0.0 <5.10.0", "vite": ">=4.0.0", }, }, "plugins/webpack": { "name": "@boperators/webpack-loader", - "version": "0.2.1", + "version": "0.3.0", "devDependencies": { "@types/node": "^25.2.3", "@types/webpack": "^5.28.5", @@ -126,7 +127,7 @@ "webpack": "^5.105.2", }, "peerDependencies": { - "boperators": "0.2.1", + "boperators": "0.3.0", "typescript": ">=5.0.0 <5.10.0", }, }, From adf00ba8602452edf0c467891311b556d36294b0 Mon Sep 17 00:00:00 2001 From: Dief Bell Date: Mon, 23 Feb 2026 18:43:13 +0000 Subject: [PATCH 10/18] Note about breakpoints with the Bun plugin --- plugins/bun/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/bun/README.md b/plugins/bun/README.md index 69582db..fa87f09 100644 --- a/plugins/bun/README.md +++ b/plugins/bun/README.md @@ -32,6 +32,10 @@ and reference that in your `bunfig.toml`: preload = ["./preload.ts"] ``` +## Source Maps + +Bun's runtime plugin API (`OnLoadResult`) does not support returning a source map from `onLoad` callbacks. This means breakpoints will land on the **transformed** code (e.g. `Vec2["+"](a, b)`) rather than the original operator expression (e.g. `a + b`). The [webpack loader](../webpack/) and [Vite plugin](../vite/) do not have this limitation. + ## Comparison with Other Approaches | Approach | When it runs | Use case | From a9523e7c8b0dbe0511e8f75f39175e0fb55089a4 Mon Sep 17 00:00:00 2001 From: Dief Bell Date: Mon, 23 Feb 2026 18:52:38 +0000 Subject: [PATCH 11/18] Sorted sourcemaps for method-based overloads --- package/src/index.ts | 1 + plugins/ts-language-server/src/index.ts | 161 +++++++++--------------- 2 files changed, 59 insertions(+), 103 deletions(-) diff --git a/package/src/index.ts b/package/src/index.ts index f3a1d7c..e6f366e 100644 --- a/package/src/index.ts +++ b/package/src/index.ts @@ -9,6 +9,7 @@ export type { } from "./core/BopConfig"; export { ConsoleLogger, loadConfig } from "./core/BopConfig"; export { ErrorDescription, ErrorManager } from "./core/ErrorManager"; +export { getOperatorStringFromMethod } from "./core/helpers/getOperatorStringFromMethod"; export { getOperatorStringFromProperty } from "./core/helpers/getOperatorStringFromProperty"; export { resolveExpressionType } from "./core/helpers/resolveExpressionType"; export { unwrapInitializer } from "./core/helpers/unwrapInitializer"; diff --git a/plugins/ts-language-server/src/index.ts b/plugins/ts-language-server/src/index.ts index 7591e92..1c33a52 100644 --- a/plugins/ts-language-server/src/index.ts +++ b/plugins/ts-language-server/src/index.ts @@ -1,7 +1,7 @@ import { type BopLogger, ErrorManager, - getOperatorStringFromProperty, + getOperatorStringFromMethod, isOperatorSyntaxKind, isPostfixUnaryOperatorSyntaxKind, isPrefixUnaryOperatorSyntaxKind, @@ -13,7 +13,6 @@ import { SyntaxKind, Project as TsMorphProject, type SourceFile as TsMorphSourceFile, - unwrapInitializer, } from "boperators"; import type tsRuntime from "typescript/lib/tsserverlibrary"; import { SourceMap } from "./SourceMap"; @@ -43,7 +42,13 @@ type OverloadEditInfo = { className: string; classFilePath: string; operatorString: string; - index: number; + returnType: string; + /** LHS type (binary overloads only) */ + lhsType?: string; + /** RHS type (binary overloads only) */ + rhsType?: string; + /** Operand type (unary overloads only) */ + operandType?: string; isStatic: boolean; kind: "binary" | "prefixUnary" | "postfixUnary"; }; @@ -262,7 +267,9 @@ function findOverloadEdits( className: overloadDesc.className, classFilePath: overloadDesc.classFilePath, operatorString: overloadDesc.operatorString, - index: overloadDesc.index, + returnType: overloadDesc.returnType, + lhsType: leftType, + rhsType: rightType, isStatic: overloadDesc.isStatic, kind: "binary", }); @@ -299,7 +306,8 @@ function findOverloadEdits( className: overloadDesc.className, classFilePath: overloadDesc.classFilePath, operatorString: overloadDesc.operatorString, - index: overloadDesc.index, + returnType: overloadDesc.returnType, + operandType: operandType, isStatic: overloadDesc.isStatic, kind: "prefixUnary", }); @@ -336,7 +344,8 @@ function findOverloadEdits( className: overloadDesc.className, classFilePath: overloadDesc.classFilePath, operatorString: overloadDesc.operatorString, - index: overloadDesc.index, + returnType: overloadDesc.returnType, + operandType: operandType, isStatic: overloadDesc.isStatic, kind: "postfixUnary", }); @@ -358,135 +367,81 @@ function getOverloadHoverInfo( edit: OverloadEditInfo, ): tsRuntime.QuickInfo | undefined { try { - const classSourceFile = project.getSourceFile(edit.classFilePath); - if (!classSourceFile) return undefined; - - const classDecl = classSourceFile.getClass(edit.className); - if (!classDecl) return undefined; - - // Find the property with the matching operator string - const prop = classDecl.getProperties().find((p) => { - if (!Node.isPropertyDeclaration(p)) return false; - return getOperatorStringFromProperty(p) === edit.operatorString; - }); - if (!prop || !Node.isPropertyDeclaration(prop)) return undefined; - - // Extract param types and return type from either the initializer (regular - // .ts files) or the type annotation (.d.ts files where initializers are - // stripped by TypeScript's declaration emit). - let params: { typeName: string }[] = []; - let returnTypeName: string; + // Extract JSDoc from the method declaration (or its first overload signature). let docText: string | undefined; - - const initializer = unwrapInitializer(prop.getInitializer()); - if (initializer && Node.isArrayLiteralExpression(initializer)) { - const element = initializer.getElements()[edit.index]; - if ( - !element || - (!Node.isFunctionExpression(element) && !Node.isArrowFunction(element)) - ) - return undefined; - - const nonThisParams = element - .getParameters() - .filter((p) => p.getName() !== "this"); - params = nonThisParams.map((p) => ({ - typeName: p.getType().getText(element), - })); - returnTypeName = element.getReturnType().getText(element); - - const jsDocs = element.getJsDocs(); - if (jsDocs.length > 0) { - const raw = jsDocs[0].getText(); - docText = raw - .replace(/^\/\*\*\s*/, "") - .replace(/\s*\*\/$/, "") - .replace(/^\s*\* ?/gm, "") - .trim(); - } - } else { - // Type-annotation fallback for .d.ts files - const propertyType = prop.getType(); - if (!propertyType.isTuple()) return undefined; - const tupleElements = propertyType.getTupleElements(); - if (edit.index >= tupleElements.length) return undefined; - - const elementType = tupleElements[edit.index]; - const callSigs = elementType.getCallSignatures(); - if (callSigs.length === 0) return undefined; - const sig = callSigs[0]; - - for (const sym of sig.getParameters()) { - if (sym.getName() === "this") continue; - const decl = sym.getValueDeclaration(); - if (!decl) continue; - params.push({ typeName: decl.getType().getText(prop) }); + const classSourceFile = project.getSourceFile(edit.classFilePath); + if (classSourceFile) { + const classDecl = classSourceFile.getClass(edit.className); + if (classDecl) { + const method = classDecl + .getMethods() + .find((m) => getOperatorStringFromMethod(m) === edit.operatorString); + if (method) { + const overloads = method.getOverloads(); + const source = overloads.length > 0 ? overloads[0] : method; + const jsDocs = source.getJsDocs(); + if (jsDocs.length > 0) { + const raw = jsDocs[0].getText(); + docText = raw + .replace(/^\/\*\*\s*/, "") + .replace(/\s*\*\/$/, "") + .replace(/^\s*\* ?/gm, "") + .trim(); + } + } } - returnTypeName = sig.getReturnType().getText(prop); } - // Build display signature parts based on overload kind + // Build display signature parts based on overload kind. + // Types are sourced from the resolved expression types stored at scan time. + const returnTypeName = edit.returnType; const displayParts: tsRuntime.SymbolDisplayPart[] = []; if (edit.kind === "prefixUnary") { // Prefix unary: "-Vector3 = Vector3" + displayParts.push({ text: edit.operatorString, kind: "operator" }); displayParts.push({ - text: edit.operatorString, - kind: "operator", + text: edit.operandType ?? edit.className, + kind: "className", }); - const operandType = - params.length >= 1 ? params[0].typeName : edit.className; - displayParts.push({ text: operandType, kind: "className" }); if (returnTypeName !== "void") { displayParts.push({ text: " = ", kind: "punctuation" }); - displayParts.push({ - text: returnTypeName, - kind: "className", - }); + displayParts.push({ text: returnTypeName, kind: "className" }); } } else if (edit.kind === "postfixUnary") { // Postfix unary: "Vector3++" displayParts.push({ text: edit.className, kind: "className" }); + displayParts.push({ text: edit.operatorString, kind: "operator" }); + } else if (edit.isStatic) { + // Binary static: "LhsType + RhsType = ReturnType" displayParts.push({ - text: edit.operatorString, - kind: "operator", + text: edit.lhsType ?? edit.className, + kind: "className", }); - } else if (edit.isStatic && params.length >= 2) { - // Binary static: "LhsType + RhsType = ReturnType" - const lhsType = params[0].typeName; - const rhsType = params[1].typeName; - displayParts.push({ text: lhsType, kind: "className" }); + displayParts.push({ text: " ", kind: "space" }); + displayParts.push({ text: edit.operatorString, kind: "operator" }); displayParts.push({ text: " ", kind: "space" }); displayParts.push({ - text: edit.operatorString, - kind: "operator", + text: edit.rhsType ?? edit.className, + kind: "className", }); - displayParts.push({ text: " ", kind: "space" }); - displayParts.push({ text: rhsType, kind: "className" }); if (returnTypeName !== "void") { displayParts.push({ text: " = ", kind: "punctuation" }); - displayParts.push({ - text: returnTypeName, - kind: "className", - }); + displayParts.push({ text: returnTypeName, kind: "className" }); } } else { // Binary instance: "ClassName += RhsType" - const rhsType = params.length >= 1 ? params[0].typeName : "unknown"; displayParts.push({ text: edit.className, kind: "className" }); displayParts.push({ text: " ", kind: "space" }); + displayParts.push({ text: edit.operatorString, kind: "operator" }); + displayParts.push({ text: " ", kind: "space" }); displayParts.push({ - text: edit.operatorString, - kind: "operator", + text: edit.rhsType ?? "unknown", + kind: "className", }); - displayParts.push({ text: " ", kind: "space" }); - displayParts.push({ text: rhsType, kind: "className" }); if (returnTypeName !== "void") { displayParts.push({ text: " = ", kind: "punctuation" }); - displayParts.push({ - text: returnTypeName, - kind: "className", - }); + displayParts.push({ text: returnTypeName, kind: "className" }); } } From 2ccf5b3e21071819224b31cc3dc3ed1d34f8ac53 Mon Sep 17 00:00:00 2001 From: Dief Bell Date: Mon, 23 Feb 2026 19:04:02 +0000 Subject: [PATCH 12/18] Stipping "import" from intellisense hover --- plugins/ts-language-server/src/index.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/plugins/ts-language-server/src/index.ts b/plugins/ts-language-server/src/index.ts index 1c33a52..88417d9 100644 --- a/plugins/ts-language-server/src/index.ts +++ b/plugins/ts-language-server/src/index.ts @@ -356,6 +356,14 @@ function findOverloadEdits( // ----- Overload hover info ----- +/** + * Strip fully-qualified import paths from a type name so that + * `import("/path/to/Vec2").Vec2` is displayed as just `Vec2`. + */ +function simplifyTypeName(typeName: string): string { + return typeName.replace(/\bimport\("[^"]*"\)\./g, ""); +} + /** * Build a QuickInfo response for hovering over an operator token * that corresponds to an overloaded operator. Extracts the function @@ -394,14 +402,14 @@ function getOverloadHoverInfo( // Build display signature parts based on overload kind. // Types are sourced from the resolved expression types stored at scan time. - const returnTypeName = edit.returnType; + const returnTypeName = simplifyTypeName(edit.returnType); const displayParts: tsRuntime.SymbolDisplayPart[] = []; if (edit.kind === "prefixUnary") { // Prefix unary: "-Vector3 = Vector3" displayParts.push({ text: edit.operatorString, kind: "operator" }); displayParts.push({ - text: edit.operandType ?? edit.className, + text: simplifyTypeName(edit.operandType ?? edit.className), kind: "className", }); if (returnTypeName !== "void") { @@ -415,14 +423,14 @@ function getOverloadHoverInfo( } else if (edit.isStatic) { // Binary static: "LhsType + RhsType = ReturnType" displayParts.push({ - text: edit.lhsType ?? edit.className, + text: simplifyTypeName(edit.lhsType ?? edit.className), kind: "className", }); displayParts.push({ text: " ", kind: "space" }); displayParts.push({ text: edit.operatorString, kind: "operator" }); displayParts.push({ text: " ", kind: "space" }); displayParts.push({ - text: edit.rhsType ?? edit.className, + text: simplifyTypeName(edit.rhsType ?? edit.className), kind: "className", }); if (returnTypeName !== "void") { @@ -436,7 +444,7 @@ function getOverloadHoverInfo( displayParts.push({ text: edit.operatorString, kind: "operator" }); displayParts.push({ text: " ", kind: "space" }); displayParts.push({ - text: edit.rhsType ?? "unknown", + text: simplifyTypeName(edit.rhsType ?? "unknown"), kind: "className", }); if (returnTypeName !== "void") { From f2b716b44e2ab8d4d98afa25ba85b0829bbb0dd9 Mon Sep 17 00:00:00 2001 From: Dief Bell Date: Mon, 23 Feb 2026 19:09:10 +0000 Subject: [PATCH 13/18] Updated Bun lockfiles for example projects --- examples/cli/bun.lock | 51 +++++++ examples/esbuild/bun.lock | 87 +++++++++++ examples/nextjs/bun.lock | 152 +++++++++++++++++++ examples/vite/bun.lock | 159 ++++++++++++++++++++ examples/webpack/bun.lock | 301 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 750 insertions(+) create mode 100644 examples/cli/bun.lock create mode 100644 examples/esbuild/bun.lock create mode 100644 examples/nextjs/bun.lock create mode 100644 examples/vite/bun.lock create mode 100644 examples/webpack/bun.lock diff --git a/examples/cli/bun.lock b/examples/cli/bun.lock new file mode 100644 index 0000000..2abcb29 --- /dev/null +++ b/examples/cli/bun.lock @@ -0,0 +1,51 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "@boperators/example-cli", + "devDependencies": { + "@boperators/cli": "file:../../cli", + "boperators": "file:../../package", + "typescript": "^5.0.0", + }, + }, + }, + "packages": { + "@boperators/cli": ["@boperators/cli@file:../../cli", { "dependencies": { "@commander-js/extra-typings": "^13.0.0" }, "devDependencies": { "boperators": "file:../package" }, "peerDependencies": { "boperators": "0.3.0", "typescript": ">=5.0.0 <5.10.0" }, "bin": { "bop": "dist/index.js", "boperate": "dist/index.js" } }], + + "@commander-js/extra-typings": ["@commander-js/extra-typings@13.1.0", "", { "peerDependencies": { "commander": "~13.1.0" } }, "sha512-q5P52BYb1hwVWE6dtID7VvuJWrlfbCv4klj7BjUUOqMz4jbSZD4C9fJ9lRjL2jnBGTg+gDDlaXN51rkWcLk4fg=="], + + "@ts-morph/common": ["@ts-morph/common@0.28.1", "", { "dependencies": { "minimatch": "^10.0.1", "path-browserify": "^1.0.1", "tinyglobby": "^0.2.14" } }, "sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g=="], + + "@types/node": ["@types/node@22.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w=="], + + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "boperators": ["boperators@file:../../package", { "dependencies": { "ts-morph": "^27.0.0" }, "devDependencies": { "@types/node": "^22.0.0" }, "peerDependencies": { "typescript": ">=5.0.0 <5.10.0" } }], + + "brace-expansion": ["brace-expansion@5.0.3", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA=="], + + "code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="], + + "commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "minimatch": ["minimatch@10.2.2", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw=="], + + "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "ts-morph": ["ts-morph@27.0.2", "", { "dependencies": { "@ts-morph/common": "~0.28.1", "code-block-writer": "^13.0.3" } }, "sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "@boperators/cli/boperators": ["boperators@file:..\\..\\package", {}], + } +} diff --git a/examples/esbuild/bun.lock b/examples/esbuild/bun.lock new file mode 100644 index 0000000..948878c --- /dev/null +++ b/examples/esbuild/bun.lock @@ -0,0 +1,87 @@ +{ + "lockfileVersion": 1, + "configVersion": 0, + "workspaces": { + "": { + "name": "@boperators/example-esbuild", + "devDependencies": { + "@boperators/plugin-esbuild": "file:../../plugins/esbuild", + "@boperators/plugin-ts-language-server": "file:../../plugins/ts-language-server", + "boperators": "file:../../package", + "esbuild": "^0.25.12", + "typescript": "^5.0.0", + }, + }, + }, + "packages": { + "@boperators/plugin-esbuild": ["@boperators/plugin-esbuild@file:../../plugins/esbuild", { "devDependencies": { "boperators": "file:../../package", "esbuild": "^0.25.12" }, "peerDependencies": { "boperators": "0.1.4", "esbuild": ">=0.17.0", "typescript": ">=5.0.0 <5.10.0" } }], + + "@boperators/plugin-ts-language-server": ["@boperators/plugin-ts-language-server@file:../../plugins/ts-language-server", { "devDependencies": { "boperators": "file:../../package" }, "peerDependencies": { "boperators": "0.1.4", "typescript": ">=5.0.0 <5.10.0" } }], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "boperators": ["boperators@file:../../package", { "peerDependencies": { "typescript": ">=5.0.0 <5.10.0" } }], + + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": "bin/esbuild" }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "@boperators/plugin-esbuild/boperators": ["boperators@file:../../package", { "peerDependencies": { "typescript": ">=5.0.0 <5.10.0" } }], + + "@boperators/plugin-esbuild/boperators": ["boperators@file:../../package", { "peerDependencies": { "typescript": ">=5.0.0 <5.10.0" } }], + + "@boperators/plugin-ts-language-server/boperators": ["boperators@file:../../package", { "peerDependencies": { "typescript": ">=5.0.0 <5.10.0" } }], + + "@boperators/plugin-ts-language-server/boperators": ["boperators@file:../../package", { "peerDependencies": { "typescript": ">=5.0.0 <5.10.0" } }], + } +} diff --git a/examples/nextjs/bun.lock b/examples/nextjs/bun.lock new file mode 100644 index 0000000..2eee2f7 --- /dev/null +++ b/examples/nextjs/bun.lock @@ -0,0 +1,152 @@ +{ + "lockfileVersion": 1, + "configVersion": 0, + "workspaces": { + "": { + "name": "@boperators/example-nextjs", + "dependencies": { + "next": "^15.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + }, + "devDependencies": { + "@boperators/plugin-ts-language-server": "file:../../plugins/ts-language-server", + "@boperators/webpack-loader": "file:../../plugins/webpack", + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "boperators": "file:../../package", + "typescript": "^5.0.0", + }, + }, + }, + "packages": { + "@boperators/plugin-ts-language-server": ["@boperators/plugin-ts-language-server@file:../../plugins/ts-language-server", { "devDependencies": { "boperators": "file:../../package" }, "peerDependencies": { "boperators": "0.1.4", "typescript": ">=5.0.0 <5.10.0" } }], + + "@boperators/webpack-loader": ["@boperators/webpack-loader@file:../../plugins/webpack", { "devDependencies": { "@types/node": "^25.2.3", "boperators": "file:../../package" }, "peerDependencies": { "boperators": "0.1.4", "typescript": ">=5.0.0 <5.10.0" } }], + + "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], + + "@img/colour": ["@img/colour@1.0.0", "", {}, ""], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, ""], + + "@next/env": ["@next/env@15.5.12", "", {}, ""], + + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.5.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RnRjBtH8S8eXCpUNkQ+543DUc7ys8y15VxmFU9HRqlo9BG3CcBUiwNtF8SNoi2xvGCVJq1vl2yYq+3oISBS0Zg=="], + + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.5.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-nqa9/7iQlboF1EFtNhWxQA0rQstmYRSBGxSM6g3GxvxHxcoeqVXfGNr9stJOme674m2V7r4E3+jEhhGvSQhJRA=="], + + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.5.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-dCzAjqhDHwmoB2M4eYfVKqXs99QdQxNQVpftvP1eGVppamXh/OkDAwV737Zr0KPXEqRUMN4uCjh6mjO+XtF3Mw=="], + + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.5.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-+fpGWvQiITgf7PUtbWY1H7qUSnBZsPPLyyq03QuAKpVoTy/QUx1JptEDTQMVvQhvizCEuNLEeghrQUyXQOekuw=="], + + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.5.12", "", { "os": "linux", "cpu": "x64" }, "sha512-jSLvgdRRL/hrFAPqEjJf1fFguC719kmcptjNVDJl26BnJIpjL3KH5h6mzR4mAweociLQaqvt4UyzfbFjgAdDcw=="], + + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.5.12", "", { "os": "linux", "cpu": "x64" }, "sha512-/uaF0WfmYqQgLfPmN6BvULwxY0dufI2mlN2JbOKqqceZh1G4hjREyi7pg03zjfyS6eqNemHAZPSoP84x17vo6w=="], + + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.5.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-xhsL1OvQSfGmlL5RbOmU+FV120urrgFpYLq+6U8C6KIym32gZT6XF/SDE92jKzzlPWskkbjOKCpqk5m4i8PEfg=="], + + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.12", "", { "os": "win32", "cpu": "x64" }, ""], + + "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, ""], + + "@types/node": ["@types/node@22.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, ""], + + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, ""], + + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, ""], + + "boperators": ["boperators@file:../../package", { "devDependencies": { "@types/node": "^22.0.0" }, "peerDependencies": { "typescript": ">=5.0.0 <5.10.0" } }], + + "caniuse-lite": ["caniuse-lite@1.0.30001770", "", {}, ""], + + "client-only": ["client-only@0.0.1", "", {}, ""], + + "csstype": ["csstype@3.2.3", "", {}, ""], + + "detect-libc": ["detect-libc@2.1.2", "", {}, ""], + + "nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, ""], + + "next": ["next@15.5.12", "", { "dependencies": { "@next/env": "15.5.12", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.12", "@next/swc-darwin-x64": "15.5.12", "@next/swc-linux-arm64-gnu": "15.5.12", "@next/swc-linux-arm64-musl": "15.5.12", "@next/swc-linux-x64-gnu": "15.5.12", "@next/swc-linux-x64-musl": "15.5.12", "@next/swc-win32-arm64-msvc": "15.5.12", "@next/swc-win32-x64-msvc": "15.5.12", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": "dist/bin/next" }, ""], + + "picocolors": ["picocolors@1.1.1", "", {}, ""], + + "postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, ""], + + "react": ["react@19.2.4", "", {}, ""], + + "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, ""], + + "scheduler": ["scheduler@0.27.0", "", {}, ""], + + "semver": ["semver@7.7.4", "", { "bin": "bin/semver.js" }, ""], + + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, ""], + + "source-map-js": ["source-map-js@1.2.1", "", {}, ""], + + "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, ""], + + "tslib": ["tslib@2.8.1", "", {}, ""], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, ""], + + "undici-types": ["undici-types@6.21.0", "", {}, ""], + + "@boperators/plugin-ts-language-server/boperators": ["boperators@file:../../package", { "devDependencies": { "@types/node": "^22.0.0" }, "peerDependencies": { "typescript": ">=5.0.0 <5.10.0" } }], + + "@boperators/plugin-ts-language-server/boperators": ["boperators@file:../../package", { "devDependencies": { "@types/node": "^22.0.0" }, "peerDependencies": { "typescript": ">=5.0.0 <5.10.0" } }], + + "@boperators/webpack-loader/boperators": ["boperators@file:../../package", { "devDependencies": { "@types/node": "^22.0.0" }, "peerDependencies": { "typescript": ">=5.0.0 <5.10.0" } }], + + "@boperators/webpack-loader/boperators": ["boperators@file:../../package", { "devDependencies": { "@types/node": "^22.0.0" }, "peerDependencies": { "typescript": ">=5.0.0 <5.10.0" } }], + } +} diff --git a/examples/vite/bun.lock b/examples/vite/bun.lock new file mode 100644 index 0000000..391a743 --- /dev/null +++ b/examples/vite/bun.lock @@ -0,0 +1,159 @@ +{ + "lockfileVersion": 1, + "configVersion": 0, + "workspaces": { + "": { + "name": "@boperators/example-vite", + "devDependencies": { + "@boperators/plugin-ts-language-server": "file:../../plugins/ts-language-server", + "@boperators/plugin-vite": "file:../../plugins/vite", + "boperators": "file:../../package", + "typescript": "^5.0.0", + "vite": "^6.3.5", + }, + }, + }, + "packages": { + "@boperators/plugin-ts-language-server": ["@boperators/plugin-ts-language-server@file:../../plugins/ts-language-server", { "devDependencies": { "boperators": "file:../../package" }, "peerDependencies": { "boperators": "0.1.4", "typescript": ">=5.0.0 <5.10.0" } }], + + "@boperators/plugin-vite": ["@boperators/plugin-vite@file:../../plugins/vite", { "devDependencies": { "boperators": "file:../../package", "vite": "^6.3.5" }, "peerDependencies": { "boperators": "0.1.4", "typescript": ">=5.0.0 <5.10.0", "vite": ">=4.0.0" } }], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.58.0", "", { "os": "android", "cpu": "arm" }, "sha512-mr0tmS/4FoVk1cnaeN244A/wjvGDNItZKR8hRhnmCzygyRXYtKF5jVDSIILR1U97CTzAYmbgIj/Dukg62ggG5w=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.58.0", "", { "os": "android", "cpu": "arm64" }, "sha512-+s++dbp+/RTte62mQD9wLSbiMTV+xr/PeRJEc/sFZFSBRlHPNPVaf5FXlzAL77Mr8FtSfQqCN+I598M8U41ccQ=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.58.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MFWBwTcYs0jZbINQBXHfSrpSQJq3IUOakcKPzfeSznONop14Pxuqa0Kg19GD0rNBMPQI2tFtu3UzapZpH0Uc1Q=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.58.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-yiKJY7pj9c9JwzuKYLFaDZw5gma3fI9bkPEIyofvVfsPqjCWPglSHdpdwXpKGvDeYDms3Qal8qGMEHZ1M/4Udg=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.58.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-x97kCoBh5MOevpn/CNK9W1x8BEzO238541BGWBc315uOlN0AD/ifZ1msg+ZQB05Ux+VF6EcYqpiagfLJ8U3LvQ=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.58.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Aa8jPoZ6IQAG2eIrcXPpjRcMjROMFxCt1UYPZZtCxRV68WkuSigYtQ/7Zwrcr2IvtNJo7T2JfDXyMLxq5L4Jlg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.58.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Ob8YgT5kD/lSIYW2Rcngs5kNB/44Q2RzBSPz9brf2WEtcGR7/f/E9HeHn1wYaAwKBni+bdXEwgHvUd0x12lQSA=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.58.0", "", { "os": "linux", "cpu": "arm" }, "sha512-K+RI5oP1ceqoadvNt1FecL17Qtw/n9BgRSzxif3rTL2QlIu88ccvY+Y9nnHe/cmT5zbH9+bpiJuG1mGHRVwF4Q=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.58.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-T+17JAsCKUjmbopcKepJjHWHXSjeW7O5PL7lEFaeQmiVyw4kkc5/lyYKzrv6ElWRX/MrEWfPiJWqbTvfIvjM1Q=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.58.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-cCePktb9+6R9itIJdeCFF9txPU7pQeEHB5AbHu/MKsfH/k70ZtOeq1k4YAtBv9Z7mmKI5/wOLYjQ+B9QdxR6LA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.58.0", "", { "os": "linux", "cpu": "none" }, "sha512-iekUaLkfliAsDl4/xSdoCJ1gnnIXvoNz85C8U8+ZxknM5pBStfZjeXgB8lXobDQvvPRCN8FPmmuTtH+z95HTmg=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.58.0", "", { "os": "linux", "cpu": "none" }, "sha512-68ofRgJNl/jYJbxFjCKE7IwhbfxOl1muPN4KbIqAIe32lm22KmU7E8OPvyy68HTNkI2iV/c8y2kSPSm2mW/Q9Q=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.58.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-dpz8vT0i+JqUKuSNPCP5SYyIV2Lh0sNL1+FhM7eLC457d5B9/BC3kDPp5BBftMmTNsBarcPcoz5UGSsnCiw4XQ=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.58.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4gdkkf9UJ7tafnweBCR/mk4jf3Jfl0cKX9Np80t5i78kjIH0ZdezUv/JDI2VtruE5lunfACqftJ8dIMGN4oHew=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.58.0", "", { "os": "linux", "cpu": "none" }, "sha512-YFS4vPnOkDTD/JriUeeZurFYoJhPf9GQQEF/v4lltp3mVcBmnsAdjEWhr2cjUCZzZNzxCG0HZOvJU44UGHSdzw=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.58.0", "", { "os": "linux", "cpu": "none" }, "sha512-x2xgZlFne+QVNKV8b4wwaCS8pwq3y14zedZ5DqLzjdRITvreBk//4Knbcvm7+lWmms9V9qFp60MtUd0/t/PXPw=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.58.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-jIhrujyn4UnWF8S+DHSkAkDEO3hLX0cjzxJZPLF80xFyzyUIYgSMRcYQ3+uqEoyDD2beGq7Dj7edi8OnJcS/hg=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.58.0", "", { "os": "linux", "cpu": "x64" }, "sha512-+410Srdoh78MKSJxTQ+hZ/Mx+ajd6RjjPwBPNd0R3J9FtL6ZA0GqiiyNjCO9In0IzZkCNrpGymSfn+kgyPQocg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.58.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZjMyby5SICi227y1MTR3VYBpFTdZs823Rs/hpakufleBoufoOIB6jtm9FEoxn/cgO7l6PM2rCEl5Kre5vX0QrQ=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.58.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-ds4iwfYkSQ0k1nb8LTcyXw//ToHOnNTJtceySpL3fa7tc/AsE+UpUFphW126A6fKBGJD5dhRvg8zw1rvoGFxmw=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.58.0", "", { "os": "none", "cpu": "arm64" }, "sha512-fd/zpJniln4ICdPkjWFhZYeY/bpnaN9pGa6ko+5WD38I0tTqk9lXMgXZg09MNdhpARngmxiCg0B0XUamNw/5BQ=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.58.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-YpG8dUOip7DCz3nr/JUfPbIUo+2d/dy++5bFzgi4ugOGBIox+qMbbqt/JoORwvI/C9Kn2tz6+Bieoqd5+B1CjA=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.58.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-b9DI8jpFQVh4hIXFr0/+N/TzLdpBIoPzjt0Rt4xJbW3mzguV3mduR9cNgiuFcuL/TeORejJhCWiAXe3E/6PxWA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.58.0", "", { "os": "win32", "cpu": "x64" }, "sha512-CSrVpmoRJFN06LL9xhkitkwUcTZtIotYAF5p6XOR2zW0Zz5mzb3IPpcoPhB02frzMHFNo1reQ9xSF5fFm3hUsQ=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.58.0", "", { "os": "win32", "cpu": "x64" }, "sha512-QFsBgQNTnh5K0t/sBsjJLq24YVqEIVkGpfN2VHsnN90soZyhaiA9UUHufcctVNL4ypJY0wrwad0wslx2KJQ1/w=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "boperators": ["boperators@file:../../package", { "peerDependencies": { "typescript": ">=5.0.0 <5.10.0" } }], + + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": "bin/esbuild" }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "rollup": ["rollup@4.58.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.58.0", "@rollup/rollup-android-arm64": "4.58.0", "@rollup/rollup-darwin-arm64": "4.58.0", "@rollup/rollup-darwin-x64": "4.58.0", "@rollup/rollup-freebsd-arm64": "4.58.0", "@rollup/rollup-freebsd-x64": "4.58.0", "@rollup/rollup-linux-arm-gnueabihf": "4.58.0", "@rollup/rollup-linux-arm-musleabihf": "4.58.0", "@rollup/rollup-linux-arm64-gnu": "4.58.0", "@rollup/rollup-linux-arm64-musl": "4.58.0", "@rollup/rollup-linux-loong64-gnu": "4.58.0", "@rollup/rollup-linux-loong64-musl": "4.58.0", "@rollup/rollup-linux-ppc64-gnu": "4.58.0", "@rollup/rollup-linux-ppc64-musl": "4.58.0", "@rollup/rollup-linux-riscv64-gnu": "4.58.0", "@rollup/rollup-linux-riscv64-musl": "4.58.0", "@rollup/rollup-linux-s390x-gnu": "4.58.0", "@rollup/rollup-linux-x64-gnu": "4.58.0", "@rollup/rollup-linux-x64-musl": "4.58.0", "@rollup/rollup-openbsd-x64": "4.58.0", "@rollup/rollup-openharmony-arm64": "4.58.0", "@rollup/rollup-win32-arm64-msvc": "4.58.0", "@rollup/rollup-win32-ia32-msvc": "4.58.0", "@rollup/rollup-win32-x64-gnu": "4.58.0", "@rollup/rollup-win32-x64-msvc": "4.58.0", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-wbT0mBmWbIvvq8NeEYWWvevvxnOyhKChir47S66WCxw1SXqhw7ssIYejnQEVt7XYQpsj2y8F9PM+Cr3SNEa0gw=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": "bin/vite.js" }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], + + "@boperators/plugin-ts-language-server/boperators": ["boperators@file:../../package", { "peerDependencies": { "typescript": ">=5.0.0 <5.10.0" } }], + + "@boperators/plugin-ts-language-server/boperators": ["boperators@file:../../package", { "peerDependencies": { "typescript": ">=5.0.0 <5.10.0" } }], + + "@boperators/plugin-vite/boperators": ["boperators@file:../../package", { "peerDependencies": { "typescript": ">=5.0.0 <5.10.0" } }], + + "@boperators/plugin-vite/boperators": ["boperators@file:../../package", { "peerDependencies": { "typescript": ">=5.0.0 <5.10.0" } }], + } +} diff --git a/examples/webpack/bun.lock b/examples/webpack/bun.lock new file mode 100644 index 0000000..5211e05 --- /dev/null +++ b/examples/webpack/bun.lock @@ -0,0 +1,301 @@ +{ + "lockfileVersion": 1, + "configVersion": 0, + "workspaces": { + "": { + "name": "@boperators/example-webpack", + "devDependencies": { + "@boperators/plugin-ts-language-server": "file:../../plugins/ts-language-server", + "@boperators/webpack-loader": "file:../../plugins/webpack", + "boperators": "file:../../package", + "ts-loader": "^9.5.2", + "typescript": "^5.0.0", + "webpack": "^5.0.0", + "webpack-cli": "^6.0.0", + }, + }, + }, + "packages": { + "@boperators/plugin-ts-language-server": ["@boperators/plugin-ts-language-server@file:../../plugins/ts-language-server", { "devDependencies": { "boperators": "file:../../package" }, "peerDependencies": { "boperators": "0.1.4", "typescript": ">=5.0.0 <5.10.0" } }], + + "@boperators/webpack-loader": ["@boperators/webpack-loader@file:../../plugins/webpack", { "devDependencies": { "@types/node": "^25.2.3", "boperators": "file:../../package", "webpack": "^5.105.2" }, "peerDependencies": { "boperators": "0.1.4", "typescript": ">=5.0.0 <5.10.0" } }], + + "@discoveryjs/json-ext": ["@discoveryjs/json-ext@0.6.3", "", {}, ""], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, ""], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, ""], + + "@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, ""], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, ""], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, ""], + + "@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, ""], + + "@types/eslint-scope": ["@types/eslint-scope@3.7.7", "", { "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, ""], + + "@types/estree": ["@types/estree@1.0.8", "", {}, ""], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, ""], + + "@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, ""], + + "@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, ""], + + "@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, ""], + + "@webassemblyjs/helper-api-error": ["@webassemblyjs/helper-api-error@1.13.2", "", {}, ""], + + "@webassemblyjs/helper-buffer": ["@webassemblyjs/helper-buffer@1.14.1", "", {}, ""], + + "@webassemblyjs/helper-numbers": ["@webassemblyjs/helper-numbers@1.13.2", "", { "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, ""], + + "@webassemblyjs/helper-wasm-bytecode": ["@webassemblyjs/helper-wasm-bytecode@1.13.2", "", {}, ""], + + "@webassemblyjs/helper-wasm-section": ["@webassemblyjs/helper-wasm-section@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/wasm-gen": "1.14.1" } }, ""], + + "@webassemblyjs/ieee754": ["@webassemblyjs/ieee754@1.13.2", "", { "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, ""], + + "@webassemblyjs/leb128": ["@webassemblyjs/leb128@1.13.2", "", { "dependencies": { "@xtuc/long": "4.2.2" } }, ""], + + "@webassemblyjs/utf8": ["@webassemblyjs/utf8@1.13.2", "", {}, ""], + + "@webassemblyjs/wasm-edit": ["@webassemblyjs/wasm-edit@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/helper-wasm-section": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-opt": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1", "@webassemblyjs/wast-printer": "1.14.1" } }, ""], + + "@webassemblyjs/wasm-gen": ["@webassemblyjs/wasm-gen@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, ""], + + "@webassemblyjs/wasm-opt": ["@webassemblyjs/wasm-opt@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1" } }, ""], + + "@webassemblyjs/wasm-parser": ["@webassemblyjs/wasm-parser@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, ""], + + "@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, ""], + + "@webpack-cli/configtest": ["@webpack-cli/configtest@3.0.1", "", { "peerDependencies": { "webpack": "^5.82.0", "webpack-cli": "6.x.x" } }, ""], + + "@webpack-cli/info": ["@webpack-cli/info@3.0.1", "", { "peerDependencies": { "webpack": "^5.82.0", "webpack-cli": "6.x.x" } }, ""], + + "@webpack-cli/serve": ["@webpack-cli/serve@3.0.1", "", { "peerDependencies": { "webpack": "^5.82.0", "webpack-cli": "6.x.x" } }, ""], + + "@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, ""], + + "@xtuc/long": ["@xtuc/long@4.2.2", "", {}, ""], + + "acorn": ["acorn@8.16.0", "", { "bin": "bin/acorn" }, ""], + + "acorn-import-phases": ["acorn-import-phases@1.0.4", "", { "peerDependencies": { "acorn": "^8.14.0" } }, ""], + + "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, ""], + + "ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" }, "peerDependencies": { "ajv": "^8.0.0" } }, ""], + + "ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, ""], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, ""], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": "dist/cli.cjs" }, ""], + + "boperators": ["boperators@file:../../package", { "devDependencies": { "@types/node": "^22.0.0" }, "peerDependencies": { "typescript": ">=5.0.0 <5.10.0" } }], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, ""], + + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": "cli.js" }, ""], + + "buffer-from": ["buffer-from@1.1.2", "", {}, ""], + + "caniuse-lite": ["caniuse-lite@1.0.30001770", "", {}, ""], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, ""], + + "chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, ""], + + "clone-deep": ["clone-deep@4.0.1", "", { "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", "shallow-clone": "^3.0.0" } }, ""], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, ""], + + "color-name": ["color-name@1.1.4", "", {}, ""], + + "colorette": ["colorette@2.0.20", "", {}, ""], + + "commander": ["commander@12.1.0", "", {}, ""], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, ""], + + "electron-to-chromium": ["electron-to-chromium@1.5.302", "", {}, ""], + + "enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, ""], + + "envinfo": ["envinfo@7.21.0", "", { "bin": "dist/cli.js" }, ""], + + "es-module-lexer": ["es-module-lexer@2.0.0", "", {}, ""], + + "escalade": ["escalade@3.2.0", "", {}, ""], + + "eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, ""], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, ""], + + "estraverse": ["estraverse@4.3.0", "", {}, ""], + + "events": ["events@3.3.0", "", {}, ""], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, ""], + + "fast-uri": ["fast-uri@3.1.0", "", {}, ""], + + "fastest-levenshtein": ["fastest-levenshtein@1.0.16", "", {}, ""], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, ""], + + "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, ""], + + "flat": ["flat@5.0.2", "", { "bin": "cli.js" }, ""], + + "function-bind": ["function-bind@1.1.2", "", {}, ""], + + "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, ""], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, ""], + + "has-flag": ["has-flag@4.0.0", "", {}, ""], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, ""], + + "import-local": ["import-local@3.2.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, ""], + + "interpret": ["interpret@3.1.1", "", {}, ""], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, ""], + + "is-number": ["is-number@7.0.0", "", {}, ""], + + "is-plain-object": ["is-plain-object@2.0.4", "", { "dependencies": { "isobject": "^3.0.1" } }, ""], + + "isexe": ["isexe@2.0.0", "", {}, ""], + + "isobject": ["isobject@3.0.1", "", {}, ""], + + "jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, ""], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, ""], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, ""], + + "kind-of": ["kind-of@6.0.3", "", {}, ""], + + "loader-runner": ["loader-runner@4.3.1", "", {}, ""], + + "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, ""], + + "merge-stream": ["merge-stream@2.0.0", "", {}, ""], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, ""], + + "mime-db": ["mime-db@1.52.0", "", {}, ""], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, ""], + + "neo-async": ["neo-async@2.6.2", "", {}, ""], + + "node-releases": ["node-releases@2.0.27", "", {}, ""], + + "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, ""], + + "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, ""], + + "p-try": ["p-try@2.2.0", "", {}, ""], + + "path-exists": ["path-exists@4.0.0", "", {}, ""], + + "path-key": ["path-key@3.1.1", "", {}, ""], + + "path-parse": ["path-parse@1.0.7", "", {}, ""], + + "picocolors": ["picocolors@1.1.1", "", {}, ""], + + "picomatch": ["picomatch@2.3.1", "", {}, ""], + + "pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, ""], + + "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, ""], + + "rechoir": ["rechoir@0.8.0", "", { "dependencies": { "resolve": "^1.20.0" } }, ""], + + "require-from-string": ["require-from-string@2.0.2", "", {}, ""], + + "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, ""], + + "resolve-cwd": ["resolve-cwd@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, ""], + + "resolve-from": ["resolve-from@5.0.0", "", {}, ""], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, ""], + + "schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, ""], + + "semver": ["semver@7.7.4", "", { "bin": "bin/semver.js" }, ""], + + "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, ""], + + "shallow-clone": ["shallow-clone@3.0.1", "", { "dependencies": { "kind-of": "^6.0.2" } }, ""], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, ""], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, ""], + + "source-map": ["source-map@0.7.6", "", {}, ""], + + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, ""], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, ""], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, ""], + + "tapable": ["tapable@2.3.0", "", {}, ""], + + "terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": "bin/terser" }, ""], + + "terser-webpack-plugin": ["terser-webpack-plugin@5.3.16", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, ""], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, ""], + + "ts-loader": ["ts-loader@9.5.4", "", { "dependencies": { "chalk": "^4.1.0", "enhanced-resolve": "^5.0.0", "micromatch": "^4.0.0", "semver": "^7.3.4", "source-map": "^0.7.4" }, "peerDependencies": { "typescript": "*", "webpack": "^5.0.0" } }, ""], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, ""], + + "undici-types": ["undici-types@7.18.2", "", {}, ""], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, ""], + + "watchpack": ["watchpack@2.5.1", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, ""], + + "webpack": ["webpack@5.105.2", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.15.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.19.0", "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.16", "watchpack": "^2.5.1", "webpack-sources": "^3.3.3" }, "bin": "bin/webpack.js" }, ""], + + "webpack-cli": ["webpack-cli@6.0.1", "", { "dependencies": { "@discoveryjs/json-ext": "^0.6.1", "@webpack-cli/configtest": "^3.0.1", "@webpack-cli/info": "^3.0.1", "@webpack-cli/serve": "^3.0.1", "colorette": "^2.0.14", "commander": "^12.1.0", "cross-spawn": "^7.0.3", "envinfo": "^7.14.0", "fastest-levenshtein": "^1.0.12", "import-local": "^3.0.2", "interpret": "^3.1.1", "rechoir": "^0.8.0", "webpack-merge": "^6.0.1" }, "peerDependencies": { "webpack": "^5.82.0" }, "bin": "bin/cli.js" }, ""], + + "webpack-merge": ["webpack-merge@6.0.1", "", { "dependencies": { "clone-deep": "^4.0.1", "flat": "^5.0.2", "wildcard": "^2.0.1" } }, ""], + + "webpack-sources": ["webpack-sources@3.3.4", "", {}, ""], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, ""], + + "wildcard": ["wildcard@2.0.1", "", {}, ""], + + "@boperators/plugin-ts-language-server/boperators": ["boperators@file:../../package", { "devDependencies": { "@types/node": "^22.0.0" }, "peerDependencies": { "typescript": ">=5.0.0 <5.10.0" } }], + + "@boperators/plugin-ts-language-server/boperators": ["boperators@file:../../package", { "devDependencies": { "@types/node": "^22.0.0" }, "peerDependencies": { "typescript": ">=5.0.0 <5.10.0" } }], + + "@boperators/webpack-loader/boperators": ["boperators@file:../../package", { "devDependencies": { "@types/node": "^22.0.0" }, "peerDependencies": { "typescript": ">=5.0.0 <5.10.0" } }], + + "@boperators/webpack-loader/boperators": ["boperators@file:../../package", { "devDependencies": { "@types/node": "^22.0.0" }, "peerDependencies": { "typescript": ">=5.0.0 <5.10.0" } }], + + "esrecurse/estraverse": ["estraverse@5.3.0", "", {}, ""], + + "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, ""], + + "source-map-support/source-map": ["source-map@0.6.1", "", {}, ""], + + "terser/commander": ["commander@2.20.3", "", {}, ""], + } +} From cc8d5bb36d7962f4de78a1900e61c7ab1e6d83f5 Mon Sep 17 00:00:00 2001 From: Dief Bell Date: Mon, 23 Feb 2026 19:10:14 +0000 Subject: [PATCH 14/18] Fixed Vite example --- .github/workflows/e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index cd0bfff..cb006ca 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -145,7 +145,7 @@ jobs: working-directory: examples/vite - name: Assert bundle contains operator overload transformation - run: grep -rq 'Vec2\["+"\]\[0\]' examples/vite/dist/ + run: grep -rq 'Vec2\["+"\](' examples/vite/dist/ # ─── Next.js example ──────────────────────────────────────────────────────── nextjs-example: From cff6837115db60d59157fe08b2f0b9aaf5f3f4fe Mon Sep 17 00:00:00 2001 From: Dief Bell Date: Mon, 23 Feb 2026 19:12:01 +0000 Subject: [PATCH 15/18] e2e test for Bun --- .github/workflows/e2e.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index cb006ca..a730032 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -114,6 +114,27 @@ jobs: run: node dist/bundle.js | grep -q "Vec2(4, 6)" working-directory: examples/esbuild + # ─── Bun example ──────────────────────────────────────────────────────────── + bun-example: + name: Bun example + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/actions/setup-monorepo + + - name: Build packages + run: bun run build + + - name: Install Bun example dependencies + run: bun install + working-directory: examples/bun + + - name: Assert Bun output + run: bun run start | grep -q "Vec2(4, 6)" + working-directory: examples/bun + # ─── Vite example ─────────────────────────────────────────────────────────── vite-example: name: Vite example From a79a05703d5060dc3809b0e4963983ea77402969 Mon Sep 17 00:00:00 2001 From: Dief Bell Date: Mon, 23 Feb 2026 19:33:57 +0000 Subject: [PATCH 16/18] Testing overloaded operator methods --- README.md | 25 +++++++++++++ examples/bun/src/Vec2.ts | 9 +++++ examples/bun/src/index.ts | 4 ++ examples/cli/src/Vec2.ts | 7 +++- examples/cli/src/index.ts | 6 ++- examples/cli/src/test.ts | 1 + examples/esbuild/src/Vec2.ts | 9 +++++ examples/esbuild/src/index.ts | 4 ++ examples/nextjs/src/Vec2.ts | 9 +++++ examples/nextjs/src/app/page.tsx | 4 ++ examples/vite/src/Vec2.ts | 9 +++++ examples/vite/src/main.ts | 13 +++++-- examples/webpack/src/Vec2.ts | 9 +++++ examples/webpack/src/index.ts | 4 ++ package/src/core/OverloadInjector.test.ts | 45 +++++++++++++++++++++++ 15 files changed, 152 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6c3ba1b..5f5fbcc 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,31 @@ v1 += v2; // => v1["+="](v2) Overloads defined on a parent class are automatically inherited by subclasses. For example, if `Expr` defines `+` and `*`, a `Sym extends Expr` class can use those operators without redeclaring them. +## Multiple overloads per operator + +A single operator can handle multiple type combinations using standard TypeScript method overload signatures. boperators registers each signature separately and dispatches to the correct one based on the operand types at each call site: + +```typescript +class Vec2 { + // Vec2 * Vec2 → component-wise multiplication + static "*"(a: Vec2, b: Vec2): Vec2; + // Vec2 * number → scalar multiplication + static "*"(a: Vec2, b: number): Vec2; + static "*"(a: Vec2, b: Vec2 | number): Vec2 { + if (b instanceof Vec2) return new Vec2(a.x * b.x, a.y * b.y); + return new Vec2(a.x * b, a.y * b); + } +} + +const a = new Vec2(1, 2); +const b = new Vec2(3, 4); + +a * b; // => Vec2["*"](a, b) → Vec2(3, 8) — routes to the Vec2 overload +a * 2; // => Vec2["*"](a, 2) → Vec2(2, 4) — routes to the number overload +``` + +The implementation method must accept the union of all overload parameter types; only the overload signatures (those without a body) are registered in the overload store. + ## Publishing a library If you are publishing a package that exports classes with operator overloads, consumers need to be able to import those classes for the transformed code to work. Run the following before publishing to catch any missing exports: diff --git a/examples/bun/src/Vec2.ts b/examples/bun/src/Vec2.ts index eaf258a..b9a6b2a 100644 --- a/examples/bun/src/Vec2.ts +++ b/examples/bun/src/Vec2.ts @@ -11,6 +11,15 @@ export class Vec2 { return new Vec2(a.x + b.x, a.y + b.y); } + // Multiple type overloads for the same operator: + // boperators dispatches Vec2 * Vec2 and Vec2 * number to separate implementations. + static "*"(a: Vec2, b: Vec2): Vec2; + static "*"(a: Vec2, b: number): Vec2; + static "*"(a: Vec2, b: Vec2 | number): Vec2 { + if (b instanceof Vec2) return new Vec2(a.x * b.x, a.y * b.y); + return new Vec2(a.x * b, a.y * b); + } + toString(): string { return `Vec2(${this.x}, ${this.y})`; } diff --git a/examples/bun/src/index.ts b/examples/bun/src/index.ts index 63716d6..76f5352 100644 --- a/examples/bun/src/index.ts +++ b/examples/bun/src/index.ts @@ -3,5 +3,9 @@ import { Vec2 } from "./Vec2"; const a = new Vec2(1, 2); const b = new Vec2(3, 4); const c = a + b; +const d = a * b; // Vec2 * Vec2 — component-wise +const e = a * 2; // Vec2 * number — scalar console.log(c.toString()); // Vec2(4, 6) +console.log(d.toString()); // Vec2(3, 8) +console.log(e.toString()); // Vec2(2, 4) diff --git a/examples/cli/src/Vec2.ts b/examples/cli/src/Vec2.ts index 2dc0261..cc25b47 100644 --- a/examples/cli/src/Vec2.ts +++ b/examples/cli/src/Vec2.ts @@ -29,8 +29,11 @@ export class Vec2 { return new Vec2(-a.x, -a.y); } - // Scalar operations - static "*"(a: Vec2, b: number): Vec2 { + // Component-wise multiplication (Vec2 * Vec2) and scalar multiplication (Vec2 * number) + static "*"(a: Vec2, b: Vec2): Vec2; + static "*"(a: Vec2, b: number): Vec2; + static "*"(a: Vec2, b: Vec2 | number): Vec2 { + if (b instanceof Vec2) return new Vec2(a.x * b.x, a.y * b.y); return new Vec2(a.x * b, a.y * b); } diff --git a/examples/cli/src/index.ts b/examples/cli/src/index.ts index 2b0f3a9..76f5352 100644 --- a/examples/cli/src/index.ts +++ b/examples/cli/src/index.ts @@ -3,5 +3,9 @@ import { Vec2 } from "./Vec2"; const a = new Vec2(1, 2); const b = new Vec2(3, 4); const c = a + b; +const d = a * b; // Vec2 * Vec2 — component-wise +const e = a * 2; // Vec2 * number — scalar -console.log(c.toString()); +console.log(c.toString()); // Vec2(4, 6) +console.log(d.toString()); // Vec2(3, 8) +console.log(e.toString()); // Vec2(2, 4) diff --git a/examples/cli/src/test.ts b/examples/cli/src/test.ts index e0765d2..f26d636 100644 --- a/examples/cli/src/test.ts +++ b/examples/cli/src/test.ts @@ -52,6 +52,7 @@ const v0 = new Vec2(0, 0); console.log("::group::Vec2 binary arithmetic"); assertEq(String(v1 + v2), "Vec2(4, 6)", "Vec2: +"); assertEq(String(v2 - v1), "Vec2(2, 2)", "Vec2: -"); +assertEq(String(v1 * v2), "Vec2(3, 8)", "Vec2: * Vec2 (component-wise)"); // 1*3=3, 2*4=8 assertEq(String(v1 * 3), "Vec2(3, 6)", "Vec2: * scalar"); assertEq(String(v2 / 2), "Vec2(1.5, 2)", "Vec2: / scalar"); assertEq(String(v2 % 3), "Vec2(0, 1)", "Vec2: % scalar"); // 3%3=0, 4%3=1 diff --git a/examples/esbuild/src/Vec2.ts b/examples/esbuild/src/Vec2.ts index 384cf80..ce2cece 100644 --- a/examples/esbuild/src/Vec2.ts +++ b/examples/esbuild/src/Vec2.ts @@ -8,6 +8,15 @@ export class Vec2 { return new Vec2(a.x + b.x, a.y + b.y); } + // Multiple type overloads for the same operator: + // boperators dispatches Vec2 * Vec2 and Vec2 * number to separate implementations. + static "*"(a: Vec2, b: Vec2): Vec2; + static "*"(a: Vec2, b: number): Vec2; + static "*"(a: Vec2, b: Vec2 | number): Vec2 { + if (b instanceof Vec2) return new Vec2(a.x * b.x, a.y * b.y); + return new Vec2(a.x * b, a.y * b); + } + toString(): string { return `Vec2(${this.x}, ${this.y})`; } diff --git a/examples/esbuild/src/index.ts b/examples/esbuild/src/index.ts index 63716d6..76f5352 100644 --- a/examples/esbuild/src/index.ts +++ b/examples/esbuild/src/index.ts @@ -3,5 +3,9 @@ import { Vec2 } from "./Vec2"; const a = new Vec2(1, 2); const b = new Vec2(3, 4); const c = a + b; +const d = a * b; // Vec2 * Vec2 — component-wise +const e = a * 2; // Vec2 * number — scalar console.log(c.toString()); // Vec2(4, 6) +console.log(d.toString()); // Vec2(3, 8) +console.log(e.toString()); // Vec2(2, 4) diff --git a/examples/nextjs/src/Vec2.ts b/examples/nextjs/src/Vec2.ts index 384cf80..ce2cece 100644 --- a/examples/nextjs/src/Vec2.ts +++ b/examples/nextjs/src/Vec2.ts @@ -8,6 +8,15 @@ export class Vec2 { return new Vec2(a.x + b.x, a.y + b.y); } + // Multiple type overloads for the same operator: + // boperators dispatches Vec2 * Vec2 and Vec2 * number to separate implementations. + static "*"(a: Vec2, b: Vec2): Vec2; + static "*"(a: Vec2, b: number): Vec2; + static "*"(a: Vec2, b: Vec2 | number): Vec2 { + if (b instanceof Vec2) return new Vec2(a.x * b.x, a.y * b.y); + return new Vec2(a.x * b, a.y * b); + } + toString(): string { return `Vec2(${this.x}, ${this.y})`; } diff --git a/examples/nextjs/src/app/page.tsx b/examples/nextjs/src/app/page.tsx index e67e8c8..7e114d8 100644 --- a/examples/nextjs/src/app/page.tsx +++ b/examples/nextjs/src/app/page.tsx @@ -3,11 +3,15 @@ import { Vec2 } from "../Vec2"; const a = new Vec2(1, 2); const b = new Vec2(3, 4); const c = a + b; +const d = a * b; // Vec2 * Vec2 — component-wise +const e = a * 2; // Vec2 * number — scalar export default function Home() { return (

Vec2(1, 2) + Vec2(3, 4) = {c.toString()}

+

Vec2(1, 2) * Vec2(3, 4) = {d.toString()}

+

Vec2(1, 2) * 2 = {e.toString()}

); } diff --git a/examples/vite/src/Vec2.ts b/examples/vite/src/Vec2.ts index 384cf80..ce2cece 100644 --- a/examples/vite/src/Vec2.ts +++ b/examples/vite/src/Vec2.ts @@ -8,6 +8,15 @@ export class Vec2 { return new Vec2(a.x + b.x, a.y + b.y); } + // Multiple type overloads for the same operator: + // boperators dispatches Vec2 * Vec2 and Vec2 * number to separate implementations. + static "*"(a: Vec2, b: Vec2): Vec2; + static "*"(a: Vec2, b: number): Vec2; + static "*"(a: Vec2, b: Vec2 | number): Vec2 { + if (b instanceof Vec2) return new Vec2(a.x * b.x, a.y * b.y); + return new Vec2(a.x * b, a.y * b); + } + toString(): string { return `Vec2(${this.x}, ${this.y})`; } diff --git a/examples/vite/src/main.ts b/examples/vite/src/main.ts index df91330..0d4cece 100644 --- a/examples/vite/src/main.ts +++ b/examples/vite/src/main.ts @@ -3,11 +3,18 @@ import { Vec2 } from "./Vec2"; const a = new Vec2(1, 2); const b = new Vec2(3, 4); const c = a + b; +const d = a * b; // Vec2 * Vec2 — component-wise +const e = a * 2; // Vec2 * number — scalar -// Displays "Vec2(4, 6)" in the browser console -console.log(c.toString()); +console.log(c.toString()); // Vec2(4, 6) +console.log(d.toString()); // Vec2(3, 8) +console.log(e.toString()); // Vec2(2, 4) -// Also render the result to the page so the e2e test can verify it +// Also render the results to the page const p = document.createElement("p"); p.textContent = c.toString(); document.body.appendChild(p); + +const p2 = document.createElement("p"); +p2.textContent = `${d.toString()}, ${e.toString()}`; +document.body.appendChild(p2); diff --git a/examples/webpack/src/Vec2.ts b/examples/webpack/src/Vec2.ts index 384cf80..ce2cece 100644 --- a/examples/webpack/src/Vec2.ts +++ b/examples/webpack/src/Vec2.ts @@ -8,6 +8,15 @@ export class Vec2 { return new Vec2(a.x + b.x, a.y + b.y); } + // Multiple type overloads for the same operator: + // boperators dispatches Vec2 * Vec2 and Vec2 * number to separate implementations. + static "*"(a: Vec2, b: Vec2): Vec2; + static "*"(a: Vec2, b: number): Vec2; + static "*"(a: Vec2, b: Vec2 | number): Vec2 { + if (b instanceof Vec2) return new Vec2(a.x * b.x, a.y * b.y); + return new Vec2(a.x * b, a.y * b); + } + toString(): string { return `Vec2(${this.x}, ${this.y})`; } diff --git a/examples/webpack/src/index.ts b/examples/webpack/src/index.ts index 63716d6..76f5352 100644 --- a/examples/webpack/src/index.ts +++ b/examples/webpack/src/index.ts @@ -3,5 +3,9 @@ import { Vec2 } from "./Vec2"; const a = new Vec2(1, 2); const b = new Vec2(3, 4); const c = a + b; +const d = a * b; // Vec2 * Vec2 — component-wise +const e = a * 2; // Vec2 * number — scalar console.log(c.toString()); // Vec2(4, 6) +console.log(d.toString()); // Vec2(3, 8) +console.log(e.toString()); // Vec2(2, 4) diff --git a/package/src/core/OverloadInjector.test.ts b/package/src/core/OverloadInjector.test.ts index 88c955a..1101a4e 100644 --- a/package/src/core/OverloadInjector.test.ts +++ b/package/src/core/OverloadInjector.test.ts @@ -144,4 +144,49 @@ const y = 1 + 2 + 3; const result = injector.overloadFile(usageFile); expect(result.edits.length).toBe(0); }); + + it("routes each TypeScript overload signature to the correct transformed call", () => { + // Vec2 with two overloads for "*": Vec2*Vec2 (component-wise) and Vec2*number (scalar). + // Each overload signature is a separate entry in the store and dispatches correctly. + const multiOverloadSource = ` +export class Vec2 { + x: number; + y: number; + constructor(x: number, y: number) { this.x = x; this.y = y; } + static "*"(a: Vec2, b: Vec2): Vec2; + static "*"(a: Vec2, b: number): Vec2; + static "*"(a: Vec2, b: Vec2 | number): Vec2 { + if (b instanceof Vec2) return new Vec2(a.x * b.x, a.y * b.y); + return new Vec2(a.x * b, a.y * b); + } +} +`.trim(); + + const project = new Project({ useInMemoryFileSystem: true }); + const vec2File = project.createSourceFile( + "/Vec2Multi.ts", + multiOverloadSource, + ); + const errorManager = new ErrorManager(silentConfig); + const store = new OverloadStore(project, errorManager, silentConfig.logger); + store.addOverloadsFromFile(vec2File); + const injector = new OverloadInjector(project, store, silentConfig.logger); + + const usageFile = project.createSourceFile( + "/usage_multi.ts", + ` +import { Vec2 } from "./Vec2Multi"; +const a = new Vec2(1, 2); +const b = new Vec2(3, 4); +const c = a * b; +const d = a * 2; +`.trim(), + ); + + const result = injector.overloadFile(usageFile); + expect(result.text).toContain('Vec2["*"](a, b)'); + expect(result.text).toContain('Vec2["*"](a, 2)'); + expect(result.text).not.toContain("a * b"); + expect(result.text).not.toContain("a * 2"); + }); }); From 5881c14b956e0ada1d3f8959544ee9fdd90cfc6f Mon Sep 17 00:00:00 2001 From: Dief Bell Date: Mon, 23 Feb 2026 22:15:45 +0000 Subject: [PATCH 17/18] Tests for helper functions of LS plugin --- plugins/ts-language-server/package.json | 1 + .../ts-language-server/src/helpers.test.ts | 351 ++++++++++++++++++ plugins/ts-language-server/src/helpers.ts | 350 +++++++++++++++++ plugins/ts-language-server/src/index.ts | 348 +---------------- plugins/ts-language-server/tsconfig.json | 1 + 5 files changed, 708 insertions(+), 343 deletions(-) create mode 100644 plugins/ts-language-server/src/helpers.test.ts create mode 100644 plugins/ts-language-server/src/helpers.ts diff --git a/plugins/ts-language-server/package.json b/plugins/ts-language-server/package.json index a46ebc1..94f540b 100644 --- a/plugins/ts-language-server/package.json +++ b/plugins/ts-language-server/package.json @@ -27,6 +27,7 @@ "scripts": { "build": "tsc", "watch": "tsc --watch", + "test": "bun test", "prepublish": "bun run build" }, "files": [ diff --git a/plugins/ts-language-server/src/helpers.test.ts b/plugins/ts-language-server/src/helpers.test.ts new file mode 100644 index 0000000..c037860 --- /dev/null +++ b/plugins/ts-language-server/src/helpers.test.ts @@ -0,0 +1,351 @@ +import { describe, expect, it, mock } from "bun:test"; +import { + type BopConfig, + ErrorManager, + OverloadStore, + Project, +} from "boperators"; +import { + findOverloadEdits, + getOverloadHoverInfo, + simplifyTypeName, +} from "./helpers"; + +// Minimal config that silences all logging +const silentConfig: BopConfig = { + errorOnWarning: false, + logLevel: "silent", + logger: { debug: mock(), info: mock(), warn: mock(), error: mock() }, +}; + +// Minimal mock of the TypeScript language service runtime. +// getOverloadHoverInfo only uses ts.ScriptElementKind.functionElement. +// biome-ignore lint/suspicious/noExplicitAny: intentional test mock +const mockTs = { ScriptElementKind: { functionElement: "function" } } as any; + +// A minimal in-memory Vec2 class with static "+" and "-" overloads. +const VEC2_SOURCE = ` +export class Vec2 { + x: number; + y: number; + constructor(x: number, y: number) { this.x = x; this.y = y; } + /** Adds two vectors component-wise. */ + static "+"(a: Vec2, b: Vec2): Vec2 { return new Vec2(a.x + b.x, a.y + b.y); } + static "-"(a: Vec2, b: Vec2): Vec2 { return new Vec2(a.x - b.x, a.y - b.y); } +} +`.trim(); + +function makeProject() { + const project = new Project({ useInMemoryFileSystem: true }); + const vec2File = project.createSourceFile("/Vec2.ts", VEC2_SOURCE); + const errorManager = new ErrorManager(silentConfig); + const store = new OverloadStore(project, errorManager, silentConfig.logger); + store.addOverloadsFromFile(vec2File); + return { project, store }; +} + +// ----- simplifyTypeName ----- + +describe("simplifyTypeName", () => { + it("strips an import() prefix from a type name", () => { + expect(simplifyTypeName('import("/path/to/Vec2").Vec2')).toBe("Vec2"); + }); + + it("strips import() prefixes from all type arguments in a generic", () => { + expect(simplifyTypeName('Map')).toBe( + "Map", + ); + }); + + it("leaves a plain type name unchanged", () => { + expect(simplifyTypeName("Vec2")).toBe("Vec2"); + expect(simplifyTypeName("number")).toBe("number"); + }); + + it("leaves an empty string unchanged", () => { + expect(simplifyTypeName("")).toBe(""); + }); +}); + +// ----- findOverloadEdits ----- + +describe("findOverloadEdits", () => { + it("returns one edit for a single overloaded binary expression", () => { + const { project, store } = makeProject(); + const usageFile = project.createSourceFile( + "/usage.ts", + [ + 'import { Vec2 } from "./Vec2";', + "const a = new Vec2(1, 2);", + "const b = new Vec2(3, 4);", + "const c = a + b;", + ].join("\n"), + ); + + const edits = findOverloadEdits(usageFile, store); + + expect(edits.length).toBe(1); + expect(edits[0].kind).toBe("binary"); + expect(edits[0].className).toBe("Vec2"); + expect(edits[0].operatorString).toBe("+"); + expect(edits[0].isStatic).toBe(true); + expect(typeof edits[0].lhsType).toBe("string"); + expect(typeof edits[0].rhsType).toBe("string"); + expect(typeof edits[0].returnType).toBe("string"); + }); + + it("returns multiple edits when multiple overloaded expressions appear", () => { + const { project, store } = makeProject(); + const usageFile = project.createSourceFile( + "/usage2.ts", + [ + 'import { Vec2 } from "./Vec2";', + "const a = new Vec2(1, 2);", + "const b = new Vec2(3, 4);", + "const c = a + b;", + "const d = a - b;", + ].join("\n"), + ); + + const edits = findOverloadEdits(usageFile, store); + + expect(edits.length).toBe(2); + expect(edits.map((e) => e.operatorString).sort()).toEqual( + ["+", "-"].sort(), + ); + }); + + it("returns no edits when no overloads match", () => { + const { project, store } = makeProject(); + const usageFile = project.createSourceFile( + "/usage3.ts", + "const x = 1 + 2;", + ); + + const edits = findOverloadEdits(usageFile, store); + + expect(edits.length).toBe(0); + }); + + it("records operator token positions accurately", () => { + const { project, store } = makeProject(); + const source = [ + 'import { Vec2 } from "./Vec2";', + "const a = new Vec2(1, 2);", + "const b = new Vec2(3, 4);", + "const c = a + b;", + ].join("\n"); + const usageFile = project.createSourceFile("/usage4.ts", source); + + const edits = findOverloadEdits(usageFile, store); + + expect(edits.length).toBe(1); + const { operatorStart, operatorEnd } = edits[0]; + expect(source.slice(operatorStart, operatorEnd)).toBe("+"); + }); + + it("records classFilePath matching the source file path", () => { + const { project, store } = makeProject(); + const usageFile = project.createSourceFile( + "/usage5.ts", + [ + 'import { Vec2 } from "./Vec2";', + "const a = new Vec2(1, 2);", + "const b = new Vec2(3, 4);", + "const c = a + b;", + ].join("\n"), + ); + + const edits = findOverloadEdits(usageFile, store); + + expect(edits.length).toBe(1); + expect(edits[0].classFilePath).toBe("/Vec2.ts"); + }); +}); + +// ----- getOverloadHoverInfo ----- + +describe("getOverloadHoverInfo", () => { + it("returns display parts for a binary static overload", () => { + const { project } = makeProject(); + const edit = { + operatorStart: 0, + operatorEnd: 1, + hoverStart: 0, + hoverEnd: 1, + exprStart: 0, + exprEnd: 10, + className: "Vec2", + classFilePath: "/Vec2.ts", + operatorString: "+", + returnType: "Vec2", + lhsType: "Vec2", + rhsType: "Vec2", + isStatic: true, + kind: "binary" as const, + }; + + const result = getOverloadHoverInfo(mockTs, project, edit); + + expect(result).toBeDefined(); + const joined = result!.displayParts.map((p) => p.text).join(""); + expect(joined).toBe("Vec2 + Vec2 = Vec2"); + }); + + it("strips import() prefixes from type names in display parts", () => { + const { project } = makeProject(); + const edit = { + operatorStart: 0, + operatorEnd: 1, + hoverStart: 0, + hoverEnd: 1, + exprStart: 0, + exprEnd: 10, + className: "Vec2", + classFilePath: "/Vec2.ts", + operatorString: "+", + returnType: 'import("/Vec2").Vec2', + lhsType: 'import("/Vec2").Vec2', + rhsType: 'import("/Vec2").Vec2', + isStatic: true, + kind: "binary" as const, + }; + + const result = getOverloadHoverInfo(mockTs, project, edit); + + expect(result).toBeDefined(); + const joined = result!.displayParts.map((p) => p.text).join(""); + expect(joined).not.toContain("import("); + expect(joined).toContain("Vec2"); + }); + + it("extracts JSDoc from the first overload signature", () => { + const { project } = makeProject(); + const edit = { + operatorStart: 0, + operatorEnd: 1, + hoverStart: 0, + hoverEnd: 1, + exprStart: 0, + exprEnd: 10, + className: "Vec2", + classFilePath: "/Vec2.ts", + operatorString: "+", + returnType: "Vec2", + lhsType: "Vec2", + rhsType: "Vec2", + isStatic: true, + kind: "binary" as const, + }; + + const result = getOverloadHoverInfo(mockTs, project, edit); + + expect(result).toBeDefined(); + expect(result!.documentation).toBeDefined(); + expect(result!.documentation![0].text).toContain("Adds two vectors"); + }); + + it("returns a result without documentation when the class file is not in the project", () => { + const { project } = makeProject(); + const edit = { + operatorStart: 0, + operatorEnd: 1, + hoverStart: 0, + hoverEnd: 1, + exprStart: 0, + exprEnd: 10, + className: "Vec2", + classFilePath: "/DoesNotExist.ts", + operatorString: "+", + returnType: "Vec2", + lhsType: "Vec2", + rhsType: "Vec2", + isStatic: true, + kind: "binary" as const, + }; + + const result = getOverloadHoverInfo(mockTs, project, edit); + + // No class source file → no JSDoc, but display parts still built from edit fields + expect(result).toBeDefined(); + expect(result!.documentation).toBeUndefined(); + const joined = result!.displayParts.map((p) => p.text).join(""); + expect(joined).toContain("+"); + }); + + it("builds prefix unary display parts as 'op operand = return'", () => { + const { project } = makeProject(); + const edit = { + operatorStart: 0, + operatorEnd: 1, + hoverStart: 0, + hoverEnd: 1, + exprStart: 0, + exprEnd: 6, + className: "Vec2", + classFilePath: "/Vec2.ts", + operatorString: "-", + returnType: "Vec2", + operandType: "Vec2", + isStatic: true, + kind: "prefixUnary" as const, + }; + + const result = getOverloadHoverInfo(mockTs, project, edit); + + expect(result).toBeDefined(); + const parts = result!.displayParts; + expect(parts[0].text).toBe("-"); + expect(parts[1].text).toBe("Vec2"); + }); + + it("builds postfix unary display parts as 'className op'", () => { + const { project } = makeProject(); + const edit = { + operatorStart: 5, + operatorEnd: 7, + hoverStart: 5, + hoverEnd: 7, + exprStart: 0, + exprEnd: 7, + className: "Vec2", + classFilePath: "/Vec2.ts", + operatorString: "++", + returnType: "Vec2", + operandType: "Vec2", + isStatic: true, + kind: "postfixUnary" as const, + }; + + const result = getOverloadHoverInfo(mockTs, project, edit); + + expect(result).toBeDefined(); + const parts = result!.displayParts; + expect(parts[0].text).toBe("Vec2"); + expect(parts[1].text).toBe("++"); + }); + + it("sets kindModifiers to 'static' for static overloads", () => { + const { project } = makeProject(); + const edit = { + operatorStart: 0, + operatorEnd: 1, + hoverStart: 0, + hoverEnd: 1, + exprStart: 0, + exprEnd: 10, + className: "Vec2", + classFilePath: "/Vec2.ts", + operatorString: "+", + returnType: "Vec2", + lhsType: "Vec2", + rhsType: "Vec2", + isStatic: true, + kind: "binary" as const, + }; + + const result = getOverloadHoverInfo(mockTs, project, edit); + + expect(result!.kindModifiers).toBe("static"); + }); +}); diff --git a/plugins/ts-language-server/src/helpers.ts b/plugins/ts-language-server/src/helpers.ts new file mode 100644 index 0000000..3160b19 --- /dev/null +++ b/plugins/ts-language-server/src/helpers.ts @@ -0,0 +1,350 @@ +import { + getOperatorStringFromMethod, + isOperatorSyntaxKind, + isPostfixUnaryOperatorSyntaxKind, + isPrefixUnaryOperatorSyntaxKind, + Node, + type OverloadStore, + resolveExpressionType, + SyntaxKind, + type Project as TsMorphProject, + type SourceFile as TsMorphSourceFile, +} from "boperators"; +import type tsRuntime from "typescript/lib/tsserverlibrary"; + +// ----- Types ----- + +export type OverloadEditInfo = { + /** Start of the operator token in the original source */ + operatorStart: number; + /** End of the operator token in the original source */ + operatorEnd: number; + /** Start of the hover hit-test area (includes surrounding whitespace) */ + hoverStart: number; + /** End of the hover hit-test area (includes surrounding whitespace) */ + hoverEnd: number; + /** Start of the full expression in the original source */ + exprStart: number; + /** End of the full expression in the original source */ + exprEnd: number; + className: string; + classFilePath: string; + operatorString: string; + returnType: string; + /** LHS type (binary overloads only) */ + lhsType?: string; + /** RHS type (binary overloads only) */ + rhsType?: string; + /** Operand type (unary overloads only) */ + operandType?: string; + isStatic: boolean; + kind: "binary" | "prefixUnary" | "postfixUnary"; +}; + +// ----- Internal helpers ----- + +/** + * Recursively resolve the effective type of an expression, accounting for + * operator overloads. For sub-expressions that match a registered overload, + * uses the overload's declared return type instead of what TypeScript infers + * (since TS doesn't know about operator overloading). + */ +function resolveOverloadedType( + node: Node, + overloadStore: OverloadStore, +): string { + if (Node.isParenthesizedExpression(node)) { + return resolveOverloadedType(node.getExpression(), overloadStore); + } + + if (Node.isBinaryExpression(node)) { + const operatorKind = node.getOperatorToken().getKind(); + if (isOperatorSyntaxKind(operatorKind)) { + const leftType = resolveOverloadedType(node.getLeft(), overloadStore); + const rightType = resolveOverloadedType(node.getRight(), overloadStore); + const overload = overloadStore.findOverload( + operatorKind, + leftType, + rightType, + ); + if (overload) return overload.returnType; + } + } + + if (Node.isPrefixUnaryExpression(node)) { + const operatorKind = node.getOperatorToken(); + if (isPrefixUnaryOperatorSyntaxKind(operatorKind)) { + const operandType = resolveOverloadedType( + node.getOperand(), + overloadStore, + ); + const overload = overloadStore.findPrefixUnaryOverload( + operatorKind, + operandType, + ); + if (overload) return overload.returnType; + } + } + + if (Node.isPostfixUnaryExpression(node)) { + const operatorKind = node.getOperatorToken(); + if (isPostfixUnaryOperatorSyntaxKind(operatorKind)) { + const operandType = resolveOverloadedType( + node.getOperand(), + overloadStore, + ); + const overload = overloadStore.findPostfixUnaryOverload( + operatorKind, + operandType, + ); + if (overload) return overload.returnType; + } + } + + return resolveExpressionType(node); +} + +// ----- Exported helpers ----- + +/** + * Before transformation, find all expressions (binary, prefix unary, postfix unary) + * that match registered overloads and record their operator token positions. + * This is used to provide hover info for overloaded operators. + */ +export function findOverloadEdits( + sourceFile: TsMorphSourceFile, + overloadStore: OverloadStore, +): OverloadEditInfo[] { + const edits: OverloadEditInfo[] = []; + const binaryExpressions = sourceFile.getDescendantsOfKind( + SyntaxKind.BinaryExpression, + ); + + for (const expression of binaryExpressions) { + const operatorToken = expression.getOperatorToken(); + const operatorKind = operatorToken.getKind(); + + if (!isOperatorSyntaxKind(operatorKind)) continue; + + const leftType = resolveOverloadedType(expression.getLeft(), overloadStore); + const rightType = resolveOverloadedType( + expression.getRight(), + overloadStore, + ); + + const overloadDesc = overloadStore.findOverload( + operatorKind, + leftType, + rightType, + ); + if (!overloadDesc) continue; + + edits.push({ + operatorStart: operatorToken.getStart(), + operatorEnd: operatorToken.getEnd(), + hoverStart: expression.getLeft().getEnd(), + hoverEnd: expression.getRight().getStart(), + exprStart: expression.getStart(), + exprEnd: expression.getEnd(), + className: overloadDesc.className, + classFilePath: overloadDesc.classFilePath, + operatorString: overloadDesc.operatorString, + returnType: overloadDesc.returnType, + lhsType: leftType, + rhsType: rightType, + isStatic: overloadDesc.isStatic, + kind: "binary", + }); + } + + // Scan prefix unary expressions + const prefixExpressions = sourceFile.getDescendantsOfKind( + SyntaxKind.PrefixUnaryExpression, + ); + for (const expression of prefixExpressions) { + const operatorKind = expression.getOperatorToken(); + if (!isPrefixUnaryOperatorSyntaxKind(operatorKind)) continue; + + const operandType = resolveOverloadedType( + expression.getOperand(), + overloadStore, + ); + const overloadDesc = overloadStore.findPrefixUnaryOverload( + operatorKind, + operandType, + ); + if (!overloadDesc) continue; + + const exprStart = expression.getStart(); + const operand = expression.getOperand(); + + edits.push({ + operatorStart: exprStart, + operatorEnd: operand.getStart(), + hoverStart: exprStart, + hoverEnd: operand.getStart(), + exprStart, + exprEnd: expression.getEnd(), + className: overloadDesc.className, + classFilePath: overloadDesc.classFilePath, + operatorString: overloadDesc.operatorString, + returnType: overloadDesc.returnType, + operandType: operandType, + isStatic: overloadDesc.isStatic, + kind: "prefixUnary", + }); + } + + // Scan postfix unary expressions + const postfixExpressions = sourceFile.getDescendantsOfKind( + SyntaxKind.PostfixUnaryExpression, + ); + for (const expression of postfixExpressions) { + const operatorKind = expression.getOperatorToken(); + if (!isPostfixUnaryOperatorSyntaxKind(operatorKind)) continue; + + const operandType = resolveOverloadedType( + expression.getOperand(), + overloadStore, + ); + const overloadDesc = overloadStore.findPostfixUnaryOverload( + operatorKind, + operandType, + ); + if (!overloadDesc) continue; + + const operand = expression.getOperand(); + const operatorStart = operand.getEnd(); + + edits.push({ + operatorStart, + operatorEnd: expression.getEnd(), + hoverStart: operatorStart, + hoverEnd: expression.getEnd(), + exprStart: expression.getStart(), + exprEnd: expression.getEnd(), + className: overloadDesc.className, + classFilePath: overloadDesc.classFilePath, + operatorString: overloadDesc.operatorString, + returnType: overloadDesc.returnType, + operandType: operandType, + isStatic: overloadDesc.isStatic, + kind: "postfixUnary", + }); + } + + return edits; +} + +/** + * Strip fully-qualified import paths from a type name so that + * `import("/path/to/Vec2").Vec2` is displayed as just `Vec2`. + */ +export function simplifyTypeName(typeName: string): string { + return typeName.replace(/\bimport\("[^"]*"\)\./g, ""); +} + +/** + * Build a QuickInfo response for hovering over an operator token + * that corresponds to an overloaded operator. Extracts the function + * signature and JSDoc from the overload definition. + */ +export function getOverloadHoverInfo( + ts: typeof tsRuntime, + project: TsMorphProject, + edit: OverloadEditInfo, +): tsRuntime.QuickInfo | undefined { + try { + // Extract JSDoc from the method declaration (or its first overload signature). + let docText: string | undefined; + const classSourceFile = project.getSourceFile(edit.classFilePath); + if (classSourceFile) { + const classDecl = classSourceFile.getClass(edit.className); + if (classDecl) { + const method = classDecl + .getMethods() + .find((m) => getOperatorStringFromMethod(m) === edit.operatorString); + if (method) { + const overloads = method.getOverloads(); + const source = overloads.length > 0 ? overloads[0] : method; + const jsDocs = source.getJsDocs(); + if (jsDocs.length > 0) { + const raw = jsDocs[0].getText(); + docText = raw + .replace(/^\/\*\*\s*/, "") + .replace(/\s*\*\/$/, "") + .replace(/^\s*\* ?/gm, "") + .trim(); + } + } + } + } + + // Build display signature parts based on overload kind. + // Types are sourced from the resolved expression types stored at scan time. + const returnTypeName = simplifyTypeName(edit.returnType); + const displayParts: tsRuntime.SymbolDisplayPart[] = []; + + if (edit.kind === "prefixUnary") { + // Prefix unary: "-Vec2 = Vec2" + displayParts.push({ text: edit.operatorString, kind: "operator" }); + displayParts.push({ + text: simplifyTypeName(edit.operandType ?? edit.className), + kind: "className", + }); + if (returnTypeName !== "void") { + displayParts.push({ text: " = ", kind: "punctuation" }); + displayParts.push({ text: returnTypeName, kind: "className" }); + } + } else if (edit.kind === "postfixUnary") { + // Postfix unary: "Vec2++" + displayParts.push({ text: edit.className, kind: "className" }); + displayParts.push({ text: edit.operatorString, kind: "operator" }); + } else if (edit.isStatic) { + // Binary static: "LhsType + RhsType = ReturnType" + displayParts.push({ + text: simplifyTypeName(edit.lhsType ?? edit.className), + kind: "className", + }); + displayParts.push({ text: " ", kind: "space" }); + displayParts.push({ text: edit.operatorString, kind: "operator" }); + displayParts.push({ text: " ", kind: "space" }); + displayParts.push({ + text: simplifyTypeName(edit.rhsType ?? edit.className), + kind: "className", + }); + if (returnTypeName !== "void") { + displayParts.push({ text: " = ", kind: "punctuation" }); + displayParts.push({ text: returnTypeName, kind: "className" }); + } + } else { + // Binary instance: "ClassName += RhsType" + displayParts.push({ text: edit.className, kind: "className" }); + displayParts.push({ text: " ", kind: "space" }); + displayParts.push({ text: edit.operatorString, kind: "operator" }); + displayParts.push({ text: " ", kind: "space" }); + displayParts.push({ + text: simplifyTypeName(edit.rhsType ?? "unknown"), + kind: "className", + }); + if (returnTypeName !== "void") { + displayParts.push({ text: " = ", kind: "punctuation" }); + displayParts.push({ text: returnTypeName, kind: "className" }); + } + } + + return { + kind: ts.ScriptElementKind.functionElement, + kindModifiers: edit.isStatic ? "static" : "", + textSpan: { + start: edit.operatorStart, + length: edit.operatorEnd - edit.operatorStart, + }, + displayParts, + documentation: docText ? [{ text: docText, kind: "text" }] : undefined, + tags: [], + }; + } catch { + return undefined; + } +} diff --git a/plugins/ts-language-server/src/index.ts b/plugins/ts-language-server/src/index.ts index 88417d9..d02b8a2 100644 --- a/plugins/ts-language-server/src/index.ts +++ b/plugins/ts-language-server/src/index.ts @@ -1,20 +1,17 @@ import { type BopLogger, ErrorManager, - getOperatorStringFromMethod, - isOperatorSyntaxKind, - isPostfixUnaryOperatorSyntaxKind, - isPrefixUnaryOperatorSyntaxKind, loadConfig, - Node, OverloadInjector, OverloadStore, - resolveExpressionType, - SyntaxKind, Project as TsMorphProject, - type SourceFile as TsMorphSourceFile, } from "boperators"; import type tsRuntime from "typescript/lib/tsserverlibrary"; +import { + findOverloadEdits, + getOverloadHoverInfo, + type OverloadEditInfo, +} from "./helpers"; import { SourceMap } from "./SourceMap"; // ----- Types ----- @@ -26,33 +23,6 @@ type CacheEntry = { overloadEdits: OverloadEditInfo[]; }; -type OverloadEditInfo = { - /** Start of the operator token in the original source */ - operatorStart: number; - /** End of the operator token in the original source */ - operatorEnd: number; - /** Start of the hover hit-test area (includes surrounding whitespace) */ - hoverStart: number; - /** End of the hover hit-test area (includes surrounding whitespace) */ - hoverEnd: number; - /** Start of the full expression in the original source */ - exprStart: number; - /** End of the full expression in the original source */ - exprEnd: number; - className: string; - classFilePath: string; - operatorString: string; - returnType: string; - /** LHS type (binary overloads only) */ - lhsType?: string; - /** RHS type (binary overloads only) */ - rhsType?: string; - /** Operand type (unary overloads only) */ - operandType?: string; - isStatic: boolean; - kind: "binary" | "prefixUnary" | "postfixUnary"; -}; - // ----- Plugin entry ----- export = function init(modules: { @@ -161,314 +131,6 @@ export = function init(modules: { return { create }; }; -// ----- Overload edit scanner ----- - -/** - * Before transformation, find all expressions (binary, prefix unary, postfix unary) - * that match registered overloads and record their operator token positions. - * This is used to provide hover info for overloaded operators. - */ -/** - * Recursively resolve the effective type of an expression, accounting for - * operator overloads. For sub-expressions that match a registered overload, - * uses the overload's declared return type instead of what TypeScript infers - * (since TS doesn't know about operator overloading). - */ -function resolveOverloadedType( - node: Node, - overloadStore: OverloadStore, -): string { - if (Node.isParenthesizedExpression(node)) { - return resolveOverloadedType(node.getExpression(), overloadStore); - } - - if (Node.isBinaryExpression(node)) { - const operatorKind = node.getOperatorToken().getKind(); - if (isOperatorSyntaxKind(operatorKind)) { - const leftType = resolveOverloadedType(node.getLeft(), overloadStore); - const rightType = resolveOverloadedType(node.getRight(), overloadStore); - const overload = overloadStore.findOverload( - operatorKind, - leftType, - rightType, - ); - if (overload) return overload.returnType; - } - } - - if (Node.isPrefixUnaryExpression(node)) { - const operatorKind = node.getOperatorToken(); - if (isPrefixUnaryOperatorSyntaxKind(operatorKind)) { - const operandType = resolveOverloadedType( - node.getOperand(), - overloadStore, - ); - const overload = overloadStore.findPrefixUnaryOverload( - operatorKind, - operandType, - ); - if (overload) return overload.returnType; - } - } - - if (Node.isPostfixUnaryExpression(node)) { - const operatorKind = node.getOperatorToken(); - if (isPostfixUnaryOperatorSyntaxKind(operatorKind)) { - const operandType = resolveOverloadedType( - node.getOperand(), - overloadStore, - ); - const overload = overloadStore.findPostfixUnaryOverload( - operatorKind, - operandType, - ); - if (overload) return overload.returnType; - } - } - - return resolveExpressionType(node); -} - -function findOverloadEdits( - sourceFile: TsMorphSourceFile, - overloadStore: OverloadStore, -): OverloadEditInfo[] { - const edits: OverloadEditInfo[] = []; - const binaryExpressions = sourceFile.getDescendantsOfKind( - SyntaxKind.BinaryExpression, - ); - - for (const expression of binaryExpressions) { - const operatorToken = expression.getOperatorToken(); - const operatorKind = operatorToken.getKind(); - - if (!isOperatorSyntaxKind(operatorKind)) continue; - - const leftType = resolveOverloadedType(expression.getLeft(), overloadStore); - const rightType = resolveOverloadedType( - expression.getRight(), - overloadStore, - ); - - const overloadDesc = overloadStore.findOverload( - operatorKind, - leftType, - rightType, - ); - if (!overloadDesc) continue; - - edits.push({ - operatorStart: operatorToken.getStart(), - operatorEnd: operatorToken.getEnd(), - hoverStart: expression.getLeft().getEnd(), - hoverEnd: expression.getRight().getStart(), - exprStart: expression.getStart(), - exprEnd: expression.getEnd(), - className: overloadDesc.className, - classFilePath: overloadDesc.classFilePath, - operatorString: overloadDesc.operatorString, - returnType: overloadDesc.returnType, - lhsType: leftType, - rhsType: rightType, - isStatic: overloadDesc.isStatic, - kind: "binary", - }); - } - - // Scan prefix unary expressions - const prefixExpressions = sourceFile.getDescendantsOfKind( - SyntaxKind.PrefixUnaryExpression, - ); - for (const expression of prefixExpressions) { - const operatorKind = expression.getOperatorToken(); - if (!isPrefixUnaryOperatorSyntaxKind(operatorKind)) continue; - - const operandType = resolveOverloadedType( - expression.getOperand(), - overloadStore, - ); - const overloadDesc = overloadStore.findPrefixUnaryOverload( - operatorKind, - operandType, - ); - if (!overloadDesc) continue; - - const exprStart = expression.getStart(); - const operand = expression.getOperand(); - - edits.push({ - operatorStart: exprStart, - operatorEnd: operand.getStart(), - hoverStart: exprStart, - hoverEnd: operand.getStart(), - exprStart, - exprEnd: expression.getEnd(), - className: overloadDesc.className, - classFilePath: overloadDesc.classFilePath, - operatorString: overloadDesc.operatorString, - returnType: overloadDesc.returnType, - operandType: operandType, - isStatic: overloadDesc.isStatic, - kind: "prefixUnary", - }); - } - - // Scan postfix unary expressions - const postfixExpressions = sourceFile.getDescendantsOfKind( - SyntaxKind.PostfixUnaryExpression, - ); - for (const expression of postfixExpressions) { - const operatorKind = expression.getOperatorToken(); - if (!isPostfixUnaryOperatorSyntaxKind(operatorKind)) continue; - - const operandType = resolveOverloadedType( - expression.getOperand(), - overloadStore, - ); - const overloadDesc = overloadStore.findPostfixUnaryOverload( - operatorKind, - operandType, - ); - if (!overloadDesc) continue; - - const operand = expression.getOperand(); - const operatorStart = operand.getEnd(); - - edits.push({ - operatorStart, - operatorEnd: expression.getEnd(), - hoverStart: operatorStart, - hoverEnd: expression.getEnd(), - exprStart: expression.getStart(), - exprEnd: expression.getEnd(), - className: overloadDesc.className, - classFilePath: overloadDesc.classFilePath, - operatorString: overloadDesc.operatorString, - returnType: overloadDesc.returnType, - operandType: operandType, - isStatic: overloadDesc.isStatic, - kind: "postfixUnary", - }); - } - - return edits; -} - -// ----- Overload hover info ----- - -/** - * Strip fully-qualified import paths from a type name so that - * `import("/path/to/Vec2").Vec2` is displayed as just `Vec2`. - */ -function simplifyTypeName(typeName: string): string { - return typeName.replace(/\bimport\("[^"]*"\)\./g, ""); -} - -/** - * Build a QuickInfo response for hovering over an operator token - * that corresponds to an overloaded operator. Extracts the function - * signature and JSDoc from the overload definition. - */ -function getOverloadHoverInfo( - ts: typeof tsRuntime, - project: TsMorphProject, - edit: OverloadEditInfo, -): tsRuntime.QuickInfo | undefined { - try { - // Extract JSDoc from the method declaration (or its first overload signature). - let docText: string | undefined; - const classSourceFile = project.getSourceFile(edit.classFilePath); - if (classSourceFile) { - const classDecl = classSourceFile.getClass(edit.className); - if (classDecl) { - const method = classDecl - .getMethods() - .find((m) => getOperatorStringFromMethod(m) === edit.operatorString); - if (method) { - const overloads = method.getOverloads(); - const source = overloads.length > 0 ? overloads[0] : method; - const jsDocs = source.getJsDocs(); - if (jsDocs.length > 0) { - const raw = jsDocs[0].getText(); - docText = raw - .replace(/^\/\*\*\s*/, "") - .replace(/\s*\*\/$/, "") - .replace(/^\s*\* ?/gm, "") - .trim(); - } - } - } - } - - // Build display signature parts based on overload kind. - // Types are sourced from the resolved expression types stored at scan time. - const returnTypeName = simplifyTypeName(edit.returnType); - const displayParts: tsRuntime.SymbolDisplayPart[] = []; - - if (edit.kind === "prefixUnary") { - // Prefix unary: "-Vector3 = Vector3" - displayParts.push({ text: edit.operatorString, kind: "operator" }); - displayParts.push({ - text: simplifyTypeName(edit.operandType ?? edit.className), - kind: "className", - }); - if (returnTypeName !== "void") { - displayParts.push({ text: " = ", kind: "punctuation" }); - displayParts.push({ text: returnTypeName, kind: "className" }); - } - } else if (edit.kind === "postfixUnary") { - // Postfix unary: "Vector3++" - displayParts.push({ text: edit.className, kind: "className" }); - displayParts.push({ text: edit.operatorString, kind: "operator" }); - } else if (edit.isStatic) { - // Binary static: "LhsType + RhsType = ReturnType" - displayParts.push({ - text: simplifyTypeName(edit.lhsType ?? edit.className), - kind: "className", - }); - displayParts.push({ text: " ", kind: "space" }); - displayParts.push({ text: edit.operatorString, kind: "operator" }); - displayParts.push({ text: " ", kind: "space" }); - displayParts.push({ - text: simplifyTypeName(edit.rhsType ?? edit.className), - kind: "className", - }); - if (returnTypeName !== "void") { - displayParts.push({ text: " = ", kind: "punctuation" }); - displayParts.push({ text: returnTypeName, kind: "className" }); - } - } else { - // Binary instance: "ClassName += RhsType" - displayParts.push({ text: edit.className, kind: "className" }); - displayParts.push({ text: " ", kind: "space" }); - displayParts.push({ text: edit.operatorString, kind: "operator" }); - displayParts.push({ text: " ", kind: "space" }); - displayParts.push({ - text: simplifyTypeName(edit.rhsType ?? "unknown"), - kind: "className", - }); - if (returnTypeName !== "void") { - displayParts.push({ text: " = ", kind: "punctuation" }); - displayParts.push({ text: returnTypeName, kind: "className" }); - } - } - - return { - kind: ts.ScriptElementKind.functionElement, - kindModifiers: edit.isStatic ? "static" : "", - textSpan: { - start: edit.operatorStart, - length: edit.operatorEnd - edit.operatorStart, - }, - displayParts, - documentation: docText ? [{ text: docText, kind: "text" }] : undefined, - tags: [], - }; - } catch { - return undefined; - } -} - // ----- LanguageService proxy ----- function getSourceMapForFile( diff --git a/plugins/ts-language-server/tsconfig.json b/plugins/ts-language-server/tsconfig.json index 05c0bae..0ccba40 100644 --- a/plugins/ts-language-server/tsconfig.json +++ b/plugins/ts-language-server/tsconfig.json @@ -1,5 +1,6 @@ { "include": ["src"], + "exclude": ["**/*.test.ts"], "compilerOptions": { "target": "es2016", "module": "commonjs", From c4dc2646917f4fa36855843c723f90c81db1526f Mon Sep 17 00:00:00 2001 From: Dief Bell Date: Tue, 24 Feb 2026 13:57:16 +0000 Subject: [PATCH 18/18] Integration tests for LS plugin --- .../ts-language-server/src/helpers.test.ts | 18 +- plugins/ts-language-server/src/index.test.ts | 240 ++++++++++++++++++ plugins/ts-language-server/src/index.ts | 93 +++---- 3 files changed, 287 insertions(+), 64 deletions(-) create mode 100644 plugins/ts-language-server/src/index.test.ts diff --git a/plugins/ts-language-server/src/helpers.test.ts b/plugins/ts-language-server/src/helpers.test.ts index c037860..f36dfec 100644 --- a/plugins/ts-language-server/src/helpers.test.ts +++ b/plugins/ts-language-server/src/helpers.test.ts @@ -188,7 +188,7 @@ describe("getOverloadHoverInfo", () => { const result = getOverloadHoverInfo(mockTs, project, edit); expect(result).toBeDefined(); - const joined = result!.displayParts.map((p) => p.text).join(""); + const joined = result?.displayParts.map((p) => p.text).join(""); expect(joined).toBe("Vec2 + Vec2 = Vec2"); }); @@ -214,7 +214,7 @@ describe("getOverloadHoverInfo", () => { const result = getOverloadHoverInfo(mockTs, project, edit); expect(result).toBeDefined(); - const joined = result!.displayParts.map((p) => p.text).join(""); + const joined = result?.displayParts.map((p) => p.text).join(""); expect(joined).not.toContain("import("); expect(joined).toContain("Vec2"); }); @@ -241,8 +241,8 @@ describe("getOverloadHoverInfo", () => { const result = getOverloadHoverInfo(mockTs, project, edit); expect(result).toBeDefined(); - expect(result!.documentation).toBeDefined(); - expect(result!.documentation![0].text).toContain("Adds two vectors"); + expect(result?.documentation).toBeDefined(); + expect(result?.documentation?.[0].text).toContain("Adds two vectors"); }); it("returns a result without documentation when the class file is not in the project", () => { @@ -268,8 +268,8 @@ describe("getOverloadHoverInfo", () => { // No class source file → no JSDoc, but display parts still built from edit fields expect(result).toBeDefined(); - expect(result!.documentation).toBeUndefined(); - const joined = result!.displayParts.map((p) => p.text).join(""); + expect(result?.documentation).toBeUndefined(); + const joined = result?.displayParts.map((p) => p.text).join(""); expect(joined).toContain("+"); }); @@ -294,7 +294,7 @@ describe("getOverloadHoverInfo", () => { const result = getOverloadHoverInfo(mockTs, project, edit); expect(result).toBeDefined(); - const parts = result!.displayParts; + const parts = result?.displayParts; expect(parts[0].text).toBe("-"); expect(parts[1].text).toBe("Vec2"); }); @@ -320,7 +320,7 @@ describe("getOverloadHoverInfo", () => { const result = getOverloadHoverInfo(mockTs, project, edit); expect(result).toBeDefined(); - const parts = result!.displayParts; + const parts = result?.displayParts; expect(parts[0].text).toBe("Vec2"); expect(parts[1].text).toBe("++"); }); @@ -346,6 +346,6 @@ describe("getOverloadHoverInfo", () => { const result = getOverloadHoverInfo(mockTs, project, edit); - expect(result!.kindModifiers).toBe("static"); + expect(result?.kindModifiers).toBe("static"); }); }); diff --git a/plugins/ts-language-server/src/index.test.ts b/plugins/ts-language-server/src/index.test.ts new file mode 100644 index 0000000..6823644 --- /dev/null +++ b/plugins/ts-language-server/src/index.test.ts @@ -0,0 +1,240 @@ +/** + * Integration tests for the TypeScript Language Server plugin. + * + * These tests create a real `ts.LanguageService` with in-memory virtual files, + * load the plugin's `create()` function, and assert on observable language + * service behaviour: hover info, diagnostics, and source-map position remapping. + * + * Why no source-map access is needed: + * The boperators transformer expands `a + b` (5 chars) into + * `Vec2["+"](a, b)` (15 chars) — a shift of +10. Any position in the + * transformed file that falls *after* the replacement lands outside the + * original source bounds. We use that fact to distinguish "remapped" from + * "not remapped" in the diagnostic position test without ever reading the + * internal source-map cache. + */ + +import { describe, expect, it } from "bun:test"; +import { existsSync, readFileSync } from "node:fs"; +import ts from "typescript"; +import pluginInit from "../dist/index.js"; + +// --------------------------------------------------------------------------- +// Virtual source files +// --------------------------------------------------------------------------- + +const VEC2_SOURCE = ` +export class Vec2 { + x: number; + y: number; + constructor(x: number, y: number) { this.x = x; this.y = y; } + toString() { return \`Vec2(\${this.x}, \${this.y})\`; } + /** Adds two vectors component-wise. */ + static "+"(a: Vec2, b: Vec2): Vec2 { return new Vec2(a.x + b.x, a.y + b.y); } +}`.trim(); + +// Used for hover and TS2365 absence tests. +const USAGE_SOURCE = [ + 'import { Vec2 } from "./Vec2";', + "const a = new Vec2(1, 2);", + "const b = new Vec2(3, 4);", + "const c = a + b;", +].join("\n"); + +// Used for the diagnostic position-remapping test. +// After transformation `a + b` (5 chars) → `Vec2["+"](a, b)` (15 chars), +// everything after the replacement shifts by +10. The undeclared identifier +// `zzz` on the last line sits at a position that, in the transformed source, +// exceeds the original source length — so an un-remapped diagnostic would +// trivially fail the bounds check. +const USAGE_WITH_UNDECLARED = [ + 'import { Vec2 } from "./Vec2";', + "const a = new Vec2(1, 2);", + "const b = new Vec2(3, 4);", + "const c = a + b;", + "zzz;", +].join("\n"); + +// Pre-computed position of `zzz` in USAGE_WITH_UNDECLARED. +const ZZZ_POS_IN_ORIGINAL = USAGE_WITH_UNDECLARED.lastIndexOf("zzz"); + +// --------------------------------------------------------------------------- +// LanguageServiceHost factory +// --------------------------------------------------------------------------- + +function makeHost(files: Map): ts.LanguageServiceHost { + const compilerOptions: ts.CompilerOptions = { + target: ts.ScriptTarget.ES2016, + module: ts.ModuleKind.CommonJS, + strict: false, + skipLibCheck: true, + }; + + return { + getCompilationSettings: () => compilerOptions, + getScriptFileNames: () => [...files.keys()], + getScriptVersion: () => "1", + getScriptSnapshot: (fileName) => { + const content = files.get(fileName); + if (content !== undefined) return ts.ScriptSnapshot.fromString(content); + // Fall back to the real filesystem for TypeScript lib files. + try { + return ts.ScriptSnapshot.fromString(readFileSync(fileName, "utf-8")); + } catch { + return undefined; + } + }, + getCurrentDirectory: () => "/", + getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options), + fileExists: (fileName) => files.has(fileName) || existsSync(fileName), + readFile: (fileName) => { + const content = files.get(fileName); + if (content !== undefined) return content; + try { + return readFileSync(fileName, "utf-8"); + } catch { + return undefined; + } + }, + readDirectory: () => [], + useCaseSensitiveFileNames: () => false, + // Resolve relative imports to our virtual files. + resolveModuleNames: (moduleNames, containingFile) => + moduleNames.map((name) => { + if (!name.startsWith(".")) return undefined; + const dir = containingFile.slice( + 0, + containingFile.lastIndexOf("/") + 1, + ); + const resolved = `${dir}${name.replace(/^\.\//, "")}.ts`; + if (files.has(resolved)) { + return { + resolvedFileName: resolved, + isExternalLibraryImport: false, + extension: ts.Extension.Ts, + }; + } + return undefined; + }), + }; +} + +// --------------------------------------------------------------------------- +// Plugin loader +// --------------------------------------------------------------------------- + +function makeIntegration(usageSource: string) { + const files = new Map([ + ["/Vec2.ts", VEC2_SOURCE], + ["/usage.ts", usageSource], + ]); + + const host = makeHost(files); + const baseLS = ts.createLanguageService(host); + + // biome-ignore lint/suspicious/noExplicitAny: CJS `export =` interop + const pluginModule = (pluginInit as any)({ typescript: ts }); + const pluginLS = pluginModule.create({ + languageService: baseLS, + languageServiceHost: host, + project: { + getProjectName: () => "integration-test", + projectService: { logger: { info: () => {} } }, + }, + config: {}, + serverHost: {}, + }); + + // getSemanticDiagnostics reads `cache` AFTER the inner LS call, so the + // source-map cache is populated on this first call. For getQuickInfoAtPosition + // (which reads cache BEFORE the inner call) this warm-up ensures the cache + // is ready on the first hover query. + pluginLS.getSemanticDiagnostics("/usage.ts"); + + return { pluginLS }; +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe("LS plugin integration", () => { + describe("hover info", () => { + it("returns custom display parts when hovering over an overloaded operator", () => { + const { pluginLS } = makeIntegration(USAGE_SOURCE); + + // Position of the '+' in 'a + b'. + const plusPos = USAGE_SOURCE.indexOf("a + b") + 2; // +2 skips 'a ' + + const hover = pluginLS.getQuickInfoAtPosition("/usage.ts", plusPos); + + expect(hover).toBeDefined(); + const joined = + hover?.displayParts?.map((p: { text: string }) => p.text).join("") ?? + ""; + expect(joined).toContain("+"); + expect(joined).toContain("Vec2"); + }); + + it("falls through to regular TypeScript hover for non-operator positions", () => { + const { pluginLS } = makeIntegration(USAGE_SOURCE); + + // Hover over 'a' in 'const a = new Vec2(...)'. + const aPos = USAGE_SOURCE.indexOf("const a") + "const ".length; + + const hover = pluginLS.getQuickInfoAtPosition("/usage.ts", aPos); + + // Should get normal TS hover, not our custom operator format. + expect(hover).toBeDefined(); + const joined = + hover?.displayParts?.map((p: { text: string }) => p.text).join("") ?? + ""; + expect(joined).not.toMatch(/Vec2 \+ Vec2/); + }); + + it("includes JSDoc extracted from the operator method", () => { + const { pluginLS } = makeIntegration(USAGE_SOURCE); + + const plusPos = USAGE_SOURCE.indexOf("a + b") + 2; + + const hover = pluginLS.getQuickInfoAtPosition("/usage.ts", plusPos); + + expect(hover?.documentation?.[0]?.text).toContain("Adds two vectors"); + }); + }); + + describe("diagnostics", () => { + it("suppresses TS2365 for an overloaded binary expression", () => { + const { pluginLS } = makeIntegration(USAGE_SOURCE); + + // Call again after warm-up to get the final diagnostics. + const diagnostics = pluginLS.getSemanticDiagnostics("/usage.ts"); + const ts2365 = diagnostics.find((d: { code: number }) => d.code === 2365); + + // After transformation the operator expression is gone, so TypeScript + // should not report "Operator '+' cannot be applied". + expect(ts2365).toBeUndefined(); + }); + + it("remaps diagnostic positions back to the original source coordinates", () => { + const { pluginLS } = makeIntegration(USAGE_WITH_UNDECLARED); + + const diagnostics = pluginLS.getSemanticDiagnostics("/usage.ts"); + // TS2304: Cannot find name 'zzz' + const ts2304 = diagnostics.find((d: { code: number }) => d.code === 2304); + + expect(ts2304).toBeDefined(); + if (!ts2304) return; + + // The transformation expands 'a + b' (5 chars) to 'Vec2["+"](a, b)' + // (15 chars) — a shift of +10. In the transformed file, 'zzz' on the + // last line sits at ZZZ_POS_IN_ORIGINAL + 10, which exceeds + // USAGE_WITH_UNDECLARED.length. An un-remapped diagnostic would fail + // this bounds check. + expect(ts2304.start).toBeLessThan(USAGE_WITH_UNDECLARED.length); + + // With correct remapping the diagnostic must land on 'zzz'. + expect(ts2304.start).toBe(ZZZ_POS_IN_ORIGINAL); + }); + }); +}); diff --git a/plugins/ts-language-server/src/index.ts b/plugins/ts-language-server/src/index.ts index d02b8a2..2372bc6 100644 --- a/plugins/ts-language-server/src/index.ts +++ b/plugins/ts-language-server/src/index.ts @@ -142,18 +142,16 @@ function getSourceMapForFile( return entry.sourceMap; } -function remapDiagnosticSpan( - diag: { start?: number; length?: number }, +function withRemappedSpan( + diag: T, sourceMap: SourceMap, -): void { - if (diag.start !== undefined && diag.length !== undefined) { - const remapped = sourceMap.remapSpan({ - start: diag.start, - length: diag.length, - }); - diag.start = remapped.start; - diag.length = remapped.length; - } +): T { + if (diag.start === undefined || diag.length === undefined) return diag; + const remapped = sourceMap.remapSpan({ + start: diag.start, + length: diag.length, + }); + return { ...diag, start: remapped.start, length: remapped.length }; } function createProxy( @@ -186,27 +184,31 @@ function createProxy( return false; }; - proxy.getSemanticDiagnostics = (fileName) => { - const result = ls.getSemanticDiagnostics(fileName); - const entry = cache.get(fileName); + function remapDiagnostics< + T extends tsRuntime.Diagnostic | tsRuntime.DiagnosticWithLocation, + >(result: readonly T[], entry: CacheEntry | undefined): T[] { const sourceMap = entry?.sourceMap.isEmpty === false ? entry.sourceMap : undefined; + if (!sourceMap) return result as T[]; + return result.map((diag) => { + const remapped = withRemappedSpan(diag, sourceMap); + if (!remapped.relatedInformation?.length) return remapped; + return { + ...remapped, + relatedInformation: remapped.relatedInformation.map((related) => { + const relatedMap = related.file + ? getSourceMapForFile(cache, related.file.fileName) + : undefined; + return relatedMap ? withRemappedSpan(related, relatedMap) : related; + }), + }; + }); + } - if (sourceMap) { - for (const diag of result) { - remapDiagnosticSpan(diag, sourceMap); - if (diag.relatedInformation) { - for (const related of diag.relatedInformation) { - const relatedMap = related.file - ? getSourceMapForFile(cache, related.file.fileName) - : undefined; - if (relatedMap) remapDiagnosticSpan(related, relatedMap); - } - } - } - } - - return result.filter( + proxy.getSemanticDiagnostics = (fileName) => { + const result = ls.getSemanticDiagnostics(fileName); + const entry = cache.get(fileName); + return remapDiagnostics(result, entry).filter( (diag) => !isOverloadSuppressed(diag.code, diag.start, entry), ); }; @@ -214,37 +216,18 @@ function createProxy( proxy.getSyntacticDiagnostics = (fileName) => { const result = ls.getSyntacticDiagnostics(fileName); const entry = cache.get(fileName); - const sourceMap = - entry?.sourceMap.isEmpty === false ? entry.sourceMap : undefined; - - if (sourceMap) { - for (const diag of result) { - remapDiagnosticSpan(diag, sourceMap); - if (diag.relatedInformation) { - for (const related of diag.relatedInformation) { - const relatedMap = related.file - ? getSourceMapForFile(cache, related.file.fileName) - : undefined; - if (relatedMap) remapDiagnosticSpan(related, relatedMap); - } - } - } - } - - return result.filter( + return remapDiagnostics(result, entry).filter( (diag) => !isOverloadSuppressed(diag.code, diag.start, entry), - ); + ) as tsRuntime.DiagnosticWithLocation[]; }; proxy.getSuggestionDiagnostics = (fileName) => { const result = ls.getSuggestionDiagnostics(fileName); - const sourceMap = getSourceMapForFile(cache, fileName); - if (!sourceMap) return result; - - for (const diag of result) { - remapDiagnosticSpan(diag, sourceMap); - } - return result; + const entry = cache.get(fileName); + return remapDiagnostics( + result, + entry, + ) as tsRuntime.DiagnosticWithLocation[]; }; // --- Hover: remap input position + output span, custom operator hover ---