@interfere/next 1.0.5 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/config.d.mts +11 -1
  2. package/dist/config.d.mts.map +1 -1
  3. package/dist/config.mjs +28 -40
  4. package/dist/config.mjs.map +1 -1
  5. package/dist/internal/build/pipeline.d.mts +49 -0
  6. package/dist/internal/build/pipeline.d.mts.map +1 -0
  7. package/dist/internal/build/pipeline.mjs +67 -0
  8. package/dist/internal/build/pipeline.mjs.map +1 -0
  9. package/dist/internal/build/release/destinations/vercel.d.mts +1 -1
  10. package/dist/internal/build/release/destinations/vercel.mjs +2 -2
  11. package/dist/internal/build/release/destinations/vercel.mjs.map +1 -1
  12. package/dist/internal/build/release/index.d.mts +4 -3
  13. package/dist/internal/build/release/index.d.mts.map +1 -1
  14. package/dist/internal/build/release/index.mjs +8 -14
  15. package/dist/internal/build/release/index.mjs.map +1 -1
  16. package/dist/internal/build/source-maps/discover.mjs +1 -1
  17. package/dist/internal/build/source-maps/index.d.mts +23 -20
  18. package/dist/internal/build/source-maps/index.d.mts.map +1 -1
  19. package/dist/internal/build/source-maps/index.mjs +21 -27
  20. package/dist/internal/build/source-maps/index.mjs.map +1 -1
  21. package/dist/internal/env.mjs.map +1 -1
  22. package/dist/internal/logger.d.mts +1 -0
  23. package/dist/internal/logger.d.mts.map +1 -1
  24. package/dist/internal/logger.mjs +23 -7
  25. package/dist/internal/logger.mjs.map +1 -1
  26. package/dist/internal/route/handle-post.mjs +4 -2
  27. package/dist/internal/route/handle-post.mjs.map +1 -1
  28. package/dist/internal/route/sw-script.d.mts +1 -1
  29. package/dist/internal/route/sw-script.mjs +1 -1
  30. package/dist/internal/route/sw-script.mjs.map +1 -1
  31. package/package.json +9 -10
package/dist/config.d.mts CHANGED
@@ -6,6 +6,16 @@ interface InterfereConfig extends Partial<Pick<Envelope, "buildId" | "releaseId"
6
6
  interface NextConfigWithInterfere extends NextConfig {
7
7
  interfere?: InterfereConfig;
8
8
  }
9
+ interface ResolvedBuildConfig {
10
+ readonly apiKey: string | null;
11
+ readonly apiUrl: string;
12
+ readonly buildId: string | null;
13
+ readonly releaseId: string | null;
14
+ }
9
15
  declare function withInterfere(nextConfig?: NextConfigWithInterfere): NextConfig;
16
+ interface ProductionCompileContext {
17
+ readonly distDir: string;
18
+ readonly projectDir: string;
19
+ }
10
20
  //#endregion
11
- export { InterfereConfig, NextConfigWithInterfere, withInterfere };
21
+ export { InterfereConfig, NextConfigWithInterfere, ProductionCompileContext, ResolvedBuildConfig, withInterfere };
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.mts","names":[],"sources":["../src/config.ts"],"mappings":";;;;UAeiB,eAAA,SACP,OAAA,CAAQ,IAAA,CAAK,QAAA;AAAA,UAEN,uBAAA,SAAgC,UAAA;EAC/C,SAAA,GAAY,eAAA;AAAA;AAAA,iBAUE,aAAA,CACd,UAAA,GAAY,uBAAA,GACX,UAAA"}
1
+ {"version":3,"file":"config.d.mts","names":[],"sources":["../src/config.ts"],"mappings":";;;;UAeiB,eAAA,SACP,OAAA,CAAQ,IAAA,CAAK,QAAA;AAAA,UAEN,uBAAA,SAAgC,UAAA;EAC/C,SAAA,GAAY,eAAA;AAAA;AAAA,UAGG,mBAAA;EAAA,SACN,MAAA;EAAA,SACA,MAAA;EAAA,SACA,OAAA;EAAA,SACA,SAAA;AAAA;AAAA,iBAGK,aAAA,CACd,UAAA,GAAY,uBAAA,GACX,UAAA;AAAA,UAqFc,wBAAA;EAAA,SACN,OAAA;EAAA,SACA,UAAA;AAAA"}
package/dist/config.mjs CHANGED
@@ -18,7 +18,7 @@ function withInterfere(nextConfig = {}) {
18
18
  __INTERFERE_RELEASE_ID__: config.releaseId
19
19
  }
20
20
  });
21
- if (config.apiKey !== null && !build.webpack && !build.turbopack) throw new Error("[Interfere] INTERFERE_API_KEY is set but no instrumentation-client file was found. Create an instrumentation-client.ts file in your project root or src/ directory. See: https://interfere.com/docs/nextjs/setup");
21
+ if (config.apiKey !== null && !build.webpack && !build.turbopack) return log.fatal("Missing instrumentation client", ["INTERFERE_API_KEY is set but no instrumentation-client file was found.", "Create an instrumentation-client.ts file in your project root or src/ directory."]);
22
22
  const existingHook = compiler?.runAfterProductionCompile;
23
23
  return {
24
24
  ...rest,
@@ -38,11 +38,12 @@ function withInterfere(nextConfig = {}) {
38
38
  function resolveBuildConfig(interfere) {
39
39
  const { apiKey, apiUrl } = readInterfereEnv();
40
40
  const buildId = parseEnvValue(interfere?.buildId) ?? readFirstEnvValue(process.env, releaseSourceIdEnvKeys) ?? runGitCommand("git rev-parse HEAD");
41
+ const releaseId = parseEnvValue(interfere?.releaseId) ?? readFirstEnvValue(process.env, releaseDestinationIdEnvKeys) ?? buildId;
41
42
  return {
42
- apiKey,
43
+ apiKey: apiKey ?? null,
43
44
  apiUrl,
44
45
  buildId,
45
- releaseId: parseEnvValue(interfere?.releaseId) ?? readFirstEnvValue(process.env, releaseDestinationIdEnvKeys) ?? buildId
46
+ releaseId
46
47
  };
47
48
  }
48
49
  function mergeEnvConfig(userEnv, config) {
@@ -54,57 +55,44 @@ function mergeEnvConfig(userEnv, config) {
54
55
  ...userEnv
55
56
  };
56
57
  }
58
+ function isNetworkError(error) {
59
+ if (error instanceof TypeError) return true;
60
+ if (error instanceof Error) {
61
+ const msg = error.message.toLowerCase();
62
+ return msg.includes("econnrefused") || msg.includes("enotfound") || msg.includes("fetch failed") || msg.includes("network");
63
+ }
64
+ return false;
65
+ }
57
66
  async function runBuildReleasePipeline(context, config) {
58
67
  const { apiKey, apiUrl, buildId } = config;
59
- if (apiKey === null) throw new Error("[Interfere] INTERFERE_API_KEY is not set. withInterfere() requires an API key to upload source maps during production builds. Set the INTERFERE_API_KEY environment variable, or remove withInterfere() from your Next.js config. See: https://interfere.com/docs/nextjs/setup");
60
- if (buildId === null) {
61
- log.error("Build ID missing", [
62
- "Could not resolve a build ID from config, environment variables, or git.",
63
- "Source maps will not be uploaded and errors will have unresolved stack traces.",
64
- "Set INTERFERE_BUILD_ID, or ensure git is available in your build environment."
65
- ]);
66
- return;
67
- }
68
- const { createRelease } = await import("./internal/build/release/index.mjs");
69
- const { runSourceMapPipeline } = await import("./internal/build/source-maps/index.mjs");
70
- const client = {
71
- async createRelease() {
72
- const release = await createRelease(apiKey, apiUrl, buildId);
73
- return {
74
- slug: release.destination.slug,
75
- orgSlug: release.org.slug,
76
- buildId: release.build.hash ?? buildId
77
- };
78
- },
79
- async uploadSourceMaps(releaseSlug, body) {
80
- const url = `${apiUrl}/v1/releases/${releaseSlug}/source-maps`;
81
- const response = await fetch(url, {
82
- method: "POST",
83
- headers: { "x-api-key": apiKey },
84
- body
85
- });
86
- if (!response.ok) {
87
- const text = await response.text().catch(() => "");
88
- throw new Error(`Source map upload failed: ${response.status} ${response.statusText} ${text}`);
89
- }
90
- }
91
- };
68
+ if (!apiKey) return log.fatal("API key not set", [
69
+ "withInterfere() requires an API key to upload source maps during production builds.",
70
+ "Set the INTERFERE_API_KEY environment variable, or remove withInterfere() from your Next.js config.",
71
+ "See: https://interfere.com/docs/nextjs/setup"
72
+ ]);
73
+ if (!buildId) return log.fatal("Build ID missing", ["Could not resolve a build ID from config, environment variables, or git.", "Set INTERFERE_BUILD_ID, or ensure git is available in your build environment."]);
74
+ const { runBuildPipeline } = await import("./internal/build/pipeline.mjs");
92
75
  try {
93
- const result = await runSourceMapPipeline(context.projectDir, context.distDir, client);
76
+ const result = await runBuildPipeline(context, {
77
+ ...config,
78
+ apiKey,
79
+ buildId
80
+ });
94
81
  if (!result.ready) {
95
82
  log.warn("Skipping", ["No source maps found"]);
96
83
  return;
97
84
  }
98
85
  log.info("Completed", [
99
- `https://interfere.com/~/${result.orgSlug}`,
100
- `Release: ${result.releaseSlug}`,
86
+ `https://interfere.com/~/${result.config?.org.slug}/surfaces/${result.config?.surface.slug}`,
87
+ `Release: ${result.release?.destination.slug ?? "Unknown"}`,
101
88
  `Build: ${result.buildId}`,
102
89
  `Artifacts: ${result.fileCount} source maps`
103
90
  ]);
104
91
  } catch (error) {
105
92
  const message = error instanceof Error ? error.message : String(error);
106
93
  if (apiUrl !== API_URL) {
107
- log.warn("Skipping release pipeline", [message, `Custom API URL (${apiUrl}) is unreachable — this is expected during local development.`]);
94
+ const isNetwork = isNetworkError(error);
95
+ log.warn("Skipping release pipeline", [message, isNetwork ? `${apiUrl} is unreachable — this is expected during local development.` : `${apiUrl} returned an error. Ensure the collector is running and configured correctly.`]);
108
96
  return;
109
97
  }
110
98
  log.error("Error", [message]);
@@ -1 +1 @@
1
- {"version":3,"file":"config.mjs","names":[],"sources":["../src/config.ts"],"sourcesContent":["import { API_URL } from \"@interfere/constants/api\";\nimport {\n releaseDestinationIdEnvKeys,\n releaseSourceIdEnvKeys,\n} from \"@interfere/types/integrations\";\nimport { parseEnvValue, readFirstEnvValue } from \"@interfere/types/sdk/env\";\nimport type { Envelope } from \"@interfere/types/sdk/envelope\";\n\nimport type { NextConfig } from \"next\";\n\nimport { configureBuild } from \"./internal/build/configure-build.js\";\nimport { runGitCommand } from \"./internal/build/release/git.js\";\nimport { readInterfereEnv } from \"./internal/env.js\";\nimport { log } from \"./internal/logger.js\";\n\nexport interface InterfereConfig\n extends Partial<Pick<Envelope, \"buildId\" | \"releaseId\">> {}\n\nexport interface NextConfigWithInterfere extends NextConfig {\n interfere?: InterfereConfig;\n}\n\ninterface ResolvedBuildConfig {\n readonly apiKey: string | null;\n readonly apiUrl: string;\n readonly buildId: string | null;\n readonly releaseId: string | null;\n}\n\nexport function withInterfere(\n nextConfig: NextConfigWithInterfere = {}\n): NextConfig {\n const {\n interfere,\n env: userEnv,\n webpack,\n turbopack,\n compiler,\n productionBrowserSourceMaps,\n ...rest\n } = nextConfig;\n\n const config = resolveBuildConfig(interfere);\n const build = configureBuild({\n existingWebpack: webpack,\n existingTurbopack: turbopack,\n projectDir: process.cwd(),\n values: {\n __INTERFERE_BUILD_ID__: config.buildId,\n __INTERFERE_RELEASE_ID__: config.releaseId,\n },\n });\n\n if (config.apiKey !== null && !build.webpack && !build.turbopack) {\n throw new Error(\n \"[Interfere] INTERFERE_API_KEY is set but no instrumentation-client file was found. \" +\n \"Create an instrumentation-client.ts file in your project root or src/ directory. \" +\n \"See: https://interfere.com/docs/nextjs/setup\"\n );\n }\n\n const existingHook = (compiler as NextCompilerWithProductionHook | undefined)\n ?.runAfterProductionCompile;\n\n return {\n ...rest,\n env: mergeEnvConfig(userEnv, config),\n compiler: {\n ...(compiler ?? {}),\n async runAfterProductionCompile(context: ProductionCompileContext) {\n if (existingHook) {\n await existingHook(context);\n }\n\n await runBuildReleasePipeline(context, config);\n },\n } as NextConfig[\"compiler\"],\n webpack: build.webpack,\n turbopack: build.turbopack,\n productionBrowserSourceMaps:\n config.apiKey === null ? productionBrowserSourceMaps : true,\n };\n}\n\nfunction resolveBuildConfig(interfere?: InterfereConfig): ResolvedBuildConfig {\n const { apiKey, apiUrl } = readInterfereEnv();\n\n const buildId =\n parseEnvValue(interfere?.buildId) ??\n readFirstEnvValue(process.env, releaseSourceIdEnvKeys) ??\n runGitCommand(\"git rev-parse HEAD\");\n const releaseId =\n parseEnvValue(interfere?.releaseId) ??\n readFirstEnvValue(process.env, releaseDestinationIdEnvKeys) ??\n buildId;\n\n return { apiKey, apiUrl, buildId, releaseId };\n}\n\nfunction mergeEnvConfig(\n userEnv: NextConfig[\"env\"] | undefined,\n config: ResolvedBuildConfig\n): NextConfig[\"env\"] {\n const merged: Record<string, string> = {};\n\n if (config.buildId !== null) {\n merged.NEXT_PUBLIC_INTERFERE_BUILD_ID = config.buildId;\n }\n\n if (config.releaseId !== null) {\n merged.NEXT_PUBLIC_INTERFERE_RELEASE_ID = config.releaseId;\n }\n\n return { ...merged, ...userEnv };\n}\n\ninterface ProductionCompileContext {\n readonly distDir: string;\n readonly projectDir: string;\n}\n\ntype NextCompilerWithProductionHook = NonNullable<NextConfig[\"compiler\"]> & {\n runAfterProductionCompile?: (\n context: ProductionCompileContext\n ) => void | Promise<void>;\n};\n\nasync function runBuildReleasePipeline(\n context: ProductionCompileContext,\n config: ResolvedBuildConfig\n): Promise<void> {\n const { apiKey, apiUrl, buildId } = config;\n\n if (apiKey === null) {\n throw new Error(\n \"[Interfere] INTERFERE_API_KEY is not set. \" +\n \"withInterfere() requires an API key to upload source maps during production builds. \" +\n \"Set the INTERFERE_API_KEY environment variable, or remove withInterfere() from your Next.js config. \" +\n \"See: https://interfere.com/docs/nextjs/setup\"\n );\n }\n\n if (buildId === null) {\n log.error(\"Build ID missing\", [\n \"Could not resolve a build ID from config, environment variables, or git.\",\n \"Source maps will not be uploaded and errors will have unresolved stack traces.\",\n \"Set INTERFERE_BUILD_ID, or ensure git is available in your build environment.\",\n ]);\n return;\n }\n\n const { createRelease } = await import(\"./internal/build/release/index.js\");\n const { runSourceMapPipeline } = await import(\n \"./internal/build/source-maps/index.js\"\n );\n\n const client: import(\"./internal/build/source-maps/index.js\").BuildClient = {\n async createRelease() {\n const release = await createRelease(apiKey, apiUrl, buildId);\n return {\n slug: release.destination.slug,\n orgSlug: release.org.slug,\n buildId: release.build.hash ?? buildId,\n };\n },\n async uploadSourceMaps(releaseSlug, body) {\n const url = `${apiUrl}/v1/releases/${releaseSlug}/source-maps`;\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"x-api-key\": apiKey },\n body,\n });\n\n if (!response.ok) {\n const text = await response.text().catch(() => \"\");\n throw new Error(\n `Source map upload failed: ${response.status} ${response.statusText} ${text}`\n );\n }\n },\n };\n\n try {\n const result = await runSourceMapPipeline(\n context.projectDir,\n context.distDir,\n client\n );\n\n if (!result.ready) {\n log.warn(\"Skipping\", [\"No source maps found\"]);\n return;\n }\n\n log.info(\"Completed\", [\n `https://interfere.com/~/${result.orgSlug}`,\n `Release: ${result.releaseSlug}`,\n `Build: ${result.buildId}`,\n `Artifacts: ${result.fileCount} source maps`,\n ]);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n\n if (apiUrl !== API_URL) {\n log.warn(\"Skipping release pipeline\", [\n message,\n `Custom API URL (${apiUrl}) is unreachable — this is expected during local development.`,\n ]);\n return;\n }\n\n log.error(\"Error\", [message]);\n throw error;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"config.mjs","names":[],"sources":["../src/config.ts"],"sourcesContent":["import { API_URL } from \"@interfere/constants/api\";\nimport {\n releaseDestinationIdEnvKeys,\n releaseSourceIdEnvKeys,\n} from \"@interfere/types/integrations\";\nimport { parseEnvValue, readFirstEnvValue } from \"@interfere/types/sdk/env\";\nimport type { Envelope } from \"@interfere/types/sdk/envelope\";\n\nimport type { NextConfig } from \"next\";\n\nimport { configureBuild } from \"./internal/build/configure-build.js\";\nimport { runGitCommand } from \"./internal/build/release/git.js\";\nimport { readInterfereEnv } from \"./internal/env.js\";\nimport { log } from \"./internal/logger.js\";\n\nexport interface InterfereConfig\n extends Partial<Pick<Envelope, \"buildId\" | \"releaseId\">> {}\n\nexport interface NextConfigWithInterfere extends NextConfig {\n interfere?: InterfereConfig;\n}\n\nexport interface ResolvedBuildConfig {\n readonly apiKey: string | null;\n readonly apiUrl: string;\n readonly buildId: string | null;\n readonly releaseId: string | null;\n}\n\nexport function withInterfere(\n nextConfig: NextConfigWithInterfere = {}\n): NextConfig {\n const {\n interfere,\n env: userEnv,\n webpack,\n turbopack,\n compiler,\n productionBrowserSourceMaps,\n ...rest\n } = nextConfig;\n\n const config = resolveBuildConfig(interfere);\n const build = configureBuild({\n existingWebpack: webpack,\n existingTurbopack: turbopack,\n projectDir: process.cwd(),\n values: {\n __INTERFERE_BUILD_ID__: config.buildId,\n __INTERFERE_RELEASE_ID__: config.releaseId,\n },\n });\n\n if (config.apiKey !== null && !build.webpack && !build.turbopack) {\n return log.fatal(\"Missing instrumentation client\", [\n \"INTERFERE_API_KEY is set but no instrumentation-client file was found.\",\n \"Create an instrumentation-client.ts file in your project root or src/ directory.\",\n ]);\n }\n\n const existingHook = (compiler as NextCompilerWithProductionHook | undefined)\n ?.runAfterProductionCompile;\n\n return {\n ...rest,\n env: mergeEnvConfig(userEnv, config),\n compiler: {\n ...(compiler ?? {}),\n async runAfterProductionCompile(context: ProductionCompileContext) {\n if (existingHook) {\n await existingHook(context);\n }\n\n await runBuildReleasePipeline(context, config);\n },\n } as NextConfig[\"compiler\"],\n webpack: build.webpack,\n turbopack: build.turbopack,\n productionBrowserSourceMaps:\n config.apiKey === null ? productionBrowserSourceMaps : true,\n };\n}\n\nfunction resolveBuildConfig(interfere?: InterfereConfig): ResolvedBuildConfig {\n const { apiKey, apiUrl } = readInterfereEnv();\n\n const buildId =\n parseEnvValue(interfere?.buildId) ??\n readFirstEnvValue(process.env, releaseSourceIdEnvKeys) ??\n runGitCommand(\"git rev-parse HEAD\");\n\n const releaseId =\n parseEnvValue(interfere?.releaseId) ??\n readFirstEnvValue(process.env, releaseDestinationIdEnvKeys) ??\n buildId;\n\n return { apiKey: apiKey ?? null, apiUrl, buildId, releaseId };\n}\n\nfunction mergeEnvConfig(\n userEnv: NextConfig[\"env\"] | undefined,\n config: ResolvedBuildConfig\n): NextConfig[\"env\"] {\n const merged: Record<string, string> = {};\n\n if (config.buildId !== null) {\n merged.NEXT_PUBLIC_INTERFERE_BUILD_ID = config.buildId;\n }\n\n if (config.releaseId !== null) {\n merged.NEXT_PUBLIC_INTERFERE_RELEASE_ID = config.releaseId;\n }\n\n return { ...merged, ...userEnv };\n}\n\nexport interface ProductionCompileContext {\n readonly distDir: string;\n readonly projectDir: string;\n}\n\ntype NextCompilerWithProductionHook = NonNullable<NextConfig[\"compiler\"]> & {\n runAfterProductionCompile?: (\n context: ProductionCompileContext\n ) => void | Promise<void>;\n};\n\nfunction isNetworkError(error: unknown): boolean {\n if (error instanceof TypeError) {\n return true;\n }\n if (error instanceof Error) {\n const msg = error.message.toLowerCase();\n return (\n msg.includes(\"econnrefused\") ||\n msg.includes(\"enotfound\") ||\n msg.includes(\"fetch failed\") ||\n msg.includes(\"network\")\n );\n }\n return false;\n}\n\nasync function runBuildReleasePipeline(\n context: ProductionCompileContext,\n config: ResolvedBuildConfig\n): Promise<void> {\n const { apiKey, apiUrl, buildId } = config;\n\n if (!apiKey) {\n return log.fatal(\"API key not set\", [\n \"withInterfere() requires an API key to upload source maps during production builds.\",\n \"Set the INTERFERE_API_KEY environment variable, or remove withInterfere() from your Next.js config.\",\n \"See: https://interfere.com/docs/nextjs/setup\",\n ]);\n }\n\n if (!buildId) {\n return log.fatal(\"Build ID missing\", [\n \"Could not resolve a build ID from config, environment variables, or git.\",\n \"Set INTERFERE_BUILD_ID, or ensure git is available in your build environment.\",\n ]);\n }\n\n const { runBuildPipeline } = await import(\"./internal/build/pipeline.js\");\n\n try {\n const result = await runBuildPipeline(context, {\n ...config,\n apiKey,\n buildId,\n });\n\n if (!result.ready) {\n log.warn(\"Skipping\", [\"No source maps found\"]);\n return;\n }\n\n log.info(\"Completed\", [\n `https://interfere.com/~/${result.config?.org.slug}/surfaces/${result.config?.surface.slug}`,\n `Release: ${result.release?.destination.slug ?? \"Unknown\"}`,\n `Build: ${result.buildId}`,\n `Artifacts: ${result.fileCount} source maps`,\n ]);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n\n if (apiUrl !== API_URL) {\n const isNetwork = isNetworkError(error);\n\n log.warn(\"Skipping release pipeline\", [\n message,\n isNetwork\n ? `${apiUrl} is unreachable — this is expected during local development.`\n : `${apiUrl} returned an error. Ensure the collector is running and configured correctly.`,\n ]);\n\n return;\n }\n\n log.error(\"Error\", [message]);\n throw error;\n }\n}\n"],"mappings":";;;;;;;;AA6BA,SAAgB,cACd,aAAsC,EAAE,EAC5B;CACZ,MAAM,EACJ,WACA,KAAK,SACL,SACA,WACA,UACA,6BACA,GAAG,SACD;CAEJ,MAAM,SAAS,mBAAmB,UAAU;CAC5C,MAAM,QAAQ,eAAe;EAC3B,iBAAiB;EACjB,mBAAmB;EACnB,YAAY,QAAQ,KAAK;EACzB,QAAQ;GACN,wBAAwB,OAAO;GAC/B,0BAA0B,OAAO;GAClC;EACF,CAAC;AAEF,KAAI,OAAO,WAAW,QAAQ,CAAC,MAAM,WAAW,CAAC,MAAM,UACrD,QAAO,IAAI,MAAM,kCAAkC,CACjD,0EACA,mFACD,CAAC;CAGJ,MAAM,eAAgB,UAClB;AAEJ,QAAO;EACL,GAAG;EACH,KAAK,eAAe,SAAS,OAAO;EACpC,UAAU;GACR,GAAI,YAAY,EAAE;GAClB,MAAM,0BAA0B,SAAmC;AACjE,QAAI,aACF,OAAM,aAAa,QAAQ;AAG7B,UAAM,wBAAwB,SAAS,OAAO;;GAEjD;EACD,SAAS,MAAM;EACf,WAAW,MAAM;EACjB,6BACE,OAAO,WAAW,OAAO,8BAA8B;EAC1D;;AAGH,SAAS,mBAAmB,WAAkD;CAC5E,MAAM,EAAE,QAAQ,WAAW,kBAAkB;CAE7C,MAAM,UACJ,cAAc,WAAW,QAAQ,IACjC,kBAAkB,QAAQ,KAAK,uBAAuB,IACtD,cAAc,qBAAqB;CAErC,MAAM,YACJ,cAAc,WAAW,UAAU,IACnC,kBAAkB,QAAQ,KAAK,4BAA4B,IAC3D;AAEF,QAAO;EAAE,QAAQ,UAAU;EAAM;EAAQ;EAAS;EAAW;;AAG/D,SAAS,eACP,SACA,QACmB;CACnB,MAAM,SAAiC,EAAE;AAEzC,KAAI,OAAO,YAAY,KACrB,QAAO,iCAAiC,OAAO;AAGjD,KAAI,OAAO,cAAc,KACvB,QAAO,mCAAmC,OAAO;AAGnD,QAAO;EAAE,GAAG;EAAQ,GAAG;EAAS;;AAclC,SAAS,eAAe,OAAyB;AAC/C,KAAI,iBAAiB,UACnB,QAAO;AAET,KAAI,iBAAiB,OAAO;EAC1B,MAAM,MAAM,MAAM,QAAQ,aAAa;AACvC,SACE,IAAI,SAAS,eAAe,IAC5B,IAAI,SAAS,YAAY,IACzB,IAAI,SAAS,eAAe,IAC5B,IAAI,SAAS,UAAU;;AAG3B,QAAO;;AAGT,eAAe,wBACb,SACA,QACe;CACf,MAAM,EAAE,QAAQ,QAAQ,YAAY;AAEpC,KAAI,CAAC,OACH,QAAO,IAAI,MAAM,mBAAmB;EAClC;EACA;EACA;EACD,CAAC;AAGJ,KAAI,CAAC,QACH,QAAO,IAAI,MAAM,oBAAoB,CACnC,4EACA,gFACD,CAAC;CAGJ,MAAM,EAAE,qBAAqB,MAAM,OAAO;AAE1C,KAAI;EACF,MAAM,SAAS,MAAM,iBAAiB,SAAS;GAC7C,GAAG;GACH;GACA;GACD,CAAC;AAEF,MAAI,CAAC,OAAO,OAAO;AACjB,OAAI,KAAK,YAAY,CAAC,uBAAuB,CAAC;AAC9C;;AAGF,MAAI,KAAK,aAAa;GACpB,2BAA2B,OAAO,QAAQ,IAAI,KAAK,YAAY,OAAO,QAAQ,QAAQ;GACtF,YAAY,OAAO,SAAS,YAAY,QAAQ;GAChD,UAAU,OAAO;GACjB,cAAc,OAAO,UAAU;GAChC,CAAC;UACK,OAAO;EACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AAEtE,MAAI,WAAW,SAAS;GACtB,MAAM,YAAY,eAAe,MAAM;AAEvC,OAAI,KAAK,6BAA6B,CACpC,SACA,YACI,GAAG,OAAO,gEACV,GAAG,OAAO,+EACf,CAAC;AAEF;;AAGF,MAAI,MAAM,SAAS,CAAC,QAAQ,CAAC;AAC7B,QAAM"}
@@ -0,0 +1,49 @@
1
+ import { ProductionCompileContext, ResolvedBuildConfig } from "../../config.mjs";
2
+ import * as _interfere_sdk_models_releases_create_release_response_js0 from "@interfere/sdk/models/releases-create-release-response.js";
3
+ import { ReleasesConfigResponse } from "@interfere/sdk/models/releases-config-response.js";
4
+ import { CreateReleaseResponse } from "@interfere/types/releases/definition";
5
+
6
+ //#region src/internal/build/pipeline.d.ts
7
+ interface BuildTiming {
8
+ discover: number;
9
+ createRelease: number;
10
+ upload: number;
11
+ cleanup: number;
12
+ total: number;
13
+ fileCount: number;
14
+ totalBytes: number;
15
+ }
16
+ type PipelineResult = {
17
+ ready: false;
18
+ reason: "no_source_maps";
19
+ fileCount: 0;
20
+ } | {
21
+ ready: true;
22
+ fileCount: number;
23
+ release: CreateReleaseResponse;
24
+ config: ReleasesConfigResponse;
25
+ buildId: string;
26
+ timing: BuildTiming;
27
+ };
28
+ declare function runBuildPipeline(context: ProductionCompileContext, metadata: ResolvedBuildConfig & {
29
+ apiKey: string;
30
+ buildId: string;
31
+ }): Promise<{
32
+ ready: boolean;
33
+ reason: string;
34
+ fileCount: number;
35
+ release?: undefined;
36
+ config?: undefined;
37
+ buildId?: undefined;
38
+ timing?: undefined;
39
+ } | {
40
+ ready: true;
41
+ fileCount: number;
42
+ release: _interfere_sdk_models_releases_create_release_response_js0.ReleasesCreateReleaseResponse;
43
+ config: ReleasesConfigResponse;
44
+ buildId: string;
45
+ timing: BuildTiming;
46
+ reason?: undefined;
47
+ }>;
48
+ //#endregion
49
+ export { BuildTiming, PipelineResult, runBuildPipeline };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipeline.d.mts","names":[],"sources":["../../../src/internal/build/pipeline.ts"],"mappings":";;;;;;UAaiB,WAAA;EACf,QAAA;EACA,aAAA;EACA,MAAA;EACA,OAAA;EACA,KAAA;EACA,SAAA;EACA,UAAA;AAAA;AAAA,KAGU,cAAA;EACN,KAAA;EAAc,MAAA;EAA0B,SAAA;AAAA;EACxC,KAAA;EAAa,SAAA;EAAmB,OAAA,EAAS,qBAAA;EAAuB,MAAA,EAAQ,sBAAA;EAAwB,OAAA;EAAiB,MAAA,EAAQ,WAAA;AAAA;AAAA,iBAYzG,gBAAA,CACpB,OAAA,EAAS,wBAAA,EACT,QAAA,EAAU,mBAAA;EAAwB,MAAA;EAAgB,OAAA;AAAA,IAAiB,OAAA;;;;;;;;;;;WAAtC,0DAAA,CAAA,6BAAA"}
@@ -0,0 +1,67 @@
1
+ import { resolveReleaseRequest } from "./release/index.mjs";
2
+ import { discover, normalizeDistDir } from "./source-maps/discover.mjs";
3
+ import { buildUploadBody, cleanupSourceMaps } from "./source-maps/index.mjs";
4
+ import { HTTPClient, Interfere } from "@interfere/sdk";
5
+ //#region src/internal/build/pipeline.ts
6
+ async function timed(fn) {
7
+ const start = performance.now();
8
+ return [await fn(), Math.round(performance.now() - start)];
9
+ }
10
+ async function fetchSurfaceConfig(sdk) {
11
+ return await sdk.releases.getConfig();
12
+ }
13
+ async function runBuildPipeline(context, metadata) {
14
+ const { apiUrl, apiKey } = metadata;
15
+ const httpClient = new HTTPClient();
16
+ httpClient.addHook("beforeRequest", (request) => {
17
+ const nextRequest = new Request(request);
18
+ nextRequest.headers.set("x-api-key", apiKey);
19
+ return nextRequest;
20
+ });
21
+ const sdk = new Interfere({
22
+ serverURL: apiUrl,
23
+ httpClient
24
+ });
25
+ const start = performance.now();
26
+ const [{ discovered, config }, discoverMs] = await timed(async () => {
27
+ const [discovered, config] = await Promise.all([discover(context.projectDir, normalizeDistDir(context.distDir)), fetchSurfaceConfig(sdk)]);
28
+ return {
29
+ discovered,
30
+ config
31
+ };
32
+ });
33
+ if (discovered.files.length === 0) return {
34
+ ready: false,
35
+ reason: "no_source_maps",
36
+ fileCount: 0
37
+ };
38
+ const releaseRequest = resolveReleaseRequest(metadata.buildId, config);
39
+ const [release, createReleaseMs] = await timed(() => sdk.releases.create(releaseRequest));
40
+ const releaseSlug = release.destination.slug;
41
+ const buildId = release.build.hash ?? metadata.buildId;
42
+ const { body, totalBytes } = buildUploadBody(discovered);
43
+ const [, uploadMs] = await timed(() => sdk.sourceMaps.uploadMultipart({
44
+ releaseSlug,
45
+ body
46
+ }));
47
+ const [, cleanupMs] = await timed(() => cleanupSourceMaps(discovered.files));
48
+ const timing = {
49
+ discover: discoverMs,
50
+ createRelease: createReleaseMs,
51
+ upload: uploadMs,
52
+ cleanup: cleanupMs,
53
+ total: Math.round(performance.now() - start),
54
+ fileCount: discovered.files.length,
55
+ totalBytes
56
+ };
57
+ return {
58
+ ready: true,
59
+ fileCount: discovered.files.length,
60
+ release,
61
+ config,
62
+ buildId,
63
+ timing
64
+ };
65
+ }
66
+ //#endregion
67
+ export { runBuildPipeline };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipeline.mjs","names":[],"sources":["../../../src/internal/build/pipeline.ts"],"sourcesContent":["import { HTTPClient, Interfere } from \"@interfere/sdk\";\n\nimport { resolveReleaseRequest } from \"./release/index.js\";\nimport {\n buildUploadBody,\n cleanupSourceMaps,\n discover,\n normalizeDistDir,\n} from \"./source-maps/index.js\";\nimport type { ProductionCompileContext, ResolvedBuildConfig } from \"../../config.js\";\nimport type { ReleasesConfigResponse } from \"@interfere/sdk/models/releases-config-response.js\";\nimport type { CreateReleaseResponse } from \"@interfere/types/releases/definition\";\n\nexport interface BuildTiming {\n discover: number;\n createRelease: number;\n upload: number;\n cleanup: number;\n total: number;\n fileCount: number;\n totalBytes: number;\n}\n\nexport type PipelineResult =\n | { ready: false; reason: \"no_source_maps\"; fileCount: 0 }\n | { ready: true; fileCount: number; release: CreateReleaseResponse; config: ReleasesConfigResponse; buildId: string; timing: BuildTiming };\n\nasync function timed<T>(fn: () => Promise<T>): Promise<[T, number]> {\n const start = performance.now();\n const result = await fn();\n return [result, Math.round(performance.now() - start)];\n}\n\nasync function fetchSurfaceConfig(sdk: Interfere) {\n return await sdk.releases.getConfig();\n}\n\nexport async function runBuildPipeline(\n context: ProductionCompileContext,\n metadata: ResolvedBuildConfig & { apiKey: string; buildId: string }\n) {\n const { apiUrl, apiKey } = metadata;\n\n const httpClient = new HTTPClient();\n\nhttpClient.addHook(\"beforeRequest\", (request) => {\n const nextRequest = new Request(request);\n\n nextRequest.headers.set(\"x-api-key\", apiKey);\n\n return nextRequest;\n});\n\n const sdk = new Interfere({ serverURL: apiUrl, httpClient });\n\n\n const start = performance.now();\n\n const [{ discovered, config }, discoverMs] = await timed(async () => {\n const [discovered, config] = await Promise.all([\n discover(context.projectDir, normalizeDistDir(context.distDir)),\n fetchSurfaceConfig(sdk),\n ]);\n return { discovered, config };\n });\n\n if (discovered.files.length === 0) {\n return { ready: false, reason: \"no_source_maps\", fileCount: 0 };\n }\n\n\n const releaseRequest = resolveReleaseRequest(\n metadata.buildId,\n config,\n );\n\n const [release, createReleaseMs] = await timed(() =>\n sdk.releases.create(releaseRequest)\n );\n\n const releaseSlug = release.destination.slug;\n const buildId = release.build.hash ?? metadata.buildId;\n\n const { body, totalBytes } = buildUploadBody(discovered);\n\n const [, uploadMs] = await timed(() =>\n sdk.sourceMaps.uploadMultipart({ releaseSlug, body })\n );\n\n const [, cleanupMs] = await timed(() => cleanupSourceMaps(discovered.files));\n\n const timing: BuildTiming = {\n discover: discoverMs,\n createRelease: createReleaseMs,\n upload: uploadMs,\n cleanup: cleanupMs,\n total: Math.round(performance.now() - start),\n fileCount: discovered.files.length,\n totalBytes,\n };\n\n return { ready: true, fileCount: discovered.files.length, release, config, buildId, timing } satisfies PipelineResult;\n}\n"],"mappings":";;;;;AA2BA,eAAe,MAAS,IAA4C;CAClE,MAAM,QAAQ,YAAY,KAAK;AAE/B,QAAO,CADQ,MAAM,IAAI,EACT,KAAK,MAAM,YAAY,KAAK,GAAG,MAAM,CAAC;;AAGxD,eAAe,mBAAmB,KAAgB;AAChD,QAAO,MAAM,IAAI,SAAS,WAAW;;AAGvC,eAAsB,iBACpB,SACA,UACA;CACA,MAAM,EAAE,QAAQ,WAAW;CAE3B,MAAM,aAAa,IAAI,YAAY;AAErC,YAAW,QAAQ,kBAAkB,YAAY;EAC/C,MAAM,cAAc,IAAI,QAAQ,QAAQ;AAExC,cAAY,QAAQ,IAAI,aAAa,OAAO;AAE5C,SAAO;GACP;CAEA,MAAM,MAAM,IAAI,UAAU;EAAE,WAAW;EAAQ;EAAY,CAAC;CAG5D,MAAM,QAAQ,YAAY,KAAK;CAE/B,MAAM,CAAC,EAAE,YAAY,UAAU,cAAc,MAAM,MAAM,YAAY;EACnE,MAAM,CAAC,YAAY,UAAU,MAAM,QAAQ,IAAI,CAC7C,SAAS,QAAQ,YAAY,iBAAiB,QAAQ,QAAQ,CAAC,EAC/D,mBAAmB,IAAI,CACxB,CAAC;AACF,SAAO;GAAE;GAAY;GAAQ;GAC7B;AAEF,KAAI,WAAW,MAAM,WAAW,EAC9B,QAAO;EAAE,OAAO;EAAO,QAAQ;EAAkB,WAAW;EAAG;CAIjE,MAAM,iBAAiB,sBACrB,SAAS,SACT,OACD;CAED,MAAM,CAAC,SAAS,mBAAmB,MAAM,YACvC,IAAI,SAAS,OAAO,eAAe,CACpC;CAED,MAAM,cAAc,QAAQ,YAAY;CACxC,MAAM,UAAU,QAAQ,MAAM,QAAQ,SAAS;CAE/C,MAAM,EAAE,MAAM,eAAe,gBAAgB,WAAW;CAExD,MAAM,GAAG,YAAY,MAAM,YACzB,IAAI,WAAW,gBAAgB;EAAE;EAAa;EAAM,CAAC,CACtD;CAED,MAAM,GAAG,aAAa,MAAM,YAAY,kBAAkB,WAAW,MAAM,CAAC;CAE5E,MAAM,SAAsB;EAC1B,UAAU;EACV,eAAe;EACf,QAAQ;EACR,SAAS;EACT,OAAO,KAAK,MAAM,YAAY,KAAK,GAAG,MAAM;EAC5C,WAAW,WAAW,MAAM;EAC5B;EACD;AAED,QAAO;EAAE,OAAO;EAAM,WAAW,WAAW,MAAM;EAAQ;EAAS;EAAQ;EAAS;EAAQ"}
@@ -1,6 +1,6 @@
1
1
  import { ReleaseDestinationMetadata } from "@interfere/types/integrations";
2
2
 
3
3
  //#region src/internal/build/release/destinations/vercel.d.ts
4
- declare function resolve(): ReleaseDestinationMetadata | null;
4
+ declare function resolve(): ReleaseDestinationMetadata;
5
5
  //#endregion
6
6
  export { resolve };
@@ -3,11 +3,11 @@ import { parseEnvValue } from "@interfere/types/sdk/env";
3
3
  //#region src/internal/build/release/destinations/vercel.ts
4
4
  var vercel_exports = /* @__PURE__ */ __exportAll({ resolve: () => resolve });
5
5
  function resolve() {
6
- const deploymentId = parseEnvValue(process.env.VERCEL_DEPLOYMENT_ID);
7
6
  const environment = parseEnvValue(process.env.VERCEL_ENV ?? process.env.VERCEL_TARGET_ENV);
8
- if (!deploymentId && !environment) return null;
7
+ const deploymentId = parseEnvValue(process.env.VERCEL_DEPLOYMENT_ID);
9
8
  return {
10
9
  provider: "vercel",
10
+ destinationReleaseId: deploymentId,
11
11
  environment,
12
12
  deploymentId,
13
13
  deploymentUrl: resolveDeploymentUrl(),
@@ -1 +1 @@
1
- {"version":3,"file":"vercel.mjs","names":[],"sources":["../../../../../src/internal/build/release/destinations/vercel.ts"],"sourcesContent":["import type { ReleaseDestinationMetadata } from \"@interfere/types/integrations\";\nimport { parseEnvValue } from \"@interfere/types/sdk/env\";\n\nexport function resolve(): ReleaseDestinationMetadata | null {\n const deploymentId = parseEnvValue(process.env.VERCEL_DEPLOYMENT_ID);\n const environment = parseEnvValue(\n process.env.VERCEL_ENV ?? process.env.VERCEL_TARGET_ENV,\n );\n\n if (!deploymentId && !environment) {\n return null;\n }\n\n return {\n provider: \"vercel\",\n environment,\n deploymentId,\n deploymentUrl: resolveDeploymentUrl(),\n environmentName: environment,\n environmentTarget: environment,\n };\n}\n\nfunction resolveDeploymentUrl(): string | null {\n const deploymentUrl = parseEnvValue(process.env.VERCEL_URL);\n\n if (deploymentUrl === null) {\n return null;\n }\n\n if (\n deploymentUrl.startsWith(\"https://\") ||\n deploymentUrl.startsWith(\"http://\")\n ) {\n return deploymentUrl;\n }\n\n return `https://${deploymentUrl}`;\n}\n"],"mappings":";;;;AAGA,SAAgB,UAA6C;CAC3D,MAAM,eAAe,cAAc,QAAQ,IAAI,qBAAqB;CACpE,MAAM,cAAc,cAClB,QAAQ,IAAI,cAAc,QAAQ,IAAI,kBACvC;AAED,KAAI,CAAC,gBAAgB,CAAC,YACpB,QAAO;AAGT,QAAO;EACL,UAAU;EACV;EACA;EACA,eAAe,sBAAsB;EACrC,iBAAiB;EACjB,mBAAmB;EACpB;;AAGH,SAAS,uBAAsC;CAC7C,MAAM,gBAAgB,cAAc,QAAQ,IAAI,WAAW;AAE3D,KAAI,kBAAkB,KACpB,QAAO;AAGT,KACE,cAAc,WAAW,WAAW,IACpC,cAAc,WAAW,UAAU,CAEnC,QAAO;AAGT,QAAO,WAAW"}
1
+ {"version":3,"file":"vercel.mjs","names":[],"sources":["../../../../../src/internal/build/release/destinations/vercel.ts"],"sourcesContent":["import type { ReleaseDestinationMetadata } from \"@interfere/types/integrations\";\nimport { parseEnvValue } from \"@interfere/types/sdk/env\";\n\nexport function resolve(): ReleaseDestinationMetadata {\n const environment = parseEnvValue(\n process.env.VERCEL_ENV ?? process.env.VERCEL_TARGET_ENV,\n );\n\n const deploymentId = parseEnvValue(process.env.VERCEL_DEPLOYMENT_ID);\n\n return {\n provider: \"vercel\",\n destinationReleaseId: deploymentId,\n environment,\n deploymentId,\n deploymentUrl: resolveDeploymentUrl(),\n environmentName: environment,\n environmentTarget: environment,\n };\n}\n\nfunction resolveDeploymentUrl(): string | null {\n const deploymentUrl = parseEnvValue(process.env.VERCEL_URL);\n\n if (deploymentUrl === null) {\n return null;\n }\n\n if (\n deploymentUrl.startsWith(\"https://\") ||\n deploymentUrl.startsWith(\"http://\")\n ) {\n return deploymentUrl;\n }\n\n return `https://${deploymentUrl}`;\n}\n"],"mappings":";;;;AAGA,SAAgB,UAAsC;CACpD,MAAM,cAAc,cAClB,QAAQ,IAAI,cAAc,QAAQ,IAAI,kBACvC;CAED,MAAM,eAAe,cAAc,QAAQ,IAAI,qBAAqB;AAEpE,QAAO;EACL,UAAU;EACV,sBAAsB;EACtB;EACA;EACA,eAAe,sBAAsB;EACrC,iBAAiB;EACjB,mBAAmB;EACpB;;AAGH,SAAS,uBAAsC;CAC7C,MAAM,gBAAgB,cAAc,QAAQ,IAAI,WAAW;AAE3D,KAAI,kBAAkB,KACpB,QAAO;AAGT,KACE,cAAc,WAAW,WAAW,IACpC,cAAc,WAAW,UAAU,CAEnC,QAAO;AAGT,QAAO,WAAW"}
@@ -1,7 +1,8 @@
1
1
  import { runGitCommand } from "./git.mjs";
2
- import { CreateReleaseResponse } from "@interfere/types/releases/definition";
2
+ import { ReleasesConfigResponse } from "@interfere/sdk/models/releases-config-response.js";
3
+ import { CreateReleaseRequest } from "@interfere/types/releases/definition";
3
4
 
4
5
  //#region src/internal/build/release/index.d.ts
5
- declare function createRelease(apiKey: string, apiUrl: string, buildId: string): Promise<CreateReleaseResponse>;
6
+ declare function resolveReleaseRequest(buildId: string, config: ReleasesConfigResponse): CreateReleaseRequest;
6
7
  //#endregion
7
- export { createRelease, runGitCommand };
8
+ export { resolveReleaseRequest, runGitCommand };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../../../src/internal/build/release/index.ts"],"mappings":";;;;iBAgCsB,aAAA,CACpB,MAAA,UACA,MAAA,UACA,OAAA,WACC,OAAA,CAAQ,qBAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../../../src/internal/build/release/index.ts"],"mappings":";;;;;iBA2BgB,qBAAA,CACd,OAAA,UACA,MAAA,EAAQ,sBAAA,GACP,oBAAA"}
@@ -1,24 +1,18 @@
1
+ import { log } from "../../logger.mjs";
1
2
  import { runGitCommand } from "./git.mjs";
2
3
  import { vercel_exports } from "./destinations/vercel.mjs";
3
4
  import { github_exports } from "./sources/github.mjs";
4
- import { Interfere } from "@interfere/sdk";
5
- import { InterfereHTTPError } from "@interfere/sdk/models/errors/interfere-http-error.js";
6
5
  //#region src/internal/build/release/index.ts
7
6
  const sources = { github: github_exports };
8
7
  const destinations = { vercel: vercel_exports };
9
- async function createRelease(apiKey, apiUrl, buildId) {
10
- const sdk = new Interfere({ serverURL: apiUrl });
11
- const request = {
12
- source: sources.github.resolve(),
13
- destination: destinations.vercel.resolve(),
8
+ function resolveReleaseRequest(buildId, config) {
9
+ if (!config.surface.sourceProvider) return log.fatal("Missing version control provider", ["This surface does not have a version control provider configured.", `Configure one at https://interfere.com/~/${config.org.slug}/surfaces/${config.surface.slug}`]);
10
+ if (!config.surface.destinationProvider) return log.fatal("Missing deployment target", ["This surface does not have a deployment target integration configured.", `Configure one at https://interfere.com/~/${config.org.slug}/surfaces/${config.surface.slug}`]);
11
+ return {
12
+ source: sources[config.surface.sourceProvider].resolve(),
13
+ destination: destinations[config.surface.destinationProvider].resolve(),
14
14
  buildId
15
15
  };
16
- try {
17
- return await sdk.releases.createRelease(request, { headers: { "x-api-key": apiKey } });
18
- } catch (error) {
19
- if (error instanceof InterfereHTTPError && error.statusCode === 404) throw new Error(`Interfere release API returned 404 at '${error.rawResponse.url}'. Is INTERFERE_API_URL incorrectly set?`);
20
- throw error;
21
- }
22
16
  }
23
17
  //#endregion
24
- export { createRelease, runGitCommand };
18
+ export { resolveReleaseRequest, runGitCommand };
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/internal/build/release/index.ts"],"sourcesContent":["import type {\n DestinationProvider,\n ReleaseDestinationMetadata,\n ReleaseSourceMetadata,\n SourceProvider,\n} from \"@interfere/types/integrations\";\nimport type { CreateReleaseResponse } from \"@interfere/types/releases/definition\";\n\nimport { Interfere } from \"@interfere/sdk\";\nimport { InterfereHTTPError } from \"@interfere/sdk/models/errors/interfere-http-error.js\";\n\nimport * as vercel from \"./destinations/vercel.js\";\nimport * as github from \"./sources/github.js\";\n\ninterface SourceResolver {\n resolve(): ReleaseSourceMetadata;\n}\n\ninterface DestinationResolver {\n resolve(): ReleaseDestinationMetadata | null;\n}\n\nexport { runGitCommand } from \"./git.js\";\n\nconst sources: Record<SourceProvider, SourceResolver> = {\n github,\n};\n\nconst destinations: Record<DestinationProvider, DestinationResolver> = {\n vercel,\n};\n\nexport async function createRelease(\n apiKey: string,\n apiUrl: string,\n buildId: string,\n): Promise<CreateReleaseResponse> {\n const sdk = new Interfere({ serverURL: apiUrl });\n\n const request = {\n source: sources.github.resolve(),\n destination: destinations.vercel.resolve(),\n buildId,\n };\n\n try {\n return await sdk.releases.createRelease(request, {\n headers: { \"x-api-key\": apiKey },\n });\n } catch (error) {\n if (error instanceof InterfereHTTPError && error.statusCode === 404) {\n throw new Error(\n `Interfere release API returned 404 at '${error.rawResponse.url}'. Is INTERFERE_API_URL incorrectly set?`,\n );\n }\n\n throw error;\n }\n}\n"],"mappings":";;;;;;AAwBA,MAAM,UAAkD,EACtD,QAAA,gBACD;AAED,MAAM,eAAiE,EACrE,QAAA,gBACD;AAED,eAAsB,cACpB,QACA,QACA,SACgC;CAChC,MAAM,MAAM,IAAI,UAAU,EAAE,WAAW,QAAQ,CAAC;CAEhD,MAAM,UAAU;EACd,QAAQ,QAAQ,OAAO,SAAS;EAChC,aAAa,aAAa,OAAO,SAAS;EAC1C;EACD;AAED,KAAI;AACF,SAAO,MAAM,IAAI,SAAS,cAAc,SAAS,EAC/C,SAAS,EAAE,aAAa,QAAQ,EACjC,CAAC;UACK,OAAO;AACd,MAAI,iBAAiB,sBAAsB,MAAM,eAAe,IAC9D,OAAM,IAAI,MACR,0CAA0C,MAAM,YAAY,IAAI,0CACjE;AAGH,QAAM"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/internal/build/release/index.ts"],"sourcesContent":["import type {\n DestinationProvider,\n ReleaseDestinationMetadata,\n ReleaseSourceMetadata,\n SourceProvider,\n} from \"@interfere/types/integrations\";\n\nimport * as vercel from \"./destinations/vercel.js\";\nimport * as github from \"./sources/github.js\";\nimport type { CreateReleaseRequest } from \"@interfere/types/releases/definition\";\nimport type { ReleasesConfigResponse } from \"@interfere/sdk/models/releases-config-response.js\";\nimport { log } from \"../../logger.js\";\n\nexport { runGitCommand } from \"./git.js\";\n\ninterface Resolver<T> {\n resolve(): T;\n}\n\nconst sources: Record<SourceProvider, Resolver<ReleaseSourceMetadata>> = {\n github,\n};\n\nconst destinations: Record<DestinationProvider, Resolver<ReleaseDestinationMetadata>> = {\n vercel,\n};\n\nexport function resolveReleaseRequest(\n buildId: string,\n config: ReleasesConfigResponse\n): CreateReleaseRequest {\n if (!config.surface.sourceProvider) {\n return log.fatal(\"Missing version control provider\", [\n \"This surface does not have a version control provider configured.\",\n `Configure one at https://interfere.com/~/${config.org.slug}/surfaces/${config.surface.slug}`,\n ]);\n }\n\n if (!config.surface.destinationProvider) {\n return log.fatal(\"Missing deployment target\", [\n \"This surface does not have a deployment target integration configured.\",\n `Configure one at https://interfere.com/~/${config.org.slug}/surfaces/${config.surface.slug}`,\n ]);\n }\n\n return {\n source: sources[config.surface.sourceProvider].resolve(),\n destination: destinations[config.surface.destinationProvider].resolve(),\n buildId,\n };\n}\n"],"mappings":";;;;;AAmBA,MAAM,UAAmE,EACvE,QAAA,gBACD;AAED,MAAM,eAAkF,EACtF,QAAA,gBACD;AAED,SAAgB,sBACd,SACA,QACsB;AACtB,KAAI,CAAC,OAAO,QAAQ,eAClB,QAAO,IAAI,MAAM,oCAAoC,CACnD,qEACA,4CAA4C,OAAO,IAAI,KAAK,YAAY,OAAO,QAAQ,OACxF,CAAC;AAGJ,KAAI,CAAC,OAAO,QAAQ,oBAClB,QAAO,IAAI,MAAM,6BAA6B,CAC5C,0EACA,4CAA4C,OAAO,IAAI,KAAK,YAAY,OAAO,QAAQ,OACxF,CAAC;AAGJ,QAAO;EACL,QAAQ,QAAQ,OAAO,QAAQ,gBAAgB,SAAS;EACxD,aAAa,aAAa,OAAO,QAAQ,qBAAqB,SAAS;EACvE;EACD"}
@@ -1,6 +1,6 @@
1
1
  import { isAbsolute, join, relative, resolve } from "node:path";
2
- import { createHash } from "node:crypto";
3
2
  import { readFile, readdir } from "node:fs/promises";
3
+ import { createHash } from "node:crypto";
4
4
  //#region src/internal/build/source-maps/discover.ts
5
5
  const SOURCEMAPPING_RE = /\/\/[#@]\s*sourceMappingURL=(\S+)\s*$/;
6
6
  function resolveDistDir(projectDir, distDir) {
@@ -1,23 +1,26 @@
1
+ import { SourceMapFile, discover, normalizeDistDir } from "./discover.mjs";
2
+
1
3
  //#region src/internal/build/source-maps/index.d.ts
2
- interface BuildClient {
3
- createRelease(): Promise<{
4
- slug: string;
5
- orgSlug: string;
6
- buildId: string;
7
- }>;
8
- uploadSourceMaps(releaseSlug: string, body: FormData): Promise<void>;
9
- }
10
- type PipelineResult = {
11
- ready: false;
12
- reason: "no_source_maps";
13
- fileCount: 0;
14
- } | {
15
- ready: true;
16
- fileCount: number;
17
- releaseSlug: string;
18
- orgSlug: string;
19
- buildId: string;
4
+ declare function buildUploadBody(discovered: {
5
+ files: SourceMapFile[];
6
+ mapping: Record<string, string>;
7
+ sourceFileCount: number;
8
+ }): {
9
+ body: {
10
+ files: {
11
+ fileName: string;
12
+ content: Blob;
13
+ }[];
14
+ metadata: {
15
+ sourceMapToGenerated: Record<string, string>;
16
+ hashes: {
17
+ [k: string]: string;
18
+ };
19
+ sourceFileCount: number;
20
+ };
21
+ };
22
+ totalBytes: number;
20
23
  };
21
- declare function runSourceMapPipeline(projectDir: string, distDir: string | undefined, client: BuildClient, cleanup?: boolean): Promise<PipelineResult>;
24
+ declare function cleanupSourceMaps(files: SourceMapFile[]): Promise<void>;
22
25
  //#endregion
23
- export { BuildClient, PipelineResult, runSourceMapPipeline };
26
+ export { type SourceMapFile, buildUploadBody, cleanupSourceMaps, discover, normalizeDistDir };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../../../src/internal/build/source-maps/index.ts"],"mappings":";UAIiB,WAAA;EACf,aAAA,IAAiB,OAAA;IAAU,IAAA;IAAc,OAAA;IAAiB,OAAA;EAAA;EAC1D,gBAAA,CAAiB,WAAA,UAAqB,IAAA,EAAM,QAAA,GAAW,OAAA;AAAA;AAAA,KAG7C,cAAA;EACN,KAAA;EAAc,MAAA;EAA0B,SAAA;AAAA;EACxC,KAAA;EAAa,SAAA;EAAmB,WAAA;EAAqB,OAAA;EAAiB,OAAA;AAAA;AAAA,iBAEtD,oBAAA,CACpB,UAAA,UACA,OAAA,sBACA,MAAA,EAAQ,WAAA,EACR,OAAA,aACC,OAAA,CAAQ,cAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../../../src/internal/build/source-maps/index.ts"],"mappings":";;;iBAMgB,eAAA,CAAgB,UAAA;EAC9B,KAAA,EAAO,aAAA;EACP,OAAA,EAAS,MAAA;EACT,eAAA;AAAA;;;;;;;;;;;;;;;;iBAqBoB,iBAAA,CAAkB,KAAA,EAAO,aAAA,KAAe,OAAA"}
@@ -1,34 +1,28 @@
1
1
  import { discover, normalizeDistDir } from "./discover.mjs";
2
2
  import { unlink } from "node:fs/promises";
3
3
  //#region src/internal/build/source-maps/index.ts
4
- async function runSourceMapPipeline(projectDir, distDir, client, cleanup = true) {
5
- const { files, mapping, sourceFileCount } = await discover(projectDir, normalizeDistDir(distDir));
6
- if (files.length === 0) return {
7
- ready: false,
8
- reason: "no_source_maps",
9
- fileCount: 0
10
- };
11
- const { slug, orgSlug, buildId } = await client.createRelease();
12
- const body = new FormData();
13
- const hashes = {};
14
- for (const file of files) {
15
- body.append("files", new Blob([file.content], { type: "application/json" }), file.path);
16
- hashes[file.path] = file.hash;
17
- }
18
- body.append("metadata", JSON.stringify({
19
- sourceMapToGenerated: mapping,
20
- hashes,
21
- sourceFileCount
22
- }));
23
- await client.uploadSourceMaps(slug, body);
24
- if (cleanup) await Promise.all(files.map((f) => unlink(f.absolute).catch(() => void 0)));
4
+ function buildUploadBody(discovered) {
5
+ let totalBytes = 0;
25
6
  return {
26
- ready: true,
27
- fileCount: files.length,
28
- releaseSlug: slug,
29
- orgSlug,
30
- buildId
7
+ body: {
8
+ files: discovered.files.map((file) => {
9
+ totalBytes += file.content.length;
10
+ return {
11
+ fileName: file.path,
12
+ content: new Blob([file.content], { type: "application/json" })
13
+ };
14
+ }),
15
+ metadata: {
16
+ sourceMapToGenerated: discovered.mapping,
17
+ hashes: Object.fromEntries(discovered.files.map((f) => [f.path, f.hash])),
18
+ sourceFileCount: discovered.sourceFileCount
19
+ }
20
+ },
21
+ totalBytes
31
22
  };
32
23
  }
24
+ async function cleanupSourceMaps(files) {
25
+ await Promise.all(files.map((f) => unlink(f.absolute).catch(() => void 0)));
26
+ }
33
27
  //#endregion
34
- export { runSourceMapPipeline };
28
+ export { buildUploadBody, cleanupSourceMaps, discover, normalizeDistDir };
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/internal/build/source-maps/index.ts"],"sourcesContent":["import { unlink } from \"node:fs/promises\";\n\nimport { discover, normalizeDistDir } from \"./discover.js\";\n\nexport interface BuildClient {\n createRelease(): Promise<{ slug: string; orgSlug: string; buildId: string }>;\n uploadSourceMaps(releaseSlug: string, body: FormData): Promise<void>;\n}\n\nexport type PipelineResult =\n | { ready: false; reason: \"no_source_maps\"; fileCount: 0 }\n | { ready: true; fileCount: number; releaseSlug: string; orgSlug: string; buildId: string };\n\nexport async function runSourceMapPipeline(\n projectDir: string,\n distDir: string | undefined,\n client: BuildClient,\n cleanup = true,\n): Promise<PipelineResult> {\n const { files, mapping, sourceFileCount } = await discover(projectDir, normalizeDistDir(distDir));\n\n if (files.length === 0) {\n return { ready: false, reason: \"no_source_maps\", fileCount: 0 };\n }\n\n const { slug, orgSlug, buildId } = await client.createRelease();\n\n const body = new FormData();\n const hashes: Record<string, string> = {};\n\n for (const file of files) {\n body.append(\"files\", new Blob([file.content], { type: \"application/json\" }), file.path);\n hashes[file.path] = file.hash;\n }\n\n body.append(\"metadata\", JSON.stringify({ sourceMapToGenerated: mapping, hashes, sourceFileCount }));\n\n await client.uploadSourceMaps(slug, body);\n\n if (cleanup) {\n await Promise.all(files.map((f) => unlink(f.absolute).catch(() => undefined)));\n }\n\n return { ready: true, fileCount: files.length, releaseSlug: slug, orgSlug, buildId };\n}\n"],"mappings":";;;AAaA,eAAsB,qBACpB,YACA,SACA,QACA,UAAU,MACe;CACzB,MAAM,EAAE,OAAO,SAAS,oBAAoB,MAAM,SAAS,YAAY,iBAAiB,QAAQ,CAAC;AAEjG,KAAI,MAAM,WAAW,EACnB,QAAO;EAAE,OAAO;EAAO,QAAQ;EAAkB,WAAW;EAAG;CAGjE,MAAM,EAAE,MAAM,SAAS,YAAY,MAAM,OAAO,eAAe;CAE/D,MAAM,OAAO,IAAI,UAAU;CAC3B,MAAM,SAAiC,EAAE;AAEzC,MAAK,MAAM,QAAQ,OAAO;AACxB,OAAK,OAAO,SAAS,IAAI,KAAK,CAAC,KAAK,QAAQ,EAAE,EAAE,MAAM,oBAAoB,CAAC,EAAE,KAAK,KAAK;AACvF,SAAO,KAAK,QAAQ,KAAK;;AAG3B,MAAK,OAAO,YAAY,KAAK,UAAU;EAAE,sBAAsB;EAAS;EAAQ;EAAiB,CAAC,CAAC;AAEnG,OAAM,OAAO,iBAAiB,MAAM,KAAK;AAEzC,KAAI,QACF,OAAM,QAAQ,IAAI,MAAM,KAAK,MAAM,OAAO,EAAE,SAAS,CAAC,YAAY,KAAA,EAAU,CAAC,CAAC;AAGhF,QAAO;EAAE,OAAO;EAAM,WAAW,MAAM;EAAQ,aAAa;EAAM;EAAS;EAAS"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/internal/build/source-maps/index.ts"],"sourcesContent":["import { unlink } from \"node:fs/promises\";\n\nimport type { SourceMapFile } from \"./discover.js\";\n\nexport { discover, normalizeDistDir, type SourceMapFile } from \"./discover.js\";\n\nexport function buildUploadBody(discovered: {\n files: SourceMapFile[];\n mapping: Record<string, string>;\n sourceFileCount: number;\n}) {\n let totalBytes = 0;\n\n const files = discovered.files.map((file) => {\n totalBytes += file.content.length;\n return {\n fileName: file.path,\n content: new Blob([file.content], { type: \"application/json\" }),\n };\n });\n\n const metadata = {\n sourceMapToGenerated: discovered.mapping,\n hashes: Object.fromEntries(discovered.files.map((f) => [f.path, f.hash])),\n sourceFileCount: discovered.sourceFileCount,\n };\n\n return { body: { files, metadata }, totalBytes };\n}\n\nexport async function cleanupSourceMaps(files: SourceMapFile[]) {\n await Promise.all(files.map((f) => unlink(f.absolute).catch(() => undefined)));\n}\n"],"mappings":";;;AAMA,SAAgB,gBAAgB,YAI7B;CACD,IAAI,aAAa;AAgBjB,QAAO;EAAE,MAAM;GAAE,OAdH,WAAW,MAAM,KAAK,SAAS;AAC3C,kBAAc,KAAK,QAAQ;AAC3B,WAAO;KACL,UAAU,KAAK;KACf,SAAS,IAAI,KAAK,CAAC,KAAK,QAAQ,EAAE,EAAE,MAAM,oBAAoB,CAAC;KAChE;KACD;GAQsB,UANP;IACf,sBAAsB,WAAW;IACjC,QAAQ,OAAO,YAAY,WAAW,MAAM,KAAK,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;IACzE,iBAAiB,WAAW;IAC7B;GAEiC;EAAE;EAAY;;AAGlD,eAAsB,kBAAkB,OAAwB;AAC9D,OAAM,QAAQ,IAAI,MAAM,KAAK,MAAM,OAAO,EAAE,SAAS,CAAC,YAAY,KAAA,EAAU,CAAC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"env.mjs","names":[],"sources":["../../src/internal/env.ts"],"sourcesContent":["import { API_URL } from \"@interfere/constants/api\";\nimport { parseEnvValue } from \"@interfere/types/sdk/env\";\nimport type { Env } from \"@interfere/types/sdk/runtime\";\nimport { normalizeEnv } from \"@interfere/types/sdk/runtime\";\n\nexport interface InterfereEnv {\n readonly apiKey: string | null;\n readonly apiUrl: string;\n readonly nextRuntime: string | null;\n readonly nodeEnvironment: Exclude<Env, null>;\n readonly release: {\n readonly sourceId: string | null;\n readonly destinationId: string | null;\n };\n}\n\nexport function readInterfereEnv(): InterfereEnv {\n const nodeEnvironment = normalizeEnv(process.env.NODE_ENV) ?? \"production\";\n return {\n apiKey: parseEnvValue(process.env.INTERFERE_API_KEY),\n apiUrl: parseEnvValue(process.env.INTERFERE_API_URL) ?? API_URL,\n nextRuntime: parseEnvValue(process.env.NEXT_RUNTIME),\n nodeEnvironment,\n release: {\n sourceId: parseEnvValue(process.env.NEXT_PUBLIC_INTERFERE_BUILD_ID),\n destinationId: parseEnvValue(\n process.env.NEXT_PUBLIC_INTERFERE_RELEASE_ID\n ),\n },\n };\n}\n"],"mappings":";;;;AAgBA,SAAgB,mBAAiC;CAC/C,MAAM,kBAAkB,aAAa,QAAQ,IAAI,SAAS,IAAI;AAC9D,QAAO;EACL,QAAQ,cAAc,QAAQ,IAAI,kBAAkB;EACpD,QAAQ,cAAc,QAAQ,IAAI,kBAAkB,IAAI;EACxD,aAAa,cAAc,QAAQ,IAAI,aAAa;EACpD;EACA,SAAS;GACP,UAAU,cAAc,QAAQ,IAAI,+BAA+B;GACnE,eAAe,cACb,QAAQ,IAAI,iCACb;GACF;EACF"}
1
+ {"version":3,"file":"env.mjs","names":[],"sources":["../../src/internal/env.ts"],"sourcesContent":["import { API_URL } from \"@interfere/constants/api\";\nimport { parseEnvValue } from \"@interfere/types/sdk/env\";\nimport type { Env } from \"@interfere/types/sdk/runtime\";\nimport { normalizeEnv } from \"@interfere/types/sdk/runtime\";\n\nexport interface InterfereEnv {\n readonly apiKey: string | null;\n readonly apiUrl: string;\n readonly nextRuntime: string | null;\n readonly nodeEnvironment: Exclude<Env, null>;\n readonly release: {\n readonly sourceId: string | null;\n readonly destinationId: string | null;\n };\n}\n\nexport function readInterfereEnv(): InterfereEnv {\n const nodeEnvironment = normalizeEnv(process.env.NODE_ENV) ?? \"production\";\n\n return {\n apiKey: parseEnvValue(process.env.INTERFERE_API_KEY),\n apiUrl: parseEnvValue(process.env.INTERFERE_API_URL) ?? API_URL,\n nextRuntime: parseEnvValue(process.env.NEXT_RUNTIME),\n nodeEnvironment,\n release: {\n sourceId: parseEnvValue(process.env.NEXT_PUBLIC_INTERFERE_BUILD_ID),\n destinationId: parseEnvValue(\n process.env.NEXT_PUBLIC_INTERFERE_RELEASE_ID\n ),\n },\n };\n}\n"],"mappings":";;;;AAgBA,SAAgB,mBAAiC;CAC/C,MAAM,kBAAkB,aAAa,QAAQ,IAAI,SAAS,IAAI;AAE9D,QAAO;EACL,QAAQ,cAAc,QAAQ,IAAI,kBAAkB;EACpD,QAAQ,cAAc,QAAQ,IAAI,kBAAkB,IAAI;EACxD,aAAa,cAAc,QAAQ,IAAI,aAAa;EACpD;EACA,SAAS;GACP,UAAU,cAAc,QAAQ,IAAI,+BAA+B;GACnE,eAAe,cACb,QAAQ,IAAI,iCACb;GACF;EACF"}
@@ -3,6 +3,7 @@ declare const log: {
3
3
  info: (title: string, lines: string[]) => void;
4
4
  warn: (title: string, lines: string[]) => void;
5
5
  error: (title: string, lines: string[]) => void;
6
+ fatal(title: string, lines: string[]): never;
6
7
  };
7
8
  //#endregion
8
9
  export { log };
@@ -1 +1 @@
1
- {"version":3,"file":"logger.d.mts","names":[],"sources":["../../src/internal/logger.ts"],"mappings":";cA0Da,GAAA;wBACS,KAAA;wBACA,KAAA;yBACC,KAAA;AAAA"}
1
+ {"version":3,"file":"logger.d.mts","names":[],"sources":["../../src/internal/logger.ts"],"mappings":";cA6Ea,GAAA;wBACS,KAAA;wBACA,KAAA;yBACC,KAAA;uBACF,KAAA;AAAA"}
@@ -4,23 +4,33 @@ const styles = {
4
4
  info: {
5
5
  prefix: `${chalk.whiteBright.bold("❖")}`,
6
6
  text: chalk.cyan.bold,
7
- content: chalk.white
7
+ content: chalk.white,
8
+ prependLevel: false
8
9
  },
9
10
  warn: {
10
11
  prefix: `${chalk.yellow.bold("⚠")} `,
11
12
  text: chalk.yellow.bold,
12
- content: chalk.yellowBright
13
+ content: chalk.yellowBright,
14
+ prependLevel: false
13
15
  },
14
16
  error: {
15
17
  prefix: `${chalk.red.bold("⨯")} `,
16
18
  text: chalk.red.bold,
17
- content: chalk.redBright
19
+ content: chalk.redBright,
20
+ prependLevel: true
21
+ },
22
+ fatal: {
23
+ prefix: `${chalk.red.bold("⨯")} `,
24
+ text: chalk.red.bold,
25
+ content: chalk.redBright,
26
+ prependLevel: true
18
27
  }
19
28
  };
20
29
  const consoleMethods = {
21
30
  info: "log",
22
31
  warn: "warn",
23
- error: "error"
32
+ error: "error",
33
+ fatal: "error"
24
34
  };
25
35
  function isTestEnv() {
26
36
  return Boolean(process.env.VITEST || process.env.VITEST_WORKER_ID);
@@ -32,13 +42,19 @@ function emit(level, title, lines) {
32
42
  const fn = globalThis.console[method];
33
43
  if (typeof fn !== "function") return;
34
44
  const invoke = (...args) => Reflect.apply(fn, globalThis.console, args);
35
- invoke(`${style.prefix} ${chalk.white("Interfere →")} ${style.text(title)}`);
36
- for (const [i, line] of lines.entries()) invoke(`${i === lines.length - 1 ? "└" : "├"} ${style.content(line)}`);
45
+ const prependLevel = style.prependLevel ? `[${style.text(level.toUpperCase())}] ` : "";
46
+ invoke(`${prependLevel} ${chalk.white("Interfere ")} ${style.text(title)}`);
47
+ for (const [i, line] of lines.entries()) invoke(`${prependLevel} ${i === lines.length - 1 ? "└" : "├"} ${style.content(line)}`);
37
48
  }
38
49
  const log = {
39
50
  info: (title, lines) => emit("info", title, lines),
40
51
  warn: (title, lines) => emit("warn", title, lines),
41
- error: (title, lines) => emit("error", title, lines)
52
+ error: (title, lines) => emit("error", title, lines),
53
+ fatal(title, lines) {
54
+ emit("fatal", title, lines);
55
+ if (isTestEnv()) throw new Error(title);
56
+ process.exit(1);
57
+ }
42
58
  };
43
59
  //#endregion
44
60
  export { log };
@@ -1 +1 @@
1
- {"version":3,"file":"logger.mjs","names":[],"sources":["../../src/internal/logger.ts"],"sourcesContent":["import chalk from \"chalk\";\n\ntype LogLevel = \"info\" | \"warn\" | \"error\";\n\nconst styles = {\n info: {\n prefix: `${chalk.whiteBright.bold(\"❖\")}`,\n text: chalk.cyan.bold,\n content: chalk.white,\n },\n warn: {\n prefix: `${chalk.yellow.bold(\"⚠\")} `,\n text: chalk.yellow.bold,\n content: chalk.yellowBright,\n },\n error: {\n prefix: `${chalk.red.bold(\"⨯\")} `,\n text: chalk.red.bold,\n content: chalk.redBright,\n },\n} satisfies Record<\n LogLevel,\n { prefix: string; text: typeof chalk.bold; content: typeof chalk }\n>;\n\nconst consoleMethods = {\n info: \"log\",\n warn: \"warn\",\n error: \"error\",\n} satisfies Record<LogLevel, string>;\n\nfunction isTestEnv() {\n return Boolean(process.env.VITEST || process.env.VITEST_WORKER_ID);\n}\n\nfunction emit(level: LogLevel, title: string, lines: string[]) {\n if (isTestEnv()) {\n return;\n }\n\n const style = styles[level];\n const method = consoleMethods[level] as keyof Console;\n const fn = globalThis.console[method];\n if (typeof fn !== \"function\") {\n return;\n }\n\n const invoke = (...args: unknown[]) =>\n Reflect.apply(fn, globalThis.console, args);\n\n invoke(`${style.prefix} ${chalk.white(\"Interfere →\")} ${style.text(title)}`);\n\n for (const [i, line] of lines.entries()) {\n const connector = i === lines.length - 1 ? \"└\" : \"├\";\n invoke(`${connector} ${style.content(line)}`);\n }\n}\n\nexport const log = {\n info: (title: string, lines: string[]) => emit(\"info\", title, lines),\n warn: (title: string, lines: string[]) => emit(\"warn\", title, lines),\n error: (title: string, lines: string[]) => emit(\"error\", title, lines),\n};\n"],"mappings":";;AAIA,MAAM,SAAS;CACb,MAAM;EACJ,QAAQ,GAAG,MAAM,YAAY,KAAK,IAAI;EACtC,MAAM,MAAM,KAAK;EACjB,SAAS,MAAM;EAChB;CACD,MAAM;EACJ,QAAQ,GAAG,MAAM,OAAO,KAAK,IAAI,CAAC;EAClC,MAAM,MAAM,OAAO;EACnB,SAAS,MAAM;EAChB;CACD,OAAO;EACL,QAAQ,GAAG,MAAM,IAAI,KAAK,IAAI,CAAC;EAC/B,MAAM,MAAM,IAAI;EAChB,SAAS,MAAM;EAChB;CACF;AAKD,MAAM,iBAAiB;CACrB,MAAM;CACN,MAAM;CACN,OAAO;CACR;AAED,SAAS,YAAY;AACnB,QAAO,QAAQ,QAAQ,IAAI,UAAU,QAAQ,IAAI,iBAAiB;;AAGpE,SAAS,KAAK,OAAiB,OAAe,OAAiB;AAC7D,KAAI,WAAW,CACb;CAGF,MAAM,QAAQ,OAAO;CACrB,MAAM,SAAS,eAAe;CAC9B,MAAM,KAAK,WAAW,QAAQ;AAC9B,KAAI,OAAO,OAAO,WAChB;CAGF,MAAM,UAAU,GAAG,SACjB,QAAQ,MAAM,IAAI,WAAW,SAAS,KAAK;AAE7C,QAAO,GAAG,MAAM,OAAO,GAAG,MAAM,MAAM,cAAc,CAAC,GAAG,MAAM,KAAK,MAAM,GAAG;AAE5E,MAAK,MAAM,CAAC,GAAG,SAAS,MAAM,SAAS,CAErC,QAAO,GADW,MAAM,MAAM,SAAS,IAAI,MAAM,IAC7B,GAAG,MAAM,QAAQ,KAAK,GAAG;;AAIjD,MAAa,MAAM;CACjB,OAAO,OAAe,UAAoB,KAAK,QAAQ,OAAO,MAAM;CACpE,OAAO,OAAe,UAAoB,KAAK,QAAQ,OAAO,MAAM;CACpE,QAAQ,OAAe,UAAoB,KAAK,SAAS,OAAO,MAAM;CACvE"}
1
+ {"version":3,"file":"logger.mjs","names":[],"sources":["../../src/internal/logger.ts"],"sourcesContent":["import chalk from \"chalk\";\n\ntype LogLevel = \"info\" | \"warn\" | \"error\" | \"fatal\";\n\nconst styles = {\n info: {\n prefix: `${chalk.whiteBright.bold(\"❖\")}`,\n text: chalk.cyan.bold,\n content: chalk.white,\n prependLevel: false,\n },\n warn: {\n prefix: `${chalk.yellow.bold(\"⚠\")} `,\n text: chalk.yellow.bold,\n content: chalk.yellowBright,\n prependLevel: false,\n },\n error: {\n prefix: `${chalk.red.bold(\"⨯\")} `,\n text: chalk.red.bold,\n content: chalk.redBright,\n prependLevel: true,\n },\n fatal: {\n prefix: `${chalk.red.bold(\"⨯\")} `,\n text: chalk.red.bold,\n content: chalk.redBright,\n prependLevel: true,\n },\n} satisfies Record<\n LogLevel,\n {\n prefix: string;\n text: typeof chalk.bold;\n content: typeof chalk;\n prependLevel: boolean;\n }\n>;\n\nconst consoleMethods = {\n info: \"log\",\n warn: \"warn\",\n error: \"error\",\n fatal: \"error\",\n} satisfies Record<LogLevel, string>;\n\nfunction isTestEnv() {\n return Boolean(process.env.VITEST || process.env.VITEST_WORKER_ID);\n}\n\nfunction emit(level: LogLevel, title: string, lines: string[]) {\n if (isTestEnv()) {\n return;\n }\n\n const style = styles[level];\n const method = consoleMethods[level] as keyof Console;\n const fn = globalThis.console[method];\n if (typeof fn !== \"function\") {\n return;\n }\n\n const invoke = (...args: unknown[]) =>\n Reflect.apply(fn, globalThis.console, args);\n\n const prependLevel = style.prependLevel\n ? `[${style.text(level.toUpperCase())}] `\n : \"\";\n\n invoke(`${prependLevel} ${chalk.white(\"Interfere →\")} ${style.text(title)}`);\n\n for (const [i, line] of lines.entries()) {\n const connector = i === lines.length - 1 ? \"└\" : \"├\";\n invoke(`${prependLevel} ${connector} ${style.content(line)}`);\n }\n}\n\nexport const log = {\n info: (title: string, lines: string[]) => emit(\"info\", title, lines),\n warn: (title: string, lines: string[]) => emit(\"warn\", title, lines),\n error: (title: string, lines: string[]) => emit(\"error\", title, lines),\n fatal(title: string, lines: string[]): never {\n emit(\"fatal\", title, lines);\n\n if (isTestEnv()) {\n throw new Error(title);\n }\n\n process.exit(1);\n },\n};\n"],"mappings":";;AAIA,MAAM,SAAS;CACb,MAAM;EACJ,QAAQ,GAAG,MAAM,YAAY,KAAK,IAAI;EACtC,MAAM,MAAM,KAAK;EACjB,SAAS,MAAM;EACf,cAAc;EACf;CACD,MAAM;EACJ,QAAQ,GAAG,MAAM,OAAO,KAAK,IAAI,CAAC;EAClC,MAAM,MAAM,OAAO;EACnB,SAAS,MAAM;EACf,cAAc;EACf;CACD,OAAO;EACL,QAAQ,GAAG,MAAM,IAAI,KAAK,IAAI,CAAC;EAC/B,MAAM,MAAM,IAAI;EAChB,SAAS,MAAM;EACf,cAAc;EACf;CACD,OAAO;EACL,QAAQ,GAAG,MAAM,IAAI,KAAK,IAAI,CAAC;EAC/B,MAAM,MAAM,IAAI;EAChB,SAAS,MAAM;EACf,cAAc;EACf;CACF;AAUD,MAAM,iBAAiB;CACrB,MAAM;CACN,MAAM;CACN,OAAO;CACP,OAAO;CACR;AAED,SAAS,YAAY;AACnB,QAAO,QAAQ,QAAQ,IAAI,UAAU,QAAQ,IAAI,iBAAiB;;AAGpE,SAAS,KAAK,OAAiB,OAAe,OAAiB;AAC7D,KAAI,WAAW,CACb;CAGF,MAAM,QAAQ,OAAO;CACrB,MAAM,SAAS,eAAe;CAC9B,MAAM,KAAK,WAAW,QAAQ;AAC9B,KAAI,OAAO,OAAO,WAChB;CAGF,MAAM,UAAU,GAAG,SACjB,QAAQ,MAAM,IAAI,WAAW,SAAS,KAAK;CAE7C,MAAM,eAAe,MAAM,eACvB,IAAI,MAAM,KAAK,MAAM,aAAa,CAAC,CAAC,MACpC;AAEJ,QAAO,GAAG,aAAa,GAAG,MAAM,MAAM,cAAc,CAAC,GAAG,MAAM,KAAK,MAAM,GAAG;AAE5E,MAAK,MAAM,CAAC,GAAG,SAAS,MAAM,SAAS,CAErC,QAAO,GAAG,aAAa,GADL,MAAM,MAAM,SAAS,IAAI,MAAM,IACb,GAAG,MAAM,QAAQ,KAAK,GAAG;;AAIjE,MAAa,MAAM;CACjB,OAAO,OAAe,UAAoB,KAAK,QAAQ,OAAO,MAAM;CACpE,OAAO,OAAe,UAAoB,KAAK,QAAQ,OAAO,MAAM;CACpE,QAAQ,OAAe,UAAoB,KAAK,SAAS,OAAO,MAAM;CACtE,MAAM,OAAe,OAAwB;AAC3C,OAAK,SAAS,OAAO,MAAM;AAE3B,MAAI,WAAW,CACb,OAAM,IAAI,MAAM,MAAM;AAGxB,UAAQ,KAAK,EAAE;;CAElB"}
@@ -84,7 +84,8 @@ async function handleIngest(request, env) {
84
84
  "x-api-key": env.apiKey,
85
85
  ...traceparent ? { traceparent } : {}
86
86
  },
87
- body: JSON.stringify(injectReleaseMetadata(envelopes, env.release))
87
+ body: JSON.stringify(injectReleaseMetadata(envelopes, env.release)),
88
+ signal: AbortSignal.timeout(1e4)
88
89
  });
89
90
  return new Response(upstream.body, {
90
91
  status: upstream.status,
@@ -102,7 +103,8 @@ async function forwardToCollector(request, env, subPath) {
102
103
  "x-api-key": env.apiKey,
103
104
  ...traceparent ? { traceparent } : {}
104
105
  },
105
- body
106
+ body,
107
+ signal: AbortSignal.timeout(1e4)
106
108
  });
107
109
  if (!upstream.ok) {
108
110
  const body = await upstream.text().catch(() => "");
@@ -1 +1 @@
1
- {"version":3,"file":"handle-post.mjs","names":[],"sources":["../../../src/internal/route/handle-post.ts"],"sourcesContent":["import { API_PATHS } from \"@interfere/constants/api\";\nimport type { Envelope } from \"@interfere/types/sdk/envelope\";\n\nimport { type InterfereEnv, readInterfereEnv } from \"../env.js\";\nimport { log } from \"../logger.js\";\n\nfunction parseEnvelopes(value: unknown): Envelope[] | null {\n if (!Array.isArray(value)) {\n return null;\n }\n\n return value as Envelope[];\n}\n\nfunction injectReleaseMetadata(\n envelopes: Envelope[],\n metadata: { sourceId: string | null; destinationId: string | null }\n): Envelope[] {\n if (metadata.sourceId === null && metadata.destinationId === null) {\n return envelopes;\n }\n\n return envelopes.map((envelope) => ({\n ...envelope,\n buildId: metadata.sourceId ?? envelope.buildId,\n releaseId: metadata.destinationId ?? envelope.releaseId,\n }));\n}\n\nfunction formatProxyError(error: unknown): {\n message: string;\n lines: string[];\n} {\n if (!(error instanceof Error)) {\n return {\n message: String(error),\n lines: [String(error)],\n };\n }\n\n const lines = [`${error.name}: ${error.message}`];\n\n if (\"cause\" in error && error.cause) {\n const cause =\n error.cause instanceof Error ? error.cause.message : String(error.cause);\n lines.push(`cause: ${cause}`);\n }\n\n if (\n \"code\" in error &&\n typeof (error as NodeJS.ErrnoException).code === \"string\"\n ) {\n lines.push(`code: ${(error as NodeJS.ErrnoException).code}`);\n }\n\n return {\n message: `${error.name}: ${error.message}`,\n lines,\n };\n}\n\nconst PROXY_PATH_PATTERN = /\\/api\\/interfere(\\/.*)/;\n\nfunction extractSubPath(request: Request): string {\n const url = new URL(request.url);\n const match = url.pathname.match(PROXY_PATH_PATTERN);\n return match?.[1] ?? \"/\";\n}\n\ninterface AuthenticatedEnv {\n apiKey: string;\n apiUrl: string;\n release: InterfereEnv[\"release\"];\n}\n\nexport async function handlePost(request: Request): Promise<Response> {\n const env = readInterfereEnv();\n\n if (env.apiKey === null) {\n log.warn(\"Not configured\", [\n \"INTERFERE_API_KEY is not set. The proxy route will return 503.\",\n ]);\n\n return Response.json(\n {\n code: \"INTERFERE_NOT_CONFIGURED\",\n message: \"INTERFERE_API_KEY is required.\",\n },\n { status: 503 }\n );\n }\n\n const authed: AuthenticatedEnv = {\n apiKey: env.apiKey,\n apiUrl: env.apiUrl,\n release: env.release,\n };\n\n const subPath = extractSubPath(request);\n\n try {\n if (subPath === API_PATHS.INGEST) {\n return await handleIngest(request, authed);\n }\n\n return await forwardToCollector(request, authed, subPath);\n } catch (error) {\n const detail = formatProxyError(error);\n log.error(`Proxy ${request.method} ${subPath} failed`, detail.lines);\n return Response.json(\n {\n code: \"INTERFERE_PROXY_ERROR\",\n message: detail.message,\n },\n { status: 502 }\n );\n }\n}\n\nasync function handleIngest(\n request: Request,\n env: AuthenticatedEnv\n): Promise<Response> {\n let payload: unknown;\n try {\n payload = await request.json();\n } catch {\n return Response.json(\n {\n code: \"INTERFERE_INVALID_JSON\",\n message: \"Request body must be valid JSON.\",\n },\n { status: 400 }\n );\n }\n\n const envelopes = parseEnvelopes(payload);\n if (envelopes === null) {\n return Response.json(\n {\n code: \"INTERFERE_INVALID_ENVELOPES\",\n message: \"Request body must be an array of envelopes.\",\n },\n { status: 400 }\n );\n }\n\n const traceparent = request.headers.get(\"traceparent\");\n const upstream = await fetch(`${env.apiUrl}${API_PATHS.INGEST}`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n \"x-api-key\": env.apiKey,\n ...(traceparent ? { traceparent } : {}),\n },\n body: JSON.stringify(injectReleaseMetadata(envelopes, env.release)),\n });\n\n return new Response(upstream.body, {\n status: upstream.status,\n headers: {\n \"content-type\":\n upstream.headers.get(\"content-type\") ?? \"application/json\",\n },\n });\n}\n\nasync function forwardToCollector(\n request: Request,\n env: AuthenticatedEnv,\n subPath: string\n): Promise<Response> {\n const url = `${env.apiUrl}${subPath}`;\n const traceparent = request.headers.get(\"traceparent\");\n const body = await request.text();\n\n const upstream = await fetch(url, {\n method: request.method,\n headers: {\n \"content-type\": request.headers.get(\"content-type\") ?? \"application/json\",\n \"x-api-key\": env.apiKey,\n ...(traceparent ? { traceparent } : {}),\n },\n body,\n });\n\n if (!upstream.ok) {\n const body = await upstream.text().catch(() => \"\");\n log.error(`Upstream ${upstream.status} for ${request.method} ${subPath}`, [\n body,\n ]);\n return Response.json(\n { code: \"INTERFERE_UPSTREAM_ERROR\", message: body },\n { status: upstream.status }\n );\n }\n\n return new Response(upstream.body, {\n status: upstream.status,\n headers: {\n \"content-type\":\n upstream.headers.get(\"content-type\") ?? \"application/json\",\n },\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"handle-post.mjs","names":[],"sources":["../../../src/internal/route/handle-post.ts"],"sourcesContent":["import { API_PATHS } from \"@interfere/constants/api\";\nimport type { Envelope } from \"@interfere/types/sdk/envelope\";\n\nimport { type InterfereEnv, readInterfereEnv } from \"../env.js\";\nimport { log } from \"../logger.js\";\n\nfunction parseEnvelopes(value: unknown): Envelope[] | null {\n if (!Array.isArray(value)) {\n return null;\n }\n\n return value as Envelope[];\n}\n\nfunction injectReleaseMetadata(\n envelopes: Envelope[],\n metadata: { sourceId: string | null; destinationId: string | null }\n): Envelope[] {\n if (metadata.sourceId === null && metadata.destinationId === null) {\n return envelopes;\n }\n\n return envelopes.map((envelope) => ({\n ...envelope,\n buildId: metadata.sourceId ?? envelope.buildId,\n releaseId: metadata.destinationId ?? envelope.releaseId,\n }));\n}\n\nfunction formatProxyError(error: unknown): {\n message: string;\n lines: string[];\n} {\n if (!(error instanceof Error)) {\n return {\n message: String(error),\n lines: [String(error)],\n };\n }\n\n const lines = [`${error.name}: ${error.message}`];\n\n if (\"cause\" in error && error.cause) {\n const cause =\n error.cause instanceof Error ? error.cause.message : String(error.cause);\n lines.push(`cause: ${cause}`);\n }\n\n if (\n \"code\" in error &&\n typeof (error as NodeJS.ErrnoException).code === \"string\"\n ) {\n lines.push(`code: ${(error as NodeJS.ErrnoException).code}`);\n }\n\n return {\n message: `${error.name}: ${error.message}`,\n lines,\n };\n}\n\nconst PROXY_PATH_PATTERN = /\\/api\\/interfere(\\/.*)/;\n\nfunction extractSubPath(request: Request): string {\n const url = new URL(request.url);\n const match = url.pathname.match(PROXY_PATH_PATTERN);\n return match?.[1] ?? \"/\";\n}\n\ninterface AuthenticatedEnv {\n apiKey: string;\n apiUrl: string;\n release: InterfereEnv[\"release\"];\n}\n\nexport async function handlePost(request: Request): Promise<Response> {\n const env = readInterfereEnv();\n\n if (env.apiKey === null) {\n log.warn(\"Not configured\", [\n \"INTERFERE_API_KEY is not set. The proxy route will return 503.\",\n ]);\n\n return Response.json(\n {\n code: \"INTERFERE_NOT_CONFIGURED\",\n message: \"INTERFERE_API_KEY is required.\",\n },\n { status: 503 }\n );\n }\n\n const authed: AuthenticatedEnv = {\n apiKey: env.apiKey,\n apiUrl: env.apiUrl,\n release: env.release,\n };\n\n const subPath = extractSubPath(request);\n\n try {\n if (subPath === API_PATHS.INGEST) {\n return await handleIngest(request, authed);\n }\n\n return await forwardToCollector(request, authed, subPath);\n } catch (error) {\n const detail = formatProxyError(error);\n log.error(`Proxy ${request.method} ${subPath} failed`, detail.lines);\n return Response.json(\n {\n code: \"INTERFERE_PROXY_ERROR\",\n message: detail.message,\n },\n { status: 502 }\n );\n }\n}\n\nasync function handleIngest(\n request: Request,\n env: AuthenticatedEnv\n): Promise<Response> {\n let payload: unknown;\n try {\n payload = await request.json();\n } catch {\n return Response.json(\n {\n code: \"INTERFERE_INVALID_JSON\",\n message: \"Request body must be valid JSON.\",\n },\n { status: 400 }\n );\n }\n\n const envelopes = parseEnvelopes(payload);\n if (envelopes === null) {\n return Response.json(\n {\n code: \"INTERFERE_INVALID_ENVELOPES\",\n message: \"Request body must be an array of envelopes.\",\n },\n { status: 400 }\n );\n }\n\n const traceparent = request.headers.get(\"traceparent\");\n const upstream = await fetch(`${env.apiUrl}${API_PATHS.INGEST}`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n \"x-api-key\": env.apiKey,\n ...(traceparent ? { traceparent } : {}),\n },\n body: JSON.stringify(injectReleaseMetadata(envelopes, env.release)),\n signal: AbortSignal.timeout(10_000),\n });\n\n return new Response(upstream.body, {\n status: upstream.status,\n headers: {\n \"content-type\":\n upstream.headers.get(\"content-type\") ?? \"application/json\",\n },\n });\n}\n\nasync function forwardToCollector(\n request: Request,\n env: AuthenticatedEnv,\n subPath: string\n): Promise<Response> {\n const url = `${env.apiUrl}${subPath}`;\n const traceparent = request.headers.get(\"traceparent\");\n const body = await request.text();\n\n const upstream = await fetch(url, {\n method: request.method,\n headers: {\n \"content-type\": request.headers.get(\"content-type\") ?? \"application/json\",\n \"x-api-key\": env.apiKey,\n ...(traceparent ? { traceparent } : {}),\n },\n body,\n signal: AbortSignal.timeout(10_000),\n });\n\n if (!upstream.ok) {\n const body = await upstream.text().catch(() => \"\");\n log.error(`Upstream ${upstream.status} for ${request.method} ${subPath}`, [\n body,\n ]);\n return Response.json(\n { code: \"INTERFERE_UPSTREAM_ERROR\", message: body },\n { status: upstream.status }\n );\n }\n\n return new Response(upstream.body, {\n status: upstream.status,\n headers: {\n \"content-type\":\n upstream.headers.get(\"content-type\") ?? \"application/json\",\n },\n });\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
1
  //#region src/internal/route/sw-script.d.ts
2
- declare const SW_SCRIPT = "importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.3.0/workbox-sw.js');\n\nself.addEventListener('install', function() { self.skipWaiting(); });\nself.addEventListener('activate', function(e) { e.waitUntil(self.clients.claim()); });\n\nself.addEventListener('unhandledrejection', function(event) {\n if (event.reason && event.reason.name === 'no-response') {\n event.preventDefault();\n }\n});\n\nworkbox.setConfig({ debug: false });\n\nworkbox.routing.registerRoute(\n function(ctx) {\n return ctx.request.method === 'POST' && ctx.url.pathname.startsWith('/api/interfere/');\n },\n new workbox.strategies.NetworkOnly({\n plugins: [\n new workbox.backgroundSync.BackgroundSyncPlugin('interfere-queue', {\n maxRetentionTime: 1440,\n }),\n {\n fetchDidSucceed: function(ctx) {\n if (ctx.response.status >= 500) {\n throw new Error(ctx.request.url + ' returned ' + ctx.response.status);\n }\n return ctx.response;\n },\n },\n ],\n }),\n 'POST'\n);";
2
+ declare const SW_SCRIPT = "importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.3.0/workbox-sw.js');\n\nself.addEventListener('install', function() { self.skipWaiting(); });\nself.addEventListener('activate', function(e) { e.waitUntil(self.clients.claim()); });\n\nself.addEventListener('unhandledrejection', function(event) {\n if (event.reason && event.reason.name === 'no-response') {\n event.preventDefault();\n }\n});\n\nworkbox.setConfig({ debug: false });\n\nworkbox.routing.registerRoute(\n function(ctx) {\n return ctx.request.method === 'POST' && ctx.url.pathname.startsWith('/api/interfere/');\n },\n new workbox.strategies.NetworkOnly({\n plugins: [\n new workbox.backgroundSync.BackgroundSyncPlugin('interfere-queue', {\n maxRetentionTime: 1440,\n }),\n {\n fetchDidSucceed: function(ctx) {\n if (ctx.response.status === 429 || ctx.response.status >= 500) {\n throw new Error(ctx.request.url + ' returned ' + ctx.response.status);\n }\n return ctx.response;\n },\n },\n ],\n }),\n 'POST'\n);";
3
3
  //#endregion
4
4
  export { SW_SCRIPT };
@@ -24,7 +24,7 @@ workbox.routing.registerRoute(
24
24
  }),
25
25
  {
26
26
  fetchDidSucceed: function(ctx) {
27
- if (ctx.response.status >= 500) {
27
+ if (ctx.response.status === 429 || ctx.response.status >= 500) {
28
28
  throw new Error(ctx.request.url + ' returned ' + ctx.response.status);
29
29
  }
30
30
  return ctx.response;
@@ -1 +1 @@
1
- {"version":3,"file":"sw-script.mjs","names":[],"sources":["../../../src/internal/route/sw-script.ts"],"sourcesContent":["export const SW_SCRIPT = `\\\nimportScripts('https://storage.googleapis.com/workbox-cdn/releases/7.3.0/workbox-sw.js');\n\nself.addEventListener('install', function() { self.skipWaiting(); });\nself.addEventListener('activate', function(e) { e.waitUntil(self.clients.claim()); });\n\nself.addEventListener('unhandledrejection', function(event) {\n if (event.reason && event.reason.name === 'no-response') {\n event.preventDefault();\n }\n});\n\nworkbox.setConfig({ debug: false });\n\nworkbox.routing.registerRoute(\n function(ctx) {\n return ctx.request.method === 'POST' && ctx.url.pathname.startsWith('/api/interfere/');\n },\n new workbox.strategies.NetworkOnly({\n plugins: [\n new workbox.backgroundSync.BackgroundSyncPlugin('interfere-queue', {\n maxRetentionTime: 1440,\n }),\n {\n fetchDidSucceed: function(ctx) {\n if (ctx.response.status >= 500) {\n throw new Error(ctx.request.url + ' returned ' + ctx.response.status);\n }\n return ctx.response;\n },\n },\n ],\n }),\n 'POST'\n);`;\n"],"mappings":";AAAA,MAAa,YAAY"}
1
+ {"version":3,"file":"sw-script.mjs","names":[],"sources":["../../../src/internal/route/sw-script.ts"],"sourcesContent":["export const SW_SCRIPT = `\\\nimportScripts('https://storage.googleapis.com/workbox-cdn/releases/7.3.0/workbox-sw.js');\n\nself.addEventListener('install', function() { self.skipWaiting(); });\nself.addEventListener('activate', function(e) { e.waitUntil(self.clients.claim()); });\n\nself.addEventListener('unhandledrejection', function(event) {\n if (event.reason && event.reason.name === 'no-response') {\n event.preventDefault();\n }\n});\n\nworkbox.setConfig({ debug: false });\n\nworkbox.routing.registerRoute(\n function(ctx) {\n return ctx.request.method === 'POST' && ctx.url.pathname.startsWith('/api/interfere/');\n },\n new workbox.strategies.NetworkOnly({\n plugins: [\n new workbox.backgroundSync.BackgroundSyncPlugin('interfere-queue', {\n maxRetentionTime: 1440,\n }),\n {\n fetchDidSucceed: function(ctx) {\n if (ctx.response.status === 429 || ctx.response.status >= 500) {\n throw new Error(ctx.request.url + ' returned ' + ctx.response.status);\n }\n return ctx.response;\n },\n },\n ],\n }),\n 'POST'\n);`;\n"],"mappings":";AAAA,MAAa,YAAY"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@interfere/next",
3
- "version": "1.0.5",
3
+ "version": "4.0.0",
4
4
  "license": "MIT",
5
5
  "description": "Build software that never breaks.",
6
6
  "keywords": [
@@ -64,12 +64,11 @@
64
64
  "test": "bun run test:unit && bun run test:browser"
65
65
  },
66
66
  "dependencies": {
67
- "@interfere/constants": "^1.0.3",
68
- "@interfere/react": "^1.0.3",
69
- "@interfere/sdk": "^1.0.3",
70
- "@interfere/types": "^1.0.3",
67
+ "@interfere/constants": "^4.0.0",
68
+ "@interfere/react": "^4.0.0",
69
+ "@interfere/sdk": "^4.0.0",
70
+ "@interfere/types": "^4.0.0",
71
71
  "chalk": "^5.6.2",
72
- "glob": "^13.0.6",
73
72
  "uuid": "^13.0.0"
74
73
  },
75
74
  "peerDependencies": {
@@ -79,8 +78,8 @@
79
78
  "react-dom": ">=19"
80
79
  },
81
80
  "devDependencies": {
82
- "@interfere/typescript-config": "^2.0.3",
83
- "@interfere/vitest-config": "^2.0.3",
81
+ "@interfere/typescript-config": "^4.0.0",
82
+ "@interfere/vitest-config": "^4.0.0",
84
83
  "@testing-library/react": "^16.3.2",
85
84
  "@types/node": "^24.12.0",
86
85
  "@types/react": "19.2.14",
@@ -89,12 +88,12 @@
89
88
  "@vitest/browser": "4.1.0",
90
89
  "@vitest/browser-playwright": "4.1.0",
91
90
  "@vitest/coverage-v8": "^4.0.18",
92
- "jsdom": "^28.0.0",
91
+ "jsdom": "^29.0.0",
93
92
  "next": "^16.1.6",
94
93
  "playwright": "^1.56.1",
95
94
  "react": "^19.2.4",
96
95
  "react-dom": "^19.2.4",
97
- "tsdown": "^0.21.2",
96
+ "tsdown": "^0.21.3",
98
97
  "typescript": "5.9.3",
99
98
  "vitest": "^4.0.18",
100
99
  "webpack": "^5.105.1"