diff --git a/package.json b/package.json index 6074768..83bace7 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "escape-string-regexp": "1.0.5", "source-map": "0.5.6", "string-hash": "1.1.1", - "stylis": "3.0.17" + "stylis": "3.1.5" }, "devDependencies": { "ava": "0.19.1", diff --git a/src/_utils.js b/src/_utils.js index 8182b32..e33467e 100644 --- a/src/_utils.js +++ b/src/_utils.js @@ -58,18 +58,14 @@ export const getExpressionText = expr => { // e.g. // p { color: ${myConstant}; } // becomes - // p { color: var(--styled-jsx-expression-${id}--); } - // - // We use a dummy custom property so that the resulting css - // passes the css validation which is needed to detect - // external styles. + // p { color: %%styled-jsx-placeholder-${id}%%; } const replacements = expressions .map((e, id) => ({ pattern: new RegExp( `\\$\\{\\s*${escapeStringRegExp(e.getSource())}\\s*\\}` ), - replacement: `var(--styled-jsx-expression-${id}--)`, + replacement: `%%styled-jsx-placeholder-${id}%%`, initial: `$\{${e.getSource()}}` })) .sort((a, b) => a.initial.length < b.initial.length) @@ -94,7 +90,7 @@ export const getExpressionText = expr => { export const restoreExpressions = (css, replacements) => replacements.reduce((css, currentReplacement) => { css = css.replace( - new RegExp(escapeStringRegExp(currentReplacement.replacement), 'g'), + new RegExp(currentReplacement.replacement, 'g'), currentReplacement.initial ) return css @@ -197,7 +193,49 @@ export const generateAttribute = (name, value) => export const isValidCss = str => { try { - parseCss(str) + parseCss( + // Replace the placeholders with some valid CSS + // so that parsing doesn't fail for otherwise valid CSS. + str + // Replace all the placeholders with `all` + .replace( + // `\S` (the `delimiter`) is to match + // the beginning of a block `{` + // a property `:` + // or the end of a property `;` + /(\S)?\s*%%styled-jsx-placeholder-[^%]+%%(?:\s*(\}))?/gi, + (match, delimiter, isBlockEnd) => { + // The `end` of the replacement would be + let end + + if (delimiter === ':' && isBlockEnd) { + // ';}' single property block without semicolon + // E.g. { color: all;} + end = `;}` + } else if (delimiter === '{' || isBlockEnd) { + // ':;' when we are at the beginning or the end of a block + // E.g. { all:; ...otherstuff + // E.g. all:; } + end = `:;${isBlockEnd || ''}` + } else if (delimiter === ';') { + // ':' when we are inside of a block + // E.g. color: red; all:; display: block; + end = ':' + } else { + // Otherwise empty + end = '' + } + + return `${delimiter || ''}all${end}` + } + ) + // Replace block placeholders before media queries + // E.g. all @media (all) {} + .replace(/all\s*([@])/g, (match, delimiter) => `all {} ${delimiter}`) + // Replace block placeholders at the beginning of a media query block + // E.g. @media (all) { all:; div { ... }} + .replace(/@media[^{]+{\s*all:;/g, '@media (all) { ') + ) return true } catch (err) {} return false diff --git a/test/__snapshots__/external.js.snap b/test/__snapshots__/external.js.snap index f1e8501..e85fcd3 100644 --- a/test/__snapshots__/external.js.snap +++ b/test/__snapshots__/external.js.snap @@ -14,15 +14,26 @@ exports[`transpiles external stylesheets 1`] = ` export const foo = new String(\`div{color:\${color}}\`); -foo.__hash = '1882068550'; -foo.__scoped = \`div[data-jsx-ext~=\\"2882068550\\"]{color:\${color}}\`; -foo.__scopedHash = '2882068550'; +foo.__hash = '166851635'; +foo.__scoped = \`div[data-jsx-ext~=\\"266851635\\"]{color:\${color}}\`; +foo.__scopedHash = '266851635'; var __styledJsxDefaultExport = new String(\`div{font-size:3em}p{color:\${color}}\`); -__styledJsxDefaultExport.__hash = '12515736096'; -__styledJsxDefaultExport.__scoped = \`div[data-jsx-ext~=\\"22515736096\\"]{font-size:3em}p[data-jsx-ext~=\\"22515736096\\"]{color:\${color}}\`; -__styledJsxDefaultExport.__scopedHash = '22515736096'; +__styledJsxDefaultExport.__hash = '12602670606'; +__styledJsxDefaultExport.__scoped = \`div[data-jsx-ext~=\\"22602670606\\"]{font-size:3em}p[data-jsx-ext~=\\"22602670606\\"]{color:\${color}}\`; +__styledJsxDefaultExport.__scopedHash = '22602670606'; +export default __styledJsxDefaultExport;" +`; + +exports[`transpiles external stylesheets with validation (expressions) 1`] = ` +"const expr = 'test'; + +var __styledJsxDefaultExport = new String(\`\${expr} \${expr} \${expr},div{display:\${expr};color:\${expr};\${expr};\${expr} \${expr};background:red;-webkit-animation:\${expr} 10s ease-out;animation:\${expr} 10s ease-out}div{color:red;\${expr}}div{color:red;\${expr}}@media (\${expr}){\${expr} span.\${expr}{color:red}\${expr} \${expr}{color:red}\${expr},\${expr}{color:red}\${expr} div,\${expr}{color:red}}@media (min-width:\${expr}){div.\${expr}{color:red}all\${expr}{\${expr} color:red}}@font-face{\${expr}}\`); + +__styledJsxDefaultExport.__hash = '12538838655'; +__styledJsxDefaultExport.__scoped = \`\${expr}[data-jsx-ext~=\\"22538838655\\"] \${expr}[data-jsx-ext~=\\"22538838655\\"] \${expr}[data-jsx-ext~=\\"22538838655\\"],div[data-jsx-ext~=\\"22538838655\\"]{display:\${expr};color:\${expr};\${expr};\${expr} \${expr};background:red;-webkit-animation:\${expr} 10s ease-out;animation:\${expr} 10s ease-out}div[data-jsx-ext~=\\"22538838655\\"]{color:red;\${expr}}div[data-jsx-ext~=\\"22538838655\\"]{color:red;\${expr}}@media (\${expr}){\${expr} span.\${expr}[data-jsx-ext~=\\"22538838655\\"]{color:red}\${expr} \${expr}[data-jsx-ext~=\\"22538838655\\"]{color:red}\${expr}[data-jsx-ext~=\\"22538838655\\"],\${expr}[data-jsx-ext~=\\"22538838655\\"]{color:red}\${expr} div[data-jsx-ext~=\\"22538838655\\"],\${expr}[data-jsx-ext~=\\"22538838655\\"]{color:red}}@media (min-width:\${expr}){div.\${expr}[data-jsx-ext~=\\"22538838655\\"]{color:red}all\${expr}[data-jsx-ext~=\\"22538838655\\"]{\${expr} color:red}}@font-face{\${expr}}\`; +__styledJsxDefaultExport.__scopedHash = '22538838655'; export default __styledJsxDefaultExport;" `; @@ -31,14 +42,53 @@ exports[`transpiles external stylesheets with validation 1`] = ` export const foo = new String(\`div{color:\${color}}\`); -foo.__hash = '1882068550'; -foo.__scoped = \`div[data-jsx-ext~=\\"2882068550\\"]{color:\${color}}\`; -foo.__scopedHash = '2882068550'; +foo.__hash = '166851635'; +foo.__scoped = \`div[data-jsx-ext~=\\"266851635\\"]{color:\${color}}\`; +foo.__scopedHash = '266851635'; var __styledJsxDefaultExport = new String(\`div{font-size:3em}p{color:\${color}}\`); -__styledJsxDefaultExport.__hash = '12515736096'; -__styledJsxDefaultExport.__scoped = \`div[data-jsx-ext~=\\"22515736096\\"]{font-size:3em}p[data-jsx-ext~=\\"22515736096\\"]{color:\${color}}\`; -__styledJsxDefaultExport.__scopedHash = '22515736096'; +__styledJsxDefaultExport.__hash = '12602670606'; +__styledJsxDefaultExport.__scoped = \`div[data-jsx-ext~=\\"22602670606\\"]{font-size:3em}p[data-jsx-ext~=\\"22602670606\\"]{color:\${color}}\`; +__styledJsxDefaultExport.__scopedHash = '22602670606'; export default __styledJsxDefaultExport;" `; + +exports[`transpiles external stylesheets with validation 2`] = ` +"const expr = 'test'; + +export const expressionsTest = \` + div { + display: \${expr}; + color: \${expr}; + \${expr}; + \${expr} + \${expr}; + background: red; + animation: \${expr} 10s ease-out; + } + + @media (\${expr}) { + div.\${expr} { + color: red; + } + \${expr} + \${expr} { + color: red; + } + } + + @media (min-width: \${expr}) { + div.\${expr} { + color: red; + } + all\${expr} { + color: red; + } + } + + @font-face { + \${expr} + } +\`;" +`; diff --git a/test/__snapshots__/index.js.snap b/test/__snapshots__/index.js.snap index c4f4e62..3c632c1 100644 --- a/test/__snapshots__/index.js.snap +++ b/test/__snapshots__/index.js.snap @@ -5,7 +5,7 @@ exports[`generates source maps 1`] = ` export default (() =>

test

woot

- <_JSXStyle styleId={188072295} css={\\"p[data-jsx=\\\\\\"188072295\\\\\\"]{color:red}\\\\n/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsImZpbGUiOiJzb3VyY2UtbWFwcy5qcyIsInNvdXJjZXNDb250ZW50IjpbXX0= */\\\\n/*@ sourceURL=source-maps.js */\\"} /> + <_JSXStyle styleId={188072295} css={\\"p[data-jsx=\\\\\\"188072295\\\\\\"]{color:red}\\\\n/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInNvdXJjZS1tYXBzLmpzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUlnQixBQUNjLFdBQUMiLCJmaWxlIjoic291cmNlLW1hcHMuanMiLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZGVmYXVsdCAoKSA9PiAoXG4gIDxkaXY+XG4gICAgPHA+dGVzdDwvcD5cbiAgICA8cD53b290PC9wPlxuICAgIDxzdHlsZSBqc3g+eydwIHsgY29sb3I6IHJlZCB9J308L3N0eWxlPlxuICA8L2Rpdj5cbilcbiJdfQ== */\\\\n/*@ sourceURL=source-maps.js */\\"} />
);" `; @@ -92,19 +92,19 @@ const animationName = 'my-cool-animation'; const obj = { display: 'block' }; // eslint-disable-next-line no-unused-vars -export default (({ display }) =>
-

test

- <_JSXStyle styleId={2129566164} css={\`p.\${color}[data-jsx=\\"290536030\\"]{color:\${otherColor};display:\${obj.display}}\`} /> - <_JSXStyle styleId={188072295} css={\\"p[data-jsx=\\\\\\"290536030\\\\\\"]{color:red}\\"} /> +export default (({ display }) =>
+

test

+ <_JSXStyle styleId={2129566164} css={\`p.\${color}[data-jsx=\\"4091813028\\"]{color:\${otherColor};display:\${obj.display}}\`} /> + <_JSXStyle styleId={188072295} css={\\"p[data-jsx=\\\\\\"4091813028\\\\\\"]{color:red}\\"} /> <_JSXStyle styleId={806016056} css={\`body{background:\${color}}\`} /> <_JSXStyle styleId={806016056} css={\`body{background:\${color}}\`} /> - <_JSXStyle styleId={924167211} css={\`p[data-jsx=\\"290536030\\"]{color:\${color}}\`} /> - <_JSXStyle styleId={924167211} css={\`p[data-jsx=\\"290536030\\"]{color:\${color}}\`} /> - <_JSXStyle styleId={3469794077} css={\`p[data-jsx=\\"290536030\\"]{color:\${darken(color)}}\`} /> - <_JSXStyle styleId={945380644} css={\`p[data-jsx=\\"290536030\\"]{color:\${darken(color) + 2}}\`} /> - <_JSXStyle styleId={4106311606} css={\`@media (min-width:\${mediumScreen}){p[data-jsx=\\"290536030\\"]{color:green}p[data-jsx=\\"290536030\\"]{color \${\`red\`}}}p[data-jsx=\\"290536030\\"]{color:red}\`} /> - <_JSXStyle styleId={2369334310} css={\`p[data-jsx=\\"290536030\\"]{-webkit-animation-duration:\${animationDuration};animation-duration:\${animationDuration}}\`} /> - <_JSXStyle styleId={3168033860} css={\`p[data-jsx=\\"290536030\\"]{-webkit-animation:\${animationDuration} forwards \${animationName};animation:\${animationDuration} forwards \${animationName}}\`} /> + <_JSXStyle styleId={924167211} css={\`p[data-jsx=\\"4091813028\\"]{color:\${color}}\`} /> + <_JSXStyle styleId={924167211} css={\`p[data-jsx=\\"4091813028\\"]{color:\${color}}\`} /> + <_JSXStyle styleId={3469794077} css={\`p[data-jsx=\\"4091813028\\"]{color:\${darken(color)}}\`} /> + <_JSXStyle styleId={945380644} css={\`p[data-jsx=\\"4091813028\\"]{color:\${darken(color) + 2}}\`} /> + <_JSXStyle styleId={3617592140} css={\`@media (min-width:\${mediumScreen}){p[data-jsx=\\"4091813028\\"]{color:green}p[data-jsx=\\"4091813028\\"]{color:\${\`red\`}}}p[data-jsx=\\"4091813028\\"]{color:red}\`} /> + <_JSXStyle styleId={2369334310} css={\`p[data-jsx=\\"4091813028\\"]{-webkit-animation-duration:\${animationDuration};animation-duration:\${animationDuration}}\`} /> + <_JSXStyle styleId={3168033860} css={\`p[data-jsx=\\"4091813028\\"]{-webkit-animation:\${animationDuration} forwards \${animationName};animation:\${animationDuration} forwards \${animationName}}\`} />
);" `; diff --git a/test/external.js b/test/external.js index 5a7a711..7cdbd37 100644 --- a/test/external.js +++ b/test/external.js @@ -29,3 +29,11 @@ test('transpiles external stylesheets with validation', async t => { t.regex(code, new RegExp(escapeStringRegExp(MARKUP_ATTRIBUTE_EXTERNAL), 'g')) t.snapshot(code) }) + +test('transpiles external stylesheets with validation (expressions)', async t => { + const { code } = await transform('./fixtures/styles-expressions.js', { + validate: true + }) + t.regex(code, new RegExp(escapeStringRegExp(MARKUP_ATTRIBUTE_EXTERNAL), 'g')) + t.snapshot(code) +}) diff --git a/test/fixtures/expressions.js b/test/fixtures/expressions.js index 8efb86c..53a1524 100644 --- a/test/fixtures/expressions.js +++ b/test/fixtures/expressions.js @@ -23,7 +23,7 @@ export default ({ display }) => ( diff --git a/test/fixtures/styles-expressions.js b/test/fixtures/styles-expressions.js new file mode 100644 index 0000000..1072007 --- /dev/null +++ b/test/fixtures/styles-expressions.js @@ -0,0 +1,60 @@ +const expr = 'test' + +export default `${expr} ${expr} ${expr}, + div { + display: ${expr}; + color: ${expr}; + ${expr}; + ${expr} + ${expr}; + background: red; + animation: ${expr} 10s ease-out; + } + + div { + color: red; +${expr} +} + +div { + color: red; +${expr}; +} + + @media (${expr}) { + +${expr} + span.${expr} { + color: red; + } + +${expr} + + ${expr} { + color: red; + } + ${expr}, ${expr} { + color: red; + } + + ${expr} + + div, ${expr} { + color: red; + } + } + + + @media (min-width: ${expr}) { + div.${expr} { + color: red; + } + all${expr} { +${expr} + color: red; + } + } + + @font-face { + ${expr} +}` diff --git a/test/fixtures/styles.js b/test/fixtures/styles.js index bde1d07..b76deb2 100644 --- a/test/fixtures/styles.js +++ b/test/fixtures/styles.js @@ -4,5 +4,5 @@ export const foo = `div { color: ${color}}` export default ` div { font-size: 3em } - p { color: ${color}} + p { color: ${color};} ` diff --git a/yarn.lock b/yarn.lock index 32d2d0d..55fa6b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4625,9 +4625,9 @@ strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" -stylis@3.0.17: - version "3.0.17" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.0.17.tgz#978643aed384f2138c54af9c02adeb61f1aa75f6" +stylis@3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.1.5.tgz#c585186286aaa79856c9ac62bbb38113923edda3" supports-color@^2.0.0: version "2.0.0"