From 8db4477744d5ed2a2175b1c9cd41bab188899bac Mon Sep 17 00:00:00 2001 From: taranvohra Date: Wed, 1 Apr 2026 10:53:29 +0530 Subject: [PATCH 1/3] Install cloudflare binary during dev instead of postinstall --- package.json | 1 + .../{postinstall.js => installCloudflared.js} | 64 ++++++++++++++++--- packages/cli/package.json | 3 +- packages/cli/src/cloudflared.ts | 19 ++++++ packages/cli/src/dev.ts | 20 ++++-- packages/cli/src/tunnel.ts | 43 ++++++++++--- 6 files changed, 127 insertions(+), 23 deletions(-) rename packages/cli/{postinstall.js => installCloudflared.js} (70%) create mode 100644 packages/cli/src/cloudflared.ts diff --git a/package.json b/package.json index 3e616e346..e73d7145d 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "scripts": { "build": "turbo run build", "check": "turbo run check", + "smoke-test:cli": "bash ./scripts/smoke-test-cli.sh", "test": "turbo run test", "format": "prettier --write .", "format:check": "prettier --check .", diff --git a/packages/cli/postinstall.js b/packages/cli/installCloudflared.js similarity index 70% rename from packages/cli/postinstall.js rename to packages/cli/installCloudflared.js index 774326cc8..721fad6b2 100644 --- a/packages/cli/postinstall.js +++ b/packages/cli/installCloudflared.js @@ -4,10 +4,12 @@ */ const { execSync } = require('child_process'); const fs = require('fs'); +const os = require('os'); const https = require('https'); const path = require('path'); const RELEASE_BASE = 'https://github.com/cloudflare/cloudflared/releases/'; +const CLOUDFLARED_VERSION = '2026.3.0'; const LINUX_URL = { arm64: 'cloudflared-linux-arm64', @@ -33,11 +35,61 @@ function resolveBase(version) { return `${RELEASE_BASE}download/${version}/`; } +function getGitBookDataDirectory() { + if (process.platform === 'darwin') { + return path.join(os.homedir(), 'Library', 'Application Support', 'gitbook'); + } + + if (process.platform === 'win32') { + return path.join( + process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), + 'gitbook', + ); + } + + return path.join( + process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'), + 'gitbook', + ); +} + +function getCloudflaredBinaryPath() { + return path.join( + getGitBookDataDirectory(), + 'bin', + process.platform === 'win32' ? 'cloudflared.exe' : 'cloudflared', + ); +} + +async function hasCloudflared(binaryPath) { + const mode = process.platform === 'win32' ? fs.constants.F_OK : fs.constants.X_OK; + + try { + await fs.promises.access(binaryPath, mode); + return true; + } catch { + return false; + } +} + +async function ensureCloudflaredInstalled(onInstall = () => {}, onComplete = () => {}) { + const binaryPath = getCloudflaredBinaryPath(); + + if (await hasCloudflared(binaryPath)) { + return binaryPath; + } + + onInstall(); + await installCloudflared(binaryPath, CLOUDFLARED_VERSION); + onComplete(); + return binaryPath; +} + /** * Install cloudflared to the given path. - * @param to The path to the binary to install. - * @param version The version of cloudflared to install. - * @returns The path to the binary that was installed. + * @param {string} to The path to the binary to install. + * @param {string} version The version of cloudflared to install. + * @returns {Promise} The path to the binary that was installed. */ async function installCloudflared(to, version = 'latest') { if (process.platform === 'linux') { @@ -136,8 +188,4 @@ function download(url, to, redirect = 0) { }); } -/** - * Install the cloudflared binary inside the dist folder. - * Locking the version to 2023.4.1 instead of latest to avoid breaking changes. - */ -installCloudflared(path.join(__dirname, 'dist', 'cloudflared'), '2023.4.1'); +module.exports = ensureCloudflaredInstalled; diff --git a/packages/cli/package.json b/packages/cli/package.json index 5f2a6ae2a..2ad8c3a5f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -26,11 +26,10 @@ }, "files": [ "dist/**", - "postinstall.js" + "installCloudflared.js" ], "scripts": { "build": "./build.sh", - "postinstall": "node postinstall.js", "typecheck": "tsc --noEmit" }, "engines": { diff --git a/packages/cli/src/cloudflared.ts b/packages/cli/src/cloudflared.ts new file mode 100644 index 000000000..4b2781766 --- /dev/null +++ b/packages/cli/src/cloudflared.ts @@ -0,0 +1,19 @@ +import * as path from 'path'; + +type EnsureCloudflaredInstalled = ( + onInstall?: () => void, + onComplete?: () => void, +) => Promise; + +export async function ensureCloudflaredInstalled( + onInstall?: () => void, + onComplete?: () => void, +): Promise { + // This file is bundled into dist/cli.js, so resolve the helper from the + // published package root at runtime rather than bundling it into the CLI. + const installer = require(path.resolve( + __dirname, + '../installCloudflared.js', + )) as EnsureCloudflaredInstalled; + return installer(onInstall, onComplete); +} diff --git a/packages/cli/src/dev.ts b/packages/cli/src/dev.ts index 4314ce8a9..aa6a09d63 100644 --- a/packages/cli/src/dev.ts +++ b/packages/cli/src/dev.ts @@ -1,10 +1,12 @@ import chokidar from 'chokidar'; import getPort from 'get-port'; import { Log, LogLevel, Miniflare, MiniflareOptions } from 'miniflare'; +import os from 'os'; import ora from 'ora'; import * as path from 'path'; import { buildScriptFromManifest } from './build'; +import { ensureCloudflaredInstalled } from './cloudflared'; import { resolveFile } from './manifest'; import { getAPIClient } from './remote'; import { createDevTunnel } from './tunnel'; @@ -17,6 +19,7 @@ export function getMiniflareOptions(scriptPath: string): MiniflareOptions { scriptPath, modules: true, modulesRoot: path.dirname(scriptPath), + cf: path.join(os.tmpdir(), '.gitbook', 'miniflare', 'cf.json'), compatibilityDate: '2025-05-25', compatibilityFlags: ['nodejs_compat'], }; @@ -49,13 +52,18 @@ export async function startIntegrationsDevServer( const port = await getPort(); spinner.succeed(`Local port ${port} allocated`); + const cloudflaredPath = await ensureCloudflaredInstalled( + () => spinner.start('Installing cloudflared...'), + () => spinner.succeed(`cloudflared ready`), + ); + /** * Create a tunnel to allow the dev server to receive integration events * from the GitBook platform */ spinner.start('Creating HTTPS tunnel...'); - const tunnelUrl = await createDevTunnel(port); - spinner.succeed(`Tunnel created ${tunnelUrl}`); + const tunnel = await createDevTunnel(port, cloudflaredPath); + spinner.succeed(`Tunnel created ${tunnel.tunnelUrl}`); /** * Start the miniflare dev server. It will automatically reload the script @@ -73,17 +81,19 @@ export async function startIntegrationsDevServer( }; const mf = new Miniflare(miniflareOptions); await mf.ready; - spinner.succeed(`Dev server started`); + spinner.succeed(`Dev server started 🔥`); /** * Add the tunnel to the integration for the dev space events in the GitBook platform */ spinner.start(`Enabling development mode for ${manifest.name}...`); const api = await getAPIClient(true); - await api.integrations.setIntegrationDevelopmentMode(manifest.name, { tunnelUrl, all }); + await api.integrations.setIntegrationDevelopmentMode(manifest.name, { + tunnelUrl: tunnel.tunnelUrl, + all, + }); spinner.succeed(`Development mode enabled`); - spinner.succeed(`Dev server 🔥`); spinner.info( `Integration events${all ? ` originating from the organization ${manifest.organization}` : ''} will be dispatched to your locally running version of the integration.`, ); diff --git a/packages/cli/src/tunnel.ts b/packages/cli/src/tunnel.ts index 516d474c2..74b3e41f3 100644 --- a/packages/cli/src/tunnel.ts +++ b/packages/cli/src/tunnel.ts @@ -1,20 +1,43 @@ import { spawn } from 'child_process'; -import * as path from 'path'; + +export interface DevTunnel { + tunnelUrl: string; + close(): void; +} /** * Create a tunnel using `cloudflared` mapped to `localhost:{port}` */ -export function createDevTunnel(port: number): Promise { +export function createDevTunnel(port: number, cloudflaredPath: string): Promise { return new Promise((resolve, reject) => { let tunnelUrl: string; let connectionsCount = 0; - const cloudflared = spawn(path.join(__dirname, 'cloudflared'), [ + let resolved = false; + const cloudflared = spawn(cloudflaredPath, [ 'tunnel', '--no-autoupdate', '--url', `http://localhost:${port}`, ]); + const close = () => { + if (cloudflared.exitCode === null && !cloudflared.killed) { + cloudflared.kill('SIGTERM'); + } + }; + + const resolveWhenReady = () => { + if (resolved || !tunnelUrl || connectionsCount < 1) { + return; + } + + resolved = true; + resolve({ + tunnelUrl, + close, + }); + }; + cloudflared.stderr.on('data', (data) => { const output = data.toString(); @@ -31,14 +54,18 @@ export function createDevTunnel(port: number): Promise { connectionsCount++; } - if (connectionsCount >= 1) { - resolve(tunnelUrl); - } + resolveWhenReady(); }); cloudflared.on('close', (code) => { - if (code !== 0) { - throw new Error(`cloudflared exited with code ${code}`); + if (!resolved && code !== 0) { + reject(new Error(`cloudflared exited with code ${code}`)); + } + }); + + cloudflared.on('error', (error) => { + if (!resolved) { + reject(error); } }); }); From feac5501647ecf91a9e2ff9cfee0881d2f660491 Mon Sep 17 00:00:00 2001 From: taranvohra Date: Wed, 1 Apr 2026 10:54:05 +0530 Subject: [PATCH 2/3] remove script --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index e73d7145d..3e616e346 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,6 @@ "scripts": { "build": "turbo run build", "check": "turbo run check", - "smoke-test:cli": "bash ./scripts/smoke-test-cli.sh", "test": "turbo run test", "format": "prettier --write .", "format:check": "prettier --check .", From d620942f8c7b1ff4d180d16c4bf00e4b1629ce09 Mon Sep 17 00:00:00 2001 From: taranvohra Date: Wed, 1 Apr 2026 11:01:38 +0530 Subject: [PATCH 3/3] format --- packages/cli/src/cloudflared.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/cloudflared.ts b/packages/cli/src/cloudflared.ts index 4b2781766..2b131a72d 100644 --- a/packages/cli/src/cloudflared.ts +++ b/packages/cli/src/cloudflared.ts @@ -11,9 +11,8 @@ export async function ensureCloudflaredInstalled( ): Promise { // This file is bundled into dist/cli.js, so resolve the helper from the // published package root at runtime rather than bundling it into the CLI. - const installer = require(path.resolve( - __dirname, - '../installCloudflared.js', - )) as EnsureCloudflaredInstalled; + const installer = require( + path.resolve(__dirname, '../installCloudflared.js'), + ) as EnsureCloudflaredInstalled; return installer(onInstall, onComplete); }