From fb253cf3d300de1d3c20b59166a56a568c6d3e47 Mon Sep 17 00:00:00 2001 From: Satyajit Sahoo Date: Mon, 15 Oct 2018 12:11:03 +0200 Subject: [PATCH] fix: check if import is from linaria (#231) --- .../__snapshots__/babel.test.js.snap | 66 +++++++---- .../__snapshots__/preval.test.js.snap | 39 ++++--- src/__tests__/babel.test.js | 34 ++++++ src/__tests__/preval.test.js | 17 +++ src/babel/extract.js | 104 ++++++++++++++++-- 5 files changed, 213 insertions(+), 47 deletions(-) diff --git a/src/__tests__/__snapshots__/babel.test.js.snap b/src/__tests__/__snapshots__/babel.test.js.snap index 534103515..f504ede21 100644 --- a/src/__tests__/__snapshots__/babel.test.js.snap +++ b/src/__tests__/__snapshots__/babel.test.js.snap @@ -8,7 +8,8 @@ const title = String.raw\`This is something\`;" exports[`does not output CSS if none present 2`] = `Object {}`; exports[`evaluates and inlines expressions in scope 1`] = ` -"const color = 'blue'; +"import { styled } from 'linaria/react'; +const color = 'blue'; const Title = /*#__PURE__*/ styled(\\"h1\\")({ @@ -30,7 +31,10 @@ Dependencies: NA `; -exports[`handles css template literal in JSX element 1`] = `";"`; +exports[`handles css template literal in JSX element 1`] = ` +"import { css } from 'linaria'; +<Title class={\\"th6xni0\\"} />;" +`; exports[`handles css template literal in JSX element 2`] = ` @@ -43,7 +47,8 @@ Dependencies: NA `; exports[`handles css template literal in object property 1`] = ` -"const components = { +"import { css } from 'linaria'; +const components = { title: \\"th6xni0\\" };" `; @@ -61,7 +66,8 @@ Dependencies: NA `; exports[`handles interpolation followed by unit 1`] = ` -"const Title = +"import { styled } from 'linaria/react'; +const Title = /*#__PURE__*/ styled(\\"h1\\")({ name: \\"Title\\", @@ -105,7 +111,8 @@ Dependencies: NA `; exports[`handles nested blocks 1`] = ` -"const Button = +"import { styled } from 'linaria/react'; +const Button = /*#__PURE__*/ styled(\\"button\\")({ name: \\"Button\\", @@ -137,7 +144,8 @@ Dependencies: NA `; exports[`inlines object styles as CSS string 1`] = ` -"const cover = { +"import { styled } from 'linaria/react'; +const cover = { position: 'absolute', top: 0, right: 0, @@ -181,7 +189,8 @@ Dependencies: NA `; exports[`outputs valid CSS classname 1`] = ` -"const ᾩPage$Title = +"import { styled } from 'linaria/react'; +const ᾩPage$Title = /*#__PURE__*/ styled(\\"h1\\")({ name: \\"\\\\u1FA9Page$Title\\", @@ -202,7 +211,8 @@ Dependencies: NA `; exports[`prevents class name collision 1`] = ` -"const Title = +"import { styled } from 'linaria/react'; +const Title = /*#__PURE__*/ styled(\\"h1\\")({ name: \\"Title\\", @@ -243,7 +253,8 @@ Dependencies: NA `; exports[`replaces unknown expressions with CSS custom properties 1`] = ` -"const Title = +"import { styled } from 'linaria/react'; +const Title = /*#__PURE__*/ styled(\\"h1\\")({ name: \\"Title\\", @@ -270,10 +281,11 @@ Dependencies: NA exports[`throws when contains dynamic expression without evaluate: true in css tag 1`] = ` "<<DIRNAME>>/app/index.js: The CSS cannot contain JavaScript expressions when using the 'css' tag. To evaluate the expressions at build time, pass 'evaluate: true' to the babel plugin. -  1 | const title = css\` -> 2 |  font-size: \${size}px; +  2 |  +  3 | const title = css\` +> 4 |  font-size: \${size}px;   |  ^ -  3 | \`;" +  5 | \`;" `; exports[`throws when not attached to a variable 1`] = ` @@ -282,14 +294,19 @@ exports[`throws when not attached to a variable 1`] = ` - Is an object property - Is a prop in a JSX element -> 1 | styled.h1\` +  1 | import { styled } from 'linaria/react'; +  2 |  +> 3 | styled.h1\`   | ^ -  2 |  font-size: \${size}px; -  3 |  color: \${props => props.color} -  4 | \`;" +  4 |  font-size: \${size}px; +  5 |  color: \${props => props.color} +  6 | \`;" `; -exports[`transpiles css template literal 1`] = `"const title = \\"th6xni0\\";"`; +exports[`transpiles css template literal 1`] = ` +"import { css } from 'linaria'; +const title = \\"th6xni0\\";" +`; exports[`transpiles css template literal 2`] = ` @@ -304,7 +321,8 @@ Dependencies: NA `; exports[`transpiles styled template literal with function and component 1`] = ` -"const Title = +"import { styled } from 'linaria/react'; +const Title = /*#__PURE__*/ styled(Heading)({ name: \\"Title\\", @@ -325,7 +343,8 @@ Dependencies: NA `; exports[`transpiles styled template literal with function and tag 1`] = ` -"const Title = +"import { styled } from 'linaria/react'; +const Title = /*#__PURE__*/ styled('h1')({ name: \\"Title\\", @@ -346,7 +365,8 @@ Dependencies: NA `; exports[`transpiles styled template literal with object 1`] = ` -"const Title = +"import { styled } from 'linaria/react'; +const Title = /*#__PURE__*/ styled(\\"h1\\")({ name: \\"Title\\", @@ -367,7 +387,8 @@ Dependencies: NA `; exports[`uses the same custom property for the same expression 1`] = ` -"const Box = +"import { styled } from 'linaria/react'; +const Box = /*#__PURE__*/ styled(\\"div\\")({ name: \\"Box\\", @@ -392,7 +413,8 @@ Dependencies: NA `; exports[`uses the same custom property for the same identifier 1`] = ` -"const Box = +"import { styled } from 'linaria/react'; +const Box = /*#__PURE__*/ styled(\\"div\\")({ name: \\"Box\\", diff --git a/src/__tests__/__snapshots__/preval.test.js.snap b/src/__tests__/__snapshots__/preval.test.js.snap index 015ad7e12..890598165 100644 --- a/src/__tests__/__snapshots__/preval.test.js.snap +++ b/src/__tests__/__snapshots__/preval.test.js.snap @@ -37,7 +37,8 @@ Dependencies: ../react `; exports[`evaluates expressions with dependencies 1`] = ` -"import slugify from '../slugify'; +"import { styled } from 'linaria/react'; +import slugify from '../slugify'; const Title = /*#__PURE__*/ styled(\\"h1\\")({ @@ -61,7 +62,9 @@ Dependencies: ../slugify `; exports[`evaluates expressions with expressions depending on shared dependency 1`] = ` -"const slugify = require('../slugify'); +"import { styled } from 'linaria/react'; + +const slugify = require('../slugify'); const boo = t => slugify(t) + 'boo'; @@ -90,7 +93,8 @@ Dependencies: ../slugify `; exports[`evaluates identifier in scope 1`] = ` -"const answer = 42; +"import { styled } from 'linaria/react'; +const answer = 42; const foo = () => answer; @@ -118,7 +122,8 @@ Dependencies: NA `; exports[`evaluates local expressions 1`] = ` -"const answer = 42; +"import { styled } from 'linaria/react'; +const answer = 42; const foo = () => answer; @@ -145,7 +150,9 @@ Dependencies: NA `; exports[`evaluates multiple expressions with shared dependency 1`] = ` -"const slugify = require('../slugify'); +"import { styled } from 'linaria/react'; + +const slugify = require('../slugify'); const boo = t => slugify(t) + 'boo'; @@ -201,7 +208,9 @@ Dependencies: NA `; exports[`ignores external expressions 1`] = ` -"const generate = props => props.content; +"import { styled } from 'linaria/react'; + +const generate = props => props.content; const Title = /*#__PURE__*/ @@ -229,7 +238,8 @@ Dependencies: NA `; exports[`ignores inline arrow function expressions 1`] = ` -"const Title = +"import { styled } from 'linaria/react'; +const Title = /*#__PURE__*/ styled(\\"h1\\")({ name: \\"Title\\", @@ -255,7 +265,8 @@ Dependencies: NA `; exports[`ignores inline vanilla function expressions 1`] = ` -"const Title = +"import { styled } from 'linaria/react'; +const Title = /*#__PURE__*/ styled(\\"h1\\")({ name: \\"Title\\", @@ -283,7 +294,9 @@ Dependencies: NA `; exports[`inlines object styles as CSS string 1`] = ` -"const fill = (top = 0, left = 0, right = 0, bottom = 0) => ({ +"import { styled } from 'linaria/react'; + +const fill = (top = 0, left = 0, right = 0, bottom = 0) => ({ position: 'absolute', top, right, @@ -313,9 +326,9 @@ Dependencies: NA exports[`throws codeframe error when evaluation fails 1`] = ` "<<DIRNAME>>/source.js: An error occurred when evaluating the expression: This will fail. Make sure you are not using a browser or Node specific API. -  2 |  -  3 | const Title = styled.h1\` -> 4 |  font-size: \${foo()}px; +  4 |  +  5 | const Title = styled.h1\` +> 6 |  font-size: \${foo()}px;   |  ^ -  5 | \`;" +  7 | \`;" `; diff --git a/src/__tests__/babel.test.js b/src/__tests__/babel.test.js index 1983380b2..45e012520 100644 --- a/src/__tests__/babel.test.js +++ b/src/__tests__/babel.test.js @@ -19,6 +19,8 @@ const transpile = input => it('transpiles styled template literal with object', async () => { const { code, metadata } = await transpile( dedent` + import { styled } from 'linaria/react'; + const Title = styled.h1\` font-size: 14px; \`; @@ -32,6 +34,8 @@ it('transpiles styled template literal with object', async () => { it('transpiles styled template literal with function and tag', async () => { const { code, metadata } = await transpile( dedent` + import { styled } from 'linaria/react'; + const Title = styled('h1')\` font-size: 14px; \`; @@ -45,6 +49,8 @@ it('transpiles styled template literal with function and tag', async () => { it('transpiles styled template literal with function and component', async () => { const { code, metadata } = await transpile( dedent` + import { styled } from 'linaria/react'; + const Title = styled(Heading)\` font-size: 14px; \`; @@ -58,6 +64,8 @@ it('transpiles styled template literal with function and component', async () => it('outputs valid CSS classname', async () => { const { code, metadata } = await transpile( dedent` + import { styled } from 'linaria/react'; + const ᾩPage$Title = styled.h1\` font-size: 14px; \`; @@ -71,6 +79,8 @@ it('outputs valid CSS classname', async () => { it('evaluates and inlines expressions in scope', async () => { const { code, metadata } = await transpile( dedent` + import { styled } from 'linaria/react'; + const color = 'blue'; const Title = styled.h1\` @@ -87,6 +97,8 @@ it('evaluates and inlines expressions in scope', async () => { it('inlines object styles as CSS string', async () => { const { code, metadata } = await transpile( dedent` + import { styled } from 'linaria/react'; + const cover = { position: 'absolute', top: 0, @@ -125,6 +137,8 @@ it('inlines object styles as CSS string', async () => { it('replaces unknown expressions with CSS custom properties', async () => { const { code, metadata } = await transpile( dedent` + import { styled } from 'linaria/react'; + const Title = styled.h1\` font-size: ${'${size}'}px; color: ${'${props => props.color}'}; @@ -139,6 +153,8 @@ it('replaces unknown expressions with CSS custom properties', async () => { it('handles interpolation followed by unit', async () => { const { code, metadata } = await transpile( dedent` + import { styled } from 'linaria/react'; + const Title = styled.h1\` font-size: ${'${size}'}em; text-shadow: black 1px ${'${shadow}'}px, white -2px -2px; @@ -158,6 +174,8 @@ it('handles interpolation followed by unit', async () => { it('uses the same custom property for the same identifier', async () => { const { code, metadata } = await transpile( dedent` + import { styled } from 'linaria/react'; + const Box = styled.div\` height: ${'${size}'}px; width: ${'${size}'}px; @@ -172,6 +190,8 @@ it('uses the same custom property for the same identifier', async () => { it('uses the same custom property for the same expression', async () => { const { code, metadata } = await transpile( dedent` + import { styled } from 'linaria/react'; + const Box = styled.div\` height: ${'${props => props.size}'}px; width: ${'${props => props.size}'}px; @@ -186,6 +206,8 @@ it('uses the same custom property for the same expression', async () => { it('handles nested blocks', async () => { const { code, metadata } = await transpile( dedent` + import { styled } from 'linaria/react'; + const Button = styled.button\` font-family: ${'${regular}'}; @@ -207,6 +229,8 @@ it('handles nested blocks', async () => { it('prevents class name collision', async () => { const { code, metadata } = await transpile( dedent` + import { styled } from 'linaria/react'; + const Title = styled.h1\` font-size: ${'${size}'}px; color: ${'${props => props.color}'} @@ -230,6 +254,8 @@ it('throws when not attached to a variable', async () => { try { await transpile( dedent` + import { styled } from 'linaria/react'; + styled.h1\` font-size: ${'${size}'}px; color: ${'${props => props.color}'} @@ -257,6 +283,8 @@ it('does not output CSS if none present', async () => { it('transpiles css template literal', async () => { const { code, metadata } = await transpile( dedent` + import { css } from 'linaria'; + const title = css\` font-size: 14px; \`; @@ -270,6 +298,8 @@ it('transpiles css template literal', async () => { it('handles css template literal in object property', async () => { const { code, metadata } = await transpile( dedent` + import { css } from 'linaria'; + const components = { title: css\` font-size: 14px; @@ -285,6 +315,8 @@ it('handles css template literal in object property', async () => { it('handles css template literal in JSX element', async () => { const { code, metadata } = await transpile( dedent` + import { css } from 'linaria'; + <Title class={css\` font-size: 14px; \`} /> ` ); @@ -299,6 +331,8 @@ it('throws when contains dynamic expression without evaluate: true in css tag', try { await transpile( dedent` + import { css } from 'linaria'; + const title = css\` font-size: ${'${size}'}px; \`; diff --git a/src/__tests__/preval.test.js b/src/__tests__/preval.test.js index be6eeccbf..26c8865c6 100644 --- a/src/__tests__/preval.test.js +++ b/src/__tests__/preval.test.js @@ -31,6 +31,8 @@ const transpile = async input => { it('evaluates identifier in scope', async () => { const { code, metadata } = await transpile( dedent` + import { styled } from 'linaria/react'; + const answer = 42; const foo = () => answer; const days = foo() + ' days'; @@ -50,6 +52,8 @@ it('evaluates identifier in scope', async () => { it('evaluates local expressions', async () => { const { code, metadata } = await transpile( dedent` + import { styled } from 'linaria/react'; + const answer = 42; const foo = () => answer; @@ -68,6 +72,7 @@ it('evaluates local expressions', async () => { it('evaluates expressions with dependencies', async () => { const { code, metadata } = await transpile( dedent` + import { styled } from 'linaria/react'; import slugify from '../slugify'; const Title = styled.h1\` @@ -85,6 +90,7 @@ it('evaluates expressions with dependencies', async () => { it('evaluates expressions with expressions depending on shared dependency', async () => { const { code, metadata } = await transpile( dedent` + import { styled } from 'linaria/react'; const slugify = require('../slugify'); const boo = t => slugify(t) + 'boo'; @@ -105,6 +111,7 @@ it('evaluates expressions with expressions depending on shared dependency', asyn it('evaluates multiple expressions with shared dependency', async () => { const { code, metadata } = await transpile( dedent` + import { styled } from 'linaria/react'; const slugify = require('../slugify'); const boo = t => slugify(t) + 'boo'; @@ -147,6 +154,8 @@ it('evaluates component interpolations', async () => { it('inlines object styles as CSS string', async () => { const { code, metadata } = await transpile( dedent` + import { styled } from 'linaria/react'; + const fill = (top = 0, left = 0, right = 0, bottom = 0) => ({ position: 'absolute', top, @@ -168,6 +177,8 @@ it('inlines object styles as CSS string', async () => { it('ignores inline arrow function expressions', async () => { const { code, metadata } = await transpile( dedent` + import { styled } from 'linaria/react'; + const Title = styled.h1\` &:before { content: "${'${props => props.content}'}" @@ -183,6 +194,8 @@ it('ignores inline arrow function expressions', async () => { it('ignores inline vanilla function expressions', async () => { const { code, metadata } = await transpile( dedent` + import { styled } from 'linaria/react'; + const Title = styled.h1\` &:before { content: "${'${function(props) { return props.content }}'}" @@ -198,6 +211,8 @@ it('ignores inline vanilla function expressions', async () => { it('ignores external expressions', async () => { const { code, metadata } = await transpile( dedent` + import { styled } from 'linaria/react'; + const generate = props => props.content; const Title = styled.h1\` @@ -218,6 +233,8 @@ it('throws codeframe error when evaluation fails', async () => { try { await transpile( dedent` + import { styled } from 'linaria/react'; + const foo = props => { throw new Error('This will fail') }; const Title = styled.h1\` diff --git a/src/babel/extract.js b/src/babel/extract.js index 0aa087f06..b8aa68c2e 100644 --- a/src/babel/extract.js +++ b/src/babel/extract.js @@ -1,7 +1,7 @@ /* eslint-disable no-param-reassign */ /* @flow */ -const { relative } = require('path'); +const { relative, dirname } = require('path'); const generator = require('@babel/generator').default; const { isValidElementType } = require('react-is'); const Module = require('./module'); @@ -71,6 +71,69 @@ const stripLines = (loc, text) => { return result; }; +// Verify if the binding is imported from the specified source +const imports = (t, scope, filename, identifier, source) => { + const binding = scope.getAllBindings()[identifier]; + + if (!binding) { + return false; + } + + const p = binding.path; + + const resolveFromFile = id => { + /* $FlowFixMe */ + const M = require('module'); + + try { + return M._resolveFilename(id, { + id: filename, + filename, + paths: M._nodeModulePaths(dirname(filename)), + }); + } catch (e) { + return null; + } + }; + + const isImportingModule = value => + // If the value is an exact match, assume it imports the module + value === source || + // Otherwise try to resolve both and check if they are the same file + resolveFromFile(value) === + // eslint-disable-next-line no-nested-ternary + (source === 'linaria' + ? require.resolve('../index') + : source === 'linaria/react' + ? require.resolve('../react/') + : resolveFromFile(source)); + + if (t.isImportSpecifier(p) && t.isImportDeclaration(p.parentPath)) { + return isImportingModule(p.parentPath.node.source.value); + } + + if (t.isVariableDeclarator(p)) { + if ( + t.isCallExpression(p.node.init) && + t.isIdentifier(p.node.init.callee) && + p.node.init.callee.name === 'require' && + p.node.init.arguments.length === 1 + ) { + const node = p.node.init.arguments[0]; + + if (t.isStringLiteral(node)) { + return isImportingModule(node.value); + } + + if (t.isTemplateLiteral(node) && node.quasis.length === 1) { + return isImportingModule(node.quasis[0].value.cooked); + } + } + } + + return false; +}; + // Match any valid CSS units followed by a separator such as ;, newline etc. const unitRegex = new RegExp(`^(${units.join('|')})(;|,|\n| |\\))`); @@ -154,24 +217,41 @@ module.exports = function extract( const { quasi, tag } = path.node; let styled; + let css; if ( - t.isCallExpression(tag) && - t.isIdentifier(tag.callee) && - tag.arguments.length === 1 && - tag.callee.name === 'styled' + imports( + t, + path.scope, + state.file.opts.filename, + 'styled', + 'linaria/react' + ) ) { - styled = { component: path.get('tag').get('arguments')[0] }; + if ( + t.isCallExpression(tag) && + t.isIdentifier(tag.callee) && + tag.arguments.length === 1 && + tag.callee.name === 'styled' + ) { + styled = { component: path.get('tag').get('arguments')[0] }; + } else if ( + t.isMemberExpression(tag) && + t.isIdentifier(tag.object) && + t.isIdentifier(tag.property) && + tag.object.name === 'styled' + ) { + styled = { + component: { node: t.stringLiteral(tag.property.name) }, + }; + } } else if ( - t.isMemberExpression(tag) && - t.isIdentifier(tag.object) && - t.isIdentifier(tag.property) && - tag.object.name === 'styled' + imports(t, path.scope, state.file.opts.filename, 'css', 'linaria') ) { - styled = { component: { node: t.stringLiteral(tag.property.name) } }; + css = t.isIdentifier(tag) && tag.name === 'css'; } - if (styled || (t.isIdentifier(tag) && tag.name === 'css')) { + if (styled || css) { const interpolations = []; // Try to determine a readable class name