diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cda0231..a565a22 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,3 +33,76 @@ npm run serve # Lint code npm run lint ``` + +### Development Scripts + +The project includes several utility scripts in the `scripts/` directory to help with development and maintenance: + +#### `scripts/build-docs.js` + +Automatically generates documentation by injecting CSS test fixtures into markdown files. + +```bash +node scripts/build-docs.js +``` + +This script: + +- Scans for `` placeholders in markdown files +- Replaces them with the corresponding input/expected CSS from `test/fixtures/` +- Updates documentation files like `docs/TEST_FIXTURES.md` and package READMEs + +#### `scripts/generate-fixtures.js` + +Utility script for generating expected output files for new test fixtures by running them through the polyfill transformation engine. + +```bash +node scripts/generate-fixtures.js +``` + +This script: + +- Reads CSS from `.input.css` files in `test/fixtures/` +- Runs them through the `buildTimeTransform` function +- Generates corresponding `.expected.css` files +- Useful when adding new test cases to ensure correct expected outputs + +**Note:** This script is primarily for development use when creating new fixtures. Modify the `newFixtures` array in the script to specify which fixtures to process. + +### Test Fixture System + +The project uses a centralized test fixture system located in `test/fixtures/` with pairs of `.input.css` and `.expected.css` files. This system is managed through `test/scripts/fixture-utils.js`. + +#### Adding New Test Fixtures + +1. **Create fixture files:** + + ```text + test/fixtures/my-new-feature.input.css + test/fixtures/my-new-feature.expected.css + ``` + +2. **Add to fixture arrays in `test/scripts/fixture-utils.js`:** + - `basicFixtureTests` - for build-time transformable features + - `runtimeOnlyFixtureTests` - for runtime-only features + - `postcssFixtureTests` - for PostCSS plugin tests + +3. **Generate expected outputs (optional):** + + ```bash + # Modify scripts/generate-fixtures.js to include your fixture + node scripts/generate-fixtures.js + ``` + +4. **Update documentation:** + ```bash + node scripts/build-docs.js + ``` + +#### Fixture Categories + +- **Build-time transformable**: Features that can be converted to native CSS (`@media`, `@supports`) +- **Runtime-only**: Features requiring JavaScript processing (`style()` queries, boolean negation, empty token streams) +- **PostCSS-specific**: Additional fixtures used only by the PostCSS plugin + +For more details, see [`docs/TEST_FIXTURES.md`](docs/TEST_FIXTURES.md). diff --git a/docs/TEST_FIXTURES.md b/docs/TEST_FIXTURES.md index bba4f67..ba5bb7f 100644 --- a/docs/TEST_FIXTURES.md +++ b/docs/TEST_FIXTURES.md @@ -70,7 +70,7 @@ This document demonstrates the centralized test fixture system that provides a s ```css .test { - color: if(style(--theme): var(--primary) ; else: blue); + color: if(style(--theme): var(--primary); else: blue); } ``` diff --git a/packages/css-if-polyfill/test/integrated.test.js b/packages/css-if-polyfill/test/integrated.test.js index f594197..dea3b44 100644 --- a/packages/css-if-polyfill/test/integrated.test.js +++ b/packages/css-if-polyfill/test/integrated.test.js @@ -2,7 +2,8 @@ import { beforeEach, describe, expect, test } from 'vitest'; import { basicFixtureTests, loadFixture, - normalizeCSS + normalizeCSS, + runtimeOnlyFixtureTests } from '../../../test/scripts/fixture-utils.js'; import { buildTimeTransform, init, processCSSText } from '../src/index.js'; @@ -30,6 +31,22 @@ describe('Integrated CSS if() Polyfill', () => { }); } + // Runtime-only fixtures that cannot be transformed at build time + for (const { fixture, description } of runtimeOnlyFixtureTests) { + test(description, () => { + const { input, expected } = loadFixture(fixture); + const result = buildTimeTransform(input); + + // These fixtures should preserve the if() functions for runtime processing + expect(result.hasRuntimeRules).toBe(true); + expect(normalizeCSS(result.runtimeCSS)).toBe( + normalizeCSS(expected) + ); + // NativeCSS should be empty or contain only fallback values + expect(result.nativeCSS).toBe(''); + }); + } + test('handles mixed media and style conditions', () => { const { input } = loadFixture('mixed-conditions'); const result = buildTimeTransform(input); diff --git a/packages/postcss-if-function/src/index.js b/packages/postcss-if-function/src/index.js index cef8157..ca69b1c 100644 --- a/packages/postcss-if-function/src/index.js +++ b/packages/postcss-if-function/src/index.js @@ -71,8 +71,19 @@ function postcssIfFunction(options = {}) { // Apply transformation const transformed = buildTimeTransform(cssText); + // If no transformations were made, keep original if (transformed.nativeCSS === cssText) { - // No transformations were made + return; + } + + // If we have runtime rules but no native CSS (or only empty nativeCSS), + // we can't transform at build-time, so preserve the original + if ( + (!transformed.nativeCSS || + transformed.nativeCSS.trim() === '') && + transformed.hasRuntimeRules + ) { + // Keep the original CSS unchanged since these features need runtime processing return; } diff --git a/packages/postcss-if-function/test/plugin.test.js b/packages/postcss-if-function/test/plugin.test.js index 217d31b..a72afb6 100644 --- a/packages/postcss-if-function/test/plugin.test.js +++ b/packages/postcss-if-function/test/plugin.test.js @@ -3,7 +3,8 @@ import { describe, expect, it, vi } from 'vitest'; import { loadFixture, normalizeCSS, - postcssFixtureTests + postcssFixtureTests, + runtimeOnlyFixtureTests } from '../../../test/scripts/fixture-utils.js'; import { postcssIfFunction } from '../src/index.js'; @@ -27,6 +28,16 @@ describe('postcss-if-function plugin', () => { }); } + // Runtime-only fixtures that cannot be transformed at build time + // PostCSS should preserve these unchanged + for (const { fixture, description } of runtimeOnlyFixtureTests) { + it(`should preserve ${description} unchanged`, async () => { + const { input } = loadFixture(fixture); + // For runtime-only features, PostCSS should preserve the original CSS + await run(input, input); + }); + } + it('should work with logTransformations option', async () => { const { input, expected } = loadFixture('basic-media'); diff --git a/scripts/generate-fixtures.js b/scripts/generate-fixtures.js new file mode 100644 index 0000000..1c9d082 --- /dev/null +++ b/scripts/generate-fixtures.js @@ -0,0 +1,72 @@ +#!/usr/bin/env node + +/** + * Generate expected outputs for new fixtures + * + * This development utility script helps generate expected output files for new test fixtures + * by running input CSS through the polyfill transformation engine. + * + * Usage: + * 1. Create your .input.css files in test/fixtures/ + * 2. Add the fixture names to the newFixtures array below + * 3. Run: node scripts/generate-fixtures.js + * 4. Review and adjust the generated .expected.css files as needed + * + * Note: Always review the generated outputs to ensure they match expected behavior. + */ + +import { readFile, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// Import the transform function +import { buildTimeTransform } from '../packages/css-if-polyfill/src/index.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const fixturesDir = path.join(__dirname, '..', 'test', 'fixtures'); + +// Configure which fixtures to process +// Add fixture names (without .input.css suffix) to generate expected outputs +const newFixtures = [ + 'empty-token-stream', + 'cyclic-substitution', + 'complex-supports', + 'boolean-negation', + 'multiple-mixed-conditions', + 'nested-media-features' +]; + +async function generateExpectedOutputs() { + const results = await Promise.all( + newFixtures.map(async (fixture) => { + const inputPath = path.join(fixturesDir, `${fixture}.input.css`); + const expectedPath = path.join( + fixturesDir, + `${fixture}.expected.css` + ); + + try { + const input = await readFile(inputPath, 'utf8'); + console.log(`Processing ${fixture}...`); + console.log('Input:', input); + + const result = buildTimeTransform(input); + console.log('Output:', result); + + // Extract the appropriate CSS output (nativeCSS for static transforms, runtimeCSS for dynamic) + const outputCSS = result.nativeCSS || result.runtimeCSS || ''; + + await writeFile(expectedPath, outputCSS); + console.log(`✓ Generated ${fixture}.expected.css\n`); + return { fixture, success: true }; + } catch (error) { + console.error(`✗ Failed to process ${fixture}:`, error.message); + return { fixture, success: false, error }; + } + }) + ); + + return results; +} + +await generateExpectedOutputs(); diff --git a/test/fixtures/boolean-negation.expected.css b/test/fixtures/boolean-negation.expected.css new file mode 100644 index 0000000..6845802 --- /dev/null +++ b/test/fixtures/boolean-negation.expected.css @@ -0,0 +1,16 @@ +.test { + color: black; +} +@media (not (print)) { + .test { + color: blue; + } +} +.test { + display: grid; +} +@supports (not display: grid) { + .test { + display: block; + } +} diff --git a/test/fixtures/boolean-negation.input.css b/test/fixtures/boolean-negation.input.css new file mode 100644 index 0000000..020ec29 --- /dev/null +++ b/test/fixtures/boolean-negation.input.css @@ -0,0 +1,4 @@ +.test { + color: if(not media(print): blue; else: black); + display: if(not supports(display: grid): block; else: grid); +} diff --git a/test/fixtures/complex-supports.expected.css b/test/fixtures/complex-supports.expected.css new file mode 100644 index 0000000..16d5062 --- /dev/null +++ b/test/fixtures/complex-supports.expected.css @@ -0,0 +1,21 @@ +.test { + display: block; +} +@supports ((display: grid) and (gap: 1rem)) { + .test { + display: grid; + } +} +.test { + color: blue; +} +@supports (color: hsl(from red h s l)) { + .test { + color: hsl(from red h s l); + } +} +@supports (color: lab(50% 20 -30)) { + .test { + color: lab(50% 20 -30); + } +} diff --git a/test/fixtures/complex-supports.input.css b/test/fixtures/complex-supports.input.css new file mode 100644 index 0000000..e654b45 --- /dev/null +++ b/test/fixtures/complex-supports.input.css @@ -0,0 +1,4 @@ +.test { + display: if(supports((display: grid) and (gap: 1rem)): grid; else: block); + color: if(supports(color: lab(50% 20 -30)): lab(50% 20 -30); supports(color: hsl(from red h s l)): hsl(from red h s l); else: blue); +} diff --git a/test/fixtures/cyclic-substitution.expected.css b/test/fixtures/cyclic-substitution.expected.css new file mode 100644 index 0000000..98f16db --- /dev/null +++ b/test/fixtures/cyclic-substitution.expected.css @@ -0,0 +1,3 @@ +.test { + --foo: default; +} diff --git a/test/fixtures/cyclic-substitution.input.css b/test/fixtures/cyclic-substitution.input.css new file mode 100644 index 0000000..ab4acb8 --- /dev/null +++ b/test/fixtures/cyclic-substitution.input.css @@ -0,0 +1,3 @@ +.test { + --foo: if(style(--foo: bar): baz; else: default); +} diff --git a/test/fixtures/empty-token-stream.expected.css b/test/fixtures/empty-token-stream.expected.css new file mode 100644 index 0000000..9fa3c82 --- /dev/null +++ b/test/fixtures/empty-token-stream.expected.css @@ -0,0 +1,3 @@ +.test { + background: transparent; +} diff --git a/test/fixtures/empty-token-stream.input.css b/test/fixtures/empty-token-stream.input.css new file mode 100644 index 0000000..d2c8165 --- /dev/null +++ b/test/fixtures/empty-token-stream.input.css @@ -0,0 +1,4 @@ +.test { + color: if(media(print): red); + background: if(supports(display: none): transparent); +} diff --git a/test/fixtures/multiple-mixed-conditions.expected.css b/test/fixtures/multiple-mixed-conditions.expected.css new file mode 100644 index 0000000..add876a --- /dev/null +++ b/test/fixtures/multiple-mixed-conditions.expected.css @@ -0,0 +1,18 @@ +.responsive { + padding: 15px; +} +@supports (display: grid) { + .responsive { + padding: 25px; + } +} +@media (width >= 768px) { + .responsive { + padding: 30px; + } +} +@media (width >= 1200px) { + .responsive { + padding: 40px; + } +} diff --git a/test/fixtures/multiple-mixed-conditions.input.css b/test/fixtures/multiple-mixed-conditions.input.css new file mode 100644 index 0000000..aa7c15f --- /dev/null +++ b/test/fixtures/multiple-mixed-conditions.input.css @@ -0,0 +1,9 @@ +.responsive { + padding: if( + media(width >= 1200px): 40px; + media(width >= 768px): 30px; + supports(display: grid): 25px; + style(--large-padding): 35px; + else: 15px + ); +} diff --git a/test/fixtures/nested-media-features.expected.css b/test/fixtures/nested-media-features.expected.css new file mode 100644 index 0000000..7f49ef7 --- /dev/null +++ b/test/fixtures/nested-media-features.expected.css @@ -0,0 +1,16 @@ +.test { + width: 100%; +} +@media ((orientation: landscape) and (hover: hover)) { + .test { + width: 80%; + } +} +.test { + font-size: 1rem; +} +@media ((min-width: 768px) and (max-width: 1024px) and (prefers-reduced-motion: no-preference)) { + .test { + font-size: 1.2rem; + } +} diff --git a/test/fixtures/nested-media-features.input.css b/test/fixtures/nested-media-features.input.css new file mode 100644 index 0000000..5b05e32 --- /dev/null +++ b/test/fixtures/nested-media-features.input.css @@ -0,0 +1,4 @@ +.test { + width: if(media((orientation: landscape) and (hover: hover)): 80%; else: 100%); + font-size: if(media((min-width: 768px) and (max-width: 1024px) and (prefers-reduced-motion: no-preference)): 1.2rem; else: 1rem); +} diff --git a/test/scripts/fixture-utils.js b/test/scripts/fixture-utils.js index e446735..4c6b3d3 100644 --- a/test/scripts/fixture-utils.js +++ b/test/scripts/fixture-utils.js @@ -122,6 +122,38 @@ export const basicFixtureTests = [ { fixture: 'no-if-functions', description: 'preserve CSS without if() functions' + }, + { + fixture: 'complex-supports', + description: 'handle complex supports conditions with multiple values' + }, + { + fixture: 'nested-media-features', + description: 'handle complex nested media feature queries' + } +]; + +/** + * Runtime-only test configurations for features that cannot be transformed at build time + */ +export const runtimeOnlyFixtureTests = [ + { + fixture: 'empty-token-stream', + description: + 'handle if() functions without else clause (empty token stream)' + }, + { + fixture: 'cyclic-substitution', + description: 'prevent cyclic substitution context in style queries' + }, + { + fixture: 'boolean-negation', + description: 'handle boolean negation in conditions' + }, + { + fixture: 'multiple-mixed-conditions', + description: + 'handle multiple mixed condition types in single if() function' } ];