diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77c956f2..ea0266b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,7 @@ jobs: - helia-nestjs - helia-nextjs - helia-parcel + - helia-remix - helia-remote-pinning - helia-script-tag - helia-ts-node @@ -94,9 +95,10 @@ jobs: - helia-jest - helia-jest-typescript - helia-nestjs + - helia-nextjs - helia-parcel + - helia-remix - helia-remote-pinning - - helia-nextjs - helia-script-tag - helia-ts-node - helia-typescript diff --git a/examples/helia-remix/.eslintrc.cjs b/examples/helia-remix/.eslintrc.cjs new file mode 100644 index 00000000..d438a591 --- /dev/null +++ b/examples/helia-remix/.eslintrc.cjs @@ -0,0 +1,85 @@ +/** + * This is intended to be a basic starting point for linting in your app. + * It relies on recommended configs out of the box for simplicity, but you can + * and should modify this configuration to best suit your team's needs. + */ + +/** @type {import('eslint').Linter.Config} */ +module.exports = { + root: true, + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + ecmaFeatures: { + jsx: true, + }, + }, + env: { + browser: true, + commonjs: true, + es6: true, + }, + ignorePatterns: ["!**/.server", "!**/.client"], + + // Base config + extends: ["eslint:recommended", "ipfs"], + + overrides: [ + // React + { + files: ["**/*.{js,jsx,ts,tsx}"], + plugins: ["react", "jsx-a11y"], + extends: [ + "plugin:react/recommended", + "plugin:react/jsx-runtime", + "plugin:react-hooks/recommended", + "plugin:jsx-a11y/recommended", + ], + settings: { + react: { + version: "detect", + }, + formComponents: ["Form"], + linkComponents: [ + { name: "Link", linkAttribute: "to" }, + { name: "NavLink", linkAttribute: "to" }, + ], + "import/resolver": { + typescript: {}, + }, + }, + }, + + // Typescript + { + files: ["**/*.{ts,tsx}"], + plugins: ["@typescript-eslint", "import"], + parser: "@typescript-eslint/parser", + settings: { + "import/internal-regex": "^~/", + "import/resolver": { + node: { + extensions: [".ts", ".tsx"], + }, + typescript: { + alwaysTryTypes: true, + }, + }, + }, + extends: [ + "plugin:@typescript-eslint/recommended", + "plugin:import/recommended", + "plugin:import/typescript", + "ipfs" + ], + }, + + // Node + { + files: [".eslintrc.cjs"], + env: { + node: true, + }, + }, + ], +}; diff --git a/examples/helia-remix/.gitignore b/examples/helia-remix/.gitignore new file mode 100644 index 00000000..80ec311f --- /dev/null +++ b/examples/helia-remix/.gitignore @@ -0,0 +1,5 @@ +node_modules + +/.cache +/build +.env diff --git a/examples/helia-remix/README.md b/examples/helia-remix/README.md new file mode 100644 index 00000000..f0252fac --- /dev/null +++ b/examples/helia-remix/README.md @@ -0,0 +1,114 @@ +

+ + Helia logo + +

+ +

Using Helia inside a Remix app

+ +

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

+ +## Table of Contents + +See https://github.com/ipfs-examples/helia-examples#table-of-contents for more information about Helia examples. + +## Getting Started + +### Prerequisites + +https://github.com/ipfs-examples/helia-examples#prerequisites + +### Installation and Running example + +```console +> npm install +> npm run dev +``` + +Now open your browser at `http://localhost:5173/` + +### Available Scripts + +In the project directory, you can run: + +#### `npm run dev` + +Runs the app in the development mode.
+Open [http://localhost:5173/](http://localhost:5173/) to view it in the browser. + +The page will reload if you make edits.
+You will also see any lint errors in the console. + +#### `npm run build` + +Builds the app for production to the `dist` folder.
+It correctly bundles in production mode and optimizes the build for the best performance. + +## Usage + +This is a [Remix.js](https://remix.run/) project bootstrapped with [`create-remix@latest`](https://github.com/remix-run/remix/blob/main/packages/create-remix) integrated with `helia`. + +You can start editing the page by modifying `app/routes/_index.tsx`. The page auto-updates as you edit the file. + +### Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Remix Documentation](https://remix.run/docs/en/main) - learn about Remix features and API. +- [Learn Remix](https://remix.run/docs/en/main/start/tutorial) - an interactive Remix tutorial. + +You can check out [the Remix GitHub repository](https://github.com/remix-run/remix) - your feedback and contributions are welcome! + +### Deploy on Vercel + +The easiest way to deploy your Remix app is to use the [Vercel Platform](https://vercel.com/guides/deploying-remix-with-vercel). + +Check out our [Remix deployment documentation](https://vercel.com/docs/frameworks/remix) for more details. + +_For more examples, please refer to the [Documentation](https://github.com/ipfs-examples/helia-examples#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 + diff --git a/examples/helia-remix/__mocks__/helia.ts b/examples/helia-remix/__mocks__/helia.ts new file mode 100644 index 00000000..689743d6 --- /dev/null +++ b/examples/helia-remix/__mocks__/helia.ts @@ -0,0 +1,6 @@ +export const createHelia = jest.fn().mockResolvedValue({ + libp2p: { + peerId: { toString: () => 'mock-peer-id' }, + status: 'started' + } + }); \ No newline at end of file diff --git a/examples/helia-remix/__tests__/HeliaComponents.test.tsx b/examples/helia-remix/__tests__/HeliaComponents.test.tsx new file mode 100644 index 00000000..df339123 --- /dev/null +++ b/examples/helia-remix/__tests__/HeliaComponents.test.tsx @@ -0,0 +1,24 @@ +import { initHelia } from "../components/helia"; + +describe('initialize Helia', () => { + it('checks if HeliaNode has started', async () => { + + const { heliaNode } = await initHelia(); + + expect(heliaNode.libp2p.status).toEqual('started'); + }) + + it('checks if there is a nodeId', async () => { + + const { nodeId } = await initHelia(); + + expect(nodeId).toEqual('mock-peer-id'); + }) + + it('checks if nodeId is online', async () => { + + const { nodeIsOnline } = await initHelia(); + + expect(nodeIsOnline).toBe(true); + }) +}) \ No newline at end of file diff --git a/examples/helia-remix/app/root.tsx b/examples/helia-remix/app/root.tsx new file mode 100644 index 00000000..61c8b983 --- /dev/null +++ b/examples/helia-remix/app/root.tsx @@ -0,0 +1,45 @@ +import { + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from "@remix-run/react"; +import type { LinksFunction } from "@remix-run/node"; + +import "./tailwind.css"; + +export const links: LinksFunction = () => [ + { rel: "preconnect", href: "https://fonts.googleapis.com" }, + { + rel: "preconnect", + href: "https://fonts.gstatic.com", + crossOrigin: "anonymous", + }, + { + rel: "stylesheet", + href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", + }, +]; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ); +} + +export default function App() { + return ; +} diff --git a/examples/helia-remix/app/routes/_index.tsx b/examples/helia-remix/app/routes/_index.tsx new file mode 100644 index 00000000..47f6dae9 --- /dev/null +++ b/examples/helia-remix/app/routes/_index.tsx @@ -0,0 +1,169 @@ +import type { MetaFunction } from "@remix-run/node"; +import HeliaComponent from "../../components/helia"; + +export const meta: MetaFunction = () => { + return [ + { title: "New Helia Remix App" }, + { name: "description", content: "Welcome to Helia Remix!" }, + ]; +}; + +export default function Index() { + return ( +
+
+
+

+ Welcome to Helia Remix +

+
+ Remix + Remix +
+
+ + + + +
+
+ ); +} + +const resources = [ + { + href: "https://remix.run/start/quickstart", + text: "Quick Start (5 min)", + icon: ( + + + + ), + }, + { + href: "https://remix.run/start/tutorial", + text: "Tutorial (30 min)", + icon: ( + + + + ), + }, + { + href: "https://remix.run/docs", + text: "Remix Docs", + icon: ( + + + + ), + }, + { + href: "https://rmx.as/discord", + text: "Join Discord", + icon: ( + + + + ), + }, +]; diff --git a/examples/helia-remix/app/tailwind.css b/examples/helia-remix/app/tailwind.css new file mode 100644 index 00000000..303fe158 --- /dev/null +++ b/examples/helia-remix/app/tailwind.css @@ -0,0 +1,12 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, +body { + @apply bg-white dark:bg-gray-950; + + @media (prefers-color-scheme: dark) { + color-scheme: dark; + } +} diff --git a/examples/helia-remix/components/helia.tsx b/examples/helia-remix/components/helia.tsx new file mode 100644 index 00000000..20e952ea --- /dev/null +++ b/examples/helia-remix/components/helia.tsx @@ -0,0 +1,45 @@ +import React, { useState, useEffect } from 'react' +import { createHelia } from 'helia' + +export const initHelia = async () => { + + const heliaNode = await createHelia() + + const nodeId = heliaNode.libp2p.peerId.toString() + const nodeIsOnline = heliaNode.libp2p.status === 'started' + + return { heliaNode, nodeId, nodeIsOnline } +} + +const HeliaComponent: React.FC = () => { + const [id, setId] = useState(null) + //@ts-expect-error : Type 'null' is not assignable to type 'HeliaNode'. + const [helia, setHelia] = useState(null) + const [isOnline, setIsOnline] = useState(false) + + useEffect(() => { + if (helia != null) return + + const initHeliaNode = async () => { + const { heliaNode, nodeId, nodeIsOnline } = await initHelia() + setId(nodeId); + setHelia(heliaNode); + setIsOnline(nodeIsOnline); + } + + initHeliaNode() + }, [helia]) + + if (helia == null || id == null) { + return

Starting Helia...

+ } + + return ( +
+

ID: {id}

+

Status: {isOnline ? 'Online' : 'Offline'}

+
+ ) +} + +export default HeliaComponent diff --git a/examples/helia-remix/jest.setup.ts b/examples/helia-remix/jest.setup.ts new file mode 100644 index 00000000..331666ce --- /dev/null +++ b/examples/helia-remix/jest.setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; \ No newline at end of file diff --git a/examples/helia-remix/package.json b/examples/helia-remix/package.json new file mode 100644 index 00000000..74bedc33 --- /dev/null +++ b/examples/helia-remix/package.json @@ -0,0 +1,77 @@ +{ + "name": "my-remix-project", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "build": "remix vite:build", + "dev": "remix vite:dev", + "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", + "start": "remix-serve ./build/server/index.js", + "typecheck": "tsc", + "test": "jest", + "test:watch": "jest --watch" + }, + "eslintConfig": { + "extends": "ipfs" + }, + "dependencies": { + "@remix-run/node": "^2.14.0", + "@remix-run/react": "^2.14.0", + "@remix-run/serve": "^2.14.0", + "helia": "^5.1.1", + "isbot": "^4.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "vitest": "^3.0.5" + }, + "devDependencies": { + "@remix-run/dev": "^2.14.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.2.0", + "@types/jest": "^29.5.14", + "@types/react": "^18.2.20", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.7.4", + "@typescript-eslint/parser": "^6.7.4", + "autoprefixer": "^10.4.19", + "eslint": "^8.38.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "identity-obj-proxy": "^3.0.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "postcss": "^8.4.38", + "tailwindcss": "^3.4.4", + "ts-jest": "^29.2.5", + "typescript": "^5.1.6", + "vite": "^5.1.0", + "vite-tsconfig-paths": "^4.2.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "jest": { + "testEnvironment": "jsdom", + "transform": { + "^.+\\.(ts|tsx)$": "ts-jest" + }, + "moduleNameMapper": { + "\\.(css|less|scss|sass)$": "identity-obj-proxy", + "^helia$": "/node_modules/helia" + }, + "setupFilesAfterEnv": [ + "/jest.setup.ts" + ], + "moduleDirectories": [ + "node_modules" + ], + "testMatch": [ + "**/__tests__/**/*.[jt]s?(x)", + "**/?(*.)+(spec|test).[jt]s?(x)" + ] + } +} diff --git a/examples/helia-remix/postcss.config.js b/examples/helia-remix/postcss.config.js new file mode 100644 index 00000000..2aa7205d --- /dev/null +++ b/examples/helia-remix/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/examples/helia-remix/public/favicon.ico b/examples/helia-remix/public/favicon.ico new file mode 100644 index 00000000..3580aaf4 Binary files /dev/null and b/examples/helia-remix/public/favicon.ico differ diff --git a/examples/helia-remix/public/logo-dark.png b/examples/helia-remix/public/logo-dark.png new file mode 100644 index 00000000..b24c7aee Binary files /dev/null and b/examples/helia-remix/public/logo-dark.png differ diff --git a/examples/helia-remix/public/logo-light.png b/examples/helia-remix/public/logo-light.png new file mode 100644 index 00000000..4490ae79 Binary files /dev/null and b/examples/helia-remix/public/logo-light.png differ diff --git a/examples/helia-remix/tailwind.config.ts b/examples/helia-remix/tailwind.config.ts new file mode 100644 index 00000000..5f06ad4b --- /dev/null +++ b/examples/helia-remix/tailwind.config.ts @@ -0,0 +1,22 @@ +import type { Config } from "tailwindcss"; + +export default { + content: ["./app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}"], + theme: { + extend: { + fontFamily: { + sans: [ + "Inter", + "ui-sans-serif", + "system-ui", + "sans-serif", + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol", + "Noto Color Emoji", + ], + }, + }, + }, + plugins: [], +} satisfies Config; diff --git a/examples/helia-remix/tsconfig.json b/examples/helia-remix/tsconfig.json new file mode 100644 index 00000000..759deabf --- /dev/null +++ b/examples/helia-remix/tsconfig.json @@ -0,0 +1,33 @@ +{ + "include": [ + "**/*.ts", + "**/*.tsx", + "**/.server/**/*.ts", + "**/.server/**/*.tsx", + "**/.client/**/*.ts", + "**/.client/**/*.tsx", + "**/__tests__/**/*" + ], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["@remix-run/node", "vite/client", "jest", "@testing-library/jest-dom", "node"], + "isolatedModules": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "target": "ES2022", + "strict": true, + "allowJs": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, + + // Vite takes care of building everything, not tsc. + "noEmit": true + } +} diff --git a/examples/helia-remix/vite.config.ts b/examples/helia-remix/vite.config.ts new file mode 100644 index 00000000..e4e8cefc --- /dev/null +++ b/examples/helia-remix/vite.config.ts @@ -0,0 +1,24 @@ +import { vitePlugin as remix } from "@remix-run/dev"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +declare module "@remix-run/node" { + interface Future { + v3_singleFetch: true; + } +} + +export default defineConfig({ + plugins: [ + remix({ + future: { + v3_fetcherPersist: true, + v3_relativeSplatPath: true, + v3_throwAbortReason: true, + v3_singleFetch: true, + v3_lazyRouteDiscovery: true, + }, + }), + tsconfigPaths(), + ], +});