diff --git a/package-lock.json b/package-lock.json index 4b89873..89946ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "oxlint": "^1.24.0", "publint": "^0.3.15", "tsdown": "^0.21.0", - "typescript": "^5.9.3", + "typescript": "^6.0.2", "vitest": "^4.0.3" }, "engines": { @@ -3438,9 +3438,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index be1d537..68fbfbd 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "oxlint": "^1.24.0", "publint": "^0.3.15", "tsdown": "^0.21.0", - "typescript": "^5.9.3", + "typescript": "^6.0.2", "vitest": "^4.0.3" }, "engines": { diff --git a/test/api.test.js b/test/api.test.js new file mode 100644 index 0000000..97f2b7c --- /dev/null +++ b/test/api.test.js @@ -0,0 +1,54 @@ +import { test, expect } from 'vitest' +import { format } from '../src/lib/index.js' +test('empty input', () => { + let actual = format(``) + let expected = `` + expect(actual).toEqual(expected) +}) +test('handles invalid input', () => { + let actual = format(`;`) + let expected = `` + expect(actual).toEqual(expected) +}) +test('Vadim Makeevs example works', () => { + let actual = format(` + @layer what { + @container (width > 0) { + ul:has(:nth-child(1 of li)) { + @media (height > 0) { + &:hover { + --is: this; + } + } + } + } + } + `) + let expected = `@layer what { + @container (width > 0) { + ul:has(:nth-child(1 of li)) { + @media (height > 0) { + &:hover { + --is: this; + } + } + } + } +}` + expect(actual).toEqual(expected) +}) +test('minified Vadims example', () => { + let actual = format( + `@layer what{@container (width>0){@media (min-height:.001px){ul:has(:nth-child(1 of li)):hover{--is:this}}}}`, + ) + let expected = `@layer what { + @container (width > 0) { + @media (min-height: .001px) { + ul:has(:nth-child(1 of li)):hover { + --is: this; + } + } + } +}` + expect(actual).toEqual(expected) +}) diff --git a/test/atrules.test.js b/test/atrules.test.js new file mode 100644 index 0000000..d3406cc --- /dev/null +++ b/test/atrules.test.js @@ -0,0 +1,351 @@ +import { test, expect } from 'vitest' +import { format, minify } from '../src/lib/index.js' +test('AtRules start on a new line', () => { + let actual = format(` + @media (min-width: 1000px) { + selector { property: value; } + } + @layer test { + selector { property: value; } + } + `) + let expected = `@media (min-width: 1000px) { + selector { + property: value; + } +} + +@layer test { + selector { + property: value; + } +}` + expect(actual).toEqual(expected) +}) +test('Atrule blocks are surrounded by {} with correct spacing and indentation', () => { + let actual = format(` + @media (min-width:1000px){selector{property:value1}} + + @media (min-width:1000px) + { + selector + { + property:value2 + } +}`) + let expected = `@media (min-width: 1000px) { + selector { + property: value1; + } +} + +@media (min-width: 1000px) { + selector { + property: value2; + } +}` + expect(actual).toEqual(expected) +}) +test('adds whitespace between prelude and {', () => { + let actual = format(`@media all{}`) + let expected = `@media all {}` + expect(actual).toEqual(expected) +}) +test('collapses whitespaces in prelude', () => { + let actual = format(`@media all and (min-width: 1000px) {}`) + let expected = `@media all and (min-width: 1000px) {}` + expect(actual).toEqual(expected) +}) +test('removes newlines in prelude', () => { + let actual = format(`@media + all, + screen, + print, + (min-width: 1000px) {}`) + let expected = `@media all, screen, print, (min-width: 1000px) {}` + expect(actual).toEqual(expected) +}) +test('adds whitespace to @media (min-width:1000px)', () => { + let actual = format(`@media (min-width:1000px) {}`) + let expected = `@media (min-width: 1000px) {}` + expect(actual).toEqual(expected) +}) +test('removes excess whitespace around min-width : 1000px', () => { + let actual = format(`@media (min-width : 1000px) {}`) + let expected = `@media (min-width: 1000px) {}` + expect(actual).toEqual(expected) +}) +test('formats @layer with excess whitespace', () => { + let actual = format(`@layer test;`) + let expected = `@layer test;` + expect(actual).toEqual(expected) +}) +test('adds whitespace to @layer tbody,thead', () => { + let actual = format(`@layer tbody,thead;`) + let expected = `@layer tbody, thead;` + expect(actual).toEqual(expected) +}) +test('adds whitespace to @supports (display:grid)', () => { + let actual = format(`@supports (display:grid){}`) + let expected = `@supports (display: grid) {}` + expect(actual).toEqual(expected) +}) +test('@media prelude formatting', () => { + let fixtures = [ + [`@media all and (transform-3d) {}`, `@media all and (transform-3d) {}`], + [ + `@media only screen and (min-width: 1024px)and (max-width: 1439px), only screen and (min-width: 768px)and (max-width: 1023px) {}`, + `@media only screen and (min-width: 1024px) and (max-width: 1439px), only screen and (min-width: 768px) and (max-width: 1023px) {}`, + ], + [ + `@media (min-width: 1024px)or (max-width: 1439px) {}`, + `@media (min-width: 1024px) or (max-width: 1439px) {}`, + ], + [`@media (width>=44rem)or (width<=33rem) {}`, `@media (width >= 44rem) or (width <= 33rem) {}`], + [ + `@media all and (transform-3d), (-webkit-transform-3d) {}`, + `@media all and (transform-3d), (-webkit-transform-3d) {}`, + ], + [`@media screen or print {}`, `@media screen or print {}`], + [`@media (update: slow) or (hover: none) {}`, `@media (update: slow) or (hover: none) {}`], + [`@media (update: slow)or (hover: none) {}`, `@media (update: slow) or (hover: none) {}`], + [ + `@media all and (-moz-images-in-menus:0) and (min-resolution:.001dpcm) {}`, + `@media all and (-moz-images-in-menus: 0) and (min-resolution: .001dpcm) {}`, + ], + [ + `@media all and (-webkit-min-device-pixel-ratio: 10000),not all and (-webkit-min-device-pixel-ratio: 0) {}`, + `@media all and (-webkit-min-device-pixel-ratio: 10000), not all and (-webkit-min-device-pixel-ratio: 0) {}`, + ], + ] + for (let [css, expected] of fixtures) { + let actual = format(css) + expect(actual).toEqual(expected) + } +}) +test('lowercases functions inside atrule preludes', () => { + let actual = format(` +@import URL("style.css") LAYER(test) SUPPORTS(display:grid); +@supports SELECTOR([popover]:open) {} +`) + let expected = `@import url("style.css") layer(test) supports(display: grid); + +@supports selector([popover]:open) {}` + expect(actual).toEqual(expected) +}) +test('formats @scope', () => { + let actual = format(` + @scope (.light-scheme) {} + @scope (.media-object) to (.content > *) {} +`) + let expected = `@scope (.light-scheme) {} + +@scope (.media-object) to (.content > *) {}` + expect(actual).toEqual(expected) +}) +test('calc() inside @media', () => { + let actual = format(` + @media (min-width: calc(1px*1)) {} + @media (min-width: calc(2px* 2)) {} + @media (min-width: calc(3px *3)) {} + @media (min-width: calc(4px * 4)) {} + @media (min-width: calc(5px * 5)) {} + `) + let expected = `@media (min-width: calc(1px * 1)) {} + +@media (min-width: calc(2px * 2)) {} + +@media (min-width: calc(3px * 3)) {} + +@media (min-width: calc(4px * 4)) {} + +@media (min-width: calc(5px * 5)) {}` + expect(actual).toEqual(expected) +}) +test('minify: calc(*) inside @media', () => { + let actual = minify(`@media (min-width: calc(1px*1)) {}`) + let expected = `@media (min-width:calc(1px*1)){}` + expect(actual).toEqual(expected) +}) +test('minify: calc(+) inside @media', () => { + let actual = minify(`@media (min-width: calc(1px + 1em)) {}`) + let expected = `@media (min-width:calc(1px + 1em)){}` + expect(actual).toEqual(expected) +}) +test('minify: calc(-) inside @media', () => { + let actual = minify(`@media (min-width: calc(1em - 1px)) {}`) + let expected = `@media (min-width:calc(1em - 1px)){}` + expect(actual).toEqual(expected) +}) +test('@import prelude formatting', () => { + let fixtures = [ + ['@import url("fineprint.css") print;', '@import url("fineprint.css") print;'], + ['@import url("style.css") layer;', '@import url("style.css") layer;'], + [ + '@import url("style.css") layer(test.first) supports(display:grid);', + '@import url("style.css") layer(test.first) supports(display: grid);', + ], + ] + for (let [css, expected] of fixtures) { + let actual = format(css) + expect(actual).toEqual(expected) + } +}) +test('@supports prelude formatting', () => { + let fixtures = [ + [`@supports (display:grid){}`, `@supports (display: grid) {}`], + [`@supports (-webkit-appearance: none) {}`, `@supports (-webkit-appearance: none) {}`], + ['@supports selector([popover]:open) {}', '@supports selector([popover]:open) {}'], + ] + for (let [css, expected] of fixtures) { + let actual = format(css) + expect(actual).toEqual(expected) + } +}) +test('@layer prelude formatting', () => { + let fixtures = [ + [`@layer test;`, `@layer test;`], + [`@layer tbody,thead;`, `@layer tbody, thead;`], + ] + for (let [css, expected] of fixtures) { + let actual = format(css) + expect(actual).toEqual(expected) + } +}) +test('minify: @layer prelude formatting', () => { + let fixtures = [ + [`@layer test;`, `@layer test;`], + [`@layer tbody,thead;`, `@layer tbody,thead;`], + ] + for (let [css, expected] of fixtures) { + let actual = minify(css) + expect(actual).toEqual(expected) + } +}) +test('single empty line after a rule, before atrule', () => { + let actual = format(` + rule1 { property: value } + @media (min-width: 1000px) { + rule2 { property: value } + } + `) + let expected = `rule1 { + property: value; +} + +@media (min-width: 1000px) { + rule2 { + property: value; + } +}` + expect(actual).toEqual(expected) +}) +test('single empty line in between atrules', () => { + let actual = format(` + @layer test1; + @media (min-width: 1000px) { + rule2 { property: value } + } + `) + let expected = `@layer test1; + +@media (min-width: 1000px) { + rule2 { + property: value; + } +}` + expect(actual).toEqual(expected) +}) +test('newline between last declaration and nested atrule', () => { + let actual = format(` + test { + property1: value1; + @media all { + property2: value2; + } + } + `) + let expected = `test { + property1: value1; + + @media all { + property2: value2; + } +}` + expect(actual).toEqual(expected) +}) +test('lowercases the atrule name', () => { + let actual = format(`@LAYER test {}`) + let expected = `@layer test {}` + expect(actual).toEqual(expected) +}) +test('does not lowercase the atrule value', () => { + let actual = format('@keyframes TEST {}') + let expected = '@keyframes TEST {}' + expect(actual).toEqual(expected) +}) +test('Atrules w/o Block are terminated with a semicolon', () => { + let actual = format(` + @layer test; + @import url('test'); + `) + let expected = `@layer test; + +@import url('test');` + expect(actual).toEqual(expected) +}) +test('Empty atrule braces are placed on the same line', () => { + let actual = format(`@media all { + + } + + @supports (display: grid) {}`) + let expected = `@media all {} + +@supports (display: grid) {}` + expect(actual).toEqual(expected) +}) +test('new-fangled comparators (width > 1000px)', () => { + let actual = format(` + @container (width>1000px) {} + @media (width>1000px) {} + @media (width=>1000px) {} + @media (width<=1000px) {} + @media (200px 1000px) {} + +@media (width > 1000px) {} + +@media (width => 1000px) {} + +@media (width <= 1000px) {} + +@media (200px < width < 1000px) {}` + expect(actual).toEqual(expected) +}) +test('minify: new-fangled comparators (width > 1000px)', () => { + let actual = minify(`@container (width>1000px) {}`) + let expected = `@container (width>1000px){}` + expect(actual).toEqual(expected) +}) +test.skip('preserves comments', () => { + let actual = format(` + @media /* comment */ all {} + @media all /* comment */ {} + @media (min-width: 1000px /* comment */) {} + @media (/* comment */ min-width: 1000px) {} + @layer /* comment */ {} + `) + let expected = `@media /* comment */ all {} + +@media all /* comment */ {} + +@media (min-width: 1000px /* comment */) {} + +@media (/* comment */ min-width: 1000px) {} + +@layer /* comment */ {} +` + expect(actual).toEqual(expected) +}) diff --git a/test/comments.test.js b/test/comments.test.js new file mode 100644 index 0000000..13ec3bc --- /dev/null +++ b/test/comments.test.js @@ -0,0 +1,528 @@ +import { describe, test, expect } from 'vitest' +import { format } from '../src/lib/index.js' +describe('comments', () => { + test('only comment', () => { + let actual = format(`/* comment */`) + let expected = `/* comment */` + expect(actual).toEqual(expected) + }) + test('bang comment before rule', () => { + let actual = format(` + /*! comment */ + selector {} + `) + let expected = `/*! comment */ +selector {}` + expect(actual).toEqual(expected) + }) + test('before selectors', () => { + let actual = format(` + /* comment */ + selector1, + selector2 { + property: value; + } + `) + let expected = `/* comment */ +selector1, +selector2 { + property: value; +}` + expect(actual).toEqual(expected) + }) + test('before nested selectors', () => { + let actual = format(` + a { + /* comment */ + & nested1, + & nested2 { + property: value; + } + } + `) + let expected = `a { + /* comment */ + & nested1, + & nested2 { + property: value; + } +}` + expect(actual).toEqual(expected) + }) + test('after selectors', () => { + let actual = format(` + selector1, + selector2 + /* comment */ { + property: value; + } + `) + let expected = `selector1, +selector2 +/* comment */ { + property: value; +}` + expect(actual).toEqual(expected) + }) + test('in between selectors', () => { + let actual = format(` + selector1, + /* comment */ + selector2 { + property: value; + } + `) + let expected = `selector1, +/* comment */ +selector2 { + property: value; +}` + expect(actual).toEqual(expected) + }) + test('in between nested selectors', () => { + let actual = format(` + a { + & nested1, + /* comment */ + & nested2 { + property: value; + } + } + `) + let expected = `a { + & nested1, + /* comment */ + & nested2 { + property: value; + } +}` + expect(actual).toEqual(expected) + }) + test('as first child in rule', () => { + let actual = format(` + selector { + /* comment */ + property: value; + } + `) + let expected = `selector { + /* comment */ + property: value; +}` + expect(actual).toEqual(expected) + }) + test('as last child in rule', () => { + let actual = format(` + selector { + property: value; + /* comment */ + } + `) + let expected = `selector { + property: value; + /* comment */ +}` + expect(actual).toEqual(expected) + }) + test('as last child in nested rule', () => { + let actual = format(` + a { + & selector { + property: value; + /* comment */ + } + } + `) + let expected = `a { + & selector { + property: value; + /* comment */ + } +}` + expect(actual).toEqual(expected) + }) + test('as only child in rule', () => { + let actual = format(` + selector { + /* comment */ + } + `) + let expected = `selector { + /* comment */ +}` + expect(actual).toEqual(expected) + }) + test('as only child in nested rule', () => { + let actual = format(`a { + & selector { + /* comment */ + } +}`) + let expected = `a { + & selector { + /* comment */ + } +}` + expect(actual).toEqual(expected) + }) + test('in between declarations', () => { + let actual = format(` + selector { + property: value; + /* comment */ + property: value; + } + `) + let expected = `selector { + property: value; + /* comment */ + property: value; +}` + expect(actual).toEqual(expected) + }) + test('in between nested declarations', () => { + let actual = format(` + a { + & selector { + property: value; + /* comment */ + property: value; + } + } + `) + let expected = `a { + & selector { + property: value; + /* comment */ + property: value; + } +}` + expect(actual).toEqual(expected) + }) + test('as first child in atrule', () => { + let actual = format(` + @media (min-width: 1000px) { + /* comment */ + selector { + property: value; + } + } + `) + let expected = `@media (min-width: 1000px) { + /* comment */ + selector { + property: value; + } +}` + expect(actual).toEqual(expected) + }) + test('as first child in nested atrule', () => { + let actual = format(` + @media all { + @media (min-width: 1000px) { + /* comment */ + selector { + property: value; + } + } + } + `) + let expected = `@media all { + @media (min-width: 1000px) { + /* comment */ + selector { + property: value; + } + } +}` + expect(actual).toEqual(expected) + }) + test('as last child in atrule', () => { + let actual = format(` + @media (min-width: 1000px) { + selector { + property: value; + } + /* comment */ + } + `) + let expected = `@media (min-width: 1000px) { + selector { + property: value; + } + /* comment */ +}` + expect(actual).toEqual(expected) + }) + test('as last child in nested atrule', () => { + let actual = format(` + @media all { + @media (min-width: 1000px) { + selector { + property: value; + } + /* comment */ + } + } + `) + let expected = `@media all { + @media (min-width: 1000px) { + selector { + property: value; + } + /* comment */ + } +}` + expect(actual).toEqual(expected) + }) + test('as only child in atrule', () => { + let actual = format(` + @media (min-width: 1000px) { + /* comment */ + } + `) + let expected = `@media (min-width: 1000px) { + /* comment */ +}` + expect(actual).toEqual(expected) + }) + test('as only child in nested atrule', () => { + let actual = format(` + @media all { + @media (min-width: 1000px) { + /* comment */ + } + } + `) + let expected = `@media all { + @media (min-width: 1000px) { + /* comment */ + } +}` + expect(actual).toEqual(expected) + }) + test('in between rules and atrules', () => { + let actual = format(` + /* comment 1 */ + selector {} + /* comment 2 */ + @media (min-width: 1000px) { + /* comment 3 */ + selector {} + /* comment 4 */ + } + /* comment 5 */ + `) + let expected = `/* comment 1 */ +selector {} +/* comment 2 */ +@media (min-width: 1000px) { + /* comment 3 */ + selector {} + /* comment 4 */ +} +/* comment 5 */` + expect(actual).toEqual(expected) + }) + test('comment before rule and atrule should not be separated by newline', () => { + let actual = format(` + /* comment 1 */ + selector {} + + /* comment 2 */ + @media (min-width: 1000px) { + /* comment 3 */ + + selector {} + /* comment 4 */ + } + `) + let expected = `/* comment 1 */ +selector {} +/* comment 2 */ +@media (min-width: 1000px) { + /* comment 3 */ + selector {} + /* comment 4 */ +}` + expect(actual).toEqual(expected) + }) + test('a declaration after multiple comments starts on a new line', () => { + let actual = format(` + selector { + /* comment 1 */ + /* comment 2 */ + --custom-property: value; + + /* comment 3 */ + /* comment 4 */ + --custom-property: value; + + /* comment 5 */ + /* comment 6 */ + --custom-property: value; + } + `) + let expected = `selector { + /* comment 1 */ + /* comment 2 */ + --custom-property: value; + /* comment 3 */ + /* comment 4 */ + --custom-property: value; + /* comment 5 */ + /* comment 6 */ + --custom-property: value; +}` + expect(actual).toEqual(expected) + }) + test('multiple comments in between rules and atrules', () => { + let actual = format(` + /* comment 1 */ + /* comment 1.1 */ + selector {} + /* comment 2 */ + /* comment 2.1 */ + @media (min-width: 1000px) { + /* comment 3 */ + /* comment 3.1 */ + selector {} + /* comment 4 */ + /* comment 4.1 */ + } + /* comment 5 */ + /* comment 5.1 */ + `) + let expected = `/* comment 1 */ +/* comment 1.1 */ +selector {} +/* comment 2 */ +/* comment 2.1 */ +@media (min-width: 1000px) { + /* comment 3 */ + /* comment 3.1 */ + selector {} + /* comment 4 */ + /* comment 4.1 */ +} +/* comment 5 */ +/* comment 5.1 */` + expect(actual).toEqual(expected) + }) + test('puts every comment on a new line', () => { + let actual = format(` + x { + /*--font-family: inherit;*/ /*--font-style: normal;*/ + --border-top-color: var(--root-color--support); + } +`) + let expected = `x { + /*--font-family: inherit;*/ + /*--font-style: normal;*/ + --border-top-color: var(--root-color--support); +}` + expect(actual).toEqual(expected) + }) + test('in @media prelude', () => { + // from CSSTree https://github.com/csstree/csstree/blob/ba6dfd8bb0e33055c05f13803d04825d98dd2d8d/fixtures/ast/mediaQuery/MediaQuery.json#L147 + let actual = format('@media all /*0*/ (/*1*/foo/*2*/:/*3*/1/*4*/) {}') + let expected = '@media all /*0*/ (/*1*/foo/*2*/: /*3*/1/*4*/) {}' + expect(actual).toEqual(expected) + }) + test('in @supports prelude', () => { + // from CSSTree https://github.com/csstree/csstree/blob/ba6dfd8bb0e33055c05f13803d04825d98dd2d8d/fixtures/ast/atrule/atrule/supports.json#L119 + let actual = format('@supports not /*0*/(/*1*/flex :/*3*/1/*4*/)/*5*/{}') + let expected = '@supports not /*0*/(/*1*/flex: /*3*/1/*4*/) {}' + expect(actual).toEqual(expected) + }) + test('skip in @import prelude before specifier', () => { + let actual = format('@import /*test*/"foo";') + let expected = '@import "foo";' + expect(actual).toEqual(expected) + }) + test('skip in @import prelude after specifier', () => { + let actual = format('@import "foo"/*test*/;') + let expected = '@import "foo";' + expect(actual).toEqual(expected) + }) + test('skip in selector combinator', () => { + let actual = format(` + a/*test*/ /*test*/b, + a/*test*/+/*test*/b {} + `) + let expected = `a /*test*/ /*test*/ b, +a + b {}` + expect(actual).toEqual(expected) + }) + test('in attribute selector', () => { + let actual = format(`[/*test*/a='b' i/*test*/] {}`) + let expected = `[a="b" i] {}` + expect(actual).toEqual(expected) + }) + test('skip in var() with fallback', () => { + let actual = format(`a { prop: var( /* 1 */ --name /* 2 */ , /* 3 */ 1 /* 4 */ ) }`) + let expected = `a { + prop: var(--name, 1); +}` + expect(actual).toEqual(expected) + }) + test('skip in custom property declaration (space toggle)', () => { + let actual = format(`a { --test: /*test*/; }`) + let expected = `a { + --test: ; +}` + expect(actual).toEqual(expected) + }) + test('before value', () => { + let actual = format(`a { prop: /*test*/value; }`) + let expected = `a { + prop: value; +}` + expect(actual).toEqual(expected) + }) + test('after value', () => { + let actual = format(`a { + prop: value/*test*/; + }`) + let expected = `a { + prop: value; +}` + expect(actual).toEqual(expected) + }) + test('skip in value functions', () => { + let actual = format(` + a { + background-image: linear-gradient(/* comment */red, green); + background-image: linear-gradient(red/* comment */, green); + background-image: linear-gradient(red, green/* comment */); + background-image: linear-gradient(red, green)/* comment */ + } + `) + let expected = `a { + background-image: linear-gradient(red, green); + background-image: linear-gradient(red, green); + background-image: linear-gradient(red, green); + background-image: linear-gradient(red, green); + /* comment */ +}` + expect(actual).toEqual(expected) + }) + test('strips comments in minification mode', () => { + let actual = format( + ` + /* comment 1 */ + selector {} + /* comment 2 */ + @media (min-width: 1000px) { + /* comment 3 */ + selector {} + /* comment 4 */ + } + /* comment 5 */ + `, + { minify: true }, + ) + let expected = `selector{}@media (min-width:1000px){selector{}}` + expect(actual).toEqual(expected) + }) +}) diff --git a/test/declarations.test.js b/test/declarations.test.js new file mode 100644 index 0000000..5059d7c --- /dev/null +++ b/test/declarations.test.js @@ -0,0 +1,95 @@ +import { test, expect } from 'vitest' +import { format } from '../src/lib/index.js' +test('Declarations end with a semicolon (;)', () => { + let actual = format(` + @font-face { + src: url('test'); + font-family: Test + } + + css { + property1: value1; + property2: value2; + + & .nested { + property1: value3; + property2: value4 + } + } + + @media (min-width: 1000px) { + @layer test { + css { + property1: value5 + } + } + } + `) + let expected = `@font-face { + src: url("test"); + font-family: Test; +} + +css { + property1: value1; + property2: value2; + + & .nested { + property1: value3; + property2: value4; + } +} + +@media (min-width: 1000px) { + @layer test { + css { + property1: value5; + } + } +}` + expect(actual).toEqual(expected) +}) +test('lowercases properties', () => { + let actual = format(`a { COLOR: green }`) + let expected = `a { + color: green; +}` + expect(actual).toEqual(expected) +}) +test('does not lowercase custom properties', () => { + let actual = format(`a { + --myVar: 1; + }`) + let expected = `a { + --myVar: 1; +}` + expect(actual).toEqual(expected) +}) +test('!important is added', () => { + let actual = format(`a { color: green !important}`) + let expected = `a { + color: green !important; +}` + expect(actual).toEqual(expected) +}) +test('!important is lowercase', () => { + let actual = format(`a { color: green !IMPORTANT }`) + let expected = `a { + color: green !important; +}` + expect(actual).toEqual(expected) +}) +test('browserhack !ie is printed', () => { + let actual = format(`a { color: green !ie}`) + let expected = `a { + color: green !ie; +}` + expect(actual).toEqual(expected) +}) +test('browserhack !IE is lowercased', () => { + let actual = format(`a { color: green !IE}`) + let expected = `a { + color: green !ie; +}` + expect(actual).toEqual(expected) +}) diff --git a/test/minify.test.js b/test/minify.test.js new file mode 100644 index 0000000..ab40852 --- /dev/null +++ b/test/minify.test.js @@ -0,0 +1,83 @@ +import { test, expect } from 'vitest' +import { minify } from '../src/lib/index.js' +test('empty rule', () => { + let actual = minify(`a {}`) + let expected = `a{}` + expect(actual).toEqual(expected) +}) +test('simple declaration', () => { + let actual = minify(`:root { --color: red; }`) + let expected = `:root{--color:red}` + expect(actual).toEqual(expected) +}) +test('simple atrule', () => { + let actual = minify(`@media (min-width: 100px) { body { color: red; } }`) + let expected = `@media (min-width:100px){body{color:red}}` + expect(actual).toEqual(expected) +}) +test('empty atrule', () => { + let actual = minify(`@media (min-width: 100px) {}`) + let expected = `@media (min-width:100px){}` + expect(actual).toEqual(expected) +}) +test('formats multiline values on a single line', () => { + let actual = minify(` +a { + background: linear-gradient( + red, + 10% blue, +20% green,100% yellow); +} + `) + let expected = `a{background:linear-gradient(red,10% blue,20% green,100% yellow)}` + expect(actual).toEqual(expected) +}) +test('correctly minifies operators', () => { + let actual = minify(`a { width: calc(100% - 10px); height: calc(100 * 1%); }`) + let expected = `a{width:calc(100% - 10px);height:calc(100*1%)}` + expect(actual).toEqual(expected) +}) +test('correctly minifiers modern colors', () => { + let actual = minify(`a { color: rgb(0 0 0 / 0.1); }`) + let expected = `a{color:rgb(0 0 0/0.1)}` + expect(actual).toEqual(expected) +}) +test('Vadim Makeevs example works', () => { + let actual = minify(` + @layer what { + @container (width > 0) { + ul:has(:nth-child(1 of li)) { + @media (height > 0) { + &:hover { + --is: this; + } + } + } + } + } + `) + let expected = `@layer what{@container (width>0){ul:has(:nth-child(1 of li)){@media (height>0){&:hover{--is:this}}}}}` + expect(actual).toEqual(expected) +}) +test('minified Vadims example', () => { + let actual = minify( + `@layer what{@container (width>0){@media (min-height:.001px){ul:has(:nth-child(1 of li)):hover{--is:this}}}}`, + ) + let expected = `@layer what{@container (width>0){@media (min-height:.001px){ul:has(:nth-child(1 of li)):hover{--is:this}}}}` + expect(actual).toEqual(expected) +}) +test('removes whitespace before !important', () => { + let actual = minify(`a { color: green !important }`) + let expected = `a{color:green!important}` + expect(actual).toEqual(expected) +}) +test('minifies complex selectors', () => { + let actual = minify(`:is(a, b) { color: green }`) + let expected = `:is(a,b){color:green}` + expect(actual).toEqual(expected) +}) +test('removes whitespace around non-whitespace selector combinators', () => { + let actual = minify(`a + b {} c d {}`) + let expected = `a+b{}c d{}` + expect(actual).toEqual(expected) +}) diff --git a/test/rules.test.js b/test/rules.test.js new file mode 100644 index 0000000..86c0fb8 --- /dev/null +++ b/test/rules.test.js @@ -0,0 +1,245 @@ +import { test, expect } from 'vitest' +import { format } from '../src/lib/index.js' +test('AtRules and Rules start on a new line', () => { + let actual = format(` + selector { property: value; } + @media (min-width: 1000px) { + selector { property: value; } + } + selector { property: value; } + @layer test { + selector { property: value; } + } + `) + let expected = `selector { + property: value; +} + +@media (min-width: 1000px) { + selector { + property: value; + } +} + +selector { + property: value; +} + +@layer test { + selector { + property: value; + } +}` + expect(actual).toEqual(expected) +}) +test('An empty line is rendered in between Rules', () => { + let actual = format(` + rule1 { property: value } + rule2 { property: value } + `) + let expected = `rule1 { + property: value; +} + +rule2 { + property: value; +}` + expect(actual).toEqual(expected) +}) +test('single empty line after a rule, before atrule', () => { + let actual = format(` + rule1 { property: value } + @media (min-width: 1000px) { + rule2 { property: value } + } + `) + let expected = `rule1 { + property: value; +} + +@media (min-width: 1000px) { + rule2 { + property: value; + } +}` + expect(actual).toEqual(expected) +}) +test('newline between last declaration and nested ruleset', () => { + let actual = format(` + test { + property1: value1; + & > item { + property2: value2; + & + another { + property3: value3; + } + } + } + `) + let expected = `test { + property1: value1; + + & > item { + property2: value2; + + & + another { + property3: value3; + } + } +}` + expect(actual).toEqual(expected) +}) +test('newline between last declaration and nested atrule', () => { + let actual = format(` + test { + property1: value1; + @media all { + property2: value2; + } + } + `) + let expected = `test { + property1: value1; + + @media all { + property2: value2; + } +}` + expect(actual).toEqual(expected) +}) +test('no trailing newline on empty nested rule', () => { + let actual = format(` + @layer test { + empty {} + } + `) + let expected = `@layer test { + empty {} +}` + expect(actual).toEqual(expected) +}) +test('formats nested rules with selectors starting with', () => { + let actual = format(` + selector { + & > item { + property: value; + } + } + `) + let expected = `selector { + & > item { + property: value; + } +}` + expect(actual).toEqual(expected) +}) +test('newlines between declarations, nested rules and more declarations', () => { + let actual = format(`a { font: 0/0; & b { color: red; } color: green;}`) + let expected = `a { + font: 0/0; + + & b { + color: red; + } + color: green; +}` + expect(actual).toEqual(expected) +}) +test('formats nested rules with a selector starting with &', () => { + let actual = format(` + selector { + & a { color: red; } + } + `) + let expected = `selector { + & a { + color: red; + } +}` + expect(actual).toEqual(expected) +}) +test('formats unknown stuff in curly braces', () => { + let actual = format(` + selector { + { color: red; } + } + `) + let expected = `selector { + { + color: red; + } +}` + expect(actual).toEqual(expected) +}) +test('Relaxed nesting: formats nested rules with a selector with a &', () => { + let actual = format(` + selector { + a & { color:red } + } + `) + let expected = `selector { + a & { + color: red; + } +}` + expect(actual).toEqual(expected) +}) +test('Relaxed nesting: formats nested rules with a selector with a &', () => { + let actual = format(` + selector { + a & { color:red } + } + `) + let expected = `selector { + a & { + color: red; + } +}` + expect(actual).toEqual(expected) +}) +test('Relaxed nesting: formats nested rules with a selector without a &', () => { + let actual = format(` + selector { + a { color:red } + } + `) + let expected = `selector { + a { + color: red; + } +}` + expect(actual).toEqual(expected) +}) +test('Relaxed nesting: formats nested rules with a selector starting with a selector combinator', () => { + let actual = format(` + selector { + > a { color:red } + ~ a { color:red } + + a { color:red } + } + `) + let expected = `selector { + > a { + color: red; + } + + ~ a { + color: red; + } + + + a { + color: red; + } +}` + expect(actual).toEqual(expected) +}) +test('handles syntax errors: unclosed block', () => { + let actual = format(`a { mumblejumble`) + let expected = 'a {}' + expect(actual).toEqual(expected) +}) +test('handles syntax errors: premature closed block', () => { + let actual = format(`a { mumblejumble: }`) + let expected = 'a {\n\tmumblejumble: ;\n}' + expect(actual).toEqual(expected) +}) diff --git a/test/selectors.test.js b/test/selectors.test.js new file mode 100644 index 0000000..0424d47 --- /dev/null +++ b/test/selectors.test.js @@ -0,0 +1,238 @@ +import { test, expect } from 'vitest' +import { format } from '../src/lib/index.js' +test('A single selector is rendered without a trailing comma', () => { + let actual = format('a {}') + let expected = 'a {}' + expect(actual).toEqual(expected) +}) +test('Multiple selectors are placed on a new line, separated by commas', () => { + let actual = format(` + selector1, + selector1a, + selector1b, + selector1aa, + selector2, + + selector3 { + } + `) + let expected = `selector1, +selector1a, +selector1b, +selector1aa, +selector2, +selector3 {}` + expect(actual).toEqual(expected) +}) +test('formats multiline selectors on a single line', () => { + let actual = format(` +a.b + .c .d + .e .f { +color: green } + `) + let expected = `a.b .c .d .e .f { + color: green; +}` + expect(actual).toEqual(expected) +}) +test('formats simple selector combinators', () => { + let actual = format(` + a>b, + a>b~c d, + .article-content ol li>* {} + `) + let expected = `a > b, +a > b ~ c d, +.article-content ol li > * {}` + expect(actual).toEqual(expected) +}) +test('lowercases type selectors', () => { + let actual = format(` + A, + B, + C {} + `) + let expected = `a, +b, +c {}` + expect(actual).toEqual(expected) +}) +test('formats nested selector combinators', () => { + let fixtures = [ + [`:where(a+b) {}`, `:where(a + b) {}`], + [`:where(:is(ol,ul)) {}`, `:where(:is(ol, ul)) {}`], + [`li:nth-of-type(1) {}`, `li:nth-of-type(1) {}`], + [`li:nth-of-type(2n) {}`, `li:nth-of-type(2n) {}`], + ] + for (let [css, expected] of fixtures) { + let actual = format(css) + expect(actual).toEqual(expected) + } +}) +test('formats pseudo selectors', () => { + let css = ` + a::before, + a::after, + b:before, + b:after, + c::first-letter {} + ` + let expected = `a::before, +a::after, +b::before, +b::after, +c::first-letter {}` + let actual = format(css) + expect(actual).toEqual(expected) +}) +test('formats pseudo elements with odd casing', () => { + let css = ` + a::Before, + a::After, + b:Before, + b:After, + c:After, + d::First-letter {} + ` + let expected = `a::before, +a::after, +b::before, +b::after, +c::after, +d::first-letter {}` + let actual = format(css) + expect(actual).toEqual(expected) +}) +test.each([ + [`li:nth-child(3n-2) {}`, `li:nth-child(3n -2) {}`], + [`li:nth-child(0n+1) {}`, `li:nth-child(0n + 1) {}`], + [`li:nth-child(even of .noted) {}`, `li:nth-child(even of .noted) {}`], + [`li:nth-child(2n of .noted) {}`, `li:nth-child(2n of .noted) {}`], + [`li:nth-child(-n + 3 of .noted) {}`, `li:nth-child(-n + 3 of .noted) {}`], + [`li:nth-child(-n+3 of li.important) {}`, `li:nth-child(-n + 3 of li.important) {}`], + [`p:nth-child(n+8):nth-child(-n+15) {}`, `p:nth-child(n + 8):nth-child(-n + 15) {}`], +])('formats nth selector: %s', (css, expected) => { + let actual = format(css) + expect(actual).toEqual(expected) +}) +test.each([ + [`li:nth-child(3n-2) {}`, `li:nth-child(3n-2){}`], + [`li:nth-child(0n+1) {}`, `li:nth-child(0n+1){}`], + [`li:nth-child(even of .noted) {}`, `li:nth-child(even of .noted){}`], + [`li:nth-child(2n of .noted) {}`, `li:nth-child(2n of .noted){}`], + [`li:nth-child(-n + 3 of .noted) {}`, `li:nth-child(-n+3 of .noted){}`], + [`li:nth-child(-n+3 of li.important) {}`, `li:nth-child(-n+3 of li.important){}`], + [`p:nth-child(n+8):nth-child(-n+15) {}`, `p:nth-child(n+8):nth-child(-n+15){}`], +])('minifies nth selector: %s', (css, expected) => { + let actual = format(css, { minify: true }) + expect(actual).toEqual(expected) +}) +test('formats multiline selectors', () => { + let actual = format(` + a:is( + a, + b, + c + ) {} + `) + let expected = `a:is(a, b, c) {}` + expect(actual).toEqual(expected) +}) +test('format nesting selectors', () => { + let actual = format(` + & a {} + b & c {} + `) + let expected = `& a {} + +b & c {}` + expect(actual).toEqual(expected) +}) +test('prints all possible attribute selectors', () => { + let actual = format(` + [title="test"], + [title|="test"], + [title^="test"], + [title*="test"], + [title$="test"], + [title~="test"] {} + `) + let expected = `[title="test"], +[title|="test"], +[title^="test"], +[title*="test"], +[title$="test"], +[title~="test"] {}` + expect(actual).toEqual(expected) +}) +test('forces attribute selectors to have quoted values', () => { + let actual = format(` + [title=foo], + [title="bar"], + [title='baz'] {} + `) + let expected = `[title="foo"], +[title="bar"], +[title="baz"] {}` + expect(actual).toEqual(expected) +}) +test('adds a space before attribute selector flags', () => { + let actual = format(` + [title="foo" i], + [title="baz"i], + [title=foo S] {} + `) + let expected = `[title="foo" i], +[title="baz" i], +[title="foo" s] {}` + expect(actual).toEqual(expected) +}) +test('formats :lang correctly', () => { + let actual = format(`:lang("nl","de"),li:nth-child() {}`) + let expected = `:lang("nl", "de"), +li:nth-child() {}` + expect(actual).toEqual(expected) +}) +test(`formats ::highlight and ::highlight(Name) correctly`, () => { + let actual = format(`::highlight,::highlight(Name),::highlight(my-thing) {}`) + let expected = `::highlight, +::highlight(Name), +::highlight(my-thing) {}` + expect(actual).toEqual(expected) +}) +test('formats keyframes selectors (50%) correctly', () => { + let actual = format( + `@keyframes Toastify__bounceInUp { 0% {animation-timing-function: cubic-bezier(.215, .61, .355, 1);} 50% {} 80%,to {} }`, + ) + let expected = `@keyframes Toastify__bounceInUp { + 0% { + animation-timing-function: cubic-bezier(.215, .61, .355, 1); + } + + 50% {} + + 80%, + to {} +}` + expect(actual).toBe(expected) +}) +test('formats unknown pseudos correctly', () => { + let actual = format(` + ::foo-bar, + :unkown-thing(), + :unnowkn(kjsa.asddk,asd) {} + `) + let expected = `::foo-bar, +:unkown-thing(), +:unnowkn(kjsa.asddk, asd) {}` + expect(actual).toEqual(expected) +}) +test('handles syntax errors', () => { + let actual = format(` + test, + @test {} + `) + let expected = `test {}` + expect(actual).toEqual(expected) +}) diff --git a/test/tab-size.test.js b/test/tab-size.test.js new file mode 100644 index 0000000..9d3f816 --- /dev/null +++ b/test/tab-size.test.js @@ -0,0 +1,47 @@ +import { test, expect } from 'vitest' +import { format } from '../src/lib/index.js' +let fixture = ` + selector { + color: red; + } +` +test('tab_size: 2', () => { + let actual = format( + ` + selector { + color: red; + } + + @media (min-width: 100px) { + selector { + color: blue; + } + } + `, + { tab_size: 2 }, + ) + let expected = `selector { + color: red; +} + +@media (min-width: 100px) { + selector { + color: blue; + } +}` + expect(actual).toEqual(expected) +}) +test('invalid tab_size: 0', () => { + expect(() => format(fixture, { tab_size: 0 })).toThrow() +}) +test('invalid tab_size: negative', () => { + expect(() => format(fixture, { tab_size: -1 })).toThrow() +}) +test('combine tab_size and minify', () => { + let actual = format(fixture, { + tab_size: 2, + minify: true, + }) + let expected = `selector{color:red}` + expect(actual).toEqual(expected) +}) diff --git a/test/values.test.js b/test/values.test.js new file mode 100644 index 0000000..ee175d6 --- /dev/null +++ b/test/values.test.js @@ -0,0 +1,316 @@ +import { test, expect } from 'vitest' +import { format } from '../src/lib/index.js' +test('collapses abundant whitespace', () => { + let actual = format(`a { + transition: all 100ms ease; + color: rgb( 0 , 0 , 0 ); + color: red ; + }`) + let expected = `a { + transition: all 100ms ease; + color: rgb(0, 0, 0); + color: red; +}` + expect(actual).toEqual(expected) +}) +test('formats simple value lists', () => { + let actual = format(` + a { + transition-property: all,opacity; + transition: all 100ms ease,opacity 10ms 20ms linear; + ANIMATION: COLOR 123MS EASE-OUT; + color: rgb(0,0,0); + color: HSL(0%,10%,50%); + content: 'Test'; + background-image: url("EXAMPLE.COM"); + } + `) + let expected = `a { + transition-property: all, opacity; + transition: all 100ms ease, opacity 10ms 20ms linear; + animation: COLOR 123ms EASE-OUT; + color: rgb(0, 0, 0); + color: hsl(0%, 10%, 50%); + content: "Test"; + background-image: url("EXAMPLE.COM"); +}` + expect(actual).toEqual(expected) +}) +test('formats nested value lists', () => { + let actual = format(` + a { + background: red,linear-gradient(to bottom,red 10%,green 50%,blue 100%); + } + `) + let expected = `a { + background: red, linear-gradient(to bottom, red 10%, green 50%, blue 100%); +}` + expect(actual).toEqual(expected) +}) +test('formats nested var()', () => { + let actual = format(` + a { + color: var(--test1,var(--test2,green)); + color: var(--test3,rgb(0,0,0)); + } + `) + let expected = `a { + color: var(--test1, var(--test2, green)); + color: var(--test3, rgb(0, 0, 0)); +}` + expect(actual).toEqual(expected) +}) +test('formats multiline values on a single line', () => { + let actual = format(` +a { + background: linear-gradient( + red, + 10% blue, +20% green,100% yellow); + color: rgb( + 0, + 0, + 0 + ); +} + `) + let expected = `a { + background: linear-gradient(red, 10% blue, 20% green, 100% yellow); + color: rgb(0, 0, 0); +}` + expect(actual).toEqual(expected) +}) +test('does not break font shorthand', () => { + let actual = format(`a { + font: 2em/2 sans-serif; + font: 2em/ 2 sans-serif; + font: 2em / 2 sans-serif; + }`) + let expected = `a { + font: 2em/2 sans-serif; + font: 2em/2 sans-serif; + font: 2em/2 sans-serif; +}` + expect(actual).toEqual(expected) +}) +test('formats whitespace around operators (*/+-) correctly', () => { + let actual = format(`a { + font: 2em/2 sans-serif; + font-size: calc(2em/2); + font-size: calc(2em * 2); + font-size: calc(2em + 2px); + font-size: calc(2em - 2px); +}`) + let expected = `a { + font: 2em/2 sans-serif; + font-size: calc(2em / 2); + font-size: calc(2em * 2); + font-size: calc(2em + 2px); + font-size: calc(2em - 2px); +}` + expect(actual).toEqual(expected) +}) +test('formats whitespace around operators (*/+-) correctly in nested parenthesis', () => { + let actual = format(`a { + width: calc(((100% - var(--x))/ 12 * 6) + (-1 * var(--y))); + width: calc(((100% - var(--x))/ 12 * 6) + (-1 * var(--y))); + width: calc(((100% - var(--x))/ 12 * 6) + (-1 * var(--y))); + width: calc(((100% - var(--x))/ 12 * 6) + (-1 * var(--y))); +}`) + let expected = `a { + width: calc(((100% - var(--x)) / 12 * 6) + (-1 * var(--y))); + width: calc(((100% - var(--x)) / 12 * 6) + (-1 * var(--y))); + width: calc(((100% - var(--x)) / 12 * 6) + (-1 * var(--y))); + width: calc(((100% - var(--x)) / 12 * 6) + (-1 * var(--y))); +}` + expect(actual).toEqual(expected) +}) +test('formats parenthesis correctly', () => { + let actual = format(`a { + width: calc(100% - var(--x)); + width: calc((100% - var(--x))); + width: calc(100% - (var(--x))); + width: calc((100% - (var(--x)))); +}`) + let expected = `a { + width: calc(100% - var(--x)); + width: calc((100% - var(--x))); + width: calc(100% - (var(--x))); + width: calc((100% - (var(--x)))); +}` + expect(actual).toEqual(expected) +}) +test('does not lowercase grid-area names', () => { + let actual = format(`a { grid-area: emailInputBox; }`) + let expected = `a { + grid-area: emailInputBox; +}` + expect(actual).toEqual(expected) +}) +test('does not lowercase custom properties in var()', () => { + let actual = format(`a { color: var(--MyColor); }`) + let expected = `a { + color: var(--MyColor); +}` + expect(actual).toEqual(expected) +}) +test('lowercases CSS functions', () => { + let actual = format(`a { + color: RGB(0, 0, 0); + transform: translateX(100px); + }`) + let expected = `a { + color: rgb(0, 0, 0); + transform: translatex(100px); +}` + expect(actual).toEqual(expected) +}) +test('relative colors', () => { + let actual = format(`a { + color: rgb( from red 0 0 255); + color: rgb( from rgb( 200 0 0 ) r r r ) ; + color: hwb( from var( --base-color ) h w b / var( --standard-opacity ) ) ; + color: lch(from var(--base-color) calc(l + 20) c h); + }`) + let expected = `a { + color: rgb(from red 0 0 255); + color: rgb(from rgb(200 0 0) r r r); + color: hwb(from var(--base-color) h w b / var(--standard-opacity)); + color: lch(from var(--base-color) calc(l + 20) c h); +}` + expect(actual).toEqual(expected) +}) +test('does not change casing of `NaN`', () => { + let actual = format(`a { + height: calc(1 * NaN); + }`) + let expected = `a { + height: calc(1 * NaN); +}` + expect(actual).toEqual(expected) +}) +test('does not change casing of URLs', () => { + let actual = format(`a { + background-image: url("My-Url.png"); + }`) + let expected = `a { + background-image: url("My-Url.png"); +}` + expect(actual).toEqual(expected) +}) +test('lowercases dimensions', () => { + let actual = format(`a { + font-size: 12PX; + width: var(--test, 33REM); + }`) + let expected = `a { + font-size: 12px; + width: var(--test, 33rem); +}` + expect(actual).toEqual(expected) +}) +test('formats unknown content in value', () => { + let actual = format(`a { + content: 'Test' counter(page); + }`) + let expected = `a { + content: "Test" counter(page); +}` + expect(actual).toEqual(expected) +}) +test('does not break space toggles', () => { + let actual = format(`a { + --ON: initial; + --OFF: ; + }`) + let expected = `a { + --ON: initial; + --OFF: ; +}` + expect(actual).toEqual(expected) +}) +test('does not break space toggles (minified)', () => { + let actual = format( + `a { + --ON: initial; + --OFF: ; + }`, + { minify: true }, + ) + let expected = `a{--ON:initial;--OFF: }` + expect(actual).toEqual(expected) +}) +test('adds quotes around strings in url()', () => { + let actual = format(`a { + background-image: url("star.gif"); + list-style-image: url('../images/bullet.jpg'); + content: url("pdficon.jpg"); + cursor: url(mycursor.cur); + border-image-source: url(/media/diamonds.png); + src: url('fantasticfont.woff'); + offset-path: url(#path); + mask-image: url("masks.svg#mask1"); + }`) + let expected = `a { + background-image: url("star.gif"); + list-style-image: url("../images/bullet.jpg"); + content: url("pdficon.jpg"); + cursor: url("mycursor.cur"); + border-image-source: url("/media/diamonds.png"); + src: url("fantasticfont.woff"); + offset-path: url("#path"); + mask-image: url("masks.svg#mask1"); +}` + expect(actual).toEqual(expected) +}) +test.each([ + `data:image/svg+xml;utf8,`, + `data:image/svg+xml;utf8,`, +])('Does not mess up URLs with inlined SVG', (input) => { + let actual = format(`test { + background-image: url('${input}'); + background-image: url(${input}); + }`) + let expected = `test { + background-image: url(${input}); + background-image: url(${input}); +}` + expect(actual).toEqual(expected) +}) +test.each([ + // Examples from https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data + 'data:,Hello%2C%20World%21', + 'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==', + 'data:text/html,%3Ch1%3EHello%2C%20World%21%3C%2Fh1%3E', + 'data:text/html,%3Cscript%3Ealert%28%27hi%27%29%3B%3C%2Fscript%3E', + // from https://github.com/projectwallace/format-css/issues/144 + `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgaGVpZ2h0PSIyNHB4IiB3aWR0aD0iMjRweCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQiIHgxPSIyMi4zMSIgeTE9IjIzLjYyIiB4Mj0iMy43MyIgeTI9IjMuMDUiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiNlOTM3MjIiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNmODZmMjUiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48dGl0bGU+TWFnbmlmaWVyPC90aXRsZT48cGF0aCBmaWxsPSJ1cmwoI2xpbmVhci1ncmFkaWVudCkiIGQ9Ik0yMy4zMyAyMC4xbC00LjczLTQuNzRhMTAuMDYgMTAuMDYgMCAxIDAtMy4yMyAzLjIzbDQuNzQgNC43NGEyLjI5IDIuMjkgMCAxIDAgMy4yMi0zLjIzem0tMTcuNDgtNS44NGE1Ljk0IDUuOTQgMCAxIDEgOC40MiAwIDYgNiAwIDAgMS04LjQyIDB6Ii8+PC9zdmc+`, +])('Does not mess up URLs with encoded inlined content: %s', (input) => { + let actual = format(`test { + background-image: url(${input}); + }`) + let expected = `test { + background-image: url(${input}); +}` + expect(actual).toBe(expected) +}) +test.each([ + `U+26`, // single code point + `U+0-7F`, + `U+0025-00FF`, // code point range + `U+4??`, // wildcard range + `U+0025-00FF, U+4??`, // multiple values +])('Formats unicode-range: %s', (unicode_range) => { + let actual = format(`test { unicode-range: ${unicode_range}; }`) + let expected = `test { + unicode-range: ${unicode_range}; +}` + expect(actual).toBe(expected) +}) +test('formats multi-value unicode range', () => { + let actual = format(`test { unicode-range: U+0025-00FF,U+4??; }`) + let expected = `test { + unicode-range: U+0025-00FF, U+4??; +}` + expect(actual).toBe(expected) +}) diff --git a/tsconfig.json b/tsconfig.json index 0ed86e1..e3b1b20 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "verbatimModuleSyntax": true, "allowJs": false, "moduleDetection": "force", + "types": ["node"], // Strictness "strict": true, "noUncheckedIndexedAccess": true, @@ -13,8 +14,7 @@ "module": "ESNext", "moduleResolution": "bundler", "lib": ["es2024", "DOM"], - "declaration": true, - "rootDirs": ["src/lib", "src/cli"], + "rootDir": "src", "outDir": "dist", "paths": { // So we can import like an external in the CLI diff --git a/tsdown.config.js b/tsdown.config.js new file mode 100644 index 0000000..2a97b23 --- /dev/null +++ b/tsdown.config.js @@ -0,0 +1,32 @@ +import { defineConfig } from 'tsdown' +import { codecovRollupPlugin } from '@codecov/rollup-plugin' +export default defineConfig([ + { + entry: 'src/lib/index.ts', + platform: 'neutral', + publint: true, + plugins: [ + codecovRollupPlugin({ + enableBundleAnalysis: process.env.CODECOV_TOKEN !== undefined, + bundleName: 'formatCss', + uploadToken: process.env.CODECOV_TOKEN, + }), + ], + }, + { + entry: 'src/cli/cli.ts', + platform: 'node', + dts: false, + // Reference the lib via its package name to avoid bundling it twice + deps: { + neverBundle: ['@projectwallace/format-css'], + }, + plugins: [ + codecovRollupPlugin({ + enableBundleAnalysis: process.env.CODECOV_TOKEN !== undefined, + bundleName: 'formatCssCli', + uploadToken: process.env.CODECOV_TOKEN, + }), + ], + }, +]) diff --git a/vitest.config.js b/vitest.config.js new file mode 100644 index 0000000..6f9ea30 --- /dev/null +++ b/vitest.config.js @@ -0,0 +1,14 @@ +import { defineConfig } from 'vitest/config' +import { resolve } from 'node:path' +export default defineConfig({ + resolve: { + alias: { + '@projectwallace/format-css': resolve('./src/lib/index.ts'), + }, + }, + test: { + coverage: { + provider: 'v8', + }, + }, +})