Skip to content

Commit 41ed47a

Browse files
committed
[feat] Add defineMarker() API for custom markers for combinator selectors
1 parent 900f459 commit 41ed47a

File tree

8 files changed

+262
-0
lines changed

8 files changed

+262
-0
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+
});
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/utils/evaluate-path.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,9 @@ function evaluateThemeRef(
201201
{},
202202
{
203203
get(_, key: string) {
204+
if (key === 'toString') {
205+
return () => resolveKey('');
206+
}
204207
return resolveKey(key);
205208
},
206209
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
@@ -131,6 +131,7 @@ export default class StateManager {
131131
+stylexKeyframesImport: Set<string> = new Set();
132132
+stylexPositionTryImport: Set<string> = new Set();
133133
+stylexDefineVarsImport: Set<string> = new Set();
134+
+stylexDefineMarkerImport: Set<string> = new Set();
134135
+stylexDefineConstsImport: Set<string> = new Set();
135136
+stylexCreateThemeImport: Set<string> = new Set();
136137
+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+
}

packages/@stylexjs/stylex/src/stylex.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ export const defineVars: StyleX$DefineVars = function stylexDefineVars(
8181
throw errorForFn('defineVars');
8282
};
8383

84+
export const defineMarker = (): StaticStyles<> => {
85+
throw errorForFn('defineMarker');
86+
};
87+
8488
export const firstThatWorks = <T: string | number>(
8589
..._styles: $ReadOnlyArray<T>
8690
): $ReadOnlyArray<T> => {
@@ -220,6 +224,7 @@ type IStyleX = {
220224
create: StyleX$Create,
221225
createTheme: StyleX$CreateTheme,
222226
defineVars: StyleX$DefineVars,
227+
defineMarker: () => StaticStyles<>,
223228
defaultMarker: () => StaticStyles<>,
224229
firstThatWorks: <T: string | number>(
225230
...v: $ReadOnlyArray<T>
@@ -254,6 +259,7 @@ function _legacyMerge(
254259

255260
_legacyMerge.create = create;
256261
_legacyMerge.createTheme = createTheme;
262+
_legacyMerge.defineMarker = defineMarker;
257263
_legacyMerge.defineVars = defineVars;
258264
_legacyMerge.defaultMarker = defaultMarker;
259265
_legacyMerge.firstThatWorks = firstThatWorks;

0 commit comments

Comments
 (0)