diff --git a/examples/helia-service-worker-gateway/.github/pull_request_template.md b/examples/helia-service-worker-gateway/.github/pull_request_template.md new file mode 100644 index 00000000..04bd7d52 --- /dev/null +++ b/examples/helia-service-worker-gateway/.github/pull_request_template.md @@ -0,0 +1,17 @@ +# ⚠️ IMPORTANT ⚠️ + +# Please do not create a Pull Request for this repository + +The contents of this repository are automatically synced from the parent [Helia Examples Project](https://github.com/ipfs-examples/helia-examples) so any changes made to the standalone repository will be lost after the next sync. + +Please open a PR against [IPFS Examples](https://github.com/ipfs-examples/helia-examples) instead. + +## Contributing + +Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. + +1. Fork the [Helia Examples Project](https://github.com/ipfs-examples/helia-examples) +2. Create your Feature Branch (`git checkout -b feature/amazing-example`) +3. Commit your Changes (`git commit -a -m 'feat: add some amazing example'`) +4. Push to the Branch (`git push origin feature/amazing-example`) +5. Open a Pull Request diff --git a/examples/helia-service-worker-gateway/.github/workflows/sync.yml b/examples/helia-service-worker-gateway/.github/workflows/sync.yml new file mode 100644 index 00000000..3f8b7446 --- /dev/null +++ b/examples/helia-service-worker-gateway/.github/workflows/sync.yml @@ -0,0 +1,19 @@ +name: pull + +on: + workflow_dispatch + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Pull from another repository + uses: ipfs-examples/actions-pull-directory-from-repo@main + with: + source-repo: ipfs-examples/helia-examples + source-folder-path: examples/${{ github.event.repository.name }} + source-branch: main + target-branch: main + git-username: github-actions + git-email: github-actions@github.com diff --git a/examples/helia-service-worker-gateway/README.md b/examples/helia-service-worker-gateway/README.md new file mode 100644 index 00000000..0770ef9d --- /dev/null +++ b/examples/helia-service-worker-gateway/README.md @@ -0,0 +1,108 @@ +

+ + Helia logo + +

+ +

Bundle Helia with Webpack

+ +

+ +
+ Explore the docs + · + View Demo + · + Report Bug + · + Request Feature/Example +

+ +## Table of Contents + +- [Table of Contents](#table-of-contents) +- [About The Project](#about-the-project) +- [Getting Started](#getting-started) + - [Prerequisites](#prerequisites) + - [Installation and Running example](#installation-and-running-example) +- [Usage](#usage) +- [Documentation](#documentation) +- [Contributing](#contributing) +- [Want to hack on IPFS?](#want-to-hack-on-ipfs) + +## About The Project + +- Read the [docs](https://ipfs.github.io/helia/modules/helia.html) +- Look into other [examples](https://github.com/ipfs-examples/helia-examples) to learn how to spawn a Helia node in Node.js and in the Browser +- Visit https://dweb-primer.ipfs.io to learn about IPFS and the concepts that underpin it +- Head over to https://proto.school to take interactive tutorials that cover core IPFS APIs +- Check out https://docs.ipfs.io for tips, how-tos and more +- See https://blog.ipfs.io for news and more +- Need help? Please ask 'How do I?' questions on https://discuss.ipfs.io + +## Getting Started + +### Prerequisites + +Make sure you have installed all of the following prerequisites on your development machine: + +- Git - [Download & Install Git](https://git-scm.com/downloads). OSX and Linux machines typically have this already installed. +- Node.js - [Download & Install Node.js](https://nodejs.org/en/download/) and the npm package manager. + +### Installation and Running example + +```console +> npm install +> npm start +``` + +Now open your browser at `http://localhost:3000` + +## Usage + +In this example, you will find a boilerplate you can use to guide yourself into bundling helia with webpack, so that you can use it in your own web app! + +You should see the following: + +![](./img/1.png) + +_For more examples, please refer to the [Documentation](#documentation)_ + +## Documentation + +- [IPFS Primer](https://dweb-primer.ipfs.io/) +- [IPFS Docs](https://docs.ipfs.io/) +- [Tutorials](https://proto.school) +- [More examples](https://github.com/ipfs-examples/helia-examples) +- [API - Helia](https://ipfs.github.io/helia/modules/helia.html) +- [API - @helia/unixfs](https://ipfs.github.io/helia-unixfs/modules/helia.html) + +## Contributing + +Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. + +1. Fork the IPFS Project +2. Create your Feature Branch (`git checkout -b feature/amazing-feature`) +3. Commit your Changes (`git commit -a -m 'feat: add some amazing feature'`) +4. Push to the Branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +## Want to hack on IPFS? + +[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) + +The IPFS implementation in JavaScript needs your help! There are a few things you can do right now to help out: + +Read the [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md) and [JavaScript Contributing Guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md). + +- **Check out existing issues** The [issue list](https://github.com/ipfs/helia/issues) has many that are marked as ['help wanted'](https://github.com/ipfs/helia/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22help+wanted%22) or ['difficulty:easy'](https://github.com/ipfs/helia/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Adifficulty%3Aeasy) which make great starting points for development, many of which can be tackled with no prior IPFS knowledge +- **Look at the [Helia Roadmap](https://github.com/ipfs/helia/blob/main/ROADMAP.md)** This are the high priority items being worked on right now +- **Perform code reviews** More eyes will help + a. speed the project along + b. ensure quality, and + c. reduce possible future bugs +- **Add tests**. There can never be enough tests + +[cid]: https://docs.ipfs.tech/concepts/content-addressing "Content Identifier" +[Uint8Array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array +[libp2p]: https://libp2p.io diff --git a/examples/helia-service-worker-gateway/img/1.png b/examples/helia-service-worker-gateway/img/1.png new file mode 100644 index 00000000..e7dd1bbc Binary files /dev/null and b/examples/helia-service-worker-gateway/img/1.png differ diff --git a/examples/helia-service-worker-gateway/package.json b/examples/helia-service-worker-gateway/package.json new file mode 100644 index 00000000..0e44cd77 --- /dev/null +++ b/examples/helia-service-worker-gateway/package.json @@ -0,0 +1,56 @@ +{ + "name": "helia-service-worker-gateway", + "version": "1.0.0", + "private": true, + "type": "module", + "description": "Bundle Helia with Webpack", + "keywords": [], + "license": "MIT", + "scripts": { + "clean": "rimraf ./dist", + "build": "webpack --env production", + "serve": "webpack serve --mode=development", + "start": "npm run serve", + "test": "npm run build && playwright test tests" + }, + "browserslist": [ + "last 1 Chrome version" + ], + "dependencies": { + "@chainsafe/libp2p-noise": "^11.0.0", + "@chainsafe/libp2p-yamux": "^3.0.5", + "@helia/unixfs": "^1.0.2", + "@libp2p/bootstrap": "^6.0.0", + "@libp2p/delegated-content-routing": "^4.0.1", + "@libp2p/delegated-peer-routing": "^4.0.1", + "@libp2p/websockets": "^5.0.3", + "@libp2p/webtransport": "^1.0.7", + "blockstore-core": "^3.0.0", + "datastore-core": "^8.0.4", + "helia": "next", + "kubo-rpc-client": "^3.0.1", + "libp2p": "^0.42.2", + "multiformats": "^11.0.2", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@babel/core": "^7.13.10", + "@babel/preset-env": "^7.13.12", + "@babel/preset-react": "^7.12.13", + "@playwright/test": "^1.12.3", + "babel-loader": "^9.1.2", + "copy-webpack-plugin": "^11.0.0", + "css-loader": "^6.2.0", + "html-webpack-plugin": "^5.3.2", + "node-polyfill-webpack-plugin": "^2.0.1", + "playwright": "^1.12.3", + "rimraf": "^4.1.2", + "style-loader": "^3.1.0", + "test-util-ipfs-example": "^1.0.2", + "webpack": "^5.45.1", + "webpack-cli": "^5.0.1", + "webpack-dev-server": "^4.6.0", + "webpack-merge": "^5.8.0" + } +} diff --git a/examples/helia-service-worker-gateway/public/favicon.ico b/examples/helia-service-worker-gateway/public/favicon.ico new file mode 100644 index 00000000..b2f1f968 Binary files /dev/null and b/examples/helia-service-worker-gateway/public/favicon.ico differ diff --git a/examples/helia-service-worker-gateway/public/index.html b/examples/helia-service-worker-gateway/public/index.html new file mode 100644 index 00000000..97483b22 --- /dev/null +++ b/examples/helia-service-worker-gateway/public/index.html @@ -0,0 +1,19 @@ + + + + + + + + <%= htmlWebpackPlugin.options.title %> + + + + + +
+ + diff --git a/examples/helia-service-worker-gateway/src/app.css b/examples/helia-service-worker-gateway/src/app.css new file mode 100644 index 00000000..1c9bd170 --- /dev/null +++ b/examples/helia-service-worker-gateway/src/app.css @@ -0,0 +1,42 @@ +::placeholder { + color: rgb(0 0 0 / 30%); +} + +form { + margin: 1.25rem 0; +} + +.window { + display: flex; + flex-direction: column; + background: #222; + color: #fff; + height: 400px; +} + +.window .header { + flex-basis: 3rem; + background: #c6c6c6; + position: relative; +} + +.window .header:after { + content: ". . ."; + position: absolute; + left: 12px; + right: 0; + top: -3px; + font-family: "Times New Roman", Times, serif; + font-size: 96px; + color: #fff; + line-height: 0; + letter-spacing: -12px; +} + +.terminal { + margin: 20px; + font-family: monospace; + font-size: 16px; + overflow: auto; + flex: 1; +} diff --git a/examples/helia-service-worker-gateway/src/app.js b/examples/helia-service-worker-gateway/src/app.js new file mode 100644 index 00000000..dbe4ff5b --- /dev/null +++ b/examples/helia-service-worker-gateway/src/app.js @@ -0,0 +1,134 @@ +// @ts-check +import React, { useState, useRef } from 'react'; +import ipfsLogo from './ipfs-logo.svg' +import { getHelia } from './get-helia.js' +import { unixfs } from '@helia/unixfs' +import { CID } from 'multiformats/cid' + +function App() { + const [output, setOutput] = useState([]); + const [helia, setHelia] = useState(null); + const [fileCid, setFileCid] = useState(''); + + const terminalEl = useRef(null); + + const COLORS = { + active: '#357edd', + success: '#0cb892', + error: '#ea5037' + } + + const showStatus = (text, color, id) => { + setOutput((prev) => { + return [...prev, + { + 'content': text, + 'color': color, + 'id': id + } + ] + }) + + terminalEl.current.scroll({ top: terminal.scrollHeight, behavior: 'smooth' }) + } + + const getFile = async (fileCid) => { + let node = helia; + + if (!helia) { + showStatus('Creating Helia node...', COLORS.active) + + node = await getHelia() + + setHelia(node) + } + + const peerId = node.libp2p.peerId + console.log(peerId) + showStatus(`My ID is ${peerId}`, COLORS.active, peerId) + + const fs = unixfs(node) + const cid = CID.parse(fileCid) + + showStatus(`Reading UnixFS text file ${cid}...`, COLORS.active) + const decoder = new TextDecoder() + let text = '' + + for await (const chunk of fs.cat(cid)) { + text += decoder.decode(chunk, { + stream: true + }) + } + + showStatus(`\u2514\u2500 CID: ${cid}`) + showStatus(`Preview: https://ipfs.io/ipfs/${cid}`, COLORS.success) + } + + const handleSubmit = async (e) => { + e.preventDefault(); + + try { + if (fileCid == null || fileCid.trim() === '') { + throw new Error('File CID is missing...') + } + + await getFile(fileCid) + } catch (err) { + showStatus(err.message, COLORS.error) + } + } + + return ( + <> +
+ + IPFS logo + +
+ +
+

Add data to Helia from the browser

+ +
+ + setFileCid(e.target.value)} + /> + + +
+ +

Output

+ +
+
+
+ { output.length > 0 && +
+ { output.map((log, index) => +

+ {log.content} +

) + } +
+ } +
+
+
+ + ); +} + +export default App; diff --git a/examples/helia-service-worker-gateway/src/get-helia.js b/examples/helia-service-worker-gateway/src/get-helia.js new file mode 100644 index 00000000..d58ea87d --- /dev/null +++ b/examples/helia-service-worker-gateway/src/get-helia.js @@ -0,0 +1,80 @@ +import { createHelia } from 'helia' +import { createLibp2p } from 'libp2p' +import { noise } from '@chainsafe/libp2p-noise' +import { yamux } from '@chainsafe/libp2p-yamux' +import { webSockets } from '@libp2p/websockets' +import { webTransport } from '@libp2p/webtransport' +import { bootstrap } from '@libp2p/bootstrap' +import { MemoryBlockstore } from 'blockstore-core' +import { MemoryDatastore } from 'datastore-core' +import { delegatedPeerRouting } from "@libp2p/delegated-peer-routing"; +import { delegatedContentRouting } from "@libp2p/delegated-content-routing"; +import { create as kuboClient } from "kubo-rpc-client"; + +export async function getHelia () { + // the blockstore is where we store the blocks that make up files + const blockstore = new MemoryBlockstore() + + // application-specific data lives in the datastore + const datastore = new MemoryDatastore() + + // default is to use ipfs.io + const delegatedClient = kuboClient({ + // use default api settings + protocol: "https", + port: 443, + host: "node3.delegate.ipfs.io", +}) + + // libp2p is the networking layer that underpins Helia + const libp2p = await createLibp2p({ + datastore, + transports: [ + webSockets(), webTransport() + ], + connectionEncryption: [ + noise() + ], + streamMuxers: [ + yamux() + ], + peerRouters: [delegatedPeerRouting(delegatedClient)], + contentRouters: [delegatedContentRouting(delegatedClient)], + /** + * @see https://github.com/libp2p/js-libp2p/blob/master/doc/CONFIGURATION.md#configuring-dialing + */ + dialer: { + dialTimeout: 120000, + }, + /** + * @see https://github.com/libp2p/js-libp2p/blob/master/doc/CONFIGURATION.md#configuring-connection-manager + */ + connectionManager: { + // Auto connect to discovered peers (limited by ConnectionManager minConnections) + autoDial: true, + // maxConnections: 10, + // minConnections: 0, + // pollInterval: 2000, + }, + /** + * @see https://github.com/libp2p/js-libp2p/blob/master/doc/CONFIGURATION.md#configuring-peerstore + */ + peerStore: { + persistence: true, + /** + * The threshold number represents the maximum number of "dirty peers" allowed in the PeerStore, i.e. peers that + * are not updated in the datastore. In this context, browser nodes should use a threshold of 1, since they + * might not "stop" properly in several scenarios and the PeerStore might end up with unflushed records when the + * window is closed. + */ + threshold: 1, + }, + }) + + // create a Helia node + return await createHelia({ + datastore, + blockstore, + libp2p + }) +} diff --git a/examples/helia-service-worker-gateway/src/index.js b/examples/helia-service-worker-gateway/src/index.js new file mode 100644 index 00000000..716ccedf --- /dev/null +++ b/examples/helia-service-worker-gateway/src/index.js @@ -0,0 +1,11 @@ +import React from 'react' +import ReactDOM from 'react-dom' +import './app.css'; +import App from './app.js' + +ReactDOM.render( + + + , + document.getElementById('root') +); diff --git a/examples/helia-service-worker-gateway/src/ipfs-logo.svg b/examples/helia-service-worker-gateway/src/ipfs-logo.svg new file mode 100644 index 00000000..f8633f62 --- /dev/null +++ b/examples/helia-service-worker-gateway/src/ipfs-logo.svg @@ -0,0 +1 @@ + diff --git a/examples/helia-service-worker-gateway/tests/test.js b/examples/helia-service-worker-gateway/tests/test.js new file mode 100644 index 00000000..3c09a556 --- /dev/null +++ b/examples/helia-service-worker-gateway/tests/test.js @@ -0,0 +1,37 @@ +import { test, expect } from '@playwright/test'; +import { playwright } from 'test-util-ipfs-example'; + +// Setup +const play = test.extend({ + ...playwright.servers(), +}); + +play.describe('bundle Helia with Webpack:', () => { + // DOM + const nameInput = "#file-name" + const contentInput = "#file-content" + const submitBtn = "#add-submit" + const output = "#output" + + play.beforeEach(async ({servers, page}) => { + await page.goto(`http://localhost:${servers[0].port}/`); + }) + + play('should properly initialized a Helia node and add/get a file', async ({ page }) => { + const fileName = 'test.txt' + const stringToUse = 'Hello world!' + + await page.fill(nameInput, fileName) + await page.fill(contentInput, stringToUse) + await page.click(submitBtn) + + await page.waitForSelector(`${output}:has-text("/bafkreigaknpexyvxt76zgkitavbwx6ejgfheup5oybpm77f3pxzrvwpfdi")`) + + const outputContent = await page.textContent(output) + + expect(outputContent).toContain("bafkreigaknpexyvxt76zgkitavbwx6ejgfheup5oybpm77f3pxzrvwpfdi"); + expect(outputContent).toContain("https://ipfs.io/ipfs/bafkreigaknpexyvxt76zgkitavbwx6ejgfheup5oybpm77f3pxzrvwpfdi"); + expect(outputContent).toContain(fileName); + expect(outputContent).toContain(stringToUse); + }); +}); diff --git a/examples/helia-service-worker-gateway/webpack.config.js b/examples/helia-service-worker-gateway/webpack.config.js new file mode 100644 index 00000000..b175fa27 --- /dev/null +++ b/examples/helia-service-worker-gateway/webpack.config.js @@ -0,0 +1,170 @@ +import path from 'path' +import webpack from 'webpack' +import { merge } from 'webpack-merge' +import { fileURLToPath } from 'url' +import CopyWebpackPlugin from 'copy-webpack-plugin' +import NodePolyfillPlugin from 'node-polyfill-webpack-plugin' +import HtmlWebpackPlugin from 'html-webpack-plugin' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +/** + * HMR/Live Reloading broken after Webpack 5 rc.0 -> rc.1 update + * https://github.com/webpack/webpack-dev-server/issues/2758 + * + * target: 'web' for the moment under your development mode. + */ + +const paths = { + // Source files + src: path.resolve(__dirname, './src'), + + // Production build files + build: path.resolve(__dirname, './dist'), + + // Static files that get copied to build folder + public: path.resolve(__dirname, './public') +} + +const prod = { + mode: 'production', + devtool: false, + output: { + path: paths.build, + publicPath: '/', + filename: '[name].[contenthash].bundle.js' + }, + performance: { + hints: false, + maxEntrypointSize: 512000, + maxAssetSize: 512000 + }, + // fix: https://github.com/webpack/webpack-dev-server/issues/2758 + target: 'browserslist' +} + +const dev = { + // Set the mode to development or production + mode: 'development', + + // Control how source maps are generated + devtool: 'inline-source-map', + + // Where webpack outputs the assets and bundles + output: { + path: paths.build, + filename: '[name].bundle.js', + publicPath: '/' + }, + + // Spin up a server for quick development + devServer: { + historyApiFallback: true, + static: paths.build, + open: true, + compress: true, + hot: true, + port: 3000 + }, + + plugins: [ + // Only update what has changed on hot reload + new webpack.HotModuleReplacementPlugin() + ] +} + +const common = { +// Where webpack looks to start building the bundle + entry: [paths.src + '/index.js'], + + // Customize the webpack build process + plugins: [ + // Copies files from target to destination folder + new CopyWebpackPlugin({ + patterns: [ + { + from: `${paths.public}/assets`, + to: 'assets', + globOptions: { + ignore: ['*.DS_Store'] + }, + noErrorOnMissing: true + } + ] + }), + + // fixes Module not found: Error: Can't resolve 'stream' in '.../node_modules/nofilter/lib' + new NodePolyfillPlugin(), + // Note: stream-browserify has assumption about `Buffer` global in its + // dependencies causing runtime errors. This is a workaround to provide + // global `Buffer` until https://github.com/isaacs/core-util-is/issues/29 + // is fixed. + new webpack.ProvidePlugin({ + Buffer: ['buffer', 'Buffer'], + process: 'process/browser' + }), + + // Generates an HTML file from a template + // Generates deprecation warning: https://github.com/jantimon/html-webpack-plugin/issues/1501 + new HtmlWebpackPlugin({ + title: 'Helia bundle by Webpack', + favicon: paths.public + '/favicon.ico', + template: paths.public + '/index.html', // template file + filename: 'index.html', // output file, + minify: false + }) + ], + + // Determine how modules within the project are treated + module: { + rules: [ + // JavaScript: Use Babel to transpile JavaScript files + { + test: /\.js$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + options: { + presets: [ + [ + '@babel/preset-env', + { + targets: { + esmodules: true + } + } + ], + '@babel/preset-react' + ] + } + } + }, + + // Images: Copy image files to build folder + { test: /\.(?:ico|gif|png|jpg|jpeg)$/i, type: 'asset/resource' }, + + // Fonts and SVGs: Inline files + { test: /\.(woff(2)?|eot|ttf|otf|svg|)$/, type: 'asset/inline' }, + + { test: /\.(css)$/, use: ['style-loader','css-loader'] } + ] + }, + + resolve: { + modules: [paths.src, 'node_modules'], + extensions: ['.js', '.jsx', '.json'], + alias: { + '@': paths.src + } + }, + + // fix: https://github.com/webpack/webpack-dev-server/issues/2758 + target: 'web' +} + +export default (cmd) => { + const production = cmd.production + const config = production ? prod : dev + + return merge(common, config) +}