Skip to content

How does it work?

This is for the curious minds who want to know how next-yak works under the hood and it's not necessary to understand it in order to use it. A live playground to see the code transformation can be found here.

The basics

next-yak has 3 parts:

  1. The compile time part
  2. The runtime part
  3. Optional: Context

The compile time part is responsible for extracting styles and transforming your code into something that uses the runtime part. The runtime part is responsible for merging styles and props.

The compile time part

next-yak uses two webpack loaders to transform your code. The first loader is responsible for transforming the usages of the tagged template literals (like styled and css), the second loader reads the results from the first loader, resolves cross module dependencies and writes the finished css in a css module file.

The first loader (ts-loader.ts)

The first loader takes in the source code and transforms the tagged template literals into next-yak runtime function calls with the imported styles (from the final css module output) as arguments. It also transforms the dynamic parts of the tagged template literals into function calls with the properties as argument and adds it as css value to the style object.

Add style import

The first step of the transformation is to add the import statement for the styles. These styles don't exist yet, but will be generated by the second loader.

output
import __styleYak from "./page.yak.module.css!=!./page?./page.yak.module.css";

This strange looking import string is a poorly documented feature of webpack. It uses the Inline matchResource to instrument webpack to use the loaders as if it was a normal .module.css file that points to the current file (./page). In order that this import statement works in next.js, we need to add a query parameter that ends with .module.css, as this is the way next.js recognizes CSS modules.

So that means ./page.yak.module.css is a virtual file that is generated by the second loader and contains the styles of the current file. ./page.yak.module.css!=!./page says that the second loader (matching .yak.module.css files) should be used with the current file as input. Lastly ?./page.yak.module.css is the query parameter that tells next.js that this is a CSS module and it's loaders should be used.

Transform tagged template literals

As the next step, the tagged template literals are transformed into next-yak runtime function calls. The static CSS parts are passed as references to the class names from the "not yet generated" css module file.

input
import { styled } from "next-yak";
 
const Button = styled.button`
  font-size: 2rem;
  font-weight: bold;
  color: blue;
  &:hover {
    color: red;
  }
`;

The dynamic parts are preserved and apply the classes or CSS variables at runtime. There are two different types of dynamic parts: The first type is the dynamic CSS property value (color in the following example) and the second type is the dynamic CSS property itself (background in the following example).

The property value is transformed into a CSS variable and set on the element itself directly based on the functions return value.

The property itself is transformed into a class name that is referenced during runtime when the function returns them.

input
import { styled, css } from "next-yak";
 
const Button = styled.button`
	font-size: 2rem;
	font-weight: bold;
	color: ${props => props.$primary ? "white" : "blue"};
	${props => props.$primary ? css`background: black;` : css`background: white;`}
    &:hover {
		color: ${props => props.$primary ? "red" : "blue"};
	}
`;

The second loader (css-loader.ts)

The second loader takes the output of the first loader and resolves the cross module dependencies. The result is valid CSS that will be written to the context.

Resolve cross module dependencies

To resolve cross module dependencies, the first loader generates a unique string to signal the second loader that this is a dependency with a specific name. The second loader then reads the generated CSS from the first loader and resolves the dependencies. Once the final css is generated the loader hands it over as a CSS module file so it can be processed by Next.js like a normal CSS module file.

The runtime part

The runtime part is responsible for merging styles and props. It's a simple function that takes in the styles and the props and returns the merged styles.

See: Styled function

A lot of this code is types and comments to indicate why certain things are done. The actual runtime code is very small.

Context

The yak theme context allows you to configure the theme of your yak application. It works for react-server components and normal react components.

The first part of the puzzle is to export two different modules for the server and the client.

"./context": {
	"react-server": {
		"import": "./dist/context/index.server.js",
		"require": "./dist/context/index.server.cjs"
	},
	"default": {
		"import": "./dist/context/index.js",
		"require": "./dist/context/index.cjs"
	}
}

For the client components, we use the default context from react.

client: index.tsx
"use client";
//
// This file is the client component (browser & ssr) version of index.server.tsx
//
import React, { ReactNode, createContext, useContext } from "react";
 
export interface YakTheme {}
 
/**
 * The yak theme context
 * @see https://github.com/jantimon/next-yak/blob/main/packages/next-yak/runtime/context/README.md
 */
const YakContext = createContext<YakTheme>({});
 
/**
 * Returns the current yak theme context
 *
 * @see https://github.com/jantimon/next-yak/blob/main/packages/next-yak/runtime/context/README.md
 */
export const useTheme = (): YakTheme => useContext(YakContext);
 
/**
 * Yak theme context provider
 *
 * @see https://github.com/jantimon/next-yak/blob/main/packages/next-yak/runtime/context/README.md
 */
export const YakThemeProvider = ({
  children,
  theme = {},
}: {
  children: ReactNode;
  theme?: YakTheme;
}) => <YakContext.Provider value={theme}>{children}</YakContext.Provider>;

For the server components, we use a module that exports exactly the same API as the one for client components, but with a different implementation where we use the cache function from react to cache the result of the context.

server: index.server.tsx
//
// This file is the react-server component version of index.tsx
//
 
// @ts-ignore - in the current @types/react "cache" is not typed
import React, { ReactNode, cache } from "react";
import { YakThemeProvider as YakThemeClientProvider } from "./index.js";
 
// the following import might be changed by
// the user config in withYak to point to their own
// context
import { getYakThemeContext } from "next-yak/context/baseContext";
 
export const useTheme = cache(() => getYakThemeContext());
export const YakThemeProvider = ({ children }: { children: ReactNode }) => {
  return (
    <YakThemeClientProvider theme={useTheme()}>
      {children}
    </YakThemeClientProvider>
  );
};

The getYakThemeContext function is imported from the user's yak.context.ts file. This file is a regular typescript file that exports a function that returns the theme context. You can use every api that works with server components like headers or cookies and use them to determine your values for the theme context.

yak.context.ts
import { cookies } from 'next/headers'
import { cache } from "react";
 
const hasHighContrast = cache(() => {
    const cookieStore = cookies()
    return cookieStore.get("highContrast")?.value === "true"
});
 
export function getYakThemeContext() {
    return {
        highContrast: hasHighContrast()
    }
}
 
declare module "next-yak" {
    export interface YakTheme extends ReturnType<typeof getYakThemeContext> { }
}

The second part of the puzzle is that the withYak function changes to what file webpack resolves the context so that it points to the individual yak.context.ts file in the root directory.

withYak/index.ts
// With the following alias the internal next-yak code 
// is able to import a context which works for server components
const yakContext = resolveYakContext(yakOptions.contextPath, webpackConfig.context);
if (yakContext) {
	webpackConfig.resolve.alias['next-yak/context/baseContext'] = yakContext;
}