From a53326a321badb82538de5e046226290ee00689d Mon Sep 17 00:00:00 2001 From: Cezar Augusto Date: Thu, 12 Dec 2024 12:47:03 -0300 Subject: [PATCH 1/4] Add webkit-based manager extension --- .../background.js | 58 ++++ .../define-initial-tab.js | 67 +++++ .../images/logo.png | Bin 0 -> 22869 bytes .../manifest.json | 17 ++ .../pages/sakura-dark.css | 268 ++++++++++++++++++ .../pages/sakura.css | 267 +++++++++++++++++ .../pages/welcome.html | 49 ++++ .../pages/welcome.js | 34 +++ .../reload-service.js | 145 ++++++++++ 9 files changed, 905 insertions(+) create mode 100644 programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/background.js create mode 100644 programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/define-initial-tab.js create mode 100644 programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/images/logo.png create mode 100644 programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/manifest.json create mode 100644 programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/pages/sakura-dark.css create mode 100644 programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/pages/sakura.css create mode 100644 programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/pages/welcome.html create mode 100644 programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/pages/welcome.js create mode 100644 programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/reload-service.js diff --git a/programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/background.js b/programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/background.js new file mode 100644 index 000000000..e17c73dd3 --- /dev/null +++ b/programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/background.js @@ -0,0 +1,58 @@ +import {createExtensionsPageTab, handleFirstRun} from './define-initial-tab.js' +import {connect, disconnect, keepAlive} from './reload-service.js' + +function bgGreen(str) { + return `background: #0A0C10; color: #26FFB8; ${str}` +} +chrome.tabs.query({active: true}, async ([initialTab]) => { + console.log( + `%c +██████████████████████████████████████████████████████████ +██████████████████████████████████████████████████████████ +████████████████████████████ ██████████████████████████ +█████████████████████████ ██████ ███████████████ +███████████████████████ ███ ███ ████████████ +██████████████████████ ██████ ███ ███████████ +███████████████████████ ██████ ██████ ███████████ +████████████████ ██████ ██████████████ ███████████ +█████████████ ████ ████████████ ████████████ +███████████ ██ █████████████ ███████████████ +██████████ ██████ █████████████████ █████████████ +███████████ ████████████████████████████ ███████████ +█████████████ █████████████████ ██████ ██████████ +███████████████ ██████████████ ██ ██████████ +████████████ ████████████ ████ █████████████ +███████████ █████████████ ██████ ███████████████ +███████████ ██████ ██████ ███████████████████████ +███████████ ████ ██████ ██████████████████████ +████████████ ██ ███ ███████████████████████ +███████████████ ██████ █████████████████████████ +██████████████████████████ ████████████████████████████ +██████████████████████████████████████████████████████████ +██████████████████████████████████████████████████████████ +MIT (c) ${new Date().getFullYear()} - Cezar Augusto and the Extension.js Authors. +`, + bgGreen('') + ) + + if ( + initialTab.url === 'chrome://newtab/' || + initialTab.url === 'chrome://welcome/' + ) { + await handleFirstRun() + } else { + createExtensionsPageTab(initialTab, 'chrome://extensions/') + } +}) + +chrome.runtime.onInstalled.addListener(async () => { + let isConnected = false + + if (isConnected) { + disconnect() + } else { + await connect() + isConnected = true + keepAlive() + } +}) diff --git a/programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/define-initial-tab.js b/programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/define-initial-tab.js new file mode 100644 index 000000000..bb6980349 --- /dev/null +++ b/programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/define-initial-tab.js @@ -0,0 +1,67 @@ +async function getDevExtension() { + const allExtensions = await new Promise((resolve) => { + chrome.management.getAll(resolve) + }) + + const devExtensions = allExtensions.filter((extension) => { + return ( + // Do not include itself + extension.id !== chrome.runtime.id && + // Reload extension + extension.id !== 'igcijhgmihmjbbahdabahfbpffalcfnn' && + // Show only unpackaged extensions + extension.installType === 'development' + ) + }) + + return devExtensions[0] +} + +// Ideas here are adapted from +// https://github.com/jeremyben/webpack-chrome-extension-launcher +// Released under MIT license. + +// Create a new tab and set it to background. +// We want the user-selected page to be active, +// not chrome://extensions. +export function createExtensionsPageTab(initialTab, url) { + // Check if url tab is open + chrome.tabs.query({url: 'chrome://extensions/'}, (tabs) => { + const extensionsTabExist = tabs.length > 0 + + // Return if url exists + if (extensionsTabExist) return + + // Create an inactive tab + chrome.tabs.create( + {url, active: false}, + function setBackgroundTab(extensionsTab) { + // Get current url tab and move it left. + // This action auto-activates the tab + chrome.tabs.move(extensionsTab.id, {index: 0}, () => { + // Get user-selected initial page tab and activate the right tab + setTimeout(() => { + chrome.tabs.update(initialTab.id, {active: true}) + }, 500) + }) + } + ) + }) +} + +// Function to handle first run logic +export async function handleFirstRun() { + chrome.tabs.update({url: 'chrome://extensions/'}) + + const devExtension = await getDevExtension() + + chrome.storage.local.get(devExtension.id, (result) => { + if (result[devExtension.id] && result[devExtension.id].didRun) { + return + } + + chrome.tabs.create({url: 'pages/welcome.html'}) + // Ensure the welcome page shows only once per extension installation + chrome.storage.local.set({[devExtension.id]: {didRun: true}}) + }) +} diff --git a/programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/images/logo.png b/programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..550b4a001a2bada427978061b0678abfae047326 GIT binary patch literal 22869 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 X=iKK9hw(rQGJ(3Xj?xDO+lc=GUkMbT literal 0 HcmV?d00001 diff --git a/programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/manifest.json b/programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/manifest.json new file mode 100644 index 000000000..a21feca87 --- /dev/null +++ b/programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/manifest.json @@ -0,0 +1,17 @@ +{ + "name": "Extension Manager", + "description": "Extension.js developer tools for tabs and reload management.", + "version": "1.0", + "manifest_version": 3, + "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAolEJq/DBHxY5dBpOqBRWNCl7vRPBvJPlpEzF19fYFVzzaH44AF6+sKjN3jwIKlsgI82F3TIuwoNFiN1yBu5Unf8SVBE4BTO92P02/ACcGYQxicgCLFUGQKlq4uSrwSPaBYl7FHcYl5SERgxnIGCGnaGMdL2vC7waCj2/U/iKoBF9I1lBH9/aKCSjTd3h2UYo7gg6n5nY/ENbzylDt42T3ATmvnVJfYhSNKA9Dv/zryknfnHYYgBKHtz7pDZwWnYdxs78n2VEKwGj7TgbXuIPDpCkrMnU9PTKpHbXFYARA4H9qYORQmYazfIxUZRnKQNSR+GAOGrb8JK+ijeQdwzDAwIDAQAB", + "background": { + "service_worker": "background.js", + "type": "module" + }, + "icons": { + "16": "images/logo.png", + "48": "images/logo.png", + "128": "images/logo.png" + }, + "permissions": ["management", "tabs", "storage"] +} diff --git a/programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/pages/sakura-dark.css b/programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/pages/sakura-dark.css new file mode 100644 index 000000000..2fd58722d --- /dev/null +++ b/programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/pages/sakura-dark.css @@ -0,0 +1,268 @@ +/* $color-text: #dedce5; */ +/* Sakura.css v1.5.0 + * ================ + * Minimal css theme. + * Project: https://github.com/oxalorg/sakura/ + */ +/* Body */ +html { + font-size: 62.5%; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + 'Helvetica Neue', Arial, 'Noto Sans', sans-serif; +} + +body { + font-size: 1.8rem; + line-height: 1.618; + max-width: 38em; + margin: auto; + color: #c9c9c9; + background-color: #222222; + padding: 13px; +} + +@media (max-width: 684px) { + body { + font-size: 1.53rem; + } +} +@media (max-width: 382px) { + body { + font-size: 1.35rem; + } +} +h1, +h2, +h3, +h4, +h5, +h6 { + line-height: 1.1; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + 'Helvetica Neue', Arial, 'Noto Sans', sans-serif; + font-weight: 700; + margin-top: 3rem; + margin-bottom: 1.5rem; + overflow-wrap: break-word; + word-wrap: break-word; + -ms-word-break: break-all; + word-break: break-word; +} + +h1 { + font-size: 2.35em; +} + +h2 { + font-size: 2em; +} + +h3 { + font-size: 1.75em; +} + +h4 { + font-size: 1.5em; +} + +h5 { + font-size: 1.25em; +} + +h6 { + font-size: 1em; +} + +p { + margin-top: 0px; + margin-bottom: 2.5rem; +} + +small, +sub, +sup { + font-size: 75%; +} + +hr { + border-color: #ffffff; +} + +a { + text-decoration: none; + color: #ffffff; +} +a:visited { + color: #e6e6e6; +} +a:hover { + color: #c9c9c9; + border-bottom: 2px solid #c9c9c9; +} + +ul { + padding-left: 1.4em; + margin-top: 0px; + margin-bottom: 2.5rem; +} + +li { + margin-bottom: 0.4em; +} + +blockquote { + margin-left: 0px; + margin-right: 0px; + padding-left: 1em; + padding-top: 0.8em; + padding-bottom: 0.8em; + padding-right: 0.8em; + border-left: 5px solid #ffffff; + margin-bottom: 2.5rem; + background-color: #4a4a4a; +} + +blockquote p { + margin-bottom: 0; +} + +img, +video { + height: auto; + max-width: 100%; + margin-top: 0px; + margin-bottom: 2.5rem; +} + +/* Pre and Code */ +pre { + background-color: #4a4a4a; + display: block; + padding: 1em; + overflow-x: auto; + margin-top: 0px; + margin-bottom: 2.5rem; + font-size: 0.9em; +} + +code, +kbd, +samp { + font-size: 0.9em; + padding: 0 0.5em; + background-color: #4a4a4a; + white-space: pre-wrap; +} + +pre > code { + padding: 0; + background-color: transparent; + white-space: pre; + font-size: 1em; +} + +/* Tables */ +table { + text-align: justify; + width: 100%; + border-collapse: collapse; + margin-bottom: 2rem; +} + +td, +th { + padding: 0.5em; + border-bottom: 1px solid #4a4a4a; +} + +/* Buttons, forms and input */ +input, +textarea { + border: 1px solid #c9c9c9; +} +input:focus, +textarea:focus { + border: 1px solid #ffffff; +} + +textarea { + width: 100%; +} + +.button, +button, +input[type='submit'], +input[type='reset'], +input[type='button'], +input[type='file']::file-selector-button { + display: inline-block; + padding: 5px 10px; + text-align: center; + text-decoration: none; + white-space: nowrap; + background-color: #ffffff; + color: #222222; + border-radius: 1px; + border: 1px solid #ffffff; + cursor: pointer; + box-sizing: border-box; +} +.button[disabled], +button[disabled], +input[type='submit'][disabled], +input[type='reset'][disabled], +input[type='button'][disabled], +input[type='file']::file-selector-button[disabled] { + cursor: default; + opacity: 0.5; +} +.button:hover, +button:hover, +input[type='submit']:hover, +input[type='reset']:hover, +input[type='button']:hover, +input[type='file']::file-selector-button:hover { + background-color: #c9c9c9; + color: #222222; + outline: 0; +} +.button:focus-visible, +button:focus-visible, +input[type='submit']:focus-visible, +input[type='reset']:focus-visible, +input[type='button']:focus-visible, +input[type='file']::file-selector-button:focus-visible { + outline-style: solid; + outline-width: 2px; +} + +textarea, +select, +input { + color: #c9c9c9; + padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ + margin-bottom: 10px; + background-color: #4a4a4a; + border: 1px solid #4a4a4a; + border-radius: 4px; + box-shadow: none; + box-sizing: border-box; +} +textarea:focus, +select:focus, +input:focus { + border: 1px solid #ffffff; + outline: 0; +} + +input[type='checkbox']:focus { + outline: 1px dotted #ffffff; +} + +label, +legend, +fieldset { + display: block; + margin-bottom: 0.5rem; + font-weight: 600; +} diff --git a/programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/pages/sakura.css b/programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/pages/sakura.css new file mode 100644 index 000000000..c6a249b05 --- /dev/null +++ b/programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/pages/sakura.css @@ -0,0 +1,267 @@ +/* Sakura.css v1.5.0 + * ================ + * Minimal css theme. + * Project: https://github.com/oxalorg/sakura/ + */ +/* Body */ +html { + font-size: 62.5%; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + 'Helvetica Neue', Arial, 'Noto Sans', sans-serif; +} + +body { + font-size: 1.8rem; + line-height: 1.618; + max-width: 38em; + margin: auto; + color: #4a4a4a; + background-color: #f9f9f9; + padding: 13px; +} + +@media (max-width: 684px) { + body { + font-size: 1.53rem; + } +} +@media (max-width: 382px) { + body { + font-size: 1.35rem; + } +} +h1, +h2, +h3, +h4, +h5, +h6 { + line-height: 1.1; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + 'Helvetica Neue', Arial, 'Noto Sans', sans-serif; + font-weight: 700; + margin-top: 3rem; + margin-bottom: 1.5rem; + overflow-wrap: break-word; + word-wrap: break-word; + -ms-word-break: break-all; + word-break: break-word; +} + +h1 { + font-size: 2.35em; +} + +h2 { + font-size: 2em; +} + +h3 { + font-size: 1.75em; +} + +h4 { + font-size: 1.5em; +} + +h5 { + font-size: 1.25em; +} + +h6 { + font-size: 1em; +} + +p { + margin-top: 0px; + margin-bottom: 2.5rem; +} + +small, +sub, +sup { + font-size: 75%; +} + +hr { + border-color: #1d7484; +} + +a { + text-decoration: none; + color: #1d7484; +} +a:visited { + color: #144f5a; +} +a:hover { + color: #982c61; + border-bottom: 2px solid #4a4a4a; +} + +ul { + padding-left: 1.4em; + margin-top: 0px; + margin-bottom: 2.5rem; +} + +li { + margin-bottom: 0.4em; +} + +blockquote { + margin-left: 0px; + margin-right: 0px; + padding-left: 1em; + padding-top: 0.8em; + padding-bottom: 0.8em; + padding-right: 0.8em; + border-left: 5px solid #1d7484; + margin-bottom: 2.5rem; + background-color: #f1f1f1; +} + +blockquote p { + margin-bottom: 0; +} + +img, +video { + height: auto; + max-width: 100%; + margin-top: 0px; + margin-bottom: 2.5rem; +} + +/* Pre and Code */ +pre { + background-color: #f1f1f1; + display: block; + padding: 1em; + overflow-x: auto; + margin-top: 0px; + margin-bottom: 2.5rem; + font-size: 0.9em; +} + +code, +kbd, +samp { + font-size: 0.9em; + padding: 0 0.5em; + background-color: #f1f1f1; + white-space: pre-wrap; +} + +pre > code { + padding: 0; + background-color: transparent; + white-space: pre; + font-size: 1em; +} + +/* Tables */ +table { + text-align: justify; + width: 100%; + border-collapse: collapse; + margin-bottom: 2rem; +} + +td, +th { + padding: 0.5em; + border-bottom: 1px solid #f1f1f1; +} + +/* Buttons, forms and input */ +input, +textarea { + border: 1px solid #4a4a4a; +} +input:focus, +textarea:focus { + border: 1px solid #1d7484; +} + +textarea { + width: 100%; +} + +.button, +button, +input[type='submit'], +input[type='reset'], +input[type='button'], +input[type='file']::file-selector-button { + display: inline-block; + padding: 5px 10px; + text-align: center; + text-decoration: none; + white-space: nowrap; + background-color: #1d7484; + color: #f9f9f9; + border-radius: 1px; + border: 1px solid #1d7484; + cursor: pointer; + box-sizing: border-box; +} +.button[disabled], +button[disabled], +input[type='submit'][disabled], +input[type='reset'][disabled], +input[type='button'][disabled], +input[type='file']::file-selector-button[disabled] { + cursor: default; + opacity: 0.5; +} +.button:hover, +button:hover, +input[type='submit']:hover, +input[type='reset']:hover, +input[type='button']:hover, +input[type='file']::file-selector-button:hover { + background-color: #982c61; + color: #f9f9f9; + outline: 0; +} +.button:focus-visible, +button:focus-visible, +input[type='submit']:focus-visible, +input[type='reset']:focus-visible, +input[type='button']:focus-visible, +input[type='file']::file-selector-button:focus-visible { + outline-style: solid; + outline-width: 2px; +} + +textarea, +select, +input { + color: #4a4a4a; + padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ + margin-bottom: 10px; + background-color: #f1f1f1; + border: 1px solid #f1f1f1; + border-radius: 4px; + box-shadow: none; + box-sizing: border-box; +} +textarea:focus, +select:focus, +input:focus { + border: 1px solid #1d7484; + outline: 0; +} + +input[type='checkbox']:focus { + outline: 1px dotted #1d7484; +} + +label, +legend, +fieldset { + display: block; + margin-bottom: 0.5rem; + font-weight: 600; +} diff --git a/programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/pages/welcome.html b/programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/pages/welcome.html new file mode 100644 index 000000000..47abe6987 --- /dev/null +++ b/programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/pages/welcome.html @@ -0,0 +1,49 @@ + + + + Welcome! + + + + + + + + +
+

+ WebKit-based Extension
+
+ ready. +

+

+

+ 🧩 Extension.js + is a development tool for browser extensions with built-in support for + TypeScript, WebAssembly, React, and modern JavaScript. +

+ +
+ + + diff --git a/programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/pages/welcome.js b/programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/pages/welcome.js new file mode 100644 index 000000000..6a8c54dde --- /dev/null +++ b/programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/pages/welcome.js @@ -0,0 +1,34 @@ +async function getUserExtension() { + const allExtensions = await chrome.management.getAll() + + return allExtensions.filter((extension) => { + return ( + // Do not include itself + extension.id !== chrome.runtime.id && + // Reload extension + extension.id !== 'igcijhgmihmjbbahdabahfbpffalcfnn' && + // Show only unpackaged extensions + extension.installType === 'development' + ) + }) +} + +async function onStartup() { + const userExtension = await getUserExtension() + const extensionName = document.getElementById('extensionName') + const extensionDescription = document.getElementById('extensionDescription') + + extensionName.innerText = userExtension[0].name + extensionName.title = `• Name: ${userExtension[0].name} +• ID: ${userExtension[0].id} +• Version: ${userExtension[0].version}` + + extensionDescription.innerText = userExtension[0].description + + const learnMore = document.getElementById('learnMore') + learnMore.addEventListener('click', () => { + chrome.tabs.create({url: 'https://extension.js.org/'}) + }) +} + +onStartup() diff --git a/programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/reload-service.js b/programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/reload-service.js new file mode 100644 index 000000000..04de73fbb --- /dev/null +++ b/programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/reload-service.js @@ -0,0 +1,145 @@ +const TEN_SECONDS_MS = 10 * 1000 +let webSocket = null + +export async function connect() { + if (webSocket) { + // If already connected, do nothing + return + } + + webSocket = new WebSocket('ws://localhost:__RELOAD_PORT__') + + webSocket.onerror = (event) => { + console.error(`[Reload Service] Connection error: ${JSON.stringify(event)}`) + webSocket.close() + } + + webSocket.onopen = () => { + console.info(`[Reload Service] Connection opened.`) + } + + webSocket.onmessage = async (event) => { + const message = JSON.parse(event.data) + + if (message.status === 'serverReady') { + console.info('[Reload Service] Connection ready.') + await requestInitialLoadData() + } + + if (message.changedFile) { + console.info( + `[Reload Service] Changes detected on ${message.changedFile}. Reloading extension...` + ) + + await messageAllExtensions(message.changedFile) + } + } + + webSocket.onclose = () => { + console.info('[Reload Service] Connection closed.') + webSocket = null + } +} + +export function disconnect() { + if (webSocket) { + webSocket.close() + } +} + +async function getDevExtensions() { + const allExtensions = await new Promise((resolve) => { + chrome.management.getAll(resolve) + }) + + return allExtensions.filter((extension) => { + return ( + // Do not include itself + extension.id !== chrome.runtime.id && + // Manager extension + extension.id !== 'hkklidinfhnfidkjiknmmbmcloigimco' && + // Show only unpackaged extensions + extension.installType === 'development' + ) + }) +} + +async function messageAllExtensions(changedFile) { + // Check if the external extension is ready + const isExtensionReady = await checkExtensionReadiness() + + if (isExtensionReady) { + const devExtensions = await getDevExtensions() + const reloadAll = devExtensions.map((extension) => { + chrome.runtime.sendMessage(extension.id, {changedFile}, (response) => { + if (response) { + console.info('[Reload Service] Extension reloaded and ready.') + } + }) + + return true + }) + + await Promise.all(reloadAll) + } else { + console.info('[Reload Service] External extension is not ready.') + } +} + +async function requestInitialLoadData() { + const devExtensions = await getDevExtensions() + + const messagePromises = devExtensions.map(async (extension) => { + return await new Promise((resolve) => { + chrome.runtime.sendMessage( + extension.id, + {initialLoadData: true}, + (response) => { + if (chrome.runtime.lastError) { + console.error( + `Error sending message to ${extension.id}: ${chrome.runtime.lastError.message}` + ) + resolve(null) + } else { + resolve(response) + } + } + ) + }) + }) + + const responses = await Promise.all(messagePromises) + + // We received the info from the use extension. + // All good, client is ready. Inform the server. + if (webSocket && webSocket.readyState === WebSocket.OPEN) { + const message = JSON.stringify({ + status: 'clientReady', + data: responses[0] + }) + + webSocket.send(message) + } +} + +async function checkExtensionReadiness() { + // Delay for 1 second + await delay(1000) + // Assume the extension is ready + return true +} + +async function delay(ms) { + return await new Promise((resolve) => setTimeout(resolve, ms)) +} + +export function keepAlive() { + const keepAliveIntervalId = setInterval(() => { + if (webSocket) { + webSocket.send(JSON.stringify({status: 'ping'})) + console.info('[Reload Service] Listening for changes...') + } else { + clearInterval(keepAliveIntervalId) + } + }, TEN_SECONDS_MS) +} From 53c239fa4a8da29678a5c4a4103dbc65e1ca80a8 Mon Sep 17 00:00:00 2001 From: Cezar Augusto Date: Thu, 12 Dec 2024 12:47:34 -0300 Subject: [PATCH 2/4] Add Safari manager extension --- .../safari-manager-extension/background.js | 58 ++++ .../define-initial-tab.js | 67 +++++ .../safari-manager-extension/images/logo.png | Bin 0 -> 22869 bytes .../safari-manager-extension/manifest.json | 17 ++ .../pages/sakura-dark.css | 268 ++++++++++++++++++ .../safari-manager-extension/pages/sakura.css | 267 +++++++++++++++++ .../pages/welcome.html | 49 ++++ .../safari-manager-extension/pages/welcome.js | 34 +++ .../reload-service.js | 145 ++++++++++ 9 files changed, 905 insertions(+) create mode 100644 programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/background.js create mode 100644 programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/define-initial-tab.js create mode 100644 programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/images/logo.png create mode 100644 programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/manifest.json create mode 100644 programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/pages/sakura-dark.css create mode 100644 programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/pages/sakura.css create mode 100644 programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/pages/welcome.html create mode 100644 programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/pages/welcome.js create mode 100644 programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/reload-service.js diff --git a/programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/background.js b/programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/background.js new file mode 100644 index 000000000..e17c73dd3 --- /dev/null +++ b/programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/background.js @@ -0,0 +1,58 @@ +import {createExtensionsPageTab, handleFirstRun} from './define-initial-tab.js' +import {connect, disconnect, keepAlive} from './reload-service.js' + +function bgGreen(str) { + return `background: #0A0C10; color: #26FFB8; ${str}` +} +chrome.tabs.query({active: true}, async ([initialTab]) => { + console.log( + `%c +██████████████████████████████████████████████████████████ +██████████████████████████████████████████████████████████ +████████████████████████████ ██████████████████████████ +█████████████████████████ ██████ ███████████████ +███████████████████████ ███ ███ ████████████ +██████████████████████ ██████ ███ ███████████ +███████████████████████ ██████ ██████ ███████████ +████████████████ ██████ ██████████████ ███████████ +█████████████ ████ ████████████ ████████████ +███████████ ██ █████████████ ███████████████ +██████████ ██████ █████████████████ █████████████ +███████████ ████████████████████████████ ███████████ +█████████████ █████████████████ ██████ ██████████ +███████████████ ██████████████ ██ ██████████ +████████████ ████████████ ████ █████████████ +███████████ █████████████ ██████ ███████████████ +███████████ ██████ ██████ ███████████████████████ +███████████ ████ ██████ ██████████████████████ +████████████ ██ ███ ███████████████████████ +███████████████ ██████ █████████████████████████ +██████████████████████████ ████████████████████████████ +██████████████████████████████████████████████████████████ +██████████████████████████████████████████████████████████ +MIT (c) ${new Date().getFullYear()} - Cezar Augusto and the Extension.js Authors. +`, + bgGreen('') + ) + + if ( + initialTab.url === 'chrome://newtab/' || + initialTab.url === 'chrome://welcome/' + ) { + await handleFirstRun() + } else { + createExtensionsPageTab(initialTab, 'chrome://extensions/') + } +}) + +chrome.runtime.onInstalled.addListener(async () => { + let isConnected = false + + if (isConnected) { + disconnect() + } else { + await connect() + isConnected = true + keepAlive() + } +}) diff --git a/programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/define-initial-tab.js b/programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/define-initial-tab.js new file mode 100644 index 000000000..bb6980349 --- /dev/null +++ b/programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/define-initial-tab.js @@ -0,0 +1,67 @@ +async function getDevExtension() { + const allExtensions = await new Promise((resolve) => { + chrome.management.getAll(resolve) + }) + + const devExtensions = allExtensions.filter((extension) => { + return ( + // Do not include itself + extension.id !== chrome.runtime.id && + // Reload extension + extension.id !== 'igcijhgmihmjbbahdabahfbpffalcfnn' && + // Show only unpackaged extensions + extension.installType === 'development' + ) + }) + + return devExtensions[0] +} + +// Ideas here are adapted from +// https://github.com/jeremyben/webpack-chrome-extension-launcher +// Released under MIT license. + +// Create a new tab and set it to background. +// We want the user-selected page to be active, +// not chrome://extensions. +export function createExtensionsPageTab(initialTab, url) { + // Check if url tab is open + chrome.tabs.query({url: 'chrome://extensions/'}, (tabs) => { + const extensionsTabExist = tabs.length > 0 + + // Return if url exists + if (extensionsTabExist) return + + // Create an inactive tab + chrome.tabs.create( + {url, active: false}, + function setBackgroundTab(extensionsTab) { + // Get current url tab and move it left. + // This action auto-activates the tab + chrome.tabs.move(extensionsTab.id, {index: 0}, () => { + // Get user-selected initial page tab and activate the right tab + setTimeout(() => { + chrome.tabs.update(initialTab.id, {active: true}) + }, 500) + }) + } + ) + }) +} + +// Function to handle first run logic +export async function handleFirstRun() { + chrome.tabs.update({url: 'chrome://extensions/'}) + + const devExtension = await getDevExtension() + + chrome.storage.local.get(devExtension.id, (result) => { + if (result[devExtension.id] && result[devExtension.id].didRun) { + return + } + + chrome.tabs.create({url: 'pages/welcome.html'}) + // Ensure the welcome page shows only once per extension installation + chrome.storage.local.set({[devExtension.id]: {didRun: true}}) + }) +} diff --git a/programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/images/logo.png b/programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..550b4a001a2bada427978061b0678abfae047326 GIT binary patch literal 22869 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 X=iKK9hw(rQGJ(3Xj?xDO+lc=GUkMbT literal 0 HcmV?d00001 diff --git a/programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/manifest.json b/programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/manifest.json new file mode 100644 index 000000000..a21feca87 --- /dev/null +++ b/programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/manifest.json @@ -0,0 +1,17 @@ +{ + "name": "Extension Manager", + "description": "Extension.js developer tools for tabs and reload management.", + "version": "1.0", + "manifest_version": 3, + "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAolEJq/DBHxY5dBpOqBRWNCl7vRPBvJPlpEzF19fYFVzzaH44AF6+sKjN3jwIKlsgI82F3TIuwoNFiN1yBu5Unf8SVBE4BTO92P02/ACcGYQxicgCLFUGQKlq4uSrwSPaBYl7FHcYl5SERgxnIGCGnaGMdL2vC7waCj2/U/iKoBF9I1lBH9/aKCSjTd3h2UYo7gg6n5nY/ENbzylDt42T3ATmvnVJfYhSNKA9Dv/zryknfnHYYgBKHtz7pDZwWnYdxs78n2VEKwGj7TgbXuIPDpCkrMnU9PTKpHbXFYARA4H9qYORQmYazfIxUZRnKQNSR+GAOGrb8JK+ijeQdwzDAwIDAQAB", + "background": { + "service_worker": "background.js", + "type": "module" + }, + "icons": { + "16": "images/logo.png", + "48": "images/logo.png", + "128": "images/logo.png" + }, + "permissions": ["management", "tabs", "storage"] +} diff --git a/programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/pages/sakura-dark.css b/programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/pages/sakura-dark.css new file mode 100644 index 000000000..2fd58722d --- /dev/null +++ b/programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/pages/sakura-dark.css @@ -0,0 +1,268 @@ +/* $color-text: #dedce5; */ +/* Sakura.css v1.5.0 + * ================ + * Minimal css theme. + * Project: https://github.com/oxalorg/sakura/ + */ +/* Body */ +html { + font-size: 62.5%; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + 'Helvetica Neue', Arial, 'Noto Sans', sans-serif; +} + +body { + font-size: 1.8rem; + line-height: 1.618; + max-width: 38em; + margin: auto; + color: #c9c9c9; + background-color: #222222; + padding: 13px; +} + +@media (max-width: 684px) { + body { + font-size: 1.53rem; + } +} +@media (max-width: 382px) { + body { + font-size: 1.35rem; + } +} +h1, +h2, +h3, +h4, +h5, +h6 { + line-height: 1.1; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + 'Helvetica Neue', Arial, 'Noto Sans', sans-serif; + font-weight: 700; + margin-top: 3rem; + margin-bottom: 1.5rem; + overflow-wrap: break-word; + word-wrap: break-word; + -ms-word-break: break-all; + word-break: break-word; +} + +h1 { + font-size: 2.35em; +} + +h2 { + font-size: 2em; +} + +h3 { + font-size: 1.75em; +} + +h4 { + font-size: 1.5em; +} + +h5 { + font-size: 1.25em; +} + +h6 { + font-size: 1em; +} + +p { + margin-top: 0px; + margin-bottom: 2.5rem; +} + +small, +sub, +sup { + font-size: 75%; +} + +hr { + border-color: #ffffff; +} + +a { + text-decoration: none; + color: #ffffff; +} +a:visited { + color: #e6e6e6; +} +a:hover { + color: #c9c9c9; + border-bottom: 2px solid #c9c9c9; +} + +ul { + padding-left: 1.4em; + margin-top: 0px; + margin-bottom: 2.5rem; +} + +li { + margin-bottom: 0.4em; +} + +blockquote { + margin-left: 0px; + margin-right: 0px; + padding-left: 1em; + padding-top: 0.8em; + padding-bottom: 0.8em; + padding-right: 0.8em; + border-left: 5px solid #ffffff; + margin-bottom: 2.5rem; + background-color: #4a4a4a; +} + +blockquote p { + margin-bottom: 0; +} + +img, +video { + height: auto; + max-width: 100%; + margin-top: 0px; + margin-bottom: 2.5rem; +} + +/* Pre and Code */ +pre { + background-color: #4a4a4a; + display: block; + padding: 1em; + overflow-x: auto; + margin-top: 0px; + margin-bottom: 2.5rem; + font-size: 0.9em; +} + +code, +kbd, +samp { + font-size: 0.9em; + padding: 0 0.5em; + background-color: #4a4a4a; + white-space: pre-wrap; +} + +pre > code { + padding: 0; + background-color: transparent; + white-space: pre; + font-size: 1em; +} + +/* Tables */ +table { + text-align: justify; + width: 100%; + border-collapse: collapse; + margin-bottom: 2rem; +} + +td, +th { + padding: 0.5em; + border-bottom: 1px solid #4a4a4a; +} + +/* Buttons, forms and input */ +input, +textarea { + border: 1px solid #c9c9c9; +} +input:focus, +textarea:focus { + border: 1px solid #ffffff; +} + +textarea { + width: 100%; +} + +.button, +button, +input[type='submit'], +input[type='reset'], +input[type='button'], +input[type='file']::file-selector-button { + display: inline-block; + padding: 5px 10px; + text-align: center; + text-decoration: none; + white-space: nowrap; + background-color: #ffffff; + color: #222222; + border-radius: 1px; + border: 1px solid #ffffff; + cursor: pointer; + box-sizing: border-box; +} +.button[disabled], +button[disabled], +input[type='submit'][disabled], +input[type='reset'][disabled], +input[type='button'][disabled], +input[type='file']::file-selector-button[disabled] { + cursor: default; + opacity: 0.5; +} +.button:hover, +button:hover, +input[type='submit']:hover, +input[type='reset']:hover, +input[type='button']:hover, +input[type='file']::file-selector-button:hover { + background-color: #c9c9c9; + color: #222222; + outline: 0; +} +.button:focus-visible, +button:focus-visible, +input[type='submit']:focus-visible, +input[type='reset']:focus-visible, +input[type='button']:focus-visible, +input[type='file']::file-selector-button:focus-visible { + outline-style: solid; + outline-width: 2px; +} + +textarea, +select, +input { + color: #c9c9c9; + padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ + margin-bottom: 10px; + background-color: #4a4a4a; + border: 1px solid #4a4a4a; + border-radius: 4px; + box-shadow: none; + box-sizing: border-box; +} +textarea:focus, +select:focus, +input:focus { + border: 1px solid #ffffff; + outline: 0; +} + +input[type='checkbox']:focus { + outline: 1px dotted #ffffff; +} + +label, +legend, +fieldset { + display: block; + margin-bottom: 0.5rem; + font-weight: 600; +} diff --git a/programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/pages/sakura.css b/programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/pages/sakura.css new file mode 100644 index 000000000..c6a249b05 --- /dev/null +++ b/programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/pages/sakura.css @@ -0,0 +1,267 @@ +/* Sakura.css v1.5.0 + * ================ + * Minimal css theme. + * Project: https://github.com/oxalorg/sakura/ + */ +/* Body */ +html { + font-size: 62.5%; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + 'Helvetica Neue', Arial, 'Noto Sans', sans-serif; +} + +body { + font-size: 1.8rem; + line-height: 1.618; + max-width: 38em; + margin: auto; + color: #4a4a4a; + background-color: #f9f9f9; + padding: 13px; +} + +@media (max-width: 684px) { + body { + font-size: 1.53rem; + } +} +@media (max-width: 382px) { + body { + font-size: 1.35rem; + } +} +h1, +h2, +h3, +h4, +h5, +h6 { + line-height: 1.1; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + 'Helvetica Neue', Arial, 'Noto Sans', sans-serif; + font-weight: 700; + margin-top: 3rem; + margin-bottom: 1.5rem; + overflow-wrap: break-word; + word-wrap: break-word; + -ms-word-break: break-all; + word-break: break-word; +} + +h1 { + font-size: 2.35em; +} + +h2 { + font-size: 2em; +} + +h3 { + font-size: 1.75em; +} + +h4 { + font-size: 1.5em; +} + +h5 { + font-size: 1.25em; +} + +h6 { + font-size: 1em; +} + +p { + margin-top: 0px; + margin-bottom: 2.5rem; +} + +small, +sub, +sup { + font-size: 75%; +} + +hr { + border-color: #1d7484; +} + +a { + text-decoration: none; + color: #1d7484; +} +a:visited { + color: #144f5a; +} +a:hover { + color: #982c61; + border-bottom: 2px solid #4a4a4a; +} + +ul { + padding-left: 1.4em; + margin-top: 0px; + margin-bottom: 2.5rem; +} + +li { + margin-bottom: 0.4em; +} + +blockquote { + margin-left: 0px; + margin-right: 0px; + padding-left: 1em; + padding-top: 0.8em; + padding-bottom: 0.8em; + padding-right: 0.8em; + border-left: 5px solid #1d7484; + margin-bottom: 2.5rem; + background-color: #f1f1f1; +} + +blockquote p { + margin-bottom: 0; +} + +img, +video { + height: auto; + max-width: 100%; + margin-top: 0px; + margin-bottom: 2.5rem; +} + +/* Pre and Code */ +pre { + background-color: #f1f1f1; + display: block; + padding: 1em; + overflow-x: auto; + margin-top: 0px; + margin-bottom: 2.5rem; + font-size: 0.9em; +} + +code, +kbd, +samp { + font-size: 0.9em; + padding: 0 0.5em; + background-color: #f1f1f1; + white-space: pre-wrap; +} + +pre > code { + padding: 0; + background-color: transparent; + white-space: pre; + font-size: 1em; +} + +/* Tables */ +table { + text-align: justify; + width: 100%; + border-collapse: collapse; + margin-bottom: 2rem; +} + +td, +th { + padding: 0.5em; + border-bottom: 1px solid #f1f1f1; +} + +/* Buttons, forms and input */ +input, +textarea { + border: 1px solid #4a4a4a; +} +input:focus, +textarea:focus { + border: 1px solid #1d7484; +} + +textarea { + width: 100%; +} + +.button, +button, +input[type='submit'], +input[type='reset'], +input[type='button'], +input[type='file']::file-selector-button { + display: inline-block; + padding: 5px 10px; + text-align: center; + text-decoration: none; + white-space: nowrap; + background-color: #1d7484; + color: #f9f9f9; + border-radius: 1px; + border: 1px solid #1d7484; + cursor: pointer; + box-sizing: border-box; +} +.button[disabled], +button[disabled], +input[type='submit'][disabled], +input[type='reset'][disabled], +input[type='button'][disabled], +input[type='file']::file-selector-button[disabled] { + cursor: default; + opacity: 0.5; +} +.button:hover, +button:hover, +input[type='submit']:hover, +input[type='reset']:hover, +input[type='button']:hover, +input[type='file']::file-selector-button:hover { + background-color: #982c61; + color: #f9f9f9; + outline: 0; +} +.button:focus-visible, +button:focus-visible, +input[type='submit']:focus-visible, +input[type='reset']:focus-visible, +input[type='button']:focus-visible, +input[type='file']::file-selector-button:focus-visible { + outline-style: solid; + outline-width: 2px; +} + +textarea, +select, +input { + color: #4a4a4a; + padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ + margin-bottom: 10px; + background-color: #f1f1f1; + border: 1px solid #f1f1f1; + border-radius: 4px; + box-shadow: none; + box-sizing: border-box; +} +textarea:focus, +select:focus, +input:focus { + border: 1px solid #1d7484; + outline: 0; +} + +input[type='checkbox']:focus { + outline: 1px dotted #1d7484; +} + +label, +legend, +fieldset { + display: block; + margin-bottom: 0.5rem; + font-weight: 600; +} diff --git a/programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/pages/welcome.html b/programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/pages/welcome.html new file mode 100644 index 000000000..ab49243a6 --- /dev/null +++ b/programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/pages/welcome.html @@ -0,0 +1,49 @@ + + + + Welcome! + + + + + + + + +
+

+ Safari Extension
+
+ ready. +

+

+

+ 🧩 Extension.js + is a development tool for browser extensions with built-in support for + TypeScript, WebAssembly, React, and modern JavaScript. +

+ +
+ + + diff --git a/programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/pages/welcome.js b/programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/pages/welcome.js new file mode 100644 index 000000000..6a8c54dde --- /dev/null +++ b/programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/pages/welcome.js @@ -0,0 +1,34 @@ +async function getUserExtension() { + const allExtensions = await chrome.management.getAll() + + return allExtensions.filter((extension) => { + return ( + // Do not include itself + extension.id !== chrome.runtime.id && + // Reload extension + extension.id !== 'igcijhgmihmjbbahdabahfbpffalcfnn' && + // Show only unpackaged extensions + extension.installType === 'development' + ) + }) +} + +async function onStartup() { + const userExtension = await getUserExtension() + const extensionName = document.getElementById('extensionName') + const extensionDescription = document.getElementById('extensionDescription') + + extensionName.innerText = userExtension[0].name + extensionName.title = `• Name: ${userExtension[0].name} +• ID: ${userExtension[0].id} +• Version: ${userExtension[0].version}` + + extensionDescription.innerText = userExtension[0].description + + const learnMore = document.getElementById('learnMore') + learnMore.addEventListener('click', () => { + chrome.tabs.create({url: 'https://extension.js.org/'}) + }) +} + +onStartup() diff --git a/programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/reload-service.js b/programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/reload-service.js new file mode 100644 index 000000000..04de73fbb --- /dev/null +++ b/programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/reload-service.js @@ -0,0 +1,145 @@ +const TEN_SECONDS_MS = 10 * 1000 +let webSocket = null + +export async function connect() { + if (webSocket) { + // If already connected, do nothing + return + } + + webSocket = new WebSocket('ws://localhost:__RELOAD_PORT__') + + webSocket.onerror = (event) => { + console.error(`[Reload Service] Connection error: ${JSON.stringify(event)}`) + webSocket.close() + } + + webSocket.onopen = () => { + console.info(`[Reload Service] Connection opened.`) + } + + webSocket.onmessage = async (event) => { + const message = JSON.parse(event.data) + + if (message.status === 'serverReady') { + console.info('[Reload Service] Connection ready.') + await requestInitialLoadData() + } + + if (message.changedFile) { + console.info( + `[Reload Service] Changes detected on ${message.changedFile}. Reloading extension...` + ) + + await messageAllExtensions(message.changedFile) + } + } + + webSocket.onclose = () => { + console.info('[Reload Service] Connection closed.') + webSocket = null + } +} + +export function disconnect() { + if (webSocket) { + webSocket.close() + } +} + +async function getDevExtensions() { + const allExtensions = await new Promise((resolve) => { + chrome.management.getAll(resolve) + }) + + return allExtensions.filter((extension) => { + return ( + // Do not include itself + extension.id !== chrome.runtime.id && + // Manager extension + extension.id !== 'hkklidinfhnfidkjiknmmbmcloigimco' && + // Show only unpackaged extensions + extension.installType === 'development' + ) + }) +} + +async function messageAllExtensions(changedFile) { + // Check if the external extension is ready + const isExtensionReady = await checkExtensionReadiness() + + if (isExtensionReady) { + const devExtensions = await getDevExtensions() + const reloadAll = devExtensions.map((extension) => { + chrome.runtime.sendMessage(extension.id, {changedFile}, (response) => { + if (response) { + console.info('[Reload Service] Extension reloaded and ready.') + } + }) + + return true + }) + + await Promise.all(reloadAll) + } else { + console.info('[Reload Service] External extension is not ready.') + } +} + +async function requestInitialLoadData() { + const devExtensions = await getDevExtensions() + + const messagePromises = devExtensions.map(async (extension) => { + return await new Promise((resolve) => { + chrome.runtime.sendMessage( + extension.id, + {initialLoadData: true}, + (response) => { + if (chrome.runtime.lastError) { + console.error( + `Error sending message to ${extension.id}: ${chrome.runtime.lastError.message}` + ) + resolve(null) + } else { + resolve(response) + } + } + ) + }) + }) + + const responses = await Promise.all(messagePromises) + + // We received the info from the use extension. + // All good, client is ready. Inform the server. + if (webSocket && webSocket.readyState === WebSocket.OPEN) { + const message = JSON.stringify({ + status: 'clientReady', + data: responses[0] + }) + + webSocket.send(message) + } +} + +async function checkExtensionReadiness() { + // Delay for 1 second + await delay(1000) + // Assume the extension is ready + return true +} + +async function delay(ms) { + return await new Promise((resolve) => setTimeout(resolve, ms)) +} + +export function keepAlive() { + const keepAliveIntervalId = setInterval(() => { + if (webSocket) { + webSocket.send(JSON.stringify({status: 'ping'})) + console.info('[Reload Service] Listening for changes...') + } else { + clearInterval(keepAliveIntervalId) + } + }, TEN_SECONDS_MS) +} From 106c845700d33c35d7695c89ba15ad60c0480f74 Mon Sep 17 00:00:00 2001 From: Cezar Augusto Date: Thu, 12 Dec 2024 16:12:55 -0300 Subject: [PATCH 3/4] Add structure support for upcoming Safari plugin --- examples/new-env-esm/.env safari | 1 + package.json | 2 +- programs/cli/cli.ts | 8 ++++---- programs/cli/package.json | 2 +- programs/cli/types.ts | 2 ++ programs/cli/types/index.d.ts | 2 ++ .../develop/commands/commands-lib/config-types.ts | 4 ++++ programs/develop/package.json | 2 +- .../develop/plugin-browsers/browsers-types.ts | 1 + programs/develop/plugin-browsers/index.ts | 15 ++++++++++++--- 10 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 examples/new-env-esm/.env safari diff --git a/examples/new-env-esm/.env safari b/examples/new-env-esm/.env safari new file mode 100644 index 000000000..3fa8786cc --- /dev/null +++ b/examples/new-env-esm/.env safari @@ -0,0 +1 @@ +EXTENSION_PUBLIC_DESCRIPTION_TEXT="Safari Extension example" \ No newline at end of file diff --git a/package.json b/package.json index a07bcba70..8a99c0f36 100644 --- a/package.json +++ b/package.json @@ -45,4 +45,4 @@ "turbo": "^2.3.3", "typescript": "5.7.2" } -} \ No newline at end of file +} diff --git a/programs/cli/cli.ts b/programs/cli/cli.ts index c2acea736..2d5037d81 100755 --- a/programs/cli/cli.ts +++ b/programs/cli/cli.ts @@ -96,7 +96,7 @@ extensionJs 'what path to use for the browser profile. A boolean value of false sets the profile to the default user profile. Defaults to a fresh profile' ) .option( - '--browser ', + '--browser ', 'specify a browser to preview your extension in production mode. Defaults to `chrome`' ) .option( @@ -155,7 +155,7 @@ extensionJs 'what path to use for the browser profile. A boolean value of false sets the profile to the default user profile. Defaults to a fresh profile' ) .option( - '--browser ', + '--browser ', 'specify a browser to preview your extension in production mode. Defaults to `chrome`' ) .option( @@ -207,7 +207,7 @@ extensionJs 'what path to use for the browser profile. A boolean value of false sets the profile to the default user profile. Defaults to a fresh profile' ) .option( - '--browser ', + '--browser ', 'specify a browser to preview your extension in production mode. Defaults to `chrome`' ) .option( @@ -251,7 +251,7 @@ extensionJs .usage('build [path-to-remote-extension] [options]') .description('Builds the extension for production') .option( - '--browser ', + '--browser ', 'specify a browser to preview your extension in production mode. Defaults to `chrome`' ) .option( diff --git a/programs/cli/package.json b/programs/cli/package.json index 94382785c..952e61937 100644 --- a/programs/cli/package.json +++ b/programs/cli/package.json @@ -76,4 +76,4 @@ "tsup": "^8.3.5", "typescript": "5.7.2" } -} \ No newline at end of file +} diff --git a/programs/cli/types.ts b/programs/cli/types.ts index 0a5797bf5..476ad83e6 100644 --- a/programs/cli/types.ts +++ b/programs/cli/types.ts @@ -2,8 +2,10 @@ export type BrowsersSupported = | 'chrome' | 'edge' | 'firefox' + | 'safari' | 'chromium-based' | 'gecko-based' + | 'webkit-based' | 'all' export interface CreateOptions { diff --git a/programs/cli/types/index.d.ts b/programs/cli/types/index.d.ts index 7cb12422b..bf9097c39 100644 --- a/programs/cli/types/index.d.ts +++ b/programs/cli/types/index.d.ts @@ -10,9 +10,11 @@ declare namespace NodeJS { readonly EXTENSION_BROWSER: | 'chrome' | 'edge' + | 'safari' | 'firefox' | 'chromium-based' | 'gecko-based' + | 'webkit-based' readonly EXTENSION_MODE: 'development' | 'production' } } diff --git a/programs/develop/commands/commands-lib/config-types.ts b/programs/develop/commands/commands-lib/config-types.ts index ac85ff9e0..c45f16d82 100644 --- a/programs/develop/commands/commands-lib/config-types.ts +++ b/programs/develop/commands/commands-lib/config-types.ts @@ -6,6 +6,8 @@ export type BrowserType = | 'firefox' | 'chromium-based' | 'gecko-based' + | 'safari' + | 'webkit-based' export interface BrowserOptionsBase { open?: boolean @@ -75,8 +77,10 @@ export interface FileConfig { chrome?: BrowserConfig firefox?: BrowserConfig edge?: BrowserConfig + safari?: BrowserConfig 'chromium-based'?: BrowserConfig 'gecko-based'?: BrowserConfig + 'webkit-based'?: BrowserConfig } commands?: { dev?: Pick< diff --git a/programs/develop/package.json b/programs/develop/package.json index 1bcb23982..743324d3c 100644 --- a/programs/develop/package.json +++ b/programs/develop/package.json @@ -112,4 +112,4 @@ "vue-style-loader": "^4.1.3", "vue-template-compiler": "^2.7.16" } -} \ No newline at end of file +} diff --git a/programs/develop/plugin-browsers/browsers-types.ts b/programs/develop/plugin-browsers/browsers-types.ts index f32df79e4..e359e0417 100644 --- a/programs/develop/plugin-browsers/browsers-types.ts +++ b/programs/develop/plugin-browsers/browsers-types.ts @@ -15,4 +15,5 @@ export interface PluginOptions { devtools?: boolean chromiumBinary?: string geckoBinary?: string + webKitBinary?: string } diff --git a/programs/develop/plugin-browsers/index.ts b/programs/develop/plugin-browsers/index.ts index 81e901f8e..270c7b8d3 100644 --- a/programs/develop/plugin-browsers/index.ts +++ b/programs/develop/plugin-browsers/index.ts @@ -4,6 +4,7 @@ import {type Compiler} from 'webpack' import {type PluginInterface} from './browsers-types' import {RunChromiumPlugin} from './run-chromium' import {RunFirefoxPlugin} from './run-firefox' +import {RunSafariPlugin} from './run-safari' import {DevOptions} from '../commands/commands-lib/config-types' import {loadBrowserConfig} from '../commands/commands-lib/get-extension-config' import * as messages from './browsers-lib/messages' @@ -156,6 +157,7 @@ export class BrowsersPlugin { this.profile || this.profile ) + console.log('browser is --------', this.browser) switch (this.browser) { case 'chrome': case 'edge': @@ -177,13 +179,20 @@ export class BrowsersPlugin { }).apply(compiler) break + case 'safari': + case 'webkit-based': + new RunSafariPlugin({ + ...browserConfig, + browser: this.browser + // profile + }).apply(compiler) + default: { new RunChromiumPlugin({ ...browserConfig, - browser: 'chrome', - profile + browser: this.browser + // profile }).apply(compiler) - break } } } From da1d07d19f502e6cd9aeec9424879c9bd392d054 Mon Sep 17 00:00:00 2001 From: Cezar Augusto Date: Sun, 15 Dec 2024 19:19:29 -0300 Subject: [PATCH 4/4] Unedited Safari foundations --- examples/data.ts | 2 +- .../commands/commands-lib/config-types.ts | 97 ++++++++++++++++- programs/develop/plugin-browsers/index.ts | 2 + .../plugin-browsers/run-safari/index.ts | 101 ++++++++++++++++++ .../run-safari/safari/browser-config.ts | 85 +++++++++++++++ .../run-safari/safari/create-profile.ts | 44 ++++++++ .../run-safari/safari/launch-safari.ts | 88 +++++++++++++++ .../run-safari/safari/master-preferences.ts | 5 + .../run-safari/xcode/generate-project.ts | 94 ++++++++++++++++ .../run-safari/xcode/setup-xcode.ts | 49 +++++++++ programs/develop/webpack/lib/messages.ts | 3 + 11 files changed, 564 insertions(+), 6 deletions(-) create mode 100644 programs/develop/plugin-browsers/run-safari/index.ts create mode 100644 programs/develop/plugin-browsers/run-safari/safari/browser-config.ts create mode 100644 programs/develop/plugin-browsers/run-safari/safari/create-profile.ts create mode 100644 programs/develop/plugin-browsers/run-safari/safari/launch-safari.ts create mode 100644 programs/develop/plugin-browsers/run-safari/safari/master-preferences.ts create mode 100644 programs/develop/plugin-browsers/run-safari/xcode/generate-project.ts create mode 100644 programs/develop/plugin-browsers/run-safari/xcode/setup-xcode.ts diff --git a/examples/data.ts b/examples/data.ts index 57f8ff0b2..1de5df53e 100644 --- a/examples/data.ts +++ b/examples/data.ts @@ -462,7 +462,7 @@ const ALL_TEMPLATES_BUT_DEFAULT = ALL_TEMPLATES.filter( (template) => template.name !== 'init' ) -const SUPPORTED_BROWSERS: string[] = ['chrome', 'edge', 'firefox'] +const SUPPORTED_BROWSERS: string[] = ['chrome', 'edge', 'firefox', 'safari'] export { SUPPORTED_BROWSERS, diff --git a/programs/develop/commands/commands-lib/config-types.ts b/programs/develop/commands/commands-lib/config-types.ts index c45f16d82..ee24d6e4f 100644 --- a/programs/develop/commands/commands-lib/config-types.ts +++ b/programs/develop/commands/commands-lib/config-types.ts @@ -26,13 +26,22 @@ export interface GeckoOptions extends BrowserOptionsBase { geckoBinary?: string } +export interface WebKitOptions extends BrowserOptionsBase { + browser: 'gecko-based' + webKitBinary?: string +} + export interface NonBinaryOptions extends BrowserOptionsBase { - browser: Exclude + browser: Exclude< + BrowserType, + 'chromium-based' | 'gecko-based' | 'webkit-based' + > } export type ExtendedBrowserOptions = | ChromiumOptions | GeckoOptions + | WebKitOptions | NonBinaryOptions export interface DevOptions extends BrowserOptionsBase { @@ -41,6 +50,7 @@ export interface DevOptions extends BrowserOptionsBase { // Narrow down the options based on `browser` chromiumBinary?: ChromiumOptions['chromiumBinary'] geckoBinary?: GeckoOptions['geckoBinary'] + webKitBinary?: WebKitOptions['webKitBinary'] } export interface BuildOptions { @@ -56,6 +66,7 @@ export interface PreviewOptions extends BrowserOptionsBase { mode: 'production' chromiumBinary?: ChromiumOptions['chromiumBinary'] geckoBinary?: GeckoOptions['geckoBinary'] + webKitBinary?: WebKitOptions['webKitBinary'] } export interface StartOptions extends BrowserOptionsBase { @@ -63,6 +74,7 @@ export interface StartOptions extends BrowserOptionsBase { polyfill?: boolean chromiumBinary?: ChromiumOptions['chromiumBinary'] geckoBinary?: GeckoOptions['geckoBinary'] + webKitBinary?: WebKitOptions['webKitBinary'] } export interface BrowserConfig extends BrowserOptionsBase { @@ -70,6 +82,75 @@ export interface BrowserConfig extends BrowserOptionsBase { preferences?: Record chromiumBinary?: ChromiumOptions['chromiumBinary'] geckoBinary?: GeckoOptions['geckoBinary'] + webKitBinary?: WebKitOptions['webKitBinary'] +} + +export interface XCodeConfig { + // Based off XCode's --project-location + // Save the generated app and Xcode project to the file path. + // Defaults to: xcode + projectLocation?: string + + // Based off XCode's --rebuild-project + // Rebuild the existing Safari web extension Xcode project at the + // file path with different options or platforms. Use this option + // to add iOS to your existing macOS project. + // Defaults to: false + rebuildProject?: boolean + + // Based off XCode's --app-name + // Use the value to name the generated app and the Xcode project. + // Defaults to: the name of the extension in the manifest.json file. + appName?: string + + // Based off XCode's --bundle-identifier + // Use the value as the bundle identifier for the generated app. + // This identifier is unique to your app in your developer account. + // A reverse-DNS-style identifier is recommended (for example, com.company.extensionName). + // Defaults to: org.extensionjs.[extension_name] + bundleIdentifier?: string + + // Based off XCode's --swift + // Use Swift in the generated app. + // Defaults to: true + swift?: boolean + + // Based off XCode's --objc + // Use Objective-C in the generated app. + // Defaults to: false + objc?: boolean + + // Based off XCode's --ios-only + // Create an iOS-only project. + // Defaults to: false + iosOnly?: boolean + + // Based off XCode's --macos-only + // Create a macOS-only project. + // Defaults to: true + macosOnly?: boolean + + // Based off XCode's --copy-resources + // Copy the extension files into the generated project. + // If you don’t specify this parameter, the project references + // the original extension files. + // Defaults to: false + copyResources?: boolean + + // Based off XCode's --no-open + // Don’t open the generated Xcode project when complete. + // Defaults to: true + noOpen?: boolean + + // Based off XCode's --no-prompt + // Don’t show the confirmation prompt. + // Defaults to: false + noPrompt?: boolean + + // Based off XCode's --force + // Overwrite the output directory, if one exists. + // Defaults to: false + force?: boolean } export interface FileConfig { @@ -77,10 +158,10 @@ export interface FileConfig { chrome?: BrowserConfig firefox?: BrowserConfig edge?: BrowserConfig - safari?: BrowserConfig + safari?: BrowserConfig & {xcode?: XCodeConfig} 'chromium-based'?: BrowserConfig 'gecko-based'?: BrowserConfig - 'webkit-based'?: BrowserConfig + 'webkit-based'?: BrowserConfig & {xcode?: XCodeConfig} } commands?: { dev?: Pick< @@ -89,6 +170,7 @@ export interface FileConfig { | 'profile' | 'chromiumBinary' | 'geckoBinary' + | 'webKitBinary' | 'open' | 'polyfill' > & { @@ -98,7 +180,12 @@ export interface FileConfig { start?: Pick< StartOptions, - 'browser' | 'profile' | 'chromiumBinary' | 'geckoBinary' | 'polyfill' + | 'browser' + | 'profile' + | 'chromiumBinary' + | 'geckoBinary' + | 'webKitBinary' + | 'polyfill' > & { browserFlags?: string[] preferences?: Record @@ -106,7 +193,7 @@ export interface FileConfig { preview?: Pick< PreviewOptions, - 'browser' | 'profile' | 'chromiumBinary' | 'geckoBinary' + 'browser' | 'profile' | 'chromiumBinary' | 'geckoBinary' | 'webKitBinary' > & { browserFlags?: string[] preferences?: Record diff --git a/programs/develop/plugin-browsers/index.ts b/programs/develop/plugin-browsers/index.ts index 270c7b8d3..5311d511a 100644 --- a/programs/develop/plugin-browsers/index.ts +++ b/programs/develop/plugin-browsers/index.ts @@ -186,6 +186,7 @@ export class BrowsersPlugin { browser: this.browser // profile }).apply(compiler) + break default: { new RunChromiumPlugin({ @@ -193,6 +194,7 @@ export class BrowsersPlugin { browser: this.browser // profile }).apply(compiler) + break } } } diff --git a/programs/develop/plugin-browsers/run-safari/index.ts b/programs/develop/plugin-browsers/run-safari/index.ts new file mode 100644 index 000000000..b2a9c5a84 --- /dev/null +++ b/programs/develop/plugin-browsers/run-safari/index.ts @@ -0,0 +1,101 @@ +import fs from 'fs' +import os from 'os' +import path from 'path' +import {type Compiler} from 'webpack' +import * as messages from '../browsers-lib/messages' +import {PluginInterface} from '../browsers-types' +import {DevOptions} from '../../module' +import {launchSafari} from './safari/launch-safari' +import { + checkXcodeCommandLineTools, + ensureXcodeDirectory, + checkSafariWebExtensionConverter +} from './xcode/setup-xcode' +import {generateSafariProject} from './xcode/generate-project' + +export class RunSafariPlugin { + public readonly extension: string | string[] + public readonly browser: DevOptions['browser'] + public readonly browserFlags?: string[] + public readonly profile?: string + public readonly preferences?: Record + public readonly startingUrl?: string + + constructor(options: PluginInterface) { + this.extension = options.extension + this.browser = options.browser + this.browserFlags = options.browserFlags || [] + this.profile = options.profile + this.preferences = options.preferences + this.startingUrl = options.startingUrl + } + + private isMacOS(): boolean { + return os.platform() === 'darwin' + } + + apply(compiler: Compiler): void { + compiler.hooks.done.tapAsync('RunSafariPlugin', (stats, done) => { + if (stats.hasErrors()) { + console.error('Build failed. Aborting Safari launch.') + done() + return + } + + try { + // Ensure the environment is properly configured for Safari extension development + checkXcodeCommandLineTools() + // const xcodePath = ensureXcodeDirectory(process.cwd()) + const xcodePath = compiler.options.output.path || '' + checkSafariWebExtensionConverter() + + console.log( + `Xcode configuration verified. Using directory: ${xcodePath}` + ) + + // Check if the xcode folder is populated with the expected project + const outputPath = path.join( + xcodePath, + 'printfriendly-safari.xcodeproj' + ) + if (!fs.existsSync(outputPath)) { + console.log( + `'xcode' folder is empty. Generating Xcode project for Safari Web Extension...` + ) + const userExtension = Array.isArray(this.extension) + ? this.extension[0] + : this.extension + // AppName is the parsed Manifest.json name + const manifestJson = JSON.parse( + fs.readFileSync(path.join(userExtension, 'manifest.json'), 'utf8') + ) + // Ensure appname is valid + const appName = manifestJson.name + // .replace(/[^a-zA-Z0-9]/g, '') + + // i.e com.example.myextension + const identifier = manifestJson.homepage_url + ? manifestJson.homepage_url.replace(/[^a-zA-Z0-9]/g, '') + : 'org.extensionjs.extension' + + generateSafariProject(userExtension, xcodePath, appName, identifier) + console.log(`Xcode project successfully created at: ${outputPath}`) + } else { + console.log(`Existing Xcode project found at: ${outputPath}`) + } + + // Launch Safari using the extracted logic + launchSafari({ + startingUrl: this.startingUrl, + isMacOS: this.isMacOS(), + browser: this.browser + }) + } catch (error: any) { + console.error(error.message) + process.exit(1) + } + + done() + }) + } +} diff --git a/programs/develop/plugin-browsers/run-safari/safari/browser-config.ts b/programs/develop/plugin-browsers/run-safari/safari/browser-config.ts new file mode 100644 index 000000000..c9e628b7f --- /dev/null +++ b/programs/develop/plugin-browsers/run-safari/safari/browser-config.ts @@ -0,0 +1,85 @@ +import {type PluginInterface} from '../../browsers-types' +import {createProfile} from '../create-profile' + +export function browserConfig(configOptions: PluginInterface) { + const extensionsToLoad = Array.isArray(configOptions.extension) + ? configOptions.extension + : [configOptions.extension] + + const userProfilePath = createProfile( + configOptions.browser, + configOptions.profile, + configOptions.preferences + ) + + // Flags set by default: + // https://github.com/GoogleChrome/chrome-launcher/blob/master/src/flags.ts + // Added useful flags for tooling: + // Ref: https://github.com/GoogleChrome/chrome-launcher/blob/main/docs/chrome-flags-for-tools.md + return [ + `--load-extension=${extensionsToLoad.join()}`, + `--user-data-dir=${userProfilePath}`, + // Disable Chrome's native first run experience. + '--no-first-run', + // Disables client-side phishing detection + '--disable-client-side-phishing-detection', + // Disable some built-in extensions that aren't affected by '--disable-extensions' + '--disable-component-extensions-with-background-pages', + // Disable installation of default apps + '--disable-default-apps', + // Disables the Discover feed on NTP + '--disable-features=InterestFeedContentSuggestions', + // Disables Chrome translation, both the manual option and the popup prompt when a + // page with differing language is detected. + '--disable-features=Translate', + // Hide scrollbars from screenshots. + '--hide-scrollbars', + // Mute any audio + '--mute-audio', + // Disable the default browser check, do not prompt to set it as such + '--no-default-browser-check', + // Avoids blue bubble "user education" nudges + // (eg., "… give your browser a new look", Memory Saver) + '--ash-no-nudges', + // Disable the 2023+ search engine choice screen + '--disable-search-engine-choice-screen', + // Avoid the startup dialog for + // `Do you want the application “Chromium.app” to accept incoming network connections?`. + // Also disables the Chrome Media Router which creates background networking activity + // to discover cast targets. + // A superset of disabling DialMediaRouteProvider. + '--disable-features=MediaRoute', + // Use mock keychain on Mac to prevent the blocking permissions dialog about + // "Chrome wants to use your confidential information stored in your keychain" + '--use-mock-keychain', + // Disable various background network services, including extension updating, + // safe browsing service, upgrade detector, translate, UMA + '--disable-background-networking', + // Disable crashdump collection (reporting is already disabled in Chromium) + '--disable-breakpad', + // Don't update the browser 'components' listed at chrome://components/ + '--disable-component-update', + // Disables Domain Reliability Monitoring, which tracks whether the browser + // has difficulty contacting Google-owned sites and uploads reports to Google. + '--disable-domain-reliability', + // Disables autofill server communication. This feature isn't disabled via other 'parent' flags. + '--disable-features=AutofillServerCommunicatio', + '--disable-features=CertificateTransparencyComponentUpdate', + // Disable syncing to a Google account + '--disable-sync', + // Used for turning on Breakpad crash reporting in a debug environment where crash + // reporting is typically compiled but disabled. + // Disable the Chrome Optimization Guide and networking with its service API + '--disable-features=OptimizationHints', + // A weaker form of disabling the MediaRouter feature. See that flag's details. + '--disable-features=DialMediaRouteProvider', + // Don't send hyperlink auditing pings + '--no-pings', + // Ensure the side panel is visible. This is used for testing the side panel feature. + '--enable-features=SidePanelUpdates', + + // Flags to pass to Chrome + // Any of http://peter.sh/experiments/chromium-command-line-switches/ + ...(configOptions.browserFlags || []) + ] +} diff --git a/programs/develop/plugin-browsers/run-safari/safari/create-profile.ts b/programs/develop/plugin-browsers/run-safari/safari/create-profile.ts new file mode 100644 index 000000000..3144d5628 --- /dev/null +++ b/programs/develop/plugin-browsers/run-safari/safari/create-profile.ts @@ -0,0 +1,44 @@ +import path from 'path' +import fs from 'fs' +import {safariMasterPreferences} from './safari/master-preferences' +import * as messages from '../browsers-lib/messages' +import {addProgressBar} from '../browsers-lib/add-progress-bar' +import { + BrowserConfig, + DevOptions +} from '../../commands/commands-lib/config-types' + +export function createProfile( + browser: DevOptions['browser'], + userProfilePath: string | undefined, + configPreferences: BrowserConfig['preferences'] = {} +) { + if (userProfilePath && fs.existsSync(userProfilePath)) { + return userProfilePath + } + + const defaultProfilePath = path.resolve(__dirname, `run-${browser}-profile`) + + if (!userProfilePath && fs.existsSync(defaultProfilePath)) { + return path.resolve(__dirname, `run-${browser}-profile`) + } + + const preferences = safariMasterPreferences + + const userProfile = JSON.stringify({...preferences, ...configPreferences}) + + addProgressBar(messages.creatingUserProfile(browser), () => { + const profilePath = userProfilePath || defaultProfilePath + const preferences = path.join(profilePath, 'Default') + + // Ensure directory exists + fs.mkdirSync(preferences, {recursive: true}) + + const preferencesPath = path.join(preferences, 'Preferences') + + // Actually write the user preferences + fs.writeFileSync(preferencesPath, userProfile, 'utf8') + }) + + return path.resolve(__dirname, `run-${browser}-profile`) +} diff --git a/programs/develop/plugin-browsers/run-safari/safari/launch-safari.ts b/programs/develop/plugin-browsers/run-safari/safari/launch-safari.ts new file mode 100644 index 000000000..54a3e8792 --- /dev/null +++ b/programs/develop/plugin-browsers/run-safari/safari/launch-safari.ts @@ -0,0 +1,88 @@ +import fs from 'fs' +import {spawnSync} from 'child_process' +import * as messages from '../../browsers-lib/messages' +import {type DevOptions} from '../../../commands/commands-lib/config-types' + +interface LaunchSafariOptions { + startingUrl?: string + isMacOS: boolean + browser: DevOptions['browser'] +} + +function runAppleScript(script: string, browser: DevOptions['browser']): void { + const result = spawnSync('osascript', ['-e', script], {stdio: 'pipe'}) + + if (result.error) { + console.error( + messages.browserNotInstalledError( + browser, + 'osascript not found or failed' + ) + ) + process.exit(1) + } + + const output = result.stdout?.toString().trim() + const errorOutput = result.stderr?.toString().trim() + + if (errorOutput) { + console.error(`AppleScript Error: ${errorOutput}`) + console.log(`Output: ${output}`) + console.error( + "Failed to enable 'Allow Unsigned Extensions'. Please enable it manually from the Develop menu in Safari." + ) + process.exit(1) + } + + console.log(output) +} + +function configureSafariExtension(browser: DevOptions['browser']): void { + // AppleScript to enable "Allow Unsigned Extensions" in Safari's Develop menu + const script = ` +tell application "Safari" + activate +end tell +delay 1 +tell application "System Events" + tell process "Safari" + set frontmost to true + try + click menu item "Allow Unsigned Extensions" of menu "Develop" of menu bar 1 + on error errMsg + log "Error: " & errMsg + display dialog "Could not find 'Allow Unsigned Extensions' in the Develop menu. Please enable it manually." buttons {"OK"} + end try + end tell +end tell + ` + + runAppleScript(script, browser) +} + +export function launchSafari(options: LaunchSafariOptions): void { + const {startingUrl, isMacOS, browser} = options + + if (!isMacOS) { + console.log('RunSafariPlugin is supported only on macOS. Exiting.') + process.exit(0) + } + + const safariPath = '/Applications/Safari.app' + if (!fs.existsSync(safariPath)) { + console.error(messages.browserNotInstalledError('safari', safariPath)) + process.exit(1) + } + + // Configure Safari for the development extension + configureSafariExtension(browser) + + const args = ['open', safariPath] + if (startingUrl) args.push('--args', startingUrl) + + const result = spawnSync('open', args, {stdio: 'inherit'}) + if (result.error) { + console.error(`Failed to launch Safari: ${result.error.message}`) + process.exit(1) + } +} diff --git a/programs/develop/plugin-browsers/run-safari/safari/master-preferences.ts b/programs/develop/plugin-browsers/run-safari/safari/master-preferences.ts new file mode 100644 index 000000000..65bd6a899 --- /dev/null +++ b/programs/develop/plugin-browsers/run-safari/safari/master-preferences.ts @@ -0,0 +1,5 @@ +// PROFILE PREFS aka "Master Preferences" aka "User Preferences" +// * Official ref: ? +const safariMasterPreferences = {} + +export {safariMasterPreferences} diff --git a/programs/develop/plugin-browsers/run-safari/xcode/generate-project.ts b/programs/develop/plugin-browsers/run-safari/xcode/generate-project.ts new file mode 100644 index 000000000..dbb71f34b --- /dev/null +++ b/programs/develop/plugin-browsers/run-safari/xcode/generate-project.ts @@ -0,0 +1,94 @@ +import fs from 'fs' +import path from 'path' +import {spawnSync} from 'child_process' + +/** + * Validates the web extension source directory. + * Ensures the directory exists and contains a valid `manifest.json` file. + * @param sourcePath - The directory containing the web extension files. + */ +function validateWebExtensionSource(sourcePath: string): void { + if (!fs.existsSync(sourcePath)) { + throw new Error(`Source path does not exist: ${sourcePath}`) + } + + const manifestPath = path.join(sourcePath, 'manifest.json') + if (!fs.existsSync(manifestPath)) { + throw new Error( + `Invalid web extension source: Missing "manifest.json" in ${sourcePath}.\n` + + `Please ensure the directory contains a valid browser extension with a "manifest.json" file.` + ) + } + + console.log(`Validated web extension source at: ${sourcePath}`) +} + +/** + * Generates an Xcode project for a Safari Web Extension using the `safari-web-extension-converter` tool. + * @param sourcePath - The directory containing the browser extension to convert. + * @param outputPath - The directory where the Xcode project will be created. + * @param appName - The name of the project (and app). + */ +export function generateSafariProject( + sourcePath: string, + outputPath: string, + appName: string, + identifier: string +): void { + // Validate the web extension source directory + validateWebExtensionSource(sourcePath) + + // Ensure the output directory exists + if (!fs.existsSync(outputPath)) { + fs.mkdirSync(outputPath, {recursive: true}) + console.log(`Created output directory: ${outputPath}`) + } + + console.log(`Starting Safari Web Extension conversion...`) + console.log(`Source Pathzzzzzzz2: ${sourcePath}`) + console.log(`Output Pathzzzzzzzz2: ${outputPath}`) + // console.log(`App Name: ${appName}`) + + // Run the safari-web-extension-converter tool + const result = spawnSync( + 'xcrun', + [ + 'safari-web-extension-converter', + sourcePath, + '--project-location', + outputPath, + '--app-name', + appName, + '--macos-only', + '--bundle-identifier', + `${identifier}`, + '--no-open', + '--no-prompt', + // Overwrite the output directory if it exists + '--force' + ], + {stdio: 'inherit'} + ) + + if (result.error) { + throw new Error( + `Failed to generate Safari project: ${result.error.message}` + ) + } + + if (result.status !== 0) { + throw new Error( + `safari-web-extension-converter returned a non-zero status.\n` + + `Please ensure the source directory is valid and contains a "manifest.json" file.\n` + + `To debug, run the command manually:\n` + + ` xcrun safari-web-extension-converter ${sourcePath} --output ${outputPath} --project-name ${appName} --verbose` + ) + } + + console.log( + `Safari Web Extension project created successfully in: ${path.join( + outputPath, + `${appName}.xcodeproj` + )}` + ) +} diff --git a/programs/develop/plugin-browsers/run-safari/xcode/setup-xcode.ts b/programs/develop/plugin-browsers/run-safari/xcode/setup-xcode.ts new file mode 100644 index 000000000..9511e5d10 --- /dev/null +++ b/programs/develop/plugin-browsers/run-safari/xcode/setup-xcode.ts @@ -0,0 +1,49 @@ +import fs from 'fs' +import path from 'path' +import {spawnSync} from 'child_process' + +/** + * Checks if Xcode command-line tools are installed by verifying the presence of 'xcode-select'. + * Throws an error if not installed. + */ +export function checkXcodeCommandLineTools(): void { + const result = spawnSync('xcode-select', ['-p']) + if (result.error || result.status !== 0) { + throw new Error( + 'Xcode command-line tools are not installed. Please install them using "xcode-select --install".' + ) + } +} + +/** + * Ensures that the specified 'xcode' directory exists. + * Logs whether the directory was found or created. + * @param basePath - The base path where the 'xcode' directory should reside. + * @returns The full path to the 'xcode' directory. + */ +export function ensureXcodeDirectory(basePath: string): string { + const xcodeDir = path.join(basePath, 'xcode') + if (!fs.existsSync(xcodeDir)) { + fs.mkdirSync(xcodeDir, {recursive: true}) + console.log(`'xcode' directory was not found. Created at: ${xcodeDir}`) + } else { + console.log(`'xcode' directory already exists at: ${xcodeDir}`) + } + return xcodeDir +} + +/** + * Checks if the 'safari-web-extension-converter' tool is available. + * Throws an error if the tool is not found. + */ +export function checkSafariWebExtensionConverter(): void { + const result = spawnSync('xcrun', [ + '--find', + 'safari-web-extension-converter' + ]) + if (result.error || result.status !== 0) { + throw new Error( + 'The "safari-web-extension-converter" tool is not available. Please ensure Xcode is installed and configured correctly.' + ) + } +} diff --git a/programs/develop/webpack/lib/messages.ts b/programs/develop/webpack/lib/messages.ts index 975cf96ab..ea7a859ac 100644 --- a/programs/develop/webpack/lib/messages.ts +++ b/programs/develop/webpack/lib/messages.ts @@ -470,6 +470,9 @@ export function runningInDevelopment( case 'firefox': browserDevToolsUrl = 'about:debugging#/runtime/this-firefox' break + case 'safari': + browserDevToolsUrl = 'Settings > Extensions' + break default: browserDevToolsUrl = '' }