@boperators/webpack-loader 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -0
- package/dist/index.js +56 -0
- package/dist/toRawSourceMap.js +121 -0
- package/license.txt +8 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# @boperators/webpack-loader
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Webpack loader for [boperators](https://www.npmjs.com/package/boperators) that transforms operator overloads during the webpack build. Runs as a pre-loader before your TypeScript loader, replacing operator expressions with function calls and generating source maps.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install -D boperators @boperators/webpack-loader ts-loader webpack
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Configuration
|
|
14
|
+
|
|
15
|
+
Add the boperators loader as an `enforce: "pre"` rule in your `webpack.config.js`, alongside your TypeScript loader:
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
module.exports = {
|
|
19
|
+
devtool: "source-map",
|
|
20
|
+
module: {
|
|
21
|
+
rules: [
|
|
22
|
+
{
|
|
23
|
+
test: /\.ts$/,
|
|
24
|
+
loader: "ts-loader",
|
|
25
|
+
options: { transpileOnly: true },
|
|
26
|
+
exclude: /node_modules/,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
test: /\.ts$/,
|
|
30
|
+
enforce: "pre",
|
|
31
|
+
loader: "@boperators/webpack-loader",
|
|
32
|
+
exclude: /node_modules/,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The `enforce: "pre"` ensures boperators transforms your source before `ts-loader` compiles it.
|
|
40
|
+
|
|
41
|
+
### Options
|
|
42
|
+
|
|
43
|
+
| Option | Type | Default | Description |
|
|
44
|
+
|--------|------|---------|-------------|
|
|
45
|
+
| `project` | `string` | `"tsconfig.json"` | Path to `tsconfig.json`, relative to webpack's root context |
|
|
46
|
+
| `errorOnWarning` | `boolean` | `false` | Treat conflicting overload warnings as errors |
|
|
47
|
+
|
|
48
|
+
Options are passed via the loader options:
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
{
|
|
52
|
+
test: /\.ts$/,
|
|
53
|
+
enforce: "pre",
|
|
54
|
+
loader: "@boperators/webpack-loader",
|
|
55
|
+
options: {
|
|
56
|
+
project: "./tsconfig.build.json",
|
|
57
|
+
},
|
|
58
|
+
exclude: /node_modules/,
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Next.js
|
|
63
|
+
|
|
64
|
+
### Webpack (default)
|
|
65
|
+
|
|
66
|
+
Next.js exposes webpack config via `next.config.js`. Add boperators as a pre-loader before `ts-loader` handles your TypeScript:
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
// next.config.js
|
|
70
|
+
/** @type {import('next').NextConfig} */
|
|
71
|
+
const nextConfig = {
|
|
72
|
+
webpack(config) {
|
|
73
|
+
config.module.rules.push({
|
|
74
|
+
test: /\.tsx?$/,
|
|
75
|
+
enforce: "pre",
|
|
76
|
+
loader: "@boperators/webpack-loader",
|
|
77
|
+
exclude: /node_modules/,
|
|
78
|
+
});
|
|
79
|
+
return config;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
module.exports = nextConfig;
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
You don't need to add a separate `ts-loader` rule — Next.js already configures TypeScript compilation internally.
|
|
87
|
+
|
|
88
|
+
### Turbopack (Next.js 15+)
|
|
89
|
+
|
|
90
|
+
Turbopack supports webpack loaders via `turbopack.rules`. The boperators loader is likely compatible, but note that Turbopack's webpack loader API is partial and `this.rootContext` (used for tsconfig discovery) may not be populated. Use the `project` option to specify the tsconfig path explicitly:
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
// next.config.js
|
|
94
|
+
/** @type {import('next').NextConfig} */
|
|
95
|
+
const nextConfig = {
|
|
96
|
+
turbopack: {
|
|
97
|
+
rules: {
|
|
98
|
+
"*.{ts,tsx}": {
|
|
99
|
+
loaders: [
|
|
100
|
+
{
|
|
101
|
+
loader: "@boperators/webpack-loader",
|
|
102
|
+
options: { project: "./tsconfig.json" },
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
as: "*.tsx",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
module.exports = nextConfig;
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
> **Note:** Turbopack support is best-effort. If you encounter issues, fall back to the webpack config above (removing the `--turbopack` flag from your `next dev` command).
|
|
115
|
+
|
|
116
|
+
## How It Works
|
|
117
|
+
|
|
118
|
+
The loader runs as a webpack pre-loader, executing before TypeScript compilation:
|
|
119
|
+
|
|
120
|
+
1. Creates a [ts-morph](https://ts-morph.com) Project from your tsconfig
|
|
121
|
+
2. Scans all source files for operator overload definitions
|
|
122
|
+
3. Transforms expressions in the current file (e.g. `v1 + v2` becomes `Vector3["+"][0](v1, v2)`)
|
|
123
|
+
4. Generates a V3 source map so stack traces and debugger breakpoints map back to your original source
|
|
124
|
+
5. Passes the transformed code to the next loader (e.g. `ts-loader`)
|
|
125
|
+
|
|
126
|
+
## Comparison with Other Approaches
|
|
127
|
+
|
|
128
|
+
| Approach | When it runs | Use case |
|
|
129
|
+
|----------|-------------|----------|
|
|
130
|
+
| **`@boperators/cli`** | Before compilation | Batch transform to disk, then compile normally |
|
|
131
|
+
| **`@boperators/plugin-tsc`** | During compilation | Seamless `tsc` integration, no intermediate files |
|
|
132
|
+
| **`@boperators/webpack-loader`** | During bundling | Webpack projects, integrates into existing build pipeline |
|
|
133
|
+
| **`@boperators/plugin-vite`** | During bundling | Vite projects, integrates into Rollup-based pipeline |
|
|
134
|
+
| **`@boperators/plugin-esbuild`** | During bundling | ESBuild projects, fast bundler integration |
|
|
135
|
+
| **`@boperators/plugin-bun`** | At runtime | Bun-only, transforms on module load |
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
7
|
+
const boperators_1 = require("boperators");
|
|
8
|
+
function normalizePath(p) {
|
|
9
|
+
return p.replace(/\\/g, "/");
|
|
10
|
+
}
|
|
11
|
+
const loader = function (source, _map) {
|
|
12
|
+
var _a;
|
|
13
|
+
const callback = this.async();
|
|
14
|
+
if (!callback)
|
|
15
|
+
return;
|
|
16
|
+
try {
|
|
17
|
+
const resourcePath = normalizePath(this.resourcePath);
|
|
18
|
+
// Resolve tsconfig path: explicit option, or default to rootContext/tsconfig.json
|
|
19
|
+
const projectOption = (_a = this.getOptions()) === null || _a === void 0 ? void 0 : _a.project;
|
|
20
|
+
const tsconfigPath = projectOption
|
|
21
|
+
? node_path_1.default.resolve(this.rootContext, projectOption)
|
|
22
|
+
: node_path_1.default.join(this.rootContext, "tsconfig.json");
|
|
23
|
+
const bopConfig = (0, boperators_1.loadConfig)({
|
|
24
|
+
searchDir: node_path_1.default.dirname(tsconfigPath),
|
|
25
|
+
});
|
|
26
|
+
// Create ts-morph project from tsconfig
|
|
27
|
+
const tsMorphProject = new boperators_1.Project({
|
|
28
|
+
tsConfigFilePath: tsconfigPath,
|
|
29
|
+
});
|
|
30
|
+
const errorManager = new boperators_1.ErrorManager(bopConfig);
|
|
31
|
+
const overloadStore = new boperators_1.OverloadStore(tsMorphProject, errorManager, bopConfig.logger);
|
|
32
|
+
const overloadInjector = new boperators_1.OverloadInjector(tsMorphProject, overloadStore, bopConfig.logger);
|
|
33
|
+
// Parse all files for overload definitions
|
|
34
|
+
const allFiles = tsMorphProject.getSourceFiles();
|
|
35
|
+
for (const file of allFiles) {
|
|
36
|
+
overloadStore.addOverloadsFromFile(file);
|
|
37
|
+
}
|
|
38
|
+
errorManager.throwIfErrorsElseLogWarnings();
|
|
39
|
+
// Only transform this file
|
|
40
|
+
const sourceFile = tsMorphProject.getSourceFile(resourcePath);
|
|
41
|
+
if (!sourceFile) {
|
|
42
|
+
return callback(null, source, null);
|
|
43
|
+
}
|
|
44
|
+
const originalText = sourceFile.getFullText();
|
|
45
|
+
const result = overloadInjector.overloadFile(sourceFile);
|
|
46
|
+
if (result.text === originalText) {
|
|
47
|
+
return callback(null, source, null);
|
|
48
|
+
}
|
|
49
|
+
const sourceMap = (0, boperators_1.toV3SourceMap)(result.edits, originalText, result.text, resourcePath);
|
|
50
|
+
return callback(null, result.text, sourceMap);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
callback(err);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
exports.default = loader;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toRawSourceMap = toRawSourceMap;
|
|
4
|
+
/**
|
|
5
|
+
* Convert boperators EditRecord[] into a V3 RawSourceMap.
|
|
6
|
+
*
|
|
7
|
+
* Generates line-level mappings: unchanged regions map 1:1, and edit
|
|
8
|
+
* boundaries map back to the start of the original edit region.
|
|
9
|
+
*/
|
|
10
|
+
function toRawSourceMap(edits, originalText, transformedText, fileName) {
|
|
11
|
+
const origLineStarts = buildLineStarts(originalText);
|
|
12
|
+
const transLineStarts = buildLineStarts(transformedText);
|
|
13
|
+
// Each segment is [transCol, sourceIndex, origLine, origCol]
|
|
14
|
+
// We only have one source (index 0), so sourceIndex is always 0.
|
|
15
|
+
const mappingLines = [];
|
|
16
|
+
const transLineCount = transLineStarts.length;
|
|
17
|
+
// Initialise empty lines
|
|
18
|
+
for (let i = 0; i < transLineCount; i++) {
|
|
19
|
+
mappingLines.push([]);
|
|
20
|
+
}
|
|
21
|
+
// For positions outside any edit, the mapping is identity + accumulated delta.
|
|
22
|
+
// For positions inside an edit's transformed region, map to the original edit start.
|
|
23
|
+
// We generate one segment per transformed line start.
|
|
24
|
+
for (let transLine = 0; transLine < transLineCount; transLine++) {
|
|
25
|
+
const transOffset = transLineStarts[transLine];
|
|
26
|
+
const origOffset = transformedToOriginal(edits, transOffset);
|
|
27
|
+
const { line: origLine, col: origCol } = offsetToLineCol(origLineStarts, origOffset);
|
|
28
|
+
mappingLines[transLine].push([0, 0, origLine, origCol]);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
version: 3,
|
|
32
|
+
file: fileName,
|
|
33
|
+
sources: [fileName],
|
|
34
|
+
sourcesContent: [originalText],
|
|
35
|
+
names: [],
|
|
36
|
+
mappings: encodeMappings(mappingLines),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** Map a transformed-source offset back to the original source. */
|
|
40
|
+
function transformedToOriginal(edits, pos) {
|
|
41
|
+
let delta = 0;
|
|
42
|
+
for (const edit of edits) {
|
|
43
|
+
if (pos < edit.transStart) {
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
if (pos < edit.transEnd) {
|
|
47
|
+
return edit.origStart;
|
|
48
|
+
}
|
|
49
|
+
delta = edit.origEnd - edit.transEnd;
|
|
50
|
+
}
|
|
51
|
+
return pos + delta;
|
|
52
|
+
}
|
|
53
|
+
/** Build an array where index i is the character offset of line i. */
|
|
54
|
+
function buildLineStarts(text) {
|
|
55
|
+
const starts = [0];
|
|
56
|
+
for (let i = 0; i < text.length; i++) {
|
|
57
|
+
if (text[i] === "\n") {
|
|
58
|
+
starts.push(i + 1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return starts;
|
|
62
|
+
}
|
|
63
|
+
/** Convert a character offset to 0-based line and column. */
|
|
64
|
+
function offsetToLineCol(lineStarts, offset) {
|
|
65
|
+
// Binary search for the line containing this offset
|
|
66
|
+
let lo = 0;
|
|
67
|
+
let hi = lineStarts.length - 1;
|
|
68
|
+
while (lo < hi) {
|
|
69
|
+
const mid = (lo + hi + 1) >>> 1;
|
|
70
|
+
if (lineStarts[mid] <= offset) {
|
|
71
|
+
lo = mid;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
hi = mid - 1;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { line: lo, col: offset - lineStarts[lo] };
|
|
78
|
+
}
|
|
79
|
+
/** Encode segment arrays into a VLQ mappings string. */
|
|
80
|
+
function encodeMappings(lines) {
|
|
81
|
+
let prevTransCol = 0;
|
|
82
|
+
let prevOrigLine = 0;
|
|
83
|
+
let prevOrigCol = 0;
|
|
84
|
+
let prevSourceIdx = 0;
|
|
85
|
+
const lineStrings = [];
|
|
86
|
+
for (const segments of lines) {
|
|
87
|
+
prevTransCol = 0; // reset column tracking per line
|
|
88
|
+
const segStrings = [];
|
|
89
|
+
for (const seg of segments) {
|
|
90
|
+
const [transCol, sourceIdx, origLine, origCol] = seg;
|
|
91
|
+
segStrings.push(encodeVLQ(transCol - prevTransCol) +
|
|
92
|
+
encodeVLQ(sourceIdx - prevSourceIdx) +
|
|
93
|
+
encodeVLQ(origLine - prevOrigLine) +
|
|
94
|
+
encodeVLQ(origCol - prevOrigCol));
|
|
95
|
+
prevTransCol = transCol;
|
|
96
|
+
prevSourceIdx = sourceIdx;
|
|
97
|
+
prevOrigLine = origLine;
|
|
98
|
+
prevOrigCol = origCol;
|
|
99
|
+
}
|
|
100
|
+
lineStrings.push(segStrings.join(","));
|
|
101
|
+
}
|
|
102
|
+
return lineStrings.join(";");
|
|
103
|
+
}
|
|
104
|
+
const VLQ_BASE = 32;
|
|
105
|
+
const VLQ_BASE_MASK = VLQ_BASE - 1;
|
|
106
|
+
const VLQ_CONTINUATION = VLQ_BASE;
|
|
107
|
+
const BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
108
|
+
/** Encode a single integer as a VLQ base64 string. */
|
|
109
|
+
function encodeVLQ(value) {
|
|
110
|
+
let vlq = value < 0 ? (-value << 1) | 1 : value << 1;
|
|
111
|
+
let result = "";
|
|
112
|
+
do {
|
|
113
|
+
let digit = vlq & VLQ_BASE_MASK;
|
|
114
|
+
vlq >>>= 5;
|
|
115
|
+
if (vlq > 0) {
|
|
116
|
+
digit |= VLQ_CONTINUATION;
|
|
117
|
+
}
|
|
118
|
+
result += BASE64_CHARS[digit];
|
|
119
|
+
} while (vlq > 0);
|
|
120
|
+
return result;
|
|
121
|
+
}
|
package/license.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Copyright 2025 Dief Bell
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
8
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@boperators/webpack-loader",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "webpack loader for boperators",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/DiefBell/boperators",
|
|
9
|
+
"directory": "plugins/webpack"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/DiefBell/boperators/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/DiefBell/boperators/tree/main/plugins/webpack",
|
|
15
|
+
"type": "commonjs",
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"keywords": [
|
|
18
|
+
"boperators",
|
|
19
|
+
"typescript",
|
|
20
|
+
"operator",
|
|
21
|
+
"overload",
|
|
22
|
+
"operator-overloading",
|
|
23
|
+
"webpack",
|
|
24
|
+
"transform"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc",
|
|
28
|
+
"watch": "tsc -w",
|
|
29
|
+
"test": "bun test",
|
|
30
|
+
"test:watch": "bun test --watch",
|
|
31
|
+
"prepublish": "bun run build"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"package.json",
|
|
35
|
+
"README.md",
|
|
36
|
+
"license.txt",
|
|
37
|
+
"dist"
|
|
38
|
+
],
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"boperators": "0.1.4",
|
|
41
|
+
"typescript": ">=5.0.0 <5.10.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^25.2.3",
|
|
45
|
+
"@types/webpack": "^5.28.5",
|
|
46
|
+
"boperators": "file:../../package",
|
|
47
|
+
"webpack": "^5.105.2"
|
|
48
|
+
}
|
|
49
|
+
}
|