mirror of
https://github.com/tlsnotary/tlsn-js.git
synced 2026-04-02 03:00:18 -04:00
feat: Playground for P2P demo using websocket and streams (#76)
This commit is contained in:
1
demo/web-to-web-p2p/.gitignore
vendored
Normal file
1
demo/web-to-web-p2p/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
package-lock.json
|
||||
24
demo/web-to-web-p2p/README.md
Normal file
24
demo/web-to-web-p2p/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Web-to-Web P2P Demo
|
||||
|
||||
This project demonstrates a peer-to-peer (P2P) communication between two web clients using TLSNotary.
|
||||
The web prover will get data from <https://swapi.dev> and prove it to the web verifier.
|
||||
|
||||
In this demo, the two web clients run in the same browser page (`./src/app.tsx`) and communicate via a simple websocket server (`./server/index.js`)
|
||||
|
||||
## Run the demo
|
||||
|
||||
1. Run the demo:
|
||||
```
|
||||
npm i
|
||||
npm run dev
|
||||
```
|
||||
2. Open <http://localhost:3456/>
|
||||
3. Click the **Start Demo** button
|
||||
|
||||
The Prover window logs the Prover's output, the Verifier logs the Verifier's output. In the console view you can see the websocket log.
|
||||
You can also open the Browser developer tools (F12) to see more TLSNotary protocol logs.
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `src/`: Contains the source code for the demo.
|
||||
- `server/`: Contains the WebSocket server code.
|
||||
16
demo/web-to-web-p2p/index.ejs
Normal file
16
demo/web-to-web-p2p/index.ejs
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>React/Typescrip Example</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script>
|
||||
</script>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
48
demo/web-to-web-p2p/package.json
Normal file
48
demo/web-to-web-p2p/package.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "web-to-web-p2p",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "webpack.js",
|
||||
"scripts": {
|
||||
"dev:server": "node ./server/index.js",
|
||||
"dev:ui": "webpack-dev-server --config webpack.js",
|
||||
"dev": "concurrently npm:dev:ui npm:dev:server",
|
||||
"build": "webpack --config webpack.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"comlink": "^4.4.1",
|
||||
"concurrently": "^9.1.0",
|
||||
"express": "^4.21.1",
|
||||
"qs": "^6.13.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-loader-spinner": "^6.1.6",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"tlsn-js": "0.1.0-alpha.7.1",
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"babel-loader": "^9.1.3",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"css-loader": "^6.7.3",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"postcss-loader": "^7.3.3",
|
||||
"postcss-preset-env": "^9.1.1",
|
||||
"sass": "^1.57.1",
|
||||
"sass-loader": "^13.2.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"source-map-loader": "^5.0.0",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^4.9.4",
|
||||
"vm-browserify": "^1.1.2",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-dev-server": "^4.11.1"
|
||||
}
|
||||
}
|
||||
3439
demo/web-to-web-p2p/pnpm-lock.yaml
generated
Normal file
3439
demo/web-to-web-p2p/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
4
demo/web-to-web-p2p/postcss.config.js
Normal file
4
demo/web-to-web-p2p/postcss.config.js
Normal file
@@ -0,0 +1,4 @@
|
||||
const tailwindcss = require("tailwindcss");
|
||||
module.exports = {
|
||||
plugins: ["postcss-preset-env", tailwindcss],
|
||||
};
|
||||
27
demo/web-to-web-p2p/server/index.js
Normal file
27
demo/web-to-web-p2p/server/index.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// This file runs a WebSocket server which enables a web prover and web verifier to connect to each other
|
||||
|
||||
const express = require('express');
|
||||
const { createServer } = require('http');
|
||||
const { WebSocketServer } = require('ws');
|
||||
const qs = require('qs');
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3001;
|
||||
const server = createServer(app);
|
||||
|
||||
const wss = new WebSocketServer({ server });
|
||||
|
||||
const clients = new Map();
|
||||
wss.on('connection', async (client, request) => {
|
||||
const query = qs.parse((request.url || '').replace(/\/\?/g, ''));
|
||||
const id = query.id;
|
||||
clients.set(id, client);
|
||||
client.on('message', (data) => {
|
||||
const target = id === 'prover' ? 'verifier' : 'prover';
|
||||
console.log(target, data.length);
|
||||
clients.get(target).send(data);
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(port, () => {
|
||||
console.log(`ws server listening on port ${port}`);
|
||||
});
|
||||
3
demo/web-to-web-p2p/src/app.scss
Normal file
3
demo/web-to-web-p2p/src/app.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
272
demo/web-to-web-p2p/src/app.tsx
Normal file
272
demo/web-to-web-p2p/src/app.tsx
Normal file
@@ -0,0 +1,272 @@
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Watch } from 'react-loader-spinner';
|
||||
import * as Comlink from 'comlink';
|
||||
import {
|
||||
Prover as TProver,
|
||||
Verifier as TVerifier,
|
||||
Commit,
|
||||
Transcript,
|
||||
} from 'tlsn-js';
|
||||
import './app.scss';
|
||||
import WebSocketStream from './stream';
|
||||
|
||||
const { init, Prover, Verifier }: any = Comlink.wrap(
|
||||
new Worker(new URL('./worker.ts', import.meta.url)),
|
||||
);
|
||||
|
||||
const container = document.getElementById('root');
|
||||
const root = createRoot(container!);
|
||||
|
||||
root.render(<App />);
|
||||
|
||||
let proverLogs: string[] = [];
|
||||
let verifierLogs: string[] = [];
|
||||
|
||||
const p2pProxyUrl = 'ws://localhost:3001';
|
||||
const serverDns = 'swapi.dev';
|
||||
const webSocketProxy = `wss://notary.pse.dev/proxy?token=${serverDns}`;
|
||||
const requestUrl = `https://swapi.dev/api/people/1`;
|
||||
|
||||
function App(): ReactElement {
|
||||
const [ready, setReady] = useState(false);
|
||||
const [proverMessages, setProverMessages] = useState<string[]>([]);
|
||||
const [verifierMessages, setVerifierMessages] = useState<string[]>([]);
|
||||
const [started, setStarted] = useState(false);
|
||||
|
||||
// Initialize TLSNotary
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await init({ loggingLevel: 'Debug' });
|
||||
setReady(true);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
// Set up streams for prover and verifier
|
||||
// This is just for demo purposes. In the future we want to pass in the stream to the
|
||||
// prover instead of using the websocket url.
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
(async () => {
|
||||
const proverStream = new WebSocketStream(`${p2pProxyUrl}?id=prover`);
|
||||
const reader = await proverStream.reader();
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
console.log('stream finished');
|
||||
break;
|
||||
}
|
||||
console.log(`Received data from stream:`, await value.text());
|
||||
}
|
||||
})();
|
||||
|
||||
// Set up stream for verifier
|
||||
(async () => {
|
||||
const verifierStream = new WebSocketStream(
|
||||
`${p2pProxyUrl}?id=verifier`,
|
||||
);
|
||||
const writer = await verifierStream.writer();
|
||||
writer.write('Hello');
|
||||
writer.write('World!');
|
||||
writer.close();
|
||||
})();
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const addProverLog = useCallback((log: string) => {
|
||||
proverLogs = proverLogs.concat(
|
||||
`${new Date().toLocaleTimeString()} - ${log}`,
|
||||
);
|
||||
setProverMessages(proverLogs);
|
||||
}, []);
|
||||
|
||||
const addVerifierLog = useCallback((log: string) => {
|
||||
verifierLogs = verifierLogs.concat(
|
||||
`${new Date().toLocaleTimeString()} - ${log}`,
|
||||
);
|
||||
setVerifierMessages(verifierLogs);
|
||||
}, []);
|
||||
|
||||
const start = useCallback(async () => {
|
||||
if (!ready) return;
|
||||
setStarted(true);
|
||||
addProverLog('Instantiate Prover class');
|
||||
const prover: TProver = await new Prover({
|
||||
serverDns: serverDns,
|
||||
});
|
||||
addProverLog('Prover class instantiated');
|
||||
|
||||
addVerifierLog('Instantiate Verifier class');
|
||||
const verifier: TVerifier = await new Verifier({});
|
||||
addVerifierLog('Verifier class instantiated');
|
||||
|
||||
addVerifierLog('Connect verifier to p2p proxy');
|
||||
// TODO tlsn-wasm: we want to pass in the stream here instead of the websocket url
|
||||
// The stream is both readable and writable (duplex)
|
||||
try {
|
||||
await verifier.connect(`${p2pProxyUrl}?id=verifier`);
|
||||
} catch (e: any) {
|
||||
addVerifierLog('Error connecting verifier to p2p proxy');
|
||||
addVerifierLog(e.message);
|
||||
return;
|
||||
}
|
||||
addVerifierLog('Verifier connected to p2p proxy');
|
||||
|
||||
addProverLog('Set up prover and connect to p2p proxy');
|
||||
// TODO: we also want to pass in the stream here
|
||||
const proverSetup = prover.setup(`${p2pProxyUrl}?id=prover`);
|
||||
addProverLog('Prover connected to p2p proxy');
|
||||
|
||||
// Wait for prover to finish setting up websocket
|
||||
// TODO: Make the setup better and avoid this wait
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
|
||||
addVerifierLog('Start verifier');
|
||||
// This needs to be called before we send the request
|
||||
// This starts the verifier and makes it wait for the prover to send the request
|
||||
const verified = verifier.verify();
|
||||
|
||||
await proverSetup;
|
||||
addProverLog('Finished prover setup');
|
||||
|
||||
addProverLog('Send request');
|
||||
try {
|
||||
await prover.sendRequest(webSocketProxy, {
|
||||
url: requestUrl,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: {
|
||||
hello: 'world',
|
||||
one: 1,
|
||||
},
|
||||
});
|
||||
} catch (e: any) {
|
||||
addProverLog(`Error sending request to ${requestUrl}`);
|
||||
addProverLog(e.message);
|
||||
return;
|
||||
}
|
||||
addProverLog('Request sent');
|
||||
const transcript = await prover.transcript();
|
||||
|
||||
addProverLog('Response received');
|
||||
addProverLog('Transcript sent');
|
||||
addProverLog(transcript.sent);
|
||||
addProverLog('Transcript received');
|
||||
addProverLog(transcript.recv);
|
||||
|
||||
addProverLog('Revealing data to verifier');
|
||||
// Prover only reveals parts the transcript to the verifier
|
||||
const commit: Commit = {
|
||||
sent: [
|
||||
transcript.ranges.sent.info!,
|
||||
transcript.ranges.sent.headers!['content-type'],
|
||||
transcript.ranges.sent.headers!['host'],
|
||||
...transcript.ranges.sent.lineBreaks,
|
||||
],
|
||||
recv: [
|
||||
transcript.ranges.recv.info!,
|
||||
transcript.ranges.recv.headers!['server'],
|
||||
transcript.ranges.recv.headers!['date'],
|
||||
transcript.ranges.recv.json!['name'],
|
||||
transcript.ranges.recv.json!['gender'],
|
||||
...transcript.ranges.recv.lineBreaks,
|
||||
],
|
||||
};
|
||||
await prover.reveal(commit);
|
||||
addProverLog('Data revealed to verifier');
|
||||
|
||||
const result = await verified;
|
||||
addVerifierLog('Verification completed');
|
||||
|
||||
const t = new Transcript({
|
||||
sent: result.transcript.sent,
|
||||
recv: result.transcript.recv,
|
||||
});
|
||||
|
||||
addVerifierLog('Verified data:');
|
||||
addVerifierLog(`transcript.sent: ${t.sent()}`);
|
||||
addVerifierLog(`transcript.recv: ${t.recv()}`);
|
||||
setStarted(false);
|
||||
}, [ready]);
|
||||
|
||||
return (
|
||||
<div className="w-screen h-screen flex flex-col overflow-hidden">
|
||||
<div className="w-full p-2.5 bg-slate-200 mb-5 flex-shrink-0">
|
||||
<h1>Web-to-Web P2P Demo</h1>
|
||||
<p>
|
||||
This demo showcases peer-to-peer communication between a web prover
|
||||
and a web verifier using TLSNotary. The prover fetches data from{' '}
|
||||
<a href="https://swapi.dev" target="_blank" rel="noopener noreferrer">
|
||||
swapi.dev
|
||||
</a>{' '}
|
||||
and proves it to the verifier.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-rows-2 grid-cols-2 p-2 gap-2 flex-grow">
|
||||
<div className="flex flex-col items-center border border-slate-300 bg-slate-50 rounded row-span-1 col-span-1 p-4 gap-2">
|
||||
<div className="font-semibold">Prover</div>
|
||||
<div className="flex flex-col text-sm bg-white border border-slate-300 w-full flex-grow cursor-text py-1 overflow-y-auto">
|
||||
{proverMessages.map((m, index) => (
|
||||
<span key={index} className="px-2 py-1 text-slate-600 break-all">
|
||||
{m}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center border border-slate-300 bg-slate-100 rounded row-span-1 col-span-1 p-4 gap-2">
|
||||
<div className="font-semibold">Verifier</div>
|
||||
<div className="flex flex-col text-sm bg-white border border-slate-300 w-full flex-grow cursor-text py-1 overflow-y-auto">
|
||||
{verifierMessages.map((m, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-1 py-0.5 text-slate-600 break-all"
|
||||
>
|
||||
{m}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row justify-center row-span-1 col-span-2">
|
||||
<Button
|
||||
className="h-fit"
|
||||
disabled={!ready || started}
|
||||
onClick={start}
|
||||
>
|
||||
<div>
|
||||
{ready && !started ? (
|
||||
<>Start Demo</>
|
||||
) : (
|
||||
<Watch
|
||||
visible={true}
|
||||
height="40"
|
||||
width="40"
|
||||
radius="48"
|
||||
color="#000000"
|
||||
ariaLabel="watch-loading"
|
||||
wrapperStyle={{}}
|
||||
wrapperClass=""
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if ((module as any).hot) {
|
||||
(module as any).hot.accept();
|
||||
}
|
||||
|
||||
function Button(props: any) {
|
||||
const { className = '', ...p } = props;
|
||||
return (
|
||||
<button
|
||||
className={`px-4 py-2 bg-slate-300 rounded transition-colors border border-b-slate-400 border-r-slate-400 border-t-white border-l-white hover:bg-slate-200 active:bg-slate-300 active:border-t-slate-400 active:border-l-slate-400 active:border-b-white active:border-r-white disabled:opacity-50 disabled:bg-slate-200 ${className}`}
|
||||
{...p}
|
||||
/>
|
||||
);
|
||||
}
|
||||
62
demo/web-to-web-p2p/src/stream.ts
Normal file
62
demo/web-to-web-p2p/src/stream.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
export default class WebSocketStream {
|
||||
client: WebSocket;
|
||||
readable: Promise<ReadableStream>;
|
||||
writable: Promise<WritableStream>;
|
||||
constructor(url: string) {
|
||||
const client = new WebSocket(url);
|
||||
|
||||
const deferredReadable = defer<ReadableStream>();
|
||||
const deferredWritable = defer<WritableStream>();
|
||||
this.client = client;
|
||||
this.readable = deferredReadable.promise;
|
||||
this.writable = deferredWritable.promise;
|
||||
|
||||
client.onopen = () => {
|
||||
const readable = new ReadableStream({
|
||||
start(controller) {
|
||||
client.onmessage = async (event) => {
|
||||
controller.enqueue(event.data);
|
||||
};
|
||||
},
|
||||
cancel() {
|
||||
client.close();
|
||||
},
|
||||
});
|
||||
|
||||
const writable = new WritableStream({
|
||||
write(chunk) {
|
||||
client.send(chunk);
|
||||
},
|
||||
close() {
|
||||
client.close();
|
||||
},
|
||||
});
|
||||
|
||||
deferredReadable.resolve(readable);
|
||||
deferredWritable.resolve(writable);
|
||||
};
|
||||
}
|
||||
|
||||
async reader() {
|
||||
return this.readable.then((stream) => stream.getReader());
|
||||
}
|
||||
|
||||
async writer() {
|
||||
return this.writable.then((stream) => stream.getWriter());
|
||||
}
|
||||
}
|
||||
|
||||
function defer<value = any>(): {
|
||||
promise: Promise<value>;
|
||||
resolve: (val: value | Promise<value>) => void;
|
||||
reject: (err: any) => void;
|
||||
} {
|
||||
let resolve: (val: value | Promise<value>) => void,
|
||||
reject: (err: any) => void;
|
||||
const promise: Promise<value> = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
|
||||
return { promise, resolve: resolve!, reject: reject! };
|
||||
}
|
||||
10
demo/web-to-web-p2p/src/worker.ts
Normal file
10
demo/web-to-web-p2p/src/worker.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import * as Comlink from 'comlink';
|
||||
import init, { Prover, Attestation, Presentation, Verifier } from 'tlsn-js';
|
||||
|
||||
Comlink.expose({
|
||||
init,
|
||||
Prover,
|
||||
Verifier,
|
||||
Presentation,
|
||||
Attestation,
|
||||
});
|
||||
12
demo/web-to-web-p2p/tailwind.config.js
Normal file
12
demo/web-to-web-p2p/tailwind.config.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#243f5f',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
21
demo/web-to-web-p2p/tsconfig.json
Normal file
21
demo/web-to-web-p2p/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2015",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"noEmit": false,
|
||||
"jsx": "react"
|
||||
},
|
||||
"include": [
|
||||
"src/app.tsx"
|
||||
]
|
||||
}
|
||||
150
demo/web-to-web-p2p/webpack.js
Normal file
150
demo/web-to-web-p2p/webpack.js
Normal file
@@ -0,0 +1,150 @@
|
||||
var webpack = require('webpack'),
|
||||
path = require('path'),
|
||||
CopyWebpackPlugin = require('copy-webpack-plugin'),
|
||||
HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
|
||||
const ASSET_PATH = process.env.ASSET_PATH || '/';
|
||||
|
||||
var alias = {};
|
||||
|
||||
var fileExtensions = [
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'png',
|
||||
'gif',
|
||||
'eot',
|
||||
'otf',
|
||||
'svg',
|
||||
'ttf',
|
||||
'woff',
|
||||
'woff2',
|
||||
];
|
||||
|
||||
var options = {
|
||||
ignoreWarnings: [
|
||||
/Circular dependency between chunks with runtime/,
|
||||
/ResizeObserver loop completed with undelivered notifications/,
|
||||
],
|
||||
mode: 'development',
|
||||
entry: {
|
||||
app: path.join(__dirname, 'src', 'app.tsx'),
|
||||
},
|
||||
output: {
|
||||
filename: '[name].bundle.js',
|
||||
path: path.resolve(__dirname, 'build'),
|
||||
clean: true,
|
||||
publicPath: ASSET_PATH,
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: new RegExp('.(' + fileExtensions.join('|') + ')$'),
|
||||
type: 'asset/resource',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.html$/,
|
||||
loader: 'html-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.(ts|tsx)$/,
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
{
|
||||
loader: 'source-map-loader',
|
||||
},
|
||||
{
|
||||
loader: require.resolve('ts-loader'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'source-map-loader',
|
||||
},
|
||||
{
|
||||
loader: require.resolve('babel-loader'),
|
||||
},
|
||||
],
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
// look for .css or .scss files
|
||||
test: /\.(css|scss)$/,
|
||||
// in the `web` directory
|
||||
use: [
|
||||
{
|
||||
loader: 'style-loader',
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: { importLoaders: 1 },
|
||||
},
|
||||
{
|
||||
loader: 'postcss-loader',
|
||||
},
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
sourceMap: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
alias: alias,
|
||||
extensions: fileExtensions
|
||||
.map((extension) => '.' + extension)
|
||||
.concat(['.js', '.jsx', '.ts', '.tsx', '.css']),
|
||||
fallback: {
|
||||
crypto: require.resolve('crypto-browserify'),
|
||||
stream: require.resolve('stream-browserify'),
|
||||
vm: require.resolve('vm-browserify'),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: 'node_modules/tlsn-js/build',
|
||||
to: path.join(__dirname, 'build'),
|
||||
force: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.join(__dirname, 'index.ejs'),
|
||||
filename: 'index.html',
|
||||
cache: false,
|
||||
}),
|
||||
new webpack.ProvidePlugin({
|
||||
process: 'process/browser',
|
||||
}),
|
||||
new webpack.ProvidePlugin({
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
}),
|
||||
].filter(Boolean),
|
||||
// Required by wasm-bindgen-rayon, in order to use SharedArrayBuffer on the Web
|
||||
// Ref:
|
||||
// - https://github.com/GoogleChromeLabs/wasm-bindgen-rayon#setting-up
|
||||
// - https://web.dev/i18n/en/coop-coep/
|
||||
devServer: {
|
||||
port: 3456,
|
||||
host: 'localhost',
|
||||
hot: true,
|
||||
headers: {
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||
},
|
||||
client: {
|
||||
overlay: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = options;
|
||||
Reference in New Issue
Block a user