diff --git a/.gitignore b/.gitignore index 123ae94..a68f649 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,14 @@ +**/node_modules/ +**/*.log +test/repo-tests* +**/bundle.js +docs # Logs logs *.log +coverage + # Runtime data pids *.pid @@ -19,9 +26,18 @@ coverage # node-waf configuration .lock-wscript -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release +build # Dependency directory # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git node_modules + +lib +dist +test/fixtures/go-ipfs-repo/LOCK +test/fixtures/go-ipfs-repo/LOG +test/fixtures/go-ipfs-repo/LOG.old + +# while testing npm5 +package-lock.json +yarn.lock diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 85dceda..0000000 --- a/.travis.yml +++ /dev/null @@ -1,33 +0,0 @@ -sudo: false -language: node_js -matrix: - include: - - node_js: 4 - env: CXX=g++-4.8 - - node_js: 6 - env: CXX=g++-4.8 - - node_js: stable - env: CXX=g++-4.8 - -# Make sure we have new NPM. -before_install: - - npm install -g npm - -script: - - npm run lint - - npm test - - npm run coverage - -before_script: - - export DISPLAY=:99.0 - - sh -e /etc/init.d/xvfb start - -after_success: - - npm run coverage-publish - -addons: - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - g++-4.8 \ No newline at end of file diff --git a/README.md b/README.md index 4e5af31..e7e6270 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,50 @@ -ipscend -======= +dweb-publish (previously known as ipscend) +============ -[![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://ipn.io) [![](https://img.shields.io/badge/freenode-%23ipfs-blue.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23ipfs) ![](https://img.shields.io/badge/coverage-%3F-yellow.svg?style=flat-square) [![Dependency Status](https://david-dm.org/diasdavid/ipscend.svg?style=flat-square)](https://david-dm.org/diasdavid/ipscend) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard) +[![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://ipn.io) +[![](https://img.shields.io/badge/freenode-%23ipfs-blue.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23ipfs) +![](https://img.shields.io/badge/coverage-%3F-yellow.svg?style=flat-square) +[![](https://david-dm.org/ipfs-shipyard/dweb-publish.svg?style=flat-square)](https://david-dm.org/ipfs-shipyard/dweb-publish) +[![](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard) -> Web Application publishing, simple and distributed with IPFS +> Publish Websites and Web Applications to the DWeb using IPFS -## Usage - -Install via npm - -``` -$ npm install ipscend --global -``` - -Run the CLI to show the available commands - -``` -$ ipscend -Usage: ipscend COMMAND [OPTIONS] - -Available commands: - -browse Open your application in a browser -init Initialize a ipscend project -ipfs start Start your a local IPFS node -preview Preview your application before you publish it -publish Publish your project -screenshot View or generate screenshots for your application -versions Check each version published -``` - -### commands - -#### `ipscend browse` - -Opens the last published version of your application in the browser. - -#### `ipscend init` +## `dweb-publish` Description and Goals -Initializes your project. Asks for the folder where the web application will be available and stores an `ipscend.json` object in your current path to store all the metadata it generates, such as published versions and taken screenshots. +Let's make publish to the DWeb super simple and fun ✨! -#### `ipscend ipfs start` +This project started as [`ipscend`](http://daviddias.me/blog/ipscend/) and it was a fun tool at the time to get Websites on the DWeb super quick. Things like DNSLink were still recent and IPFS itself didn't support multiple formats other than the MerkleDAG. -Start and init an IPFS node (in case you don't want to install and run IPFS yourself) +Today things are different! IPFS can now support other formats such as Git (so that you don't have to re-add files), it also has lot better APIs, the JS implementation is now fully interoperable with the Go implementation (no more need to run daemons separetly), APIs have improved a ton and there is also now dozens of Gateways and Pinning Services outthere for users to ensure that their Website stays in the network without having to run their own daemon. -#### `ipscend preview` +It is time to revamp the project and improve it so that we achieve the big goal, making the act of publishing a Website to the DWeb incredibly simple, enjoyful and functional! Below you can find the rough roadmap of the steps ahead. Want to help? Join the effort by contributing to this repo or show up at one of the [Weekly IPFS All Hands calls](https://github.com/ipfs/pm/#weekly-all-hands). -Serves your application on a local static file server, so that you can try it out before you feel ready to publish it. +### (rough) Roadmap -#### `ipscend publish` +#### v1 - Make a tool that enables Website authors and Web Developers to publish their Websites with IPFS -Publishes the current state of your application to IPFS and stores a reference to it. +- [ ] Refactor and update old ipscend code to use the latest and greatest IPFS APIs +- [ ] Create an ipfs-push tool that automatically pushes the latest publish to multiple IPFS Gateways +- [ ] Add support for IPFS Cluster pinbot so that the Website is stored -**Note:** You have to have a local IPFS node running in order to publish, check below for how to get your local IPFS node running. +#### v2 - Support git directly -#### `ipscend screenshot` +- [ ] Publish the Git Hash directly through IPFS using CID & IPLD. This will avoid duplicating the info that already exists on the git repo + - [ ] Add support to the IPFS Files API to understand Git graphs as files and folders (e.g. ipfs.io/ipfs/gitHash/public) -Opens a screenshot preview of all the published versions of your app. In order to generate the screenshots, you must first run `ipscend screenshot --gen`. +#### v3 - Autopublish -![](http://zippy.gfycat.com/TameDampKob.gif) +- [ ] Find .dweb files in Github repositories and make the published versions available automatically through IPFS -**Note:** The webapp used for this is [ipscend-screenshot-visualizer](https://github.com/diasdavid/ipscend-screenshot-visualizer). +## Usage -#### `ipscend versions` +Install via npm -Prints out the published versions for the app and its respective timestamp. +``` +$ npm install dweb-publish --global +``` -**Note:** In order to use this feature, you must set `API_ORIGIN=*` as an environment variable before running your IPFS node. +### Commands ## Use IPFS to host your webpage using a standard domain (includes cool DNS trick!) @@ -76,7 +54,7 @@ Every IPFS node HTTP interface checks the host header when it receives a request To make this work, simply: -1. Publish your application using `ipscend publish`. +1. Publish your application using `dweb-publish`. 2. Save the returned hash. 3. Find the IPFS ip addresses using `$ dig ipfs.io`. Example: diff --git a/ci/Jenkinsfile b/ci/Jenkinsfile new file mode 100644 index 0000000..a7da2e5 --- /dev/null +++ b/ci/Jenkinsfile @@ -0,0 +1,2 @@ +// Warning: This file is automatically synced from https://github.com/ipfs/ci-sync so if you want to change it, please change it there and ask someone to sync all repositories. +javascript() diff --git a/circle.yml b/circle.yml deleted file mode 100644 index 434211a..0000000 --- a/circle.yml +++ /dev/null @@ -1,12 +0,0 @@ -machine: - node: - version: stable - -dependencies: - pre: - - google-chrome --version - - wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - - - sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' - - sudo apt-get update - - sudo apt-get --only-upgrade install google-chrome-stable - - google-chrome --version diff --git a/package.json b/package.json index c1c66ba..637bd3b 100644 --- a/package.json +++ b/package.json @@ -1,26 +1,22 @@ { - "name": "ipscend", + "name": "dweb-publish", "version": "0.4.3", "description": "Web Application publishing made simple and distributed with IPFS ", "bin": { - "ipscend": "src/cli/bin.js", - "lch": "src/cli/bin.js" + "dweb-publish": "src/bin.js" }, "scripts": { - "lint": "aegir-lint", - "test": "aegir-test --env node", - "release": "aegir-release --env no-build", - "release-minor": "aegir-release --type minor --env no-build", - "release-major": "aegir-release --type major --env no-build", - "coverage": "aegir-coverage", - "coverage-publish": "aegir-coverage publish" + "lint": "aegir lint", + "test": "aegir test -t node", + "release": "aegir release --no-build", + "release-minor": "aegir release --type minor --no-build", + "release-major": "aegir release --type major --eno-build", + "coverage": "aegir coverage", + "coverage-publish": "aegir coverage publish" }, - "pre-commit": [ - "lint" - ], "repository": { "type": "git", - "url": "https://github.com/diasdavid/ipscend.git" + "url": "https://github.com/ipfs-shipyard/dweb-publish.git" }, "keywords": [ "IPFS" @@ -28,22 +24,23 @@ "author": "David Dias ", "license": "MIT", "bugs": { - "url": "https://github.com/diasdavid/ipscend/issues" + "url": "https://github.com/ipfs-shipyard/dweb-publish/issues" }, - "homepage": "https://github.com/diasdavid/ipscend", + "homepage": "https://github.com/ipfs-shipyard/dweb-publish", "dependencies": { - "asking": "^0.1.2", - "ipfs-api": "^14.0.2", - "ipfsd-ctl": "^0.21.0", - "ndjson-aggregator": "^0.1.2", - "node-static": "^0.7.9", - "open": "0.0.5", - "ronin": "^0.3.11", - "webshot": "^0.18.0" + "asking": "~0.1.2", + "ipfs": "^0.32.3", + "ipfs-api": "^25.0.0", + "ipfsd-ctl": "^0.39.3", + "ipfsx": "~0.16.0", + "ndjson-aggregator": "~0.1.2", + "node-static": "~0.7.11", + "open": "~0.0.5", + "webshot": "~0.18.0", + "yargs": "^12.0.2" }, "devDependencies": { - "aegir": "^11.0.2", - "pre-commit": "^1.2.2" + "aegir": "^15.3.1" }, "contributors": [ "David Dias ", diff --git a/src/bin.js b/src/bin.js new file mode 100755 index 0000000..4e31660 --- /dev/null +++ b/src/bin.js @@ -0,0 +1,34 @@ +#! /usr/bin/env node + +'use strict' + +const yargs = require('yargs') +const fs = require('fs') +const updateNotifier = require('update-notifier') +const readPkgUp = require('read-pkg-up') + +const pkg = readPkgUp.sync({ cwd: __dirname }).pkg +const week = 1000 * 60 * 60 * 24 * 7 // 1 week + +updateNotifier({ pkg, updateCheckInterval: week }).notify() + +const args = process.argv.slice(2) + +yargs + .demandCommand(1) + // .epilog('wowza!') text after all commands + .fail((msg, err, yargs) => { + if (err) { throw err } // preserve stack + + if (args.length > 0) { + console.log(msg) + } + + yargs.showHelp() + }) + +fs.readdirSync(__dirname) + .filter((name) => !name.indexOf('command-')) + .forEach((command) => require('./' + command)(yargs)) + +yargs.argv // eslint-disable-line no-unused-expressions diff --git a/src/cli/bin.js b/src/cli/bin.js deleted file mode 100755 index c006725..0000000 --- a/src/cli/bin.js +++ /dev/null @@ -1,10 +0,0 @@ -#! /usr/bin/env node -'use strict' - -const ronin = require('ronin') - -const cli = ronin(__dirname) - -// cli.autoupdate(() => { -cli.run() -// }) diff --git a/src/cli/commands/init.js b/src/cli/commands/init.js deleted file mode 100644 index 334953f..0000000 --- a/src/cli/commands/init.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict' - -var Command = require('ronin').Command -var fs = require('fs') -var ask = require('asking').ask - -module.exports = Command.extend({ - desc: 'Initialize a ipscend project', - - run: function (name) { - try { - fs.statSync(process.cwd() + '/ipscend.json') - console.log('ipscend was already initiated on this repo') - } catch (err) { - bootstrap() - } - - function bootstrap () { - var config = { - versions: [] - } - console.log('This utility will walk you through creating a ipscend.json file.') - ask('Path of your Web Application (project)?', { default: 'public' }, function (err, path) { - if (err) { - return console.log(err) // TODO Handle this err properly - } - config['path'] = path - var fd = fs.openSync(process.cwd() + '/ipscend.json', 'w') - fs.writeSync(fd, JSON.stringify(config, null, ' '), 0, 'utf-8') - }) - } - } -}) diff --git a/src/cli/commands/ipfs/start.js b/src/cli/commands/ipfs/start.js deleted file mode 100644 index 3775a5f..0000000 --- a/src/cli/commands/ipfs/start.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict' - -const fs = require('fs') -const ipfsd = require('ipfsd-ctl') -const os = require('os') - -var Command = require('ronin').Command -module.exports = Command.extend({ - desc: 'Start your a local IPFS node', - run: () => { - const repoPath = process.env.IPFS_PATH || os.homedir() + '/.ipfs' - var init = false - try { - fs.statSync(repoPath) - // TODO check if it is right repo version, if not, inform the user - // how to migrate - } catch (err) { - init = true - console.log('no IPFS repo found, going to start a new one') - } - - ipfsd.disposable({ - repoPath: repoPath, - init: init, - apiAddr: '/ip4/127.0.0.1/tcp/5001', - gatewayAddr: '/ip4/127.0.0.1/tcp/8080' - }, (err, node) => { - if (err) { - return console.log(err) - } - console.log('starting IPFS daemon (this might take some seconds)') - node.startDaemon((err) => { - if (err) { - return console.log('failed to start a daemon') - } - console.log('IPFS daemon has started, you can now publish with ipscend') - process.on('SIGINT', () => { - console.log('Got interrupt signal(SIGINT), shutting down.') - node.stopDaemon(() => { - process.exit(0) - }) - }) - }) - }) - } -}) diff --git a/src/cli/commands/publish.js b/src/cli/commands/publish.js deleted file mode 100644 index 4741039..0000000 --- a/src/cli/commands/publish.js +++ /dev/null @@ -1,63 +0,0 @@ -'use strict' - -const Command = require('ronin').Command -const fs = require('fs') -const ipfsAPI = require('ipfs-api') -const path = require('path') - -module.exports = Command.extend({ - desc: 'Publish your project', - - run: function (name) { - let configPath - try { - configPath = path.resolve(process.cwd() + '/ipscend.json') - fs.statSync(configPath) - publish() - } catch (err) { - console.log('Project must be initiated first, run `ipscend init`') - } - - function publish () { - const config = require(configPath) - const ipfs = ipfsAPI('localhost', '5001') - - ipfs.util.addFromFs(config.path, { - recursive: true, - 'stream-channels': false - }, (err, res) => { - if (err || !res) { - return console.error('err', err) - } - - console.log(res) - - const hash = res[res.length - 2].hash - - const duplicate = config.versions.filter(function (v) { - return v.hash === hash - })[0] - - if (duplicate) { - console.log('This version (' + duplicate.hash + ') has already been published on:', duplicate.timestamp) - return - } - - const version = { - hash: hash, - timestamp: new Date() - } - - console.log('Published', config.path, 'with the following hash:', version.hash) - console.log('You can access it through your local node or through a public IPFS gateway:') - console.log('http://localhost:8080/ipfs/' + version.hash) - console.log('http://ipfs.io/ipfs/' + version.hash) - - config.versions.push(version) - - const fd = fs.openSync(configPath, 'w') - fs.writeSync(fd, JSON.stringify(config, null, ' '), 0, 'utf-8') - }) - } - } -}) diff --git a/src/cli/commands/version.js b/src/cli/commands/version.js deleted file mode 100644 index 7945d5b..0000000 --- a/src/cli/commands/version.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict' - -const Command = require('ronin').Command - -module.exports = Command.extend({ - desc: 'ipscend version', - - options: { - }, - - run: () => { - const pkg = require('../../../package.json') - console.log('Version: ', pkg.version) - } -}) diff --git a/src/cli/commands/versions.js b/src/cli/commands/versions.js deleted file mode 100644 index c76b81f..0000000 --- a/src/cli/commands/versions.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict' - -var Command = require('ronin').Command -var fs = require('fs') -var path = require('path') - -module.exports = Command.extend({ - desc: 'Check each version published', - - run: function (name) { - var configPath - try { - configPath = path.resolve(process.cwd() + '/ipscend.json') - fs.statSync(configPath) - publish() - } catch (err) { - console.log('Project must be initiated first, run `ipscend init`') - } - - function publish () { - var config = JSON.parse(fs.readFileSync(configPath)) - config.versions.forEach(function (version) { - console.log(version.timestamp, version.hash) - }) - } - } -}) diff --git a/src/command-init.js b/src/command-init.js new file mode 100644 index 0000000..cb63707 --- /dev/null +++ b/src/command-init.js @@ -0,0 +1,35 @@ +'use strict' + +const fs = require('fs') +const { hasDWebFile } = require('./utils.js') +const { ask, choose } = require('asking') + +module.exports = (yargs) => { + yargs.command('init', 'Init your project', (yargs) => {}, (options) => { + function init () { + ask('Path to publish (where your webapp/page lives)?', { default: 'public' }, (err, path) => { + if (err) { console.log(err.message) } + + const config = { + versions: [], + gateways: [], // Gateways for pre-loading/pushing + pinning: [] // Pinning services + } + + config.path = path + const fd = fs.openSync(process.cwd() + '/.dweb', 'w') + fs.writeSync(fd, JSON.stringify(config, null, ' '), 0, 'utf-8') + }) + } + + if (hasDWebFile()) { + choose('.dweb already exists, want to overwrite?', ['yes', 'no'], (err, answer) => { + if (err) { return console.log(err.message) } + if (answer === 'no') { process.exit() } + init() + }) + } else { + init() + } + }) +} diff --git a/src/command-list.js b/src/command-list.js new file mode 100644 index 0000000..99dbcad --- /dev/null +++ b/src/command-list.js @@ -0,0 +1,16 @@ +'use strict' + +const fs = require('fs') +const path = require('path') + +module.exports = (yargs) => { + yargs.command('list', 'List the published versions', (yargs) => { + }, (options) => { + const projectConfigPath = path.join(process.cwd(), './.dweb') + const projectConfig = JSON.parse(fs.readFileSync(projectConfigPath)) + + projectConfig.versions.forEach((version) => { + console.log(version.timestamp, version.cid) + }) + }) +} diff --git a/src/command-main.js b/src/command-main.js new file mode 100644 index 0000000..341f3c5 --- /dev/null +++ b/src/command-main.js @@ -0,0 +1,56 @@ +'use strict' + +const { hasDWebFile } = require('./utils.js') +const fs = require('fs') +const getIPFS = require('./get-ipfs.js') +const path = require('path') + +module.exports = (yargs) => { + yargs.command('$0', 'Publish your Website to DWeb with IPFS', (yargs) => { // eslint-disable-line no-unused-expressions + yargs + .option('ipfs-daemon', { + describe: 'Connect and use a running IPFS Daemon (e.g. /ip4/127.0.0.1/tcp/5001)' + }) + }, (options) => { + if (!hasDWebFile()) { + return console.log('No init file found, please run first dweb-publish init') + } + + const projectConfigPath = path.join(process.cwd(), './.dweb') + const projectConfig = JSON.parse(fs.readFileSync(projectConfigPath)) + + getIPFS(options, (err, ipfsd) => { + if (err) { throw err } + + // TODO replace addFromFs with proper ipfs.add that understands files + ipfsd.api.util.addFromFs( + path.join(process.cwd(), projectConfig.path), + { recursive: true }, + (err, files) => { + if (err) { throw err } + + const cid = files[files.length - 2].hash + + const duplicate = projectConfig.versions.filter((v) => v.cid === cid)[0] + + if (duplicate) { + console.log('Latest publish (' + duplicate.hash + ') has already been published on:', duplicate.timestamp) + return + } + + const version = { cid, timestamp: new Date() } + + console.log('Published', projectConfig.path, 'with the following CID:', version.cid) + console.log('You can access it through your local node or through a public IPFS gateway:') + console.log('http://localhost:9090/ipfs/' + version.cid) + console.log('http://ipfs.io/ipfs/' + version.cid) + + projectConfig.versions.push(version) + + // TODO push to multiple gateways + const fd = fs.openSync(projectConfigPath, 'w') + fs.writeSync(fd, JSON.stringify(projectConfig, null, ' '), 0, 'utf-8') + }) + }) + }) +} diff --git a/src/cli/commands/browse.js b/src/commands-old/browse.js similarity index 100% rename from src/cli/commands/browse.js rename to src/commands-old/browse.js diff --git a/src/cli/commands/preview.js b/src/commands-old/preview.js similarity index 100% rename from src/cli/commands/preview.js rename to src/commands-old/preview.js diff --git a/src/cli/commands/screenshot.js b/src/commands-old/screenshot.js similarity index 100% rename from src/cli/commands/screenshot.js rename to src/commands-old/screenshot.js diff --git a/src/get-ipfs.js b/src/get-ipfs.js new file mode 100644 index 0000000..20910bf --- /dev/null +++ b/src/get-ipfs.js @@ -0,0 +1,29 @@ +'use strict' + +const IPFSFactory = require('ipfsd-ctl') + +module.exports = (options, callback) => { + if (options.ipfsDaemon) { + // TODO + // do the remote node with ipfs-api, don't start a factory + // add a fake stop method + } else { + const f = IPFSFactory.create({ + // exec: require('ipfs') TODO use js-ipfs node by default + type: 'js' + }) + + f.spawn({ + // disposable: false + }, (err, ipfsd) => { + if (err) { throw err } + + process.on('SIGINT', () => { + console.log('Got interrupt signal(SIGINT), shutting down the IPFS node') + ipfsd.stopDaemon(() => process.exit(0)) + }) + + callback(null, ipfsd) + }) + } +} diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..f954ff8 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,10 @@ +const fs = require('fs') +const path = require('path') + +function hasDWebFile () { + return Boolean(fs.existsSync(path.join(process.cwd(), '.dweb'))) +} + +module.exports = { + hasDWebFile +}