u2a 2.1.8 → 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,5 +1,5 @@
1
1
  <div align="center">
2
- <a href="#" style="display: block; text-align: center;">
2
+ <a href="https://urltoapp.xyz" style="display: block; text-align: center;">
3
3
  <img
4
4
  alt="Image of this repo"
5
5
  src="https://togp.xyz?owner=douxxtech&repo=urltoapp&theme=json-dark-all&cache=false"
@@ -86,8 +86,37 @@ This will:
86
86
  2. Delete the application files
87
87
  3. Remove the entry from the U2A database
88
88
 
89
- ## How It Works
90
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`
91
120
  U2A creates a minimal Electron application that loads the specified website URL. It:
92
121
 
93
122
  1. Downloads the site's favicon to use as the application icon
package/package.json CHANGED
@@ -1,39 +1,42 @@
1
1
  {
2
2
  "name": "u2a",
3
- "version": "2.1.8",
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');
@@ -557,7 +557,10 @@ app.on('activate', () => {
557
557
  `;
558
558
  }
559
559
 
560
- function generatePackageJson(appName, iconPath) {
560
+
561
+
562
+
563
+ async function generatePackageJson(appName, iconPath, isExecutable = false, createSetup = false) {
561
564
  const u2aPackagePath = path.resolve(__dirname, '../../package.json');
562
565
 
563
566
  let u2aVersion = '1.0.0';
@@ -566,14 +569,19 @@ function generatePackageJson(appName, iconPath) {
566
569
  const u2aPackage = JSON.parse(u2aPackageContent);
567
570
  u2aVersion = u2aPackage.version || u2aVersion;
568
571
  } catch (error) {
569
- logger.error('Error while fetching u2a package.json', error)
572
+ logger.error('Error while fetching u2a package.json', error);
573
+ }
574
+
575
+ if (createSetup) {
576
+ iconPath = await processFavicon(iconPath);
570
577
  }
571
578
 
572
- return {
579
+ const packageJson = {
573
580
  name: `u2a-${appName.replace(/\s+/g, '-')}`,
574
581
  version: u2aVersion,
575
582
  description: `Web app for ${appName}`,
576
583
  main: 'main.js',
584
+ author: `${appName}`,
577
585
  scripts: {
578
586
  start: 'electron .'
579
587
  },
@@ -586,6 +594,201 @@ function generatePackageJson(appName, iconPath) {
586
594
  icon: iconPath
587
595
  }
588
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;
648
+ }
649
+
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
+ }
589
792
  }
590
793
 
591
794
  async function createApp(url, options) {
@@ -615,8 +818,10 @@ async function createApp(url, options) {
615
818
  fs.writeFileSync(mainJsPath, mainJsContent);
616
819
  logger.debug(`main.js file created`);
617
820
 
821
+ const isExecutable = !!options.executable;
822
+ const createSetup = !!options.setup;
618
823
  const packageJsonPath = path.join(appDir, 'package.json');
619
- const packageJsonContent = generatePackageJson(appName, iconPath);
824
+ const packageJsonContent = await generatePackageJson(appName, iconPath, isExecutable, createSetup);
620
825
  fs.writeFileSync(packageJsonPath, JSON.stringify(packageJsonContent, null, 2));
621
826
  logger.debug(`package.json file created`);
622
827
 
@@ -628,10 +833,59 @@ async function createApp(url, options) {
628
833
  windowsHide: true
629
834
  };
630
835
 
631
- const stdout = execSync('npm install --only=prod', installOptions);
632
- logger.debug(`npm install completed: ${stdout.toString().trim()}`);
633
-
634
- const desktopPath = addAppToOS(appName, 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
+ }
635
889
 
636
890
  const appData = {
637
891
  url,
@@ -639,6 +893,7 @@ async function createApp(url, options) {
639
893
  path: appDir,
640
894
  icon: iconPath,
641
895
  desktopPath,
896
+ executablePath,
642
897
  name: options.name,
643
898
  width: options.width,
644
899
  height: options.height
@@ -651,12 +906,15 @@ async function createApp(url, options) {
651
906
  if (desktopPath) {
652
907
  logger.info(`A shortcut has been created in your system's applications directory`);
653
908
  }
909
+ if (executablePath) {
910
+ logger.info(`A standalone executable has been created at: ${executablePath}`);
911
+ }
654
912
  } catch (error) {
655
- logger.error(`Error while creating an application for ${url}`, error);
913
+ logger.error(`Error while creating an application for ${url}: ${error}`);
656
914
  }
657
915
  }
658
916
 
659
917
  module.exports = {
660
918
  createApp,
661
919
  removeAppFromOS
662
- };
920
+ };
package/src/index.js CHANGED
@@ -20,6 +20,9 @@ program
20
20
  .option('--name <name>', 'Specify the application name')
21
21
  .option('--width <width>', 'Specify the window width', parseInt)
22
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')
23
26
  .action((url, options) => {
24
27
  createApp(url, options);
25
28
  });
@@ -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