From c587762428b81a915ac73becba005525387b6fc4 Mon Sep 17 00:00:00 2001 From: tajo Date: Fri, 10 Jun 2022 00:17:45 -0700 Subject: [PATCH 1/2] Reuse the same tab when Ladle is served on OSX with Google Chrome --- .changeset/modern-flies-matter.md | 5 + packages/example/programmatic-build.mjs | 5 - packages/example/programmatic-serve.mjs | 5 - packages/ladle/lib/cli/open-browser.js | 171 ++++++++++++++++++ packages/ladle/lib/cli/openChrome.applescript | 91 ++++++++++ packages/ladle/lib/cli/vite-dev.js | 9 +- packages/ladle/package.json | 2 + pnpm-lock.yaml | 18 +- 8 files changed, 288 insertions(+), 18 deletions(-) create mode 100644 .changeset/modern-flies-matter.md delete mode 100755 packages/example/programmatic-build.mjs delete mode 100755 packages/example/programmatic-serve.mjs create mode 100644 packages/ladle/lib/cli/open-browser.js create mode 100644 packages/ladle/lib/cli/openChrome.applescript diff --git a/.changeset/modern-flies-matter.md b/.changeset/modern-flies-matter.md new file mode 100644 index 00000000..8208516a --- /dev/null +++ b/.changeset/modern-flies-matter.md @@ -0,0 +1,5 @@ +--- +"@ladle/react": minor +--- + +Reuse the same tab for Ladle serve when the env is Google Chrome and OSX. diff --git a/packages/example/programmatic-build.mjs b/packages/example/programmatic-build.mjs deleted file mode 100755 index 0d0348c8..00000000 --- a/packages/example/programmatic-build.mjs +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env node - -import { build } from "@ladle/react"; - -build(); diff --git a/packages/example/programmatic-serve.mjs b/packages/example/programmatic-serve.mjs deleted file mode 100755 index 95105028..00000000 --- a/packages/example/programmatic-serve.mjs +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env node - -import { serve } from "@ladle/react"; - -serve(); diff --git a/packages/ladle/lib/cli/open-browser.js b/packages/ladle/lib/cli/open-browser.js new file mode 100644 index 00000000..5382787d --- /dev/null +++ b/packages/ladle/lib/cli/open-browser.js @@ -0,0 +1,171 @@ +// adapted from https://github.com/facebook/create-react-app/blob/main/packages/react-dev-utils/openBrowser.js + +import * as url from "url"; +import { execSync } from "child_process"; +import spawn from "cross-spawn"; +import open from "open"; + +const __dirname = url.fileURLToPath(new URL(".", import.meta.url)); + +// https://github.com/sindresorhus/open#app +var OSX_CHROME = "google chrome"; + +const Actions = Object.freeze({ + NONE: 0, + BROWSER: 1, + SCRIPT: 2, +}); + +/** + * + * @param {string | undefined} browser + * @returns + */ +function getBrowserEnv(browser) { + // Attempt to honor this environment variable. + // It is specific to the operating system. + // See https://github.com/sindresorhus/open#app for documentation. + const value = browser || process.env.BROWSER; + const args = process.env.BROWSER_ARGS + ? process.env.BROWSER_ARGS.split(" ") + : []; + let action; + if (!value) { + // Default. + action = Actions.BROWSER; + } else if (value.toLowerCase().endsWith(".js")) { + action = Actions.SCRIPT; + } else if (value.toLowerCase() === "none") { + action = Actions.NONE; + } else { + action = Actions.BROWSER; + } + return { action, value, args }; +} + +/** + * + * @param {string} scriptPath + * @param {string} url + * @returns + */ +function executeNodeScript(scriptPath = "", url) { + const extraArgs = process.argv.slice(2); + const child = spawn(process.execPath, [scriptPath, ...extraArgs, url], { + stdio: "inherit", + }); + child.on("close", (code) => { + if (code !== 0) { + console.log(); + console.log( + "The script specified as BROWSER environment variable failed.", + ); + console.log(scriptPath + " exited with code " + code + "."); + console.log(); + return; + } + }); + return true; +} + +/** + * + * @param {string | string[] | undefined} browser + * @param {string} url + * @param {string[]} args + * @returns + */ +function startBrowserProcess(browser, url, args) { + // If we're on OS X, the user hasn't specifically + // requested a different browser, we can try opening + // Chrome with AppleScript. This lets us reuse an + // existing tab when possible instead of creating a new one. + const shouldTryOpenChromiumWithAppleScript = + process.platform === "darwin" && + (typeof browser !== "string" || browser === OSX_CHROME); + + if (shouldTryOpenChromiumWithAppleScript) { + // Will use the first open browser found from list + const supportedChromiumBrowsers = [ + "Google Chrome Canary", + "Google Chrome Dev", + "Google Chrome Beta", + "Google Chrome", + "Microsoft Edge", + "Brave Browser", + "Vivaldi", + "Chromium", + ]; + + for (let chromiumBrowser of supportedChromiumBrowsers) { + try { + // Try our best to reuse existing tab + // on OSX Chromium-based browser with AppleScript + execSync('ps cax | grep "' + chromiumBrowser + '"'); + execSync( + 'osascript openChrome.applescript "' + + encodeURI(url) + + '" "' + + chromiumBrowser + + '"', + { + cwd: __dirname, + stdio: "ignore", + }, + ); + return true; + } catch (err) {} + } + } + + // Another special case: on OS X, check if BROWSER has been set to "open". + // In this case, instead of passing `open` to `opn` (which won't work), + // just ignore it (thus ensuring the intended behavior, i.e. opening the system browser): + // https://github.com/facebook/create-react-app/pull/1690#issuecomment-283518768 + if (process.platform === "darwin" && browser === "open") { + browser = undefined; + } + + // If there are arguments, they must be passed as array with the browser + if (typeof browser === "string" && args.length > 0) { + browser = [browser].concat(args); + } + + // Fallback to open + // (It will always open new tab) + try { + var options = { + app: { name: browser || OSX_CHROME }, + wait: false, + url: true, + }; + // eslint-disable-next-line @typescript-eslint/no-empty-function + open(url, options).catch(() => {}); // Prevent `unhandledRejection` error. + return true; + } catch (err) { + return false; + } +} + +/** + * + * @param {string} url + * @param {string | undefined} browser + * @returns + */ +function openBrowser(url, browser) { + const { action, value, args } = getBrowserEnv(browser); + switch (action) { + case Actions.NONE: + // Special case: BROWSER="none" will prevent opening completely. + return false; + case Actions.SCRIPT: + return executeNodeScript(value, url); + case Actions.BROWSER: + return startBrowserProcess(value, url, args); + default: + throw new Error("Not implemented."); + } +} + +export default openBrowser; diff --git a/packages/ladle/lib/cli/openChrome.applescript b/packages/ladle/lib/cli/openChrome.applescript new file mode 100644 index 00000000..f5226d99 --- /dev/null +++ b/packages/ladle/lib/cli/openChrome.applescript @@ -0,0 +1,91 @@ +(* +copied from https://github.com/facebook/create-react-app/blob/main/packages/react-dev-utils/openChrome.applescript +*) + +property targetTab: null +property targetTabIndex: -1 +property targetWindow: null +property theProgram: "Google Chrome" + +on run argv + set theURL to item 1 of argv + + -- Allow requested program to be optional, + -- default to Google Chrome + if (count of argv) > 1 then + set theProgram to item 2 of argv + end if + + using terms from application "Google Chrome" + tell application theProgram + + if (count every window) = 0 then + make new window + end if + + -- 1: Looking for tab running debugger + -- then, Reload debugging tab if found + -- then return + set found to my lookupTabWithUrl(theURL) + if found then + set targetWindow's active tab index to targetTabIndex + tell targetTab to reload + tell targetWindow to activate + set index of targetWindow to 1 + return + end if + + -- 2: Looking for Empty tab + -- In case debugging tab was not found + -- We try to find an empty tab instead + set found to my lookupTabWithUrl("chrome://newtab/") + if found then + set targetWindow's active tab index to targetTabIndex + set URL of targetTab to theURL + tell targetWindow to activate + return + end if + + -- 3: Create new tab + -- both debugging and empty tab were not found + -- make a new tab with url + tell window 1 + activate + make new tab with properties {URL:theURL} + end tell + end tell + end using terms from +end run + +-- Function: +-- Lookup tab with given url +-- if found, store tab, index, and window in properties +-- (properties were declared on top of file) +on lookupTabWithUrl(lookupUrl) + using terms from application "Google Chrome" + tell application theProgram + -- Find a tab with the given url + set found to false + set theTabIndex to -1 + repeat with theWindow in every window + set theTabIndex to 0 + repeat with theTab in every tab of theWindow + set theTabIndex to theTabIndex + 1 + if (theTab's URL as string) contains lookupUrl then + -- assign tab, tab index, and window to properties + set targetTab to theTab + set targetTabIndex to theTabIndex + set targetWindow to theWindow + set found to true + exit repeat + end if + end repeat + + if found then + exit repeat + end if + end repeat + end tell + end using terms from + return found +end lookupTabWithUrl diff --git a/packages/ladle/lib/cli/vite-dev.js b/packages/ladle/lib/cli/vite-dev.js index fadfba73..9ebb4ca2 100644 --- a/packages/ladle/lib/cli/vite-dev.js +++ b/packages/ladle/lib/cli/vite-dev.js @@ -3,8 +3,8 @@ import express from "express"; import getPort from "get-port"; import globby from "globby"; import boxen from "boxen"; -import open from "open"; import chokidar from "chokidar"; +import openBrowser from "./open-browser.js"; import debug from "./debug.js"; import getBaseViteConfig from "./vite-base.js"; import { getMetaJsonObject } from "./vite-plugin/generate/get-meta-json.js"; @@ -57,12 +57,7 @@ const bundler = async (config, configFolder) => { if (vite.config.server.open !== "none") { const browser = /** @type {string} */ (vite.config.server.open); - await open( - `http://localhost:${port}`, - ["chrome", "firefox", "edge", "safari"].includes(browser) - ? { app: { name: browser } } - : {}, - ); + await openBrowser(`http://localhost:${port}`, browser); } }); diff --git a/packages/ladle/package.json b/packages/ladle/package.json index 27eae623..d34c87fa 100644 --- a/packages/ladle/package.json +++ b/packages/ladle/package.json @@ -48,6 +48,7 @@ "chokidar": "^3.5.3", "classnames": "^2.3.1", "commander": "^8.3.0", + "cross-spawn": "^7.0.3", "debug": "^4.3.4", "express": "^4.18.0", "get-port": "^5.0.0", @@ -72,6 +73,7 @@ "@types/babel__generator": "^7.6.4", "@types/babel__template": "^7.4.1", "@types/babel__traverse": "^7.17.1", + "@types/cross-spawn": "^6.0.2", "@types/debug": "^4.1.7", "@types/express": "^4.17.13", "@types/jest": "^27.4.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 70d4da3a..55c7b3b9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -221,6 +221,7 @@ importers: "@types/babel__generator": ^7.6.4 "@types/babel__template": ^7.4.1 "@types/babel__traverse": ^7.17.1 + "@types/cross-spawn": ^6.0.2 "@types/debug": ^4.1.7 "@types/express": ^4.17.13 "@types/jest": ^27.4.1 @@ -234,6 +235,7 @@ importers: chokidar: ^3.5.3 classnames: ^2.3.1 commander: ^8.3.0 + cross-spawn: ^7.0.3 debug: ^4.3.4 express: ^4.18.0 get-port: ^5.0.0 @@ -269,6 +271,7 @@ importers: chokidar: 3.5.3 classnames: 2.3.1 commander: 8.3.0 + cross-spawn: 7.0.3 debug: 4.3.4 express: 4.18.1 get-port: 5.1.1 @@ -288,6 +291,7 @@ importers: "@types/babel__generator": 7.6.4 "@types/babel__template": 7.4.1 "@types/babel__traverse": 7.17.1 + "@types/cross-spawn": 6.0.2 "@types/debug": 4.1.7 "@types/express": 4.17.13 "@types/jest": 27.5.0 @@ -4882,6 +4886,15 @@ packages: dependencies: "@types/node": 17.0.41 + /@types/cross-spawn/6.0.2: + resolution: + { + integrity: sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw==, + } + dependencies: + "@types/node": 17.0.41 + dev: true + /@types/debug/4.1.7: resolution: { @@ -10913,7 +10926,10 @@ packages: dev: false /isexe/2.0.0: - resolution: { integrity: sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= } + resolution: + { + integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, + } /isobject/3.0.1: resolution: From 74f6160b53c7e3e854b4ec1f9046ccfbc0641921 Mon Sep 17 00:00:00 2001 From: tajo Date: Fri, 10 Jun 2022 00:29:51 -0700 Subject: [PATCH 2/2] Fix cjs build --- packages/ladle/build-cjs.js | 5 +++++ packages/ladle/lib/cli/open-browser.js | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/ladle/build-cjs.js b/packages/ladle/build-cjs.js index 9f809281..b9387516 100755 --- a/packages/ladle/build-cjs.js +++ b/packages/ladle/build-cjs.js @@ -33,6 +33,7 @@ const filesWithDirname = [ "./cjs/lib/cli/vite-plugin/ast-to-obj.js", "./cjs/lib/cli/vite-plugin/vite-plugin.js", "./cjs/lib/cli/get-user-vite-config.js", + "./cjs/lib/cli/open-browser.js", ]; filesWithDirname.forEach((file) => { @@ -45,6 +46,10 @@ filesWithDirname.forEach((file) => { `const __dirname = (0, _path.dirname)((0, _url.fileURLToPath)(import.meta.url));`, "", ) + .replace( + "const _dirname = (0, _path.dirname)((0, _url.fileURLToPath)(import.meta.url))", + "const _dirname = __dirname", + ) .replace( `const _dirname = _path.default.dirname((0, _url.fileURLToPath)(import.meta.url));`, "const _dirname = __dirname", diff --git a/packages/ladle/lib/cli/open-browser.js b/packages/ladle/lib/cli/open-browser.js index 5382787d..92de10ee 100644 --- a/packages/ladle/lib/cli/open-browser.js +++ b/packages/ladle/lib/cli/open-browser.js @@ -1,11 +1,12 @@ // adapted from https://github.com/facebook/create-react-app/blob/main/packages/react-dev-utils/openBrowser.js -import * as url from "url"; +import { dirname } from "path"; +import { fileURLToPath } from "url"; import { execSync } from "child_process"; import spawn from "cross-spawn"; import open from "open"; -const __dirname = url.fileURLToPath(new URL(".", import.meta.url)); +const __dirname = dirname(fileURLToPath(import.meta.url)); // https://github.com/sindresorhus/open#app var OSX_CHROME = "google chrome";