| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(`<span class="green">Success!</span>`); | ||
| 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(`<span class="green">Success!</span>`); | ||
| 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( | ||
| `<span class="${ | ||
| addr.endsWith(peer.peer) ? "teal" : "" | ||
| }">${fullAddr}</span>` | ||
| ); | ||
| }); | ||
| 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(`<span class="red">${err.message}</span>`); | ||
| 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(`<span class="red">[Fail] !Pubsub.ls ${topic}</span>`); | ||
| else sLog(`[Pass] Pubsub.ls`); | ||
|
|
||
| let remListSubs = await ipfsAPI.name.pubsub.subs(); // API | ||
| if (!remListSubs.includes(`/ipns/${keys.id}`)) | ||
| sLog(`<span class="red">[Fail] !Name.Pubsub.subs ${keys.id}</span>`); | ||
| 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(`<span class="green">IPNS Publish Success!</span>`); | ||
| log( | ||
| `<span class="green">Look at that! /ipns/${keys.id} resolves to ${content}</span>` | ||
| ); | ||
| } else { | ||
| log( | ||
| `<span class="red">Error, resolve did not match ${name} !== ${content}</span>` | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| 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(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(`<span class="red">${err.message}</span>`) | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, | ||
| }; |