Browse Source

Fix expressions placeholders parsing/validation for external files (#227)

#223 fixed only one case, this branch aims to fix them all :)

Our safeguard is the `t.regex` from "transpiles external stylesheets with validation (expressions)" in `tests/external.js`, if that matches then the files are parsed correctly as css and therefore processed by styled-jsx.

Fixes #224
add-plugins-support
Giuseppe 7 years ago
committed by GitHub
parent
commit
52c9c1ee6d
  1. 2
      package.json
  2. 54
      src/_utils.js
  3. 74
      test/__snapshots__/external.js.snap
  4. 24
      test/__snapshots__/index.js.snap
  5. 8
      test/external.js
  6. 2
      test/fixtures/expressions.js
  7. 60
      test/fixtures/styles-expressions.js
  8. 2
      test/fixtures/styles.js
  9. 6
      yarn.lock

2
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",

54
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

74
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}
}
\`;"
`;

24
test/__snapshots__/index.js.snap

@ -5,7 +5,7 @@ exports[`generates source maps 1`] = `
export default (() => <div data-jsx={188072295}>
<p data-jsx={188072295}>test</p>
<p data-jsx={188072295}>woot</p>
<_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 */\\"} />
</div>);"
`;
@ -92,19 +92,19 @@ const animationName = 'my-cool-animation';
const obj = { display: 'block' };
// eslint-disable-next-line no-unused-vars
export default (({ display }) => <div data-jsx={290536030}>
<p data-jsx={290536030}>test</p>
<_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 }) => <div data-jsx={4091813028}>
<p data-jsx={4091813028}>test</p>
<_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}}\`} />
</div>);"
`;

8
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)
})

2
test/fixtures/expressions.js

@ -23,7 +23,7 @@ export default ({ display }) => (
<style jsx>{`
@media (min-width: ${mediumScreen}) {
p { color: green }
p { color ${`red`}}
p { color: ${`red`}}
}
p { color: red }`}</style>
<style jsx>{`p { animation-duration: ${animationDuration} }`}</style>

60
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}
}`

2
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};}
`

6
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"

Loading…
Cancel
Save