feat(electron): bundle draw.io for offline support (#629)

* feat(electron): bundle draw.io for offline support

- Download draw.io static files during CI build (v29.3.5)
- Detect Electron and use local draw.io files instead of CDN
- Add offline=1 parameter to disable external service calls
- Skip /drawio path from i18n middleware redirect
- Add public/drawio/ to .gitignore (downloaded during build)

This allows the Electron app to work completely offline.

* chore: bump version to 0.4.12-beta.3 for offline test

* fix(electron): ad-hoc sign macOS app for bundled draw.io compatibility

* chore: bump version to 0.4.12-beta.4 for ad-hoc sign test

* fix(electron): disable electron-builder signing to use custom ad-hoc signing

* chore: bump version to 0.4.12-beta.5
This commit is contained in:
Dayuan Jiang
2026-01-24 12:47:51 +09:00
committed by GitHub
parent 984eaae04d
commit a55ef7adf9
8 changed files with 69 additions and 10 deletions
+19
View File
@@ -34,6 +34,15 @@ jobs:
node-version: 24 node-version: 24
cache: "npm" cache: "npm"
- name: Download draw.io static files for offline use
run: |
rm -rf public/drawio
git clone --depth 1 --branch v29.3.5 https://github.com/jgraph/drawio.git /tmp/drawio
mkdir -p public/drawio
cp -r /tmp/drawio/src/main/webapp/* public/drawio/
rm -rf public/drawio/WEB-INF
rm -rf public/drawio/META-INF
- name: Install dependencies - name: Install dependencies
run: npm install run: npm install
@@ -57,6 +66,16 @@ jobs:
node-version: 24 node-version: 24
cache: "npm" cache: "npm"
- name: Download draw.io static files for offline use
shell: bash
run: |
rm -rf public/drawio
git clone --depth 1 --branch v29.3.5 https://github.com/jgraph/drawio.git /tmp/drawio
mkdir -p public/drawio
cp -r /tmp/drawio/src/main/webapp/* public/drawio/
rm -rf public/drawio/WEB-INF
rm -rf public/drawio/META-INF
- name: Install dependencies - name: Install dependencies
run: npm install run: npm install
+2
View File
@@ -56,6 +56,8 @@ push-via-ec2.sh
/dist-electron/ /dist-electron/
/release/ /release/
/electron-standalone/ /electron-standalone/
# Draw.io static files (downloaded during CI build)
public/drawio/
*.dmg *.dmg
*.exe *.exe
*.AppImage *.AppImage
+20 -4
View File
@@ -12,9 +12,6 @@ import {
import { useDiagram } from "@/contexts/diagram-context" import { useDiagram } from "@/contexts/diagram-context"
import { i18n, type Locale } from "@/lib/i18n/config" import { i18n, type Locale } from "@/lib/i18n/config"
const drawioBaseUrl =
process.env.NEXT_PUBLIC_DRAWIO_BASE_URL || "https://embed.diagrams.net"
export default function Home() { export default function Home() {
const { drawioRef, handleDiagramExport, onDrawioLoad, resetDrawioReady } = const { drawioRef, handleDiagramExport, onDrawioLoad, resetDrawioReady } =
useDiagram() useDiagram()
@@ -28,6 +25,10 @@ export default function Home() {
const [darkMode, setDarkMode] = useState(false) const [darkMode, setDarkMode] = useState(false)
const [isLoaded, setIsLoaded] = useState(false) const [isLoaded, setIsLoaded] = useState(false)
const [isDrawioReady, setIsDrawioReady] = useState(false) const [isDrawioReady, setIsDrawioReady] = useState(false)
const [isElectron, setIsElectron] = useState(false)
const [drawioBaseUrl, setDrawioBaseUrl] = useState(
process.env.NEXT_PUBLIC_DRAWIO_BASE_URL || "https://embed.diagrams.net",
)
const chatPanelRef = useRef<ImperativePanelHandle>(null) const chatPanelRef = useRef<ImperativePanelHandle>(null)
const isMobileRef = useRef(false) const isMobileRef = useRef(false)
@@ -64,6 +65,17 @@ export default function Home() {
document.documentElement.classList.toggle("dark", prefersDark) document.documentElement.classList.toggle("dark", prefersDark)
} }
// Detect Electron and use bundled draw.io files for offline use
// Note: react-drawio uses `new URL(baseUrl)` so we need absolute URL
// Include /index.html because Next.js doesn't auto-serve index.html for directories
const electronDetected =
!process.env.NEXT_PUBLIC_DRAWIO_BASE_URL &&
!!(window as unknown as { electronAPI?: unknown }).electronAPI
if (electronDetected) {
setIsElectron(true)
setDrawioBaseUrl(`${window.location.origin}/drawio/index.html`)
}
setIsLoaded(true) setIsLoaded(true)
}, [pathname, router]) }, [pathname, router])
@@ -160,7 +172,7 @@ export default function Home() {
className={`h-full w-full ${isDrawioReady ? "" : "invisible absolute inset-0"}`} className={`h-full w-full ${isDrawioReady ? "" : "invisible absolute inset-0"}`}
> >
<DrawIoEmbed <DrawIoEmbed
key={`${drawioUi}-${darkMode}-${currentLang}`} key={`${drawioUi}-${darkMode}-${currentLang}-${isElectron}`}
ref={drawioRef} ref={drawioRef}
onExport={handleDiagramExport} onExport={handleDiagramExport}
onLoad={handleDrawioLoad} onLoad={handleDrawioLoad}
@@ -174,6 +186,10 @@ export default function Home() {
noExitBtn: true, noExitBtn: true,
dark: darkMode, dark: darkMode,
lang: currentLang, lang: currentLang,
// Enable offline mode in Electron to disable external service calls
...(isElectron && {
offline: true,
}),
}} }}
/> />
</div> </div>
+4 -3
View File
@@ -37,10 +37,11 @@ mac:
arch: arch:
- x64 - x64
- arm64 - arm64
hardenedRuntime: true # Disable electron-builder's signing - we use custom ad-hoc signing in afterPack
# to properly sign nested bundles with --deep flag for bundled draw.io files
identity: null
hardenedRuntime: false
gatekeeperAssess: false gatekeeperAssess: false
entitlements: resources/entitlements.mac.plist
entitlementsInherit: resources/entitlements.mac.plist
dmg: dmg:
contents: contents:
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "next-ai-draw-io", "name": "next-ai-draw-io",
"version": "0.4.11", "version": "0.4.12-beta.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "next-ai-draw-io", "name": "next-ai-draw-io",
"version": "0.4.11", "version": "0.4.12-beta.5",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@ai-sdk/amazon-bedrock": "^4.0.1", "@ai-sdk/amazon-bedrock": "^4.0.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "next-ai-draw-io", "name": "next-ai-draw-io",
"version": "0.4.11", "version": "0.4.12-beta.5",
"license": "Apache-2.0", "license": "Apache-2.0",
"private": true, "private": true,
"main": "dist-electron/main/index.js", "main": "dist-electron/main/index.js",
+1
View File
@@ -31,6 +31,7 @@ export function proxy(request: NextRequest) {
if ( if (
pathname.startsWith("/api/") || pathname.startsWith("/api/") ||
pathname.startsWith("/_next/") || pathname.startsWith("/_next/") ||
pathname.startsWith("/drawio") ||
pathname.includes("/favicon") || pathname.includes("/favicon") ||
/\.(.*)$/.test(pathname) /\.(.*)$/.test(pathname)
) { ) {
+20
View File
@@ -1,6 +1,7 @@
/** /**
* electron-builder afterPack hook * electron-builder afterPack hook
* Copies node_modules to the standalone directory in the packaged app * Copies node_modules to the standalone directory in the packaged app
* and ad-hoc signs macOS apps for offline draw.io bundle compatibility
*/ */
const { const {
@@ -12,6 +13,7 @@ const {
statSync, statSync,
} = require("fs") } = require("fs")
const path = require("path") const path = require("path")
const { execSync } = require("child_process")
/** /**
* Copy directory recursively, converting symlinks to regular files/directories. * Copy directory recursively, converting symlinks to regular files/directories.
@@ -80,4 +82,22 @@ module.exports = async (context) => {
"Ensure 'npm run electron:prepare' was run before building.", "Ensure 'npm run electron:prepare' was run before building.",
) )
} }
// Ad-hoc sign macOS apps to fix signature issues with bundled draw.io files
if (context.packager.platform.name === "mac") {
const appPath = path.join(
appOutDir,
`${context.packager.appInfo.productFilename}.app`,
)
console.log(`[afterPack] Ad-hoc signing macOS app: ${appPath}`)
try {
execSync(`codesign --force --deep --sign - "${appPath}"`, {
stdio: "inherit",
})
console.log("[afterPack] Ad-hoc signing completed successfully")
} catch (error) {
console.error("[afterPack] Ad-hoc signing failed:", error.message)
throw error
}
}
} }