@interfere/vite 11.2.1 → 12.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -33
- package/dist/init.d.mts +5 -5
- package/dist/init.mjs +1 -1
- package/dist/internal/browser-globals.d.mts +2 -3
- package/dist/internal/build/build-receipt.d.mts +62 -0
- package/dist/internal/build/build-receipt.mjs +1 -0
- package/dist/internal/build/canonical-build-path.d.mts +3 -0
- package/dist/internal/build/canonical-build-path.mjs +1 -0
- package/dist/internal/build/git.d.mts +2 -3
- package/dist/internal/build/git.mjs +1 -1
- package/dist/internal/build/pipeline.d.mts +81 -55
- package/dist/internal/build/pipeline.mjs +1 -1
- package/dist/internal/build/read-publication-status.d.mts +39 -0
- package/dist/internal/build/read-publication-status.mjs +1 -0
- package/dist/internal/build/render-debug-id.d.mts +6 -0
- package/dist/internal/build/render-debug-id.mjs +1 -0
- package/dist/internal/build/source-maps.d.mts +8 -26
- package/dist/internal/build/source-maps.mjs +1 -1
- package/dist/internal/build/transfer-source-maps.d.mts +59 -0
- package/dist/internal/build/transfer-source-maps.mjs +1 -0
- package/dist/internal/build/wait-for-source-maps.d.mts +48 -0
- package/dist/internal/build/wait-for-source-maps.mjs +1 -0
- package/dist/internal/release-slug.d.mts +4 -6
- package/dist/internal/server/capture.d.mts +2 -3
- package/dist/internal/server/capture.mjs +1 -1
- package/dist/internal/server/console-bridge.d.mts +2 -3
- package/dist/internal/server/console-bridge.mjs +1 -1
- package/dist/internal/server/env.d.mts +8 -10
- package/dist/internal/server/env.mjs +1 -1
- package/dist/internal/server/freshness-propagator.d.mts +18 -0
- package/dist/internal/server/freshness-propagator.mjs +1 -0
- package/dist/internal/server/instrumentation-options.d.mts +2 -3
- package/dist/internal/server/register.d.mts +2 -3
- package/dist/internal/server/register.mjs +1 -1
- package/dist/internal/server/remote-config.d.mts +3 -4
- package/dist/internal/server/remote-config.mjs +1 -1
- package/dist/internal/server/request-trace.d.mts +4 -5
- package/dist/internal/server/request-trace.mjs +1 -1
- package/dist/internal/server/trace-meta.d.mts +9 -8
- package/dist/internal/server/trace-meta.mjs +1 -1
- package/dist/internal/server/types.d.mts +2 -3
- package/dist/package.mjs +1 -1
- package/dist/plugin.d.mts +12 -7
- package/dist/plugin.mjs +2 -2
- package/dist/provider.d.mts +3 -3
- package/dist/provider.mjs +1 -1
- package/dist/tanstack-start.d.mts +2 -2
- package/dist/tanstack-start.mjs +1 -1
- package/dist/version.d.mts +2 -3
- package/package.json +45 -43
- package/dist/internal/build/baked-config.d.mts +0 -13
- package/dist/internal/build/baked-config.mjs +0 -1
- package/dist/internal/server/traceparent.d.mts +0 -17
- package/dist/internal/server/traceparent.mjs +0 -1
- package/dist/internal/server/url.d.mts +0 -4
- 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
|
|
139
|
-
|
|
|
140
|
-
| `VITE_INTERFERE_PUBLIC_KEY`
|
|
141
|
-
| `INTERFERE_API_KEY`
|
|
142
|
-
| `INTERFERE_API_URL`
|
|
143
|
-
| `VITE_INTERFERE_API_URL`
|
|
144
|
-
| `VITE_INTERFERE_COMMIT_SHA`
|
|
145
|
-
| `VITE_INTERFERE_FORCE_ENABLE` | No
|
|
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
|
|
156
|
-
|
|
|
157
|
-
| `apiHost`
|
|
158
|
-
| `commitSha`
|
|
159
|
-
| `sourceMaps`
|
|
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
|
|
198
|
-
|
|
|
199
|
-
| `identifier` | Yes
|
|
200
|
-
| `source`
|
|
201
|
-
| `name`
|
|
202
|
-
| `email`
|
|
203
|
-
| `avatar`
|
|
204
|
-
| `traits`
|
|
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
|
|
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()`
|
|
212
|
-
| `identity.get()`
|
|
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
|
|
235
|
-
|
|
|
236
|
-
| `necessary` | Error tracking
|
|
237
|
-
| `analytics` | Page events, rage clicks, fingerprint | Yes
|
|
238
|
-
| `replay`
|
|
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
|
|
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();
|
|
251
|
-
consent.get();
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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};
|
|
@@ -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 @@
|
|
|
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};
|
|
@@ -6,6 +6,5 @@
|
|
|
6
6
|
* importing it from runtime server code, which must never spawn
|
|
7
7
|
* subprocesses.
|
|
8
8
|
*/
|
|
9
|
-
declare function runGitCommand(command: string): string | null;
|
|
10
|
-
//#endregion
|
|
11
|
-
export { runGitCommand };
|
|
9
|
+
export declare function runGitCommand(command: string): string | null;
|
|
10
|
+
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{execSync}from"node:child_process";function runGitCommand(command){try{let output=execSync(command,{encoding:`
|
|
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
|
|
9
|
-
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
66
|
-
//#endregion
|
|
67
|
-
export { ReleasePipelineInput, ReleasePipelineResult, createSdkClient, runReleasePipeline };
|
|
91
|
+
ok: true;
|
|
92
|
+
}>;
|
|
93
|
+
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
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 @@
|
|
|
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 {
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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 {
|
|
31
|
+
export { injectDebugIdIntoJs };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
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
|