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
text/typescript
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,
});
}