UNPKG

postcss-jit-props

Version:

PostCSS plugin to insert variables from a data source based on discovered usage

593 lines (540 loc) 12.3 kB
/** * Copyright 2020 Google Inc. All Rights Reserved. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const postcss = require('postcss') const plugin = require('./') const MockProps = { '--red': '#f00', '--pink': '#ffc0cb', '--h': 200, '--s': '50%', '--l': '50%', '--size-1': '1rem', '--size-2': '2rem', '--fade-in': 'fade-in .5s ease', '--fade-in-@': '@keyframes fade-in {to { opacity: 1 }}', '--adaptive-fade': 'adaptive-fade .5s ease', '--adaptive-fade-@': '@keyframes adaptive-fade {to { background: white }}', '--adaptive-fade-@media:dark': '@keyframes adaptive-fade {to { background: black }}', '--dark': '@custom-media --dark (prefers-color-scheme: dark);', '--text': 'white', '--text-@media:dark': 'black', } const MockPropsWithCustomAdaptiveProp = { '--text': 'white', '--text-dark': 'black', } async function run (input, output, options = { }) { let result = await postcss([plugin(options)]).process(input, { from: 'input.css', to: 'output.css', map: { inline: false } }) expect(result.css.replace('\n/*# sourceMappingURL=output.css.map */', '')).toEqual(output) expect(result.warnings()).toHaveLength(0) if (options.files?.length) { expect(result.messages.filter(x => x.type === 'dependency')).toHaveLength(options.files?.length) } const map = result.map.toJSON() expect(map.sources).toEqual(['input.css']) } it('Can jit a single prop', async () => { await run( `a { color: var(--red); }`, `:root { --red: #f00; } a { color: var(--red); }`, MockProps ) }) it('Can jit a single prop with spaces', async () => { await run( `a { color: var( --red ); }`, `:root { --red: #f00; } a { color: var( --red ); }`, MockProps ) }) it('Can jit a single prop that has fallbacks', async () => { await run( `a { color: var(--red, hotpink); }`, `:root { --red: #f00; } a { color: var(--red, hotpink); }`, MockProps ) }) it('Can jit a single prop with spaces that has fallbacks', async () => { await run( `a { color: var( --red, hotpink); }`, `:root { --red: #f00; } a { color: var( --red, hotpink); }`, MockProps ) }) it('Can jit a single prop that has fallbacks and nested props', async () => { await run( `a { color: var(--red, var(--pink), hotpink); }`, `:root { --red: #f00; --pink: #ffc0cb; } a { color: var(--red, var(--pink), hotpink); }`, MockProps ) }) it('Can jit a single, undefined prop that has fallbacks and nested props', async () => { await run( `a { color: var(--orange, var(--pink), hotpink); }`, `:root { --pink: #ffc0cb; } a { color: var(--orange, var(--pink), hotpink); }`, MockProps ) }) it('Can jit a single prop with spaces that has fallbacks and nested props', async () => { await run( `a { color: var( --red, var( --pink ), hotpink); }`, `:root { --red: #f00; --pink: #ffc0cb; } a { color: var( --red, var( --pink ), hotpink); }`, MockProps ) }) it('Can jit multiple props', async () => { await run( `a { color: var(--red); border-color: var(--pink); padding-block-start: var( --size-1 ); }`, `:root { --red: #f00; --pink: #ffc0cb; --size-1: 1rem; } a { color: var(--red); border-color: var(--pink); padding-block-start: var( --size-1 ); }`, MockProps ) }) it('Can jit multiple props from shorthand', async () => { await run( `a { padding-block: var(--size-1) var( --size-2 ); }`, `:root { --size-1: 1rem; --size-2: 2rem; } a { padding-block: var(--size-1) var( --size-2 ); }`, MockProps ) }) it('Can jit props from inside functions', async () => { await run( `a { color: hsl(var(--h) var(--s) var( --l )); }`, `:root { --h: 200; --s: 50%; --l: 50%; } a { color: hsl(var(--h) var(--s) var( --l )); }`, MockProps ) }) it('Only adds a prop one time to :root', async () => { await run( `a { color: var(--red); border-color: var(--red ); }`, `:root { --red: #f00; } a { color: var(--red); border-color: var(--red ); }`, MockProps ) }) it('Can jit props into a layer', async () => { await run( `a { color: hsl(var(--h) var(--s) var( --l )); }`, `@layer test { :root { --h: 200; --s: 50%; --l: 50%; } } a { color: hsl(var(--h) var(--s) var( --l )); }`, { ... MockProps, layer: 'test', } ) }) it('Can jit a keyframe animation', async () => { await run( `a { animation: var(--fade-in); }`, `:root { --fade-in: fade-in .5s ease; }a { animation: var(--fade-in); }@keyframes fade-in {to { opacity: 1 }}`, MockProps ) }) it('Can jit an adaptive keyframe animation', async () => { await run( `a { animation: var(--adaptive-fade); }`, `:root { --adaptive-fade: adaptive-fade .5s ease; }a { animation: var(--adaptive-fade); }@keyframes adaptive-fade {to { background: white }}@media (prefers-color-scheme: dark) {:root {}@keyframes adaptive-fade {to { background: black }} }`, MockProps ) }) it('Can jit @custom-media', async () => { await run( `@media (--dark) { a { color: white; } }`, `@custom-media --dark (prefers-color-scheme: dark); :root{} @media (--dark) { a { color: white; } }`, MockProps ) }) it('Can jit @custom-media with spaces', async () => { await run( `@media ( --dark ) { a { color: white; } }`, `@custom-media --dark (prefers-color-scheme: dark); :root{} @media ( --dark ) { a { color: white; } }`, MockProps ) }) it('Can jit props from JSON', async () => { await run( `a { color: var(--red); border-color: var( --pink ); }`, `:root { --red: #f00; --pink: #ffc0cb; } a { color: var(--red); border-color: var( --pink ); }`, MockProps ) }) it('Can jit props from a CSS file', async () => { await run( `@media (--dark) { a { color: var(--red); border-color: var( --pink ); animation: var(--fade-in); } }`, `@custom-media --dark (prefers-color-scheme: dark); :root{ --red: #f00; --pink: #ffc0cb; --fade-in: fade-in .5s ease; } @media (--dark) { a { color: var(--red); border-color: var( --pink ); animation: var(--fade-in); } } @keyframes fade-in {to { opacity: 1 }}`, { files: ['./props.test.css']} ) }) it('Can jit props from a CSS file via glob', async () => { await run( `@media (--dark) { a { color: var(--red); border-color: var( --pink ); animation: var(--fade-in); } }`, `@custom-media --dark (prefers-color-scheme: dark); :root{ --red: #f00; --pink: #ffc0cb; --fade-in: fade-in .5s ease; } @media (--dark) { a { color: var(--red); border-color: var( --pink ); animation: var(--fade-in); } } @keyframes fade-in {to { opacity: 1 }}`, { files: ['./*.test.css']} ) }) it('Can fail without srcProps options gracefully', async () => { console.warn = jest.fn() await postcss([plugin({})]).process(``, { from: undefined }) expect(console.warn).toHaveBeenCalledWith('postcss-jit-props: Variable source(s) not passed.') }) it('Can jit props to a custom selector', async () => { await run( `a { color: var(--red); }`, `:global { --red: #f00; } a { color: var(--red); }`, { ... MockProps, custom_selector: ':global', } ) }) it('Can jit light and dark props to a custom selector', async () => { await run( `a { color: var(--text); }`, `:global { --text: white; } a { color: var(--text); } @media (prefers-color-scheme: dark) { :global { --text: black; } }`, { ... MockProps, custom_selector: ':global', } ) }) it('Can jit light & dark props to a custom selector for use with a client side switch', async () => { await run( `a { color: var(--text); }`, `.light { --text: white; } a { color: var(--text); } @media (prefers-color-scheme: dark) { .dark { --text: black; } }`, { ... MockProps, custom_selector: '.light', custom_selector_dark: '.dark', } ) }) it('Wont create a :root {} context unless props are found', async () => { await run( `a { color: red; }`, `a { color: red; }`, { ... MockProps } ) }) it('Can jit a light and dark adaptive prop', async () => { await run( `p { color: var(--text); }`, `:root { --text: white; } p { color: var(--text); } @media (prefers-color-scheme: dark) { :root { --text: black; } }`, MockProps ) }) it('Can jit a light and dark color with a custom adaptive prop parameter', async () => { await run( `p { color: var(--text); }`, `:root { --text: white; } p { color: var(--text); } @media (prefers-color-scheme: dark) { :root { --text: black; } }`, { ...MockPropsWithCustomAdaptiveProp, adaptive_prop_selector: '-dark' } ) }) it('Supports parallel runners', async () => { const pluginInstance = plugin({ '--red': '#f00', '--pink': '#ffc0cb', }); let [resultA, resultB, resultC, resultD] = await Promise.all([ postcss([pluginInstance]).process(`a { color: var(--red); }`, { from: undefined }), postcss([pluginInstance]).process(`a { color: var(--pink); }`, { from: undefined }), postcss([pluginInstance]).process(`a { color: var(--red); }`, { from: undefined }), postcss([pluginInstance]).process(`a { color: var(--pink); }`, { from: undefined }), ]) let resultE = await postcss([pluginInstance]).process(`a { color: green; }`, { from: undefined }) expect(resultA.css).toEqual(':root { --red: #f00; }\na { color: var(--red); }') expect(resultA.warnings()).toHaveLength(0) expect(resultB.css).toEqual(':root { --pink: #ffc0cb; }\na { color: var(--pink); }') expect(resultB.warnings()).toHaveLength(0) expect(resultC.css).toEqual(':root { --red: #f00; }\na { color: var(--red); }') expect(resultC.warnings()).toHaveLength(0) expect(resultD.css).toEqual(':root { --pink: #ffc0cb; }\na { color: var(--pink); }') expect(resultD.warnings()).toHaveLength(0) expect(resultE.css).toEqual('a { color: green; }') expect(resultE.warnings()).toHaveLength(0) }) it('Supports parallel runners when reading from a file', async () => { const pluginInstance = plugin({ files: ['./props.test.css'] }); let [resultA, resultB, resultC, resultD] = await Promise.all([ postcss([pluginInstance]).process(`a { color: var(--red); }`, { from: undefined }), postcss([pluginInstance]).process(`a { color: var(--pink); }`, { from: undefined }), postcss([pluginInstance]).process(`a { color: var(--red); }`, { from: undefined }), postcss([pluginInstance]).process(`a { color: var(--pink); }`, { from: undefined }), ]) let resultE = await postcss([pluginInstance]).process(`a { color: green; }`, { from: undefined }) expect(resultA.css).toEqual(':root { --red: #f00; }\na { color: var(--red); }') expect(resultA.warnings()).toHaveLength(0) expect(resultB.css).toEqual(':root { --pink: #ffc0cb; }\na { color: var(--pink); }') expect(resultB.warnings()).toHaveLength(0) expect(resultC.css).toEqual(':root { --red: #f00; }\na { color: var(--red); }') expect(resultC.warnings()).toHaveLength(0) expect(resultD.css).toEqual(':root { --pink: #ffc0cb; }\na { color: var(--pink); }') expect(resultD.warnings()).toHaveLength(0) expect(resultE.css).toEqual('a { color: green; }') expect(resultE.warnings()).toHaveLength(0) }) // situation encountered when using postcs-jit-props for Open Props // together with having @nuxt/fonts module with no global font definition // it still creates an empty .nuxt/nuxt-fonts-global.css file // and postcs-jit-props started to fail trying to parse it // -- // fixed by adding "node.first" null check into parsing method // (lines 130 and 136 in index.js) it('Parses an empty file and rule', async () => { const pluginInstance = plugin({ files: ['./props.test.empty.css'] }); let result = await postcss([pluginInstance]).process(''); expect(result.css).toEqual(""); expect(result.warnings()).toHaveLength(0); })