@fixback/cli 0.1.0 → 0.2.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/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/cli.d.ts +2 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/main.d.ts +11 -0
- package/dist/main.js +15 -16
- package/dist/release.d.ts +46 -0
- package/dist/release.js +24 -19
- package/dist/upload.d.ts +94 -0
- package/dist/upload.js +82 -26
- package/package.json +33 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fixback
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -5,6 +5,8 @@ production build's sourcemaps** so the Issues Fixback captures on your site
|
|
|
5
5
|
arrive with a symbolicated **code-area pointer** — the original `file:line`
|
|
6
6
|
(with source context) where the error likely lives, not a minified frame.
|
|
7
7
|
|
|
8
|
+
> 📖 **Full documentation: [docs.fixback.dev/sdk/sourcemaps](https://docs.fixback.dev/sdk/sourcemaps)** — the source maps guide and the CLI reference.
|
|
9
|
+
|
|
8
10
|
```bash
|
|
9
11
|
# after your production build, locally or in CI
|
|
10
12
|
FIXBACK_SECRET_KEY=sk_… npx @fixback/cli sourcemaps upload ./dist
|
|
@@ -13,6 +15,11 @@ FIXBACK_SECRET_KEY=sk_… npx @fixback/cli sourcemaps upload ./dist
|
|
|
13
15
|
That's the whole integration. If a release has no uploaded sourcemaps, nothing
|
|
14
16
|
changes — the feature is simply off for it.
|
|
15
17
|
|
|
18
|
+
> **Using Vite?** [`@fixback/vite-plugin`](../vite-plugin) does this automatically
|
|
19
|
+
> as part of `vite build`, so there's no separate CI step to add or forget. Reach
|
|
20
|
+
> for this CLI when your build isn't Vite, or when you want the upload decoupled
|
|
21
|
+
> from the build.
|
|
22
|
+
|
|
16
23
|
## How it fits together
|
|
17
24
|
|
|
18
25
|
1. **Build with sourcemaps.** e.g. Vite: `build: { sourcemap: true }` (or
|
package/dist/cli.d.ts
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@fixback/cli` doubles as the **build-time sourcemap library** shared with the
|
|
3
|
+
* bundler plugins (e.g. `@fixback/vite-plugin`): the one implementation of
|
|
4
|
+
* sourcemap discovery, the (release, `~/`-abstract path) upload wire (ADR-0024),
|
|
5
|
+
* the upload loop, and release resolution. The `fixback` bin (`cli.ts`) is a thin
|
|
6
|
+
* command wrapper over these same functions — importing this module runs none of it.
|
|
7
|
+
*/
|
|
8
|
+
export { discoverSourcemaps, postSourcemap, toArtifactPath, uploadAll, uploadSourcemaps, } from "./upload.js";
|
|
9
|
+
export type { DiscoveredMap, PostSourcemapOptions, PostSourcemapResult, UploadAllDeps, UploadAllResult, UploadDeps, UploadOptions, } from "./upload.js";
|
|
10
|
+
export { normaliseRelease, RELEASE_MAX_LENGTH, resolveRelease, } from "./release.js";
|
|
11
|
+
export type { ResolveReleaseInput, ResolveReleaseResult } from "./release.js";
|
|
12
|
+
export { gitHeadSha } from "./main.js";
|
|
13
|
+
export { DEFAULT_API_URL } from "@fixback/sdk-core";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@fixback/cli` doubles as the **build-time sourcemap library** shared with the
|
|
3
|
+
* bundler plugins (e.g. `@fixback/vite-plugin`): the one implementation of
|
|
4
|
+
* sourcemap discovery, the (release, `~/`-abstract path) upload wire (ADR-0024),
|
|
5
|
+
* the upload loop, and release resolution. The `fixback` bin (`cli.ts`) is a thin
|
|
6
|
+
* command wrapper over these same functions — importing this module runs none of it.
|
|
7
|
+
*/
|
|
8
|
+
export { discoverSourcemaps, postSourcemap, toArtifactPath, uploadAll, uploadSourcemaps, } from "./upload.js";
|
|
9
|
+
export { normaliseRelease, RELEASE_MAX_LENGTH, resolveRelease, } from "./release.js";
|
|
10
|
+
export { gitHeadSha } from "./main.js";
|
|
11
|
+
// The hosted API origin is defined once in the shared core (ADR-0028); re-exported
|
|
12
|
+
// here so the bundler plugins take their default from the same constant the SDKs do.
|
|
13
|
+
export { DEFAULT_API_URL } from "@fixback/sdk-core";
|
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type UploadDeps } from "./upload.js";
|
|
2
|
+
/** Everything the CLI touches outside its own logic — injectable for tests. */
|
|
3
|
+
export interface CliDeps extends UploadDeps {
|
|
4
|
+
readonly env: NodeJS.ProcessEnv;
|
|
5
|
+
/** Resolve the git HEAD SHA of the working directory, or null when not a repo. */
|
|
6
|
+
readonly resolveGitSha: () => Promise<string | null>;
|
|
7
|
+
}
|
|
8
|
+
/** The production `resolveGitSha`: `git rev-parse HEAD`, null on any failure. */
|
|
9
|
+
export declare function gitHeadSha(): Promise<string | null>;
|
|
10
|
+
/** Run the CLI; returns the process exit code. */
|
|
11
|
+
export declare function runCli(argv: readonly string[], deps: CliDeps): Promise<number>;
|
package/dist/main.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
-
import {
|
|
3
|
+
import { DEFAULT_API_URL } from "@fixback/sdk-core";
|
|
4
|
+
import { resolveRelease } from "./release.js";
|
|
4
5
|
import { uploadSourcemaps } from "./upload.js";
|
|
5
6
|
/**
|
|
6
7
|
* The `fixback` CLI (#117, ADR-0024) — Fixback's build-time companion, Sentry-
|
|
@@ -13,8 +14,6 @@ import { uploadSourcemaps } from "./upload.js";
|
|
|
13
14
|
* Everything is injectable (`CliDeps`) so the command is tested without a
|
|
14
15
|
* network, a repo, or real env.
|
|
15
16
|
*/
|
|
16
|
-
/** The hosted API origin, mirrored by value from the SDK's `DEFAULT_API_URL`. */
|
|
17
|
-
const DEFAULT_API_URL = "https://api.fixback.dev";
|
|
18
17
|
const USAGE = `fixback — Fixback's build-time CLI
|
|
19
18
|
|
|
20
19
|
Usage:
|
|
@@ -82,22 +81,22 @@ export async function runCli(argv, deps) {
|
|
|
82
81
|
deps.error(`Missing <dir> — the build output directory to scan.\n\n${USAGE}`);
|
|
83
82
|
return 1;
|
|
84
83
|
}
|
|
85
|
-
// Release: flag > env > git HEAD SHA
|
|
84
|
+
// Release: flag > env > git HEAD SHA — the precedence the bundler plugins share
|
|
85
|
+
// (`resolveRelease`), so a plugin upload and a CLI upload key maps identically.
|
|
86
86
|
const flagRelease = flags.get("release");
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
deps.error(`Invalid release ${JSON.stringify(rawRelease)} — use 1-100 visible characters ` +
|
|
98
|
-
"with no whitespace or slashes (e.g. a git SHA or semver).");
|
|
87
|
+
const resolved = await resolveRelease({
|
|
88
|
+
release: typeof flagRelease === "string" ? flagRelease : undefined,
|
|
89
|
+
env: deps.env,
|
|
90
|
+
resolveGitSha: deps.resolveGitSha,
|
|
91
|
+
});
|
|
92
|
+
if (!resolved.ok) {
|
|
93
|
+
deps.error(resolved.reason === "missing"
|
|
94
|
+
? "No release given and no git repository found. Pass --release <r> or set FIXBACK_RELEASE."
|
|
95
|
+
: `Invalid release ${JSON.stringify(resolved.raw)} — use 1-100 visible characters ` +
|
|
96
|
+
"with no whitespace or slashes (e.g. a git SHA or semver).");
|
|
99
97
|
return 1;
|
|
100
98
|
}
|
|
99
|
+
const release = resolved.release;
|
|
101
100
|
const dryRun = flags.get("dry-run") === true;
|
|
102
101
|
const flagKey = flags.get("key");
|
|
103
102
|
const secretKey = (typeof flagKey === "string" ? flagKey : undefined) ??
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolving a build's **Release** — the one precedence rule shared by the
|
|
3
|
+
* `fixback` CLI and the bundler plugins (`@fixback/vite-plugin`).
|
|
4
|
+
*
|
|
5
|
+
* A release is only useful if the value the build uploads maps under is the value
|
|
6
|
+
* the SDK stamps on its captures (#117, ADR-0024). Two things have to agree for
|
|
7
|
+
* that: the *validation* (which lives once in `@fixback/sdk-core`, shared with the
|
|
8
|
+
* SDKs themselves) and the *precedence* — an explicit value, else `FIXBACK_RELEASE`,
|
|
9
|
+
* else the git HEAD SHA (Sentry's propose-version equivalent). The precedence used
|
|
10
|
+
* to be written out twice, once per caller; it is {@link resolveRelease} now.
|
|
11
|
+
*/
|
|
12
|
+
import { normaliseRelease, RELEASE_MAX_LENGTH } from "@fixback/sdk-core";
|
|
13
|
+
export { normaliseRelease, RELEASE_MAX_LENGTH };
|
|
14
|
+
/** What {@link resolveRelease} reads, in precedence order. */
|
|
15
|
+
export interface ResolveReleaseInput {
|
|
16
|
+
/** An explicitly configured release (a `--release` flag, a plugin option). */
|
|
17
|
+
readonly release?: string;
|
|
18
|
+
/** The process environment — `FIXBACK_RELEASE` is the second source. */
|
|
19
|
+
readonly env: {
|
|
20
|
+
readonly FIXBACK_RELEASE?: string | undefined;
|
|
21
|
+
};
|
|
22
|
+
/** The git HEAD SHA, or `null` when the build is not in a repo — the fallback. */
|
|
23
|
+
readonly resolveGitSha: () => Promise<string | null>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A resolved release, or *why* there is none — `missing` (nothing configured and
|
|
27
|
+
* no repo to fall back on) versus `invalid` (a value that would be discarded
|
|
28
|
+
* server-side). The two need different messages, so they stay distinct rather than
|
|
29
|
+
* collapsing to `undefined`.
|
|
30
|
+
*/
|
|
31
|
+
export type ResolveReleaseResult = {
|
|
32
|
+
readonly ok: true;
|
|
33
|
+
readonly release: string;
|
|
34
|
+
} | {
|
|
35
|
+
readonly ok: false;
|
|
36
|
+
readonly reason: "missing";
|
|
37
|
+
} | {
|
|
38
|
+
readonly ok: false;
|
|
39
|
+
readonly reason: "invalid";
|
|
40
|
+
readonly raw: string;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the release to upload under: an explicit value, else `FIXBACK_RELEASE`,
|
|
44
|
+
* else the git HEAD SHA — then validate it with the shared rules.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveRelease(input: ResolveReleaseInput): Promise<ResolveReleaseResult>;
|
package/dist/release.js
CHANGED
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* Resolving a build's **Release** — the one precedence rule shared by the
|
|
3
|
+
* `fixback` CLI and the bundler plugins (`@fixback/vite-plugin`).
|
|
4
|
+
*
|
|
5
|
+
* A release is only useful if the value the build uploads maps under is the value
|
|
6
|
+
* the SDK stamps on its captures (#117, ADR-0024). Two things have to agree for
|
|
7
|
+
* that: the *validation* (which lives once in `@fixback/sdk-core`, shared with the
|
|
8
|
+
* SDKs themselves) and the *precedence* — an explicit value, else `FIXBACK_RELEASE`,
|
|
9
|
+
* else the git HEAD SHA (Sentry's propose-version equivalent). The precedence used
|
|
10
|
+
* to be written out twice, once per caller; it is {@link resolveRelease} now.
|
|
8
11
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
import { normaliseRelease, RELEASE_MAX_LENGTH } from "@fixback/sdk-core";
|
|
13
|
+
// Release validation is the shared core's (ADR-0028) — the SDKs validate what they
|
|
14
|
+
// stamp with the very same function. Re-exported so `@fixback/cli` consumers (and
|
|
15
|
+
// the bundler plugins) keep one import path for it.
|
|
16
|
+
export { normaliseRelease, RELEASE_MAX_LENGTH };
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the release to upload under: an explicit value, else `FIXBACK_RELEASE`,
|
|
19
|
+
* else the git HEAD SHA — then validate it with the shared rules.
|
|
20
|
+
*/
|
|
21
|
+
export async function resolveRelease(input) {
|
|
22
|
+
const raw = input.release ?? input.env.FIXBACK_RELEASE ?? (await input.resolveGitSha()) ?? undefined;
|
|
23
|
+
if (!raw)
|
|
24
|
+
return { ok: false, reason: "missing" };
|
|
25
|
+
const release = normaliseRelease(raw);
|
|
26
|
+
return release ? { ok: true, release } : { ok: false, reason: "invalid", raw };
|
|
22
27
|
}
|
package/dist/upload.d.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** One discovered sourcemap: where it is on disk, and the bundle path it maps. */
|
|
2
|
+
export interface DiscoveredMap {
|
|
3
|
+
/** Absolute path of the `.map` file on disk. */
|
|
4
|
+
readonly filePath: string;
|
|
5
|
+
/** The generated bundle's path relative to the build dir, POSIX-separated (no `.map`). */
|
|
6
|
+
readonly bundlePath: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Walk a build directory for JS sourcemaps. The bundle path is the map's
|
|
10
|
+
* relative path minus `.map` (a `.js.map` maps the `.js` beside it), normalised
|
|
11
|
+
* to POSIX separators — ready to be joined onto the `~/` prefix.
|
|
12
|
+
*/
|
|
13
|
+
export declare function discoverSourcemaps(dir: string): Promise<DiscoveredMap[]>;
|
|
14
|
+
/** Join the `~/` URL prefix and a bundle-relative path into the abstract path. */
|
|
15
|
+
export declare function toArtifactPath(urlPrefix: string, bundlePath: string): string;
|
|
16
|
+
export interface UploadOptions {
|
|
17
|
+
readonly dir: string;
|
|
18
|
+
readonly release: string;
|
|
19
|
+
/** The abstract prefix bundles are served under. Defaults to `~/`. */
|
|
20
|
+
readonly urlPrefix: string;
|
|
21
|
+
readonly apiUrl: string;
|
|
22
|
+
readonly secretKey: string;
|
|
23
|
+
/** List what would upload without any network. */
|
|
24
|
+
readonly dryRun: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface UploadDeps {
|
|
27
|
+
readonly fetch: typeof globalThis.fetch;
|
|
28
|
+
readonly log: (line: string) => void;
|
|
29
|
+
readonly error: (line: string) => void;
|
|
30
|
+
}
|
|
31
|
+
/** Just the fields `postSourcemap` needs — a structural subset of {@link UploadOptions}. */
|
|
32
|
+
export interface PostSourcemapOptions {
|
|
33
|
+
readonly release: string;
|
|
34
|
+
readonly urlPrefix: string;
|
|
35
|
+
readonly apiUrl: string;
|
|
36
|
+
readonly secretKey: string;
|
|
37
|
+
}
|
|
38
|
+
/** The outcome of one artifact POST. */
|
|
39
|
+
export interface PostSourcemapResult {
|
|
40
|
+
readonly ok: boolean;
|
|
41
|
+
/** The HTTP status, or `0` if the request itself never completed. */
|
|
42
|
+
readonly status: number;
|
|
43
|
+
/** The `~/`-abstract path the artifact was keyed under. */
|
|
44
|
+
readonly artifactPath: string;
|
|
45
|
+
/** A human-readable failure reason; present only when `ok` is false. */
|
|
46
|
+
readonly message?: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* POST one discovered sourcemap to `POST /api/sourcemaps`, keyed by
|
|
50
|
+
* (release, `~/`-abstract path) and authenticated with the Project's secret key.
|
|
51
|
+
* The single implementation of the ADR-0024 upload wire — shared by the CLI's
|
|
52
|
+
* {@link uploadSourcemaps} loop and the bundler plugins (`@fixback/vite-plugin`),
|
|
53
|
+
* so both key artifacts identically. It never throws on an API error (that is
|
|
54
|
+
* reported through the result); a transport failure still rejects, for the caller
|
|
55
|
+
* to handle.
|
|
56
|
+
*/
|
|
57
|
+
export declare function postSourcemap(map: DiscoveredMap, options: PostSourcemapOptions, deps: Pick<UploadDeps, "fetch">): Promise<PostSourcemapResult>;
|
|
58
|
+
/** The outcome of an {@link uploadAll} run. */
|
|
59
|
+
export interface UploadAllResult {
|
|
60
|
+
/** The `.map` files that landed, by absolute path — what a caller may prune. */
|
|
61
|
+
readonly uploaded: string[];
|
|
62
|
+
/** How many maps failed (an API refusal or a transport error). */
|
|
63
|
+
readonly failed: number;
|
|
64
|
+
/** Whether the loop stopped early on a `401` (every remaining map would fail too). */
|
|
65
|
+
readonly stoppedOnAuth: boolean;
|
|
66
|
+
}
|
|
67
|
+
/** Injectable collaborators for {@link uploadAll}. */
|
|
68
|
+
export interface UploadAllDeps {
|
|
69
|
+
readonly fetch: typeof globalThis.fetch;
|
|
70
|
+
/**
|
|
71
|
+
* Called once per map with its outcome, so each caller reports in its own voice
|
|
72
|
+
* (the CLI logs lines and returns an exit code; the plugin warns through Vite's
|
|
73
|
+
* logger). A transport failure arrives here as a `status: 0` result rather than
|
|
74
|
+
* a throw, so one unreachable request never abandons the remaining maps.
|
|
75
|
+
*/
|
|
76
|
+
readonly onResult?: (map: DiscoveredMap, result: PostSourcemapResult) => void;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* POST every discovered map for one release — the single upload loop behind both
|
|
80
|
+
* `fixback sourcemaps upload` and the bundler plugins, so a plugin upload and a
|
|
81
|
+
* CLI upload keep byte-for-byte identical wire behaviour (ADR-0024).
|
|
82
|
+
*
|
|
83
|
+
* It reports rather than decides: no logging, no exit code, no pruning. A `401`
|
|
84
|
+
* **breaks the loop** — an invalid secret key would fail identically for every
|
|
85
|
+
* remaining map, and hammering the API with the rest tells no one anything new.
|
|
86
|
+
*/
|
|
87
|
+
export declare function uploadAll(maps: readonly DiscoveredMap[], options: PostSourcemapOptions, deps: UploadAllDeps): Promise<UploadAllResult>;
|
|
88
|
+
/**
|
|
89
|
+
* Upload every discovered map for one release. Returns a process exit code:
|
|
90
|
+
* `0` when everything (or a clean dry run) succeeded, `1` when nothing was
|
|
91
|
+
* found or any upload failed — CI should fail loudly rather than silently
|
|
92
|
+
* shipping a release without symbolication.
|
|
93
|
+
*/
|
|
94
|
+
export declare function uploadSourcemaps(options: UploadOptions, deps: UploadDeps): Promise<number>;
|
package/dist/upload.js
CHANGED
|
@@ -51,6 +51,74 @@ async function apiErrorMessage(response) {
|
|
|
51
51
|
}
|
|
52
52
|
return response.statusText || `HTTP ${response.status}`;
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* POST one discovered sourcemap to `POST /api/sourcemaps`, keyed by
|
|
56
|
+
* (release, `~/`-abstract path) and authenticated with the Project's secret key.
|
|
57
|
+
* The single implementation of the ADR-0024 upload wire — shared by the CLI's
|
|
58
|
+
* {@link uploadSourcemaps} loop and the bundler plugins (`@fixback/vite-plugin`),
|
|
59
|
+
* so both key artifacts identically. It never throws on an API error (that is
|
|
60
|
+
* reported through the result); a transport failure still rejects, for the caller
|
|
61
|
+
* to handle.
|
|
62
|
+
*/
|
|
63
|
+
export async function postSourcemap(map, options, deps) {
|
|
64
|
+
const path = toArtifactPath(options.urlPrefix, map.bundlePath);
|
|
65
|
+
const body = new FormData();
|
|
66
|
+
body.set("payload", JSON.stringify({ release: options.release, path }));
|
|
67
|
+
body.set("map", new Blob([await readFile(map.filePath)], { type: "application/json" }), `${map.bundlePath.split("/").pop() ?? "bundle.js"}.map`);
|
|
68
|
+
const response = await deps.fetch(`${options.apiUrl}/api/sourcemaps`, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: { Authorization: `Bearer ${options.secretKey}` },
|
|
71
|
+
body,
|
|
72
|
+
});
|
|
73
|
+
if (response.ok)
|
|
74
|
+
return { ok: true, status: response.status, artifactPath: path };
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
status: response.status,
|
|
78
|
+
artifactPath: path,
|
|
79
|
+
message: await apiErrorMessage(response),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* POST every discovered map for one release — the single upload loop behind both
|
|
84
|
+
* `fixback sourcemaps upload` and the bundler plugins, so a plugin upload and a
|
|
85
|
+
* CLI upload keep byte-for-byte identical wire behaviour (ADR-0024).
|
|
86
|
+
*
|
|
87
|
+
* It reports rather than decides: no logging, no exit code, no pruning. A `401`
|
|
88
|
+
* **breaks the loop** — an invalid secret key would fail identically for every
|
|
89
|
+
* remaining map, and hammering the API with the rest tells no one anything new.
|
|
90
|
+
*/
|
|
91
|
+
export async function uploadAll(maps, options, deps) {
|
|
92
|
+
const uploaded = [];
|
|
93
|
+
let failed = 0;
|
|
94
|
+
let stoppedOnAuth = false;
|
|
95
|
+
for (const map of maps) {
|
|
96
|
+
let result;
|
|
97
|
+
try {
|
|
98
|
+
result = await postSourcemap(map, options, { fetch: deps.fetch });
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
result = {
|
|
102
|
+
ok: false,
|
|
103
|
+
status: 0,
|
|
104
|
+
artifactPath: toArtifactPath(options.urlPrefix, map.bundlePath),
|
|
105
|
+
message: error instanceof Error ? error.message : String(error),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
deps.onResult?.(map, result);
|
|
109
|
+
if (result.ok) {
|
|
110
|
+
uploaded.push(map.filePath);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
failed += 1;
|
|
114
|
+
// 401 means every remaining upload will fail identically — stop early.
|
|
115
|
+
if (result.status === 401) {
|
|
116
|
+
stoppedOnAuth = true;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { uploaded, failed, stoppedOnAuth };
|
|
121
|
+
}
|
|
54
122
|
/**
|
|
55
123
|
* Upload every discovered map for one release. Returns a process exit code:
|
|
56
124
|
* `0` when everything (or a clean dry run) succeeded, `1` when nothing was
|
|
@@ -64,36 +132,24 @@ export async function uploadSourcemaps(options, deps) {
|
|
|
64
132
|
"Enable them in your build (e.g. Vite: build.sourcemap = true) and re-run.");
|
|
65
133
|
return 1;
|
|
66
134
|
}
|
|
67
|
-
let failed = 0;
|
|
68
|
-
for (const map of maps) {
|
|
69
|
-
const path = toArtifactPath(options.urlPrefix, map.bundlePath);
|
|
70
|
-
if (options.dryRun) {
|
|
71
|
-
deps.log(`would upload ${map.bundlePath}.map as ${path}`);
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
const body = new FormData();
|
|
75
|
-
body.set("payload", JSON.stringify({ release: options.release, path }));
|
|
76
|
-
body.set("map", new Blob([await readFile(map.filePath)], { type: "application/json" }), `${map.bundlePath.split("/").pop() ?? "bundle.js"}.map`);
|
|
77
|
-
const response = await deps.fetch(`${options.apiUrl}/api/sourcemaps`, {
|
|
78
|
-
method: "POST",
|
|
79
|
-
headers: { Authorization: `Bearer ${options.secretKey}` },
|
|
80
|
-
body,
|
|
81
|
-
});
|
|
82
|
-
if (response.ok) {
|
|
83
|
-
deps.log(`uploaded ${map.bundlePath}.map as ${path}`);
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
failed += 1;
|
|
87
|
-
deps.error(`failed ${map.bundlePath}.map (${response.status}): ${await apiErrorMessage(response)}`);
|
|
88
|
-
// 401 means every remaining upload will fail identically — stop early.
|
|
89
|
-
if (response.status === 401)
|
|
90
|
-
break;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
135
|
if (options.dryRun) {
|
|
136
|
+
for (const map of maps) {
|
|
137
|
+
deps.log(`would upload ${map.bundlePath}.map as ${toArtifactPath(options.urlPrefix, map.bundlePath)}`);
|
|
138
|
+
}
|
|
94
139
|
deps.log(`Dry run: ${maps.length} sourcemap(s) would upload for release ${options.release}.`);
|
|
95
140
|
return 0;
|
|
96
141
|
}
|
|
142
|
+
const { failed } = await uploadAll(maps, options, {
|
|
143
|
+
fetch: deps.fetch,
|
|
144
|
+
onResult: (map, result) => {
|
|
145
|
+
if (result.ok) {
|
|
146
|
+
deps.log(`uploaded ${map.bundlePath}.map as ${result.artifactPath}`);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
deps.error(`failed ${map.bundlePath}.map (${result.status}): ${result.message ?? "upload failed"}`);
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
});
|
|
97
153
|
if (failed > 0) {
|
|
98
154
|
deps.error(`${failed} upload(s) failed for release ${options.release}.`);
|
|
99
155
|
return 1;
|
package/package.json
CHANGED
|
@@ -1,24 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fixback/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Fixback's build-time CLI — upload sourcemaps per release so Issues gain symbolicated code-area pointers.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"homepage": "https://docs.fixback.dev/sdk/sourcemaps",
|
|
6
7
|
"type": "module",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
7
16
|
"bin": {
|
|
8
17
|
"fixback": "./dist/cli.js"
|
|
9
18
|
},
|
|
10
19
|
"files": [
|
|
11
20
|
"dist",
|
|
12
|
-
"README.md"
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
13
23
|
],
|
|
14
24
|
"engines": {
|
|
15
25
|
"node": ">=20.12"
|
|
16
26
|
},
|
|
17
27
|
"devDependencies": {
|
|
18
|
-
"@types/node": "^22.
|
|
28
|
+
"@types/node": "^22.20.1",
|
|
19
29
|
"typescript": "5.9.3",
|
|
20
30
|
"vitest": "^4.1.10"
|
|
21
31
|
},
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"keywords": [
|
|
34
|
+
"fixback",
|
|
35
|
+
"sourcemaps",
|
|
36
|
+
"source-maps",
|
|
37
|
+
"cli",
|
|
38
|
+
"symbolication"
|
|
39
|
+
],
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/wemuda/fixback.git",
|
|
43
|
+
"directory": "packages/cli"
|
|
44
|
+
},
|
|
45
|
+
"bugs": "https://docs.fixback.dev",
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@fixback/sdk-core": "0.3.0"
|
|
51
|
+
},
|
|
22
52
|
"scripts": {
|
|
23
53
|
"build": "tsc -p tsconfig.build.json",
|
|
24
54
|
"lint": "eslint .",
|