diff --git a/packages/@stylexjs/babel-plugin/__tests__/golden-fixtures-test.js b/packages/@stylexjs/babel-plugin/__tests__/golden-fixtures-test.js new file mode 100644 index 000000000..ee7031a4a --- /dev/null +++ b/packages/@stylexjs/babel-plugin/__tests__/golden-fixtures-test.js @@ -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)); + } + }); +}); diff --git a/packages/@stylexjs/babel-plugin/package.json b/packages/@stylexjs/babel-plugin/package.json index df6a432ae..00807a8b2 100644 --- a/packages/@stylexjs/babel-plugin/package.json +++ b/packages/@stylexjs/babel-plugin/package.json @@ -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.1", "path-browserify": "^1.0.1", "rollup": "^4.59.0", "scripts": "0.19.1" diff --git a/packages/@stylexjs/babel-plugin/test-utils/babel-conformance-adapter.js b/packages/@stylexjs/babel-plugin/test-utils/babel-conformance-adapter.js new file mode 100644 index 000000000..01e6c1f98 --- /dev/null +++ b/packages/@stylexjs/babel-plugin/test-utils/babel-conformance-adapter.js @@ -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, +}; diff --git a/packages/compiler-conformance/README.md b/packages/compiler-conformance/README.md new file mode 100644 index 000000000..63da3c48a --- /dev/null +++ b/packages/compiler-conformance/README.md @@ -0,0 +1,199 @@ +# 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// + 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` | required | 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. + +This package's tests reject fields missing from this table and require a +`description`, so a misspelled `pluginOption` fails instead of quietly compiling +the fixture with default options. + +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. + +Every `expected.json` is stored already normalized (see below), exactly as the +update command records it, so an implementation reading it directly only has to +normalize its own output. This package's tests enforce that. + +## 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 `` and ``. +- In diagnostics, terminal color escapes are removed, a leading + `/: ` 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 + `:` — 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. diff --git a/packages/compiler-conformance/__tests__/conformance-harness-test.js b/packages/compiler-conformance/__tests__/conformance-harness-test.js new file mode 100644 index 000000000..c02bb5d53 --- /dev/null +++ b/packages/compiler-conformance/__tests__/conformance-harness-test.js @@ -0,0 +1,396 @@ +/** + * 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'; + +const fs = require('node:fs'); +const path = require('node:path'); + +const { + FIXTURES_DIR, + MANIFEST_FILE, + exactPart, + getExpectedPath, + getFixtureNames, + isJsEquivalent, + jsPart, + loadFixture, + normalizeCss, + normalizeData, + normalizeDiagnostic, + normalizePaths, + normalizeResult, + readExpected, +} = require('../src/index'); + +// The fields of the `manifest.json` schema in README.md. +const MANIFEST_FIELDS = [ + 'description', + 'entry', + 'pluginOptions', + 'processOptions', + 'syntax', +]; +const IMPLEMENTATION_TAG = '@stylexjs/babel-plugin'; +const FIXTURE = { + dir: '/repo/packages/compiler-conformance/fixtures/demo', + entry: 'input.js', + repoRoot: '/repo', +}; +const CONTEXT = { + entry: FIXTURE.entry, + fixtureDir: FIXTURE.dir, + implementationTag: IMPLEMENTATION_TAG, + repoRoot: FIXTURE.repoRoot, +}; +const WINDOWS_CONTEXT = { + entry: 'input.js', + fixtureDir: 'C:\\repo\\packages\\compiler-conformance\\fixtures\\demo', + implementationTag: IMPLEMENTATION_TAG, + repoRoot: 'C:\\repo', +}; + +function validSuccess(overrides = {}) { + return { + css: '', + errors: [], + js: '', + metadata: [], + status: 'ok', + warnings: [], + ...overrides, + }; +} + +function readJson(filePath) { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); +} + +describe('normalizePaths', () => { + test('replaces the fixture directory before the repository root', () => { + expect( + normalizePaths(`${FIXTURE.dir}/input.js imports /repo/other.js`, CONTEXT), + ).toBe('/input.js imports /other.js'); + }); + + test('only replaces roots at a path boundary', () => { + expect(normalizePaths('/repo-copy/result.js', CONTEXT)).toBe( + '/repo-copy/result.js', + ); + }); + + test('normalizes Windows roots and separators', () => { + expect( + normalizePaths( + `${WINDOWS_CONTEXT.fixtureDir}\\input.js imports C:/repo/other.js`, + WINDOWS_CONTEXT, + ), + ).toBe('/input.js imports /other.js'); + }); + + test('normalizes line endings', () => { + expect(normalizePaths('a\r\nb', CONTEXT)).toBe('a\nb'); + }); +}); + +describe('normalizeDiagnostic', () => { + test('drops the location prefix and the source excerpt', () => { + const message = [ + `${FIXTURE.dir}/input.js: create() can only accept an object.`, + ' 1 | import * as stylex from "@stylexjs/stylex";', + '> 2 | export const styles = stylex.create(1);', + ' | ^^^^^^^^^^^^^^^^', + ].join('\n'); + + expect(normalizeDiagnostic(message, CONTEXT)).toBe( + 'create() can only accept an object.', + ); + }); + + test('drops a Windows location prefix with line and column coordinates', () => { + expect( + normalizeDiagnostic( + `${WINDOWS_CONTEXT.fixtureDir}\\input.js:12:3: bad token`, + WINDOWS_CONTEXT, + ), + ).toBe('bad token'); + }); + + test('collapses the caller-provided implementation tag', () => { + expect( + normalizeDiagnostic( + '[@stylexjs/babel-plugin] Expected a boolean.', + CONTEXT, + ), + ).toBe('[stylex] Expected a boolean.'); + }); + + test('preserves a semantic bracketed prefix', () => { + expect(normalizeDiagnostic('[E100] Expected a boolean.', CONTEXT)).toBe( + '[E100] Expected a boolean.', + ); + }); + + test('preserves pipe-prefixed alternatives in a multiline message', () => { + expect( + normalizeDiagnostic('Expected one of:\n | string\n | number', CONTEXT), + ).toBe('Expected one of:\n | string\n | number'); + }); + + // Babel's code frame colorizes whenever the environment looks color-capable, + // which on GitHub Actions it does — so this is the shape the very same + // diagnostic takes on CI, and the escapes must not hide the excerpt. + test('drops a colorized source excerpt', () => { + const esc = '\u001B'; + const message = [ + `${FIXTURE.dir}/input.js: create() can only accept an object.`, + `${esc}[0m ${esc}[90m 1 |${esc}[39m ${esc}[36mimport${esc}[39m stylex${esc}[33m;${esc}[39m`, + `${esc}[31m${esc}[1m>${esc}[22m${esc}[39m${esc}[90m 2 |${esc}[39m stylex${esc}[33m.${esc}[39mcreate(${esc}[35m1${esc}[39m)${esc}[33m;${esc}[39m`, + `${esc}[90m |${esc}[39m ${esc}[31m${esc}[1m^${esc}[22m${esc}[39m${esc}[0m`, + ].join('\n'); + + expect(normalizeDiagnostic(message, CONTEXT)).toBe( + 'create() can only accept an object.', + ); + }); + + test('removes color from a message that has no source excerpt', () => { + expect( + normalizeDiagnostic( + '\u001B[1m\u001B[31m[@stylexjs/babel-plugin]\u001B[39m Expected a boolean.\u001B[22m', + CONTEXT, + ), + ).toBe('[stylex] Expected a boolean.'); + }); +}); + +describe('normalizeCss', () => { + test('strips trailing whitespace from every line and trims', () => { + expect( + normalizeCss('\n.a{color:red} \n.b{color:blue}\t\n\n', CONTEXT), + ).toBe('.a{color:red}\n.b{color:blue}'); + }); +}); + +describe('normalizeData', () => { + test('sorts object keys', () => { + expect(JSON.stringify(normalizeData({ b: 1, a: 2 }, CONTEXT))).toBe( + '{"a":2,"b":1}', + ); + }); + + test('preserves array order, because rule order is part of the contract', () => { + expect(normalizeData(['z', 'a'], CONTEXT)).toEqual(['z', 'a']); + }); +}); + +describe('isJsEquivalent', () => { + test('ignores formatting, quote style and comments', () => { + expect( + isJsEquivalent( + 'export const a = {x: 1};', + '// a comment\nexport const a = {\n "x": 1,\n}\n', + ['flow'], + ), + ).toBe(true); + }); + + test('ignores how a property key is spelled', () => { + expect(isJsEquivalent('({a: 1});', '({"a": 1});', ['flow'])).toBe(true); + expect(isJsEquivalent('({1: x});', '({"1": x});', ['flow'])).toBe(true); + expect(isJsEquivalent('({1n: x});', '({"1": x});', ['flow'])).toBe(true); + }); + + test('ignores property shorthand', () => { + expect(isJsEquivalent('({a});', '({a: a});', ['flow'])).toBe(true); + expect( + isJsEquivalent('const {a} = x;', 'const {a: a} = x;', ['flow']), + ).toBe(true); + }); + + test('preserves the semantic distinction for __proto__ shorthand', () => { + expect( + isJsEquivalent( + 'const __proto__ = null; ({__proto__});', + 'const __proto__ = null; ({__proto__: __proto__});', + ['flow'], + ), + ).toBe(false); + }); + + test('reports a genuine difference', () => { + expect( + isJsEquivalent('export const a = 1;', 'export const a = 2;', ['flow']), + ).toBe(false); + }); + + test('still reports a differently named property', () => { + expect(isJsEquivalent('({a: 1});', '({b: 1});', ['flow'])).toBe(false); + }); + + // `{[a]: 1}` names whatever `a` holds, `{["a"]: 1}` names "a". Only a + // non-computed key may be reduced to the name it spells. + test('keeps a computed key distinct from the name it spells', () => { + expect(isJsEquivalent('({[a]: 1});', '({["a"]: 1});', ['flow'])).toBe( + false, + ); + }); + + test('honors the requested syntax', () => { + expect( + isJsEquivalent('const a =
;', 'const a =
;', [ + 'flow', + 'jsx', + ]), + ).toBe(true); + }); + + // Otherwise invalid JavaScript, once recorded, would match itself forever. + test('fails loudly when identical output is unparseable', () => { + expect(() => isJsEquivalent('const = ;', 'const = ;', ['flow'])).toThrow( + /Failed to parse the expected output as JavaScript/, + ); + }); +}); + +describe('normalizeResult', () => { + test('normalizes a successful transform', () => { + const result = normalizeResult( + FIXTURE, + validSuccess({ + css: '.a{color:red} ', + js: ' export const a = 1; ', + metadata: [['a', { rtl: null, ltr: '.a{color:red}' }, 3000]], + warnings: ['[@stylexjs/babel-plugin] heads up'], + }), + IMPLEMENTATION_TAG, + ); + + expect(result).toEqual({ + css: '.a{color:red}', + errors: [], + js: 'export const a = 1;', + metadata: [['a', { ltr: '.a{color:red}', rtl: null }, 3000]], + status: 'ok', + warnings: ['[stylex] heads up'], + }); + // Babel's output would still pass the golden test if these slipped: with + // `js` in the exact part every implementation would have to print like + // Babel, and a `jsPart` that lost the code would compare no JS at all. + expect(exactPart(result)).not.toHaveProperty('js'); + expect(jsPart(result)).toBe('export const a = 1;'); + }); + + test('normalizes a failed transform', () => { + const result = normalizeResult(FIXTURE, { + error: { + message: `${FIXTURE.dir}/input.js: create() can only accept an object.\n> 1 | x\n | ^`, + }, + errors: [], + status: 'error', + warnings: [], + }); + + expect(result).toEqual({ + error: { message: 'create() can only accept an object.' }, + errors: [], + status: 'error', + warnings: [], + }); + }); + + test.each([ + [ + 'an unknown status', + { errors: [], status: 'failed', warnings: [] }, + /result.status/, + ], + [ + 'missing diagnostics', + { css: '', js: '', metadata: [], status: 'ok' }, + /result.warnings/, + ], + [ + 'a non-string diagnostic', + validSuccess({ warnings: [{}] }), + /result.warnings/, + ], + [ + 'missing JavaScript', + { css: '', errors: [], metadata: [], status: 'ok', warnings: [] }, + /result.js/, + ], + [ + 'missing metadata', + { css: '', errors: [], js: '', status: 'ok', warnings: [] }, + /result.metadata/, + ], + [ + 'missing CSS', + { errors: [], js: '', metadata: [], status: 'ok', warnings: [] }, + /result.css/, + ], + [ + 'a missing fatal message', + { error: {}, errors: [], status: 'error', warnings: [] }, + /result.error.message/, + ], + ])('rejects %s', (_description, result, expectedError) => { + expect(() => normalizeResult(FIXTURE, result)).toThrow(expectedError); + }); +}); + +describe.each(getFixtureNames())('fixture %s', (name) => { + // `loadFixture` ignores fields it does not know, so a misspelled + // `pluginOption` would compile the fixture with default options and record + // that as its expectation. `syntax` must stay abstract names: a string would + // be split into one-letter parser plugins, and a Babel `[name, options]` pair + // would make the contract Babel-specific. + test('manifest sets only schema fields, a description, and an existing entry', () => { + const manifest = readJson(path.join(FIXTURES_DIR, name, MANIFEST_FILE)); + const fixture = loadFixture(name); + + expect( + Object.keys(manifest).filter((field) => !MANIFEST_FIELDS.includes(field)), + ).toEqual([]); + expect(fixture.description).toMatch(/\S/); + expect(fixture.syntax).toBeInstanceOf(Array); + expect(fixture.syntax.filter((item) => typeof item !== 'string')).toEqual( + [], + ); + expect(fs.existsSync(fixture.entryPath)).toBe(true); + }); + + // `readExpected` normalizes on read, so a hand edit that normalization undoes + // (unsorted keys, an absolute path, trailing CSS whitespace, a code frame) + // would still pass the golden test while the stored file drifts from what + // `writeExpected` records, and from what an implementation reading + // expected.json directly compares against. + test('expected.json is stored already normalized', () => { + const stored = readJson(getExpectedPath(name)); + + expect(JSON.stringify(stored, null, 2)).toBe( + JSON.stringify(readExpected(name), null, 2), + ); + }); +}); + +describe('loadFixture', () => { + test('default arrays and objects are isolated between loads', () => { + const first = loadFixture('create-basic'); + const second = loadFixture('create-basic'); + + expect(first.processOptions).not.toBe(second.processOptions); + expect(first.syntax).not.toBe(second.syntax); + + first.processOptions.useLayers = true; + first.syntax.push('jsx'); + + expect(second.processOptions.useLayers).toBe(false); + expect(second.syntax).toEqual(['flow']); + }); +}); diff --git a/packages/compiler-conformance/fixtures/create-basic/expected.json b/packages/compiler-conformance/fixtures/create-basic/expected.json new file mode 100644 index 000000000..fc6bdd2c0 --- /dev/null +++ b/packages/compiler-conformance/fixtures/create-basic/expected.json @@ -0,0 +1,25 @@ +{ + "css": ".xrkmrrc{background-color:red}\n.xju2f9n{color:blue}", + "errors": [], + "js": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport * as stylex from '@stylexjs/stylex';\nexport const styles = {\n root: {\n kWkggS: \"xrkmrrc\",\n kMwMTN: \"xju2f9n\",\n $$css: true\n }\n};", + "metadata": [ + [ + "xrkmrrc", + { + "ltr": ".xrkmrrc{background-color:red}", + "rtl": null + }, + 3000 + ], + [ + "xju2f9n", + { + "ltr": ".xju2f9n{color:blue}", + "rtl": null + }, + 3000 + ] + ], + "status": "ok", + "warnings": [] +} diff --git a/packages/compiler-conformance/fixtures/create-basic/input.js b/packages/compiler-conformance/fixtures/create-basic/input.js new file mode 100644 index 000000000..7014bc7a9 --- /dev/null +++ b/packages/compiler-conformance/fixtures/create-basic/input.js @@ -0,0 +1,15 @@ +/** + * 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. + */ + +import * as stylex from '@stylexjs/stylex'; + +export const styles = stylex.create({ + root: { + backgroundColor: 'red', + color: 'blue', + }, +}); diff --git a/packages/compiler-conformance/fixtures/create-basic/manifest.json b/packages/compiler-conformance/fixtures/create-basic/manifest.json new file mode 100644 index 000000000..cc7c695b2 --- /dev/null +++ b/packages/compiler-conformance/fixtures/create-basic/manifest.json @@ -0,0 +1,4 @@ +{ + "description": "stylex.create() with static declarations compiles to a style object and two atomic CSS rules.", + "entry": "input.js" +} diff --git a/packages/compiler-conformance/fixtures/define-vars-theme/expected.json b/packages/compiler-conformance/fixtures/define-vars-theme/expected.json new file mode 100644 index 000000000..f95273d6f --- /dev/null +++ b/packages/compiler-conformance/fixtures/define-vars-theme/expected.json @@ -0,0 +1,33 @@ +{ + "css": ":root, .xnsr5hh{--xkoxvr5:red;--x19047mj:white;}\n@media (prefers-color-scheme: dark){:root, .xnsr5hh{--x19047mj:black;}}\n.x19gg9ko.x19gg9ko, .x19gg9ko.x19gg9ko:root{--x19047mj:yellow;--xkoxvr5:blue;}", + "errors": [], + "js": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport * as stylex from '@stylexjs/stylex';\nexport const vars = {\n fg: \"var(--xkoxvr5)\",\n bg: \"var(--x19047mj)\",\n __varGroupHash__: \"xnsr5hh\"\n};\nexport const theme = {\n xnsr5hh: \"x19gg9ko xnsr5hh\",\n $$css: true\n};", + "metadata": [ + [ + "xnsr5hh", + { + "ltr": ":root, .xnsr5hh{--xkoxvr5:red;--x19047mj:white;}", + "rtl": null + }, + 0.1 + ], + [ + "xnsr5hh-1lveb7", + { + "ltr": "@media (prefers-color-scheme: dark){:root, .xnsr5hh{--x19047mj:black;}}", + "rtl": null + }, + 0.2 + ], + [ + "x19gg9ko", + { + "ltr": ".x19gg9ko, .x19gg9ko:root{--x19047mj:yellow;--xkoxvr5:blue;}", + "rtl": null + }, + 0.5 + ] + ], + "status": "ok", + "warnings": [] +} diff --git a/packages/compiler-conformance/fixtures/define-vars-theme/input.stylex.js b/packages/compiler-conformance/fixtures/define-vars-theme/input.stylex.js new file mode 100644 index 000000000..3b1c47060 --- /dev/null +++ b/packages/compiler-conformance/fixtures/define-vars-theme/input.stylex.js @@ -0,0 +1,21 @@ +/** + * 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. + */ + +import * as stylex from '@stylexjs/stylex'; + +export const vars = stylex.defineVars({ + fg: 'red', + bg: { + default: 'white', + '@media (prefers-color-scheme: dark)': 'black', + }, +}); + +export const theme = stylex.createTheme(vars, { + fg: 'blue', + bg: 'yellow', +}); diff --git a/packages/compiler-conformance/fixtures/define-vars-theme/manifest.json b/packages/compiler-conformance/fixtures/define-vars-theme/manifest.json new file mode 100644 index 000000000..1075071b5 --- /dev/null +++ b/packages/compiler-conformance/fixtures/define-vars-theme/manifest.json @@ -0,0 +1,9 @@ +{ + "description": "stylex.defineVars() and stylex.createTheme() emit CSS custom properties, including a media-query override. The entry is a `.stylex.js` file so that `commonJS` module resolution derives a canonical name for it; that name is `:`, so the generated hashes depend on the fixture's location inside this package but not on where the repository is checked out.", + "entry": "input.stylex.js", + "pluginOptions": { + "unstable_moduleResolution": { + "type": "commonJS" + } + } +} diff --git a/packages/compiler-conformance/fixtures/keyframes-and-when/expected.json b/packages/compiler-conformance/fixtures/keyframes-and-when/expected.json new file mode 100644 index 000000000..231d3a0b1 --- /dev/null +++ b/packages/compiler-conformance/fixtures/keyframes-and-when/expected.json @@ -0,0 +1,41 @@ +{ + "css": "@keyframes xekv6nw-B{0%{opacity:0;}100%{opacity:1;}}\n.x127lhb5:not(#\\#){animation-name:xekv6nw-B}\n.x1e2nbdu:not(#\\#){color:red}\n.x1tavwxn.x1tavwxn:where(.x-default-marker:focus *):not(#\\#){color:blue}", + "errors": [], + "js": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport * as stylex from '@stylexjs/stylex';\nexport const styles = {\n root: {\n kKVMdj: \"x127lhb5\",\n kMwMTN: \"x1e2nbdu x1tavwxn\",\n $$css: true\n }\n};", + "metadata": [ + [ + "xekv6nw-B", + { + "ltr": "@keyframes xekv6nw-B{0%{opacity:0;}100%{opacity:1;}}", + "rtl": null + }, + 0 + ], + [ + "x127lhb5", + { + "ltr": ".x127lhb5{animation-name:xekv6nw-B}", + "rtl": null + }, + 3000 + ], + [ + "x1e2nbdu", + { + "ltr": ".x1e2nbdu{color:red}", + "rtl": null + }, + 3000 + ], + [ + "x1tavwxn", + { + "ltr": ".x1tavwxn.x1tavwxn:where(.x-default-marker:focus *){color:blue}", + "rtl": null + }, + 3011.5 + ] + ], + "status": "ok", + "warnings": [] +} diff --git a/packages/compiler-conformance/fixtures/keyframes-and-when/input.js b/packages/compiler-conformance/fixtures/keyframes-and-when/input.js new file mode 100644 index 000000000..3dbfb8ee5 --- /dev/null +++ b/packages/compiler-conformance/fixtures/keyframes-and-when/input.js @@ -0,0 +1,25 @@ +/** + * 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. + */ + +import * as stylex from '@stylexjs/stylex'; + +export const styles = stylex.create({ + root: { + animationName: stylex.keyframes({ + '0%': { + opacity: 0, + }, + '100%': { + opacity: 1, + }, + }), + color: { + default: 'red', + [stylex.when.ancestor(':focus')]: 'blue', + }, + }, +}); diff --git a/packages/compiler-conformance/fixtures/keyframes-and-when/manifest.json b/packages/compiler-conformance/fixtures/keyframes-and-when/manifest.json new file mode 100644 index 000000000..b00fdc7f3 --- /dev/null +++ b/packages/compiler-conformance/fixtures/keyframes-and-when/manifest.json @@ -0,0 +1,4 @@ +{ + "description": "stylex.keyframes() emits an @keyframes rule at priority 0 and stylex.when.ancestor() emits a conditional rule with raised specificity.", + "entry": "input.js" +} diff --git a/packages/compiler-conformance/fixtures/options-warning/expected.json b/packages/compiler-conformance/fixtures/options-warning/expected.json new file mode 100644 index 000000000..7e9460a62 --- /dev/null +++ b/packages/compiler-conformance/fixtures/options-warning/expected.json @@ -0,0 +1,10 @@ +{ + "css": "", + "errors": [ + "[stylex] Expected (options.enableFontSizePxToRem) to be a boolean, but got `\"true\"`." + ], + "js": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nexport const value = 1;", + "metadata": [], + "status": "ok", + "warnings": [] +} diff --git a/packages/compiler-conformance/fixtures/options-warning/input.js b/packages/compiler-conformance/fixtures/options-warning/input.js new file mode 100644 index 000000000..b1fb6b659 --- /dev/null +++ b/packages/compiler-conformance/fixtures/options-warning/input.js @@ -0,0 +1,8 @@ +/** + * 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. + */ + +export const value = 1; diff --git a/packages/compiler-conformance/fixtures/options-warning/manifest.json b/packages/compiler-conformance/fixtures/options-warning/manifest.json new file mode 100644 index 000000000..468c9e31f --- /dev/null +++ b/packages/compiler-conformance/fixtures/options-warning/manifest.json @@ -0,0 +1,7 @@ +{ + "description": "An option of the wrong type reports a non-fatal diagnostic on the error channel while the transform still succeeds.", + "entry": "input.js", + "pluginOptions": { + "enableFontSizePxToRem": "true" + } +} diff --git a/packages/compiler-conformance/fixtures/props-and-merge/expected.json b/packages/compiler-conformance/fixtures/props-and-merge/expected.json new file mode 100644 index 000000000..b1863f4b0 --- /dev/null +++ b/packages/compiler-conformance/fixtures/props-and-merge/expected.json @@ -0,0 +1,25 @@ +{ + "css": ".x1e2nbdu{color:red}\n.x17z2mba:hover{color:blue}", + "errors": [], + "js": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport * as stylex from '@stylexjs/stylex';\nimport React from 'react';\nexport function Button(props) {\n return