From 5e963f46645ffd3eeeea30574780acffb1645a97 Mon Sep 17 00:00:00 2001 From: Bart Langelaan Date: Tue, 6 Oct 2020 21:50:06 +0200 Subject: [PATCH] Add an `unstable_subtitute` option that replaces all literal expressions with a sass-like dummy value --- substitute-javascript.js | 75 ++++++++++++++++++++++++++++++++++ template-parse.js | 6 +++ template-safe-parse.js | 6 +++ template-stringify.js | 9 ++++ test/styled-components.js | 86 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 182 insertions(+) create mode 100644 substitute-javascript.js diff --git a/substitute-javascript.js b/substitute-javascript.js new file mode 100644 index 0000000..af2abad --- /dev/null +++ b/substitute-javascript.js @@ -0,0 +1,75 @@ +'use strict'; + +const literalsKey = Symbol('Literals'); +const replacementsKey = Symbol('Replaced properties'); + +function addSubstitution(item, key, literals) { + item[replacementsKey] = item[replacementsKey] || []; + + literals.forEach((literal) => { + const position = item[key].indexOf(literal); + + if (position !== -1) { + const substitution = '$dummyValue' + position; + + item[replacementsKey].push({ + key, + original: literal, + substitution, + }); + + item[key] = item[key].replace(literal, substitution); + } + }); +} + +function addSubstitutions(node, css, opts) { + if (typeof node.walk !== 'function') return; + + if (css && opts) { + const offset = opts.quasis[0].start; + + node[literalsKey] = []; + + opts.expressions.forEach(({ start, end }) => { + node[literalsKey].push('${' + css.substring(start - offset, end - offset) + '}'); + }); + } + + if (!node[literalsKey]) return; + + node.walk((item) => { + if (item.type === 'atrule') { + addSubstitution(item, 'name', node[literalsKey]); + addSubstitution(item, 'params', node[literalsKey]); + } else if (item.type === 'rule') { + addSubstitution(item, 'selector', node[literalsKey]); + } else if (item.type === 'decl') { + addSubstitution(item, 'prop', node[literalsKey]); + addSubstitution(item, 'value', node[literalsKey]); + } else if (item.type === 'root') { + addSubstitutions(item); + } + }); +} + +exports.addSubstitutions = addSubstitutions; + +exports.removeSubstitutions = (node) => { + if (typeof node.walk !== 'function') return; + + node.walk((item) => { + if (!item[replacementsKey]) { + return; + } + + item[replacementsKey].forEach((replacement) => { + item[replacement.key] = item[replacement.key].replace( + replacement.substitution, + replacement.original, + ); + }); + + delete item[replacementsKey]; + }); +}; diff --git a/template-parse.js b/template-parse.js index 1bc48c8..5fc39ee 100644 --- a/template-parse.js +++ b/template-parse.js @@ -1,6 +1,7 @@ 'use strict'; const Input = require('postcss/lib/input'); +const substitutions = require('./substitute-javascript'); const TemplateParser = require('./template-parser'); function templateParse(css, opts) { @@ -13,6 +14,11 @@ function templateParse(css, opts) { parser.parse(); + if (((opts.syntax.config.jsx || {}).config || opts.syntax.config).unstable_substitute) { + parser.root.unstable_substitute = true; + substitutions.addSubstitutions(parser.root, css, opts); + } + return parser.root; } diff --git a/template-safe-parse.js b/template-safe-parse.js index ec0ccd8..b1c5805 100644 --- a/template-safe-parse.js +++ b/template-safe-parse.js @@ -1,6 +1,7 @@ 'use strict'; const Input = require('postcss/lib/input'); +const substitutions = require('./substitute-javascript'); const TemplateSafeParser = require('./template-safe-parser'); function templateSafeParse(css, opts) { @@ -13,6 +14,11 @@ function templateSafeParse(css, opts) { parser.parse(); + if (opts.syntax.config.unstable_substitute) { + parser.root.unstable_substitute = true; + substitutions.addSubstitutions(parser.root, css, opts); + } + return parser.root; } diff --git a/template-stringify.js b/template-stringify.js index 291e66e..7ccbd70 100644 --- a/template-stringify.js +++ b/template-stringify.js @@ -1,9 +1,18 @@ 'use strict'; +const substitutions = require('./substitute-javascript'); const TemplateStringifier = require('./template-stringifier'); module.exports = function TemplateStringify(node, builder) { + if (node.unstable_substitute) { + substitutions.removeSubstitutions(node); + } + const str = new TemplateStringifier(builder); str.stringify(node); + + if (node.unstable_substitute) { + substitutions.addSubstitutions(node); + } }; diff --git a/test/styled-components.js b/test/styled-components.js index ceb53b3..e5ba8a7 100644 --- a/test/styled-components.js +++ b/test/styled-components.js @@ -323,4 +323,90 @@ describe('styled-components', () => { expect(document.source).toHaveProperty('lang', 'jsx'); expect(document.nodes).toHaveLength(3); }); + + it('re-writes various stuff to substitutes', () => { + const code = ` + import styled from 'styled-components'; + + const greenColor = 'green'; + const returnGreenColor = () => 'green' + + const C = styled.div\` + @media (min-width: \${t => t.test}) { + color: \${p => p.width}; + \${prop}: green; + background-\${prop}: dark\${greenColor}; + \${decl}; + \${css\` + color: light\${returnGreenColor()} + \`} + } + \`; + `; + + const expectation = { + nodes: [ + { + nodes: [ + { + type: 'atrule', + params: '(min-width: $dummyValue12)', + nodes: [ + { + type: 'decl', + prop: 'color', + value: '$dummyValue0', + }, + { + type: 'decl', + prop: '$dummyValue0', + value: 'green', + }, + { + type: 'decl', + prop: 'background-$dummyValue11', + value: 'dark$dummyValue4', + }, + { + type: 'literal', + }, + { + type: 'literal', + nodes: [ + { + type: 'root', + nodes: [ + { + type: 'decl', + prop: 'color', + value: 'light$dummyValue5', + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }; + + const document = syntax({ jsx: { config: { unstable_substitute: true } } }).parse(code); + + // The document should have the right properties. + expect(document.source).toHaveProperty('lang', 'jsx'); + + // The document should, without stringifying first, have the right values. + expect(document).toMatchObject(expectation); + + // If we stringify the document, it should not have changed in any way. + expect(document.toString()).toBe(code); + + // During stringifying, the substitutions are removed and added. After that they should still have the right values. + expect(document).toMatchObject(expectation); + + // And one last check - the first time the document is stringified, should not affect subsequent stringifications. + expect(document.toString()).toBe(code); + }); });