UNPKG

next-yak

Version:

next-yak is a CSS-in-JS solution tailored for Next.js that seamlessly combines the expressive power of styled-components syntax with efficient build-time extraction of CSS using Next.js's built-in CSS configuration

315 lines (311 loc) 8.84 kB
import { existsSync } from "node:fs"; import { isAbsolute } from "node:path"; import { fileURLToPath } from "node:url"; import { Worker } from "node:worker_threads"; //#region isolated-source-eval/loader-hooks.ts const LOADER_DATA_URL = `data:text/javascript;base64,${Buffer.from(` import { fileURLToPath } from "node:url"; let port; const deps = new Set(); export function initialize(data) { port = data.port; port.on("message", (msg) => { if (msg === "getDeps") { port.postMessage([...deps]); deps.clear(); } }); } export async function resolve(specifier, context, nextResolve) { const result = await nextResolve(specifier); if (result.url.startsWith("file://") && !result.url.includes("/node_modules/")) { deps.add(fileURLToPath(result.url)); } return result; } `).toString("base64")}`; function createLoaderDataUrl() { return LOADER_DATA_URL; } //#endregion //#region isolated-source-eval/evaluator.ts function resolveWorkerPath() { const jsPath = fileURLToPath(new URL("./worker.js", import.meta.url)); if (existsSync(jsPath)) return jsPath; return fileURLToPath(new URL("./worker.ts", import.meta.url)); } function bootWorker() { const worker = new Worker(resolveWorkerPath(), { workerData: { loaderDataUrl: createLoaderDataUrl() } }); return { worker, ready: new Promise((resolve) => { const onMessage = (msg) => { if (msg.type === "ready") { worker.off("message", onMessage); resolve(); } }; worker.on("message", onMessage); }) }; } const DEFAULT_EVAL_TIMEOUT_MS = 3e4; const MAX_STALENESS_RETRIES = 3; async function createEvaluator() { const resultCache = /* @__PURE__ */ new Map(); const inflight = /* @__PURE__ */ new Map(); const reverseDeps = /* @__PURE__ */ new Map(); const forwardDeps = /* @__PURE__ */ new Map(); let nextId = 0; let currentEval = null; let currentTimeout = null; const queue = []; const invalidatedDuringEval = /* @__PURE__ */ new Set(); let disposed = false; let primary = bootWorker(); let shadow = bootWorker(); await Promise.all([primary.ready, shadow.ready]); function unrefWorkers(primaryWorker, shadowWorker) { primaryWorker.unref(); shadowWorker.unref(); } function setupMessageHandler(workerObj) { workerObj.worker.on("message", (msg) => { if (msg.type !== "result") return; handleResult(msg); }); workerObj.worker.on("error", (err) => { if (currentEval) { clearEvalTimeout(); const pending = currentEval; currentEval = null; const result = { ok: false, error: { message: err.message, stack: err.stack ?? "" } }; updateDeps(pending.absolutePath, [pending.absolutePath]); resultCache.set(pending.absolutePath, result); inflight.delete(pending.absolutePath); pending.resolve(result); processQueue(); } }); } setupMessageHandler(primary); unrefWorkers(primary.worker, shadow.worker); function clearEvalTimeout() { if (currentTimeout !== null) { clearTimeout(currentTimeout); currentTimeout = null; } } function handleResult(msg) { if (!currentEval) return; if (msg.id !== currentEval.id) return; clearEvalTimeout(); const pending = currentEval; currentEval = null; if (msg.ok && invalidatedDuringEval.size > 0) { if (msg.dependencies.some((dep) => invalidatedDuringEval.has(dep))) { if (pending.retryCount >= MAX_STALENESS_RETRIES) { pending.resolve({ ok: false, error: { message: `Evaluation of ${pending.absolutePath} exceeded maximum staleness retries (${MAX_STALENESS_RETRIES})`, stack: "" } }); processQueue(); return; } pending.retryCount++; swapWorkers(); queue.unshift(pending); processQueue(); return; } } let result; if (msg.ok) { result = { ok: true, value: msg.value, dependencies: msg.dependencies }; updateDeps(pending.absolutePath, msg.dependencies); } else { result = { ok: false, error: msg.error }; updateDeps(pending.absolutePath, msg.dependencies); } resultCache.set(pending.absolutePath, result); inflight.delete(pending.absolutePath); pending.resolve(result); processQueue(); } function updateDeps(entryPoint, deps) { const oldDeps = forwardDeps.get(entryPoint); if (oldDeps) for (const dep of oldDeps) { const entries = reverseDeps.get(dep); if (entries) { entries.delete(entryPoint); if (entries.size === 0) reverseDeps.delete(dep); } } const depSet = new Set(deps); forwardDeps.set(entryPoint, depSet); for (const dep of depSet) { let entries = reverseDeps.get(dep); if (!entries) { entries = /* @__PURE__ */ new Set(); reverseDeps.set(dep, entries); } entries.add(entryPoint); } } function processQueue() { if (currentEval || queue.length === 0 || disposed) return; const next = queue.shift(); const alreadyCached = resultCache.get(next.absolutePath); if (alreadyCached) { inflight.delete(next.absolutePath); next.resolve(alreadyCached); processQueue(); return; } currentEval = next; primary.ready.then(() => { if (disposed || currentEval !== next) return; primary.worker.postMessage({ type: "evaluate", id: next.id, absolutePath: next.absolutePath }); currentTimeout = setTimeout(() => { if (currentEval !== next) return; currentEval = null; currentTimeout = null; const result = { ok: false, error: { message: `Evaluation of ${next.absolutePath} timed out after ${DEFAULT_EVAL_TIMEOUT_MS}ms`, stack: "" } }; updateDeps(next.absolutePath, [next.absolutePath]); resultCache.set(next.absolutePath, result); inflight.delete(next.absolutePath); next.resolve(result); swapWorkers(); processQueue(); }, DEFAULT_EVAL_TIMEOUT_MS); }); } function swapWorkers() { primary.worker.removeAllListeners(); primary.worker.terminate(); primary = shadow; setupMessageHandler(primary); shadow = bootWorker(); unrefWorkers(primary.worker, shadow.worker); invalidatedDuringEval.clear(); } function evaluate(absolutePath) { if (disposed) return Promise.reject(/* @__PURE__ */ new Error("Evaluator has been disposed")); if (!isAbsolute(absolutePath)) return Promise.reject(/* @__PURE__ */ new Error(`evaluate() requires an absolute path, got: ${absolutePath}`)); const cached = resultCache.get(absolutePath); if (cached) return Promise.resolve(cached); const pending = inflight.get(absolutePath); if (pending) return pending; const promise = new Promise((resolve, reject) => { const id = nextId++; queue.push({ id, absolutePath, resolve, reject, retryCount: 0 }); processQueue(); }); inflight.set(absolutePath, promise); return promise; } function invalidate(...absolutePaths) { if (absolutePaths.length === 0) return; for (const path of absolutePaths) invalidatedDuringEval.add(path); const allEntryPoints = /* @__PURE__ */ new Set(); for (const path of absolutePaths) { const entryPoints = reverseDeps.get(path); if (entryPoints) for (const entry of entryPoints) allEntryPoints.add(entry); } if (allEntryPoints.size === 0) return; for (const entry of allEntryPoints) { resultCache.delete(entry); inflight.delete(entry); const deps = forwardDeps.get(entry); if (deps) { for (const dep of deps) { const entries = reverseDeps.get(dep); if (entries) { entries.delete(entry); if (entries.size === 0) reverseDeps.delete(dep); } } forwardDeps.delete(entry); } } clearEvalTimeout(); const inflightEval = currentEval; currentEval = null; swapWorkers(); if (inflightEval) queue.unshift(inflightEval); processQueue(); } function invalidateAll() { resultCache.clear(); inflight.clear(); forwardDeps.clear(); reverseDeps.clear(); clearEvalTimeout(); const inflightEval = currentEval; currentEval = null; swapWorkers(); if (inflightEval) queue.unshift(inflightEval); processQueue(); } function getDependentsOf(absolutePath) { const entryPoints = reverseDeps.get(absolutePath); if (!entryPoints) return []; return [...entryPoints]; } async function dispose() { if (disposed) return; disposed = true; clearEvalTimeout(); if (currentEval) { currentEval.reject(/* @__PURE__ */ new Error("Evaluator has been disposed")); currentEval = null; } for (const pending of queue) pending.reject(/* @__PURE__ */ new Error("Evaluator has been disposed")); queue.length = 0; inflight.clear(); await Promise.all([primary.worker.terminate(), shadow.worker.terminate()]); } return { evaluate, invalidate, invalidateAll, getDependentsOf, dispose, [Symbol.asyncDispose]: dispose }; } //#endregion export { createEvaluator }; //# sourceMappingURL=index.js.map