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

135 lines (127 loc) 5.36 kB
/** * Turbopack evaluator adapter. * * Bridges the low-level `Evaluator` (worker-thread module evaluator) and * turbopack's loader lifecycle. A single `Evaluator` instance is kept as a * singleton on `globalThis` because turbopack may create multiple loader * instances that share the same process — `Symbol.for` keys guarantee all * instances converge on the same evaluator and modification time map. * * Invalidation uses synchronous modification time polling rather than `fs.watch` because * turbopack can re-run a loader before a filesystem watcher event fires, * which would cause the evaluator to return stale cached results. */ import { statSync } from "node:fs"; import type { Evaluator } from "../isolated-source-eval/evaluator.js"; const EVALUATOR_KEY = Symbol.for("next-yak-evaluator"); const MTIMES_KEY = Symbol.for("next-yak-mtimes"); /** * A shared `Map` of file paths to their last known modification times, used to detect changes on disk * * e.g. bar.ts -> 1680000000000 (Sat Mar 28 2024 12:00:00 GMT+0000) * * fs.fileWatch is not used as it might be out of sync with turbopacks own file watching also turbopack * spawns a loader pool which could cause a lot of file watchers to be created which can cause performance issues and hitting OS limits */ const getFileModificationTimes = () => ((globalThis as typeof globalThis & { [MTIMES_KEY]?: Map<string, number> })[MTIMES_KEY] ??= new Map<string, number>()); /** * Returns the shared evaluator, creating it on first call. * * Uses `Symbol.for` keys on `globalThis` so every turbopack loader instance * in the process converges on the same evaluator and modification time map. A * `beforeExit` handler terminates the worker threads when the process winds * down. */ export const getEvaluatorSingleton = async (): Promise<Evaluator> => { const processGlobal = globalThis as typeof globalThis & { [EVALUATOR_KEY]?: Promise<Evaluator>; }; if (!processGlobal[EVALUATOR_KEY]) { processGlobal[EVALUATOR_KEY] = import("../isolated-source-eval/index.js").then( ({ createEvaluator }) => createEvaluator(), ); // Kill the worker threads when the process exits process.on("beforeExit", async () => { const evaluator = await processGlobal[EVALUATOR_KEY]!; await evaluator.dispose(); }); } return processGlobal[EVALUATOR_KEY]!; }; /** * Turbopack runs the loader for the same compilation file-by-file, but multiple files can share the * same dependency. This function detects which dependencies actually changed * on disk and invalidates them once — this prevents re-evaluating the same module multiple times in the same compilation */ function invalidateDependenciesWithDiskModifications(evaluator: Evaluator): void { const modificationTimes = getFileModificationTimes(); const modifiedDependencies: string[] = []; for (const [dep, lastModificationTime] of modificationTimes) { try { const currentModificationTime = statSync(dep).mtimeMs; if (currentModificationTime !== lastModificationTime) { modifiedDependencies.push(dep); modificationTimes.set(dep, currentModificationTime); } } catch { modifiedDependencies.push(dep); modificationTimes.delete(dep); } } if (modifiedDependencies.length > 0) { evaluator.invalidate(...modifiedDependencies); } } /** Snapshots the current modification times of `deps` so the next evaluation can detect changes. */ function recordModificationTimes(deps: string[]): void { const modificationTimes = getFileModificationTimes(); for (const dep of deps) { try { modificationTimes.set(dep, statSync(dep).mtimeMs); } catch { /* file may not exist */ } } } /** * Creates an evaluator function scoped to a single loader invocation. * * Before evaluation, checks whether any previously recorded dependency has * changed on disk and invalidates the evaluator cache if so. After a * successful evaluation, records fresh modification times and notifies the caller of * every transitive dependency via `onDependency`. * * @returns A function that evaluates a yak module and returns its exports. * The returned function accepts: * - `modulePath` — absolute path to the module to evaluate. * - `onDependency` — called once per transitive dependency so the loader * can register file watchers with turbopack. * @throws If the evaluated module throws at runtime — the original stack * trace from the worker thread is preserved. */ export async function createCompilationEvaluator(): Promise< (modulePath: string, onDependency?: (dep: string) => void) => Promise<Record<string, unknown>> > { const evaluator = await getEvaluatorSingleton(); invalidateDependenciesWithDiskModifications(evaluator); return async (modulePath, onDependency?) => { const result = await evaluator.evaluate(modulePath); if (!result.ok) { const error = new Error(result.error.message); if (result.error.stack) { error.stack = result.error.stack; } throw error; } // Store times of newly discovered dependencies for next compilation recordModificationTimes(result.dependencies); if (onDependency) { for (const dep of result.dependencies) { onDependency(dep); } } return result.value; }; }