@interfere/vite 12.2.0 → 12.3.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 CHANGED
@@ -144,7 +144,7 @@ For **Vercel preview / production**, set `INTERFERE_API_KEY` in your `.env.local
144
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
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
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
- | `VITE_INTERFERE_SERVICE_NAME` | No | The OTel `service.name` the browser SDK reports when `init()` is called without one. Defaults to the project package name without its scope (`@acme/storefront` → `storefront`). The `serviceName` plugin option overrides it; an explicit `init({ serviceName })` always wins. |
147
+ | `VITE_INTERFERE_SERVICE_NAME` | No | Legacy build default for browser and server OTel `service.name`. The plugin `serviceName` option and `OTEL_SERVICE_NAME` take precedence; otherwise this overrides the unscoped package name (`@acme/storefront` → `storefront`). Explicit runtime service names always win. |
148
148
 
149
149
  ## Plugin Options
150
150
 
@@ -154,14 +154,14 @@ interfere({
154
154
  });
155
155
  ```
156
156
 
157
- | Option | Default | Description |
158
- | -------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
159
- | `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. |
160
- | `commitSha` | `VITE_INTERFERE_COMMIT_SHA` → `git rev-parse HEAD` | Commit SHA used to derive the deterministic release slug. |
161
- | `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. |
162
- | `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. |
163
- | `serviceName` | `VITE_INTERFERE_SERVICE_NAME` → package name | The OTel `service.name` stamped into the page for `init()` calls that pass none, so the Services page never shows `unknown_service`. Falls back to the project package name without its scope. An explicit `init({ serviceName })` still wins. |
164
- | `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. |
157
+ | Option | Default | Description |
158
+ | -------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
159
+ | `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. |
160
+ | `commitSha` | `VITE_INTERFERE_COMMIT_SHA` → `git rev-parse HEAD` | Commit SHA used to derive the deterministic release slug. |
161
+ | `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. |
162
+ | `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. |
163
+ | `serviceName` | `OTEL_SERVICE_NAME` → `VITE_INTERFERE_SERVICE_NAME` → package name | Build default for browser and server OTel `service.name`, resolved by the same shared policy as Next.js. Uses the unscoped package name when no override is set. Explicit runtime service names and server runtime `OTEL_SERVICE_NAME` take precedence over the build default. |
164
+ | `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. |
165
165
 
166
166
  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.
167
167
 
@@ -0,0 +1,58 @@
1
+ //#region src/internal/plugin-options.d.ts
2
+ export interface InterfereViteOptions {
3
+ /** Writes private build evidence outside public and build output directories. */
4
+ buildReceipt?: string;
5
+ /**
6
+ * The endpoint the browser SDK posts telemetry to. Defaults to
7
+ * Interfere's endpoint. Set it to a subdomain of your own site that
8
+ * resolves to Interfere via a DNS record (e.g.
9
+ * `"https://in.example.com"`) to survive ad-blockers, or to a
10
+ * regional/local endpoint. Must be a full URL.
11
+ */
12
+ apiHost?: string;
13
+ /**
14
+ * Override the commit SHA used to derive the release slug. Defaults
15
+ * to `VITE_INTERFERE_COMMIT_SHA` env then `git rev-parse HEAD`.
16
+ * Set this when the build runs outside a git checkout (e.g. a
17
+ * Docker layer-cached image build).
18
+ */
19
+ commitSha?: string;
20
+ /** Separates source maps for distinct bundles built from the same commit. */
21
+ buildVariant?: string;
22
+ /**
23
+ * Default service name for browser and server telemetry. Falls back to
24
+ * `OTEL_SERVICE_NAME`, `VITE_INTERFERE_SERVICE_NAME`, then the unscoped
25
+ * app package name. Explicit runtime service names still win.
26
+ */
27
+ serviceName?: string;
28
+ /**
29
+ * Directory (relative to the project root) where the SSR/server build
30
+ * writes its `.js.map` files, when that isn't `<build.outDir>/server`.
31
+ * Set this for server hosts that emit outside Vite's `outDir` — e.g.
32
+ * Nitro's `".output/server"`.
33
+ */
34
+ serverSourcemapsDir?: string;
35
+ /**
36
+ * Upload source maps for the release. A production build registers a release
37
+ * when `INTERFERE_API_KEY` is set, and skips it with a warning when the key is
38
+ * missing. `upload: false` registers the release without uploading source maps.
39
+ * Interfere API failures, including a rejected secret key, never fail the
40
+ * build: they are logged, local source maps are deleted and the build finishes.
41
+ *
42
+ * `retain: true` builds once for every deployment environment: the build
43
+ * registers nothing, emits debug-id-paired source maps next to its chunks and
44
+ * leaves them in the output for `releaseBuildOutput()` (`@interfere/vite/release`)
45
+ * to register and upload per environment at deploy time.
46
+ */
47
+ sourceMaps?: {
48
+ upload?: boolean;
49
+ retain?: boolean;
50
+ };
51
+ /**
52
+ * Turns Interfere off for this build: no release, no source-map upload, and
53
+ * the browser and server SDKs send no events.
54
+ */
55
+ disable?: boolean;
56
+ }
57
+ export declare function rejectRemovedOptions(options: InterfereViteOptions): void;
58
+ //#endregion
@@ -0,0 +1,2 @@
1
+ const DOCS_URL=`https://interfere.com/docs/sdk/vite`;function buildError(title,details){return Error([`Interfere: ${title}`,...details].join(`
2
+ `))}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: ${DOCS_URL}`]);if(`sourceMapReadinessTimeoutMs`in options)throw buildError(`Removed: interfere({ sourceMapReadinessTimeoutMs })`,[`The build no longer waits for source-map processing. Remove the option.`,`See: ${DOCS_URL}`])}export{rejectRemovedOptions};
@@ -1 +1 @@
1
- import{PRODUCER_VERSION}from"../../version.mjs";import{resolveReleaseSlug}from"../release-slug.mjs";import{readInterfereServerEnv}from"./env.mjs";import{fetchAndCacheRemoteConfig}from"./remote-config.mjs";import{bridgeConsoleToOtel}from"./console-bridge.mjs";import{FreshnessStampingPropagator}from"./freshness-propagator.mjs";import{drainUrl}from"@interfere/constants/api";import{omitUndefined}from"@interfere/helpers/omit-undefined";import{metrics,propagation}from"@opentelemetry/api";import{logSourcingModeSchema,readLogSourcingMode}from"@interfere/types/sdk/log-sourcing";import{readServerTracingMode,serverTracingModeSchema}from"@interfere/types/sdk/server-tracing";import{BaggageSpanProcessor}from"@opentelemetry/baggage-span-processor";import{AsyncLocalStorageContextManager}from"@opentelemetry/context-async-hooks";import{CompositePropagator,W3CBaggagePropagator,W3CTraceContextPropagator,getStringFromEnv}from"@opentelemetry/core";import{OTLPLogExporter}from"@opentelemetry/exporter-logs-otlp-http";import{AggregationTemporalityPreference,OTLPMetricExporter}from"@opentelemetry/exporter-metrics-otlp-http";import{OTLPTraceExporter}from"@opentelemetry/exporter-trace-otlp-http";import{registerInstrumentations}from"@opentelemetry/instrumentation";import{UndiciInstrumentation}from"@opentelemetry/instrumentation-undici";import{defaultServiceName,resourceFromAttributes}from"@opentelemetry/resources";import{BatchLogRecordProcessor,LoggerProvider}from"@opentelemetry/sdk-logs";import{MeterProvider,PeriodicExportingMetricReader}from"@opentelemetry/sdk-metrics";import{BatchSpanProcessor}from"@opentelemetry/sdk-trace-base";import{NodeTracerProvider}from"@opentelemetry/sdk-trace-node";let registered=!1;function matchesAny(url,patterns){for(let pattern of patterns)if(pattern instanceof RegExp?pattern.test(url):url.includes(pattern))return!0;return!1}async function register(opts={}){if(registered)return;let env=readInterfereServerEnv();if(!env.publicKey)return;let{slug:releaseSlug}=resolveReleaseSlug();releaseSlug||console.warn("[interfere] No commit SHA available; server spans will ship without `release.slug`. Set `INTERFERE_SOURCE_ID` (or any of `VERCEL_GIT_COMMIT_SHA`, `GITHUB_SHA`) on the runtime env.");let{publicKey}=env,tracesUrl=drainUrl(env.apiUrl,`traces`,publicKey),logsUrl=drainUrl(env.apiUrl,`logs`,publicKey),metricsUrl=drainUrl(env.apiUrl,`metrics`,publicKey),ignoreUrls=[tracesUrl,logsUrl,metricsUrl,...opts.ignoreUrls??[]],propagateContextUrls=opts.propagateContextUrls??[],bakedTracing=serverTracingModeSchema.safeParse(import.meta.env?.VITE_INTERFERE_SERVER_TRACING),bakedLogs=logSourcingModeSchema.safeParse(import.meta.env?.VITE_INTERFERE_LOG_SOURCING),serverTracingMode=bakedTracing.success?bakedTracing.data:readServerTracingMode(),isLogDrainMode=(bakedLogs.success?bakedLogs.data:readLogSourcingMode())===`drain`,exporterHeaders={"x-interfere-producer-version":PRODUCER_VERSION},resourceAttributes=omitUndefined({"deployment.environment.name":env.nodeEnvironment,"interfere.sdk.name":`@interfere/vite`,"interfere.sdk.version":PRODUCER_VERSION,"service.name":opts.serviceName||getStringFromEnv(`OTEL_SERVICE_NAME`)||defaultServiceName(),"service.namespace":opts.serviceNamespace,"telemetry.sdk.language":`nodejs`,"release.slug":releaseSlug||void 0}),resource=resourceFromAttributes(resourceAttributes),isDrainMode=serverTracingMode===`drain`,tracerProvider=new NodeTracerProvider({resource,spanProcessors:[new BaggageSpanProcessor(()=>!0),...isDrainMode?[]:[new BatchSpanProcessor(new OTLPTraceExporter({headers:exporterHeaders,url:tracesUrl}))]]});if(propagation.fields().length===0&&propagation.setGlobalPropagator(new CompositePropagator({propagators:[new W3CTraceContextPropagator,new FreshnessStampingPropagator,new W3CBaggagePropagator]})),tracerProvider.register({contextManager:new AsyncLocalStorageContextManager().enable()}),!isLogDrainMode){let loggerProvider=new LoggerProvider({processors:[new BatchLogRecordProcessor({exporter:new OTLPLogExporter({url:logsUrl,headers:exporterHeaders})})],resource}),{logs:logsApi}=await import(`@opentelemetry/api-logs`);logsApi.setGlobalLoggerProvider(loggerProvider)}let meterProvider=new MeterProvider({readers:[new PeriodicExportingMetricReader({exporter:new OTLPMetricExporter({url:metricsUrl,headers:exporterHeaders,temporalityPreference:AggregationTemporalityPreference.DELTA}),exportIntervalMillis:3e4})],resource});metrics.setGlobalMeterProvider(meterProvider),isDrainMode?console.info(`[interfere] server tracing in drain mode; SDK trace exporter + outbound HTTP instrumentation disabled — server traces come from the platform trace drain.`):registerInstrumentations({instrumentations:[new UndiciInstrumentation({ignoreRequestHook:req=>matchesAny(`${req.origin}${req.path}`,ignoreUrls),requestHook:(span,req)=>{matchesAny(`${req.origin}${req.path}`,propagateContextUrls)&&span.setAttribute(`interfere.propagated`,!0)}})],meterProvider,tracerProvider}),!isLogDrainMode&&opts.consoleBridge!==!1&&bridgeConsoleToOtel(),registered=!0,await fetchAndCacheRemoteConfig()}export{register};
1
+ import{PRODUCER_VERSION}from"../../version.mjs";import{resolveReleaseSlug}from"../release-slug.mjs";import{readInterfereServerEnv}from"./env.mjs";import{fetchAndCacheRemoteConfig}from"./remote-config.mjs";import{bridgeConsoleToOtel}from"./console-bridge.mjs";import{FreshnessStampingPropagator}from"./freshness-propagator.mjs";import{drainUrl}from"@interfere/constants/api";import{omitUndefined}from"@interfere/helpers/omit-undefined";import{metrics,propagation}from"@opentelemetry/api";import{resolveLogSourcingMode}from"@interfere/types/sdk/log-sourcing";import{VERCEL_EXPORT_TIMEOUT_MS,resolveServerTracing}from"@interfere/types/sdk/server-tracing";import{BaggageSpanProcessor}from"@opentelemetry/baggage-span-processor";import{AsyncLocalStorageContextManager}from"@opentelemetry/context-async-hooks";import{CompositePropagator,W3CBaggagePropagator,W3CTraceContextPropagator,getStringFromEnv}from"@opentelemetry/core";import{OTLPLogExporter}from"@opentelemetry/exporter-logs-otlp-http";import{AggregationTemporalityPreference,OTLPMetricExporter}from"@opentelemetry/exporter-metrics-otlp-http";import{OTLPTraceExporter}from"@opentelemetry/exporter-trace-otlp-http";import{registerInstrumentations}from"@opentelemetry/instrumentation";import{UndiciInstrumentation}from"@opentelemetry/instrumentation-undici";import{defaultServiceName,resourceFromAttributes}from"@opentelemetry/resources";import{BatchLogRecordProcessor,LoggerProvider}from"@opentelemetry/sdk-logs";import{MeterProvider,PeriodicExportingMetricReader}from"@opentelemetry/sdk-metrics";import{BatchSpanProcessor}from"@opentelemetry/sdk-trace-base";import{NodeTracerProvider}from"@opentelemetry/sdk-trace-node";let registered=!1;function matchesAny(url,patterns){for(let pattern of patterns)if(pattern instanceof RegExp?pattern.test(url):url.includes(pattern))return!0;return!1}async function register(opts={}){if(registered)return;let env=readInterfereServerEnv();if(!env.publicKey)return;let{slug:releaseSlug}=resolveReleaseSlug();releaseSlug||console.warn("[interfere] No commit SHA available; server spans will ship without `release.slug`. Set `INTERFERE_SOURCE_ID` (or any of `VERCEL_GIT_COMMIT_SHA`, `GITHUB_SHA`) on the runtime env.");let{publicKey}=env,tracesUrl=drainUrl(env.apiUrl,`traces`,publicKey),logsUrl=drainUrl(env.apiUrl,`logs`,publicKey),metricsUrl=drainUrl(env.apiUrl,`metrics`,publicKey),ignoreUrls=[tracesUrl,logsUrl,metricsUrl,...opts.ignoreUrls??[]],propagateContextUrls=opts.propagateContextUrls??[],tracing=resolveServerTracing({injected:import.meta.env?.VITE_INTERFERE_SERVER_TRACING,platform:import.meta.env?.VITE_INTERFERE_PLATFORM}),isLogDrainMode=resolveLogSourcingMode(import.meta.env?.VITE_INTERFERE_LOG_SOURCING)===`drain`,exportTimeout=tracing.vercel?{timeoutMillis:VERCEL_EXPORT_TIMEOUT_MS}:{},batchTimeout=tracing.vercel?{exportTimeoutMillis:VERCEL_EXPORT_TIMEOUT_MS}:{},exporterHeaders={"x-interfere-producer-version":PRODUCER_VERSION},resourceAttributes=omitUndefined({"deployment.environment.name":env.nodeEnvironment,"interfere.sdk.name":`@interfere/vite`,"interfere.sdk.version":PRODUCER_VERSION,"service.name":opts.serviceName||getStringFromEnv(`OTEL_SERVICE_NAME`)||import.meta.env.VITE_INTERFERE_SERVICE_NAME||defaultServiceName(),"service.namespace":opts.serviceNamespace,"telemetry.sdk.language":`nodejs`,"release.slug":releaseSlug||void 0}),resource=resourceFromAttributes(resourceAttributes),isDrainMode=tracing.mode===`drain`,tracerProvider=new NodeTracerProvider({resource,spanProcessors:[new BaggageSpanProcessor(()=>!0),...isDrainMode?[]:[new BatchSpanProcessor(new OTLPTraceExporter({...exportTimeout,headers:exporterHeaders,url:tracesUrl}),batchTimeout)]]});if(propagation.fields().length===0&&propagation.setGlobalPropagator(new CompositePropagator({propagators:[new W3CTraceContextPropagator,new FreshnessStampingPropagator,new W3CBaggagePropagator]})),tracerProvider.register({contextManager:new AsyncLocalStorageContextManager().enable()}),!isLogDrainMode){let loggerProvider=new LoggerProvider({processors:[new BatchLogRecordProcessor({...batchTimeout,exporter:new OTLPLogExporter({...exportTimeout,url:logsUrl,headers:exporterHeaders})})],resource}),{logs:logsApi}=await import(`@opentelemetry/api-logs`);logsApi.setGlobalLoggerProvider(loggerProvider)}let meterProvider=new MeterProvider({readers:[new PeriodicExportingMetricReader({exporter:new OTLPMetricExporter({url:metricsUrl,headers:exporterHeaders,temporalityPreference:AggregationTemporalityPreference.DELTA}),exportIntervalMillis:3e4})],resource});metrics.setGlobalMeterProvider(meterProvider),isDrainMode?console.info(`[interfere] server tracing in drain mode; SDK trace exporter + outbound HTTP instrumentation disabled — server traces come from the platform trace drain.`):registerInstrumentations({instrumentations:[new UndiciInstrumentation({ignoreRequestHook:req=>matchesAny(`${req.origin}${req.path}`,ignoreUrls),requestHook:(span,req)=>{matchesAny(`${req.origin}${req.path}`,propagateContextUrls)&&span.setAttribute(`interfere.propagated`,!0)}})],meterProvider,tracerProvider}),!isLogDrainMode&&opts.consoleBridge!==!1&&bridgeConsoleToOtel(),registered=!0,await fetchAndCacheRemoteConfig()}export{register};
package/dist/package.mjs CHANGED
@@ -1 +1 @@
1
- var name=`@interfere/vite`,version=`12.2.0`;export{name,version};
1
+ var name=`@interfere/vite`,version=`12.3.1`;export{name,version};
package/dist/plugin.d.mts CHANGED
@@ -1,54 +1,6 @@
1
+ import { InterfereViteOptions } from "./internal/plugin-options.mjs";
1
2
  import { Plugin } from "vite";
2
3
  //#region src/plugin.d.ts
3
- export interface InterfereViteOptions {
4
- /** Writes private build evidence outside public and build output directories. */
5
- buildReceipt?: string;
6
- /**
7
- * The endpoint the browser SDK posts telemetry to. Defaults to
8
- * Interfere's endpoint. Set it to a subdomain of your own site that
9
- * resolves to Interfere via a DNS record (e.g.
10
- * `"https://in.example.com"`) to survive ad-blockers, or to a
11
- * regional/local endpoint. Must be a full URL.
12
- */
13
- apiHost?: string;
14
- /**
15
- * Override the commit SHA used to derive the release slug. Defaults
16
- * to `VITE_INTERFERE_COMMIT_SHA` env then `git rev-parse HEAD`.
17
- * Set this when the build runs outside a git checkout (e.g. a
18
- * Docker layer-cached image build).
19
- */
20
- commitSha?: string;
21
- /** Separates source maps for distinct bundles built from the same commit. */
22
- buildVariant?: string;
23
- /**
24
- * The OTel `service.name` the browser SDK reports when `init()` is called
25
- * without one. Defaults to `VITE_INTERFERE_SERVICE_NAME`, then to the
26
- * project package name without its scope (`@acme/storefront` →
27
- * `storefront`). An explicit `init({ serviceName })` still wins.
28
- */
29
- serviceName?: string;
30
- /**
31
- * Directory (relative to the project root) where the SSR/server build
32
- * writes its `.js.map` files, when that isn't `<build.outDir>/server`.
33
- * Set this for server hosts that emit outside Vite's `outDir` — e.g.
34
- * Nitro's `".output/server"`.
35
- */
36
- serverSourcemapsDir?: string;
37
- /**
38
- * Upload source maps for the release. A production build registers a release
39
- * when `INTERFERE_API_KEY` is set, and skips it with a warning when the key is
40
- * missing. `upload: false` registers the release without uploading source maps.
41
- * Interfere API failures, including a rejected secret key, never fail the
42
- * build: they are logged, local source maps are deleted and the build finishes.
43
- */
44
- sourceMaps?: {
45
- upload?: boolean;
46
- };
47
- /**
48
- * Turns Interfere off for this build: no release, no source-map upload, and
49
- * the browser and server SDKs send no events.
50
- */
51
- disable?: boolean;
52
- }
53
4
  export declare function interfere(options?: InterfereViteOptions): Plugin<any>;
54
- //#endregion
5
+ //#endregion
6
+ export type { InterfereViteOptions };
package/dist/plugin.mjs CHANGED
@@ -1,2 +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{resolveServiceName}from"./internal/service-name.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,PUBLIC_KEY_HEADER}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,root){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,serviceName:resolveServiceName({option:options.serviceName,env,root}),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),"import.meta.env.VITE_INTERFERE_SERVICE_NAME":JSON.stringify(browser.serviceName??null)}}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}/apps/${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}/apps/${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,publicKey}=state,client=new InterfereClient({fetch:sourceMapUploadFetch,baseUrl:apiUrl,apiKey:{token:apiKey},headers:{"user-agent":PRODUCER_VERSION,[PUBLIC_KEY_HEADER]:publicKey},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 resolveRoot(userConfig){return userConfig.root?path.resolve(process.cwd(),userConfig.root):process.cwd()}function resolveEnvDir(userConfig){let root=resolveRoot(userConfig);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,resolveRoot(userConfig)),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,config.root),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};
1
+ import{PRODUCER_VERSION}from"./version.mjs";import{runGitCommand}from"./internal/build/git.mjs";import{rejectRemovedOptions}from"./internal/plugin-options.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{resolveBuildServiceName}from"@interfere/build/service-name";import{sourceMapUploadFetch}from"@interfere/build/source-map-upload-fetch";import{InterfereClient}from"@interfere/javascript";import{createBuildConfigResolver}from"@interfere/build/config";import{API_URL,PUBLIC_KEY_HEADER}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,root){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,serviceName:resolveBuildServiceName({option:options.serviceName,env,root,legacyEnvName:`VITE_INTERFERE_SERVICE_NAME`}),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),"import.meta.env.VITE_INTERFERE_SERVICE_NAME":JSON.stringify(browser.serviceName??null)}}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}/apps/${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}/apps/${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,publicKey}=state,client=new InterfereClient({fetch:sourceMapUploadFetch,baseUrl:apiUrl,apiKey:{token:apiKey},headers:{"user-agent":PRODUCER_VERSION,[PUBLIC_KEY_HEADER]:publicKey},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,retainSourceMaps=isProductionBuild&&options.sourceMaps?.retain===!0,release=isProductionBuild&&!retainSourceMaps?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,retainSourceMaps,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.retainSourceMaps&&state.logger.info(`[interfere] source maps are retained in the build output for a release step to upload per environment.`),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 resolveRoot(userConfig){return userConfig.root?path.resolve(process.cwd(),userConfig.root):process.cwd()}function resolveEnvDir(userConfig){let root=resolveRoot(userConfig);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?.retain===!0||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 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||state?.retainSourceMaps?{...output,sourcemapDebugIds:!1}:null},renderChunk:{order:`post`,handler(code){return state?.uploadSourceMaps||state?.retainSourceMaps?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,resolveRoot(userConfig)),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":JSON.stringify(baked?.serverTracing??null),"import.meta.env.VITE_INTERFERE_LOG_SOURCING":JSON.stringify(baked?.logSourcing??null),"import.meta.env.VITE_INTERFERE_PLATFORM":baked?.platform===void 0?`undefined`:JSON.stringify(baked.platform),"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,config.root),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};
@@ -0,0 +1,43 @@
1
+ //#region src/release.d.ts
2
+ export interface ReleaseLogger {
3
+ error: (message: string) => void;
4
+ info: (message: string) => void;
5
+ warn: (message: string) => void;
6
+ }
7
+ export interface ReleaseBuildOutputOptions {
8
+ /** Secret API key of the surface this build belongs to; without it the retained source maps are only deleted. */
9
+ apiKey?: string | undefined;
10
+ /** Interfere API origin; defaults to the hosted API. */
11
+ apiUrl?: string | undefined;
12
+ /** Build output directory holding the chunks and the source maps the build retained. */
13
+ distDir: string;
14
+ /** Deployment environment recorded on the release; defaults to the deploy provider's environment variables. */
15
+ environment?: string | null | undefined;
16
+ logger?: ReleaseLogger | undefined;
17
+ /** Project root the build ran in; defaults to the working directory. */
18
+ projectDir?: string | undefined;
19
+ /** Public key of the surface, sent with the release requests. */
20
+ publicKey?: string | undefined;
21
+ /** The private build receipt the plugin wrote (`interfere({ buildReceipt })`). */
22
+ receipt: string;
23
+ }
24
+ export type ReleaseBuildOutputResult = {
25
+ fileCount: number;
26
+ releaseSlug: string;
27
+ status: "released";
28
+ } | {
29
+ reason: string;
30
+ status: "skipped";
31
+ } | {
32
+ message: string;
33
+ status: "failed";
34
+ };
35
+ /**
36
+ * Registers a release for a build that retained its source maps
37
+ * (`interfere({ sourceMaps: { retain: true } })`) and uploads them, then
38
+ * deletes the maps from the output so they are never served. Run it once per
39
+ * deployment environment on the same build output. API failures are reported,
40
+ * never thrown, and the maps are deleted either way.
41
+ */
42
+ export declare function releaseBuildOutput(options: ReleaseBuildOutputOptions): Promise<ReleaseBuildOutputResult>;
43
+ //#endregion
@@ -0,0 +1 @@
1
+ import{PRODUCER_VERSION}from"./version.mjs";import{runReleasePipeline}from"./internal/build/pipeline.mjs";import{discoverByPairing}from"./internal/build/source-maps.mjs";import{cleanupSourceMaps}from"@interfere/build/cleanup-source-maps";import{sourceMapUploadFetch}from"@interfere/build/source-map-upload-fetch";import{InterfereClient,InterfereError}from"@interfere/javascript";import{API_URL,PUBLIC_KEY_HEADER}from"@interfere/constants/api";import{releaseSlugSchema}from"@interfere/types/releases/slug";import{resolveDeployEnvironment}from"@interfere/types/sdk/deploy-environment";import path from"node:path";import{readFile}from"node:fs/promises";import{z}from"zod";const receiptSchema=z.object({build:z.object({buildId:z.string().nullable(),commitSha:z.string().nullable(),releaseSlug:releaseSlugSchema.nullable()}),schemaVersion:z.literal(1)}),releaseUploadConflictSchema=z.object({code:z.literal(`RELEASE_UPLOAD_CONFLICT`)});async function readReceipt(projectDir,receipt){return receiptSchema.parse(JSON.parse(await readFile(path.resolve(projectDir,receipt),`utf8`)))}async function discover(distDir,projectDir){return discoverByPairing({distDir,projectDir,publicPathRewrites:[{from:`public`,to:``}],subtrees:[`.`]})}async function releaseBuildOutput(options){let logger=options.logger??console,projectDir=options.projectDir??process.cwd(),discovered=await discover(options.distDir,projectDir),finish=async result=>{for(let failure of await cleanupSourceMaps(discovered.files))logger.warn(`[interfere] cleanup left ${failure.absolute} on disk: ${failure.code??failure.message}`);return result};if(!options.apiKey)return finish({reason:`INTERFERE_API_KEY is not set; retained source maps were deleted without a release.`,status:`skipped`});let{build}=await readReceipt(projectDir,options.receipt);if(!build.releaseSlug||!build.commitSha)return finish({reason:`the build receipt has no release identity (no commit SHA at build time).`,status:`skipped`});if(discovered.files.length===0)return finish({reason:`no source maps paired with chunks under ${options.distDir}.`,status:`skipped`});let environment=options.environment===void 0?resolveDeployEnvironment(process.env):options.environment,client=new InterfereClient({apiKey:{token:options.apiKey},baseUrl:options.apiUrl||API_URL,fetch:sourceMapUploadFetch,headers:{"user-agent":PRODUCER_VERSION,[PUBLIC_KEY_HEADER]:options.publicKey},maxRetries:4,timeoutInSeconds:10});try{let releasesConfig=await client.releases.getReleasesConfig({abortSignal:AbortSignal.timeout(6e4)}),result=await runReleasePipeline({buildId:build.buildId??build.commitSha,bundler:`rollup`,client,discovery:discovered,environment,producerVersion:PRODUCER_VERSION,releaseSlug:build.releaseSlug,releasesConfig,uploadSourceMaps:!0});if(!result.ok)return await finish({reason:`release skipped: ${result.failure.kind} for ${result.failure.config.org.slug}/${result.failure.config.surface.slug}.`,status:`skipped`});for(let failure of result.data.cleanupFailures)logger.warn(`[interfere] cleanup left ${failure.absolute} on disk: ${failure.code??failure.message}`);return logger.info(`[interfere] release ${result.data.releaseSlug} confirmed for ${environment??`no environment`}: ${result.data.fileCount} maps, ${result.data.timing?.total??0}ms.`),{fileCount:result.data.fileCount,releaseSlug:result.data.releaseSlug,status:`released`}}catch(error){return error instanceof InterfereError&&error.statusCode===409&&releaseUploadConflictSchema.safeParse(error.body).success?await finish({reason:`release ${build.releaseSlug} for ${environment??`no environment`} already holds source maps from another build of this bundle (receipt commit ${build.commitSha}); keeping the existing release.`,status:`skipped`}):await finish({message:error instanceof Error?error.message:String(error),status:`failed`})}}export{releaseBuildOutput};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@interfere/vite",
3
- "version": "12.2.0",
3
+ "version": "12.3.1",
4
4
  "description": "Vite build plugin and client + server initialization for Interfere.",
5
5
  "keywords": [
6
6
  "error-tracking",
@@ -51,6 +51,11 @@
51
51
  "monorepo": "./src/tanstack-start.ts",
52
52
  "types": "./dist/tanstack-start.d.mts",
53
53
  "default": "./dist/tanstack-start.mjs"
54
+ },
55
+ "./release": {
56
+ "monorepo": "./src/release.ts",
57
+ "types": "./dist/release.d.mts",
58
+ "default": "./dist/release.mjs"
54
59
  }
55
60
  },
56
61
  "publishConfig": {
@@ -62,12 +67,12 @@
62
67
  "typecheck": "bun check"
63
68
  },
64
69
  "dependencies": {
65
- "@interfere/build": "^0.3.3",
70
+ "@interfere/build": "^0.4.1",
66
71
  "@interfere/constants": "^9.3.0",
67
72
  "@interfere/helpers": "^9.3.0",
68
- "@interfere/javascript": "^0.3.0",
69
- "@interfere/react": "^12.2.0",
70
- "@interfere/types": "^10.4.0",
73
+ "@interfere/javascript": "^0.3.1",
74
+ "@interfere/react": "^12.3.1",
75
+ "@interfere/types": "^10.5.1",
71
76
  "@opentelemetry/api": "1.9.1",
72
77
  "@opentelemetry/api-logs": "0.222.0",
73
78
  "@opentelemetry/baggage-span-processor": "0.5.0",
@@ -1,12 +0,0 @@
1
- //#region src/internal/service-name.d.ts
2
- /**
3
- * The `service.name` a build stamps into the page when `init()` is called
4
- * without one: the plugin option, then `VITE_INTERFERE_SERVICE_NAME`, then
5
- * the project's package name without its scope.
6
- */
7
- export declare function resolveServiceName(input: {
8
- option: string | undefined;
9
- env: Record<string, string | undefined>;
10
- root: string;
11
- }): string | undefined;
12
- //#endregion
@@ -1 +0,0 @@
1
- import{existsSync,readFileSync}from"node:fs";import path from"node:path";const SCOPE_PREFIX=/^@[^/]+\//u;function nonEmpty(value){return value?.trim()||void 0}function unscopedPackageName(name){return nonEmpty(name.replace(SCOPE_PREFIX,``))}function packageServiceName(root){let manifest=path.join(root,`package.json`);if(!existsSync(manifest))return;let parsed;try{parsed=JSON.parse(readFileSync(manifest,`utf8`))}catch{return}if(typeof parsed!=`object`||!parsed||!(`name`in parsed))return;let{name}=parsed;return typeof name==`string`?unscopedPackageName(name):void 0}function resolveServiceName(input){return nonEmpty(input.option)??nonEmpty(input.env.VITE_INTERFERE_SERVICE_NAME)??packageServiceName(input.root)}export{resolveServiceName};