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/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..ee24d6e4f 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 @@ -24,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 { @@ -39,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 { @@ -54,6 +66,7 @@ export interface PreviewOptions extends BrowserOptionsBase { mode: 'production' chromiumBinary?: ChromiumOptions['chromiumBinary'] geckoBinary?: GeckoOptions['geckoBinary'] + webKitBinary?: WebKitOptions['webKitBinary'] } export interface StartOptions extends BrowserOptionsBase { @@ -61,6 +74,7 @@ export interface StartOptions extends BrowserOptionsBase { polyfill?: boolean chromiumBinary?: ChromiumOptions['chromiumBinary'] geckoBinary?: GeckoOptions['geckoBinary'] + webKitBinary?: WebKitOptions['webKitBinary'] } export interface BrowserConfig extends BrowserOptionsBase { @@ -68,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 { @@ -75,8 +158,10 @@ export interface FileConfig { chrome?: BrowserConfig firefox?: BrowserConfig edge?: BrowserConfig + safari?: BrowserConfig & {xcode?: XCodeConfig} 'chromium-based'?: BrowserConfig 'gecko-based'?: BrowserConfig + 'webkit-based'?: BrowserConfig & {xcode?: XCodeConfig} } commands?: { dev?: Pick< @@ -85,6 +170,7 @@ export interface FileConfig { | 'profile' | 'chromiumBinary' | 'geckoBinary' + | 'webKitBinary' | 'open' | 'polyfill' > & { @@ -94,7 +180,12 @@ export interface FileConfig { start?: Pick< StartOptions, - 'browser' | 'profile' | 'chromiumBinary' | 'geckoBinary' | 'polyfill' + | 'browser' + | 'profile' + | 'chromiumBinary' + | 'geckoBinary' + | 'webKitBinary' + | 'polyfill' > & { browserFlags?: string[] preferences?: Record @@ -102,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/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..5311d511a 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,11 +179,20 @@ export class BrowsersPlugin { }).apply(compiler) break + case 'safari': + case 'webkit-based': + new RunSafariPlugin({ + ...browserConfig, + browser: this.browser + // profile + }).apply(compiler) + break + default: { new RunChromiumPlugin({ ...browserConfig, - browser: 'chrome', - profile + 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 = '' } 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 000000000..550b4a001 Binary files /dev/null and b/programs/develop/webpack/plugin-reload/extensions/safari-manager-extension/images/logo.png differ 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) +} 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 000000000..550b4a001 Binary files /dev/null and b/programs/develop/webpack/plugin-reload/extensions/webkit-based-manager-extension/images/logo.png differ 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) +}