@flareapp/webpack 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.oxlintrc.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "../../node_modules/oxlint/configuration_schema.json",
3
+ "extends": ["../../.oxlintrc.json"],
4
+ "env": {
5
+ "node": true
6
+ }
7
+ }
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # @flareapp/webpack
2
+
3
+ Webpack 5 plugin for [Flare](https://flareapp.io) that uploads sourcemaps after each build. With sourcemaps, error reports sent by `@flareapp/js` will show the original source code instead of minified output.
4
+
5
+ The plugin also injects the Flare API key and a sourcemap version identifier into your build via `DefinePlugin`, so `flare.light()` works without any additional configuration.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @flareapp/webpack
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ Add the plugin to your `webpack.config.js`:
16
+
17
+ ```js
18
+ const { FlareWebpackPlugin } = require('@flareapp/webpack');
19
+
20
+ module.exports = {
21
+ // ...
22
+ devtool: 'source-map',
23
+ plugins: [
24
+ new FlareWebpackPlugin({
25
+ apiKey: 'YOUR_FLARE_API_KEY',
26
+ }),
27
+ ],
28
+ };
29
+ ```
30
+
31
+ ## Options
32
+
33
+ | Option | Type | Default | Description |
34
+ | ------------------ | --------- | ------------------------------------ | -------------------------------------------------- |
35
+ | `apiKey` | `string` | (required) | Your Flare API key |
36
+ | `apiEndpoint` | `string` | `https://flareapp.io/api/sourcemaps` | Sourcemap upload endpoint |
37
+ | `version` | `string` | random UUID | Sourcemap version identifier |
38
+ | `removeSourcemaps` | `boolean` | `false` | Delete `.map` files from build output after upload |
39
+ | `runInDevelopment` | `boolean` | `false` | Upload sourcemaps even in development mode |
40
+ | `publicPath` | `string` | from webpack config | Override the public path prepended to filenames |
41
+
42
+ ## Compatibility
43
+
44
+ - Webpack 5
45
+
46
+ ## Documentation
47
+
48
+ Full documentation is available at [flareapp.io/docs/javascript/general/resolving-bundled-code](https://flareapp.io/docs/javascript/general/resolving-bundled-code).
49
+
50
+ ## License
51
+
52
+ The MIT License (MIT). Please see [License File](../../LICENSE.md) for more information.
package/dist/index.cjs ADDED
@@ -0,0 +1,195 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
12
+ key = keys[i];
13
+ if (!__hasOwnProp.call(to, key) && key !== except) {
14
+ __defProp(to, key, {
15
+ get: ((k) => from[k]).bind(null, key),
16
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
17
+ });
18
+ }
19
+ }
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
24
+ value: mod,
25
+ enumerable: true
26
+ }) : target, mod));
27
+
28
+ //#endregion
29
+ let node_crypto = require("node:crypto");
30
+ let node_fs = require("node:fs");
31
+ let node_path = require("node:path");
32
+ let node_zlib = require("node:zlib");
33
+ let webpack = require("webpack");
34
+ webpack = __toESM(webpack);
35
+
36
+ //#region ../flare-api/dist/index.mjs
37
+ var FlareApiError = class extends Error {
38
+ constructor(message) {
39
+ super(message);
40
+ this.name = "FlareApiError";
41
+ }
42
+ };
43
+ const RETRIABLE_STATUS_CODES = new Set([
44
+ 429,
45
+ 502,
46
+ 503,
47
+ 504
48
+ ]);
49
+ var FlareApi = class {
50
+ constructor(endpoint, key, version) {
51
+ this.endpoint = endpoint;
52
+ this.key = key;
53
+ this.version = version;
54
+ }
55
+ uploadSourcemap(sourcemap) {
56
+ const base64GzipSourcemap = (0, node_zlib.deflateRawSync)(sourcemap.content).toString("base64");
57
+ return this.postWithRetry({
58
+ key: this.key,
59
+ version_id: this.version,
60
+ relative_filename: sourcemap.originalFile,
61
+ sourcemap: base64GzipSourcemap
62
+ });
63
+ }
64
+ async postWithRetry(data, maxRetries = 3) {
65
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
66
+ try {
67
+ const response = await fetch(this.endpoint, {
68
+ method: "POST",
69
+ headers: { "Content-Type": "application/json" },
70
+ body: JSON.stringify(data)
71
+ });
72
+ if (response.ok) return;
73
+ if (!RETRIABLE_STATUS_CODES.has(response.status)) {
74
+ const body = await response.text();
75
+ throw new FlareApiError(`Flare API returned ${response.status}: ${body}`);
76
+ }
77
+ if (attempt === maxRetries) throw new FlareApiError(`Flare API returned ${response.status} after ${maxRetries} attempts`);
78
+ } catch (error) {
79
+ if (error instanceof FlareApiError) throw error;
80
+ if (attempt === maxRetries) {
81
+ const message = error instanceof Error ? error.message : String(error);
82
+ throw new Error(`Network error after ${maxRetries} attempts: ${message}`, { cause: error });
83
+ }
84
+ }
85
+ await this.delay(Math.pow(2, attempt - 1) * 1e3);
86
+ }
87
+ }
88
+ delay(ms) {
89
+ return new Promise((resolve) => setTimeout(resolve, ms));
90
+ }
91
+ };
92
+
93
+ //#endregion
94
+ //#region src/FlareWebpackPlugin.ts
95
+ function log(message, isError = false) {
96
+ const formatted = `@flareapp/webpack: ${message}`;
97
+ if (isError) console.error(formatted);
98
+ else console.log(formatted);
99
+ }
100
+ var FlareWebpackPlugin = class {
101
+ apiKey;
102
+ apiEndpoint;
103
+ runInDevelopment;
104
+ version;
105
+ removeSourcemaps;
106
+ publicPathOverride;
107
+ constructor({ apiKey, apiEndpoint = "https://flareapp.io/api/sourcemaps", runInDevelopment = false, version = (0, node_crypto.randomUUID)(), removeSourcemaps = false, publicPath }) {
108
+ this.apiKey = apiKey;
109
+ this.apiEndpoint = apiEndpoint;
110
+ this.runInDevelopment = runInDevelopment;
111
+ this.version = version;
112
+ this.removeSourcemaps = removeSourcemaps;
113
+ this.publicPathOverride = publicPath;
114
+ }
115
+ apply(compiler) {
116
+ const { DefinePlugin } = webpack.default;
117
+ new DefinePlugin({
118
+ FLARE_JS_KEY: JSON.stringify(this.apiKey),
119
+ FLARE_SOURCEMAP_VERSION: JSON.stringify(this.version)
120
+ }).apply(compiler);
121
+ const flare = new FlareApi(this.apiEndpoint, this.apiKey, this.version);
122
+ compiler.hooks.afterEmit.tapPromise("FlareWebpackPlugin", async (compilation) => {
123
+ if (!this.shouldUpload(compiler, compilation)) return;
124
+ const resolvedPublicPath = this.resolvePublicPath(compiler);
125
+ const sourcemaps = this.getSourcemaps(compilation, resolvedPublicPath);
126
+ if (!sourcemaps.length) {
127
+ compilation.warnings.push(new webpack.default.WebpackError("@flareapp/webpack: No sourcemap files found. Make sure sourcemaps are enabled in your webpack config."));
128
+ return;
129
+ }
130
+ log(`Uploading ${sourcemaps.length} sourcemap(s) to Flare.`);
131
+ const results = await Promise.allSettled(sourcemaps.map(({ sourcemap }) => flare.uploadSourcemap(sourcemap)));
132
+ const failed = results.filter((r) => r.status === "rejected");
133
+ if (failed.length > 0) for (const result of failed) compilation.warnings.push(new webpack.default.WebpackError(`@flareapp/webpack: Upload failed: ${result.reason}`));
134
+ else log("Successfully uploaded all sourcemaps to Flare.");
135
+ if (this.removeSourcemaps) {
136
+ for (let i = 0; i < sourcemaps.length; i++) {
137
+ if (results[i].status === "rejected") continue;
138
+ try {
139
+ (0, node_fs.unlinkSync)(sourcemaps[i].path);
140
+ } catch (error) {
141
+ log(`Error removing ${sourcemaps[i].path}: ${error}`, true);
142
+ }
143
+ }
144
+ log("Removed sourcemap files from build output.");
145
+ }
146
+ });
147
+ }
148
+ shouldUpload(compiler, compilation) {
149
+ if (!this.apiKey) {
150
+ compilation.warnings.push(new webpack.default.WebpackError("@flareapp/webpack: No Flare API key provided, not uploading sourcemaps."));
151
+ return false;
152
+ }
153
+ if (!this.runInDevelopment && compiler.options.mode === "development") {
154
+ log("Running webpack in development mode, not uploading sourcemaps.");
155
+ return false;
156
+ }
157
+ if (compiler.options.watch) {
158
+ log("Running webpack in watch mode, not uploading sourcemaps.");
159
+ return false;
160
+ }
161
+ return true;
162
+ }
163
+ resolvePublicPath(compiler) {
164
+ if (this.publicPathOverride != null) return this.publicPathOverride.endsWith("/") ? this.publicPathOverride : `${this.publicPathOverride}/`;
165
+ const configPublicPath = compiler.options.output?.publicPath;
166
+ if (typeof configPublicPath === "string" && configPublicPath && configPublicPath !== "auto") return configPublicPath.endsWith("/") ? configPublicPath : `${configPublicPath}/`;
167
+ return "/";
168
+ }
169
+ getSourcemaps(compilation, publicPath) {
170
+ const outputPath = compilation.getPath(compilation.compiler.outputPath);
171
+ const sourcemaps = [];
172
+ for (const chunk of compilation.chunks) {
173
+ const jsFile = [...chunk.files].find((file) => file.endsWith(".js"));
174
+ const mapFile = [...chunk.auxiliaryFiles].find((file) => file.endsWith(".js.map"));
175
+ if (!jsFile || !mapFile) continue;
176
+ const mapPath = (0, node_path.join)(outputPath, mapFile);
177
+ try {
178
+ const content = (0, node_fs.readFileSync)(mapPath, "utf8");
179
+ sourcemaps.push({
180
+ sourcemap: {
181
+ originalFile: `${publicPath}${jsFile}`,
182
+ content
183
+ },
184
+ path: mapPath
185
+ });
186
+ } catch (error) {
187
+ log(`Error reading sourcemap ${mapPath}: ${error}`, true);
188
+ }
189
+ }
190
+ return sourcemaps;
191
+ }
192
+ };
193
+
194
+ //#endregion
195
+ exports.FlareWebpackPlugin = FlareWebpackPlugin;
@@ -0,0 +1,35 @@
1
+ import { Compiler } from "webpack";
2
+
3
+ //#region src/types.d.ts
4
+ type FlareWebpackPluginOptions = {
5
+ apiKey: string;
6
+ apiEndpoint?: string;
7
+ runInDevelopment?: boolean;
8
+ version?: string;
9
+ removeSourcemaps?: boolean;
10
+ publicPath?: string;
11
+ };
12
+ //#endregion
13
+ //#region src/FlareWebpackPlugin.d.ts
14
+ declare class FlareWebpackPlugin {
15
+ private readonly apiKey;
16
+ private readonly apiEndpoint;
17
+ private readonly runInDevelopment;
18
+ private readonly version;
19
+ private readonly removeSourcemaps;
20
+ private readonly publicPathOverride;
21
+ constructor({
22
+ apiKey,
23
+ apiEndpoint,
24
+ runInDevelopment,
25
+ version,
26
+ removeSourcemaps,
27
+ publicPath
28
+ }: FlareWebpackPluginOptions);
29
+ apply(compiler: Compiler): void;
30
+ private shouldUpload;
31
+ private resolvePublicPath;
32
+ private getSourcemaps;
33
+ }
34
+ //#endregion
35
+ export { FlareWebpackPlugin, type FlareWebpackPluginOptions };
@@ -0,0 +1,35 @@
1
+ import { Compiler } from "webpack";
2
+
3
+ //#region src/types.d.ts
4
+ type FlareWebpackPluginOptions = {
5
+ apiKey: string;
6
+ apiEndpoint?: string;
7
+ runInDevelopment?: boolean;
8
+ version?: string;
9
+ removeSourcemaps?: boolean;
10
+ publicPath?: string;
11
+ };
12
+ //#endregion
13
+ //#region src/FlareWebpackPlugin.d.ts
14
+ declare class FlareWebpackPlugin {
15
+ private readonly apiKey;
16
+ private readonly apiEndpoint;
17
+ private readonly runInDevelopment;
18
+ private readonly version;
19
+ private readonly removeSourcemaps;
20
+ private readonly publicPathOverride;
21
+ constructor({
22
+ apiKey,
23
+ apiEndpoint,
24
+ runInDevelopment,
25
+ version,
26
+ removeSourcemaps,
27
+ publicPath
28
+ }: FlareWebpackPluginOptions);
29
+ apply(compiler: Compiler): void;
30
+ private shouldUpload;
31
+ private resolvePublicPath;
32
+ private getSourcemaps;
33
+ }
34
+ //#endregion
35
+ export { FlareWebpackPlugin, type FlareWebpackPluginOptions };
package/dist/index.mjs ADDED
@@ -0,0 +1,166 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readFileSync, unlinkSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { deflateRawSync } from "node:zlib";
5
+ import webpack from "webpack";
6
+
7
+ //#region ../flare-api/dist/index.mjs
8
+ var FlareApiError = class extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = "FlareApiError";
12
+ }
13
+ };
14
+ const RETRIABLE_STATUS_CODES = new Set([
15
+ 429,
16
+ 502,
17
+ 503,
18
+ 504
19
+ ]);
20
+ var FlareApi = class {
21
+ constructor(endpoint, key, version) {
22
+ this.endpoint = endpoint;
23
+ this.key = key;
24
+ this.version = version;
25
+ }
26
+ uploadSourcemap(sourcemap) {
27
+ const base64GzipSourcemap = deflateRawSync(sourcemap.content).toString("base64");
28
+ return this.postWithRetry({
29
+ key: this.key,
30
+ version_id: this.version,
31
+ relative_filename: sourcemap.originalFile,
32
+ sourcemap: base64GzipSourcemap
33
+ });
34
+ }
35
+ async postWithRetry(data, maxRetries = 3) {
36
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
37
+ try {
38
+ const response = await fetch(this.endpoint, {
39
+ method: "POST",
40
+ headers: { "Content-Type": "application/json" },
41
+ body: JSON.stringify(data)
42
+ });
43
+ if (response.ok) return;
44
+ if (!RETRIABLE_STATUS_CODES.has(response.status)) {
45
+ const body = await response.text();
46
+ throw new FlareApiError(`Flare API returned ${response.status}: ${body}`);
47
+ }
48
+ if (attempt === maxRetries) throw new FlareApiError(`Flare API returned ${response.status} after ${maxRetries} attempts`);
49
+ } catch (error) {
50
+ if (error instanceof FlareApiError) throw error;
51
+ if (attempt === maxRetries) {
52
+ const message = error instanceof Error ? error.message : String(error);
53
+ throw new Error(`Network error after ${maxRetries} attempts: ${message}`, { cause: error });
54
+ }
55
+ }
56
+ await this.delay(Math.pow(2, attempt - 1) * 1e3);
57
+ }
58
+ }
59
+ delay(ms) {
60
+ return new Promise((resolve) => setTimeout(resolve, ms));
61
+ }
62
+ };
63
+
64
+ //#endregion
65
+ //#region src/FlareWebpackPlugin.ts
66
+ function log(message, isError = false) {
67
+ const formatted = `@flareapp/webpack: ${message}`;
68
+ if (isError) console.error(formatted);
69
+ else console.log(formatted);
70
+ }
71
+ var FlareWebpackPlugin = class {
72
+ apiKey;
73
+ apiEndpoint;
74
+ runInDevelopment;
75
+ version;
76
+ removeSourcemaps;
77
+ publicPathOverride;
78
+ constructor({ apiKey, apiEndpoint = "https://flareapp.io/api/sourcemaps", runInDevelopment = false, version = randomUUID(), removeSourcemaps = false, publicPath }) {
79
+ this.apiKey = apiKey;
80
+ this.apiEndpoint = apiEndpoint;
81
+ this.runInDevelopment = runInDevelopment;
82
+ this.version = version;
83
+ this.removeSourcemaps = removeSourcemaps;
84
+ this.publicPathOverride = publicPath;
85
+ }
86
+ apply(compiler) {
87
+ const { DefinePlugin } = webpack;
88
+ new DefinePlugin({
89
+ FLARE_JS_KEY: JSON.stringify(this.apiKey),
90
+ FLARE_SOURCEMAP_VERSION: JSON.stringify(this.version)
91
+ }).apply(compiler);
92
+ const flare = new FlareApi(this.apiEndpoint, this.apiKey, this.version);
93
+ compiler.hooks.afterEmit.tapPromise("FlareWebpackPlugin", async (compilation) => {
94
+ if (!this.shouldUpload(compiler, compilation)) return;
95
+ const resolvedPublicPath = this.resolvePublicPath(compiler);
96
+ const sourcemaps = this.getSourcemaps(compilation, resolvedPublicPath);
97
+ if (!sourcemaps.length) {
98
+ compilation.warnings.push(new webpack.WebpackError("@flareapp/webpack: No sourcemap files found. Make sure sourcemaps are enabled in your webpack config."));
99
+ return;
100
+ }
101
+ log(`Uploading ${sourcemaps.length} sourcemap(s) to Flare.`);
102
+ const results = await Promise.allSettled(sourcemaps.map(({ sourcemap }) => flare.uploadSourcemap(sourcemap)));
103
+ const failed = results.filter((r) => r.status === "rejected");
104
+ if (failed.length > 0) for (const result of failed) compilation.warnings.push(new webpack.WebpackError(`@flareapp/webpack: Upload failed: ${result.reason}`));
105
+ else log("Successfully uploaded all sourcemaps to Flare.");
106
+ if (this.removeSourcemaps) {
107
+ for (let i = 0; i < sourcemaps.length; i++) {
108
+ if (results[i].status === "rejected") continue;
109
+ try {
110
+ unlinkSync(sourcemaps[i].path);
111
+ } catch (error) {
112
+ log(`Error removing ${sourcemaps[i].path}: ${error}`, true);
113
+ }
114
+ }
115
+ log("Removed sourcemap files from build output.");
116
+ }
117
+ });
118
+ }
119
+ shouldUpload(compiler, compilation) {
120
+ if (!this.apiKey) {
121
+ compilation.warnings.push(new webpack.WebpackError("@flareapp/webpack: No Flare API key provided, not uploading sourcemaps."));
122
+ return false;
123
+ }
124
+ if (!this.runInDevelopment && compiler.options.mode === "development") {
125
+ log("Running webpack in development mode, not uploading sourcemaps.");
126
+ return false;
127
+ }
128
+ if (compiler.options.watch) {
129
+ log("Running webpack in watch mode, not uploading sourcemaps.");
130
+ return false;
131
+ }
132
+ return true;
133
+ }
134
+ resolvePublicPath(compiler) {
135
+ if (this.publicPathOverride != null) return this.publicPathOverride.endsWith("/") ? this.publicPathOverride : `${this.publicPathOverride}/`;
136
+ const configPublicPath = compiler.options.output?.publicPath;
137
+ if (typeof configPublicPath === "string" && configPublicPath && configPublicPath !== "auto") return configPublicPath.endsWith("/") ? configPublicPath : `${configPublicPath}/`;
138
+ return "/";
139
+ }
140
+ getSourcemaps(compilation, publicPath) {
141
+ const outputPath = compilation.getPath(compilation.compiler.outputPath);
142
+ const sourcemaps = [];
143
+ for (const chunk of compilation.chunks) {
144
+ const jsFile = [...chunk.files].find((file) => file.endsWith(".js"));
145
+ const mapFile = [...chunk.auxiliaryFiles].find((file) => file.endsWith(".js.map"));
146
+ if (!jsFile || !mapFile) continue;
147
+ const mapPath = join(outputPath, mapFile);
148
+ try {
149
+ const content = readFileSync(mapPath, "utf8");
150
+ sourcemaps.push({
151
+ sourcemap: {
152
+ originalFile: `${publicPath}${jsFile}`,
153
+ content
154
+ },
155
+ path: mapPath
156
+ });
157
+ } catch (error) {
158
+ log(`Error reading sourcemap ${mapPath}: ${error}`, true);
159
+ }
160
+ }
161
+ return sourcemaps;
162
+ }
163
+ };
164
+
165
+ //#endregion
166
+ export { FlareWebpackPlugin };
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@flareapp/webpack",
3
+ "version": "2.0.0",
4
+ "description": "Webpack plugin for uploading sourcemaps to Flare",
5
+ "homepage": "https://flareapp.io",
6
+ "bugs": "https://github.com/spatie/flare-client-js/issues",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/spatie/flare-client-js.git"
10
+ },
11
+ "license": "MIT",
12
+ "author": {
13
+ "name": "Spatie",
14
+ "email": "info@spatie.be"
15
+ },
16
+ "contributors": [
17
+ "Dries Heyninck <dries@spatie.be>"
18
+ ],
19
+ "main": "./dist/index.cjs",
20
+ "module": "./dist/index.mjs",
21
+ "types": "./dist/index.d.cts",
22
+ "exports": {
23
+ ".": {
24
+ "import": {
25
+ "types": "./dist/index.d.mts",
26
+ "default": "./dist/index.mjs"
27
+ },
28
+ "require": {
29
+ "types": "./dist/index.d.cts",
30
+ "default": "./dist/index.cjs"
31
+ }
32
+ }
33
+ },
34
+ "engines": {
35
+ "node": ">=18"
36
+ },
37
+ "scripts": {
38
+ "prepublishOnly": "npm run build",
39
+ "build": "tsdown src/index.ts --format cjs,esm --dts --clean --noExternal @flareapp/flare-api",
40
+ "test": "vitest run",
41
+ "typescript": "tsc --noEmit",
42
+ "release": "release-it"
43
+ },
44
+ "devDependencies": {
45
+ "@flareapp/flare-api": "*",
46
+ "@types/webpack": "^5.0.0",
47
+ "tsdown": "^0.20.3",
48
+ "typescript": "^5.7.0",
49
+ "vitest": "^4.0.0",
50
+ "webpack": "^5.0.0"
51
+ },
52
+ "peerDependencies": {
53
+ "webpack": "^5.0.0"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public"
57
+ }
58
+ }