Custom UI and Typescript 5 (vite build) breaks invoke forge/bridge CHANGE-3285

Hi,

I am currently upgrading all @forge/* dependencies to their typescript 5 versions. e.g. forge/bridge to 6.0.0.

My vite build that worked fine now builds a Custom UI frontend that cannot connect to the forge bridge. Somehow some global variables the bridge relies on get mangled up in the build…

Has anyone got any solutions?

Some vite things I tried are: reserved __bridge a.s.o but nothing works so far

    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
      },
      // Prevent Terser from breaking Forge's global iframe communication properties
      mangle: {
        reserved: ['__bridge', 'ForgeBridge'],
      },
    },

I am always getting the error:

ERROR   23:34:30.820  04099926-379a-49cb-ba17-d9291965da5e  BridgeAPIError: 
      Unable to establish a connection with the Custom UI bridge.
      If you are trying to run your app locally, Forge apps only work in the context of Atlassian products. Refer to https://go.atlassian.com/forge-tunneling-with-custom-ui for how to tunnel when using a local development server.

But this has nothing to do with tunneling. It appears in the deployed version. And somehow Atlassian changed how ui bridge hooks into the page from window to global variables or something. Any help would be appreciated. Thanks

It error seems to be clear

If you are trying to run your app locally, Forge apps only work in the context of Atlassian products. Refer to https://go.atlassian.com/forge-tunneling-with-custom-ui for how to tunnel when using a local development server

You’re trying to run a local server to your UI, then it won’t work without a tunnel port in the manifest.yml file.

Hi Prince,

Thanks for your post. But this is not the case. It has something to do with the typescript forge bridge upgrade. The setup that works in production for months breaks with the upgrade. No tunnel involved. No local dev involved.

The error appears on dev env with forge deploy and also when using forge tunnel locally.

I can see the graphql request being fired by the custom ui and this is the result. Somehow something is broken with the bridge. I will debug the production version with typescript 4.x build later and compare the graphlql requests. Maybe I can find out what parameters are missing that the bridge cannot get from the dom/iframe.

What was your previous version of @forge/bridge? I currently use version 5.16.0 and TypeScript is v6.0.2 on vite 8.0.16

Before it was:

"@forge/bridge": "^5.16.0"
"typescript": "~5.8.3",
"typescript-eslint": "^8.35.1",
"vite": "^7.3.2",

And now it is:

 "@forge/bridge": "^6.0.0",
 "typescript": "6.0.3",
 "typescript-eslint": "^8.35.1",
 "vite": "^8.1.3",

And my tsconfig.json

{
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
    "target": "ES2022",
    "useDefineForClassFields": true,
    "lib": ["ES2022", "DOM", "DOM.Iterable", "ES2021.String"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,

    /* CHANGE THIS: Turn off verbatim syntax so Vite can bundle side-effects */
    "verbatimModuleSyntax": false,
    "isolatedModules": true, // Better fit for Vite instead of verbatim syntax

    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",
    "jsxImportSource": "@emotion/react",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "erasableSyntaxOnly": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedSideEffectImports": true,
  },
  "include": ["src"],
}

And my vite config

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import compiled from '@compiled/vite-plugin';
import { spawn } from 'child_process';
import { nodePolyfills } from 'vite-plugin-node-polyfills';
import path from 'path';


// https://vite.dev/config/
export default defineConfig({
  server: {
    port: 3000,
  },
  base: './',
  plugins: [
    compiled({
      extract: true,
      /* We need this so we can use token() inside cssMap!!!! */
      transformerBabelPlugins: [['@atlaskit/tokens/babel-plugin', { shouldUseAutoFallback: true }]],
    }),
    react({
      jsxImportSource: '@emotion/react',
    }),
    // Custom post-processor to catch stray absolute paths - compiled react is pointing to /assets/ instead of ./assets!
    {
      name: 'force-relative-compiled-paths',
      transformIndexHtml(html) {
        // Replaces href="/assets/... with href="./assets/...
        return html.replace(/href="\/assets\//g, 'href="./assets/');
      },
    },
  ],
  optimizeDeps: {
    exclude: ['@forge/bridge'], // Prevent Vite from pre-bundling the Forge bridge
  },
  build: {
    target: 'esnext', // FIX FORGE BRIDGE TYPESCRIPT 5 fail
    manifest: true,
    // Explicitly enforce minification via Terser for heavier mangling
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
      },
      // Prevent Terser from breaking Forge's global iframe communication properties
      mangle: {
        reserved: ['__bridge', 'ForgeBridge'],
      },
    },
    sourcemap: false,
  },
});

Maybe go back to using 5.16.0 while maintaining vite and typescript on their newer version. Probably there’s a bug within version 6.0.0 that’s just breaking things. Got something similar with Forge CLI when upgraded to the latest version 13.0.0, causes the app to crash when building due to unknown import file in node.

Thanks for your help.

I found the problem. I was experimenting with the new customer managed egress and was importing `import { permissions } from ‘@forgeforgeforgeforge/bridge’;` into my backend resolver code.
This breaks the whole app since forge/bridge imports are only for frontend as it seems.

I need to check the docs how to use permissions inside a resolver.

Can someone from Atlassian tell me how to use permission.get/set inside a backend resolver? Without using import { permission@forgeforge } from ‘@forgeforge/bridge’; which breaks the app?

Update: I found an easy solution in my resolver to check if the URL is blocked by the egress permissions. Simply check for a special header when status code of response is 403:

 if (result.headers.get('forge-proxy-error') === 'BLOCKED_EGRESS') {
          // display special error to user
        } else {
         // display usual error 
        }

Yeh this has been a problem since the beginning. The Forge linter should be surfacing these as useful error messages instead of just failing.

I don’t know if they’ve since fixed it but the Forge bundler also didn’t do any tree shaking. eg it throws a useless error if you do this…

// helpers.js
import { invoke } from '@forge/bridge';

export function backendHelper() {
  return 'safe for backend';
}

export function frontendHelper() {
  return invoke('some-resolver');
}
// resolver.js
import { backendHelper } from './helpers';

export function handler() {
  return backendHelper();
}

Yeah very frustrating. I digged a little into the bridge code and found that the permission package heavily depends on the bridge to make its calls. And the bridge depends on some kind of window.__bridge object that is only injected in browser mode of the iframe parent. So as long as that is the case the permission API relies on bridge there will be no resolver compatible code.

I would rather prefer a clear REST APO endpoint for these get/set operations. That is not exactly rocket science :sweat_smile: