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.
Playground
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 an SWC plugin and an additional webpack loader to transform your code. The plugin is responsible for transforming the
usages of the tagged template literals (like styled
and css
), while the loader reads the results from the plugin, resolves cross module dependencies and writes the finished css in a css-modules file.
The SWC plugin
See: yak-swc
The SWC plugin 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.
Change import
The first step of the transformation is to change the import statement from styled
and css
to the next-yak
runtime.
The normal import just references a kind of stub behavior to be used in tests without the need to have a transpilation step.
Add style import
The next 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 webpack loader at a later stage.
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.
This recognition is not always working correctly as seen with the various issues & PR's we created on the next.js repository like this one, this one or this one.
So that means ./page.yak.module.css
is a virtual file that is afterwards generated by the webpack loader and contains the
styles of the current file. ./page.yak.module.css!=!./page
says that the 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
In another 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-modules file.
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.
Transform CSS
The next step is to extract the CSS to a comment in the source code. This is useful because the transformation should happen as quickly as possible and this is way easier and faster with the SWC plugin instead of the webpack loader and we're already transforming the current file.
The comment gets marked with /*YAK Extracted CSS:
so that the loader can easily find it and extract the CSS.
Additionally, the comment is marked with /*#__PURE__*/
so that the minifier knows that behind the runtime function call there is no side effect and can move it around.
The webpack loader
See: 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 SWC plugin 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-modules file.
The runtime part
See: styled.tsx
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.
A lot of this code is types and comments to indicate why certain things are done. The actual runtime code is very small.
Context
As of next.js >15.0.0, this feature doesn't work with server components anymore.
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.
For the client components, we use the default context from react.
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.
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.
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.