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

163 lines (148 loc) 5.98 kB
import { transform as swcTransform } from "@swc/core"; import { createRequire } from "node:module"; import { dirname } from "node:path"; import type { LoaderContext } from "webpack"; import { parseModule } from "../cross-file-resolver/parseModule.js"; import { resolveCrossFileConstant } from "../cross-file-resolver/resolveCrossFileConstant.js"; import type { YakConfigOptions } from "../withYak/index.js"; import { createDebugLogger } from "./lib/debugLogger.js"; import { extractCss } from "./lib/extractCss.js"; import { parseExports } from "./lib/resolveCrossFileSelectors.js"; const universalRequire = typeof require === "undefined" ? createRequire(import.meta.url) : require; const yakSwcPluginPath = universalRequire.resolve("yak-swc"); /** * This loader transforms styled-components styles to a static data-url import * The compile-time nexy-yak transformation takes javascript/typescript as input, * strips all inline css code and adds the css as static css urls * e.g.: `import "data:text/css;base64,"` */ export default async function cssExtractLoader( this: LoaderContext<{ yakOptions: YakConfigOptions; yakPluginOptions: any }>, code: string, sourceMap: string | undefined, ): Promise<string | void> { const callback = this.async(); // process only files which include next-yak for maximal compile performance if (!code.includes("next-yak")) { return callback(null, code, sourceMap); } const { yakPluginOptions, yakOptions: { experiments }, } = this.getOptions(); const debugLog = createDebugLogger(experiments?.debug, this.rootContext); const resolveTurbopack = this.getResolve({}); const transform = createTransform(yakPluginOptions, yakSwcPluginPath); const resolveFn = (specifier: string, importer: string) => { return new Promise<string>((resolve, reject) => { resolveTurbopack(dirname(importer), specifier, (err, result) => { if (err) return reject(err); if (!result) return reject(new Error(`Could not resolve ${specifier}`)); resolve(result); }); }); }; const crossFileDeps = new Set<string>(); let evaluate: | Awaited<ReturnType<typeof import("./turbo-evaluator.js").createCompilationEvaluator>> | undefined; const fsReadFile = (filePath: string) => { crossFileDeps.add(filePath); return new Promise<string>((resolve, reject) => this.fs.readFile(filePath, "utf-8", (err, result) => { if (err) return reject(err); if (!result) return reject(new Error(`File not found: ${filePath}`)); resolve(result); }), ); }; try { const result = await transform(code, this.resourcePath, this.rootContext, sourceMap); debugLog("ts", result.code, this.resourcePath); let css = extractCss(result.code, "Css"); debugLog("css", css, this.resourcePath); const { resolved } = await resolveCrossFileConstant( { parse: (modulePath) => { return parseModule( { transpilationMode: "Css", extractExports: async (modulePath) => { const sourceContents = await fsReadFile(modulePath); return parseExports(sourceContents); }, getTransformed: async (modulePath) => { const sourceContent = await fsReadFile(modulePath); return transform(sourceContent, modulePath, this.rootContext); }, evaluateYakModule: async (modulePath: string) => { crossFileDeps.add(modulePath); /* * Turbopack doesn't let us know when a compilation start so by using a singleton evaluator we * we can at least ensture that we scan for file modifications only once per loader call */ evaluate ??= await ( await import("./turbo-evaluator.js") ).createCompilationEvaluator(); return evaluate(modulePath, (dep) => crossFileDeps.add(dep)); }, }, modulePath, ); }, resolve: resolveFn, }, this.resourcePath, css, ); // Register cross-file dependencies so turbopack re-runs this loader // when any dependency changes (analogous to webpack's this.addDependency) for (const dep of crossFileDeps) { this.addDependency(dep); } const dataUrl = result.code.split("\n").find((line) => line.includes("data:text/css;base64"))!; const codeWithCrossFileResolved = result.code.replace( dataUrl, `import "data:text/css;base64,${Buffer.from(resolved).toString("base64")}"`, ); debugLog("css-resolved", resolved, this.resourcePath); return callback(null, codeWithCrossFileResolved, result.map); } catch (error) { // Register cross-file dependencies even on error so turbopack re-runs // this loader when a broken dependency is fixed. for (const dep of crossFileDeps) { this.addDependency(dep); } return callback(error instanceof Error ? error : new Error(String(error))); } } function createTransform(yakPluginOptions: any, yakSwcPluginPath: string) { return (data: string, modulePath: string, rootPath: string, sourceMap?: any) => // https://github.com/vercel/next.js/blob/canary/packages/next/src/build/webpack/loaders/next-swc-loader.ts#L143 swcTransform(data, { filename: modulePath, inputSourceMap: sourceMap, sourceMaps: true, sourceFileName: modulePath, sourceRoot: rootPath, jsc: { experimental: { plugins: [[yakSwcPluginPath, yakPluginOptions]], }, transform: { react: { runtime: "preserve", }, }, target: "es2022", loose: false, minify: { compress: false, mangle: false, }, preserveAllComments: true, }, minify: false, isModule: true, }); }