diff --git a/packages/website/package.json b/packages/website/package.json index 60f83934..9930c208 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -19,6 +19,7 @@ "@sveltejs/kit": "^2.5.7", "@sveltejs/vite-plugin-svelte": "^3.1.0", "@types/node": "^20.12.7", + "@types/sharedworker": "^0.0.115", "glob": "^10.3.12", "mdsvex": "^0.11.0", "rehype-slug": "^6.0.0", diff --git a/packages/website/src/lib/workers/terser.ts b/packages/website/src/lib/workers/terser.ts new file mode 100644 index 00000000..fabde9d7 --- /dev/null +++ b/packages/website/src/lib/workers/terser.ts @@ -0,0 +1,48 @@ +import type { MinifyOptions, MinifyOutput } from "terser"; +import { recieveMessageData, sendMessageData } from "./util"; + +export function init() { + + let worker: SharedWorker; + let currentId = 0; + let terserModule: typeof import("terser"); + let promises: { [id: number]: [(value: MinifyOutput | PromiseLike) => void, (reason?: any) => void] } = {}; + return { + minify: async function minify(files: string | string[] | { + [file: string]: string; + }, options?: MinifyOptions): Promise { + + if (!!window.SharedWorker) { + if (!worker) { + worker = new SharedWorker(new URL('./terserWorker.ts', import.meta.url), { type: "module" }) + worker.port.onmessage = (e: MessageEvent) => { + // invoke the promise's resolve() or reject() depending on whether there was an error. + promises[e.data[recieveMessageData.MessageId]][e.data[recieveMessageData.MessageType]](e.data[recieveMessageData.Return]); + + // ... then delete the promise controller + delete promises[e.data[recieveMessageData.MessageId]]; + + } + } + worker.port.start() + return new Promise((resolve, reject) => { + promises[++currentId] = [resolve, reject]; + + let data = { + [sendMessageData.MessageId]: currentId, + [sendMessageData.Parameters]: [files, options + ] + } + worker.port.postMessage(data) + }); + + } else { + if (!terserModule) { + terserModule = await import("terser") + } + return await terserModule.minify(files, options) + } + } + } + +} \ No newline at end of file diff --git a/packages/website/src/lib/workers/terserWorker.ts b/packages/website/src/lib/workers/terserWorker.ts new file mode 100644 index 00000000..715c2501 --- /dev/null +++ b/packages/website/src/lib/workers/terserWorker.ts @@ -0,0 +1,35 @@ +import { minify, type MinifyOptions } from "terser"; +import { recieveMessageTypes, sendMessageData } from "./util"; + +/// +declare var self: SharedWorkerGlobalScope; + +self.onconnect = function (event) { + const port = event.ports[0]; + port.onmessage = function (e: MessageEvent<{ + [sendMessageData.MessageId]: number, + [sendMessageData.Parameters]: [string | string[] | { + [file: string]: string; + }, MinifyOptions? + ] + }>) { + minify(...e.data[sendMessageData.Parameters]).then( + // success handler - callback(id, SUCCESS(0), result) + // if `d` is transferable transfer zero-copy + d => { + + port.postMessage([e.data[0], recieveMessageTypes.RESOLVE, d], + // @ts-ignore + [d].filter(x => ( + (x instanceof ArrayBuffer) || + (x instanceof MessagePort) + // || (self.ImageBitmap && x instanceof ImageBitmap) + ))); + }, + // error handler - callback(id, ERROR(1), error) + er => { postMessage([e.data[0], recieveMessageTypes.REJECT, '' + er]); } + ); + }; + + +}; \ No newline at end of file diff --git a/packages/website/src/lib/workers/util.ts b/packages/website/src/lib/workers/util.ts new file mode 100644 index 00000000..326453e4 --- /dev/null +++ b/packages/website/src/lib/workers/util.ts @@ -0,0 +1,105 @@ +export type FunctionMap = { [x: string]: Function } + +export enum sendMessageData { + MessageId, + Function, + Parameters +} + +export interface sendMessageMap { + [sendMessageData.MessageId]: number, + [sendMessageData.Function]: number, + [sendMessageData.Parameters]: T[], +} + +export enum recieveMessageTypes { + RESOLVE, // OK + REJECT // ERROR +} + + +export enum recieveMessageData { + MessageId, + MessageType, + Return +} + +export interface recieveMessageMap { + [recieveMessageData.MessageId]: number, + [recieveMessageData.MessageType]: recieveMessageTypes, + [recieveMessageData.Return]: T, +} + + +// // worker +// import { recieveMessageTypes, type FunctionMap } from "./util"; + +// function makeMessageHandler(functions: FunctionMap) { + +// return (e) => { +// // Invoking within then() captures exceptions in the supplied async function as rejections +// Promise.resolve(e.data[1]).then( +// v => $$.apply($$, v) +// ).then( +// // success handler - callback(id, SUCCESS(0), result) +// // if `d` is transferable transfer zero-copy +// d => { +// postMessage([e.data[0], recieveMessageTypes.SUCCESS, d], [d].filter(x => ( +// (x instanceof ArrayBuffer) || +// (x instanceof MessagePort) || +// (self.ImageBitmap && x instanceof ImageBitmap) +// ))); +// }, +// // error handler - callback(id, ERROR(1), error) +// er => { postMessage([e.data[0], recieveMessageTypes.ERROR, '' + er]); } +// ); +// } +// } + +// // host +// import { recieveMessageData, recieveMessageTypes, sendMessageData, type recieveMessageMap, type sendMessageMap } from "./util"; + +// function makeHostHandler(worker: Worker) { + +// let currentId = 0; + +// // Outward-facing promises store their "controllers" (`[request, reject]`) here: +// const promises: { [id: number]: { [t: number]: IArguments } } = {} +// ; +// /** Handle RPC results/errors coming back out of the worker. +// * Messages coming from the worker take the form `[id, status, result]`: +// * id - counter-based unique ID for the RPC call +// * status - 0 for success, 1 for failure +// * result - the result or error, depending on `status` +// */ +// worker.onmessage = (e: MessageEvent) => { +// // invoke the promise's resolve() or reject() depending on whether there was an error. +// promises[e.data[0]][e.data[1]](e.data[2]); + +// // ... then delete the promise controller +// promises[e.data[0]] = null; +// }; + +// // Return a proxy function that forwards calls to the worker & returns a promise for the result. +// return function () { +// let args = [].slice.call(arguments); +// return new Promise(function () { +// // Add the promise controller to the registry +// promises[++currentId] = arguments; + +// // Send an RPC call to the worker - call(id, params) +// // The filter is to provide a list of transferables to send zero-copy +// let data: sendMessageMap = { +// [sendMessageData.MessageId]: currentId, +// [sendMessageData.Function]: 1, +// [sendMessageData.Parameters]: args +// } +// worker.postMessage(data, args.filter(x => ( +// (x instanceof ArrayBuffer) || +// (x instanceof MessagePort) || +// (self.ImageBitmap && x instanceof ImageBitmap) +// ))); +// }); +// }; + +// } diff --git a/packages/website/src/routes/(tools)/bookmarklets/+page.svelte b/packages/website/src/routes/(tools)/bookmarklets/+page.svelte index fd6341e0..4d4b6034 100644 --- a/packages/website/src/routes/(tools)/bookmarklets/+page.svelte +++ b/packages/website/src/routes/(tools)/bookmarklets/+page.svelte @@ -3,7 +3,7 @@ import SvelteSeo from "svelte-seo"; import { bookmarkify, parseMeta } from "./bookmarklets"; import type { Config } from "./config"; - + import { init } from "$lib/workers/terser"; import { SITE_URL } from '$lib/metadata'; /** @type {import('./$types').Snapshot} */ @@ -12,12 +12,14 @@ restore: (v: string) => (value = v), }; + let minify = init().minify + let value = ""; let output = ""; let options: Config = {}; async function process(str: string) { options = await parseMeta(str); - let res = await bookmarkify(str, options); + let res = await bookmarkify(str, options, minify); if (typeof res == "string") { output = res; } diff --git a/packages/website/src/routes/(tools)/bookmarklets/bookmarklets.ts b/packages/website/src/routes/(tools)/bookmarklets/bookmarklets.ts index e9d7d3b4..c3ba4025 100644 --- a/packages/website/src/routes/(tools)/bookmarklets/bookmarklets.ts +++ b/packages/website/src/routes/(tools)/bookmarklets/bookmarklets.ts @@ -2,14 +2,17 @@ import MagicString from "magic-string"; import { Parser } from "acorn"; -import { minify } from "terser"; +import { type MinifyOptions, type MinifyOutput } from "terser"; let sourceMap = false; import { configSchema } from "./config.schema"; import type { Config } from "./config"; // console.log(configSchema) -export async function bookmarkify(code: string, options: Config) { +export async function bookmarkify(code: string, options: Config, minify: (files: string | string[] | { + [file: string]: string; +}, options?: MinifyOptions | undefined) => Promise +) { // try { if (options.script) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 70a0f307..97f5ca4e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,6 +101,9 @@ importers: '@types/node': specifier: ^20.12.7 version: 20.12.7 + '@types/sharedworker': + specifier: ^0.0.115 + version: 0.0.115 glob: specifier: ^10.3.12 version: 10.3.12 @@ -872,6 +875,9 @@ packages: '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/sharedworker@0.0.115': + resolution: {integrity: sha512-istxrCv9mbZQt7kXMVMsc4U+dbxG5y+ae5N+9f6pM9VOJmclF7FBWtTog9SMT16s1pZUTjga7ewaMVgqhDpTOg==} + '@types/unist@2.0.10': resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==} @@ -2841,6 +2847,8 @@ snapshots: '@types/resolve@1.20.2': {} + '@types/sharedworker@0.0.115': {} + '@types/unist@2.0.10': {} '@types/unist@3.0.2': {}