feat: initial port from prototype
This commit is contained in:
parent
1dcf53b92b
commit
cbb7414932
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"preset": [
|
||||
"@lumeweb/node-library-preset"
|
||||
],
|
||||
"config": {
|
||||
"tsconfig.build": {
|
||||
"include": {
|
||||
"0": "{buildSource}/backend"
|
||||
},
|
||||
"compilerOptions": {
|
||||
"outDir": "{source}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"variable": {
|
||||
"source": "build",
|
||||
"output": "dist",
|
||||
"buildSource": "src"
|
||||
},
|
||||
"scripts": {
|
||||
"build:vite": "vite build -c vite.config.backend.js",
|
||||
"build:astro": "astro build",
|
||||
"build": "run-s clean build:astro build:typescript:* build:vite",
|
||||
"clean:buildOutput": "shx rm -rf {source}",
|
||||
"build:typescript:mjs:tsc": "tsc -p tsconfig.backend.json",
|
||||
"build:typescript:mjs:alias:lib": "tsc-alias --resolve-full-paths --dir {output} -p tsconfig.backend.json",
|
||||
"build:typescript:mjs:alias:root": "tsc-alias --resolve-full-paths --dir . -p tsconfig.backend.json",
|
||||
"build:typescript:mjs:fix": "tsc-esm-fix --sourceMap --target {output} -p tsconfig.backend.json"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
node_modules/presetter/generated/app/.releaserc.json
|
|
@ -2,8 +2,17 @@ import { defineConfig } from 'astro/config'
|
|||
|
||||
import react from '@astrojs/react'
|
||||
import tailwind from '@astrojs/tailwind'
|
||||
import optimizer from 'vite-plugin-optimizer'
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [react(), tailwind({ applyBaseStyles: false, })]
|
||||
integrations: [react(), tailwind({ applyBaseStyles: false, })],
|
||||
vite: {
|
||||
plugins: [
|
||||
optimizer({
|
||||
'node-fetch':
|
||||
'const e = undefined; export default e;export {e as Response, e as FormData, e as Blob};',
|
||||
}),
|
||||
]
|
||||
}
|
||||
})
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "app/globals.css",
|
||||
"css": "src/styles/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true
|
||||
},
|
||||
|
|
File diff suppressed because it is too large
Load Diff
104
package.json
104
package.json
|
@ -1,33 +1,75 @@
|
|||
{
|
||||
"name": "app",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/react": "^3.0.3",
|
||||
"@astrojs/tailwind": "^5.0.1",
|
||||
"@lumeweb/sdk": "^0.1.0-develop.5",
|
||||
"@types/react": "^18.2.25",
|
||||
"@types/react-dom": "^18.2.11",
|
||||
"astro": "^3.2.3",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"lucide-react": "^0.284.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.0.3",
|
||||
"prettier-plugin-astro": "^0.12.0",
|
||||
"sass": "^1.69.0"
|
||||
}
|
||||
"name": "app",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"prepare": "presetter bootstrap",
|
||||
"build": "run build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/react": "^3.0.3",
|
||||
"@astrojs/tailwind": "^5.0.1",
|
||||
"@helia/unixfs": "^1.4.2",
|
||||
"@lumeweb/kernel-dns-client": "^0.1.0-develop.7",
|
||||
"@lumeweb/kernel-eth-client": "^0.1.0-develop.16",
|
||||
"@lumeweb/kernel-handshake-client": "^0.1.0-develop.8",
|
||||
"@lumeweb/kernel-ipfs-client": "^0.1.0-develop.24",
|
||||
"@lumeweb/kernel-network-registry-client": "^0.1.0-develop.10",
|
||||
"@lumeweb/kernel-peer-discovery-client": "^0.0.2-develop.16",
|
||||
"@lumeweb/kernel-swarm-client": "^0.1.0-develop.10",
|
||||
"@lumeweb/libkernel": "^0.1.0-develop.63",
|
||||
"@lumeweb/libresolver": "^0.1.0-develop.1",
|
||||
"@lumeweb/sdk": "^0.1.0-develop.14",
|
||||
"@lumeweb/tld-enum": "^0.1.0-develop.1",
|
||||
"@radix-ui/react-form": "^0.0.3",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@types/react": "^18.2.25",
|
||||
"@types/react-dom": "^18.2.11",
|
||||
"astro": "^3.2.3",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"clsx": "^2.0.0",
|
||||
"file-type": "^18.5.0",
|
||||
"is-ipfs": "^8.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"tailwindcss": "^3.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/react": "^3.0.3",
|
||||
"@astrojs/tailwind": "^5.0.2",
|
||||
"@lumeweb/kernel-dns-client": "^0.1.0-develop.7",
|
||||
"@lumeweb/kernel-eth-client": "^0.1.0-develop.16",
|
||||
"@lumeweb/kernel-handshake-client": "^0.1.0-develop.8",
|
||||
"@lumeweb/kernel-ipfs-client": "^0.1.0-develop.24",
|
||||
"@lumeweb/kernel-peer-discovery-client": "^0.0.2-develop.16",
|
||||
"@lumeweb/kernel-swarm-client": "^0.1.0-develop.10",
|
||||
"@lumeweb/libresolver": "^0.1.0-develop.1",
|
||||
"@lumeweb/node-library-preset": "^0.2.7",
|
||||
"@lumeweb/sdk": "^0.1.0-develop.14",
|
||||
"@types/react": "^18.2.28",
|
||||
"@types/react-dom": "^18.2.13",
|
||||
"astro": "^3.2.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"lucide-react": "^0.284.0",
|
||||
"presetter": "^4.4.1",
|
||||
"presetter-preset-esm": "^4.4.1",
|
||||
"presetter-preset-strict": "^4.4.1",
|
||||
"prettier": "^3.0.3",
|
||||
"prettier-plugin-astro": "^0.12.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"sass": "^1.69.2",
|
||||
"shx": "^0.3.4",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tsc-alias": "^1.8.8",
|
||||
"tsc-esm-fix": "^2.20.17",
|
||||
"typescript": "^5.2.2",
|
||||
"vite-plugin-optimizer": "^1.4.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import type { ContentFilter } from "./types.js";
|
||||
|
||||
export class ContentProcessor {
|
||||
private filters: ContentFilter[] = [];
|
||||
|
||||
registerFilter(filter: ContentFilter) {
|
||||
this.filters.push(filter);
|
||||
}
|
||||
|
||||
async process(response: Response, mimeType: string): Promise<Response> {
|
||||
let processedResponse = response;
|
||||
|
||||
for (const filter of this.filters) {
|
||||
processedResponse = await filter.process(processedResponse, mimeType);
|
||||
}
|
||||
|
||||
return processedResponse;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import type { ContentFilter } from "../types.js";
|
||||
import { getTld } from "@lumeweb/libresolver";
|
||||
import tldEnum from "@lumeweb/tld-enum";
|
||||
import * as cheerio from "cheerio";
|
||||
|
||||
export default class URLRewriteFilter implements ContentFilter {
|
||||
async process(response: Response, mimeType: string): Promise<Response> {
|
||||
if (mimeType !== "text/html") {
|
||||
return response;
|
||||
}
|
||||
|
||||
let html = await response.text();
|
||||
|
||||
const $ = cheerio.load(html);
|
||||
["a", "link", "script", "img"].forEach((tag) => {
|
||||
$.root()
|
||||
.find(tag)
|
||||
.each((index, element) => {
|
||||
let attrName = ["a", "link"].includes(tag) ? "href" : "src";
|
||||
let urlValue = $(element).attr(attrName);
|
||||
|
||||
if (urlValue) {
|
||||
if (!isICANN(urlValue)) {
|
||||
if (urlValue.startsWith("/")) {
|
||||
$(element).attr(attrName, `/browse${urlValue}`);
|
||||
} else if (urlValue.startsWith("http")) {
|
||||
$(element).attr(attrName, `/browse/${urlValue}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return new Response($.html(), {
|
||||
headers: response.headers,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function isICANN(url: string) {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
const domain = parsedUrl.hostname;
|
||||
return tldEnum.list.includes(getTld(domain));
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
const extToMimes = new Map(
|
||||
Object.entries({
|
||||
html: "text/html",
|
||||
xhtml: "application/xhtml+xml",
|
||||
xml: "application/xml",
|
||||
})
|
||||
);
|
||||
Object.freeze(extToMimes);
|
||||
|
||||
export default extToMimes;
|
|
@ -0,0 +1,38 @@
|
|||
import { ContentProcessor } from "./contentProcessor.js";
|
||||
import type { ContentProvider } from "./types.js";
|
||||
import type { DNSResult } from "@lumeweb/libresolver";
|
||||
|
||||
export class ProviderManager {
|
||||
private providers: ContentProvider[] = [];
|
||||
|
||||
private _processor = new ContentProcessor();
|
||||
|
||||
get processor(): ContentProcessor {
|
||||
return this._processor;
|
||||
}
|
||||
|
||||
register(provider: ContentProvider) {
|
||||
this.providers.push(provider);
|
||||
}
|
||||
|
||||
async fetch(dnsResult: DNSResult, path: string): Promise<Response> {
|
||||
for (const record of dnsResult.records) {
|
||||
for (const provider of this.providers) {
|
||||
if (provider.supports(record.value)) {
|
||||
const content = await provider.fetchContent(record.value, path);
|
||||
|
||||
if (content.headers.get("Content-Type")) {
|
||||
return this._processor.process(
|
||||
content,
|
||||
content.headers.get("Content-Type")!,
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("No suitable provider found.");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
import type { ContentProvider } from "../types.js";
|
||||
import { ipfsPath, ipnsPath, path as checkPath } from "is-ipfs";
|
||||
import { createClient } from "@lumeweb/kernel-ipfs-client";
|
||||
import { CID } from "multiformats/cid";
|
||||
import type { UnixFSStats } from "@helia/unixfs";
|
||||
import * as nodePath from "path";
|
||||
import { fileTypeFromBuffer } from "file-type";
|
||||
import extToMimes from "../mimes.js";
|
||||
|
||||
export default class IPFSProvider implements ContentProvider {
|
||||
private _client = createClient();
|
||||
|
||||
async fetchContent(
|
||||
uri: string,
|
||||
path: string,
|
||||
query?: string,
|
||||
): Promise<Response> {
|
||||
let cid = translatePath(uri);
|
||||
let stat: UnixFSStats | null = null;
|
||||
let urlPath = path;
|
||||
const parsedPath = nodePath.parse(urlPath);
|
||||
let err;
|
||||
try {
|
||||
if (ipnsPath(cid)) {
|
||||
const cidHash = cid.replace("/ipns/", "");
|
||||
cid = await this._client.ipns(cidHash);
|
||||
cid = `/ipfs/${cid}`;
|
||||
}
|
||||
|
||||
if (ipfsPath(cid)) {
|
||||
cid = CID.parse(cid.replace("/ipfs/", "")).toV1().toString();
|
||||
stat = await this._client.stat(cid);
|
||||
}
|
||||
} catch (e) {
|
||||
err = (e as Error).message;
|
||||
}
|
||||
|
||||
if (!err && stat?.type === "directory") {
|
||||
if (!parsedPath.base.length || !parsedPath.ext.length) {
|
||||
let found = false;
|
||||
for (const indexFile of ["index.html", "index.htm"]) {
|
||||
try {
|
||||
const subPath = nodePath.join(urlPath, indexFile);
|
||||
await this._client.stat(cid, {
|
||||
path: subPath,
|
||||
});
|
||||
urlPath = subPath;
|
||||
found = true;
|
||||
break;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
err = "404";
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await this._client.stat(cid, {
|
||||
path: urlPath,
|
||||
});
|
||||
} catch {
|
||||
err = "404";
|
||||
}
|
||||
}
|
||||
|
||||
if (err) {
|
||||
throw new Error(err);
|
||||
}
|
||||
}
|
||||
|
||||
let bufferRead = 0;
|
||||
const fileTypeBufferLength = 4100;
|
||||
const mimeBuffer: Uint8Array[] = [];
|
||||
let reader = await this._client.cat(cid, { path: urlPath });
|
||||
|
||||
for await (const chunk of reader.iterable()) {
|
||||
if (bufferRead < fileTypeBufferLength) {
|
||||
if (chunk.length >= fileTypeBufferLength) {
|
||||
mimeBuffer.push(chunk.slice(0, fileTypeBufferLength));
|
||||
bufferRead += fileTypeBufferLength;
|
||||
} else {
|
||||
mimeBuffer.push(chunk);
|
||||
bufferRead += chunk.length;
|
||||
}
|
||||
|
||||
if (bufferRead >= fileTypeBufferLength) {
|
||||
reader.abort();
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
reader.abort();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mime;
|
||||
|
||||
if (bufferRead >= fileTypeBufferLength) {
|
||||
const totalLength = mimeBuffer.reduce((acc, val) => acc + val.length, 0);
|
||||
const concatenated = new Uint8Array(totalLength);
|
||||
let offset = 0;
|
||||
for (const chunk of mimeBuffer) {
|
||||
concatenated.set(chunk, offset);
|
||||
offset += chunk.length;
|
||||
}
|
||||
mime = await fileTypeFromBuffer(concatenated);
|
||||
|
||||
if (!mime) {
|
||||
const ext = nodePath.parse(urlPath).ext.replace(".", "");
|
||||
if (extToMimes.has(ext)) {
|
||||
mime = extToMimes.get(ext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reader = await this._client.cat(cid, { path: urlPath });
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
for await (const chunk of reader.iterable()) {
|
||||
controller.enqueue(chunk);
|
||||
}
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
const headers: HeadersInit = {};
|
||||
|
||||
if (mime) {
|
||||
headers["Content-Type"] = mime as string;
|
||||
}
|
||||
|
||||
return new Response(stream, {
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
supports(uri: string): boolean {
|
||||
return checkPath(translatePath(uri));
|
||||
}
|
||||
}
|
||||
|
||||
function translatePath(uri: string) {
|
||||
return uri.replace(/:\/\//, "/").replace(/^/, "/");
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
export interface ContentProvider {
|
||||
supports: (uri: string) => boolean;
|
||||
fetchContent: (
|
||||
uri: string,
|
||||
path: string,
|
||||
query?: string,
|
||||
) => Promise<Response>;
|
||||
}
|
||||
|
||||
export interface ContentFilter {
|
||||
process: (response: Response, mineType: string) => Promise<Response>;
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
import { createClient as createDnsClient } from "@lumeweb/kernel-dns-client";
|
||||
import { ProviderManager } from "./providerManager.js";
|
||||
import IPFSProvider from "./providers/ipfs.js";
|
||||
import URLRewriteFilter from "./filters/urlRewrite.js";
|
||||
|
||||
const dnsClient = createDnsClient();
|
||||
|
||||
const providerManager = new ProviderManager();
|
||||
providerManager.register(new IPFSProvider());
|
||||
providerManager.processor.registerFilter(new URLRewriteFilter());
|
||||
|
||||
globalThis.postMessage = async (...args) => {
|
||||
// @ts-ignore
|
||||
let ret = await clients.matchAll({ includeUncontrolled: true });
|
||||
ret.forEach((item: any) => item.postMessage(...args));
|
||||
|
||||
if (!ret.length) {
|
||||
const cb = (event: any) => {
|
||||
// @ts-ignore
|
||||
postMessage(...args);
|
||||
self.removeEventListener("activate", cb);
|
||||
};
|
||||
self.addEventListener("activate", cb);
|
||||
}
|
||||
};
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
// @ts-ignore
|
||||
event.waitUntil(
|
||||
(async () => {
|
||||
// @ts-ignore
|
||||
await clients.claim();
|
||||
// @ts-ignore
|
||||
})(),
|
||||
);
|
||||
});
|
||||
|
||||
addEventListener("fetch", (event: any) => {
|
||||
event.respondWith(
|
||||
(async () => {
|
||||
const req = event.request;
|
||||
const url = new URL(req.url);
|
||||
|
||||
if (
|
||||
["/index.html", "/index.js", "/"].includes(url.pathname) ||
|
||||
!url.pathname.startsWith("/browse/")
|
||||
) {
|
||||
return fetch(event.request).then((response: any) => {
|
||||
response.redirectToFinalURL = true;
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
let realUrl = url.pathname.replace(/^\/browse\//, "").replace(/\/$/, "");
|
||||
|
||||
if (!realUrl.match(/^https?:\/\//)) {
|
||||
realUrl = `http://${realUrl}`;
|
||||
}
|
||||
// Use your existing communication framework to resolve DNS.
|
||||
const dnsResult = await dnsClient.resolve(new URL(realUrl).hostname);
|
||||
|
||||
if (!dnsResult.error && dnsResult.records.length > 0) {
|
||||
return providerManager.fetch(dnsResult, new URL(realUrl).pathname);
|
||||
}
|
||||
|
||||
return new Response("Sorry, that is not a valid web3 website.");
|
||||
})(),
|
||||
);
|
||||
});
|
|
@ -0,0 +1,25 @@
|
|||
import { createClient as createDnsClient } from "@lumeweb/kernel-dns-client";
|
||||
import { createClient as createIpfsClient } from "@lumeweb/kernel-ipfs-client";
|
||||
import { createClient as createSwarmClient } from "@lumeweb/kernel-swarm-client";
|
||||
import { createClient as createPeerDiscoveryClient } from "@lumeweb/kernel-peer-discovery-client";
|
||||
import { createClient as createNetworkRegistryClient } from "@lumeweb/kernel-network-registry-client";
|
||||
import { createClient as createHandshakeClient } from "@lumeweb/kernel-handshake-client";
|
||||
import { createClient as createEthClient } from "@lumeweb/kernel-eth-client";
|
||||
|
||||
const dnsClient = createDnsClient();
|
||||
const ipfsClient = createIpfsClient();
|
||||
const swarmClient = createSwarmClient();
|
||||
const peerDiscoveryClient = createPeerDiscoveryClient();
|
||||
const networkRegistryClient = createNetworkRegistryClient();
|
||||
const handshakeClient = createHandshakeClient();
|
||||
const ethClient = createEthClient();
|
||||
|
||||
export {
|
||||
dnsClient,
|
||||
ipfsClient,
|
||||
swarmClient,
|
||||
peerDiscoveryClient,
|
||||
networkRegistryClient,
|
||||
handshakeClient,
|
||||
ethClient,
|
||||
};
|
|
@ -0,0 +1,24 @@
|
|||
import {
|
||||
Browser,
|
||||
BrowserStateProvider,
|
||||
Navigator,
|
||||
} from "@/components/Browser.tsx";
|
||||
import Lume from "@/components/Lume.tsx";
|
||||
|
||||
export default function () {
|
||||
return (
|
||||
<BrowserStateProvider>
|
||||
<>
|
||||
<header className="relative h-14 px-2 pl-2 py-2 w-full bg-neutral-900 flex">
|
||||
<div className="relative h-full w-full rounded-full bg-neutral-800 border border-neutral-700 flex items-center [>input:focus]:ring-2 [>input:focus]:ring-white">
|
||||
<Navigator />
|
||||
</div>
|
||||
<div className="w-32 flex justify-end">
|
||||
<Lume />
|
||||
</div>
|
||||
</header>
|
||||
<Browser />
|
||||
</>
|
||||
</BrowserStateProvider>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
import {
|
||||
createContext,
|
||||
createRef,
|
||||
forwardRef,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
dnsClient,
|
||||
ethClient,
|
||||
handshakeClient,
|
||||
ipfsClient,
|
||||
networkRegistryClient,
|
||||
peerDiscoveryClient,
|
||||
swarmClient,
|
||||
} from "@/clients.ts";
|
||||
import * as kernel from "@lumeweb/libkernel/kernel";
|
||||
import { kernelLoaded } from "@lumeweb/libkernel/kernel";
|
||||
import Arrow from "@/components/Arrow.tsx";
|
||||
import type React from "react";
|
||||
import { Input } from "@/components/ui/input.tsx";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
|
||||
let BOOT_FUNCTIONS: (() => Promise<any>)[] = [];
|
||||
|
||||
interface BrowserContextType {
|
||||
url: string;
|
||||
setUrl: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
const BrowserStateContext = createContext<BrowserContextType | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export function BrowserStateProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactElement;
|
||||
}) {
|
||||
const [url, setUrl] = useState("about:blank");
|
||||
|
||||
return (
|
||||
<BrowserStateContext.Provider value={{ url, setUrl }}>
|
||||
{children}
|
||||
</BrowserStateContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useBrowserState() {
|
||||
const context = useContext(BrowserStateContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useBrowserState must be used within a BrowserStateProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
async function boot() {
|
||||
const reg = await navigator.serviceWorker.register("/sw.js");
|
||||
await reg.update();
|
||||
|
||||
await kernel.serviceWorkerReady();
|
||||
await kernelLoaded();
|
||||
|
||||
BOOT_FUNCTIONS.push(
|
||||
async () =>
|
||||
await swarmClient.addRelay(
|
||||
"2d7ae1517caf4aae4de73c6d6f400765d2dd00b69d65277a29151437ef1c7d1d",
|
||||
),
|
||||
);
|
||||
|
||||
// IRC
|
||||
BOOT_FUNCTIONS.push(
|
||||
async () =>
|
||||
await peerDiscoveryClient.register(
|
||||
"zdiN5eJ3RfHpZHTYorGxBt1GCsrGJYV9GprwVWkj8snGsjWSrptFm8BtQX",
|
||||
),
|
||||
);
|
||||
BOOT_FUNCTIONS.push(
|
||||
async () => await networkRegistryClient.registerType("content"),
|
||||
);
|
||||
BOOT_FUNCTIONS.push(
|
||||
async () => await networkRegistryClient.registerType("blockchain"),
|
||||
);
|
||||
BOOT_FUNCTIONS.push(async () => await handshakeClient.register());
|
||||
BOOT_FUNCTIONS.push(async () => await ethClient.register());
|
||||
BOOT_FUNCTIONS.push(async () => await ipfsClient.register());
|
||||
|
||||
const resolvers = [
|
||||
"zdiJdDdBJWAdYFTcRa9So5TQQ9f1pYMiMy4dqYcKp9imomQtR11LJUyJyV", // CID
|
||||
"zdiKvnZYNjDqXaM8uF3pGEs7Tt6jqGc7t7M4eqbvJwpkTnrZymncfUW9Cj", // ENS
|
||||
"zrjEH3iojPLr7986o7iCn9THBmJmHiuDWmS1G6oT8DnfuFM", // HNS
|
||||
];
|
||||
|
||||
for (const resolver of resolvers) {
|
||||
BOOT_FUNCTIONS.push(async () => dnsClient.registerResolver(resolver));
|
||||
}
|
||||
|
||||
await bootup();
|
||||
|
||||
await Promise.all([
|
||||
ethClient.ready(),
|
||||
handshakeClient.ready(),
|
||||
ipfsClient.ready(),
|
||||
]);
|
||||
}
|
||||
|
||||
async function bootup() {
|
||||
for (const entry of Object.entries(BOOT_FUNCTIONS)) {
|
||||
await entry[1]();
|
||||
}
|
||||
}
|
||||
|
||||
export function Navigator() {
|
||||
const { url, setUrl } = useBrowserState();
|
||||
const inputRef = createRef<HTMLInputElement>();
|
||||
|
||||
const browse = () => {
|
||||
let input = inputRef.current?.value.trim();
|
||||
|
||||
// If the input doesn't contain a protocol, assume it's http
|
||||
if (!input?.match(/^https?:\/\//)) {
|
||||
input = `http://${input}`;
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to parse it as a URL
|
||||
const url = new URL(input);
|
||||
|
||||
setUrl(
|
||||
`/browse/${url.hostname}${url.pathname}${url.search}${url.hash}` ||
|
||||
"about:blank",
|
||||
);
|
||||
} catch (e) {
|
||||
// Handle invalid URLs here, if needed
|
||||
console.error("Invalid URL:", e);
|
||||
}
|
||||
};
|
||||
|
||||
const NavInput = forwardRef((props: any, ref) => (
|
||||
<Input ref={ref} {...props}></Input>
|
||||
));
|
||||
|
||||
return (
|
||||
<>
|
||||
<NavInput ref={inputRef} />
|
||||
<Button onClick={browse}>
|
||||
Navigate
|
||||
<Arrow />
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function Browser() {
|
||||
const { url } = useBrowserState();
|
||||
|
||||
useEffect(() => {
|
||||
boot();
|
||||
}, []);
|
||||
|
||||
return <iframe src={url} className="w-full h-full"></iframe>;
|
||||
}
|
|
@ -6,13 +6,12 @@ import {
|
|||
useLume,
|
||||
LumeProvider,
|
||||
} from "@lumeweb/sdk";
|
||||
|
||||
const Lume = () => {
|
||||
const { isLoggedIn } = useLume();
|
||||
const { isLoggedIn, ready } = useLume();
|
||||
return (
|
||||
<>
|
||||
<LumeIdentity>
|
||||
<LumeIdentityTrigger asChild>
|
||||
<LumeIdentityTrigger asChild disabled={!ready}>
|
||||
{isLoggedIn ? (
|
||||
<LumeDashboard>
|
||||
<LumeDashboardTrigger asChild>
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
|
@ -0,0 +1,25 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
|
@ -0,0 +1,6 @@
|
|||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
|
@ -1,8 +1,7 @@
|
|||
---
|
||||
import App from "../components/App";
|
||||
import "@lumeweb/sdk/lib/style.css";
|
||||
import "@/styles/globals.scss";
|
||||
import ArrowSvg from "@/components/Arrow";
|
||||
import Lume from "../components/Lume";
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
|
@ -14,25 +13,10 @@ import Lume from "../components/Lume";
|
|||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
<main class="flex h-screen max-h-screen flex-col items-center justify-between">
|
||||
<header class="relative h-14 px-2 pl-2 py-2 w-full bg-neutral-900 flex">
|
||||
<div
|
||||
class="relative h-full w-full rounded-full bg-neutral-800 border border-neutral-700 flex items-center [>input:focus]:ring-2 [>input:focus]:ring-white"
|
||||
>
|
||||
<input class="ml-3 text-gray-300 w-[calc(100%-150px)] h-full bg-transparent focus:ring-0 focus:outline-none focus:border-0"/>
|
||||
<button
|
||||
class="absolute bg-neutral-700 text-neutral-400 px-4 py-2 right-0 rounded-r-full"
|
||||
>
|
||||
Navigate
|
||||
<ArrowSvg className="inline-block ml-2 -mt-1 w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="w-32 flex justify-end">
|
||||
<Lume client:only="react" />
|
||||
</div>
|
||||
</header>
|
||||
<iframe id="mainIframe" src="https://google.com" class="w-full h-full">
|
||||
</iframe>
|
||||
<main
|
||||
class="flex h-screen max-h-screen flex-col items-center justify-between"
|
||||
>
|
||||
<App client:only="react" />
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: ["class"],
|
||||
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
|
||||
content: [
|
||||
'./pages/**/*.{ts,tsx}',
|
||||
'./components/**/*.{ts,tsx}',
|
||||
'./app/**/*.{ts,tsx}',
|
||||
'./src/**/*.{ts,tsx}',
|
||||
],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
|
@ -68,4 +73,4 @@ module.exports = {
|
|||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
};
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "build",
|
||||
"module": "NodeNext",
|
||||
"target": "ES2022",
|
||||
"declaration": true,
|
||||
"inlineSourceMap": true,
|
||||
"noImplicitUseStrict": false,
|
||||
"preserveConstEnums": true,
|
||||
"removeComments": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"isolatedModules": false,
|
||||
"alwaysStrict": true,
|
||||
"noImplicitAny": false,
|
||||
"noImplicitThis": false,
|
||||
"strictBindCallApply": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictNullChecks": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"allowUnreachableCode": false,
|
||||
"allowUnusedLabels": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "nodenext",
|
||||
"resolveJsonModule": true,
|
||||
"baseUrl": ".",
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"ES2021",
|
||||
"dom"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/backend"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules",
|
||||
"lib"
|
||||
]
|
||||
}
|
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "react",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "react",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import { defineConfig } from "vite";
|
||||
import optimizer from "vite-plugin-optimizer";
|
||||
import { nodePolyfills } from "vite-plugin-node-polyfills";
|
||||
|
||||
export default defineConfig({
|
||||
define: { "window.": "globalThis." },
|
||||
build: {
|
||||
emptyOutDir: false,
|
||||
outDir: "dist",
|
||||
lib: {
|
||||
entry: "build/worker.js",
|
||||
name: "sw",
|
||||
formats: ["cjs"],
|
||||
fileName: () => "sw.js",
|
||||
},
|
||||
minify: false,
|
||||
rollupOptions: { output: { inlineDynamicImports: true } },
|
||||
},
|
||||
resolve: {
|
||||
dedupe: ["@lumeweb/libportal", "@lumeweb/libweb", "@lumeweb/libkernel"],
|
||||
},
|
||||
plugins: [
|
||||
optimizer({
|
||||
"node-fetch":
|
||||
"const e = undefined; export default e;export {e as Response, e as FormData, e as Blob};",
|
||||
}),
|
||||
nodePolyfills({
|
||||
exclude: ["fs"],
|
||||
globals: { Buffer: true, global: true, process: true },
|
||||
}),
|
||||
],
|
||||
});
|
Loading…
Reference in New Issue