jantimon / next-yak

a streamlined CSS-in-JS solution tailor-made for Next.js, seamlessly combining the expressive power of styled-components syntax with efficient build-time extraction and minimal runtime footprint, ensuring optimal performance and easy integration with existing atomic CSS frameworks like Tailwind CSS
https://yak.js.org
109 stars 3 forks source link

Components as Selectors cross files #75

Closed jantimon closed 2 months ago

jantimon commented 6 months ago

styled-components allows to manipulate internal styles of a components. For example here we would overrule the padding of the TableHead component although we render only the <Table /> component:

import { styled } from 'styled-compoennts';
import { Table, TableHead } from '@my-ui-library';

const StyledTable = styled(Table)`
  border-collapse: collapse;
  border-spacing: 0;
  width: 100%;
  ${TableHead} {
    padding: 8px;
    text-align: left;
    border-bottom: 1px solid #ddd;
  }
`;

const MyComponent = () => {
  return (
    <StyledTable data={[...]} />
  );
};

In yak the class name for the TableHead component is generated based on the file name and the component name. Without a proper module resolution it is impossible to know wether @my-ui-library is "my-ui-library/dist" or "my-ui-library/dist/Table/TableHead". Unfortunately module resolution and watching module resolution with typesctipt alias handlings, babel alias handling and package.json export alias handling is complex and slow.

However we could introduce a new api which allows to declare a component as global selecor.

import { styled } from 'next-yak';

export const TableHead = styled.thead.withSelector('TableHead', '@my-ui-library')`
  padding: 8px;
  text-align: left;
  border-bottom: 1px solid #ddd;
`;

A short hand could even allow to use the component name and take the package name from the closest package.json.

import { styled } from 'next-yak';

export const TableHead = styled.thead.withSelector`
  padding: 8px;
  text-align: left;
  border-bottom: 1px solid #ddd;
`;

This would allow to use the same syntax as styled-components. It would also cover cases where multiple components can be overruled with the same selector.

import { styled } from 'next-yak';

export const Icon = styled.svg.withSelector``;

export const HomeIcon = styled(Icon).withSelector("Icon")`
  fill: red;
`;

export const UserIcon = styled(Icon).withSelector("Icon")`
  fill: blue;
`;

It would also cover cases where package.json exports are used.

import { styled } from 'next-yak';

export const TableHead = styled.thead.withSelector("TableHead", "@my-ui-library/TableHead")`
  padding: 8px;
  text-align: left;
  border-bottom: 1px solid #ddd;
`;

Unfortunately the developers would have to ensure that the selector is unique and matches the finale export name.

Because of our current same file logic we would not be able to use typescript to distinguish between a component and a component with selector.

Mad-Kat commented 6 months ago

Just for credit where it's due: The proposed solution is somewhat comparable to the solution of panda-css with the same benefits (to allow styling components outside of your import) and the same limitations (the developer needs to do the work of keeping it distinct).

I think it may be possible to add types to only allow targeting of yak components, but I'm not sure if we want that 😅 but we can try and see how it feels and maybe discuss when it's time to merge.

jantimon commented 3 months ago

Our main problem is that resolving an import statement is expensive and complicated because of bundler/typescript/package.json aliases, file extensions, export * from "./foo" and other things.

However a bundler has to do all that anyway so the best thing would be to ask the bundler for this information.\ That way the resolving would cost almost no performance and we would know the source location of the code of something from import { something } from "./foo".

Hashing the source location would allow us to be able to generate consistent class names over the entire codebase.

For example import { something } from "./foo" and import { something } from "../foo" and import { something } from "@my-mono-repo/something - when resolving to the same source file - would all generate a common selector like :global(".somefile_something__0iqTb")

So I asked ⁠@sokra and he said that:

[a performant solution] is currently not possible in Webpack because exports are only resolved after the module graph

but also:

In Turbopack, it would be technically possible because the exports are resolved during the module graph

jantimon commented 2 months ago

We have a first working prototype for the original styled-components syntax.

It has to parse the same file multiple times but looks promising.

https://github.com/jantimon/next-yak/tree/feature/cross-file-selectors/packages/next-yak

import { ClockHands } from "../Test";

const MyWrapper = styled.div`
  ${ClockHands} {
    background: pink;
  }
`;

export default function Home() {
  return (
    <main className={styles.main}>
      <Headline>Hello world</Headline>
      <MyWrapper>
        <Clock />
      </MyWrapper>
    </main>
  );
}

shot-2FlIqZPb@2x

Open Todos:

jantimon commented 2 months ago

released as 0.2.3 behind an experimental flag