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
JavaScript
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