diff --git a/package.json b/package.json index 60747686..83bace70 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 8182b323..e33467e8 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 f1e8501c..e85fcd33 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 c4f4e62e..3c632c15 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 */\\"} />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}}\`} />