jantimon/next-yak

Components as Selectors cross files

jantimon opened this issue · 4 comments

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.

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.

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

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:

  • handle or throw for constant imports
  • handle or throw for mixin imports
  • experimental flag
  • resolve caching per compilation
  • cleanup example app

released as 0.2.3 behind an experimental flag