mirror of
https://github.com/MercuryWorkshop/scramjet.git
synced 2025-05-13 06:20:02 -04:00
parent
759ebd747e
commit
35b1d08bfb
21 changed files with 196 additions and 257 deletions
|
@ -19,5 +19,3 @@ Running `pnpm dev` will build Scramjet and start a dev server on localhost:1337.
|
||||||
- Only thing rewritten currently are imports and exports
|
- Only thing rewritten currently are imports and exports
|
||||||
- Check imports/exports for values contained in the `importmap` array, don't rewrite the node value if present
|
- Check imports/exports for values contained in the `importmap` array, don't rewrite the node value if present
|
||||||
- Write client APIs
|
- Write client APIs
|
||||||
- The DAMN SCRIPTS not being executed
|
|
||||||
- esbuild bundling more than what it's supposed to (instead of using imports, it bundles the whole file which is not what we want)
|
|
|
@ -15,7 +15,7 @@ const bare = createBareServer("/bare/", {
|
||||||
});
|
});
|
||||||
|
|
||||||
const fastify = Fastify({
|
const fastify = Fastify({
|
||||||
serverFactory: (handler) => {
|
serverFactory: (handler, opts) => {
|
||||||
return createServer()
|
return createServer()
|
||||||
.on("request", (req, res) => {
|
.on("request", (req, res) => {
|
||||||
if (bare.shouldRoute(req)) {
|
if (bare.shouldRoute(req)) {
|
||||||
|
@ -53,14 +53,12 @@ const devServer = await context({
|
||||||
worker: "./src/worker/index.ts",
|
worker: "./src/worker/index.ts",
|
||||||
codecs: "./src/codecs/index.ts",
|
codecs: "./src/codecs/index.ts",
|
||||||
config: "./src/scramjet.config.ts",
|
config: "./src/scramjet.config.ts",
|
||||||
html: "./src/html/index.ts"
|
|
||||||
},
|
},
|
||||||
entryNames: "scramjet.[name]",
|
entryNames: "scramjet.[name]",
|
||||||
outdir: "./dist",
|
outdir: "./dist",
|
||||||
bundle: true,
|
bundle: true,
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
logLevel: "info",
|
logLevel: "info",
|
||||||
format: "esm",
|
|
||||||
plugins: [
|
plugins: [
|
||||||
copy({
|
copy({
|
||||||
resolveFrom: "cwd",
|
resolveFrom: "cwd",
|
||||||
|
|
|
@ -9,7 +9,6 @@ const scramjetBuild = await build({
|
||||||
worker: "./src/worker/index.ts",
|
worker: "./src/worker/index.ts",
|
||||||
codecs: "./src/codecs/index.ts",
|
codecs: "./src/codecs/index.ts",
|
||||||
config: "./src/scramjet.config.ts",
|
config: "./src/scramjet.config.ts",
|
||||||
html: "./src/html/index.ts"
|
|
||||||
},
|
},
|
||||||
entryNames: "scramjet.[name]",
|
entryNames: "scramjet.[name]",
|
||||||
outdir: "./dist",
|
outdir: "./dist",
|
||||||
|
@ -20,8 +19,7 @@ const scramjetBuild = await build({
|
||||||
logLevel: "info",
|
logLevel: "info",
|
||||||
metafile: true,
|
metafile: true,
|
||||||
treeShaking: true,
|
treeShaking: true,
|
||||||
minify: true,
|
minify: true
|
||||||
format: "esm"
|
|
||||||
});
|
});
|
||||||
|
|
||||||
writeFileSync("./meta.json", JSON.stringify(scramjetBuild.metafile));
|
writeFileSync("./meta.json", JSON.stringify(scramjetBuild.metafile));
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { encodeUrl, decodeUrl } from "./rewriters/url";
|
import { encodeUrl, decodeUrl } from "./rewriters/url";
|
||||||
import { rewriteCss } from "./rewriters/css";
|
import { rewriteCss } from "./rewriters/css";
|
||||||
import { rewriteSrcset } from "./rewriters/srcset";
|
import { rewriteHtml, rewriteSrcset } from "./rewriters/html";
|
||||||
import { rewriteJs } from "./rewriters/js";
|
import { rewriteJs } from "./rewriters/js";
|
||||||
import { rewriteHeaders } from "./rewriters/headers";
|
import { rewriteHeaders } from "./rewriters/headers";
|
||||||
|
|
||||||
|
@ -13,4 +13,24 @@ export function isScramjetFile(src: string) {
|
||||||
return bool;
|
return bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { encodeUrl, decodeUrl, rewriteCss, rewriteSrcset, rewriteJs, rewriteHeaders };
|
const bundle = {
|
||||||
|
rewriters: {
|
||||||
|
url: {
|
||||||
|
encodeUrl, decodeUrl
|
||||||
|
},
|
||||||
|
rewriteCss,
|
||||||
|
rewriteHtml,
|
||||||
|
rewriteSrcset,
|
||||||
|
rewriteJs,
|
||||||
|
rewriteHeaders
|
||||||
|
},
|
||||||
|
isScramjetFile
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
__scramjet$bundle: typeof bundle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.__scramjet$bundle = bundle;
|
86
src/bundle/rewriters/html.ts
Normal file
86
src/bundle/rewriters/html.ts
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import { Parser } from "htmlparser2";
|
||||||
|
import { DomHandler, Element } from "domhandler";
|
||||||
|
import { hasAttrib } from "domutils";
|
||||||
|
import render from "dom-serializer";
|
||||||
|
import { encodeUrl } from "./url";
|
||||||
|
import { rewriteCss } from "./css";
|
||||||
|
import { rewriteJs } from "./js";
|
||||||
|
import { isScramjetFile } from "../";
|
||||||
|
|
||||||
|
export function rewriteHtml(html: string, origin?: URL) {
|
||||||
|
const handler = new DomHandler((err, dom) => dom);
|
||||||
|
const parser = new Parser(handler);
|
||||||
|
|
||||||
|
parser.write(html);
|
||||||
|
parser.end();
|
||||||
|
|
||||||
|
return render(traverseParsedHtml(handler.root, origin));
|
||||||
|
}
|
||||||
|
|
||||||
|
// i need to add the attributes in during rewriting
|
||||||
|
|
||||||
|
function traverseParsedHtml(node, origin?: URL) {
|
||||||
|
/* csp attributes */
|
||||||
|
if (hasAttrib(node, "nonce")) delete node.attribs.nonce;
|
||||||
|
if (hasAttrib(node, "integrity")) delete node.attribs.integrity;
|
||||||
|
if (hasAttrib(node, "csp")) delete node.attribs.csp;
|
||||||
|
|
||||||
|
/* url attributes */
|
||||||
|
if (hasAttrib(node, "src") && !isScramjetFile(node.attribs.src)) node.attribs.src = encodeUrl(node.attribs.src, origin);
|
||||||
|
if (hasAttrib(node, "href")) node.attribs.href = encodeUrl(node.attribs.href, origin);
|
||||||
|
if (hasAttrib(node, "data")) node.attribs.data = encodeUrl(node.attribs.data, origin);
|
||||||
|
if (hasAttrib(node, "action")) node.attribs.action = encodeUrl(node.attribs.action, origin);
|
||||||
|
if (hasAttrib(node, "formaction")) node.attribs.formaction = encodeUrl(node.attribs.formaction, origin);
|
||||||
|
|
||||||
|
/* other */
|
||||||
|
if (hasAttrib(node, "srcdoc")) node.attribs.srcdoc = rewriteHtml(node.attribs.srcdoc, origin);
|
||||||
|
if (hasAttrib(node, "srcset")) node.attribs.srcset = rewriteSrcset(node.attribs.srcset, origin);
|
||||||
|
if (hasAttrib(node, "imagesrcset")) node.attribs.imagesrcset = rewriteSrcset(node.attribs.imagesrcset, origin);
|
||||||
|
if (hasAttrib(node, "style")) node.attribs.style = rewriteCss(node.attribs.style, origin);
|
||||||
|
|
||||||
|
if (node.name === "style" && node.children[0] !== undefined) node.children[0].data = rewriteCss(node.children[0].data, origin);
|
||||||
|
if (node.name === "script" && /(application|text)\/javascript|importmap|undefined/.test(node.attribs.type) && node.children[0] !== undefined) node.children[0].data = rewriteJs(node.children[0].data, origin);
|
||||||
|
if (node.name === "meta" && hasAttrib(node, "http-equiv")) {
|
||||||
|
if (node.attribs["http-equiv"] === "content-security-policy") {
|
||||||
|
node = {};
|
||||||
|
} else if (node.attribs["http-equiv"] === "refresh" && node.attribs.content.includes("url")) {
|
||||||
|
const contentArray = node.attribs.content.split("url=");
|
||||||
|
contentArray[1] = encodeUrl(contentArray[1].trim(), origin);
|
||||||
|
node.attribs.content = contentArray.join("url=");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.name === "head") {
|
||||||
|
const scramjetScripts = [];
|
||||||
|
["codecs", "config", "bundle", "client"].forEach((script) => {
|
||||||
|
scramjetScripts.push(new Element("script", {
|
||||||
|
src: self.__scramjet$config[script]
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
node.children.unshift(...scramjetScripts);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.childNodes) {
|
||||||
|
for (const childNode in node.childNodes) {
|
||||||
|
node.childNodes[childNode] = traverseParsedHtml(node.childNodes[childNode], origin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
// stole from osana lmao
|
||||||
|
export function rewriteSrcset(srcset: string, origin?: URL) {
|
||||||
|
const urls = srcset.split(/ [0-9]+x,? ?/g);
|
||||||
|
if (!urls) return "";
|
||||||
|
const sufixes = srcset.match(/ [0-9]+x,? ?/g);
|
||||||
|
if (!sufixes) return "";
|
||||||
|
const rewrittenUrls = urls.map((url, i) => {
|
||||||
|
if (url && sufixes[i]) {
|
||||||
|
return encodeUrl(url, origin) + sufixes[i];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return rewrittenUrls.join("");
|
||||||
|
}
|
|
@ -1,15 +0,0 @@
|
||||||
import { encodeUrl } from "./url";
|
|
||||||
|
|
||||||
export function rewriteSrcset(srcset: string, origin?: URL) {
|
|
||||||
const urls = srcset.split(/ [0-9]+x,? ?/g);
|
|
||||||
if (!urls) return "";
|
|
||||||
const sufixes = srcset.match(/ [0-9]+x,? ?/g);
|
|
||||||
if (!sufixes) return "";
|
|
||||||
const rewrittenUrls = urls.map((url, i) => {
|
|
||||||
if (url && sufixes[i]) {
|
|
||||||
return encodeUrl(url, origin) + sufixes[i];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return rewrittenUrls.join("");
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { rewriteJs } from "./js";
|
import { rewriteJs } from "./js";
|
||||||
|
|
||||||
function canParseUrl(url: string, origin: string | URL) {
|
function canParseUrl(url: string, origin?: URL) {
|
||||||
try {
|
try {
|
||||||
new URL(url, origin);
|
new URL(url, origin);
|
||||||
|
|
||||||
|
@ -11,14 +11,14 @@ function canParseUrl(url: string, origin: string | URL) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// something is broken with this but i didn't debug it
|
// something is broken with this but i didn't debug it
|
||||||
export function encodeUrl(url: string, origin?: string | URL) {
|
export function encodeUrl(url: string, origin?: URL) {
|
||||||
if (!origin) {
|
if (!origin) {
|
||||||
origin = new URL(self.__scramjet$config.codec.decode(location.href.slice((location.origin + self.__scramjet$config.prefix).length)));
|
origin = new URL(self.__scramjet$config.codec.decode(location.href.slice((location.origin + self.__scramjet$config.prefix).length)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url.startsWith("javascript:")) {
|
if (url.startsWith("javascript:")) {
|
||||||
return "javascript:" + rewriteJs(url.slice("javascript:".length));
|
return "javascript:" + rewriteJs(url.slice("javascript:".length));
|
||||||
} else if (/^(#|mailto|about|data)/.test(url) || url.startsWith(location.origin + self.__scramjet$config.prefix)) {
|
} else if (/^(#|mailto|about|data)/.test(url)) {
|
||||||
return url;
|
return url;
|
||||||
} else if (canParseUrl(url, origin)) {
|
} else if (canParseUrl(url, origin)) {
|
||||||
return location.origin + self.__scramjet$config.prefix + self.__scramjet$config.codec.encode(new URL(url, origin).href);
|
return location.origin + self.__scramjet$config.prefix + self.__scramjet$config.codec.encode(new URL(url, origin).href);
|
||||||
|
@ -29,7 +29,9 @@ export function encodeUrl(url: string, origin?: string | URL) {
|
||||||
export function decodeUrl(url: string) {
|
export function decodeUrl(url: string) {
|
||||||
if (/^(#|about|data|mailto|javascript)/.test(url)) {
|
if (/^(#|about|data|mailto|javascript)/.test(url)) {
|
||||||
return url;
|
return url;
|
||||||
} else {
|
} else if (canParseUrl(url)) {
|
||||||
return self.__scramjet$config.codec.decode(url.slice((location.origin + self.__scramjet$config.prefix).length))
|
return self.__scramjet$config.codec.decode(url.slice((location.origin + self.__scramjet$config.prefix).length))
|
||||||
|
} else {
|
||||||
|
return url;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,8 +1,6 @@
|
||||||
import { encodeUrl } from "../bundle";
|
|
||||||
|
|
||||||
navigator.sendBeacon = new Proxy(navigator.sendBeacon, {
|
navigator.sendBeacon = new Proxy(navigator.sendBeacon, {
|
||||||
apply(target, thisArg, argArray) {
|
apply(target, thisArg, argArray) {
|
||||||
argArray[0] = encodeUrl(argArray[0]);
|
argArray[0] = self.__scramjet$bundle.rewriters.url.encodeUrl(argArray[0]);
|
||||||
|
|
||||||
return Reflect.apply(target, thisArg, argArray);
|
return Reflect.apply(target, thisArg, argArray);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,23 +1,21 @@
|
||||||
import { rewriteCss } from "../bundle";
|
|
||||||
|
|
||||||
const cssProperties = ["background", "background-image", "mask", "mask-image", "list-style", "list-style-image", "border-image", "border-image-source", "cursor"];
|
const cssProperties = ["background", "background-image", "mask", "mask-image", "list-style", "list-style-image", "border-image", "border-image-source", "cursor"];
|
||||||
const jsProperties = ["background", "backgroundImage", "mask", "maskImage", "listStyle", "listStyleImage", "borderImage", "borderImageSource", "cursor"];
|
const jsProperties = ["background", "backgroundImage", "mask", "maskImage", "listStyle", "listStyleImage", "borderImage", "borderImageSource", "cursor"];
|
||||||
|
|
||||||
|
|
||||||
CSSStyleDeclaration.prototype.setProperty = new Proxy(CSSStyleDeclaration.prototype.setProperty, {
|
CSSStyleDeclaration.prototype.setProperty = new Proxy(CSSStyleDeclaration.prototype.setProperty, {
|
||||||
apply(target, thisArg, argArray) {
|
apply(target, thisArg, argArray) {
|
||||||
if (cssProperties.includes(argArray[0])) argArray[1] = rewriteCss(argArray[1]);
|
if (cssProperties.includes(argArray[0])) argArray[1] = self.__scramjet$bundle.rewriters.rewriteCss(argArray[1]);
|
||||||
|
|
||||||
return Reflect.apply(target, thisArg, argArray);
|
return Reflect.apply(target, thisArg, argArray);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
jsProperties.forEach((prop) => {
|
jsProperties.forEach((prop) => {
|
||||||
const propDescriptor = Object.getOwnPropertyDescriptor(CSSStyleDeclaration.prototype, prop);
|
const propDescriptor = Object.getOwnPropertyDescriptor(CSSStyleDeclaration.prototype, prop);
|
||||||
|
|
||||||
Object.defineProperty(CSSStyleDeclaration.prototype, prop, {
|
Object.defineProperty(CSSStyleDeclaration.prototype, prop, {
|
||||||
set(v) {
|
set(v) {
|
||||||
propDescriptor.set.call(this, rewriteCss(v));
|
propDescriptor.set.call(this, self.__scramjet$bundle.rewriters.rewriteCss(v));
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
import { encodeUrl, rewriteCss, rewriteJs, rewriteSrcset } from "../bundle";
|
|
||||||
import { rewriteHtml } from "../html";
|
|
||||||
|
|
||||||
// object
|
// object
|
||||||
// iframe
|
// iframe
|
||||||
// embed
|
// embed
|
||||||
|
@ -40,34 +37,25 @@ Object.keys(attribs).forEach((attrib: string) => {
|
||||||
const descriptor = Object.getOwnPropertyDescriptor(element.prototype, attrib);
|
const descriptor = Object.getOwnPropertyDescriptor(element.prototype, attrib);
|
||||||
Object.defineProperty(element.prototype, attrib, {
|
Object.defineProperty(element.prototype, attrib, {
|
||||||
get() {
|
get() {
|
||||||
return this.dataset[`${attrib}`];
|
return descriptor.get.call(this, [this.dataset[`_${attrib}`]]);
|
||||||
},
|
},
|
||||||
|
|
||||||
set(value) {
|
set(value) {
|
||||||
if (this.dataset["scramjet"]) {
|
this.dataset[`_${attrib}`] = value;
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log(value);
|
|
||||||
this.dataset[`${attrib}`] = value;
|
|
||||||
if (/nonce|integrity|csp/.test(attrib)) {
|
if (/nonce|integrity|csp/.test(attrib)) {
|
||||||
this.removeAttribute(attrib);
|
this.removeAttribute(attrib);
|
||||||
} else if (/src|href|data|action|formaction/.test(attrib)) {
|
} else if (/src|href|data|action|formaction/.test(attrib)) {
|
||||||
// @ts-expect-error
|
|
||||||
// TrustedScriptURL does not exist as a type yet, but it is a real thing
|
|
||||||
if (value instanceof TrustedScriptURL) {
|
if (value instanceof TrustedScriptURL) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
value = encodeUrl(value);
|
value = self.__scramjet$bundle.rewriters.url.encodeUrl(value);
|
||||||
} else if (attrib === "srcdoc") {
|
} else if (attrib === "srcdoc") {
|
||||||
// @ts-ignore
|
value = self.__scramjet$bundle.rewriters.rewriteHtml(value);
|
||||||
// This needs to be ignored because I'm bad at TypeScript
|
|
||||||
|
|
||||||
value = rewriteHtml(value).documentElement.innerHTML;
|
|
||||||
} else if (/(image)?srcset/.test(attrib)) {
|
} else if (/(image)?srcset/.test(attrib)) {
|
||||||
value = rewriteSrcset(value);
|
value = self.__scramjet$bundle.rewriters.rewriteSrcset(value);
|
||||||
} else if (attrib === "style") {
|
} else if (attrib === "style") {
|
||||||
value = rewriteCss(value);
|
value = self.__scramjet$bundle.rewriters.rewriteCss(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
descriptor.set.call(this, value);
|
descriptor.set.call(this, value);
|
||||||
|
@ -76,35 +64,38 @@ Object.keys(attribs).forEach((attrib: string) => {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
Element.prototype.getAttribute = new Proxy(Element.prototype.getAttribute, {
|
HTMLElement.prototype.getAttribute = new Proxy(Element.prototype.getAttribute, {
|
||||||
apply(target, thisArg, argArray) {
|
apply(target, thisArg, argArray) {
|
||||||
if (Object.keys(attribs).includes(argArray[0]) && thisArg.dataset[`${argArray[0]}`]) {
|
console.log(thisArg);
|
||||||
return thisArg.dataset[`${argArray[0]}`];
|
if (Object.keys(attribs).includes(argArray[0])) {
|
||||||
|
argArray[0] = `_${argArray[0]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Reflect.apply(target, thisArg, argArray);
|
return Reflect.apply(target, thisArg, argArray);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
Element.prototype.setAttribute = new Proxy(Element.prototype.setAttribute, {
|
// setAttribute proxy is currently broken
|
||||||
|
|
||||||
|
HTMLElement.prototype.setAttribute = new Proxy(Element.prototype.setAttribute, {
|
||||||
apply(target, thisArg, argArray) {
|
apply(target, thisArg, argArray) {
|
||||||
if (thisArg.dataset["scramjet"]) {
|
if (thisArg.dataset["scramjet"]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(argArray[1])
|
console.log(argArray[1])
|
||||||
if (Object.keys(attribs).includes(argArray[0])) {
|
if (Object.keys(attribs).includes(argArray[0])) {
|
||||||
thisArg.dataset[`${argArray[0]}`] = argArray[1];
|
thisArg.dataset[`_${argArray[0]}`] = argArray[1];
|
||||||
if (/nonce|integrity|csp/.test(argArray[0])) {
|
if (/nonce|integrity|csp/.test(argArray[0])) {
|
||||||
return;
|
return;
|
||||||
} else if (/src|href|data|action|formaction/.test(argArray[0])) {
|
} else if (/src|href|data|action|formaction/.test(argArray[0])) {
|
||||||
argArray[1] = encodeUrl(argArray[1]);
|
console.log(thisArg);
|
||||||
|
argArray[1] = self.__scramjet$bundle.rewriters.url.encodeUrl(argArray[1]);
|
||||||
} else if (argArray[0] === "srcdoc") {
|
} else if (argArray[0] === "srcdoc") {
|
||||||
// @ts-ignore
|
argArray[1] = self.__scramjet$bundle.rewriters.rewriteHtml(argArray[1]);
|
||||||
argArray[1] = rewriteHtml(argArray[1]).documentElement.innerHTML;
|
|
||||||
} else if (/(image)?srcset/.test(argArray[0])) {
|
} else if (/(image)?srcset/.test(argArray[0])) {
|
||||||
argArray[1] = rewriteSrcset(argArray[1]);
|
argArray[1] = self.__scramjet$bundle.rewriters.rewriteSrcset(argArray[1]);
|
||||||
} else if (argArray[1] === "style") {
|
} else if (argArray[1] === "style") {
|
||||||
argArray[1] = rewriteCss(argArray[1]);
|
argArray[1] = self.__scramjet$bundle.rewriters.rewriteCss(argArray[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,22 +108,17 @@ const innerHTML = Object.getOwnPropertyDescriptor(Element.prototype, "innerHTML"
|
||||||
Object.defineProperty(HTMLElement.prototype, "innerHTML", {
|
Object.defineProperty(HTMLElement.prototype, "innerHTML", {
|
||||||
set(value) {
|
set(value) {
|
||||||
if (this instanceof HTMLScriptElement) {
|
if (this instanceof HTMLScriptElement) {
|
||||||
// @ts-expect-error
|
|
||||||
// TrustedScript does not exist as a type yet, but it is a real thing
|
|
||||||
if (!(value instanceof TrustedScript)) {
|
if (!(value instanceof TrustedScript)) {
|
||||||
value = rewriteJs(value);
|
value = self.__scramjet$bundle.rewriters.rewriteJs(value);
|
||||||
}
|
}
|
||||||
} else if (this instanceof HTMLStyleElement) {
|
} else if (this instanceof HTMLStyleElement) {
|
||||||
value = rewriteCss(value);
|
value = self.__scramjet$bundle.rewriters.rewriteCss(value);
|
||||||
} else {
|
} else {
|
||||||
// @ts-expect-error
|
|
||||||
// TrustedHTML does not exist as a type, but it is a real thing
|
|
||||||
if (!(value instanceof TrustedHTML)) {
|
if (!(value instanceof TrustedHTML)) {
|
||||||
// @ts-ignore
|
value = self.__scramjet$bundle.rewriters.rewriteHtml(value);
|
||||||
value = rewriteHtml(value).documentElement.innerHTML;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
innerHTML.set.call(this, value);
|
return innerHTML.set.call(this, value);
|
||||||
},
|
},
|
||||||
});
|
})
|
|
@ -1,18 +1,16 @@
|
||||||
import { rewriteJs } from "../bundle";
|
|
||||||
|
|
||||||
const FunctionProxy = new Proxy(Function, {
|
const FunctionProxy = new Proxy(Function, {
|
||||||
construct(target, argArray) {
|
construct(target, argArray) {
|
||||||
if (argArray.length === 1) {
|
if (argArray.length === 1) {
|
||||||
return Reflect.construct(target, rewriteJs(argArray[0]));
|
return Reflect.construct(target, self.__scramjet$bundle.rewriters.rewriteJs(argArray[0]));
|
||||||
} else {
|
} else {
|
||||||
return Reflect.construct(target, rewriteJs(argArray[argArray.length - 1]))
|
return Reflect.construct(target, self.__scramjet$bundle.rewriters.rewriteJs(argArray[argArray.length - 1]))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
apply(target, thisArg, argArray) {
|
apply(target, thisArg, argArray) {
|
||||||
if (argArray.length === 1) {
|
if (argArray.length === 1) {
|
||||||
return Reflect.apply(target, undefined, rewriteJs(argArray[0]));
|
return Reflect.apply(target, undefined, self.__scramjet$bundle.rewriters.rewriteJs(argArray[0]));
|
||||||
} else {
|
} else {
|
||||||
return Reflect.apply(target, undefined, [...argArray.map((x, index) => index === argArray.length - 1), rewriteJs(argArray[argArray.length - 1])])
|
return Reflect.apply(target, undefined, [...argArray.map((x, index) => index === argArray.length - 1), self.__scramjet$bundle.rewriters.rewriteJs(argArray[argArray.length - 1])])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
// ts throws an error if you dont do window.fetch
|
// ts throws an error if you dont do window.fetch
|
||||||
|
|
||||||
import { encodeUrl, rewriteHeaders } from "../bundle";
|
|
||||||
|
|
||||||
window.fetch = new Proxy(window.fetch, {
|
window.fetch = new Proxy(window.fetch, {
|
||||||
apply(target, thisArg, argArray) {
|
apply(target, thisArg, argArray) {
|
||||||
console.log(argArray);
|
argArray[0] = self.__scramjet$bundle.rewriters.url.encodeUrl(argArray[0]);
|
||||||
if (!(argArray[0] instanceof Request)) argArray[0] = encodeUrl(argArray[0]);
|
|
||||||
console.log(argArray);
|
|
||||||
|
|
||||||
return Reflect.apply(target, thisArg, argArray);
|
return Reflect.apply(target, thisArg, argArray);
|
||||||
},
|
},
|
||||||
|
@ -14,7 +10,7 @@ window.fetch = new Proxy(window.fetch, {
|
||||||
|
|
||||||
Headers = new Proxy(Headers, {
|
Headers = new Proxy(Headers, {
|
||||||
construct(target, argArray, newTarget) {
|
construct(target, argArray, newTarget) {
|
||||||
argArray[0] = rewriteHeaders(argArray[0]);
|
argArray[0] = self.__scramjet$bundle.rewriters.rewriteHeaders(argArray[0]);
|
||||||
|
|
||||||
return Reflect.construct(target, argArray, newTarget);
|
return Reflect.construct(target, argArray, newTarget);
|
||||||
},
|
},
|
||||||
|
@ -22,7 +18,7 @@ Headers = new Proxy(Headers, {
|
||||||
|
|
||||||
Request = new Proxy(Request, {
|
Request = new Proxy(Request, {
|
||||||
construct(target, argArray, newTarget) {
|
construct(target, argArray, newTarget) {
|
||||||
if (typeof argArray[0] === "string") argArray[0] = encodeUrl(argArray[0]);
|
if (typeof argArray[0] === "string") argArray[0] = self.__scramjet$bundle.rewriters.url.encodeUrl(argArray[0]);
|
||||||
|
|
||||||
return Reflect.construct(target, argArray, newTarget);
|
return Reflect.construct(target, argArray, newTarget);
|
||||||
},
|
},
|
||||||
|
@ -30,8 +26,8 @@ Request = new Proxy(Request, {
|
||||||
|
|
||||||
Response.redirect = new Proxy(Response.redirect, {
|
Response.redirect = new Proxy(Response.redirect, {
|
||||||
apply(target, thisArg, argArray) {
|
apply(target, thisArg, argArray) {
|
||||||
argArray[0] = encodeUrl(argArray[0]);
|
argArray[0] = self.__scramjet$bundle.rewriters.url.encodeUrl(argArray[0]);
|
||||||
|
|
||||||
return Reflect.apply(target, thisArg, argArray);
|
return Reflect.apply(target, thisArg, argArray);
|
||||||
},
|
},
|
||||||
});
|
});
|
|
@ -11,4 +11,4 @@ declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
__location: Location;
|
__location: Location;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { encodeUrl, decodeUrl } from "../bundle";
|
|
||||||
|
|
||||||
function urlLocation() {
|
function urlLocation() {
|
||||||
const loc = new URL(decodeUrl(location.href));
|
let loc = new URL(self.__scramjet$bundle.rewriters.url.decodeUrl(location.href));
|
||||||
loc.assign = (url: string) => location.assign(encodeUrl(url));
|
loc.assign = (url: string) => location.assign(self.__scramjet$bundle.rewriters.url.encodeUrl(url));
|
||||||
loc.reload = () => location.reload();
|
loc.reload = () => location.reload();
|
||||||
loc.replace = (url: string) => location.replace(encodeUrl(url));
|
loc.replace = (url: string) => location.replace(self.__scramjet$bundle.rewriters.url.encodeUrl(url));
|
||||||
loc.toString = () => loc.href;
|
loc.toString = () => loc.href;
|
||||||
|
|
||||||
return loc;
|
return loc;
|
||||||
|
@ -21,7 +20,7 @@ export function LocationProxy() {
|
||||||
|
|
||||||
set(obj, prop, value) {
|
set(obj, prop, value) {
|
||||||
if (prop === "href") {
|
if (prop === "href") {
|
||||||
location.href = encodeUrl(value);
|
location.href = self.__scramjet$bundle.rewriters.url.encodeUrl(value);
|
||||||
} else {
|
} else {
|
||||||
loc[prop] = value;
|
loc[prop] = value;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,9 @@
|
||||||
import { encodeUrl, rewriteJs } from "../bundle";
|
|
||||||
import { rewriteHtml } from "../html";
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
// trustedTypes isn't a type on the global scope yet
|
|
||||||
trustedTypes.createPolicy = new Proxy(trustedTypes.createPolicy, {
|
trustedTypes.createPolicy = new Proxy(trustedTypes.createPolicy, {
|
||||||
apply(target, thisArg, argArray) {
|
apply(target, thisArg, argArray) {
|
||||||
if (argArray[1].createHTML) {
|
if (argArray[1].createHTML) {
|
||||||
argArray[1].createHTML = new Proxy(argArray[1].createHTML, {
|
argArray[1].createHTML = new Proxy(argArray[1].createHTML, {
|
||||||
apply(target1, thisArg1, argArray1) {
|
apply(target1, thisArg1, argArray1) {
|
||||||
return rewriteHtml(target1(...argArray1)).documentElement.innerHTML;
|
return self.__scramjet$bundle.rewriters.rewriteHtml(target1(...argArray1));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -16,7 +11,7 @@ trustedTypes.createPolicy = new Proxy(trustedTypes.createPolicy, {
|
||||||
if (argArray[1].createScript) {
|
if (argArray[1].createScript) {
|
||||||
argArray[1].createScript = new Proxy(argArray[1].createScript, {
|
argArray[1].createScript = new Proxy(argArray[1].createScript, {
|
||||||
apply(target1, thisArg1, argArray1) {
|
apply(target1, thisArg1, argArray1) {
|
||||||
return rewriteJs(target1(...argArray1));
|
return self.__scramjet$bundle.rewriters.rewriteJs(target1(...argArray1));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -24,7 +19,7 @@ trustedTypes.createPolicy = new Proxy(trustedTypes.createPolicy, {
|
||||||
if (argArray[1].createScriptURL) {
|
if (argArray[1].createScriptURL) {
|
||||||
argArray[1].createScriptURL = new Proxy(argArray[1].createScriptURL, {
|
argArray[1].createScriptURL = new Proxy(argArray[1].createScriptURL, {
|
||||||
apply(target1, thisArg1, argArray1) {
|
apply(target1, thisArg1, argArray1) {
|
||||||
return encodeUrl(target1(...argArray1))
|
return self.__scramjet$bundle.rewriters.url.encodeUrl(target1(...argArray1))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,18 @@
|
||||||
import { encodeUrl, rewriteHeaders } from "../bundle";
|
|
||||||
|
|
||||||
XMLHttpRequest.prototype.open = new Proxy(XMLHttpRequest.prototype.open, {
|
XMLHttpRequest.prototype.open = new Proxy(XMLHttpRequest.prototype.open, {
|
||||||
apply(target, thisArg, argArray) {
|
apply(target, thisArg, argArray) {
|
||||||
if (argArray[1]) argArray[1] = encodeUrl(argArray[1]);
|
if (argArray[1]) argArray[1] = self.__scramjet$bundle.rewriters.url.encodeUrl(argArray[1]);
|
||||||
|
|
||||||
return Reflect.apply(target, thisArg, argArray);
|
return Reflect.apply(target, thisArg, argArray);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
XMLHttpRequest.prototype.setRequestHeader = new Proxy(XMLHttpRequest.prototype.setRequestHeader, {
|
XMLHttpRequest.prototype.setRequestHeader = new Proxy(XMLHttpRequest.prototype.setRequestHeader, {
|
||||||
apply(target, thisArg, argArray) {
|
apply(target, thisArg, argArray) {
|
||||||
let headerObject = Object.fromEntries([argArray]);
|
let headerObject = Object.fromEntries([argArray]);
|
||||||
headerObject = rewriteHeaders(headerObject);
|
headerObject = self.__scramjet$bundle.rewriters.rewriteHeaders(headerObject);
|
||||||
|
|
||||||
argArray = Object.entries(headerObject)[0];
|
argArray = Object.entries(headerObject)[0];
|
||||||
|
|
||||||
return Reflect.apply(target, thisArg, argArray);
|
return Reflect.apply(target, thisArg, argArray);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,91 +0,0 @@
|
||||||
// this code needs to be bundled as a separate file due to when it is ran
|
|
||||||
|
|
||||||
import { encodeUrl, rewriteCss, rewriteJs, rewriteSrcset } from "../bundle";
|
|
||||||
import Clone from "./cloner.ts";
|
|
||||||
|
|
||||||
const parser = new DOMParser();
|
|
||||||
|
|
||||||
function parseHtml(html: string) {
|
|
||||||
return parser.parseFromString(html, "text/html");
|
|
||||||
}
|
|
||||||
|
|
||||||
function traverseParsedHtml(node: Element) {
|
|
||||||
for (const cspAttr of ["csp", "nonce", "integrity"]) {
|
|
||||||
if (node.hasAttribute(cspAttr)) {
|
|
||||||
node.setAttribute("data-" + cspAttr, node.getAttribute(cspAttr));
|
|
||||||
node.removeAttribute(cspAttr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const urlAttr of ["src", "href", "data", "action"]) {
|
|
||||||
if (node.hasAttribute(urlAttr) && !node.hasAttribute("data-scramjet")) {
|
|
||||||
const url = node.getAttribute(urlAttr);
|
|
||||||
node.setAttribute("data-" + urlAttr, url)
|
|
||||||
node.setAttribute(urlAttr, encodeUrl(node.getAttribute(urlAttr)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const srcsetAttr of ["srcset", "imagesrcset"]) {
|
|
||||||
if (node.hasAttribute(srcsetAttr)) {
|
|
||||||
const srcset = node.getAttribute(srcsetAttr);
|
|
||||||
node.setAttribute("data-", srcset);
|
|
||||||
node.setAttribute(srcsetAttr, rewriteSrcset(srcset));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.hasAttribute("srcdoc")) {
|
|
||||||
const srcdoc = node.getAttribute("srcdoc");
|
|
||||||
node.setAttribute("data-srcdoc", srcdoc);
|
|
||||||
|
|
||||||
const rewrittenSrcdoc = rewriteHtml(srcdoc);
|
|
||||||
node.setAttribute("srcdoc", rewrittenSrcdoc.documentElement.innerHTML);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node instanceof HTMLScriptElement) {
|
|
||||||
if (node.hasAttribute("data-scramjet")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
node.innerHTML = rewriteJs(node.textContent);
|
|
||||||
} else if (node instanceof HTMLStyleElement) {
|
|
||||||
node.innerHTML = rewriteCss(node.textContent);
|
|
||||||
} else if (node instanceof HTMLHeadElement) {
|
|
||||||
// this array is reversed because it uses node.prepend()
|
|
||||||
for (const scramjetScript of ["client", "config", "codecs"]) {
|
|
||||||
const script = document.createElement("script");
|
|
||||||
script.src = self.__scramjet$config[scramjetScript];
|
|
||||||
script.setAttribute("data-scramjet", "");
|
|
||||||
node.prepend(script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.children) {
|
|
||||||
for (const child of node.children) {
|
|
||||||
traverseParsedHtml(child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function rewriteHtml(html: string) {
|
|
||||||
const parsedHtml = parseHtml(html);
|
|
||||||
traverseParsedHtml(parsedHtml.documentElement);
|
|
||||||
|
|
||||||
return parsedHtml;
|
|
||||||
}
|
|
||||||
|
|
||||||
navigator.serviceWorker.ready.then(({ active }) => {
|
|
||||||
if (active) {
|
|
||||||
active.postMessage("rewriteHtml");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
navigator.serviceWorker.addEventListener("message", (message) => {
|
|
||||||
document.documentElement.replaceWith(rewriteHtml(message.data).documentElement);
|
|
||||||
|
|
||||||
const scripts = document.querySelectorAll("script:not([data-scramjet])");
|
|
||||||
|
|
||||||
for (const script of scripts) {
|
|
||||||
const clone = new Clone(script);
|
|
||||||
clone.insertCopy();
|
|
||||||
clone.removeElement();
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -10,7 +10,6 @@ declare global {
|
||||||
worker: string;
|
worker: string;
|
||||||
client: string;
|
client: string;
|
||||||
codecs: string;
|
codecs: string;
|
||||||
html: string;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,6 +21,5 @@ self.__scramjet$config = {
|
||||||
bundle: "/scramjet.bundle.js",
|
bundle: "/scramjet.bundle.js",
|
||||||
worker: "/scramjet.worker.js",
|
worker: "/scramjet.worker.js",
|
||||||
client: "/scramjet.client.js",
|
client: "/scramjet.client.js",
|
||||||
codecs: "/scramjet.codecs.js",
|
codecs: "/scramjet.codecs.js"
|
||||||
html: "/scramjet.html.js"
|
|
||||||
}
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
import { BareClient } from "@mercuryworkshop/bare-mux";
|
import { BareClient } from "@mercuryworkshop/bare-mux";
|
||||||
import { BareResponseFetch } from "@mercuryworkshop/bare-mux";
|
import { BareResponseFetch } from "@mercuryworkshop/bare-mux"
|
||||||
import { encodeUrl, decodeUrl, rewriteCss, rewriteJs, rewriteHeaders } from "../bundle";
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
@ -8,11 +7,9 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ScramjetServiceWorker {
|
self.ScramjetServiceWorker = class ScramjetServiceWorker {
|
||||||
client: typeof BareClient.prototype;
|
client: typeof BareClient.prototype;
|
||||||
config: typeof self.__scramjet$config;
|
config: typeof self.__scramjet$config;
|
||||||
html: string;
|
|
||||||
|
|
||||||
constructor(config = self.__scramjet$config) {
|
constructor(config = self.__scramjet$config) {
|
||||||
this.client = new BareClient();
|
this.client = new BareClient();
|
||||||
if (!config.prefix) config.prefix = "/scramjet/";
|
if (!config.prefix) config.prefix = "/scramjet/";
|
||||||
|
@ -28,11 +25,11 @@ export class ScramjetServiceWorker {
|
||||||
const urlParam = new URLSearchParams(new URL(request.url).search);
|
const urlParam = new URLSearchParams(new URL(request.url).search);
|
||||||
|
|
||||||
if (urlParam.has("url")) {
|
if (urlParam.has("url")) {
|
||||||
return Response.redirect(encodeUrl(urlParam.get("url"), new URL(urlParam.get("url"))))
|
return Response.redirect(self.__scramjet$bundle.rewriters.url.encodeUrl(urlParam.get("url"), new URL(urlParam.get("url"))))
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = new URL(decodeUrl(request.url));
|
const url = new URL(self.__scramjet$bundle.rewriters.url.decodeUrl(request.url));
|
||||||
|
|
||||||
const response: BareResponseFetch = await this.client.fetch(url, {
|
const response: BareResponseFetch = await this.client.fetch(url, {
|
||||||
method: request.method,
|
method: request.method,
|
||||||
|
@ -45,28 +42,18 @@ export class ScramjetServiceWorker {
|
||||||
});
|
});
|
||||||
|
|
||||||
let responseBody;
|
let responseBody;
|
||||||
const responseHeaders = rewriteHeaders(response.rawHeaders, url);
|
const responseHeaders = self.__scramjet$bundle.rewriters.rewriteHeaders(response.rawHeaders, url);
|
||||||
if (response.body) {
|
if (response.body) {
|
||||||
switch (request.destination) {
|
switch (request.destination) {
|
||||||
case "iframe":
|
case "iframe":
|
||||||
case "document":
|
case "document":
|
||||||
if (responseHeaders["content-type"].startsWith("text/html")) {
|
responseBody = self.__scramjet$bundle.rewriters.rewriteHtml(await response.text(), url);
|
||||||
responseBody =
|
|
||||||
`<html>
|
|
||||||
<head>
|
|
||||||
${["codecs", "config", "html"].map((script) => "<script type=\"module\" src=" + this.config[script] + "></script>").join("")}
|
|
||||||
</head>
|
|
||||||
</html>`;
|
|
||||||
this.html = await response.text();
|
|
||||||
} else {
|
|
||||||
responseBody = response.body
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case "script":
|
case "script":
|
||||||
responseBody = rewriteJs(await response.text(), url);
|
responseBody = self.__scramjet$bundle.rewriters.rewriteJs(await response.text(), url);
|
||||||
break;
|
break;
|
||||||
case "style":
|
case "style":
|
||||||
responseBody = rewriteCss(await response.text(), url);
|
responseBody = self.__scramjet$bundle.rewriters.rewriteCss(await response.text(), url);
|
||||||
break;
|
break;
|
||||||
case "sharedworker":
|
case "sharedworker":
|
||||||
break;
|
break;
|
||||||
|
@ -117,13 +104,7 @@ export class ScramjetServiceWorker {
|
||||||
|
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
return renderError(err, decodeUrl(request.url));
|
return renderError(err, self.__scramjet$bundle.rewriters.url.decodeUrl(request.url));
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async messageListener(message: MessageEvent) {
|
|
||||||
if (message.data === "rewriteHtml") {
|
|
||||||
message.source.postMessage(this.html);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
13
static/sw.js
13
static/sw.js
|
@ -1,6 +1,7 @@
|
||||||
import { ScramjetServiceWorker } from "./scramjet.worker.js";
|
importScripts("scramjet.codecs.js");
|
||||||
import "./scramjet.codecs.js";
|
importScripts("scramjet.config.js");
|
||||||
import "./scramjet.config.js";
|
importScripts( __scramjet$config.bundle || "scramjet.bundle.js")
|
||||||
|
importScripts( __scramjet$config.worker || "scramjet.worker.js");
|
||||||
|
|
||||||
const scramjet = new ScramjetServiceWorker();
|
const scramjet = new ScramjetServiceWorker();
|
||||||
|
|
||||||
|
@ -12,8 +13,4 @@ self.addEventListener("fetch", async (event) => {
|
||||||
return await fetch(event.request);
|
return await fetch(event.request);
|
||||||
}
|
}
|
||||||
})())
|
})())
|
||||||
});
|
})
|
||||||
|
|
||||||
self.addEventListener("message", async (message) => {
|
|
||||||
await scramjet.messageListener(message);
|
|
||||||
});
|
|
|
@ -1,6 +1,5 @@
|
||||||
navigator.serviceWorker.register("./sw.js", {
|
navigator.serviceWorker.register("./sw.js", {
|
||||||
scope: __scramjet$config.prefix,
|
scope: __scramjet$config.prefix
|
||||||
type: "module"
|
|
||||||
})
|
})
|
||||||
const connection = new BareMux.BareMuxConnection("/bare-mux-worker.js")
|
const connection = new BareMux.BareMuxConnection("/bare-mux-worker.js")
|
||||||
const flex = css`display: flex;`;
|
const flex = css`display: flex;`;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue