Imported from lucasols/vindur (
AGENTS.md). Install upstream withnpx skills add lucasols/vindur. Copyright stays with the author.
This file provides guidance to Code Agents when working with code in this repository.
Project Structure
This is a monorepo using pnpm workspaces for Vindur - a compile-time CSS-in-JS library focused on performance. The project is organized into:
lib/- Core library package with Babel transform logicapp-test/- React test application using Vite for development and testinge2e-tests/- End-to-end tests using Playwrightvite-plugin/- Vite plugin for Vindureslint-plugin/- ESLint plugin for Vindurnotes/spec.md- Feature specifications and roadmap
Running ts code
The node version installed supports running ts code directly. No build step is needed. Just use node to run ts code
Development Commands
Important: Use pnpm only, never npx.
DO NOT USE npx to run commands, NEVER.
From Root
pnpm test-all # Run tests for all packages
pnpm build-all # Build all packages
pnpm lint-all # Run eslint + tsc in all packages
pnpm tsc-all # TypeScript compilation check in all packages
Library (lib/)
cd lib
pnpm test # Run tests
pnpm lint # TypeScript + ESLint
pnpm build # Build library
pnpm tsc # TypeScript compilation check
pnpm build # Build library
Vite Plugin (vite-plugin/)
cd vite-plugin
pnpm build # Build plugin
pnpm lint # TypeScript + ESLint
pnpm tsc # TypeScript compilation check
pnpm build # Build plugin
E2E Tests (e2e-tests/)
cd e2e-tests
pnpm test # Run e2e tests
pnpm eslint # Run eslint
pnpm lint # TypeScript + ESLint
pnpm tsc # TypeScript compilation check
Architecture
Core Transform Logic
The library centers around a Babel-based transform function in lib/src/transform.ts that:
- Extracts CSS from template literals (
cssfunction calls) - Generates hashed class names
- Returns both transformed JavaScript and extracted CSS
Build System
- tsup for library building (ESM + CJS outputs)
- Vite for test app development
- ESLint with TypeScript integration
- pnpm workspaces for monorepo management
Key Features (Planned)
cssfunction for template literal stylesstyled.*component functions- Variable interpolation and mixins
- Scoped classes and CSS variables
- JSX
cxandcssprops - Global styles and media queries
Code Style Guidelines
- Use types instead of interfaces
Plugin error handling
-
The plugin should be strict about errors in transform process, using the 'Fail Fast' approach, it should either be 100% successful or fail if a error or unexpected behavior, or unhandled case occurs, no fallback values or partial results should be returned. Only use warnings for optimization suggestions or such as removing unused code, etc.
-
Transform errors should use
TransformErrorclass, with properly setlocandmessage -
IMPORTANT: Do not throw TransformError without a proper location! If is not possible to provide the exact error location, use the nearest available location.
Warnings
Warnings are used to flag potential issues or suggest optimizations without failing the build. They are surfaced to the user as ESLint warnings.
Use cases for warnings:
- Optimization suggestions: e.g., suggesting a more efficient way to write a style.
- Unused code: e.g., a defined
styledcomponent that is never used. - Potential issues: e.g., a CSS property that might not be supported in all target browsers.
Implementation:
- Warnings must be created using the
Warningclass fromlib/src/custom-errors.ts. - Each warning must include:
message: A clear and concise description of the issue.loc: The BabelSourceLocationof the code causing the warning. This is crucial for ESLint to highlight the correct code.
- Warnings should be propagated to the user via the
onWarningcallback.
Typesafety
- Do not use
any - Do not use
as Typecasts, except foras const - Do not use non-null assertions (
!) - Avoid using optional parameters, use default values or
| undefinedinstead
Code Organization
- Abstract redundant types into a single type
- Abstract redundant code into a single function
- Split up large files (+500 lines) into smaller files
- Comments and empty lines are not counted towards the line count
- Split up large or complex functions into smaller functions
- Do not use barrel files
- NEVER use re-exports
VERY IMPORTANT CODE QUALITY GUIDELINES
- NEVER use
eslint-disable,eslint-disable-next-line, or similar comments to disable eslint rules, fix the underlying issue causing the rule to be violated instead. - NEVER use
@ts-expect-erroror@ts-ignoreto skip type errors, fix the underlying issue causing the type error instead. - NEVER override eslint rules with comments (e.g:
/* eslint max-lines: ["error", 700] */), fix the underlying issue causing the rule to be violated instead.
Testing
Tests use Vitest and are located in lib/tests/. Run tests from the lib directory:
# Must be in the lib directory
cd lib
# Run all tests
pnpm test
# Run tests for a specific file
pnpm test tests/filename.test.ts
# Run a specific test matching a pattern
pnpm test tests/filename.test.ts -t "test name pattern"
# Use additional arguments supported by Vitest
pnpm test [...args]
-
Prefer using
toMatchInlineSnapshotwhen possible -
Do not update snapshots via
vitest run --u, update them manually -
If there are too many snapshots to update manually, ask for the user to update them
-
Do not use top level
describein tests to group all tests in a file, use describe to group tests only -
Use
compactSnapshotwhen possible for inline snapshots of objects and arrays, it produces more readable snapshots in yaml format
End-to-end tests use Playwright and are located in e2e-tests/. Run tests from the e2e-tests directory:
# Must be in the e2e-tests directory
cd e2e-tests
# Run all tests
pnpm test
# Run tests for a specific file
pnpm test tests/filename.spec.ts
# Run a specific test matching a pattern
pnpm test tests/filename.spec.ts --grep "test name pattern"
# Run with debug logs
DEBUG=1 pnpm test ...
# Use additional arguments supported by Playwright
pnpm test [...args]
IMPORTANT: Playwright tests already have a 10s timeout configured
E2E Test Structure
E2E tests follow a consistent pattern for performance and maintainability:
Key Guidelines:
- Prefer reusing
startEnvcalls, use new ones only if the test requires a different environment - Serial execution with
test.describe.configure({ mode: 'serial' }) - Shared setup/teardown using
beforeAll/afterAllhooks - Data-testid selectors for reliable element targeting
- Simplified CSS - minimal CSS for focused testing
- Test only the core features functionality related to the vite plugin - implementations details are already tested in lib/ tests, e2e tests should only test if the vite plugin is working as expected
- Follow Playwright best practices
- Tests should be concise
Example structure:
test.describe.configure({ mode: 'serial' });
let env: TestEnv;
let page: Page;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
env = await startEnv('test-name', {
'App.tsx': dedent`
// Single comprehensive App component
// covering all test scenarios
`,
});
await page.goto(env.baseUrl);
});
test.afterAll(async () => {
await page.close();
await env.cleanup();
});
test('should test feature A', async () => {
const element = page.getByTestId('test-element');
await expect(element).toHaveCSS('property', 'value');
});
Debugging
- The
test-resultsandtest-runsare auto generated directories from the./testsdirectory. Focus on the code insideteststo find bugs and issues.
Transform tests
Tests for transform function should follow this structure:
import { describe, expect, test } from 'vitest';
import { dedent } from '@ls-stack/utils/dedent';
import { transformWithFormat } from './testUtils';
// ...
test('should handle ...', async () => {
const result = await transformWithFormat({
// source should be inlined if it is not used multiple times
source: dedent`
// ...
`,
// prefer using default props for `fs` and `importAliases` unless the test requires it
});
// code assertion should come first, then css assertion
expect(result.code).toMatchInlineSnapshot(`
// ...
`);
expect(result.css).toMatchInlineSnapshot(`
// ...
`);
});
DON'T DO THIS:
- using
toContaininstead oftoMatchInlineSnapshot
Documentation
README.md
The README.md serves as the main user documentation and should:
- Focus on implemented features only - do not document planned/future features
- Use concise examples - keep code examples short and focused
- Use correct syntax - ensure all examples use supported language features
- Be concise and to the point, do not include unnecessary details or redundant information
ROADMAP.md
The ROADMAP.md tracks feature development:
- Use checkboxes -
[x]for completed,[ ]for planned - Status indicators - ✅ (completed), 🚧 (in progress), 🔮 (future)
Documentation Updates
When implementing features:
- Update ROADMAP.md - mark features as completed
[x]and change status to ✅ - Update README.md - add documentation section with examples
- Keep examples current - ensure examples match actual implementation
- Test examples - verify all documentation examples actually work
Feature implementation guidelines
- Add or adjust documentation in README.md
- If the feature has complex implementation, document it in SPEC.md
- Implement the tests
- The feature may be already implemented, so run the tests first
- If the feature is not simple, wait me for review the tests first
- Implement the feature
- Ensure all tests pass and no other features are broken
- Run tsc and lint and fix all errors
- Update ROADMAP.md
- Update CLAUDE.md Main features section if needed
Test Utility Memories
overrideDefaultFsshould usecreateFsMock- do not use
overrideDefaultFsandoverrideDefaultImportAliaseswhen using the default values
Code Optimization Memories
- Use
filterWithNarrowinginstead of type guards on array.map - Use
findWithNarrowinginstead of type guards on array.find
Static Analysis and Template Literals
All Vindur CSS-in-JS features that use template literals are statically analyzed at compile-time:
- Static values are allowed: String literals, numbers, constants defined at module level
- Dynamic runtime values are NOT allowed: State variables, props, function calls, or any values that change at runtime
- Use vindurFn for dynamic CSS: When you need CSS that depends on runtime values, wrap your function with
vindurFn
Valid examples:
const BRAND_COLOR = '#667eea'; // Static constant - OK
const styles = css`
color: ${BRAND_COLOR}; // Static variable - OK
padding: 16px; // Literal value - OK
`;
Invalid examples:
function Component({ color, isActive }) {
const styles = css`
color: ${color}; // ❌ Dynamic prop - NOT allowed
padding: ${isActive ? 16 : 8}px; // ❌ Dynamic expression - NOT allowed
`;
}
For dynamic CSS, use vindurFn:
const dynamicStyles = vindurFn(
(color: string, isActive: boolean) => `
color: ${color}; // ✅ OK inside vindurFn
padding: ${isActive ? 16 : 8}px; // ✅ OK inside vindurFn
`,
);
This applies to: css, styled.*, css prop, keyframes, and createGlobalStyle.
Main features
-
css tagged template, eg.
css`color: red`- Template literals are statically analyzed and transformed to hashed class names at compile-time
- No runtime CSS processing, styles are extracted to separate CSS files
- CSS style interpolation with semicolon extension
- String and number variable interpolation
- Mixins/functions support
-
styled components, eg.
styled.div`color: blue`- Non-exported styled components: Class is injected directly into JSX usages, no intermediate component
- Exported styled components: Generate intermediate React components using
styledComponenthelper - Style flags: Always generate intermediate components regardless of export status
- Component references are replaced with native elements + className for non-exported cases
- Style extension with
styled(Button)syntax - CSS selector usage in styled components used as a selector
- Styled component references with
&selector - withComponent method: Change element type while keeping styles, eg.
Button.withComponent('a')
-
css prop - JSX prop that accepts template literals, eg.
<div css={color: green} />- Only works on native DOM elements (lowercase names) and styled components
- Transformed to className prop with extracted CSS at compile-time
- Template literals must be statically analyzable
- Replaces css prop with className in final output
- css extension with interpolation of other css variables
- Merges with existing className prop and handles spread props
-
cx prop - Conditional class name prop, eg.
<div cx={{ active: isActive, $notHashed: true }} />- Only works on native DOM elements and styled components
- Uses object syntax for conditional classes
- Properties prefixed with
$are not hashed (pass-through) - Merges with existing className prop via runtime
cxfunction - cx props not supports computed properties, eg.
cx={{ [styleName]: true }} - Class names are file-scoped with incremental indices
- Integrates with css prop, dynamicColor prop, and spread props
-
vindurFn - Compile-time CSS function utility, eg.
vindurFn((size: number) => \width: ${size}px`)`- Functions must be wrapped with
vindurFnfor compile-time evaluation - Must be synchronous and self-contained (no external dependencies)
- Used for creating reusable CSS utilities across files
- Variable interpolation from external files
- Functions must be wrapped with
-
scoped css variables - CSS custom properties with automatic scoping, eg.
---primaryColor: blue- Use
---variableNamesyntax in CSS, reference withvar(---variableName) - Automatically prefixed with scope hash for isolation
- In dev mode includes original variable name for debugging
- Dynamic variables with style prop support
- Validation warnings for declared but unused variables
- File-level scoping with shared index assignment
- Use
-
keyframes - Animation definitions, eg.
keyframes`from { opacity: 0 }`- Extracted to CSS with unique identifiers
- Returns animation name for use in CSS
- Supports cross-file imports
-
createGlobalStyle - Global CSS injection, eg.
createGlobalStyle`body { margin: 0 }`- Build-time extraction and deduplication
- Injected once per unique style definition
- No runtime global style injection, expression produces no output
-
style flags - Boolean and string union props for styled components, eg.
styled.div<{ active: boolean; size: 'small' | 'large' }>- Automatic modifier class generation from component prop types
- Boolean props apply classes when true:
&.active { ... } - String union props apply value-based classes:
&.size-small { ... } - Always generates intermediate components using
vComponentWithModifiers - Hashed class names for optimal bundle size
-
dynamic color props - Runtime color application, eg.
<StyledButton dynamicColor={color.set('#ff6b6b')} />- Color identifier with
.set()method for runtime color values - Uses
color._spfunction injected by the compiler to merge className and style props - Handles multiple dynamic colors and spread props
- Integrates with existing className, style, and other Vindur props
- Color identifier with
-
static theme colors - Compile-time color utilities
- Static color definitions for consistent theming
- Processed at build time for optimal performance
-
stable IDs - Deterministic identifier generation via
stableIdfunction- Generates consistent IDs across builds for reliable references
Hash generation
Vindur generates unique, deterministic hashes for CSS classes and variables using a file-scoped system.
Hash Format
- File hash:
v${murmur2(filePath)}(e.g., "v1560qbr") - Development:
{fileHash}-{index}-{name}(e.g., "v1560qbr-1-Button") - Production:
{fileHash}-{index}(e.g., "v1560qbr-1")
Index Assignment
All Vindur features share a file-scoped incremental counter:
const styles = css`
color: red;
`; // Index 1
<div cx={{ active: true }} />; // Index 2
const Button = styled.button`
color: blue;
`; // Index 3
<div cx={{ test: true, loading: true }} />; // test uses index 4, loading gets index 5
Key rules:
- Same names within a file reuse the same index
- Different names get sequential indices
- Each file has its own hash and counter space