Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: ensure that class names produced by S2 style macro differs for each theme #7822

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
24 changes: 24 additions & 0 deletions packages/@react-spectrum/s2/style/__tests__/style-macro.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,30 @@ function testStyle(...args) {
}

describe('style-macro', () => {
it('should prefix rules with the hash of theme version', () => {
let {css, js} = testStyle({
alignItems: 'center',
color: 'red-400'
});
expect(css).toMatchInlineSnapshot(`
"@layer _.a, _.b;

@layer _.a {
.chc_2c {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chc here is the hash of 0.6.0 read from package.json. I pushed this test just to illustrate how my changes affect produced CSS.

align-items: center;
}


.chcaJ {
color: light-dark(rgb(255, 188, 180), rgb(115, 24, 11));
}
}

"
`);
expect(js).toMatchInlineSnapshot('" chc_2c chcaJ"');
});

it('should handle nested css conditions', () => {
let {css, js} = testStyle({
marginTop: {
Expand Down
2 changes: 2 additions & 0 deletions packages/@react-spectrum/s2/style/spectrum-theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {ArbitraryValue, CSSProperties, CSSValue, PropertyValueMap} from './types
import {autoStaticColor, colorScale, colorToken, fontSizeToken, generateOverlayColorScale, getToken, simpleColorScale, weirdColorToken} from './tokens' with {type: 'macro'};
import {Color, createArbitraryProperty, createColorProperty, createMappedProperty, createRenamedProperty, createSizingProperty, createTheme, parseArbitraryValue} from './style-macro';
import type * as CSS from 'csstype';
import {getThemeVersion} from './version';

interface MacroContext {
addAsset(asset: {type: string, content: string}): void
Expand Down Expand Up @@ -453,6 +454,7 @@ const fontSize = {
} as const;

export const style = createTheme({
version: getThemeVersion(),
properties: {
// colors
color: createColorProperty({
Expand Down
9 changes: 5 additions & 4 deletions packages/@react-spectrum/s2/style/style-macro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,11 @@ function mapConditionalShorthand<T, C extends string, R extends RenderProps<stri
}
}

function createValueLookup(values: Array<CSSValue>, atStart = false) {
function createValueLookup(values: Array<CSSValue>, atStart = false, prefix = '') {
let map = new Map<CSSValue, string>();
for (let value of values) {
if (!map.has(value)) {
map.set(value, generateName(map.size, atStart));
map.set(value, prefix + generateName(map.size, atStart));
}
}
return map;
Expand All @@ -138,8 +138,9 @@ interface MacroContext {
}

export function createTheme<T extends Theme>(theme: T): StyleFunction<ThemeProperties<T>, 'default' | Extract<keyof T['conditions'], string>> {
let themePropertyMap = createValueLookup(Object.keys(theme.properties), true);
let themeConditionMap = createValueLookup(Object.keys(theme.conditions), true);
let themePropertyMap = createValueLookup(Object.keys(theme.properties), true, theme.version);
let themeConditionMap = createValueLookup(Object.keys(theme.conditions), true, theme.version);

let propertyFunctions = new Map(Object.entries(theme.properties).map(([k, v]) => {
if (typeof v === 'function') {
return [k, v];
Expand Down
3 changes: 2 additions & 1 deletion packages/@react-spectrum/s2/style/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export type PropertyFunction<T> = (value: T, property: string) => PropertyValueD
export type ShorthandProperty<T> = (value: T) => {[name: string]: Value};

export interface Theme {
version?: string,
properties: {
[name: string]: PropertyValueMap | PropertyFunction<any> | string[]
},
Expand All @@ -53,7 +54,7 @@ type PropertyValue<T> =
export type ArbitraryValue = CustomProperty | `[${string}]`;
type PropertyValue2<T> = PropertyValue<T> | ArbitraryValue;
type Merge<T> = T extends any ? T : never;
type ShorthandValue<T extends Theme, P> =
type ShorthandValue<T extends Theme, P> =
P extends string[]
? PropertyValue2<T['properties'][P[0]]>
: P extends ShorthandProperty<infer V>
Expand Down
34 changes: 34 additions & 0 deletions packages/@react-spectrum/s2/style/version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import fs from 'node:fs';
import path from 'node:path';

export function getThemeVersion() {
let s2PkgPath = require.resolve('@react-spectrum/s2');
while (s2PkgPath.length > 2) {
const filePath = path.join(s2PkgPath, 'package.json');
try {
const stats = fs.statSync(filePath, {throwIfNoEntry: false});
if (stats?.isFile()) {
break;
}
} catch { /* empty */ }
s2PkgPath = path.dirname(s2PkgPath);
}
const s2PkgMetadata = require(path.join(s2PkgPath, 'package.json'));
// This logic sucks, but 'A' is present at the beginning of all allowedOverrides
// We add the hash of the version to the 'A' to make regex in S2 components allow the rules
return 'A' + hashSemVer(s2PkgMetadata.version);
}

function hashSemVer(version: string) {
const alphabet = 'abcdefghijklmnopqrstuvwxyz';
const base = alphabet.length;
const [major, minor, patch] = version.split('.').map(Number);
const combined = (major << 16) | (minor << 8) | patch;
let hashStr = '';
let num = combined;
while (num > 0) {
hashStr = alphabet[num % base] + hashStr;
num = Math.floor(num / base);
}
return hashStr;
}