├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── README.md ├── assets ├── Airhorner_512.icns └── pwaify.png ├── bin └── pwaify ├── index.js ├── lib ├── icon.js └── manifest.js ├── package.json └── template ├── index.html ├── main.js └── package.json /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [10.x, 12.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm ci 28 | - run: npm run build --if-present 29 | - run: npm test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | template/node_modules 3 | template/prefs.json 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PWAify 2 | 3 | > Experimental project to convert your PWA (Progressive Web App) into a cross-platform Electron app. Brings PWAs to your desktop. 4 | 5 | ![](assets/pwaify.png) 6 | 7 | ## Usage 8 | 9 | > Node 4+ required. 10 | 11 | Install: 12 | 13 | ``` 14 | npm install -g pwaify 15 | ``` 16 | 17 | ## Run against your PWA app 18 | 19 | ``` 20 | pwaify https://airhorner.com 21 | pwaify https://voice-memos.appspot.com/ --platforms=darwin --icon chrome-touch-icon-384x384.icns 22 | pwaify https://m.weibo.cn --platforms=darwin --manifest=https://m.weibo.cn/static/pwa/manifest.json 23 | ``` 24 | 25 | (Might require `sudo` at the moment if you get `pref.json` error). 26 | 27 | Open the app on your platform, test and send it to your friends! 28 | 29 | More apps at [pwa.rocks](https://pwa.rocks/). At this moment you need to convert your icons for your platform, using something like [iconverticons.com/online/](https://iconverticons.com/online/). 30 | 31 | ![](http://i.imgur.com/F76UA6h.gif) 32 | 33 | ## Advanced 34 | 35 | ### Custom platforms 36 | 37 | Example, build only for OS X: 38 | ``` 39 | node bin/pwaify --platforms=darwin https://airhorner.com 40 | ``` 41 | 42 | ## Known Issues / TODO 43 | 44 | - `sudo` requirements and permission issues. 45 | - icons are a manual process right now. 46 | 47 | ## Changelog 48 | 49 | * 1.0.0 - First experimental release 50 | -------------------------------------------------------------------------------- /assets/Airhorner_512.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vladikoff/PWAify/5077d09ef28adc99d38e01960f2e733ae7abb7c5/assets/Airhorner_512.icns -------------------------------------------------------------------------------- /assets/pwaify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vladikoff/PWAify/5077d09ef28adc99d38e01960f2e733ae7abb7c5/assets/pwaify.png -------------------------------------------------------------------------------- /bin/pwaify: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const meow = require('meow'); 4 | const cli = meow(` 5 | Usage 6 | $ pwaify 7 | 8 | Options 9 | --platforms Platforms to build the app. 10 | --icon App Icon. 11 | --manifest Manifest.json url. 12 | 13 | Examples 14 | $ pwaify https://airhorner.com --platforms=darwin 15 | `); 16 | 17 | require('../index')({ 18 | appUrl: cli.input[0], 19 | platforms: cli.flags.platforms || 'all', 20 | path: cli.flags.path || '.', 21 | icon: cli.flags.icon, 22 | manifestUrl: cli.flags.manifest 23 | }); 24 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('index'); 2 | const packager = require('electron-packager'); 3 | const fs = require('fs'); 4 | const chalk = require('chalk'); 5 | const path = require('path'); 6 | 7 | const manifest = require('./lib/manifest'); 8 | 9 | module.exports = function (options) { 10 | options = options || {}; 11 | debug('options', options); 12 | const appUrl = options.appUrl; 13 | const appPath = options.path; 14 | const manifestUrl = options.manifestUrl; 15 | return manifest.fetchManifestDetails(appUrl, manifestUrl) 16 | .then(function(manifestJson) { 17 | debug('manifestJson', manifestJson); 18 | var name = manifestJson.name || manifestJson.short_name; 19 | var start_url = appUrl + (manifestJson.start_url || ''); 20 | var appData = { 21 | appUrl: start_url 22 | }; 23 | fs.writeFileSync(path.join(__dirname, 'template', 'prefs.json'), JSON.stringify(appData)); 24 | var packagerOpts = { 25 | name: name, 26 | arch: 'all', 27 | overwrite: true, 28 | dir: path.join(__dirname, 'template'), 29 | platform: options.platforms || 'all' 30 | }; 31 | 32 | if (options.icon) { 33 | packagerOpts.icon = options.icon 34 | } 35 | 36 | return new Promise(function (resolve) { 37 | if(appPath) { 38 | if(fs.existsSync(appPath)) { 39 | process.chdir(appPath); 40 | } else { 41 | throw new Error("Path doesn't exsists."); 42 | } 43 | } 44 | 45 | packager(packagerOpts, function done_callback(err, appPaths) { 46 | if (err) { 47 | throw err; 48 | } else { 49 | resolve({ 50 | appPaths: appPaths 51 | }) 52 | } 53 | }) 54 | }) 55 | }) 56 | .then(function (result) { 57 | console.log(chalk.green('Your app is ready!'), result.appPaths); 58 | }).catch(function (err) { 59 | throw err; 60 | }) 61 | }; 62 | -------------------------------------------------------------------------------- /lib/icon.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | 4 | module.exports = { 5 | createIcns: function createIcns(manifestJson) { 6 | // TODO 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/manifest.js: -------------------------------------------------------------------------------- 1 | const got = require('got'); 2 | const debug = require('debug')('manifest'); 3 | const Xray = require('x-ray'); 4 | 5 | module.exports = { 6 | fetchManifestDetails: function fetchManifestDetails (appUrl, manifestUrl) { 7 | return new Promise(function (resolve, reject) { 8 | if (manifestUrl) { 9 | return resolve({ 10 | manifestTarget: manifestUrl 11 | }); 12 | } 13 | var xray = Xray(); 14 | xray(appUrl, 'link[rel=manifest]@href')(function(err, manifestTarget) { 15 | debug(err, manifestTarget); 16 | 17 | if (err) { 18 | return reject(err); 19 | } 20 | 21 | return resolve({ 22 | manifestTarget: manifestTarget 23 | }); 24 | }); 25 | }).then(function (result) { 26 | if (result.manifestTarget) { 27 | // found manifest via html 28 | return got(result.manifestTarget) 29 | } else { 30 | // no manifest in html, fetch at root 31 | return got(appUrl + '/manifest.json') 32 | } 33 | }).then(function (result) { 34 | var manifestResponse = result.body; 35 | var manifestJson = null; 36 | try { 37 | manifestJson = JSON.parse(manifestResponse); 38 | } catch (e) { 39 | throw new Error('Failed to parse manifest.json') 40 | } 41 | 42 | return manifestJson; 43 | }) 44 | .catch(function (err) { 45 | throw err; 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pwaify", 3 | "version": "1.1.0", 4 | "description": "Electron PWA (Progressive Web App) Generator", 5 | "main": "index.js", 6 | "bin": { 7 | "pwaify": "bin/pwaify" 8 | }, 9 | "author": { 10 | "name": "vladikoff", 11 | "email": "vlad.filippov@gmail.com", 12 | "url": "http://vf.io" 13 | }, 14 | "license": "MIT", 15 | "devDependencies": {}, 16 | "dependencies": { 17 | "chalk": "1.1.3", 18 | "debug": "2.2.0", 19 | "electron-prebuilt": "1.1.2", 20 | "electron-packager": "7.0.2", 21 | "got": "6.3.0", 22 | "meow": "3.7.0", 23 | "x-ray": "2.3.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /template/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PWAify 6 | 7 | 8 |

Loading app...

9 | 10 | 11 | -------------------------------------------------------------------------------- /template/main.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron') 2 | const fs = require('fs') 3 | // Module to control application life. 4 | const app = electron.app; 5 | // Module to create native browser window. 6 | const BrowserWindow = electron.BrowserWindow; 7 | 8 | let mainWindow; 9 | 10 | function createWindow() { 11 | mainWindow = new BrowserWindow({ 12 | width: 800, 13 | height: 600, 14 | webPreferences: { 15 | // disable node for security reasons 16 | nodeIntegration: false 17 | } 18 | }); 19 | 20 | // load the app 21 | mainWindow.loadURL(require('./prefs.json').appUrl); 22 | mainWindow.on('closed', function () { 23 | // Dereference the window object, usually you would store windows 24 | // in an array if your app supports multi windows, this is the time 25 | // when you should delete the corresponding element. 26 | mainWindow = null 27 | }) 28 | } 29 | 30 | app.on('ready', createWindow); 31 | 32 | // Quit when all windows are closed. 33 | app.on('window-all-closed', function () { 34 | // On OS X it is common for applications and their menu bar 35 | // to stay active until the user quits explicitly with Cmd + Q 36 | if (process.platform !== 'darwin') { 37 | app.quit() 38 | } 39 | }); 40 | 41 | app.on('activate', function () { 42 | // On OS X it's common to re-create a window in the app when the 43 | // dock icon is clicked and there are no other windows open. 44 | if (mainWindow === null) { 45 | createWindow() 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /template/package.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "name": "pwaify-template", 4 | "version": "1.0.0", 5 | "description": "Progressive Web App", 6 | "productName": "PWAify", 7 | "main": "main.js" 8 | } 9 | --------------------------------------------------------------------------------