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 react from '@astrojs/react'
|
||||||
import tailwind from '@astrojs/tailwind'
|
import tailwind from '@astrojs/tailwind'
|
||||||
|
import optimizer from 'vite-plugin-optimizer'
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
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,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "tailwind.config.js",
|
"config": "tailwind.config.js",
|
||||||
"css": "app/globals.css",
|
"css": "src/styles/globals.css",
|
||||||
"baseColor": "neutral",
|
"baseColor": "neutral",
|
||||||
"cssVariables": true
|
"cssVariables": true
|
||||||
},
|
},
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
58
package.json
58
package.json
|
@ -5,29 +5,71 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"start": "astro dev",
|
"start": "astro dev",
|
||||||
"build": "astro build",
|
"prepare": "presetter bootstrap",
|
||||||
|
"build": "run build",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"astro": "astro"
|
"astro": "astro"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/react": "^3.0.3",
|
"@astrojs/react": "^3.0.3",
|
||||||
"@astrojs/tailwind": "^5.0.1",
|
"@astrojs/tailwind": "^5.0.1",
|
||||||
"@lumeweb/sdk": "^0.1.0-develop.5",
|
"@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": "^18.2.25",
|
||||||
"@types/react-dom": "^18.2.11",
|
"@types/react-dom": "^18.2.11",
|
||||||
"astro": "^3.2.3",
|
"astro": "^3.2.3",
|
||||||
"class-variance-authority": "^0.7.0",
|
"cheerio": "^1.0.0-rc.12",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"lucide-react": "^0.284.0",
|
"file-type": "^18.5.0",
|
||||||
|
"is-ipfs": "^8.0.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"tailwind-merge": "^1.14.0",
|
"tailwindcss": "^3.3.3"
|
||||||
"tailwindcss": "^3.3.3",
|
|
||||||
"tailwindcss-animate": "^1.0.7"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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": "^3.0.3",
|
||||||
"prettier-plugin-astro": "^0.12.0",
|
"prettier-plugin-astro": "^0.12.0",
|
||||||
"sass": "^1.69.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,
|
useLume,
|
||||||
LumeProvider,
|
LumeProvider,
|
||||||
} from "@lumeweb/sdk";
|
} from "@lumeweb/sdk";
|
||||||
|
|
||||||
const Lume = () => {
|
const Lume = () => {
|
||||||
const { isLoggedIn } = useLume();
|
const { isLoggedIn, ready } = useLume();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LumeIdentity>
|
<LumeIdentity>
|
||||||
<LumeIdentityTrigger asChild>
|
<LumeIdentityTrigger asChild disabled={!ready}>
|
||||||
{isLoggedIn ? (
|
{isLoggedIn ? (
|
||||||
<LumeDashboard>
|
<LumeDashboard>
|
||||||
<LumeDashboardTrigger asChild>
|
<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 "@lumeweb/sdk/lib/style.css";
|
||||||
import "@/styles/globals.scss";
|
import "@/styles/globals.scss";
|
||||||
import ArrowSvg from "@/components/Arrow";
|
|
||||||
import Lume from "../components/Lume";
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
@ -14,25 +13,10 @@ import Lume from "../components/Lume";
|
||||||
<title>Astro</title>
|
<title>Astro</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main class="flex h-screen max-h-screen flex-col items-center justify-between">
|
<main
|
||||||
<header class="relative h-14 px-2 pl-2 py-2 w-full bg-neutral-900 flex">
|
class="flex h-screen max-h-screen flex-col items-center justify-between"
|
||||||
<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"/>
|
<App client:only="react" />
|
||||||
<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>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
darkMode: ["class"],
|
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: {
|
theme: {
|
||||||
container: {
|
container: {
|
||||||
center: true,
|
center: true,
|
||||||
|
@ -68,4 +73,4 @@ module.exports = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [require("tailwindcss-animate")],
|
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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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