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:
- The compile time part
- The runtime part
- 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.
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.
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.
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.
"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.
//
// 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.
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.
// 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;
}