@interfere/vite 11.1.1 → 12.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.
Files changed (57) hide show
  1. package/README.md +37 -33
  2. package/dist/init.d.mts +5 -5
  3. package/dist/init.mjs +1 -1
  4. package/dist/internal/browser-globals.d.mts +2 -3
  5. package/dist/internal/build/build-receipt.d.mts +62 -0
  6. package/dist/internal/build/build-receipt.mjs +1 -0
  7. package/dist/internal/build/canonical-build-path.d.mts +3 -0
  8. package/dist/internal/build/canonical-build-path.mjs +1 -0
  9. package/dist/internal/build/git.d.mts +10 -0
  10. package/dist/internal/build/git.mjs +1 -0
  11. package/dist/internal/build/pipeline.d.mts +81 -55
  12. package/dist/internal/build/pipeline.mjs +1 -1
  13. package/dist/internal/build/read-publication-status.d.mts +39 -0
  14. package/dist/internal/build/read-publication-status.mjs +1 -0
  15. package/dist/internal/build/render-debug-id.d.mts +6 -0
  16. package/dist/internal/build/render-debug-id.mjs +1 -0
  17. package/dist/internal/build/source-maps.d.mts +8 -26
  18. package/dist/internal/build/source-maps.mjs +1 -1
  19. package/dist/internal/build/transfer-source-maps.d.mts +59 -0
  20. package/dist/internal/build/transfer-source-maps.mjs +1 -0
  21. package/dist/internal/build/wait-for-source-maps.d.mts +48 -0
  22. package/dist/internal/build/wait-for-source-maps.mjs +1 -0
  23. package/dist/internal/release-slug.d.mts +5 -6
  24. package/dist/internal/release-slug.mjs +1 -1
  25. package/dist/internal/server/capture.d.mts +2 -3
  26. package/dist/internal/server/capture.mjs +1 -1
  27. package/dist/internal/server/console-bridge.d.mts +7 -3
  28. package/dist/internal/server/console-bridge.mjs +1 -1
  29. package/dist/internal/server/env.d.mts +8 -10
  30. package/dist/internal/server/env.mjs +1 -1
  31. package/dist/internal/server/freshness-propagator.d.mts +18 -0
  32. package/dist/internal/server/freshness-propagator.mjs +1 -0
  33. package/dist/internal/server/instrumentation-options.d.mts +2 -3
  34. package/dist/internal/server/register.d.mts +2 -3
  35. package/dist/internal/server/register.mjs +1 -1
  36. package/dist/internal/server/remote-config.d.mts +3 -4
  37. package/dist/internal/server/remote-config.mjs +1 -1
  38. package/dist/internal/server/request-trace.d.mts +4 -5
  39. package/dist/internal/server/request-trace.mjs +1 -1
  40. package/dist/internal/server/trace-meta.d.mts +9 -8
  41. package/dist/internal/server/trace-meta.mjs +1 -1
  42. package/dist/internal/server/types.d.mts +2 -3
  43. package/dist/package.mjs +1 -1
  44. package/dist/plugin.d.mts +12 -7
  45. package/dist/plugin.mjs +2 -2
  46. package/dist/provider.d.mts +3 -3
  47. package/dist/provider.mjs +1 -1
  48. package/dist/tanstack-start.d.mts +2 -2
  49. package/dist/tanstack-start.mjs +1 -1
  50. package/dist/version.d.mts +2 -3
  51. package/package.json +45 -43
  52. package/dist/internal/build/baked-config.d.mts +0 -13
  53. package/dist/internal/build/baked-config.mjs +0 -1
  54. package/dist/internal/server/traceparent.d.mts +0 -17
  55. package/dist/internal/server/traceparent.mjs +0 -1
  56. package/dist/internal/server/url.d.mts +0 -4
  57. package/dist/internal/server/url.mjs +0 -1
package/README.md CHANGED
@@ -135,14 +135,14 @@ For **Vercel preview / production**, set `INTERFERE_API_KEY` in your `.env.local
135
135
 
136
136
  ## Environment Variables
137
137
 
138
- | Variable | Required | Description |
139
- | --- | --- | --- |
140
- | `VITE_INTERFERE_PUBLIC_KEY` | Yes | Surface public key in the `interfere_public_us_*` or `interfere_public_eu_*` format. Stamped into the page by the plugin at build time, and reused server-side (`register()`, request middleware) — the plugin mirrors it into the dev server's process, so no separate server variable is needed. |
141
- | `INTERFERE_API_KEY` | Yes for release metadata | Interfere secret key in the `interfere_secret_us_*` or `interfere_secret_eu_*` format. Used by the plugin at build time. Without it, release metadata is not published and the collector rejects events as `COLLECTOR_RELEASE_NOT_FOUND`. |
142
- | `INTERFERE_API_URL` | No | Override the collector URL the build pipeline targets. Defaults to `https://in.interfere.com`. |
143
- | `VITE_INTERFERE_API_URL` | No | Override the collector URL the browser kernel targets in direct mode. Proxy mode uses `INTERFERE_API_URL` server-side. |
144
- | `VITE_INTERFERE_COMMIT_SHA` | No | Override the auto-detected commit SHA used to derive the release slug. Defaults to `git rev-parse HEAD`. |
145
- | `VITE_INTERFERE_FORCE_ENABLE` | No | `"1"` / `"true"` flips the SDK into force-enable mode so `vite dev` (which sets `NODE_ENV=development`) emits events. Production collectors silently 202-drop force-enabled batches; safe to ship by mistake but useless. Use only in dev. |
138
+ | Variable | Required | Description |
139
+ | ----------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
140
+ | `VITE_INTERFERE_PUBLIC_KEY` | Yes | Surface public key in the `interfere_public_us_*` or `interfere_public_eu_*` format. Stamped into the page by the plugin at build time, and reused server-side (`register()`, request middleware) — the plugin mirrors it into the dev server's process, so no separate server variable is needed. |
141
+ | `INTERFERE_API_KEY` | Yes for release metadata | Interfere secret key in the `interfere_secret_us_*` or `interfere_secret_eu_*` format. Used by the plugin at build time. Without it, release metadata is not published and the collector rejects events as `COLLECTOR_RELEASE_NOT_FOUND`. |
142
+ | `INTERFERE_API_URL` | No | Override the collector URL the build pipeline targets. Defaults to `https://in.interfere.com`. |
143
+ | `VITE_INTERFERE_API_URL` | No | Override the collector URL the browser kernel targets in direct mode. Proxy mode uses `INTERFERE_API_URL` server-side. |
144
+ | `VITE_INTERFERE_COMMIT_SHA` | No | Override the auto-detected commit SHA used to derive the release slug. Defaults to `git rev-parse HEAD`. |
145
+ | `VITE_INTERFERE_FORCE_ENABLE` | No | `"1"` / `"true"` flips the SDK into force-enable mode so `vite dev` (which sets `NODE_ENV=development`) emits events. Production collectors silently 202-drop force-enabled batches; safe to ship by mistake but useless. Use only in dev. |
146
146
 
147
147
  ## Plugin Options
148
148
 
@@ -152,11 +152,15 @@ interfere({
152
152
  });
153
153
  ```
154
154
 
155
- | Option | Default | Description |
156
- | --- | --- | --- |
157
- | `apiHost` | — | The endpoint the browser SDK posts to. Defaults to Interfere's endpoint. Set it to a subdomain of your own site (e.g. `"https://in.yourapp.com"`) that resolves to Interfere via a DNS record to survive ad-blockers, or to a regional endpoint. Add a CNAME pointing at the target shown in your dashboard; Interfere provisions TLS automatically. |
158
- | `commitSha` | `VITE_INTERFERE_COMMIT_SHA` → `git rev-parse HEAD` | Commit SHA used to derive the deterministic release slug. |
159
- | `sourceMaps` | auto | `false` disables build-time release metadata publishing. By default the pipeline runs when `INTERFERE_API_KEY` and a commit SHA are available. |
155
+ | Option | Default | Description |
156
+ | ----------------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
157
+ | `apiHost` | — | The endpoint the browser SDK posts to. Defaults to Interfere's endpoint. Set it to a subdomain of your own site (e.g. `"https://in.yourapp.com"`) that resolves to Interfere via a DNS record to survive ad-blockers, or to a regional endpoint. Add a CNAME pointing at the target shown in your dashboard; Interfere provisions TLS automatically. |
158
+ | `commitSha` | `VITE_INTERFERE_COMMIT_SHA` → `git rev-parse HEAD` | Commit SHA used to derive the deterministic release slug. |
159
+ | `sourceMaps` | auto | `false` disables build-time release metadata publishing. `"required"` fails the build if credentials, release identity, referenced source maps, or server acceptance are missing. By default the pipeline runs when `INTERFERE_API_KEY` and a commit SHA are available and reports publication failures as warnings. |
160
+ | `sourceMapReadinessTimeoutMs` | disabled | Opt in to waiting for the same upload session, exact accepted batch, and published manifest. Requires `sourceMaps: "required"`; accepts 1 to 600000 milliseconds. Deploy the compatible status API before enabling. Transfer expiry remains separate. |
161
+ | `buildReceipt` | disabled | Private JSON path inside the project and outside public/build output directories. Records consumed SDK config and fetch status, original release/build identity, source publication status when available, and hashes of native Vite output files. Capture does not wait for readiness. Keep this artifact private; missing or fallback evidence cannot establish safe deployment reuse. |
162
+
163
+ Build logs report release creation, signing, session lookup, upload, finalization, readiness waiting and cleanup in milliseconds, plus transferred bytes. Finalization confirms durable acceptance. Readiness waiting is disabled by default; when enabled, source maps remain local until the requested gate succeeds. The status API verifies the manifest in the upload's pinned storage bucket. It does not replace runtime stack/source canaries or continually rehash every previously published map.
160
164
 
161
165
  Ingest requests never carry cookies. `apiHost` posts to your own site's subdomain, surviving ad-blockers with no server-side component.
162
166
 
@@ -194,22 +198,22 @@ function useInterfereIdentity() {
194
198
 
195
199
  ### Parameters
196
200
 
197
- | Field | Required | Description |
198
- | --- | --- | --- |
199
- | `identifier` | Yes | Unique user ID (your internal ID, not email) |
200
- | `source` | Yes | Auth source: `{ type: "clerk", name: "Clerk" }`, `{ type: "auth0", name: "Auth0" }`, or `{ type: "custom", name: "Your Provider" }` |
201
- | `name` | No | Display name |
202
- | `email` | No | Email address |
203
- | `avatar` | No | Avatar URL |
204
- | `traits` | No | Arbitrary key-value metadata (`Record<string, unknown>`) |
201
+ | Field | Required | Description |
202
+ | ------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------- |
203
+ | `identifier` | Yes | Unique user ID (your internal ID, not email) |
204
+ | `source` | Yes | Auth source: `{ type: "clerk", name: "Clerk" }`, `{ type: "auth0", name: "Auth0" }`, or `{ type: "custom", name: "Your Provider" }` |
205
+ | `name` | No | Display name |
206
+ | `email` | No | Email address |
207
+ | `avatar` | No | Avatar URL |
208
+ | `traits` | No | Arbitrary key-value metadata (`Record<string, unknown>`) |
205
209
 
206
210
  ### API
207
211
 
208
- | Method | Description |
209
- | --- | --- |
212
+ | Method | Description |
213
+ | ---------------------- | --------------------------------------------------------------------------------------------------- |
210
214
  | `identity.set(params)` | Link the current session to a user. Deduplicated per session — only the first call sends a request. |
211
- | `identity.clear()` | Clears the linked identity and rotates the session. Call on logout. |
212
- | `identity.get()` | Returns the current `IdentifyParams`, or `null` if no identity has been set. |
215
+ | `identity.clear()` | Clears the linked identity and rotates the session. Call on logout. |
216
+ | `identity.get()` | Returns the current `IdentifyParams`, or `null` if no identity has been set. |
213
217
 
214
218
  Identity is automatically cleared when the SDK is closed. Call `identity.clear()` on logout to start a fresh anonymous session.
215
219
 
@@ -231,13 +235,13 @@ function App() {
231
235
 
232
236
  ### Consent categories
233
237
 
234
- | Category | Plugins | Gated? |
235
- | --- | --- | --- |
236
- | `necessary` | Error tracking | Always on |
237
- | `analytics` | Page events, rage clicks, fingerprint | Yes |
238
- | `replay` | Session replay | Yes |
238
+ | Category | Plugins | Gated? |
239
+ | ----------- | ------------------------------------- | --------- |
240
+ | `necessary` | Error tracking | Always on |
241
+ | `analytics` | Page events, rage clicks, fingerprint | Yes |
242
+ | `replay` | Session replay | Yes |
239
243
 
240
- Omitting the `consent` prop disables gating entirely (all features load). Passing it enables gating — only `necessary` plugins plus explicitly consented categories will activate.
244
+ Omitting the `consent` prop preserves consent supplied to `init()`, including later updates through `consent.set()`. If no initial consent was supplied, omitting the prop allows all features. Passing the prop enables only `necessary` plugins and explicitly consented categories.
241
245
 
242
246
  ### Imperative API
243
247
 
@@ -247,8 +251,8 @@ Use `consent.set()` and `consent.get()` from the `useInterfere` hook:
247
251
  const { consent } = useInterfere();
248
252
 
249
253
  consent.set({ analytics: true, replay: true }); // selective
250
- consent.set(); // grant all
251
- consent.get(); // current state, or null if no gating
254
+ consent.set(); // grant all
255
+ consent.get(); // current state, or null if no gating
252
256
  ```
253
257
 
254
258
  ### Initial consent via init
package/dist/init.d.mts CHANGED
@@ -1,15 +1,15 @@
1
1
  import { capture, span } from "@interfere/react/api";
2
- import { Kernel, Kernel as Kernel$1, KernelOptions, KernelOptions as KernelOptions$1 } from "@interfere/react/internal/kernel";
2
+ import { Kernel, KernelOptions, KernelOptions as KernelOptions$1 } from "@interfere/react/internal/kernel";
3
3
  import { ConsentCategory, ConsentState, GateableCategory } from "@interfere/types/sdk/plugins/manifest";
4
4
  //#region src/init.d.ts
5
- declare function init(opts?: KernelOptions$1): Promise<Kernel$1 | null>;
6
- declare const close: () => Promise<void>, consent: {
5
+ export declare function init(opts?: KernelOptions$1): Promise<import("@interfere/react/internal/kernel").Kernel | null>;
6
+ export declare const close: () => Promise<void>, consent: {
7
7
  get: () => import("@interfere/types/sdk/plugins/manifest").ConsentState | null;
8
8
  set: (value?: import("@interfere/types/sdk/plugins/manifest").ConsentState) => void;
9
- }, getKernel: () => Kernel$1, getKernelOrNull: () => Kernel$1 | null, identity: {
9
+ }, getKernel: () => import("@interfere/react/internal/kernel").Kernel, getKernelOrNull: () => import("@interfere/react/internal/kernel").Kernel | null, identity: {
10
10
  clear: () => void;
11
11
  get: () => import("@interfere/types/sdk/identify").IdentifyParams | null;
12
12
  set: (params: import("@interfere/types/sdk/identify").IdentifyParams) => Promise<void>;
13
13
  }, subscribeToKernel: (listener: () => void) => () => void;
14
14
  //#endregion
15
- export { type ConsentCategory, type ConsentState, type GateableCategory, type Kernel, type KernelOptions, capture, close, consent, getKernel, getKernelOrNull, identity, init, span, subscribeToKernel };
15
+ export { type ConsentCategory, type ConsentState, type GateableCategory, type Kernel, type KernelOptions, capture, span };
package/dist/init.mjs CHANGED
@@ -1 +1 @@
1
- import{applyBrowserGlobals}from"./internal/browser-globals.mjs";import{PRODUCER_VERSION}from"./version.mjs";import{createWrapperSingleton}from"@interfere/react/internal/wrapper-singleton";import{capture,span}from"@interfere/react/api";const wrapper=createWrapperSingleton({producerVersion:PRODUCER_VERSION,initEntryName:`Vite`});function init(opts){return applyBrowserGlobals(),wrapper.init(opts)}const{close,consent,getKernel,getKernelOrNull,identity,subscribeToKernel}=wrapper;export{capture,close,consent,getKernel,getKernelOrNull,identity,init,span,subscribeToKernel};
1
+ import{applyBrowserGlobals}from"./internal/browser-globals.mjs";import{PRODUCER_VERSION}from"./version.mjs";import{createWrapperSingleton}from"@interfere/react/internal/wrapper-singleton";import{capture,span}from"@interfere/react/api";const wrapper=createWrapperSingleton({initEntryName:`Vite`,producerVersion:PRODUCER_VERSION});async function init(opts){return applyBrowserGlobals(),wrapper.init(opts)}const{close,consent,getKernel,getKernelOrNull,identity,subscribeToKernel}=wrapper;export{capture,close,consent,getKernel,getKernelOrNull,identity,init,span,subscribeToKernel};
@@ -1,4 +1,3 @@
1
1
  //#region src/internal/browser-globals.d.ts
2
- declare function applyBrowserGlobals(): void;
3
- //#endregion
4
- export { applyBrowserGlobals };
2
+ export declare function applyBrowserGlobals(): void;
3
+ //#endregion
@@ -0,0 +1,62 @@
1
+ import { createBuildConfigResolver } from "@interfere/build/config";
2
+ import { ReleaseSlug } from "@interfere/types/releases/slug";
3
+ import { ResolvedConfig } from "vite";
4
+ //#region src/internal/build/build-receipt.d.ts
5
+ type ConfigObservation = Parameters<NonNullable<Parameters<typeof createBuildConfigResolver>[0]>>[0];
6
+ export declare function createBuildReceipt(filename: string): {
7
+ observeConfig(this: void, observation: ConfigObservation): void;
8
+ configure(resolved: ResolvedConfig, build: NonNullable<{
9
+ commitSha: string | null;
10
+ releaseSlug: ReleaseSlug | null;
11
+ environment: string | null;
12
+ publicKey: string | undefined;
13
+ apiOrigin: string;
14
+ producerVersion: string;
15
+ } | null>): Promise<void>;
16
+ recordOutput(directory: string, files: string[]): Promise<void>;
17
+ recordPublication(source: NonNullable<{
18
+ releaseSlug: `rel_${string}` & import("zod").$brand<"ReleaseSlug">;
19
+ buildId: string;
20
+ environment: string | null;
21
+ sessionId: string | null;
22
+ batchDigest: string;
23
+ status: {
24
+ releaseSlug: `rel_${string}` & import("zod").$brand<"ReleaseSlug">;
25
+ environment: string | null;
26
+ sessionId: string | null;
27
+ state: "absent" | "accepted" | "expired" | "expiring" | "ready" | "uploading";
28
+ fileCount: number;
29
+ publishedFileCount: number;
30
+ batchDigest: string | null;
31
+ manifestReady: boolean;
32
+ diagnostics?: {
33
+ attempt: number;
34
+ scheduledAtMs: number | null;
35
+ startedAtMs: number;
36
+ completedAtMs: number | null;
37
+ phase: "complete" | "completion" | "continuation" | "files" | "manifest" | "release";
38
+ publishedAtStart: number;
39
+ progressObservedAtMs: number | null;
40
+ nativeRetryCount: number;
41
+ interruptedAttempts: number;
42
+ longestAttempt: {
43
+ attempt: number;
44
+ durationMs: number;
45
+ } | null;
46
+ longestAlarmDelay: {
47
+ attempt: number;
48
+ durationMs: number;
49
+ } | null;
50
+ nextAlarmAtMs: number | null;
51
+ lastFailure: {
52
+ attempt: number;
53
+ atMs: number;
54
+ phase: "complete" | "completion" | "continuation" | "files" | "manifest" | "release";
55
+ kind: "aborted" | "error" | "non-error" | "timeout";
56
+ } | null;
57
+ } | undefined;
58
+ } | null;
59
+ } | null>): void;
60
+ write(): Promise<void>;
61
+ };
62
+ //#endregion
@@ -0,0 +1 @@
1
+ import{canonicalBuildPath}from"./canonical-build-path.mjs";import{hash}from"node:crypto";import path from"node:path";import{mkdir,readFile,rm,writeFile}from"node:fs/promises";function contains(directory,file){let relative=path.relative(directory,file);return relative===``||relative!==`..`&&!relative.startsWith(`..${path.sep}`)&&!path.isAbsolute(relative)}function createBuildReceipt(filename){let destination=null,root=``,config=null,identity=null,publication=null,outputs=new Set,directories=new Set;function assertPrivate(file){if([...directories].some(directory=>contains(directory,file)))throw Error(`Build receipt must be outside public and build output directories`)}return{observeConfig(observation){config=observation},async configure(resolved,build){root=await canonicalBuildPath(resolved.root),identity=build,directories.add(await canonicalBuildPath(path.resolve(root,resolved.build.outDir))),resolved.publicDir&&directories.add(await canonicalBuildPath(resolved.publicDir));let next=await canonicalBuildPath(path.resolve(root,filename));if(assertPrivate(next),!contains(root,next))throw Error(`Build receipt must be inside the project directory`);destination!==next&&await rm(next,{force:!0}),destination=next},async recordOutput(directory,files){let absolute=await canonicalBuildPath(path.resolve(root,directory));directories.add(absolute),destination&&assertPrivate(destination);for(let file of files)file.endsWith(`.map`)||outputs.add(path.resolve(absolute,file))},recordPublication(source){publication=source},async write(){if(!destination||!identity)return;assertPrivate(destination);let assets=await Promise.all([...outputs].toSorted().map(async file=>({path:path.relative(root,file).split(path.sep).join(`/`),sha256:hash(`sha256`,await readFile(file))}))),receipt={schemaVersion:1,build:{...identity,buildId:identity.commitSha},config:config?{status:config.status,consumed:config.config,digest:config.status===`fetched`?hash(`sha256`,JSON.stringify(config.config)):null}:{status:`unavailable`,consumed:null,digest:null},source:publication,assets};await mkdir(path.dirname(destination),{recursive:!0}),await writeFile(destination,`${JSON.stringify(receipt,null,2)}\n`,{mode:384})}}}export{createBuildReceipt};
@@ -0,0 +1,3 @@
1
+ //#region src/internal/build/canonical-build-path.d.ts
2
+ export declare const canonicalBuildPath: (file: string) => Promise<string>;
3
+ //#endregion
@@ -0,0 +1 @@
1
+ import path from"node:path";import{realpath}from"node:fs/promises";const canonicalBuildPath=async file=>{let existing=await realpath(file).catch(()=>null);if(existing)return existing;let parent=path.dirname(file);return parent===file?file:path.join(await canonicalBuildPath(parent),path.basename(file))};export{canonicalBuildPath};
@@ -0,0 +1,10 @@
1
+ //#region src/internal/build/git.d.ts
2
+ /**
3
+ * Runs a git command and returns its stdout, or `null` when git isn't
4
+ * available. Build-time only — inject it into the shared resolvers
5
+ * (`resolveReleaseSlug`, `resolveGithubReleaseSource`) rather than
6
+ * importing it from runtime server code, which must never spawn
7
+ * subprocesses.
8
+ */
9
+ export declare function runGitCommand(command: string): string | null;
10
+ //#endregion
@@ -0,0 +1 @@
1
+ import{execSync}from"node:child_process";function runGitCommand(command){try{let output=execSync(command,{encoding:`utf8`,stdio:[`ignore`,`pipe`,`ignore`]}).trim();return output.length>0?output:null}catch{return null}}export{runGitCommand};
@@ -1,67 +1,93 @@
1
1
  import { DiscoveryResult } from "./source-maps.mjs";
2
+ import { Interfere, InterfereClient } from "@interfere/javascript";
2
3
  import { ReleaseSlug } from "@interfere/types/releases/slug";
3
- import { HTTPClient, Interfere } from "@interfere/sdk";
4
4
  import { ManifestBundler } from "@interfere/types/data/source-maps";
5
- import { ReleasesConfigResponse } from "@interfere/sdk/models/releases-config-response.js";
6
- import { CreateReleaseResponse } from "@interfere/types/releases/definition";
7
5
  //#region src/internal/build/pipeline.d.ts
8
- interface BuildTiming {
9
- cleanup: number;
10
- createRelease: number;
11
- fileCount: number;
12
- preflight: number;
13
- total: number;
14
- totalBytes: number;
15
- upload: number;
16
- }
17
- type ReleasePipelineFailure = {
18
- kind: "missing_source_provider";
19
- config: ReleasesConfigResponse;
20
- } | {
21
- kind: "missing_destination_provider";
22
- config: ReleasesConfigResponse;
23
- };
24
- type ReleasePipelineResult = {
25
- ok: true;
26
- data: ReleasePipelineSuccess;
27
- } | {
28
- ok: false;
29
- failure: ReleasePipelineFailure;
30
- };
31
- interface ReleasePipelineSuccess {
32
- buildId: string;
33
- cleanupFailures: {
34
- absolute: string;
35
- code: string | undefined;
36
- message: string;
37
- }[];
38
- config: ReleasesConfigResponse;
39
- fileCount: number;
40
- release: CreateReleaseResponse;
41
- releaseSlug: ReleaseSlug;
42
- timing: BuildTiming;
43
- }
44
- interface ReleasePipelineInput {
45
- apiKey: string;
46
- apiUrl: string;
6
+ export interface ReleasePipelineInput {
7
+ client: InterfereClient;
47
8
  buildId: string;
48
9
  bundler: ManifestBundler;
49
10
  discovery: DiscoveryResult;
50
11
  environment: string | null;
51
12
  producerVersion: string;
52
13
  releaseSlug: ReleaseSlug;
53
- releasesConfig: ReleasesConfigResponse;
14
+ releasesConfig: Interfere.ReleasesConfigResponse;
15
+ readinessTimeoutMs?: number | undefined;
16
+ capturePublication?: boolean;
54
17
  }
55
- declare function createSdkClient(opts: {
56
- apiKey: string;
57
- apiUrl: string;
58
- }): {
59
- httpClient: HTTPClient;
60
- sdk: Interfere;
61
- security: {
62
- apiKey: string;
18
+ export declare function runReleasePipeline(input: ReleasePipelineInput): Promise<{
19
+ failure: {
20
+ config: Interfere.ReleasesConfigResponse;
21
+ kind: "missing_destination_provider" | "missing_source_provider";
22
+ };
23
+ ok: false;
24
+ data?: never;
25
+ } | {
26
+ failure?: never;
27
+ data: {
28
+ cleanupFailures: {
29
+ absolute: string;
30
+ code: string | undefined;
31
+ message: string;
32
+ }[];
33
+ fileCount: number;
34
+ releaseSlug: `rel_${string}` & import("zod").$brand<"ReleaseSlug">;
35
+ publication: {
36
+ releaseSlug: `rel_${string}` & import("zod").$brand<"ReleaseSlug">;
37
+ buildId: string;
38
+ environment: string | null;
39
+ sessionId: string | null;
40
+ batchDigest: string;
41
+ status: {
42
+ releaseSlug: `rel_${string}` & import("zod").$brand<"ReleaseSlug">;
43
+ environment: string | null;
44
+ sessionId: string | null;
45
+ state: "absent" | "accepted" | "expired" | "expiring" | "ready" | "uploading";
46
+ fileCount: number;
47
+ publishedFileCount: number;
48
+ batchDigest: string | null;
49
+ manifestReady: boolean;
50
+ diagnostics?: {
51
+ attempt: number;
52
+ scheduledAtMs: number | null;
53
+ startedAtMs: number;
54
+ completedAtMs: number | null;
55
+ phase: "complete" | "completion" | "continuation" | "files" | "manifest" | "release";
56
+ publishedAtStart: number;
57
+ progressObservedAtMs: number | null;
58
+ nativeRetryCount: number;
59
+ interruptedAttempts: number;
60
+ longestAttempt: {
61
+ attempt: number;
62
+ durationMs: number;
63
+ } | null;
64
+ longestAlarmDelay: {
65
+ attempt: number;
66
+ durationMs: number;
67
+ } | null;
68
+ nextAlarmAtMs: number | null;
69
+ lastFailure: {
70
+ attempt: number;
71
+ atMs: number;
72
+ phase: "complete" | "completion" | "continuation" | "files" | "manifest" | "release";
73
+ kind: "aborted" | "error" | "non-error" | "timeout";
74
+ } | null;
75
+ } | undefined;
76
+ } | null;
77
+ } | undefined;
78
+ timing: {
79
+ sign: number;
80
+ session: number;
81
+ upload: number;
82
+ finalize: number;
83
+ totalBytes: number;
84
+ createRelease: number;
85
+ readiness: number;
86
+ receipt: number;
87
+ cleanup: number;
88
+ total: number;
89
+ };
63
90
  };
64
- };
65
- declare function runReleasePipeline(input: ReleasePipelineInput): Promise<ReleasePipelineResult>;
66
- //#endregion
67
- export { ReleasePipelineInput, ReleasePipelineResult, createSdkClient, runReleasePipeline };
91
+ ok: true;
92
+ }>;
93
+ //#endregion
@@ -1 +1 @@
1
- import{cleanupSourceMaps,uploadSourceMaps}from"./source-maps.mjs";import{releaseSlugSchema}from"@interfere/types/releases/slug";import{applyDeployEnvironment}from"@interfere/types/sdk/deploy-environment";import{execSync}from"node:child_process";import{HTTPClient,Interfere}from"@interfere/sdk";import{configuredSourceProviderSchema,releaseDestinationProviderSchema}from"@interfere/types/integrations";import{resolveCliReleaseDestination}from"@interfere/types/sdk/cli-destination";import{parseEnvValue}from"@interfere/types/sdk/env";import{resolveGithubReleaseSource}from"@interfere/types/sdk/github-source";const sources={github:resolveGithubSource,ghe_cloud:resolveGithubSource,ghe_server:resolveGithubSource},destinations={vercel:resolveVercelDestination,cloudflare:resolveCloudflareDestination,cli:resolveCliDestination};async function timed(fn){let start=performance.now();return[await fn(),Math.round(performance.now()-start)]}function createSdkClient(opts){let httpClient=new HTTPClient;return httpClient.addHook(`beforeRequest`,request=>{let next=new Request(request);return next.headers.set(`Authorization`,`Bearer ${opts.apiKey}`),next}),{httpClient,sdk:new Interfere({serverURL:opts.apiUrl,httpClient}),security:{apiKey:opts.apiKey}}}function resolveReleaseRequest(buildId,environment,releaseSlug,producerVersion,config){let sourceProvider=configuredSourceProviderSchema.safeParse(config.surface.sourceProvider);if(!sourceProvider.success)return{ok:!1,reason:`missing_source_provider`};let destinationProvider=releaseDestinationProviderSchema.safeParse(config.surface.destinationProvider);return destinationProvider.success?{ok:!0,request:{source:sources[sourceProvider.data](),destination:applyDeployEnvironment(destinations[destinationProvider.data](releaseSlug),environment),buildId,slug:releaseSlug,producerVersion}}:{ok:!1,reason:`missing_destination_provider`}}async function runReleasePipeline(input){let{apiKey,apiUrl,bundler,discovery,releasesConfig}=input,releaseRequest=resolveReleaseRequest(input.buildId,input.environment,input.releaseSlug,input.producerVersion,releasesConfig);if(!releaseRequest.ok)return{ok:!1,failure:{kind:releaseRequest.reason,config:releasesConfig}};let start=performance.now(),{httpClient,sdk,security}=createSdkClient({apiKey,apiUrl}),[release,createReleaseMs]=await timed(()=>sdk.releases.create(security,releaseRequest.request)),releaseSlug=releaseSlugSchema.parse(release.destination.slug),buildId=release.build.hash??input.buildId,[{totalBytes},uploadMs]=await timed(()=>uploadSourceMaps({apiUrl,bundler,discovered:discovery,httpClient,releaseSlug})),[,preflightMs]=await timed(()=>sdk.releases.preflight(security,{releaseSlug})),[{failures},cleanupMs]=await timed(()=>cleanupSourceMaps(discovery.files));return{ok:!0,data:{buildId,cleanupFailures:failures,config:releasesConfig,fileCount:discovery.files.length,release:{...release,destination:{...release.destination,slug:releaseSlug}},releaseSlug,timing:{createRelease:createReleaseMs,upload:uploadMs,preflight:preflightMs,cleanup:cleanupMs,total:Math.round(performance.now()-start),fileCount:discovery.files.length,totalBytes}}}}function runGitCommand(command){try{let output=execSync(command,{encoding:`utf8`,stdio:[`ignore`,`pipe`,`ignore`]}).trim();return output.length>0?output:null}catch{return null}}function resolveGithubSource(){return resolveGithubReleaseSource(process.env,runGitCommand)}function resolveVercelDestination(){let environment=parseEnvValue(process.env.VERCEL_ENV??process.env.VERCEL_TARGET_ENV),deploymentId=parseEnvValue(process.env.VERCEL_DEPLOYMENT_ID);return{provider:`vercel`,destinationReleaseId:deploymentId,environment,deploymentId,deploymentUrl:resolveDeploymentUrl(),environmentName:environment,environmentTarget:environment}}function resolveDeploymentUrl(){let deploymentUrl=parseEnvValue(process.env.VERCEL_URL);return deploymentUrl===null?null:deploymentUrl.startsWith(`https://`)||deploymentUrl.startsWith(`http://`)?deploymentUrl:`https://${deploymentUrl}`}function resolveCloudflareDestination(){let raw=parseEnvValue(process.env.CF_VERSION_METADATA),versionMetadata=null;if(raw)try{versionMetadata=JSON.parse(raw)}catch{versionMetadata=null}let versionId=parseEnvValue(process.env.CF_VERSION_ID)??versionMetadata?.id??parseEnvValue(process.env.WORKERS_CI_BUILD_UUID)??null,scriptName=parseEnvValue(process.env.CF_WORKER_NAME)??parseEnvValue(process.env.WORKERS_CI_SCRIPT_NAME)??null,accountId=parseEnvValue(process.env.CF_ACCOUNT_ID)??parseEnvValue(process.env.CLOUDFLARE_ACCOUNT_ID)??null,environment=parseEnvValue(process.env.WORKERS_CI_BRANCH)??versionMetadata?.tag??null;return{provider:`cloudflare`,destinationReleaseId:versionId,environment,versionId,scriptName,accountId,deploymentUrl:resolveCloudflareDeploymentUrl(),environmentName:environment,environmentTarget:environment}}function resolveCloudflareDeploymentUrl(){let candidate=parseEnvValue(process.env.CF_PAGES_URL)??parseEnvValue(process.env.WORKERS_CI_DEPLOY_URL);return candidate===null?null:candidate.startsWith(`https://`)||candidate.startsWith(`http://`)?candidate:`https://${candidate}`}function resolveCliDestination(releaseSlug){return resolveCliReleaseDestination(process.env,releaseSlug)}export{createSdkClient,runReleasePipeline};
1
+ import{readPublicationStatus}from"./read-publication-status.mjs";import{transferSourceMaps}from"./transfer-source-maps.mjs";import{waitForSourceMaps}from"./wait-for-source-maps.mjs";import{sourceMapBatchDigest}from"@interfere/types/data/source-map-batch";import{cleanupSourceMaps}from"@interfere/build/cleanup-source-maps";import{resolveReleaseRequest}from"@interfere/build/release-request";import{createReleaseResponse}from"@interfere/types/releases/definition";async function runReleasePipeline(input){let{client,discovery,releasesConfig}=input,{files}=discovery,releaseRequest=resolveReleaseRequest({...input,config:releasesConfig});if(!releaseRequest.ok)return{failure:{config:releasesConfig,kind:releaseRequest.reason},ok:!1};if(files.length===0)throw Error(`Source maps are required to prepare a release`);let start=performance.now(),release=createReleaseResponse.parse(await client.releases.createRelease(releaseRequest.request,{abortSignal:AbortSignal.timeout(6e4)})),createdAt=performance.now(),releaseSlug=release.destination.slug,transfer=await transferSourceMaps({...input,releaseSlug}),{completion,session,acceptedAt}=transfer;input.readinessTimeoutMs!==void 0&&session?.sessionId&&await waitForSourceMaps({client,releaseSlug,environment:input.environment,sessionId:session.sessionId,batchDigest:await sourceMapBatchDigest(completion),fileCount:files.length,timeoutMs:input.readinessTimeoutMs});let readyAt=performance.now(),publication=input.capturePublication?{releaseSlug,buildId:release.build.hash,environment:input.environment,sessionId:session?.releaseSlug===releaseSlug&&session.environment===input.environment?session.sessionId:null,batchDigest:await sourceMapBatchDigest(completion),status:await readPublicationStatus({...input,releaseSlug})}:void 0,publicationReadAt=performance.now();return{data:{cleanupFailures:await cleanupSourceMaps(files),fileCount:files.length,releaseSlug,publication,timing:{createRelease:Math.round(createdAt-start),...transfer.timing,readiness:Math.round(readyAt-acceptedAt),receipt:Math.round(publicationReadAt-readyAt),cleanup:Math.round(performance.now()-publicationReadAt),total:Math.round(performance.now()-start)}},ok:!0}}export{runReleasePipeline};
@@ -0,0 +1,39 @@
1
+ import { ReleasePipelineInput } from "./pipeline.mjs";
2
+ //#region src/internal/build/read-publication-status.d.ts
3
+ export declare function readPublicationStatus(input: Pick<ReleasePipelineInput, "client" | "releaseSlug" | "environment">): Promise<{
4
+ releaseSlug: `rel_${string}` & import("zod").$brand<"ReleaseSlug">;
5
+ environment: string | null;
6
+ sessionId: string | null;
7
+ state: "absent" | "accepted" | "expired" | "expiring" | "ready" | "uploading";
8
+ fileCount: number;
9
+ publishedFileCount: number;
10
+ batchDigest: string | null;
11
+ manifestReady: boolean;
12
+ diagnostics?: {
13
+ attempt: number;
14
+ scheduledAtMs: number | null;
15
+ startedAtMs: number;
16
+ completedAtMs: number | null;
17
+ phase: "complete" | "completion" | "continuation" | "files" | "manifest" | "release";
18
+ publishedAtStart: number;
19
+ progressObservedAtMs: number | null;
20
+ nativeRetryCount: number;
21
+ interruptedAttempts: number;
22
+ longestAttempt: {
23
+ attempt: number;
24
+ durationMs: number;
25
+ } | null;
26
+ longestAlarmDelay: {
27
+ attempt: number;
28
+ durationMs: number;
29
+ } | null;
30
+ nextAlarmAtMs: number | null;
31
+ lastFailure: {
32
+ attempt: number;
33
+ atMs: number;
34
+ phase: "complete" | "completion" | "continuation" | "files" | "manifest" | "release";
35
+ kind: "aborted" | "error" | "non-error" | "timeout";
36
+ } | null;
37
+ } | undefined;
38
+ } | null>;
39
+ //#endregion
@@ -0,0 +1 @@
1
+ import{sourceMapPublicationStatus}from"@interfere/types/data/source-map-status";async function readPublicationStatus(input){try{return sourceMapPublicationStatus.parse(await input.client.releases.getSourceMapPublicationStatus({releaseSlug:input.releaseSlug,environment:input.environment??void 0},{abortSignal:AbortSignal.timeout(1e4)}))}catch{return null}}export{readPublicationStatus};
@@ -0,0 +1,6 @@
1
+ //#region src/internal/build/render-debug-id.d.ts
2
+ export declare function renderDebugId(code: string): {
3
+ code: string;
4
+ map: null;
5
+ };
6
+ //#endregion
@@ -0,0 +1 @@
1
+ import{createHash}from"node:crypto";import{injectDebugIdIntoJs}from"@interfere/types/data/source-maps";function renderDebugId(code){let hash=createHash(`sha256`).update(code).digest(`hex`),debugId=[hash.slice(0,8),hash.slice(8,12),`8${hash.slice(13,16)}`,`8${hash.slice(17,20)}`,hash.slice(20,32)].join(`-`);return{code:injectDebugIdIntoJs(code,debugId),map:null}}export{renderDebugId};
@@ -1,16 +1,15 @@
1
- import { ReleaseSlug } from "@interfere/types/releases/slug";
2
- import { HTTPClient } from "@interfere/sdk";
3
- import { ManifestBundler, injectDebugIdIntoJs } from "@interfere/types/data/source-maps";
1
+ import { injectDebugIdIntoJs } from "@interfere/types/data/source-maps";
4
2
  //#region src/internal/build/source-maps.d.ts
5
3
  interface SourceMapFile {
6
4
  readonly absolute: string;
7
5
  readonly chunkUrl: string;
6
+ readonly chunkAbsolutes: string[];
8
7
  readonly content: string;
9
8
  readonly debugId: string;
10
9
  readonly hash: string;
11
10
  readonly path: string;
12
11
  }
13
- interface DiscoveryResult {
12
+ export interface DiscoveryResult {
14
13
  readonly files: SourceMapFile[];
15
14
  readonly sourceFileCount: number;
16
15
  }
@@ -18,32 +17,15 @@ interface PublicPathRewrite {
18
17
  from: string;
19
18
  to: string;
20
19
  }
21
- interface DiscoverByPairingOptions {
20
+ export interface DiscoverByPairingOptions {
22
21
  distDir: string;
23
22
  projectDir: string;
24
23
  publicPathRewrites: readonly PublicPathRewrite[];
25
24
  subtrees: readonly string[];
26
25
  }
27
- interface CleanupFailure {
28
- readonly absolute: string;
29
- readonly code: string | undefined;
30
- readonly message: string;
31
- }
32
- interface UploadParams {
33
- apiUrl: string;
34
- bundler: ManifestBundler;
35
- discovered: DiscoveryResult;
36
- httpClient: HTTPClient;
37
- releaseSlug: ReleaseSlug;
38
- }
39
- interface UploadResult {
40
- fileCount: number;
41
- totalBytes: number;
42
- }
43
- declare function discoverByPairing(opts: DiscoverByPairingOptions): Promise<DiscoveryResult>;
44
- declare function cleanupSourceMaps(files: readonly SourceMapFile[]): Promise<{
45
- failures: CleanupFailure[];
26
+ export declare function discoverByPairing(opts: DiscoverByPairingOptions): Promise<{
27
+ files: SourceMapFile[];
28
+ sourceFileCount: number;
46
29
  }>;
47
- declare function uploadSourceMaps({ apiUrl, bundler, discovered, httpClient, releaseSlug }: UploadParams): Promise<UploadResult>;
48
30
  //#endregion
49
- export { CleanupFailure, DiscoverByPairingOptions, DiscoveryResult, SourceMapFile, UploadResult, cleanupSourceMaps, discoverByPairing, injectDebugIdIntoJs, uploadSourceMaps };
31
+ export { injectDebugIdIntoJs };
@@ -1 +1 @@
1
- import{isAbsolute,join,relative,resolve}from"node:path";import{createHash,randomUUID}from"node:crypto";import{readFile,readdir,unlink,writeFile}from"node:fs/promises";import{injectDebugIdIntoJs,injectDebugIdIntoJs as injectDebugIdIntoJs$1,readDebugIdFromJs}from"@interfere/types/data/source-maps";const LEADING_DOT_SLASH_RE=/^(\.\/)+/,SOURCEMAPPING_RE=/\/\/[#@]\s*sourceMappingURL=(\S+)\s*$/,SCRIPT_SUFFIXES=[`.js`,`.mjs`,`.cjs`];function normalizeDistDir(distDir){return distDir.replaceAll(`\\`,`/`).replace(LEADING_DOT_SLASH_RE,``)}function resolveDistDir(projectDir,distDir){return isAbsolute(distDir)?distDir:resolve(projectDir,distDir)}async function walkDistTrees(absDistDir,suffixes,subtrees){return(await Promise.all(subtrees.map(async subtree=>{let root=subtree===`.`?absDistDir:join(absDistDir,subtree),entries=await readdir(root,{recursive:!0}).catch(()=>[]),out=[];for(let entry of entries)typeof entry==`string`&&suffixes.some(suffix=>entry.endsWith(suffix))&&out.push(join(root,entry));return out}))).flat()}function toPublicPath(rel,rewrites){let normalized=rel.replaceAll(`\\`,`/`);for(let{from,to}of rewrites){let prefix=from.endsWith(`/`)?from:`${from}/`;if(normalized.startsWith(prefix))return`${to}${normalized.slice(prefix.length)}`}return normalized}function injectDebugIdIntoMap(content,debugId){let json=JSON.parse(content);return json.debugId=debugId,JSON.stringify(json)}async function discoverByPairing(opts){let absDistDir=resolveDistDir(opts.projectDir,normalizeDistDir(opts.distDir)),jsPaths=await walkDistTrees(absDistDir,SCRIPT_SUFFIXES,opts.subtrees),pairs=new Map;for(let jsAbs of jsPaths){let content=await readFile(jsAbs,`utf8`),match=SOURCEMAPPING_RE.exec(content);if(!match?.[1])continue;let mapAbs=`${jsAbs.slice(0,jsAbs.lastIndexOf(`/`)+1)}${decodeURIComponent(match[1])}`,jsPublic=toPublicPath(relative(absDistDir,jsAbs),opts.publicPathRewrites),mapPublic=toPublicPath(relative(absDistDir,mapAbs),opts.publicPathRewrites);pairs.set(mapPublic,{existingDebugId:readDebugIdFromJs(content),jsAbs,jsContent:content,jsPublic,mapAbs})}return{files:(await Promise.all(Array.from(pairs.entries(),async([mapPublic,pair])=>{let mapContent=await readFile(pair.mapAbs,`utf8`).catch(()=>null);if(mapContent===null)return null;let debugId=pair.existingDebugId??randomUUID();pair.existingDebugId||await writeFile(pair.jsAbs,injectDebugIdIntoJs$1(pair.jsContent,debugId),`utf8`);let finalMap=JSON.parse(mapContent).debugId===debugId?mapContent:injectDebugIdIntoMap(mapContent,debugId);return finalMap!==mapContent&&await writeFile(pair.mapAbs,finalMap,`utf8`),{absolute:pair.mapAbs,path:mapPublic,content:finalMap,hash:createHash(`sha256`).update(finalMap).digest(`hex`),debugId,chunkUrl:pair.jsPublic}}))).filter(file=>file!==null),sourceFileCount:pairs.size}}async function cleanupSourceMaps(files){let results=await Promise.allSettled(files.map(file=>unlink(file.absolute))),failures=[];for(let[index,result]of results.entries()){if(result.status!==`rejected`)continue;let error=result.reason;if(error.code===`ENOENT`)continue;let file=files[index];failures.push({absolute:file?.absolute??`<unknown>`,code:error.code,message:error.message??String(error)})}return{failures}}async function uploadSourceMaps({apiUrl,bundler,discovered,httpClient,releaseSlug}){let filesByPath=new Map(discovered.files.map(file=>[file.path,file])),signRequest={files:discovered.files.map(file=>({path:file.path,sizeBytes:byteLengthOf(file.content),...extractRichness(file.content)}))},signResponse=await postJson(httpClient,new URL(`/v1/releases/${encodeURIComponent(releaseSlug)}/source-maps/sign`,apiUrl),signRequest),totalBytes=0;await mapWithConcurrency(signResponse.uploads,32,async upload=>{let file=filesByPath.get(upload.path);if(!file)throw Error(`Sign response referenced unknown path "${upload.path}"`);totalBytes+=byteLengthOf(file.content),await putToR2(upload.presignedUrl,file.content)});let completeRequest={files:discovered.files.map(file=>({path:file.path,hash:file.hash,debugId:file.debugId,chunkUrl:file.chunkUrl})),sourceFileCount:discovered.sourceFileCount,bundler};return{fileCount:(await postJson(httpClient,new URL(`/v1/releases/${encodeURIComponent(releaseSlug)}/source-maps/complete`,apiUrl),completeRequest)).fileCount,totalBytes}}function byteLengthOf(content){return new TextEncoder().encode(content).byteLength}const EMPTY_RICHNESS={hasSourcesContent:!1,hasNames:!1,hasFile:!1,mappingsPresent:!1};function leafRichness(map){let sourcesContent=map.sourcesContent,names=map.names,file=map.file,mappings=map.mappings;return{hasSourcesContent:Array.isArray(sourcesContent)&&sourcesContent.length>0,hasNames:Array.isArray(names)&&names.length>0,hasFile:typeof file==`string`&&file.length>0,mappingsPresent:typeof mappings==`string`&&mappings.length>0}}function extractRichness(content){let parsed;try{parsed=JSON.parse(content)}catch{return EMPTY_RICHNESS}if(!(parsed&&typeof parsed==`object`))return EMPTY_RICHNESS;let top=parsed,sections=top.sections;if(!Array.isArray(sections))return leafRichness(top);let merged={hasSourcesContent:!1,hasNames:!1,hasFile:typeof top.file==`string`&&top.file.length>0,mappingsPresent:!1};for(let section of sections){let inner=section?.map;if(!(inner&&typeof inner==`object`))continue;let richness=leafRichness(inner);merged.hasSourcesContent||=richness.hasSourcesContent,merged.hasNames||=richness.hasNames,merged.mappingsPresent||=richness.mappingsPresent}return merged}async function postJson(httpClient,url,body){let response=await httpClient.request(new Request(url,{method:`POST`,headers:{"content-type":`application/json`,accept:`application/json`},body:JSON.stringify(body)}));if(!response.ok){let detail=await response.text().catch(()=>``);throw Error(`Source-map API request failed: POST ${url.pathname} -> ${response.status} ${detail}`)}return await response.json()}async function putToR2(presignedUrl,content){let response=await fetch(presignedUrl,{method:`PUT`,headers:{"content-type":`application/json`},body:content});if(!response.ok){let detail=await response.text().catch(()=>``);throw Error(`Release metadata file upload failed: ${response.status} ${detail}`)}}async function mapWithConcurrency(items,concurrency,fn){let cursor=0;async function worker(){for(;;){let index=cursor++;if(index>=items.length)return;let item=items[index];if(item===void 0)return;await fn(item,index)}}await Promise.all(Array.from({length:Math.min(concurrency,items.length)},()=>worker()))}export{cleanupSourceMaps,discoverByPairing,injectDebugIdIntoJs,uploadSourceMaps};
1
+ import{createHash,randomUUID}from"node:crypto";import{isAbsolute,join,relative,resolve}from"node:path";import{readFile,readdir,writeFile}from"node:fs/promises";import{injectDebugIdIntoJs,injectDebugIdIntoJs as injectDebugIdIntoJs$1,readDebugIdFromJs}from"@interfere/types/data/source-maps";import{mapWithConcurrency}from"@interfere/helpers/map-with-concurrency";const LEADING_DOT_SLASH_RE=/^(?:\.\/)+/u,SOURCEMAPPING_RE=/\/\/[#@]\s*sourceMappingURL=(?<sourceMap>\S+)\s*$/u,RUNTIME_DEBUG_ID_RE=/_debugIds\[[^\]]+\]\s*=\s*["'`](?<id>[a-fA-F0-9-]{36})["'`]/gu,SCRIPT_SUFFIXES=[`.js`,`.mjs`,`.cjs`];function normalizeDistDir(distDir){return distDir.replaceAll(`\\`,`/`).replace(LEADING_DOT_SLASH_RE,``)}function resolveDistDir(projectDir,distDir){return isAbsolute(distDir)?distDir:resolve(projectDir,distDir)}async function walkDistTrees(absDistDir,suffixes,subtrees){return(await Promise.all(subtrees.map(async subtree=>{let root=subtree===`.`?absDistDir:join(absDistDir,subtree),entries=await readdir(root,{recursive:!0}).catch(()=>[]),out=[];for(let entry of entries)suffixes.some(suffix=>entry.endsWith(suffix))&&out.push(join(root,entry));return out}))).flat()}function toPublicPath(rel,rewrites){let normalized=rel.replaceAll(`\\`,`/`);for(let{from,to}of rewrites){let prefix=from.endsWith(`/`)?from:`${from}/`;if(normalized.startsWith(prefix))return`${to}${normalized.slice(prefix.length)}`}return normalized}function injectDebugIdIntoMap(content,debugId){let json=JSON.parse(content);if(typeof json!=`object`||!json||Array.isArray(json))throw TypeError(`Source map must be a JSON object`);return Reflect.set(json,`debugId`,debugId),JSON.stringify(json)}async function discoverByPairing(opts){let absDistDir=resolveDistDir(opts.projectDir,normalizeDistDir(opts.distDir)),jsPaths=await walkDistTrees(absDistDir,SCRIPT_SUFFIXES,opts.subtrees),discovered=await mapWithConcurrency(jsPaths,16,async jsAbs=>{let content=await readFile(jsAbs,`utf8`),match=SOURCEMAPPING_RE.exec(content);if(!match?.groups?.sourceMap)return null;let mapAbs=`${jsAbs.slice(0,jsAbs.lastIndexOf(`/`)+1)}${decodeURIComponent(match.groups.sourceMap)}`,jsPublic=toPublicPath(relative(absDistDir,jsAbs),opts.publicPathRewrites);return[toPublicPath(relative(absDistDir,mapAbs),opts.publicPathRewrites),{existingDebugId:readDebugIdFromJs(content)??[...content.matchAll(RUNTIME_DEBUG_ID_RE)].at(-1)?.groups?.id,jsAbs,jsContent:content,jsPublic,mapAbs}]}),pairs=new Map;for(let[mapPublic,pair]of discovered.filter(entry=>entry!==null)){let chunkAbsolutes=pairs.get(mapPublic)?.chunkAbsolutes??[];pairs.set(mapPublic,{...pair,chunkAbsolutes:[...chunkAbsolutes,pair.jsAbs]})}return{files:(await mapWithConcurrency([...pairs],16,async([mapPublic,pair])=>{let mapContent=await readFile(pair.mapAbs,`utf8`).catch(()=>null);if(mapContent===null)return null;let debugId=pair.existingDebugId??randomUUID(),mapJson=JSON.parse(mapContent),finalMap=typeof mapJson==`object`&&mapJson&&`debugId`in mapJson&&mapJson.debugId===debugId?mapContent:injectDebugIdIntoMap(mapContent,debugId);return pair.existingDebugId||await writeFile(pair.jsAbs,injectDebugIdIntoJs$1(pair.jsContent,debugId),`utf8`),finalMap!==mapContent&&await writeFile(pair.mapAbs,finalMap,`utf8`),{absolute:pair.mapAbs,chunkAbsolutes:pair.chunkAbsolutes,chunkUrl:pair.jsPublic,content:finalMap,debugId,hash:createHash(`sha256`).update(finalMap).digest(`hex`),path:mapPublic}})).filter(file=>file!==null),sourceFileCount:pairs.size}}export{discoverByPairing,injectDebugIdIntoJs};
@@ -0,0 +1,59 @@
1
+ import { ReleasePipelineInput } from "./pipeline.mjs";
2
+ //#region src/internal/build/transfer-source-maps.d.ts
3
+ export declare function transferSourceMaps(input: Pick<ReleasePipelineInput, "client" | "bundler" | "discovery" | "environment" | "releaseSlug" | "readinessTimeoutMs" | "capturePublication">): Promise<{
4
+ completion: {
5
+ bundler: "rollup" | "tsc" | "turbopack" | "webpack";
6
+ sourceFileCount: number;
7
+ files: {
8
+ path: string;
9
+ hash: string;
10
+ debugId: string;
11
+ chunkUrl: string;
12
+ }[];
13
+ };
14
+ session: {
15
+ releaseSlug: `rel_${string}` & import("zod").$brand<"ReleaseSlug">;
16
+ environment: string | null;
17
+ sessionId: string | null;
18
+ state: "absent" | "accepted" | "expired" | "expiring" | "ready" | "uploading";
19
+ fileCount: number;
20
+ publishedFileCount: number;
21
+ batchDigest: string | null;
22
+ manifestReady: boolean;
23
+ diagnostics?: {
24
+ attempt: number;
25
+ scheduledAtMs: number | null;
26
+ startedAtMs: number;
27
+ completedAtMs: number | null;
28
+ phase: "complete" | "completion" | "continuation" | "files" | "manifest" | "release";
29
+ publishedAtStart: number;
30
+ progressObservedAtMs: number | null;
31
+ nativeRetryCount: number;
32
+ interruptedAttempts: number;
33
+ longestAttempt: {
34
+ attempt: number;
35
+ durationMs: number;
36
+ } | null;
37
+ longestAlarmDelay: {
38
+ attempt: number;
39
+ durationMs: number;
40
+ } | null;
41
+ nextAlarmAtMs: number | null;
42
+ lastFailure: {
43
+ attempt: number;
44
+ atMs: number;
45
+ phase: "complete" | "completion" | "continuation" | "files" | "manifest" | "release";
46
+ kind: "aborted" | "error" | "non-error" | "timeout";
47
+ } | null;
48
+ } | undefined;
49
+ } | null;
50
+ acceptedAt: number;
51
+ timing: {
52
+ sign: number;
53
+ session: number;
54
+ upload: number;
55
+ finalize: number;
56
+ totalBytes: number;
57
+ };
58
+ }>;
59
+ //#endregion