diff --git a/examples/browser-ipns-publish/.gitignore b/examples/browser-ipns-publish/.gitignore new file mode 100644 index 0000000000..46b007034c --- /dev/null +++ b/examples/browser-ipns-publish/.gitignore @@ -0,0 +1,3 @@ +bundle.js +.cache +/node_modules/ \ No newline at end of file diff --git a/examples/browser-ipns-publish/README.md b/examples/browser-ipns-publish/README.md new file mode 100644 index 0000000000..8bdb02546d --- /dev/null +++ b/examples/browser-ipns-publish/README.md @@ -0,0 +1,85 @@ +# Publish to IPNS from the browser + +> Use ipns from the browser! + +This example is a demo web application that allows you to connect to a go-IPFS node, and publish your IPNS record to the go-DHT network but using your js-ipfs private key. We'll start two IPFS nodes; one in the browser and one on a go-Server. We'll use `ipfs-http-client` to connect to the go-Node to ensure our pubsub messages are getting through, and confirm the IPNS record resolves on the go-Node. We're aiming for something like this: + +``` + +-----------+ websocket +-----------+ + | +-------------------> | + | js-ipfs | pubsub | go-ipfs | + | <-------------------+ | + +-----^-----+ +-----^-----+ + | | + | IPFS in browser | HTTP API + | | ++-------------------------------------------------+ +| Browser | ++-------------------------------------------------+ +| | | | +| | | | +| IPFS direct | | js-http-client | +| a.k.a. ipfsNode | | a.k.a. ipfsAPI | +| | | | ++-------------------------------------------------+ +``` + +## 1. Get started + +With Node.js and git installed, clone the repo and install the project dependencies: + +```sh +git clone https://github.com/ipfs/js-ipfs.git +cd js-ipfs +npm install # Installs ipfs-http-client dependencies +cd examples/browser-ipns-publish +npm install # Installs browser-pubsub app dependencies +``` + +Start the example application: + +```sh +npm start +``` + +You should see something similar to the following in your terminal and the web app should now be available if you navigate to http://127.0.0.1:1234 using your browser: + +```sh +Starting up http-server, serving ./ +Available on: + http://127.0.0.1:1234 +``` + +## 2. Start two IPFS nodes + +The first node is the js-ipfs made in the browser and the demo does that for you. + +The second is a go-ipfs node on a server. To get our IPNS record to the DHT, we'll need [a server running go-IPFS](https://blog.ipfs.io/22-run-ipfs-on-a-vps/) with the API enabled on port 5001. + +Right now the easiest way to do this is to install and start a `go-ipfs` node. + +### Install and start the Go IPFS node + +Head over to https://dist.ipfs.io/#go-ipfs and hit the "Download go-ipfs" button. Extract the archive and read the instructions to install. + +After installation: + +```sh +ipfs init +# Configure CORS to allow ipfs-http-client to access this IPFS node +ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["http://127.0.0.1:1234"]' +# Start the IPFS node, enabling pubsub +ipfs daemon --enable-pubsub-experiment +``` + +## 3. Open the demo in a browser and connect to the go-node + +Now, open up the demo in a browser window. + +In the "CONNECT TO GO-IPFS VIA API MULTIADDRESS" field enter `/ip4/YourServerIP/tcp/5001` (where `YourSeverIP` is your server's IP address or use `/dns4/yourdomain.com/tcp/5001`) and click connect. Once it connects, put your go-IPFS websocket address in the next field `/dns4/yourdomain.com/tcp/4003/wss/p2p/QmPeerIDHash` and hit the second "Connect" button. + +This connects the API to the go-Node and connects your js-IPFS node via websocket to the go-Node so pubsub will work. + +You can choose whether to publish this record under the PeerId of the node that is running in the browser ('self') or choose to add a custom key to the IPFS keychain and publish under that instead. Either should work. + +Finally, enter `/ipfs/QmSomeHash` as the content you want to publish to IPNS. You should see the messages sent from the browser to the server appear in the logs below, ending with "Success, reolved" if it all worked. diff --git a/examples/browser-ipns-publish/index.html b/examples/browser-ipns-publish/index.html new file mode 100644 index 0000000000..74e52fd1e6 --- /dev/null +++ b/examples/browser-ipns-publish/index.html @@ -0,0 +1,209 @@ + + + + Publish to IPNS from the browser + + + + +
+ + + +

+ IPNS Publish from JS to Go Peer +

+
+
+
+ IPNS PUBLISH SET-UP STEPS +
+
+
+
+

+ Code repo for this demo + on Github +

+

+ The idea is to publish from js-ipfs so you control your own + private keys, but publish to go-ipfs to benefit from the DHT. + Since the DHT in js-ipfs isn't working yet, we need to use PubSub + and have a go-ipfs node subscribed to that PubSub to get our IPNS + record onto the DHT. In order to use PubSub between these two + nodes, you'll need a websocket to connect them. +

+
+ To make this demo work, you're going to need: +
    +
  • + 1. Access to a go-ipfs node and it's API, a-la +
    /dns4/domain.com/tcp/5001
    +
  • +
  • + This is how the demo talks to the server, to ensure things + like: +
  • +
      +
    • + A) pubsub is enabled, { EXPERIMENTAL: { ipnsPubsub: true } } + and --enable-pubsub-experiment +
    • +
    • + B) go-ipfs is connected to the js-ipfs node in the browser, + via node.swarm.peers(), +
    • +
    • + C) the pubsub messages are getting through to the go node, + via node.pubsub.subscribe(). +
    • +
    +
+
    +
  • + 2. Access to a go-ipfs Websocket port, a-la +
    /dns4/domain.com/tcp/4003/wss/p2p/QmTheDomainPeerId
    +
  • +
  • + Since we need PubSub for IPNS to reach the go-IPFS node (and + further replicate through the go-DHT network) we need to + connect our pubsub enabled JS-IPFS node in the browser to our + go-IPFS node on the server. The way we connect is via + Websocket. See + this example + for reference. +
  • +
+

+ Once we can talk to go-IPFS and we're connected via Websocket, + then we can publish in our browser node, have the pubsub push it + to the go-IPFS server, and then check with the server that it's + confirmed as published. Once it's on the go-IPFS node it should + replicate throughout the rest of the DHT amongst the go-Nodes. +

+
+
+
+
+
+
+
+ 1. Connect to Go-IPFS via API MultiAddress +
+ + +
+
+
+ 2. Connect to Go-IPFS via Websocket MultiAddress (for PubSub to work) +
+ + +
+ +
+
+ 3. Choose a key: +
+
+ +
+
+
+
+ 4. IPFS hash to publish +
+ + +
+ +
+
Browser Console
+
+
+
+
Server Console
+
+
+ + + diff --git a/examples/browser-ipns-publish/index.js b/examples/browser-ipns-publish/index.js new file mode 100644 index 0000000000..5040105a3e --- /dev/null +++ b/examples/browser-ipns-publish/index.js @@ -0,0 +1,286 @@ +"use strict"; + +const IpfsHttpClient = require("ipfs-http-client"); +const ipns = require("ipns"); +const IPFS = require("ipfs"); +const pRetry = require("p-retry"); +const base64url = require("base64url"); +const last = require("it-last"); +const cryptoKeys = require("human-crypto-keys"); // { getKeyPairFromSeed } + +const { sleep, Logger, onEnterPress, catchAndLog } = require("./util"); +const { Date, Element } = require("ipfs-utils/src/globalthis"); + +async function main() { + const apiUrlInput = document.getElementById("api-url"); + const nodeConnectBtn = document.getElementById("node-connect"); + + const peerAddrInput = document.getElementById("peer-addr"); + const wsConnectBtn = document.getElementById("peer-connect"); + + const ipnsInput = document.getElementById("topic"); + const publishBtn = document.getElementById("publish"); + + const namespace = "/record/"; + const retryOptions = { + retries: 5, + }; + + let log = Logger(document.getElementById("console")); + let sLog = Logger(document.getElementById("server-console")); + let keyName = "self"; + + let ipfsAPI; // remote server IPFS node + let ipfsBrowser; // local browser IPFS node + let peerId; + + // init local browser node right away + log(`Browser IPFS getting ready...`); + ipfsBrowser = await IPFS.create({ + pass: "01234567890123456789", + EXPERIMENTAL: { ipnsPubsub: true }, + }); + const { id } = await ipfsBrowser.id(); + log(`Browser IPFS ready! Node id: ${id}`); + document.getElementById("api-url").disabled = false; + document.getElementById("node-connect").disabled = false; + + async function nodeConnect(url) { + log(`Connecting to ${url}`); + ipfsAPI = IpfsHttpClient(url); + const { id, agentVersion } = await ipfsAPI.id(); + peerId = id; + log(`Success!`); + log(`Version ${agentVersion}`); + log(`Peer ID ${id}`); + document.getElementById("peer-addr").disabled = false; + document.getElementById("peer-connect").disabled = false; + } + + async function wsConnect(addr) { + if (!addr) throw new Error("Missing peer multiaddr"); + if (!ipfsBrowser) + throw new Error("Wait for the local IPFS node to start first"); + log(`Connecting to peer ${addr}`); + await ipfsBrowser.swarm.connect(addr); + log(`Success!`); + log("Listing swarm peers..."); + await sleep(); + const peers = await ipfsBrowser.swarm.peers(); + peers.forEach((peer) => { + //console.log(`peer: ${JSON.stringify(peer, null, 2)}`); + const fullAddr = `${peer.addr}/ipfs/${peer.peer}`; + log( + `${fullAddr}` + ); + }); + log(`(${peers.length} peers total)`); + document.getElementById("topic").disabled = false; + document.getElementById("publish").disabled = false; + } + + // Wait until a peer subscribes a topic + const waitForPeerToSubscribe = async (daemon, topic) => { + await pRetry(async () => { + const res = await daemon.pubsub.ls(); + + if (!res || !res.length || !res.includes(topic)) { + throw new Error("Could not find subscription"); + } + + return res[0]; + }, retryOptions); + }; + + // wait until a peer know about other peer to subscribe a topic + const waitForNotificationOfSubscription = (daemon, topic, peerId) => + pRetry(async () => { + const res = await daemon.pubsub.peers(topic); + + if (!res || !res.length || !res.includes(peerId)) { + throw new Error("Could not find peer subscribing"); + } + }, retryOptions); + + async function subs(node, topic, tLog) { + tLog(`Subscribing to ${topic}`); + await node.pubsub.subscribe( + topic, + (msg) => { + const from = msg.from; + const seqno = msg.seqno.toString("hex"); + + tLog( + `${new Date( + Date.now() + ).toLocaleTimeString()}\n Message ${seqno} from ${from}` + ); + + let regex = "/record/"; + if (topic.match(regex) ? topic.match(regex).length > 0 : false) { + tLog( + "\n#" + + ipns.unmarshal(msg.data).sequence.toString() + + ") Topic: " + + msg.topicIDs[0].toString() + ); + tLog("Value:\n" + ipns.unmarshal(msg.data).value.toString()); + } else { + try { + tLog(JSON.stringify(msg.data.toString(), null, 2)); + } catch (_) { + tLog(msg.data.toString("hex")); + } + } + }, + { + onError: (err, fatal) => { + if (fatal) { + console.error(err); + tLog(`${err.message}`); + tLog(`Resubscribing in 5s to ${topic}...`); + setTimeout( + catchAndLog(() => subs(node, topic, tLog), tLog), + 5000 + ); + } else { + console.warn(err); + } + }, + } + ); + } + + async function createKey(keyName) { + return new Promise(async (resolve, reject) => { + try { + // quick and dirty key gen, don't do this in real life + const key = await IPFS.multihashing.digest( + Buffer.from(keyName + Math.random().toString(36).substring(2)), + "sha2-256" + ); + const keyPair = await cryptoKeys.getKeyPairFromSeed(key, "rsa"); + + // put it on the browser IPNS keychain and name it + await ipfsBrowser.key.import(keyName, keyPair.privateKey); + // now this key can be used to publish to this ipns publicKey + resolve(true); + } catch (err) { + console.log(`Error creating Key ${keyName}: \n ${err}`); + reject(false); + } + }); + } + + async function publish(content) { + if (!content) throw new Error("Missing ipns content to publish"); + if (!ipfsAPI) throw new Error("Connect to a go-server node first"); + if (!ipfsAPI.name.pubsub.state() || !ipfsBrowser.name.pubsub.state()) + throw new Error( + "IPNS Pubsub must be enabled on bother peers, use --enable-namesys-pubsub" + ); + + log(`Publish to IPNS`); // subscribes the server to our IPNS topic + + let browserNode = await ipfsBrowser.id(); + let serverNode = await ipfsAPI.id(); + + // get which key this will be publish under, self or an imported custom key + keyName = document.querySelector('input[name="keyName"]:checked').value; + let keys = { name: "self", id: browserNode.id }; // default init + + if (keyName != "self") { + if (!(await ipfsBrowser.key.list()).find((k) => k.name == keyName)) + // skip if custom key exists already + await createKey(keyName); + let r = await ipfsBrowser.key.list(); + keys = r.find((k) => k.name == keyName); + log(JSON.stringify(keys)); + } + + log(`Initial Resolve ${keys.id}`); // subscribes the server to our IPNS topic + last(ipfsAPI.name.resolve(keys.id, { stream: false })); // save the pubsub topic to the server to make them listen + + await sleep(1000); // give it a moment to save it + + // set up the topic from ipns key + let b58 = await IPFS.multihash.fromB58String(keys.id); + const ipnsKeys = ipns.getIdKeys(b58); + const topic = `${namespace}${base64url.encode( + ipnsKeys.routingKey.toBuffer() + )}`; + + // subscribe and log on both nodes + await subs(ipfsBrowser, topic, log); // browserLog + await subs(ipfsAPI, topic, sLog); // serverLog + + // confirm they are subscribed + await waitForPeerToSubscribe(ipfsAPI, topic); // confirm topic is on THEIR list // API + await waitForNotificationOfSubscription(ipfsBrowser, topic, serverNode.id); // confirm they are on OUR list + + await sleep(2500); // give it a moment to save it + + let remList = await ipfsAPI.pubsub.ls(); // API + if (!remList.includes(topic)) + sLog(`[Fail] !Pubsub.ls ${topic}`); + else sLog(`[Pass] Pubsub.ls`); + + let remListSubs = await ipfsAPI.name.pubsub.subs(); // API + if (!remListSubs.includes(`/ipns/${keys.id}`)) + sLog(`[Fail] !Name.Pubsub.subs ${keys.id}`); + else sLog(`[Pass] Name.Pubsub.subs`); + + // publish will send a pubsub msg to the server to update their ipns record + log(`Publishing ${content} to ${keys.name} /ipns/${keys.id}`); + const results = await ipfsBrowser.name.publish(content, { + resolve: false, + key: keyName, + }); + log(`Published ${results.name} to ${results.value}`); // + + log(`Wait 5 seconds, then resolve...`); + await sleep(5000); + + log(`Try resolve ${keys.id} on server through API`); + + let name = await last( + ipfsAPI.name.resolve(keys.id, { + stream: false, + }) + ); + log(`Resolved: ${name}`); + if (name == content) { + log(`IPNS Publish Success!`); + log( + `Look at that! /ipns/${keys.id} resolves to ${content}` + ); + } else { + log( + `Error, resolve did not match ${name} !== ${content}` + ); + } + } + + const onNodeConnectClick = catchAndLog( + () => nodeConnect(apiUrlInput.value), + log + ); + + apiUrlInput.addEventListener("keydown", onEnterPress(onNodeConnectClick)); + nodeConnectBtn.addEventListener("click", onNodeConnectClick); + + const onwsConnectClick = catchAndLog( + () => wsConnect(peerAddrInput.value), + log + ); + peerAddrInput.addEventListener("keydown", onEnterPress(onwsConnectClick)); + wsConnectBtn.addEventListener("click", onwsConnectClick); + + const onPublishClick = catchAndLog(() => publish(ipnsInput.value), log); + ipnsInput.addEventListener("keydown", onEnterPress(onPublishClick)); + publishBtn.addEventListener("click", onPublishClick); +} + +main(); diff --git a/examples/browser-ipns-publish/package.json b/examples/browser-ipns-publish/package.json new file mode 100644 index 0000000000..0f14202a14 --- /dev/null +++ b/examples/browser-ipns-publish/package.json @@ -0,0 +1,44 @@ +{ + "name": "browser-ipns-publish", + "version": "1.0.0", + "description": "An example demonstrating publishing to IPNS in the browser", + "private": true, + "main": "index.js", + "scripts": { + "build": "parcel build index.html --public-url ./", + "start": "parcel index.html", + "test": "test-ipfs-example" + }, + "author": "Doug Anderson", + "license": "MIT", + "dependencies": { + "base64url": "^3.0.1", + "human-crypto-keys": "^0.1.4", + "ipfs": "file:../../packages/ipfs/", + "ipfs-http-client": "file:../../packages/ipfs-http-client/", + "ipfs-utils": "^2.3.1", + "ipns": "^0.7.3", + "it-last": "^1.0.2", + "p-retry": "^4.2.0" + }, + "browserslist": [ + "last 2 versions and not dead and > 2%" + ], + "devDependencies": { + "delay": "^4.3.0", + "execa": "^4.0.0", + "ipfsd-ctl": "^5.0.0", + "go-ipfs": "^0.6.0", + "parcel-bundler": "^1.12.4", + "path": "^0.12.7", + "test-ipfs-example": "file:../test-ipfs-example/" + }, + "repository": { + "type": "git", + "url": "https://github.com/ipfs/js-ipfs/examples" + }, + "keywords": [ + "IPNS", + "Publish" + ] +} diff --git a/examples/browser-ipns-publish/test.js b/examples/browser-ipns-publish/test.js new file mode 100644 index 0000000000..918e40cb3b --- /dev/null +++ b/examples/browser-ipns-publish/test.js @@ -0,0 +1,136 @@ +"use strict"; + +const path = require("path"); +const execa = require("execa"); +const { createFactory } = require("ipfsd-ctl"); +const df = createFactory({ + ipfsHttpModule: require("ipfs-http-client"), + ipfsBin: require("go-ipfs").path(), + args: ["--enable-pubsub-experiment"], + disposable: true, +}); +const { startServer } = require("test-ipfs-example/utils"); +const pkg = require("./package.json"); + +async function testUI(url, apiAddr, peerAddr, topic) { + const proc = execa( + require.resolve("test-ipfs-example/node_modules/.bin/nightwatch"), + [ + "--config", + require.resolve("test-ipfs-example/nightwatch.conf.js"), + path.join(__dirname, "test.js"), + ], + { + cwd: path.resolve(__dirname, "../"), + env: { + ...process.env, + CI: true, + IPFS_EXAMPLE_TEST_URL: url, + IPFS_API_ADDRESS: apiAddr, + IPFS_PEER_ADDRESS: peerAddr, + IPFS_TOPIC: topic, + }, + all: true, + } + ); + proc.all.on("data", (data) => { + process.stdout.write(data); + }); + + await proc; +} + +async function runTest() { + const app = await startServer(__dirname); + const go = await df.spawn({ + type: "go", + test: true, + ipfsOptions: { + config: { + Addresses: { + API: "/ip4/127.0.0.1/tcp/0", + }, + API: { + HTTPHeaders: { + "Access-Control-Allow-Origin": [app.url], + }, + }, + }, + }, + }); + + const go2 = await df.spawn({ + type: "go", + test: true, + }); + + await go.api.swarm.connect(go2.api.peerId.addresses[0]); + + const topic = `/ipfs/QmWCnkCXYYPP7NgH6ZHQiQxw7LJAMjnAySdoz9i1oxD5XJ`; + + try { + await testUI( + app.url, + go.apiAddr, + go.api.peerId.addresses[0].toString(), + topic + ); + } finally { + await go.stop(); + await go2.stop(); + await app.stop(); + } +} + +module.exports = runTest; + +module.exports[pkg.name] = function (browser) { + const apiSelector = "#api-url:enabled"; + + // connect to the API + browser + .url(process.env.IPFS_EXAMPLE_TEST_URL) + .waitForElementVisible(apiSelector) + .clearValue(apiSelector) + .setValue(apiSelector, process.env.IPFS_API_ADDRESS) + .pause(1000) + .perform(() => { + console.log( + "process.env.IPFS_API_ADDRESS: ", + process.env.IPFS_API_ADDRESS + ); + }) + .click("#node-connect"); + + browser.expect + .element("#console") + .text.to.contain(`Connecting to ${process.env.IPFS_API_ADDRESS}\nSuccess!`); + + // connect via websocket + const peerAddrSelector = "#peer-addr:enabled"; + browser + .waitForElementVisible(peerAddrSelector) + .clearValue(peerAddrSelector) + .setValue(peerAddrSelector, process.env.IPFS_PEER_ADDRESS) + .pause(1000) + .click("#peer-connect"); + + browser.expect + .element("#console") + .text.to.contain( + `Connecting to peer ${process.env.IPFS_PEER_ADDRESS}\nSuccess!` + ); + + // publish to IPNS + const publishSelector = "#topic:enabled"; + browser + .waitForElementVisible(publishSelector) + .clearValue(publishSelector) + .setValue(publishSelector, process.env.IPFS_TOPIC) + .pause(1000) + .click("#publish"); + + browser.expect.element("#console").text.to.contain(`IPNS Publish Success!`); + + browser.end(); +}; diff --git a/examples/browser-ipns-publish/util.js b/examples/browser-ipns-publish/util.js new file mode 100644 index 0000000000..e6aada61f8 --- /dev/null +++ b/examples/browser-ipns-publish/util.js @@ -0,0 +1,31 @@ +exports.sleep = (ms = 1000) => new Promise(resolve => setTimeout(resolve, ms)) + +exports.Logger = outEl => { + outEl.innerHTML = '' + return message => { + const container = document.createElement('div') + container.innerHTML = message + outEl.appendChild(container) + outEl.scrollTop = outEl.scrollHeight + } +} + +exports.onEnterPress = fn => { + return e => { + if (event.which == 13 || event.keyCode == 13) { + e.preventDefault() + fn() + } + } +} + +exports.catchAndLog = (fn, log) => { + return async (...args) => { + try { + await fn(...args) + } catch (err) { + console.error(err) + log(`${err.message}`) + } + } +} diff --git a/examples/test-ipfs-example/nightwatch.conf.js b/examples/test-ipfs-example/nightwatch.conf.js index fa7def7b23..524dc76a6b 100644 --- a/examples/test-ipfs-example/nightwatch.conf.js +++ b/examples/test-ipfs-example/nightwatch.conf.js @@ -2,6 +2,7 @@ const { ephemeralPort } = require('./utils') const path = require('path') +const chrome = require('chromedriver') // added to work on windows const WEBRIVER_PORT = ephemeralPort() @@ -11,7 +12,7 @@ module.exports = { webdriver: { start_process: true, - server_path: require.resolve(path.resolve(__dirname, 'node_modules/.bin/chromedriver')), + server_path: chrome.path, //require.resolve(path.resolve(__dirname, 'node_modules/.bin/chromedriver')), // this wasn't working on windows port: WEBRIVER_PORT, cli_args: [ `--port=${WEBRIVER_PORT}` diff --git a/examples/test-ipfs-example/package.json b/examples/test-ipfs-example/package.json index 4f5590c846..f110bb9bdd 100644 --- a/examples/test-ipfs-example/package.json +++ b/examples/test-ipfs-example/package.json @@ -10,7 +10,7 @@ "chromedriver": "^83.0.0", "execa": "^4.0.0", "fs-extra": "^8.1.0", - "http-server": "^0.11.1", + "http-server": "^0.12.3", "nightwatch": "^1.2.4", "which": "^2.0.1" } diff --git a/examples/test-ipfs-example/test.js b/examples/test-ipfs-example/test.js index 42cd7947b7..cd93337cd7 100755 --- a/examples/test-ipfs-example/test.js +++ b/examples/test-ipfs-example/test.js @@ -25,7 +25,7 @@ async function testExample (dir) { //await installDeps(dir) await build(dir) - if (dir.includes('examples/browser-')) { + if (dir.includes('examples/browser-') || dir.includes('examples\\browser-') ) { await runBrowserTest(dir) } else { await runNodeTest(dir) diff --git a/examples/test-ipfs-example/utils.js b/examples/test-ipfs-example/utils.js index 6261efd2c1..30aebac0b4 100644 --- a/examples/test-ipfs-example/utils.js +++ b/examples/test-ipfs-example/utils.js @@ -1,159 +1,182 @@ -'use strict' +"use strict"; -const fs = require('fs-extra') -const path = require('path') -const execa = require('execa') -const which = require('which') +const fs = require("fs-extra"); +const path = require("path"); +const execa = require("execa"); +const which = require("which"); -async function startServer (dir) { - async function serveFrom (appDir) { +async function startServer(dir) { + async function serveFrom(appDir) { return new Promise((resolve, reject) => { - let output = '' - - const proc = execa.command(`${path.resolve(__dirname, 'node_modules/.bin/http-server')} ${appDir} -a 127.0.0.1`, { - cwd: __dirname, - all: true - }) - proc.all.on('data', (data) => { - process.stdout.write(data) - - const line = data.toString('utf8') - output += line - - if (output.includes('Hit CTRL-C to stop the server')) { - // find the port - const port = output.match(/http:\/\/127.0.0.1:(\d+)/)[1] + let output = ""; + + const proc = execa.command( + `${path.resolve( + __dirname, + "node_modules/.bin/http-server" + )} ${appDir} -a 127.0.0.1`, + { + cwd: __dirname, + all: true, + } + ); + proc.all.on("data", (data) => { + process.stdout.write(data); + + const line = data.toString("utf8"); + output += line; + + let port; + if (output.includes("Hit CTRL-C to stop the server")) { + // windows hack, windows inserts a bunch of unicode around the port + if (output.includes(`\u001b[32m`)) { + port = output.split(`\u001b[32m`)[1].split(`\u001b[39m`)[0]; + } else { + // find the port + const regex = /http:\/\/127.0.0.1:(\d+)/; //|(?<=\\u001b\[32m)(.\d+?)(?=\\u001b\[39m)/; // positive lookbehind regex didn't work + port = output.match(regex)[output.match(regex).length - 1]; + } if (!port) { - throw new Error(`Could not find port in ${output}`) + throw new Error(`Could not find port in ${output}`); } resolve({ stop: () => { - console.info('Stopping server') - proc.kill('SIGINT', { - forceKillAfterTimeout: 2000 - }) + console.info("Stopping server"); + proc.kill("SIGINT", { + forceKillAfterTimeout: 2000, + }); }, - url: `http://127.0.0.1:${port}` - }) + url: `http://127.0.0.1:${port}`, + }); } - }) + }); - proc.then(() => {}, (err) => reject(err)) - }) + proc.then( + () => {}, + (err) => reject(err) + ); + }); } // start something.. const serverPaths = [ - path.join(dir, 'build'), - path.join(dir, 'dist'), - path.join(dir, 'public') - ] + path.join(dir, "build"), + path.join(dir, "dist"), + path.join(dir, "public"), + ]; for (const p of serverPaths) { if (fs.existsSync(p)) { - return serveFrom(p) + return serveFrom(p); } } // running a bare index.html file - const files = [ - path.join(dir, 'index.html') - ] + const files = [path.join(dir, "index.html")]; for (const f of files) { if (fs.existsSync(f)) { - console.info('Found bare file', f) + console.info("Found bare file", f); return Promise.resolve({ url: `file://${f}`, - stop: () => {} - }) + stop: () => {}, + }); } } - throw new Error('Browser examples must contain a `public`, `dist` or `build` folder or an `index.html` file') + throw new Error( + "Browser examples must contain a `public`, `dist` or `build` folder or an `index.html` file" + ); } -function ephemeralPort (min = 49152, max = 65535) { - return Math.floor(Math.random() * (max - min + 1) + min) +function ephemeralPort(min = 49152, max = 65535) { + return Math.floor(Math.random() * (max - min + 1) + min); } -async function isExecutable (command) { +async function isExecutable(command) { try { - await fs.access(command, fs.constants.X_OK) + await fs.access(command, fs.constants.X_OK); - return true + return true; } catch (err) { - if (err.code === 'ENOENT') { - return isExecutable(await which(command)) + if (err.code === "ENOENT") { + return isExecutable(await which(command)); } - if (err.code === 'EACCES') { - return false + if (err.code === "EACCES") { + return false; } - throw err + throw err; } } -async function waitForOutput (expectedOutput, command, args = [], opts = {}) { - if (!await isExecutable(command)) { - args.unshift(command) - command = 'node' +async function waitForOutput(expectedOutput, command, args = [], opts = {}) { + if (!(await isExecutable(command))) { + args.unshift(command); + command = "node"; } - const proc = execa(command, args, { ...opts, all: true }) - let output = '' - const time = 120000 + const proc = execa(command, args, { ...opts, all: true }); + let output = ""; + const time = 120000; - let foundExpectedOutput = false - let cancelTimeout + let foundExpectedOutput = false; + let cancelTimeout; const timeoutPromise = new Promise((resolve, reject) => { const timeout = setTimeout(() => { - reject(new Error(`Did not see "${expectedOutput}" in output from "${[command].concat(args).join(' ')}" after ${time / 1000}s`)) + reject( + new Error( + `Did not see "${expectedOutput}" in output from "${[command] + .concat(args) + .join(" ")}" after ${time / 1000}s` + ) + ); setTimeout(() => { - proc.kill() - }, 100) - }, time) + proc.kill(); + }, 100); + }, time); cancelTimeout = () => { - clearTimeout(timeout) - resolve() - } - }) + clearTimeout(timeout); + resolve(); + }; + }); - proc.all.on('data', (data) => { - process.stdout.write(data) - output += data.toString('utf8') + proc.all.on("data", (data) => { + process.stdout.write(data); + output += data.toString("utf8"); if (output.includes(expectedOutput)) { - foundExpectedOutput = true - proc.kill() - cancelTimeout() + foundExpectedOutput = true; + proc.kill(); + cancelTimeout(); } - }) + }); try { - await Promise.race([ - proc, - timeoutPromise - ]) + await Promise.race([proc, timeoutPromise]); } catch (err) { if (!err.killed) { - throw err + throw err; } } if (!foundExpectedOutput) { - throw new Error(`Did not see "${expectedOutput}" in output from "${[command].concat(args).join(' ')}"`) + throw new Error( + `Did not see "${expectedOutput}" in output from "${[command] + .concat(args) + .join(" ")}"` + ); } } module.exports = { startServer, ephemeralPort, - waitForOutput -} + waitForOutput, +}; diff --git a/lerna.json b/lerna.json index 60756e2343..8ed30bf2ea 100644 --- a/lerna.json +++ b/lerna.json @@ -1,7 +1,8 @@ { "lerna": "3.22.0", "packages": [ - "packages/*" + "packages/*", + "examples/*" ], "version": "independent", "command": { @@ -9,7 +10,9 @@ "hoist": true, "nohoist": [ "libp2p-delegated-content-routing", - "libp2p-delegated-peer-routing" + "libp2p-delegated-peer-routing", + "ipfs-css", + "tachyons" ] }, "run": {