Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .changeset/wyw-alpha-static-eval.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
"@linaria/atomic": major
"@linaria/core": major
"@linaria/babel-plugin-interop": major
"linaria": major
"@linaria/postcss-linaria": major
"@linaria/react": major
"@linaria/server": major
"@linaria/stylelint-config-standard-linaria": major
"@linaria/stylelint": major
---

Release Linaria 8 with WyW 2.x stable dependencies and Node.js 22.12+ support.

Linaria processors now expose WyW 2 static evaluation semantics, allowing the default `eval.strategy: "hybrid"` mode to resolve statically provable values before falling back to the evaluator. This keeps existing dynamic/runtime-only interpolation support while reducing evaluator work for values that can be resolved from static bindings and imports.

Performance and stability:

With the default hybrid mode, a large share of style computation now moves out of runtime-like evaluator execution and into analytical static evaluation. This reduces evaluator startup and module execution work, makes builds less sensitive to runtime-only side effects, and gives the pipeline a more stable foundation for further optimization. It also opens the path for substantially larger speedups as WyW moves more of the pipeline to Rust; see the [WyW roadmap](https://wyw-in-js.dev/stability#roadmap-high-level) for more detail.

Migration notes:

- Node.js 22.12 or newer is required.
- `@wyw-in-js/transform` is updated to 2.0.2 to avoid duplicate CSS emitted for same-file processor bindings referenced from another processor template inside a local scope and to keep mixed static/processor object-member interpolations statically resolvable.
- Top-level `evaluate` config should be migrated to `eval.strategy`. Use `execute` for evaluator-only compatibility, keep the default `hybrid` for static-first resolution with fallback, or use `static` to reject evaluator fallback.
- Babel config and Babel resolver plugins are no longer used as an implicit module-resolution fallback during WyW evaluation. Move build-time alias handling to WyW configuration with `eval.customResolver`, `eval.resolver`, or `staticBindings`.
- CSS rule emission order may change for cascade ties with identical specificity because WyW 2 uses the Oxc/static-first pipeline and can preserve/process imports differently. Make precedence explicit with selector specificity, composition, or source structure where order matters.
8 changes: 4 additions & 4 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest]
node-version: [20.x, 22.x]
node-version: [22.x, 24.x]
include:
- os: windows-latest
node-version: 20.x
node-version: 22.x

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
Expand Down Expand Up @@ -51,10 +51,10 @@ jobs:
- name: Install and prepare
run: pnpm install --frozen-lockfile --strict-peer-dependencies
- name: ESLint
if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20.x'
if: matrix.os == 'ubuntu-latest' && matrix.node-version == '22.x'
run: pnpm lint
- name: TSLint
if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20.x'
if: matrix.os == 'ubuntu-latest' && matrix.node-version == '22.x'
run: pnpm turbo run test:dts
- name: Tests
run: pnpm turbo run test
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ jobs:
version: 9
run_install: false

- name: Setup Node.js 20.x
- name: Setup Node.js 22.x
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 20.x
node-version: 22.x
registry-url: 'https://registry.npmjs.org'

- name: Upgrade npm for trusted publishing (OIDC)
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/site-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ jobs:
version: 9
run_install: false

- name: Use Node.js 20.x
- name: Use Node.js 22.x
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 20.x
node-version: 22.x

- name: Install and prepare
run: pnpm install --frozen-lockfile --strict-peer-dependencies
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ See [Configuration](https://wyw-in-js.dev/configuration) to customize how Linari

Linaria relies on WyW (`@wyw-in-js/*`) to evaluate your modules at build time and extract CSS. If you hit issues like slow builds, invalidation storms, or unexpected code being executed during the build, it’s usually related to the WyW evaluation model and how your modules are structured.

Linaria 7 requires Node.js `>=20` (WyW 1.x enforces this via `engines`).
Linaria 8 requires Node.js `>=22.12.0` (aligned with the WyW 2 / Oxc dependency graph). WyW 2 defaults to `eval.strategy: "hybrid"`, so statically provable values are resolved before falling back to the evaluator for dynamic values.

If your build depends on evaluator-only side effects or exact CSS rule order ties, review the Linaria 8 migration notes in [docs/MIGRATION_GUIDE.md](./docs/MIGRATION_GUIDE.md).

See https://wyw-in-js.dev/stability for practical guidance and common pitfalls.

Expand Down
25 changes: 25 additions & 0 deletions babel-plugins/transform-import-meta.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module.exports = ({ types: t }) => ({
name: 'transform-import-meta-url',
visitor: {
MemberExpression(path) {
const { object, property, computed } = path.node;
if (computed) return;
if (!t.isMetaProperty(object)) return;
if (!t.isIdentifier(object.meta, { name: 'import' })) return;
if (!t.isIdentifier(object.property, { name: 'meta' })) return;
if (!t.isIdentifier(property, { name: 'url' })) return;
const requireUrl = t.callExpression(
t.memberExpression(t.identifier('module'), t.identifier('require')),
[t.stringLiteral('url')]
);
const pathToFileURL = t.memberExpression(
requireUrl,
t.identifier('pathToFileURL')
);
const fileUrl = t.callExpression(pathToFileURL, [
t.identifier('__filename'),
]);
path.replaceWith(t.memberExpression(fileUrl, t.identifier('href')));
},
},
});
3 changes: 3 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const commonJSTargets = {
node: '12',
};

const transformImportMeta = require('./babel-plugins/transform-import-meta.cjs');

module.exports = {
presets: ['@babel/preset-typescript'],
plugins: ['@babel/plugin-proposal-explicit-resource-management'],
Expand Down Expand Up @@ -47,6 +49,7 @@ module.exports = {
],
'@babel/preset-typescript',
],
plugins: [transformImportMeta],
},
},
overrides: [
Expand Down
16 changes: 12 additions & 4 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,22 @@ Example `wyw-in-js.config.js`:

```js
module.exports = {
evaluate: true,
eval: {
strategy: 'hybrid',
},
displayName: false,
};
```

## Options

- `evaluate: boolean` (default: `true`):
- `eval.strategy: "hybrid" | "execute" | "static"` (default: `"hybrid"`):

Controls how WyW resolves values used in CSS interpolations. The recommended default, `"hybrid"`, uses static-first resolution: WyW first tries to prove values from Linaria processor static semantics, `staticBindings`, and statically resolvable imports, then falls back to the evaluator for values that cannot be proven statically.

Enabling this will evaluate dynamic expressions in the CSS. You need to enable this if you want to use imported variables in the CSS or interpolate other components. Enabling this also ensures that your styled components wrapping other styled components will have the correct specificity and override styles properly.
Use `"execute"` when you need evaluator-only compatibility, for example while migrating code that relies on build-time side effects or exact import execution order. Use `"static"` as a strict validation mode when fallback to the evaluator should be rejected. The older top-level `evaluate` option should be migrated to `eval.strategy` in Linaria 8 / WyW 2.

Evaluated values are included in the generated CSS. Since fallback evaluation runs at build time in Node.js, avoid browser-only APIs, unavailable runtime globals, Node native modules such as `fs`, and side effects in evaluated expressions.

- `displayName: boolean` (default: `false`):

Expand Down Expand Up @@ -445,7 +451,9 @@ module.exports = {
[
'@linaria',
{
evaluate: true,
eval: {
strategy: 'hybrid',
},
displayName: process.env.NODE_ENV !== 'production',
},
],
Expand Down
4 changes: 1 addition & 3 deletions docs/FEATURE_FLAGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,4 @@ The `softErrors` is disabled by default. It is designed to provide a more lenien

# 'useBabelConfigs' Feature

The `useBabelConfigs` feature is enabled by default. If it is enabled, Linaria will try to resolve the `.babelrc` file for each processed file. Otherwise, it will use the default Babel configuration from `babelOptions` in the configuration.

Please note that the default value of `useBabelConfigs` will be changed to `false` in the next major release.
Linaria 8 uses the WyW 2 static-first pipeline and no longer treats Babel configs or Babel resolver plugins as an implicit module-resolution fallback during evaluation. If evaluated code imports aliased specifiers, configure WyW resolution explicitly with `eval.customResolver`, `eval.resolver`, or `staticBindings`.
12 changes: 6 additions & 6 deletions docs/HOW_IT_WORKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ const Container = styled.h1`

We support this usage because it allows you to use a library such as [polished.js](https://polished.js.org) which outputs object based styles along with Linaria.

If you've configured the plugin to evaluate expressions with `evaluate: true` (default), any dynamic expressions we encounter will be evaluated during the build-time in a sandbox, and the result will be included in the CSS. Since these expressions are evaluated at build time in Node, you cannot use any browser specific APIs or any API which is only available in runtime. Access to Node native modules such as `fs` is also not allowed inside the sandbox to prevent malicious scripts. In addition, to achieve consistent build output, you should also avoid doing any side effects in these expressions and keep them pure.
By default, Linaria uses WyW's `eval.strategy: "hybrid"` mode. WyW first tries to resolve values statically from Linaria processor metadata, `staticBindings`, and statically resolvable imports. If a value cannot be proven statically, WyW falls back to build-time evaluation and includes the result in the generated CSS. Since fallback evaluation runs in Node.js, you cannot use browser-specific APIs, runtime-only globals, or Node native modules such as `fs`. To keep build output consistent, avoid side effects in evaluated expressions and keep them pure.

You might want to skip evaluating a certain interpolation if you're using a browser API, a global variable which is only available at runtime, or a module which breaks when evaluating in the sandbox for some reason. To skip evaluating an interpolation, you can always wrap it in a function, like so:

Expand All @@ -162,13 +162,13 @@ But keep in mind that if you're doing SSR for your app, this won't work with SSR

### Evaluators

Linaria can use different strategies for evaluating the interpolated values.
Currently, we have two built-in strategies:
Linaria relies on WyW strategies for resolving interpolated values:

- `extractor` was the default strategy in `1.x` version. It takes an interpolated expression, finds all the referenced identifiers, gets all its declarations, repeats cycle for all identifiers in found declarations, and then constructs a new tree of statements from all found declarations. It's a pretty simple strategy, but it significantly changes an evaluated code and doesn't work for non-primitive js-constructions.
- `shaker` was introduced as an option in `1.4` and became the default in `2.0` version. In contrast to `extractor`, `shaker` tries to find all irrelevant code and cuts it out of the file. As a result, interpolated values can be defined without any restrictions.
- `hybrid` is the default in Linaria 8 / WyW 2. It resolves statically provable values without starting the evaluator, then falls back to evaluator execution for unresolved dynamic values.
- `execute` uses evaluator-only behavior and is the compatibility escape hatch for projects that depend on build-time side effects or exact import execution order.
- `static` is a strict validation mode that rejects fallback to evaluator execution.

If an interpolated value or one of its dependencies is imported from another module, that module will be also processed with an evaluator (the implementation of evaluator will be chosen by matching `rules` from [the Linaria config](./CONFIGURATION.md#options)).
If an interpolated value or one of its dependencies is imported from another module, WyW processes that module according to the configured strategy. In `hybrid` mode, the imported value may be resolved statically; otherwise WyW falls back to the evaluator selected by matching `rules` from [the Linaria config](./CONFIGURATION.md#options).

Sometimes it can be useful to implement your own strategy (it can be just a mocked version of some heavy or browser-only library). You can do it by implementing `Evaluator` function:

Expand Down
28 changes: 28 additions & 0 deletions docs/MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
# Migration Guide

# 8.x from 7.x

## For Users

Linaria 8 updates the WyW toolchain (`@wyw-in-js/*`) to 2.x stable releases. This is a major release because WyW 2 changes the build-time evaluation pipeline and raises the minimum Node.js version.

- Linaria 8 requires Node.js 22.12+ (aligned with the WyW 2 / Oxc dependency graph).
- The default evaluation mode is WyW's `eval.strategy: "hybrid"`. It resolves statically provable values first using Linaria processor static semantics, `staticBindings`, and statically resolvable imports, then falls back to the evaluator for values that cannot be proven statically.
- Top-level `evaluate` config should be migrated to `eval.strategy`. Use `execute` for evaluator-only compatibility, keep the default `hybrid` for static-first resolution with evaluator fallback, or use `static` to reject evaluator fallback.
- Babel config and Babel resolver plugins are no longer used as an implicit module-resolution fallback during WyW evaluation. If evaluated code imports aliased specifiers, move those aliases to WyW configuration with `eval.customResolver`, `eval.resolver`, or `staticBindings`.
- If your project relies on build-time side effects or on the exact order in which evaluated imports execute, compare the generated CSS/JS output after upgrading and use `eval.strategy: "execute"` where evaluator-only behavior is required.
- CSS rule emission order can change for cascade ties with identical specificity. WyW 2 uses the Oxc/static-first pipeline and can preserve or process imports differently, so make precedence explicit through selector specificity, composition, or source structure where order matters.
- Review https://wyw-in-js.dev/migration/v2 and https://wyw-in-js.dev/stability for the WyW 2 evaluation model, debugging notes, and performance guidance.

Example evaluator-only compatibility config:

```js
module.exports = {
eval: {
strategy: 'execute',
},
};
```

## For Custom Processor Developers

Linaria processors now expose WyW 2 static evaluation semantics. Custom processors that integrate with WyW's static-first path should implement the optional static processor contract in `@wyw-in-js/processor-utils`; unresolved values can still fall back to the evaluator in `hybrid` mode.

# 7.x from 6.x

## For Users
Expand Down
2 changes: 1 addition & 1 deletion examples/astro-solid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"@astrojs/solid-js": "^1.2.3",
"@babel/core": "^7.23.5",
"@linaria/core": "workspace:^",
"@wyw-in-js/vite": "^1.0.6",
"@wyw-in-js/vite": "2.0.0",
"astro": "^1.6.10",
"solid-js": "^1.6.2",
"vite": "^3",
Expand Down
2 changes: 1 addition & 1 deletion examples/esbuild/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"linaria-website": "workspace:^"
},
"devDependencies": {
"@wyw-in-js/esbuild": "^1.0.6",
"@wyw-in-js/esbuild": "2.0.0",
"esbuild": "^0.15.16"
},
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion examples/rollup/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"@rollup/plugin-commonjs": "^25.0.4",
"@rollup/plugin-image": "^3.0.2",
"@rollup/plugin-node-resolve": "^15.2.1",
"@wyw-in-js/rollup": "^1.0.6",
"@wyw-in-js/rollup": "2.0.0",
"rollup": "^4.0.0",
"rollup-plugin-css-only": "^4.3.0"
},
Expand Down
8 changes: 8 additions & 0 deletions examples/rollup/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ export default {
image(),
wyw({
sourceMap: process.env.NODE_ENV !== 'production',
eval: {
strategy: 'static',
},
staticBindings: {
'../../assets/linaria-logomark.svg?url': {
default: '../../assets/linaria-logomark.svg',
},
},
// Rollup can deadlock when WyW resolves imports during transform.
serializeTransform: false,
}),
Expand Down
2 changes: 1 addition & 1 deletion examples/vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.2.1",
"@vitejs/plugin-react": "^2.1.0",
"@wyw-in-js/vite": "^1.0.6",
"@wyw-in-js/vite": "2.0.0",
"vite": "^3.2.10"
},
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion examples/vpssr-linaria-solid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"license": "ISC",
"dependencies": {
"@linaria/core": "workspace:^",
"@wyw-in-js/vite": "^1.0.6",
"@wyw-in-js/vite": "2.0.0",
"babel-preset-solid": "^1.6.2",
"compression": "^1.7.4",
"express": "^4.20.0",
Expand Down
2 changes: 1 addition & 1 deletion examples/webpack5/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
},
"devDependencies": {
"@babel/core": "^7.23.5",
"@wyw-in-js/webpack-loader": "^1.0.6",
"@wyw-in-js/webpack-loader": "2.0.0",
"babel-loader": "^9.1.0",
"cross-env": "^7.0.3",
"css-hot-loader": "^1.4.4",
Expand Down
8 changes: 7 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
module.exports = {
cache: false,
testEnvironment: 'node',
collectCoverageFrom: ['src/*.ts'],
transformIgnorePatterns: ['node_modules/(?!@linaria)'],
transformIgnorePatterns: [
'node_modules/(?!.*(@linaria|@wyw-in-js|oxc-parser|oxc-resolver|oxc-transform|@oxc-project))',
],
transform: {
'^.+\\.[tj]sx?$': ['babel-jest', { rootMode: 'upward' }],
},
testPathIgnorePatterns: ['/__utils__/'],
};
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
"@changesets/cli": "^2.22.0",
"@commitlint/config-conventional": "^8.3.4",
"@definitelytyped/dtslint": "^0.0.176",
"@emnapi/core": "^1.10.0",
"@emnapi/runtime": "^1.10.0",
"@types/jest": "^28.1.0",
"@types/node": "^17.0.39",
"@types/resolve": "^1.20.6",
Expand Down Expand Up @@ -82,7 +84,7 @@
"typescript": "^5.2.2"
},
"engines": {
"node": ">=20.0.0",
"node": ">=22.12.0",
"pnpm": "^9.0.0"
},
"packageManager": "pnpm@9.15.9+sha256.cf86a7ad764406395d4286a6d09d730711720acc6d93e9dce9ac7ac4dc4a28a7"
Expand Down
6 changes: 3 additions & 3 deletions packages/atomic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@
"dependencies": {
"@linaria/core": "workspace:^",
"@linaria/react": "workspace:^",
"@wyw-in-js/processor-utils": "^1.0.4",
"@wyw-in-js/shared": "^1.0.4",
"@wyw-in-js/processor-utils": "2.0.0",
"@wyw-in-js/shared": "2.0.0",
"known-css-properties": "^0.24.0",
"postcss": "^8.4.31",
"stylis": "^4.3.0",
Expand All @@ -72,7 +72,7 @@
}
},
"engines": {
"node": ">=20.0.0"
"node": ">=22.12.0"
},
"publishConfig": {
"access": "public"
Expand Down
7 changes: 5 additions & 2 deletions packages/atomic/src/processors/css.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { SourceLocation } from '@babel/types';
import type { Rules, ValueCache } from '@wyw-in-js/processor-utils';
import type {
Rules,
SourceLocation,
ValueCache,
} from '@wyw-in-js/processor-utils';
import { logger } from '@wyw-in-js/shared';

import CssProcessor from '@linaria/core/processors/css';
Expand Down
7 changes: 5 additions & 2 deletions packages/atomic/src/processors/styled.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { SourceLocation } from '@babel/types';
import type { Rules, ValueCache } from '@wyw-in-js/processor-utils';
import type {
Rules,
SourceLocation,
ValueCache,
} from '@wyw-in-js/processor-utils';
import { logger, hasEvalMeta } from '@wyw-in-js/shared';

import type { IProps } from '@linaria/react/processors/styled';
Expand Down
Loading
Loading