Move codebase into riot-web
This commit is contained in:
parent
3e77418fd7
commit
4110e2dfa3
4 changed files with 506 additions and 1 deletions
107
res/decoder-ring/datatypes.js
Normal file
107
res/decoder-ring/datatypes.js
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
/*
|
||||||
|
* Quick-n-dirty algebraic datatypes.
|
||||||
|
*
|
||||||
|
* These let us handle the possibility of failure without having to constantly write code to check for it.
|
||||||
|
* We can apply all of the transformations we need as if the data is present using `map`.
|
||||||
|
* If there's a None, or a FetchError, or a Pending, those are left untouched.
|
||||||
|
*
|
||||||
|
* I've used perhaps an odd bit of terminology from scalaz in `fold`. This is basically a `switch` statement:
|
||||||
|
* You pass it a set of functions to handle the various different states of the datatype, and if it finds the
|
||||||
|
* function it'll call it on its value.
|
||||||
|
*
|
||||||
|
* It's handy to have this in functional style when dealing with React as we can dispatch different ways of rendering
|
||||||
|
* really simply:
|
||||||
|
* ```
|
||||||
|
* bundleFetchStatus.fold({
|
||||||
|
* some: (fetchStatus) => <ProgressBar fetchsStatus={fetchStatus} />,
|
||||||
|
* }),
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
class Optional {
|
||||||
|
static from(value) {
|
||||||
|
return value && Some.of(value) || None;
|
||||||
|
}
|
||||||
|
map(f) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
flatMap(f) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
fold({ none }) {
|
||||||
|
return none && none();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Some extends Optional {
|
||||||
|
constructor(value) {
|
||||||
|
super();
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
map(f) {
|
||||||
|
return Some.of(f(this.value));
|
||||||
|
}
|
||||||
|
flatMap(f) {
|
||||||
|
return f(this.value);
|
||||||
|
}
|
||||||
|
fold({ some }) {
|
||||||
|
return some && some(this.value);
|
||||||
|
}
|
||||||
|
static of(value) {
|
||||||
|
return new Some(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const None = new Optional();
|
||||||
|
|
||||||
|
class FetchStatus {
|
||||||
|
constructor(opt = {}) {
|
||||||
|
this.opt = { at: Date.now(), ...opt };
|
||||||
|
}
|
||||||
|
map(f) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
flatMap(f) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Success extends FetchStatus {
|
||||||
|
static of(value) {
|
||||||
|
return new Success(value);
|
||||||
|
}
|
||||||
|
constructor(value, opt) {
|
||||||
|
super(opt);
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
map(f) {
|
||||||
|
return new Success(f(this.value), this.opt);
|
||||||
|
}
|
||||||
|
flatMap(f) {
|
||||||
|
return f(this.value, this.opt);
|
||||||
|
}
|
||||||
|
fold({ success }) {
|
||||||
|
return success instanceof Function ? success(this.value, this.opt) : undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Pending extends FetchStatus {
|
||||||
|
static of(opt) {
|
||||||
|
return new Pending(opt);
|
||||||
|
}
|
||||||
|
constructor(opt) {
|
||||||
|
super(opt);
|
||||||
|
}
|
||||||
|
fold({ pending }) {
|
||||||
|
return pending instanceof Function ? pending(this.opt) : undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class FetchError extends FetchStatus {
|
||||||
|
static of(reason, opt) {
|
||||||
|
return new FetchError(reason, opt);
|
||||||
|
}
|
||||||
|
constructor(reason, opt) {
|
||||||
|
super(opt);
|
||||||
|
this.reason = reason;
|
||||||
|
}
|
||||||
|
fold({ error }) {
|
||||||
|
return error instanceof Function ? error(this.reason, this.opt) : undefined;
|
||||||
|
}
|
||||||
|
}
|
319
res/decoder-ring/decoder.js
Normal file
319
res/decoder-ring/decoder.js
Normal file
|
@ -0,0 +1,319 @@
|
||||||
|
class StartupError extends Error {}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* We need to know the bundle path before we can fetch the sourcemap files. In a production environment, we can guess
|
||||||
|
* it using this.
|
||||||
|
*/
|
||||||
|
async function getBundleName() {
|
||||||
|
const res = await fetch("../index.html");
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new StartupError(`Couldn't fetch index.html to prefill bundle; ${res.status} ${res.statusText}`);
|
||||||
|
}
|
||||||
|
const index = await res.text();
|
||||||
|
return index.split("\n").map((line) =>
|
||||||
|
line.match(/<script src="bundles\/([^/]+)\/bundle.js"/),
|
||||||
|
)
|
||||||
|
.filter((result) => result)
|
||||||
|
.map((result) => result[1])[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateBundle(value) {
|
||||||
|
return value.match(/^[0-9a-f]{20}$/) ? Some.of(value) : None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A custom fetcher that abandons immediately upon getting a response.
|
||||||
|
* The purpose of this is just to validate that the user entered a real bundle, and provide feedback.
|
||||||
|
*/
|
||||||
|
const bundleCache = new Map();
|
||||||
|
function bundleSubject(bundle) {
|
||||||
|
if (!bundle.match(/^[0-9a-f]{20}$/)) throw new Error("Bad input");
|
||||||
|
if (bundleCache.has(bundle)) {
|
||||||
|
return bundleCache.get(bundle);
|
||||||
|
}
|
||||||
|
const fetcher = new rxjs.BehaviorSubject(Pending.of());
|
||||||
|
bundleCache.set(bundle, fetcher);
|
||||||
|
|
||||||
|
fetch(`/bundles/${bundle}/bundle.js.map`).then((res) => {
|
||||||
|
res.body.cancel(); /* Bail on the download immediately - it could be big! */
|
||||||
|
const status = res.ok;
|
||||||
|
if (status) {
|
||||||
|
fetcher.next(Success.of());
|
||||||
|
} else {
|
||||||
|
fetcher.next(FetchError.of(`Failed to fetch: ${res.status} ${res.statusText}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return fetcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Convert a ReadableStream of bytes into an Observable of a string
|
||||||
|
* The observable will emit a stream of Pending objects and will concatenate
|
||||||
|
* the number of bytes received with whatever pendingContext has been supplied.
|
||||||
|
* Finally, it will emit a Success containing the result.
|
||||||
|
* You'd use this on a Response.body.
|
||||||
|
*/
|
||||||
|
function observeReadableStream(readableStream, pendingContext = {}) {
|
||||||
|
let bytesReceived = 0;
|
||||||
|
let buffer = "";
|
||||||
|
const pendingSubject = new rxjs.BehaviorSubject(Pending.of({ ...pendingContext, bytesReceived }));
|
||||||
|
const throttledPending = pendingSubject.pipe(rxjs.operators.throttleTime(100));
|
||||||
|
const resultObservable = new rxjs.Subject();
|
||||||
|
const reader = readableStream.getReader();
|
||||||
|
const utf8Decoder = new TextDecoder("utf-8");
|
||||||
|
function readNextChunk() {
|
||||||
|
reader.read().then(({ done, value }) => {
|
||||||
|
if (done) {
|
||||||
|
pendingSubject.complete();
|
||||||
|
resultObservable.next(Success.of(buffer));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bytesReceived += value.length;
|
||||||
|
pendingSubject.next(Pending.of({...pendingContext, bytesReceived }));
|
||||||
|
/* string concatenation is apparently the most performant way to do this */
|
||||||
|
buffer += utf8Decoder.decode(value);
|
||||||
|
readNextChunk();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
readNextChunk();
|
||||||
|
return rxjs.concat(throttledPending, resultObservable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A wrapper which converts the browser's `fetch()` mechanism into an Observable. The Observable then provides us with
|
||||||
|
* a stream of datatype values: first, a sequence of Pending objects that keep us up to date with the download progress,
|
||||||
|
* finally followed by either a Success or Failure object. React then just has to render each of these appropriately.
|
||||||
|
*/
|
||||||
|
const fetchCache = new Map();
|
||||||
|
function fetchAsSubject(endpoint) {
|
||||||
|
if (fetchCache.has(endpoint)) {
|
||||||
|
// TODO: expiry/retry logic here?
|
||||||
|
return fetchCache.get(endpoint);
|
||||||
|
}
|
||||||
|
const fetcher = new rxjs.BehaviorSubject(Pending.of());
|
||||||
|
fetchCache.set(endpoint, fetcher);
|
||||||
|
|
||||||
|
fetch(endpoint).then((res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
fetcher.next(FetchError.of(`Failed to fetch endpoint ${endpoint}: ${res.status} ${res.statusText}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentLength = res.headers.get("content-length");
|
||||||
|
const context = contentLength ? { length: parseInt(contentLength) } : {};
|
||||||
|
|
||||||
|
const streamer = observeReadableStream(res.body, context, endpoint);
|
||||||
|
streamer.subscribe((value) => {
|
||||||
|
fetcher.next(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return fetcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================== */
|
||||||
|
/* ==== React stuff ==== */
|
||||||
|
/* ===================== */
|
||||||
|
/* Rather than importing an entire build infrastructure, for now we just use React without JSX */
|
||||||
|
const e = React.createElement;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Provides user feedback given a FetchStatus object.
|
||||||
|
*/
|
||||||
|
function ProgressBar({ fetchStatus }) {
|
||||||
|
return e('span', { className: "progress "},
|
||||||
|
fetchStatus.fold({
|
||||||
|
pending: ({ bytesReceived, length }) => {
|
||||||
|
if (!bytesReceived) {
|
||||||
|
return e('span', { className: "spinner" }, "\u29b5");
|
||||||
|
}
|
||||||
|
const kB = Math.floor(10 * bytesReceived / 1024) / 10;
|
||||||
|
if (!length) {
|
||||||
|
return e('span', null, `Fetching (${kB}kB)`);
|
||||||
|
}
|
||||||
|
const percent = Math.floor(100 * bytesReceived / length);
|
||||||
|
return e('span', null, `Fetching (${kB}kB) ${percent}%`);
|
||||||
|
},
|
||||||
|
success: () => e('span', null, "\u2713"),
|
||||||
|
error: (reason) => {
|
||||||
|
return e('span', { className: 'error'}, `\u2717 ${reason}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The main component.
|
||||||
|
*/
|
||||||
|
function BundlePicker() {
|
||||||
|
const [bundle, setBundle] = React.useState("");
|
||||||
|
const [file, setFile] = React.useState("");
|
||||||
|
const [line, setLine] = React.useState("1");
|
||||||
|
const [column, setColumn] = React.useState("");
|
||||||
|
const [result, setResult] = React.useState(None);
|
||||||
|
const [bundleFetchStatus, setBundleFetchStatus] = React.useState(None);
|
||||||
|
const [fileFetchStatus, setFileFetchStatus] = React.useState(None);
|
||||||
|
|
||||||
|
/* At startup, try to fill in the bundle name for the user */
|
||||||
|
React.useEffect(() => {
|
||||||
|
getBundleName().then((name) => {
|
||||||
|
if (bundle === "" && validateBundle(name) !== None) {
|
||||||
|
setBundle(name);
|
||||||
|
}
|
||||||
|
}, console.log.bind(console));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
/* ------------------------- */
|
||||||
|
/* Follow user state changes */
|
||||||
|
/* ------------------------- */
|
||||||
|
const onBundleChange = React.useCallback((event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
setBundle(value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onFileChange = React.useCallback((event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
setFile(value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onLineChange = React.useCallback((event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
setLine(value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onColumnChange = React.useCallback((event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
setColumn(value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
/* ------------------------------------------------ */
|
||||||
|
/* Plumb data-fetching observables through to React */
|
||||||
|
/* ------------------------------------------------ */
|
||||||
|
|
||||||
|
/* Whenever a valid bundle name is input, go see if it's a real bundle on the server */
|
||||||
|
React.useEffect(() =>
|
||||||
|
validateBundle(bundle).fold({
|
||||||
|
some: (value) => {
|
||||||
|
const subscription = bundleSubject(value)
|
||||||
|
.pipe(rxjs.operators.map(Some.of))
|
||||||
|
.subscribe(setBundleFetchStatus);
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
},
|
||||||
|
none: () => setBundleFetchStatus(None),
|
||||||
|
}),
|
||||||
|
[bundle]);
|
||||||
|
|
||||||
|
/* Whenever a valid javascript file is input, see if it corresponds to a sourcemap file and initiate a fetch
|
||||||
|
* if so. */
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!file.match(/.\.js$/) || validateBundle(bundle) === None) {
|
||||||
|
setFileFetchStatus(None);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const observable = fetchAsSubject(`/bundles/${bundle}/${file}.map`)
|
||||||
|
.pipe(
|
||||||
|
rxjs.operators.map((fetchStatus) => fetchStatus.flatMap(value => {
|
||||||
|
try {
|
||||||
|
return Success.of(JSON.parse(value));
|
||||||
|
} catch (e) {
|
||||||
|
return FetchError.of(e);
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
rxjs.operators.map(Some.of),
|
||||||
|
);
|
||||||
|
const subscription = observable.subscribe(setFileFetchStatus);
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
}, [bundle, file]);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Whenever we have a valid fetched sourcemap, and a valid line, attempt to find the original position from the
|
||||||
|
* sourcemap.
|
||||||
|
*/
|
||||||
|
React.useEffect(() => {
|
||||||
|
// `fold` dispatches on the datatype, like a switch statement
|
||||||
|
fileFetchStatus.fold({
|
||||||
|
some: (fetchStatus) =>
|
||||||
|
// `fold` just returns null for all of the cases that aren't `Success` objects here
|
||||||
|
fetchStatus.fold({
|
||||||
|
success: (value) => {
|
||||||
|
if (!line) return setResult(None);
|
||||||
|
const pLine = parseInt(line);
|
||||||
|
const pCol = parseInt(column);
|
||||||
|
sourceMap.SourceMapConsumer.with(value, undefined, (consumer) =>
|
||||||
|
consumer.originalPositionFor({ line: pLine, column: pCol }),
|
||||||
|
).then((result) => setResult(Some.of(JSON.stringify(result))));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
none: () => setResult(None),
|
||||||
|
});
|
||||||
|
}, [fileFetchStatus, line, column]);
|
||||||
|
|
||||||
|
|
||||||
|
/* ------ */
|
||||||
|
/* Render */
|
||||||
|
/* ------ */
|
||||||
|
return e('div', {},
|
||||||
|
e('div', { className: 'inputs' },
|
||||||
|
e('div', { className: 'bundle' },
|
||||||
|
e('label', { htmlFor: 'bundle'}, 'Bundle'),
|
||||||
|
e('input', {
|
||||||
|
name: 'bundle',
|
||||||
|
required: true,
|
||||||
|
pattern: "[0-9a-f]{20}",
|
||||||
|
onChange: onBundleChange,
|
||||||
|
value: bundle,
|
||||||
|
}),
|
||||||
|
bundleFetchStatus.fold({
|
||||||
|
some: (fetchStatus) => e(ProgressBar, { fetchStatus }),
|
||||||
|
none: () => null,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
e('div', { className: 'file' },
|
||||||
|
e('label', { htmlFor: 'file' }, 'File'),
|
||||||
|
e('input', {
|
||||||
|
name: 'file',
|
||||||
|
required: true,
|
||||||
|
pattern: ".+\\.js",
|
||||||
|
onChange: onFileChange,
|
||||||
|
value: file,
|
||||||
|
}),
|
||||||
|
fileFetchStatus.fold({
|
||||||
|
some: (fetchStatus) => e(ProgressBar, { fetchStatus }),
|
||||||
|
none: () => null,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
e('div', { className: 'line' },
|
||||||
|
e('label', { htmlFor: 'line' }, 'Line'),
|
||||||
|
e('input', {
|
||||||
|
name: 'line',
|
||||||
|
required: true,
|
||||||
|
pattern: "[0-9]+",
|
||||||
|
onChange: onLineChange,
|
||||||
|
value: line,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
e('div', { className: 'column' },
|
||||||
|
e('label', { htmlFor: 'column' }, 'Column'),
|
||||||
|
e('input', {
|
||||||
|
name: 'column',
|
||||||
|
required: true,
|
||||||
|
pattern: "[0-9]+",
|
||||||
|
onChange: onColumnChange,
|
||||||
|
value: column,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
e('div', null,
|
||||||
|
result.fold({
|
||||||
|
none: () => "Select a bundle, file and line",
|
||||||
|
some: (value) => e('pre', null, value),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Global stuff */
|
||||||
|
window.Decoder = {
|
||||||
|
BundlePicker,
|
||||||
|
};
|
79
res/decoder-ring/index.html
Normal file
79
res/decoder-ring/index.html
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Rageshake decoder ring</title>
|
||||||
|
<script crossorigin src="https://unpkg.com/source-map@0.7.3/dist/source-map.js"></script>
|
||||||
|
<script>
|
||||||
|
sourceMap.SourceMapConsumer.initialize({
|
||||||
|
"lib/mappings.wasm": "https://unpkg.com/source-map@0.7.3/lib/mappings.wasm"
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
|
||||||
|
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
|
||||||
|
<!--<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
|
||||||
|
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>-->
|
||||||
|
<script crossorigin src="https://unpkg.com/rxjs/bundles/rxjs.umd.min.js"></script>
|
||||||
|
<script src="datatypes.js"></script>
|
||||||
|
<script src="decoder.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@keyframes spin {
|
||||||
|
from {transform:rotate(0deg);}
|
||||||
|
to {transform:rotate(359deg);}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: sans-serif
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
animation: spin 4s infinite linear;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
font-size: larger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
padding-left: 0.5em;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bundle input {
|
||||||
|
width: 24ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.valid::after {
|
||||||
|
content: "✓"
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
width: 3em;
|
||||||
|
margin-right: 1em;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:valid {
|
||||||
|
border: 1px solid green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputs > div {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header><h2>Decoder ring</h2></header>
|
||||||
|
<content id="main">Waiting for javascript to run...</content>
|
||||||
|
<script type="text/javascript">
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
try {
|
||||||
|
ReactDOM.render(React.createElement(Decoder.BundlePicker), document.getElementById("main"))
|
||||||
|
} catch (e) {
|
||||||
|
const n = document.createElement("div");
|
||||||
|
n.innerText = `Error starting: ${e.message}`;
|
||||||
|
document.getElementById("main").appendChild(n);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -63,11 +63,11 @@ const COPY_LIST = [
|
||||||
["res/welcome/**", "webapp/welcome"],
|
["res/welcome/**", "webapp/welcome"],
|
||||||
["res/themes/**", "webapp/themes"],
|
["res/themes/**", "webapp/themes"],
|
||||||
["res/vector-icons/**", "webapp/vector-icons"],
|
["res/vector-icons/**", "webapp/vector-icons"],
|
||||||
|
["res/decoder-ring/**", "webapp/decoder-ring"],
|
||||||
["node_modules/matrix-react-sdk/res/media/**", "webapp/media"],
|
["node_modules/matrix-react-sdk/res/media/**", "webapp/media"],
|
||||||
["node_modules/olm/olm_legacy.js", "webapp", { directwatch: 1 }],
|
["node_modules/olm/olm_legacy.js", "webapp", { directwatch: 1 }],
|
||||||
["./config.json", "webapp", { directwatch: 1 }],
|
["./config.json", "webapp", { directwatch: 1 }],
|
||||||
["contribute.json", "webapp"],
|
["contribute.json", "webapp"],
|
||||||
["node_modules/matrix-react-sdk/res/decoder-ring/**", "webapp/decoder-ring"],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const parseArgs = require('minimist');
|
const parseArgs = require('minimist');
|
||||||
|
|
Loading…
Reference in a new issue