@interfere/vite 12.0.5 → 12.1.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/README.md CHANGED
@@ -135,15 +135,15 @@ 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_SOURCEMAP_UPLOAD_CONCURRENCY` | No | Maximum concurrent source-map uploads. Defaults to 20; must be a positive integer. |
143
- | `INTERFERE_API_URL` | No | Override the collector URL the build pipeline targets. Defaults to `https://in.interfere.com`. |
144
- | `VITE_INTERFERE_API_URL` | No | Override the collector URL the browser kernel targets in direct mode. Proxy mode uses `INTERFERE_API_URL` server-side. |
145
- | `VITE_INTERFERE_COMMIT_SHA` | No | Override the auto-detected commit SHA used to derive the release slug. Defaults to `git rev-parse HEAD`. |
146
- | `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. A production build without it logs a warning and sends no events. |
141
+ | `INTERFERE_API_KEY` | Yes for deploy builds | Interfere secret key in the `interfere_secret_us_*` or `interfere_secret_eu_*` format. Used by the plugin at build time to register the release and upload source maps. A production build without it logs a warning and skips the release. A key with the wrong prefix fails the build. If Interfere rejects the key, the build logs an error and continues without a release. |
142
+ | `INTERFERE_SOURCEMAP_UPLOAD_CONCURRENCY` | No | Maximum concurrent source-map uploads. Defaults to 20; must be a positive integer. |
143
+ | `INTERFERE_API_URL` | No | Override the collector URL the build pipeline targets. Defaults to `https://in.interfere.com`. |
144
+ | `VITE_INTERFERE_API_URL` | No | Override the collector URL the browser kernel targets in direct mode. Proxy mode uses `INTERFERE_API_URL` server-side. |
145
+ | `VITE_INTERFERE_COMMIT_SHA` | No | Override the auto-detected commit SHA used to derive the release slug. Defaults to `git rev-parse HEAD`. |
146
+ | `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. |
147
147
 
148
148
  ## Plugin Options
149
149
 
@@ -153,15 +153,15 @@ interfere({
153
153
  });
154
154
  ```
155
155
 
156
- | Option | Default | Description |
157
- | ----------------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
158
- | `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. |
159
- | `commitSha` | `VITE_INTERFERE_COMMIT_SHA` → `git rev-parse HEAD` | Commit SHA used to derive the deterministic release slug. |
160
- | `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. |
161
- | `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. |
162
- | `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. |
156
+ | Option | Default | Description |
157
+ | -------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
158
+ | `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. |
159
+ | `commitSha` | `VITE_INTERFERE_COMMIT_SHA` → `git rev-parse HEAD` | Commit SHA used to derive the deterministic release slug. |
160
+ | `sourceMaps` | `{ upload: true }` | `{ upload: false }` registers the release without uploading source maps. Interfere API failures, including a rejected secret key, timeouts, network failures and HTTP error responses, never fail the build: they are logged, local source maps are deleted and the build finishes. |
161
+ | `disable` | `false` | Turns Interfere off for this build: no release, no source-map upload, no build checks, and the browser and server SDKs send no events. |
162
+ | `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. Keep this artifact private; missing or fallback evidence cannot establish safe deployment reuse. |
163
163
 
164
- 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.
164
+ Build logs report release creation, signing, session lookup, upload, finalization, receipt capture and cleanup in milliseconds, plus transferred bytes. Finalization confirms durable acceptance. When Vite builds several environments, the plugin uploads once after all of them finish. It does not replace runtime stack/source canaries or continually rehash every previously published map.
165
165
 
166
166
  Ingest requests never carry cookies. `apiHost` posts to your own site's subdomain, surviving ad-blockers with no server-side component.
167
167
 
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({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
+ 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 import.meta.env.VITE_INTERFERE_DISABLED?null:(applyBrowserGlobals(),wrapper.init(opts))}const{close,consent,getKernel,getKernelOrNull,identity,subscribeToKernel}=wrapper;export{capture,close,consent,getKernel,getKernelOrNull,identity,init,span,subscribeToKernel};
@@ -7,13 +7,13 @@ export interface ReleasePipelineInput {
7
7
  client: InterfereClient;
8
8
  buildId: string;
9
9
  bundler: ManifestBundler;
10
- discovery: DiscoveryResult;
10
+ discovery: Pick<DiscoveryResult, "files" | "sourceFileCount">;
11
11
  environment: string | null;
12
12
  producerVersion: string;
13
13
  releaseSlug: ReleaseSlug;
14
14
  releasesConfig: Interfere.ReleasesConfigResponse;
15
- readinessTimeoutMs?: number | undefined;
16
15
  capturePublication?: boolean;
16
+ uploadSourceMaps: boolean;
17
17
  }
18
18
  export declare function runReleasePipeline(input: ReleasePipelineInput): Promise<{
19
19
  failure: {
@@ -22,6 +22,16 @@ export declare function runReleasePipeline(input: ReleasePipelineInput): Promise
22
22
  };
23
23
  ok: false;
24
24
  data?: never;
25
+ } | {
26
+ failure?: never;
27
+ data: {
28
+ cleanupFailures: never[];
29
+ fileCount: number;
30
+ releaseSlug: `rel_${string}` & import("zod").$brand<"ReleaseSlug">;
31
+ publication: undefined;
32
+ timing: undefined;
33
+ };
34
+ ok: true;
25
35
  } | {
26
36
  failure?: never;
27
37
  data: {
@@ -82,7 +92,6 @@ export declare function runReleasePipeline(input: ReleasePipelineInput): Promise
82
92
  finalize: number;
83
93
  totalBytes: number;
84
94
  createRelease: number;
85
- readiness: number;
86
95
  receipt: number;
87
96
  cleanup: number;
88
97
  total: number;
@@ -1 +1 @@
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};
1
+ import{readPublicationStatus}from"./read-publication-status.mjs";import{transferSourceMaps}from"./transfer-source-maps.mjs";import{cleanupSourceMaps}from"@interfere/build/cleanup-source-maps";import{sourceMapBatchDigest}from"@interfere/types/data/source-map-batch";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(input.uploadSourceMaps&&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;if(!input.uploadSourceMaps)return{data:{cleanupFailures:[],fileCount:0,releaseSlug,publication:void 0,timing:void 0},ok:!0};let transfer=await transferSourceMaps({...input,releaseSlug}),{completion,session,acceptedAt}=transfer,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,receipt:Math.round(publicationReadAt-acceptedAt),cleanup:Math.round(performance.now()-publicationReadAt),total:Math.round(performance.now()-start)}},ok:!0}}export{runReleasePipeline};
@@ -11,6 +11,7 @@ interface SourceMapFile {
11
11
  }
12
12
  export interface DiscoveryResult {
13
13
  readonly files: SourceMapFile[];
14
+ readonly missingSourceMapCount: number;
14
15
  readonly sourceFileCount: number;
15
16
  }
16
17
  interface PublicPathRewrite {
@@ -25,6 +26,7 @@ export interface DiscoverByPairingOptions {
25
26
  }
26
27
  export declare function discoverByPairing(opts: DiscoverByPairingOptions): Promise<{
27
28
  files: SourceMapFile[];
29
+ missingSourceMapCount: number;
28
30
  sourceFileCount: number;
29
31
  }>;
30
32
  //#endregion
@@ -1 +1 @@
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};
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||match.groups.sourceMap.startsWith(`data:`))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]})}let loaded=(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);return{files:loaded,missingSourceMapCount:pairs.size-loaded.length,sourceFileCount:pairs.size}}export{discoverByPairing,injectDebugIdIntoJs};
@@ -1,6 +1,6 @@
1
1
  import { ReleasePipelineInput } from "./pipeline.mjs";
2
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<{
3
+ export declare function transferSourceMaps(input: Pick<ReleasePipelineInput, "client" | "bundler" | "discovery" | "environment" | "releaseSlug" | "capturePublication">): Promise<{
4
4
  completion: {
5
5
  bundler: "rollup" | "tsc" | "turbopack" | "webpack";
6
6
  sourceFileCount: number;
@@ -1 +1 @@
1
- import{readPublicationStatus}from"./read-publication-status.mjs";import{uploadSourceMapChunks}from"@interfere/build/upload-source-map-chunks";import{completeSourceMapsResponse}from"@interfere/types/data/source-maps";import{sourceMapPublicationStatus}from"@interfere/types/data/source-map-status";import{extractRichness}from"@interfere/types/sdk/source-map-richness";async function transferSourceMaps(input){let{client,bundler,releaseSlug}=input,{files,sourceFileCount}=input.discovery,start=performance.now(),signFiles=files.map(file=>({path:file.path,sizeBytes:new TextEncoder().encode(file.content).byteLength,...extractRichness(file.content)})),environment=input.environment??void 0,signed=await client.releases.signSourceMapUploads({releaseSlug,environment,files:signFiles},{abortSignal:AbortSignal.timeout(6e4)}),signedAt=performance.now(),session=null;if(input.readinessTimeoutMs===void 0?input.capturePublication&&(session=await readPublicationStatus(input)):session=sourceMapPublicationStatus.parse(await client.releases.getSourceMapPublicationStatus({releaseSlug,environment},{abortSignal:AbortSignal.timeout(6e4)})),input.readinessTimeoutMs!==void 0&&session&&(!session.sessionId||session.releaseSlug!==releaseSlug||session.environment!==input.environment))throw Error(`Source-map publication requires an identified upload session for this environment`);let sessionReadAt=performance.now(),totalBytes=await uploadSourceMapChunks({compression:`gzip`,client,environment,releaseSlug,files,signed}),uploadedAt=performance.now(),completion={bundler,sourceFileCount,files:files.map(({path,hash,debugId,chunkUrl})=>({path,hash,debugId,chunkUrl}))},accepted=completeSourceMapsResponse.safeParse(await client.releases.finalizeSourceMaps({releaseSlug,environment,...completion},{abortSignal:AbortSignal.timeout(Math.max(0,signed.expiresAt*1e3-Date.now()))}));if(!accepted.success||!accepted.data.ok||accepted.data.fileCount!==files.length)throw Error(`Source-map server did not accept the complete batch`);let acceptedAt=performance.now();return{completion,session,acceptedAt,timing:{sign:Math.round(signedAt-start),session:Math.round(sessionReadAt-signedAt),upload:Math.round(uploadedAt-sessionReadAt),finalize:Math.round(acceptedAt-uploadedAt),totalBytes}}}export{transferSourceMaps};
1
+ import{readPublicationStatus}from"./read-publication-status.mjs";import{uploadSourceMapChunks}from"@interfere/build/upload-source-map-chunks";import{completeSourceMapsResponse}from"@interfere/types/data/source-maps";import{extractRichness}from"@interfere/types/sdk/source-map-richness";async function transferSourceMaps(input){let{client,bundler,releaseSlug}=input,{files,sourceFileCount}=input.discovery,start=performance.now(),signFiles=files.map(file=>({path:file.path,sizeBytes:new TextEncoder().encode(file.content).byteLength,...extractRichness(file.content)})),environment=input.environment??void 0,signed=await client.releases.signSourceMapUploads({releaseSlug,environment,files:signFiles},{abortSignal:AbortSignal.timeout(6e4)}),signedAt=performance.now(),session=input.capturePublication?await readPublicationStatus(input):null,sessionReadAt=performance.now(),totalBytes=await uploadSourceMapChunks({compression:`gzip`,client,environment,releaseSlug,files,signed}),uploadedAt=performance.now(),completion={bundler,sourceFileCount,files:files.map(({path,hash,debugId,chunkUrl})=>({path,hash,debugId,chunkUrl}))},accepted=completeSourceMapsResponse.safeParse(await client.releases.finalizeSourceMaps({releaseSlug,environment,...completion},{abortSignal:AbortSignal.timeout(Math.max(0,signed.expiresAt*1e3-Date.now()))}));if(!accepted.success||!accepted.data.ok||accepted.data.fileCount!==files.length)throw Error(`Source-map server did not accept the complete batch`);let acceptedAt=performance.now();return{completion,session,acceptedAt,timing:{sign:Math.round(signedAt-start),session:Math.round(sessionReadAt-signedAt),upload:Math.round(uploadedAt-sessionReadAt),finalize:Math.round(acceptedAt-uploadedAt),totalBytes}}}export{transferSourceMaps};
@@ -1 +1 @@
1
- import{API_URL}from"@interfere/constants/api";import{parseEnvValue}from"@interfere/types/sdk/env";import{resolveEnvironment}from"@interfere/types/sdk/runtime";function isEnabledOnServer(){return process.env.NODE_ENV===`production`||!!process.env.VITE_INTERFERE_PUBLIC_KEY}function readInterfereServerEnv(){let nodeEnvironment=resolveEnvironment({env:process.env,explicit:import.meta.env.VITE_INTERFERE_ENVIRONMENT});return{apiUrl:parseEnvValue(process.env.INTERFERE_API_URL)??API_URL,forceEnable:!!process.env.INTERFERE_FORCE_ENABLE,nodeEnvironment,publicKey:parseEnvValue(process.env.VITE_INTERFERE_PUBLIC_KEY)}}export{isEnabledOnServer,readInterfereServerEnv};
1
+ import{API_URL}from"@interfere/constants/api";import{parseEnvValue}from"@interfere/types/sdk/env";import{resolveEnvironment}from"@interfere/types/sdk/runtime";function isEnabledOnServer(){return import.meta.env.VITE_INTERFERE_DISABLED?!1:process.env.NODE_ENV===`production`||!!process.env.VITE_INTERFERE_PUBLIC_KEY}function readInterfereServerEnv(){let nodeEnvironment=resolveEnvironment({env:process.env,explicit:import.meta.env.VITE_INTERFERE_ENVIRONMENT});return{apiUrl:parseEnvValue(process.env.INTERFERE_API_URL)??API_URL,forceEnable:!!process.env.INTERFERE_FORCE_ENABLE,nodeEnvironment,publicKey:import.meta.env.VITE_INTERFERE_DISABLED?null:parseEnvValue(process.env.VITE_INTERFERE_PUBLIC_KEY)}}export{isEnabledOnServer,readInterfereServerEnv};
package/dist/package.mjs CHANGED
@@ -1 +1 @@
1
- var name=`@interfere/vite`,version=`12.0.5`;export{name,version};
1
+ var name=`@interfere/vite`,version=`12.1.0`;export{name,version};
package/dist/plugin.d.mts CHANGED
@@ -28,15 +28,20 @@ export interface InterfereViteOptions {
28
28
  */
29
29
  serverSourcemapsDir?: string;
30
30
  /**
31
- * `false` skips the post-build release metadata pipeline. Defaults
32
- * to enabled when an `INTERFERE_API_KEY` is set; defaults to
33
- * disabled when one isn't (so dev runs without creds don't fail).
34
- * Useful when source maps are uploaded separately through the release API.
35
- * `"required"` fails the build unless source-map publication succeeds.
31
+ * Upload source maps for the release. A production build registers a release
32
+ * when `INTERFERE_API_KEY` is set, and skips it with a warning when the key is
33
+ * missing. `upload: false` registers the release without uploading source maps.
34
+ * Interfere API failures, including a rejected secret key, never fail the
35
+ * build: they are logged, local source maps are deleted and the build finishes.
36
36
  */
37
- sourceMaps?: boolean | "required";
38
- /** Wait for the exact source-map batch to be published, with a separate deadline in milliseconds. Requires sourceMaps: "required". */
39
- sourceMapReadinessTimeoutMs?: number;
37
+ sourceMaps?: {
38
+ upload?: boolean;
39
+ };
40
+ /**
41
+ * Turns Interfere off for this build: no release, no source-map upload, and
42
+ * the browser and server SDKs send no events.
43
+ */
44
+ disable?: boolean;
40
45
  }
41
46
  export declare function interfere(options?: InterfereViteOptions): Plugin<any>;
42
47
  //#endregion
package/dist/plugin.mjs CHANGED
@@ -1,3 +1,2 @@
1
- import{PRODUCER_VERSION}from"./version.mjs";import{runGitCommand}from"./internal/build/git.mjs";import{createBuildReceipt}from"./internal/build/build-receipt.mjs";import{runReleasePipeline}from"./internal/build/pipeline.mjs";import{renderDebugId}from"./internal/build/render-debug-id.mjs";import{discoverByPairing}from"./internal/build/source-maps.mjs";import{resolveCommitSha}from"./internal/release-slug.mjs";import{sourceMapUploadFetch}from"@interfere/build/source-map-upload-fetch";import{InterfereClient}from"@interfere/javascript";import{createBuildConfigResolver}from"@interfere/build/config";import{API_URL}from"@interfere/constants/api";import{omitUndefined}from"@interfere/helpers/omit-undefined";import{deriveReleaseSlug}from"@interfere/types/releases/slug";import{classifyReleasePipelineError,diagnoseRelease,isInterfereApiKey}from"@interfere/types/sdk/build-diagnostics";import{resolveDeployEnvironment}from"@interfere/types/sdk/deploy-environment";import{createHash}from"node:crypto";import{existsSync,readdirSync,statSync}from"node:fs";import path from"node:path";import{loadEnv}from"vite";const SERVER_RUNTIME_PLUGIN_HINTS=[`tanstack-start`,`nitro`,`@tanstack/react-start`],DIAGNOSTIC_CONTEXT={publicKeyEnvVar:`VITE_INTERFERE_PUBLIC_KEY`};function reportReleaseDiagnostic(logger,code,input){let diagnostic=diagnoseRelease(code,DIAGNOSTIC_CONTEXT,input);logger.warn(`[interfere] ${diagnostic.title}`);for(let line of diagnostic.details)logger.warn(`[interfere] ${line}`)}function isTestEnv(){return!!(process.env.VITEST||process.env.VITEST_WORKER_ID)}function pickEnvValue(env,key){let value=env[key];return value&&value.length>0?value:void 0}function failMissingPublicKey(){let lines=[`interfere() requires VITE_INTERFERE_PUBLIC_KEY to identify your project at runtime.`,`Set the VITE_INTERFERE_PUBLIC_KEY environment variable, or remove interfere() from your Vite config.`,`See: https://interfere.com/docs/sdk/vite`];if(isTestEnv())throw Error(`Interfere: Public key not set\n${lines.join(`
2
- `)}`);console.error(`
3
- ⨯ Interfere → Public key not set`);for(let i=0;i<lines.length;i+=1){let line=lines[i];if(!line)continue;let connector=i===lines.length-1?`└`:`├`;console.error(` ${connector} ${line}`)}process.exit(1)}function hasServerRuntimePlugin(pluginNames){return pluginNames.some(name=>SERVER_RUNTIME_PLUGIN_HINTS.some(hint=>name.includes(hint)))}function normalizeOrigin(host){try{return new URL(host).origin}catch{return null}}function resolveApiHost(options,env){let host=options.apiHost??env.VITE_INTERFERE_API_URL;if(!host)return;let origin=normalizeOrigin(host);if(!origin)throw Error(`interfere({ apiHost }) requires a full URL, e.g. apiHost: "https://in.yourapp.com"`);return origin}function resolveBrowserConfig(options,env,command){let commitSha=options.commitSha??env.VITE_INTERFERE_COMMIT_SHA??resolveCommitSha(runGitCommand);return{apiHost:resolveApiHost(options,env),commitSha,environment:command===`build`?resolveDeployEnvironment({...env,...process.env}):null,publicKey:env.VITE_INTERFERE_PUBLIC_KEY,releaseSlug:commitSha?deriveReleaseSlug(options.buildVariant?createHash(`sha256`).update(JSON.stringify([commitSha,options.buildVariant])).digest(`hex`):commitSha):null}}function browserDefine(browser){return{"import.meta.env.VITE_INTERFERE_API_HOST":JSON.stringify(browser.apiHost??null),"import.meta.env.VITE_INTERFERE_ENVIRONMENT":JSON.stringify(browser.environment),"import.meta.env.VITE_INTERFERE_RELEASE_SLUG":JSON.stringify(browser.releaseSlug)}}function hasFreshServerSourceMaps(root,sinceMs){if(!existsSync(root))return!1;let entries=readdirSync(root,{withFileTypes:!0});for(let entry of entries){let absolute=path.join(root,entry.name);if(entry.isDirectory()){if(hasFreshServerSourceMaps(absolute,sinceMs))return!0;continue}if(entry.name.endsWith(`.map`)&&statSync(absolute).mtimeMs>=sinceMs)return!0}return!1}function isUploadReady(state){return!!(state.isProductionBuild&&state.enableUpload&&state.apiKey&&state.releaseSlug&&state.commitSha&&!state.uploadStarted)}function shouldWaitForServerOutput(state){return state.serverBuild&&!hasFreshServerSourceMaps(state.serverSourcemapsDir,state.buildStartedAt)}function logSkippedRelease(logger,failure){if(failure.kind===`missing_source_provider`){logger.warn(`[interfere] release skipped: this surface has no version control provider configured. Configure one at https://interfere.com/~/${failure.config.org.slug}/surfaces/${failure.config.surface.slug}.`);return}logger.warn(`[interfere] release skipped: this surface has no deployment target configured. Configure one at https://interfere.com/~/${failure.config.org.slug}/surfaces/${failure.config.surface.slug}.`)}function logCleanupFailures(logger,failures){for(let failure of failures)logger.warn(`[interfere] cleanup left ${failure.absolute} on disk: ${failure.code??failure.message}`)}async function uploadSourceMaps(state){let{apiKey,apiUrl,releaseSlug,commitSha,logger,projectDir,uploadOutDir}=state;if(!isInterfereApiKey(apiKey)){if(reportReleaseDiagnostic(logger,`API_KEY_WRONG_PREFIX`,{apiKey}),state.requireUpload)throw Error(`Required source-map publication needs a valid INTERFERE_API_KEY`);return}logger.info(`[interfere] uploading source maps for release ${releaseSlug}…`);try{let client=new InterfereClient({fetch:sourceMapUploadFetch,baseUrl:apiUrl,apiKey:{token:apiKey},headers:{"user-agent":PRODUCER_VERSION},maxRetries:4,timeoutInSeconds:10}),[discovered,releasesConfig]=await Promise.all([discoverByPairing({distDir:uploadOutDir,projectDir,publicPathRewrites:[{from:`public`,to:``}],subtrees:[`.`]}),client.releases.getReleasesConfig({abortSignal:AbortSignal.timeout(6e4)})]);if(discovered.files.length===0){if(state.requireUpload)throw Error(`No source maps found for required publication`);logger.warn(`[interfere] No source maps found; the release remains unready. Enable source-map generation and upload them during the build.`);return}if(state.requireUpload&&discovered.files.length!==discovered.sourceFileCount)throw Error(`Required source-map publication is incomplete: loaded ${discovered.files.length} of ${discovered.sourceFileCount} referenced maps`);let result=await runReleasePipeline({client,buildId:commitSha,bundler:`rollup`,discovery:discovered,environment:state.environment,producerVersion:PRODUCER_VERSION,releaseSlug,releasesConfig,readinessTimeoutMs:state.readinessTimeoutMs,capturePublication:state.receipt!==void 0});if(!result.ok){if(logSkippedRelease(logger,result.failure),state.requireUpload)throw Error(`Required source-map publication failed: ${result.failure.kind}`);return}logCleanupFailures(logger,result.data.cleanupFailures),result.data.publication&&state.receipt?.recordPublication(result.data.publication),logger.info(`[interfere] source-map stages (ms): create=${result.data.timing.createRelease}, sign=${result.data.timing.sign}, session=${result.data.timing.session}, upload=${result.data.timing.upload}, finalize=${result.data.timing.finalize}, readiness=${result.data.timing.readiness}, receipt=${result.data.timing.receipt}, cleanup=${result.data.timing.cleanup}; bytes=${result.data.timing.totalBytes}.`),state.uploadComplete=!0,logger.info(`[interfere] release ${result.data.releaseSlug} confirmed — ${result.data.fileCount} maps, ${result.data.timing.total}ms.`)}catch(error){let message=error instanceof Error?error.message:String(error);if(reportReleaseDiagnostic(logger,classifyReleasePipelineError(error),{errorMessage:message}),state.requireUpload)throw error}}async function uploadSourceMapsWhenReady(state){return isUploadReady(state)?shouldWaitForServerOutput(state)?(state.logger.info(`[interfere] waiting for server build output in ${path.relative(state.projectDir,state.serverSourcemapsDir)} before uploading source maps.`),!1):(state.uploadStarted=!0,await uploadSourceMaps(state),state.uploadComplete):state.uploadComplete}function emitSetupWarnings(state){let{logger,apiKey,releaseSlug,sourceMapsExplicit,isProductionBuild}=state;sourceMapsExplicit!==!1&&apiKey&&!releaseSlug&&logger.warn(`[interfere] No commit SHA available. Release metadata publishing is disabled. Set INTERFERE_SOURCE_ID, expose VERCEL_GIT_COMMIT_SHA / GITHUB_SHA, or run inside a git checkout.`),isProductionBuild&&sourceMapsExplicit!==!1&&!apiKey&&logger.warn("[interfere] INTERFERE_API_KEY is not set. Release metadata will not be published. Create an Interfere secret key in the dashboard and set it in your build environment, or pass `interfere({ sourceMaps: false })` to suppress this warning if release metadata is intentionally handled out-of-band.")}function resolveReleaseEnv(config){return{...loadEnv(config.mode,config.envDir,``),...process.env}}function shouldEnableUpload({apiKey,isProductionBuild,releaseSlug,sourceMaps}){return isProductionBuild?sourceMaps===`required`||(sourceMaps??!!(apiKey&&releaseSlug)):!1}function buildPluginState(options,config,browser){let{logger}=config,isProductionBuild=config.command===`build`;isProductionBuild&&!browser.publicKey&&failMissingPublicKey();let releaseEnv=resolveReleaseEnv(config),apiKey=pickEnvValue(releaseEnv,`INTERFERE_API_KEY`),apiUrl=pickEnvValue(releaseEnv,`INTERFERE_API_URL`)??API_URL,requireUpload=isProductionBuild&&options.sourceMaps===`required`,readinessTimeoutMs=options.sourceMapReadinessTimeoutMs;if(readinessTimeoutMs!==void 0&&(options.sourceMaps!==`required`||!Number.isSafeInteger(readinessTimeoutMs)||readinessTimeoutMs<1||readinessTimeoutMs>6e5))throw Error(`sourceMapReadinessTimeoutMs requires sourceMaps: required and an integer between 1 and 600000 milliseconds`);if(requireUpload&&!apiKey)throw Error(`Required source-map publication needs INTERFERE_API_KEY`);if(requireUpload&&!browser.commitSha)throw Error(`Required source-map publication needs a commit SHA`);let uploadOutDir=path.resolve(config.root,config.build.outDir),serverSourcemapsDir=options.serverSourcemapsDir?path.resolve(config.root,options.serverSourcemapsDir):path.join(uploadOutDir,`server`),serverBuild=hasServerRuntimePlugin(config.plugins.map(p=>p.name))||!!config.build.ssr;return{apiHost:browser.apiHost,readinessTimeoutMs,apiKey,apiUrl,buildStartedAt:Math.floor(Date.now()/1e3)*1e3,commitSha:browser.commitSha,enableUpload:shouldEnableUpload({apiKey,isProductionBuild,releaseSlug:browser.releaseSlug,sourceMaps:options.sourceMaps}),environment:browser.environment,isProductionBuild,logger,projectDir:config.root,publicKey:browser.publicKey,requireUpload,releaseSlug:browser.releaseSlug,serverBuild,serverSourcemapsDir,uploadOutDir,uploadStarted:!1,uploadComplete:!1}}function announceSetup(state,options){state.logger.info(`[interfere] ingest → ${state.apiHost??`Interfere (default)`}`),!state.apiHost&&state.serverBuild&&state.logger.warn(`[interfere] Telemetry posts directly to Interfere. On a server-rendered app, set interfere({ apiHost }) to a subdomain of your own site to survive ad-blockers.`),emitSetupWarnings({apiKey:state.apiKey,isProductionBuild:state.isProductionBuild,logger:state.logger,releaseSlug:state.releaseSlug,sourceMapsExplicit:options.sourceMaps})}function mirrorPublicKeyForDevServer(command,env){if(command!==`serve`)return;let publicKey=env.VITE_INTERFERE_PUBLIC_KEY;publicKey&&!process.env.VITE_INTERFERE_PUBLIC_KEY&&(process.env.VITE_INTERFERE_PUBLIC_KEY=publicKey)}function resolveEnvDir(userConfig){let root=userConfig.root?path.resolve(process.cwd(),userConfig.root):process.cwd();return userConfig.envDir===!1||userConfig.envDir===void 0||userConfig.envDir===``?root:path.resolve(root,userConfig.envDir)}function forceSourcemapForUpload(userConfig,options,command,env){if(!(command===`build`&&options.sourceMaps!==!1&&pickEnvValue(env,`INTERFERE_API_KEY`)))return;let previous=userConfig.build?.sourcemap;return userConfig.build={...userConfig.build,sourcemap:!0},previous===void 0||previous===!0?void 0:previous}function carryOverUploadProgress(previous,next){previous?.serverBuild&&next.serverBuild&&(next.uploadOutDir=previous.uploadOutDir,next.serverSourcemapsDir=previous.serverSourcemapsDir,next.uploadStarted=previous.uploadStarted,next.uploadComplete=previous.uploadComplete,next.buildStartedAt=previous.buildStartedAt)}function warnForcedSourcemap(logger,overridden){overridden!==void 0&&logger.warn(`[interfere] Forced build.sourcemap to true (was ${JSON.stringify(overridden)}). Source maps are required to symbolicate errors; they are uploaded then deleted from the build output, so they are never served. Pass interfere({ sourceMaps: false }) to opt out.`)}function interfere(options={}){let receipt=options.buildReceipt?createBuildReceipt(options.buildReceipt):void 0,resolveBuildConfig=createBuildConfigResolver(receipt?.observeConfig),state=null,overriddenSourcemap,announced=!1,writeReceipt=async()=>{try{await receipt?.write()}catch{state?.logger.warn(`[interfere] Private build receipt could not be written.`)}};return{buildApp:{order:`post`,async handler(builder){if(!state?.requireUpload)return;let environments=Object.values(builder.environments);if(environments.every(environment=>!environment.isBuilt))for(let environment of environments)await builder.build(environment);if(!await uploadSourceMapsWhenReady(state))throw Error(`Required publication is waiting for fresh server source maps`);await writeReceipt()}},buildStart(){state&&!announced&&(announceSetup(state,options),warnForcedSourcemap(state.logger,overriddenSourcemap),announced=!0)},async closeBundle(){state&&await uploadSourceMapsWhenReady(state),await writeReceipt()},async writeBundle(output,bundle){output.dir&&await receipt?.recordOutput(output.dir,Object.keys(bundle))},outputOptions(output){return state?.enableUpload?{...output,sourcemapDebugIds:!1}:null},renderChunk:{order:`post`,handler(code){return state?.enableUpload?renderDebugId(code):null}},async config(userConfig,{command,mode}){let env=loadEnv(mode,resolveEnvDir(userConfig),``);mirrorPublicKeyForDevServer(command,env),overriddenSourcemap=forceSourcemapForUpload(userConfig,options,command,env);let browser=resolveBrowserConfig(options,env,command),baked=command===`build`&&browser.publicKey&&!isTestEnv()?await resolveBuildConfig(browser.apiHost??API_URL,browser.publicKey):null;return{define:omitUndefined({...browserDefine(browser),"import.meta.env.VITE_INTERFERE_INGEST_URL":baked?.ingestUrl?JSON.stringify(baked.ingestUrl):void 0,"import.meta.env.VITE_INTERFERE_SERVER_TRACING":baked?.serverTracing?JSON.stringify(baked.serverTracing):void 0,"import.meta.env.VITE_INTERFERE_LOG_SOURCING":baked?.logSourcing?JSON.stringify(baked.logSourcing):void 0,"import.meta.env.VITE_INTERFERE_PLUGINS":baked?.plugins?JSON.stringify(JSON.stringify(baked.plugins)):void 0}),ssr:{noExternal:[`@interfere/vite`]}}},async configResolved(config){let browser=resolveBrowserConfig(options,config.env,config.command),next=buildPluginState(options,config,browser);carryOverUploadProgress(state,next),state={...next,receipt},config.command===`build`&&await receipt?.configure(config,{commitSha:browser.commitSha,releaseSlug:browser.releaseSlug,environment:browser.environment,publicKey:browser.publicKey,apiOrigin:browser.apiHost??API_URL,producerVersion:PRODUCER_VERSION})},enforce:`pre`,name:`interfere`,sharedDuringBuild:!0}}export{interfere};
1
+ import{PRODUCER_VERSION}from"./version.mjs";import{runGitCommand}from"./internal/build/git.mjs";import{createBuildReceipt}from"./internal/build/build-receipt.mjs";import{runReleasePipeline}from"./internal/build/pipeline.mjs";import{renderDebugId}from"./internal/build/render-debug-id.mjs";import{discoverByPairing}from"./internal/build/source-maps.mjs";import{resolveCommitSha}from"./internal/release-slug.mjs";import{cleanupSourceMaps}from"@interfere/build/cleanup-source-maps";import{sourceMapUploadFetch}from"@interfere/build/source-map-upload-fetch";import{InterfereClient}from"@interfere/javascript";import{createBuildConfigResolver}from"@interfere/build/config";import{API_URL}from"@interfere/constants/api";import{omitUndefined}from"@interfere/helpers/omit-undefined";import{deriveReleaseSlug}from"@interfere/types/releases/slug";import{checkReleaseConfig,diagnoseRelease}from"@interfere/types/sdk/build-diagnostics";import{resolveDeployEnvironment}from"@interfere/types/sdk/deploy-environment";import{createHash}from"node:crypto";import{existsSync,readdirSync,statSync}from"node:fs";import path from"node:path";import{loadEnv}from"vite";const SERVER_RUNTIME_PLUGIN_HINTS=[`tanstack-start`,`nitro`,`@tanstack/react-start`],DIAGNOSTIC_CONTEXT={publicKeyEnvVar:`VITE_INTERFERE_PUBLIC_KEY`,docsUrl:`https://interfere.com/docs/sdk/vite`};function buildError(title,details){return Error([`Interfere: ${title}`,...details].join(`
2
+ `))}function logDiagnostic(logger,diagnostic){for(let line of[diagnostic.title,...diagnostic.details])logger[diagnostic.severity](`[interfere] ${line}`)}function isTestEnv(){return!!(process.env.VITEST||process.env.VITEST_WORKER_ID)}function pickEnvValue(env,key){let value=env[key];return value&&value.length>0?value:void 0}function hasServerRuntimePlugin(pluginNames){return pluginNames.some(name=>SERVER_RUNTIME_PLUGIN_HINTS.some(hint=>name.includes(hint)))}function normalizeOrigin(host){try{return new URL(host).origin}catch{return null}}function resolveApiHost(options,env){let host=options.apiHost??env.VITE_INTERFERE_API_URL;if(!host)return;let origin=normalizeOrigin(host);if(!origin)throw Error(`interfere({ apiHost }) requires a full URL, e.g. apiHost: "https://in.yourapp.com"`);return origin}function resolveBrowserConfig(options,env,command){let commitSha=options.commitSha??env.VITE_INTERFERE_COMMIT_SHA??resolveCommitSha(runGitCommand);return{apiHost:resolveApiHost(options,env),commitSha,environment:command===`build`?resolveDeployEnvironment({...env,...process.env}):null,publicKey:env.VITE_INTERFERE_PUBLIC_KEY,releaseSlug:commitSha?deriveReleaseSlug(options.buildVariant?createHash(`sha256`).update(JSON.stringify([commitSha,options.buildVariant])).digest(`hex`):commitSha):null}}function browserDefine(browser){return{"import.meta.env.VITE_INTERFERE_API_HOST":JSON.stringify(browser.apiHost??null),"import.meta.env.VITE_INTERFERE_ENVIRONMENT":JSON.stringify(browser.environment),"import.meta.env.VITE_INTERFERE_RELEASE_SLUG":JSON.stringify(browser.releaseSlug)}}function hasFreshServerSourceMaps(root,sinceMs){if(!existsSync(root))return!1;let entries=readdirSync(root,{withFileTypes:!0});for(let entry of entries){let absolute=path.join(root,entry.name);if(entry.isDirectory()){if(hasFreshServerSourceMaps(absolute,sinceMs))return!0;continue}if(entry.name.endsWith(`.map`)&&statSync(absolute).mtimeMs>=sinceMs)return!0}return!1}function isReleaseReady(state){return!!(state.releaseEnabled&&state.apiKey&&state.releaseSlug&&state.commitSha&&!state.releaseStarted)}function shouldWaitForServerOutput(state){return state.uploadSourceMaps&&state.serverBuild&&!hasFreshServerSourceMaps(state.serverSourcemapsDir,state.buildStartedAt)}function logSkippedRelease(logger,failure){if(failure.kind===`missing_source_provider`){logger.warn(`[interfere] release skipped: this surface has no version control provider configured. Configure one at https://interfere.com/~/${failure.config.org.slug}/surfaces/${failure.config.surface.slug}.`);return}logger.warn(`[interfere] release skipped: this surface has no deployment target configured. Configure one at https://interfere.com/~/${failure.config.org.slug}/surfaces/${failure.config.surface.slug}.`)}function logCleanupFailures(logger,failures){for(let failure of failures)logger.warn(`[interfere] cleanup left ${failure.absolute} on disk: ${failure.code??failure.message}`)}async function discoverUploadableSourceMaps(state){let discovered;try{discovered=await discoverByPairing({distDir:state.uploadOutDir,projectDir:state.projectDir,publicPathRewrites:[{from:`public`,to:``}],subtrees:[`.`]})}catch(error){return logDiagnostic(state.logger,diagnoseRelease(DIAGNOSTIC_CONTEXT,error)),null}return discovered.missingSourceMapCount>0&&state.logger.warn(`[interfere] ${discovered.missingSourceMapCount} referenced source map(s) not found. Errors in those chunks will not resolve to source.`),discovered.files.length===0?(state.logger.warn(`[interfere] No source maps found; the release remains unready. Enable source-map generation and upload them during the build.`),null):discovered}async function publishRelease(state,discovered){let{apiKey,apiUrl,releaseSlug,commitSha,logger}=state,client=new InterfereClient({fetch:sourceMapUploadFetch,baseUrl:apiUrl,apiKey:{token:apiKey},headers:{"user-agent":PRODUCER_VERSION},maxRetries:4,timeoutInSeconds:10}),releasesConfig=await client.releases.getReleasesConfig({abortSignal:AbortSignal.timeout(6e4)}),result=await runReleasePipeline({client,buildId:commitSha,bundler:`rollup`,discovery:discovered,environment:state.environment,producerVersion:PRODUCER_VERSION,releaseSlug,releasesConfig,capturePublication:state.receipt!==void 0,uploadSourceMaps:state.uploadSourceMaps});if(!result.ok){logSkippedRelease(logger,result.failure);return}if(logCleanupFailures(logger,result.data.cleanupFailures),result.data.publication&&state.receipt?.recordPublication(result.data.publication),!result.data.timing){logger.info(`[interfere] release ${result.data.releaseSlug} registered without source maps.`);return}logger.info(`[interfere] source-map stages (ms): create=${result.data.timing.createRelease}, sign=${result.data.timing.sign}, session=${result.data.timing.session}, upload=${result.data.timing.upload}, finalize=${result.data.timing.finalize}, receipt=${result.data.timing.receipt}, cleanup=${result.data.timing.cleanup}; bytes=${result.data.timing.totalBytes}.`),state.uploadComplete=!0,logger.info(`[interfere] release ${result.data.releaseSlug} confirmed — ${result.data.fileCount} maps, ${result.data.timing.total}ms.`)}async function releaseBuild(state){let{logger,releaseSlug}=state;logger.info(state.uploadSourceMaps?`[interfere] uploading source maps for release ${releaseSlug}…`:`[interfere] registering release ${releaseSlug} without source maps…`);let discovered=state.uploadSourceMaps?await discoverUploadableSourceMaps(state):{files:[],missingSourceMapCount:0,sourceFileCount:0};if(discovered){try{await publishRelease(state,discovered)}catch(error){logDiagnostic(logger,diagnoseRelease(DIAGNOSTIC_CONTEXT,error))}state.uploadComplete||logCleanupFailures(logger,await cleanupSourceMaps(discovered.files))}}async function releaseBuildWhenReady(state){if(isReleaseReady(state)){if(shouldWaitForServerOutput(state)){state.logger.info(`[interfere] waiting for server build output in ${path.relative(state.projectDir,state.serverSourcemapsDir)} before uploading source maps.`);return}state.releaseStarted=!0,await releaseBuild(state)}}function resolveReleaseEnv(config){return{...loadEnv(config.mode,config.envDir,``),...process.env}}function buildPluginState(options,config,browser){let{logger}=config,isProductionBuild=config.command===`build`,releaseEnv=resolveReleaseEnv(config),apiKey=pickEnvValue(releaseEnv,`INTERFERE_API_KEY`),apiUrl=pickEnvValue(releaseEnv,`INTERFERE_API_URL`)??API_URL,release=isProductionBuild?checkReleaseConfig(DIAGNOSTIC_CONTEXT,{apiKey,commitSha:browser.commitSha,releaseSlug:browser.releaseSlug}):null;if(release?.status===`fail`)throw buildError(release.title,release.details);let releaseEnabled=release?.status===`ok`,uploadOutDir=path.resolve(config.root,config.build.outDir),serverSourcemapsDir=options.serverSourcemapsDir?path.resolve(config.root,options.serverSourcemapsDir):path.join(uploadOutDir,`server`),serverBuild=hasServerRuntimePlugin(config.plugins.map(p=>p.name))||!!config.build.ssr;return{apiHost:browser.apiHost,apiKey,apiUrl,buildStartedAt:Math.floor(Date.now()/1e3)*1e3,commitSha:browser.commitSha,releaseEnabled,skippedRelease:release?.status===`skip`?release:null,uploadSourceMaps:releaseEnabled&&options.sourceMaps?.upload!==!1,environment:browser.environment,isProductionBuild,logger,projectDir:config.root,publicKey:browser.publicKey,releaseSlug:browser.releaseSlug,serverBuild,serverSourcemapsDir,uploadOutDir,releaseStarted:!1,uploadComplete:!1}}function announceSetup(state){if(state.logger.info(`[interfere] ingest → ${state.apiHost??`Interfere (default)`}`),!state.apiHost&&state.serverBuild&&state.logger.warn(`[interfere] Telemetry posts directly to Interfere. On a server-rendered app, set interfere({ apiHost }) to a subdomain of your own site to survive ad-blockers.`),state.isProductionBuild&&!state.publicKey&&state.logger.warn(`[interfere] VITE_INTERFERE_PUBLIC_KEY is not set, so this build sends no events. Set it on builds that you deploy.`),state.skippedRelease)for(let line of[state.skippedRelease.title,...state.skippedRelease.details])state.logger.warn(`[interfere] ${line}`)}function mirrorPublicKeyForDevServer(command,env){if(command!==`serve`)return;let publicKey=env.VITE_INTERFERE_PUBLIC_KEY;publicKey&&!process.env.VITE_INTERFERE_PUBLIC_KEY&&(process.env.VITE_INTERFERE_PUBLIC_KEY=publicKey)}function resolveEnvDir(userConfig){let root=userConfig.root?path.resolve(process.cwd(),userConfig.root):process.cwd();return userConfig.envDir===!1||userConfig.envDir===void 0||userConfig.envDir===``?root:path.resolve(root,userConfig.envDir)}function forceSourcemapForUpload(userConfig,options,command,env){if(!(command===`build`&&options.sourceMaps?.upload!==!1&&pickEnvValue(env,`INTERFERE_API_KEY`)))return;let previous=userConfig.build?.sourcemap;return userConfig.build={...userConfig.build,sourcemap:!0},previous===void 0||previous===!0?void 0:previous}function carryOverUploadProgress(previous,next){previous?.serverBuild&&next.serverBuild&&(next.uploadOutDir=previous.uploadOutDir,next.serverSourcemapsDir=previous.serverSourcemapsDir,next.releaseStarted=previous.releaseStarted,next.uploadComplete=previous.uploadComplete,next.buildStartedAt=previous.buildStartedAt)}function warnForcedSourcemap(logger,overridden){overridden!==void 0&&logger.warn(`[interfere] Forced build.sourcemap to true (was ${JSON.stringify(overridden)}). Source maps are required to symbolicate errors; they are uploaded then deleted from the build output, so they are never served. Pass interfere({ sourceMaps: { upload: false } }) to opt out.`)}function disabledPlugin(){return{name:`interfere`,config(){return{define:{"import.meta.env.VITE_INTERFERE_DISABLED":JSON.stringify(`true`)},ssr:{noExternal:[`@interfere/vite`]}}}}}function rejectRemovedOptions(options){let sourceMaps=options.sourceMaps;if(sourceMaps!==void 0&&(typeof sourceMaps!=`object`||!sourceMaps))throw buildError(`Removed: interfere({ sourceMaps })`,['`sourceMaps: true`, `sourceMaps: false` and `sourceMaps: "required"` were removed.',"Use `sourceMaps: { upload: false }` to register the release without source maps, or `disable: true` to turn Interfere off for the build.",`See: ${DIAGNOSTIC_CONTEXT.docsUrl}`]);if(`sourceMapReadinessTimeoutMs`in options)throw buildError(`Removed: interfere({ sourceMapReadinessTimeoutMs })`,[`The build no longer waits for source-map processing. Remove the option.`,`See: ${DIAGNOSTIC_CONTEXT.docsUrl}`])}function interfere(options={}){if(rejectRemovedOptions(options),options.disable)return disabledPlugin();let receipt=options.buildReceipt?createBuildReceipt(options.buildReceipt):void 0,remoteConfigFallback=!1,resolveBuildConfig=createBuildConfigResolver(result=>{receipt?.observeConfig(result),remoteConfigFallback||=result.status===`fallback`}),state=null,overriddenSourcemap,announced=!1,uploadAfterAppBuild=!1,writeReceipt=async()=>{try{await receipt?.write()}catch{state?.logger.warn(`[interfere] Private build receipt could not be written.`)}};return{buildApp:{order:`post`,async handler(builder){if(!state)return;uploadAfterAppBuild=!0;let environments=Object.values(builder.environments);if(environments.every(environment=>!environment.isBuilt))for(let environment of environments)await builder.build(environment);isReleaseReady(state)&&(state.releaseStarted=!0,await releaseBuild(state)),await writeReceipt()}},buildStart(){state&&!announced&&(announceSetup(state),warnForcedSourcemap(state.logger,overriddenSourcemap),remoteConfigFallback&&state.logger.warn(`[interfere] Could not load SDK settings from Interfere. The build uses the default settings.`),announced=!0)},async closeBundle(){state&&!uploadAfterAppBuild&&await releaseBuildWhenReady(state),await writeReceipt()},async writeBundle(output,bundle){output.dir&&await receipt?.recordOutput(output.dir,Object.keys(bundle))},outputOptions(output){return state?.uploadSourceMaps?{...output,sourcemapDebugIds:!1}:null},renderChunk:{order:`post`,handler(code){return state?.uploadSourceMaps?renderDebugId(code):null}},async config(userConfig,{command,mode}){let env=loadEnv(mode,resolveEnvDir(userConfig),``);mirrorPublicKeyForDevServer(command,env),overriddenSourcemap=forceSourcemapForUpload(userConfig,options,command,env);let browser=resolveBrowserConfig(options,env,command),baked=command===`build`&&browser.publicKey&&!isTestEnv()?await resolveBuildConfig(browser.apiHost??API_URL,browser.publicKey):null;return{define:omitUndefined({...browserDefine(browser),"import.meta.env.VITE_INTERFERE_INGEST_URL":baked?.ingestUrl?JSON.stringify(baked.ingestUrl):void 0,"import.meta.env.VITE_INTERFERE_SERVER_TRACING":baked?.serverTracing?JSON.stringify(baked.serverTracing):void 0,"import.meta.env.VITE_INTERFERE_LOG_SOURCING":baked?.logSourcing?JSON.stringify(baked.logSourcing):void 0,"import.meta.env.VITE_INTERFERE_PLUGINS":baked?.plugins?JSON.stringify(JSON.stringify(baked.plugins)):void 0}),ssr:{noExternal:[`@interfere/vite`]}}},async configResolved(config){let browser=resolveBrowserConfig(options,config.env,config.command),next=buildPluginState(options,config,browser);carryOverUploadProgress(state,next),state={...next,receipt},uploadAfterAppBuild||=config.builder!==void 0,config.command===`build`&&await receipt?.configure(config,{commitSha:browser.commitSha,releaseSlug:browser.releaseSlug,environment:browser.environment,publicKey:browser.publicKey,apiOrigin:browser.apiHost??API_URL,producerVersion:PRODUCER_VERSION})},enforce:`pre`,name:`interfere`,sharedDuringBuild:!0}}export{interfere};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@interfere/vite",
3
- "version": "12.0.5",
3
+ "version": "12.1.0",
4
4
  "description": "Vite build plugin and client + server initialization for Interfere.",
5
5
  "keywords": [
6
6
  "error-tracking",
@@ -62,12 +62,12 @@
62
62
  "typecheck": "bun check"
63
63
  },
64
64
  "dependencies": {
65
- "@interfere/build": "^0.3.0",
65
+ "@interfere/build": "^0.3.1",
66
66
  "@interfere/constants": "^9.2.0",
67
67
  "@interfere/helpers": "^9.2.0",
68
68
  "@interfere/javascript": "^0.2.1",
69
- "@interfere/react": "^12.0.5",
70
- "@interfere/types": "^10.2.2",
69
+ "@interfere/react": "^12.1.0",
70
+ "@interfere/types": "^10.3.0",
71
71
  "@opentelemetry/api": "^1.9.1",
72
72
  "@opentelemetry/api-logs": "^0.222.0",
73
73
  "@opentelemetry/baggage-span-processor": "^0.5.0",
@@ -91,21 +91,21 @@
91
91
  "@interfere/vitest-config": "0.0.0",
92
92
  "@jridgewell/trace-mapping": "^0.3.31",
93
93
  "@playwright/test": "^1.63.0",
94
- "@tanstack/react-start": "1.168.49",
94
+ "@tanstack/react-start": "1.168.60",
95
95
  "@types/node": "^26.4.1",
96
96
  "@types/react": "19.2.18",
97
97
  "@types/react-dom": "19.2.7",
98
- "@vitest/coverage-v8": "5.0.1",
98
+ "@vitest/coverage-v8": "5.0.5",
99
99
  "msw": "^2.14.6",
100
100
  "react": "19.3.0",
101
101
  "react-dom": "19.3.0",
102
102
  "tsdown": "^0.23.0",
103
103
  "typescript": "^7.0.2",
104
104
  "vite": "^8.2.2",
105
- "vitest": "5.0.1"
105
+ "vitest": "5.0.5"
106
106
  },
107
107
  "peerDependencies": {
108
- "@tanstack/react-start": "1.168.49",
108
+ "@tanstack/react-start": "1.168.60",
109
109
  "react": "^19.0.0",
110
110
  "react-dom": "^19.0.0",
111
111
  "vite": "^8.2.2"
@@ -1,48 +0,0 @@
1
- import { InterfereClient } from "@interfere/javascript";
2
- //#region src/internal/build/wait-for-source-maps.d.ts
3
- export declare function waitForSourceMaps(input: {
4
- client: InterfereClient;
5
- releaseSlug: string;
6
- environment: string | null;
7
- sessionId: string;
8
- batchDigest: string;
9
- fileCount: number;
10
- timeoutMs: number;
11
- signal?: AbortSignal;
12
- }): Promise<{
13
- releaseSlug: `rel_${string}` & import("zod").$brand<"ReleaseSlug">;
14
- environment: string | null;
15
- sessionId: string | null;
16
- state: "absent" | "accepted" | "expired" | "expiring" | "ready" | "uploading";
17
- fileCount: number;
18
- publishedFileCount: number;
19
- batchDigest: string | null;
20
- manifestReady: boolean;
21
- diagnostics?: {
22
- attempt: number;
23
- scheduledAtMs: number | null;
24
- startedAtMs: number;
25
- completedAtMs: number | null;
26
- phase: "complete" | "completion" | "continuation" | "files" | "manifest" | "release";
27
- publishedAtStart: number;
28
- progressObservedAtMs: number | null;
29
- nativeRetryCount: number;
30
- interruptedAttempts: number;
31
- longestAttempt: {
32
- attempt: number;
33
- durationMs: number;
34
- } | null;
35
- longestAlarmDelay: {
36
- attempt: number;
37
- durationMs: number;
38
- } | null;
39
- nextAlarmAtMs: number | null;
40
- lastFailure: {
41
- attempt: number;
42
- atMs: number;
43
- phase: "complete" | "completion" | "continuation" | "files" | "manifest" | "release";
44
- kind: "aborted" | "error" | "non-error" | "timeout";
45
- } | null;
46
- } | undefined;
47
- }>;
48
- //#endregion
@@ -1 +0,0 @@
1
- import{sourceMapPublicationStatus}from"@interfere/types/data/source-map-status";import{setTimeout}from"node:timers/promises";async function waitForSourceMaps(input){let startedAt=performance.now(),deadline=AbortSignal.timeout(input.timeoutMs),signal=input.signal?AbortSignal.any([deadline,input.signal]):deadline,delayMs=250,phase=`request`,requestOutcome=`interrupted`,requestStartedAt=startedAt,requestFinishedAt,lastValidatedAt,lastStatus;try{for(;;){signal.throwIfAborted(),phase=`request`,requestStartedAt=performance.now(),requestFinishedAt=void 0,requestOutcome=`interrupted`;let response=await input.client.releases.getSourceMapPublicationStatus({releaseSlug:input.releaseSlug,environment:input.environment??void 0},{abortSignal:signal});requestFinishedAt=performance.now(),requestOutcome=`returned`;let status=sourceMapPublicationStatus.parse(response);if(signal.throwIfAborted(),status.releaseSlug!==input.releaseSlug||status.environment!==input.environment||status.sessionId!==input.sessionId)throw Error(`Source-map publication session changed`);if(status.state===`expired`||status.state===`expiring`||status.state===`absent`)throw Error(`Source-map publication session is ${status.state}`);if(status.batchDigest!==input.batchDigest||status.fileCount!==input.fileCount)throw Error(`Source-map publication batch does not match the accepted build`);if(lastStatus=status,lastValidatedAt=performance.now(),status.state===`ready`&&status.manifestReady&&status.publishedFileCount===input.fileCount)return status;phase=`backoff`,await setTimeout(delayMs,void 0,{signal}),delayMs=Math.min(delayMs*2,5e3)}}catch(error){if(!deadline.aborted||signal.reason!==deadline.reason)throw error;let now=performance.now(),elapsedMs=Math.round(now-startedAt),polling={phase,lastValidatedAfterMs:lastValidatedAt===void 0?null:Math.round(lastValidatedAt-startedAt),lastValidatedAgeMs:lastValidatedAt===void 0?null:Math.round(now-lastValidatedAt),requestDurationMs:Math.round((requestFinishedAt??now)-requestStartedAt),requestOutcome},progress=lastStatus?`last validated status: state=${lastStatus.state}, published=${lastStatus.publishedFileCount}/${input.fileCount}, manifestReady=${lastStatus.manifestReady}`:`no validated status received`,diagnostics=lastStatus?.diagnostics?`; diagnostics=${JSON.stringify(lastStatus.diagnostics)}`:``;throw Error(`Source-map publication timed out after ${elapsedMs}ms (deadline=${input.timeoutMs}ms, release=${input.releaseSlug}, environment=${input.environment??`unspecified`}, session=${input.sessionId}); ${progress}${diagnostics}; polling=${JSON.stringify(polling)}`,{cause:error})}}export{waitForSourceMaps};