u2a 2.1.6 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  <div align="center">
2
- <a href="#" style="display: block; text-align: center;">
3
- <img
4
- alt="Image of this repo"
5
- src="https://togp.xyz?owner=douxxtech&repo=urltoapp&theme=json-dark-all&cache=false"
6
- type="image/svg+xml"
7
- style="border-radius: 20px; overflow: hidden;"
2
+ <a href="https://urltoapp.xyz" style="display: block; text-align: center;">
3
+ <img
4
+ alt="Image of this repo"
5
+ src="https://togp.xyz?owner=douxxtech&repo=urltoapp&theme=json-dark-all&cache=false"
6
+ type="image/svg+xml"
7
+ style="border-radius: 20px; overflow: hidden;"
8
8
  />
9
9
  <h1 align="center">U2A (URL to App)</h1>
10
10
  </a>
@@ -25,6 +25,7 @@ U2A is a command-line utility that allows you to transform any web URL into a st
25
25
  - 🔄 Automatic favicon retrieval for app icons
26
26
  - 📋 Easy management of created applications
27
27
  - 📊 Detailed logging for troubleshooting
28
+ - 🏷️ Customizable application names and window sizes
28
29
 
29
30
  ## Installation
30
31
 
@@ -39,17 +40,17 @@ npm install -g u2a
39
40
  To create a desktop application from a website:
40
41
 
41
42
  ```bash
42
- u2a create <url>
43
+ u2a create <url> [--name <appName>] [--width <width>] [--height <height>]
43
44
  ```
44
45
 
45
46
  Example:
46
47
  ```bash
47
- u2a create github.com
48
+ u2a create github.com --name "GitHub App" --width 1200 --height 800
48
49
  ```
49
50
 
50
51
  This will:
51
52
  1. Download the website's favicon (if available)
52
- 2. Create an Electron wrapper application
53
+ 2. Create an Electron wrapper application with the specified name and window size
53
54
  3. Add the application to your system menu/launcher
54
55
  4. Track the application in the U2A database
55
56
 
@@ -62,7 +63,7 @@ u2a list
62
63
  ```
63
64
 
64
65
  This will display a list of all created applications with their details:
65
- - Domain name
66
+ - Application name
66
67
  - Original URL
67
68
  - Creation date
68
69
  - Application directory
@@ -72,12 +73,12 @@ This will display a list of all created applications with their details:
72
73
  To remove an application:
73
74
 
74
75
  ```bash
75
- u2a remove <url>
76
+ u2a remove <appName>
76
77
  ```
77
78
 
78
79
  Example:
79
80
  ```bash
80
- u2a remove github.com
81
+ u2a remove "GitHub App"
81
82
  ```
82
83
 
83
84
  This will:
@@ -85,8 +86,37 @@ This will:
85
86
  2. Delete the application files
86
87
  3. Remove the entry from the U2A database
87
88
 
88
- ## How It Works
89
89
 
90
+ ### Creating an executable
91
+
92
+ To directly create a windows, macos or linux executable, you can use the `--executable [windows|darwin|linux] [--arch <architecture>]` argument with the create command.
93
+
94
+ This will:
95
+ 1. Temporarily install the application
96
+ 2. Create an executable and move it to your working directory
97
+ 3. Delete the application
98
+
99
+ > [!WARNING]
100
+ > To launch an executable, you will need all the files that are created by U2A.
101
+
102
+ ### Creating a setup
103
+
104
+ You can also directly create a setup file, so people can install it on their machine. Use the `--executable [...] --setup` to do so.
105
+
106
+ This will:
107
+ 1. Temporarily install the application
108
+ 2. Create an executable and move it to your working directory
109
+ 3. Create a setup file and move it to your working directory
110
+ 4. Delete the application
111
+
112
+
113
+ > [!WARNING]
114
+ > To use the setup, you will need all the files that are created by U2A.
115
+
116
+
117
+ ## How It Works
118
+
119
+ `This dont apply for executables and setup`
90
120
  U2A creates a minimal Electron application that loads the specified website URL. It:
91
121
 
92
122
  1. Downloads the site's favicon to use as the application icon
@@ -127,7 +157,3 @@ This project is licensed under the GNU General Public License v3.0 - see the [LI
127
157
  ## Author
128
158
 
129
159
  Created by [Douxx](https://douxx.tech)
130
-
131
- ## Disclaimer
132
-
133
- This tool is for personal use only. Always respect the terms of service of websites you convert to desktop apps.
package/package.json CHANGED
@@ -1,39 +1,42 @@
1
1
  {
2
2
  "name": "u2a",
3
- "version": "2.1.6",
3
+ "version": "3.0.0",
4
4
  "description": "URL to App - Turn any URL into a desktop application",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
- "u2a": "./src/index.js"
7
+ "u2a": "./src/index.js"
8
8
  },
9
9
  "scripts": {
10
- "start": "node src/index.js"
10
+ "start": "node src/index.js"
11
11
  },
12
12
  "keywords": [
13
- "cli",
14
- "webapp",
15
- "electron",
16
- "url",
17
- "desktop",
18
- "build",
19
- "application"
13
+ "cli",
14
+ "webapp",
15
+ "electron",
16
+ "url",
17
+ "desktop",
18
+ "build",
19
+ "application"
20
20
  ],
21
21
  "author": "Douxx",
22
22
  "license": "GPL-3.0-only",
23
23
  "dependencies": {
24
- "axios": "^1.6.0",
25
- "chalk": "^4.1.2",
26
- "commander": "^10.0.0",
27
- "electron": "^22.0.0",
28
- "inquirer": "^8.2.5",
29
- "open": "^8.4.0"
24
+ "axios": "^1.6.0",
25
+ "chalk": "^4.1.2",
26
+ "commander": "^10.0.0",
27
+ "electron": "^22.0.0",
28
+ "icojs": "^0.19.5",
29
+ "inquirer": "^8.2.5",
30
+ "open": "^8.4.0",
31
+ "png-to-ico": "^2.1.8",
32
+ "sharp": "^0.33.5"
30
33
  },
31
34
  "homepage": "https://urltoapp.xyz",
32
35
  "repository": {
33
- "type": "git",
34
- "url": "https://github.com/douxxtech/urltoapp"
35
- },
36
- "bugs": {
37
- "url": "https://github.com/douxxtech/urltoapp/issues"
38
- }
39
- }
36
+ "type": "git",
37
+ "url": "https://github.com/douxxtech/urltoapp"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/douxxtech/urltoapp/issues"
41
+ }
42
+ }
@@ -2,7 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { execSync } = require('child_process');
4
4
  const { normalizeUrl, getDomainName } = require('../utils/url');
5
- const { getFavicon } = require('../utils/favicon');
5
+ const { getFavicon, processFavicon } = require('../utils/favicon');
6
6
  const { APPS_DIR, readDB, writeDB } = require('../utils/config');
7
7
  const Logger = require('../utils/logger');
8
8
  const os = require('os');
@@ -11,14 +11,14 @@ const logger = new Logger('create');
11
11
 
12
12
  function createWindowsShortcut(appInfo) {
13
13
  try {
14
- const { domain, appDir, iconPath } = appInfo;
14
+ const { appName, appDir, iconPath } = appInfo;
15
15
  const startMenuPath = path.join(process.env.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'U2A Apps');
16
16
 
17
17
  if (!fs.existsSync(startMenuPath)) {
18
18
  fs.mkdirSync(startMenuPath, { recursive: true });
19
19
  }
20
20
 
21
- const shortcutPath = path.join(startMenuPath, `${domain}.lnk`);
21
+ const shortcutPath = path.join(startMenuPath, `${appName}.lnk`);
22
22
  const targetPath = path.join(appDir, 'node_modules', '.bin', 'electron.cmd');
23
23
  const workingDir = appDir;
24
24
 
@@ -29,11 +29,11 @@ function createWindowsShortcut(appInfo) {
29
29
  $Shortcut.Arguments = "."
30
30
  $Shortcut.WorkingDirectory = "${workingDir.replace(/\\/g, '\\\\')}"
31
31
  $Shortcut.IconLocation = "${iconPath.replace(/\\/g, '\\\\')}"
32
- $Shortcut.Description = "Application Web pour ${domain}"
32
+ $Shortcut.Description = "Application Web pour ${appName}"
33
33
  $Shortcut.Save()
34
34
  `;
35
35
 
36
- const tempScriptPath = path.join(os.tmpdir(), `create_shortcut_${domain}.ps1`);
36
+ const tempScriptPath = path.join(os.tmpdir(), `create_shortcut_${appName}.ps1`);
37
37
  fs.writeFileSync(tempScriptPath, psScript);
38
38
 
39
39
  execSync(`powershell -ExecutionPolicy Bypass -File "${tempScriptPath}"`, {
@@ -53,7 +53,7 @@ function createWindowsShortcut(appInfo) {
53
53
 
54
54
  function createLinuxDesktopEntry(appInfo) {
55
55
  try {
56
- const { domain, url, appDir, iconPath } = appInfo;
56
+ const { appName, url, appDir, iconPath } = appInfo;
57
57
  const appsDir = path.join(os.homedir(), '.local', 'share', 'applications');
58
58
 
59
59
  if (!fs.existsSync(appsDir)) {
@@ -62,7 +62,7 @@ function createLinuxDesktopEntry(appInfo) {
62
62
 
63
63
  const desktopEntry = `[Desktop Entry]
64
64
  Type=Application
65
- Name=${domain}
65
+ Name=${appName}
66
66
  Exec=${path.join(appDir, 'node_modules', '.bin', 'electron')} ${path.join(appDir, 'main.js')}
67
67
  Icon=${iconPath}
68
68
  Comment=Application Web pour ${url}
@@ -70,7 +70,7 @@ Categories=Network;WebBrowser;
70
70
  Terminal=false
71
71
  `;
72
72
 
73
- const desktopFilePath = path.join(appsDir, `u2a-${domain}.desktop`);
73
+ const desktopFilePath = path.join(appsDir, `u2a-${appName}.desktop`);
74
74
  fs.writeFileSync(desktopFilePath, desktopEntry);
75
75
 
76
76
  fs.chmodSync(desktopFilePath, '755');
@@ -85,15 +85,14 @@ Terminal=false
85
85
 
86
86
  function createMacOSApp(appInfo) {
87
87
  try {
88
- const { domain, appDir, iconPath } = appInfo;
88
+ const { appName, appDir, iconPath } = appInfo;
89
89
  const appsDir = path.join(os.homedir(), 'Applications', 'U2A Apps');
90
90
 
91
91
  if (!fs.existsSync(appsDir)) {
92
92
  fs.mkdirSync(appsDir, { recursive: true });
93
93
  }
94
94
 
95
- const appName = `${domain}.app`;
96
- const appPath = path.join(appsDir, appName);
95
+ const appPath = path.join(appsDir, `${appName}.app`);
97
96
  const macOsPath = path.join(appPath, 'Contents', 'MacOS');
98
97
  const resourcesPath = path.join(appPath, 'Contents', 'Resources');
99
98
 
@@ -109,11 +108,11 @@ function createMacOSApp(appInfo) {
109
108
  <key>CFBundleIconFile</key>
110
109
  <string>icon.icns</string>
111
110
  <key>CFBundleIdentifier</key>
112
- <string>com.u2a.${domain}</string>
111
+ <string>com.u2a.${appName.replace(/\s+/g, '-')}</string>
113
112
  <key>CFBundleName</key>
114
- <string>${domain}</string>
113
+ <string>${appName}</string>
115
114
  <key>CFBundleDisplayName</key>
116
- <string>${domain}</string>
115
+ <string>${appName}</string>
117
116
  <key>CFBundlePackageType</key>
118
117
  <string>APPL</string>
119
118
  <key>CFBundleVersion</key>
@@ -143,8 +142,8 @@ cd "${appDir}"
143
142
  }
144
143
  }
145
144
 
146
- function addAppToOS(domain, url, appDir, iconPath) {
147
- const appInfo = { domain, url, appDir, iconPath };
145
+ function addAppToOS(appName, url, appDir, iconPath) {
146
+ const appInfo = { appName, url, appDir, iconPath };
148
147
  let desktopPath = null;
149
148
 
150
149
  if (process.platform === 'win32') {
@@ -160,41 +159,46 @@ function addAppToOS(domain, url, appDir, iconPath) {
160
159
  return desktopPath;
161
160
  }
162
161
 
163
- function removeAppFromOS(domain) {
162
+ function removeAppFromOS(appName) {
164
163
  try {
165
164
  if (process.platform === 'win32') {
166
- const startMenuPath = path.join(process.env.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'U2A Apps', `${domain}.lnk`);
165
+ const startMenuPath = path.join(process.env.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'U2A Apps', `${appName}.lnk`);
167
166
  if (fs.existsSync(startMenuPath)) {
168
167
  fs.unlinkSync(startMenuPath);
169
168
  logger.success(`Shortcut removed from the Start Menu: ${startMenuPath}`);
170
169
  }
171
170
  } else if (process.platform === 'darwin') {
172
- const appPath = path.join(os.homedir(), 'Applications', 'U2A Apps', `${domain}.app`);
171
+ const appPath = path.join(os.homedir(), 'Applications', 'U2A Apps', `${appName}.app`);
173
172
  if (fs.existsSync(appPath)) {
174
173
  fs.rmSync(appPath, { recursive: true, force: true });
175
174
  logger.success(`macOS application removed: ${appPath}`);
176
175
  }
177
176
  } else if (process.platform === 'linux') {
178
- const desktopFilePath = path.join(os.homedir(), '.local', 'share', 'applications', `u2a-${domain}.desktop`);
177
+ const desktopFilePath = path.join(os.homedir(), '.local', 'share', 'applications', `u2a-${appName}.desktop`);
179
178
  if (fs.existsSync(desktopFilePath)) {
180
179
  fs.unlinkSync(desktopFilePath);
181
180
  logger.success(`Linux desktop entry removed: ${desktopFilePath}`);
182
181
  }
183
182
  }
184
183
  } catch (error) {
185
- logger.error(`Error while removing desktop integration for ${domain}`, error);
184
+ logger.error(`Error while removing desktop integration for ${appName}`, error);
186
185
  }
187
186
  }
188
187
 
189
- function generateMainJs(domain, url, iconPath) {
188
+ function generateMainJs(appName, url, iconPath, options = {}) {
189
+ const width = options.width || 1200;
190
+ const height = options.height || 800;
191
+
190
192
  return `
191
193
  const { app, BrowserWindow, Menu, shell } = require('electron');
192
194
  const path = require('path');
193
195
  const fs = require('fs');
194
196
 
195
- const APP_DOMAIN = "${domain}";
197
+ const APP_NAME = "${appName}";
196
198
  const APP_URL = "${url}";
197
199
  const APP_ICON_PATH = "${iconPath.replace(/\\/g, '\\\\')}";
200
+ const WINDOW_WIDTH = ${width};
201
+ const WINDOW_HEIGHT = ${height};
198
202
 
199
203
  let mainWindow;
200
204
  let splashWindow;
@@ -214,7 +218,7 @@ function logAppInfo() {
214
218
  console.log('\\n--------------------------------');
215
219
  console.log(' APPLICATION INFORMATION');
216
220
  console.log('--------------------------------');
217
- console.log(\`Application: \${APP_DOMAIN}\`);
221
+ console.log(\`Application: \${APP_NAME}\`);
218
222
  console.log(\`URL: \${APP_URL}\`);
219
223
  console.log(\`Started at: \${new Date().toLocaleString()}\`);
220
224
  console.log(\`App directory: \${__dirname}\`);
@@ -390,7 +394,7 @@ function createSplashScreen() {
390
394
  </head>
391
395
  <body>
392
396
  <div class="container">
393
- <div class="domain">\${APP_DOMAIN}</div>
397
+ <div class="domain">\${APP_NAME}</div>
394
398
  <div class="spinner"></div>
395
399
  <div id="loading-text" class="loading-text">Loading...</div>
396
400
  <div class="progress-bar">
@@ -413,7 +417,7 @@ function createSplashScreen() {
413
417
  </html>
414
418
  \`;
415
419
 
416
- const splashPath = path.join(app.getPath('temp'), \`\${APP_DOMAIN}-splash.html\`);
420
+ const splashPath = path.join(app.getPath('temp'), \`\${APP_NAME}-splash.html\`);
417
421
  fs.writeFileSync(splashPath, splashHtml);
418
422
 
419
423
  splashWindow.loadFile(splashPath);
@@ -423,12 +427,12 @@ function createSplashScreen() {
423
427
  function createWindow() {
424
428
  logAppInfo();
425
429
 
426
- app.setAppUserModelId(APP_DOMAIN);
430
+ app.setAppUserModelId(APP_NAME);
427
431
 
428
432
  mainWindow = new BrowserWindow({
429
- width: 1200,
430
- height: 800,
431
- title: APP_DOMAIN,
433
+ width: WINDOW_WIDTH,
434
+ height: WINDOW_HEIGHT,
435
+ title: APP_NAME,
432
436
  icon: APP_ICON_PATH,
433
437
  show: false,
434
438
  webPreferences: {
@@ -470,7 +474,7 @@ function createWindow() {
470
474
  });
471
475
 
472
476
  mainWindow.webContents.on('did-start-loading', () => {
473
- updateSplashScreen('Connecting to ' + APP_DOMAIN + '...');
477
+ updateSplashScreen('Connecting to ' + APP_NAME + '...');
474
478
  });
475
479
 
476
480
  mainWindow.webContents.on('did-start-navigation', (event, url) => {
@@ -553,7 +557,10 @@ app.on('activate', () => {
553
557
  `;
554
558
  }
555
559
 
556
- function generatePackageJson(domain, iconPath) {
560
+
561
+
562
+
563
+ async function generatePackageJson(appName, iconPath, isExecutable = false, createSetup = false) {
557
564
  const u2aPackagePath = path.resolve(__dirname, '../../package.json');
558
565
 
559
566
  let u2aVersion = '1.0.0';
@@ -562,14 +569,19 @@ function generatePackageJson(domain, iconPath) {
562
569
  const u2aPackage = JSON.parse(u2aPackageContent);
563
570
  u2aVersion = u2aPackage.version || u2aVersion;
564
571
  } catch (error) {
565
- logger.error('Error while fetching u2a package.json', error)
572
+ logger.error('Error while fetching u2a package.json', error);
566
573
  }
567
574
 
568
- return {
569
- name: `u2a-${domain}`,
575
+ if (createSetup) {
576
+ iconPath = await processFavicon(iconPath);
577
+ }
578
+
579
+ const packageJson = {
580
+ name: `u2a-${appName.replace(/\s+/g, '-')}`,
570
581
  version: u2aVersion,
571
- description: `Web app for ${domain}`,
582
+ description: `Web app for ${appName}`,
572
583
  main: 'main.js',
584
+ author: `${appName}`,
573
585
  scripts: {
574
586
  start: 'electron .'
575
587
  },
@@ -577,45 +589,243 @@ function generatePackageJson(domain, iconPath) {
577
589
  electron: '^22.0.0'
578
590
  },
579
591
  build: {
580
- appId: `com.u2a.${domain.replace(/\./g, '-')}`,
581
- productName: domain,
592
+ appId: `com.u2a.${appName.replace(/\s+/g, '-')}`,
593
+ productName: appName,
582
594
  icon: iconPath
583
595
  }
584
596
  };
597
+
598
+ if (isExecutable) {
599
+ packageJson.devDependencies = {
600
+ "electron-packager": "^17.1.1",
601
+ "electron-builder": "^24.6.3",
602
+ "electron": "^22.0.0"
603
+ };
604
+
605
+ packageJson.dependencies = {};
606
+
607
+ packageJson.scripts.package = "electron-packager . --overwrite --asar";
608
+ packageJson.scripts.setup = "electron-builder";
609
+ }
610
+
611
+ if (isExecutable && createSetup) {
612
+ packageJson.build = {
613
+ ...packageJson.build,
614
+ appId: `com.u2a.${appName.replace(/\s+/g, '-')}`,
615
+ productName: appName,
616
+ directories: {
617
+ output: "installer"
618
+ },
619
+ files: [
620
+ "**/*",
621
+ "!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}",
622
+ "!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}",
623
+ "!**/node_modules/*.d.ts",
624
+ "!**/node_modules/.bin",
625
+ "!**/.{idea,git,cache,build,dist}",
626
+ "!dist/**/*",
627
+ "!installer/**/*"
628
+ ],
629
+ win: {
630
+ target: "nsis",
631
+ icon: iconPath
632
+ },
633
+ mac: {
634
+ target: "dmg"
635
+ },
636
+ linux: {
637
+ target: "AppImage",
638
+ icon: iconPath
639
+ },
640
+ nsis: {
641
+ oneClick: false,
642
+ allowToChangeInstallationDirectory: true
643
+ }
644
+ };
645
+ }
646
+
647
+ return packageJson;
585
648
  }
586
649
 
587
- async function createApp(url) {
650
+ function copyFolderRecursiveSync(source, target) {
651
+ if (!fs.existsSync(target)) {
652
+ fs.mkdirSync(target, { recursive: true });
653
+ }
654
+
655
+ const files = fs.readdirSync(source);
656
+
657
+ files.forEach((file) => {
658
+ const sourcePath = path.join(source, file);
659
+ const targetPath = path.join(target, file);
660
+
661
+ if (fs.lstatSync(sourcePath).isDirectory()) {
662
+ copyFolderRecursiveSync(sourcePath, targetPath);
663
+ } else {
664
+ fs.copyFileSync(sourcePath, targetPath);
665
+ }
666
+ });
667
+ }
668
+
669
+ async function buildExecutable(appDir, appName, platform, iconPath, options) {
670
+ logger.info(`Building executable for ${platform}...`);
671
+
672
+ try {
673
+ const installOptions = {
674
+ cwd: appDir,
675
+ stdio: ['ignore', 'pipe', 'pipe'],
676
+ windowsHide: true
677
+ };
678
+
679
+ execSync('npm install --save-dev electron-packager electron', installOptions);
680
+
681
+ let platformFlag = '';
682
+ let archFlag = `--arch=${options.arch || 'x64'}`;
683
+ let iconOption = '';
684
+
685
+ switch(platform) {
686
+ case 'windows':
687
+ platformFlag = '--platform=win32';
688
+ iconOption = iconPath ? `--icon="${iconPath}"` : '';
689
+ break;
690
+ case 'darwin':
691
+ platformFlag = '--platform=darwin';
692
+ if (iconPath && !iconPath.endsWith('.icns')) {
693
+ logger.warn('MacOs Icons are not supported at this time.');
694
+ }
695
+ iconOption = iconPath ? `--icon="${iconPath}"` : '';
696
+ break;
697
+ case 'linux':
698
+ platformFlag = '--platform=linux';
699
+ iconOption = iconPath ? `--icon="${iconPath}"` : '';
700
+ break;
701
+ default:
702
+ platformFlag = `--platform=${process.platform}`;
703
+ }
704
+
705
+ const packageCommand = `npx electron-packager . "${appName}" ${platformFlag} ${archFlag} --out=dist --overwrite --asar ${iconOption}`;
706
+
707
+ logger.debug(`Executing: ${packageCommand}`);
708
+
709
+ execSync(packageCommand, installOptions);
710
+
711
+ let distPlatform = '';
712
+ switch(platform) {
713
+ case 'windows': distPlatform = 'win32'; break;
714
+ case 'darwin': distPlatform = 'darwin'; break;
715
+ case 'linux': distPlatform = 'linux'; break;
716
+ default: distPlatform = process.platform;
717
+ }
718
+
719
+ const outputPath = path.join(appDir, 'dist', `${appName}-${distPlatform}-x64`);
720
+
721
+ if (fs.existsSync(outputPath)) {
722
+ logger.debug(`Executable built successfully at: ${outputPath}`);
723
+ return outputPath;
724
+ } else {
725
+ logger.error(`Failed to find the built executable at: ${outputPath}`);
726
+ return null;
727
+ }
728
+ } catch (error) {
729
+ logger.error(`Error while building executable:`, error);
730
+ return null;
731
+ }
732
+ }
733
+
734
+ function remove(path) {
735
+ try {
736
+ if (fs.existsSync(path)) {
737
+ fs.rmSync(path, { recursive: true, force: true });
738
+ logger.debug(`Dir/file removed: ${path}`);
739
+ }
740
+ } catch (error) {
741
+ logger.error(`Error while removing dir/file ${path}`, error);
742
+ }
743
+ }
744
+
745
+ async function buildSetup(appDir, platform, arch) {
746
+ logger.info(`Building setup for ${platform}${arch ? ` (${arch})` : ''}...`);
747
+
748
+ try {
749
+ const installOptions = {
750
+ cwd: appDir,
751
+ stdio: ['ignore', 'pipe', 'pipe'],
752
+ windowsHide: true
753
+ };
754
+
755
+ execSync('npm install --save-dev electron-builder', installOptions);
756
+
757
+ let builderArgs = '';
758
+ switch(platform) {
759
+ case 'windows':
760
+ builderArgs = '--win';
761
+ break;
762
+ case 'darwin':
763
+ builderArgs = '--mac';
764
+ break;
765
+ case 'linux':
766
+ builderArgs = '--linux';
767
+ break;
768
+ default:
769
+ builderArgs = '';
770
+ }
771
+
772
+ if (arch) {
773
+ builderArgs += ` --${arch}`;
774
+ }
775
+
776
+ const builderCommand = `npx electron-builder ${builderArgs}`;
777
+ logger.debug(`Executing: ${builderCommand}`);
778
+ execSync(builderCommand, installOptions);
779
+
780
+ const installerPath = path.join(appDir, 'installer');
781
+ if (fs.existsSync(installerPath)) {
782
+ logger.debug(`Setup created at: ${installerPath}`);
783
+ return installerPath;
784
+ } else {
785
+ logger.error(`Failed to find the built installer at: ${installerPath}`);
786
+ return null;
787
+ }
788
+ } catch (error) {
789
+ logger.error(`Error while building setup:`, error);
790
+ return null;
791
+ }
792
+ }
793
+
794
+ async function createApp(url, options) {
588
795
  logger.info(`Creating application for ${url}`);
589
796
 
590
797
  try {
591
798
  url = await normalizeUrl(url);
592
799
  const domain = getDomainName(url);
800
+ const appName = options.name || domain;
593
801
 
594
802
  const db = readDB();
595
- if (db.hasOwnProperty(domain)) {
596
- logger.warn(`Application for ${domain} already exists`);
803
+ if (db.hasOwnProperty(appName)) {
804
+ logger.warn(`Application for ${appName} already exists`);
597
805
  return;
598
806
  }
599
807
 
600
808
  const iconPath = await getFavicon(url);
601
809
 
602
- const appDir = path.join(APPS_DIR, domain);
810
+ const appDir = path.join(APPS_DIR, appName);
603
811
  if (!fs.existsSync(appDir)) {
604
812
  fs.mkdirSync(appDir, { recursive: true });
605
813
  logger.debug(`Directory created: ${appDir}`);
606
814
  }
607
815
 
608
816
  const mainJsPath = path.join(appDir, 'main.js');
609
- const mainJsContent = generateMainJs(domain, url, iconPath);
817
+ const mainJsContent = generateMainJs(appName, url, iconPath, options);
610
818
  fs.writeFileSync(mainJsPath, mainJsContent);
611
819
  logger.debug(`main.js file created`);
612
820
 
821
+ const isExecutable = !!options.executable;
822
+ const createSetup = !!options.setup;
613
823
  const packageJsonPath = path.join(appDir, 'package.json');
614
- const packageJsonContent = generatePackageJson(domain, iconPath);
824
+ const packageJsonContent = await generatePackageJson(appName, iconPath, isExecutable, createSetup);
615
825
  fs.writeFileSync(packageJsonPath, JSON.stringify(packageJsonContent, null, 2));
616
826
  logger.debug(`package.json file created`);
617
827
 
618
- logger.info(`Installing dependencies for ${domain}`);
828
+ logger.info(`Installing dependencies for ${appName}`);
619
829
 
620
830
  const installOptions = {
621
831
  cwd: appDir,
@@ -623,32 +833,88 @@ async function createApp(url) {
623
833
  windowsHide: true
624
834
  };
625
835
 
626
- const stdout = execSync('npm install --only=prod', installOptions);
627
- logger.debug(`npm install completed: ${stdout.toString().trim()}`);
628
-
629
- const desktopPath = addAppToOS(domain, url, appDir, iconPath);
836
+ execSync('npm install --only=prod', installOptions);
837
+ logger.debug(`npm install completed`);
838
+
839
+ let executablePath = null;
840
+ let desktopPath = null;
841
+
842
+ if (isExecutable) {
843
+ const targetPlatform = options.executable === true ? process.platform : options.executable;
844
+ executablePath = await buildExecutable(appDir, appName, targetPlatform, iconPath, options);
845
+
846
+ if (options.setup) {
847
+ const setupPath = await buildSetup(appDir, targetPlatform, options.arch);
848
+ if (setupPath) {
849
+ logger.debug(`Setup installer created at: ${setupPath}`);
850
+
851
+ const currentDir = process.cwd();
852
+ const setupTargetDir = path.join(currentDir, `${appName}-setup`);
853
+
854
+ if (!fs.existsSync(setupTargetDir)) {
855
+ fs.mkdirSync(setupTargetDir, { recursive: true });
856
+ }
857
+
858
+ copyFolderRecursiveSync(setupPath, setupTargetDir);
859
+ logger.success(`Setup installer created at: ${setupTargetDir}`);
860
+ }
861
+ }
862
+
863
+ if (executablePath) {
864
+ logger.debug(`Executable created at: ${executablePath}`);
865
+
866
+ const currentDir = process.cwd();
867
+ const targetDir = path.join(currentDir, `${appName}-executable`);
868
+
869
+ if (!fs.existsSync(targetDir)) {
870
+ fs.mkdirSync(targetDir, { recursive: true });
871
+ }
872
+
873
+ copyFolderRecursiveSync(executablePath, targetDir);
874
+
875
+ logger.success(`Executable created at: ${targetDir}`);
876
+
877
+ executablePath = targetDir;
878
+
879
+ removeAppFromOS(appName);
880
+ remove(appDir);
881
+ remove(iconPath);
882
+
883
+ logger.debug(`Temporary application files removed after executable creation`);
884
+ return;
885
+ }
886
+ } else {
887
+ desktopPath = addAppToOS(appName, url, appDir, iconPath);
888
+ }
630
889
 
631
890
  const appData = {
632
891
  url,
633
892
  created: new Date().toISOString(),
634
893
  path: appDir,
635
894
  icon: iconPath,
636
- desktopPath
895
+ desktopPath,
896
+ executablePath,
897
+ name: options.name,
898
+ width: options.width,
899
+ height: options.height
637
900
  };
638
901
 
639
- db[domain] = appData;
902
+ db[appName] = appData;
640
903
  writeDB(db);
641
904
 
642
905
  logger.success(`Application successfully created for ${url}`);
643
906
  if (desktopPath) {
644
907
  logger.info(`A shortcut has been created in your system's applications directory`);
645
908
  }
909
+ if (executablePath) {
910
+ logger.info(`A standalone executable has been created at: ${executablePath}`);
911
+ }
646
912
  } catch (error) {
647
- logger.error(`Error while creating an application for ${url}`, error);
913
+ logger.error(`Error while creating an application for ${url}: ${error}`);
648
914
  }
649
915
  }
650
916
 
651
917
  module.exports = {
652
918
  createApp,
653
919
  removeAppFromOS
654
- };
920
+ };
@@ -8,13 +8,12 @@ const path = require('path');
8
8
 
9
9
  const logger = new Logger('remove');
10
10
 
11
- async function removeApp(url) {
11
+ async function removeApp(appName) {
12
12
  try {
13
- const domain = getDomainName(await normalizeUrl(url));
14
13
  const db = readDB();
15
14
 
16
- if (!db.hasOwnProperty(domain)) {
17
- logger.warn(`The application for ${domain} does not exist`);
15
+ if (!db.hasOwnProperty(appName)) {
16
+ logger.warn(`The application for ${appName} does not exist`);
18
17
  return;
19
18
  }
20
19
 
@@ -22,7 +21,7 @@ async function removeApp(url) {
22
21
  {
23
22
  type: 'confirm',
24
23
  name: 'confirm',
25
- message: `Are you sure you want to remove the application for ${domain}?`,
24
+ message: `Are you sure you want to remove the application for ${appName}?`,
26
25
  default: false
27
26
  }
28
27
  ]);
@@ -32,25 +31,25 @@ async function removeApp(url) {
32
31
  return;
33
32
  }
34
33
 
35
- const appInfo = db[domain];
34
+ const appInfo = db[appName];
36
35
  const appDir = appInfo.path;
37
36
 
38
- removeAppFromOS(domain);
39
- logger.info(`Removing the application ${domain}...`);
37
+ removeAppFromOS(appName);
38
+ logger.info(`Removing the application ${appName}...`);
40
39
 
41
- const iconPath = path.join(APPS_DIR, `${domain}.ico`);
40
+ const iconPath = path.join(APPS_DIR, `${appName}.ico`);
42
41
  if (fs.existsSync(iconPath)) {
43
42
  fs.unlinkSync(iconPath);
44
- logger.success(`Icon for ${domain} removed`);
43
+ logger.success(`Icon for ${appName} removed`);
45
44
  }
46
45
 
47
46
  fs.rmSync(appDir, { recursive: true, force: true });
48
- delete db[domain];
47
+ delete db[appName];
49
48
  writeDB(db);
50
49
 
51
- logger.success(`The application for ${domain} has been successfully removed`);
50
+ logger.success(`The application for ${appName} has been successfully removed`);
52
51
  } catch (error) {
53
- logger.error(`Error removing the application ${url}`, error);
52
+ logger.error(`Error removing the application ${appName}`, error);
54
53
  }
55
54
  }
56
55
 
package/src/index.js CHANGED
@@ -14,10 +14,18 @@ program
14
14
  .description('Convert websites into desktop applications')
15
15
  .version(version);
16
16
 
17
- program
17
+ program
18
18
  .command('create <url>')
19
19
  .description('Create a new application from a URL')
20
- .action(createApp);
20
+ .option('--name <name>', 'Specify the application name')
21
+ .option('--width <width>', 'Specify the window width', parseInt)
22
+ .option('--height <height>', 'Specify the window height', parseInt)
23
+ .option('--executable [windows|darwin|linux]', 'Create a single executable for the target system')
24
+ .option('--arch [x64|armv7l|arm64|universal]', 'Specify the target architecture for the executable')
25
+ .option('--setup', 'Creates a setup file for the executable')
26
+ .action((url, options) => {
27
+ createApp(url, options);
28
+ });
21
29
 
22
30
  program
23
31
  .command('list')
@@ -4,6 +4,10 @@ const axios = require('axios');
4
4
  const { APPS_DIR } = require('./config');
5
5
  const { normalizeUrl, getDomainName } = require('./url');
6
6
  const Logger = require('./logger');
7
+ const { parseICO} = require('icojs');
8
+ const sharp = require('sharp');
9
+ const pngToIco = require('png-to-ico');
10
+
7
11
 
8
12
  const logger = new Logger('favicon');
9
13
 
@@ -19,7 +23,7 @@ async function getFavicon(url) {
19
23
  const iconResponse = await axios.get(faviconUrl, { responseType: 'arraybuffer' });
20
24
 
21
25
  const contentType = iconResponse.headers['content-type'];
22
- let fileExtension = '.ico'; // Default extension
26
+ let fileExtension = '.ico';
23
27
  if (contentType.includes('png')) {
24
28
  fileExtension = '.png';
25
29
  } else if (contentType.includes('jpeg') || contentType.includes('jpg')) {
@@ -44,6 +48,51 @@ async function getFavicon(url) {
44
48
  }
45
49
  }
46
50
 
51
+ async function processFavicon(iconPath) {
52
+ const dir = path.dirname(iconPath);
53
+ const ext = path.extname(iconPath);
54
+ const baseName = path.basename(iconPath, ext);
55
+
56
+ if (baseName === 'favicon' && ext === '.ico') {
57
+ const newPath = path.join(dir, 'favicon256.ico');
58
+ fs.copyFileSync(iconPath, newPath);
59
+ logger.debug("Default favicon.ico updated to favicon256.ico");
60
+ return newPath;
61
+ } else {
62
+ try {
63
+ const icoBuffer = fs.readFileSync(iconPath);
64
+ const images = await parseICO(icoBuffer, 'image/png');
65
+
66
+ if (images && images.length > 0) {
67
+ const pngBuffer = Buffer.from(images[0].buffer);
68
+ const tempPngPath = iconPath + '.png';
69
+ const resizedPngPath = iconPath + '_resized.png';
70
+
71
+ fs.writeFileSync(tempPngPath, pngBuffer);
72
+
73
+ await sharp(tempPngPath)
74
+ .resize(256, 256)
75
+ .toFile(resizedPngPath);
76
+
77
+ fs.renameSync(resizedPngPath, tempPngPath);
78
+
79
+ const newIcoBuffer = await pngToIco([tempPngPath]);
80
+ fs.writeFileSync(iconPath, newIcoBuffer);
81
+ fs.unlinkSync(tempPngPath);
82
+
83
+ logger.warn(`To proceed to setup, favicon has been resized to 256x256. Quality loss is possible.`);
84
+ return iconPath;
85
+ }
86
+ } catch (error) {
87
+ logger.error('Error processing ICO file:', error);
88
+ return iconPath;
89
+ }
90
+ }
91
+ }
92
+
93
+
94
+
47
95
  module.exports = {
48
- getFavicon
96
+ getFavicon,
97
+ processFavicon
49
98
  };
Binary file
@@ -41,7 +41,7 @@ class Logger {
41
41
  }
42
42
 
43
43
  error(message, error = null) {
44
- const formattedMessage = this._format('ERROR', message);
44
+ const formattedMessage = this._format('ERROR', `${message} ${error}`);
45
45
  console.log(chalk.red(formattedMessage));
46
46
  this._writeToFile(formattedMessage);
47
47