Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

import {
exactPart,
getFixtureNames,
isJsEquivalent,
jsPart,
loadFixture,
normalizeResult,
readExpected,
writeExpected,
} from 'compiler-conformance';

import { babelAdapter } from '../test-utils/babel-conformance-adapter';

// Re-records every `expected.json` from the current Babel output. Only use it
// after reviewing the resulting diff.
const UPDATE_GOLDEN = process.env.STYLEX_UPDATE_GOLDEN === '1';

describe('@stylexjs/babel-plugin golden fixtures', () => {
test.each(getFixtureNames())('%s', (fixtureName) => {
const fixture = loadFixture(fixtureName);
const rawResult = babelAdapter.transform(fixture);

if (UPDATE_GOLDEN) {
writeExpected(fixtureName, rawResult, fixture, babelAdapter.name);
}

const actual = normalizeResult(fixture, rawResult, babelAdapter.name);
const expected = readExpected(fixtureName, fixture);

// Transform status, StyleX metadata, generated CSS and diagnostics have to
// match exactly once normalized.
expect(exactPart(actual)).toEqual(exactPart(expected));

// Generated JavaScript is compared as a normalized AST so that an
// implementation is free to print it differently. When the programs really
// do differ, assert on the sources to get a readable diff.
if (!isJsEquivalent(jsPart(expected), jsPart(actual), fixture.syntax)) {
expect(jsPart(actual)).toBe(jsPart(expected));
}
});
});
1 change: 1 addition & 0 deletions packages/@stylexjs/babel-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-replace": "^6.0.1",
"babel-plugin-syntax-hermes-parser": "^0.36.1",
"compiler-conformance": "0.19.0",
"path-browserify": "^1.0.1",
"rollup": "^4.59.0",
"scripts": "0.19.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

import { transformFileSync } from '@babel/core';

import stylexPlugin from '../src/index';

/**
* Adapts the StyleX Babel plugin to the `compiler-conformance` adapter
* contract. Everything Babel-specific lives here: parser configuration, the
* shape of the transform result, and capturing diagnostics off the console.
*
* See `packages/compiler-conformance/README.md` for the contract itself.
*/

function formatDiagnostic(args) {
return args
.map((value) =>
typeof value === 'string' ? value : JSON.stringify(value, null, 2),
)
.join(' ');
}

// The fixture `syntax` list uses the same names as `@babel/parser` plugins.
function toParserOpts(syntax) {
return { plugins: [...syntax] };
}

function transform(fixture) {
const warnings = [];
const errors = [];
const originalWarn = console.warn;
const originalError = console.error;

console.warn = (...args) => {
warnings.push(formatDiagnostic(args));
};
console.error = (...args) => {
errors.push(formatDiagnostic(args));
};

try {
const result = transformFileSync(fixture.entryPath, {
babelrc: false,
configFile: false,
filename: fixture.entryPath,
parserOpts: toParserOpts(fixture.syntax),
plugins: [[stylexPlugin, fixture.pluginOptions]],
});

if (result == null) {
throw new Error(
`Babel produced no result for fixture "${fixture.name}".`,
);
}

const metadata = result.metadata?.stylex ?? [];

return {
css: stylexPlugin.processStylexRules(metadata, fixture.processOptions),
errors,
js: result.code,
metadata,
status: 'ok',
warnings,
};
} catch (error) {
return {
error: {
message: error instanceof Error ? error.message : String(error),
},
errors,
status: 'error',
warnings,
};
} finally {
console.warn = originalWarn;
console.error = originalError;
}
}

export const babelAdapter = {
name: '@stylexjs/babel-plugin',
transform,
};
191 changes: 191 additions & 0 deletions packages/compiler-conformance/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# compiler-conformance

Golden fixtures that describe what a StyleX compiler must do, independently of
how it is implemented.

Each fixture pins the four observable outputs of compiling one file: the
generated JavaScript, the StyleX metadata, the generated CSS, and the
diagnostics. Fixtures are plain data — an implementation written in any language
can read them and check itself against the same contract.

The StyleX Babel plugin is the first consumer, via the adapter in
`packages/@stylexjs/babel-plugin/test-utils/babel-conformance-adapter.js` and
the test in `packages/@stylexjs/babel-plugin/__tests__/golden-fixtures-test.js`.

## Layout

```
fixtures/<fixture-name>/
manifest.json how to compile the fixture
input.js the entry file (name set by `entry`)
expected.json the recorded result
... any extra files the entry imports
```

## `manifest.json`

| Field | Type | Default | Meaning |
| ---------------- | ---------- | ----------------------------------------------------- | ------------------------------------------------------------ |
| `description` | `string` | `""` | What behavior this fixture pins down. |
| `entry` | `string` | `"input.js"` | File in the fixture directory to compile. |
| `syntax` | `string[]` | `["flow"]` | Source syntax the entry uses, e.g. `["flow", "jsx"]`. |
| `pluginOptions` | `object` | `{}` | StyleX compiler options. |
| `processOptions` | `object` | `{"useLayers": false, "enableLTRRTLComments": false}` | Options for the step that assembles rules into a stylesheet. |

`pluginOptions` and `processOptions` are StyleX's own documented options, so
they carry over unchanged between implementations. `syntax` is deliberately
abstract: an adapter maps it onto whatever its parser needs.

Avoid options that take an absolute path. Fixture output must not depend on
where the repository is checked out.

## `expected.json`

For a fixture that compiles successfully:

```json
{
"status": "ok",
"js": "…generated JavaScript…",
"metadata": [
["x1e2nbdu", { "ltr": ".x1e2nbdu{color:red}", "rtl": null }, 3000]
],
"css": ".x1e2nbdu{color:red}",
"warnings": [],
"errors": []
}
```

For a fixture whose transform is expected to fail:

```json
{
"status": "error",
"error": { "message": "create() can only accept an object." },
"warnings": [],
"errors": []
}
```

`warnings` and `errors` are non-fatal diagnostics, split by the channel they
were reported on. A fixture can succeed and still report diagnostics —
`options-warning` does exactly that. `error` is the fatal one that ended the
transform.

## How results are compared

**`js` is compared semantically**, as a normalized syntax tree. Ignored are
whitespace, indentation, semicolons, comments, quote style and numeric literal
spelling, how a non-computed property key is written (`{a: 1}`, `{'a': 1}`,
`{1: x}` and `{1n: x}` name the same properties as `{"a": 1}`, `{a: 1}` and
`{"1": x}`), and property shorthand (`{a}` equals `{a: a}`). So an
implementation only has to agree on the program it emits, not on how it prints
it. The `__proto__` shorthand remains distinct because `{__proto__}` creates a
data property while `{__proto__: value}` sets the object's prototype. The string
stored in `expected.json` is one valid printing of that program; reformatting it
does not change the outcome. When the programs genuinely differ, the Babel test
falls back to asserting on the sources so the failure shows a readable diff.

**`metadata`, `css`, `warnings`, `errors` and `status` are compared exactly**,
after normalization:

- Line endings are normalized to `\n`, and trailing whitespace is stripped from
each CSS line.
- Absolute paths become `<FIXTURE_ROOT>` and `<REPO_ROOT>`.
- In diagnostics, terminal color escapes are removed, a leading
`<FIXTURE_ROOT>/<entry>: ` location prefix is dropped, and a trailing numbered
source excerpt (code frame) is dropped. When the adapter passes the exact text
of its leading `[implementation-name]` tag to `normalizeResult`, that tag is
collapsed to `[stylex]`. What is compared is the message itself, not how an
implementation decorates it. Color matters here in practice: Babel
syntax-highlights a code frame whenever `CI` is set, so an uncolored message
locally and a colored one on CI have to normalize to the same text.
- Object keys are sorted, because key order carries no meaning in JSON. **Array
order is preserved**, because the order of emitted rules is part of the
contract.

## Consuming this suite from another implementation

Implement an adapter — compile one fixture, return its result in the shape below
— and the shared runner does the rest:

```js
{
status: 'ok',
js: '…', // generated JavaScript
metadata: [...], // StyleX metadata
css: '…', // stylesheet assembled from the metadata
warnings: [...], // non-fatal diagnostics
errors: [...],
}
```

or, when the transform fails:

```js
{ status: 'error', error: { message: '…' }, warnings: [...], errors: [...] }
```

Every field shown for the selected status is required. `normalizeResult` rejects
unknown statuses, missing output, and non-string diagnostics instead of filling
in defaults that could hide an incomplete adapter. If an implementation prefixes
diagnostics with `[my-compiler]`, pass `my-compiler` as the third argument to
`normalizeResult`; only that exact tag is collapsed to `[stylex]`, so semantic
prefixes such as `[E100]` remain part of the contract.

From JavaScript, drive it with the helpers this package exports:

```js
const {
exactPart,
getFixtureNames,
isJsEquivalent,
jsPart,
loadFixture,
normalizeResult,
readExpected,
} = require('compiler-conformance');

for (const name of getFixtureNames()) {
const fixture = loadFixture(name);
const actual = normalizeResult(
fixture,
myAdapter.transform(fixture),
myAdapter.name,
);
const expected = readExpected(name, fixture);

assert.deepStrictEqual(exactPart(actual), exactPart(expected));
assert.ok(isJsEquivalent(jsPart(expected), jsPart(actual), fixture.syntax));
}
```

From another language, read `manifest.json` and `expected.json` directly and
apply the rules in [How results are compared](#how-results-are-compared). The
fixtures are the contract; these helpers are only a convenience.

Two things to be aware of when porting:

- Some fixtures depend on their own location. `define-vars-theme` uses
`commonJS` module resolution, which names a file
`<package name>:<path within the package>` — here
`compiler-conformance:fixtures/define-vars-theme/input.stylex.js`. That name
is hashed into the generated CSS variables, so it is stable across checkouts
but changes if the fixture is moved or the package is renamed.
- `props-and-merge` is the only fixture that needs JSX.

## Updating the recorded output

Re-record every `expected.json` from the current Babel output and review the
diff:

```
STYLEX_UPDATE_GOLDEN=1 npx jest golden-fixtures
```

from `packages/@stylexjs/babel-plugin`. A change here is a change to the
behavioral contract, so the diff deserves the same scrutiny as the code that
caused it.

The comparison rules above are themselves tested, independently of any compiler
— run `npm test` in this package.
Loading
Loading