Skip to content

Commit 7ddc5b7

Browse files
committed
[feat] Add defineMarker() API for custom markers for combinator selectors
1 parent 037f6fc commit 7ddc5b7

File tree

12 files changed

+328
-13
lines changed

12 files changed

+328
-13
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
'use strict';
9+
10+
jest.autoMockOff();
11+
12+
import { transformSync } from '@babel/core';
13+
import stylexPlugin from '../src/index';
14+
15+
function transform(source, opts = {}) {
16+
const { code, metadata } = transformSync(source, {
17+
filename: opts.filename || '/stylex/packages/vars.stylex.js',
18+
parserOpts: {
19+
flow: 'all',
20+
},
21+
babelrc: false,
22+
plugins: [
23+
[
24+
stylexPlugin,
25+
{
26+
unstable_moduleResolution: {
27+
rootDir: '/stylex/packages/',
28+
type: 'commonJS',
29+
},
30+
...opts,
31+
},
32+
],
33+
],
34+
});
35+
return { code, metadata };
36+
}
37+
38+
describe('@stylexjs/babel-plugin', () => {
39+
describe('[transform] stylex.defineMarker()', () => {
40+
test('member call', () => {
41+
const { code, metadata } = transform(`
42+
import * as stylex from '@stylexjs/stylex';
43+
export const fooBar = stylex.defineMarker();
44+
`);
45+
46+
expect(code).toMatchInlineSnapshot(`
47+
"import * as stylex from '@stylexjs/stylex';
48+
export const fooBar = {
49+
x1jdyizh: "x1jdyizh",
50+
$$css: true
51+
};"
52+
`);
53+
expect(metadata).toMatchInlineSnapshot(`
54+
{
55+
"stylex": [],
56+
}
57+
`);
58+
});
59+
60+
test('named import call', () => {
61+
const { code } = transform(`
62+
import { defineMarker } from '@stylexjs/stylex';
63+
export const baz = defineMarker();
64+
`);
65+
66+
expect(code).toMatchInlineSnapshot(`
67+
"import { defineMarker } from '@stylexjs/stylex';
68+
export const baz = {
69+
x1i61hkd: "x1i61hkd",
70+
$$css: true
71+
};"
72+
`);
73+
});
74+
});
75+
});

packages/@stylexjs/babel-plugin/__tests__/transform-stylex-when-test.js

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,16 @@ function transform(source, opts = {}) {
1919
flow: 'all',
2020
},
2121
babelrc: false,
22-
plugins: [[stylexPlugin, { ...opts }]],
22+
plugins: [
23+
[
24+
stylexPlugin,
25+
{
26+
treeshakeCompensation: true,
27+
unstable_moduleResolution: { type: 'haste' },
28+
...opts,
29+
},
30+
],
31+
],
2332
});
2433

2534
return result;
@@ -336,4 +345,42 @@ describe('@stylexjs/babel-plugin', () => {
336345
`);
337346
});
338347
});
348+
349+
describe.only('[transform] using custom markers', () => {
350+
test.only('named import', () => {
351+
const { code } = transform(
352+
`
353+
import * as stylex from '@stylexjs/stylex';
354+
import {customMarker} from 'custom-marker.stylex';
355+
356+
const styles = stylex.create({
357+
foo: {
358+
backgroundColor: {
359+
default: 'blue',
360+
[stylex.when.ancestor(':hover', customMarker)]: 'red',
361+
},
362+
},
363+
});
364+
365+
const container = stylex.props(customMarker);
366+
const classNames = stylex.props(styles.foo);
367+
`,
368+
{ runtimeInjection: true },
369+
);
370+
371+
expect(code).toMatchInlineSnapshot(`
372+
"import _inject from "@stylexjs/stylex/lib/stylex-inject";
373+
var _inject2 = _inject;
374+
import * as stylex from '@stylexjs/stylex';
375+
import 'custom-marker.stylex';
376+
import { customMarker } from 'custom-marker.stylex';
377+
_inject2(".x1t391ir{background-color:blue}", 3000);
378+
_inject2(".x7rpj1w:where(.x1lc2aw:hover *){background-color:red}", 3011.3);
379+
const container = stylex.props(customMarker);
380+
const classNames = {
381+
className: "x1t391ir x7rpj1w"
382+
};"
383+
`);
384+
});
385+
});
339386
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
'use strict';
9+
10+
jest.autoMockOff();
11+
12+
import { transformSync } from '@babel/core';
13+
import stylexPlugin from '../src/index';
14+
import * as messages from '../src/shared/messages';
15+
16+
function transform(source, opts = {}) {
17+
const { code, metadata } = transformSync(source, {
18+
filename: opts.filename || '/stylex/packages/vars.stylex.js',
19+
parserOpts: {
20+
flow: 'all',
21+
},
22+
babelrc: false,
23+
plugins: [
24+
[
25+
stylexPlugin,
26+
{
27+
unstable_moduleResolution: {
28+
rootDir: '/stylex/packages/',
29+
type: 'commonJS',
30+
},
31+
...opts,
32+
},
33+
],
34+
],
35+
});
36+
return { code, metadata };
37+
}
38+
39+
describe('@stylexjs/babel-plugin', () => {
40+
describe('[validation] stylex.defineMarker()', () => {
41+
test('must be bound to a named export', () => {
42+
expect(() => {
43+
transform(`
44+
import * as stylex from '@stylexjs/stylex';
45+
const marker = stylex.defineMarker();
46+
`);
47+
}).toThrow(messages.nonExportNamedDeclaration('defineMarker'));
48+
});
49+
50+
test('no arguments allowed', () => {
51+
expect(() => {
52+
transform(`
53+
import * as stylex from '@stylexjs/stylex';
54+
export const marker = stylex.defineMarker(1);
55+
`);
56+
}).toThrow(messages.illegalArgumentLength('defineMarker', 0));
57+
});
58+
});
59+
});

packages/@stylexjs/babel-plugin/src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import transformStylexProps from './visitors/stylex-props';
3333
import { skipStylexPropsChildren } from './visitors/stylex-props';
3434
import transformStyleXViewTransitionClass from './visitors/stylex-view-transition-class';
3535
import transformStyleXDefaultMarker from './visitors/stylex-default-marker';
36+
import transformStyleXDefineMarker from './visitors/stylex-define-marker';
3637

3738
const NAME = 'stylex';
3839

@@ -306,6 +307,7 @@ function styleXTransform(): PluginObj<> {
306307
}
307308

308309
transformStyleXDefaultMarker(path, state);
310+
transformStyleXDefineMarker(path, state);
309311
transformStyleXDefineVars(path, state);
310312
transformStyleXDefineConsts(path, state);
311313
transformStyleXCreateTheme(path, state);

packages/@stylexjs/babel-plugin/src/shared/when/when.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,24 @@ import type { StyleXOptions } from '../common-types';
1111

1212
import { defaultOptions } from '../utils/default-options';
1313

14+
function isProxy(value: mixed): boolean {
15+
if (
16+
typeof value === 'object' &&
17+
value != null &&
18+
value.__IS_PROXY === true &&
19+
typeof value.toString === 'function'
20+
) {
21+
return true;
22+
}
23+
return false;
24+
}
25+
1426
function getDefaultMarkerClassName(
1527
options: StyleXOptions = defaultOptions,
1628
): string {
29+
if (isProxy(options)) {
30+
return options.toString();
31+
}
1732
const prefix =
1833
options.classNamePrefix != null ? `${options.classNamePrefix}-` : '';
1934
return `${prefix}default-marker`;

packages/@stylexjs/babel-plugin/src/utils/evaluate-path.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,11 @@ function evaluateThemeRef(
204204
if (key === '__IS_PROXY') {
205205
return true;
206206
}
207+
if (key === 'toString') {
208+
return () =>
209+
state.traversalState.options.classNamePrefix +
210+
utils.hash(utils.genFileBasedIdentifier({ fileName, exportName }));
211+
}
207212
return resolveKey(key);
208213
},
209214
set(_, key: string, value: string) {

packages/@stylexjs/babel-plugin/src/utils/state-manager.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ export default class StateManager {
132132
+stylexKeyframesImport: Set<string> = new Set();
133133
+stylexPositionTryImport: Set<string> = new Set();
134134
+stylexDefineVarsImport: Set<string> = new Set();
135+
+stylexDefineMarkerImport: Set<string> = new Set();
135136
+stylexDefineConstsImport: Set<string> = new Set();
136137
+stylexCreateThemeImport: Set<string> = new Set();
137138
+stylexTypesImport: Set<string> = new Set();

packages/@stylexjs/babel-plugin/src/visitors/imports.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ export function readImportDeclarations(
7979
if (importedName === 'defineVars') {
8080
state.stylexDefineVarsImport.add(localName);
8181
}
82+
if (importedName === 'defineMarker') {
83+
state.stylexDefineMarkerImport.add(localName);
84+
}
8285
if (importedName === 'defineConsts') {
8386
state.stylexDefineConstsImport.add(localName);
8487
}
@@ -158,6 +161,9 @@ export function readRequires(
158161
if (prop.key.name === 'defineVars') {
159162
state.stylexDefineVarsImport.add(value.name);
160163
}
164+
if (prop.key.name === 'defineMarker') {
165+
state.stylexDefineMarkerImport.add(value.name);
166+
}
161167
if (prop.key.name === 'defineConsts') {
162168
state.stylexDefineConstsImport.add(value.name);
163169
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict
8+
*/
9+
10+
import type { NodePath } from '@babel/traverse';
11+
12+
import * as t from '@babel/types';
13+
import StateManager from '../utils/state-manager';
14+
import * as messages from '../shared/messages';
15+
import { utils } from '../shared';
16+
import { convertObjectToAST } from '../utils/js-to-ast';
17+
18+
/**
19+
* Transforms calls to `stylex.defineMarker()` (or imported `defineMarker()`)
20+
* into an object: { $$css: true, [hash]: hash } where `hash` is generated from
21+
* the file path and the export name.
22+
*/
23+
export default function transformStyleXDefineMarker(
24+
path: NodePath<t.CallExpression>,
25+
state: StateManager,
26+
): void {
27+
const { node } = path;
28+
29+
if (node.type !== 'CallExpression') {
30+
return;
31+
}
32+
33+
const isDefineMarkerCall =
34+
(node.callee.type === 'Identifier' &&
35+
state.stylexDefineMarkerImport.has(node.callee.name)) ||
36+
(node.callee.type === 'MemberExpression' &&
37+
node.callee.object.type === 'Identifier' &&
38+
node.callee.property.type === 'Identifier' &&
39+
node.callee.property.name === 'defineMarker' &&
40+
state.stylexImport.has(node.callee.object.name));
41+
42+
if (!isDefineMarkerCall) {
43+
return;
44+
}
45+
46+
// Validate call shape and location: must be bound to an exported const
47+
validateStyleXDefineMarker(path);
48+
49+
// We know the parent is a VariableDeclarator
50+
const variableDeclaratorPath = path.parentPath;
51+
if (!variableDeclaratorPath.isVariableDeclarator()) {
52+
return;
53+
}
54+
const variableDeclaratorNode = variableDeclaratorPath.node;
55+
if (variableDeclaratorNode.id.type !== 'Identifier') {
56+
return;
57+
}
58+
59+
// No arguments allowed
60+
if (node.arguments.length !== 0) {
61+
throw path.buildCodeFrameError(
62+
messages.illegalArgumentLength('defineMarker', 0),
63+
SyntaxError,
64+
);
65+
}
66+
67+
const fileName = state.fileNameForHashing;
68+
if (fileName == null) {
69+
throw new Error(messages.cannotGenerateHash('defineMarker'));
70+
}
71+
72+
const exportName = variableDeclaratorNode.id.name;
73+
const exportId = utils.genFileBasedIdentifier({ fileName, exportName });
74+
const id = state.options.classNamePrefix + utils.hash(exportId);
75+
76+
const markerObj = {
77+
[id]: id,
78+
$$css: true,
79+
};
80+
81+
path.replaceWith(convertObjectToAST(markerObj));
82+
}
83+
84+
function validateStyleXDefineMarker(path: NodePath<t.CallExpression>) {
85+
const variableDeclaratorPath: any = path.parentPath;
86+
const exportNamedDeclarationPath =
87+
variableDeclaratorPath.parentPath?.parentPath;
88+
89+
if (
90+
variableDeclaratorPath == null ||
91+
variableDeclaratorPath.isExpressionStatement() ||
92+
!variableDeclaratorPath.isVariableDeclarator() ||
93+
variableDeclaratorPath.node.id.type !== 'Identifier'
94+
) {
95+
throw path.buildCodeFrameError(
96+
messages.unboundCallValue('defineMarker'),
97+
SyntaxError,
98+
);
99+
}
100+
101+
if (
102+
exportNamedDeclarationPath == null ||
103+
!exportNamedDeclarationPath.isExportNamedDeclaration()
104+
) {
105+
throw path.buildCodeFrameError(
106+
messages.nonExportNamedDeclaration('defineMarker'),
107+
SyntaxError,
108+
);
109+
}
110+
}

0 commit comments

Comments
 (0)